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