2 * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
7 * @fileOverview Handles the indentation of lists.
13 var isNotWhitespaces
= CKEDITOR
.dom
.walker
.whitespaces( true ),
14 isNotBookmark
= CKEDITOR
.dom
.walker
.bookmark( false, true ),
15 TRISTATE_DISABLED
= CKEDITOR
.TRISTATE_DISABLED
,
16 TRISTATE_OFF
= CKEDITOR
.TRISTATE_OFF
;
18 CKEDITOR
.plugins
.add( 'indentlist', {
20 init: function( editor
) {
21 var globalHelpers
= CKEDITOR
.plugins
.indent
;
24 globalHelpers
.registerCommands( editor
, {
25 indentlist: new commandDefinition( editor
, 'indentlist', true ),
26 outdentlist: new commandDefinition( editor
, 'outdentlist' )
29 function commandDefinition( editor
) {
30 globalHelpers
.specificDefinition
.apply( this, arguments
);
32 // Require ul OR ol list.
33 this.requiredContent
= [ 'ul', 'ol' ];
35 // Indent and outdent lists with TAB/SHIFT+TAB key. Indenting can
36 // be done for any list item that isn't the first child of the parent.
37 editor
.on( 'key', function( evt
) {
38 if ( editor
.mode
!= 'wysiwyg' )
41 if ( evt
.data
.keyCode
== this.indentKey
) {
42 var list
= this.getContext( editor
.elementPath() );
45 // Don't indent if in first list item of the parent.
46 // Outdent, however, can always be done to collapse
47 // the list into a paragraph (div).
48 if ( this.isIndent
&& CKEDITOR
.plugins
.indentList
.firstItemInPath( this.context
, editor
.elementPath(), list
) )
51 // Exec related global indentation command. Global
52 // commands take care of bookmarks and selection,
53 // so it's much easier to use them instead of
54 // content-specific commands.
55 editor
.execCommand( this.relatedGlobal
);
57 // Cancel the key event so editor doesn't lose focus.
63 // There are two different jobs for this plugin:
65 // * Indent job (priority=10), before indentblock.
67 // This job is before indentblock because, if this plugin is
68 // loaded it has higher priority over indentblock. It means that,
69 // if possible, nesting is performed, and then block manipulation,
72 // * Outdent job (priority=30), after outdentblock.
74 // This job got to be after outdentblock because in some cases
75 // (margin, config#indentClass on list) outdent must be done on
78 this.jobs
[ this.isIndent
? 10 : 30 ] = {
79 refresh: this.isIndent
?
80 function( editor
, path
) {
81 var list
= this.getContext( path
),
82 inFirstListItem
= CKEDITOR
.plugins
.indentList
.firstItemInPath( this.context
, path
, list
);
84 if ( !list
|| !this.isIndent
|| inFirstListItem
)
85 return TRISTATE_DISABLED
;
88 } : function( editor
, path
) {
89 var list
= this.getContext( path
);
91 if ( !list
|| this.isIndent
)
92 return TRISTATE_DISABLED
;
97 exec: CKEDITOR
.tools
.bind( indentList
, this )
101 CKEDITOR
.tools
.extend( commandDefinition
.prototype, globalHelpers
.specificDefinition
.prototype, {
102 // Elements that, if in an elementpath, will be handled by this
103 // command. They restrict the scope of the plugin.
104 context: { ol: 1, ul: 1 }
109 function indentList( editor
) {
111 database
= this.database
,
112 context
= this.context
;
114 function indent( listNode
) {
115 // Our starting and ending points of the range might be inside some blocks under a list item...
116 // So before playing with the iterator, we need to expand the block to include the list items.
117 var startContainer
= range
.startContainer
,
118 endContainer
= range
.endContainer
;
119 while ( startContainer
&& !startContainer
.getParent().equals( listNode
) )
120 startContainer
= startContainer
.getParent();
121 while ( endContainer
&& !endContainer
.getParent().equals( listNode
) )
122 endContainer
= endContainer
.getParent();
124 if ( !startContainer
|| !endContainer
)
127 // Now we can iterate over the individual items on the same tree depth.
128 var block
= startContainer
,
132 while ( !stopFlag
) {
133 if ( block
.equals( endContainer
) )
136 itemsToMove
.push( block
);
137 block
= block
.getNext();
140 if ( itemsToMove
.length
< 1 )
143 // Do indent or outdent operations on the array model of the list, not the
144 // list's DOM tree itself. The array model demands that it knows as much as
145 // possible about the surrounding lists, we need to feed it the further
146 // ancestor node that is still a list.
147 var listParents
= listNode
.getParents( true );
148 for ( var i
= 0; i
< listParents
.length
; i
++ ) {
149 if ( listParents
[ i
].getName
&& context
[ listParents
[ i
].getName() ] ) {
150 listNode
= listParents
[ i
];
155 var indentOffset
= that
.isIndent
? 1 : -1,
156 startItem
= itemsToMove
[ 0 ],
157 lastItem
= itemsToMove
[ itemsToMove
.length
- 1 ],
159 // Convert the list DOM tree into a one dimensional array.
160 listArray
= CKEDITOR
.plugins
.list
.listToArray( listNode
, database
),
162 // Apply indenting or outdenting on the array.
163 baseIndent
= listArray
[ lastItem
.getCustomData( 'listarray_index' ) ].indent
;
165 for ( i
= startItem
.getCustomData( 'listarray_index' ); i
<= lastItem
.getCustomData( 'listarray_index' ); i
++ ) {
166 listArray
[ i
].indent
+= indentOffset
;
167 // Make sure the newly created sublist get a brand-new element of the same type. (#5372)
168 if ( indentOffset
> 0 ) {
169 var listRoot
= listArray
[ i
].parent
;
170 listArray
[ i
].parent
= new CKEDITOR
.dom
.element( listRoot
.getName(), listRoot
.getDocument() );
174 for ( i
= lastItem
.getCustomData( 'listarray_index' ) + 1; i
< listArray
.length
&& listArray
[ i
].indent
> baseIndent
; i
++ )
175 listArray
[ i
].indent
+= indentOffset
;
177 // Convert the array back to a DOM forest (yes we might have a few subtrees now).
178 // And replace the old list with the new forest.
179 var newList
= CKEDITOR
.plugins
.list
.arrayToList( listArray
, database
, null, editor
.config
.enterMode
, listNode
.getDirection() );
181 // Avoid nested <li> after outdent even they're visually same,
182 // recording them for later refactoring.(#3982)
183 if ( !that
.isIndent
) {
185 if ( ( parentLiElement
= listNode
.getParent() ) && parentLiElement
.is( 'li' ) ) {
186 var children
= newList
.listNode
.getChildren(),
188 count
= children
.count(),
191 for ( i
= count
- 1; i
>= 0; i
-- ) {
192 if ( ( child
= children
.getItem( i
) ) && child
.is
&& child
.is( 'li' ) )
193 pendingLis
.push( child
);
199 newList
.listNode
.replace( listNode
);
201 // Move the nested <li> to be appeared after the parent.
202 if ( pendingLis
&& pendingLis
.length
) {
203 for ( i
= 0; i
< pendingLis
.length
; i
++ ) {
204 var li
= pendingLis
[ i
],
207 // Nest preceding <ul>/<ol> inside current <li> if any.
208 while ( ( followingList
= followingList
.getNext() ) && followingList
.is
&& followingList
.getName() in context
) {
209 // IE requires a filler NBSP for nested list inside empty list item,
210 // otherwise the list item will be inaccessiable. (#4476)
211 if ( CKEDITOR
.env
.needsNbspFiller
&& !li
.getFirst( neitherWhitespacesNorBookmark
) )
212 li
.append( range
.document
.createText( '\u00a0' ) );
214 li
.append( followingList
);
217 li
.insertAfter( parentLiElement
);
222 editor
.fire( 'contentDomInvalidated' );
227 var selection
= editor
.getSelection(),
228 ranges
= selection
&& selection
.getRanges(),
229 iterator
= ranges
.createIterator(),
232 while ( ( range
= iterator
.getNextRange() ) ) {
233 var nearestListBlock
= range
.getCommonAncestor();
235 while ( nearestListBlock
&& !( nearestListBlock
.type
== CKEDITOR
.NODE_ELEMENT
&& context
[ nearestListBlock
.getName() ] ) ) {
236 // Avoid having plugin propagate to parent of editor in inline mode by canceling the indentation. (#12796)
237 if ( editor
.editable().equals( nearestListBlock
) ) {
238 nearestListBlock
= false;
241 nearestListBlock
= nearestListBlock
.getParent();
244 // Avoid having selection boundaries out of the list.
245 // <ul><li>[...</li></ul><p>...]</p> => <ul><li>[...]</li></ul><p>...</p>
246 if ( !nearestListBlock
) {
247 if ( ( nearestListBlock
= range
.startPath().contains( context
) ) )
248 range
.setEndAt( nearestListBlock
, CKEDITOR
.POSITION_BEFORE_END
);
251 // Avoid having selection enclose the entire list. (#6138)
252 // [<ul><li>...</li></ul>] =><ul><li>[...]</li></ul>
253 if ( !nearestListBlock
) {
254 var selectedNode
= range
.getEnclosedNode();
255 if ( selectedNode
&& selectedNode
.type
== CKEDITOR
.NODE_ELEMENT
&& selectedNode
.getName() in context
) {
256 range
.setStartAt( selectedNode
, CKEDITOR
.POSITION_AFTER_START
);
257 range
.setEndAt( selectedNode
, CKEDITOR
.POSITION_BEFORE_END
);
258 nearestListBlock
= selectedNode
;
262 // Avoid selection anchors under list root.
263 // <ul>[<li>...</li>]</ul> => <ul><li>[...]</li></ul>
264 if ( nearestListBlock
&& range
.startContainer
.type
== CKEDITOR
.NODE_ELEMENT
&& range
.startContainer
.getName() in context
) {
265 var walker
= new CKEDITOR
.dom
.walker( range
);
266 walker
.evaluator
= listItem
;
267 range
.startContainer
= walker
.next();
270 if ( nearestListBlock
&& range
.endContainer
.type
== CKEDITOR
.NODE_ELEMENT
&& range
.endContainer
.getName() in context
) {
271 walker
= new CKEDITOR
.dom
.walker( range
);
272 walker
.evaluator
= listItem
;
273 range
.endContainer
= walker
.previous();
276 if ( nearestListBlock
)
277 return indent( nearestListBlock
);
282 // Determines whether a node is a list <li> element.
283 function listItem( node
) {
284 return node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.is( 'li' );
287 function neitherWhitespacesNorBookmark( node
) {
288 return isNotWhitespaces( node
) && isNotBookmark( node
);
292 * Global namespace for methods exposed by the Indent List plugin.
297 CKEDITOR
.plugins
.indentList
= {};
300 * Checks whether the first child of the list is in the path.
301 * The list can be extracted from the path or given explicitly
302 * e.g. for better performance if cached.
305 * @param {Object} query See the {@link CKEDITOR.dom.elementPath#contains} method arguments.
306 * @param {CKEDITOR.dom.elementPath} path
307 * @param {CKEDITOR.dom.element} [list]
309 * @member CKEDITOR.plugins.indentList
311 CKEDITOR
.plugins
.indentList
.firstItemInPath = function( query
, path
, list
) {
312 var firstListItemInPath
= path
.contains( listItem
);
314 list
= path
.contains( query
);
316 return list
&& firstListItemInPath
&& firstListItemInPath
.equals( list
.getFirst( listItem
) );