1 /**
  2  * Add ul and ol command identifier for KISSY Editor.
  3  * @author yiminghe@gmail.com
  4  */
  5 KISSY.add("editor/plugin/listUtils/cmd", function (S, Editor, ListUtils, undefined) {
  6 
  7     var insertUnorderedList = "insertUnorderedList",
  8         insertOrderedList = "insertOrderedList",
  9         listNodeNames = {ol:insertOrderedList, ul:insertUnorderedList},
 10         KER = Editor.RANGE,
 11         ElementPath = Editor.ElementPath,
 12         Walker = Editor.Walker,
 13         UA = S.UA,
 14         Node = S.Node,
 15         DOM = S.DOM,
 16         headerTagRegex = /^h[1-6]$/;
 17 
 18     function ListCommand(type) {
 19         this.type = type;
 20     }
 21 
 22     ListCommand.prototype = {
 23 
 24         changeListType:function (editor, groupObj, database, listsCreated) {
 25             // This case is easy...
 26             // 1. Convert the whole list into a one-dimensional array.
 27             // 2. Change the list type by modifying the array.
 28             // 3. Recreate the whole list by converting the array to a list.
 29             // 4. Replace the original list with the recreated list.
 30             var listArray = ListUtils.listToArray(groupObj.root, database,
 31                     undefined, undefined, undefined),
 32                 selectedListItems = [];
 33 
 34             for (var i = 0; i < groupObj.contents.length; i++) {
 35                 var itemNode = groupObj.contents[i];
 36                 itemNode = itemNode.closest('li', undefined);
 37                 if ((!itemNode || !itemNode[0]) ||
 38                     itemNode.data('list_item_processed'))
 39                     continue;
 40                 selectedListItems.push(itemNode);
 41                 itemNode._4e_setMarker(database, 'list_item_processed', true, undefined);
 42             }
 43 
 44             var fakeParent = new Node(groupObj.root[0].ownerDocument.createElement(this.type));
 45             for (i = 0; i < selectedListItems.length; i++) {
 46                 var listIndex = selectedListItems[i].data('listarray_index');
 47                 listArray[listIndex].parent = fakeParent;
 48             }
 49             var newList = ListUtils.arrayToList(listArray, database, null, "p");
 50             var child, length = newList.listNode.childNodes.length;
 51             for (i = 0; i < length &&
 52                 ( child = new Node(newList.listNode.childNodes[i]) ); i++) {
 53                 if (child.nodeName() == this.type)
 54                     listsCreated.push(child);
 55             }
 56             groupObj.root.before(newList.listNode);
 57             groupObj.root.remove();
 58         },
 59 
 60         createList:function (editor, groupObj, listsCreated) {
 61             var contents = groupObj.contents,
 62                 doc = groupObj.root[0].ownerDocument,
 63                 listContents = [];
 64 
 65             // It is possible to have the contents returned by DomRangeIterator to be the same as the root.
 66             // e.g. when we're running into table cells.
 67             // In such a case, enclose the childNodes of contents[0] into a <div>.
 68             if (contents.length == 1
 69                 && contents[0][0] === groupObj.root[0]) {
 70                 var divBlock = new Node(doc.createElement('div'));
 71                 contents[0][0].nodeType != DOM.TEXT_NODE &&
 72                 contents[0]._4e_moveChildren(divBlock, undefined, undefined);
 73                 contents[0][0].appendChild(divBlock[0]);
 74                 contents[0] = divBlock;
 75             }
 76 
 77             // Calculate the common parent node of all content blocks.
 78             var commonParent = groupObj.contents[0].parent();
 79 
 80             for (var i = 0; i < contents.length; i++) {
 81                 commonParent = commonParent._4e_commonAncestor(contents[i].parent(), undefined);
 82             }
 83 
 84             // We want to insert things that are in the same tree level only,
 85             // so calculate the contents again
 86             // by expanding the selected blocks to the same tree level.
 87             for (i = 0; i < contents.length; i++) {
 88                 var contentNode = contents[i],
 89                     parentNode;
 90                 while (( parentNode = contentNode.parent() )) {
 91                     if (parentNode[0] === commonParent[0]) {
 92                         listContents.push(contentNode);
 93                         break;
 94                     }
 95                     contentNode = parentNode;
 96                 }
 97             }
 98 
 99             if (listContents.length < 1)
100                 return;
101 
102             // Insert the list to the DOM tree.
103             var insertAnchor = new Node(
104                     listContents[ listContents.length - 1 ][0].nextSibling),
105                 listNode = new Node(doc.createElement(this.type));
106 
107             listsCreated.push(listNode);
108             while (listContents.length) {
109                 var contentBlock = listContents.shift(),
110                     listItem = new Node(doc.createElement('li'));
111 
112                 // Preserve heading structure when converting to list item. (#5271)
113                 if (headerTagRegex.test(contentBlock.nodeName())) {
114                     listItem[0].appendChild(contentBlock[0]);
115                 } else {
116                     contentBlock._4e_copyAttributes(listItem, undefined, undefined);
117                     contentBlock._4e_moveChildren(listItem, undefined, undefined);
118                     contentBlock.remove();
119                 }
120                 listNode[0].appendChild(listItem[0]);
121 
122                 // Append a bogus BR to force the LI to render at full height
123                 if (!UA['ie'])
124                     listItem._4e_appendBogus(undefined);
125             }
126             if (insertAnchor[0]) {
127                 listNode.insertBefore(insertAnchor, undefined);
128             } else {
129                 commonParent.append(listNode);
130             }
131         },
132 
133         removeList:function (editor, groupObj, database) {
134             // This is very much like the change list type operation.
135             // Except that we're changing the selected items' indent to -1 in the list array.
136             var listArray = ListUtils.listToArray(groupObj.root, database,
137                     undefined, undefined, undefined),
138                 selectedListItems = [];
139 
140             for (var i = 0; i < groupObj.contents.length; i++) {
141                 var itemNode = groupObj.contents[i];
142                 itemNode = itemNode.closest('li', undefined);
143                 if (!itemNode || itemNode.data('list_item_processed'))
144                     continue;
145                 selectedListItems.push(itemNode);
146                 itemNode._4e_setMarker(database, 'list_item_processed', true, undefined);
147             }
148 
149             var lastListIndex = null;
150 
151             for (i = 0; i < selectedListItems.length; i++) {
152                 var listIndex = selectedListItems[i].data('listarray_index');
153                 listArray[listIndex].indent = -1;
154                 lastListIndex = listIndex;
155             }
156 
157             // After cutting parts of the list out with indent=-1, we still have to maintain the array list
158             // model's nextItem.indent <= currentItem.indent + 1 invariant. Otherwise the array model of the
159             // list cannot be converted back to a real DOM list.
160             for (i = lastListIndex + 1; i < listArray.length; i++) {
161                 //if (listArray[i].indent > listArray[i - 1].indent + 1) {
162                 //modified by yiminghe
163                 if (listArray[i].indent > Math.max(listArray[i - 1].indent, 0)) {
164                     var indentOffset = listArray[i - 1].indent + 1 -
165                         listArray[i].indent;
166                     var oldIndent = listArray[i].indent;
167                     while (listArray[i]
168                         && listArray[i].indent >= oldIndent) {
169                         listArray[i].indent += indentOffset;
170                         i++;
171                     }
172                     i--;
173                 }
174             }
175 
176             var newList = ListUtils.arrayToList(listArray, database, null, "p");
177 
178             // Compensate <br> before/after the list node if the surrounds are non-blocks.(#3836)
179             var docFragment = newList.listNode, boundaryNode, siblingNode;
180 
181             function compensateBrs(isStart) {
182                 if (( boundaryNode = new Node(docFragment[ isStart ? 'firstChild' : 'lastChild' ]) )
183                     && !( boundaryNode[0].nodeType == DOM.ELEMENT_NODE &&
184                     boundaryNode._4e_isBlockBoundary(undefined, undefined) )
185                     && ( siblingNode = groupObj.root[ isStart ? 'prev' : 'next' ]
186                     (Walker.whitespaces(true), 1) )
187                     && !( boundaryNode[0].nodeType == DOM.ELEMENT_NODE &&
188                     siblingNode._4e_isBlockBoundary({ br:1 }, undefined) )) {
189                     boundaryNode[ isStart ? 'before' : 'after' ](editor.get("document")[0].createElement('br'));
190                 }
191             }
192 
193             compensateBrs(true);
194             compensateBrs(undefined);
195             groupObj.root.before(docFragment);
196             groupObj.root.remove();
197         },
198 
199         exec:function (editor) {
200             var selection = editor.getSelection(),
201                 ranges = selection && selection.getRanges();
202 
203             // There should be at least one selected range.
204             if (!ranges || ranges.length < 1)
205                 return;
206 
207 
208             var startElement = selection.getStartElement(),
209                 currentPath = new Editor.ElementPath(startElement);
210 
211             var state = queryActive(this.type, currentPath);
212 
213             var bookmarks = selection.createBookmarks(true);
214 
215             // Group the blocks up because there are many cases where multiple lists have to be created,
216             // or multiple lists have to be cancelled.
217             var listGroups = [],
218                 database = {};
219             while (ranges.length > 0) {
220                 var range = ranges.shift();
221 
222                 var boundaryNodes = range.getBoundaryNodes(),
223                     startNode = boundaryNodes.startNode,
224                     endNode = boundaryNodes.endNode;
225 
226                 if (startNode[0].nodeType == DOM.ELEMENT_NODE && startNode.nodeName() == 'td')
227                     range.setStartAt(boundaryNodes.startNode, KER.POSITION_AFTER_START);
228 
229                 if (endNode[0].nodeType == DOM.ELEMENT_NODE && endNode.nodeName() == 'td')
230                     range.setEndAt(boundaryNodes.endNode, KER.POSITION_BEFORE_END);
231 
232                 var iterator = range.createIterator(),
233                     block;
234 
235                 iterator.forceBrBreak = false;
236 
237                 while (( block = iterator.getNextParagraph() )) {
238 
239                     // Avoid duplicate blocks get processed across ranges.
240                     if (block.data('list_block'))
241                         continue;
242                     else
243                         block._4e_setMarker(database, 'list_block', 1, undefined);
244 
245 
246                     var path = new ElementPath(block),
247                         pathElements = path.elements,
248                         pathElementsCount = pathElements.length,
249                         listNode = null,
250                         processedFlag = false,
251                         blockLimit = path.blockLimit,
252                         element;
253 
254                     // First, try to group by a list ancestor.
255                     //2010-11-17 :
256                     //注意从上往下,从body开始找到最早的list祖先,从那里开始重建!!!
257                     for (var i = pathElementsCount - 1; i >= 0 &&
258                         ( element = pathElements[ i ] ); i--) {
259                         if (listNodeNames[ element.nodeName() ]
260                             && blockLimit.contains(element))     // Don't leak outside block limit (#3940).
261                         {
262                             // If we've encountered a list inside a block limit
263                             // The last group object of the block limit element should
264                             // no longer be valid. Since paragraphs after the list
265                             // should belong to a different group of paragraphs before
266                             // the list. (Bug #1309)
267                             blockLimit.removeData('list_group_object');
268 
269                             var groupObj = element.data('list_group_object');
270                             if (groupObj)
271                                 groupObj.contents.push(block);
272                             else {
273                                 groupObj = { root:element, contents:[ block ] };
274                                 listGroups.push(groupObj);
275                                 element._4e_setMarker(database, 'list_group_object', groupObj, undefined);
276                             }
277                             processedFlag = true;
278                             break;
279                         }
280                     }
281 
282                     if (processedFlag) {
283                         continue;
284                     }
285 
286                     // No list ancestor? Group by block limit.
287                     var root = blockLimit || path.block;
288                     if (root.data('list_group_object')) {
289                         root.data('list_group_object').contents.push(block);
290                     } else {
291                         groupObj = { root:root, contents:[ block ] };
292                         root._4e_setMarker(database, 'list_group_object', groupObj, undefined);
293                         listGroups.push(groupObj);
294                     }
295                 }
296             }
297 
298             // Now we have two kinds of list groups, groups rooted at a list, and groups rooted at a block limit element.
299             // We either have to build lists or remove lists, for removing a list does not makes sense when we are looking
300             // at the group that's not rooted at lists. So we have three cases to handle.
301             var listsCreated = [];
302             while (listGroups.length > 0) {
303                 groupObj = listGroups.shift();
304                 if (!state) {
305                     if (listNodeNames[ groupObj.root.nodeName() ]) {
306                         this.changeListType(editor, groupObj, database, listsCreated);
307                     } else {
308                         //2010-11-17
309                         //先将之前原来元素的 expando 去除,
310                         //防止 ie li 复制原来标签属性带来的输出代码多余
311                         Editor.Utils.clearAllMarkers(database);
312                         this.createList(editor, groupObj, listsCreated);
313                     }
314                 } else if (listNodeNames[ groupObj.root.nodeName() ]) {
315                     this.removeList(editor, groupObj, database);
316                 }
317             }
318 
319             var self = this;
320 
321             // For all new lists created, merge adjacent, same type lists.
322             for (i = 0; i < listsCreated.length; i++) {
323                 listNode = listsCreated[i];
324 
325                 // note by yiminghe,why not use merge sibling directly
326                 // listNode._4e_mergeSiblings();
327                 function mergeSibling(rtl, listNode) {
328                     var sibling = listNode[ rtl ?
329                         'prev' : 'next' ](Walker.whitespaces(true), 1);
330                     if (sibling && sibling[0] &&
331                         sibling.nodeName() == self.type) {
332                         sibling.remove();
333                         // Move children order by merge direction.(#3820)
334                         sibling._4e_moveChildren(listNode, rtl ? true : false, undefined);
335                     }
336                 }
337 
338                 mergeSibling(undefined, listNode);
339                 mergeSibling(true, listNode);
340             }
341 
342             // Clean up, restore selection and update toolbar button states.
343             Editor.Utils.clearAllMarkers(database);
344             selection.selectBookmarks(bookmarks);
345         }
346     };
347 
348     function queryActive(type, elementPath) {
349         var element,
350             name,
351             blockLimit = elementPath.blockLimit,
352             elements = elementPath.elements;
353         if (!blockLimit) {
354             return false;
355         }
356         // Grouping should only happen under blockLimit.(#3940).
357         if (elements) {
358             for (var i = 0; i < elements.length &&
359                 ( element = elements[ i ] ) &&
360                 element[0] !== blockLimit[0];
361                  i++) {
362                 if (listNodeNames[name = element.nodeName()]) {
363                     if (name == type) {
364                         return true;
365                     }
366                 }
367             }
368         }
369         return false;
370     }
371 
372 
373     return {
374         ListCommand:ListCommand,
375         queryActive:queryActive
376     };
377 
378 }, {
379     requires:['editor', '../listUtils/']
380 });