mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
WIP: liverange
This commit is contained in:
committed by
Nick Martin
parent
4a1db0db06
commit
31bc551746
@@ -1,179 +1,239 @@
|
||||
Sky.ui = Sky.ui || {};
|
||||
|
||||
// XXX correct namespace? should probably be private to package, actually..
|
||||
// XXX maybe take out of funtion(){}() -- unnecessary at the moment
|
||||
(function () {
|
||||
// XXX correct namespace? should probably be private to package, actually..
|
||||
|
||||
// Possible optimization: get rid of start_idx/end_idx and just search
|
||||
// the list. Not clear which strategy will be faster.
|
||||
// Possible optimization: get rid of start_idx/end_idx and just search
|
||||
// the list. Not clear which strategy will be faster.
|
||||
|
||||
// Possible extension: could allow zero-length ranges is some cases,
|
||||
// by encoding both 'enter' and 'leave' type events in the same list
|
||||
// Possible extension: could allow zero-length ranges is some cases,
|
||||
// by encoding both 'enter' and 'leave' type events in the same list
|
||||
|
||||
// can also pass just one node, or a document/documentfragment
|
||||
|
||||
// tag is an arbitrary string (the 'class' of range.) an expando
|
||||
// attribute named 'tag' will be set on the endpoints of the range.
|
||||
Sky.ui._LiveRange = function (tag, start, end) {
|
||||
if ((start instanceof Document) || (start instanceof DocumentFragment)) {
|
||||
end = start.lastChild;
|
||||
start = start.firstChild;
|
||||
}
|
||||
end = end || start;
|
||||
|
||||
this._tag = tag;
|
||||
// can also pass just one node, or a document/documentfragment
|
||||
|
||||
// this._start is the node N such that we begin before N, but not
|
||||
// before the node before N in the preorder traversal of the
|
||||
// document (if there is such a node.) this._start[this._tag][0]
|
||||
// will be the list of all LiveRanges for which this._start is N,
|
||||
// including us, sorted in the order that the ranges start. and
|
||||
// finally, this._start[this._start_idx] === this.
|
||||
this._start = start;
|
||||
if (!(tag in start))
|
||||
start[tag] = [[], []];
|
||||
this._start_idx = start[tag][0].length;
|
||||
start[tag][0].push(this);
|
||||
// tag is an arbitrary string (the 'class' of range.) an expando
|
||||
// attribute named 'tag' will be set on the endpoints of the range.
|
||||
|
||||
// just like this._end, except it's the node N such that we end
|
||||
// after N, but not after the node after N in the postorder
|
||||
// traversal; and the data is stored in this._end[this._tag][1], and
|
||||
// it's sorted in the order that the ranges end.
|
||||
this._end = end;
|
||||
if (!(tag in end))
|
||||
end[tag] = [[], []];
|
||||
this._end_idx = 0;
|
||||
end[tag][1].splice(0, 0, this);
|
||||
};
|
||||
// 'start' - start point, in preorder traversal (range starts just before start)
|
||||
// 'end' - end point, in postorder traversal (range ends just after end)
|
||||
Sky.ui._LiveRange = function (tag, start, end) {
|
||||
if ((start instanceof Document) || (start instanceof DocumentFragment)) {
|
||||
end = start.lastChild;
|
||||
start = start.firstChild;
|
||||
}
|
||||
end = end || start;
|
||||
|
||||
// You shouldn't need to call this function for GC reasons on a modern
|
||||
// browser. It's more like removeChild -- you'd call it because you
|
||||
// don't want to see the range in contained() anymore. However, on old
|
||||
// versions of IE, you do need to manually remove all ranges because
|
||||
// IE can't GC reference cycles through the DOM.
|
||||
Sky.ui._LiveRange.prototype.destroy = function () {
|
||||
var start_data = this._start[this._tag];
|
||||
start_data[0].splice(this._start_idx, 1);
|
||||
if (start_data[0].length === 0 && start_data[1].length === 0)
|
||||
delete this._start[this._tag];
|
||||
this._tag = tag;
|
||||
this._ensure_tags([start, end]);
|
||||
|
||||
var end_data = this._end[this._tag];
|
||||
end_data[1].splice(this._end_idx, 1);
|
||||
if (end_data[0].length === 0 && end_data[1].length === 0)
|
||||
delete this._end[this._tag];
|
||||
// this._start is the node N such that we begin before N, but not
|
||||
// before the node before N in the preorder traversal of the
|
||||
// document (if there is such a node.) this._start[this._tag][0]
|
||||
// will be the list of all LiveRanges for which this._start is N,
|
||||
// including us, sorted in the order that the ranges start. and
|
||||
// finally, this._start[this._start_idx] === this.
|
||||
this._start = start;
|
||||
this._start_idx = start[tag][0].length;
|
||||
start[tag][0].push(this);
|
||||
|
||||
this._start = this._end = null;
|
||||
};
|
||||
|
||||
// (returns only ranges with the same tag as this one)
|
||||
Sky.ui._LiveRange.prototype.contained = function () {
|
||||
// visit() is invoked for each node start-point or end-point that we
|
||||
// encounter as we walk the range stored in 'this' (not counting the
|
||||
// endpoints of 'this' itself.)
|
||||
var result = {children: []};
|
||||
var stack = [result];
|
||||
var visit = function (is_start, range) {
|
||||
if (is_start) {
|
||||
var record = {range: range, children: []};
|
||||
stack[stack.length - 1].children.push(record);
|
||||
stack.push(record);
|
||||
} else
|
||||
if (stack.pop().range !== range)
|
||||
throw new Error("Overlapping ranges detected");
|
||||
// just like this._end, except it's the node N such that we end
|
||||
// after N, but not after the node after N in the postorder
|
||||
// traversal; and the data is stored in this._end[this._tag][1], and
|
||||
// it's sorted in the order that the ranges end.
|
||||
this._end = end;
|
||||
this._end_idx = 0;
|
||||
end[tag][1].splice(0, 0, this);
|
||||
};
|
||||
|
||||
var traverse = function (node) {
|
||||
var data = node[this._tag] || [[], []];
|
||||
for (var i = 0; i < data[0].length; i++)
|
||||
visit(true, data[0][i]);
|
||||
for (var walk = node.firstChild; walk; walk = walk.nextSibling)
|
||||
traverse(walk);
|
||||
for (var i = 0; i < data[1].length; i++)
|
||||
visit(false, data[1][i]);
|
||||
Sky.ui._LiveRange.prototype._ensure_tags = function (nodes) {
|
||||
for (var i = 0; i < nodes.length; i++)
|
||||
if (!(this._tag in nodes[i]))
|
||||
nodes[i][this._tag] = [[], []];
|
||||
};
|
||||
|
||||
var start_enter = this._start[this._tag][0];
|
||||
for (var i = this._start_idx + 1; i < start_enter.length; i++)
|
||||
visit(true, start_enter[i]);
|
||||
Sky.ui._LiveRange.prototype._clean_tags = function (nodes) {
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var data = nodes[i][this._tag];
|
||||
if (data && !(data[0].length + data[1].length))
|
||||
delete nodes[i][this._tag];
|
||||
}
|
||||
};
|
||||
|
||||
var walk = this._start;
|
||||
while (true) {
|
||||
traverse(walk);
|
||||
if (walk === this._end)
|
||||
break;
|
||||
walk = walk.nextSibling;
|
||||
}
|
||||
// You shouldn't need to call this function for GC reasons on a modern
|
||||
// browser. It's more like removeChild -- you'd call it because you
|
||||
// don't want to see the range in contained() anymore. However, on old
|
||||
// versions of IE, you do need to manually remove all ranges because
|
||||
// IE can't GC reference cycles through the DOM.
|
||||
Sky.ui._LiveRange.prototype.destroy = function () {
|
||||
this._start[this._tag][0].splice(this._start_idx, 1);
|
||||
this._end[this._tag][1].splice(this._end_idx, 1);
|
||||
this._clean_tags([this._start, this._end]);
|
||||
|
||||
var end_leave = this._end[this._tag][1];
|
||||
for (var i = 0; i < this._end_idx; i++)
|
||||
visit(false, end_leave[i]);
|
||||
this._start = this._end = null;
|
||||
};
|
||||
|
||||
return result.children;
|
||||
};
|
||||
// The first node in the range (in preorder traversal)
|
||||
Sky.ui._LiveRange.prototype.start = function () {
|
||||
return this._start;
|
||||
};
|
||||
|
||||
Sky.ui._LiveRange.prototype.replace_contents = function (new_frag) {
|
||||
if (!new_frag.firstChild)
|
||||
throw new Error("Ranges must contain at least one element");
|
||||
// The last node in the range (in postorder traversal)
|
||||
Sky.ui._LiveRange.prototype.end = function () {
|
||||
return this._end;
|
||||
};
|
||||
|
||||
// Fix up range pointers on departing fragment
|
||||
var old_enter = this._start[this._tag][0];
|
||||
var save_enter = old_enter.splice(0, this._start_idx + 1);
|
||||
for (var i = 0; i < old_enter.length; i++)
|
||||
old_enter[i]._start_idx = i;
|
||||
// visit_range(is_start, range) is invoked for each range
|
||||
// start-point or end-point that we encounter as we walk the range
|
||||
// stored in 'this' (not counting the endpoints of 'this' itself.)
|
||||
// visit_node(is_start, node) is similar but for nodes, and is
|
||||
// optional.
|
||||
// -- would be nice to let your visit function return false when
|
||||
// is_start is true to skip visiting that range/node's children..
|
||||
Sky.ui._LiveRange.prototype.visit = function (visit_range, visit_node) {
|
||||
// Stand back, I'm going to try SCIENCE.
|
||||
var traverse = function (node, data, start_bound, end_bound) {
|
||||
for (var i = start_bound; i < data[0].length; i++)
|
||||
visit_range(true, data[0][i]);
|
||||
visit_node && visit_node(true, node);
|
||||
for (var walk = node.firstChild; walk; walk = walk.nextSibling) {
|
||||
var walk_data = walk[this._tag] || [[], []];
|
||||
traverse(walk, walk_data, 0, walk_data[1].length);
|
||||
}
|
||||
visit_node && visit_node(false, node);
|
||||
for (var i = 0; i < end_bound; i++)
|
||||
visit_range(false, data[1][i]);
|
||||
};
|
||||
|
||||
var old_leave = this._end[this._tag][1]
|
||||
var save_leave = old_leave.splice(this._end_idx, old_leave.length);
|
||||
var walk = this._start;
|
||||
while (true) {
|
||||
var walk_data = walk[this._tag] || [[], []];
|
||||
traverse(walk, walk_data, walk === this._start ? this._start_idx + 1 : 0,
|
||||
walk === this._end ? this._end_idx : walk_data[1].length);
|
||||
if (walk === this._end)
|
||||
break;
|
||||
walk = walk.nextSibling;
|
||||
}
|
||||
};
|
||||
|
||||
// Insert new fragment
|
||||
var new_start = new_frag.firstChild;
|
||||
var new_end = new_frag.lastChild;
|
||||
this._start.parentNode.insertBefore(new_frag, this._start);
|
||||
// (returns only ranges with the same tag as this one)
|
||||
// XXX could remove .. or just provide a verify() method in debug mode..
|
||||
Sky.ui._LiveRange.prototype.contained = function () {
|
||||
var result = {children: []};
|
||||
var stack = [result];
|
||||
|
||||
// Pull out departing fragment
|
||||
// Possible optimization: use W3C Ranges on browsers that support them
|
||||
var ret = this._start.ownerDocument.createDocumentFragment();
|
||||
var walk = this._start;
|
||||
while (true) {
|
||||
var next = walk.nextSibling;
|
||||
ret.appendChild(walk);
|
||||
if (walk === this._end)
|
||||
break;
|
||||
walk = next;
|
||||
}
|
||||
this.visit(function (is_start, range) {
|
||||
if (is_start) {
|
||||
var record = {range: range, children: []};
|
||||
stack[stack.length - 1].children.push(record);
|
||||
stack.push(record);
|
||||
} else
|
||||
if (stack.pop().range !== range)
|
||||
throw new Error("Overlapping ranges detected");
|
||||
});
|
||||
|
||||
// Fix up range pointers on new fragment -- including our own
|
||||
// Clobbers this._start(_idx), this._end(_idx)
|
||||
var new_enter = new_start[this._tag][0];
|
||||
Array.prototype.splice.apply(new_enter, [0, 0].concat(save_enter));
|
||||
for (var i = 0; i < new_enter.length; i++) {
|
||||
new_enter[i]._start = new_start;
|
||||
new_enter[i]._start_idx = i;
|
||||
}
|
||||
return result.children;
|
||||
};
|
||||
|
||||
var new_leave = new_end[this._tag][1];
|
||||
for (var i = 0; i < save_leave.length; i++) {
|
||||
save_leave[i]._end = new_end;
|
||||
save_leave[i]._end_idx = new_leave.length + i;
|
||||
}
|
||||
Array.prototype.push.apply(new_leave, save_leave);
|
||||
// XXX need to make sure that tags are removed if they become empty
|
||||
Sky.ui._LiveRange.prototype.replace_contents = function (new_frag) {
|
||||
if (!new_frag.firstChild)
|
||||
throw new Error("Ranges must contain at least one element");
|
||||
|
||||
return ret;
|
||||
};
|
||||
// Fix up range pointers on departing fragment
|
||||
var old_enter = this._start[this._tag][0];
|
||||
var save_enter = old_enter.splice(0, this._start_idx + 1);
|
||||
for (var i = 0; i < old_enter.length; i++)
|
||||
old_enter[i]._start_idx = i;
|
||||
|
||||
// Remove the range from inside its current parent, and return a
|
||||
// fragment that contains exactly the range's contents (including any
|
||||
// subranges.) Throw an exception if this would make a parent range
|
||||
// empty.
|
||||
Sky.ui._LiveRange.prototype.extract = function () {
|
||||
// XXX IMPLEMENT
|
||||
};
|
||||
var old_leave = this._end[this._tag][1]
|
||||
var save_leave = old_leave.splice(this._end_idx, old_leave.length);
|
||||
|
||||
// Insert frag so that it comes immediately before the start of the
|
||||
// range.
|
||||
Sky.ui._LiveRange.prototype.insertBefore = function (frag) {
|
||||
// XXX IMPLEMENT
|
||||
};
|
||||
this._clean_tags([this._start, this._end]);
|
||||
|
||||
// Insert frag so that it comes immediately after the start of the
|
||||
// range.
|
||||
Sky.ui._LiveRange.prototype.insertAfter = function (frag) {
|
||||
// XXX IMPLEMENT
|
||||
};
|
||||
// Insert new fragment
|
||||
var new_start = new_frag.firstChild;
|
||||
var new_end = new_frag.lastChild;
|
||||
this._ensure_tags([new_start, new_end]);
|
||||
this._start.parentNode.insertBefore(new_frag, this._start);
|
||||
|
||||
// Pull out departing fragment
|
||||
// Possible optimization: use W3C Ranges on browsers that support them
|
||||
var ret = this._start.ownerDocument.createDocumentFragment();
|
||||
var walk = this._start;
|
||||
while (true) {
|
||||
var next = walk.nextSibling;
|
||||
ret.appendChild(walk);
|
||||
if (walk === this._end)
|
||||
break;
|
||||
walk = next;
|
||||
}
|
||||
|
||||
// Fix up range pointers on new fragment -- including our own
|
||||
// Clobbers this._start(_idx), this._end(_idx)
|
||||
var new_enter = new_start[this._tag][0];
|
||||
Array.prototype.splice.apply(new_enter, [0, 0].concat(save_enter));
|
||||
for (var i = 0; i < new_enter.length; i++) {
|
||||
new_enter[i]._start = new_start;
|
||||
new_enter[i]._start_idx = i;
|
||||
}
|
||||
|
||||
var new_leave = new_end[this._tag][1];
|
||||
for (var i = 0; i < save_leave.length; i++) {
|
||||
save_leave[i]._end = new_end;
|
||||
save_leave[i]._end_idx = new_leave.length + i;
|
||||
}
|
||||
Array.prototype.push.apply(new_leave, save_leave);
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Remove the range from inside its current parent, and return a
|
||||
// fragment that contains exactly the range's contents (including any
|
||||
// subranges.) Throw an exception if this would make a parent range
|
||||
// empty.
|
||||
Sky.ui._LiveRange.prototype.extract = function () {
|
||||
throw new Error("Unimplemented");
|
||||
// XXX IMPLEMENT
|
||||
|
||||
// A range is abutting on the left if there are no elements between
|
||||
// its start and the end of the previous sibling range, or if there
|
||||
// are no siblings, the beginning of its immediate containing range,
|
||||
// or if there is no containing range, the beginning of the
|
||||
// document. "Abutting on the right" has a similar definition.
|
||||
|
||||
// We throw an exception if we're both abutting on both the left and
|
||||
// the right.
|
||||
|
||||
// We're abutting on the left if this._start_idx > 0. We're abutting
|
||||
// on the right if this._end_idx !== this._end[this._tag][1].length - 1.
|
||||
// XXX is this complete, eg maybe need to look at eg this._end.
|
||||
|
||||
// ---
|
||||
|
||||
// As usual we need to repair just the start and the end of the range
|
||||
//
|
||||
// What's happening to the departing range is clear.
|
||||
//
|
||||
// On the start side, there are the start contexts that occur before
|
||||
// this._start_idx. They need to be relocated.
|
||||
|
||||
};
|
||||
|
||||
// Insert frag so that it comes immediately before the start of the
|
||||
// range.
|
||||
Sky.ui._LiveRange.prototype.insertBefore = function (frag) {
|
||||
throw new Error("Unimplemented");
|
||||
// XXX IMPLEMENT
|
||||
};
|
||||
|
||||
// Insert frag so that it comes immediately after the start of the
|
||||
// range.
|
||||
Sky.ui._LiveRange.prototype.insertAfter = function (frag) {
|
||||
throw new Error("Unimplemented");
|
||||
// XXX IMPLEMENT
|
||||
};
|
||||
|
||||
})();
|
||||
@@ -77,8 +77,8 @@ Sky.ui.render = function (render_func, events, event_data) {
|
||||
if (old_context.killed)
|
||||
return; // _cleanup is killing us
|
||||
|
||||
if (!(document.body.contains ? document.body.contains(start)
|
||||
: (document.body.compareDocumentPosition(start) & 16))) {
|
||||
if (!(document.body.contains ? document.body.contains(range.start())
|
||||
: (document.body.compareDocumentPosition(range.start()) & 16))) {
|
||||
// It was taken offscreen. Stop updating it so it can get GC'd.
|
||||
Sky.ui._cleanup(range);
|
||||
range.destroy();
|
||||
@@ -138,6 +138,7 @@ Sky.ui.render = function (render_func, events, event_data) {
|
||||
/// XXX what can now be a collection, or the handle of an existing
|
||||
/// findlive. messy.
|
||||
Sky.ui.renderList = function (what, options) {
|
||||
throw new Error("Unimplemented");
|
||||
var outer_range;
|
||||
var entry_ranges = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user