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 != null )
 67 			{
 68 				indentClass = indentClass[1];
 69 				indentStep = this.indentClassMap[ indentClass ];
 70 			}
 71 			if ( ( this.name == 'outdent' && indentStep == 0 ) ||
 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.getComputedStyle( 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 == false )
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 ( var i = startItem.getCustomData( 'listarray_index' ) ; i <= lastItem.getCustomData( 'listarray_index' ) ; i++ )
139 			listArray[i].indent += indentOffset;
140 		for ( var 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, null, null, editor.config.enterMode );
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 		range.enlarge( CKEDITOR.ENLARGE_BLOCK_CONTENTS );
160 		var commonParent = range.getCommonAncestor(),
161 			block;
162
163 		while ( ( block = iterator.getNextParagraph() ) )
164 		{
165 			// We don't want to indent subtrees recursively, so only perform the indent
166 			// operation if the block itself is the nearestParent, or the block's parent
167 			// is the commonParent.
168 			if ( !( block.equals( commonParent ) || block.getParent().equals( commonParent ) ) )
169 				continue;
170
171 			if ( this.useIndentClasses )
172 			{
173 				// Transform current class name to indent step index.
174 				var indentClass = block.$.className.match( this.classNameRegex ),
175 					indentStep = 0;
176 				if ( indentClass != null )
177 				{
178 					indentClass = indentClass[1];
179 					indentStep = this.indentClassMap[ indentClass ];
180 				}
181
182 				// Operate on indent step index, transform indent step index back to class
183 				// name.
184 				if ( this.name == 'outdent' )
185 					indentStep--;
186 				else
187 					indentStep++;
188 				indentStep = Math.min( indentStep, editor.config.indentClasses.length );
189 				indentStep = Math.max( indentStep, 0 );
190 				var className = CKEDITOR.tools.ltrim( block.$.className.replace( this.classNameRegex, '' ) );
191 				if ( indentStep < 1 )
192 					block.$.className = className;
193 				else
194 					block.addClass( editor.config.indentClasses[ indentStep - 1 ] );
195 			}
196 			else
197 			{
198 				var currentOffset = parseInt( block.getComputedStyle( this.indentCssProperty ), 10 );
199 				if ( isNaN( currentOffset ) )
200 					currentOffset = 0;
201 				currentOffset += ( this.name == 'indent' ? 1 : -1 ) * editor.config.indentOffset;
202 				currentOffset = Math.max( currentOffset, 0 );
203 				currentOffset = Math.ceil( currentOffset / editor.config.indentOffset ) * editor.config.indentOffset;
204 				block.setStyle( this.indentCssProperty, currentOffset ? currentOffset + editor.config.indentUnit : '' );
205 				if ( block.getAttribute( 'style' ) == '' )
206 					block.removeAttribute( 'style' );
207 			}
208 		}
209 	}
210
211 	function indentCommand( editor, name )
212 	{
213 		this.name = name;
214 		this.useIndentClasses = editor.config.indentClasses && editor.config.indentClasses.length > 0;
215 		if ( this.useIndentClasses )
216 		{
217 			this.classNameRegex = new RegExp( '(?:^|\\s+)(' + editor.config.indentClasses.join( '|' ) + ')(?=$|\\s)' );
218 			this.indentClassMap = {};
219 			for ( var i = 0 ; i < editor.config.indentClasses.length ; i++ )
220 				this.indentClassMap[ editor.config.indentClasses[i] ] = i + 1;
221 		}
222 		else
223 			this.indentCssProperty = editor.config.contentsLangDirection == 'ltr' ? 'margin-left' : 'margin-right';
224 	}
225
226 	indentCommand.prototype = {
227 		exec : function( editor )
228 		{
229 			var selection = editor.getSelection(),
230 				range = selection && selection.getRanges()[0];
231
232 			if ( !selection || !range )
233 				return;
234
235 			var bookmarks = selection.createBookmarks( true ),
236 				boundaryNodes = range.getBoundaryNodes(),
237 				nearestListBlock = boundaryNodes.startNode.getCommonAncestor( boundaryNodes.endNode );
238
239 			while ( nearestListBlock )
240 			{
241 				if ( nearestListBlock.type == CKEDITOR.NODE_ELEMENT && listNodeNames[ nearestListBlock.getName() ] )
242 					break;
243 				nearestListBlock = nearestListBlock.getParent();
244 			}
245
246 			if ( nearestListBlock )
247 				indentList.call( this, editor, range, nearestListBlock );
248 			else
249 				indentBlock.call( this, editor, range );
250
251 			editor.focus();
252 			editor.forceNextSelectionCheck();
253 			selection.selectBookmarks( bookmarks );
254 		}
255 	};
256
257 	CKEDITOR.plugins.add( 'indent',
258 	{
259 		init : function( editor )
260 		{
261 			// Register commands.
262 			var indent = new indentCommand( editor, 'indent' ),
263 				outdent = new indentCommand( editor, 'outdent' );
264 			editor.addCommand( 'indent', indent );
265 			editor.addCommand( 'outdent', outdent );
266
267 			// Register the toolbar buttons.
268 			editor.ui.addButton( 'Indent',
269 				{
270 					label : editor.lang.indent,
271 					command : 'indent'
272 				});
273 			editor.ui.addButton( 'Outdent',
274 				{
275 					label : editor.lang.outdent,
276 					command : 'outdent'
277 				});
278
279 			// Register the state changing handlers.
280 			editor.on( 'selectionChange', CKEDITOR.tools.bind( onSelectionChange, indent ) );
281 			editor.on( 'selectionChange', CKEDITOR.tools.bind( onSelectionChange, outdent ) );
282 		},
283
284 		requires : [ 'domiterator', 'list' ]
285 	} );
286 })();
287
288 CKEDITOR.tools.extend( CKEDITOR.config,
289 	{
290 		indentOffset : 40,
291 		indentUnit : 'px',
292 		indentClasses : null
293 	});
294