diff options
Diffstat (limited to 'sources/plugins/indentlist')
-rw-r--r-- | sources/plugins/indentlist/plugin.js | 318 |
1 files changed, 318 insertions, 0 deletions
diff --git a/sources/plugins/indentlist/plugin.js b/sources/plugins/indentlist/plugin.js new file mode 100644 index 0000000..126af11 --- /dev/null +++ b/sources/plugins/indentlist/plugin.js | |||
@@ -0,0 +1,318 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * @fileOverview Handles the indentation of lists. | ||
8 | */ | ||
9 | |||
10 | ( function() { | ||
11 | 'use strict'; | ||
12 | |||
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; | ||
17 | |||
18 | CKEDITOR.plugins.add( 'indentlist', { | ||
19 | requires: 'indent', | ||
20 | init: function( editor ) { | ||
21 | var globalHelpers = CKEDITOR.plugins.indent; | ||
22 | |||
23 | // Register commands. | ||
24 | globalHelpers.registerCommands( editor, { | ||
25 | indentlist: new commandDefinition( editor, 'indentlist', true ), | ||
26 | outdentlist: new commandDefinition( editor, 'outdentlist' ) | ||
27 | } ); | ||
28 | |||
29 | function commandDefinition( editor ) { | ||
30 | globalHelpers.specificDefinition.apply( this, arguments ); | ||
31 | |||
32 | // Require ul OR ol list. | ||
33 | this.requiredContent = [ 'ul', 'ol' ]; | ||
34 | |||
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' ) | ||
39 | return; | ||
40 | |||
41 | if ( evt.data.keyCode == this.indentKey ) { | ||
42 | var list = this.getContext( editor.elementPath() ); | ||
43 | |||
44 | if ( list ) { | ||
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 ) ) | ||
49 | return; | ||
50 | |||
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 ); | ||
56 | |||
57 | // Cancel the key event so editor doesn't lose focus. | ||
58 | evt.cancel(); | ||
59 | } | ||
60 | } | ||
61 | }, this ); | ||
62 | |||
63 | // There are two different jobs for this plugin: | ||
64 | // | ||
65 | // * Indent job (priority=10), before indentblock. | ||
66 | // | ||
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, | ||
70 | // if necessary. | ||
71 | // | ||
72 | // * Outdent job (priority=30), after outdentblock. | ||
73 | // | ||
74 | // This job got to be after outdentblock because in some cases | ||
75 | // (margin, config#indentClass on list) outdent must be done on | ||
76 | // block-level. | ||
77 | |||
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 ); | ||
83 | |||
84 | if ( !list || !this.isIndent || inFirstListItem ) | ||
85 | return TRISTATE_DISABLED; | ||
86 | |||
87 | return TRISTATE_OFF; | ||
88 | } : function( editor, path ) { | ||
89 | var list = this.getContext( path ); | ||
90 | |||
91 | if ( !list || this.isIndent ) | ||
92 | return TRISTATE_DISABLED; | ||
93 | |||
94 | return TRISTATE_OFF; | ||
95 | }, | ||
96 | |||
97 | exec: CKEDITOR.tools.bind( indentList, this ) | ||
98 | }; | ||
99 | } | ||
100 | |||
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 } | ||
105 | } ); | ||
106 | } | ||
107 | } ); | ||
108 | |||
109 | function indentList( editor ) { | ||
110 | var that = this, | ||
111 | database = this.database, | ||
112 | context = this.context; | ||
113 | |||
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(); | ||
123 | |||
124 | if ( !startContainer || !endContainer ) | ||
125 | return false; | ||
126 | |||
127 | // Now we can iterate over the individual items on the same tree depth. | ||
128 | var block = startContainer, | ||
129 | itemsToMove = [], | ||
130 | stopFlag = false; | ||
131 | |||
132 | while ( !stopFlag ) { | ||
133 | if ( block.equals( endContainer ) ) | ||
134 | stopFlag = true; | ||
135 | |||
136 | itemsToMove.push( block ); | ||
137 | block = block.getNext(); | ||
138 | } | ||
139 | |||
140 | if ( itemsToMove.length < 1 ) | ||
141 | return false; | ||
142 | |||
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 ]; | ||
151 | break; | ||
152 | } | ||
153 | } | ||
154 | |||
155 | var indentOffset = that.isIndent ? 1 : -1, | ||
156 | startItem = itemsToMove[ 0 ], | ||
157 | lastItem = itemsToMove[ itemsToMove.length - 1 ], | ||
158 | |||
159 | // Convert the list DOM tree into a one dimensional array. | ||
160 | listArray = CKEDITOR.plugins.list.listToArray( listNode, database ), | ||
161 | |||
162 | // Apply indenting or outdenting on the array. | ||
163 | baseIndent = listArray[ lastItem.getCustomData( 'listarray_index' ) ].indent; | ||
164 | |||
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() ); | ||
171 | } | ||
172 | } | ||
173 | |||
174 | for ( i = lastItem.getCustomData( 'listarray_index' ) + 1; i < listArray.length && listArray[ i ].indent > baseIndent; i++ ) | ||
175 | listArray[ i ].indent += indentOffset; | ||
176 | |||
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() ); | ||
180 | |||
181 | // Avoid nested <li> after outdent even they're visually same, | ||
182 | // recording them for later refactoring.(#3982) | ||
183 | if ( !that.isIndent ) { | ||
184 | var parentLiElement; | ||
185 | if ( ( parentLiElement = listNode.getParent() ) && parentLiElement.is( 'li' ) ) { | ||
186 | var children = newList.listNode.getChildren(), | ||
187 | pendingLis = [], | ||
188 | count = children.count(), | ||
189 | child; | ||
190 | |||
191 | for ( i = count - 1; i >= 0; i-- ) { | ||
192 | if ( ( child = children.getItem( i ) ) && child.is && child.is( 'li' ) ) | ||
193 | pendingLis.push( child ); | ||
194 | } | ||
195 | } | ||
196 | } | ||
197 | |||
198 | if ( newList ) | ||
199 | newList.listNode.replace( listNode ); | ||
200 | |||
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 ], | ||
205 | followingList = li; | ||
206 | |||
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' ) ); | ||
213 | |||
214 | li.append( followingList ); | ||
215 | } | ||
216 | |||
217 | li.insertAfter( parentLiElement ); | ||
218 | } | ||
219 | } | ||
220 | |||
221 | if ( newList ) | ||
222 | editor.fire( 'contentDomInvalidated' ); | ||
223 | |||
224 | return true; | ||
225 | } | ||
226 | |||
227 | var selection = editor.getSelection(), | ||
228 | ranges = selection && selection.getRanges(), | ||
229 | iterator = ranges.createIterator(), | ||
230 | range; | ||
231 | |||
232 | while ( ( range = iterator.getNextRange() ) ) { | ||
233 | var nearestListBlock = range.getCommonAncestor(); | ||
234 | |||
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; | ||
239 | break; | ||
240 | } | ||
241 | nearestListBlock = nearestListBlock.getParent(); | ||
242 | } | ||
243 | |||
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 ); | ||
249 | } | ||
250 | |||
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; | ||
259 | } | ||
260 | } | ||
261 | |||
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(); | ||
268 | } | ||
269 | |||
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(); | ||
274 | } | ||
275 | |||
276 | if ( nearestListBlock ) | ||
277 | return indent( nearestListBlock ); | ||
278 | } | ||
279 | return 0; | ||
280 | } | ||
281 | |||
282 | // Determines whether a node is a list <li> element. | ||
283 | function listItem( node ) { | ||
284 | return node.type == CKEDITOR.NODE_ELEMENT && node.is( 'li' ); | ||
285 | } | ||
286 | |||
287 | function neitherWhitespacesNorBookmark( node ) { | ||
288 | return isNotWhitespaces( node ) && isNotBookmark( node ); | ||
289 | } | ||
290 | |||
291 | /** | ||
292 | * Global namespace for methods exposed by the Indent List plugin. | ||
293 | * | ||
294 | * @singleton | ||
295 | * @class | ||
296 | */ | ||
297 | CKEDITOR.plugins.indentList = {}; | ||
298 | |||
299 | /** | ||
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. | ||
303 | * | ||
304 | * @since 4.4.6 | ||
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] | ||
308 | * @returns {Boolean} | ||
309 | * @member CKEDITOR.plugins.indentList | ||
310 | */ | ||
311 | CKEDITOR.plugins.indentList.firstItemInPath = function( query, path, list ) { | ||
312 | var firstListItemInPath = path.contains( listItem ); | ||
313 | if ( !list ) | ||
314 | list = path.contains( query ); | ||
315 | |||
316 | return list && firstListItemInPath && firstListItemInPath.equals( list.getFirst( listItem ) ); | ||
317 | }; | ||
318 | } )(); | ||