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 |
|
---|
20 | bootstrap.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 | */
|
---|
38 | var _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 | */
|
---|
777 | function _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
|
---|
965 | de.buildFragment = _buildFragment;
|
---|