diff --git a/packages/liveui/liverange.js b/packages/liveui/liverange.js index 8f436a81b8..81279a6d8c 100644 --- a/packages/liveui/liverange.js +++ b/packages/liveui/liverange.js @@ -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 -}; \ No newline at end of file + // 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 + }; + +})(); \ No newline at end of file diff --git a/packages/liveui/liveui.js b/packages/liveui/liveui.js index b166c5cfc4..84a2ee3650 100644 --- a/packages/liveui/liveui.js +++ b/packages/liveui/liveui.js @@ -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 = [];