` or ``.
+ *
+ * @property {Boolean} [enforceRealBlocks=false]
+ */
+ this.enforceRealBlocks = 0;
+
+ this._ || ( this._ = {} );
+ }
+
+ /**
+ * Default iterator's filter. It is set only for nested iterators.
+ *
+ * @since 4.3
+ * @readonly
+ * @property {CKEDITOR.filter} filter
+ */
+
+ /**
+ * Iterator's active filter. It is set by the {@link #getNextParagraph} method
+ * when it enters nested editable.
+ *
+ * @since 4.3
+ * @readonly
+ * @property {CKEDITOR.filter} activeFilter
+ */
+
+ var beginWhitespaceRegex = /^[\r\n\t ]+$/,
+ // Ignore bookmark nodes.(#3783)
+ bookmarkGuard = CKEDITOR.dom.walker.bookmark( false, true ),
+ whitespacesGuard = CKEDITOR.dom.walker.whitespaces( true ),
+ skipGuard = function( node ) {
+ return bookmarkGuard( node ) && whitespacesGuard( node );
+ };
+
+ // Get a reference for the next element, bookmark nodes are skipped.
+ function getNextSourceNode( node, startFromSibling, lastNode ) {
+ var next = node.getNextSourceNode( startFromSibling, null, lastNode );
+ while ( !bookmarkGuard( next ) )
+ next = next.getNextSourceNode( startFromSibling, null, lastNode );
+ return next;
+ }
+
+ iterator.prototype = {
+ /**
+ * Returns next paragraph-like element or `null` if reached the end of range.
+ *
+ * @param {String} [blockTag='p'] Name of a block element which will be established by
+ * iterator in block-less elements (see {@link #enforceRealBlocks}).
+ */
+ getNextParagraph: function( blockTag ) {
+ // The block element to be returned.
+ var block;
+
+ // The range object used to identify the paragraph contents.
+ var range;
+
+ // Indicats that the current element in the loop is the last one.
+ var isLast;
+
+ // Instructs to cleanup remaining BRs.
+ var removePreviousBr, removeLastBr;
+
+ blockTag = blockTag || 'p';
+
+ // We're iterating over nested editable.
+ if ( this._.nestedEditable ) {
+ // Get next block from nested iterator and returns it if was found.
+ block = this._.nestedEditable.iterator.getNextParagraph( blockTag );
+ if ( block ) {
+ // Inherit activeFilter from the nested iterator.
+ this.activeFilter = this._.nestedEditable.iterator.activeFilter;
+ return block;
+ }
+
+ // No block in nested iterator means that we reached the end of the nested editable.
+ // Reset the active filter to the default filter (or undefined if this iterator didn't have it).
+ this.activeFilter = this.filter;
+
+ // Try to find next nested editable or get back to parent (this) iterator.
+ if ( startNestedEditableIterator( this, blockTag, this._.nestedEditable.container, this._.nestedEditable.remaining ) ) {
+ // Inherit activeFilter from the nested iterator.
+ this.activeFilter = this._.nestedEditable.iterator.activeFilter;
+ return this._.nestedEditable.iterator.getNextParagraph( blockTag );
+ } else
+ this._.nestedEditable = null;
+ }
+
+ // Block-less range should be checked first.
+ if ( !this.range.root.getDtd()[ blockTag ] )
+ return null;
+
+ // This is the first iteration. Let's initialize it.
+ if ( !this._.started )
+ range = startIterator.call( this );
+
+ var currentNode = this._.nextNode,
+ lastNode = this._.lastNode;
+
+ this._.nextNode = null;
+ while ( currentNode ) {
+ // closeRange indicates that a paragraph boundary has been found,
+ // so the range can be closed.
+ var closeRange = 0,
+ parentPre = currentNode.hasAscendant( 'pre' );
+
+ // includeNode indicates that the current node is good to be part
+ // of the range. By default, any non-element node is ok for it.
+ var includeNode = ( currentNode.type != CKEDITOR.NODE_ELEMENT ),
+ continueFromSibling = 0;
+
+ // If it is an element node, let's check if it can be part of the range.
+ if ( !includeNode ) {
+ var nodeName = currentNode.getName();
+
+ // Non-editable block was found - return it and move to processing
+ // its nested editables if they exist.
+ if ( CKEDITOR.dtd.$block[ nodeName ] && currentNode.getAttribute( 'contenteditable' ) == 'false' ) {
+ block = currentNode;
+
+ // Setup iterator for first of nested editables.
+ // If there's no editable, then algorithm will move to next element after current block.
+ startNestedEditableIterator( this, blockTag, block );
+
+ // Gets us straight to the end of getParagraph() because block variable is set.
+ break;
+ } else if ( currentNode.isBlockBoundary( this.forceBrBreak && !parentPre && { br: 1 } ) ) {
+ // boundaries must be part of the range. It will
+ // happen only if ForceBrBreak.
+ if ( nodeName == 'br' )
+ includeNode = 1;
+ else if ( !range && !currentNode.getChildCount() && nodeName != 'hr' ) {
+ // If we have found an empty block, and haven't started
+ // the range yet, it means we must return this block.
+ block = currentNode;
+ isLast = currentNode.equals( lastNode );
+ break;
+ }
+
+ // The range must finish right before the boundary,
+ // including possibly skipped empty spaces. (#1603)
+ if ( range ) {
+ range.setEndAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
+
+ // The found boundary must be set as the next one at this
+ // point. (#1717)
+ if ( nodeName != 'br' )
+ this._.nextNode = currentNode;
+ }
+
+ closeRange = 1;
+ } else {
+ // If we have child nodes, let's check them.
+ if ( currentNode.getFirst() ) {
+ // If we don't have a range yet, let's start it.
+ if ( !range ) {
+ range = this.range.clone();
+ range.setStartAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
+ }
+
+ currentNode = currentNode.getFirst();
+ continue;
+ }
+ includeNode = 1;
+ }
+ } else if ( currentNode.type == CKEDITOR.NODE_TEXT ) {
+ // Ignore normal whitespaces (i.e. not including or
+ // other unicode whitespaces) before/after a block node.
+ if ( beginWhitespaceRegex.test( currentNode.getText() ) )
+ includeNode = 0;
+ }
+
+ // The current node is good to be part of the range and we are
+ // starting a new range, initialize it first.
+ if ( includeNode && !range ) {
+ range = this.range.clone();
+ range.setStartAt( currentNode, CKEDITOR.POSITION_BEFORE_START );
+ }
+
+ // The last node has been found.
+ isLast = ( ( !closeRange || includeNode ) && currentNode.equals( lastNode ) );
+
+ // If we are in an element boundary, let's check if it is time
+ // to close the range, otherwise we include the parent within it.
+ if ( range && !closeRange ) {
+ while ( !currentNode.getNext( skipGuard ) && !isLast ) {
+ var parentNode = currentNode.getParent();
+
+ if ( parentNode.isBlockBoundary( this.forceBrBreak && !parentPre && { br: 1 } ) ) {
+ closeRange = 1;
+ includeNode = 0;
+ isLast = isLast || ( parentNode.equals( lastNode ) );
+ // Make sure range includes bookmarks at the end of the block. (#7359)
+ range.setEndAt( parentNode, CKEDITOR.POSITION_BEFORE_END );
+ break;
+ }
+
+ currentNode = parentNode;
+ includeNode = 1;
+ isLast = ( currentNode.equals( lastNode ) );
+ continueFromSibling = 1;
+ }
+ }
+
+ // Now finally include the node.
+ if ( includeNode )
+ range.setEndAt( currentNode, CKEDITOR.POSITION_AFTER_END );
+
+ currentNode = getNextSourceNode( currentNode, continueFromSibling, lastNode );
+ isLast = !currentNode;
+
+ // We have found a block boundary. Let's close the range and move out of the
+ // loop.
+ if ( isLast || ( closeRange && range ) )
+ break;
+ }
+
+ // Now, based on the processed range, look for (or create) the block to be returned.
+ if ( !block ) {
+ // If no range has been found, this is the end.
+ if ( !range ) {
+ this._.docEndMarker && this._.docEndMarker.remove();
+ this._.nextNode = null;
+ return null;
+ }
+
+ var startPath = new CKEDITOR.dom.elementPath( range.startContainer, range.root );
+ var startBlockLimit = startPath.blockLimit,
+ checkLimits = { div: 1, th: 1, td: 1 };
+ block = startPath.block;
+
+ if ( !block && startBlockLimit && !this.enforceRealBlocks && checkLimits[ startBlockLimit.getName() ] && range.checkStartOfBlock() && range.checkEndOfBlock() && !startBlockLimit.equals( range.root ) )
+ block = startBlockLimit;
+ else if ( !block || ( this.enforceRealBlocks && block.getName() == 'li' ) ) {
+ // Create the fixed block.
+ block = this.range.document.createElement( blockTag );
+
+ // Move the contents of the temporary range to the fixed block.
+ range.extractContents().appendTo( block );
+ block.trim();
+
+ // Insert the fixed block into the DOM.
+ range.insertNode( block );
+
+ removePreviousBr = removeLastBr = true;
+ } else if ( block.getName() != 'li' ) {
+ // If the range doesn't includes the entire contents of the
+ // block, we must split it, isolating the range in a dedicated
+ // block.
+ if ( !range.checkStartOfBlock() || !range.checkEndOfBlock() ) {
+ // The resulting block will be a clone of the current one.
+ block = block.clone( false );
+
+ // Extract the range contents, moving it to the new block.
+ range.extractContents().appendTo( block );
+ block.trim();
+
+ // Split the block. At this point, the range will be in the
+ // right position for our intents.
+ var splitInfo = range.splitBlock();
+
+ removePreviousBr = !splitInfo.wasStartOfBlock;
+ removeLastBr = !splitInfo.wasEndOfBlock;
+
+ // Insert the new block into the DOM.
+ range.insertNode( block );
+ }
+ } else if ( !isLast ) {
+ // LIs are returned as is, with all their children (due to the
+ // nested lists). But, the next node is the node right after
+ // the current range, which could be an child (nested
+ // lists) or the next sibling .
+
+ this._.nextNode = ( block.equals( lastNode ) ? null : getNextSourceNode( range.getBoundaryNodes().endNode, 1, lastNode ) );
+ }
+ }
+
+ if ( removePreviousBr ) {
+ var previousSibling = block.getPrevious();
+ if ( previousSibling && previousSibling.type == CKEDITOR.NODE_ELEMENT ) {
+ if ( previousSibling.getName() == 'br' )
+ previousSibling.remove();
+ else if ( previousSibling.getLast() && previousSibling.getLast().$.nodeName.toLowerCase() == 'br' )
+ previousSibling.getLast().remove();
+ }
+ }
+
+ if ( removeLastBr ) {
+ var lastChild = block.getLast();
+ if ( lastChild && lastChild.type == CKEDITOR.NODE_ELEMENT && lastChild.getName() == 'br' ) {
+ // Remove br filler on browser which do not need it.
+ if ( !CKEDITOR.env.needsBrFiller || lastChild.getPrevious( bookmarkGuard ) || lastChild.getNext( bookmarkGuard ) )
+ lastChild.remove();
+ }
+ }
+
+ // Get a reference for the next element. This is important because the
+ // above block can be removed or changed, so we can rely on it for the
+ // next interation.
+ if ( !this._.nextNode )
+ this._.nextNode = ( isLast || block.equals( lastNode ) || !lastNode ) ? null : getNextSourceNode( block, 1, lastNode );
+
+ return block;
+ }
+ };
+
+ // @context CKEDITOR.dom.iterator
+ // @returns Collapsed range which will be reused when during furter processing.
+ function startIterator() {
+ var range = this.range.clone(),
+ // Indicate at least one of the range boundaries is inside a preformat block.
+ touchPre;
+
+ // Shrink the range to exclude harmful "noises" (#4087, #4450, #5435).
+ range.shrink( CKEDITOR.SHRINK_ELEMENT, true );
+
+ touchPre = range.endContainer.hasAscendant( 'pre', true ) || range.startContainer.hasAscendant( 'pre', true );
+
+ range.enlarge( this.forceBrBreak && !touchPre || !this.enlargeBr ? CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS : CKEDITOR.ENLARGE_BLOCK_CONTENTS );
+
+ if ( !range.collapsed ) {
+ var walker = new CKEDITOR.dom.walker( range.clone() ),
+ ignoreBookmarkTextEvaluator = CKEDITOR.dom.walker.bookmark( true, true );
+ // Avoid anchor inside bookmark inner text.
+ walker.evaluator = ignoreBookmarkTextEvaluator;
+ this._.nextNode = walker.next();
+ // TODO: It's better to have walker.reset() used here.
+ walker = new CKEDITOR.dom.walker( range.clone() );
+ walker.evaluator = ignoreBookmarkTextEvaluator;
+ var lastNode = walker.previous();
+ this._.lastNode = lastNode.getNextSourceNode( true );
+
+ // We may have an empty text node at the end of block due to [3770].
+ // If that node is the lastNode, it would cause our logic to leak to the
+ // next block.(#3887)
+ if ( this._.lastNode && this._.lastNode.type == CKEDITOR.NODE_TEXT && !CKEDITOR.tools.trim( this._.lastNode.getText() ) && this._.lastNode.getParent().isBlockBoundary() ) {
+ var testRange = this.range.clone();
+ testRange.moveToPosition( this._.lastNode, CKEDITOR.POSITION_AFTER_END );
+ if ( testRange.checkEndOfBlock() ) {
+ var path = new CKEDITOR.dom.elementPath( testRange.endContainer, testRange.root ),
+ lastBlock = path.block || path.blockLimit;
+ this._.lastNode = lastBlock.getNextSourceNode( true );
+ }
+ }
+
+ // The end of document or range.root was reached, so we need a marker node inside.
+ if ( !this._.lastNode || !range.root.contains( this._.lastNode ) ) {
+ this._.lastNode = this._.docEndMarker = range.document.createText( '' );
+ this._.lastNode.insertAfter( lastNode );
+ }
+
+ // Let's reuse this variable.
+ range = null;
+ }
+
+ this._.started = 1;
+
+ return range;
+ }
+
+ // Does a nested editables lookup inside editablesContainer.
+ // If remainingEditables is set will lookup inside this array.
+ // @param {CKEDITOR.dom.element} editablesContainer
+ // @param {CKEDITOR.dom.element[]} [remainingEditables]
+ function getNestedEditableIn( editablesContainer, remainingEditables ) {
+ if ( remainingEditables == undefined )
+ remainingEditables = findNestedEditables( editablesContainer );
+
+ var editable;
+
+ while ( ( editable = remainingEditables.shift() ) ) {
+ if ( isIterableEditable( editable ) )
+ return { element: editable, remaining: remainingEditables };
+ }
+
+ return null;
+ }
+
+ // Checkes whether we can iterate over this editable.
+ function isIterableEditable( editable ) {
+ // Reject blockless editables.
+ return editable.getDtd().p;
+ }
+
+ // 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;
+ }
+
+ // Looks for a first nested editable after previousEditable (if passed) and creates
+ // nested iterator for it.
+ function startNestedEditableIterator( parentIterator, blockTag, editablesContainer, remainingEditables ) {
+ var editable = getNestedEditableIn( editablesContainer, remainingEditables );
+
+ if ( !editable )
+ return 0;
+
+ var filter = CKEDITOR.filter.instances[ editable.element.data( 'cke-filter' ) ];
+
+ // If current editable has a filter and this filter does not allow for block tag,
+ // search for next nested editable in remaining ones.
+ if ( filter && !filter.check( blockTag ) )
+ return startNestedEditableIterator( parentIterator, blockTag, editablesContainer, editable.remaining );
+
+ var range = new CKEDITOR.dom.range( editable.element );
+ range.selectNodeContents( editable.element );
+
+ var iterator = range.createIterator();
+ // This setting actually does not change anything in this case,
+ // because entire range contents is selected, so there're no s to be included.
+ // But it seems right to copy it too.
+ iterator.enlargeBr = parentIterator.enlargeBr;
+ // Inherit configuration from parent iterator.
+ iterator.enforceRealBlocks = parentIterator.enforceRealBlocks;
+ // Set the activeFilter (which can be overriden when this iteator will start nested iterator)
+ // and the default filter, which will make it possible to reset to
+ // current iterator's activeFilter after leaving nested editable.
+ iterator.activeFilter = iterator.filter = filter;
+
+ parentIterator._.nestedEditable = {
+ element: editable.element,
+ container: editablesContainer,
+ remaining: editable.remaining,
+ iterator: iterator
+ };
+
+ return 1;
+ }
+
+ /**
+ * Creates {CKEDITOR.dom.iterator} instance for this range.
+ *
+ * @member CKEDITOR.dom.range
+ * @returns {CKEDITOR.dom.iterator}
+ */
+ CKEDITOR.dom.range.prototype.createIterator = function() {
+ return new iterator( this );
+ };
+} )();
diff --git a/lam/templates/lib/extra/ckeditor/core/dom/node.js b/lam/templates/lib/extra/ckeditor/core/dom/node.js
new file mode 100644
index 00000000..05a531de
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/core/dom/node.js
@@ -0,0 +1,748 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+/**
+ * @fileOverview Defines the {@link CKEDITOR.dom.node} class which is the base
+ * class for classes that represent DOM nodes.
+ */
+
+/**
+ * Base class for classes representing DOM nodes. This constructor may return
+ * an instance of a class that inherits from this class, like
+ * {@link CKEDITOR.dom.element} or {@link CKEDITOR.dom.text}.
+ *
+ * @class
+ * @extends CKEDITOR.dom.domObject
+ * @constructor Creates a node class instance.
+ * @param {Object} domNode A native DOM node.
+ * @see CKEDITOR.dom.element
+ * @see CKEDITOR.dom.text
+ */
+CKEDITOR.dom.node = function( domNode ) {
+ if ( domNode ) {
+ var type = domNode.nodeType == CKEDITOR.NODE_DOCUMENT ? 'document' : domNode.nodeType == CKEDITOR.NODE_ELEMENT ? 'element' : domNode.nodeType == CKEDITOR.NODE_TEXT ? 'text' : domNode.nodeType == CKEDITOR.NODE_COMMENT ? 'comment' : domNode.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ? 'documentFragment' : 'domObject'; // Call the base constructor otherwise.
+
+ return new CKEDITOR.dom[ type ]( domNode );
+ }
+
+ return this;
+};
+
+CKEDITOR.dom.node.prototype = new CKEDITOR.dom.domObject();
+
+/**
+ * Element node type.
+ *
+ * @readonly
+ * @property {Number} [=1]
+ * @member CKEDITOR
+ */
+CKEDITOR.NODE_ELEMENT = 1;
+
+/**
+ * Document node type.
+ *
+ * @readonly
+ * @property {Number} [=9]
+ * @member CKEDITOR
+ */
+CKEDITOR.NODE_DOCUMENT = 9;
+
+/**
+ * Text node type.
+ *
+ * @readonly
+ * @property {Number} [=3]
+ * @member CKEDITOR
+ */
+CKEDITOR.NODE_TEXT = 3;
+
+/**
+ * Comment node type.
+ *
+ * @readonly
+ * @property {Number} [=8]
+ * @member CKEDITOR
+ */
+CKEDITOR.NODE_COMMENT = 8;
+
+/**
+ * Document fragment node type.
+ *
+ * @readonly
+ * @property {Number} [=11]
+ * @member CKEDITOR
+ */
+CKEDITOR.NODE_DOCUMENT_FRAGMENT = 11;
+
+CKEDITOR.POSITION_IDENTICAL = 0;
+CKEDITOR.POSITION_DISCONNECTED = 1;
+CKEDITOR.POSITION_FOLLOWING = 2;
+CKEDITOR.POSITION_PRECEDING = 4;
+CKEDITOR.POSITION_IS_CONTAINED = 8;
+CKEDITOR.POSITION_CONTAINS = 16;
+
+CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, {
+ /**
+ * Makes this node a child of another element.
+ *
+ * var p = new CKEDITOR.dom.element( 'p' );
+ * var strong = new CKEDITOR.dom.element( 'strong' );
+ * strong.appendTo( p );
+ *
+ * // Result: ' '.
+ *
+ * @param {CKEDITOR.dom.element} element The target element to which this node will be appended.
+ * @returns {CKEDITOR.dom.element} The target element.
+ */
+ appendTo: function( element, toStart ) {
+ element.append( this, toStart );
+ return element;
+ },
+
+ /**
+ * Clone this node.
+ *
+ * **Note**: Values set by {#setCustomData} won't be available in the clone.
+ *
+ * @param {Boolean} [includeChildren=false] If `true` then all node's
+ * children will be cloned recursively.
+ * @param {Boolean} [cloneId=false] Whether ID attributes should be cloned too.
+ * @returns {CKEDITOR.dom.node} Clone of this node.
+ */
+ clone: function( includeChildren, cloneId ) {
+ var $clone = this.$.cloneNode( includeChildren );
+
+ var removeIds = function( node ) {
+ // Reset data-cke-expando only when has been cloned (IE and only for some types of objects).
+ if ( node[ 'data-cke-expando' ] )
+ node[ 'data-cke-expando' ] = false;
+
+ if ( node.nodeType != CKEDITOR.NODE_ELEMENT )
+ return;
+ if ( !cloneId )
+ node.removeAttribute( 'id', false );
+
+ if ( includeChildren ) {
+ var childs = node.childNodes;
+ for ( var i = 0; i < childs.length; i++ )
+ removeIds( childs[ i ] );
+ }
+ };
+
+ // The "id" attribute should never be cloned to avoid duplication.
+ removeIds( $clone );
+
+ return new CKEDITOR.dom.node( $clone );
+ },
+
+ /**
+ * Check if node is preceded by any sibling.
+ *
+ * @returns {Boolean}
+ */
+ hasPrevious: function() {
+ return !!this.$.previousSibling;
+ },
+
+ /**
+ * Check if node is succeeded by any sibling.
+ *
+ * @returns {Boolean}
+ */
+ hasNext: function() {
+ return !!this.$.nextSibling;
+ },
+
+ /**
+ * Inserts this element after a node.
+ *
+ * var em = new CKEDITOR.dom.element( 'em' );
+ * var strong = new CKEDITOR.dom.element( 'strong' );
+ * strong.insertAfter( em );
+ *
+ * // Result: ''
+ *
+ * @param {CKEDITOR.dom.node} node The node that will precede this element.
+ * @returns {CKEDITOR.dom.node} The node preceding this one after insertion.
+ */
+ insertAfter: function( node ) {
+ node.$.parentNode.insertBefore( this.$, node.$.nextSibling );
+ return node;
+ },
+
+ /**
+ * Inserts this element before a node.
+ *
+ * var em = new CKEDITOR.dom.element( 'em' );
+ * var strong = new CKEDITOR.dom.element( 'strong' );
+ * strong.insertBefore( em );
+ *
+ * // result: ''
+ *
+ * @param {CKEDITOR.dom.node} node The node that will succeed this element.
+ * @returns {CKEDITOR.dom.node} The node being inserted.
+ */
+ insertBefore: function( node ) {
+ node.$.parentNode.insertBefore( this.$, node.$ );
+ return node;
+ },
+
+ /**
+ * Inserts node before this node.
+ *
+ * var em = new CKEDITOR.dom.element( 'em' );
+ * var strong = new CKEDITOR.dom.element( 'strong' );
+ * strong.insertBeforeMe( em );
+ *
+ * // result: ''
+ *
+ * @param {CKEDITOR.dom.node} node The node that will preceed this element.
+ * @returns {CKEDITOR.dom.node} The node being inserted.
+ */
+ insertBeforeMe: function( node ) {
+ this.$.parentNode.insertBefore( node.$, this.$ );
+ return node;
+ },
+
+ /**
+ * Retrieves a uniquely identifiable tree address for this node.
+ * The tree address returned is an array of integers, with each integer
+ * indicating a child index of a DOM node, starting from
+ * `document.documentElement`.
+ *
+ * For example, assuming `` is the second child
+ * of `` (`` being the first),
+ * and we would like to address the third child under the
+ * fourth child of ``, the tree address returned would be:
+ * `[1, 3, 2]`.
+ *
+ * The tree address cannot be used for finding back the DOM tree node once
+ * the DOM tree structure has been modified.
+ *
+ * @param {Boolean} [normalized=false] See {@link #getIndex}.
+ * @returns {Array} The address.
+ */
+ getAddress: function( normalized ) {
+ var address = [];
+ var $documentElement = this.getDocument().$.documentElement;
+ var node = this.$;
+
+ while ( node && node != $documentElement ) {
+ var parentNode = node.parentNode;
+
+ if ( parentNode ) {
+ // Get the node index. For performance, call getIndex
+ // directly, instead of creating a new node object.
+ address.unshift( this.getIndex.call( { $: node }, normalized ) );
+ }
+
+ node = parentNode;
+ }
+
+ return address;
+ },
+
+ /**
+ * Gets the document containing this element.
+ *
+ * var element = CKEDITOR.document.getById( 'example' );
+ * alert( element.getDocument().equals( CKEDITOR.document ) ); // true
+ *
+ * @returns {CKEDITOR.dom.document} The document.
+ */
+ getDocument: function() {
+ return new CKEDITOR.dom.document( this.$.ownerDocument || this.$.parentNode.ownerDocument );
+ },
+
+ /**
+ * Get index of a node in an array of its parent.childNodes.
+ *
+ * Let's assume having childNodes array:
+ *
+ * [ emptyText, element1, text, text, element2 ]
+ * element1.getIndex(); // 1
+ * element1.getIndex( true ); // 0
+ * element2.getIndex(); // 4
+ * element2.getIndex( true ); // 2
+ *
+ * @param {Boolean} normalized When `true` empty text nodes and one followed
+ * by another one text node are not counted in.
+ * @returns {Number} Index of a node.
+ */
+ getIndex: function( normalized ) {
+ // Attention: getAddress depends on this.$
+ // getIndex is called on a plain object: { $ : node }
+
+ var current = this.$,
+ index = -1,
+ isNormalizing;
+
+ if ( !this.$.parentNode )
+ return index;
+
+ do {
+ // Bypass blank node and adjacent text nodes.
+ if ( normalized && current != this.$ && current.nodeType == CKEDITOR.NODE_TEXT && ( isNormalizing || !current.nodeValue ) )
+ continue;
+
+ index++;
+ isNormalizing = current.nodeType == CKEDITOR.NODE_TEXT;
+ }
+ while ( ( current = current.previousSibling ) )
+
+ return index;
+ },
+
+ /**
+ * @todo
+ */
+ getNextSourceNode: function( startFromSibling, nodeType, guard ) {
+ // If "guard" is a node, transform it in a function.
+ if ( guard && !guard.call ) {
+ var guardNode = guard;
+ guard = function( node ) {
+ return !node.equals( guardNode );
+ };
+ }
+
+ var node = ( !startFromSibling && this.getFirst && this.getFirst() ),
+ parent;
+
+ // Guarding when we're skipping the current element( no children or 'startFromSibling' ).
+ // send the 'moving out' signal even we don't actually dive into.
+ if ( !node ) {
+ if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false )
+ return null;
+ node = this.getNext();
+ }
+
+ while ( !node && ( parent = ( parent || this ).getParent() ) ) {
+ // The guard check sends the "true" paramenter to indicate that
+ // we are moving "out" of the element.
+ if ( guard && guard( parent, true ) === false )
+ return null;
+
+ node = parent.getNext();
+ }
+
+ if ( !node )
+ return null;
+
+ if ( guard && guard( node ) === false )
+ return null;
+
+ if ( nodeType && nodeType != node.type )
+ return node.getNextSourceNode( false, nodeType, guard );
+
+ return node;
+ },
+
+ /**
+ * @todo
+ */
+ getPreviousSourceNode: function( startFromSibling, nodeType, guard ) {
+ if ( guard && !guard.call ) {
+ var guardNode = guard;
+ guard = function( node ) {
+ return !node.equals( guardNode );
+ };
+ }
+
+ var node = ( !startFromSibling && this.getLast && this.getLast() ),
+ parent;
+
+ // Guarding when we're skipping the current element( no children or 'startFromSibling' ).
+ // send the 'moving out' signal even we don't actually dive into.
+ if ( !node ) {
+ if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false )
+ return null;
+ node = this.getPrevious();
+ }
+
+ while ( !node && ( parent = ( parent || this ).getParent() ) ) {
+ // The guard check sends the "true" paramenter to indicate that
+ // we are moving "out" of the element.
+ if ( guard && guard( parent, true ) === false )
+ return null;
+
+ node = parent.getPrevious();
+ }
+
+ if ( !node )
+ return null;
+
+ if ( guard && guard( node ) === false )
+ return null;
+
+ if ( nodeType && node.type != nodeType )
+ return node.getPreviousSourceNode( false, nodeType, guard );
+
+ return node;
+ },
+
+ /**
+ * Gets the node that preceed this element in its parent's child list.
+ *
+ * var element = CKEDITOR.dom.element.createFromHtml( 'prevExample ' );
+ * var first = element.getLast().getPrev();
+ * alert( first.getName() ); // 'i'
+ *
+ * @param {Function} [evaluator] Filtering the result node.
+ * @returns {CKEDITOR.dom.node} The previous node or null if not available.
+ */
+ getPrevious: function( evaluator ) {
+ var previous = this.$,
+ retval;
+ do {
+ previous = previous.previousSibling;
+
+ // Avoid returning the doc type node.
+ // http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-412266927
+ retval = previous && previous.nodeType != 10 && new CKEDITOR.dom.node( previous );
+ }
+ while ( retval && evaluator && !evaluator( retval ) )
+ return retval;
+ },
+
+ /**
+ * Gets the node that follows this element in its parent's child list.
+ *
+ * var element = CKEDITOR.dom.element.createFromHtml( 'Examplenext ' );
+ * var last = element.getFirst().getNext();
+ * alert( last.getName() ); // 'i'
+ *
+ * @param {Function} [evaluator] Filtering the result node.
+ * @returns {CKEDITOR.dom.node} The next node or null if not available.
+ */
+ getNext: function( evaluator ) {
+ var next = this.$,
+ retval;
+ do {
+ next = next.nextSibling;
+ retval = next && new CKEDITOR.dom.node( next );
+ }
+ while ( retval && evaluator && !evaluator( retval ) )
+ return retval;
+ },
+
+ /**
+ * Gets the parent element for this node.
+ *
+ * var node = editor.document.getBody().getFirst();
+ * var parent = node.getParent();
+ * alert( parent.getName() ); // 'body'
+ *
+ * @param {Boolean} [allowFragmentParent=false] Consider also parent node that is of
+ * fragment type {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}.
+ * @returns {CKEDITOR.dom.element} The parent element.
+ */
+ getParent: function( allowFragmentParent ) {
+ var parent = this.$.parentNode;
+ return ( parent && ( parent.nodeType == CKEDITOR.NODE_ELEMENT || allowFragmentParent && parent.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) ? new CKEDITOR.dom.node( parent ) : null;
+ },
+
+ /**
+ * Returns array containing node parents and node itself. By default nodes are in _descending_ order.
+ *
+ * // Assuming that body has paragraph as first child.
+ * var node = editor.document.getBody().getFirst();
+ * var parents = node.getParents();
+ * alert( parents[ 0 ].getName() + ',' + parents[ 2 ].getName() ); // 'html,p'
+ *
+ * @param {Boolean} [closerFirst=false] Determines order of returned nodes.
+ * @returns {Array} Returns array of {@link CKEDITOR.dom.node}.
+ */
+ getParents: function( closerFirst ) {
+ var node = this;
+ var parents = [];
+
+ do {
+ parents[ closerFirst ? 'push' : 'unshift' ]( node );
+ }
+ while ( ( node = node.getParent() ) )
+
+ return parents;
+ },
+
+ /**
+ * @todo
+ */
+ getCommonAncestor: function( node ) {
+ if ( node.equals( this ) )
+ return this;
+
+ if ( node.contains && node.contains( this ) )
+ return node;
+
+ var start = this.contains ? this : this.getParent();
+
+ do {
+ if ( start.contains( node ) ) return start;
+ }
+ while ( ( start = start.getParent() ) );
+
+ return null;
+ },
+
+ /**
+ * @todo
+ */
+ getPosition: function( otherNode ) {
+ var $ = this.$;
+ var $other = otherNode.$;
+
+ if ( $.compareDocumentPosition )
+ return $.compareDocumentPosition( $other );
+
+ // IE and Safari have no support for compareDocumentPosition.
+
+ if ( $ == $other )
+ return CKEDITOR.POSITION_IDENTICAL;
+
+ // Only element nodes support contains and sourceIndex.
+ if ( this.type == CKEDITOR.NODE_ELEMENT && otherNode.type == CKEDITOR.NODE_ELEMENT ) {
+ if ( $.contains ) {
+ if ( $.contains( $other ) )
+ return CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING;
+
+ if ( $other.contains( $ ) )
+ return CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING;
+ }
+
+ if ( 'sourceIndex' in $ )
+ return ( $.sourceIndex < 0 || $other.sourceIndex < 0 ) ? CKEDITOR.POSITION_DISCONNECTED : ( $.sourceIndex < $other.sourceIndex ) ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING;
+
+ }
+
+ // For nodes that don't support compareDocumentPosition, contains
+ // or sourceIndex, their "address" is compared.
+
+ var addressOfThis = this.getAddress(),
+ addressOfOther = otherNode.getAddress(),
+ minLevel = Math.min( addressOfThis.length, addressOfOther.length );
+
+ // Determinate preceed/follow relationship.
+ for ( var i = 0; i <= minLevel - 1; i++ ) {
+ if ( addressOfThis[ i ] != addressOfOther[ i ] ) {
+ if ( i < minLevel )
+ return addressOfThis[ i ] < addressOfOther[ i ] ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING;
+
+ break;
+ }
+ }
+
+ // Determinate contains/contained relationship.
+ return ( addressOfThis.length < addressOfOther.length ) ? CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING;
+ },
+
+ /**
+ * Gets the closest ancestor node of this node, specified by its name.
+ *
+ * // Suppose we have the following HTML structure:
+ * //
+ * // If node ==
+ * ascendant = node.getAscendant( 'div' ); // ascendant ==
+ * ascendant = node.getAscendant( 'b' ); // ascendant == null
+ * ascendant = node.getAscendant( 'b', true ); // ascendant ==
+ * ascendant = node.getAscendant( { div:1,p:1 } ); // Searches for the first 'div' or 'p': ascendant ==
+ *
+ * @since 3.6.1
+ * @param {String} reference The name of the ancestor node to search or
+ * an object with the node names to search for.
+ * @param {Boolean} [includeSelf] Whether to include the current
+ * node in the search.
+ * @returns {CKEDITOR.dom.node} The located ancestor node or null if not found.
+ */
+ getAscendant: function( reference, includeSelf ) {
+ var $ = this.$,
+ name;
+
+ if ( !includeSelf )
+ $ = $.parentNode;
+
+ while ( $ ) {
+ if ( $.nodeName && ( name = $.nodeName.toLowerCase(), ( typeof reference == 'string' ? name == reference : name in reference ) ) )
+ return new CKEDITOR.dom.node( $ );
+
+ try {
+ $ = $.parentNode;
+ } catch ( e ) {
+ $ = null;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * @todo
+ */
+ hasAscendant: function( name, includeSelf ) {
+ var $ = this.$;
+
+ if ( !includeSelf )
+ $ = $.parentNode;
+
+ while ( $ ) {
+ if ( $.nodeName && $.nodeName.toLowerCase() == name )
+ return true;
+
+ $ = $.parentNode;
+ }
+ return false;
+ },
+
+ /**
+ * @todo
+ */
+ move: function( target, toStart ) {
+ target.append( this.remove(), toStart );
+ },
+
+ /**
+ * Removes this node from the document DOM.
+ *
+ * var element = CKEDITOR.document.getById( 'MyElement' );
+ * element.remove();
+ *
+ * @param {Boolean} [preserveChildren=false] Indicates that the children
+ * elements must remain in the document, removing only the outer tags.
+ */
+ remove: function( preserveChildren ) {
+ var $ = this.$;
+ var parent = $.parentNode;
+
+ if ( parent ) {
+ if ( preserveChildren ) {
+ // Move all children before the node.
+ for ( var child;
+ ( child = $.firstChild ); ) {
+ parent.insertBefore( $.removeChild( child ), $ );
+ }
+ }
+
+ parent.removeChild( $ );
+ }
+
+ return this;
+ },
+
+ /**
+ * @todo
+ */
+ replace: function( nodeToReplace ) {
+ this.insertBefore( nodeToReplace );
+ nodeToReplace.remove();
+ },
+
+ /**
+ * @todo
+ */
+ trim: function() {
+ this.ltrim();
+ this.rtrim();
+ },
+
+ /**
+ * @todo
+ */
+ ltrim: function() {
+ var child;
+ while ( this.getFirst && ( child = this.getFirst() ) ) {
+ if ( child.type == CKEDITOR.NODE_TEXT ) {
+ var trimmed = CKEDITOR.tools.ltrim( child.getText() ),
+ originalLength = child.getLength();
+
+ if ( !trimmed ) {
+ child.remove();
+ continue;
+ } else if ( trimmed.length < originalLength ) {
+ child.split( originalLength - trimmed.length );
+
+ // IE BUG: child.remove() may raise JavaScript errors here. (#81)
+ this.$.removeChild( this.$.firstChild );
+ }
+ }
+ break;
+ }
+ },
+
+ /**
+ * @todo
+ */
+ rtrim: function() {
+ var child;
+ while ( this.getLast && ( child = this.getLast() ) ) {
+ if ( child.type == CKEDITOR.NODE_TEXT ) {
+ var trimmed = CKEDITOR.tools.rtrim( child.getText() ),
+ originalLength = child.getLength();
+
+ if ( !trimmed ) {
+ child.remove();
+ continue;
+ } else if ( trimmed.length < originalLength ) {
+ child.split( trimmed.length );
+
+ // IE BUG: child.getNext().remove() may raise JavaScript errors here.
+ // (#81)
+ this.$.lastChild.parentNode.removeChild( this.$.lastChild );
+ }
+ }
+ break;
+ }
+
+ if ( CKEDITOR.env.needsBrFiller ) {
+ child = this.$.lastChild;
+
+ if ( child && child.type == 1 && child.nodeName.toLowerCase() == 'br' ) {
+ // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#324).
+ child.parentNode.removeChild( child );
+ }
+ }
+ },
+
+ /**
+ * Checks if this node is read-only (should not be changed).
+ *
+ * **Note:** When `attributeCheck` is not used, this method only work for elements
+ * that are already presented in the document, otherwise the result
+ * is not guaranteed, it's mainly for performance consideration.
+ *
+ * // For the following HTML:
+ * // Some text
+ *
+ * // If "ele" is the above
+ * element.isReadOnly(); // true
+ *
+ * @since 3.5
+ * @returns {Boolean}
+ */
+ isReadOnly: function() {
+ var element = this;
+ if ( this.type != CKEDITOR.NODE_ELEMENT )
+ element = this.getParent();
+
+ if ( element && typeof element.$.isContentEditable != 'undefined' )
+ return !( element.$.isContentEditable || element.data( 'cke-editable' ) );
+ else {
+ // Degrade for old browsers which don't support "isContentEditable", e.g. FF3
+
+ while ( element ) {
+ if ( element.data( 'cke-editable' ) )
+ break;
+
+ if ( element.getAttribute( 'contentEditable' ) == 'false' )
+ return true;
+ else if ( element.getAttribute( 'contentEditable' ) == 'true' )
+ break;
+
+ element = element.getParent();
+ }
+
+ // Reached the root of DOM tree, no editable found.
+ return !element;
+ }
+ }
+} );
diff --git a/lam/templates/lib/extra/ckeditor/core/dom/nodelist.js b/lam/templates/lib/extra/ckeditor/core/dom/nodelist.js
new file mode 100644
index 00000000..b6f19a52
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/core/dom/nodelist.js
@@ -0,0 +1,43 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+/**
+ * Represents a list of {@link CKEDITOR.dom.node} objects.
+ * It's a wrapper for native nodes list.
+ *
+ * var nodeList = CKEDITOR.document.getBody().getChildren();
+ * alert( nodeList.count() ); // number [0;N]
+ *
+ * @class
+ * @constructor Creates a document class instance.
+ * @param {Object} nativeList
+ */
+CKEDITOR.dom.nodeList = function( nativeList ) {
+ this.$ = nativeList;
+};
+
+CKEDITOR.dom.nodeList.prototype = {
+ /**
+ * Get count of nodes in this list.
+ *
+ * @returns {Number}
+ */
+ count: function() {
+ return this.$.length;
+ },
+
+ /**
+ * Get node from the list.
+ *
+ * @returns {CKEDITOR.dom.node}
+ */
+ getItem: function( index ) {
+ if ( index < 0 || index >= this.$.length )
+ return null;
+
+ var $node = this.$[ index ];
+ return $node ? new CKEDITOR.dom.node( $node ) : null;
+ }
+};
diff --git a/lam/templates/lib/extra/ckeditor/core/dom/range.js b/lam/templates/lib/extra/ckeditor/core/dom/range.js
new file mode 100644
index 00000000..b50a8891
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/core/dom/range.js
@@ -0,0 +1,2391 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+/**
+ * Represents a delimited piece of content in a DOM Document.
+ * It is contiguous in the sense that it can be characterized as selecting all
+ * of the content between a pair of boundary-points.
+ *
+ * This class shares much of the W3C
+ * [Document Object Model Range](http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html)
+ * ideas and features, adding several range manipulation tools to it, but it's
+ * not intended to be compatible with it.
+ *
+ * // Create a range for the entire contents of the editor document body.
+ * var range = new CKEDITOR.dom.range( editor.document );
+ * range.selectNodeContents( editor.document.getBody() );
+ * // Delete the contents.
+ * range.deleteContents();
+ *
+ * @class
+ * @constructor Creates a {@link CKEDITOR.dom.range} instance that can be used inside a specific DOM Document.
+ * @param {CKEDITOR.dom.document/CKEDITOR.dom.element} root The document or element
+ * within which the range will be scoped.
+ * @todo global "TODO" - precise algorithms descriptions needed for the most complex methods like #enlarge.
+ */
+CKEDITOR.dom.range = function( root ) {
+ /**
+ * Node within which the range begins.
+ *
+ * var range = new CKEDITOR.dom.range( editor.document );
+ * range.selectNodeContents( editor.document.getBody() );
+ * alert( range.startContainer.getName() ); // 'body'
+ *
+ * @readonly
+ * @property {CKEDITOR.dom.element/CKEDITOR.dom.text}
+ */
+ this.startContainer = null;
+
+ /**
+ * Offset within the starting node of the range.
+ *
+ * var range = new CKEDITOR.dom.range( editor.document );
+ * range.selectNodeContents( editor.document.getBody() );
+ * alert( range.startOffset ); // 0
+ *
+ * @readonly
+ * @property {Number}
+ */
+ this.startOffset = null;
+
+ /**
+ * Node within which the range ends.
+ *
+ * var range = new CKEDITOR.dom.range( editor.document );
+ * range.selectNodeContents( editor.document.getBody() );
+ * alert( range.endContainer.getName() ); // 'body'
+ *
+ * @readonly
+ * @property {CKEDITOR.dom.element/CKEDITOR.dom.text}
+ */
+ this.endContainer = null;
+
+ /**
+ * Offset within the ending node of the range.
+ *
+ * var range = new CKEDITOR.dom.range( editor.document );
+ * range.selectNodeContents( editor.document.getBody() );
+ * alert( range.endOffset ); // == editor.document.getBody().getChildCount()
+ *
+ * @readonly
+ * @property {Number}
+ */
+ this.endOffset = null;
+
+ /**
+ * Indicates that this is a collapsed range. A collapsed range has its
+ * start and end boundaries at the very same point so nothing is contained
+ * in it.
+ *
+ * var range = new CKEDITOR.dom.range( editor.document );
+ * range.selectNodeContents( editor.document.getBody() );
+ * alert( range.collapsed ); // false
+ * range.collapse();
+ * alert( range.collapsed ); // true
+ *
+ * @readonly
+ */
+ this.collapsed = true;
+
+ var isDocRoot = root instanceof CKEDITOR.dom.document;
+ /**
+ * The document within which the range can be used.
+ *
+ * // Selects the body contents of the range document.
+ * range.selectNodeContents( range.document.getBody() );
+ *
+ * @readonly
+ * @property {CKEDITOR.dom.document}
+ */
+ this.document = isDocRoot ? root : root.getDocument();
+
+ /**
+ * The ancestor DOM element within which the range manipulation are limited.
+ *
+ * @readonly
+ * @property {CKEDITOR.dom.element}
+ */
+ this.root = isDocRoot ? root.getBody() : root;
+};
+
+( function() {
+ // Updates the "collapsed" property for the given range object.
+ var updateCollapsed = function( range ) {
+ range.collapsed = ( range.startContainer && range.endContainer && range.startContainer.equals( range.endContainer ) && range.startOffset == range.endOffset );
+ };
+
+ // This is a shared function used to delete, extract and clone the range
+ // contents.
+ // V2
+ var execContentsAction = function( range, action, docFrag, mergeThen ) {
+ range.optimizeBookmark();
+
+ var startNode = range.startContainer;
+ var endNode = range.endContainer;
+
+ var startOffset = range.startOffset;
+ var endOffset = range.endOffset;
+
+ var removeStartNode;
+ var removeEndNode;
+
+ // For text containers, we must simply split the node and point to the
+ // second part. The removal will be handled by the rest of the code .
+ if ( endNode.type == CKEDITOR.NODE_TEXT )
+ endNode = endNode.split( endOffset );
+ else {
+ // If the end container has children and the offset is pointing
+ // to a child, then we should start from it.
+ if ( endNode.getChildCount() > 0 ) {
+ // If the offset points after the last node.
+ if ( endOffset >= endNode.getChildCount() ) {
+ // Let's create a temporary node and mark it for removal.
+ endNode = endNode.append( range.document.createText( '' ) );
+ removeEndNode = true;
+ } else
+ endNode = endNode.getChild( endOffset );
+ }
+ }
+
+ // For text containers, we must simply split the node. The removal will
+ // be handled by the rest of the code .
+ if ( startNode.type == CKEDITOR.NODE_TEXT ) {
+ startNode.split( startOffset );
+
+ // In cases the end node is the same as the start node, the above
+ // splitting will also split the end, so me must move the end to
+ // the second part of the split.
+ if ( startNode.equals( endNode ) )
+ endNode = startNode.getNext();
+ } else {
+ // If the start container has children and the offset is pointing
+ // to a child, then we should start from its previous sibling.
+
+ // If the offset points to the first node, we don't have a
+ // sibling, so let's use the first one, but mark it for removal.
+ if ( !startOffset ) {
+ // Let's create a temporary node and mark it for removal.
+ startNode = startNode.append( range.document.createText( '' ), 1 );
+ removeStartNode = true;
+ } else if ( startOffset >= startNode.getChildCount() ) {
+ // Let's create a temporary node and mark it for removal.
+ startNode = startNode.append( range.document.createText( '' ) );
+ removeStartNode = true;
+ } else
+ startNode = startNode.getChild( startOffset ).getPrevious();
+ }
+
+ // Get the parent nodes tree for the start and end boundaries.
+ var startParents = startNode.getParents();
+ var endParents = endNode.getParents();
+
+ // Compare them, to find the top most siblings.
+ var i, topStart, topEnd;
+
+ for ( i = 0; i < startParents.length; i++ ) {
+ topStart = startParents[ i ];
+ topEnd = endParents[ i ];
+
+ // The compared nodes will match until we find the top most
+ // siblings (different nodes that have the same parent).
+ // "i" will hold the index in the parents array for the top
+ // most element.
+ if ( !topStart.equals( topEnd ) )
+ break;
+ }
+
+ var clone = docFrag,
+ levelStartNode, levelClone, currentNode, currentSibling;
+
+ // Remove all successive sibling nodes for every node in the
+ // startParents tree.
+ for ( var j = i; j < startParents.length; j++ ) {
+ levelStartNode = startParents[ j ];
+
+ // For Extract and Clone, we must clone this level.
+ if ( clone && !levelStartNode.equals( startNode ) ) // action = 0 = Delete
+ levelClone = clone.append( levelStartNode.clone() );
+
+ currentNode = levelStartNode.getNext();
+
+ while ( currentNode ) {
+ // Stop processing when the current node matches a node in the
+ // endParents tree or if it is the endNode.
+ if ( currentNode.equals( endParents[ j ] ) || currentNode.equals( endNode ) )
+ break;
+
+ // Cache the next sibling.
+ currentSibling = currentNode.getNext();
+
+ // If cloning, just clone it.
+ if ( action == 2 ) // 2 = Clone
+ clone.append( currentNode.clone( true ) );
+ else {
+ // Both Delete and Extract will remove the node.
+ currentNode.remove();
+
+ // When Extracting, move the removed node to the docFrag.
+ if ( action == 1 ) // 1 = Extract
+ clone.append( currentNode );
+ }
+
+ currentNode = currentSibling;
+ }
+
+ if ( clone )
+ clone = levelClone;
+ }
+
+ clone = docFrag;
+
+ // Remove all previous sibling nodes for every node in the
+ // endParents tree.
+ for ( var k = i; k < endParents.length; k++ ) {
+ levelStartNode = endParents[ k ];
+
+ // For Extract and Clone, we must clone this level.
+ if ( action > 0 && !levelStartNode.equals( endNode ) ) // action = 0 = Delete
+ levelClone = clone.append( levelStartNode.clone() );
+
+ // The processing of siblings may have already been done by the parent.
+ if ( !startParents[ k ] || levelStartNode.$.parentNode != startParents[ k ].$.parentNode ) {
+ currentNode = levelStartNode.getPrevious();
+
+ while ( currentNode ) {
+ // Stop processing when the current node matches a node in the
+ // startParents tree or if it is the startNode.
+ if ( currentNode.equals( startParents[ k ] ) || currentNode.equals( startNode ) )
+ break;
+
+ // Cache the next sibling.
+ currentSibling = currentNode.getPrevious();
+
+ // If cloning, just clone it.
+ if ( action == 2 ) // 2 = Clone
+ clone.$.insertBefore( currentNode.$.cloneNode( true ), clone.$.firstChild );
+ else {
+ // Both Delete and Extract will remove the node.
+ currentNode.remove();
+
+ // When Extracting, mode the removed node to the docFrag.
+ if ( action == 1 ) // 1 = Extract
+ clone.$.insertBefore( currentNode.$, clone.$.firstChild );
+ }
+
+ currentNode = currentSibling;
+ }
+ }
+
+ if ( clone )
+ clone = levelClone;
+ }
+
+ if ( action == 2 ) // 2 = Clone.
+ {
+ // No changes in the DOM should be done, so fix the split text (if any).
+
+ var startTextNode = range.startContainer;
+ if ( startTextNode.type == CKEDITOR.NODE_TEXT ) {
+ startTextNode.$.data += startTextNode.$.nextSibling.data;
+ startTextNode.$.parentNode.removeChild( startTextNode.$.nextSibling );
+ }
+
+ var endTextNode = range.endContainer;
+ if ( endTextNode.type == CKEDITOR.NODE_TEXT && endTextNode.$.nextSibling ) {
+ endTextNode.$.data += endTextNode.$.nextSibling.data;
+ endTextNode.$.parentNode.removeChild( endTextNode.$.nextSibling );
+ }
+ } else {
+ // Collapse the range.
+
+ // If a node has been partially selected, collapse the range between
+ // topStart and topEnd. Otherwise, simply collapse it to the start. (W3C specs).
+ if ( topStart && topEnd && ( startNode.$.parentNode != topStart.$.parentNode || endNode.$.parentNode != topEnd.$.parentNode ) ) {
+ var endIndex = topEnd.getIndex();
+
+ // If the start node is to be removed, we must correct the
+ // index to reflect the removal.
+ if ( removeStartNode && topEnd.$.parentNode == startNode.$.parentNode )
+ endIndex--;
+
+ // Merge splitted parents.
+ if ( mergeThen && topStart.type == CKEDITOR.NODE_ELEMENT ) {
+ var span = CKEDITOR.dom.element.createFromHtml( ' ', range.document );
+ span.insertAfter( topStart );
+ topStart.mergeSiblings( false );
+ range.moveToBookmark( { startNode: span } );
+ } else
+ range.setStart( topEnd.getParent(), endIndex );
+ }
+
+ // Collapse it to the start.
+ range.collapse( true );
+ }
+
+ // Cleanup any marked node.
+ if ( removeStartNode )
+ startNode.remove();
+
+ if ( removeEndNode && endNode.$.parentNode )
+ endNode.remove();
+ };
+
+ var inlineChildReqElements = { abbr: 1, acronym: 1, b: 1, bdo: 1, big: 1, cite: 1, code: 1, del: 1,
+ dfn: 1, em: 1, font: 1, i: 1, ins: 1, label: 1, kbd: 1, q: 1, samp: 1, small: 1, span: 1, strike: 1,
+ strong: 1, sub: 1, sup: 1, tt: 1, u: 1, 'var': 1 };
+
+ // Creates the appropriate node evaluator for the dom walker used inside
+ // check(Start|End)OfBlock.
+ function getCheckStartEndBlockEvalFunction() {
+ var skipBogus = false,
+ whitespaces = CKEDITOR.dom.walker.whitespaces(),
+ bookmarkEvaluator = CKEDITOR.dom.walker.bookmark( true ),
+ isBogus = CKEDITOR.dom.walker.bogus();
+
+ return function( node ) {
+ // First skip empty nodes
+ if ( bookmarkEvaluator( node ) || whitespaces( node ) )
+ return true;
+
+ // Skip the bogus node at the end of block.
+ if ( isBogus( node ) && !skipBogus ) {
+ skipBogus = true;
+ return true;
+ }
+
+ // If there's any visible text, then we're not at the start.
+ if ( node.type == CKEDITOR.NODE_TEXT &&
+ ( node.hasAscendant( 'pre' ) ||
+ CKEDITOR.tools.trim( node.getText() ).length ) )
+ return false;
+
+ // If there are non-empty inline elements (e.g. ), then we're not
+ // at the start.
+ if ( node.type == CKEDITOR.NODE_ELEMENT && !node.is( inlineChildReqElements ) )
+ return false;
+
+ return true;
+ };
+ }
+
+
+ var isBogus = CKEDITOR.dom.walker.bogus(),
+ nbspRegExp = /^[\t\r\n ]*(?: |\xa0)$/,
+ editableEval = CKEDITOR.dom.walker.editable(),
+ notIgnoredEval = CKEDITOR.dom.walker.ignored( true );
+
+ // Evaluator for CKEDITOR.dom.element::checkBoundaryOfElement, reject any
+ // text node and non-empty elements unless it's being bookmark text.
+ function elementBoundaryEval( checkStart ) {
+ var whitespaces = CKEDITOR.dom.walker.whitespaces(),
+ bookmark = CKEDITOR.dom.walker.bookmark( 1 );
+
+ return function( node ) {
+ // First skip empty nodes.
+ if ( bookmark( node ) || whitespaces( node ) )
+ return true;
+
+ // Tolerant bogus br when checking at the end of block.
+ // Reject any text node unless it's being bookmark
+ // OR it's spaces.
+ // Reject any element unless it's being invisible empty. (#3883)
+ return !checkStart && isBogus( node ) ||
+ node.type == CKEDITOR.NODE_ELEMENT &&
+ node.is( CKEDITOR.dtd.$removeEmpty );
+ };
+ }
+
+ function getNextEditableNode( isPrevious ) {
+ return function() {
+ var first;
+
+ return this[ isPrevious ? 'getPreviousNode' : 'getNextNode' ]( function( node ) {
+ // Cache first not ignorable node.
+ if ( !first && notIgnoredEval( node ) )
+ first = node;
+
+ // Return true if found editable node, but not a bogus next to start of our lookup (first != bogus).
+ return editableEval( node ) && !( isBogus( node ) && node.equals( first ) );
+ } );
+ };
+ }
+
+ CKEDITOR.dom.range.prototype = {
+ /**
+ * Clones this range.
+ *
+ * @returns {CKEDITOR.dom.range}
+ */
+ clone: function() {
+ var clone = new CKEDITOR.dom.range( this.root );
+
+ clone.startContainer = this.startContainer;
+ clone.startOffset = this.startOffset;
+ clone.endContainer = this.endContainer;
+ clone.endOffset = this.endOffset;
+ clone.collapsed = this.collapsed;
+
+ return clone;
+ },
+
+ /**
+ * Makes range collapsed by moving its start point (or end point if `toStart==true`)
+ * to the second end.
+ *
+ * @param {Boolean} toStart Collapse range "to start".
+ */
+ collapse: function( toStart ) {
+ if ( toStart ) {
+ this.endContainer = this.startContainer;
+ this.endOffset = this.startOffset;
+ } else {
+ this.startContainer = this.endContainer;
+ this.startOffset = this.endOffset;
+ }
+
+ this.collapsed = true;
+ },
+
+ /**
+ * The content nodes of the range are cloned and added to a document fragment, which is returned.
+ *
+ * **Note:** Text selection may lost after invoking this method (caused by text node splitting).
+ *
+ * @returns {CKEDITOR.dom.documentFragment} Document fragment containing clone of range's content.
+ */
+ cloneContents: function() {
+ var docFrag = new CKEDITOR.dom.documentFragment( this.document );
+
+ if ( !this.collapsed )
+ execContentsAction( this, 2, docFrag );
+
+ return docFrag;
+ },
+
+ /**
+ * Deletes the content nodes of the range permanently from the DOM tree.
+ *
+ * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.
+ */
+ deleteContents: function( mergeThen ) {
+ if ( this.collapsed )
+ return;
+
+ execContentsAction( this, 0, null, mergeThen );
+ },
+
+ /**
+ * The content nodes of the range are cloned and added to a document fragment,
+ * meanwhile they are removed permanently from the DOM tree.
+ *
+ * @param {Boolean} [mergeThen] Merge any splitted elements result in DOM true due to partial selection.
+ * @returns {CKEDITOR.dom.documentFragment} Document fragment containing extracted content.
+ */
+ extractContents: function( mergeThen ) {
+ var docFrag = new CKEDITOR.dom.documentFragment( this.document );
+
+ if ( !this.collapsed )
+ execContentsAction( this, 1, docFrag, mergeThen );
+
+ return docFrag;
+ },
+
+ /**
+ * Creates a bookmark object, which can be later used to restore the
+ * range by using the {@link #moveToBookmark} function.
+ *
+ * This is an "intrusive" way to create a bookmark. It includes ` ` tags
+ * in the range boundaries. The advantage of it is that it is possible to
+ * handle DOM mutations when moving back to the bookmark.
+ *
+ * **Note:** The inclusion of nodes in the DOM is a design choice and
+ * should not be changed as there are other points in the code that may be
+ * using those nodes to perform operations.
+ *
+ * @param {Boolean} [serializable] Indicates that the bookmark nodes
+ * must contain IDs, which can be used to restore the range even
+ * when these nodes suffer mutations (like cloning or `innerHTML` change).
+ * @returns {Object} And object representing a bookmark.
+ * @returns {CKEDITOR.dom.node/String} return.startNode Node or element ID.
+ * @returns {CKEDITOR.dom.node/String} return.endNode Node or element ID.
+ * @returns {Boolean} return.serializable
+ * @returns {Boolean} return.collapsed
+ */
+ createBookmark: function( serializable ) {
+ var startNode, endNode;
+ var baseId;
+ var clone;
+ var collapsed = this.collapsed;
+
+ startNode = this.document.createElement( 'span' );
+ startNode.data( 'cke-bookmark', 1 );
+ startNode.setStyle( 'display', 'none' );
+
+ // For IE, it must have something inside, otherwise it may be
+ // removed during DOM operations.
+ startNode.setHtml( ' ' );
+
+ if ( serializable ) {
+ baseId = 'cke_bm_' + CKEDITOR.tools.getNextNumber();
+ startNode.setAttribute( 'id', baseId + ( collapsed ? 'C' : 'S' ) );
+ }
+
+ // If collapsed, the endNode will not be created.
+ if ( !collapsed ) {
+ endNode = startNode.clone();
+ endNode.setHtml( ' ' );
+
+ if ( serializable )
+ endNode.setAttribute( 'id', baseId + 'E' );
+
+ clone = this.clone();
+ clone.collapse();
+ clone.insertNode( endNode );
+ }
+
+ clone = this.clone();
+ clone.collapse( true );
+ clone.insertNode( startNode );
+
+ // Update the range position.
+ if ( endNode ) {
+ this.setStartAfter( startNode );
+ this.setEndBefore( endNode );
+ } else
+ this.moveToPosition( startNode, CKEDITOR.POSITION_AFTER_END );
+
+ return {
+ startNode: serializable ? baseId + ( collapsed ? 'C' : 'S' ) : startNode,
+ endNode: serializable ? baseId + 'E' : endNode,
+ serializable: serializable,
+ collapsed: collapsed
+ };
+ },
+
+ /**
+ * Creates a "non intrusive" and "mutation sensible" bookmark. This
+ * kind of bookmark should be used only when the DOM is supposed to
+ * remain stable after its creation.
+ *
+ * @param {Boolean} [normalized] Indicates that the bookmark must
+ * be normalized. When normalized, the successive text nodes are
+ * considered a single node. To successfully load a normalized
+ * bookmark, the DOM tree must also be normalized before calling
+ * {@link #moveToBookmark}.
+ * @returns {Object} An object representing the bookmark.
+ * @returns {Array} return.start Start container's address (see {@link CKEDITOR.dom.node#getAddress}).
+ * @returns {Array} return.end Start container's address.
+ * @returns {Number} return.startOffset
+ * @returns {Number} return.endOffset
+ * @returns {Boolean} return.collapsed
+ * @returns {Boolean} return.normalized
+ * @returns {Boolean} return.is2 This is "bookmark2".
+ */
+ createBookmark2: ( function() {
+ // Returns true for limit anchored in element and placed between text nodes.
+ //
+ // v
+ // [text node] [text node] -> true
+ //
+ // v
+ // [text node] -> false
+ //
+ // v
+ // [text node][text node] -> false (limit is anchored in text node)
+ function betweenTextNodes( container, offset ) {
+ // Not anchored in element or limit is on the edge.
+ if ( container.type != CKEDITOR.NODE_ELEMENT || offset === 0 || offset == container.getChildCount() )
+ return 0;
+
+ return container.getChild( offset - 1 ).type == CKEDITOR.NODE_TEXT &&
+ container.getChild( offset ).type == CKEDITOR.NODE_TEXT;
+ }
+
+ // Sums lengths of all preceding text nodes.
+ function getLengthOfPrecedingTextNodes( node ) {
+ var sum = 0;
+
+ while ( ( node = node.getPrevious() ) && node.type == CKEDITOR.NODE_TEXT )
+ sum += node.getLength();
+
+ return sum;
+ }
+
+ function normalize( limit ) {
+ var container = limit.container,
+ offset = limit.offset;
+
+ // If limit is between text nodes move it to the end of preceding one,
+ // because they will be merged.
+ if ( betweenTextNodes( container, offset ) ) {
+ container = container.getChild( offset - 1 );
+ offset = container.getLength();
+ }
+
+ // Now, if limit is anchored in element and has at least two nodes before it,
+ // it may happen that some of them will be merged. Normalize the offset
+ // by setting it to normalized index of its preceding node.
+ if ( container.type == CKEDITOR.NODE_ELEMENT && offset > 1 )
+ offset = container.getChild( offset - 1 ).getIndex( true ) + 1;
+
+ // The last step - fix the offset inside text node by adding
+ // lengths of preceding text nodes which will be merged with container.
+ if ( container.type == CKEDITOR.NODE_TEXT )
+ offset += getLengthOfPrecedingTextNodes( container );
+
+ limit.container = container;
+ limit.offset = offset;
+ }
+
+ return function( normalized ) {
+ var collapsed = this.collapsed,
+ bmStart = {
+ container: this.startContainer,
+ offset: this.startOffset
+ },
+ bmEnd = {
+ container: this.endContainer,
+ offset: this.endOffset
+ };
+
+ if ( normalized ) {
+ normalize( bmStart );
+
+ if ( !collapsed )
+ normalize( bmEnd );
+ }
+
+ return {
+ start: bmStart.container.getAddress( normalized ),
+ end: collapsed ? null : bmEnd.container.getAddress( normalized ),
+ startOffset: bmStart.offset,
+ endOffset: bmEnd.offset,
+ normalized: normalized,
+ collapsed: collapsed,
+ is2: true // It's a createBookmark2 bookmark.
+ };
+ };
+ } )(),
+
+ /**
+ * Moves this range to the given bookmark. See {@link #createBookmark} and {@link #createBookmark2}.
+ *
+ * If serializable bookmark passed, then its `` markers will be removed.
+ *
+ * @param {Object} bookmark
+ */
+ moveToBookmark: function( bookmark ) {
+ if ( bookmark.is2 ) // Created with createBookmark2().
+ {
+ // Get the start information.
+ var startContainer = this.document.getByAddress( bookmark.start, bookmark.normalized ),
+ startOffset = bookmark.startOffset;
+
+ // Get the end information.
+ var endContainer = bookmark.end && this.document.getByAddress( bookmark.end, bookmark.normalized ),
+ endOffset = bookmark.endOffset;
+
+ // Set the start boundary.
+ this.setStart( startContainer, startOffset );
+
+ // Set the end boundary. If not available, collapse it.
+ if ( endContainer )
+ this.setEnd( endContainer, endOffset );
+ else
+ this.collapse( true );
+ } else // Created with createBookmark().
+ {
+ var serializable = bookmark.serializable,
+ startNode = serializable ? this.document.getById( bookmark.startNode ) : bookmark.startNode,
+ endNode = serializable ? this.document.getById( bookmark.endNode ) : bookmark.endNode;
+
+ // Set the range start at the bookmark start node position.
+ this.setStartBefore( startNode );
+
+ // Remove it, because it may interfere in the setEndBefore call.
+ startNode.remove();
+
+ // Set the range end at the bookmark end node position, or simply
+ // collapse it if it is not available.
+ if ( endNode ) {
+ this.setEndBefore( endNode );
+ endNode.remove();
+ } else
+ this.collapse( true );
+ }
+ },
+
+ /**
+ * Returns two nodes which are on the boundaries of this range.
+ *
+ * @returns {Object}
+ * @returns {CKEDITOR.dom.node} return.startNode
+ * @returns {CKEDITOR.dom.node} return.endNode
+ * @todo precise desc/algorithm
+ */
+ getBoundaryNodes: function() {
+ var startNode = this.startContainer,
+ endNode = this.endContainer,
+ startOffset = this.startOffset,
+ endOffset = this.endOffset,
+ childCount;
+
+ if ( startNode.type == CKEDITOR.NODE_ELEMENT ) {
+ childCount = startNode.getChildCount();
+ if ( childCount > startOffset )
+ startNode = startNode.getChild( startOffset );
+ else if ( childCount < 1 )
+ startNode = startNode.getPreviousSourceNode();
+ else // startOffset > childCount but childCount is not 0
+ {
+ // Try to take the node just after the current position.
+ startNode = startNode.$;
+ while ( startNode.lastChild )
+ startNode = startNode.lastChild;
+ startNode = new CKEDITOR.dom.node( startNode );
+
+ // Normally we should take the next node in DFS order. But it
+ // is also possible that we've already reached the end of
+ // document.
+ startNode = startNode.getNextSourceNode() || startNode;
+ }
+ }
+ if ( endNode.type == CKEDITOR.NODE_ELEMENT ) {
+ childCount = endNode.getChildCount();
+ if ( childCount > endOffset )
+ endNode = endNode.getChild( endOffset ).getPreviousSourceNode( true );
+ else if ( childCount < 1 )
+ endNode = endNode.getPreviousSourceNode();
+ else // endOffset > childCount but childCount is not 0
+ {
+ // Try to take the node just before the current position.
+ endNode = endNode.$;
+ while ( endNode.lastChild )
+ endNode = endNode.lastChild;
+ endNode = new CKEDITOR.dom.node( endNode );
+ }
+ }
+
+ // Sometimes the endNode will come right before startNode for collapsed
+ // ranges. Fix it. (#3780)
+ if ( startNode.getPosition( endNode ) & CKEDITOR.POSITION_FOLLOWING )
+ startNode = endNode;
+
+ return { startNode: startNode, endNode: endNode };
+ },
+
+ /**
+ * Find the node which fully contains the range.
+ *
+ * @param {Boolean} [includeSelf=false]
+ * @param {Boolean} [ignoreTextNode=false] Whether ignore {@link CKEDITOR#NODE_TEXT} type.
+ * @returns {CKEDITOR.dom.element}
+ */
+ getCommonAncestor: function( includeSelf, ignoreTextNode ) {
+ var start = this.startContainer,
+ end = this.endContainer,
+ ancestor;
+
+ if ( start.equals( end ) ) {
+ if ( includeSelf && start.type == CKEDITOR.NODE_ELEMENT && this.startOffset == this.endOffset - 1 )
+ ancestor = start.getChild( this.startOffset );
+ else
+ ancestor = start;
+ } else
+ ancestor = start.getCommonAncestor( end );
+
+ return ignoreTextNode && !ancestor.is ? ancestor.getParent() : ancestor;
+ },
+
+ /**
+ * Transforms the {@link #startContainer} and {@link #endContainer} properties from text
+ * nodes to element nodes, whenever possible. This is actually possible
+ * if either of the boundary containers point to a text node, and its
+ * offset is set to zero, or after the last char in the node.
+ */
+ optimize: function() {
+ var container = this.startContainer;
+ var offset = this.startOffset;
+
+ if ( container.type != CKEDITOR.NODE_ELEMENT ) {
+ if ( !offset )
+ this.setStartBefore( container );
+ else if ( offset >= container.getLength() )
+ this.setStartAfter( container );
+ }
+
+ container = this.endContainer;
+ offset = this.endOffset;
+
+ if ( container.type != CKEDITOR.NODE_ELEMENT ) {
+ if ( !offset )
+ this.setEndBefore( container );
+ else if ( offset >= container.getLength() )
+ this.setEndAfter( container );
+ }
+ },
+
+ /**
+ * Move the range out of bookmark nodes if they'd been the container.
+ */
+ optimizeBookmark: function() {
+ var startNode = this.startContainer,
+ endNode = this.endContainer;
+
+ if ( startNode.is && startNode.is( 'span' ) && startNode.data( 'cke-bookmark' ) )
+ this.setStartAt( startNode, CKEDITOR.POSITION_BEFORE_START );
+ if ( endNode && endNode.is && endNode.is( 'span' ) && endNode.data( 'cke-bookmark' ) )
+ this.setEndAt( endNode, CKEDITOR.POSITION_AFTER_END );
+ },
+
+ /**
+ * @param {Boolean} [ignoreStart=false]
+ * @param {Boolean} [ignoreEnd=false]
+ * @todo precise desc/algorithm
+ */
+ trim: function( ignoreStart, ignoreEnd ) {
+ var startContainer = this.startContainer,
+ startOffset = this.startOffset,
+ collapsed = this.collapsed;
+ if ( ( !ignoreStart || collapsed ) && startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
+ // If the offset is zero, we just insert the new node before
+ // the start.
+ if ( !startOffset ) {
+ startOffset = startContainer.getIndex();
+ startContainer = startContainer.getParent();
+ }
+ // If the offset is at the end, we'll insert it after the text
+ // node.
+ else if ( startOffset >= startContainer.getLength() ) {
+ startOffset = startContainer.getIndex() + 1;
+ startContainer = startContainer.getParent();
+ }
+ // In other case, we split the text node and insert the new
+ // node at the split point.
+ else {
+ var nextText = startContainer.split( startOffset );
+
+ startOffset = startContainer.getIndex() + 1;
+ startContainer = startContainer.getParent();
+
+ // Check all necessity of updating the end boundary.
+ if ( this.startContainer.equals( this.endContainer ) )
+ this.setEnd( nextText, this.endOffset - this.startOffset );
+ else if ( startContainer.equals( this.endContainer ) )
+ this.endOffset += 1;
+ }
+
+ this.setStart( startContainer, startOffset );
+
+ if ( collapsed ) {
+ this.collapse( true );
+ return;
+ }
+ }
+
+ var endContainer = this.endContainer;
+ var endOffset = this.endOffset;
+
+ if ( !( ignoreEnd || collapsed ) && endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
+ // If the offset is zero, we just insert the new node before
+ // the start.
+ if ( !endOffset ) {
+ endOffset = endContainer.getIndex();
+ endContainer = endContainer.getParent();
+ }
+ // If the offset is at the end, we'll insert it after the text
+ // node.
+ else if ( endOffset >= endContainer.getLength() ) {
+ endOffset = endContainer.getIndex() + 1;
+ endContainer = endContainer.getParent();
+ }
+ // In other case, we split the text node and insert the new
+ // node at the split point.
+ else {
+ endContainer.split( endOffset );
+
+ endOffset = endContainer.getIndex() + 1;
+ endContainer = endContainer.getParent();
+ }
+
+ this.setEnd( endContainer, endOffset );
+ }
+ },
+
+ /**
+ * Expands the range so that partial units are completely contained.
+ *
+ * @param unit {Number} The unit type to expand with.
+ * @param {Boolean} [excludeBrs=false] Whether include line-breaks when expanding.
+ */
+ enlarge: function( unit, excludeBrs ) {
+ var leadingWhitespaceRegex = new RegExp( /[^\s\ufeff]/ );
+
+ switch ( unit ) {
+ case CKEDITOR.ENLARGE_INLINE:
+ var enlargeInlineOnly = 1;
+ /*jsl:fallthru*/
+ case CKEDITOR.ENLARGE_ELEMENT:
+
+ if ( this.collapsed )
+ return;
+
+ // Get the common ancestor.
+ var commonAncestor = this.getCommonAncestor();
+
+ var boundary = this.root;
+
+ // For each boundary
+ // a. Depending on its position, find out the first node to be checked (a sibling) or, if not available, to be enlarge.
+ // b. Go ahead checking siblings and enlarging the boundary as much as possible until the common ancestor is not reached. After reaching the common ancestor, just save the enlargeable node to be used later.
+
+ var startTop, endTop;
+
+ var enlargeable, sibling, commonReached;
+
+ // Indicates that the node can be added only if whitespace
+ // is available before it.
+ var needsWhiteSpace = false;
+ var isWhiteSpace;
+ var siblingText;
+
+ // Process the start boundary.
+
+ var container = this.startContainer;
+ var offset = this.startOffset;
+
+ if ( container.type == CKEDITOR.NODE_TEXT ) {
+ if ( offset ) {
+ // Check if there is any non-space text before the
+ // offset. Otherwise, container is null.
+ container = !CKEDITOR.tools.trim( container.substring( 0, offset ) ).length && container;
+
+ // If we found only whitespace in the node, it
+ // means that we'll need more whitespace to be able
+ // to expand. For example, can be expanded in
+ // "A [B]", but not in "A [B]".
+ needsWhiteSpace = !!container;
+ }
+
+ if ( container ) {
+ if ( !( sibling = container.getPrevious() ) )
+ enlargeable = container.getParent();
+ }
+ } else {
+ // If we have offset, get the node preceeding it as the
+ // first sibling to be checked.
+ if ( offset )
+ sibling = container.getChild( offset - 1 ) || container.getLast();
+
+ // If there is no sibling, mark the container to be
+ // enlarged.
+ if ( !sibling )
+ enlargeable = container;
+ }
+
+ // Ensures that enlargeable can be indeed enlarged, if not it will be nulled.
+ enlargeable = getValidEnlargeable( enlargeable );
+
+ while ( enlargeable || sibling ) {
+ if ( enlargeable && !sibling ) {
+ // If we reached the common ancestor, mark the flag
+ // for it.
+ if ( !commonReached && enlargeable.equals( commonAncestor ) )
+ commonReached = true;
+
+ if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) )
+ break;
+
+ // If we don't need space or this element breaks
+ // the line, then enlarge it.
+ if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) {
+ needsWhiteSpace = false;
+
+ // If the common ancestor has been reached,
+ // we'll not enlarge it immediately, but just
+ // mark it to be enlarged later if the end
+ // boundary also enlarges it.
+ if ( commonReached )
+ startTop = enlargeable;
+ else
+ this.setStartBefore( enlargeable );
+ }
+
+ sibling = enlargeable.getPrevious();
+ }
+
+ // Check all sibling nodes preceeding the enlargeable
+ // node. The node wil lbe enlarged only if none of them
+ // blocks it.
+ while ( sibling ) {
+ // This flag indicates that this node has
+ // whitespaces at the end.
+ isWhiteSpace = false;
+
+ if ( sibling.type == CKEDITOR.NODE_COMMENT ) {
+ sibling = sibling.getPrevious();
+ continue;
+ } else if ( sibling.type == CKEDITOR.NODE_TEXT ) {
+ siblingText = sibling.getText();
+
+ if ( leadingWhitespaceRegex.test( siblingText ) )
+ sibling = null;
+
+ isWhiteSpace = /[\s\ufeff]$/.test( siblingText );
+ } else {
+ // If this is a visible element.
+ // We need to check for the bookmark attribute because IE insists on
+ // rendering the display:none nodes we use for bookmarks. (#3363)
+ // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
+ if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
+ // We'll accept it only if we need
+ // whitespace, and this is an inline
+ // element with whitespace only.
+ if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) {
+ // It must contains spaces and inline elements only.
+
+ siblingText = sibling.getText();
+
+ if ( leadingWhitespaceRegex.test( siblingText ) ) // Spaces + Zero Width No-Break Space (U+FEFF)
+ sibling = null;
+ else {
+ var allChildren = sibling.$.getElementsByTagName( '*' );
+ for ( var i = 0, child; child = allChildren[ i++ ]; ) {
+ if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) {
+ sibling = null;
+ break;
+ }
+ }
+ }
+
+ if ( sibling )
+ isWhiteSpace = !!siblingText.length;
+ } else
+ sibling = null;
+ }
+ }
+
+ // A node with whitespaces has been found.
+ if ( isWhiteSpace ) {
+ // Enlarge the last enlargeable node, if we
+ // were waiting for spaces.
+ if ( needsWhiteSpace ) {
+ if ( commonReached )
+ startTop = enlargeable;
+ else if ( enlargeable )
+ this.setStartBefore( enlargeable );
+ } else
+ needsWhiteSpace = true;
+ }
+
+ if ( sibling ) {
+ var next = sibling.getPrevious();
+
+ if ( !enlargeable && !next ) {
+ // Set the sibling as enlargeable, so it's
+ // parent will be get later outside this while.
+ enlargeable = sibling;
+ sibling = null;
+ break;
+ }
+
+ sibling = next;
+ } else {
+ // If sibling has been set to null, then we
+ // need to stop enlarging.
+ enlargeable = null;
+ }
+ }
+
+ if ( enlargeable )
+ enlargeable = getValidEnlargeable( enlargeable.getParent() );
+ }
+
+ // Process the end boundary. This is basically the same
+ // code used for the start boundary, with small changes to
+ // make it work in the oposite side (to the right). This
+ // makes it difficult to reuse the code here. So, fixes to
+ // the above code are likely to be replicated here.
+
+ container = this.endContainer;
+ offset = this.endOffset;
+
+ // Reset the common variables.
+ enlargeable = sibling = null;
+ commonReached = needsWhiteSpace = false;
+
+ // Function check if there are only whitespaces from the given starting point
+ // (startContainer and startOffset) till the end on block.
+ // Examples ("[" is the start point):
+ // - foo[ - will return true,
+ // - foo[ - will return true,
+ // - foo[ bar - will return false,
+ // - foo[ bar - will return false,
+ // - foo[ - will return false.
+ function onlyWhiteSpaces( startContainer, startOffset ) {
+ // We need to enlarge range if there is white space at the end of the block,
+ // because it is not displayed in WYSIWYG mode and user can not select it. So
+ // "foo[bar] " should be changed to "foo[bar ] ". On the other hand
+ // we should do nothing if we are not at the end of the block, so this should not
+ // be changed: "[foo] bar ".
+ var walkerRange = new CKEDITOR.dom.range( boundary );
+ walkerRange.setStart( startContainer, startOffset );
+ // The guard will find the end of range so I put boundary here.
+ walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END );
+
+ var walker = new CKEDITOR.dom.walker( walkerRange ),
+ node;
+
+ walker.guard = function( node, movingOut ) {
+ // Stop if you exit block.
+ return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() );
+ };
+
+ while ( ( node = walker.next() ) ) {
+ if ( node.type != CKEDITOR.NODE_TEXT ) {
+ // Stop if you enter to any node (walker.next() will return node only
+ // it goes out, not if it is go into node).
+ return false;
+ } else {
+ // Trim the first node to startOffset.
+ if ( node != startContainer )
+ siblingText = node.getText();
+ else
+ siblingText = node.substring( startOffset );
+
+ // Check if it is white space.
+ if ( leadingWhitespaceRegex.test( siblingText ) )
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ if ( container.type == CKEDITOR.NODE_TEXT ) {
+ // Check if there is only white space after the offset.
+ if ( CKEDITOR.tools.trim( container.substring( offset ) ).length ) {
+ // If we found only whitespace in the node, it
+ // means that we'll need more whitespace to be able
+ // to expand. For example, can be expanded in
+ // "A [B]", but not in "A [B]".
+ needsWhiteSpace = true;
+ } else {
+ needsWhiteSpace = !container.getLength();
+
+ if ( offset == container.getLength() ) {
+ // If we are at the end of container and this is the last text node,
+ // we should enlarge end to the parent.
+ if ( !( sibling = container.getNext() ) )
+ enlargeable = container.getParent();
+ } else {
+ // If we are in the middle on text node and there are only whitespaces
+ // till the end of block, we should enlarge element.
+ if ( onlyWhiteSpaces( container, offset ) )
+ enlargeable = container.getParent();
+ }
+ }
+ } else {
+ // Get the node right after the boudary to be checked
+ // first.
+ sibling = container.getChild( offset );
+
+ if ( !sibling )
+ enlargeable = container;
+ }
+
+ while ( enlargeable || sibling ) {
+ if ( enlargeable && !sibling ) {
+ if ( !commonReached && enlargeable.equals( commonAncestor ) )
+ commonReached = true;
+
+ if ( enlargeInlineOnly ? enlargeable.isBlockBoundary() : !boundary.contains( enlargeable ) )
+ break;
+
+ if ( !needsWhiteSpace || enlargeable.getComputedStyle( 'display' ) != 'inline' ) {
+ needsWhiteSpace = false;
+
+ if ( commonReached )
+ endTop = enlargeable;
+ else if ( enlargeable )
+ this.setEndAfter( enlargeable );
+ }
+
+ sibling = enlargeable.getNext();
+ }
+
+ while ( sibling ) {
+ isWhiteSpace = false;
+
+ if ( sibling.type == CKEDITOR.NODE_TEXT ) {
+ siblingText = sibling.getText();
+
+ // Check if there are not whitespace characters till the end of editable.
+ // If so stop expanding.
+ if ( !onlyWhiteSpaces( sibling, 0 ) )
+ sibling = null;
+
+ isWhiteSpace = /^[\s\ufeff]/.test( siblingText );
+ } else if ( sibling.type == CKEDITOR.NODE_ELEMENT ) {
+ // If this is a visible element.
+ // We need to check for the bookmark attribute because IE insists on
+ // rendering the display:none nodes we use for bookmarks. (#3363)
+ // Line-breaks (br) are rendered with zero width, which we don't want to include. (#7041)
+ if ( ( sibling.$.offsetWidth > 0 || excludeBrs && sibling.is( 'br' ) ) && !sibling.data( 'cke-bookmark' ) ) {
+ // We'll accept it only if we need
+ // whitespace, and this is an inline
+ // element with whitespace only.
+ if ( needsWhiteSpace && CKEDITOR.dtd.$removeEmpty[ sibling.getName() ] ) {
+ // It must contains spaces and inline elements only.
+
+ siblingText = sibling.getText();
+
+ if ( leadingWhitespaceRegex.test( siblingText ) )
+ sibling = null;
+ else {
+ allChildren = sibling.$.getElementsByTagName( '*' );
+ for ( i = 0; child = allChildren[ i++ ]; ) {
+ if ( !CKEDITOR.dtd.$removeEmpty[ child.nodeName.toLowerCase() ] ) {
+ sibling = null;
+ break;
+ }
+ }
+ }
+
+ if ( sibling )
+ isWhiteSpace = !!siblingText.length;
+ } else
+ sibling = null;
+ }
+ } else
+ isWhiteSpace = 1;
+
+ if ( isWhiteSpace ) {
+ if ( needsWhiteSpace ) {
+ if ( commonReached )
+ endTop = enlargeable;
+ else
+ this.setEndAfter( enlargeable );
+ }
+ }
+
+ if ( sibling ) {
+ next = sibling.getNext();
+
+ if ( !enlargeable && !next ) {
+ enlargeable = sibling;
+ sibling = null;
+ break;
+ }
+
+ sibling = next;
+ } else {
+ // If sibling has been set to null, then we
+ // need to stop enlarging.
+ enlargeable = null;
+ }
+ }
+
+ if ( enlargeable )
+ enlargeable = getValidEnlargeable( enlargeable.getParent() );
+ }
+
+ // If the common ancestor can be enlarged by both boundaries, then include it also.
+ if ( startTop && endTop ) {
+ commonAncestor = startTop.contains( endTop ) ? endTop : startTop;
+
+ this.setStartBefore( commonAncestor );
+ this.setEndAfter( commonAncestor );
+ }
+ break;
+
+ case CKEDITOR.ENLARGE_BLOCK_CONTENTS:
+ case CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS:
+
+ // Enlarging the start boundary.
+ var walkerRange = new CKEDITOR.dom.range( this.root );
+
+ boundary = this.root;
+
+ walkerRange.setStartAt( boundary, CKEDITOR.POSITION_AFTER_START );
+ walkerRange.setEnd( this.startContainer, this.startOffset );
+
+ var walker = new CKEDITOR.dom.walker( walkerRange ),
+ blockBoundary, // The node on which the enlarging should stop.
+ tailBr, // In case BR as block boundary.
+ notBlockBoundary = CKEDITOR.dom.walker.blockBoundary( ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? { br: 1 } : null ),
+ inNonEditable = null,
+ // Record the encountered 'blockBoundary' for later use.
+ boundaryGuard = function( node ) {
+ // We should not check contents of non-editable elements. It may happen
+ // that inline widget has display:table child which should not block range#enlarge.
+ // When encoutered non-editable element...
+ if ( node.type == CKEDITOR.NODE_ELEMENT && node.getAttribute( 'contenteditable' ) == 'false' ) {
+ if ( inNonEditable ) {
+ // ... in which we already were, reset it (because we're leaving it) and return.
+ if ( inNonEditable.equals( node ) ) {
+ inNonEditable = null;
+ return;
+ }
+ // ... which we're entering, remember it but check it (no return).
+ } else
+ inNonEditable = node;
+ }
+ // When we are in non-editable element, do not check if current node is a block boundary.
+ else if ( inNonEditable )
+ return;
+
+ var retval = notBlockBoundary( node );
+ if ( !retval )
+ blockBoundary = node;
+ return retval;
+ },
+ // Record the encounted 'tailBr' for later use.
+ tailBrGuard = function( node ) {
+ var retval = boundaryGuard( node );
+ if ( !retval && node.is && node.is( 'br' ) )
+ tailBr = node;
+ return retval;
+ };
+
+ walker.guard = boundaryGuard;
+
+ enlargeable = walker.lastBackward();
+
+ // It's the body which stop the enlarging if no block boundary found.
+ blockBoundary = blockBoundary || boundary;
+
+ // Start the range either after the end of found block (... [text)
+ // or at the start of block ([text...), by comparing the document position
+ // with 'enlargeable' node.
+ this.setStartAt( blockBoundary, !blockBoundary.is( 'br' ) && ( !enlargeable && this.checkStartOfBlock() || enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_AFTER_END );
+
+ // Avoid enlarging the range further when end boundary spans right after the BR. (#7490)
+ if ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) {
+ var theRange = this.clone();
+ walker = new CKEDITOR.dom.walker( theRange );
+
+ var whitespaces = CKEDITOR.dom.walker.whitespaces(),
+ bookmark = CKEDITOR.dom.walker.bookmark();
+
+ walker.evaluator = function( node ) {
+ return !whitespaces( node ) && !bookmark( node );
+ };
+ var previous = walker.previous();
+ if ( previous && previous.type == CKEDITOR.NODE_ELEMENT && previous.is( 'br' ) )
+ return;
+ }
+
+
+ // Enlarging the end boundary.
+ walkerRange = this.clone();
+ walkerRange.collapse();
+ walkerRange.setEndAt( boundary, CKEDITOR.POSITION_BEFORE_END );
+ walker = new CKEDITOR.dom.walker( walkerRange );
+
+ // tailBrGuard only used for on range end.
+ walker.guard = ( unit == CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS ) ? tailBrGuard : boundaryGuard;
+ blockBoundary = null;
+ // End the range right before the block boundary node.
+
+ enlargeable = walker.lastForward();
+
+ // It's the body which stop the enlarging if no block boundary found.
+ blockBoundary = blockBoundary || boundary;
+
+ // Close the range either before the found block start (text] ... ) or at the block end (...text])
+ // by comparing the document position with 'enlargeable' node.
+ this.setEndAt( blockBoundary, ( !enlargeable && this.checkEndOfBlock() || enlargeable && blockBoundary.contains( enlargeable ) ) ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_BEFORE_START );
+ // We must include the at the end of range if there's
+ // one and we're expanding list item contents
+ if ( tailBr )
+ this.setEndAfter( tailBr );
+ }
+
+ // Ensures that returned element can be enlarged by selection, null otherwise.
+ // @param {CKEDITOR.dom.element} enlargeable
+ // @returns {CKEDITOR.dom.element/null}
+ function getValidEnlargeable( enlargeable ) {
+ return enlargeable && enlargeable.type == CKEDITOR.NODE_ELEMENT && enlargeable.hasAttribute( 'contenteditable' ) ? null : enlargeable;
+ }
+ },
+
+ /**
+ * Descrease the range to make sure that boundaries
+ * always anchor beside text nodes or innermost element.
+ *
+ * @param {Number} mode The shrinking mode ({@link CKEDITOR#SHRINK_ELEMENT} or {@link CKEDITOR#SHRINK_TEXT}).
+ *
+ * * {@link CKEDITOR#SHRINK_ELEMENT} - Shrink the range boundaries to the edge of the innermost element.
+ * * {@link CKEDITOR#SHRINK_TEXT} - Shrink the range boudaries to anchor by the side of enclosed text
+ * node, range remains if there's no text nodes on boundaries at all.
+ *
+ * @param {Boolean} selectContents Whether result range anchors at the inner OR outer boundary of the node.
+ */
+ shrink: function( mode, selectContents, shrinkOnBlockBoundary ) {
+ // Unable to shrink a collapsed range.
+ if ( !this.collapsed ) {
+ mode = mode || CKEDITOR.SHRINK_TEXT;
+
+ var walkerRange = this.clone();
+
+ var startContainer = this.startContainer,
+ endContainer = this.endContainer,
+ startOffset = this.startOffset,
+ endOffset = this.endOffset,
+ collapsed = this.collapsed;
+
+ // Whether the start/end boundary is moveable.
+ var moveStart = 1,
+ moveEnd = 1;
+
+ if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
+ if ( !startOffset )
+ walkerRange.setStartBefore( startContainer );
+ else if ( startOffset >= startContainer.getLength() )
+ walkerRange.setStartAfter( startContainer );
+ else {
+ // Enlarge the range properly to avoid walker making
+ // DOM changes caused by triming the text nodes later.
+ walkerRange.setStartBefore( startContainer );
+ moveStart = 0;
+ }
+ }
+
+ if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
+ if ( !endOffset )
+ walkerRange.setEndBefore( endContainer );
+ else if ( endOffset >= endContainer.getLength() )
+ walkerRange.setEndAfter( endContainer );
+ else {
+ walkerRange.setEndAfter( endContainer );
+ moveEnd = 0;
+ }
+ }
+
+ var walker = new CKEDITOR.dom.walker( walkerRange ),
+ isBookmark = CKEDITOR.dom.walker.bookmark();
+
+ walker.evaluator = function( node ) {
+ return node.type == ( mode == CKEDITOR.SHRINK_ELEMENT ? CKEDITOR.NODE_ELEMENT : CKEDITOR.NODE_TEXT );
+ };
+
+ var currentElement;
+ walker.guard = function( node, movingOut ) {
+ if ( isBookmark( node ) )
+ return true;
+
+ // Stop when we're shrink in element mode while encountering a text node.
+ if ( mode == CKEDITOR.SHRINK_ELEMENT && node.type == CKEDITOR.NODE_TEXT )
+ return false;
+
+ // Stop when we've already walked "through" an element.
+ if ( movingOut && node.equals( currentElement ) )
+ return false;
+
+ if ( shrinkOnBlockBoundary === false && node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary() )
+ return false;
+
+ // Stop shrinking when encountering an editable border.
+ if ( node.type == CKEDITOR.NODE_ELEMENT && node.hasAttribute( 'contenteditable' ) )
+ return false;
+
+ if ( !movingOut && node.type == CKEDITOR.NODE_ELEMENT )
+ currentElement = node;
+
+ return true;
+ };
+
+ if ( moveStart ) {
+ var textStart = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastForward' : 'next' ]();
+ textStart && this.setStartAt( textStart, selectContents ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_START );
+ }
+
+ if ( moveEnd ) {
+ walker.reset();
+ var textEnd = walker[ mode == CKEDITOR.SHRINK_ELEMENT ? 'lastBackward' : 'previous' ]();
+ textEnd && this.setEndAt( textEnd, selectContents ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_END );
+ }
+
+ return !!( moveStart || moveEnd );
+ }
+ },
+
+ /**
+ * Inserts a node at the start of the range. The range will be expanded
+ * the contain the node.
+ *
+ * @param {CKEDITOR.dom.node} node
+ */
+ insertNode: function( node ) {
+ this.optimizeBookmark();
+ this.trim( false, true );
+
+ var startContainer = this.startContainer;
+ var startOffset = this.startOffset;
+
+ var nextNode = startContainer.getChild( startOffset );
+
+ if ( nextNode )
+ node.insertBefore( nextNode );
+ else
+ startContainer.append( node );
+
+ // Check if we need to update the end boundary.
+ if ( node.getParent() && node.getParent().equals( this.endContainer ) )
+ this.endOffset++;
+
+ // Expand the range to embrace the new node.
+ this.setStartBefore( node );
+ },
+
+ /**
+ * @todo
+ */
+ moveToPosition: function( node, position ) {
+ this.setStartAt( node, position );
+ this.collapse( true );
+ },
+
+ /**
+ * @todo
+ */
+ moveToRange: function( range ) {
+ this.setStart( range.startContainer, range.startOffset );
+ this.setEnd( range.endContainer, range.endOffset );
+ },
+
+ /**
+ * Select nodes content. Range will start and end inside this node.
+ *
+ * @param {CKEDITOR.dom.node} node
+ */
+ selectNodeContents: function( node ) {
+ this.setStart( node, 0 );
+ this.setEnd( node, node.type == CKEDITOR.NODE_TEXT ? node.getLength() : node.getChildCount() );
+ },
+
+ /**
+ * Sets the start position of a range.
+ *
+ * @param {CKEDITOR.dom.node} startNode The node to start the range.
+ * @param {Number} startOffset An integer greater than or equal to zero
+ * representing the offset for the start of the range from the start
+ * of `startNode`.
+ */
+ setStart: function( startNode, startOffset ) {
+ // W3C requires a check for the new position. If it is after the end
+ // boundary, the range should be collapsed to the new start. It seams
+ // we will not need this check for our use of this class so we can
+ // ignore it for now.
+
+ // Fixing invalid range start inside dtd empty elements.
+ if ( startNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ startNode.getName() ] )
+ startOffset = startNode.getIndex(), startNode = startNode.getParent();
+
+ this.startContainer = startNode;
+ this.startOffset = startOffset;
+
+ if ( !this.endContainer ) {
+ this.endContainer = startNode;
+ this.endOffset = startOffset;
+ }
+
+ updateCollapsed( this );
+ },
+
+ /**
+ * Sets the end position of a Range.
+ *
+ * @param {CKEDITOR.dom.node} endNode The node to end the range.
+ * @param {Number} endOffset An integer greater than or equal to zero
+ * representing the offset for the end of the range from the start
+ * of `endNode`.
+ */
+ setEnd: function( endNode, endOffset ) {
+ // W3C requires a check for the new position. If it is before the start
+ // boundary, the range should be collapsed to the new end. It seams we
+ // will not need this check for our use of this class so we can ignore
+ // it for now.
+
+ // Fixing invalid range end inside dtd empty elements.
+ if ( endNode.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$empty[ endNode.getName() ] )
+ endOffset = endNode.getIndex() + 1, endNode = endNode.getParent();
+
+ this.endContainer = endNode;
+ this.endOffset = endOffset;
+
+ if ( !this.startContainer ) {
+ this.startContainer = endNode;
+ this.startOffset = endOffset;
+ }
+
+ updateCollapsed( this );
+ },
+
+ /**
+ * @todo
+ */
+ setStartAfter: function( node ) {
+ this.setStart( node.getParent(), node.getIndex() + 1 );
+ },
+
+ /**
+ * @todo
+ */
+ setStartBefore: function( node ) {
+ this.setStart( node.getParent(), node.getIndex() );
+ },
+
+ /**
+ * @todo
+ */
+ setEndAfter: function( node ) {
+ this.setEnd( node.getParent(), node.getIndex() + 1 );
+ },
+
+ /**
+ * @todo
+ */
+ setEndBefore: function( node ) {
+ this.setEnd( node.getParent(), node.getIndex() );
+ },
+
+ /**
+ * @todo
+ */
+ setStartAt: function( node, position ) {
+ switch ( position ) {
+ case CKEDITOR.POSITION_AFTER_START:
+ this.setStart( node, 0 );
+ break;
+
+ case CKEDITOR.POSITION_BEFORE_END:
+ if ( node.type == CKEDITOR.NODE_TEXT )
+ this.setStart( node, node.getLength() );
+ else
+ this.setStart( node, node.getChildCount() );
+ break;
+
+ case CKEDITOR.POSITION_BEFORE_START:
+ this.setStartBefore( node );
+ break;
+
+ case CKEDITOR.POSITION_AFTER_END:
+ this.setStartAfter( node );
+ }
+
+ updateCollapsed( this );
+ },
+
+ /**
+ * @todo
+ */
+ setEndAt: function( node, position ) {
+ switch ( position ) {
+ case CKEDITOR.POSITION_AFTER_START:
+ this.setEnd( node, 0 );
+ break;
+
+ case CKEDITOR.POSITION_BEFORE_END:
+ if ( node.type == CKEDITOR.NODE_TEXT )
+ this.setEnd( node, node.getLength() );
+ else
+ this.setEnd( node, node.getChildCount() );
+ break;
+
+ case CKEDITOR.POSITION_BEFORE_START:
+ this.setEndBefore( node );
+ break;
+
+ case CKEDITOR.POSITION_AFTER_END:
+ this.setEndAfter( node );
+ }
+
+ updateCollapsed( this );
+ },
+
+ /**
+ * @todo
+ */
+ fixBlock: function( isStart, blockTag ) {
+ var bookmark = this.createBookmark(),
+ fixedBlock = this.document.createElement( blockTag );
+
+ this.collapse( isStart );
+
+ this.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
+
+ this.extractContents().appendTo( fixedBlock );
+ fixedBlock.trim();
+
+ fixedBlock.appendBogus();
+
+ this.insertNode( fixedBlock );
+
+ this.moveToBookmark( bookmark );
+
+ return fixedBlock;
+ },
+
+ /**
+ * @todo
+ */
+ splitBlock: function( blockTag ) {
+ var startPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root ),
+ endPath = new CKEDITOR.dom.elementPath( this.endContainer, this.root );
+
+ var startBlockLimit = startPath.blockLimit,
+ endBlockLimit = endPath.blockLimit;
+
+ var startBlock = startPath.block,
+ endBlock = endPath.block;
+
+ var elementPath = null;
+ // Do nothing if the boundaries are in different block limits.
+ if ( !startBlockLimit.equals( endBlockLimit ) )
+ return null;
+
+ // Get or fix current blocks.
+ if ( blockTag != 'br' ) {
+ if ( !startBlock ) {
+ startBlock = this.fixBlock( true, blockTag );
+ endBlock = new CKEDITOR.dom.elementPath( this.endContainer, this.root ).block;
+ }
+
+ if ( !endBlock )
+ endBlock = this.fixBlock( false, blockTag );
+ }
+
+ // Get the range position.
+ var isStartOfBlock = startBlock && this.checkStartOfBlock(),
+ isEndOfBlock = endBlock && this.checkEndOfBlock();
+
+ // Delete the current contents.
+ // TODO: Why is 2.x doing CheckIsEmpty()?
+ this.deleteContents();
+
+ if ( startBlock && startBlock.equals( endBlock ) ) {
+ if ( isEndOfBlock ) {
+ elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
+ this.moveToPosition( endBlock, CKEDITOR.POSITION_AFTER_END );
+ endBlock = null;
+ } else if ( isStartOfBlock ) {
+ elementPath = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
+ this.moveToPosition( startBlock, CKEDITOR.POSITION_BEFORE_START );
+ startBlock = null;
+ } else {
+ endBlock = this.splitElement( startBlock );
+
+ // In Gecko, the last child node must be a bogus .
+ // Note: bogus added under or would cause
+ // lists to be incorrectly rendered.
+ if ( !startBlock.is( 'ul', 'ol' ) )
+ startBlock.appendBogus();
+ }
+ }
+
+ return {
+ previousBlock: startBlock,
+ nextBlock: endBlock,
+ wasStartOfBlock: isStartOfBlock,
+ wasEndOfBlock: isEndOfBlock,
+ elementPath: elementPath
+ };
+ },
+
+ /**
+ * Branch the specified element from the collapsed range position and
+ * place the caret between the two result branches.
+ *
+ * **Note:** The range must be collapsed and been enclosed by this element.
+ *
+ * @param {CKEDITOR.dom.element} element
+ * @returns {CKEDITOR.dom.element} Root element of the new branch after the split.
+ */
+ splitElement: function( toSplit ) {
+ if ( !this.collapsed )
+ return null;
+
+ // Extract the contents of the block from the selection point to the end
+ // of its contents.
+ this.setEndAt( toSplit, CKEDITOR.POSITION_BEFORE_END );
+ var documentFragment = this.extractContents();
+
+ // Duplicate the element after it.
+ var clone = toSplit.clone( false );
+
+ // Place the extracted contents into the duplicated element.
+ documentFragment.appendTo( clone );
+ clone.insertAfter( toSplit );
+ this.moveToPosition( toSplit, CKEDITOR.POSITION_AFTER_END );
+ return clone;
+ },
+
+ /**
+ * Recursively remove any empty path blocks at the range boundary.
+ *
+ * @method
+ * @param {Boolean} atEnd Removal to perform at the end boundary,
+ * otherwise to perform at the start.
+ */
+ removeEmptyBlocksAtEnd: ( function() {
+
+ var whitespace = CKEDITOR.dom.walker.whitespaces(),
+ bookmark = CKEDITOR.dom.walker.bookmark( false );
+
+ function childEval( parent ) {
+ return function( node ) {
+
+ // whitespace, bookmarks, empty inlines.
+ if ( whitespace( node ) || bookmark( node ) ||
+ node.type == CKEDITOR.NODE_ELEMENT &&
+ node.isEmptyInlineRemoveable() )
+ return false;
+ else if ( parent.is( 'table' ) && node.is( 'caption' ) )
+ return false;
+
+ return true;
+ };
+ }
+
+ return function( atEnd ) {
+
+ var bm = this.createBookmark();
+ var path = this[ atEnd ? 'endPath' : 'startPath' ]();
+ var block = path.block || path.blockLimit, parent;
+
+ // Remove any childless block, including list and table.
+ while ( block && !block.equals( path.root ) &&
+ !block.getFirst( childEval( block ) ) )
+ {
+ parent = block.getParent();
+ this[ atEnd ? 'setEndAt' : 'setStartAt' ]( block, CKEDITOR.POSITION_AFTER_END );
+ block.remove( 1 );
+ block = parent;
+ }
+
+ this.moveToBookmark( bm );
+ };
+
+ } )(),
+
+ /**
+ * Gets {@link CKEDITOR.dom.elementPath} for the {@link #startContainer}.
+ *
+ * @returns {CKEDITOR.dom.elementPath}
+ */
+ startPath: function() {
+ return new CKEDITOR.dom.elementPath( this.startContainer, this.root );
+ },
+
+ /**
+ * Gets {@link CKEDITOR.dom.elementPath} for the {@link #endContainer}.
+ *
+ * @returns {CKEDITOR.dom.elementPath}
+ */
+ endPath: function() {
+ return new CKEDITOR.dom.elementPath( this.endContainer, this.root );
+ },
+
+ /**
+ * Check whether a range boundary is at the inner boundary of a given
+ * element.
+ *
+ * @param {CKEDITOR.dom.element} element The target element to check.
+ * @param {Number} checkType The boundary to check for both the range
+ * and the element. It can be {@link CKEDITOR#START} or {@link CKEDITOR#END}.
+ * @returns {Boolean} `true` if the range boundary is at the inner
+ * boundary of the element.
+ */
+ checkBoundaryOfElement: function( element, checkType ) {
+ var checkStart = ( checkType == CKEDITOR.START );
+
+ // Create a copy of this range, so we can manipulate it for our checks.
+ var walkerRange = this.clone();
+
+ // Collapse the range at the proper size.
+ walkerRange.collapse( checkStart );
+
+ // Expand the range to element boundary.
+ walkerRange[ checkStart ? 'setStartAt' : 'setEndAt' ]
+ ( element, checkStart ? CKEDITOR.POSITION_AFTER_START : CKEDITOR.POSITION_BEFORE_END );
+
+ // Create the walker, which will check if we have anything useful
+ // in the range.
+ var walker = new CKEDITOR.dom.walker( walkerRange );
+ walker.evaluator = elementBoundaryEval( checkStart );
+
+ return walker[ checkStart ? 'checkBackward' : 'checkForward' ]();
+ },
+
+ /**
+ * **Note:** Calls to this function may produce changes to the DOM. The range may
+ * be updated to reflect such changes.
+ *
+ * @returns {Boolean}
+ * @todo
+ */
+ checkStartOfBlock: function() {
+ var startContainer = this.startContainer,
+ startOffset = this.startOffset;
+
+ // [IE] Special handling for range start in text with a leading NBSP,
+ // we it to be isolated, for bogus check.
+ if ( CKEDITOR.env.ie && startOffset && startContainer.type == CKEDITOR.NODE_TEXT )
+ {
+ var textBefore = CKEDITOR.tools.ltrim( startContainer.substring( 0, startOffset ) );
+ if ( nbspRegExp.test( textBefore ) )
+ this.trim( 0, 1 );
+ }
+
+ // Antecipate the trim() call here, so the walker will not make
+ // changes to the DOM, which would not get reflected into this
+ // range otherwise.
+ this.trim();
+
+ // We need to grab the block element holding the start boundary, so
+ // let's use an element path for it.
+ var path = new CKEDITOR.dom.elementPath( this.startContainer, this.root );
+
+ // Creates a range starting at the block start until the range start.
+ var walkerRange = this.clone();
+ walkerRange.collapse( true );
+ walkerRange.setStartAt( path.block || path.blockLimit, CKEDITOR.POSITION_AFTER_START );
+
+ var walker = new CKEDITOR.dom.walker( walkerRange );
+ walker.evaluator = getCheckStartEndBlockEvalFunction();
+
+ return walker.checkBackward();
+ },
+
+ /**
+ * **Note:** Calls to this function may produce changes to the DOM. The range may
+ * be updated to reflect such changes.
+ *
+ * @returns {Boolean}
+ * @todo
+ */
+ checkEndOfBlock: function() {
+ var endContainer = this.endContainer,
+ endOffset = this.endOffset;
+
+ // [IE] Special handling for range end in text with a following NBSP,
+ // we it to be isolated, for bogus check.
+ if ( CKEDITOR.env.ie && endContainer.type == CKEDITOR.NODE_TEXT )
+ {
+ var textAfter = CKEDITOR.tools.rtrim( endContainer.substring( endOffset ) );
+ if ( nbspRegExp.test( textAfter ) )
+ this.trim( 1, 0 );
+ }
+
+ // Antecipate the trim() call here, so the walker will not make
+ // changes to the DOM, which would not get reflected into this
+ // range otherwise.
+ this.trim();
+
+ // We need to grab the block element holding the start boundary, so
+ // let's use an element path for it.
+ var path = new CKEDITOR.dom.elementPath( this.endContainer, this.root );
+
+ // Creates a range starting at the block start until the range start.
+ var walkerRange = this.clone();
+ walkerRange.collapse( false );
+ walkerRange.setEndAt( path.block || path.blockLimit, CKEDITOR.POSITION_BEFORE_END );
+
+ var walker = new CKEDITOR.dom.walker( walkerRange );
+ walker.evaluator = getCheckStartEndBlockEvalFunction();
+
+ return walker.checkForward();
+ },
+
+ /**
+ * Traverse with {@link CKEDITOR.dom.walker} to retrieve the previous element before the range start.
+ *
+ * @param {Function} evaluator Function used as the walker's evaluator.
+ * @param {Function} [guard] Function used as the walker's guard.
+ * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
+ * default to the root editable if not defined.
+ * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
+ */
+ getPreviousNode : function( evaluator, guard, boundary ) {
+ var walkerRange = this.clone();
+ walkerRange.collapse( 1 );
+ walkerRange.setStartAt( boundary || this.root, CKEDITOR.POSITION_AFTER_START );
+
+ var walker = new CKEDITOR.dom.walker( walkerRange );
+ walker.evaluator = evaluator;
+ walker.guard = guard;
+ return walker.previous();
+ },
+
+ /**
+ * Traverse with {@link CKEDITOR.dom.walker} to retrieve the next element before the range start.
+ *
+ * @param {Function} evaluator Function used as the walker's evaluator.
+ * @param {Function} [guard] Function used as the walker's guard.
+ * @param {CKEDITOR.dom.element} [boundary] A range ancestor element in which the traversal is limited,
+ * default to the root editable if not defined.
+ * @returns {CKEDITOR.dom.element/null} The returned node from the traversal.
+ */
+ getNextNode: function( evaluator, guard, boundary ) {
+ var walkerRange = this.clone();
+ walkerRange.collapse();
+ walkerRange.setEndAt( boundary || this.root, CKEDITOR.POSITION_BEFORE_END );
+
+ var walker = new CKEDITOR.dom.walker( walkerRange );
+ walker.evaluator = evaluator;
+ walker.guard = guard;
+ return walker.next();
+ },
+
+ /**
+ * Check if elements at which the range boundaries anchor are read-only,
+ * with respect to `contenteditable` attribute.
+ *
+ * @returns {Boolean}
+ */
+ checkReadOnly: ( function() {
+ function checkNodesEditable( node, anotherEnd ) {
+ while ( node ) {
+ if ( node.type == CKEDITOR.NODE_ELEMENT ) {
+ if ( node.getAttribute( 'contentEditable' ) == 'false' && !node.data( 'cke-editable' ) )
+ return 0;
+
+ // Range enclosed entirely in an editable element.
+ else if ( node.is( 'html' ) || node.getAttribute( 'contentEditable' ) == 'true' && ( node.contains( anotherEnd ) || node.equals( anotherEnd ) ) )
+ break;
+
+ }
+ node = node.getParent();
+ }
+
+ return 1;
+ }
+
+ return function() {
+ var startNode = this.startContainer,
+ endNode = this.endContainer;
+
+ // Check if elements path at both boundaries are editable.
+ return !( checkNodesEditable( startNode, endNode ) && checkNodesEditable( endNode, startNode ) );
+ };
+ } )(),
+
+ /**
+ * Moves the range boundaries to the first/end editing point inside an
+ * element.
+ *
+ * For example, in an element tree like
+ * ` Text `, the start editing point is
+ * `^ Text ` (inside ``).
+ *
+ * @param {CKEDITOR.dom.element} el The element into which look for the
+ * editing spot.
+ * @param {Boolean} isMoveToEnd Whether move to the end editable position.
+ * @returns {Boolean} Whether range was moved.
+ */
+ moveToElementEditablePosition: function( el, isMoveToEnd ) {
+
+ function nextDFS( node, childOnly ) {
+ var next;
+
+ if ( node.type == CKEDITOR.NODE_ELEMENT && node.isEditable( false ) )
+ next = node[ isMoveToEnd ? 'getLast' : 'getFirst' ]( notIgnoredEval );
+
+ if ( !childOnly && !next )
+ next = node[ isMoveToEnd ? 'getPrevious' : 'getNext' ]( notIgnoredEval );
+
+ return next;
+ }
+
+ // Handle non-editable element e.g. HR.
+ if ( el.type == CKEDITOR.NODE_ELEMENT && !el.isEditable( false ) ) {
+ this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START );
+ return true;
+ }
+
+ var found = 0;
+
+ while ( el ) {
+ // Stop immediately if we've found a text node.
+ if ( el.type == CKEDITOR.NODE_TEXT ) {
+ // Put cursor before block filler.
+ if ( isMoveToEnd && this.endContainer && this.checkEndOfBlock() && nbspRegExp.test( el.getText() ) )
+ this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
+ else
+ this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START );
+ found = 1;
+ break;
+ }
+
+ // If an editable element is found, move inside it, but not stop the searching.
+ if ( el.type == CKEDITOR.NODE_ELEMENT ) {
+ if ( el.isEditable() ) {
+ this.moveToPosition( el, isMoveToEnd ? CKEDITOR.POSITION_BEFORE_END : CKEDITOR.POSITION_AFTER_START );
+ found = 1;
+ }
+ // Put cursor before padding block br.
+ else if ( isMoveToEnd && el.is( 'br' ) && this.endContainer && this.checkEndOfBlock() )
+ this.moveToPosition( el, CKEDITOR.POSITION_BEFORE_START );
+ // Special case - non-editable block. Select entire element, because it does not make sense
+ // to place collapsed selection next to it, because browsers can't handle that.
+ else if ( el.getAttribute( 'contenteditable' ) == 'false' && el.is( CKEDITOR.dtd.$block ) ) {
+ this.setStartBefore( el );
+ this.setEndAfter( el );
+ return true;
+ }
+ }
+
+ el = nextDFS( el, found );
+ }
+
+ return !!found;
+ },
+
+ /**
+ * Moves the range boundaries to the closest editing point after/before an
+ * element.
+ *
+ * For example, if the start element has `id="start"`,
+ * `foostart `, the closest previous editing point is
+ * `foo^start ` (between `` and ``).
+ *
+ * See also: {@link #moveToElementEditablePosition}.
+ *
+ * @since 4.3
+ * @param {CKEDITOR.dom.element} element The starting element.
+ * @param {Boolean} isMoveToEnd Whether move to the end of editable. Otherwise, look back.
+ * @returns {Boolean} Whether the range was moved.
+ */
+ moveToClosestEditablePosition: function( element, isMoveToEnd ) {
+ // We don't want to modify original range if there's no editable position.
+ var range = new CKEDITOR.dom.range( this.root ),
+ found = 0,
+ sibling,
+ positions = [ CKEDITOR.POSITION_AFTER_END, CKEDITOR.POSITION_BEFORE_START ];
+
+ // Set collapsed range at one of ends of element.
+ range.moveToPosition( element, positions[ isMoveToEnd ? 0 : 1 ] );
+
+ // Start element isn't a block, so we can automatically place range
+ // next to it.
+ if ( !element.is( CKEDITOR.dtd.$block ) )
+ found = 1;
+ else {
+ // Look for first node that fulfills eval function and place range next to it.
+ sibling = range[ isMoveToEnd ? 'getNextEditableNode' : 'getPreviousEditableNode' ]();
+ if ( sibling ) {
+ found = 1;
+
+ // Special case - eval accepts block element only if it's a non-editable block,
+ // which we want to select, not place collapsed selection next to it (which browsers
+ // can't handle).
+ if ( sibling.type == CKEDITOR.NODE_ELEMENT && sibling.is( CKEDITOR.dtd.$block ) && sibling.getAttribute( 'contenteditable' ) == 'false' ) {
+ range.setStartAt( sibling, CKEDITOR.POSITION_BEFORE_START );
+ range.setEndAt( sibling, CKEDITOR.POSITION_AFTER_END );
+ }
+ else
+ range.moveToPosition( sibling, positions[ isMoveToEnd ? 1 : 0 ] );
+ }
+ }
+
+ if ( found )
+ this.moveToRange( range );
+
+ return !!found;
+ },
+
+ /**
+ * See {@link #moveToElementEditablePosition}.
+ *
+ * @returns {Boolean} Whether range was moved.
+ */
+ moveToElementEditStart: function( target ) {
+ return this.moveToElementEditablePosition( target );
+ },
+
+ /**
+ * See {@link #moveToElementEditablePosition}.
+ *
+ * @returns {Boolean} Whether range was moved.
+ */
+ moveToElementEditEnd: function( target ) {
+ return this.moveToElementEditablePosition( target, true );
+ },
+
+ /**
+ * Get the single node enclosed within the range if there's one.
+ *
+ * @returns {CKEDITOR.dom.node}
+ */
+ getEnclosedNode: function() {
+ var walkerRange = this.clone();
+
+ // Optimize and analyze the range to avoid DOM destructive nature of walker. (#5780)
+ walkerRange.optimize();
+ if ( walkerRange.startContainer.type != CKEDITOR.NODE_ELEMENT || walkerRange.endContainer.type != CKEDITOR.NODE_ELEMENT )
+ return null;
+
+ var walker = new CKEDITOR.dom.walker( walkerRange ),
+ isNotBookmarks = CKEDITOR.dom.walker.bookmark( false, true ),
+ isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true );
+
+ walker.evaluator = function( node ) {
+ return isNotWhitespaces( node ) && isNotBookmarks( node );
+ };
+ var node = walker.next();
+ walker.reset();
+ return node && node.equals( walker.previous() ) ? node : null;
+ },
+
+ /**
+ * Get the node adjacent to the range start or {@link #startContainer}.
+ *
+ * @returns {CKEDITOR.dom.node}
+ */
+ getTouchedStartNode: function() {
+ var container = this.startContainer;
+
+ if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
+ return container;
+
+ return container.getChild( this.startOffset ) || container;
+ },
+
+ /**
+ * Get the node adjacent to the range end or {@link #endContainer}.
+ *
+ * @returns {CKEDITOR.dom.node}
+ */
+ getTouchedEndNode: function() {
+ var container = this.endContainer;
+
+ if ( this.collapsed || container.type != CKEDITOR.NODE_ELEMENT )
+ return container;
+
+ return container.getChild( this.endOffset - 1 ) || container;
+ },
+
+ /**
+ * Gets next node which can be a container of a selection.
+ * This methods mimics a behavior of right/left arrow keys in case of
+ * collapsed selection. It does not return an exact position (with offset) though,
+ * but just a selection's container.
+ *
+ * Note: use this method on a collapsed range.
+ *
+ * @since 4.3
+ * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
+ */
+ getNextEditableNode: getNextEditableNode(),
+
+ /**
+ * See {@link #getNextEditableNode}.
+ *
+ * @since 4.3
+ * @returns {CKEDITOR.dom.element/CKEDITOR.dom.text}
+ */
+ getPreviousEditableNode: getNextEditableNode( 1 ),
+
+ /**
+ * Scrolls the start of current range into view.
+ */
+ scrollIntoView: function() {
+
+ // The reference element contains a zero-width space to avoid
+ // a premature removal. The view is to be scrolled with respect
+ // to this element.
+ var reference = new CKEDITOR.dom.element.createFromHtml( ' ', this.document ),
+ afterCaretNode, startContainerText, isStartText;
+
+ var range = this.clone();
+
+ // Work with the range to obtain a proper caret position.
+ range.optimize();
+
+ // Currently in a text node, so we need to split it into two
+ // halves and put the reference between.
+ if ( isStartText = range.startContainer.type == CKEDITOR.NODE_TEXT ) {
+ // Keep the original content. It will be restored.
+ startContainerText = range.startContainer.getText();
+
+ // Split the startContainer at the this position.
+ afterCaretNode = range.startContainer.split( range.startOffset );
+
+ // Insert the reference between two text nodes.
+ reference.insertAfter( range.startContainer );
+ }
+
+ // If not in a text node, simply insert the reference into the range.
+ else
+ range.insertNode( reference );
+
+ // Scroll with respect to the reference element.
+ reference.scrollIntoView();
+
+ // Get rid of split parts if "in a text node" case.
+ // Revert the original text of the startContainer.
+ if ( isStartText ) {
+ range.startContainer.setText( startContainerText );
+ afterCaretNode.remove();
+ }
+
+ // Get rid of the reference node. It is no longer necessary.
+ reference.remove();
+ }
+ };
+} )();
+
+CKEDITOR.POSITION_AFTER_START = 1; // ^contents "^text"
+CKEDITOR.POSITION_BEFORE_END = 2; // contents^ "text^"
+CKEDITOR.POSITION_BEFORE_START = 3; // ^contents ^"text"
+CKEDITOR.POSITION_AFTER_END = 4; // contents^ "text"
+
+CKEDITOR.ENLARGE_ELEMENT = 1;
+CKEDITOR.ENLARGE_BLOCK_CONTENTS = 2;
+CKEDITOR.ENLARGE_LIST_ITEM_CONTENTS = 3;
+CKEDITOR.ENLARGE_INLINE = 4;
+
+// Check boundary types.
+
+/**
+ * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
+ *
+ * @readonly
+ * @member CKEDITOR
+ * @property {Number} [=1]
+ */
+CKEDITOR.START = 1;
+
+/**
+ * See {@link CKEDITOR.dom.range#checkBoundaryOfElement}.
+ *
+ * @readonly
+ * @member CKEDITOR
+ * @property {Number} [=2]
+ */
+CKEDITOR.END = 2;
+
+// Shrink range types.
+
+/**
+ * See {@link CKEDITOR.dom.range#shrink}.
+ *
+ * @readonly
+ * @member CKEDITOR
+ * @property {Number} [=1]
+ */
+CKEDITOR.SHRINK_ELEMENT = 1;
+
+/**
+ * See {@link CKEDITOR.dom.range#shrink}.
+ *
+ * @readonly
+ * @member CKEDITOR
+ * @property {Number} [=2]
+ */
+CKEDITOR.SHRINK_TEXT = 2;
diff --git a/lam/templates/lib/extra/ckeditor/core/dom/rangelist.js b/lam/templates/lib/extra/ckeditor/core/dom/rangelist.js
new file mode 100644
index 00000000..15447d75
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/core/dom/rangelist.js
@@ -0,0 +1,201 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+( function() {
+ /**
+ * Represents a list os CKEDITOR.dom.range objects, which can be easily
+ * iterated sequentially.
+ *
+ * @class
+ * @extends Array
+ * @constructor Creates a rangeList class instance.
+ * @param {CKEDITOR.dom.range/CKEDITOR.dom.range[]} [ranges] The ranges contained on this list.
+ * Note that, if an array of ranges is specified, the range sequence
+ * should match its DOM order. This class will not help to sort them.
+ */
+ CKEDITOR.dom.rangeList = function( ranges ) {
+ if ( ranges instanceof CKEDITOR.dom.rangeList )
+ return ranges;
+
+ if ( !ranges )
+ ranges = [];
+ else if ( ranges instanceof CKEDITOR.dom.range )
+ ranges = [ ranges ];
+
+ return CKEDITOR.tools.extend( ranges, mixins );
+ };
+
+ var mixins = {
+ /**
+ * Creates an instance of the rangeList iterator, it should be used
+ * only when the ranges processing could be DOM intrusive, which
+ * means it may pollute and break other ranges in this list.
+ * Otherwise, it's enough to just iterate over this array in a for loop.
+ *
+ * @returns {CKEDITOR.dom.rangeListIterator}
+ */
+ createIterator: function() {
+ var rangeList = this,
+ bookmark = CKEDITOR.dom.walker.bookmark(),
+ guard = function( node ) {
+ return !( node.is && node.is( 'tr' ) );
+ },
+ bookmarks = [],
+ current;
+
+ return {
+ /**
+ * Retrieves the next range in the list.
+ *
+ * @member CKEDITOR.dom.rangeListIterator
+ * @param {Boolean} [mergeConsequent=false] Whether join two adjacent
+ * ranges into single, e.g. consequent table cells.
+ */
+ getNextRange: function( mergeConsequent ) {
+ current = current == undefined ? 0 : current + 1;
+
+ var range = rangeList[ current ];
+
+ // Multiple ranges might be mangled by each other.
+ if ( range && rangeList.length > 1 ) {
+ // Bookmarking all other ranges on the first iteration,
+ // the range correctness after it doesn't matter since we'll
+ // restore them before the next iteration.
+ if ( !current ) {
+ // Make sure bookmark correctness by reverse processing.
+ for ( var i = rangeList.length - 1; i >= 0; i-- )
+ bookmarks.unshift( rangeList[ i ].createBookmark( true ) );
+ }
+
+ if ( mergeConsequent ) {
+ // Figure out how many ranges should be merged.
+ var mergeCount = 0;
+ while ( rangeList[ current + mergeCount + 1 ] ) {
+ var doc = range.document,
+ found = 0,
+ left = doc.getById( bookmarks[ mergeCount ].endNode ),
+ right = doc.getById( bookmarks[ mergeCount + 1 ].startNode ),
+ next;
+
+ // Check subsequent range.
+ while ( 1 ) {
+ next = left.getNextSourceNode( false );
+ if ( !right.equals( next ) ) {
+ // This could be yet another bookmark or
+ // walking across block boundaries.
+ if ( bookmark( next ) || ( next.type == CKEDITOR.NODE_ELEMENT && next.isBlockBoundary() ) ) {
+ left = next;
+ continue;
+ }
+ } else
+ found = 1;
+
+ break;
+ }
+
+ if ( !found )
+ break;
+
+ mergeCount++;
+ }
+ }
+
+ range.moveToBookmark( bookmarks.shift() );
+
+ // Merge ranges finally after moving to bookmarks.
+ while ( mergeCount-- ) {
+ next = rangeList[ ++current ];
+ next.moveToBookmark( bookmarks.shift() );
+ range.setEnd( next.endContainer, next.endOffset );
+ }
+ }
+
+ return range;
+ }
+ };
+ },
+
+ /**
+ * Create bookmarks for all ranges. See {@link CKEDITOR.dom.range#createBookmark}.
+ *
+ * @param {Boolean} [serializable=false] See {@link CKEDITOR.dom.range#createBookmark}.
+ * @returns {Array} Array of bookmarks.
+ */
+ createBookmarks: function( serializable ) {
+ var retval = [],
+ bookmark;
+ for ( var i = 0; i < this.length; i++ ) {
+ retval.push( bookmark = this[ i ].createBookmark( serializable, true ) );
+
+ // Updating the container & offset values for ranges
+ // that have been touched.
+ for ( var j = i + 1; j < this.length; j++ ) {
+ this[ j ] = updateDirtyRange( bookmark, this[ j ] );
+ this[ j ] = updateDirtyRange( bookmark, this[ j ], true );
+ }
+ }
+ return retval;
+ },
+
+ /**
+ * Create "unobtrusive" bookmarks for all ranges. See {@link CKEDITOR.dom.range#createBookmark2}.
+ *
+ * @param {Boolean} [normalized=false] See {@link CKEDITOR.dom.range#createBookmark2}.
+ * @returns {Array} Array of bookmarks.
+ */
+ createBookmarks2: function( normalized ) {
+ var bookmarks = [];
+
+ for ( var i = 0; i < this.length; i++ )
+ bookmarks.push( this[ i ].createBookmark2( normalized ) );
+
+ return bookmarks;
+ },
+
+ /**
+ * Move each range in the list to the position specified by a list of bookmarks.
+ *
+ * @param {Array} bookmarks The list of bookmarks, each one matching a range in the list.
+ */
+ moveToBookmarks: function( bookmarks ) {
+ for ( var i = 0; i < this.length; i++ )
+ this[ i ].moveToBookmark( bookmarks[ i ] );
+ }
+ };
+
+ // Update the specified range which has been mangled by previous insertion of
+ // range bookmark nodes.(#3256)
+ function updateDirtyRange( bookmark, dirtyRange, checkEnd ) {
+ var serializable = bookmark.serializable,
+ container = dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ],
+ offset = checkEnd ? 'endOffset' : 'startOffset';
+
+ var bookmarkStart = serializable ? dirtyRange.document.getById( bookmark.startNode ) : bookmark.startNode;
+
+ var bookmarkEnd = serializable ? dirtyRange.document.getById( bookmark.endNode ) : bookmark.endNode;
+
+ if ( container.equals( bookmarkStart.getPrevious() ) ) {
+ dirtyRange.startOffset = dirtyRange.startOffset - container.getLength() - bookmarkEnd.getPrevious().getLength();
+ container = bookmarkEnd.getNext();
+ } else if ( container.equals( bookmarkEnd.getPrevious() ) ) {
+ dirtyRange.startOffset = dirtyRange.startOffset - container.getLength();
+ container = bookmarkEnd.getNext();
+ }
+
+ container.equals( bookmarkStart.getParent() ) && dirtyRange[ offset ]++;
+ container.equals( bookmarkEnd.getParent() ) && dirtyRange[ offset ]++;
+
+ // Update and return this range.
+ dirtyRange[ checkEnd ? 'endContainer' : 'startContainer' ] = container;
+ return dirtyRange;
+ }
+} )();
+
+/**
+ * (Virtual Class) Do not call this constructor. This class is not really part
+ * of the API. It just describes the return type of {@link CKEDITOR.dom.rangeList#createIterator}.
+ *
+ * @class CKEDITOR.dom.rangeListIterator
+ */
diff --git a/lam/templates/lib/extra/ckeditor/core/dom/text.js b/lam/templates/lib/extra/ckeditor/core/dom/text.js
new file mode 100644
index 00000000..b620a425
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/core/dom/text.js
@@ -0,0 +1,139 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+/**
+ * @fileOverview Defines the {@link CKEDITOR.dom.text} class, which represents
+ * a DOM text node.
+ */
+
+/**
+ * Represents a DOM text node.
+ *
+ * var nativeNode = document.createTextNode( 'Example' );
+ * var text = CKEDITOR.dom.text( nativeNode );
+ *
+ * var text = CKEDITOR.dom.text( 'Example' );
+ *
+ * @class
+ * @extends CKEDITOR.dom.node
+ * @constructor Creates a text class instance.
+ * @param {Object/String} text A native DOM text node or a string containing
+ * the text to use to create a new text node.
+ * @param {CKEDITOR.dom.document} [ownerDocument] The document that will contain
+ * the node in case of new node creation. Defaults to the current document.
+ */
+CKEDITOR.dom.text = function( text, ownerDocument ) {
+ if ( typeof text == 'string' )
+ text = ( ownerDocument ? ownerDocument.$ : document ).createTextNode( text );
+
+ // Theoretically, we should call the base constructor here
+ // (not CKEDITOR.dom.node though). But, IE doesn't support expando
+ // properties on text node, so the features provided by domObject will not
+ // work for text nodes (which is not a big issue for us).
+ //
+ // CKEDITOR.dom.domObject.call( this, element );
+
+ this.$ = text;
+};
+
+CKEDITOR.dom.text.prototype = new CKEDITOR.dom.node();
+
+CKEDITOR.tools.extend( CKEDITOR.dom.text.prototype, {
+ /**
+ * The node type. This is a constant value set to {@link CKEDITOR#NODE_TEXT}.
+ *
+ * @readonly
+ * @property {Number} [=CKEDITOR.NODE_TEXT]
+ */
+ type: CKEDITOR.NODE_TEXT,
+
+ /**
+ * Gets length of node's value.
+ *
+ * @returns {Number}
+ */
+ getLength: function() {
+ return this.$.nodeValue.length;
+ },
+
+ /**
+ * Gets node's value.
+ *
+ * @returns {String}
+ */
+ getText: function() {
+ return this.$.nodeValue;
+ },
+
+ /**
+ * Sets node's value.
+ *
+ * @param {String} text
+ */
+ setText: function( text ) {
+ this.$.nodeValue = text;
+ },
+
+ /**
+ * Breaks this text node into two nodes at the specified offset,
+ * keeping both in the tree as siblings. This node then only contains
+ * all the content up to the offset point. A new text node, which is
+ * inserted as the next sibling of this node, contains all the content
+ * at and after the offset point. When the offset is equal to the
+ * length of this node, the new node has no data.
+ *
+ * @param {Number} The position at which to split, starting from zero.
+ * @returns {CKEDITOR.dom.text} The new text node.
+ */
+ split: function( offset ) {
+
+ // Saved the children count and text length beforehand.
+ var parent = this.$.parentNode,
+ count = parent.childNodes.length,
+ length = this.getLength();
+
+ var doc = this.getDocument();
+ var retval = new CKEDITOR.dom.text( this.$.splitText( offset ), doc );
+
+ if ( parent.childNodes.length == count )
+ {
+ // If the offset is after the last char, IE creates the text node
+ // on split, but don't include it into the DOM. So, we have to do
+ // that manually here.
+ if ( offset >= length )
+ {
+ retval = doc.createText( '' );
+ retval.insertAfter( this );
+ }
+ else
+ {
+ // IE BUG: IE8+ does not update the childNodes array in DOM after splitText(),
+ // we need to make some DOM changes to make it update. (#3436)
+ var workaround = doc.createText( '' );
+ workaround.insertAfter( retval );
+ workaround.remove();
+ }
+ }
+
+ return retval;
+ },
+
+ /**
+ * Extracts characters from indexA up to but not including `indexB`.
+ *
+ * @param {Number} indexA An integer between `0` and one less than the
+ * length of the text.
+ * @param {Number} [indexB] An integer between `0` and the length of the
+ * string. If omitted, extracts characters to the end of the text.
+ */
+ substring: function( indexA, indexB ) {
+ // We need the following check due to a Firefox bug
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=458886
+ if ( typeof indexB != 'number' )
+ return this.$.nodeValue.substr( indexA );
+ else
+ return this.$.nodeValue.substring( indexA, indexB );
+ }
+} );
diff --git a/lam/templates/lib/extra/ckeditor/core/dom/walker.js b/lam/templates/lib/extra/ckeditor/core/dom/walker.js
new file mode 100644
index 00000000..9ac1cd3a
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/core/dom/walker.js
@@ -0,0 +1,616 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+( function() {
+ // This function is to be called under a "walker" instance scope.
+ function iterate( rtl, breakOnFalse ) {
+ var range = this.range;
+
+ // Return null if we have reached the end.
+ if ( this._.end )
+ return null;
+
+ // This is the first call. Initialize it.
+ if ( !this._.start ) {
+ this._.start = 1;
+
+ // A collapsed range must return null at first call.
+ if ( range.collapsed ) {
+ this.end();
+ return null;
+ }
+
+ // Move outside of text node edges.
+ range.optimize();
+ }
+
+ var node,
+ startCt = range.startContainer,
+ endCt = range.endContainer,
+ startOffset = range.startOffset,
+ endOffset = range.endOffset,
+ guard,
+ userGuard = this.guard,
+ type = this.type,
+ getSourceNodeFn = ( rtl ? 'getPreviousSourceNode' : 'getNextSourceNode' );
+
+ // Create the LTR guard function, if necessary.
+ if ( !rtl && !this._.guardLTR ) {
+ // The node that stops walker from moving up.
+ var limitLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? endCt : endCt.getParent();
+
+ // The node that stops the walker from going to next.
+ var blockerLTR = endCt.type == CKEDITOR.NODE_ELEMENT ? endCt.getChild( endOffset ) : endCt.getNext();
+
+ this._.guardLTR = function( node, movingOut ) {
+ return ( ( !movingOut || !limitLTR.equals( node ) ) && ( !blockerLTR || !node.equals( blockerLTR ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || !node.equals( range.root ) ) );
+ };
+ }
+
+ // Create the RTL guard function, if necessary.
+ if ( rtl && !this._.guardRTL ) {
+ // The node that stops walker from moving up.
+ var limitRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? startCt : startCt.getParent();
+
+ // The node that stops the walker from going to next.
+ var blockerRTL = startCt.type == CKEDITOR.NODE_ELEMENT ? startOffset ? startCt.getChild( startOffset - 1 ) : null : startCt.getPrevious();
+
+ this._.guardRTL = function( node, movingOut ) {
+ return ( ( !movingOut || !limitRTL.equals( node ) ) && ( !blockerRTL || !node.equals( blockerRTL ) ) && ( node.type != CKEDITOR.NODE_ELEMENT || !movingOut || !node.equals( range.root ) ) );
+ };
+ }
+
+ // Define which guard function to use.
+ var stopGuard = rtl ? this._.guardRTL : this._.guardLTR;
+
+ // Make the user defined guard function participate in the process,
+ // otherwise simply use the boundary guard.
+ if ( userGuard ) {
+ guard = function( node, movingOut ) {
+ if ( stopGuard( node, movingOut ) === false )
+ return false;
+
+ return userGuard( node, movingOut );
+ };
+ } else
+ guard = stopGuard;
+
+ if ( this.current )
+ node = this.current[ getSourceNodeFn ]( false, type, guard );
+ else {
+ // Get the first node to be returned.
+ if ( rtl ) {
+ node = endCt;
+
+ if ( node.type == CKEDITOR.NODE_ELEMENT ) {
+ if ( endOffset > 0 )
+ node = node.getChild( endOffset - 1 );
+ else
+ node = ( guard( node, true ) === false ) ? null : node.getPreviousSourceNode( true, type, guard );
+ }
+ } else {
+ node = startCt;
+
+ if ( node.type == CKEDITOR.NODE_ELEMENT ) {
+ if ( !( node = node.getChild( startOffset ) ) )
+ node = ( guard( startCt, true ) === false ) ? null : startCt.getNextSourceNode( true, type, guard );
+ }
+ }
+
+ if ( node && guard( node ) === false )
+ node = null;
+ }
+
+ while ( node && !this._.end ) {
+ this.current = node;
+
+ if ( !this.evaluator || this.evaluator( node ) !== false ) {
+ if ( !breakOnFalse )
+ return node;
+ } else if ( breakOnFalse && this.evaluator )
+ return false;
+
+ node = node[ getSourceNodeFn ]( false, type, guard );
+ }
+
+ this.end();
+ return this.current = null;
+ }
+
+ function iterateToLast( rtl ) {
+ var node,
+ last = null;
+
+ while ( ( node = iterate.call( this, rtl ) ) )
+ last = node;
+
+ return last;
+ }
+
+ /**
+ * Utility class to "walk" the DOM inside a range boundaries. If the
+ * range starts or ends in the middle of the text node this node will
+ * be included as a whole. Outside changes to the range may break the walker.
+ *
+ * The walker may return nodes that are not totaly included into the
+ * range boundaires. Let's take the following range representation,
+ * where the square brackets indicate the boundaries:
+ *
+ * [Some sample] text
+ *
+ * While walking forward into the above range, the following nodes are
+ * returned: ` `, `"Some "`, `` and `"sample"`. Going
+ * backwards instead we have: `"sample"` and `"Some "`. So note that the
+ * walker always returns nodes when "entering" them, but not when
+ * "leaving" them. The guard function is instead called both when
+ * entering and leaving nodes.
+ *
+ * @class
+ */
+ CKEDITOR.dom.walker = CKEDITOR.tools.createClass( {
+ /**
+ * Creates a walker class instance.
+ *
+ * @constructor
+ * @param {CKEDITOR.dom.range} range The range within which walk.
+ */
+ $: function( range ) {
+ this.range = range;
+
+ /**
+ * A function executed for every matched node, to check whether
+ * it's to be considered into the walk or not. If not provided, all
+ * matched nodes are considered good.
+ *
+ * If the function returns `false` the node is ignored.
+ *
+ * @property {Function} evaluator
+ */
+ // this.evaluator = null;
+
+ /**
+ * A function executed for every node the walk pass by to check
+ * whether the walk is to be finished. It's called when both
+ * entering and exiting nodes, as well as for the matched nodes.
+ *
+ * If this function returns `false`, the walking ends and no more
+ * nodes are evaluated.
+
+ * @property {Function} guard
+ */
+ // this.guard = null;
+
+ /** @private */
+ this._ = {};
+ },
+
+ // statics :
+ // {
+ // /* Creates a CKEDITOR.dom.walker instance to walk inside DOM boundaries set by nodes.
+ // * @param {CKEDITOR.dom.node} startNode The node from wich the walk
+ // * will start.
+ // * @param {CKEDITOR.dom.node} [endNode] The last node to be considered
+ // * in the walk. No more nodes are retrieved after touching or
+ // * passing it. If not provided, the walker stops at the
+ // * <body> closing boundary.
+ // * @returns {CKEDITOR.dom.walker} A DOM walker for the nodes between the
+ // * provided nodes.
+ // */
+ // createOnNodes : function( startNode, endNode, startInclusive, endInclusive )
+ // {
+ // var range = new CKEDITOR.dom.range();
+ // if ( startNode )
+ // range.setStartAt( startNode, startInclusive ? CKEDITOR.POSITION_BEFORE_START : CKEDITOR.POSITION_AFTER_END ) ;
+ // else
+ // range.setStartAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_AFTER_START ) ;
+ //
+ // if ( endNode )
+ // range.setEndAt( endNode, endInclusive ? CKEDITOR.POSITION_AFTER_END : CKEDITOR.POSITION_BEFORE_START ) ;
+ // else
+ // range.setEndAt( startNode.getDocument().getBody(), CKEDITOR.POSITION_BEFORE_END ) ;
+ //
+ // return new CKEDITOR.dom.walker( range );
+ // }
+ // },
+ //
+ proto: {
+ /**
+ * Stops walking. No more nodes are retrieved if this function gets called.
+ */
+ end: function() {
+ this._.end = 1;
+ },
+
+ /**
+ * Retrieves the next node (at right).
+ *
+ * @returns {CKEDITOR.dom.node} The next node or null if no more
+ * nodes are available.
+ */
+ next: function() {
+ return iterate.call( this );
+ },
+
+ /**
+ * Retrieves the previous node (at left).
+ *
+ * @returns {CKEDITOR.dom.node} The previous node or null if no more
+ * nodes are available.
+ */
+ previous: function() {
+ return iterate.call( this, 1 );
+ },
+
+ /**
+ * Check all nodes at right, executing the evaluation function.
+ *
+ * @returns {Boolean} `false` if the evaluator function returned
+ * `false` for any of the matched nodes. Otherwise `true`.
+ */
+ checkForward: function() {
+ return iterate.call( this, 0, 1 ) !== false;
+ },
+
+ /**
+ * Check all nodes at left, executing the evaluation function.
+ *
+ * @returns {Boolean} `false` if the evaluator function returned
+ * `false` for any of the matched nodes. Otherwise `true`.
+ */
+ checkBackward: function() {
+ return iterate.call( this, 1, 1 ) !== false;
+ },
+
+ /**
+ * Executes a full walk forward (to the right), until no more nodes
+ * are available, returning the last valid node.
+ *
+ * @returns {CKEDITOR.dom.node} The last node at the right or null
+ * if no valid nodes are available.
+ */
+ lastForward: function() {
+ return iterateToLast.call( this );
+ },
+
+ /**
+ * Executes a full walk backwards (to the left), until no more nodes
+ * are available, returning the last valid node.
+ *
+ * @returns {CKEDITOR.dom.node} The last node at the left or null
+ * if no valid nodes are available.
+ */
+ lastBackward: function() {
+ return iterateToLast.call( this, 1 );
+ },
+
+ /**
+ * Resets walker.
+ */
+ reset: function() {
+ delete this.current;
+ this._ = {};
+ }
+
+ }
+ } );
+
+ // Anything whose display computed style is block, list-item, table,
+ // table-row-group, table-header-group, table-footer-group, table-row,
+ // table-column-group, table-column, table-cell, table-caption, or whose node
+ // name is hr, br (when enterMode is br only) is a block boundary.
+ var blockBoundaryDisplayMatch = { block: 1, 'list-item': 1, table: 1, 'table-row-group': 1,
+ 'table-header-group': 1, 'table-footer-group': 1, 'table-row': 1, 'table-column-group': 1,
+ 'table-column': 1, 'table-cell': 1, 'table-caption': 1 },
+ outOfFlowPositions = { absolute: 1, fixed: 1 };
+
+ /**
+ * Checks whether element is displayed as a block.
+ *
+ * @member CKEDITOR.dom.element
+ * @param [customNodeNames] Custom list of nodes which will extend
+ * default {@link CKEDITOR.dtd#$block} list.
+ * @returns {Boolean}
+ */
+ CKEDITOR.dom.element.prototype.isBlockBoundary = function( customNodeNames ) {
+ // Whether element is in normal page flow. Floated or positioned elements are out of page flow.
+ // Don't consider floated or positioned formatting as block boundary, fall back to dtd check in that case. (#6297)
+ var inPageFlow = this.getComputedStyle( 'float' ) == 'none' && !( this.getComputedStyle( 'position' ) in outOfFlowPositions );
+
+ if ( inPageFlow && blockBoundaryDisplayMatch[ this.getComputedStyle( 'display' ) ] )
+ return true;
+
+ // Either in $block or in customNodeNames if defined.
+ return !!( this.is( CKEDITOR.dtd.$block ) || customNodeNames && this.is( customNodeNames ) );
+ };
+
+ /**
+ * Returns a function which checks whether the node is a block boundary.
+ * See {@link CKEDITOR.dom.element#isBlockBoundary}.
+ *
+ * @static
+ * @param customNodeNames
+ * @returns {Function}
+ */
+ CKEDITOR.dom.walker.blockBoundary = function( customNodeNames ) {
+ return function( node, type ) {
+ return !( node.type == CKEDITOR.NODE_ELEMENT && node.isBlockBoundary( customNodeNames ) );
+ };
+ };
+
+ /**
+ * @static
+ * @todo
+ */
+ CKEDITOR.dom.walker.listItemBoundary = function() {
+ return this.blockBoundary( { br: 1 } );
+ };
+
+ /**
+ * Returns a function which checks whether the node is a bookmark node OR bookmark node
+ * inner contents.
+ *
+ * @static
+ * @param {Boolean} [contentOnly=false] Whether only test against the text content of
+ * bookmark node instead of the element itself (default).
+ * @param {Boolean} [isReject=false] Whether should return `false` for the bookmark
+ * node instead of `true` (default).
+ * @returns {Function}
+ */
+ CKEDITOR.dom.walker.bookmark = function( contentOnly, isReject ) {
+ function isBookmarkNode( node ) {
+ return ( node && node.getName && node.getName() == 'span' && node.data( 'cke-bookmark' ) );
+ }
+
+ return function( node ) {
+ var isBookmark, parent;
+ // Is bookmark inner text node?
+ isBookmark = ( node && node.type != CKEDITOR.NODE_ELEMENT && ( parent = node.getParent() ) && isBookmarkNode( parent ) );
+ // Is bookmark node?
+ isBookmark = contentOnly ? isBookmark : isBookmark || isBookmarkNode( node );
+ return !!( isReject ^ isBookmark );
+ };
+ };
+
+ /**
+ * Returns a function which checks whether the node is a text node containing only whitespaces characters.
+ *
+ * @static
+ * @param {Boolean} [isReject=false]
+ * @returns {Function}
+ */
+ CKEDITOR.dom.walker.whitespaces = function( isReject ) {
+ return function( node ) {
+ var isWhitespace;
+ if ( node && node.type == CKEDITOR.NODE_TEXT ) {
+ // whitespace, as well as the text cursor filler node we used in Webkit. (#9384)
+ isWhitespace = !CKEDITOR.tools.trim( node.getText() ) ||
+ CKEDITOR.env.webkit && node.getText() == '\u200b';
+ }
+
+ return !!( isReject ^ isWhitespace );
+ };
+ };
+
+ /**
+ * Returns a function which checks whether the node is invisible in wysiwyg mode.
+ *
+ * @static
+ * @param {Boolean} [isReject=false]
+ * @returns {Function}
+ */
+ CKEDITOR.dom.walker.invisible = function( isReject ) {
+ var whitespace = CKEDITOR.dom.walker.whitespaces();
+ return function( node ) {
+ var invisible;
+
+ if ( whitespace( node ) )
+ invisible = 1;
+ else {
+ // Visibility should be checked on element.
+ if ( node.type == CKEDITOR.NODE_TEXT )
+ node = node.getParent();
+
+ // Nodes that take no spaces in wysiwyg:
+ // 1. White-spaces but not including NBSP;
+ // 2. Empty inline elements, e.g. we're checking here
+ // 'offsetHeight' instead of 'offsetWidth' for properly excluding
+ // all sorts of empty paragraph, e.g. .
+ invisible = !node.$.offsetHeight;
+ }
+
+ return !!( isReject ^ invisible );
+ };
+ };
+
+ /**
+ * Returns a function which checks whether node's type is equal to passed one.
+ *
+ * @static
+ * @param {Number} type
+ * @param {Boolean} [isReject=false]
+ * @returns {Function}
+ */
+ CKEDITOR.dom.walker.nodeType = function( type, isReject ) {
+ return function( node ) {
+ return !!( isReject ^ ( node.type == type ) );
+ };
+ };
+
+ /**
+ * Returns a function which checks whether node is a bogus (filler) node from
+ * contenteditable element's point of view.
+ *
+ * @static
+ * @param {Boolean} [isReject=false]
+ * @returns {Function}
+ */
+ CKEDITOR.dom.walker.bogus = function( isReject ) {
+ function nonEmpty( node ) {
+ return !isWhitespaces( node ) && !isBookmark( node );
+ }
+
+ return function( node ) {
+ var isBogus = CKEDITOR.env.needsBrFiller ? node.is && node.is( 'br' ) : node.getText && tailNbspRegex.test( node.getText() );
+
+ if ( isBogus ) {
+ var parent = node.getParent(),
+ next = node.getNext( nonEmpty );
+
+ isBogus = parent.isBlockBoundary() && ( !next || next.type == CKEDITOR.NODE_ELEMENT && next.isBlockBoundary() );
+ }
+
+ return !!( isReject ^ isBogus );
+ };
+ };
+
+ /**
+ * Returns a function which checks whether node is a temporary element
+ * (element with `data-cke-temp` attribute) or its child.
+ *
+ * @since 4.3
+ * @static
+ * @param {Boolean} [isReject=false] Whether should return `false` for the
+ * temporary element instead of `true` (default).
+ * @returns {Function}
+ */
+ CKEDITOR.dom.walker.temp = function( isReject ) {
+ return function( node ) {
+ if ( node.type != CKEDITOR.NODE_ELEMENT )
+ node = node.getParent();
+
+ var isTemp = node && node.hasAttribute( 'data-cke-temp' );
+
+ return !!( isReject ^ isTemp );
+ };
+ };
+
+ var tailNbspRegex = /^[\t\r\n ]*(?: |\xa0)$/,
+ isWhitespaces = CKEDITOR.dom.walker.whitespaces(),
+ isBookmark = CKEDITOR.dom.walker.bookmark(),
+ isTemp = CKEDITOR.dom.walker.temp(),
+ toSkip = function( node ) {
+ return isBookmark( node ) ||
+ isWhitespaces( node ) ||
+ node.type == CKEDITOR.NODE_ELEMENT && node.is( CKEDITOR.dtd.$inline ) && !node.is( CKEDITOR.dtd.$empty );
+ };
+
+ /**
+ * Returns a function which checks whether node should be ignored in terms of "editability".
+ *
+ * This includes:
+ *
+ * * whitespaces (see {@link CKEDITOR.dom.walker#whitespaces}),
+ * * bookmarks (see {@link CKEDITOR.dom.walker#bookmark}),
+ * * temporary elements (see {@link CKEDITOR.dom.walker#temp}).
+ *
+ * @since 4.3
+ * @static
+ * @param {Boolean} [isReject=false] Whether should return `false` for the
+ * ignored element instead of `true` (default).
+ * @returns {Function}
+ */
+ CKEDITOR.dom.walker.ignored = function( isReject ) {
+ return function( node ) {
+ var isIgnored = isWhitespaces( node ) || isBookmark( node ) || isTemp( node );
+
+ return !!( isReject ^ isIgnored );
+ };
+ };
+
+ var isIgnored = CKEDITOR.dom.walker.ignored();
+
+ function isEmpty( node ) {
+ var i = 0,
+ l = node.getChildCount();
+
+ for ( ; i < l; ++i ) {
+ if ( !isIgnored( node.getChild( i ) ) )
+ return false;
+ }
+ return true;
+ }
+
+ function filterTextContainers( dtd ) {
+ var hash = {},
+ name;
+
+ for ( name in dtd ) {
+ if ( CKEDITOR.dtd[ name ][ '#' ] )
+ hash[ name ] = 1;
+ }
+ return hash;
+ }
+
+ // Block elements which can contain text nodes (without ul, ol, dl, etc.).
+ var dtdTextBlock = filterTextContainers( CKEDITOR.dtd.$block );
+
+ function isEditable( node ) {
+ // Skip temporary elements, bookmarks and whitespaces.
+ if ( isIgnored( node ) )
+ return false;
+
+ if ( node.type == CKEDITOR.NODE_TEXT )
+ return true;
+
+ if ( node.type == CKEDITOR.NODE_ELEMENT ) {
+ // All inline and non-editable elements are valid editable places.
+ // Note: non-editable block has to be treated differently (should be selected entirely).
+ if ( node.is( CKEDITOR.dtd.$inline ) || node.getAttribute( 'contenteditable' ) == 'false' )
+ return true;
+
+ // Empty blocks are editable on IE.
+ if ( !CKEDITOR.env.needsBrFiller && node.is( dtdTextBlock ) && isEmpty( node ) )
+ return true;
+ }
+
+ // Skip all other nodes.
+ return false;
+ }
+
+ /**
+ * Returns a function which checks whether node can be a container or a sibling
+ * of selection end.
+ *
+ * This includes:
+ *
+ * * text nodes (but not whitespaces),
+ * * inline elements,
+ * * non-editable blocks (special case - such blocks cannot be containers nor
+ * siblings, they need to be selected entirely),
+ * * empty blocks which can contain text (IE only).
+ *
+ * @since 4.3
+ * @static
+ * @param {Boolean} [isReject=false] Whether should return `false` for the
+ * ignored element instead of `true` (default).
+ * @returns {Function}
+ */
+ CKEDITOR.dom.walker.editable = function( isReject ) {
+ return function( node ) {
+ return !!( isReject ^ isEditable( node ) );
+ };
+ };
+
+ /**
+ * Checks if there's a filler node at the end of an element, and returns it.
+ *
+ * @member CKEDITOR.dom.element
+ * @returns {CKEDITOR.dom.node/Boolean} Bogus node or `false`.
+ */
+ CKEDITOR.dom.element.prototype.getBogus = function() {
+ // Bogus are not always at the end, e.g. text
(#7070).
+ var tail = this;
+ do {
+ tail = tail.getPreviousSourceNode();
+ }
+ while ( toSkip( tail ) )
+
+ if ( tail && ( CKEDITOR.env.needsBrFiller ? tail.is && tail.is( 'br' ) : tail.getText && tailNbspRegex.test( tail.getText() ) ) )
+ return tail;
+
+ return false;
+ };
+
+} )();
diff --git a/lam/templates/lib/extra/ckeditor/core/dom/window.js b/lam/templates/lib/extra/ckeditor/core/dom/window.js
new file mode 100644
index 00000000..c371e486
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/core/dom/window.js
@@ -0,0 +1,95 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+/**
+ * @fileOverview Defines the {@link CKEDITOR.dom.document} class, which
+ * represents a DOM document.
+ */
+
+/**
+ * Represents a DOM window.
+ *
+ * var document = new CKEDITOR.dom.window( window );
+ *
+ * @class
+ * @extends CKEDITOR.dom.domObject
+ * @constructor Creates a window class instance.
+ * @param {Object} domWindow A native DOM window.
+ */
+CKEDITOR.dom.window = function( domWindow ) {
+ CKEDITOR.dom.domObject.call( this, domWindow );
+};
+
+CKEDITOR.dom.window.prototype = new CKEDITOR.dom.domObject();
+
+CKEDITOR.tools.extend( CKEDITOR.dom.window.prototype, {
+ /**
+ * Moves the selection focus to this window.
+ *
+ * var win = new CKEDITOR.dom.window( window );
+ * win.focus();
+ */
+ focus: function() {
+ this.$.focus();
+ },
+
+ /**
+ * Gets the width and height of this window's viewable area.
+ *
+ * var win = new CKEDITOR.dom.window( window );
+ * var size = win.getViewPaneSize();
+ * alert( size.width );
+ * alert( size.height );
+ *
+ * @returns {Object} An object with the `width` and `height`
+ * properties containing the size.
+ */
+ getViewPaneSize: function() {
+ var doc = this.$.document,
+ stdMode = doc.compatMode == 'CSS1Compat';
+ return {
+ width: ( stdMode ? doc.documentElement.clientWidth : doc.body.clientWidth ) || 0,
+ height: ( stdMode ? doc.documentElement.clientHeight : doc.body.clientHeight ) || 0
+ };
+ },
+
+ /**
+ * Gets the current position of the window's scroll.
+ *
+ * var win = new CKEDITOR.dom.window( window );
+ * var pos = win.getScrollPosition();
+ * alert( pos.x );
+ * alert( pos.y );
+ *
+ * @returns {Object} An object with the `x` and `y` properties
+ * containing the scroll position.
+ */
+ getScrollPosition: function() {
+ var $ = this.$;
+
+ if ( 'pageXOffset' in $ ) {
+ return {
+ x: $.pageXOffset || 0,
+ y: $.pageYOffset || 0
+ };
+ } else {
+ var doc = $.document;
+ return {
+ x: doc.documentElement.scrollLeft || doc.body.scrollLeft || 0,
+ y: doc.documentElement.scrollTop || doc.body.scrollTop || 0
+ };
+ }
+ },
+
+ /**
+ * Gets the frame element containing this window context.
+ *
+ * @returns {CKEDITOR.dom.element} The frame element or `null` if not in a frame context.
+ */
+ getFrame: function() {
+ var iframe = this.$.frameElement;
+ return iframe ? new CKEDITOR.dom.element.get( iframe ) : null;
+ }
+} );
diff --git a/lam/templates/lib/extra/ckeditor/core/htmlparser/basicwriter.js b/lam/templates/lib/extra/ckeditor/core/htmlparser/basicwriter.js
new file mode 100644
index 00000000..27651b60
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/core/htmlparser/basicwriter.js
@@ -0,0 +1,152 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+/**
+ * TODO
+ *
+ * @class
+ * @todo
+ */
+CKEDITOR.htmlParser.basicWriter = CKEDITOR.tools.createClass( {
+ /**
+ * Creates a basicWriter class instance.
+ *
+ * @constructor
+ */
+ $: function() {
+ this._ = {
+ output: []
+ };
+ },
+
+ proto: {
+ /**
+ * Writes the tag opening part for a 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 ) {
+ if ( isSelfClose )
+ this._.output.push( ' />' );
+ else
+ this._.output.push( '>' );
+ },
+
+ /**
+ * 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 ) {
+ // Browsers don't always escape special character in attribute values. (#4683, #4719).
+ if ( typeof attValue == 'string' )
+ attValue = CKEDITOR.tools.htmlEncodeAttr( attValue );
+
+ 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 ) {
+ this._.output.push( '', tagName, '>' );
+ },
+
+ /**
+ * Writes text.
+ *
+ * // Writes 'Hello Word'.
+ * writer.text( 'Hello Word' );
+ *
+ * @param {String} text The text value.
+ */
+ text: function( text ) {
+ this._.output.push( text );
+ },
+
+ /**
+ * Writes a comment.
+ *
+ * // Writes ''.
+ * writer.comment( ' My comment ' );
+ *
+ * @param {String} comment The comment text.
+ */
+ comment: function( comment ) {
+ this._.output.push( '' );
+ },
+
+ /**
+ * Writes any kind of data to the ouput.
+ *
+ * writer.write( 'This is an example.' );
+ *
+ * @param {String} data
+ */
+ write: function( data ) {
+ this._.output.push( data );
+ },
+
+ /**
+ * Empties the current output buffer.
+ *
+ * writer.reset();
+ */
+ reset: function() {
+ this._.output = [];
+ this._.indent = false;
+ },
+
+ /**
+ * Empties the current output buffer.
+ *
+ * var html = writer.getHtml();
+ *
+ * @param {Boolean} reset Indicates that the {@link #reset} method is to
+ * be automatically called after retrieving the HTML.
+ * @returns {String} The HTML written to the writer so far.
+ */
+ getHtml: function( reset ) {
+ var html = this._.output.join( '' );
+
+ if ( reset )
+ this.reset();
+
+ return html;
+ }
+ }
+} );
diff --git a/lam/templates/lib/extra/ckeditor/core/htmlparser/cdata.js b/lam/templates/lib/extra/ckeditor/core/htmlparser/cdata.js
new file mode 100644
index 00000000..01172531
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/core/htmlparser/cdata.js
@@ -0,0 +1,48 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+ 'use strict';
+
+( function() {
+
+ /**
+ * A lightweight representation of HTML CDATA.
+ *
+ * @class
+ * @extends CKEDITOR.htmlParser.node
+ * @constructor Creates a cdata class instance.
+ * @param {String} value The CDATA section value.
+ */
+ CKEDITOR.htmlParser.cdata = function( value ) {
+ /**
+ * The CDATA value.
+ *
+ * @property {String}
+ */
+ this.value = value;
+ };
+
+ CKEDITOR.htmlParser.cdata.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), {
+ /**
+ * CDATA has the same type as {@link CKEDITOR.htmlParser.text} This is
+ * a constant value set to {@link CKEDITOR#NODE_TEXT}.
+ *
+ * @readonly
+ * @property {Number} [=CKEDITOR.NODE_TEXT]
+ */
+ type: CKEDITOR.NODE_TEXT,
+
+ filter: function() {},
+
+ /**
+ * Writes the CDATA with no special manipulations.
+ *
+ * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
+ */
+ writeHtml: function( writer ) {
+ writer.write( this.value );
+ }
+ } );
+} )();
diff --git a/lam/templates/lib/extra/ckeditor/core/htmlparser/comment.js b/lam/templates/lib/extra/ckeditor/core/htmlparser/comment.js
new file mode 100644
index 00000000..7a485f57
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/core/htmlparser/comment.js
@@ -0,0 +1,80 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+ 'use strict';
+
+/**
+ * A lightweight representation of an HTML comment.
+ *
+ * @class
+ * @extends CKEDITOR.htmlParser.node
+ * @constructor Creates a comment class instance.
+ * @param {String} value The comment text value.
+ */
+CKEDITOR.htmlParser.comment = function( value ) {
+ /**
+ * The comment text.
+ *
+ * @property {String}
+ */
+ this.value = value;
+
+ /** @private */
+ this._ = {
+ isBlockLike: false
+ };
+};
+
+CKEDITOR.htmlParser.comment.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), {
+ /**
+ * The node type. This is a constant value set to {@link CKEDITOR#NODE_COMMENT}.
+ *
+ * @readonly
+ * @property {Number} [=CKEDITOR.NODE_COMMENT]
+ */
+ type: CKEDITOR.NODE_COMMENT,
+
+ /**
+ * Filter this comment with given filter.
+ *
+ * @since 4.1
+ * @param {CKEDITOR.htmlParser.filter} filter
+ * @returns {Boolean} Method returns `false` when this comment has
+ * been removed or replaced with other node. This is an information for
+ * {@link CKEDITOR.htmlParser.element#filterChildren} that it has
+ * to repeat filter on current position in parent's children array.
+ */
+ filter: function( filter, context ) {
+ var comment = this.value;
+
+ if ( !( comment = filter.onComment( context, comment, this ) ) ) {
+ this.remove();
+ return false;
+ }
+
+ if ( typeof comment != 'string' ) {
+ this.replaceWith( comment );
+ return false;
+ }
+
+ this.value = comment;
+
+ return true;
+ },
+
+ /**
+ * Writes the HTML representation of this comment to a CKEDITOR.htmlWriter.
+ *
+ * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
+ * @param {CKEDITOR.htmlParser.filter} [filter] The filter to be applied to this node.
+ * **Note:** it's unsafe to filter offline (not appended) node.
+ */
+ writeHtml: function( writer, filter ) {
+ if ( filter )
+ this.filter( filter );
+
+ writer.comment( this.value );
+ }
+} );
diff --git a/lam/templates/lib/extra/ckeditor/core/htmlparser/element.js b/lam/templates/lib/extra/ckeditor/core/htmlparser/element.js
new file mode 100644
index 00000000..2b8ce51d
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/core/htmlparser/element.js
@@ -0,0 +1,519 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+'use strict';
+
+/**
+ * A lightweight representation of an HTML element.
+ *
+ * @class
+ * @extends CKEDITOR.htmlParser.node
+ * @constructor Creates an element class instance.
+ * @param {String} name The element name.
+ * @param {Object} attributes And object holding all attributes defined for
+ * this element.
+ */
+CKEDITOR.htmlParser.element = function( name, attributes ) {
+ /**
+ * The element name.
+ *
+ * @property {String}
+ */
+ this.name = name;
+
+ /**
+ * Holds the attributes defined for this element.
+ *
+ * @property {Object}
+ */
+ this.attributes = attributes || {};
+
+ /**
+ * The nodes that are direct children of this element.
+ */
+ this.children = [];
+
+ // Reveal the real semantic of our internal custom tag name (#6639),
+ // when resolving whether it's block like.
+ var realName = name || '',
+ prefixed = realName.match( /^cke:(.*)/ );
+ prefixed && ( realName = prefixed[ 1 ] );
+
+ var isBlockLike = !!( CKEDITOR.dtd.$nonBodyContent[ realName ] || CKEDITOR.dtd.$block[ realName ] || CKEDITOR.dtd.$listItem[ realName ] || CKEDITOR.dtd.$tableContent[ realName ] || CKEDITOR.dtd.$nonEditable[ realName ] || realName == 'br' );
+
+ this.isEmpty = !!CKEDITOR.dtd.$empty[ name ];
+ this.isUnknown = !CKEDITOR.dtd[ name ];
+
+ /** @private */
+ this._ = {
+ isBlockLike: isBlockLike,
+ hasInlineStarted: this.isEmpty || !isBlockLike
+ };
+};
+
+/**
+ * Object presentation of CSS style declaration text.
+ *
+ * @class
+ * @constructor Creates a cssStyle class instance.
+ * @param {CKEDITOR.htmlParser.element/String} elementOrStyleText
+ * A html parser element or the inline style text.
+ */
+CKEDITOR.htmlParser.cssStyle = function() {
+ var styleText,
+ arg = arguments[ 0 ],
+ rules = {};
+
+ styleText = arg instanceof CKEDITOR.htmlParser.element ? arg.attributes.style : arg;
+
+ // html-encoded quote might be introduced by 'font-family'
+ // from MS-Word which confused the following regexp. e.g.
+ //'font-family: "Lucida, Console"'
+ // TODO reuse CSS methods from tools.
+ ( styleText || '' ).replace( /"/g, '"' ).replace( /\s*([^ :;]+)\s*:\s*([^;]+)\s*(?=;|$)/g, function( match, name, value ) {
+ name == 'font-family' && ( value = value.replace( /["']/g, '' ) );
+ rules[ name.toLowerCase() ] = value;
+ } );
+
+ return {
+
+ rules: rules,
+
+ /**
+ * Apply the styles onto the specified element or object.
+ *
+ * @param {CKEDITOR.htmlParser.element/CKEDITOR.dom.element/Object} obj
+ */
+ populate: function( obj ) {
+ var style = this.toString();
+ if ( style )
+ obj instanceof CKEDITOR.dom.element ? obj.setAttribute( 'style', style ) : obj instanceof CKEDITOR.htmlParser.element ? obj.attributes.style = style : obj.style = style;
+
+ },
+
+ /**
+ * Serialize CSS style declaration to string.
+ *
+ * @returns {String}
+ */
+ toString: function() {
+ var output = [];
+ for ( var i in rules )
+ rules[ i ] && output.push( i, ':', rules[ i ], ';' );
+ return output.join( '' );
+ }
+ };
+};
+
+/** @class CKEDITOR.htmlParser.element */
+( function() {
+ // Used to sort attribute entries in an array, where the first element of
+ // each object is the attribute name.
+ var sortAttribs = function( a, b ) {
+ a = a[ 0 ];
+ b = b[ 0 ];
+ return a < b ? -1 : a > b ? 1 : 0;
+ },
+ fragProto = CKEDITOR.htmlParser.fragment.prototype;
+
+ CKEDITOR.htmlParser.element.prototype = CKEDITOR.tools.extend( new CKEDITOR.htmlParser.node(), {
+ /**
+ * The node type. This is a constant value set to {@link CKEDITOR#NODE_ELEMENT}.
+ *
+ * @readonly
+ * @property {Number} [=CKEDITOR.NODE_ELEMENT]
+ */
+ type: CKEDITOR.NODE_ELEMENT,
+
+ /**
+ * Adds a node to the element children list.
+ *
+ * @method
+ * @param {CKEDITOR.htmlParser.node} node The node to be added.
+ * @param {Number} [index] From where the insertion happens.
+ */
+ add: fragProto.add,
+
+ /**
+ * Clone this element.
+ *
+ * @returns {CKEDITOR.htmlParser.element} The element clone.
+ */
+ clone: function() {
+ return new CKEDITOR.htmlParser.element( this.name, this.attributes );
+ },
+
+ /**
+ * Filter this element and its children with given filter.
+ *
+ * @since 4.1
+ * @param {CKEDITOR.htmlParser.filter} filter
+ * @returns {Boolean} Method returns `false` when this element has
+ * been removed or replaced with other. This is an information for
+ * {@link #filterChildren} that it has to repeat filter on current
+ * position in parent's children array.
+ */
+ filter: function( filter, context ) {
+ var element = this,
+ originalName, name;
+
+ context = element.getFilterContext( context );
+
+ // Do not process elements with data-cke-processor attribute set to off.
+ if ( context.off )
+ return true;
+
+ // Filtering if it's the root node.
+ if ( !element.parent )
+ filter.onRoot( context, element );
+
+ while ( true ) {
+ originalName = element.name;
+
+ if ( !( name = filter.onElementName( context, originalName ) ) ) {
+ this.remove();
+ return false;
+ }
+
+ element.name = name;
+
+ if ( !( element = filter.onElement( context, element ) ) ) {
+ this.remove();
+ return false;
+ }
+
+ // New element has been returned - replace current one
+ // and process it (stop processing this and return false, what
+ // means that element has been removed).
+ if ( element !== this ) {
+ this.replaceWith( element );
+ return false;
+ }
+
+ // If name has been changed - continue loop, so in next iteration
+ // filters for new name will be applied to this element.
+ // If name hasn't been changed - stop.
+ if ( element.name == originalName )
+ break;
+
+ // If element has been replaced with something of a
+ // different type, then make the replacement filter itself.
+ if ( element.type != CKEDITOR.NODE_ELEMENT ) {
+ this.replaceWith( element );
+ return false;
+ }
+
+ // This indicate that the element has been dropped by
+ // filter but not the children.
+ if ( !element.name ) {
+ this.replaceWithChildren();
+ return false;
+ }
+ }
+
+ var attributes = element.attributes,
+ a, value, newAttrName;
+
+ for ( a in attributes ) {
+ newAttrName = a;
+ value = attributes[ a ];
+
+ // Loop until name isn't modified.
+ // A little bit senseless, but IE would do that anyway
+ // because it iterates with for-in loop even over properties
+ // created during its run.
+ while ( true ) {
+ if ( !( newAttrName = filter.onAttributeName( context, a ) ) ) {
+ delete attributes[ a ];
+ break;
+ } else if ( newAttrName != a ) {
+ delete attributes[ a ];
+ a = newAttrName;
+ continue;
+ } else
+ break;
+ }
+
+ if ( newAttrName ) {
+ if ( ( value = filter.onAttribute( context, element, newAttrName, value ) ) === false )
+ delete attributes[ newAttrName ];
+ else
+ attributes[ newAttrName ] = value;
+ }
+ }
+
+ if ( !element.isEmpty )
+ this.filterChildren( filter, false, context );
+
+ return true;
+ },
+
+ /**
+ * Filter this element's children with given filter.
+ *
+ * Element's children may only be filtered once by one
+ * instance of filter.
+ *
+ * @method filterChildren
+ * @param {CKEDITOR.htmlParser.filter} filter
+ */
+ filterChildren: fragProto.filterChildren,
+
+ /**
+ * Writes the element HTML to a CKEDITOR.htmlWriter.
+ *
+ * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
+ * @param {CKEDITOR.htmlParser.filter} [filter] The filter to be applied to this node.
+ * **Note:** it's unsafe to filter offline (not appended) node.
+ */
+ writeHtml: function( writer, filter ) {
+ if ( filter )
+ this.filter( filter );
+
+ var name = this.name,
+ attribsArray = [],
+ attributes = this.attributes,
+ attrName,
+ attr, i, l;
+
+ // Open element tag.
+ writer.openTag( name, attributes );
+
+ // Copy all attributes to an array.
+ for ( attrName in attributes )
+ attribsArray.push( [ attrName, attributes[ attrName ] ] );
+
+ // Sort the attributes by name.
+ if ( writer.sortAttributes )
+ attribsArray.sort( sortAttribs );
+
+ // Send the attributes.
+ for ( i = 0, l = attribsArray.length; i < l; i++ ) {
+ attr = attribsArray[ i ];
+ writer.attribute( attr[ 0 ], attr[ 1 ] );
+ }
+
+ // Close the tag.
+ writer.openTagClose( name, this.isEmpty );
+
+ this.writeChildrenHtml( writer );
+
+ // Close the element.
+ if ( !this.isEmpty )
+ writer.closeTag( name );
+ },
+
+ /**
+ * Send children of this element to the writer.
+ *
+ * @param {CKEDITOR.htmlParser.basicWriter} writer The writer to which write the HTML.
+ * @param {CKEDITOR.htmlParser.filter} [filter]
+ */
+ writeChildrenHtml: fragProto.writeChildrenHtml,
+
+ /**
+ * Replace this element with its children.
+ *
+ * @since 4.1
+ */
+ replaceWithChildren: function() {
+ var children = this.children;
+
+ for ( var i = children.length; i; )
+ children[ --i ].insertAfter( this );
+
+ this.remove();
+ },
+
+ /**
+ * Execute callback on each node (of given type) in this element.
+ *
+ * // Create element with foobarbom as its content.
+ * var elP = CKEDITOR.htmlParser.fragment.fromHtml( 'foobarbom', 'p' );
+ * elP.forEach( function( node ) {
+ * console.log( node );
+ * } );
+ * // Will log:
+ * // 1. document fragment,
+ * // 2. element,
+ * // 3. "foo" text node,
+ * // 4. element,
+ * // 5. "bar" text node,
+ * // 6. "bom" text node.
+ *
+ * @since 4.1
+ * @param {Function} callback Function to be executed on every node.
+ * **Since 4.3** if `callback` returned `false` descendants of current node will be ignored.
+ * @param {CKEDITOR.htmlParser.node} callback.node Node passed as argument.
+ * @param {Number} [type] If specified `callback` will be executed only on nodes of this type.
+ * @param {Boolean} [skipRoot] Don't execute `callback` on this element.
+ */
+ forEach: fragProto.forEach,
+
+ /**
+ * Gets this element's first child. If `condition` is given returns
+ * first child which satisfies that condition.
+ *
+ * @since 4.3
+ * @param {String/Object/Function} condition Name of a child, hash of names or validator function.
+ * @returns {CKEDITOR.htmlParser.node}
+ */
+ getFirst: function( condition ) {
+ if ( !condition )
+ return this.children.length ? this.children[ 0 ] : null;
+
+ if ( typeof condition != 'function' )
+ condition = nameCondition( condition );
+
+ for ( var i = 0, l = this.children.length; i < l; ++i ) {
+ if ( condition( this.children[ i ] ) )
+ return this.children[ i ];
+ }
+ return null;
+ },
+
+ /**
+ * Gets this element's inner HTML.
+ *
+ * @since 4.3
+ * @returns {String}
+ */
+ getHtml: function() {
+ var writer = new CKEDITOR.htmlParser.basicWriter();
+ this.writeChildrenHtml( writer );
+ return writer.getHtml();
+ },
+
+ /**
+ * Sets this element's inner HTML.
+ *
+ * @since 4.3
+ * @param {String} html
+ */
+ setHtml: function( html ) {
+ var children = this.children = CKEDITOR.htmlParser.fragment.fromHtml( html ).children;
+
+ for ( var i = 0, l = children.length; i < l; ++i )
+ children[ i ].parent = this;
+ },
+
+ /**
+ * Gets this element's outer HTML.
+ *
+ * @since 4.3
+ * @returns {String}
+ */
+ getOuterHtml: function() {
+ var writer = new CKEDITOR.htmlParser.basicWriter();
+ this.writeHtml( writer );
+ return writer.getHtml();
+ },
+
+ /**
+ * Splits this element at given index.
+ *
+ * @since 4.3
+ * @param {Number} index Index at which element will be split – `0` means beginning,
+ * `1` after first child node, etc.
+ * @returns {CKEDITOR.htmlParser.element} New element, following this one.
+ */
+ split: function( index ) {
+ var cloneChildren = this.children.splice( index, this.children.length - index ),
+ clone = this.clone();
+
+ for ( var i = 0; i < cloneChildren.length; ++i )
+ cloneChildren[ i ].parent = clone;
+
+ clone.children = cloneChildren;
+
+ if ( cloneChildren[ 0 ] )
+ cloneChildren[ 0 ].previous = null;
+
+ if ( index > 0 )
+ this.children[ index - 1 ].next = null;
+
+ this.parent.add( clone, this.getIndex() + 1 );
+
+ return clone;
+ },
+
+ /**
+ * Removes class name from classes list.
+ *
+ * @since 4.3
+ * @param {String} className The class name to be removed.
+ */
+ removeClass: function( className ) {
+ var classes = this.attributes[ 'class' ],
+ index;
+
+ if ( !classes )
+ return;
+
+ // We can safely assume that className won't break regexp.
+ // http://stackoverflow.com/questions/448981/what-characters-are-valid-in-css-class-names
+ classes = CKEDITOR.tools.trim( classes.replace( new RegExp( '(?:\\s+|^)' + className + '(?:\\s+|$)' ), ' ' ) );
+
+ if ( classes )
+ this.attributes[ 'class' ] = classes;
+ else
+ delete this.attributes[ 'class' ];
+ },
+
+ /**
+ * Checkes whether this element has a class name.
+ *
+ * @since 4.3
+ * @param {String} className The class name to be checked.
+ * @returns {Boolean} Whether this element has a `className`.
+ */
+ hasClass: function( className ) {
+ var classes = this.attributes[ 'class' ];
+
+ if ( !classes )
+ return false;
+
+ return ( new RegExp( '(?:^|\\s)' + className + '(?=\\s|$)' ) ).test( classes );
+ },
+
+ getFilterContext: function( ctx ) {
+ var changes = [];
+
+ if ( !ctx ) {
+ ctx = {
+ off: false,
+ nonEditable: false,
+ nestedEditable: false
+ };
+ }
+
+ if ( !ctx.off && this.attributes[ 'data-cke-processor' ] == 'off' )
+ changes.push( 'off', true );
+
+ if ( !ctx.nonEditable && this.attributes.contenteditable == 'false' )
+ changes.push( 'nonEditable', true );
+ // A context to be given nestedEditable must be nonEditable first (by inheritance) (#11372).
+ // Never set "nestedEditable" context for a body. If body is processed then it indicates
+ // a fullPage editor and there's no slightest change of nesting such editable (#11504).
+ else if ( this.name != 'body' && !ctx.nestedEditable && this.attributes.contenteditable == 'true' )
+ changes.push( 'nestedEditable', true );
+
+ if ( changes.length ) {
+ ctx = CKEDITOR.tools.copy( ctx );
+ for ( var i = 0; i < changes.length; i += 2 )
+ ctx[ changes[ i ] ] = changes[ i + 1 ];
+ }
+
+ return ctx;
+ }
+ }, true );
+
+ function nameCondition( condition ) {
+ return function( el ) {
+ return el.type == CKEDITOR.NODE_ELEMENT &&
+ ( typeof condition == 'string' ? el.name == condition : el.name in condition );
+ };
+ }
+} )();
diff --git a/lam/templates/lib/extra/ckeditor/core/htmlparser/filter.js b/lam/templates/lib/extra/ckeditor/core/htmlparser/filter.js
new file mode 100644
index 00000000..d44122a5
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/core/htmlparser/filter.js
@@ -0,0 +1,407 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+'use strict';
+
+( function() {
+ /**
+ * Filter is a configurable tool for transforming and filtering {@link CKEDITOR.htmlParser.node nodes}.
+ * It is mainly used during data processing phase which is done not on real DOM nodes,
+ * but on their simplified form represented by {@link CKEDITOR.htmlParser.node} class and its subclasses.
+ *
+ * var filter = new CKEDITOR.htmlParser.filter( {
+ * text: function( value ) {
+ * return '@' + value + '@';
+ * },
+ * elements: {
+ * p: function( element ) {
+ * element.attributes.foo = '1';
+ * }
+ * }
+ * } );
+ *
+ * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( ' Foobar! ' ),
+ * writer = new CKEDITOR.htmlParser.basicWriter();
+ * filter.applyTo( fragment );
+ * fragment.writeHtml( writer );
+ * writer.getHtml(); // '@Foo@@bar!@ '
+ *
+ * @class
+ */
+ CKEDITOR.htmlParser.filter = CKEDITOR.tools.createClass( {
+ /**
+ * @constructor Creates a filter class instance.
+ * @param {CKEDITOR.htmlParser.filterRulesDefinition} [rules]
+ */
+ $: function( rules ) {
+ /**
+ * ID of filter instance, which is used to mark elements
+ * to which this filter has been already applied.
+ *
+ * @property {Number} id
+ * @readonly
+ */
+ this.id = CKEDITOR.tools.getNextNumber();
+
+ /**
+ * Rules for element names.
+ *
+ * @property {CKEDITOR.htmlParser.filterRulesGroup}
+ * @readonly
+ */
+ this.elementNameRules = new filterRulesGroup();
+
+ /**
+ * Rules for attribute names.
+ *
+ * @property {CKEDITOR.htmlParser.filterRulesGroup}
+ * @readonly
+ */
+ this.attributeNameRules = new filterRulesGroup();
+
+ /**
+ * Hash of elementName => {@link CKEDITOR.htmlParser.filterRulesGroup rules for elements}.
+ *
+ * @readonly
+ */
+ this.elementsRules = {};
+
+ /**
+ * Hash of attributeName => {@link CKEDITOR.htmlParser.filterRulesGroup rules for attributes}.
+ *
+ * @readonly
+ */
+ this.attributesRules = {};
+
+ /**
+ * Rules for text nodes.
+ *
+ * @property {CKEDITOR.htmlParser.filterRulesGroup}
+ * @readonly
+ */
+ this.textRules = new filterRulesGroup();
+
+ /**
+ * Rules for comment nodes.
+ *
+ * @property {CKEDITOR.htmlParser.filterRulesGroup}
+ * @readonly
+ */
+ this.commentRules = new filterRulesGroup();
+
+ /**
+ * Rules for a root node.
+ *
+ * @property {CKEDITOR.htmlParser.filterRulesGroup}
+ * @readonly
+ */
+ this.rootRules = new filterRulesGroup();
+
+ if ( rules )
+ this.addRules( rules, 10 );
+ },
+
+ proto: {
+ /**
+ * Add rules to this filter.
+ *
+ * @param {CKEDITOR.htmlParser.filterRulesDefinition} rules Object containing filter rules.
+ * @param {Object/Number} [options] Object containing rules' options or a priority
+ * (for a backward compatibility with CKEditor versions up to 4.2.x).
+ * @param {Number} [options.priority=10] The priority of a rule.
+ * @param {Boolean} [options.applyToAll=false] Whether to apply rule to non-editable
+ * elements and their descendants too.
+ */
+ addRules: function( rules, options ) {
+ var priority;
+
+ // Backward compatibility.
+ if ( typeof options == 'number' )
+ priority = options;
+ // New version - try reading from options.
+ else if ( options && ( 'priority' in options ) )
+ priority = options.priority;
+
+ // Defaults.
+ if ( typeof priority != 'number' )
+ priority = 10;
+ if ( typeof options != 'object' )
+ options = {};
+
+ // Add the elementNames.
+ if ( rules.elementNames )
+ this.elementNameRules.addMany( rules.elementNames, priority, options );
+
+ // Add the attributeNames.
+ if ( rules.attributeNames )
+ this.attributeNameRules.addMany( rules.attributeNames, priority, options );
+
+ // Add the elements.
+ if ( rules.elements )
+ addNamedRules( this.elementsRules, rules.elements, priority, options );
+
+ // Add the attributes.
+ if ( rules.attributes )
+ addNamedRules( this.attributesRules, rules.attributes, priority, options );
+
+ // Add the text.
+ if ( rules.text )
+ this.textRules.add( rules.text, priority, options );
+
+ // Add the comment.
+ if ( rules.comment )
+ this.commentRules.add( rules.comment, priority, options );
+
+ // Add root node rules.
+ if ( rules.root )
+ this.rootRules.add( rules.root, priority, options );
+ },
+
+ /**
+ * Apply this filter to given node.
+ *
+ * @param {CKEDITOR.htmlParser.node} node The node to be filtered.
+ */
+ applyTo: function( node ) {
+ node.filter( this );
+ },
+
+ onElementName: function( context, name ) {
+ return this.elementNameRules.execOnName( context, name );
+ },
+
+ onAttributeName: function( context, name ) {
+ return this.attributeNameRules.execOnName( context, name );
+ },
+
+ onText: function( context, text, node ) {
+ return this.textRules.exec( context, text, node );
+ },
+
+ onComment: function( context, commentText, comment ) {
+ return this.commentRules.exec( context, commentText, comment );
+ },
+
+ onRoot: function( context, element ) {
+ return this.rootRules.exec( context, element );
+ },
+
+ onElement: function( context, element ) {
+ // We must apply filters set to the specific element name as
+ // well as those set to the generic ^/$ name. So, add both to an
+ // array and process them in a small loop.
+ var rulesGroups = [ this.elementsRules[ '^' ], this.elementsRules[ element.name ], this.elementsRules.$ ],
+ rulesGroup, ret;
+
+ for ( var i = 0; i < 3; i++ ) {
+ rulesGroup = rulesGroups[ i ];
+ if ( rulesGroup ) {
+ ret = rulesGroup.exec( context, element, this );
+
+ if ( ret === false )
+ return null;
+
+ if ( ret && ret != element )
+ return this.onNode( context, ret );
+
+ // The non-root element has been dismissed by one of the filters.
+ if ( element.parent && !element.name )
+ break;
+ }
+ }
+
+ return element;
+ },
+
+ onNode: function( context, node ) {
+ var type = node.type;
+
+ return type == CKEDITOR.NODE_ELEMENT ? this.onElement( context, node ) :
+ type == CKEDITOR.NODE_TEXT ? new CKEDITOR.htmlParser.text( this.onText( context, node.value ) ) :
+ type == CKEDITOR.NODE_COMMENT ? new CKEDITOR.htmlParser.comment( this.onComment( context, node.value ) ) : null;
+ },
+
+ onAttribute: function( context, element, name, value ) {
+ var rulesGroup = this.attributesRules[ name ];
+
+ if ( rulesGroup )
+ return rulesGroup.exec( context, value, element, this );
+ return value;
+ }
+ }
+ } );
+
+ /**
+ * Class grouping filter rules for one subject (like element or attribute names).
+ *
+ * @class CKEDITOR.htmlParser.filterRulesGroup
+ */
+ function filterRulesGroup() {
+ /**
+ * Array of objects containing rule, priority and options.
+ *
+ * @property {Object[]}
+ * @readonly
+ */
+ this.rules = [];
+ }
+
+ CKEDITOR.htmlParser.filterRulesGroup = filterRulesGroup;
+
+ filterRulesGroup.prototype = {
+ /**
+ * Adds specified rule to this group.
+ *
+ * @param {Function/Array} rule Function for function based rule or [ pattern, replacement ] array for
+ * rule applicable to names.
+ * @param {Number} priority
+ * @param options
+ */
+ add: function( rule, priority, options ) {
+ this.rules.splice( this.findIndex( priority ), 0, {
+ value: rule,
+ priority: priority,
+ options: options
+ } );
+ },
+
+ /**
+ * Adds specified rules to this group.
+ *
+ * @param {Array} rules Array of rules - see {@link #add}.
+ * @param {Number} priority
+ * @param options
+ */
+ addMany: function( rules, priority, options ) {
+ var args = [ this.findIndex( priority ), 0 ];
+
+ for ( var i = 0, len = rules.length; i < len; i++ ) {
+ args.push( {
+ value: rules[ i ],
+ priority: priority,
+ options: options
+ } );
+ }
+
+ this.rules.splice.apply( this.rules, args );
+ },
+
+ /**
+ * Finds an index at which rule with given priority should be inserted.
+ *
+ * @param {Number} priority
+ * @returns {Number} Index.
+ */
+ findIndex: function( priority ) {
+ var rules = this.rules,
+ len = rules.length,
+ i = len - 1;
+
+ // Search from the end, because usually rules will be added with default priority, so
+ // we will be able to stop loop quickly.
+ while ( i >= 0 && priority < rules[ i ].priority )
+ i--;
+
+ return i + 1;
+ },
+
+ /**
+ * Executes this rules group on given value. Applicable only if function based rules were added.
+ *
+ * All arguments passed to this function will be forwarded to rules' functions.
+ *
+ * @param {CKEDITOR.htmlParser.node/CKEDITOR.htmlParser.fragment/String} currentValue The value to be filtered.
+ * @returns {CKEDITOR.htmlParser.node/CKEDITOR.htmlParser.fragment/String} Filtered value.
+ */
+ exec: function( context, currentValue ) {
+ var isNode = currentValue instanceof CKEDITOR.htmlParser.node || currentValue instanceof CKEDITOR.htmlParser.fragment,
+ // Splice '1' to remove context, which we don't want to pass to filter rules.
+ args = Array.prototype.slice.call( arguments, 1 ),
+ rules = this.rules,
+ len = rules.length,
+ orgType, orgName, ret, i, rule;
+
+ for ( i = 0; i < len; i++ ) {
+ // Backup the node info before filtering.
+ if ( isNode ) {
+ orgType = currentValue.type;
+ orgName = currentValue.name;
+ }
+
+ rule = rules[ i ];
+ if ( isRuleApplicable( context, rule ) ) {
+ ret = rule.value.apply( null, args );
+
+ if ( ret === false )
+ return ret;
+
+ // We're filtering node (element/fragment).
+ // No further filtering if it's not anymore fitable for the subsequent filters.
+ if ( isNode && ret && ( ret.name != orgName || ret.type != orgType ) )
+ return ret;
+
+ // Update currentValue and corresponding argument in args array.
+ // Updated values will be used in next for-loop step.
+ if ( ret != undefined )
+ args[ 0 ] = currentValue = ret;
+
+ // ret == undefined will continue loop as nothing has happened.
+ }
+ }
+
+ return currentValue;
+ },
+
+ /**
+ * Executes this rules group on name. Applicable only if filter rules for names were added.
+ *
+ * @param {String} currentName The name to be filtered.
+ * @returns {String} Filtered name.
+ */
+ execOnName: function( context, currentName ) {
+ var i = 0,
+ rules = this.rules,
+ len = rules.length,
+ rule;
+
+ for ( ; currentName && i < len; i++ ) {
+ rule = rules[ i ];
+ if ( isRuleApplicable( context, rule ) )
+ currentName = currentName.replace( rule.value[ 0 ], rule.value[ 1 ] );
+ }
+
+ return currentName;
+ }
+ };
+
+ function addNamedRules( rulesGroups, newRules, priority, options ) {
+ var ruleName, rulesGroup;
+
+ for ( ruleName in newRules ) {
+ rulesGroup = rulesGroups[ ruleName ];
+
+ if ( !rulesGroup )
+ rulesGroup = rulesGroups[ ruleName ] = new filterRulesGroup();
+
+ rulesGroup.add( newRules[ ruleName ], priority, options );
+ }
+ }
+
+ function isRuleApplicable( context, rule ) {
+ if ( context.nonEditable && !rule.options.applyToAll )
+ return false;
+
+ if ( context.nestedEditable && rule.options.excludeNestedEditable )
+ return false;
+
+ return true;
+ }
+
+} )();
+
+/**
+ * @class CKEDITOR.htmlParser.filterRulesDefinition
+ * @abstract
+ */
\ No newline at end of file
diff --git a/lam/templates/lib/extra/ckeditor/core/htmlparser/fragment.js b/lam/templates/lib/extra/ckeditor/core/htmlparser/fragment.js
new file mode 100644
index 00000000..ba9fd99d
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/core/htmlparser/fragment.js
@@ -0,0 +1,643 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+'use strict';
+
+/**
+ * A lightweight representation of an HTML DOM structure.
+ *
+ * @class
+ * @constructor Creates a fragment class instance.
+ */
+CKEDITOR.htmlParser.fragment = function() {
+ /**
+ * The nodes contained in the root of this fragment.
+ *
+ * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( 'Sample Text' );
+ * alert( fragment.children.length ); // 2
+ */
+ this.children = [];
+
+ /**
+ * Get the fragment parent. Should always be null.
+ *
+ * @property {Object} [=null]
+ */
+ this.parent = null;
+
+ /** @private */
+ this._ = {
+ isBlockLike: true,
+ hasInlineStarted: false
+ };
+};
+
+( function() {
+ // Block-level elements whose internal structure should be respected during
+ // parser fixing.
+ var nonBreakingBlocks = CKEDITOR.tools.extend( { table: 1, ul: 1, ol: 1, dl: 1 }, CKEDITOR.dtd.table, CKEDITOR.dtd.ul, CKEDITOR.dtd.ol, CKEDITOR.dtd.dl );
+
+ var listBlocks = { ol: 1, ul: 1 };
+
+ // Dtd of the fragment element, basically it accept anything except for intermediate structure, e.g. orphan - .
+ var rootDtd = CKEDITOR.tools.extend( {}, { html: 1 }, CKEDITOR.dtd.html, CKEDITOR.dtd.body, CKEDITOR.dtd.head, { style: 1, script: 1 } );
+
+ // Which element to create when encountered not allowed content.
+ var structureFixes = {
+ ul: 'li',
+ ol: 'li',
+ dl: 'dd',
+ table: 'tbody',
+ tbody: 'tr',
+ thead: 'tr',
+ tfoot: 'tr',
+ tr: 'td'
+ };
+
+ function isRemoveEmpty( node ) {
+ // Keep marked element event if it is empty.
+ if ( node.attributes[ 'data-cke-survive' ] )
+ return false;
+
+ // Empty link is to be removed when empty but not anchor. (#7894)
+ return node.name == 'a' && node.attributes.href || CKEDITOR.dtd.$removeEmpty[ node.name ];
+ }
+
+ /**
+ * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string.
+ *
+ * var fragment = CKEDITOR.htmlParser.fragment.fromHtml( 'Sample Text' );
+ * alert( fragment.children[ 0 ].name ); // 'b'
+ * alert( fragment.children[ 1 ].value ); // ' Text'
+ *
+ * @static
+ * @param {String} fragmentHtml The HTML to be parsed, filling the fragment.
+ * @param {CKEDITOR.htmlParser.element/String} [parent] Optional contextual
+ * element which makes the content been parsed as the content of this element and fix
+ * to match it.
+ * If not provided, then {@link CKEDITOR.htmlParser.fragment} will be used
+ * as the parent and it will be returned.
+ * @param {String/Boolean} [fixingBlock] When `parent` is a block limit element,
+ * and the param is a string value other than `false`, it is to
+ * avoid having block-less content as the direct children of parent by wrapping
+ * the content with a block element of the specified tag, e.g.
+ * when `fixingBlock` specified as `p`, the content `foo`
+ * will be fixed into `
foo `.
+ * @returns {CKEDITOR.htmlParser.fragment/CKEDITOR.htmlParser.element} The created fragment or passed `parent`.
+ */
+ CKEDITOR.htmlParser.fragment.fromHtml = function( fragmentHtml, parent, fixingBlock ) {
+ var parser = new CKEDITOR.htmlParser();
+
+ var root = parent instanceof CKEDITOR.htmlParser.element ? parent : typeof parent == 'string' ? new CKEDITOR.htmlParser.element( parent ) : new CKEDITOR.htmlParser.fragment();
+
+ var pendingInline = [],
+ pendingBRs = [],
+ currentNode = root,
+ // Indicate we're inside a => - ^
+ // =>
+ //
+ // AND
+ //
+ // =>
+ // - =>
- ^
+ //
+ // =>
+ // =>
+
+ if ( firstChild || lastChild )
+ block[ firstChild ? 'insertBefore' : 'insertAfter' ]( 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 {
+ // 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 );
+
+ // 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. (#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. (#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 );
+ } else {
+ var newBlockDir;
+
+ if ( previousBlock ) {
+ // Do not enter this block if it's a header tag, or we are in
+ // a Shift+Enter (#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. (#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 (#7330), but we need
+ // to remove the attribute after it's onto the DOM tree because of old IEs (#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();
+ },
+
+ 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;
+
+ // Determine the block element to be used.
+ var blockTag = ( mode == CKEDITOR.ENTER_DIV ? 'div' : 'p' );
+
+ var isEndOfBlock = range.checkEndOfBlock();
+
+ var elementPath = new CKEDITOR.dom.elementPath( editor.getSelection().getStartElement() );
+
+ var startBlock = elementPath.block,
+ startBlockTag = startBlock && elementPath.block.getName();
+
+ var isPre = false;
+
+ 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 (#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 )
+ lineBreak.getParent().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();
+ }
+ };
+
+ var 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. (#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.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 ];
+ }
+} )();
diff --git a/lam/templates/lib/extra/ckeditor/plugins/entities/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/entities/plugin.js
new file mode 100644
index 00000000..87265979
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/entities/plugin.js
@@ -0,0 +1,239 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+( function() {
+ // Base HTML entities.
+ var htmlbase = 'nbsp,gt,lt,amp';
+
+ var entities =
+ // Latin-1 Entities
+ 'quot,iexcl,cent,pound,curren,yen,brvbar,sect,uml,copy,ordf,laquo,' +
+ 'not,shy,reg,macr,deg,plusmn,sup2,sup3,acute,micro,para,middot,' +
+ 'cedil,sup1,ordm,raquo,frac14,frac12,frac34,iquest,times,divide,' +
+
+ // Symbols
+ 'fnof,bull,hellip,prime,Prime,oline,frasl,weierp,image,real,trade,' +
+ 'alefsym,larr,uarr,rarr,darr,harr,crarr,lArr,uArr,rArr,dArr,hArr,' +
+ 'forall,part,exist,empty,nabla,isin,notin,ni,prod,sum,minus,lowast,' +
+ 'radic,prop,infin,ang,and,or,cap,cup,int,there4,sim,cong,asymp,ne,' +
+ 'equiv,le,ge,sub,sup,nsub,sube,supe,oplus,otimes,perp,sdot,lceil,' +
+ 'rceil,lfloor,rfloor,lang,rang,loz,spades,clubs,hearts,diams,' +
+
+ // Other Special Characters
+ 'circ,tilde,ensp,emsp,thinsp,zwnj,zwj,lrm,rlm,ndash,mdash,lsquo,' +
+ 'rsquo,sbquo,ldquo,rdquo,bdquo,dagger,Dagger,permil,lsaquo,rsaquo,' +
+ 'euro';
+
+ // Latin Letters Entities
+ var latin = 'Agrave,Aacute,Acirc,Atilde,Auml,Aring,AElig,Ccedil,Egrave,Eacute,' +
+ 'Ecirc,Euml,Igrave,Iacute,Icirc,Iuml,ETH,Ntilde,Ograve,Oacute,Ocirc,' +
+ 'Otilde,Ouml,Oslash,Ugrave,Uacute,Ucirc,Uuml,Yacute,THORN,szlig,' +
+ 'agrave,aacute,acirc,atilde,auml,aring,aelig,ccedil,egrave,eacute,' +
+ 'ecirc,euml,igrave,iacute,icirc,iuml,eth,ntilde,ograve,oacute,ocirc,' +
+ 'otilde,ouml,oslash,ugrave,uacute,ucirc,uuml,yacute,thorn,yuml,' +
+ 'OElig,oelig,Scaron,scaron,Yuml';
+
+ // Greek Letters Entities.
+ var greek = 'Alpha,Beta,Gamma,Delta,Epsilon,Zeta,Eta,Theta,Iota,Kappa,Lambda,Mu,' +
+ 'Nu,Xi,Omicron,Pi,Rho,Sigma,Tau,Upsilon,Phi,Chi,Psi,Omega,alpha,' +
+ 'beta,gamma,delta,epsilon,zeta,eta,theta,iota,kappa,lambda,mu,nu,xi,' +
+ 'omicron,pi,rho,sigmaf,sigma,tau,upsilon,phi,chi,psi,omega,thetasym,' +
+ 'upsih,piv';
+
+ // Create a mapping table between one character and its entity form from a list of entity names.
+ // @param reverse {Boolean} Whether to create a reverse map from the entity string form to an actual character.
+ function buildTable( entities, reverse ) {
+ var table = {},
+ regex = [];
+
+ // Entities that the browsers DOM don't transform to the final char
+ // automatically.
+ var specialTable = {
+ nbsp: '\u00A0', // IE | FF
+ shy: '\u00AD', // IE
+ gt: '\u003E', // IE | FF | -- | Opera
+ lt: '\u003C', // IE | FF | Safari | Opera
+ amp: '\u0026', // ALL
+ apos: '\u0027', // IE
+ quot: '\u0022' // IE
+ };
+
+ entities = entities.replace( /\b(nbsp|shy|gt|lt|amp|apos|quot)(?:,|$)/g, function( match, entity ) {
+ var org = reverse ? '&' + entity + ';' : specialTable[ entity ],
+ result = reverse ? specialTable[ entity ] : '&' + entity + ';';
+
+ table[ org ] = result;
+ regex.push( org );
+ return '';
+ } );
+
+ if ( !reverse && entities ) {
+ // Transforms the entities string into an array.
+ entities = entities.split( ',' );
+
+ // Put all entities inside a DOM element, transforming them to their
+ // final chars.
+ var div = document.createElement( 'div' ),
+ chars;
+ div.innerHTML = '&' + entities.join( ';&' ) + ';';
+ chars = div.innerHTML;
+ div = null;
+
+ // Add all chars to the table.
+ for ( var i = 0; i < chars.length; i++ ) {
+ var charAt = chars.charAt( i );
+ table[ charAt ] = '&' + entities[ i ] + ';';
+ regex.push( charAt );
+ }
+ }
+
+ table.regex = regex.join( reverse ? '|' : '' );
+
+ return table;
+ }
+
+ CKEDITOR.plugins.add( 'entities', {
+ afterInit: function( editor ) {
+ var config = editor.config;
+
+ var dataProcessor = editor.dataProcessor,
+ htmlFilter = dataProcessor && dataProcessor.htmlFilter;
+
+ if ( htmlFilter ) {
+ // Mandatory HTML base entities.
+ var selectedEntities = [];
+
+ if ( config.basicEntities !== false )
+ selectedEntities.push( htmlbase );
+
+ if ( config.entities ) {
+ if ( selectedEntities.length )
+ selectedEntities.push( entities );
+
+ if ( config.entities_latin )
+ selectedEntities.push( latin );
+
+ if ( config.entities_greek )
+ selectedEntities.push( greek );
+
+ if ( config.entities_additional )
+ selectedEntities.push( config.entities_additional );
+ }
+
+ var entitiesTable = buildTable( selectedEntities.join( ',' ) );
+
+ // Create the Regex used to find entities in the text, leave it matches nothing if entities are empty.
+ var entitiesRegex = entitiesTable.regex ? '[' + entitiesTable.regex + ']' : 'a^';
+ delete entitiesTable.regex;
+
+ if ( config.entities && config.entities_processNumerical )
+ entitiesRegex = '[^ -~]|' + entitiesRegex;
+
+ entitiesRegex = new RegExp( entitiesRegex, 'g' );
+
+ function getEntity( character ) {
+ return config.entities_processNumerical == 'force' || !entitiesTable[ character ] ? '' + character.charCodeAt( 0 ) + ';'
+ : entitiesTable[ character ];
+ }
+
+ // Decode entities that the browsers has transformed
+ // at first place.
+ var baseEntitiesTable = buildTable( [ htmlbase, 'shy' ].join( ',' ), true ),
+ baseEntitiesRegex = new RegExp( baseEntitiesTable.regex, 'g' );
+
+ function getChar( character ) {
+ return baseEntitiesTable[ character ];
+ }
+
+ htmlFilter.addRules( {
+ text: function( text ) {
+ return text.replace( baseEntitiesRegex, getChar ).replace( entitiesRegex, getEntity );
+ }
+ }, {
+ applyToAll: true,
+ excludeNestedEditable: true
+ } );
+ }
+ }
+ } );
+} )();
+
+/**
+ * Whether to escape basic HTML entities in the document, including:
+ *
+ * * `nbsp`
+ * * `gt`
+ * * `lt`
+ * * `amp`
+ *
+ * **Note:** It should not be subject to change unless when outputting a non-HTML data format like BBCode.
+ *
+ * config.basicEntities = false;
+ *
+ * @cfg {Boolean} [basicEntities=true]
+ * @member CKEDITOR.config
+ */
+CKEDITOR.config.basicEntities = true;
+
+/**
+ * Whether to use HTML entities in the output.
+ *
+ * config.entities = false;
+ *
+ * @cfg {Boolean} [entities=true]
+ * @member CKEDITOR.config
+ */
+CKEDITOR.config.entities = true;
+
+/**
+ * Whether to convert some Latin characters (Latin alphabet No. 1, ISO 8859-1)
+ * to HTML entities. The list of entities can be found in the
+ * [W3C HTML 4.01 Specification, section 24.2.1](http://www.w3.org/TR/html4/sgml/entities.html#h-24.2.1).
+ *
+ * config.entities_latin = false;
+ *
+ * @cfg {Boolean} [entities_latin=true]
+ * @member CKEDITOR.config
+ */
+CKEDITOR.config.entities_latin = true;
+
+/**
+ * Whether to convert some symbols, mathematical symbols, and Greek letters to
+ * HTML entities. This may be more relevant for users typing text written in Greek.
+ * The list of entities can be found in the
+ * [W3C HTML 4.01 Specification, section 24.3.1(http://www.w3.org/TR/html4/sgml/entities.html#h-24.3.1).
+ *
+ * config.entities_greek = false;
+ *
+ * @cfg {Boolean} [entities_greek=true]
+ * @member CKEDITOR.config
+ */
+CKEDITOR.config.entities_greek = true;
+
+/**
+ * Whether to convert all remaining characters not included in the ASCII
+ * character table to their relative decimal numeric representation of HTML entity.
+ * When set to `force`, it will convert all entities into this format.
+ *
+ * For example the phrase `'This is Chinese: 汉语.'` is output
+ * as `'This is Chinese: 汉语.'`
+ *
+ * config.entities_processNumerical = true;
+ * config.entities_processNumerical = 'force'; // Converts from ' ' into ' ';
+ *
+ * @cfg {Boolean/String} [entities_processNumerical=false]
+ * @member CKEDITOR.config
+ */
+
+/**
+ * A comma separated list of additional entities to be used. Entity names
+ * or numbers must be used in a form that excludes the `'&'` prefix and the `';'` ending.
+ *
+ * config.entities_additional = '#1049'; // Adds Cyrillic capital letter Short I (Й).
+ *
+ * @cfg {String} [entities_additional='#39' (The single quote (') character)]
+ * @member CKEDITOR.config
+ */
+CKEDITOR.config.entities_additional = '#39';
diff --git a/lam/templates/lib/extra/ckeditor/plugins/fakeobjects/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/fakeobjects/plugin.js
new file mode 100644
index 00000000..7b81703f
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/fakeobjects/plugin.js
@@ -0,0 +1,178 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/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;
+ }
+ }
+ };
+
+ var plugin = CKEDITOR.plugins.add( 'fakeobjects', {
+ lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+
+ 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 );
+ }
+ } );
+
+ /**
+ * @member CKEDITOR.editor
+ * @todo
+ */
+ 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. (#8945)
+ if ( !CKEDITOR.env.hc )
+ attributes.src = CKEDITOR.getUrl( plugin.path + 'images/spacer.gif' );
+
+ 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 } );
+ };
+
+ /**
+ * @member CKEDITOR.editor
+ * @todo
+ */
+ 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. (#8945)
+ if ( !CKEDITOR.env.hc )
+ attributes.src = CKEDITOR.getUrl( plugin.path + 'images/spacer.gif' );
+
+ 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 );
+ };
+
+ /**
+ * @member CKEDITOR.editor
+ * @todo
+ */
+ 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;
+ };
+
+} )();
diff --git a/lam/templates/lib/extra/ckeditor/plugins/floatingspace/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/floatingspace/plugin.js
new file mode 100644
index 00000000..7434eb8f
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/floatingspace/plugin.js
@@ -0,0 +1,379 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+( function() {
+ var floatSpaceTpl = CKEDITOR.addTemplate( 'floatcontainer', '' +
+ ' {voiceLabel}' +
+ ' ' +
+ ' ' ),
+ 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 ) {
+ // #10112 Do not fail on editable-less editor.
+ if ( !( editable = editor.editable() ) )
+ return;
+
+ // Show up the space on focus gain.
+ evt && evt.name == 'focus' && floatSpace.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 =
+ ( editorRect.left > 0 && editorRect.right < viewRect.width && editorRect.width > spaceRect.width ) ?
+ ( editor.config.contentsLangDirection == 'rtl' ? 'right' : 'left' )
+ :
+ ( mid - editorRect.left > editorRect.right - mid ? 'left' : 'right' ),
+ offset;
+
+ // (#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;
+ }
+
+ // (#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.
+ // (#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 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.lang.editorPanel + ', ' + editor.name
+ } ) ) ),
+
+ // 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 float space and the editable left/right
+ * boundaries when space element is docked at 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 float space and the editable top/bottom
+ * boundaries when space element is docked at 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 float space and the view port boundaries
+ * when 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 float space and the view port boundaries
+ * when space element is pinned.
+ *
+ * config.floatSpacePinnedOffsetY = 20;
+ *
+ * @cfg {Number} [floatSpacePinnedOffsetY=0]
+ * @member CKEDITOR.config
+ */
diff --git a/lam/templates/lib/extra/ckeditor/plugins/floatpanel/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/floatpanel/plugin.js
new file mode 100644
index 00000000..6a48c554
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/floatpanel/plugin.js
@@ -0,0 +1,548 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/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's 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. (#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
+ };
+
+ editor.on( 'mode', hide );
+ editor.on( 'resize', hide );
+ // Window resize doesn't cause hide on blur. (#9800)
+ doc.getWindow().on( 'resize', hide );
+
+ // 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 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.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 );
+
+ var element = this.element,
+ iframe = this._.iframe,
+ // Non IE prefer the event into a window object.
+ focused = CKEDITOR.env.ie ? 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. (#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. (#3068, #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 ) {
+ // 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 );
+
+ 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. (#9355)
+ target.removeStyle( 'width' );
+
+ if ( block.autoSize ) {
+ var panelDoc = block.element.getDocument();
+ var width = ( CKEDITOR.env.webkit? 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
+ // (#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
+ // (#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. (#6146)
+ if ( CKEDITOR.env.ie ) {
+ 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 (#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();
+
+ // #10623, #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 );
+ this._.editor.fire( 'panelShow', this );
+ }, 0, this );
+ }, CKEDITOR.env.air ? 200 : 0, this );
+ this.visible = 1;
+
+ if ( this.onShow )
+ this.onShow.call( this );
+ },
+
+ /**
+ * Restores last focused element or simply focus 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.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.is( 'a' ) && ( this._.lastFocused = active );
+ },
+
+ /**
+ * Hides panel.
+ *
+ * @todo
+ */
+ hide: function( returnFocus ) {
+ if ( this.visible && ( !this.onHide || this.onHide.call( this ) !== true ) ) {
+ this.hideChild();
+ // Blur previously focused element. (#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. (#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._.editor.fire( 'panelHide', this );
+ }
+ },
+
+ /**
+ * @todo
+ */
+ allowBlur: function( allow ) // Prevent editor from hiding the panel. #3222.
+ {
+ var panel = this._.panel;
+ if ( allow != undefined )
+ panel.allowBlur = allow;
+
+ return panel.allowBlur;
+ },
+
+ /**
+ * Shows 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();
+
+ /* #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.(#4241)
+ if ( isLastInstance )
+ panel.destroy();
+ // Panel might be used by other instances, just hide them.(#4552)
+ else
+ panel.element.hide();
+ }
+ // Remove the registration.
+ isLastInstance && ( panels = {} );
+
+ } );
+} )();
diff --git a/lam/templates/lib/extra/ckeditor/plugins/font/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/font/plugin.js
new file mode 100644
index 00000000..0cfde310
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/font/plugin.js
@@ -0,0 +1,230 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+( function() {
+ function addCombo( editor, comboName, styleType, lang, entries, defaultLabel, styleDefinition, order ) {
+ var config = editor.config,
+ style = new CKEDITOR.style( styleDefinition );
+
+ // Gets the list of fonts from the settings.
+ var names = entries.split( ';' ),
+ values = [];
+
+ // Create style objects for all fonts.
+ var styles = {};
+ for ( var i = 0; i < names.length; i++ ) {
+ var parts = names[ i ];
+
+ if ( parts ) {
+ parts = parts.split( '/' );
+
+ var vars = {},
+ name = names[ i ] = parts[ 0 ];
+
+ vars[ styleType ] = values[ i ] = parts[ 1 ] || name;
+
+ styles[ name ] = new CKEDITOR.style( styleDefinition, vars );
+ styles[ name ]._.definition.name = name;
+ } else
+ names.splice( i--, 1 );
+ }
+
+ editor.ui.addRichCombo( comboName, {
+ label: lang.label,
+ title: lang.panelTitle,
+ toolbar: 'styles,' + order,
+ allowedContent: style,
+ requiredContent: style,
+
+ panel: {
+ css: [ CKEDITOR.skin.getPath( 'editor' ) ].concat( config.contentsCss ),
+ multiSelect: false,
+ attributes: { 'aria-label': lang.panelTitle }
+ },
+
+ init: function() {
+ this.startGroup( lang.panelTitle );
+
+ for ( var i = 0; i < names.length; i++ ) {
+ var name = names[ i ];
+
+ // Add the tag entry to the panel list.
+ this.add( name, styles[ name ].buildPreview(), name );
+ }
+ },
+
+ onClick: function( value ) {
+ editor.focus();
+ editor.fire( 'saveSnapshot' );
+
+ var style = styles[ value ];
+
+ editor[ this.getValue() == value ? 'removeStyle' : 'applyStyle' ]( style );
+ editor.fire( 'saveSnapshot' );
+ },
+
+ onRender: function() {
+ editor.on( 'selectionChange', function( ev ) {
+ var currentValue = this.getValue();
+
+ var elementPath = ev.data.path,
+ elements = elementPath.elements;
+
+ // For each element into the elements path.
+ for ( var i = 0, element; i < elements.length; i++ ) {
+ element = elements[ i ];
+
+ // Check if the element is removable by any of
+ // the styles.
+ for ( var value in styles ) {
+ if ( styles[ value ].checkElementMatch( element, true ) ) {
+ if ( value != currentValue )
+ this.setValue( value );
+ return;
+ }
+ }
+ }
+
+ // If no styles match, just empty it.
+ this.setValue( '', defaultLabel );
+ }, this );
+ },
+
+ refresh: function() {
+ if ( !editor.activeFilter.check( style ) )
+ this.setState( CKEDITOR.TRISTATE_DISABLED );
+ }
+ } );
+ }
+
+ CKEDITOR.plugins.add( 'font', {
+ requires: 'richcombo',
+ lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+ init: function( editor ) {
+ var config = editor.config;
+
+ addCombo( editor, 'Font', 'family', editor.lang.font, config.font_names, config.font_defaultLabel, config.font_style, 30 );
+ addCombo( editor, 'FontSize', 'size', editor.lang.font.fontSize, config.fontSize_sizes, config.fontSize_defaultLabel, config.fontSize_style, 40 );
+ }
+ } );
+} )();
+
+/**
+ * The list of fonts names to be displayed in the Font combo in the toolbar.
+ * Entries are separated by semi-colons (`';'`), while it's possible to have more
+ * than one font for each entry, in the HTML way (separated by comma).
+ *
+ * A display name may be optionally defined by prefixing the entries with the
+ * name and the slash character. For example, `'Arial/Arial, Helvetica, sans-serif'`
+ * will be displayed as `'Arial'` in the list, but will be outputted as
+ * `'Arial, Helvetica, sans-serif'`.
+ *
+ * config.font_names =
+ * 'Arial/Arial, Helvetica, sans-serif;' +
+ * 'Times New Roman/Times New Roman, Times, serif;' +
+ * 'Verdana';
+ *
+ * config.font_names = 'Arial;Times New Roman;Verdana';
+ *
+ * @cfg {String} [font_names=see source]
+ * @member CKEDITOR.config
+ */
+CKEDITOR.config.font_names = 'Arial/Arial, Helvetica, sans-serif;' +
+ 'Comic Sans MS/Comic Sans MS, cursive;' +
+ 'Courier New/Courier New, Courier, monospace;' +
+ 'Georgia/Georgia, serif;' +
+ 'Lucida Sans Unicode/Lucida Sans Unicode, Lucida Grande, sans-serif;' +
+ 'Tahoma/Tahoma, Geneva, sans-serif;' +
+ 'Times New Roman/Times New Roman, Times, serif;' +
+ 'Trebuchet MS/Trebuchet MS, Helvetica, sans-serif;' +
+ 'Verdana/Verdana, Geneva, sans-serif';
+
+/**
+ * The text to be displayed in the Font combo is none of the available values
+ * matches the current cursor position or text selection.
+ *
+ * // If the default site font is Arial, we may making it more explicit to the end user.
+ * config.font_defaultLabel = 'Arial';
+ *
+ * @cfg {String} [font_defaultLabel='']
+ * @member CKEDITOR.config
+ */
+CKEDITOR.config.font_defaultLabel = '';
+
+/**
+ * The style definition to be used to apply the font in the text.
+ *
+ * // This is actually the default value for it.
+ * config.font_style = {
+ * element: 'span',
+ * styles: { 'font-family': '#(family)' },
+ * overrides: [ { element: 'font', attributes: { 'face': null } } ]
+ * };
+ *
+ * @cfg {Object} [font_style=see example]
+ * @member CKEDITOR.config
+ */
+CKEDITOR.config.font_style = {
+ element: 'span',
+ styles: { 'font-family': '#(family)' },
+ overrides: [ {
+ element: 'font', attributes: { 'face': null }
+ } ]
+};
+
+/**
+ * The list of fonts size to be displayed in the Font Size combo in the
+ * toolbar. Entries are separated by semi-colons (`';'`).
+ *
+ * Any kind of "CSS like" size can be used, like `'12px'`, `'2.3em'`, `'130%'`,
+ * `'larger'` or `'x-small'`.
+ *
+ * A display name may be optionally defined by prefixing the entries with the
+ * name and the slash character. For example, `'Bigger Font/14px'` will be
+ * displayed as `'Bigger Font'` in the list, but will be outputted as `'14px'`.
+ *
+ * config.fontSize_sizes = '16/16px;24/24px;48/48px;';
+ *
+ * config.fontSize_sizes = '12px;2.3em;130%;larger;x-small';
+ *
+ * config.fontSize_sizes = '12 Pixels/12px;Big/2.3em;30 Percent More/130%;Bigger/larger;Very Small/x-small';
+ *
+ * @cfg {String} [fontSize_sizes=see source]
+ * @member CKEDITOR.config
+ */
+CKEDITOR.config.fontSize_sizes = '8/8px;9/9px;10/10px;11/11px;12/12px;14/14px;16/16px;18/18px;20/20px;22/22px;24/24px;26/26px;28/28px;36/36px;48/48px;72/72px';
+
+/**
+ * The text to be displayed in the Font Size combo is none of the available
+ * values matches the current cursor position or text selection.
+ *
+ * // If the default site font size is 12px, we may making it more explicit to the end user.
+ * config.fontSize_defaultLabel = '12px';
+ *
+ * @cfg {String} [fontSize_defaultLabel='']
+ * @member CKEDITOR.config
+ */
+CKEDITOR.config.fontSize_defaultLabel = '';
+
+/**
+ * The style definition to be used to apply the font size in the text.
+ *
+ * // This is actually the default value for it.
+ * config.fontSize_style = {
+ * element: 'span',
+ * styles: { 'font-size': '#(size)' },
+ * overrides: [ { element :'font', attributes: { 'size': null } } ]
+ * };
+ *
+ * @cfg {Object} [fontSize_style=see example]
+ * @member CKEDITOR.config
+ */
+CKEDITOR.config.fontSize_style = {
+ element: 'span',
+ styles: { 'font-size': '#(size)' },
+ overrides: [ {
+ element: 'font', attributes: { 'size': null }
+ } ]
+};
diff --git a/lam/templates/lib/extra/ckeditor/plugins/format/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/format/plugin.js
new file mode 100644
index 00000000..5d1377f4
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/format/plugin.js
@@ -0,0 +1,244 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+CKEDITOR.plugins.add( 'format', {
+ requires: 'richcombo',
+ lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+ 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();
+
+ editor[ style.checkActive( elementPath ) ? 'removeStyle' : 'applyStyle' ]( style );
+
+ // Save the undo snapshot after all changes are affected. (#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 ) ) {
+ 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 semi colon separated style names (by default tags) representing
+ * the style definition for each entry to be displayed in the Format combo in
+ * the toolbar. Each entry must have its relative definition configuration in a
+ * setting named `'format_(tagName)'`. For example, the `'p'` entry has its
+ * definition taken from `config.format_p`.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * config.format_h6 = { element: 'h6', attributes: { 'class': 'contentTitle6' } };
+ *
+ * @cfg {Object} [format_h6={ element: 'h6' }]
+ * @member CKEDITOR.config
+ */
+CKEDITOR.config.format_h6 = { element: 'h6' };
diff --git a/lam/templates/lib/extra/ckeditor/plugins/horizontalrule/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/horizontalrule/plugin.js
new file mode 100644
index 00000000..48b843e8
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/horizontalrule/plugin.js
@@ -0,0 +1,41 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/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, {
+ lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+ icons: 'horizontalrule', // %REMOVE_LINE_CORE%
+ hidpi: true, // %REMOVE_LINE_CORE%
+ 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'
+ } );
+ }
+ } );
+} )();
diff --git a/lam/templates/lib/extra/ckeditor/plugins/htmlwriter/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/htmlwriter/plugin.js
new file mode 100644
index 00000000..cc2827f3
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/htmlwriter/plugin.js
@@ -0,0 +1,359 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/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;
+ }
+} );
+
+/**
+ * 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 a htmlWriter class instance.
+ *
+ * @constructor
+ */
+ $: function() {
+ // Call the base contructor.
+ this.base();
+
+ /**
+ * The characters to be used for each identation 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. (#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 a 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' ) {
+ this.forceSimpleAmpersand && ( attValue = attValue.replace( /&/g, '&' ) );
+ // Browsers don't always escape special character in attribute values. (#4683, #4719).
+ attValue = CKEDITOR.tools.htmlEncodeAttr( attValue );
+ }
+
+ 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 chars. 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;
+ },
+
+ /**
+ * Sets formatting rules for a give element. The possible rules are:
+ *
+ * * `indent`: indent the element contents.
+ * * `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 call to the function overrides
+ * already present rules, leaving the undefined 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` 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 element name to which set the rules.
+ * @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 elements attributes
+ * values, it's 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 the HTML produced by the editor.
+ * Using characters different than `' '` (space) and `'\t'` (tab) is definitely
+ * a bad idea as it'll mess the code.
+ *
+ * // No indentation.
+ * CKEDITOR.config.dataIndentationChars = '';
+ *
+ * // Use two spaces for indentation.
+ * CKEDITOR.config.dataIndentationChars = ' ';
+ *
+ * @cfg {String} [dataIndentationChars='\t']
+ * @member CKEDITOR.config
+ */
diff --git a/lam/templates/lib/extra/ckeditor/plugins/iframe/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/iframe/plugin.js
new file mode 100644
index 00000000..f98ea0bf
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/iframe/plugin.js
@@ -0,0 +1,83 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+( function() {
+ CKEDITOR.plugins.add( 'iframe', {
+ requires: 'dialog,fakeobjects',
+ lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+ icons: 'iframe', // %REMOVE_LINE_CORE%
+ hidpi: true, // %REMOVE_LINE_CORE%
+ onLoad: function() {
+ CKEDITOR.addCss( 'img.cke_iframe' +
+ '{' +
+ 'background-image: url(' + CKEDITOR.getUrl( this.path + 'images/placeholder.png' ) + ');' +
+ 'background-position: center center;' +
+ 'background-repeat: no-repeat;' +
+ 'border: 1px solid #a9a9a9;' +
+ 'width: 80px;' +
+ 'height: 80px;' +
+ '}'
+ );
+ },
+ init: function( editor ) {
+ var pluginName = 'iframe',
+ lang = editor.lang.iframe,
+ allowed = 'iframe[align,longdesc,frameborder,height,name,scrolling,src,title,width]';
+
+ if ( editor.plugins.dialogadvtab )
+ allowed += ';iframe' + editor.plugins.dialogadvtab.allowedContent( { id: 1, classes: 1, styles: 1 } );
+
+ CKEDITOR.dialog.add( pluginName, this.path + 'dialogs/iframe.js' );
+ editor.addCommand( pluginName, new CKEDITOR.dialogCommand( pluginName, {
+ allowedContent: allowed,
+ requiredContent: 'iframe'
+ } ) );
+
+ editor.ui.addButton && editor.ui.addButton( 'Iframe', {
+ label: lang.toolbar,
+ command: pluginName,
+ toolbar: 'insert,80'
+ } );
+
+ editor.on( 'doubleclick', function( evt ) {
+ var element = evt.data.element;
+ if ( element.is( 'img' ) && element.data( 'cke-real-element-type' ) == 'iframe' )
+ evt.data.dialog = 'iframe';
+ } );
+
+ if ( editor.addMenuItems ) {
+ editor.addMenuItems( {
+ iframe: {
+ label: lang.title,
+ command: 'iframe',
+ group: 'image'
+ }
+ } );
+ }
+
+ // If the "contextmenu" plugin is loaded, register the listeners.
+ if ( editor.contextMenu ) {
+ editor.contextMenu.addListener( function( element, selection ) {
+ if ( element && element.is( 'img' ) && element.data( 'cke-real-element-type' ) == 'iframe' )
+ return { iframe: CKEDITOR.TRISTATE_OFF };
+ } );
+ }
+ },
+ afterInit: function( editor ) {
+ var dataProcessor = editor.dataProcessor,
+ dataFilter = dataProcessor && dataProcessor.dataFilter;
+
+ if ( dataFilter ) {
+ dataFilter.addRules( {
+ elements: {
+ iframe: function( element ) {
+ return editor.createFakeParserElement( element, 'cke_iframe', 'iframe', true );
+ }
+ }
+ } );
+ }
+ }
+ } );
+} )();
diff --git a/lam/templates/lib/extra/ckeditor/plugins/image/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/image/plugin.js
new file mode 100644
index 00000000..1c826a20
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/image/plugin.js
@@ -0,0 +1,170 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+/**
+ * @fileOverview Image plugin
+ */
+
+( function() {
+
+ CKEDITOR.plugins.add( 'image', {
+ requires: 'dialog',
+ lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+ icons: 'image', // %REMOVE_LINE_CORE%
+ hidpi: true, // %REMOVE_LINE_CORE%
+ init: function( editor ) {
+ // Abort when Image2 is to be loaded since both plugins
+ // share the same button, command, etc. names (#11222).
+ if ( editor.plugins.image2 )
+ return;
+
+ var pluginName = 'image';
+
+ // 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, selection ) {
+ 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 (#11222).
+ if ( editor.plugins.image2 )
+ return;
+
+ // Customize the behavior of the alignment commands. (#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;
+ }
+
+} )();
+
+/**
+ * Whether to remove links when emptying the link URL field in the image dialog.
+ *
+ * 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 preview area.
+ *
+ * config.image_previewText = CKEDITOR.tools.repeat( '___ ', 100 );
+ *
+ * @cfg {String} [image_previewText='Lorem ipsum dolor...' (placeholder text)]
+ * @member CKEDITOR.config
+ */
diff --git a/lam/templates/lib/extra/ckeditor/plugins/indent/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/indent/plugin.js
new file mode 100644
index 00000000..180d4ceb
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/indent/plugin.js
@@ -0,0 +1,461 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/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', {
+ lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+ icons: 'indent,indent-rtl,outdent,outdent-rtl', // %REMOVE_LINE_CORE%
+
+ 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( evt ) {
+ editor.forceNextSelectionCheck();
+ selection.selectBookmarks( bookmarks );
+ }, command, null, 100 );
+ }
+} )();
diff --git a/lam/templates/lib/extra/ckeditor/plugins/indentlist/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/indentlist/plugin.js
new file mode 100644
index 00000000..d6338ab4
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/indentlist/plugin.js
@@ -0,0 +1,298 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/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,
+ editable = editor;
+
+ // Register commands.
+ globalHelpers.registerCommands( editor, {
+ indentlist: new commandDefinition( editor, 'indentlist', true ),
+ outdentlist: new commandDefinition( editor, 'outdentlist' )
+ } );
+
+ function commandDefinition( editor, name ) {
+ 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 ) {
+ if ( editor.mode != 'wysiwyg' )
+ return;
+
+ if ( evt.data.keyCode == this.indentKey ) {
+ var list = this.getContext( editor.elementPath() );
+
+ 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 && firstItemInPath.call( this, editor.elementPath(), 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 = firstItemInPath.call( this, 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;
+
+ 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. (#5372)
+ if ( indentOffset > 0 ) {
+ var listRoot = listArray[ i ].parent;
+ 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.(#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. (#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(),
+ range;
+
+ while ( ( range = iterator.getNextRange() ) ) {
+ var rangeRoot = range.getCommonAncestor(),
+ nearestListBlock = rangeRoot;
+
+ while ( nearestListBlock && !( nearestListBlock.type == CKEDITOR.NODE_ELEMENT && context[ nearestListBlock.getName() ] ) )
+ 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. (#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;
+ }
+
+ // Check whether a first child of a list is in the path.
+ // The list can be extracted from path or given explicitly
+ // e.g. for better performance if cached.
+ function firstItemInPath( path, list ) {
+ if ( !list )
+ list = path.contains( this.context );
+
+ return list && path.block && path.block.equals( list.getFirst( listItem ) );
+ }
+
+ // 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 );
+ }
+} )();
diff --git a/lam/templates/lib/extra/ckeditor/plugins/justify/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/justify/plugin.js
new file mode 100644
index 00000000..6626c915
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/justify/plugin.js
@@ -0,0 +1,241 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/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;
+
+ 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 ) {
+ // Append the desired class name.
+ if ( apply )
+ block.addClass( cssClassName );
+ else if ( !className )
+ block.removeAttribute( 'class' );
+ } else if ( apply )
+ block.setStyle( 'text-align', this.value );
+ }
+
+ }
+
+ editor.focus();
+ editor.forceNextSelectionCheck();
+ selection.selectBookmarks( bookmarks );
+ },
+
+ refresh: function( editor, path ) {
+ var firstBlock = path.block || path.blockLimit;
+
+ this.setState( firstBlock.getName() != 'body' && getAlignment( firstBlock, this.editor.config.useComputedState ) == this.value ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF );
+ }
+ };
+
+ CKEDITOR.plugins.add( 'justify', {
+ lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+ icons: 'justifyblock,justifycenter,justifyleft,justifyright', // %REMOVE_LINE_CORE%
+ hidpi: true, // %REMOVE_LINE_CORE%
+ 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.justify.left,
+ command: 'justifyleft',
+ toolbar: 'align,10'
+ } );
+ editor.ui.addButton( 'JustifyCenter', {
+ label: editor.lang.justify.center,
+ command: 'justifycenter',
+ toolbar: 'align,20'
+ } );
+ editor.ui.addButton( 'JustifyRight', {
+ label: editor.lang.justify.right,
+ command: 'justifyright',
+ toolbar: 'align,30'
+ } );
+ editor.ui.addButton( 'JustifyBlock', {
+ label: editor.lang.justify.block,
+ 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
+ */
diff --git a/lam/templates/lib/extra/ckeditor/plugins/link/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/link/plugin.js
new file mode 100644
index 00000000..cdcb9878
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/link/plugin.js
@@ -0,0 +1,422 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+CKEDITOR.plugins.add( 'link', {
+ requires: 'dialog,fakeobjects',
+ lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+ icons: 'anchor,anchor-rtl,link,unlink', // %REMOVE_LINE_CORE%
+ hidpi: true, // %REMOVE_LINE_CORE%
+ 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;' +
+ '}' +
+ ( CKEDITOR.plugins.link.synAnchorSelector ? ( 'a.cke_anchor_empty' +
+ '{' +
+ // Make empty anchor selectable on IE.
+ 'display:inline-block;' +
+ // IE11 doesn't display empty inline-block elements.
+ ( CKEDITOR.env.ie && CKEDITOR.env.version > 10 ? 'min-height:16px;vertical-align:middle' : '' ) +
+ '}'
+ ) : '' ) +
+ '.%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:' + ( CKEDITOR.env.opera ? 'middle' : '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]{*}(*)' );
+ 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' );
+
+ 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 ) {
+ var element = CKEDITOR.plugins.link.getSelectedLink( editor ) || evt.data.element;
+
+ if ( !element.isReadOnly() ) {
+ if ( element.is( 'a' ) ) {
+ evt.data.dialog = ( element.getAttribute( 'name' ) && ( !element.getAttribute( 'href' ) || !element.getChildCount() ) ) ? 'anchor' : 'link';
+ editor.getSelection().selectElement( element );
+ } else if ( CKEDITOR.plugins.link.tryRestoreFakeAnchor( editor, element ) )
+ evt.data.dialog = 'anchor';
+ }
+ } );
+
+ // 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, selection ) {
+ 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;
+ } );
+ }
+ },
+
+ afterInit: function( editor ) {
+ // Register a filter to displaying placeholders after mode change.
+
+ var dataProcessor = editor.dataProcessor,
+ dataFilter = dataProcessor && dataProcessor.dataFilter,
+ htmlFilter = dataProcessor && dataProcessor.htmlFilter,
+ pathFilters = editor._.elementsPath && editor._.elementsPath.filters;
+
+ if ( dataFilter ) {
+ dataFilter.addRules( {
+ elements: {
+ a: function( element ) {
+ var attributes = element.attributes;
+ if ( !attributes.name )
+ return null;
+
+ var isEmpty = !element.children.length;
+
+ if ( CKEDITOR.plugins.link.synAnchorSelector ) {
+ // IE needs a specific class name to be applied
+ // to the anchors, for appropriate styling.
+ var ieClass = isEmpty ? 'cke_anchor_empty' : 'cke_anchor';
+ var cls = attributes[ 'class' ];
+ if ( attributes.name && ( !cls || cls.indexOf( ieClass ) < 0 ) )
+ attributes[ 'class' ] = ( cls || '' ) + ' ' + ieClass;
+
+ if ( isEmpty && CKEDITOR.plugins.link.emptyAnchorFix ) {
+ attributes.contenteditable = 'false';
+ attributes[ 'data-cke-editable' ] = 1;
+ }
+ } else if ( CKEDITOR.plugins.link.fakeAnchor && isEmpty )
+ return editor.createFakeParserElement( element, 'cke_anchor', 'anchor' );
+
+ return null;
+ }
+ }
+ } );
+ }
+
+ if ( CKEDITOR.plugins.link.emptyAnchorFix && htmlFilter ) {
+ htmlFilter.addRules( {
+ elements: {
+ a: function( element ) {
+ delete element.attributes.contenteditable;
+ }
+ }
+ } );
+ }
+
+ 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';
+
+ }
+ } );
+ }
+ }
+} );
+
+/**
+ * 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
+ */
+ getSelectedLink: function( editor ) {
+ var selection = editor.getSelection();
+ var selectedElement = selection.getSelectedElement();
+ if ( selectedElement && selectedElement.is( 'a' ) )
+ return selectedElement;
+
+ var range = selection.getRanges()[ 0 ];
+
+ if ( range ) {
+ range.shrink( CKEDITOR.SHRINK_TEXT );
+ return editor.elementPath( range.getCommonAncestor() ).contains( 'a', 1 );
+ }
+ return 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 (#11359).
+ scope = ( editable.isInline() && !editor.plugins.divarea ) ? editor.document : editable,
+
+ links = scope.getElementsByTag( 'a' ),
+ 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.
+ if ( this.fakeAnchor ) {
+ var imgs = scope.getElementsByTag( 'img' );
+
+ 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
+ * @property {Boolean}
+ */
+ fakeAnchor: CKEDITOR.env.opera || CKEDITOR.env.webkit,
+
+ /**
+ * For browsers that do not support CSS3 `a[name]:empty()`. Note that IE9 is included because of #7783.
+ *
+ * @readonly
+ * @property {Boolean}
+ */
+ synAnchorSelector: CKEDITOR.env.ie,
+
+ /**
+ * For browsers that have editing issues with an empty anchor.
+ *
+ * @readonly
+ * @property {Boolean}
+ */
+ emptyAnchorFix: CKEDITOR.env.ie && CKEDITOR.env.version < 8,
+
+ /**
+ * 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;
+ }
+ }
+};
+
+// TODO Much probably there's no need to expose these as public objects.
+
+CKEDITOR.unlinkCommand = function() {};
+CKEDITOR.unlinkCommand.prototype = {
+ exec: function( editor ) {
+ var style = new CKEDITOR.style( { element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1 } );
+ editor.removeStyle( style );
+ },
+
+ 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]'
+};
+
+CKEDITOR.removeAnchorCommand = function() {};
+CKEDITOR.removeAnchorCommand.prototype = {
+ exec: function( editor ) {
+ var sel = editor.getSelection(),
+ bms = sel.createBookmarks(),
+ anchor;
+ if ( sel && ( anchor = sel.getSelectedElement() ) && ( CKEDITOR.plugins.link.fakeAnchor && !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
+} );
diff --git a/lam/templates/lib/extra/ckeditor/plugins/list/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/list/plugin.js
new file mode 100644
index 00000000..3ddb2ca8
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/list/plugin.js
@@ -0,0 +1,1031 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+/**
+ * @fileOverview Insert and remove numbered and bulleted lists.
+ */
+
+( function() {
+ var listNodeNames = { ol: 1, ul: 1 },
+ emptyTextRegex = /^[\n\r\t ]*$/;
+
+ 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' );
+ }
+ }
+
+ // Inheirt inline styles from another element.
+ function inheirtInlineStyles( 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. (#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 (#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. (#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 );
+
+ inheirtInlineStyles( 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(),
+ listRoot = listArray[ 0 ].parent;
+
+ 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. (#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.(#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 (
+ // #5335
+ block.is( 'pre' ) ||
+ // #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. (#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 );
+ }
+ }
+
+ listCommand.prototype = {
+ exec: function( editor ) {
+ // Run state check first of all.
+ this.refresh( editor, editor.elementPath() );
+
+ var doc = editor.document,
+ 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(#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.
+ if ( block.getCustomData( 'list_block' ) )
+ continue;
+ else
+ CKEDITOR.dom.element.setMarker( database, block, 'list_block', 1 );
+
+ var path = editor.elementPath( block ),
+ pathElements = path.elements,
+ pathElementsCount = pathElements.length,
+ listNode = null,
+ 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-- ) {
+ if ( listNodeNames[ element.getName() ] && blockLimit.contains( element ) ) // Don't leak outside block limit (#3940).
+ {
+ // 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 #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 ( 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() ] )
+ 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();
+ },
+
+ 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 );
+ }
+ };
+
+ var dtd = CKEDITOR.dtd;
+ var tailNbspRegex = /[\t\r\n ]*(?: |\xa0)$/;
+
+ // Merge list adjacent, of same type lists.
+ function mergeListSiblings( listNode )
+ {
+ var mergeSibling;
+ ( mergeSibling = function( 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.(#3820)
+ mergeChildren( listNode, sibling, null, !rtl );
+
+ listNode.remove();
+ listNode = sibling;
+ }
+ } )();
+ mergeSibling( 1 );
+ }
+
+ function indexOfFirstChildElement( element, tagNameList ) {
+ var child,
+ children = element.children,
+ length = children.length;
+
+ for ( var i = 0; i < length; i++ ) {
+ child = children[ i ];
+ if ( child.name && ( child.name in tagNameList ) )
+ return i;
+ }
+
+ return length;
+ }
+
+ // 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 (#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. (#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', {
+ lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+ icons: 'bulletedlist,bulletedlist-rtl,numberedlist,numberedlist-rtl', // %REMOVE_LINE_CORE%
+ hidpi: true, // %REMOVE_LINE_CORE%
+ 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. (#8248,#9080)
+ editor.on( 'key', function( evt ) {
+ var key = evt.data.keyCode;
+
+ // 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 );
+ }
+ }
+
+ 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. (#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 = 1;
+
+
+ if ( isAtEnd && next ) {
+ // Put cursor range there.
+ nextLine = range.clone();
+ nextLine.moveToElementEditStart( next );
+
+ 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 ); } );
+ }
+ } );
+ }
+ } );
+} )();
diff --git a/lam/templates/lib/extra/ckeditor/plugins/listblock/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/listblock/plugin.js
new file mode 100644
index 00000000..9c398493
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/listblock/plugin.js
@@ -0,0 +1,240 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/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 (#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 );
+ },
+
+ 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 );
+ }
+ }
+ } );
+ }
+} );
diff --git a/lam/templates/lib/extra/ckeditor/plugins/magicline/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/magicline/plugin.js
new file mode 100644
index 00000000..e4c6a944
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/magicline/plugin.js
@@ -0,0 +1,1838 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+/**
+ * @fileOverview Allows accessing difficult focus spaces.
+ */
+
+'use strict';
+
+( function() {
+ CKEDITOR.plugins.add( 'magicline', {
+ lang: 'ar,bg,ca,cs,cy,de,el,en,en-gb,eo,es,et,eu,fa,fi,fr,fr-ca,gl,he,hr,hu,id,it,ja,km,ko,ku,lv,nb,nl,no,pl,pt,pt-br,ru,si,sk,sl,sq,sv,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+ 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, checkMouseTimeout, checkMouseTimer;
+
+ // %REMOVE_START%
+ // Internal DEBUG uses tools located in the topmost window.
+
+ // (#9701) Due to security limitations some browsers may throw
+ // errors when accessing window.top object. Do it safely first then.
+ try {
+ that.debug = window.top.DEBUG;
+ }
+ catch ( e ) {}
+
+ that.debug = that.debug || {
+ groupEnd: function() {},
+ groupStart: function() {},
+ log: function() {},
+ logElements: function() {},
+ logElementsEnd: function() {},
+ logEnd: function() {},
+ mousePos: function() {},
+ showHidden: function() {},
+ showTrigger: function() {},
+ startTimer: function() {},
+ stopTimer: function() {}
+ };
+ // %REMOVE_END%
+
+ // 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,
+ listener;
+
+ // 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( event ) {
+ that.hiddenMode = 0;
+ that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
+ } );
+
+ editable.attachListener( editable, 'keydown', function( event ) {
+ if ( editor.mode != 'wysiwyg' )
+ return;
+
+ var keyStroke = event.data.getKeystroke(),
+ selection = editor.getSelection(),
+ selected = selection.getStartElement();
+
+ switch ( keyStroke ) {
+ // Shift pressed
+ case 2228240: // IE
+ case 16:
+ that.hiddenMode = 1;
+ that.line.detach();
+ }
+
+ that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
+ } );
+
+ // 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( event ) {
+ 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;
+ that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
+ }, 50 );
+
+ that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
+ }
+ } );
+
+ // 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( event ) {
+ if ( editor.mode != 'wysiwyg' )
+ return;
+
+ that.line.detach();
+ that.hiddenMode = 1;
+ that.mouseDown = 1;
+
+ that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
+ } );
+
+ // 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( event ) {
+ that.hiddenMode = 0;
+ that.mouseDown = 0;
+ that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
+ } );
+
+ // 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( event ) {
+ var elements, element, i;
+
+ for ( var t in { p: 1, br: 1, div: 1 } ) {
+ // document.find is not available in QM (#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.debug.groupStart( 'CheckMouse' ); // %REMOVE_LINE%
+ that.debug.startTimer(); // %REMOVE_LINE%
+
+ 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();
+ }
+
+ that.debug.showTrigger( that.trigger ); // %REMOVE_LINE%
+ that.debug.mousePos( mouse.y, that.element ); // %REMOVE_LINE%
+
+ checkMouseTimeoutPending = false;
+ }
+
+ that.debug.stopTimer(); // %REMOVE_LINE%
+ that.debug.groupEnd(); // %REMOVE_LINE%
+ }
+
+ // This one allows testing and debugging. It reveals some
+ // inner methods to the world.
+ this.backdoor = {
+ 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 ) {
+ return new CKEDITOR.dom.element( doc.$.elementFromPoint( mouse.x, mouse.y ) );
+ }
+
+ return function( that, ignoreBox, forceMouse ) {
+ if ( !that.mouse )
+ return null;
+
+ var doc = that.doc,
+ lineWrap = that.line.wrap,
+ mouse = forceMouse || that.mouse,
+ 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.
+ var limit = getClosestEditableLimit( trigger, true );
+
+ // 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;
+
+ return trigger;
+ } 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 = this.path + 'images/' + ( env.hidpi ? 'hidpi/' : '' ) + 'icon.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;' + CKEDITOR.tools.cssVendorPrefix( 'border-radius', '2px', 1 ),
+ 'top:-17px;' + CKEDITOR.tools.cssVendorPrefix( 'border-radius', '2px 2px 0px 0px', 1 ),
+ 'top:-1px;' + CKEDITOR.tools.cssVendorPrefix( 'border-radius', '0px 0px 2px 2px', 1 )
+ ]
+ } ),
+ 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() {
+ that.debug.groupStart( 'mouseNear' ); // %REMOVE_LINE%
+
+ 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 ) ) {
+ that.debug.logEnd( 'Mouse is near.' ); // %REMOVE_LINE%
+ return true;
+ }
+
+ that.debug.logEnd( 'Mouse isn\'t near.' ); // %REMOVE_LINE%
+ 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. (#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.
+ that.lastCmdDirection === !!insertAfter; // Command is executed in the same direction.
+
+ 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;
+
+ // (#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 ) {
+ that.debug.groupStart( 'triggerEditable' ); // %REMOVE_LINE%
+
+ 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 ) {
+ that.debug.logEnd( 'ABORT. No edge node found.' ); // %REMOVE_LINE%
+ 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 ) ) {
+ that.debug.logEnd( 'ABORT. Invalid edge node.' ); // %REMOVE_LINE%
+ 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;
+
+ that.debug.logEnd( 'SUCCESS. Created box trigger. EDGE_TOP.' ); // %REMOVE_LINE%
+
+ 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;
+
+ that.debug.logEnd( 'SUCCESS. Created box trigger. EDGE_BOTTOM.' ); // %REMOVE_LINE%
+
+ return new boxTrigger( [ edgeNode, null,
+ EDGE_BOTTOM,
+ TYPE_EDGE,
+ triggerLook
+ ] );
+ }
+
+ that.debug.logEnd( 'ABORT. No trigger created.' ); // %REMOVE_LINE%
+ 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 ) {
+ that.debug.groupStart( 'triggerEdge' ); // %REMOVE_LINE%
+
+ var mouse = that.mouse,
+ view = that.view,
+ triggerOffset = that.triggerOffset;
+
+ // Get the ascendant trigger basing on elementFromMouse.
+ var element = getAscendantTrigger( that );
+
+ that.debug.logElements( [ element ], [ 'Ascendant trigger' ], 'First stage' ); // %REMOVE_LINE%
+
+ // Abort if there's no appropriate element.
+ if ( !element ) {
+ that.debug.logEnd( 'ABORT. No element, element is editable or element contains editable.' ); // %REMOVE_LINE%
+ 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 {
+ that.debug.logEnd( 'ABORT. Not around of any edge.' ); // %REMOVE_LINE%
+ 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 ) ) {
+ that.debug.logEnd( 'ABORT. element is wrong', element ); // %REMOVE_LINE%
+ 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
+ ] );
+
+ that.debug.log( 'Configured edge trigger of ' + ( bottomTrigger ? 'EDGE_BOTTOM' : 'EDGE_TOP' ) ); // %REMOVE_LINE%
+ }
+
+ // Abort. Sibling is a text element.
+ else if ( isTextNode( elementSibling ) ) {
+ that.debug.logEnd( 'ABORT. Sibling is non-empty text element' ); // %REMOVE_LINE%
+ 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 ) ) {
+ that.debug.logEnd( 'ABORT. elementSibling is wrong', elementSibling ); // %REMOVE_LINE%
+ return null;
+ }
+
+ // Prepare a trigger.
+ triggerSetup = [ elementSibling, element ][ bottomTrigger ? 'reverse' : 'concat' ]().concat( [
+ EDGE_MIDDLE,
+ TYPE_EDGE
+ ] );
+
+ that.debug.log( 'Configured edge trigger of EDGE_MIDDLE' ); // %REMOVE_LINE%
+ }
+
+ if ( 0 in triggerSetup ) {
+ that.debug.logEnd( 'SUCCESS. Returning a trigger.' ); // %REMOVE_LINE%
+ return new boxTrigger( triggerSetup );
+ }
+
+ that.debug.logEnd( 'ABORT. No trigger generated.' ); // %REMOVE_LINE%
+ 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 ) {
+ that.debug.groupStart( 'expandEngine' ); // %REMOVE_LINE%
+
+ var startElement = that.element,
+ upper, lower, trigger;
+
+ if ( !isHtml( startElement ) || startElement.contains( that.editable ) ) {
+ that.debug.logEnd( 'ABORT. No start element, or start element contains editable.' ); // %REMOVE_LINE%
+ 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;
+
+ that.debug.logElements( [ upper, lower ], [ 'Upper', 'Lower' ], 'Pair found' ); // %REMOVE_LINE%
+
+ // Success: two siblings have been found
+ if ( areSiblings( that, upper, lower ) ) {
+ that.debug.logEnd( 'SUCCESS. Expand trigger created.' ); // %REMOVE_LINE%
+ return trigger.set( EDGE_MIDDLE, TYPE_EXPAND );
+ }
+
+ that.debug.logElements( [ startElement, upper, lower ], // %REMOVE_LINE%
+ [ 'Start', 'Upper', 'Lower' ], 'Post-processing' ); // %REMOVE_LINE%
+
+ // 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 ) {
+ that.debug.logEnd( 'ABORT. There is no upper or no lower element.' ); // %REMOVE_LINE%
+ return null;
+ }
+
+ // 2.1.
+ updateSize( that, upper );
+ updateSize( that, lower );
+
+ if ( !checkMouseBetweenElements( that, upper, lower ) ) {
+ that.debug.logEnd( 'ABORT. Mouse is already above upper or below lower.' ); // %REMOVE_LINE%
+ 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 );
+ }
+
+ that.debug.logElements( [ minElement, minElementNext ], // %REMOVE_LINE%
+ [ 'Min', 'MinNext' ], 'Post-processing results' ); // %REMOVE_LINE%
+
+ // 3.4.
+ if ( !minElement || !minElementNext ) {
+ that.debug.logEnd( 'ABORT. No Min or MinNext' ); // %REMOVE_LINE%
+ return null;
+ }
+
+ if ( !checkMouseBetweenElements( that, minElement, minElementNext ) ) {
+ that.debug.logEnd( 'ABORT. Mouse is already above minElement or below minElementNext.' ); // %REMOVE_LINE%
+ 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.
+ that.debug.logEnd( 'SUCCESSFUL post-processing. Trigger created.' ); // %REMOVE_LINE%
+ 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 ) {
+ that.debug.groupStart( 'expandFilter' ); // %REMOVE_LINE%
+
+ 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.
+ that.debug.logEnd( 'REJECTED. No upper or no lower or they contain each other.' ); // %REMOVE_LINE%
+
+ return false;
+ }
+
+ // YES: two trigger elements, pure siblings.
+ else if ( isTrigger( that, upper ) && isTrigger( that, lower ) && areSiblings( that, upper, lower ) ) {
+ that.debug.logElementsEnd( [ upper, lower ], // %REMOVE_LINE%
+ [ 'upper', 'lower' ], 'APPROVED EDGE_MIDDLE' ); // %REMOVE_LINE%
+
+ return true;
+ }
+
+ that.debug.logElementsEnd( [ upper, lower ], // %REMOVE_LINE%
+ [ 'upper', 'lower' ], 'Rejected unknown pair' ); // %REMOVE_LINE%
+
+ return false;
+ }
+
+ // Simple wrapper for expandEngine and expandFilter.
+ return function( that ) {
+ that.debug.groupStart( 'triggerExpand' ); // %REMOVE_LINE%
+
+ var trigger = expandEngine( that );
+
+ that.debug.groupEnd(); // %REMOVE_LINE%
+ 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 getStyle = ( function() {
+ // Better "cache and reuse" than "call again and again".
+ var computed = env.ie ? element.$.currentStyle : that.win.$.getComputedStyle( element.$, '' );
+
+ return env.ie ?
+ function( propertyName ) {
+ return computed[ CKEDITOR.tools.cssStyleToDomStyle( propertyName ) ];
+ } : function( propertyName ) {
+ return computed.getPropertyValue( propertyName );
+ };
+ } )(),
+ 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 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 ) {
+ that.debug.log( 'element.size: get from cache' ); // %REMOVE_LINE%
+ return null;
+ }
+
+ that.debug.log( 'element.size: capture' ); // %REMOVE_LINE%
+
+ 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 ) {
+ that.debug.log( 'win.size: get from cache' ); // %REMOVE_LINE%
+ return;
+ }
+
+ that.debug.log( 'win.size: capturing' ); // %REMOVE_LINE%
+
+ 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 element edge and mouse pointer that
+ * causes the box to appear. The distance is expressed in pixels (px).
+ *
+ * // 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 mouse pointer and the box, within
+ * which the box stays revealed and no other focus space is offered to be accessed.
+ * The value is relative to {@link #magicline_triggerOffset}.
+ *
+ * // 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 default keystroke that access the closest unreachable focus space **before**
+ * the caret (start of the selection). If there's no any focus space, selection remains.
+ *
+ * // Changes 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 default keystroke that access the closest unreachable focus space **after**
+ * the caret (start of the selection). If there's no any focus space, selection remains.
+ *
+ * // 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 magicline from being
+ * used within these elements.
+ *
+ * // Adds "data-tabu" attribute to magicline tabu list.
+ * CKEDITOR.config.magicline_tabuList = [ 'data-tabu' ];
+ *
+ * @cfg {Number} [magicline_tabuList=[ 'data-widget-wrapper' ]]
+ * @member CKEDITOR.config
+ */
+
+/**
+ * Defines box color. The color may be adjusted to enhance readability.
+ *
+ * // Changes color to blue.
+ * CKEDITOR.config.magicline_color = '#0000FF';
+ *
+ * @cfg {String} [magicline_color='#FF0000']
+ * @member CKEDITOR.config
+ */
+
+/**
+ * Activates plugin mode that considers all focus spaces between
+ * {@link CKEDITOR.dtd#$block} elements as accessible by the box.
+ *
+ * // Enables "put everywhere" mode.
+ * CKEDITOR.config.magicline_putEverywhere = true;
+ *
+ * @cfg {Boolean} [magicline_putEverywhere=false]
+ * @member CKEDITOR.config
+ */
diff --git a/lam/templates/lib/extra/ckeditor/plugins/maximize/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/maximize/plugin.js
new file mode 100644
index 00000000..9e0d174a
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/maximize/plugin.js
@@ -0,0 +1,305 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/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. (#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 (#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. (#5560)
+ body.setAttribute( 'contentEditable', false );
+ body.setAttribute( 'contentEditable', true );
+ }
+ }
+
+ if ( editor.editable().hasFocus ) {
+ editor.toolbox.focus();
+ editor.focus();
+ }
+ }
+
+ CKEDITOR.plugins.add( 'maximize', {
+ lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+ icons: 'maximize', // %REMOVE_LINE_CORE%
+ hidpi: true, // %REMOVE_LINE_CORE%
+ 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 (#8307).
+ modes: { wysiwyg: !CKEDITOR.env.iOS, source: !CKEDITOR.env.iOS },
+ readOnly: 1,
+ editorFocus: false,
+ exec: function() {
+ var container = editor.container.getChild( 1 );
+ 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 ];
+ }
+
+ if ( this.state == CKEDITOR.TRISTATE_OFF ) // Go fullscreen if the state is 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', // #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 - #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 (#7284)
+ container.setStyle( 'position', CKEDITOR.env.gecko && CKEDITOR.env.quirks ? 'fixed' : 'absolute' );
+ container.$.offsetLeft; // SAFARI BUG: See #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 (#5580)
+ container.addClass( 'cke_maximized' );
+
+ resizeHandler();
+
+ // Still not top left? Fix it. (Bug #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. (#5149)
+ CKEDITOR.env.gecko && refreshCursor( editor );
+
+ } else if ( this.state == CKEDITOR.TRISTATE_ON ) // Restore from fullscreen if the state is 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. (#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' );
+ }
+
+ this.toggleState();
+
+ // Toggle button label.
+ var button = this.uiItems[ 0 ];
+ // Only try to change the button if it exists (#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 + '");' );
+ }
+
+ // Restore selection and scroll position in editing area.
+ if ( editor.mode == 'wysiwyg' ) {
+ if ( savedSelection ) {
+ // Fixing positioning editor chrome in Firefox break design mode. (#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 (#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}.
+ */
diff --git a/lam/templates/lib/extra/ckeditor/plugins/menu/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/menu/plugin.js
new file mode 100644
index 00000000..dffa7839
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/menu/plugin.js
@@ -0,0 +1,545 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/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 array to the editor context menu.
+ *
+ * @method
+ * @param {Array} definitions List of definitions for each menu item as if {@link #addMenuItem} is called.
+ * @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 menuItemTpl = CKEDITOR.addTemplate( 'menuItem', menuItemSource ),
+ menuArrowTpl = CKEDITOR.addTemplate( 'menuArrow', menuArrowSource );
+
+ /**
+ * @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. (#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. (#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 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 (#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( index ) {
+ 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 );
+
+ editor.fire( 'menuShow', [ panel ] );
+ },
+
+ /**
+ * 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 );
+ }
+ }
+ } );
+
+ 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 = '';
+
+ 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;
+
+ 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,
+ href: 'javascript:void(\'' + ( this.label || '' ).replace( "'" + '' ) + '\')',
+ 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 ),
+ 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';
diff --git a/lam/templates/lib/extra/ckeditor/plugins/panel/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/panel/plugin.js
new file mode 100644
index 00000000..fd00edf2
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/panel/plugin.js
@@ -0,0 +1,402 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+( function() {
+ CKEDITOR.plugins.add( 'panel', {
+ beforeInit: function( editor ) {
+ editor.ui.addHandler( CKEDITOR.UI_PANEL, CKEDITOR.ui.panel.handler );
+ }
+ } );
+
+ /**
+ * Panel UI element.
+ *
+ * @readonly
+ * @property {String} [='panel']
+ * @member CKEDITOR
+ */
+ CKEDITOR.UI_PANEL = 'panel';
+
+ /**
+ * @class
+ * @constructor Creates a panel class instance.
+ * @param {CKEDITOR.dom.document} document
+ * @param {Object} definition
+ */
+ CKEDITOR.ui.panel = function( document, definition ) {
+ // Copy all definition properties to this object.
+ if ( definition )
+ CKEDITOR.tools.extend( this, definition );
+
+ // Set defaults.
+ CKEDITOR.tools.extend( this, {
+ className: '',
+ css: []
+ } );
+
+ this.id = CKEDITOR.tools.getNextId();
+ this.document = document;
+ this.isFramed = this.forceIFrame || this.css.length;
+
+ this._ = {
+ blocks: {}
+ };
+ };
+
+ /**
+ * Represents panel handler object.
+ *
+ * @class
+ * @singleton
+ * @extends CKEDITOR.ui.handlerDefinition
+ */
+ CKEDITOR.ui.panel.handler = {
+ /**
+ * Transforms a panel definition in a {@link CKEDITOR.ui.panel} instance.
+ *
+ * @param {Object} definition
+ * @returns {CKEDITOR.ui.panel}
+ */
+ create: function( definition ) {
+ return new CKEDITOR.ui.panel( definition );
+ }
+ };
+
+ var panelTpl = CKEDITOR.addTemplate( 'panel', ' ' +
+ '{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 ) {
+ 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. (#8308)
+ CKEDITOR.env.iOS && parentDiv.setStyles( {
+ 'overflow': 'scroll',
+ '-webkit-overflow-scrolling': 'touch'
+ } );
+
+ var onLoad = CKEDITOR.tools.addFunction( CKEDITOR.tools.bind( function( ev ) {
+ 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 (#4534).
+ doc.on( 'key' + ( CKEDITOR.env.opera ? 'press' : 'down' ), function( evt ) {
+ var keystroke = evt.data.getKeystroke(),
+ dir = this.document.getById( this.id ).getAttribute( 'dir' );
+
+ // Delegate key processing to block.
+ if ( this._.onKeyDown && this._.onKeyDown( keystroke ) === false ) {
+ evt.data.preventDefault();
+ return;
+ }
+
+ // ESC/ARROW-LEFT(ltr) OR ARROW-RIGHT(rtl)
+ if ( keystroke == 27 || keystroke == ( dir == 'rtl' ? 39 : 37 ) ) {
+ if ( this.onEscape && this.onEscape( keystroke ) === false )
+ evt.data.preventDefault();
+ }
+ }, this );
+
+ holder = doc.getBody();
+ holder.unselectable();
+ CKEDITOR.env.air && CKEDITOR.tools.callFunction( onLoad );
+ } else
+ holder = this.document.getById( this.id );
+
+ this._.holder = holder;
+ }
+
+ return holder;
+ };
+
+ var data = {
+ editorId: editor.id,
+ id: this.id,
+ langCode: editor.langCode,
+ dir: editor.lang.dir,
+ cls: this.className,
+ frame: '',
+ env: CKEDITOR.env.cssClass,
+ 'z-index': editor.config.baseFloatZIndex + 1
+ };
+
+ if ( this.isFramed ) {
+ // With IE, the custom domain has to be taken care at first,
+ // for other browers, the 'src' attribute should be left empty to
+ // trigger iframe's 'load' event.
+ var src =
+ CKEDITOR.env.air ? 'javascript:void(0)' :
+ CKEDITOR.env.ie ? 'javascript:void(function(){' + encodeURIComponent(
+ '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. (#8864)
+ var holder = !this.forceIFrame || CKEDITOR.env.ie ? this._.holder : this.document.getById( this.id + '_frame' );
+
+ if ( current )
+ current.hide();
+
+ this._.currentBlock = block;
+
+ CKEDITOR.fire( 'ariaWidget', holder );
+
+ // Reset the focus index, so it will always go into the first one.
+ block._.focusIndex = -1;
+
+ this._.onKeyDown = block.onKeyDown && CKEDITOR.tools.bind( block.onKeyDown, block );
+
+ block.show();
+
+ return block;
+ },
+
+ /**
+ * @todo
+ */
+ destroy: function() {
+ this.element && this.element.remove();
+ }
+ };
+
+ /**
+ * @class
+ *
+ * @todo class and all methods
+ */
+ CKEDITOR.ui.panel.block = CKEDITOR.tools.createClass( {
+ /**
+ * Creates a block class instances.
+ *
+ * @constructor
+ * @todo
+ */
+ $: function( blockHolder, blockDefinition ) {
+ this.element = blockHolder.append( blockHolder.getDocument().createElement( 'div', {
+ attributes: {
+ 'tabindex': -1,
+ 'class': 'cke_panel_block'
+ },
+ styles: {
+ display: 'none'
+ }
+ } ) );
+
+ // Copy all definition properties to this object.
+ if ( blockDefinition )
+ CKEDITOR.tools.extend( this, blockDefinition );
+
+ // Set the a11y attributes of this element ...
+ this.element.setAttributes( {
+ 'role': this.attributes.role || 'presentation',
+ 'aria-label': this.attributes[ 'aria-label' ],
+ 'title': this.attributes.title || this.attributes[ 'aria-label' ]
+ } );
+
+ this.keys = {};
+
+ this._.focusIndex = -1;
+
+ // Disable context menu for panels.
+ this.element.disableContextMenu();
+ },
+
+ _: {
+
+ /**
+ * Mark the item specified by the index as current activated.
+ */
+ markItem: function( index ) {
+ if ( index == -1 )
+ return;
+ var links = this.element.getElementsByTag( 'a' );
+ var item = links.getItem( this._.focusIndex = index );
+
+ // Safari need focus on the iframe window first(#3389), but we need
+ // lock the blur to avoid hiding the panel.
+ if ( CKEDITOR.env.webkit || CKEDITOR.env.opera )
+ item.getDocument().getWindow().focus();
+ item.focus();
+
+ this.onMark && this.onMark( item );
+ }
+ },
+
+ proto: {
+ show: function() {
+ this.element.setStyle( 'display', '' );
+ },
+
+ hide: function() {
+ if ( !this.onHide || this.onHide.call( this ) !== true )
+ this.element.setStyle( 'display', 'none' );
+ },
+
+ onKeyDown: function( keystroke, noCycle ) {
+ var keyAction = this.keys[ keystroke ];
+ switch ( keyAction ) {
+ // Move forward.
+ case 'next':
+ var index = this._.focusIndex,
+ links = this.element.getElementsByTag( 'a' ),
+ link;
+
+ while ( ( link = links.getItem( ++index ) ) ) {
+ // Move the focus only if the element is marked with
+ // the _cke_focus and it it's visible (check if it has
+ // width).
+ if ( link.getAttribute( '_cke_focus' ) && link.$.offsetWidth ) {
+ this._.focusIndex = index;
+ link.focus();
+ break;
+ }
+ }
+
+ // If no link was found, cycle and restart from the top. (#11125)
+ if ( !link && !noCycle ) {
+ this._.focusIndex = -1;
+ return this.onKeyDown( keystroke, 1 );
+ }
+
+ return false;
+
+ // Move backward.
+ case 'prev':
+ index = this._.focusIndex;
+ links = this.element.getElementsByTag( 'a' );
+
+ while ( index > 0 && ( link = links.getItem( --index ) ) ) {
+ // Move the focus only if the element is marked with
+ // the _cke_focus and it it's visible (check if it has
+ // width).
+ if ( link.getAttribute( '_cke_focus' ) && link.$.offsetWidth ) {
+ this._.focusIndex = index;
+ link.focus();
+ break;
+ }
+
+ // Make sure link is null when the loop ends and nothing was
+ // found (#11125).
+ link = null;
+ }
+
+ // If no link was found, cycle and restart from the bottom. (#11125)
+ if ( !link && !noCycle ) {
+ this._.focusIndex = links.count();
+ return this.onKeyDown( keystroke, 1 );
+ }
+
+ return false;
+
+ case 'click':
+ case 'mouseup':
+ index = this._.focusIndex;
+ link = index >= 0 && this.element.getElementsByTag( 'a' ).getItem( index );
+
+ if ( link )
+ link.$[ keyAction ] ? link.$[ keyAction ]() : link.$[ 'on' + keyAction ]();
+
+ return false;
+ }
+
+ return true;
+ }
+ }
+ } );
+
+} )();
+
+/**
+ * Fired when a panel is added to the document.
+ *
+ * @event ariaWidget
+ * @member CKEDITOR
+ * @param {Object} data The element wrapping the panel.
+ */
diff --git a/lam/templates/lib/extra/ckeditor/plugins/panelbutton/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/panelbutton/plugin.js
new file mode 100644
index 00000000..67a31237
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/panelbutton/plugin.js
@@ -0,0 +1,138 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/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 = true;
+
+ 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';
diff --git a/lam/templates/lib/extra/ckeditor/plugins/removeformat/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/removeformat/plugin.js
new file mode 100644
index 00000000..ef3c4d73
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/removeformat/plugin.js
@@ -0,0 +1,174 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+CKEDITOR.plugins.add( 'removeformat', {
+ lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+ icons: 'removeformat', // %REMOVE_LINE_CORE%
+ hidpi: true, // %REMOVE_LINE_CORE%
+ 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( ',' ) );
+
+ var filter = CKEDITOR.plugins.removeformat.filter;
+ var ranges = editor.getSelection().getRanges( 1 ),
+ iterator = ranges.createIterator(),
+ range;
+
+ while ( ( range = iterator.getNextRange() ) ) {
+ 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;
+
+ // Cache the next node to be processed. Do it now, because
+ // currentNode may be removed.
+ var nextNode = currentNode.getNextSourceNode( false, CKEDITOR.NODE_ELEMENT );
+
+ // This node must not be a fake element.
+ if ( !( currentNode.getName() == 'img' && currentNode.data( 'cke-realelement' ) ) && 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;
+ }
+ }
+
+ range.moveToBookmark( bookmark );
+ }
+
+ // The selection path may not changed, but we should force a selection
+ // change event to refresh command states, due to the above attribution change. (#9238)
+ editor.forceNextSelectionCheck();
+ editor.getSelection().selectRanges( ranges );
+ }
+ }
+ },
+
+ // 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
+ * @member CKEDITOR.editor
+ * @param {Function} func The function to be called, which will be passed a {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,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.
+ */
diff --git a/lam/templates/lib/extra/ckeditor/plugins/richcombo/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/richcombo/plugin.js
new file mode 100644
index 00000000..8ea5c12a
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/richcombo/plugin.js
@@ -0,0 +1,441 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/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}' +
+ '= 10900 && !CKEDITOR.env.hc ? '' : '" href="javascript:void(\'{titleJs}\')"' ) +
+ ' hidefocus="true"' +
+ ' role="button"' +
+ ' aria-labelledby="{id}_label"' +
+ ' aria-haspopup="true"';
+
+ // Some browsers don't cancel key events in the keydown but in the
+ // keypress.
+ // TODO: Check if really needed for Gecko+Mac.
+ if ( CKEDITOR.env.opera || ( CKEDITOR.env.gecko && CKEDITOR.env.mac ) )
+ template += ' 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 )
+ template += ' onblur="this.style.cssText = this.style.cssText;"';
+
+ template +=
+ ' onkeydown="return CKEDITOR.tools.callFunction({keydownFn},event,this);"' +
+ ' onmousedown="return CKEDITOR.tools.callFunction({mousedownFn},event);" ' +
+ ' onfocus="return CKEDITOR.tools.callFunction({focusFn},event);" ' +
+ ( CKEDITOR.env.ie ? 'onclick="return false;" onmouseup' : 'onclick' ) + // #188
+ '="CKEDITOR.tools.callFunction({clickFn},this);return false;">' +
+ '{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: {}
+ };
+ },
+
+ proto: {
+ renderHtml: function( editor ) {
+ var output = [];
+ this.render( editor, output );
+ return output.join( '' );
+ },
+
+ /**
+ * 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 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() {
+ 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.
+ editor.on( 'activeFilterChange', updateState, this );
+ editor.on( 'mode', updateState, this );
+ editor.on( 'selectionChange', updateState, this );
+ // If this combo is sensitive to readOnly state, update it accordingly.
+ !this.readOnly && editor.on( 'readOnly', updateState, this );
+
+ var keyDownFn = CKEDITOR.tools.addFunction( function( ev, element ) {
+ ev = new CKEDITOR.dom.event( ev );
+
+ var keystroke = ev.getKeystroke();
+
+ // ARROW-DOWN
+ // This call is duplicated in plugins/toolbar/plugin.js in itemKeystroke().
+ // Move focus to the first element after drop down was opened by the arrow down key.
+ if ( keystroke == 40 ) {
+ editor.once( 'panelShow', function( evt ) {
+ evt.data._.panel._.currentBlock.onKeyDown( 40 );
+ } );
+ }
+
+ 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;
+ var mouseDownFn = CKEDITOR.tools.addFunction( function() {
+ // Opera: lock to prevent loosing editable text selection when clicking on button.
+ if ( CKEDITOR.env.opera ) {
+ var edt = editor.editable();
+ if ( edt.isInline() && edt.hasFocus ) {
+ editor.lockSelection();
+ selLocked = 1;
+ }
+ }
+ } );
+
+ // 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.version >= 10900 && !env.hc ? '' : ( this.title || '' ).replace( "'", '' ),
+ keydownFn: keyDownFn,
+ mousedownFn: mouseDownFn,
+ 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();
+
+ // The "panelShow" event is fired assinchronously, after the
+ // onShow method call.
+ editor.once( 'panelShow', function() {
+ list.focus( !list.multiSelect && me.getValue() );
+ } );
+ };
+
+ 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 );
+ }
+ }
+ },
+
+ /**
+ * Represents richCombo handler object.
+ *
+ * @class CKEDITOR.ui.richCombo.handler
+ * @singleton
+ * @extends CKEDITOR.ui.handlerDefinition
+ */
+ statics: {
+ handler: {
+ /**
+ * Transforms a richCombo definition in 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 );
+ };
+
+} )();
diff --git a/lam/templates/lib/extra/ckeditor/plugins/showborders/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/showborders/plugin.js
new file mode 100644
index 00000000..940d2c19
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/showborders/plugin.js
@@ -0,0 +1,174 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+/**
+ * @fileOverview The "show border" plugin. The command display visible outline
+ * border line around all table elements if table doesn't have a none-zero 'border' attribute specified.
+ */
+
+( function() {
+ var commandDefinition = {
+ preserveState: true,
+ editorFocus: false,
+ readOnly: 1,
+
+ exec: function( editor ) {
+ this.toggleState();
+ this.refresh( editor );
+ },
+
+ refresh: function( editor ) {
+ if ( editor.document ) {
+ var funcName = ( this.state == CKEDITOR.TRISTATE_ON ) ? 'attachClass' : 'removeClass';
+ editor.editable()[ funcName ]( 'cke_show_borders' );
+ }
+ }
+ };
+
+ var showBorderClassName = 'cke_show_border';
+
+ CKEDITOR.plugins.add( 'showborders', {
+ modes: { 'wysiwyg': 1 },
+
+ onLoad: function() {
+ var cssStyleText,
+ cssTemplate =
+ // TODO: For IE6, we don't have child selector support,
+ // where nested table cells could be incorrect.
+ ( CKEDITOR.env.ie6Compat ? [
+ '.%1 table.%2,',
+ '.%1 table.%2 td, .%1 table.%2 th',
+ '{',
+ 'border : #d3d3d3 1px dotted',
+ '}'
+ ] : [
+ '.%1 table.%2,',
+ '.%1 table.%2 > tr > td, .%1 table.%2 > tr > th,',
+ '.%1 table.%2 > tbody > tr > td, .%1 table.%2 > tbody > tr > th,',
+ '.%1 table.%2 > thead > tr > td, .%1 table.%2 > thead > tr > th,',
+ '.%1 table.%2 > tfoot > tr > td, .%1 table.%2 > tfoot > tr > th',
+ '{',
+ 'border : #d3d3d3 1px dotted',
+ '}'
+ ] ).join( '' );
+
+ cssStyleText = cssTemplate.replace( /%2/g, showBorderClassName ).replace( /%1/g, 'cke_show_borders ' );
+
+ CKEDITOR.addCss( cssStyleText );
+ },
+
+ init: function( editor ) {
+
+ var command = editor.addCommand( 'showborders', commandDefinition );
+ command.canUndo = false;
+
+ if ( editor.config.startupShowBorders !== false )
+ command.setState( CKEDITOR.TRISTATE_ON );
+
+ // Refresh the command on setData.
+ editor.on( 'mode', function() {
+ if ( command.state != CKEDITOR.TRISTATE_DISABLED )
+ command.refresh( editor );
+ }, null, null, 100 );
+
+ // Refresh the command on wysiwyg frame reloads.
+ editor.on( 'contentDom', function() {
+ if ( command.state != CKEDITOR.TRISTATE_DISABLED )
+ command.refresh( editor );
+ } );
+
+ editor.on( 'removeFormatCleanup', function( evt ) {
+ var element = evt.data;
+ if ( editor.getCommand( 'showborders' ).state == CKEDITOR.TRISTATE_ON && element.is( 'table' ) && ( !element.hasAttribute( 'border' ) || parseInt( element.getAttribute( 'border' ), 10 ) <= 0 ) )
+ element.addClass( showBorderClassName );
+ } );
+ },
+
+ afterInit: function( editor ) {
+ var dataProcessor = editor.dataProcessor,
+ dataFilter = dataProcessor && dataProcessor.dataFilter,
+ htmlFilter = dataProcessor && dataProcessor.htmlFilter;
+
+ if ( dataFilter ) {
+ dataFilter.addRules( {
+ elements: {
+ 'table': function( element ) {
+ var attributes = element.attributes,
+ cssClass = attributes[ 'class' ],
+ border = parseInt( attributes.border, 10 );
+
+ if ( ( !border || border <= 0 ) && ( !cssClass || cssClass.indexOf( showBorderClassName ) == -1 ) )
+ attributes[ 'class' ] = ( cssClass || '' ) + ' ' + showBorderClassName;
+ }
+ }
+ } );
+ }
+
+ if ( htmlFilter ) {
+ htmlFilter.addRules( {
+ elements: {
+ 'table': function( table ) {
+ var attributes = table.attributes,
+ cssClass = attributes[ 'class' ];
+
+ cssClass && ( attributes[ 'class' ] = cssClass.replace( showBorderClassName, '' ).replace( /\s{2}/, ' ' ).replace( /^\s+|\s+$/, '' ) );
+ }
+ }
+ } );
+ }
+ }
+ } );
+
+ // Table dialog must be aware of it.
+ CKEDITOR.on( 'dialogDefinition', function( ev ) {
+ var dialogName = ev.data.name;
+
+ if ( dialogName == 'table' || dialogName == 'tableProperties' ) {
+ var dialogDefinition = ev.data.definition,
+ infoTab = dialogDefinition.getContents( 'info' ),
+ borderField = infoTab.get( 'txtBorder' ),
+ originalCommit = borderField.commit;
+
+ borderField.commit = CKEDITOR.tools.override( originalCommit, function( org ) {
+ return function( data, selectedTable ) {
+ org.apply( this, arguments );
+ var value = parseInt( this.getValue(), 10 );
+ selectedTable[ ( !value || value <= 0 ) ? 'addClass' : 'removeClass' ]( showBorderClassName );
+ };
+ } );
+
+ var advTab = dialogDefinition.getContents( 'advanced' ),
+ classField = advTab && advTab.get( 'advCSSClasses' );
+
+ if ( classField ) {
+ classField.setup = CKEDITOR.tools.override( classField.setup, function( originalSetup ) {
+ return function() {
+ originalSetup.apply( this, arguments );
+ this.setValue( this.getValue().replace( /cke_show_border/, '' ) );
+ };
+ } );
+
+ classField.commit = CKEDITOR.tools.override( classField.commit, function( originalCommit ) {
+ return function( data, element ) {
+ originalCommit.apply( this, arguments );
+
+ if ( !parseInt( element.getAttribute( 'border' ), 10 ) )
+ element.addClass( 'cke_show_border' );
+ };
+ } );
+ }
+ }
+ } );
+
+} )();
+
+/**
+ * Whether to automatically enable the "show borders" command when the editor loads.
+ *
+ * config.startupShowBorders = false;
+ *
+ * @cfg {Boolean} [startupShowBorders=true]
+ * @member CKEDITOR.config
+ */
diff --git a/lam/templates/lib/extra/ckeditor/plugins/sourcearea/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/sourcearea/plugin.js
new file mode 100644
index 00000000..6d7747ae
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/sourcearea/plugin.js
@@ -0,0 +1,154 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+/**
+ * @fileOverview The "sourcearea" plugin. It registers the "source" editing
+ * mode, which displays the raw data being edited in the editor.
+ */
+
+( function() {
+ CKEDITOR.plugins.add( 'sourcearea', {
+ lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+ icons: 'source,source-rtl', // %REMOVE_LINE_CORE%
+ hidpi: true, // %REMOVE_LINE_CORE%
+ init: function( editor ) {
+ // Source mode isn't available in inline mode yet.
+ 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', true ) || startNode.getAscendant( 'th', true );
+ if ( nearestCell )
+ retval.push( nearestCell );
+ } else {
+ var walker = new CKEDITOR.dom.walker( range );
+ var node;
+ walker.guard = moveOutOfCellGuard;
+
+ while ( ( node = walker.next() ) ) {
+ // If may be possible for us to have a range like this:
+ // ^1 | ^2 |
+ // The 2nd td shouldn't be included.
+ //
+ // So we have to take care to include a td we've entered only when we've
+ // walked into its children.
+
+ if ( node.type != CKEDITOR.NODE_ELEMENT || !node.is( CKEDITOR.dtd.table ) ) {
+ var parent = node.getAscendant( 'td', true ) || node.getAscendant( 'th', true );
+ if ( parent && !parent.getCustomData( 'selected_cell' ) ) {
+ CKEDITOR.dom.element.setMarker( database, parent, 'selected_cell', true );
+ retval.push( parent );
+ }
+ }
+ }
+ }
+ }
+
+ CKEDITOR.dom.element.clearAllMarkers( database );
+
+ return retval;
+ }
+
+ function getFocusElementAfterDelCells( cellsToDelete ) {
+ var i = 0,
+ last = cellsToDelete.length - 1,
+ database = {},
+ cell, focusedCell, tr;
+
+ while ( ( cell = cellsToDelete[ i++ ] ) )
+ CKEDITOR.dom.element.setMarker( database, cell, 'delete_cell', true );
+
+ // 1.first we check left or right side focusable cell row by row;
+ i = 0;
+ while ( ( cell = cellsToDelete[ i++ ] ) ) {
+ if ( ( focusedCell = cell.getPrevious() ) && !focusedCell.getCustomData( 'delete_cell' ) || ( focusedCell = cell.getNext() ) && !focusedCell.getCustomData( 'delete_cell' ) ) {
+ CKEDITOR.dom.element.clearAllMarkers( database );
+ return focusedCell;
+ }
+ }
+
+ CKEDITOR.dom.element.clearAllMarkers( database );
+
+ // 2. then we check the toppest row (outside the selection area square) focusable cell
+ tr = cellsToDelete[ 0 ].getParent();
+ if ( ( tr = tr.getPrevious() ) )
+ return tr.getLast();
+
+ // 3. last we check the lowerest row focusable cell
+ tr = cellsToDelete[ last ].getParent();
+ if ( ( tr = tr.getNext() ) )
+ return tr.getChild( 0 );
+
+ return null;
+ }
+
+ function insertRow( selection, insertBefore ) {
+ var cells = getSelectedCells( selection ),
+ firstCell = cells[ 0 ],
+ table = firstCell.getAscendant( 'table' ),
+ doc = firstCell.getDocument(),
+ startRow = cells[ 0 ].getParent(),
+ startRowIndex = startRow.$.rowIndex,
+ lastCell = cells[ cells.length - 1 ],
+ endRowIndex = lastCell.getParent().$.rowIndex + lastCell.$.rowSpan - 1,
+ endRow = new CKEDITOR.dom.element( table.$.rows[ endRowIndex ] ),
+ rowIndex = insertBefore ? startRowIndex : endRowIndex,
+ row = insertBefore ? startRow : endRow;
+
+ var map = CKEDITOR.tools.buildTableMap( table ),
+ cloneRow = map[ rowIndex ],
+ nextRow = insertBefore ? map[ rowIndex - 1 ] : map[ rowIndex + 1 ],
+ width = map[ 0 ].length;
+
+ var newRow = doc.createElement( 'tr' );
+ for ( var i = 0; cloneRow[ i ] && i < width; i++ ) {
+ var cell;
+ // Check whether there's a spanning row here, do not break it.
+ if ( cloneRow[ i ].rowSpan > 1 && nextRow && cloneRow[ i ] == nextRow[ i ] ) {
+ cell = cloneRow[ i ];
+ cell.rowSpan += 1;
+ } else {
+ cell = new CKEDITOR.dom.element( cloneRow[ i ] ).clone();
+ cell.removeAttribute( 'rowSpan' );
+ cell.appendBogus();
+ newRow.append( cell );
+ cell = cell.$;
+ }
+
+ i += cell.colSpan - 1;
+ }
+
+ insertBefore ? newRow.insertBefore( row ) : newRow.insertAfter( row );
+ }
+
+ function deleteRows( selectionOrRow ) {
+ if ( selectionOrRow instanceof CKEDITOR.dom.selection ) {
+ var cells = getSelectedCells( selectionOrRow ),
+ firstCell = cells[ 0 ],
+ table = firstCell.getAscendant( 'table' ),
+ map = CKEDITOR.tools.buildTableMap( table ),
+ startRow = cells[ 0 ].getParent(),
+ startRowIndex = startRow.$.rowIndex,
+ lastCell = cells[ cells.length - 1 ],
+ endRowIndex = lastCell.getParent().$.rowIndex + lastCell.$.rowSpan - 1,
+ rowsToDelete = [];
+
+ // Delete cell or reduce cell spans by checking through the table map.
+ for ( var i = startRowIndex; i <= endRowIndex; i++ ) {
+ var mapRow = map[ i ],
+ row = new CKEDITOR.dom.element( table.$.rows[ i ] );
+
+ for ( var j = 0; j < mapRow.length; j++ ) {
+ var cell = new CKEDITOR.dom.element( mapRow[ j ] ),
+ cellRowIndex = cell.getParent().$.rowIndex;
+
+ if ( cell.$.rowSpan == 1 )
+ cell.remove();
+ // Row spanned cell.
+ else {
+ // Span row of the cell, reduce spanning.
+ cell.$.rowSpan -= 1;
+ // Root row of the cell, root cell to next row.
+ if ( cellRowIndex == i ) {
+ var nextMapRow = map[ i + 1 ];
+ nextMapRow[ j - 1 ] ? cell.insertAfter( new CKEDITOR.dom.element( nextMapRow[ j - 1 ] ) ) : new CKEDITOR.dom.element( table.$.rows[ i + 1 ] ).append( cell, 1 );
+ }
+ }
+
+ j += cell.$.colSpan - 1;
+ }
+
+ rowsToDelete.push( row );
+ }
+
+ var rows = table.$.rows;
+
+ // Where to put the cursor after rows been deleted?
+ // 1. Into next sibling row if any;
+ // 2. Into previous sibling row if any;
+ // 3. Into table's parent element if it's the very last row.
+ var cursorPosition = new CKEDITOR.dom.element( rows[ endRowIndex + 1 ] || ( startRowIndex > 0 ? rows[ startRowIndex - 1 ] : null ) || table.$.parentNode );
+
+ for ( i = rowsToDelete.length; i >= 0; i-- )
+ deleteRows( rowsToDelete[ i ] );
+
+ return cursorPosition;
+ } else if ( selectionOrRow instanceof CKEDITOR.dom.element ) {
+ table = selectionOrRow.getAscendant( 'table' );
+
+ if ( table.$.rows.length == 1 )
+ table.remove();
+ else
+ selectionOrRow.remove();
+ }
+
+ return null;
+ }
+
+ function getCellColIndex( cell, isStart ) {
+ var row = cell.getParent(),
+ rowCells = row.$.cells;
+
+ var colIndex = 0;
+ for ( var i = 0; i < rowCells.length; i++ ) {
+ var mapCell = rowCells[ i ];
+ colIndex += isStart ? 1 : mapCell.colSpan;
+ if ( mapCell == cell.$ )
+ break;
+ }
+
+ return colIndex - 1;
+ }
+
+ function getColumnsIndices( cells, isStart ) {
+ var retval = isStart ? Infinity : 0;
+ for ( var i = 0; i < cells.length; i++ ) {
+ var colIndex = getCellColIndex( cells[ i ], isStart );
+ if ( isStart ? colIndex < retval : colIndex > retval )
+ retval = colIndex;
+ }
+ return retval;
+ }
+
+ function insertColumn( selection, insertBefore ) {
+ var cells = getSelectedCells( selection ),
+ firstCell = cells[ 0 ],
+ table = firstCell.getAscendant( 'table' ),
+ startCol = getColumnsIndices( cells, 1 ),
+ lastCol = getColumnsIndices( cells ),
+ colIndex = insertBefore ? startCol : lastCol;
+
+ var map = CKEDITOR.tools.buildTableMap( table ),
+ cloneCol = [],
+ nextCol = [],
+ height = map.length;
+
+ for ( var i = 0; i < height; i++ ) {
+ cloneCol.push( map[ i ][ colIndex ] );
+ var nextCell = insertBefore ? map[ i ][ colIndex - 1 ] : map[ i ][ colIndex + 1 ];
+ nextCol.push( nextCell );
+ }
+
+ for ( i = 0; i < height; i++ ) {
+ var cell;
+
+ if ( !cloneCol[ i ] )
+ continue;
+
+ // Check whether there's a spanning column here, do not break it.
+ if ( cloneCol[ i ].colSpan > 1 && nextCol[ i ] == cloneCol[ i ] ) {
+ cell = cloneCol[ i ];
+ cell.colSpan += 1;
+ } else {
+ cell = new CKEDITOR.dom.element( cloneCol[ i ] ).clone();
+ cell.removeAttribute( 'colSpan' );
+ cell.appendBogus();
+ cell[ insertBefore ? 'insertBefore' : 'insertAfter' ].call( cell, new CKEDITOR.dom.element( cloneCol[ i ] ) );
+ cell = cell.$;
+ }
+
+ i += cell.rowSpan - 1;
+ }
+ }
+
+ function deleteColumns( selectionOrCell ) {
+ var cells = getSelectedCells( selectionOrCell ),
+ firstCell = cells[ 0 ],
+ lastCell = cells[ cells.length - 1 ],
+ table = firstCell.getAscendant( 'table' ),
+ map = CKEDITOR.tools.buildTableMap( table ),
+ startColIndex, endColIndex,
+ rowsToDelete = [];
+
+ // Figure out selected cells' column indices.
+ for ( var i = 0, rows = map.length; i < rows; i++ ) {
+ for ( var j = 0, cols = map[ i ].length; j < cols; j++ ) {
+ if ( map[ i ][ j ] == firstCell.$ )
+ startColIndex = j;
+ if ( map[ i ][ j ] == lastCell.$ )
+ endColIndex = j;
+ }
+ }
+
+ // Delete cell or reduce cell spans by checking through the table map.
+ for ( i = startColIndex; i <= endColIndex; i++ ) {
+ for ( j = 0; j < map.length; j++ ) {
+ var mapRow = map[ j ],
+ row = new CKEDITOR.dom.element( table.$.rows[ j ] ),
+ cell = new CKEDITOR.dom.element( mapRow[ i ] );
+
+ if ( cell.$ ) {
+ if ( cell.$.colSpan == 1 )
+ cell.remove();
+ // Reduce the col spans.
+ else
+ cell.$.colSpan -= 1;
+
+ j += cell.$.rowSpan - 1;
+
+ if ( !row.$.cells.length )
+ rowsToDelete.push( row );
+ }
+ }
+ }
+
+ var firstRowCells = table.$.rows[ 0 ] && table.$.rows[ 0 ].cells;
+
+ // Where to put the cursor after columns been deleted?
+ // 1. Into next cell of the first row if any;
+ // 2. Into previous cell of the first row if any;
+ // 3. Into table's parent element;
+ var cursorPosition = new CKEDITOR.dom.element( firstRowCells[ startColIndex ] || ( startColIndex ? firstRowCells[ startColIndex - 1 ] : table.$.parentNode ) );
+
+ // Delete table rows only if all columns are gone (do not remove empty row).
+ if ( rowsToDelete.length == rows )
+ table.remove();
+
+ return cursorPosition;
+ }
+
+ function getFocusElementAfterDelCols( cells ) {
+ var cellIndexList = [],
+ table = cells[ 0 ] && cells[ 0 ].getAscendant( 'table' ),
+ i, length, targetIndex, targetCell;
+
+ // get the cellIndex list of delete cells
+ for ( i = 0, length = cells.length; i < length; i++ )
+ cellIndexList.push( cells[ i ].$.cellIndex );
+
+ // get the focusable column index
+ cellIndexList.sort();
+ for ( i = 1, length = cellIndexList.length; i < length; i++ ) {
+ if ( cellIndexList[ i ] - cellIndexList[ i - 1 ] > 1 ) {
+ targetIndex = cellIndexList[ i - 1 ] + 1;
+ break;
+ }
+ }
+
+ if ( !targetIndex )
+ targetIndex = cellIndexList[ 0 ] > 0 ? ( cellIndexList[ 0 ] - 1 ) : ( cellIndexList[ cellIndexList.length - 1 ] + 1 );
+
+ // scan row by row to get the target cell
+ var rows = table.$.rows;
+ for ( i = 0, length = rows.length; i < length; i++ ) {
+ targetCell = rows[ i ].cells[ targetIndex ];
+ if ( targetCell )
+ break;
+ }
+
+ return targetCell ? new CKEDITOR.dom.element( targetCell ) : table.getPrevious();
+ }
+
+ function insertCell( selection, insertBefore ) {
+ var startElement = selection.getStartElement();
+ var cell = startElement.getAscendant( 'td', 1 ) || startElement.getAscendant( 'th', 1 );
+
+ if ( !cell )
+ return;
+
+ // Create the new cell element to be added.
+ var newCell = cell.clone();
+ newCell.appendBogus();
+
+ if ( insertBefore )
+ newCell.insertBefore( cell );
+ else
+ newCell.insertAfter( cell );
+ }
+
+ function deleteCells( selectionOrCell ) {
+ if ( selectionOrCell instanceof CKEDITOR.dom.selection ) {
+ var cellsToDelete = getSelectedCells( selectionOrCell );
+ var table = cellsToDelete[ 0 ] && cellsToDelete[ 0 ].getAscendant( 'table' );
+ var cellToFocus = getFocusElementAfterDelCells( cellsToDelete );
+
+ for ( var i = cellsToDelete.length - 1; i >= 0; i-- )
+ deleteCells( cellsToDelete[ i ] );
+
+ if ( cellToFocus )
+ placeCursorInCell( cellToFocus, true );
+ else if ( table )
+ table.remove();
+ } else if ( selectionOrCell instanceof CKEDITOR.dom.element ) {
+ var tr = selectionOrCell.getParent();
+ if ( tr.getChildCount() == 1 )
+ tr.remove();
+ else
+ selectionOrCell.remove();
+ }
+ }
+
+ // Remove filler at end and empty spaces around the cell content.
+ function trimCell( cell ) {
+ var bogus = cell.getBogus();
+ bogus && bogus.remove();
+ cell.trim();
+ }
+
+ function placeCursorInCell( cell, placeAtEnd ) {
+ var docInner = cell.getDocument(),
+ docOuter = CKEDITOR.document;
+
+ // Fixing "Unspecified error" thrown in IE10 by resetting
+ // selection the dirty and shameful way (#10308).
+ // We can not apply this hack to IE8 because
+ // it causes error (#11058).
+ if ( CKEDITOR.env.ie && CKEDITOR.env.version == 10 ) {
+ docOuter.focus();
+ docInner.focus();
+ }
+
+ var range = new CKEDITOR.dom.range( docInner );
+ if ( !range[ 'moveToElementEdit' + ( placeAtEnd ? 'End' : 'Start' ) ]( cell ) ) {
+ range.selectNodeContents( cell );
+ range.collapse( placeAtEnd ? false : true );
+ }
+ range.select( true );
+ }
+
+ function cellInRow( tableMap, rowIndex, cell ) {
+ var oRow = tableMap[ rowIndex ];
+ if ( typeof cell == 'undefined' )
+ return oRow;
+
+ for ( var c = 0; oRow && c < oRow.length; c++ ) {
+ if ( cell.is && oRow[ c ] == cell.$ )
+ return c;
+ else if ( c == cell )
+ return new CKEDITOR.dom.element( oRow[ c ] );
+ }
+ return cell.is ? -1 : null;
+ }
+
+ function cellInCol( tableMap, colIndex ) {
+ var oCol = [];
+ for ( var r = 0; r < tableMap.length; r++ ) {
+ var row = tableMap[ r ];
+ oCol.push( row[ colIndex ] );
+
+ // Avoid adding duplicate cells.
+ if ( row[ colIndex ].rowSpan > 1 )
+ r += row[ colIndex ].rowSpan - 1;
+ }
+ return oCol;
+ }
+
+ function mergeCells( selection, mergeDirection, isDetect ) {
+ var cells = getSelectedCells( selection );
+
+ // Invalid merge request if:
+ // 1. In batch mode despite that less than two selected.
+ // 2. In solo mode while not exactly only one selected.
+ // 3. Cells distributed in different table groups (e.g. from both thead and tbody).
+ var commonAncestor;
+ if ( ( mergeDirection ? cells.length != 1 : cells.length < 2 ) || ( commonAncestor = selection.getCommonAncestor() ) && commonAncestor.type == CKEDITOR.NODE_ELEMENT && commonAncestor.is( 'table' ) )
+ return false;
+
+ var cell,
+ firstCell = cells[ 0 ],
+ table = firstCell.getAscendant( 'table' ),
+ map = CKEDITOR.tools.buildTableMap( table ),
+ mapHeight = map.length,
+ mapWidth = map[ 0 ].length,
+ startRow = firstCell.getParent().$.rowIndex,
+ startColumn = cellInRow( map, startRow, firstCell );
+
+ if ( mergeDirection ) {
+ var targetCell;
+ try {
+ var rowspan = parseInt( firstCell.getAttribute( 'rowspan' ), 10 ) || 1;
+ var colspan = parseInt( firstCell.getAttribute( 'colspan' ), 10 ) || 1;
+
+ targetCell = map[ mergeDirection == 'up' ? ( startRow - rowspan ) : mergeDirection == 'down' ? ( startRow + rowspan ) : startRow ][
+ mergeDirection == 'left' ?
+ ( startColumn - colspan ) :
+ mergeDirection == 'right' ? ( startColumn + colspan ) : startColumn ];
+
+ } catch ( er ) {
+ return false;
+ }
+
+ // 1. No cell could be merged.
+ // 2. Same cell actually.
+ if ( !targetCell || firstCell.$ == targetCell )
+ return false;
+
+ // Sort in map order regardless of the DOM sequence.
+ cells[ ( mergeDirection == 'up' || mergeDirection == 'left' ) ? 'unshift' : 'push' ]( new CKEDITOR.dom.element( targetCell ) );
+ }
+
+ // Start from here are merging way ignorance (merge up/right, batch merge).
+ var doc = firstCell.getDocument(),
+ lastRowIndex = startRow,
+ totalRowSpan = 0,
+ totalColSpan = 0,
+ // Use a documentFragment as buffer when appending cell contents.
+ frag = !isDetect && new CKEDITOR.dom.documentFragment( doc ),
+ dimension = 0;
+
+ for ( var i = 0; i < cells.length; i++ ) {
+ cell = cells[ i ];
+
+ var tr = cell.getParent(),
+ cellFirstChild = cell.getFirst(),
+ colSpan = cell.$.colSpan,
+ rowSpan = cell.$.rowSpan,
+ rowIndex = tr.$.rowIndex,
+ colIndex = cellInRow( map, rowIndex, cell );
+
+ // Accumulated the actual places taken by all selected cells.
+ dimension += colSpan * rowSpan;
+ // Accumulated the maximum virtual spans from column and row.
+ totalColSpan = Math.max( totalColSpan, colIndex - startColumn + colSpan );
+ totalRowSpan = Math.max( totalRowSpan, rowIndex - startRow + rowSpan );
+
+ if ( !isDetect ) {
+ // Trim all cell fillers and check to remove empty cells.
+ if ( trimCell( cell ), cell.getChildren().count() ) {
+ // Merge vertically cells as two separated paragraphs.
+ if ( rowIndex != lastRowIndex && cellFirstChild && !( cellFirstChild.isBlockBoundary && cellFirstChild.isBlockBoundary( { br: 1 } ) ) ) {
+ var last = frag.getLast( CKEDITOR.dom.walker.whitespaces( true ) );
+ if ( last && !( last.is && last.is( 'br' ) ) )
+ frag.append( 'br' );
+ }
+
+ cell.moveChildren( frag );
+ }
+ i ? cell.remove() : cell.setHtml( '' );
+ }
+ lastRowIndex = rowIndex;
+ }
+
+ if ( !isDetect ) {
+ frag.moveChildren( firstCell );
+
+ firstCell.appendBogus();
+
+ if ( totalColSpan >= mapWidth )
+ firstCell.removeAttribute( 'rowSpan' );
+ else
+ firstCell.$.rowSpan = totalRowSpan;
+
+ if ( totalRowSpan >= mapHeight )
+ firstCell.removeAttribute( 'colSpan' );
+ else
+ firstCell.$.colSpan = totalColSpan;
+
+ // Swip empty left at the end of table due to the merging.
+ var trs = new CKEDITOR.dom.nodeList( table.$.rows ),
+ count = trs.count();
+
+ for ( i = count - 1; i >= 0; i-- ) {
+ var tailTr = trs.getItem( i );
+ if ( !tailTr.$.cells.length ) {
+ tailTr.remove();
+ count++;
+ continue;
+ }
+ }
+
+ return firstCell;
+ }
+ // Be able to merge cells only if actual dimension of selected
+ // cells equals to the caculated rectangle.
+ else
+ return ( totalRowSpan * totalColSpan ) == dimension;
+ }
+
+ function 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 ),
+ 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 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 ),
+ 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;
+ }
+ // Context menu on table caption incorrect (#3834)
+ var contextMenuTags = { thead: 1, tbody: 1, tfoot: 1, td: 1, tr: 1, th: 1 };
+
+ CKEDITOR.plugins.tabletools = {
+ requires: 'table,dialog,contextmenu',
+ init: function( editor ) {
+ var lang = editor.lang.table;
+
+ function createDef( def ) {
+ return CKEDITOR.tools.extend( def || {}, {
+ contextSensitive: 1,
+ refresh: function( editor, path ) {
+ this.setState( path.contains( { td: 1, th: 1 }, 1 ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
+ }
+ } );
+ }
+ function addCmd( name, def ) {
+ var cmd = editor.addCommand( name, def );
+ editor.addFeature( cmd );
+ }
+
+ addCmd( 'cellProperties', new CKEDITOR.dialogCommand( 'cellProperties', createDef( {
+ allowedContent: 'td th{width,height,border-color,background-color,white-space,vertical-align,text-align}[colspan,rowspan]',
+ requiredContent: 'table'
+ } ) ) );
+ CKEDITOR.dialog.add( 'cellProperties', this.path + 'dialogs/tableCell.js' );
+
+ addCmd( 'rowDelete', createDef( {
+ requiredContent: 'table',
+ exec: function( editor ) {
+ var selection = editor.getSelection();
+ placeCursorInCell( deleteRows( selection ) );
+ }
+ } ) );
+
+ addCmd( 'rowInsertBefore', createDef( {
+ requiredContent: 'table',
+ exec: function( editor ) {
+ var selection = editor.getSelection();
+ insertRow( selection, true );
+ }
+ } ) );
+
+ addCmd( 'rowInsertAfter', createDef( {
+ requiredContent: 'table',
+ exec: function( editor ) {
+ var selection = editor.getSelection();
+ insertRow( selection );
+ }
+ } ) );
+
+ addCmd( 'columnDelete', createDef( {
+ requiredContent: 'table',
+ exec: function( editor ) {
+ var selection = editor.getSelection();
+ var element = deleteColumns( selection );
+ element && placeCursorInCell( element, true );
+ }
+ } ) );
+
+ addCmd( 'columnInsertBefore', createDef( {
+ requiredContent: 'table',
+ exec: function( editor ) {
+ var selection = editor.getSelection();
+ insertColumn( selection, true );
+ }
+ } ) );
+
+ addCmd( 'columnInsertAfter', createDef( {
+ requiredContent: 'table',
+ exec: function( editor ) {
+ var selection = editor.getSelection();
+ insertColumn( selection );
+ }
+ } ) );
+
+ addCmd( 'cellDelete', createDef( {
+ requiredContent: 'table',
+ exec: function( editor ) {
+ var selection = editor.getSelection();
+ deleteCells( selection );
+ }
+ } ) );
+
+ addCmd( 'cellMerge', createDef( {
+ allowedContent: 'td[colspan,rowspan]',
+ requiredContent: 'td[colspan,rowspan]',
+ exec: function( editor ) {
+ placeCursorInCell( mergeCells( editor.getSelection() ), true );
+ }
+ } ) );
+
+ addCmd( 'cellMergeRight', createDef( {
+ allowedContent: 'td[colspan]',
+ requiredContent: 'td[colspan]',
+ exec: function( editor ) {
+ placeCursorInCell( mergeCells( editor.getSelection(), 'right' ), true );
+ }
+ } ) );
+
+ addCmd( 'cellMergeDown', createDef( {
+ allowedContent: 'td[rowspan]',
+ requiredContent: 'td[rowspan]',
+ exec: function( editor ) {
+ placeCursorInCell( mergeCells( editor.getSelection(), 'down' ), true );
+ }
+ } ) );
+
+ addCmd( 'cellVerticalSplit', createDef( {
+ allowedContent: 'td[rowspan]',
+ requiredContent: 'td[rowspan]',
+ exec: function( editor ) {
+ placeCursorInCell( verticalSplitCell( editor.getSelection() ) );
+ }
+ } ) );
+
+ addCmd( 'cellHorizontalSplit', createDef( {
+ allowedContent: 'td[colspan]',
+ requiredContent: 'td[colspan]',
+ exec: function( editor ) {
+ placeCursorInCell( horizontalSplitCell( editor.getSelection() ) );
+ }
+ } ) );
+
+ addCmd( 'cellInsertBefore', createDef( {
+ requiredContent: 'table',
+ exec: function( editor ) {
+ var selection = editor.getSelection();
+ insertCell( selection, true );
+ }
+ } ) );
+
+ addCmd( 'cellInsertAfter', createDef( {
+ requiredContent: 'table',
+ exec: function( editor ) {
+ var selection = editor.getSelection();
+ insertCell( selection );
+ }
+ } ) );
+
+ // If the "menu" plugin is loaded, register the menu items.
+ if ( editor.addMenuItems ) {
+ editor.addMenuItems( {
+ tablecell: {
+ label: lang.cell.menu,
+ group: 'tablecell',
+ order: 1,
+ getItems: function() {
+ var selection = editor.getSelection(),
+ cells = getSelectedCells( selection );
+ return {
+ tablecell_insertBefore: CKEDITOR.TRISTATE_OFF,
+ tablecell_insertAfter: CKEDITOR.TRISTATE_OFF,
+ tablecell_delete: CKEDITOR.TRISTATE_OFF,
+ tablecell_merge: mergeCells( selection, null, true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,
+ tablecell_merge_right: mergeCells( selection, 'right', true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,
+ tablecell_merge_down: mergeCells( selection, 'down', true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,
+ tablecell_split_vertical: verticalSplitCell( selection, true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,
+ tablecell_split_horizontal: horizontalSplitCell( selection, true ) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,
+ tablecell_properties: cells.length > 0 ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED
+ };
+ }
+ },
+
+ tablecell_insertBefore: {
+ label: lang.cell.insertBefore,
+ group: 'tablecell',
+ command: 'cellInsertBefore',
+ order: 5
+ },
+
+ tablecell_insertAfter: {
+ label: lang.cell.insertAfter,
+ group: 'tablecell',
+ command: 'cellInsertAfter',
+ order: 10
+ },
+
+ tablecell_delete: {
+ label: lang.cell.deleteCell,
+ group: 'tablecell',
+ command: 'cellDelete',
+ order: 15
+ },
+
+ tablecell_merge: {
+ label: lang.cell.merge,
+ group: 'tablecell',
+ command: 'cellMerge',
+ order: 16
+ },
+
+ tablecell_merge_right: {
+ label: lang.cell.mergeRight,
+ group: 'tablecell',
+ command: 'cellMergeRight',
+ order: 17
+ },
+
+ tablecell_merge_down: {
+ label: lang.cell.mergeDown,
+ group: 'tablecell',
+ command: 'cellMergeDown',
+ order: 18
+ },
+
+ tablecell_split_horizontal: {
+ label: lang.cell.splitHorizontal,
+ group: 'tablecell',
+ command: 'cellHorizontalSplit',
+ order: 19
+ },
+
+ tablecell_split_vertical: {
+ label: lang.cell.splitVertical,
+ group: 'tablecell',
+ command: 'cellVerticalSplit',
+ order: 20
+ },
+
+ tablecell_properties: {
+ label: lang.cell.title,
+ group: 'tablecellproperties',
+ command: 'cellProperties',
+ order: 21
+ },
+
+ tablerow: {
+ label: lang.row.menu,
+ group: 'tablerow',
+ order: 1,
+ getItems: function() {
+ return {
+ tablerow_insertBefore: CKEDITOR.TRISTATE_OFF,
+ tablerow_insertAfter: CKEDITOR.TRISTATE_OFF,
+ tablerow_delete: CKEDITOR.TRISTATE_OFF
+ };
+ }
+ },
+
+ tablerow_insertBefore: {
+ label: lang.row.insertBefore,
+ group: 'tablerow',
+ command: 'rowInsertBefore',
+ order: 5
+ },
+
+ tablerow_insertAfter: {
+ label: lang.row.insertAfter,
+ group: 'tablerow',
+ command: 'rowInsertAfter',
+ order: 10
+ },
+
+ tablerow_delete: {
+ label: lang.row.deleteRow,
+ group: 'tablerow',
+ command: 'rowDelete',
+ order: 15
+ },
+
+ tablecolumn: {
+ label: lang.column.menu,
+ group: 'tablecolumn',
+ order: 1,
+ getItems: function() {
+ return {
+ tablecolumn_insertBefore: CKEDITOR.TRISTATE_OFF,
+ tablecolumn_insertAfter: CKEDITOR.TRISTATE_OFF,
+ tablecolumn_delete: CKEDITOR.TRISTATE_OFF
+ };
+ }
+ },
+
+ tablecolumn_insertBefore: {
+ label: lang.column.insertBefore,
+ group: 'tablecolumn',
+ command: 'columnInsertBefore',
+ order: 5
+ },
+
+ tablecolumn_insertAfter: {
+ label: lang.column.insertAfter,
+ group: 'tablecolumn',
+ command: 'columnInsertAfter',
+ order: 10
+ },
+
+ tablecolumn_delete: {
+ label: lang.column.deleteColumn,
+ group: 'tablecolumn',
+ command: 'columnDelete',
+ order: 15
+ }
+ } );
+ }
+
+ // If the "contextmenu" plugin is laoded, register the listeners.
+ if ( editor.contextMenu ) {
+ editor.contextMenu.addListener( function( element, selection, path ) {
+ var cell = path.contains( { 'td': 1, 'th': 1 }, 1 );
+ if ( cell && !cell.isReadOnly() ) {
+ return {
+ tablecell: CKEDITOR.TRISTATE_OFF,
+ tablerow: CKEDITOR.TRISTATE_OFF,
+ tablecolumn: CKEDITOR.TRISTATE_OFF
+ };
+ }
+
+ return null;
+ } );
+ }
+ },
+
+ getSelectedCells: getSelectedCells
+
+ };
+ CKEDITOR.plugins.add( 'tabletools', CKEDITOR.plugins.tabletools );
+} )();
+
+/**
+ * Create a two-dimension array that reflects the actual layout of table cells,
+ * with cell spans, with mappings to the original td elements.
+ *
+ * @param {CKEDITOR.dom.element} table
+ * @member CKEDITOR.tools
+ */
+CKEDITOR.tools.buildTableMap = function( table ) {
+ var aRows = table.$.rows;
+
+ // Row and Column counters.
+ var r = -1;
+
+ var aMap = [];
+
+ for ( var i = 0; i < aRows.length; i++ ) {
+ r++;
+ !aMap[ r ] && ( aMap[ r ] = [] );
+
+ var c = -1;
+
+ for ( var j = 0; j < aRows[ i ].cells.length; j++ ) {
+ var oCell = aRows[ i ].cells[ j ];
+
+ c++;
+ while ( aMap[ r ][ c ] )
+ c++;
+
+ var iColSpan = isNaN( oCell.colSpan ) ? 1 : oCell.colSpan;
+ var iRowSpan = isNaN( oCell.rowSpan ) ? 1 : oCell.rowSpan;
+
+ for ( var rs = 0; rs < iRowSpan; rs++ ) {
+ if ( !aMap[ r + rs ] )
+ aMap[ r + rs ] = [];
+
+ for ( var cs = 0; cs < iColSpan; cs++ ) {
+ aMap[ r + rs ][ c + cs ] = aRows[ i ].cells[ j ];
+ }
+ }
+
+ c += iColSpan - 1;
+ }
+ }
+ return aMap;
+};
diff --git a/lam/templates/lib/extra/ckeditor/plugins/toolbar/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/toolbar/plugin.js
new file mode 100644
index 00000000..66968154
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/toolbar/plugin.js
@@ -0,0 +1,781 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+/**
+ * @fileOverview The "toolbar" plugin. Renders the default toolbar interface in
+ * the editor.
+ */
+
+( function() {
+ var toolbox = function() {
+ this.toolbars = [];
+ this.focusCommandExecuted = false;
+ };
+
+ toolbox.prototype.focus = function() {
+ for ( var t = 0, toolbar; toolbar = this.toolbars[ t++ ]; ) {
+ for ( var i = 0, item; item = toolbar.items[ i++ ]; ) {
+ if ( item.focus ) {
+ item.focus();
+ return;
+ }
+ }
+ }
+ };
+
+ var commands = {
+ toolbarFocus: {
+ modes: { wysiwyg: 1, source: 1 },
+ readOnly: 1,
+
+ exec: function( editor ) {
+ if ( editor.toolbox ) {
+ editor.toolbox.focusCommandExecuted = true;
+
+ // Make the first button focus accessible for IE. (#3417)
+ // Adobe AIR instead need while of delay.
+ if ( CKEDITOR.env.ie || CKEDITOR.env.air )
+ setTimeout( function() {
+ editor.toolbox.focus();
+ }, 100 );
+ else
+ editor.toolbox.focus();
+ }
+ }
+ }
+ };
+
+ CKEDITOR.plugins.add( 'toolbar', {
+ requires: 'button',
+ lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+
+ init: function( editor ) {
+ var endFlag;
+
+ var itemKeystroke = function( item, keystroke ) {
+ var next, toolbar;
+ var rtl = editor.lang.dir == 'rtl',
+ toolbarGroupCycling = editor.config.toolbarGroupCycling,
+ // Picking right/left key codes.
+ rightKeyCode = rtl ? 37 : 39,
+ leftKeyCode = rtl ? 39 : 37;
+
+ toolbarGroupCycling = toolbarGroupCycling === undefined || toolbarGroupCycling;
+
+ switch ( keystroke ) {
+ case 9: // TAB
+ case CKEDITOR.SHIFT + 9: // SHIFT + TAB
+ // Cycle through the toolbars, starting from the one
+ // closest to the current item.
+ while ( !toolbar || !toolbar.items.length ) {
+ toolbar = keystroke == 9 ? ( ( toolbar ? toolbar.next : item.toolbar.next ) || editor.toolbox.toolbars[ 0 ] ) : ( ( toolbar ? toolbar.previous : item.toolbar.previous ) || editor.toolbox.toolbars[ editor.toolbox.toolbars.length - 1 ] );
+
+ // Look for the first item that accepts focus.
+ if ( toolbar.items.length ) {
+ item = toolbar.items[ endFlag ? ( toolbar.items.length - 1 ) : 0 ];
+ while ( item && !item.focus ) {
+ item = endFlag ? item.previous : item.next;
+
+ if ( !item )
+ toolbar = 0;
+ }
+ }
+ }
+
+ if ( item )
+ item.focus();
+
+ return false;
+
+ case rightKeyCode:
+ next = item;
+ do {
+ // Look for the next item in the toolbar.
+ next = next.next;
+
+ // If it's the last item, cycle to the first one.
+ if ( !next && toolbarGroupCycling ) next = item.toolbar.items[ 0 ];
+ }
+ while ( next && !next.focus );
+
+ // If available, just focus it, otherwise focus the
+ // first one.
+ if ( next )
+ next.focus();
+ else
+ // Send a TAB.
+ itemKeystroke( item, 9 );
+
+ return false;
+ case 40: // DOWN-ARROW
+ if ( item.button && item.button.hasArrow ) {
+ // Note: code is duplicated in plugins\richcombo\plugin.js in keyDownFn().
+ editor.once( 'panelShow', function( evt ) {
+ evt.data._.panel._.currentBlock.onKeyDown( 40 );
+ } );
+ item.execute();
+ } else {
+ // Send left arrow key.
+ itemKeystroke( item, keystroke == 40 ? rightKeyCode : leftKeyCode );
+ }
+ return false;
+ case leftKeyCode:
+ case 38: // UP-ARROW
+ next = item;
+ do {
+ // Look for the previous item in the toolbar.
+ next = next.previous;
+
+ // If it's the first item, cycle to the last one.
+ if ( !next && toolbarGroupCycling ) next = item.toolbar.items[ item.toolbar.items.length - 1 ];
+ }
+ while ( next && !next.focus );
+
+ // If available, just focus it, otherwise focus the
+ // last one.
+ if ( next )
+ next.focus();
+ else {
+ endFlag = 1;
+ // Send a SHIFT + TAB.
+ itemKeystroke( item, CKEDITOR.SHIFT + 9 );
+ endFlag = 0;
+ }
+
+ return false;
+
+ case 27: // ESC
+ editor.focus();
+ return false;
+
+ case 13: // ENTER
+ case 32: // SPACE
+ item.execute();
+ return false;
+ }
+ return true;
+ };
+
+ editor.on( 'uiSpace', function( event ) {
+ if ( event.data.space != editor.config.toolbarLocation )
+ return;
+
+ // Create toolbar only once.
+ event.removeListener();
+
+ editor.toolbox = new toolbox();
+
+ var labelId = CKEDITOR.tools.getNextId();
+
+ var output = [
+ '', editor.lang.toolbar.toolbars, '',
+ '' ];
+
+ var expanded = editor.config.toolbarStartupExpanded !== false,
+ groupStarted, pendingSeparator;
+
+ // If the toolbar collapser will be available, we'll have
+ // an additional container for all toolbars.
+ if ( editor.config.toolbarCanCollapse && editor.elementMode != CKEDITOR.ELEMENT_MODE_INLINE )
+ output.push( '' : ' style="display:none">' ) );
+
+ var toolbars = editor.toolbox.toolbars,
+ toolbar = getToolbarConfig( editor );
+
+ for ( var r = 0; r < toolbar.length; r++ ) {
+ var toolbarId,
+ toolbarObj = 0,
+ toolbarName,
+ row = toolbar[ r ],
+ items;
+
+ // It's better to check if the row object is really
+ // available because it's a common mistake to leave
+ // an extra comma in the toolbar definition
+ // settings, which leads on the editor not loading
+ // at all in IE. (#3983)
+ if ( !row )
+ continue;
+
+ if ( groupStarted ) {
+ output.push( '' );
+ groupStarted = 0;
+ pendingSeparator = 0;
+ }
+
+ if ( row === '/' ) {
+ output.push( '' );
+ continue;
+ }
+
+ items = row.items || row;
+
+ // Create all items defined for this toolbar.
+ for ( var i = 0; i < items.length; i++ ) {
+ var item = items[ i ],
+ canGroup;
+
+ if ( item ) {
+ if ( item.type == CKEDITOR.UI_SEPARATOR ) {
+ // Do not add the separator immediately. Just save
+ // it be included if we already have something in
+ // the toolbar and if a new item is to be added (later).
+ pendingSeparator = groupStarted && item;
+ continue;
+ }
+
+ canGroup = item.canGroup !== false;
+
+ // Initialize the toolbar first, if needed.
+ if ( !toolbarObj ) {
+ // Create the basic toolbar object.
+ toolbarId = CKEDITOR.tools.getNextId();
+ toolbarObj = { id: toolbarId, items: [] };
+ toolbarName = row.name && ( editor.lang.toolbar.toolbarGroups[ row.name ] || row.name );
+
+ // Output the toolbar opener.
+ output.push( '' );
+
+ // If a toolbar name is available, send the voice label.
+ toolbarName && output.push( '', toolbarName, '' );
+
+ output.push( '' );
+
+ // Add the toolbar to the "editor.toolbox.toolbars"
+ // array.
+ var index = toolbars.push( toolbarObj ) - 1;
+
+ // Create the next/previous reference.
+ if ( index > 0 ) {
+ toolbarObj.previous = toolbars[ index - 1 ];
+ toolbarObj.previous.next = toolbarObj;
+ }
+ }
+
+ if ( canGroup ) {
+ if ( !groupStarted ) {
+ output.push( '' );
+ groupStarted = 1;
+ }
+ } else if ( groupStarted ) {
+ output.push( '' );
+ groupStarted = 0;
+ }
+
+ function addItem( item ) {
+ var itemObj = item.render( editor, output );
+ index = toolbarObj.items.push( itemObj ) - 1;
+
+ if ( index > 0 ) {
+ itemObj.previous = toolbarObj.items[ index - 1 ];
+ itemObj.previous.next = itemObj;
+ }
+
+ itemObj.toolbar = toolbarObj;
+ itemObj.onkey = itemKeystroke;
+
+ // Fix for #3052:
+ // Prevent JAWS from focusing the toolbar after document load.
+ itemObj.onfocus = function() {
+ if ( !editor.toolbox.focusCommandExecuted )
+ editor.focus();
+ };
+ }
+
+ if ( pendingSeparator ) {
+ addItem( pendingSeparator );
+ pendingSeparator = 0;
+ }
+
+ addItem( item );
+ }
+ }
+
+ if ( groupStarted ) {
+ output.push( '' );
+ groupStarted = 0;
+ pendingSeparator = 0;
+ }
+
+ if ( toolbarObj )
+ output.push( '' );
+ }
+
+ if ( editor.config.toolbarCanCollapse )
+ output.push( '' );
+
+ // Not toolbar collapser for inline mode.
+ if ( editor.config.toolbarCanCollapse && editor.elementMode != CKEDITOR.ELEMENT_MODE_INLINE ) {
+ var collapserFn = CKEDITOR.tools.addFunction( function() {
+ editor.execCommand( 'toolbarCollapse' );
+ } );
+
+ editor.on( 'destroy', function() {
+ CKEDITOR.tools.removeFunction( collapserFn );
+ } );
+
+ editor.addCommand( 'toolbarCollapse', {
+ readOnly: 1,
+ exec: function( editor ) {
+ var collapser = editor.ui.space( 'toolbar_collapser' ),
+ toolbox = collapser.getPrevious(),
+ contents = editor.ui.space( 'contents' ),
+ toolboxContainer = toolbox.getParent(),
+ contentHeight = parseInt( contents.$.style.height, 10 ),
+ previousHeight = toolboxContainer.$.offsetHeight,
+ minClass = 'cke_toolbox_collapser_min',
+ collapsed = collapser.hasClass( minClass );
+
+ if ( !collapsed ) {
+ toolbox.hide();
+ collapser.addClass( minClass );
+ collapser.setAttribute( 'title', editor.lang.toolbar.toolbarExpand );
+ } else {
+ toolbox.show();
+ collapser.removeClass( minClass );
+ collapser.setAttribute( 'title', editor.lang.toolbar.toolbarCollapse );
+ }
+
+ // Update collapser symbol.
+ collapser.getFirst().setText( collapsed ? '\u25B2' : // BLACK UP-POINTING TRIANGLE
+ '\u25C0' ); // BLACK LEFT-POINTING TRIANGLE
+
+ var dy = toolboxContainer.$.offsetHeight - previousHeight;
+ contents.setStyle( 'height', ( contentHeight - dy ) + 'px' );
+
+ editor.fire( 'resize' );
+ },
+
+ modes: { wysiwyg: 1, source: 1 }
+ } );
+
+ editor.setKeystroke( CKEDITOR.ALT + ( CKEDITOR.env.ie || CKEDITOR.env.webkit ? 189 : 109 ) /*-*/, 'toolbarCollapse' );
+
+ output.push( '', '▲', // BLACK UP-POINTING TRIANGLE
+ '' );
+ }
+
+ output.push( '' );
+ event.data.html += output.join( '' );
+ } );
+
+ editor.on( 'destroy', function() {
+
+ if ( this.toolbox )
+ {
+ var toolbars,
+ index = 0,
+ i, items, instance;
+ toolbars = this.toolbox.toolbars;
+ for ( ; index < toolbars.length; index++ ) {
+ items = toolbars[ index ].items;
+ for ( i = 0; i < items.length; i++ ) {
+ instance = items[ i ];
+ if ( instance.clickFn )
+ CKEDITOR.tools.removeFunction( instance.clickFn );
+ if ( instance.keyDownFn )
+ CKEDITOR.tools.removeFunction( instance.keyDownFn );
+ }
+ }
+ }
+ } );
+
+ // Manage editor focus when navigating the toolbar.
+ editor.on( 'uiReady', function() {
+ var toolbox = editor.ui.space( 'toolbox' );
+ toolbox && editor.focusManager.add( toolbox, 1 );
+ } );
+
+ editor.addCommand( 'toolbarFocus', commands.toolbarFocus );
+ editor.setKeystroke( CKEDITOR.ALT + 121 /*F10*/, 'toolbarFocus' );
+
+ editor.ui.add( '-', CKEDITOR.UI_SEPARATOR, {} );
+ editor.ui.addHandler( CKEDITOR.UI_SEPARATOR, {
+ create: function() {
+ return {
+ render: function( editor, output ) {
+ output.push( '' );
+ return {};
+ }
+ };
+ }
+ } );
+ }
+ } );
+
+ function getToolbarConfig( editor ) {
+ var removeButtons = editor.config.removeButtons;
+
+ removeButtons = removeButtons && removeButtons.split( ',' );
+
+ function buildToolbarConfig() {
+
+ // Object containing all toolbar groups used by ui items.
+ var lookup = getItemDefinedGroups();
+
+ // Take the base for the new toolbar, which is basically a toolbar
+ // definition without items.
+ var toolbar = CKEDITOR.tools.clone( editor.config.toolbarGroups ) || getPrivateToolbarGroups( editor );
+
+ // Fill the toolbar groups with the available ui items.
+ for ( var i = 0; i < toolbar.length; i++ ) {
+ var toolbarGroup = toolbar[ i ];
+
+ // Skip toolbar break.
+ if ( toolbarGroup == '/' )
+ continue;
+ // Handle simply group name item.
+ else if ( typeof toolbarGroup == 'string' )
+ toolbarGroup = toolbar[ i ] = { name: toolbarGroup };
+
+ var items, subGroups = toolbarGroup.groups;
+
+ // Look for items that match sub groups.
+ if ( subGroups ) {
+ for ( var j = 0, sub; j < subGroups.length; j++ ) {
+ sub = subGroups[ j ];
+
+ // If any ui item is registered for this subgroup.
+ items = lookup[ sub ];
+ items && fillGroup( toolbarGroup, items );
+ }
+ }
+
+ // Add the main group items as well.
+ items = lookup[ toolbarGroup.name ];
+ items && fillGroup( toolbarGroup, items );
+ }
+
+ return toolbar;
+ }
+
+ // Returns an object containing all toolbar groups used by ui items.
+ function getItemDefinedGroups() {
+ var groups = {},
+ itemName, item, itemToolbar, group, order;
+
+ for ( itemName in editor.ui.items ) {
+ item = editor.ui.items[ itemName ];
+ itemToolbar = item.toolbar || 'others';
+ if ( itemToolbar ) {
+ // Break the toolbar property into its parts: "group_name[,order]".
+ itemToolbar = itemToolbar.split( ',' );
+ group = itemToolbar[ 0 ];
+ order = parseInt( itemToolbar[ 1 ] || -1, 10 );
+
+ // Initialize the group, if necessary.
+ groups[ group ] || ( groups[ group ] = [] );
+
+ // Push the data used to build the toolbar later.
+ groups[ group ].push( { name: itemName, order: order } );
+ }
+ }
+
+ // Put the items in the right order.
+ for ( group in groups ) {
+ groups[ group ] = groups[ group ].sort( function( a, b ) {
+ return a.order == b.order ? 0 :
+ b.order < 0 ? -1 :
+ a.order < 0 ? 1 :
+ a.order < b.order ? -1 :
+ 1;
+ } );
+ }
+
+ return groups;
+ }
+
+ function fillGroup( toolbarGroup, uiItems ) {
+ if ( uiItems.length ) {
+ if ( toolbarGroup.items )
+ toolbarGroup.items.push( editor.ui.create( '-' ) );
+ else
+ toolbarGroup.items = [];
+
+ var item, name;
+ while ( ( item = uiItems.shift() ) ) {
+ name = typeof item == 'string' ? item : item.name;
+
+ // Ignore items that are configured to be removed.
+ if ( !removeButtons || CKEDITOR.tools.indexOf( removeButtons, name ) == -1 ) {
+ item = editor.ui.create( name );
+
+ if ( !item )
+ continue;
+
+ if ( !editor.addFeature( item ) )
+ continue;
+
+ toolbarGroup.items.push( item );
+ }
+ }
+ }
+ }
+
+ function populateToolbarConfig( config ) {
+ var toolbar = [],
+ i, group, newGroup;
+
+ for ( i = 0; i < config.length; ++i ) {
+ group = config[ i ];
+ newGroup = {};
+
+ if ( group == '/' )
+ toolbar.push( group );
+ else if ( CKEDITOR.tools.isArray( group ) ) {
+ fillGroup( newGroup, CKEDITOR.tools.clone( group ) );
+ toolbar.push( newGroup );
+ }
+ else if ( group.items ) {
+ fillGroup( newGroup, CKEDITOR.tools.clone( group.items ) );
+ newGroup.name = group.name;
+ toolbar.push( newGroup );
+ }
+ }
+
+ return toolbar;
+ }
+
+ var toolbar = editor.config.toolbar;
+
+ // If it is a string, return the relative "toolbar_name" config.
+ if ( typeof toolbar == 'string' )
+ toolbar = editor.config[ 'toolbar_' + toolbar ];
+
+ return ( editor.toolbar = toolbar ? populateToolbarConfig( toolbar ) : buildToolbarConfig() );
+ }
+
+ /**
+ * Add toolbar group. See {@link CKEDITOR.config#toolbarGroups} for more details.
+ *
+ * **Note:** This method won't modify toolbar groups set explicitly by
+ * {@link CKEDITOR.config#toolbarGroups}. It will extend only default setting.
+ *
+ * @param {String} name Group name.
+ * @param {Number/String} previous Name of group after which this one
+ * should be added or `0` if this group should be the first one.
+ * @param {String} [subgroupOf] Name of 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 "UI space" to which rendering the toolbar. For the default editor implementation,
+ * the recommended options are `'top'` and `'bottom'`.
+ *
+ * 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`, generate toolbar automatically using all available buttons
+ * and {@link #toolbarGroups} as a toolbar groups layout.
+ *
+ * // Defines a toolbar with only one strip containing the "Source" button, a
+ * // separator and the "Bold" and "Italic" buttons.
+ * config.toolbar = [
+ * [ 'Source', '-', 'Bold', 'Italic' ]
+ * ];
+ *
+ * // Similar to example the above, defines a "Basic" toolbar with only one strip containing three buttons.
+ * // Note that this setting is composed by "toolbar_" added by 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,
+ * // so you instruct the editor which toolbar_(name) setting to use.
+ * 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 toolbar layout isn't explicitly defined by {@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 `toolbar` property in their definition objects.
+ *
+ * New groups may be dynamically added during the editor and plugins initialization by
+ * {@link CKEDITOR.ui#addToolbarGroup}. Although only if 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 collapser
+ * 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 toolbar only when
+ * {@link #toolbarCanCollapse} is set to `true`:
+ *
+ * config.toolbarCanCollapse = true;
+ * config.toolbarStartupExpanded = false;
+ *
+ * @cfg {Boolean} [toolbarStartupExpanded=true]
+ * @member CKEDITOR.config
+ */
+
+/**
+ * When enabled, makes the arrow keys navigation cycle within the current
+ * toolbar group. Otherwise the arrows will move through all items available in
+ * the toolbar. The *TAB* key will still be used to quickly jump among the
+ * toolbar groups.
+ *
+ * config.toolbarGroupCycling = false;
+ *
+ * @since 3.6
+ * @cfg {Boolean} [toolbarGroupCycling=true]
+ * @member CKEDITOR.config
+ */
+
+/**
+ * List of toolbar button names that must not be rendered. This will work as
+ * well for non-button toolbar items, like the Font combos.
+ *
+ * config.removeButtons = 'Underline,JustifyCenter';
+ *
+ * This configuration should not be overused, having
+ * {@link CKEDITOR.config#removePlugins} removing features from the editor. 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
+ */
+
+/**
+ * Toolbar definition used by the editor. It is crated from the
+ * {@link CKEDITOR.config#toolbar} if it is set or automatically
+ * based on {@link CKEDITOR.config#toolbarGroups}.
+ *
+ * @readonly
+ * @property {Object} toolbar
+ * @member CKEDITOR.editor
+ */
diff --git a/lam/templates/lib/extra/ckeditor/plugins/undo/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/undo/plugin.js
new file mode 100644
index 00000000..effb070b
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/undo/plugin.js
@@ -0,0 +1,738 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+/**
+ * @fileOverview Undo/Redo system for saving a shapshot for document modification
+ * and other recordable changes.
+ */
+
+( function() {
+ CKEDITOR.plugins.add( 'undo', {
+ lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+ icons: 'redo,redo-rtl,undo,undo-rtl', // %REMOVE_LINE_CORE%
+ hidpi: true, // %REMOVE_LINE_CORE%
+ init: function( editor ) {
+ var undoManager = editor.undoManager = new UndoManager( editor );
+
+ 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( [
+ [ CKEDITOR.CTRL + 90 /*Z*/, 'undo' ],
+ [ CKEDITOR.CTRL + 89 /*Y*/, 'redo' ],
+ [ CKEDITOR.CTRL + CKEDITOR.SHIFT + 90 /*Z*/, '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 );
+ } );
+
+ // Registering keydown on every document recreation.(#3844)
+ editor.on( 'contentDom', function() {
+ editor.editable().on( 'keydown', function( event ) {
+ var keystroke = event.data.getKey();
+
+ if ( keystroke == 8 /*Backspace*/ || keystroke == 46 /*Delete*/ )
+ undoManager.type( keystroke, 0 );
+ } );
+
+ editor.editable().on( 'keypress', function( event ) {
+ undoManager.type( event.data.getKey(), 1 );
+ } );
+ } );
+
+ // Always save an undo snapshot - the previous mode might have
+ // changed editor contents.
+ editor.on( 'beforeModeUnload', function() {
+ editor.mode == 'wysiwyg' && undoManager.save( true );
+ } );
+
+ function toggleUndoManager() {
+ undoManager.enabled = editor.readOnly ? false : editor.mode == 'wysiwyg';
+ undoManager.onChange();
+ }
+
+ // Make the undo manager available only in wysiwyg mode.
+ editor.on( 'mode', toggleUndoManager );
+
+ // Disable undo manager when in read-only mode.
+ editor.on( 'readOnly', toggleUndoManager );
+
+ if ( editor.ui.addButton ) {
+ editor.ui.addButton( 'Undo', {
+ label: editor.lang.undo.undo,
+ command: 'undo',
+ toolbar: 'undo,10'
+ } );
+
+ editor.ui.addButton( 'Redo', {
+ label: editor.lang.undo.redo,
+ command: 'redo',
+ toolbar: 'undo,20'
+ } );
+ }
+
+ /**
+ * Resets the undo stack.
+ *
+ * @member CKEDITOR.editor
+ */
+ editor.resetUndo = function() {
+ // Reset the undo stack.
+ undoManager.reset();
+
+ // Create the first image.
+ editor.fire( 'saveSnapshot' );
+ };
+
+ /**
+ * Amends the top of the undo stack (last undo image) with the current DOM changes.
+ *
+ * function() {
+ * editor.fire( 'saveSnapshot' );
+ * editor.document.body.append(...);
+ * // Makes new changes following the last undo snapshot a part of it.
+ * editor.fire( 'updateSnapshot' );
+ * ..
+ * }
+ *
+ * @event updateSnapshot
+ * @member CKEDITOR.editor
+ * @param {CKEDITOR.editor} editor This editor instance.
+ */
+ editor.on( 'updateSnapshot', function() {
+ if ( undoManager.currentImage )
+ undoManager.update();
+ } );
+
+ /**
+ * Locks the undo manager to prevent any save/update operations.
+ *
+ * It is convenient to lock the undo manager before performing DOM operations
+ * that should not be recored (e.g. auto paragraphing).
+ *
+ * See {@link CKEDITOR.plugins.undo.UndoManager#lock} for more details.
+ *
+ * **Note:** In order to unlock the undo manager, {@link #unlockSnapshot} has to be fired
+ * the same number of times that `lockSnapshot` has been fired.
+ *
+ * @since 4.0
+ * @event lockSnapshot
+ * @member CKEDITOR.editor
+ * @param {CKEDITOR.editor} editor This editor instance.
+ * @param data
+ * @param {Boolean} [data.dontUpdate] When set to `true` the last snapshot will not be updated
+ * with the current contents and selection. Read more in the {@link CKEDITOR.plugins.undo.UndoManager#lock} method.
+ */
+ editor.on( 'lockSnapshot', function( evt ) {
+ undoManager.lock( evt.data && evt.data.dontUpdate );
+ } );
+
+ /**
+ * Unlocks the undo manager and updates the latest snapshot.
+ *
+ * @since 4.0
+ * @event unlockSnapshot
+ * @member CKEDITOR.editor
+ * @param {CKEDITOR.editor} editor This editor instance.
+ */
+ editor.on( 'unlockSnapshot', undoManager.unlock, undoManager );
+ }
+ } );
+
+ CKEDITOR.plugins.undo = {};
+
+ /**
+ * Undoes the snapshot which represents the current document status.
+ *
+ * @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` image will contain only contents, without 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 = {
+ equalsContent: function( otherImage ) {
+ var thisContents = this.contents,
+ otherContents = otherImage.contents;
+
+ // For IE6/7 : Comparing only the protected attribute values but not the original ones.(#4522)
+ if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) ) {
+ thisContents = thisContents.replace( protectedAttrs, '' );
+ otherContents = otherContents.replace( protectedAttrs, '' );
+ }
+
+ if ( thisContents != otherContents )
+ return false;
+
+ return true;
+ },
+
+ 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;
+ }
+ };
+
+ /**
+ * Main logic for the Redo/Undo feature.
+ *
+ * **Note:** This class is not accessible from the global scope.
+ *
+ * @private
+ * @class CKEDITOR.plugins.undo.UndoManager
+ * @constructor Creates an UndoManager class instance.
+ * @param {CKEDITOR.editor} editor
+ */
+ function UndoManager( editor ) {
+ this.editor = editor;
+
+ // Reset the undo stack.
+ this.reset();
+ }
+
+ UndoManager.prototype = {
+ /**
+ * When `locked` property is not `null`, the undo manager is locked, so
+ * operations like `save` or `update` are forbidden.
+ *
+ * The manager can be locked/unlocked by the {@link #lock} and {@link #unlock} methods.
+ *
+ * @private
+ * @property {Object} [locked=null]
+ */
+
+ /**
+ * Handles keystroke support for the undo manager. It is called whenever a keystroke that
+ * can change the editor contents is pressed.
+ *
+ * @param {Number} keystroke The key code.
+ * @param {Boolean} isCharacter If `true`, it is a character ('a', '1', '&', ...). Otherwise it is the remove key (*Delete* or *Backspace*).
+ */
+ type: function( keystroke, isCharacter ) {
+ // Create undo snap for every different modifier key.
+ var modifierSnapshot = ( !isCharacter && keystroke != this.lastKeystroke );
+
+ // Create undo snap on the following cases:
+ // 1. Just start to type .
+ // 2. Typing some content after a modifier.
+ // 3. Typing some content after make a visible selection.
+ var startedTyping = !this.typing || ( isCharacter && !this.wasCharacter );
+
+ var editor = this.editor;
+
+ if ( startedTyping || modifierSnapshot ) {
+ var beforeTypeImage = new Image( editor ),
+ beforeTypeCount = this.snapshots.length;
+
+ // Use setTimeout, so we give the necessary time to the
+ // browser to insert the character into the DOM.
+ CKEDITOR.tools.setTimeout( function() {
+ var currentSnapshot = editor.getSnapshot();
+
+ // In IE, we need to remove the expando attributes.
+ if ( CKEDITOR.env.ie )
+ currentSnapshot = currentSnapshot.replace( /\s+data-cke-expando=".*?"/g, '' );
+
+ // If changes have taken place, while not been captured yet (#8459),
+ // compensate the snapshot.
+ if ( beforeTypeImage.contents != currentSnapshot && beforeTypeCount == this.snapshots.length ) {
+ // It's safe to now indicate typing state.
+ this.typing = true;
+
+ // This's a special save, with specified snapshot
+ // and without auto 'fireChange'.
+ if ( !this.save( false, beforeTypeImage, false ) )
+ // Drop future snapshots.
+ this.snapshots.splice( this.index + 1, this.snapshots.length - this.index - 1 );
+
+ this.hasUndo = true;
+ this.hasRedo = false;
+
+ this.typesCount = 1;
+ this.modifiersCount = 1;
+
+ this.onChange();
+ }
+ }, 0, this );
+ }
+
+ this.lastKeystroke = keystroke;
+ this.wasCharacter = isCharacter;
+
+ // Create undo snap after typed too much (over 25 times).
+ if ( !isCharacter ) {
+ this.typesCount = 0;
+ this.modifiersCount++;
+
+ if ( this.modifiersCount > 25 ) {
+ this.save( false, null, false );
+ this.modifiersCount = 1;
+ } else {
+ setTimeout( function() {
+ editor.fire( 'change' );
+ }, 0 );
+ }
+ } else {
+ this.modifiersCount = 0;
+ this.typesCount++;
+
+ if ( this.typesCount > 25 ) {
+ this.save( false, null, false );
+ this.typesCount = 1;
+ } else {
+ setTimeout( function() {
+ editor.fire( 'change' );
+ }, 0 );
+ }
+ }
+
+ },
+
+ /**
+ * Resets the undo stack.
+ */
+ reset: function() {
+ // Remember last pressed key.
+ this.lastKeystroke = 0;
+
+ // 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.limit = this.editor.config.undoStackSize || 20;
+
+ this.currentImage = null;
+
+ this.hasUndo = false;
+ this.hasRedo = false;
+ this.locked = null;
+
+ this.resetType();
+ },
+
+ /**
+ * Resets all typing variables.
+ *
+ * @see #type
+ */
+ resetType: function() {
+ this.typing = false;
+ delete this.lastKeystroke;
+ this.typesCount = 0;
+ this.modifiersCount = 0;
+ },
+
+ fireChange: function() {
+ 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.
+ */
+ save: function( onContentOnly, image, autoFireChange ) {
+ // Do not change snapshots stack when locked.
+ if ( this.locked )
+ return false;
+
+ var snapshots = this.snapshots;
+
+ // Get a content image.
+ if ( !image )
+ image = new Image( this.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
+ this.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.fireChange();
+ return true;
+ },
+
+ restoreImage: function( image ) {
+ // Bring editor focused to restore selection.
+ var editor = this.editor,
+ sel;
+
+ if ( image.bookmarks ) {
+ editor.focus();
+ // Retrieve the selection beforehand. (#8324)
+ sel = editor.getSelection();
+ }
+
+ // Start transaction - do not allow any mutations to the
+ // snapshots stack done when selecting bookmarks (much probably
+ // by selectionChange listener).
+ this.locked = 1;
+
+ 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 = 0;
+
+ this.index = image.index;
+ this.currentImage = this.snapshots[ this.index ];
+
+ // Update current image with the actual editor
+ // content, since actualy content may differ from
+ // the original snapshot due to dom change. (#4622)
+ this.update();
+ this.fireChange();
+
+ editor.fire( 'change' );
+ },
+
+ // Get the closest available image.
+ 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 undo 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 redo 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 not set defaults to image taken from 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;
+ },
+
+ /**
+ * Locks the snapshot stack to prevent any save/update operations and when necessary,
+ * updates the tip of the snapshot stack with the DOM changes introduced during the
+ * locked period, after the {@link #unlock} method is called.
+ *
+ * It is mainly used to ensure any DOM operations that should not be recorded
+ * (e.g. auto paragraphing) are not added to the stack.
+ *
+ * **Note:** For every `lock` call you must call {@link #unlock} once to unlock the undo manager.
+ *
+ * @since 4.0
+ * @param {Boolean} [dontUpdate] When set to `true`, the last snapshot will not be updated
+ * with current contents 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.
+ */
+ lock: function( dontUpdate ) {
+ if ( !this.locked ) {
+ if ( dontUpdate )
+ this.locked = { level: 1 };
+ else {
+ // Make a contents image. Don't include bookmarks, because:
+ // * we don't compare them,
+ // * there's a chance that DOM has been changed since
+ // locked (e.g. fake) selection was made, so createBookmark2 could fail.
+ // http://dev.ckeditor.com/ticket/11027#comment:3
+ var imageBefore = new Image( this.editor, true );
+
+ // If current editor content matches the tip of snapshot stack,
+ // the stack tip must be updated by unlock, to include any changes made
+ // during this period.
+ var matchedTip = this.currentImage && this.currentImage.equalsContent( imageBefore );
+
+ this.locked = { update: matchedTip ? imageBefore : null, level: 1 };
+ }
+ }
+ // Increase the level of lock.
+ else
+ this.locked.level++;
+ },
+
+ /**
+ * Unlocks the snapshot stack and checks to amend the last snapshot.
+ *
+ * See {@link #lock} for more details.
+ *
+ * @since 4.0
+ */
+ unlock: function() {
+ if ( this.locked ) {
+ // Decrease level of lock and check if equals 0, what means that undoM is completely unlocked.
+ if ( !--this.locked.level ) {
+ var updateImage = this.locked.update,
+ newImage = updateImage && new Image( this.editor, true );
+
+ this.locked = null;
+
+ if ( updateImage && !updateImage.equalsContent( newImage ) )
+ this.update();
+ }
+ }
+ }
+ };
+} )();
+
+/**
+ * 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 taken. 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 taken. 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 too often, you should compare the
+ * previous and the current editor content inside the event listener.
+ *
+ * @since 4.2
+ * @event change
+ * @member CKEDITOR.editor
+ * @param {CKEDITOR.editor} editor This editor instance.
+ */
diff --git a/lam/templates/lib/extra/ckeditor/plugins/wysiwygarea/plugin.js b/lam/templates/lib/extra/ckeditor/plugins/wysiwygarea/plugin.js
new file mode 100644
index 00000000..0b843a85
--- /dev/null
+++ b/lam/templates/lib/extra/ckeditor/plugins/wysiwygarea/plugin.js
@@ -0,0 +1,653 @@
+/**
+ * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or http://ckeditor.com/license
+ */
+
+/**
+ * @fileOverview The "wysiwygarea" plugin. It registers the "wysiwyg" editing
+ * mode, which handles the main editing area space.
+ */
+
+( function() {
+ CKEDITOR.plugins.add( 'wysiwygarea', {
+ init: function( editor ) {
+ if ( editor.config.fullPage ) {
+ editor.addFeature( {
+ allowedContent: 'html head title; style [media,type]; body (*)[id]; meta link [*]',
+ requiredContent: 'body'
+ } );
+ }
+
+ editor.addMode( 'wysiwyg', function( callback ) {
+ var src = 'document.open();' +
+ // In IE, the document domain must be set any time we call document.open().
+ ( CKEDITOR.env.ie ? '(' + CKEDITOR.tools.fixDomain + ')();' : '' ) +
+ 'document.close();';
+
+ // With IE, the custom domain has to be taken care at first,
+ // for other browers, the 'src' attribute should be left empty to
+ // trigger iframe's 'load' event.
+ src = CKEDITOR.env.air ? 'javascript:void(0)' : CKEDITOR.env.ie ? 'javascript:void(function(){' + encodeURIComponent( src ) + '}())'
+ :
+ '';
+
+ var iframe = CKEDITOR.dom.element.createFromHtml( '' );
+ iframe.setStyles( { width: '100%', height: '100%' } );
+ iframe.addClass( 'cke_wysiwyg_frame cke_reset' );
+
+ var contentSpace = editor.ui.space( 'contents' );
+ contentSpace.append( iframe );
+
+
+ // Asynchronous iframe loading is only required in IE>8 and Gecko (other reasons probably).
+ // Do not use it on WebKit as it'll break the browser-back navigation.
+ var useOnloadEvent = CKEDITOR.env.ie || CKEDITOR.env.gecko;
+ if ( useOnloadEvent )
+ iframe.on( 'load', onLoad );
+
+ var frameLabel = editor.title,
+ frameDesc = editor.lang.common.editorHelp;
+
+ if ( frameLabel ) {
+ if ( CKEDITOR.env.ie )
+ frameLabel += ', ' + frameDesc;
+
+ iframe.setAttribute( 'title', frameLabel );
+ }
+
+ var labelId = CKEDITOR.tools.getNextId(),
+ desc = CKEDITOR.dom.element.createFromHtml( '' + frameDesc + '' );
+
+ contentSpace.append( desc, 1 );
+
+ // Remove the ARIA description.
+ editor.on( 'beforeModeUnload', function( evt ) {
+ evt.removeListener();
+ desc.remove();
+ } );
+
+ iframe.setAttributes( {
+ 'aria-describedby': labelId,
+ tabIndex: editor.tabIndex,
+ allowTransparency: 'true'
+ } );
+
+ // Execute onLoad manually for all non IE||Gecko browsers.
+ !useOnloadEvent && onLoad();
+
+ if ( CKEDITOR.env.webkit ) {
+ // Webkit: iframe size doesn't auto fit well. (#7360)
+ var onResize = function() {
+ // Hide the iframe to get real size of the holder. (#8941)
+ contentSpace.setStyle( 'width', '100%' );
+
+ iframe.hide();
+ iframe.setSize( 'width', contentSpace.getSize( 'width' ) );
+ contentSpace.removeStyle( 'width' );
+ iframe.show();
+ };
+
+ iframe.setCustomData( 'onResize', onResize );
+
+ CKEDITOR.document.getWindow().on( 'resize', onResize );
+ }
+
+ editor.fire( 'ariaWidget', iframe );
+
+ function onLoad( evt ) {
+ evt && evt.removeListener();
+ editor.editable( new framedWysiwyg( editor, iframe.$.contentWindow.document.body ) );
+ editor.setData( editor.getData( 1 ), callback );
+ }
+ } );
+ }
+ } );
+
+ 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 );
+
+ if ( CKEDITOR.env.gecko ) {
+ // Force Gecko to change contentEditable from false to true on domReady
+ // (because it's previously set to true on iframe's body creation).
+ // Otherwise del/backspace and some other editable features will be broken in Fx <4
+ // See: #107 and https://bugzilla.mozilla.org/show_bug.cgi?id=440916
+ body.contentEditable = false;
+
+ // Remove any leading
which is between the and the comment.
+ // This one fixes Firefox 3.6 bug: the browser inserts a leading
+ // on document.write if the body has contenteditable="true".
+ if ( CKEDITOR.env.version < 20000 ) {
+ body.innerHTML = body.innerHTML.replace( /^.*/, '' );
+
+ // The above hack messes up the selection in FF36.
+ // To clean this up, manually select collapsed range that
+ // starts within the body.
+ setTimeout( function() {
+ var range = new CKEDITOR.dom.range( new CKEDITOR.dom.document( doc ) );
+ range.setStart( new CKEDITOR.dom.node( body ), 0 );
+ editor.getSelection().selectRanges( [ range ] );
+ }, 0 );
+ }
+ }
+
+ body.contentEditable = true;
+
+ if ( CKEDITOR.env.ie ) {
+ // Don't display the focus border.
+ body.hideFocus = true;
+
+ // Disable and re-enable the body to avoid IE from
+ // taking the editing focus at startup. (#141 / #523)
+ body.disabled = true;
+ body.removeAttribute( 'disabled' );
+ }
+
+ delete this._.isLoadingData;
+
+ // Play the magic to alter element reference to the reloaded one.
+ this.$ = body;
+
+ doc = new CKEDITOR.dom.document( doc );
+
+ this.setup();
+
+ if ( CKEDITOR.env.ie ) {
+ doc.getDocumentElement().addClass( doc.$.compatMode );
+
+ // Prevent IE from leaving new paragraph after deleting all contents in body. (#6966)
+ editor.config.enterMode != CKEDITOR.ENTER_P && this.attachListener( doc, 'selectionchange', function() {
+ var body = doc.getBody(),
+ sel = editor.getSelection(),
+ range = sel && sel.getRanges()[ 0 ];
+
+ if ( range && body.getHtml().match( /^(?: |
)<\/p>$/i ) && range.startContainer.equals( body ) ) {
+ // Avoid the ambiguity from a real user cursor position.
+ setTimeout( function() {
+ range = editor.getSelection().getRanges()[ 0 ];
+ if ( !range.startContainer.equals( 'body' ) ) {
+ body.getFirst().remove( 1 );
+ range.moveToElementEditEnd( body );
+ range.select();
+ }
+ }, 0 );
+ }
+ } );
+ }
+
+ // Fix problem with cursor not appearing in Webkit and IE11+ when clicking below the body (#10945, #10906).
+ // Fix for older IEs (8-10 and QM) is placed inside selection.js.
+ if ( CKEDITOR.env.webkit || ( CKEDITOR.env.ie && CKEDITOR.env.version > 10 ) ) {
+ doc.getDocumentElement().on( 'mousedown', function( evt ) {
+ if ( evt.data.getTarget().is( 'html' ) ) {
+ // IE needs this timeout. Webkit does not, but it does not cause problems too.
+ setTimeout( function() {
+ editor.editable().focus();
+ } );
+ }
+ } );
+ }
+
+ // ## START : disableNativeTableHandles and disableObjectResizing settings.
+
+ // Enable dragging of position:absolute elements in IE.
+ try {
+ editor.document.$.execCommand( '2D-position', false, true );
+ } catch ( e ) {}
+
+ // IE, Opera and Safari may not support it and throw errors.
+ try {
+ editor.document.$.execCommand( 'enableInlineTableEditing', false, !editor.config.disableNativeTableHandles );
+ } catch ( e ) {}
+
+ if ( editor.config.disableObjectResizing ) {
+ try {
+ this.getDocument().$.execCommand( 'enableObjectResizing', false, false );
+ } catch ( e ) {
+ // For browsers in which the above method failed, we can cancel the resizing on the fly (#4208)
+ this.attachListener( this, CKEDITOR.env.ie ? 'resizestart' : 'resize', function( evt ) {
+ evt.data.preventDefault();
+ } );
+ }
+ }
+
+ if ( CKEDITOR.env.gecko || CKEDITOR.env.ie && editor.document.$.compatMode == 'CSS1Compat' ) {
+ this.attachListener( this, 'keydown', function( evt ) {
+ var keyCode = evt.data.getKeystroke();
+
+ // PageUp OR PageDown
+ if ( keyCode == 33 || keyCode == 34 ) {
+ // PageUp/PageDown scrolling is broken in document
+ // with standard doctype, manually fix it. (#4736)
+ if ( CKEDITOR.env.ie ) {
+ setTimeout( function() {
+ editor.getSelection().scrollIntoView();
+ }, 0 );
+ }
+ // Page up/down cause editor selection to leak
+ // outside of editable thus we try to intercept
+ // the behavior, while it affects only happen
+ // when editor contents are not overflowed. (#7955)
+ else if ( editor.window.$.innerHeight > this.$.offsetHeight ) {
+ var range = editor.createRange();
+ range[ keyCode == 33 ? 'moveToElementEditStart' : 'moveToElementEditEnd' ]( this );
+ range.select();
+ evt.data.preventDefault();
+ }
+ }
+ } );
+ }
+
+ if ( CKEDITOR.env.ie ) {
+ // [IE] Iframe will still keep the selection when blurred, if
+ // focus is moved onto a non-editing host, e.g. link or button, but
+ // it becomes a problem for the object type selection, since the resizer
+ // handler attached on it will mark other part of the UI, especially
+ // for the dialog. (#8157)
+ // [IE<8 & Opera] Even worse For old IEs, the cursor will not vanish even if
+ // the selection has been moved to another text input in some cases. (#4716)
+ //
+ // Now the range restore is disabled, so we simply force IE to clean
+ // up the selection before blur.
+ this.attachListener( doc, 'blur', function() {
+ // Error proof when the editor is not visible. (#6375)
+ try {
+ doc.$.selection.empty();
+ } catch ( er ) {}
+ } );
+ }
+
+ // ## END
+
+
+ var title = editor.document.getElementsByTag( 'title' ).getItem( 0 );
+ title.data( 'cke-title', editor.document.$.title );
+
+ // [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() {
+ editor.fire( 'contentDom' );
+
+ if ( this._.isPendingFocus ) {
+ editor.focus();
+ this._.isPendingFocus = false;
+ }
+
+ setTimeout( function() {
+ editor.fire( 'dataReady' );
+ }, 0 );
+
+ // IE BUG: IE might have rendered the iframe with invisible contents.
+ // (#3623). Push some inconsequential CSS style changes to force IE to
+ // refresh it.
+ //
+ // Also, for some unknown reasons, short timeouts (e.g. 100ms) do not
+ // fix the problem. :(
+ if ( CKEDITOR.env.ie ) {
+ setTimeout( function() {
+ if ( editor.document ) {
+ var $body = editor.document.$.body;
+ $body.runtimeStyle.marginBottom = '0px';
+ $body.runtimeStyle.marginBottom = '';
+ }
+ }, 1000 );
+ }
+ }, 0, this );
+ }
+
+ var framedWysiwyg = CKEDITOR.tools.createClass( {
+ $: function( editor ) {
+ this.base.apply( this, arguments );
+
+ this._.frameLoadedHandler = CKEDITOR.tools.addFunction( function( win ) {
+ // Avoid opening design mode in a frame window thread,
+ // which will cause host page scrolling.(#4397)
+ CKEDITOR.tools.setTimeout( onDomReady, 0, this, win );
+ }, this );
+
+ this._.docTitle = this.getWindow().getFrame().getAttribute( 'title' );
+ },
+
+ base: CKEDITOR.editable,
+
+ proto: {
+ setData: function( data, isSnapshot ) {
+ var editor = this.editor;
+
+ if ( isSnapshot ) {
+ this.setHtml( data );
+ // Fire dataReady for the consistency with inline editors
+ // and because it makes sense. (#10370)
+ editor.fire( 'dataReady' );
+ }
+ else {
+ this._.isLoadingData = true;
+ editor._.dataStore = { id: 1 };
+
+ var config = editor.config,
+ fullPage = config.fullPage,
+ docType = config.docType;
+
+ // Build the additional stuff to be included into
.
+ var headExtra = CKEDITOR.tools.buildStyleHtml( iframeCssFixes() )
+ .replace( /