2 * @license Copyright (c) 2003-2017, 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 var path
= editor
.elementPath();
40 if ( editor
.mode
!= 'wysiwyg' )
43 if ( evt
.data
.keyCode
== this.indentKey
) {
44 // Prevent of getting context of empty path (#424)(https://dev.ckeditor.com/ticket/17028).
49 var list
= this.getContext( path
);
52 // Don't indent if in first list item of the parent.
53 // Outdent, however, can always be done to collapse
54 // the list into a paragraph (div).
55 if ( this.isIndent
&& CKEDITOR
.plugins
.indentList
.firstItemInPath( this.context
, path
, list
) )
58 // Exec related global indentation command. Global
59 // commands take care of bookmarks and selection,
60 // so it's much easier to use them instead of
61 // content-specific commands.
62 editor
.execCommand( this.relatedGlobal
);
64 // Cancel the key event so editor doesn't lose focus.
70 // There are two different jobs for this plugin:
72 // * Indent job (priority=10), before indentblock.
74 // This job is before indentblock because, if this plugin is
75 // loaded it has higher priority over indentblock. It means that,
76 // if possible, nesting is performed, and then block manipulation,
79 // * Outdent job (priority=30), after outdentblock.
81 // This job got to be after outdentblock because in some cases
82 // (margin, config#indentClass on list) outdent must be done on
85 this.jobs
[ this.isIndent
? 10 : 30 ] = {
86 refresh: this.isIndent
?
87 function( editor
, path
) {
88 var list
= this.getContext( path
),
89 inFirstListItem
= CKEDITOR
.plugins
.indentList
.firstItemInPath( this.context
, path
, list
);
91 if ( !list
|| !this.isIndent
|| inFirstListItem
)
92 return TRISTATE_DISABLED
;
95 } : function( editor
, path
) {
96 var list
= this.getContext( path
);
98 if ( !list
|| this.isIndent
)
99 return TRISTATE_DISABLED
;
104 exec: CKEDITOR
.tools
.bind( indentList
, this )
108 CKEDITOR
.tools
.extend( commandDefinition
.prototype, globalHelpers
.specificDefinition
.prototype, {
109 // Elements that, if in an elementpath, will be handled by this
110 // command. They restrict the scope of the plugin.
111 context: { ol: 1, ul: 1 }
116 function indentList( editor
) {
118 database
= this.database
,
119 context
= this.context
,
122 function indent( listNode
) {
123 // Our starting and ending points of the range might be inside some blocks under a list item...
124 // So before playing with the iterator, we need to expand the block to include the list items.
125 var startContainer
= range
.startContainer
,
126 endContainer
= range
.endContainer
;
127 while ( startContainer
&& !startContainer
.getParent().equals( listNode
) )
128 startContainer
= startContainer
.getParent();
129 while ( endContainer
&& !endContainer
.getParent().equals( listNode
) )
130 endContainer
= endContainer
.getParent();
132 if ( !startContainer
|| !endContainer
)
135 // Now we can iterate over the individual items on the same tree depth.
136 var block
= startContainer
,
140 while ( !stopFlag
) {
141 if ( block
.equals( endContainer
) )
144 itemsToMove
.push( block
);
145 block
= block
.getNext();
148 if ( itemsToMove
.length
< 1 )
151 // Do indent or outdent operations on the array model of the list, not the
152 // list's DOM tree itself. The array model demands that it knows as much as
153 // possible about the surrounding lists, we need to feed it the further
154 // ancestor node that is still a list.
155 var listParents
= listNode
.getParents( true );
156 for ( var i
= 0; i
< listParents
.length
; i
++ ) {
157 if ( listParents
[ i
].getName
&& context
[ listParents
[ i
].getName() ] ) {
158 listNode
= listParents
[ i
];
163 var indentOffset
= that
.isIndent
? 1 : -1,
164 startItem
= itemsToMove
[ 0 ],
165 lastItem
= itemsToMove
[ itemsToMove
.length
- 1 ],
167 // Convert the list DOM tree into a one dimensional array.
168 listArray
= CKEDITOR
.plugins
.list
.listToArray( listNode
, database
),
170 // Apply indenting or outdenting on the array.
171 baseIndent
= listArray
[ lastItem
.getCustomData( 'listarray_index' ) ].indent
;
173 for ( i
= startItem
.getCustomData( 'listarray_index' ); i
<= lastItem
.getCustomData( 'listarray_index' ); i
++ ) {
174 listArray
[ i
].indent
+= indentOffset
;
175 // Make sure the newly created sublist get a brand-new element of the same type. (http://dev.ckeditor.com/ticket/5372)
176 if ( indentOffset
> 0 ) {
177 var listRoot
= listArray
[ i
].parent
;
178 listArray
[ i
].parent
= new CKEDITOR
.dom
.element( listRoot
.getName(), listRoot
.getDocument() );
182 for ( i
= lastItem
.getCustomData( 'listarray_index' ) + 1; i
< listArray
.length
&& listArray
[ i
].indent
> baseIndent
; i
++ )
183 listArray
[ i
].indent
+= indentOffset
;
185 // Convert the array back to a DOM forest (yes we might have a few subtrees now).
186 // And replace the old list with the new forest.
187 var newList
= CKEDITOR
.plugins
.list
.arrayToList( listArray
, database
, null, editor
.config
.enterMode
, listNode
.getDirection() );
189 // Avoid nested <li> after outdent even they're visually same,
190 // recording them for later refactoring.(http://dev.ckeditor.com/ticket/3982)
191 if ( !that
.isIndent
) {
193 if ( ( parentLiElement
= listNode
.getParent() ) && parentLiElement
.is( 'li' ) ) {
194 var children
= newList
.listNode
.getChildren(),
196 count
= children
.count(),
199 for ( i
= count
- 1; i
>= 0; i
-- ) {
200 if ( ( child
= children
.getItem( i
) ) && child
.is
&& child
.is( 'li' ) )
201 pendingLis
.push( child
);
207 newList
.listNode
.replace( listNode
);
209 // Move the nested <li> to be appeared after the parent.
210 if ( pendingLis
&& pendingLis
.length
) {
211 for ( i
= 0; i
< pendingLis
.length
; i
++ ) {
212 var li
= pendingLis
[ i
],
215 // Nest preceding <ul>/<ol> inside current <li> if any.
216 while ( ( followingList
= followingList
.getNext() ) && followingList
.is
&& followingList
.getName() in context
) {
217 // IE requires a filler NBSP for nested list inside empty list item,
218 // otherwise the list item will be inaccessiable. (http://dev.ckeditor.com/ticket/4476)
219 if ( CKEDITOR
.env
.needsNbspFiller
&& !li
.getFirst( neitherWhitespacesNorBookmark
) )
220 li
.append( range
.document
.createText( '\u00a0' ) );
222 li
.append( followingList
);
225 li
.insertAfter( parentLiElement
);
230 editor
.fire( 'contentDomInvalidated' );
235 var selection
= editor
.getSelection(),
236 ranges
= selection
&& selection
.getRanges(),
237 iterator
= ranges
.createIterator();
239 while ( ( range
= iterator
.getNextRange() ) ) {
240 var nearestListBlock
= range
.getCommonAncestor();
242 while ( nearestListBlock
&& !( nearestListBlock
.type
== CKEDITOR
.NODE_ELEMENT
&& context
[ nearestListBlock
.getName() ] ) ) {
243 // Avoid having plugin propagate to parent of editor in inline mode by canceling the indentation. (http://dev.ckeditor.com/ticket/12796)
244 if ( editor
.editable().equals( nearestListBlock
) ) {
245 nearestListBlock
= false;
248 nearestListBlock
= nearestListBlock
.getParent();
251 // Avoid having selection boundaries out of the list.
252 // <ul><li>[...</li></ul><p>...]</p> => <ul><li>[...]</li></ul><p>...</p>
253 if ( !nearestListBlock
) {
254 if ( ( nearestListBlock
= range
.startPath().contains( context
) ) )
255 range
.setEndAt( nearestListBlock
, CKEDITOR
.POSITION_BEFORE_END
);
258 // Avoid having selection enclose the entire list. (http://dev.ckeditor.com/ticket/6138)
259 // [<ul><li>...</li></ul>] =><ul><li>[...]</li></ul>
260 if ( !nearestListBlock
) {
261 var selectedNode
= range
.getEnclosedNode();
262 if ( selectedNode
&& selectedNode
.type
== CKEDITOR
.NODE_ELEMENT
&& selectedNode
.getName() in context
) {
263 range
.setStartAt( selectedNode
, CKEDITOR
.POSITION_AFTER_START
);
264 range
.setEndAt( selectedNode
, CKEDITOR
.POSITION_BEFORE_END
);
265 nearestListBlock
= selectedNode
;
269 // Avoid selection anchors under list root.
270 // <ul>[<li>...</li>]</ul> => <ul><li>[...]</li></ul>
271 if ( nearestListBlock
&& range
.startContainer
.type
== CKEDITOR
.NODE_ELEMENT
&& range
.startContainer
.getName() in context
) {
272 var walker
= new CKEDITOR
.dom
.walker( range
);
273 walker
.evaluator
= listItem
;
274 range
.startContainer
= walker
.next();
277 if ( nearestListBlock
&& range
.endContainer
.type
== CKEDITOR
.NODE_ELEMENT
&& range
.endContainer
.getName() in context
) {
278 walker
= new CKEDITOR
.dom
.walker( range
);
279 walker
.evaluator
= listItem
;
280 range
.endContainer
= walker
.previous();
283 if ( nearestListBlock
)
284 return indent( nearestListBlock
);
289 // Determines whether a node is a list <li> element.
290 function listItem( node
) {
291 return node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.is( 'li' );
294 function neitherWhitespacesNorBookmark( node
) {
295 return isNotWhitespaces( node
) && isNotBookmark( node
);
299 * Global namespace for methods exposed by the Indent List plugin.
304 CKEDITOR
.plugins
.indentList
= {};
307 * Checks whether the first child of the list is in the path.
308 * The list can be extracted from the path or given explicitly
309 * e.g. for better performance if cached.
312 * @param {Object} query See the {@link CKEDITOR.dom.elementPath#contains} method arguments.
313 * @param {CKEDITOR.dom.elementPath} path
314 * @param {CKEDITOR.dom.element} [list]
316 * @member CKEDITOR.plugins.indentList
318 CKEDITOR
.plugins
.indentList
.firstItemInPath = function( query
, path
, list
) {
319 var firstListItemInPath
= path
.contains( listItem
);
321 list
= path
.contains( query
);
323 return list
&& firstListItemInPath
&& firstListItemInPath
.equals( list
.getFirst( listItem
) );