source: gs3-extensions/seaweed-debug/trunk/src/actions/ItemizeAction.js@ 25160

Last change on this file since 25160 was 25160, checked in by sjm84, 12 years ago

Initial cut at a version of seaweed for debugging purposes. Check it out live into the web/ext folder

File size: 28.8 KB
Line 
1/*
2 * file: ItemizeAction.js
3 *
4 * @BEGINLICENSE
5 * Copyright 2010 Brook Novak (email : [email protected])
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 * @ENDLICENSE
18 */
19
20// @DEPENDS: UndoMan
21bootstrap.provides("actions.ItemizeAction");
22
23(function() {
24
25 /*
26 * Lookup map containing element names which when itemized should be converted to a list item rather than being
27 * a descendant of the list items.
28 */
29 var convertContainers = $createLookupMap("p, div");
30
31 _registerAction("Itemize", {
32
33 /**
34 * An undoable itemize action. Creates/converts/destroys a list of items in a given range
35 *
36 * @author Brook Novak
37 *
38 *
39 * @param {Boolean} bullets True to itemize as bullet list, false to itemize as numbered list.
40 *
41 * @param {Node} startNode (Optional) The starting dom node of the range to align.
42 * If not provided then the current selection will be used.
43 * If provieded must also provide endNode
44 *
45 * @param {Node} endNode (Optional) The ending dom node of the range to align. Can be the same as start node
46 * If not provided then the current selection will be used.
47 *
48 */
49 exec : function(bullets, startNode, endNode) {
50
51 // Auto-set range if not provided.
52 if (!startNode) {
53
54 if (!this.selBefore)
55 return; // Nothing to select
56
57 if (this.selBefore.endNode) {
58 startNode = this.selBeforeOrdered.startNode;
59 endNode = this.selBeforeOrdered.endNode;
60 } else
61 startNode = endNode = this.selBefore.startNode;
62
63 }
64
65 debug.assert(endNode, "Supplied start node but not the end node");
66
67 // Perfom execute (recursive operation)
68 (function exec(start, end, destroy, normalizedRange) {
69
70 // Get master container
71 var masterContainer = _findAncestor(
72 _getCommonAncestor(start, end, true),
73 docBody,
74 _isBlockLevel,
75 true
76 ) || docBody, destroyListItems, itemizeContainers;
77
78 // Deepen start/end range
79 var deepStart = start, deepEnd = end;
80 while (deepStart.firstChild) {
81 deepStart = deepStart.firstChild;
82 }
83 while (deepEnd.lastChild) {
84 deepEnd = deepEnd.lastChild;
85 }
86
87 // Handle special case 1: range within tables
88 if (_isTableElement(masterContainer) &&
89 _nodeName(masterContainer) != "td" &&
90 _nodeName(masterContainer) != "th") {
91
92 // Get all the table's cells within the range
93 var tabCells = [];
94 _visitAllNodes(masterContainer, masterContainer, true, function(domNode) {
95 if (domNode.nodeType == Node.ELEMENT_NODE &&
96 (_nodeName(domNode) == "td" || _nodeName(domNode) == "th")) {
97 tabCells.push(domNode);
98 return (domNode != deepEnd && !_isAncestor(domNode, deepEnd));
99 }
100 return domNode != deepEnd;
101 });
102
103 // Check if all cells contains nothing but list items and normalize their contents
104 var areAllListEles = true, normalizedRanges = [];
105 for (var i in tabCells) {
106 var res = areAllListElements(tabCells[i], tabCells[i]);
107 if (res) {
108 areAllListEles &= res.allListEles;
109 normalizedRanges.push(res.nrange);
110 } else normalizedRanges.push(0);
111 }
112
113 // Recurse on table cell
114 for (var i in tabCells) {
115 if (normalizedRanges[i])
116 exec(tabCells[i], tabCells[i], areAllListEles, normalizedRanges[i]);
117 }
118
119 return;
120 }
121
122 // If master container is a list container itself, adjust master container to its parent
123 if (_nodeName(masterContainer) == "ul" || _nodeName(masterContainer) == "ol")
124 masterContainer = masterContainer.parentNode;
125
126 // Handle special case 2: range within a list item
127 var ca = masterContainer;
128 for (var level = 0; level < 2 && ca; level++) {
129 if (_nodeName(ca) == "li") {
130
131 // Determine if need to destroy list item or convert it to a different type
132 if ((normalizedRange && destroy) ||
133 (!normalizedRange &&
134 ((bullets && _nodeName(ca.parentNode) == "ul") ||
135 (!bullets && _nodeName(ca.parentNode) == "ol")))) {
136 destroyListItems = [ca]; // the list item is the target type
137 } else {
138 itemizeContainers = [ca];
139 masterContainer = ca.parentNode;
140 }
141 break;
142
143 }
144 ca = _findAncestor(ca.parentNode, docBody, _isBlockLevel, true);
145 }
146
147 // Check if all containers in range are list-items/list-containers
148 if (!normalizedRange && !destroyListItems && !itemizeContainers) {
149 var res = areAllListElements(start, end);
150
151 if (res) {
152
153 normalizedRange = res.nrange;
154
155 // If the range contains all list elements then destroy them
156 if (res.allListEles)
157 destroyListItems = normalizedRange;
158 else
159 itemizeContainers = normalizedRange;
160
161 } else
162 return;
163
164 } else if (normalizedRange) {
165 // If recursing on a table cell then the destroy/create operation will be predetermined
166 if (destroy)
167 destroyListItems = normalizedRange;
168 else
169 itemizeContainers = normalizedRange;
170 }
171
172 if (destroyListItems) {
173
174 // Tear down all list-items / list-containers.
175 for (var i in destroyListItems) {
176
177 var cont = destroyListItems[i];
178 var contName = _nodeName(cont);
179
180 if (contName == "li") {
181 convertLI(cont, true);
182
183 } else if (contName == "ul" || contName == "ol") {
184
185 // Determine range of list items within list container
186 var li = _isAncestor(cont, start) ? _findAncestor(start, cont) : cont.firstChild,
187 endLI = _isAncestor(cont, end) ? _findAncestor(end, cont) : cont.lastChild;
188
189 while (li) {
190 var removeLI = li;
191 li = li == endLI ? null : li.nextSibling;
192 if (_nodeName(removeLI) == "li") // ensure not white space
193 convertLI(removeLI, true);
194 } // End loop: destroying range of list items in list container
195
196 }
197
198 } // End loop: destroying list items
199
200 } else { // Creation of list items
201
202 debug.assert(itemizeContainers ? true : false);
203
204 // Create the new list container
205 var listContainer;
206
207 // Itemize all containers in range
208 for (var i in itemizeContainers) {
209
210 var cont = itemizeContainers[i];
211 var cRoot = cont == masterContainer ? cont : _findAncestor(cont, masterContainer),
212 contName = _nodeName(cont);
213
214 if (contName == "li") {
215
216 if (i == '0') {
217 listContainer = convertLI(cont, false);
218 } else {
219
220 var liParent = cont.parentNode;
221 _execOp(_Operation.REMOVE_NODE, cont);
222 _execOp(_Operation.INSERT_NODE, cont, listContainer);
223
224 // If the list items container is left without any other list items, remove it
225 var child = liParent.firstChild;
226 while (child && _nodeName(child) != "li") {
227 child = child.nextSibling;
228 }
229
230 if (!child)
231 _execOp(_Operation.REMOVE_NODE, liParent);
232 }
233
234 } else {
235
236 if (i == '0') {
237
238 listContainer = $createElement(bullets ? "ul" : "ol");
239
240 // Determine where to insert the list container
241 if (contName == "ul" || contName == "ol") {
242
243 var shouldInsertBefore = false;
244
245 var firstLI = cont.firstChild;
246 while (firstLI && _nodeName(firstLI) != "li") {
247 shouldInsertBefore |= (firstLI == start); // possible start node is white space
248 firstLI = firstLI.nextSibling;
249 }
250
251 if (firstLI)
252 shouldInsertBefore |= _isAncestor(firstLI, start);
253
254 // Insert the list container before this list container if
255 // the range starts at the first list item.
256 _execOp(_Operation.INSERT_NODE, listContainer, cRoot.parentNode, _indexInParent(cRoot) + (shouldInsertBefore ? 0 : 1));
257
258
259 } else
260 _execOp(_Operation.INSERT_NODE, listContainer, cRoot.parentNode, _indexInParent(cRoot) + 1);
261
262 }
263
264 if (contName == "ul" || contName == "ol") { // List container?
265
266 // Migrate list items in the container within the start/end range
267 var lcChild = _isAncestor(cont, start) ? _findAncestor(start, cont) : cont.firstChild, lcEndChild = _isAncestor(cont, end) ? _findAncestor(end, cont) : cont.lastChild;
268
269 while (lcChild) {
270
271 var migrateLI = _nodeName(lcChild) == "li" ? lcChild : null;
272 lcChild = lcChild == lcEndChild ? null : lcChild.nextSibling;
273
274 // NB: Keeping white space... since undo history might rely on them
275 if (migrateLI) {
276
277 // Migrate the list item into the new list item container
278 _execOp(_Operation.REMOVE_NODE, migrateLI);
279 _execOp(_Operation.INSERT_NODE, migrateLI, listContainer);
280
281 // If the list-container is encapsulated by inline elements before the master container,
282 // then the migrated list items children must also be encapsulated.
283 // TODO: PHASE THIS OUT - SUPPORTS INVLAID HTML
284 if (cRoot != cont) {
285 var liChild = migrateLI.firstChild;
286 while (liChild) { // For all list items immediate children
287 if (liChild.nodeType != Node.ELEMENT_NODE && liChild.nodeType != Node.TEXT_NODE) continue;
288
289 // Create a cloned sub-tree of the list containers inline parents
290 var inode = cont.parentNode, iSubTree = [];
291 while (inode) {
292 iSubTree.push(inode.cloneNode(false));
293 inode = inode == cRoot ? null : inode.parentNode;
294 }
295
296 // Connect up unary inline tree
297 for (var k = iSubTree.length - 1; k > 0; k--) {
298 iSubTree[k].appendChild(iSubTree[k - 1]);
299 }
300
301 // Insert the cloned inline sub tree in the migrated list item
302 // such that it encapsulated this list item child
303 _execOp(_Operation.INSERT_NODE, iSubTree[iSubTree.length - 1], migrateLI, _indexInParent(liChild));
304 _execOp(_Operation.REMOVE_NODE, liChild);
305 _execOp(_Operation.INSERT_NODE, liChild, iSubTree[0]);
306
307 liChild = iSubTree[iSubTree.length - 1].nextSibling;
308 } // End loop: encapsulating list item's contents with inline elements
309 }
310
311 }
312 } // End loop: migrating list items from an existing list container to the new list container
313 // Check if the container should be removed from the document.. if
314 // it no longer contains any list items
315 var shouldRemoveCont = true;
316 lcChild = cont.firstChild;
317 while (lcChild) {
318 if (_nodeName(lcChild) == "li") {
319 shouldRemoveCont = false;
320 break;
321 }
322 lcChild = lcChild.nextSibling;
323 }
324
325 if (shouldRemoveCont)
326 _execOp(_Operation.REMOVE_NODE, cRoot); // Remove by the root
327
328 } else {
329
330 // Create/add a new list item
331 var listItem = $createElement("li");
332 _execOp(_Operation.INSERT_NODE, listItem, listContainer);
333
334 // Migrate contents
335 _execOp(_Operation.REMOVE_NODE, cRoot);
336 _execOp(_Operation.INSERT_NODE, cRoot, listItem);
337
338 // Should this container element be converted into a list item? i.e.
339 // removed from within the list item it was just added to?
340 if (convertContainers[contName]) {
341
342 var parent = cont.parentNode; // Might not be the list container
343
344 // Remove container from the document
345 _execOp(_Operation.REMOVE_NODE, cont);
346
347 // Migrate children into the containers old parent
348 while (cont.firstChild) {
349 var miNode = cont.firstChild;
350 _execOp(_Operation.REMOVE_NODE, miNode);
351 _execOp(_Operation.INSERT_NODE, miNode, parent);
352 }
353
354 }
355
356 }
357
358 }
359
360 } // End loop: itemizing containes in range
361
362 }
363
364 /**
365 * Converts a list item, either migrates its contents up one level or changes the list item type from
366 * numbers to bullets or vice verse.
367 *
368 * @param {Node} liEle A list item ("li") element.
369 * @param {Boolean} destroy True to migrate contents up one level, false to change type.
370 *
371 * @return {Node} If destroy is false, it will return the new list container created for changing the list item type.
372 */
373 function convertLI(liEle, destroy) {
374
375 var sib = liEle.previousSibling,
376 doesHavePreceedingLI = false,
377 doesHaveProceedingLI = false,
378 lcEle = liEle.parentNode,
379 didSplit = false,
380 newLCont = null;
381
382 // Determine if the list item is preceeded with other list items
383 while(!doesHavePreceedingLI && sib) {
384 doesHavePreceedingLI |= _nodeName(sib) == "li";
385 sib = sib.previousSibling;
386 }
387
388 // Determine if the list item is proceeded with other list items
389 sib = liEle.nextSibling
390 while(!doesHaveProceedingLI && sib) {
391 doesHaveProceedingLI |= _nodeName(sib) == "li";
392 sib = sib.nextSibling;
393 }
394
395 if (doesHavePreceedingLI) {
396 lcEle = splitLIContainer(lcEle, liEle, !destroy, true);
397 didSplit = true;
398 }
399
400 if (destroy) {
401
402 // If the list item's contents are not going to be migrated within a lower level list item,
403 // then the range needs to be normalized such that inline groups are place in paragraphs.
404 if (_nodeName(lcEle.parentNode) != "li")
405 _getNormalizedContainerRange(liEle, liEle);
406
407 // Migrate the list item's contents before its list container
408 while (liEle.firstChild) {
409 var liCh = liEle.firstChild;
410 _execOp(_Operation.REMOVE_NODE, liCh);
411 _execOp(_Operation.INSERT_NODE, liCh, lcEle.parentNode, _indexInParent(lcEle));
412 }
413
414 // Remove the list item from its container
415 _execOp(_Operation.REMOVE_NODE, liEle);
416
417 } else { // change type
418
419 if (didSplit) {
420
421 if (doesHaveProceedingLI) {
422 // Get next list item element in split container
423 var nextLI = liEle.nextSibling;
424 while (_nodeName(nextLI) != "li") {
425 nextLI = nextLI.nextSibling;
426 }
427
428 // If previously split the container and migrated some remaining li's .. then split
429 // again on the new container - into a container of the original type.
430 splitLIContainer(lcEle, nextLI, true, true);
431 }
432
433 newLCont = lcEle;
434
435 } else {
436
437 // Insert a new container before and migrate the list item
438 newLCont = splitLIContainer(lcEle, liEle, true, false);
439
440 }
441
442 }
443
444 // Check if the list item container has any list items left
445 sib = lcEle.firstChild;
446 while (sib && _nodeName(sib) != "li") {
447 sib = sib.nextSibling;
448 }
449
450 // If the li element's container is left without a li, remove it from the document...
451 if (!sib) {
452
453 // Remove all ancestors of the container which do not have multiple children to keep html tidy
454 var remEle = lcEle;
455 while (remEle.parentNode.childNodes.length == 1 &&
456 !de.doc.isEditSection(remEle.parentNode) &&
457 remEle.parentNode != docBody) {
458 remEle = remEle.parentNode;
459 }
460
461 _execOp(_Operation.REMOVE_NODE, remEle);
462
463 }
464
465 return newLCont;
466
467 } // End inner function: convertLI
468
469 /**
470 * Splits a list item container in two. Records operation.
471 * @param {Node} lcEle A ul or ol element
472 * @param {Node} liEle An li element to split from within lcEle
473 * @param {Boolean} flipType True to have the split container to have a different type than lcEle
474 * @param {Boolean} downward True to split container from liEle downward - so new container is after lcEle.
475 * False to split container from liEle upward - so new container is before lcEle.
476 *
477 * @return {Node} The split container.
478 */
479 function splitLIContainer(lcEle, liEle, flipType, downward) {
480
481 // Split container in two
482 var splitC = flipType ? $createElement(_nodeName(lcEle) == "ol" ? "ul" : "ol") : lcEle.cloneNode(false);
483
484 // Migrate this and all following li's into the split container
485 if (downward) {
486
487 while (liEle) {
488 var migrateNode = liEle;
489 liEle = liEle.nextSibling;
490 _execOp(_Operation.REMOVE_NODE, migrateNode);
491 _execOp(_Operation.INSERT_NODE, migrateNode, splitC);
492 }
493
494 _execOp(_Operation.INSERT_NODE, splitC, lcEle.parentNode, _indexInParent(lcEle) + 1);
495
496 } else { // Upward
497 while (liEle) {
498 var migrateNode = liEle;
499 liEle = liEle.previousSibling;
500 _execOp(_Operation.REMOVE_NODE, migrateNode);
501 _execOp(_Operation.INSERT_NODE, migrateNode, splitC, 0);
502 }
503
504 _execOp(_Operation.INSERT_NODE, splitC, lcEle.parentNode, _indexInParent(lcEle));
505 }
506
507 return splitC;
508
509 } // End inner function splitLIContainer
510
511
512 /**
513 * Normalizes the given range and checks if all containers are list-items/list-containers of a certain type
514 * (ol or ul)
515 *
516 * @param {Node} start The start of the range to normalize. This range will be deepend
517 * @param {Node} end The end of the range to normalize. This range will be deepend
518 * @return {Object} Either Null if the range does not have containers. Or an object with
519 * a memeber called "allListEles" which is true iff all
520 * containers in the normalized range are list-items/list-containers - and
521 * another memeber "nrange" which contains the normalized range.
522 */
523 function areAllListElements(start, end) {
524
525 // Normalize the range
526 var normalizedRange = _getNormalizedContainerRange(start, end);
527
528 // Anything to itemize?
529 if (normalizedRange.length == 0)
530 return null;
531
532 // Check if range is all list elements
533 var isAllListEles = true;
534 for (var i in normalizedRange) {
535 var cname = _nodeName(normalizedRange[i]);
536
537 // Get list item container name
538 if (cname == "li")
539 cname = _nodeName(normalizedRange[i].parentNode);
540
541 if ((bullets && cname != "ul") || (!bullets && cname != "ol")) {
542 isAllListEles = false;
543 break;
544 }
545 }
546
547 return {allListEles: isAllListEles, nrange: normalizedRange};
548
549 } // End inner areAllListElements
550
551 })(startNode, endNode); // End recursive exec function
552
553 this.selAfter = this.selBefore;
554
555 } // End execute function
556 });
557
558})();
Note: See TracBrowser for help on using the repository browser.