diff --git a/CHANGES.md b/CHANGES.md
index d006187d735..05148a87f5b 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -8,6 +8,7 @@ CKEditor 4 Changelog
* [#10370](http://dev.ckeditor.com/ticket/10370): Inconsistency in data events between framed and inline editors.
* [#9794](http://dev.ckeditor.com/ticket/9794): OnChange event.
* [#9923](http://dev.ckeditor.com/ticket/9923): HiDPI support in editor UI. HiDPI icons for Moono skin.
+* [#10027](http://dev.ckeditor.com/ticket/10027): Separated list and block indentation.
## CKEditor 4.1.2
diff --git a/config.js b/config.js
index e7d54256c9a..ca700080cf9 100644
--- a/config.js
+++ b/config.js
@@ -34,7 +34,8 @@ CKEDITOR.editorConfig = function( config ) {
'htmlwriter,' +
'image,' +
'iframe,' +
- 'indent,' +
+ 'indentlist,' +
+ 'indentblock,' +
'justify,' +
'link,' +
'list,' +
diff --git a/plugins/enterkey/plugin.js b/plugins/enterkey/plugin.js
index eef41747691..cb533b1f30f 100644
--- a/plugins/enterkey/plugin.js
+++ b/plugins/enterkey/plugin.js
@@ -5,9 +5,6 @@
(function() {
CKEDITOR.plugins.add( 'enterkey', {
- // TODO: should not depend on a particular format plugin.
- requires: 'indent',
-
init: function( editor ) {
editor.addCommand( 'enter', { modes:{wysiwyg:1 },
editorFocus: false,
@@ -48,13 +45,188 @@
var atBlockStart = range.checkStartOfBlock(),
atBlockEnd = range.checkEndOfBlock(),
path = editor.elementPath( range.startContainer ),
- block = path.block;
+ block = path.block,
+
+ // Determine the block element to be used.
+ blockTag = ( mode == CKEDITOR.ENTER_DIV ? 'div' : 'p' ),
+
+ newBlock;
// Exit the list when we're inside an empty list item block. (#5376)
if ( atBlockStart && atBlockEnd ) {
// Exit the list when we're inside an empty list item block. (#5376)
if ( block && ( block.is( 'li' ) || block.getParent().is( 'li' ) ) ) {
- editor.execCommand( 'outdent' );
+ var blockParent = block.getParent(),
+ blockGrandParent = blockParent.getParent(),
+
+ firstChild = !block.hasPrevious(),
+ lastChild = !block.hasNext(),
+
+ selection = editor.getSelection(),
+ bookmarks = selection.createBookmarks(),
+
+ orgDir = block.getDirection( 1 ),
+ className = block.getAttribute( 'class' ),
+ style = block.getAttribute( 'style' ),
+ dirLoose = blockGrandParent.getDirection( 1 ) != orgDir,
+
+ enterMode = editor.config.enterMode,
+ needsBlock = enterMode != CKEDITOR.ENTER_BR || dirLoose || style || className,
+
+ child;
+
+ if ( blockGrandParent.is( 'li' ) ) {
+
+ // If block is the first or the last child of the parent
+ // list, degrade it and move to the outer list:
+ // before the parent list if block is first child and after
+ // the parent list if block is the last child, respectively.
+ //
+ //
=>
+ // - =>
-
+ // =>
+ // => - ^
+ //
=>
+ //
+ // AND
+ //
+ // =>
+ // - =>
- ^
+ //
+ // =>
+ //
=>
+
+ if ( firstChild || lastChild )
+ 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();
+
+ // 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;
}
@@ -82,9 +254,6 @@
}
}
- // Determine the block element to be used.
- var blockTag = ( mode == CKEDITOR.ENTER_DIV ? 'div' : 'p' );
-
// Split the range.
var splitInfo = range.splitBlock( blockTag );
@@ -139,7 +308,7 @@
if ( nextBlock )
range.moveToElementEditStart( nextBlock );
} else {
- var newBlock, newBlockDir;
+ var newBlockDir;
if ( previousBlock ) {
// Do not enter this block if it's a header tag, or we are in
diff --git a/plugins/indent/dev/indent.html b/plugins/indent/dev/indent.html
new file mode 100644
index 00000000000..62b56c05726
--- /dev/null
+++ b/plugins/indent/dev/indent.html
@@ -0,0 +1,290 @@
+
+
+
+
+
Indent DEV sample
+
+
+
+
+
+
+
+
Indent DEV sample
+
List & Block
+
+
+
Indent classes
+
+
+
List only
+
+
+
Block only
+
+
+
CKEDITOR.ENTER_BR
+
+
+
+
+
diff --git a/plugins/indent/plugin.js b/plugins/indent/plugin.js
index 8e182a2d93d..4167fc0252c 100755
--- a/plugins/indent/plugin.js
+++ b/plugins/indent/plugin.js
@@ -4,370 +4,50 @@
*/
/**
- * @fileOverview Increse and decrease indent commands.
+ * @fileOverview Increase and decrease indent commands.
*/
(function() {
- var listNodeNames = { ol:1,ul:1 },
- isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
- isNotBookmark = CKEDITOR.dom.walker.bookmark( false, true );
-
- function indentCommand( editor, name ) {
- this.name = name;
- var useClasses = this.useIndentClasses = editor.config.indentClasses && editor.config.indentClasses.length > 0;
- if ( useClasses ) {
- this.classNameRegex = new RegExp( '(?:^|\\s+)(' + editor.config.indentClasses.join( '|' ) + ')(?=$|\\s)' );
- this.indentClassMap = {};
- for ( var i = 0; i < editor.config.indentClasses.length; i++ )
- this.indentClassMap[ editor.config.indentClasses[ i ] ] = i + 1;
- }
-
- this.startDisabled = name == 'outdent';
-
- this.allowedContent = {
- 'div h1 h2 h3 h4 h5 h6 ol p pre ul': {
- // Do not add elements, but only text-align style if element is validated by other rule.
- propertiesOnly: true,
- styles: !useClasses ? 'margin-left,margin-right' : null,
- classes: useClasses ? editor.config.indentClasses : null
- }
- };
-
- // #10192: Either blocks intendation or lists are required - acitvate
- // indent commands in both situations. Lists are sufficient, because
- // indent is needed for leaving list with enter key.
- this.requiredContent = [
- 'p' + ( useClasses ? '(' + editor.config.indentClasses[ 0 ] + ')' : '{margin-left}' ),
- 'li'
- ];
- }
-
- // Returns the CSS property to be used for identing a given element.
- function getIndentCssProperty( element, dir ) {
- return ( dir || element.getComputedStyle( 'direction' ) ) == 'ltr' ? 'margin-left' : 'margin-right';
- }
-
- function isListItem( node ) {
- return node.type == CKEDITOR.NODE_ELEMENT && node.is( 'li' );
- }
-
- indentCommand.prototype = {
- // It applies to a "block-like" context.
- context: 'p',
-
- refresh: function( editor, path ) {
- var list = path && path.contains( listNodeNames ),
- firstBlock = path.block || path.blockLimit;
-
- if ( list )
- this.setState( CKEDITOR.TRISTATE_OFF );
-
- else if ( !this.useIndentClasses && this.name == 'indent' )
- this.setState( CKEDITOR.TRISTATE_OFF );
-
- else if ( !firstBlock )
- this.setState( CKEDITOR.TRISTATE_DISABLED );
-
- else if ( this.useIndentClasses ) {
- var indentClass = firstBlock.$.className.match( this.classNameRegex ),
- indentStep = 0;
-
- if ( indentClass ) {
- indentClass = indentClass[ 1 ];
- indentStep = this.indentClassMap[ indentClass ];
- }
-
- if ( ( this.name == 'outdent' && !indentStep ) || ( this.name == 'indent' && indentStep == editor.config.indentClasses.length ) )
- this.setState( CKEDITOR.TRISTATE_DISABLED );
- else
- this.setState( CKEDITOR.TRISTATE_OFF );
- } else {
- var indent = parseInt( firstBlock.getStyle( getIndentCssProperty( firstBlock ) ), 10 );
- if ( isNaN( indent ) )
- indent = 0;
- if ( indent <= 0 )
- this.setState( CKEDITOR.TRISTATE_DISABLED );
- else
- this.setState( CKEDITOR.TRISTATE_OFF );
- }
- },
- exec: function( editor ) {
- var self = this,
- database = {};
-
- function indentList( 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;
-
- // 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;
-
- // 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 && listNodeNames[ listParents[ i ].getName() ] ) {
- listNode = listParents[ i ];
- break;
- }
- }
- var indentOffset = self.name == 'indent' ? 1 : -1,
- startItem = itemsToMove[ 0 ],
- lastItem = itemsToMove[ itemsToMove.length - 1 ];
-
- // Convert the list DOM tree into a one dimensional array.
- var listArray = CKEDITOR.plugins.list.listToArray( listNode, database );
-
- // Apply indenting or outdenting on the array.
- var 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 ( self.name == 'outdent' ) {
- 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 listNodeNames ) {
- // IE requires a filler NBSP for nested list inside empty list item,
- // otherwise the list item will be inaccessiable. (#4476)
- if ( CKEDITOR.env.ie && !li.getFirst( function( node ) {
- return isNotWhitespaces( node ) && isNotBookmark( node );
- }))
- li.append( range.document.createText( '\u00a0' ) );
-
- li.append( followingList );
- }
-
- li.insertAfter( parentLiElement );
- }
- }
- }
-
- function indentBlock() {
- var iterator = range.createIterator(),
- enterMode = editor.config.enterMode;
- iterator.enforceRealBlocks = true;
- iterator.enlargeBr = enterMode != CKEDITOR.ENTER_BR;
- var block;
- while ( ( block = iterator.getNextParagraph( enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ) ) )
- indentElement( block );
- }
-
- function indentElement( element, dir ) {
- if ( element.getCustomData( 'indent_processed' ) )
- return false;
-
- if ( self.useIndentClasses ) {
- // Transform current class name to indent step index.
- var indentClass = element.$.className.match( self.classNameRegex ),
- indentStep = 0;
- if ( indentClass ) {
- indentClass = indentClass[ 1 ];
- indentStep = self.indentClassMap[ indentClass ];
- }
-
- // Operate on indent step index, transform indent step index back to class
- // name.
- if ( self.name == 'outdent' )
- indentStep--;
- else
- indentStep++;
-
- if ( indentStep < 0 )
- return false;
-
- indentStep = Math.min( indentStep, editor.config.indentClasses.length );
- indentStep = Math.max( indentStep, 0 );
- element.$.className = CKEDITOR.tools.ltrim( element.$.className.replace( self.classNameRegex, '' ) );
- if ( indentStep > 0 )
- element.addClass( editor.config.indentClasses[ indentStep - 1 ] );
- } else {
- var indentCssProperty = getIndentCssProperty( element, dir ),
- currentOffset = parseInt( element.getStyle( indentCssProperty ), 10 );
- if ( isNaN( currentOffset ) )
- currentOffset = 0;
- var indentOffset = editor.config.indentOffset || 40;
- currentOffset += ( self.name == 'indent' ? 1 : -1 ) * indentOffset;
-
- if ( currentOffset < 0 )
- return false;
-
- currentOffset = Math.max( currentOffset, 0 );
- currentOffset = Math.ceil( currentOffset / indentOffset ) * indentOffset;
- element.setStyle( indentCssProperty, currentOffset ? currentOffset + ( editor.config.indentUnit || 'px' ) : '' );
- if ( element.getAttribute( 'style' ) === '' )
- element.removeAttribute( 'style' );
- }
-
- CKEDITOR.dom.element.setMarker( database, element, 'indent_processed', 1 );
- return true;
- }
-
- var selection = editor.getSelection(),
- bookmarks = selection.createBookmarks( 1 ),
- ranges = selection && selection.getRanges( 1 ),
- range;
-
-
- var iterator = ranges.createIterator();
- while ( ( range = iterator.getNextRange() ) ) {
- var rangeRoot = range.getCommonAncestor(),
- nearestListBlock = rangeRoot;
-
- while ( nearestListBlock && !( nearestListBlock.type == CKEDITOR.NODE_ELEMENT && listNodeNames[ nearestListBlock.getName() ] ) )
- nearestListBlock = nearestListBlock.getParent();
-
- // Avoid having selection enclose the entire list. (#6138)
- // [] =>
- if ( !nearestListBlock ) {
- var selectedNode = range.getEnclosedNode();
- if ( selectedNode && selectedNode.type == CKEDITOR.NODE_ELEMENT && selectedNode.getName() in listNodeNames ) {
- 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 listNodeNames ) {
- var walker = new CKEDITOR.dom.walker( range );
- walker.evaluator = isListItem;
- range.startContainer = walker.next();
- }
+ 'use strict';
- if ( nearestListBlock && range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in listNodeNames ) {
- walker = new CKEDITOR.dom.walker( range );
- walker.evaluator = isListItem;
- range.endContainer = walker.previous();
- }
-
- if ( nearestListBlock ) {
- var firstListItem = nearestListBlock.getFirst( isListItem ),
- hasMultipleItems = !!firstListItem.getNext( isListItem ),
- rangeStart = range.startContainer,
- indentWholeList = firstListItem.equals( rangeStart ) || firstListItem.contains( rangeStart );
-
- // Indent the entire list if cursor is inside the first list item. (#3893)
- // Only do that for indenting or when using indent classes or when there is something to outdent. (#6141)
- if ( !( indentWholeList && ( self.name == 'indent' || self.useIndentClasses || parseInt( nearestListBlock.getStyle( getIndentCssProperty( nearestListBlock ) ), 10 ) ) && indentElement( nearestListBlock, !hasMultipleItems && firstListItem.getDirection() ) ) )
- indentList( nearestListBlock );
- } else
- indentBlock();
- }
-
- // Clean up the markers.
- CKEDITOR.dom.element.clearAllMarkers( database );
-
- editor.forceNextSelectionCheck();
- selection.selectBookmarks( bookmarks );
- }
- };
+ var TRISTATE_DISABLED = CKEDITOR.TRISTATE_DISABLED,
+ TRISTATE_OFF = CKEDITOR.TRISTATE_OFF;
CKEDITOR.plugins.add( 'indent', {
- // TODO: Remove this dependency.
- requires: '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,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%
- hidpi: true, // %REMOVE_LINE_CORE%
- onLoad: function() {
- // [IE6/7] Raw lists are using margin instead of padding for visual indentation in wysiwyg mode. (#3893)
- if ( CKEDITOR.env.ie6Compat || CKEDITOR.env.ie7Compat ) {
- CKEDITOR.addCss( ".cke_editable ul," +
- ".cke_editable ol" +
- "{" +
- " margin-left: 0px;" +
- " padding-left: 40px;" +
- "}" );
- }
- },
+
init: function( editor ) {
- if ( editor.blockless )
- return;
+ var genericDefinition = CKEDITOR.plugins.indent.genericDefinition;
- // Register commands.
- var indent = editor.addCommand( 'indent', new indentCommand( editor, 'indent' ) ),
- outdent = editor.addCommand( 'outdent', new indentCommand( editor, 'outdent' ) );
+ // 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 ) {
- // Register the toolbar buttons.
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( e ) {
- var range = editor.createRange();
- range.setStartBefore( e.data.node );
- range.setEndAfter( e.data.node );
+ 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;
@@ -375,7 +55,7 @@
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() ) {
+ if ( !node.equals( dataNode ) && node.getDirection() ) {
range.setStartAfter( node );
walker = new CKEDITOR.dom.walker( range );
continue;
@@ -384,7 +64,7 @@
// Switch alignment classes.
var classes = editor.config.indentClasses;
if ( classes ) {
- var suffix = ( e.data.dir == 'ltr' ) ? [ '_rtl', '' ] : [ '', '_rtl' ];
+ 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 ] );
@@ -401,9 +81,378 @@
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 generic command definition, mainly responsible for creating indent
+ * UI buttons, and refreshing UI states.
+ *
+ * Commands of this class do not perform any indentation itself. They
+ * delegate 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
+ * related to.
+ * @param {String} name Name of the command.
+ * @param {Boolean} [isIndent] Define command as indenting or outdenting.
+ */
+ genericDefinition: function( isIndent ) {
+ /**
+ * Determines whether the command belongs to indentation family.
+ * Otherwise it's assumed as an outdenting one.
+ *
+ * @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 limited set of elements i.e. indentlist or indentblock.
+ *
+ * Commands of this class perform real indentation and modify DOM structure.
+ * They observe 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
+ * related to.
+ * @param {String} name Name of the command.
+ * @param {Boolean} [isIndent] Define 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 consist
+ * of two functions: `refresh`, `exec` and 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 DOM if possible. Just like
+ * `refresh`, `exec` functions are executed in the order of priorities
+ * while the generic command is executed. This function isn't executed
+ * if `refresh` for this job returned {@link CKEDITOR#TRISTATE_DISABLED}.
+ *
+ * **Note**: This function must return boolean, indicating whether it was successful.
+ * If 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
+ * `setupGenericListeners` function.
+ *
+ * @readonly
+ * @property {Object} [={}]
+ */
+ this.jobs = {};
+
+ /**
+ * Determines whether the editor that command belongs to has
+ * config.enterMode set to 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 indentation family.
+ * Otherwise it's assumed as an outdenting one.
+ *
+ * @readonly
+ * @property {Boolean} [=false]
+ */
+ this.isIndent = !!isIndent;
+
+ /**
+ * The global command's name 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 exec.
+ */
+ this.database = {};
+ },
+
+ /**
+ * Registers content-specific commands as a part of indentation system
+ * directed by generic commands. Once a command is registered,
+ * it observes 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 on generic command's `exec` and
+ * try to execute 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 on generic command's `refresh`
+ * and fill `evt.data.states` object with states of jobs. A generic command
+ * uses these data to determine own state and update UI.
+ *
+ * @member CKEDITOR.plugins.indent
+ * @param {CKEDITOR.editor} editor The editor instance this command is
+ * related 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 `exec` function of a job of the given `priority`
+ * that modifies DOM.
+ *
+ * @param {CKEDITOR.editor} editor The editor instance this command
+ * will be related to.
+ * @param {Number} priority The priority of the job to be executed.
+ * @returns {Boolean} Indicates whether job was successful.
+ */
+ execJob: function( editor, priority ) {
+ var job = this.jobs[ priority ];
+
+ if ( job.state != TRISTATE_DISABLED )
+ return job.exec.call( this, editor );
+ },
+
+ /**
+ * It calls `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 related 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 ];
+
+ job.state = job.refresh.call( this, editor, path );
+
+ return job.state;
+ },
+
+ /**
+ * Method that checks if the element path contains an 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 indentation
+ * system is event-oriented, generic commands communicate with
+ * content-specific commands using `exec` and `refresh` events.
+ *
+ * Listener priorities are crucial. Different indentation phases
+ * are executed whit different priorities.
+ *
+ * For `exec` event:
+ *
+ * * 0: Selection and bookmarks are saved by generic command.
+ * * 1-99: Content-specific commands try to indent the code by executing
+ * own jobs ({@link CKEDITOR.plugins.indent.specificDefinition#jobs}).
+ * * 100: Bookmarks are re-selected by 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 `refresh` event:
+ *
+ * * <100: Content-specific commands refresh their job states according
+ * to the given path. Jobs save their states in `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 jobs with {@link CKEDITOR#TRISTATE_OFF} state,
+ * then the generic command is also {@link CKEDITOR#TRISTATE_OFF}. Otherwise,
+ * the command state is {@link CKEDITOR#TRISTATE_DISABLED}.
+ *
+ * @param {CKEDITOR.command} command 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 );
+ }
})();
/**
@@ -422,15 +471,4 @@
*
* @cfg {String} [indentUnit='px']
* @member CKEDITOR.config
- */
-
-/**
- * List of classes to use for indenting the contents. If it's `null`, no classes will be used
- * and instead the {@link #indentUnit} and {@link #indentOffset} properties will be used.
- *
- * // Use the classes 'Indent1', 'Indent2', 'Indent3'
- * config.indentClasses = ['Indent1', 'Indent2', 'Indent3'];
- *
- * @cfg {Array} [indentClasses=null]
- * @member CKEDITOR.config
- */
+ */
\ No newline at end of file
diff --git a/plugins/indentblock/plugin.js b/plugins/indentblock/plugin.js
new file mode 100644
index 00000000000..b251aaeca72
--- /dev/null
+++ b/plugins/indentblock/plugin.js
@@ -0,0 +1,289 @@
+/**
+ * @license Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.html or http://ckeditor.com/license
+ */
+
+/**
+ * @fileOverview Allows block indentation.
+ */
+
+(function() {
+ 'use strict';
+
+ var $listItem = CKEDITOR.dtd.$listItem,
+ $list = CKEDITOR.dtd.$list,
+ TRISTATE_DISABLED = CKEDITOR.TRISTATE_DISABLED,
+ TRISTATE_OFF = CKEDITOR.TRISTATE_OFF;
+
+ CKEDITOR.plugins.add( 'indentblock', {
+ requires: 'indent',
+ init: function( editor ) {
+ var globalHelpers = CKEDITOR.plugins.indent,
+ classes = editor.config.indentClasses;
+
+ // Register commands.
+ globalHelpers.registerCommands( editor, {
+ indentblock: new commandDefinition( editor, 'indentblock', true ),
+ outdentblock: new commandDefinition( editor, 'outdentblock' )
+ } );
+
+ function commandDefinition( editor, name ) {
+ globalHelpers.specificDefinition.apply( this, arguments );
+
+ this.allowedContent = {
+ 'div h1 h2 h3 h4 h5 h6 ol p pre ul': {
+ // Do not add elements, but only text-align style if element is validated by other rule.
+ propertiesOnly: true,
+ styles: !classes ? 'margin-left,margin-right' : null,
+ classes: classes || null
+ }
+ };
+
+ if ( this.enterBr )
+ this.allowedContent.div = true;
+
+ this.requiredContent = ( this.enterBr ? 'div' : 'p' ) +
+ ( classes ?
+ '(' + classes.join( ',' ) + ')'
+ :
+ '{margin-left}' );
+
+ this.jobs = {
+ '20': {
+ refresh: function( editor, path ) {
+ var firstBlock = path.block || path.blockLimit;
+
+ // Switch context from list item to list
+ // because indentblock can indent entire list
+ // but not a single list element.
+
+ if ( firstBlock.is( $listItem ) )
+ firstBlock = firstBlock.getParent();
+
+ // If firstBlock isn't list item, but still there's
+ // some ascendant (i.e.
), then this is not
+ // a job for indentblock, e.g.:
+ //
+ //
+
+ else if ( firstBlock.getAscendant( $listItem ) )
+ return TRISTATE_DISABLED;
+
+ // [-] Context in the path or ENTER_BR
+ //
+ // Don't try to indent if the element is out of
+ // this plugin's scope. This assertion is omitted
+ // if ENTER_BR is in use since there may be no block
+ // in the path.
+
+ if ( !this.enterBr && !this.getContext( path ) )
+ return TRISTATE_DISABLED;
+
+ else if ( classes ) {
+
+ // [+] Context in the path or ENTER_BR
+ // [+] IndentClasses
+ //
+ // If there are indentation classes, check if reached
+ // the highest level of indentation. If so, disable
+ // the command.
+
+ if ( indentClassLeft.call( this, firstBlock, classes ) )
+ return TRISTATE_OFF;
+ else
+ return TRISTATE_DISABLED;
+ } else {
+
+ // [+] Context in the path or ENTER_BR
+ // [-] IndentClasses
+ // [+] Indenting
+ //
+ // No indent-level limitations due to indent classes.
+ // Indent-like command can always be executed.
+
+ if ( this.isIndent )
+ return TRISTATE_OFF;
+
+ // [+] Context in the path or ENTER_BR
+ // [-] IndentClasses
+ // [-] Indenting
+ // [-] Block in the path
+ //
+ // No block in path. There's no element to apply indentation
+ // so disable the command.
+
+ else if ( !firstBlock )
+ return TRISTATE_DISABLED;
+
+ // [+] Context in the path or ENTER_BR
+ // [-] IndentClasses
+ // [-] Indenting
+ // [+] Block in path.
+ //
+ // Not using indentClasses but there is firstBlock.
+ // We can calculate current indentation level and
+ // try to increase/decrease it.
+
+ else {
+ return CKEDITOR[
+ ( getIndent( firstBlock ) || 0 ) <= 0 ?
+ 'TRISTATE_DISABLED'
+ :
+ 'TRISTATE_OFF' ];
+ }
+ }
+ },
+
+ exec: function( editor ) {
+ var selection = editor.getSelection(),
+ range = selection && selection.getRanges( 1 )[ 0 ],
+ nearestListBlock;
+
+ // If there's some list in the path, then it will be
+ // a full-list indent by increasing or decreasing margin property.
+ if ( ( nearestListBlock = editor.elementPath().contains( $list ) ) )
+ indentElement.call( this, nearestListBlock, classes );
+
+ // If no list in the path, use iterator to indent all the possible
+ // paragraphs in the range, creating them if necessary.
+ else {
+ var iterator = range.createIterator(),
+ enterMode = editor.config.enterMode,
+ block;
+
+ iterator.enforceRealBlocks = true;
+ iterator.enlargeBr = enterMode != CKEDITOR.ENTER_BR;
+
+ while ( ( block = iterator.getNextParagraph( enterMode == CKEDITOR.ENTER_P ? 'p' : 'div' ) ) )
+ indentElement.call( this, block, classes );
+ }
+
+ return true;
+ }
+ }
+ }
+ }
+
+ CKEDITOR.tools.extend( commandDefinition.prototype, globalHelpers.specificDefinition.prototype, {
+ // Elements that, if in an elementpath, will be handled by this
+ // command. They restrict the scope of the plugin.
+ context: { div: 1, dl: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ul: 1, ol: 1, p: 1, pre: 1, table: 1 },
+
+ // A regex built on config#indentClasses to detect whether an
+ // element has some indentClass or not.
+ classNameRegex: classes ?
+ new RegExp( '(?:^|\\s+)(' + classes.join( '|' ) + ')(?=$|\\s)' )
+ :
+ null
+ } );
+ }
+ } );
+
+ // Generic indentation procedure for indentation of any element
+ // either with margin property or config#indentClass.
+ function indentElement( element, classes, dir ) {
+ if ( element.getCustomData( 'indent_processed' ) )
+ return;
+
+ var editor = this.editor,
+ isIndent = this.isIndent;
+
+ if ( classes ) {
+ // Transform current class f to indent step index.
+ var indentClass = element.$.className.match( this.classNameRegex ),
+ indentStep = 0;
+
+ if ( indentClass ) {
+ indentClass = indentClass[ 1 ];
+ indentStep = CKEDITOR.tools.indexOf( classes, indentClass ) + 1;
+ }
+
+ // Operate on indent step index, transform indent step index
+ // back to class name.
+ if ( ( indentStep += isIndent ? 1 : -1 ) < 0 )
+ return;
+
+ indentStep = Math.min( indentStep, classes.length );
+ indentStep = Math.max( indentStep, 0 );
+ element.$.className = CKEDITOR.tools.ltrim( element.$.className.replace( this.classNameRegex, '' ) );
+
+ if ( indentStep > 0 )
+ element.addClass( classes[ indentStep - 1 ] );
+ } else {
+ var indentCssProperty = getIndentCss( element, dir ),
+ currentOffset = parseInt( element.getStyle( indentCssProperty ), 10 ),
+ indentOffset = editor.config.indentOffset || 40;
+
+ if ( isNaN( currentOffset ) )
+ currentOffset = 0;
+
+ currentOffset += ( isIndent ? 1 : -1 ) * indentOffset;
+
+ if ( currentOffset < 0 )
+ return;
+
+ currentOffset = Math.max( currentOffset, 0 );
+ currentOffset = Math.ceil( currentOffset / indentOffset ) * indentOffset;
+
+ element.setStyle( indentCssProperty, currentOffset ?
+ currentOffset + ( editor.config.indentUnit || 'px' )
+ :
+ '' );
+
+ if ( element.getAttribute( 'style' ) === '' )
+ element.removeAttribute( 'style' );
+ }
+
+ CKEDITOR.dom.element.setMarker( this.database, element, 'indent_processed', 1 );
+
+ return;
+ }
+
+ // Method that checks if current indentation level for an element
+ // reached the limit determined by config#indentClasses.
+ function indentClassLeft( node, classes ) {
+ var indentClass = node.$.className.match( this.classNameRegex ),
+ isIndent = this.isIndent;
+
+ // If node has one of the indentClasses:
+ // * If it holds the topmost indentClass, then
+ // no more classes have left.
+ // * If it holds any other indentClass, it can use the next one
+ // or the previous one.
+ // * Outdent is always possible. We can remove indentClass.
+ if ( indentClass )
+ return isIndent ? indentClass[ 1 ] != classes.slice( -1 ) : true;
+
+ // If node has no class which belongs to indentClasses,
+ // then it is at 0-level. It can be indented but not outdented.
+ else
+ return isIndent;
+ }
+
+ // Determines indent CSS property for an element according to
+ // what is the direction of such element. It can be either `margin-left`
+ // or `margin-right`.
+ function getIndentCss( element, dir ) {
+ return ( dir || element.getComputedStyle( 'direction' ) ) == 'ltr' ? 'margin-left' : 'margin-right';
+ }
+
+ // Return the numerical indent value of margin-left|right of an element,
+ // considering element's direction. If element has no margin specified,
+ // NaN is returned.
+ function getIndent( element ) {
+ return parseInt( element.getStyle( getIndentCss( element ) ), 10 );
+ }
+})();
+
+/**
+ * List of classes to use for indenting the contents. If it's `null`, no classes will be used
+ * and instead the {@link #indentUnit} and {@link #indentOffset} properties will be used.
+ *
+ * // Use the classes 'Indent1', 'Indent2', 'Indent3'
+ * config.indentClasses = ['Indent1', 'Indent2', 'Indent3'];
+ *
+ * @cfg {Array} [indentClasses=null]
+ * @member CKEDITOR.config
+ */
diff --git a/plugins/indentlist/plugin.js b/plugins/indentlist/plugin.js
new file mode 100644
index 00000000000..b50cfcf897e
--- /dev/null
+++ b/plugins/indentlist/plugin.js
@@ -0,0 +1,292 @@
+/**
+ * @license Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.html or http://ckeditor.com/license
+ */
+
+/**
+ * @fileOverview Allows list indentation.
+ */
+
+(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 );
+
+ this.allowedContent = { 'ol ul': true };
+ 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 indentList( 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;
+
+ // 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;
+
+ // 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.ie && !li.getFirst( function( node ) {
+ return isNotWhitespaces( node ) && isNotBookmark( node );
+ } ) )
+ li.append( range.document.createText( '\u00a0' ) );
+
+ li.append( followingList );
+ }
+
+ li.insertAfter( parentLiElement );
+ }
+ }
+
+ return true;
+ }
+
+ var selection = editor.getSelection(),
+ ranges = selection && selection.getRanges( 1 ),
+ 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 indentList( nearestListBlock );
+ }
+ }
+
+ // 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' );
+ }
+})();
\ No newline at end of file