]>
Commit | Line | Data |
---|---|---|
3332bebe IB |
1 | /**\r |
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.\r | |
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license\r | |
4 | */\r | |
5 | \r | |
6 | /**\r | |
7 | * @fileOverview Handles the indentation of lists.\r | |
8 | */\r | |
9 | \r | |
10 | ( function() {\r | |
11 | 'use strict';\r | |
12 | \r | |
13 | var isNotWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),\r | |
14 | isNotBookmark = CKEDITOR.dom.walker.bookmark( false, true ),\r | |
15 | TRISTATE_DISABLED = CKEDITOR.TRISTATE_DISABLED,\r | |
16 | TRISTATE_OFF = CKEDITOR.TRISTATE_OFF;\r | |
17 | \r | |
18 | CKEDITOR.plugins.add( 'indentlist', {\r | |
19 | requires: 'indent',\r | |
20 | init: function( editor ) {\r | |
21 | var globalHelpers = CKEDITOR.plugins.indent;\r | |
22 | \r | |
23 | // Register commands.\r | |
24 | globalHelpers.registerCommands( editor, {\r | |
25 | indentlist: new commandDefinition( editor, 'indentlist', true ),\r | |
26 | outdentlist: new commandDefinition( editor, 'outdentlist' )\r | |
27 | } );\r | |
28 | \r | |
29 | function commandDefinition( editor ) {\r | |
30 | globalHelpers.specificDefinition.apply( this, arguments );\r | |
31 | \r | |
32 | // Require ul OR ol list.\r | |
33 | this.requiredContent = [ 'ul', 'ol' ];\r | |
34 | \r | |
35 | // Indent and outdent lists with TAB/SHIFT+TAB key. Indenting can\r | |
36 | // be done for any list item that isn't the first child of the parent.\r | |
37 | editor.on( 'key', function( evt ) {\r | |
38 | if ( editor.mode != 'wysiwyg' )\r | |
39 | return;\r | |
40 | \r | |
41 | if ( evt.data.keyCode == this.indentKey ) {\r | |
42 | var list = this.getContext( editor.elementPath() );\r | |
43 | \r | |
44 | if ( list ) {\r | |
45 | // Don't indent if in first list item of the parent.\r | |
46 | // Outdent, however, can always be done to collapse\r | |
47 | // the list into a paragraph (div).\r | |
48 | if ( this.isIndent && CKEDITOR.plugins.indentList.firstItemInPath( this.context, editor.elementPath(), list ) )\r | |
49 | return;\r | |
50 | \r | |
51 | // Exec related global indentation command. Global\r | |
52 | // commands take care of bookmarks and selection,\r | |
53 | // so it's much easier to use them instead of\r | |
54 | // content-specific commands.\r | |
55 | editor.execCommand( this.relatedGlobal );\r | |
56 | \r | |
57 | // Cancel the key event so editor doesn't lose focus.\r | |
58 | evt.cancel();\r | |
59 | }\r | |
60 | }\r | |
61 | }, this );\r | |
62 | \r | |
63 | // There are two different jobs for this plugin:\r | |
64 | //\r | |
65 | // * Indent job (priority=10), before indentblock.\r | |
66 | //\r | |
67 | // This job is before indentblock because, if this plugin is\r | |
68 | // loaded it has higher priority over indentblock. It means that,\r | |
69 | // if possible, nesting is performed, and then block manipulation,\r | |
70 | // if necessary.\r | |
71 | //\r | |
72 | // * Outdent job (priority=30), after outdentblock.\r | |
73 | //\r | |
74 | // This job got to be after outdentblock because in some cases\r | |
75 | // (margin, config#indentClass on list) outdent must be done on\r | |
76 | // block-level.\r | |
77 | \r | |
78 | this.jobs[ this.isIndent ? 10 : 30 ] = {\r | |
79 | refresh: this.isIndent ?\r | |
80 | function( editor, path ) {\r | |
81 | var list = this.getContext( path ),\r | |
82 | inFirstListItem = CKEDITOR.plugins.indentList.firstItemInPath( this.context, path, list );\r | |
83 | \r | |
84 | if ( !list || !this.isIndent || inFirstListItem )\r | |
85 | return TRISTATE_DISABLED;\r | |
86 | \r | |
87 | return TRISTATE_OFF;\r | |
88 | } : function( editor, path ) {\r | |
89 | var list = this.getContext( path );\r | |
90 | \r | |
91 | if ( !list || this.isIndent )\r | |
92 | return TRISTATE_DISABLED;\r | |
93 | \r | |
94 | return TRISTATE_OFF;\r | |
95 | },\r | |
96 | \r | |
97 | exec: CKEDITOR.tools.bind( indentList, this )\r | |
98 | };\r | |
99 | }\r | |
100 | \r | |
101 | CKEDITOR.tools.extend( commandDefinition.prototype, globalHelpers.specificDefinition.prototype, {\r | |
102 | // Elements that, if in an elementpath, will be handled by this\r | |
103 | // command. They restrict the scope of the plugin.\r | |
104 | context: { ol: 1, ul: 1 }\r | |
105 | } );\r | |
106 | }\r | |
107 | } );\r | |
108 | \r | |
109 | function indentList( editor ) {\r | |
110 | var that = this,\r | |
111 | database = this.database,\r | |
112 | context = this.context;\r | |
113 | \r | |
114 | function indent( listNode ) {\r | |
115 | // Our starting and ending points of the range might be inside some blocks under a list item...\r | |
116 | // So before playing with the iterator, we need to expand the block to include the list items.\r | |
117 | var startContainer = range.startContainer,\r | |
118 | endContainer = range.endContainer;\r | |
119 | while ( startContainer && !startContainer.getParent().equals( listNode ) )\r | |
120 | startContainer = startContainer.getParent();\r | |
121 | while ( endContainer && !endContainer.getParent().equals( listNode ) )\r | |
122 | endContainer = endContainer.getParent();\r | |
123 | \r | |
124 | if ( !startContainer || !endContainer )\r | |
125 | return false;\r | |
126 | \r | |
127 | // Now we can iterate over the individual items on the same tree depth.\r | |
128 | var block = startContainer,\r | |
129 | itemsToMove = [],\r | |
130 | stopFlag = false;\r | |
131 | \r | |
132 | while ( !stopFlag ) {\r | |
133 | if ( block.equals( endContainer ) )\r | |
134 | stopFlag = true;\r | |
135 | \r | |
136 | itemsToMove.push( block );\r | |
137 | block = block.getNext();\r | |
138 | }\r | |
139 | \r | |
140 | if ( itemsToMove.length < 1 )\r | |
141 | return false;\r | |
142 | \r | |
143 | // Do indent or outdent operations on the array model of the list, not the\r | |
144 | // list's DOM tree itself. The array model demands that it knows as much as\r | |
145 | // possible about the surrounding lists, we need to feed it the further\r | |
146 | // ancestor node that is still a list.\r | |
147 | var listParents = listNode.getParents( true );\r | |
148 | for ( var i = 0; i < listParents.length; i++ ) {\r | |
149 | if ( listParents[ i ].getName && context[ listParents[ i ].getName() ] ) {\r | |
150 | listNode = listParents[ i ];\r | |
151 | break;\r | |
152 | }\r | |
153 | }\r | |
154 | \r | |
155 | var indentOffset = that.isIndent ? 1 : -1,\r | |
156 | startItem = itemsToMove[ 0 ],\r | |
157 | lastItem = itemsToMove[ itemsToMove.length - 1 ],\r | |
158 | \r | |
159 | // Convert the list DOM tree into a one dimensional array.\r | |
160 | listArray = CKEDITOR.plugins.list.listToArray( listNode, database ),\r | |
161 | \r | |
162 | // Apply indenting or outdenting on the array.\r | |
163 | baseIndent = listArray[ lastItem.getCustomData( 'listarray_index' ) ].indent;\r | |
164 | \r | |
165 | for ( i = startItem.getCustomData( 'listarray_index' ); i <= lastItem.getCustomData( 'listarray_index' ); i++ ) {\r | |
166 | listArray[ i ].indent += indentOffset;\r | |
167 | // Make sure the newly created sublist get a brand-new element of the same type. (#5372)\r | |
168 | if ( indentOffset > 0 ) {\r | |
169 | var listRoot = listArray[ i ].parent;\r | |
170 | listArray[ i ].parent = new CKEDITOR.dom.element( listRoot.getName(), listRoot.getDocument() );\r | |
171 | }\r | |
172 | }\r | |
173 | \r | |
174 | for ( i = lastItem.getCustomData( 'listarray_index' ) + 1; i < listArray.length && listArray[ i ].indent > baseIndent; i++ )\r | |
175 | listArray[ i ].indent += indentOffset;\r | |
176 | \r | |
177 | // Convert the array back to a DOM forest (yes we might have a few subtrees now).\r | |
178 | // And replace the old list with the new forest.\r | |
179 | var newList = CKEDITOR.plugins.list.arrayToList( listArray, database, null, editor.config.enterMode, listNode.getDirection() );\r | |
180 | \r | |
181 | // Avoid nested <li> after outdent even they're visually same,\r | |
182 | // recording them for later refactoring.(#3982)\r | |
183 | if ( !that.isIndent ) {\r | |
184 | var parentLiElement;\r | |
185 | if ( ( parentLiElement = listNode.getParent() ) && parentLiElement.is( 'li' ) ) {\r | |
186 | var children = newList.listNode.getChildren(),\r | |
187 | pendingLis = [],\r | |
188 | count = children.count(),\r | |
189 | child;\r | |
190 | \r | |
191 | for ( i = count - 1; i >= 0; i-- ) {\r | |
192 | if ( ( child = children.getItem( i ) ) && child.is && child.is( 'li' ) )\r | |
193 | pendingLis.push( child );\r | |
194 | }\r | |
195 | }\r | |
196 | }\r | |
197 | \r | |
198 | if ( newList )\r | |
199 | newList.listNode.replace( listNode );\r | |
200 | \r | |
201 | // Move the nested <li> to be appeared after the parent.\r | |
202 | if ( pendingLis && pendingLis.length ) {\r | |
203 | for ( i = 0; i < pendingLis.length; i++ ) {\r | |
204 | var li = pendingLis[ i ],\r | |
205 | followingList = li;\r | |
206 | \r | |
207 | // Nest preceding <ul>/<ol> inside current <li> if any.\r | |
208 | while ( ( followingList = followingList.getNext() ) && followingList.is && followingList.getName() in context ) {\r | |
209 | // IE requires a filler NBSP for nested list inside empty list item,\r | |
210 | // otherwise the list item will be inaccessiable. (#4476)\r | |
211 | if ( CKEDITOR.env.needsNbspFiller && !li.getFirst( neitherWhitespacesNorBookmark ) )\r | |
212 | li.append( range.document.createText( '\u00a0' ) );\r | |
213 | \r | |
214 | li.append( followingList );\r | |
215 | }\r | |
216 | \r | |
217 | li.insertAfter( parentLiElement );\r | |
218 | }\r | |
219 | }\r | |
220 | \r | |
221 | if ( newList )\r | |
222 | editor.fire( 'contentDomInvalidated' );\r | |
223 | \r | |
224 | return true;\r | |
225 | }\r | |
226 | \r | |
227 | var selection = editor.getSelection(),\r | |
228 | ranges = selection && selection.getRanges(),\r | |
229 | iterator = ranges.createIterator(),\r | |
230 | range;\r | |
231 | \r | |
232 | while ( ( range = iterator.getNextRange() ) ) {\r | |
233 | var nearestListBlock = range.getCommonAncestor();\r | |
234 | \r | |
235 | while ( nearestListBlock && !( nearestListBlock.type == CKEDITOR.NODE_ELEMENT && context[ nearestListBlock.getName() ] ) ) {\r | |
236 | // Avoid having plugin propagate to parent of editor in inline mode by canceling the indentation. (#12796)\r | |
237 | if ( editor.editable().equals( nearestListBlock ) ) {\r | |
238 | nearestListBlock = false;\r | |
239 | break;\r | |
240 | }\r | |
241 | nearestListBlock = nearestListBlock.getParent();\r | |
242 | }\r | |
243 | \r | |
244 | // Avoid having selection boundaries out of the list.\r | |
245 | // <ul><li>[...</li></ul><p>...]</p> => <ul><li>[...]</li></ul><p>...</p>\r | |
246 | if ( !nearestListBlock ) {\r | |
247 | if ( ( nearestListBlock = range.startPath().contains( context ) ) )\r | |
248 | range.setEndAt( nearestListBlock, CKEDITOR.POSITION_BEFORE_END );\r | |
249 | }\r | |
250 | \r | |
251 | // Avoid having selection enclose the entire list. (#6138)\r | |
252 | // [<ul><li>...</li></ul>] =><ul><li>[...]</li></ul>\r | |
253 | if ( !nearestListBlock ) {\r | |
254 | var selectedNode = range.getEnclosedNode();\r | |
255 | if ( selectedNode && selectedNode.type == CKEDITOR.NODE_ELEMENT && selectedNode.getName() in context ) {\r | |
256 | range.setStartAt( selectedNode, CKEDITOR.POSITION_AFTER_START );\r | |
257 | range.setEndAt( selectedNode, CKEDITOR.POSITION_BEFORE_END );\r | |
258 | nearestListBlock = selectedNode;\r | |
259 | }\r | |
260 | }\r | |
261 | \r | |
262 | // Avoid selection anchors under list root.\r | |
263 | // <ul>[<li>...</li>]</ul> => <ul><li>[...]</li></ul>\r | |
264 | if ( nearestListBlock && range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.getName() in context ) {\r | |
265 | var walker = new CKEDITOR.dom.walker( range );\r | |
266 | walker.evaluator = listItem;\r | |
267 | range.startContainer = walker.next();\r | |
268 | }\r | |
269 | \r | |
270 | if ( nearestListBlock && range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in context ) {\r | |
271 | walker = new CKEDITOR.dom.walker( range );\r | |
272 | walker.evaluator = listItem;\r | |
273 | range.endContainer = walker.previous();\r | |
274 | }\r | |
275 | \r | |
276 | if ( nearestListBlock )\r | |
277 | return indent( nearestListBlock );\r | |
278 | }\r | |
279 | return 0;\r | |
280 | }\r | |
281 | \r | |
282 | // Determines whether a node is a list <li> element.\r | |
283 | function listItem( node ) {\r | |
284 | return node.type == CKEDITOR.NODE_ELEMENT && node.is( 'li' );\r | |
285 | }\r | |
286 | \r | |
287 | function neitherWhitespacesNorBookmark( node ) {\r | |
288 | return isNotWhitespaces( node ) && isNotBookmark( node );\r | |
289 | }\r | |
290 | \r | |
291 | /**\r | |
292 | * Global namespace for methods exposed by the Indent List plugin.\r | |
293 | *\r | |
294 | * @singleton\r | |
295 | * @class\r | |
296 | */\r | |
297 | CKEDITOR.plugins.indentList = {};\r | |
298 | \r | |
299 | /**\r | |
300 | * Checks whether the first child of the list is in the path.\r | |
301 | * The list can be extracted from the path or given explicitly\r | |
302 | * e.g. for better performance if cached.\r | |
303 | *\r | |
304 | * @since 4.4.6\r | |
305 | * @param {Object} query See the {@link CKEDITOR.dom.elementPath#contains} method arguments.\r | |
306 | * @param {CKEDITOR.dom.elementPath} path\r | |
307 | * @param {CKEDITOR.dom.element} [list]\r | |
308 | * @returns {Boolean}\r | |
309 | * @member CKEDITOR.plugins.indentList\r | |
310 | */\r | |
311 | CKEDITOR.plugins.indentList.firstItemInPath = function( query, path, list ) {\r | |
312 | var firstListItemInPath = path.contains( listItem );\r | |
313 | if ( !list )\r | |
314 | list = path.contains( query );\r | |
315 | \r | |
316 | return list && firstListItemInPath && firstListItemInPath.equals( list.getFirst( listItem ) );\r | |
317 | };\r | |
318 | } )();\r |