source: gs3-extensions/seaweed-debug/trunk/src/Fragment.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: 41.6 KB
Line 
1/*
2 * file: Fragment.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
20bootstrap.provides("Fragment");
21
22/**
23 * @class
24 *
25 * DOMFragment's are used to describe a range of DOM nodes. They are similar
26 * W3C DOM Ranges, except they have a more rich/expressive range and contain information
27 * for reversing operations safely.
28 *
29 * @constructor
30 * @private
31 *
32 * Use de.DOMFragment.buildFragment
33 *
34 * @param {Node} domNode The dom node to wrap
35 * @param {Number} posInParent The position of the domNode in is parent child list.
36 *
37 */
38var _DOMFragment = function() {
39
40
41 var cls = function(domNode, posInParent) {
42
43 /**
44 * The wrapped dom node
45 * @type Node
46 */
47 this.node = domNode;
48
49 /**
50 * A read only member: The position of the domNode in is parent child list.
51 * @type Number
52 */
53 this.pos = posInParent;
54
55 /**
56 * A read only member: The de.dom.DOMFragment's children. Never null.
57 * @type [de.dom.DOMFragment]
58 */
59 this.children = [];
60
61 /**
62 * True indicates that this node has descendants (including self) that are outside of the
63 * fragment range. Flase indicates that the node is completely within the fragments range.
64 *
65 * @type Boolean
66 */
67 this.isShared = false;
68
69 /**
70 * The dom fragments parent. Null for root.
71 *
72 * @type de.dom.DOMFragment
73 */
74 this.parent = null;
75 }
76
77 cls.prototype = {
78
79 /**
80 * Applies a function to nodes in order starting from this node and all its children.
81 *
82 * @param {Function} func A function applied to the nodes. 1 argument is given: a visited node.
83 * The traversing will be stopped by returning false.
84 */
85 visit: function(func){
86
87 if (func(this) === false)
88 return false;
89
90 for (var i in this.children) {
91 if (!this.children[i].visit(func))
92 return false;
93 }
94
95 return true;
96 },
97
98
99
100 /**
101 *
102 * Removes the fragment from the document.
103 *
104 */
105 disconnect: function(){
106 this.visit(function(currentFrag) {
107 if (!currentFrag.isShared)
108 removeFragment(currentFrag, false);
109 });
110 },
111
112
113 /**
114 * An extension of de.dom.DOMFragment#disconnect. This routine furthermore collapses nodes after the
115 * disconnection: where nodes the have been left in the document -- within the fragment range --
116 * are removed/copied/moved in a way that a typical word-proccessor would behave.
117 * <br><br>
118 * The fragment must not be disconnected prior to this call.
119 *
120 * @return {Node} The first migrated node. Null if nothing was migrated.
121 */
122 collapse : function() {
123
124 // Get the first common ancestor between the start and end fragments... this may not be the root.
125 var firstCommonAncestor = _getCommonAncestor(this.getStartFragment().node, this.getEndFragment().node, false),
126 fcaFrag = this,
127 stopMigratingSiblings = false, // used for inner function
128 fragRoot = this, // used for inner function
129 hasCollapsibleBoundAncestor = false; // used for inner function
130
131 while(fcaFrag.node != firstCommonAncestor) {
132 fcaFrag = fcaFrag.children[0];
133 }
134
135 // Remove the fragments inclusive range from the document
136 fragRoot.disconnect();
137
138 // Insert any placeholders that needs placing
139 insertPlaceholders();
140
141 // Remove all migratable nodes and build migration trees
142 var migrations = migratePath(fcaFrag, true, nextMigrationPoint(fragRoot.getStartFragment())),
143 i, j;
144
145 // Add migration trees into their migration points
146 for (i in migrations) {
147
148 if (migrations[i].migrantRoots.length > 0) {
149
150 // Discover the insertion index in the migration point
151 var mPointNode = migrations[i].migrationPoint.node,
152 insertIndex = migrations[i].migrationPoint.children[0].pos, // Start-bound's index
153 containsNonPHTangable = false,
154 seenPlaceholder = 0;
155
156 // If the start-bound child is still in the document, then increase the index
157 if (isInDocument(migrations[i].migrationPoint.children[0].node))
158 insertIndex++;
159
160 // Insert the migrants
161 for (j=0; j<migrations[i].migrantRoots.length; j++) {
162 _execOp(_Operation.INSERT_NODE, migrations[i].migrantRoots[j], mPointNode, insertIndex + j);
163 }
164
165 // Check to see if migration point has any redundant placeholders, which may have been uneccessarely added,
166 // or just moved, via the collapse operation.
167
168 // First check if there are any tangable descendants of the migration point which are not
169 // immediate children that are placeholders...
170 if (_isPlaceholderCandidate(mPointNode)) {
171 _visitAllNodes(mPointNode, mPointNode, true, function(node){
172 if (node == mPointNode) return;
173 if (!(de.doc.isMNPlaceHolder(node, false) &&
174 (node.nodeType == Node.TEXT_NODE ? node.parentNode.parentNode == mPointNode : node.parentNode == mPointNode)) &&
175 de.cursor.getPlacementFlags(node)) {
176 containsNonPHTangable = true;
177 return false;
178 }
179
180 });
181 }
182
183 // Scan through the migration points immediate nodes
184 for (var k = 0; k < mPointNode.childNodes.length; k++) {
185
186 var domNode = mPointNode.childNodes[k];
187
188 if (de.doc.isMNPlaceHolder(domNode, true) || de.doc.isESPlaceHolder(domNode, true)) {
189
190 // If we have already seen a placeholder in this migration point, or the migration point is not
191 // a placeholder candidate, or the migration point contains tangable nodes that are not immediate
192 // children who are placeholders... then this placeholder is redundant
193 if (seenPlaceholder || !_isPlaceholderCandidate(mPointNode) || containsNonPHTangable)
194 _execOp(_Operation.REMOVE_NODE, domNode); // Remove it from the document
195
196 seenPlaceholder = 1;
197 }
198 }
199 }
200 }
201
202 // Return the first migrated node
203 return migrations.length > 0 && migrations[0].migrantRoots.length > 0 ? migrations[0].migrantRoots[0] : null;
204
205 /**
206 * Removes/clones nodes from the document and creates migration trees -- paired with their
207 * migration points, for which they should be appended to.
208 *
209 * @param {de.dom.DOMFragment} pathRoot The top-most fragment of the path to travel up to.
210 * @param {Object} isEndBoundPath True iff the path is the end-boundry path
211 * @param {de.dom.DOMFragment} curMigPoint The current migration point
212 * @return {[Object]} The migrations to make
213 */
214 function migratePath(pathRoot, isEndBoundPath, curMigPoint) {
215
216 var curMigration = {
217 /*
218 * A migration point is a dom node in the start-bound path where nodes in the
219 * end bound path can move(migrate) to.
220 */
221 migrationPoint : curMigPoint,
222
223 /*
224 * Migration roots are disconnected dom-trees which should be connected with the
225 * migration point in this instance by adding as children ... which is done outside
226 * of this function scope.
227 */
228 migrantRoots : []
229 }
230
231 var migrations = [curMigration];
232
233 // Follow a path starting from the end node and moving up to the path's root
234 for (
235 var fragment = isEndBoundPath ? pathRoot.getEndFragment() : pathRoot.getStartFragment();
236 fragment != (isEndBoundPath ? pathRoot : pathRoot.parent);
237 fragment = fragment.parent) {
238
239 var domNode, nextNode;
240
241 // Check to see if this fragment has an ancestor who is collapsible and is on the end-bound path.
242 if (isEndBoundPath) {
243 hasCollapsibleBoundAncestor = false;
244 for (var f = fragment.parent; f != pathRoot; f = f.parent) {
245 if (f.isShared && isCollapsible(f.node)) {
246 hasCollapsibleBoundAncestor = true;
247 break;
248 }
249 }
250 }
251
252 if (!isEndBoundPath || fragment.isShared) {
253
254 domNode = fragment.node;
255 nextNode = domNode.nextSibling;
256
257 // Should this node be removed? I.E: Is the node collapsable?
258 if (isCollapsible(domNode)) {
259
260 stopMigratingSiblings = true;
261
262 // All descendants of a collapsiable node on the bound path should be migrated
263 debug.assert(!isEndBoundPath || (isEndBoundPath && !domNode.firstChild));
264
265 // Remove this collapsible node if it is on the end-bound path and is shared
266 if (isEndBoundPath)
267 _execOp(_Operation.REMOVE_NODE, fragment.node);
268
269 }
270
271 // Check to see if this node should be migrated, or should stay
272 if (!isEndBoundPath || canDirectlyMigrate(domNode)) {
273
274 // Check if need to remove this migrant or duplicate it
275 if (domNode.firstChild)
276 domNode = domNode.cloneNode(false);
277 else
278 _execOp(_Operation.REMOVE_NODE, domNode);
279
280 // Is the current migration point valid?
281 while (curMigration.migrationPoint != fcaFrag
282 && !_isValidRelationship(domNode, curMigration.migrationPoint.node)
283 && _nodeName(domNode) != "div") {
284 // One exception: don't allow migration of div element: the generic container
285
286 // Search for next valid migration point...
287 curMigration.migrationPoint = nextMigrationPoint(curMigration.migrationPoint);
288 }
289
290 // Add this node to all migrations
291 for (var i = 0; i < migrations.length; i++) {
292
293 // Link up dom node to its children
294 for (var j in migrations[i].migrantRoots) {
295 _execOp(_Operation.INSERT_NODE, migrations[i].migrantRoots[j], domNode);
296 }
297
298 // Set the new roots for this tree-level
299 migrations[i].migrantRoots = [domNode];
300
301 // Duplicate node for next migration point
302 if (i < (migrations.length - 1))
303 domNode = domNode.cloneNode(false);
304
305 }
306
307 } else if (!isCollapsible(domNode)) { // Fragment in boundry path that cannot be migrated
308
309 stopMigratingSiblings = true;
310 if (!domNode.firstChild)
311 // The un-migratible node has been left childless, remove it from the document
312 _execOp(_Operation.REMOVE_NODE, fragment.node);
313 }
314
315 } else { // Is on end bound path and fragment not shared and has been disconnected. i.e. completely removed from document
316
317 // Determine next node...
318
319 // If the parent has been disconnected, then there is no use searching through the next siblings
320 if (!fragment.parent.isShared)
321 continue;
322
323 // See if this fragments parent is part of the starting bound
324 var startBound = fragRoot;
325 while (startBound.children.length > 0 && startBound != fragment.parent) {
326 startBound = startBound.children[0];
327 }
328
329 if (startBound == fragment.parent) {
330 debug.assert(startBound.children.length > 0);
331
332 // If the start bound path shares the same parent for the current fragment
333 // (which is on the end path), then the next sibling is not neccesarily the first
334 // remaining child in this fragment's parent.
335 nextNode = null;
336 if (!startBound.insertedPH) { // Watch out for these, since they are added on the fly, not in fragment structure
337
338 if (isInDocument(startBound.children[0].node)) {
339 // The first child of the start bound is in the document
340 nextNode = startBound.node.childNodes.length > (startBound.children[0].pos + 1) ?
341 startBound.node.childNodes[startBound.children[0].pos + 1] : null;
342 } else {
343 // The first child of the start bound has been removed
344 nextNode = startBound.node.childNodes.length > startBound.children[0].pos ?
345 startBound.node.childNodes[startBound.children[0].pos] : null;
346 }
347
348 }
349
350 // Get the first remainding child in this disconnected fragment's parent
351 } else nextNode = fragment.parent.node.firstChild; // May be null/not exist
352
353 }
354
355 // If the parent cannot be migrated from the end-bounds path, then don't migrate it's children to
356 // the left of the end-bounds.
357 if (isEndBoundPath && !canDirectlyMigrate(fragment.parent.node) && !isCollapsible(fragment.parent.node))
358 stopMigratingSiblings = true;
359
360 // If this fragment is for the starting iteration of a new migrant fragment, then avoid recursing into
361 // it's sibling dom nodes that are not in a fragment yet... this will be handled afterwards
362 else if (!isEndBoundPath && fragment == pathRoot)
363 continue;
364
365 // Get this fragments child-index in it's parent
366 var nextFragIndex = fragment.getIndexInParent();
367
368 // Set/reset the migration index for this fragments parent. Only applicable on the bound path
369 if (isEndBoundPath) fragment.parent.migrantIndex = 0;
370
371 while (nextNode && (hasCollapsibleBoundAncestor || !stopMigratingSiblings)) { // For each adjacent path
372
373 domNode = nextNode; // For bound path, valid for non-bound path
374 nextNode = domNode.nextSibling; // For bound path, valid for non-bound path
375 nextFragIndex++; // For non-bound path
376
377 var adjacentMigrations;
378
379 if (isEndBoundPath) {
380
381 var migrantFragRoot;
382
383 // If this operation is being repeated, then get the migration tree created from the first time
384 // this operation was performed
385 if (fragment.parent.migrants && fragment.parent.migrants.length > fragment.parent.migrantIndex) {
386
387 migrantFragRoot = fragment.parent.migrants[fragment.parent.migrantIndex];
388
389 } else { // First time this operation has been performed
390
391 // Build a fragment to capture the structure of the current document's state
392 migrantFragRoot = _buildFragment(
393 domNode.parentNode,
394 domNode,
395 0,
396 domNode,
397 _nodeLength(domNode, 1));
398
399 // Link it to the parent fragment
400 if (!fragment.parent.migrants) fragment.parent.migrants = [];
401 fragment.parent.migrants.push(migrantFragRoot);
402 }
403
404 fragment.parent.migrantIndex++;
405
406 adjacentMigrations = migratePath(migrantFragRoot.children[0], false, migrations[migrations.length-1].migrationPoint); // Recurse
407
408 } else { // Fragment already created...
409
410 adjacentMigrations = migratePath(
411 fragment.parent.children[nextFragIndex],
412 false,
413 migrations[migrations.length-1].migrationPoint); // Recurse
414 }
415
416 // Merge the adjacent migrations into the current migrations
417 for (var i in adjacentMigrations) {
418 if (adjacentMigrations[i].migrantRoots.length == 0) continue;
419
420 // Search for a matching migration point
421 var wasMerged = false;
422 for (var j in migrations) {
423
424 if (migrations[j].migrationPoint == adjacentMigrations[i].migrationPoint) {
425 // Append the adjacent migrant roots for this migration point (at the current tree-level)
426 migrations[j].migrantRoots = migrations[j].migrantRoots.concat(adjacentMigrations[i].migrantRoots);
427 wasMerged = true;
428 break;
429 }
430 }
431
432 // Must be a new migration point... add to the current set of migration points
433 if (!wasMerged) migrations.push(adjacentMigrations[i]);
434
435 }
436
437 // Keep the current migration updated
438 curMigration = migrations[migrations.length-1]; // TODO: NEEDED?
439
440 } // End loop: recursing over adjacent paths
441
442 } // End loop: travelling up path to root
443
444 return migrations;
445 } // End inner migratePath
446
447 /**
448 * Looks up the start-bound path from the given migration point.
449 * @param {_Fragment} currentPoint Must not be the first Common Ancestor - must be on the start bound path.
450 * @return {_Fragment} The next migration point
451 */
452 function nextMigrationPoint(currentPoint) {
453
454 debug.assert(currentPoint != fcaFrag);
455
456 do {
457
458 currentPoint = currentPoint.parent;
459
460 } while (currentPoint != fcaFrag && !(
461
462 // currentPoint.isShared && // NOTE: Cannot used isShared to determine if exists in document... since cna be re-inserted
463 isInDocument(currentPoint.node) && /* i.e: shared nodes or re-inserted nodes due to placeholders */
464 isCollapsible(currentPoint.node)));
465
466 return currentPoint;
467 } // End inner nextMigrationPoint
468
469 /**
470 * @param {Node} domNode
471 * @return {Boolean} True if domNode is classed as "collapsible"
472 */
473 function isCollapsible(domNode) {
474 return _isGenericBlockLevel(domNode) || _nodeName(domNode) == "li";
475 } // End inner isCollapsible
476
477 /**
478 * @param {Node} domNode
479 * @return {Boolean} True if domNode can directly be migrated from the boundry path into a migration point
480 */
481 function canDirectlyMigrate(domNode) {
482 return domNode.nodeType == Node.TEXT_NODE || _isInlineLevel(domNode);
483 } // End inner canDirectlyMigrate
484
485 function isInDocument(domNode) {
486 return _isAncestor(docBody, domNode);
487 }
488
489 /**
490 * If the start-bound path contains any placeholder candidates either left without tangable nodes
491 * or are fully disconnected, then placeholders are inserted / fragments are reconnected
492 */
493 function insertPlaceholders() {
494
495 for (var fragment = fragRoot.getStartFragment();
496 fragment != null;
497 fragment = fragment.parent ? fragment.parent : null) {
498
499 var domRef = fragment.node;
500
501 if (_isPlaceholderCandidate(domRef) || de.doc.isEditSection(domRef)) {
502
503 // Is this still in the document?
504 if (fragment.isShared) {
505
506 var phType = 0
507 if (_doesNeedESPlaceholder(domRef))
508 phType = 1;
509 else if (_doesNeedMNPlaceholder(domRef))
510 phType = 2;
511
512 if (phType) {
513
514 // The disconnection has left a placeholder candidate in the document without a tangable node.
515
516 // Create a new placeholder and add it
517 var ph = phType == 1 ?
518 de.doc.createESPlaceholder(domRef) :
519 de.doc.createMNPlaceholder();
520
521 _execOp(_Operation.INSERT_NODE, ph, domRef);
522
523 // Mark that this fragment inserted a placeholder
524 fragment.insertedPH = 1;
525 }
526
527 } else { // disconnected
528
529 // Get rid of any children that were part of the diconnection
530 while (domRef.firstChild) {
531 _execOp(_Operation.REMOVE_NODE, domRef.firstChild);
532 }
533
534 // Create a new placeholder and add it
535 _execOp(_Operation.INSERT_NODE, de.doc.createMNPlaceholder(), domRef);
536
537 // Mark that this fragment inserted a placeholder
538 fragment.insertedPH = 1;
539
540 // Link this and all it's disconnect ancestors back into the document...
541 while (!fragment.isShared) {
542
543 if (fragment.parent.isShared) {
544
545 // Reconnect this with it's parent
546 _execOp(_Operation.INSERT_NODE, fragment.node, fragment.parent.node, fragment.pos);
547
548 } else {
549
550 // Make sure the parent has all its children removed
551 var parentDomNode = fragment.parent.node;
552 while(parentDomNode.firstChild) {
553 _execOp(_Operation.REMOVE_NODE, parentDomNode.firstChild);
554 }
555
556 // Reconnect this with it's parent
557 _execOp(_Operation.INSERT_NODE, fragment.node, parentDomNode);
558
559
560
561 }
562
563 if(!fragment.parent) break;
564 fragment = fragment.parent;
565 }
566
567 }
568
569 break; // No need to search ancestors for inserting placeholders as this point
570
571 }
572 }
573 }
574
575 },
576
577
578 /**
579 * @return {de.dom.DOMFragment} The start fragment of the fragment's range.
580 */
581 getStartFragment : function() {
582 var startFrag = this;
583 while (startFrag.children.length > 0) {
584 startFrag = startFrag.children[0];
585 }
586 return startFrag;
587 },
588
589 /**
590 * @return {de.dom.DOMFragment} The end fragment of the fragment's range.
591 */
592 getEndFragment : function() {
593 var endFrag = this;
594 while (endFrag.children.length > 0) {
595 endFrag = endFrag.children[endFrag.children.length - 1];
596 }
597 return endFrag;
598 },
599
600 /**
601 * @return {Integer} The zero-based index of this fragment in it's parent. Null if this is a root fragment.
602 */
603 getIndexInParent : function() {
604
605 if (!this.parent) return null;
606
607 var index = 0;
608 while (this != this.parent.children[index]) {
609 index++;
610 }
611 return index;
612 },
613
614 /**
615 * @return {Boolean} True if the start node was a split text node, so that it has a
616 * preceeding text node which was the orginal node, false otherwise.
617 */
618 wasStartSplit : function() {
619 return this.getStartFragment().preSplitNode ? true : false;
620 },
621 /**
622 * @return {Boolean} True if the end node was a split text node, so that it has a
623 * proceeding text node which was the new split node, false otherwise.
624 */
625 wasEndSplit : function() {
626 return this.getEndFragment().postSplitNode ? true : false;
627 },
628
629 /**
630 * @return {Node} The previous text node which will split at the start fragment.
631 * Null if this framgent didn't split the start point
632 */
633 getPreSplitNode : function() {
634 var startFrag = this.getStartFragment();
635 return startFrag.preSplitNode ? startFrag.preSplitNode : null;
636 },
637
638 /**
639 * @return {Node} The following text node which will split at the end fragment.
640 * Null if this framgent didn't split the end point
641 */
642 getPostSplitNode : function() {
643 var endFrag = this.getEndFragment();
644 return endFrag.postSplitNode ? endFrag.postSplitNode : null;
645 },
646
647 /**
648 * Gets the adjusted node/index of the given tuple to point to a valid position in the DOM
649 * which they may have been left pointing to invalid indexes due to the construction of this fragment.
650 *
651 * @param {Object} node
652 *
653 * @param {Object} index
654 *
655 */
656 getAdjustedNodeIndex : function(node, index) {
657
658 // Check if node belongs in the formatting fragments' split text nodes, and see if it's index is out of bounds
659 if (node.nodeType == Node.TEXT_NODE && index >= _nodeLength(node)) {
660
661 var startFrag = this.getStartFragment(),
662 endFrag = this.getEndFragment();
663
664 // Was the start node split? And did the node previously point to this split node?
665 if (startFrag.wasStartSplit()) {
666 var preSplitNode = startFrag.getPreSplitNode();
667 if (node == preSplitNode) {
668 index -= _nodeLength(preSplitNode);
669 node = startFrag.node;
670 }
671 }
672
673 // Was the end node split? And did the node previously point to the split node?
674 if (endFrag.wasEndSplit() && node == endFrag.node && index >= _nodeLength(node)) {
675 index -= _nodeLength(endFrag.node);
676 node = endFrag.getPostSplitNode();
677 }
678 }
679
680 return {node:node,index:index};
681
682 },
683
684 /**
685 * Gets the original node/index values before this fragment was built.
686 *
687 * @param {Object} node
688 * @param {Object} index
689 */
690 getOriginalNodeIndex : function(node, index) {
691
692 if (node.nodeType == Node.TEXT_NODE) {
693
694 var startFrag = this.getStartFragment(),
695 endFrag = this.getEndFragment();
696
697 // Was the end node split? And does the node point to the added split node?
698 if (node == endFrag.getPostSplitNode()) {
699 index += _nodeLength(endFrag.node);
700 node = endFrag.node;
701 }
702
703 // Was the start node split? And does the node point to the added split node?
704 if (startFrag.wasStartSplit() && node == startFrag.node) {
705 var preSplitNode = startFrag.getPreSplitNode();
706 index += _nodeLength(preSplitNode);
707 node = preSplitNode;
708 }
709 }
710
711 return {node:node,index:index};
712 }
713
714 };
715
716 // @DEBUG ON
717 // Add tostring method for debugging
718 cls.prototype.toString = function() {
719 return _nodeName(this.node) + (this.isShared ? "[SHARED]" : "[NOT-SHARED]");
720 }
721 // @DEBUG OFF
722
723 /**
724 * Removes the fragments dom node from the document.
725 *
726 * @param {de.dom.DOMFragment} fragment
727 * @param {Boolean} forceRemoveDoc Set to true to always remove the fragments node from the document.
728 * Set to false to only remove if the fragment has a shared parent.
729 */
730 function removeFragment(fragment, forceRemoveDoc) {
731 if (forceRemoveDoc || (fragment.parent && fragment.parent.isShared))
732 _execOp(_Operation.REMOVE_NODE, fragment.node);
733 }
734
735 return cls;
736
737}();
738
739/**
740 * Builds a fragment.
741 * <br><br>
742 * If startNode / endNode are the same, then startIndex / endIndex must be different.
743 * If startNode or endNodes are placeholders, they are extended to include the whole placeholder in the range.
744 * <br>
745 * The start node/index tuple must occur before the end node/index tuple when traversing inorder.
746 * <br>
747 * The range will always been extended to the deepest descendants.
748 *
749 * @param {Node} commonAncestor (Optional) A common ancestor of the start and end nodes, can be as high as possible.
750 * If null then the closest common ancestor will be used.
751 *
752 * @param {Node} startNode The starting dom node of the fragments range.
753 *
754 * @param {Number} startIndex The inclusive start index in the start node.
755 * Ranges from 0 to the text length for text nodes.
756 * Where 0 indicates that the range begins at the first char, and text length
757 * indicates that the range begins directly after the text node, but not including it.
758 * <br>
759 * Ranges from 0 to 1 for elements.
760 * Where 0 indicates that the range includes the element and its decendants,
761 * and 1 indicates that the range excludes the element and its decendants.
762 *
763 *
764 * @param {Node} endNode The ending dom node of the fragments range.
765 *
766 * @param {Number} endIndex The inclusive end index in the end node.
767 * Ranges from 0 to the text length for text nodes.
768 * Where 0 indicates that the range ends just before the text node, but not including it,
769 * and text length indicates that the range ends at the last charactor in the text run.
770 * <br>
771 * Ranges from 0 to 1 for elements.
772 * Where 0 indicates that the range excludes the element and its decendants,
773 * and 1 indicates that the range includes the element and its decendants.
774 *
775 * @return {de.dom.DOMFragment} The fragments root FragmentNode, which will always be the commonAncestor. Never null.
776 */
777function _buildFragment(commonAncestor, startNode, startIndex, endNode, endIndex) {
778
779 debug.assert(startNode != endNode || startIndex < endIndex, "Invalid range");
780
781 debug.assert(
782 !(startNode.nodeType == Node.TEXT_NODE && (startIndex < 0 || startIndex > _nodeLength(startNode))),
783 "Start index out of range"
784 );
785
786 debug.assert(
787 !(startNode.nodeType == Node.ELEMENT_NODE && (startIndex < 0 || startIndex > 1)),
788 "Start index out of range"
789 );
790
791 debug.assert(
792 !(endNode.nodeType == Node.TEXT_NODE && (endIndex < 0 || endIndex > _nodeLength(endNode))),
793 "End index out of range"
794 );
795
796 debug.assert(
797 !(endNode.nodeType == Node.ELEMENT_NODE && (endIndex < 0 || endIndex > 1)),
798 "End index out of range"
799 );
800
801 debug.assert(
802 !(endNode == startNode && endIndex == startIndex),
803 "Invalid range"
804 );
805
806 // Set common ancestor if not given
807 if (!commonAncestor)
808 commonAncestor = _getCommonAncestor(startNode, endNode);
809
810 // Make sure the range extends to the deepest descendants
811 var adjustBoundry = false;
812 while (startNode.firstChild) {
813 startNode = startIndex == 0 ? startNode.firstChild : startNode.lastChild;
814 adjustBoundry = true;
815 }
816 if (adjustBoundry && startIndex > 0)
817 startIndex = _nodeLength(startNode, 1);
818
819 adjustBoundry = false;
820 while (endNode.firstChild) {
821 endNode = endIndex == 0 ? endNode.firstChild : endNode.lastChild;
822 adjustBoundry = true;
823 }
824 if (adjustBoundry && endIndex > 0)
825 endIndex = _nodeLength(endNode, 1);
826
827 // Make sure placeholders are fully included within the inclusive range
828 if (de.doc.isMNPlaceHolder(startNode) || de.doc.isESPlaceHolder(startNode))
829 startIndex = 0;
830
831 if (de.doc.isMNPlaceHolder(endNode) || de.doc.isESPlaceHolder(endNode))
832 endIndex = _nodeLength(endNode, 1);
833
834 // @DEBUG ON
835 // Check that end occurs after the start
836 var foundEnd = false;
837 _visitAllNodes(commonAncestor, startNode, true, function(domNode) {
838 foundEnd = domNode == endNode;
839 return !foundEnd;
840 });
841 debug.assert(foundEnd, "Invalid range: end node not found after start node");
842 // @DEBUG OFF
843
844 var affectedNodes = [];
845
846 // Get all the nodes between the start and end nodes.
847 _visitAllNodes(commonAncestor, startNode, true, function(node){
848 affectedNodes.push(node);
849 return node != endNode;
850 });
851
852 // Add the missing nodes to the start of the array
853 var ancestors = _getAncestors(startNode, commonAncestor, false, true);
854 ancestors.reverse();
855 affectedNodes = ancestors.concat(affectedNodes);
856
857 // to traverse the nodes inorder from the common ancestor through
858 // the start and end nodes range, just iterate forwards on the array:
859 var fragRoot;
860
861 for (var i in affectedNodes) {
862
863 // Build up a fragment by traversing through the affected nodes in order
864 var node = affectedNodes[i], preSplitNode = 0, postSplitNode = 0;
865
866 // Perform split operations on boundry text nodes
867 if ((node == startNode || node == endNode) && node.nodeType == Node.TEXT_NODE) {
868
869 // Because the first/last nodes are text nodes, we may have to split them up.
870 if (node == startNode && startIndex > 0 && startIndex < _nodeLength(node)) {
871
872 preSplitNode = node;
873
874 // Leave the start node's stable references as is, since the remainding text
875 // node within the range is a newly created text node...
876 node = _execOp(_Operation.SPLIT_TEXT_NODE, node, startIndex);
877
878 // Update endnode if range is all within same node
879 if (endNode == startNode) {
880 endNode = node;
881 endIndex -= _nodeLength(startNode);
882 }
883
884 startNode = node;
885 // No need to worry about start index
886 }
887
888 if (node == endNode && endIndex > 0 && endIndex < _nodeLength(node)) {
889 postSplitNode = _execOp(_Operation.SPLIT_TEXT_NODE, node, endIndex);
890 }
891
892 }
893
894 // Create a new fragment node
895 var fragment = new _DOMFragment(node, _indexInParent(node));
896
897 // Has the root been set?
898 if (!fragRoot)
899 fragRoot = fragment;
900 else { // Set up the parent relationship...
901
902 fragment.parent = null;
903 fragRoot.visit(function(frag) {
904 if (frag.node == fragment.node.parentNode)
905 fragment.parent = frag;
906 return fragment.parent == null;
907 });
908 fragment.parent.children.push(fragment);
909 }
910
911 // If the node is a boundry node and is excluded, then set a flag to
912 // note that such nodes are to be outside of the fragment range.
913 fragment.isShared = !(preSplitNode || postSplitNode) && (
914 (node == startNode && startIndex == _nodeLength(node, 1))
915 ||
916 (node == endNode && endIndex == 0)
917 );
918
919 if (preSplitNode)
920 fragment.preSplitNode = preSplitNode;
921
922 if (postSplitNode)
923 fragment.postSplitNode = postSplitNode;
924
925 } // End iterating over affected nodes
926
927 // Determine which nodes are shared with the fragment's range and document and which aren't
928 markSharedNodes(fragRoot);
929
930 return fragRoot;
931
932 /**
933 * An inner support function.
934 * Sets the "isShared" flag for a fragment and all it's descendant fragments
935 * @param {de.dom.DOMFragment} currentFrag The fragment to mark from
936 */
937 function markSharedNodes(currentFrag) {
938
939 var isAnyChildShared = false;
940
941 // Scan children first (disconnecting post order)
942 for (var ch in currentFrag.children) {
943
944 var childFrag = currentFrag.children[ch];
945
946 // Recurse
947 markSharedNodes(childFrag);
948
949 // Detect if any children of the current node are shared
950 isAnyChildShared |= childFrag.isShared;
951 }
952
953 // Determine if this fragment is shared. The root is a special case... thus explicitly must
954 // be declared as being shared. isShared will already be true if the fragment is a boundry
955 // node (start/end) that has been excluded from the range.
956 currentFrag.isShared |= (isAnyChildShared ||
957 currentFrag == fragRoot ||
958 currentFrag.node.childNodes.length != currentFrag.children.length);
959
960 } // End inner markSharedNodes
961
962}; // End buildFragment
963
964// Expose build fragment
965de.buildFragment = _buildFragment;
Note: See TracBrowser for help on using the repository browser.