2 Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
3 For licensing, see LICENSE.html or http://ckeditor.com/license
7 * @file Increse and decrease indent commands.
12 var listNodeNames
= { ol
: 1, ul
: 1 },
13 isNotWhitespaces
= CKEDITOR
.dom
.walker
.whitespaces( true ),
14 isNotBookmark
= CKEDITOR
.dom
.walker
.bookmark( false, true );
16 function onSelectionChange( evt
)
18 if ( evt
.editor
.readOnly
)
21 var editor
= evt
.editor
,
22 elementPath
= evt
.data
.path
,
23 list
= elementPath
&& elementPath
.contains( listNodeNames
),
24 firstBlock
= elementPath
.block
|| elementPath
.blockLimit
;
27 return this.setState( CKEDITOR
.TRISTATE_OFF
);
29 if ( !this.useIndentClasses
&& this.name
== 'indent' )
30 return this.setState( CKEDITOR
.TRISTATE_OFF
);
33 return this.setState( CKEDITOR
.TRISTATE_DISABLED
);
35 if ( this.useIndentClasses
)
37 var indentClass
= firstBlock
.$.className
.match( this.classNameRegex
),
41 indentClass
= indentClass
[1];
42 indentStep
= this.indentClassMap
[ indentClass
];
44 if ( ( this.name
== 'outdent' && !indentStep
) ||
45 ( this.name
== 'indent' && indentStep
== editor
.config
.indentClasses
.length
) )
46 return this.setState( CKEDITOR
.TRISTATE_DISABLED
);
47 return this.setState( CKEDITOR
.TRISTATE_OFF
);
51 var indent
= parseInt( firstBlock
.getStyle( getIndentCssProperty( firstBlock
) ), 10 );
52 if ( isNaN( indent
) )
55 return this.setState( CKEDITOR
.TRISTATE_DISABLED
);
56 return this.setState( CKEDITOR
.TRISTATE_OFF
);
60 function indentCommand( editor
, name
)
63 this.useIndentClasses
= editor
.config
.indentClasses
&& editor
.config
.indentClasses
.length
> 0;
64 if ( this.useIndentClasses
)
66 this.classNameRegex
= new RegExp( '(?:^|\\s+)(' + editor
.config
.indentClasses
.join( '|' ) + ')(?=$|\\s)' );
67 this.indentClassMap
= {};
68 for ( var i
= 0 ; i
< editor
.config
.indentClasses
.length
; i
++ )
69 this.indentClassMap
[ editor
.config
.indentClasses
[i
] ] = i
+ 1;
72 this.startDisabled
= name
== 'outdent';
75 // Returns the CSS property to be used for identing a given element.
76 function getIndentCssProperty( element
, dir
)
78 return ( dir
|| element
.getComputedStyle( 'direction' ) ) == 'ltr' ? 'margin-left' : 'margin-right';
81 function isListItem( node
)
83 return node
.type
= CKEDITOR
.NODE_ELEMENT
&& node
.is( 'li' );
86 indentCommand
.prototype = {
87 exec : function( editor
)
89 var self
= this, database
= {};
91 function indentList( listNode
)
93 // Our starting and ending points of the range might be inside some blocks under a list item...
94 // So before playing with the iterator, we need to expand the block to include the list items.
95 var startContainer
= range
.startContainer
,
96 endContainer
= range
.endContainer
;
97 while ( startContainer
&& !startContainer
.getParent().equals( listNode
) )
98 startContainer
= startContainer
.getParent();
99 while ( endContainer
&& !endContainer
.getParent().equals( listNode
) )
100 endContainer
= endContainer
.getParent();
102 if ( !startContainer
|| !endContainer
)
105 // Now we can iterate over the individual items on the same tree depth.
106 var block
= startContainer
,
111 if ( block
.equals( endContainer
) )
113 itemsToMove
.push( block
);
114 block
= block
.getNext();
116 if ( itemsToMove
.length
< 1 )
119 // Do indent or outdent operations on the array model of the list, not the
120 // list's DOM tree itself. The array model demands that it knows as much as
121 // possible about the surrounding lists, we need to feed it the further
122 // ancestor node that is still a list.
123 var listParents
= listNode
.getParents( true );
124 for ( var i
= 0 ; i
< listParents
.length
; i
++ )
126 if ( listParents
[i
].getName
&& listNodeNames
[ listParents
[i
].getName() ] )
128 listNode
= listParents
[i
];
132 var indentOffset
= self
.name
== 'indent' ? 1 : -1,
133 startItem
= itemsToMove
[0],
134 lastItem
= itemsToMove
[ itemsToMove
.length
- 1 ];
136 // Convert the list DOM tree into a one dimensional array.
137 var listArray
= CKEDITOR
.plugins
.list
.listToArray( listNode
, database
);
139 // Apply indenting or outdenting on the array.
140 var baseIndent
= listArray
[ lastItem
.getCustomData( 'listarray_index' ) ].indent
;
141 for ( i
= startItem
.getCustomData( 'listarray_index' ); i
<= lastItem
.getCustomData( 'listarray_index' ); i
++ )
143 listArray
[ i
].indent
+= indentOffset
;
144 // Make sure the newly created sublist get a brand-new element of the same type. (#5372)
145 var listRoot
= listArray
[ i
].parent
;
146 listArray
[ i
].parent
= new CKEDITOR
.dom
.element( listRoot
.getName(), listRoot
.getDocument() );
149 for ( i
= lastItem
.getCustomData( 'listarray_index' ) + 1 ;
150 i
< listArray
.length
&& listArray
[i
].indent
> baseIndent
; i
++ )
151 listArray
[i
].indent
+= indentOffset
;
153 // Convert the array back to a DOM forest (yes we might have a few subtrees now).
154 // And replace the old list with the new forest.
155 var newList
= CKEDITOR
.plugins
.list
.arrayToList( listArray
, database
, null, editor
.config
.enterMode
, listNode
.getDirection() );
157 // Avoid nested <li> after outdent even they're visually same,
158 // recording them for later refactoring.(#3982)
159 if ( self
.name
== 'outdent' )
162 if ( ( parentLiElement
= listNode
.getParent() ) && parentLiElement
.is( 'li' ) )
164 var children
= newList
.listNode
.getChildren(),
166 count
= children
.count(),
169 for ( i
= count
- 1 ; i
>= 0 ; i
-- )
171 if ( ( child
= children
.getItem( i
) ) && child
.is
&& child
.is( 'li' ) )
172 pendingLis
.push( child
);
178 newList
.listNode
.replace( listNode
);
180 // Move the nested <li> to be appeared after the parent.
181 if ( pendingLis
&& pendingLis
.length
)
183 for ( i
= 0; i
< pendingLis
.length
; i
++ )
185 var li
= pendingLis
[ i
],
188 // Nest preceding <ul>/<ol> inside current <li> if any.
189 while ( ( followingList
= followingList
.getNext() ) &&
191 followingList
.getName() in listNodeNames
)
193 // IE requires a filler NBSP for nested list inside empty list item,
194 // otherwise the list item will be inaccessiable. (#4476)
195 if ( CKEDITOR
.env
.ie
&& !li
.getFirst( function( node
){ return isNotWhitespaces( node
) && isNotBookmark( node
); } ) )
196 li
.append( range
.document
.createText( '\u00a0' ) );
198 li
.append( followingList
);
201 li
.insertAfter( parentLiElement
);
206 function indentBlock()
208 var iterator
= range
.createIterator(),
209 enterMode
= editor
.config
.enterMode
;
210 iterator
.enforceRealBlocks
= true;
211 iterator
.enlargeBr
= enterMode
!= CKEDITOR
.ENTER_BR
;
213 while ( ( block
= iterator
.getNextParagraph( enterMode
== CKEDITOR
.ENTER_P
? 'p' : 'div' ) ) )
214 indentElement( block
);
217 function indentElement( element
, dir
)
219 if ( element
.getCustomData( 'indent_processed' ) )
222 if ( self
.useIndentClasses
)
224 // Transform current class name to indent step index.
225 var indentClass
= element
.$.className
.match( self
.classNameRegex
),
229 indentClass
= indentClass
[1];
230 indentStep
= self
.indentClassMap
[ indentClass
];
233 // Operate on indent step index, transform indent step index back to class
235 if ( self
.name
== 'outdent' )
240 if ( indentStep
< 0 )
243 indentStep
= Math
.min( indentStep
, editor
.config
.indentClasses
.length
);
244 indentStep
= Math
.max( indentStep
, 0 );
245 element
.$.className
= CKEDITOR
.tools
.ltrim( element
.$.className
.replace( self
.classNameRegex
, '' ) );
246 if ( indentStep
> 0 )
247 element
.addClass( editor
.config
.indentClasses
[ indentStep
- 1 ] );
251 var indentCssProperty
= getIndentCssProperty( element
, dir
),
252 currentOffset
= parseInt( element
.getStyle( indentCssProperty
), 10 );
253 if ( isNaN( currentOffset
) )
255 var indentOffset
= editor
.config
.indentOffset
|| 40;
256 currentOffset
+= ( self
.name
== 'indent' ? 1 : -1 ) * indentOffset
;
258 if ( currentOffset
< 0 )
261 currentOffset
= Math
.max( currentOffset
, 0 );
262 currentOffset
= Math
.ceil( currentOffset
/ indentOffset
) * indentOffset
;
263 element
.setStyle( indentCssProperty
, currentOffset
? currentOffset
+ ( editor
.config
.indentUnit
|| 'px' ) : '' );
264 if ( element
.getAttribute( 'style' ) === '' )
265 element
.removeAttribute( 'style' );
268 CKEDITOR
.dom
.element
.setMarker( database
, element
, 'indent_processed', 1 );
272 var selection
= editor
.getSelection(),
273 bookmarks
= selection
.createBookmarks( 1 ),
274 ranges
= selection
&& selection
.getRanges( 1 ),
278 var iterator
= ranges
.createIterator();
279 while ( ( range
= iterator
.getNextRange() ) )
281 var rangeRoot
= range
.getCommonAncestor(),
282 nearestListBlock
= rangeRoot
;
284 while ( nearestListBlock
&& !( nearestListBlock
.type
== CKEDITOR
.NODE_ELEMENT
&&
285 listNodeNames
[ nearestListBlock
.getName() ] ) )
286 nearestListBlock
= nearestListBlock
.getParent();
288 // Avoid having selection enclose the entire list. (#6138)
289 // [<ul><li>...</li></ul>] =><ul><li>[...]</li></ul>
290 if ( !nearestListBlock
)
292 var selectedNode
= range
.getEnclosedNode();
294 && selectedNode
.type
== CKEDITOR
.NODE_ELEMENT
295 && selectedNode
.getName() in listNodeNames
)
297 range
.setStartAt( selectedNode
, CKEDITOR
.POSITION_AFTER_START
);
298 range
.setEndAt( selectedNode
, CKEDITOR
.POSITION_BEFORE_END
);
299 nearestListBlock
= selectedNode
;
303 // Avoid selection anchors under list root.
304 // <ul>[<li>...</li>]</ul> => <ul><li>[...]</li></ul>
305 if ( nearestListBlock
&& range
.startContainer
.type
== CKEDITOR
.NODE_ELEMENT
306 && range
.startContainer
.getName() in listNodeNames
)
308 var walker
= new CKEDITOR
.dom
.walker( range
);
309 walker
.evaluator
= isListItem
;
310 range
.startContainer
= walker
.next();
313 if ( nearestListBlock
&& range
.endContainer
.type
== CKEDITOR
.NODE_ELEMENT
314 && range
.endContainer
.getName() in listNodeNames
)
316 walker
= new CKEDITOR
.dom
.walker( range
);
317 walker
.evaluator
= isListItem
;
318 range
.endContainer
= walker
.previous();
321 if ( nearestListBlock
)
323 var firstListItem
= nearestListBlock
.getFirst( isListItem
),
324 hasMultipleItems
= !!firstListItem
.getNext( isListItem
),
325 rangeStart
= range
.startContainer
,
326 indentWholeList
= firstListItem
.equals( rangeStart
) || firstListItem
.contains( rangeStart
);
328 // Indent the entire list if cursor is inside the first list item. (#3893)
329 // Only do that for indenting or when using indent classes or when there is something to outdent. (#6141)
330 if ( !( indentWholeList
&&
331 ( self
.name
== 'indent' || self
.useIndentClasses
|| parseInt( nearestListBlock
.getStyle( getIndentCssProperty( nearestListBlock
) ), 10 ) ) &&
332 indentElement( nearestListBlock
, !hasMultipleItems
&& firstListItem
.getDirection() ) ) )
333 indentList( nearestListBlock
);
339 // Clean up the markers.
340 CKEDITOR
.dom
.element
.clearAllMarkers( database
);
342 editor
.forceNextSelectionCheck();
343 selection
.selectBookmarks( bookmarks
);
347 CKEDITOR
.plugins
.add( 'indent',
349 init : function( editor
)
351 // Register commands.
352 var indent
= editor
.addCommand( 'indent', new indentCommand( editor
, 'indent' ) ),
353 outdent
= editor
.addCommand( 'outdent', new indentCommand( editor
, 'outdent' ) );
355 // Register the toolbar buttons.
356 editor
.ui
.addButton( 'Indent',
358 label
: editor
.lang
.indent
,
361 editor
.ui
.addButton( 'Outdent',
363 label
: editor
.lang
.outdent
,
367 // Register the state changing handlers.
368 editor
.on( 'selectionChange', CKEDITOR
.tools
.bind( onSelectionChange
, indent
) );
369 editor
.on( 'selectionChange', CKEDITOR
.tools
.bind( onSelectionChange
, outdent
) );
371 // [IE6/7] Raw lists are using margin instead of padding for visual indentation in wysiwyg mode. (#3893)
372 if ( CKEDITOR
.env
.ie6Compat
|| CKEDITOR
.env
.ie7Compat
)
377 " margin-left: 0px;" +
378 " padding-left: 40px;" +
382 // Register dirChanged listener.
383 editor
.on( 'dirChanged', function( e
)
385 var range
= new CKEDITOR
.dom
.range( editor
.document
);
386 range
.setStartBefore( e
.data
.node
);
387 range
.setEndAfter( e
.data
.node
);
389 var walker
= new CKEDITOR
.dom
.walker( range
),
392 while ( ( node
= walker
.next() ) )
394 if ( node
.type
== CKEDITOR
.NODE_ELEMENT
)
396 // A child with the defined dir is to be ignored.
397 if ( !node
.equals( e
.data
.node
) && node
.getDirection() )
399 range
.setStartAfter( node
);
400 walker
= new CKEDITOR
.dom
.walker( range
);
404 // Switch alignment classes.
405 var classes
= editor
.config
.indentClasses
;
408 var suffix
= ( e
.data
.dir
== 'ltr' ) ? [ '_rtl', '' ] : [ '', '_rtl' ];
409 for ( var i
= 0; i
< classes
.length
; i
++ )
411 if ( node
.hasClass( classes
[ i
] + suffix
[ 0 ] ) )
413 node
.removeClass( classes
[ i
] + suffix
[ 0 ] );
414 node
.addClass( classes
[ i
] + suffix
[ 1 ] );
419 // Switch the margins.
420 var marginLeft
= node
.getStyle( 'margin-right' ),
421 marginRight
= node
.getStyle( 'margin-left' );
423 marginLeft
? node
.setStyle( 'margin-left', marginLeft
) : node
.removeStyle( 'margin-left' );
424 marginRight
? node
.setStyle( 'margin-right', marginRight
) : node
.removeStyle( 'margin-right' );
430 requires
: [ 'domiterator', 'list' ]
435 * Size of each indentation step
436 * @name CKEDITOR.config.indentOffset
440 * config.indentOffset = 4;
444 * Unit for the indentation style
445 * @name CKEDITOR.config.indentUnit
449 * config.indentUnit = 'em';
453 * List of classes to use for indenting the contents. If it's null, no classes will be used
454 * and instead the {@link #indentUnit} and {@link #indentOffset} properties will be used.
455 * @name CKEDITOR.config.indentClasses
459 * // Use the classes 'Indent1', 'Indent2', 'Indent3'
460 * config.indentClasses = ['Indent1', 'Indent2', 'Indent3'];