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 });