      1 /*
      2 Copyright (c) 2003-2009, CKSource - Frederico Knabben. All rights reserved.
      3 For licensing, see LICENSE.html or http://ckeditor.com/license
      4 */
      5
      6 /**
      7  * @file Increse and decrease indent commands.
      8  */
      9
     10 (function()
     11 {
     12 	var listNodeNames = { ol : 1, ul : 1 };
     13
     14 	function setState( editor, state )
     15 	{
     16 		editor.getCommand( this.name ).setState( state );
     17 	}
     18
     19 	function onSelectionChange( evt )
     20 	{
     21 		var elements = evt.data.path.elements,
     22 			listNode, listItem,
     23 			editor = evt.editor;
     24
     25 		for ( var i = 0 ; i < elements.length ; i++ )
     26 		{
     27 			if ( elements[i].getName() == 'li' )
     28 			{
     29 				listItem = elements[i];
     30 				continue;
     31 			}
     32 			if ( listNodeNames[ elements[i].getName() ] )
     33 			{
     34 				listNode = elements[i];
     35 				break;
     36 			}
     37 		}
     38
     39 		if ( listNode )
     40 		{
     41 			if ( this.name == 'outdent' )
     42 				return setState.call( this, editor, CKEDITOR.TRISTATE_OFF );
     43 			else
     44 			{
     45 				while ( listItem && ( listItem = listItem.getPrevious() ) )
     46 				{
     47 					if ( listItem.getName && listItem.getName() == 'li' )
     48 						return setState.call( this, editor, CKEDITOR.TRISTATE_OFF );
     49 				}
     50 				return setState.call( this, editor, CKEDITOR.TRISTATE_DISABLED );
     51 			}
     52 		}
     53
     54 		if ( !this.useIndentClasses && this.name == 'indent' )
     55 			return setState.call( this, editor, CKEDITOR.TRISTATE_OFF );
     56
     57 		var path = evt.data.path,
     58 			firstBlock = path.block || path.blockLimit;
     59 		if ( !firstBlock )
     60 			return setState.call( this, editor, CKEDITOR.TRISTATE_DISABLED );
     61
     62 		if ( this.useIndentClasses )
     63 		{
     64 			var indentClass = firstBlock.$.className.match( this.classNameRegex ),
     65 				indentStep = 0;
     66 			if ( indentClass )
     67 			{
     68 				indentClass = indentClass[1];
     69 				indentStep = this.indentClassMap[ indentClass ];
     70 			}
     71 			if ( ( this.name == 'outdent' && !indentStep ) ||
     72 					( this.name == 'indent' && indentStep == editor.config.indentClass.length ) )
     73 				return setState.call( this, editor, CKEDITOR.TRISTATE_DISABLED );
     74 			return setState.call( this, editor, CKEDITOR.TRISTATE_OFF );
     75 		}
     76 		else
     77 		{
     78 			var indent = parseInt( firstBlock.getStyle( this.indentCssProperty ), 10 );
     79 			if ( isNaN( indent ) )
     80 				indent = 0;
     81 			if ( indent <= 0 )
     82 				return setState.call( this, editor, CKEDITOR.TRISTATE_DISABLED );
     83 			return setState.call( this, editor, CKEDITOR.TRISTATE_OFF );
     84 		}
     85 	}
     86
     87 	function indentList( editor, range, listNode )
     88 	{
     89 		// Our starting and ending points of the range might be inside some blocks under a list item...
     90 		// So before playing with the iterator, we need to expand the block to include the list items.
     91 		var startContainer = range.startContainer,
     92 			endContainer = range.endContainer;
     93 		while ( startContainer && !startContainer.getParent().equals( listNode ) )
     94 			startContainer = startContainer.getParent();
     95 		while ( endContainer && !endContainer.getParent().equals( listNode ) )
     96 			endContainer = endContainer.getParent();
     97
     98 		if ( !startContainer || !endContainer )
     99 			return;
    100
    101 		// Now we can iterate over the individual items on the same tree depth.
    102 		var block = startContainer,
    103 			itemsToMove = [],
    104 			stopFlag = false;
    105 		while ( !stopFlag )
    106 		{
    107 			if ( block.equals( endContainer ) )
    108 				stopFlag = true;
    109 			itemsToMove.push( block );
    110 			block = block.getNext();
    111 		}
    112 		if ( itemsToMove.length < 1 )
    113 			return;
    114
    115 		// Do indent or outdent operations on the array model of the list, not the
    116 		// list's DOM tree itself. The array model demands that it knows as much as
    117 		// possible about the surrounding lists, we need to feed it the further
    118 		// ancestor node that is still a list.
    119 		var listParents = listNode.getParents();
    120 		for ( var i = 0 ; i < listParents.length ; i++ )
    121 		{
    122 			if ( listParents[i].getName && listNodeNames[ listParents[i].getName() ] )
    123 			{
    124 				listNode = listParents[i];
    125 				break;
    126 			}
    127 		}
    128 		var indentOffset = this.name == 'indent' ? 1 : -1,
    129 			startItem = itemsToMove[0],
    130 			lastItem = itemsToMove[ itemsToMove.length - 1 ],
    131 			database = {};
    132
    133 		// Convert the list DOM tree into a one dimensional array.
    134 		var listArray = CKEDITOR.plugins.list.listToArray( listNode, database );
    135
    136 		// Apply indenting or outdenting on the array.
    137 		var baseIndent = listArray[ lastItem.getCustomData( 'listarray_index' ) ].indent;
    138 		for ( i = startItem.getCustomData( 'listarray_index' ) ; i <= lastItem.getCustomData( 'listarray_index' ) ; i++ )
    139 			listArray[i].indent += indentOffset;
    140 		for ( i = lastItem.getCustomData( 'listarray_index' ) + 1 ;
    141 				i < listArray.length && listArray[i].indent > baseIndent ; i++ )
    142 			listArray[i].indent += indentOffset;
    143
    144 		// Convert the array back to a DOM forest (yes we might have a few subtrees now).
    145 		// And replace the old list with the new forest.
    146 		var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode, 0 );
    147 		if ( newList )
    148 			newList.listNode.replace( listNode );
    149
    150 		// Clean up the markers.
    151 		CKEDITOR.dom.element.clearAllMarkers( database );
    152 	}
    153
    154 	function indentBlock( editor, range )
    155 	{
    156 		var iterator = range.createIterator();
    157 		iterator.enforceRealBlocks = true;
    158
    159 		var block;
    160 		while ( ( block = iterator.getNextParagraph() ) )
    161 		{
    162
    163 			if ( this.useIndentClasses )
    164 			{
    165 				// Transform current class name to indent step index.
    166 				var indentClass = block.$.className.match( this.classNameRegex ),
    167 					indentStep = 0;
    168 				if ( indentClass )
    169 				{
    170 					indentClass = indentClass[1];
    171 					indentStep = this.indentClassMap[ indentClass ];
    172 				}
    173
    174 				// Operate on indent step index, transform indent step index back to class
    175 				// name.
    176 				if ( this.name == 'outdent' )
    177 					indentStep--;
    178 				else
    179 					indentStep++;
    180 				indentStep = Math.min( indentStep, editor.config.indentClasses.length );
    181 				indentStep = Math.max( indentStep, 0 );
    182 				var className = CKEDITOR.tools.ltrim( block.$.className.replace( this.classNameRegex, '' ) );
    183 				if ( indentStep < 1 )
    184 					block.$.className = className;
    185 				else
    186 					block.addClass( editor.config.indentClasses[ indentStep - 1 ] );
    187 			}
    188 			else
    189 			{
    190 				var currentOffset = parseInt( block.getStyle( this.indentCssProperty ), 10 );
    191 				if ( isNaN( currentOffset ) )
    192 					currentOffset = 0;
    193 				currentOffset += ( this.name == 'indent' ? 1 : -1 ) * editor.config.indentOffset;
    194 				currentOffset = Math.max( currentOffset, 0 );
    195 				currentOffset = Math.ceil( currentOffset / editor.config.indentOffset ) * editor.config.indentOffset;
    196 				block.setStyle( this.indentCssProperty, currentOffset ? currentOffset + editor.config.indentUnit : '' );
    197 				if ( block.getAttribute( 'style' ) === '' )
    198 					block.removeAttribute( 'style' );
    199 			}
    200 		}
    201 	}
    202
    203 	function indentCommand( editor, name )
    204 	{
    205 		this.name = name;
    206 		this.useIndentClasses = editor.config.indentClasses && editor.config.indentClasses.length > 0;
    207 		if ( this.useIndentClasses )
    208 		{
    209 			this.classNameRegex = new RegExp( '(?:^|\\s+)(' + editor.config.indentClasses.join( '|' ) + ')(?=$|\\s)' );
    210 			this.indentClassMap = {};
    211 			for ( var i = 0 ; i < editor.config.indentClasses.length ; i++ )
    212 				this.indentClassMap[ editor.config.indentClasses[i] ] = i + 1;
    213 		}
    214 		else
    215 			this.indentCssProperty = editor.config.contentsLangDirection == 'ltr' ? 'margin-left' : 'margin-right';
    216 	}
    217
    218 	indentCommand.prototype = {
    219 		exec : function( editor )
    220 		{
    221 			var selection = editor.getSelection(),
    222 				range = selection && selection.getRanges()[0];
    223
    224 			if ( !selection || !range )
    225 				return;
    226
    227 			var bookmarks = selection.createBookmarks( true ),
    228 				nearestListBlock = range.getCommonAncestor();
    229
    230 			while ( nearestListBlock && !( nearestListBlock.type == CKEDITOR.NODE_ELEMENT &&
    231 				listNodeNames[ nearestListBlock.getName() ] ) )
    232 				nearestListBlock = nearestListBlock.getParent();
    233
    234 			if ( nearestListBlock )
    235 				indentList.call( this, editor, range, nearestListBlock );
    236 			else
    237 				indentBlock.call( this, editor, range );
    238
    239 			editor.focus();
    240 			editor.forceNextSelectionCheck();
    241 			selection.selectBookmarks( bookmarks );
    242 		}
    243 	};
    244
    245 	CKEDITOR.plugins.add( 'indent',
    246 	{
    247 		init : function( editor )
    248 		{
    249 			// Register commands.
    250 			var indent = new indentCommand( editor, 'indent' ),
    251 				outdent = new indentCommand( editor, 'outdent' );
    252 			editor.addCommand( 'indent', indent );
    253 			editor.addCommand( 'outdent', outdent );
    254
    255 			// Register the toolbar buttons.
    256 			editor.ui.addButton( 'Indent',
    257 				{
    258 					label : editor.lang.indent,
    259 					command : 'indent'
    260 				});
    261 			editor.ui.addButton( 'Outdent',
    262 				{
    263 					label : editor.lang.outdent,
    264 					command : 'outdent'
    265 				});
    266
    267 			// Register the state changing handlers.
    268 			editor.on( 'selectionChange', CKEDITOR.tools.bind( onSelectionChange, indent ) );
    269 			editor.on( 'selectionChange', CKEDITOR.tools.bind( onSelectionChange, outdent ) );
    270 		},
    271
    272 		requires : [ 'domiterator', 'list' ]
    273 	} );
    274 })();
    275
    276 CKEDITOR.tools.extend( CKEDITOR.config,
    277 	{
    278 		indentOffset : 40,
    279 		indentUnit : 'px',
    280 		indentClasses : null
    281 	});
    282