mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
attachEvents works!
This commit is contained in:
@@ -29,6 +29,78 @@ var legacyLabels = {
|
||||
|
||||
///// TESTS /////
|
||||
|
||||
var eventmap = function(/*args*/) {
|
||||
// support event_buf as final argument
|
||||
var event_buf = null;
|
||||
if (arguments.length && _.isArray(arguments[arguments.length-1])) {
|
||||
event_buf = arguments[arguments.length-1];
|
||||
arguments.length--;
|
||||
}
|
||||
var events = {};
|
||||
_.each(arguments, function(esel) {
|
||||
var etyp = esel.split(' ')[0];
|
||||
events[esel] = function(evt) {
|
||||
if (evt.type !== etyp)
|
||||
throw new Error(etyp+" event arrived as "+evt.type);
|
||||
(event_buf || this).push(esel);
|
||||
};
|
||||
});
|
||||
return events;
|
||||
};
|
||||
|
||||
Tinytest.add("liveui - listChunk event handling", function(test) {
|
||||
var event_buf = [];
|
||||
var div;
|
||||
|
||||
// same thing, but with events wired by listChunk "added" and "removed"
|
||||
event_buf.length = 0;
|
||||
var lst = [];
|
||||
lst.observe = function(callbacks) {
|
||||
lst.callbacks = callbacks;
|
||||
return {
|
||||
stop: function() {
|
||||
lst.callbacks = null;
|
||||
}
|
||||
};
|
||||
};
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
var chkbx = function(doc) {
|
||||
return '<input type="checkbox">'+(doc ? doc._id : 'else');
|
||||
};
|
||||
return '<div><p><span><b>'+
|
||||
Meteor.ui.listChunk(lst, chkbx, chkbx,
|
||||
{events: eventmap('click input', event_buf),
|
||||
event_data:event_buf}) +
|
||||
'</b></span></p></div>';
|
||||
}, { events: eventmap('change b', 'change input', event_buf),
|
||||
event_data:event_buf }));
|
||||
Meteor.flush();
|
||||
test.equal(div.text().match(/\S+/)[0], 'else');
|
||||
// click on input
|
||||
var doClick = function() {
|
||||
clickElement(div.node().getElementsByTagName('input')[0]);
|
||||
event_buf.sort(); // don't care about order
|
||||
test.equal(event_buf, ['change b', 'change input', 'click input']);
|
||||
event_buf.length = 0;
|
||||
};
|
||||
doClick();
|
||||
// add item
|
||||
lst.push({_id:'foo'});
|
||||
lst.callbacks.added(lst[0], 0);
|
||||
Meteor.flush();
|
||||
test.equal(div.text().match(/\S+/)[0], 'foo');
|
||||
doClick();
|
||||
// remove item, back to "else" case
|
||||
lst.callbacks.removed(lst[0], 0);
|
||||
lst.pop();
|
||||
Meteor.flush();
|
||||
test.equal(div.text().match(/\S+/)[0], 'else');
|
||||
doClick();
|
||||
// cleanup
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("liveui - tables", function(test) {
|
||||
|
||||
@@ -756,437 +828,7 @@ Tinytest.add("liveui - events on preserved nodes", function(test) {
|
||||
Meteor.flush();
|
||||
});
|
||||
|
||||
Tinytest.add("liveui - basic tag contents", function(test) {
|
||||
|
||||
// adapted from nateps / metamorph
|
||||
|
||||
var do_onscreen = function(f) {
|
||||
var div = OnscreenDiv();
|
||||
var stuff = {
|
||||
div: div,
|
||||
node: _.bind(div.node, div),
|
||||
render: function(rfunc) {
|
||||
div.node().appendChild(Meteor.ui.render(rfunc));
|
||||
}
|
||||
};
|
||||
|
||||
f.call(stuff);
|
||||
|
||||
div.kill();
|
||||
};
|
||||
|
||||
var R, div;
|
||||
|
||||
// basic text replace
|
||||
|
||||
do_onscreen(function() {
|
||||
R = ReactiveVar("one two three");
|
||||
this.render(function() {
|
||||
return R.get();
|
||||
});
|
||||
R.set("three four five six");
|
||||
Meteor.flush();
|
||||
test.equal(this.div.html(), "three four five six");
|
||||
});
|
||||
|
||||
// work inside a table
|
||||
|
||||
do_onscreen(function() {
|
||||
R = ReactiveVar("<tr><td>HI!</td></tr>");
|
||||
this.render(function() {
|
||||
return "<table id='morphing'>" + R.get() + "</table>";
|
||||
});
|
||||
|
||||
test.equal($(this.node()).find("#morphing td").text(), "HI!");
|
||||
R.set("<tr><td>BUH BYE!</td></tr>");
|
||||
Meteor.flush();
|
||||
test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!");
|
||||
});
|
||||
|
||||
// work inside a tbody
|
||||
|
||||
do_onscreen(function() {
|
||||
R = ReactiveVar("<tr><td>HI!</td></tr>");
|
||||
this.render(function() {
|
||||
return "<table id='morphing'><tbody>" + R.get() + "</tbody></table>";
|
||||
});
|
||||
|
||||
test.equal($(this.node()).find("#morphing td").text(), "HI!");
|
||||
R.set("<tr><td>BUH BYE!</td></tr>");
|
||||
Meteor.flush();
|
||||
test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!");
|
||||
});
|
||||
|
||||
// work inside a tr
|
||||
|
||||
do_onscreen(function() {
|
||||
R = ReactiveVar("<td>HI!</td>");
|
||||
this.render(function() {
|
||||
return "<table id='morphing'><tr>" + R.get() + "</tr></table>";
|
||||
});
|
||||
|
||||
test.equal($(this.node()).find("#morphing td").text(), "HI!");
|
||||
R.set("<td>BUH BYE!</td>");
|
||||
Meteor.flush();
|
||||
test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!");
|
||||
});
|
||||
|
||||
// work inside a ul
|
||||
|
||||
do_onscreen(function() {
|
||||
R = ReactiveVar("<li>HI!</li>");
|
||||
this.render(function() {
|
||||
return "<ul id='morphing'>" + R.get() + "</ul>";
|
||||
});
|
||||
|
||||
test.equal($(this.node()).find("#morphing li").text(), "HI!");
|
||||
R.set("<li>BUH BYE!</li>");
|
||||
Meteor.flush();
|
||||
test.equal($(this.node()).find("#morphing li").text(), "BUH BYE!");
|
||||
});
|
||||
|
||||
// work inside a select
|
||||
|
||||
do_onscreen(function() {
|
||||
R = ReactiveVar("<option>HI!</option>");
|
||||
this.render(function() {
|
||||
return "<select id='morphing'>" + R.get() + "</select>";
|
||||
});
|
||||
|
||||
test.equal($(this.node()).find("#morphing option").text(), "HI!");
|
||||
R.set("<option>BUH BYE!</option>");
|
||||
Meteor.flush();
|
||||
test.equal($(this.node()).find("#morphing option").text(), "BUH BYE!");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
var eventmap = function(/*args*/) {
|
||||
// support event_buf as final argument
|
||||
var event_buf = null;
|
||||
if (arguments.length && _.isArray(arguments[arguments.length-1])) {
|
||||
event_buf = arguments[arguments.length-1];
|
||||
arguments.length--;
|
||||
}
|
||||
var events = {};
|
||||
_.each(arguments, function(esel) {
|
||||
var etyp = esel.split(' ')[0];
|
||||
events[esel] = function(evt) {
|
||||
if (evt.type !== etyp)
|
||||
throw new Error(etyp+" event arrived as "+evt.type);
|
||||
(event_buf || this).push(esel);
|
||||
};
|
||||
});
|
||||
return events;
|
||||
};
|
||||
|
||||
Tinytest.add("liveui - event handling", function(test) {
|
||||
var event_buf = [];
|
||||
var getid = function(id) {
|
||||
return document.getElementById(id);
|
||||
};
|
||||
|
||||
var div;
|
||||
|
||||
// clicking on a div at top level
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
return '<div id="foozy">Foo</div>';
|
||||
}, {events: eventmap("click"), event_data:event_buf}));
|
||||
clickElement(getid("foozy"));
|
||||
test.equal(event_buf, ['click']);
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// selector that specifies a top-level div
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
return '<div id="foozy">Foo</div>';
|
||||
}, {events: eventmap("click div"), event_data:event_buf}));
|
||||
clickElement(getid("foozy"));
|
||||
test.equal(event_buf, ['click div']);
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// selector that specifies a second-level span
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
return '<div id="foozy"><span>Foo</span></div>';
|
||||
}, {events: eventmap("click span"), event_data:event_buf}));
|
||||
clickElement(getid("foozy").firstChild);
|
||||
test.equal(event_buf, ['click span']);
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// replaced top-level elements still have event handlers
|
||||
// even if not replaced by the chunk wih the handlers
|
||||
var R = ReactiveVar("p");
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
return Meteor.ui.chunk(function() {
|
||||
return '<'+R.get()+' id="foozy">Hello</'+R.get()+'>';
|
||||
});
|
||||
}, {events: eventmap("click"), event_data:event_buf}));
|
||||
clickElement(getid("foozy"));
|
||||
test.equal(event_buf, ['click']);
|
||||
event_buf.length = 0;
|
||||
R.set("div"); // change tag, which is sure to replace element
|
||||
Meteor.flush();
|
||||
clickElement(getid("foozy")); // still clickable?
|
||||
test.equal(event_buf, ['click']);
|
||||
event_buf.length = 0;
|
||||
R.set("p");
|
||||
Meteor.flush();
|
||||
clickElement(getid("foozy"));
|
||||
test.equal(event_buf, ['click']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// bubbling from event on descendent of element matched
|
||||
// by selector
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
return '<div id="foozy"><span><u><b>Foo</b></u></span>'+
|
||||
'<span>Bar</span></div>';
|
||||
}, {events: eventmap("click span"), event_data:event_buf}));
|
||||
clickElement(
|
||||
getid("foozy").firstChild.firstChild.firstChild);
|
||||
test.equal(event_buf, ['click span']);
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// bubbling order (for same event, same render node, different selector nodes)
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
return '<div id="foozy"><span><u><b>Foo</b></u></span>'+
|
||||
'<span>Bar</span></div>';
|
||||
}, {events: eventmap("click span", "click b"), event_data:event_buf}));
|
||||
clickElement(
|
||||
getid("foozy").firstChild.firstChild.firstChild);
|
||||
test.equal(event_buf, ['click b', 'click span']);
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// "bubbling" order for handlers at same level
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
return Meteor.ui.chunk(function() {
|
||||
return Meteor.ui.chunk(function() {
|
||||
return '<span id="foozy" class="a b c">Hello</span>';
|
||||
}, {events: eventmap("click .c"), event_data:event_buf});
|
||||
}, {events: eventmap("click .b"), event_data:event_buf});
|
||||
}, {events: eventmap("click .a"), event_data:event_buf}));
|
||||
clickElement(getid("foozy"));
|
||||
test.equal(event_buf, ['click .c', 'click .b', 'click .a']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// stopPropagation doesn't prevent other event maps from
|
||||
// handling same node
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
return Meteor.ui.chunk(function() {
|
||||
return Meteor.ui.chunk(function() {
|
||||
return '<span id="foozy" class="a b c">Hello</span>';
|
||||
}, {events: eventmap("click .c"), event_data:event_buf});
|
||||
}, {events: {"click .b": function(evt) {
|
||||
event_buf.push("click .b"); evt.stopPropagation();}}});
|
||||
}, {events: eventmap("click .a"), event_data:event_buf}));
|
||||
clickElement(getid("foozy"));
|
||||
test.equal(event_buf, ['click .c', 'click .b', 'click .a']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// stopImmediatePropagation DOES
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
return Meteor.ui.chunk(function() {
|
||||
return Meteor.ui.chunk(function() {
|
||||
return '<span id="foozy" class="a b c">Hello</span>';
|
||||
}, {events: eventmap("click .c"), event_data:event_buf});
|
||||
}, {events: {"click .b": function(evt) {
|
||||
event_buf.push("click .b");
|
||||
evt.stopImmediatePropagation();}}});
|
||||
}, {events: eventmap("click .a"), event_data:event_buf}));
|
||||
clickElement(getid("foozy"));
|
||||
test.equal(event_buf, ['click .c', 'click .b']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// bubbling continues even with DOM change
|
||||
event_buf.length = 0;
|
||||
R = ReactiveVar(true);
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
return Meteor.ui.chunk(function() {
|
||||
return '<div id="blarn">'+(R.get()?'<span id="foozy">abcd</span>':'')+'</div>';
|
||||
}, {events: { 'click span': function() {
|
||||
event_buf.push('click span');
|
||||
R.set(false);
|
||||
Meteor.flush(); // kill the span
|
||||
}, 'click div': function(evt) {
|
||||
event_buf.push('click div');
|
||||
}}});
|
||||
}));
|
||||
// click on span
|
||||
clickElement(getid("foozy"));
|
||||
test.expect_fail(); // doesn't seem to work in old IE
|
||||
test.equal(event_buf, ['click span', 'click div']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// "deep reach" from high node down to replaced low node.
|
||||
// Tests that attach_secondary_events actually does the
|
||||
// right thing in IE. Also tests change event bubbling
|
||||
// and proper interpretation of event maps.
|
||||
event_buf.length = 0;
|
||||
R = ReactiveVar('foo');
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
return '<div><p><span><b>'+
|
||||
Meteor.ui.chunk(function() {
|
||||
return '<input type="checkbox">'+R.get();
|
||||
}, {events: eventmap('click input'), event_data:event_buf}) +
|
||||
'</b></span></p></div>';
|
||||
}, { events: eventmap('change b', 'change input'), event_data:event_buf }));
|
||||
R.set('bar');
|
||||
Meteor.flush();
|
||||
// click on input
|
||||
clickElement(div.node().getElementsByTagName('input')[0]);
|
||||
event_buf.sort(); // don't care about order
|
||||
test.equal(event_buf, ['change b', 'change input', 'click input']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// same thing, but with events wired by listChunk "added" and "removed"
|
||||
event_buf.length = 0;
|
||||
var lst = [];
|
||||
lst.observe = function(callbacks) {
|
||||
lst.callbacks = callbacks;
|
||||
return {
|
||||
stop: function() {
|
||||
lst.callbacks = null;
|
||||
}
|
||||
};
|
||||
};
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
var chkbx = function(doc) {
|
||||
return '<input type="checkbox">'+(doc ? doc._id : 'else');
|
||||
};
|
||||
return '<div><p><span><b>'+
|
||||
Meteor.ui.listChunk(lst, chkbx, chkbx,
|
||||
{events: eventmap('click input', event_buf),
|
||||
event_data:event_buf}) +
|
||||
'</b></span></p></div>';
|
||||
}, { events: eventmap('change b', 'change input', event_buf),
|
||||
event_data:event_buf }));
|
||||
Meteor.flush();
|
||||
test.equal(div.text().match(/\S+/)[0], 'else');
|
||||
// click on input
|
||||
var doClick = function() {
|
||||
clickElement(div.node().getElementsByTagName('input')[0]);
|
||||
event_buf.sort(); // don't care about order
|
||||
test.equal(event_buf, ['change b', 'change input', 'click input']);
|
||||
event_buf.length = 0;
|
||||
};
|
||||
doClick();
|
||||
// add item
|
||||
lst.push({_id:'foo'});
|
||||
lst.callbacks.added(lst[0], 0);
|
||||
Meteor.flush();
|
||||
test.equal(div.text().match(/\S+/)[0], 'foo');
|
||||
doClick();
|
||||
// remove item, back to "else" case
|
||||
lst.callbacks.removed(lst[0], 0);
|
||||
lst.pop();
|
||||
Meteor.flush();
|
||||
test.equal(div.text().match(/\S+/)[0], 'else');
|
||||
doClick();
|
||||
// cleanup
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// test that 'click *' fires on bubble
|
||||
event_buf.length = 0;
|
||||
R = ReactiveVar('foo');
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
return '<div><p><span><b>'+
|
||||
Meteor.ui.chunk(function() {
|
||||
return '<input type="checkbox">'+R.get();
|
||||
}, {events: eventmap('click input'), event_data:event_buf}) +
|
||||
'</b></span></p></div>';
|
||||
}, { events: eventmap('click *'), event_data:event_buf }));
|
||||
R.set('bar');
|
||||
Meteor.flush();
|
||||
// click on input
|
||||
clickElement(div.node().getElementsByTagName('input')[0]);
|
||||
test.equal(
|
||||
event_buf,
|
||||
['click input', 'click *', 'click *', 'click *', 'click *', 'click *']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// clicking on a div in a nested chunk (without patching)
|
||||
event_buf.length = 0;
|
||||
R = ReactiveVar('foo');
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
return R.get() + Meteor.ui.chunk(function() {
|
||||
return '<span>ism</span>';
|
||||
}, {events: eventmap("click"), event_data:event_buf});
|
||||
}));
|
||||
test.equal(div.text(), 'fooism');
|
||||
clickElement(div.node().getElementsByTagName('SPAN')[0]);
|
||||
test.equal(event_buf, ['click']);
|
||||
event_buf.length = 0;
|
||||
R.set('bar');
|
||||
Meteor.flush();
|
||||
test.equal(div.text(), 'barism');
|
||||
clickElement(div.node().getElementsByTagName('SPAN')[0]);
|
||||
test.equal(event_buf, ['click']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// Test that reactive fragments manually inserted inside
|
||||
// a reactive fragment eventually get wired.
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
return "<div></div>";
|
||||
}, { events: eventmap("click span", event_buf) }));
|
||||
Meteor.flush();
|
||||
div.node().firstChild.appendChild(Meteor.ui.render(function() {
|
||||
return '<span id="foozy">hello</span>';
|
||||
}));
|
||||
clickElement(getid("foozy"));
|
||||
// implementation has no way to know we've inserted the fragment
|
||||
test.equal(event_buf, []);
|
||||
event_buf.length = 0;
|
||||
Meteor.flush();
|
||||
clickElement(getid("foozy"));
|
||||
// now should be wired up
|
||||
test.equal(event_buf, ['click span']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// Event data comes from event.currentTarget, not event.target
|
||||
var data_buf = [];
|
||||
div = OnscreenDiv(Meteor.ui.render(function() {
|
||||
return "<ul>"+Meteor.ui.chunk(function() {
|
||||
return '<li id="funyard">Hello</li>';
|
||||
}, { event_data: {x:'listuff'} })+"</ul>";
|
||||
}, { event_data: {x:'ulstuff'},
|
||||
events: { 'click ul': function() { data_buf.push(this); }}}));
|
||||
clickElement(getid("funyard"));
|
||||
test.equal(data_buf, [{x:'ulstuff'}]);
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
});
|
||||
|
||||
Tinytest.add("liveui - cleanup", function(test) {
|
||||
|
||||
|
||||
@@ -4,13 +4,39 @@ Spark = {};
|
||||
|
||||
Spark._currentRenderer = new Meteor.EnvironmentVariable;
|
||||
|
||||
Spark._TAG = "_spark_"+Meteor.uuid();
|
||||
// XXX document contract for each type of annotation?
|
||||
Spark._ANNOTATION_DATA = "_spark_data";
|
||||
Spark._ANNOTATION_ISOLATE = "_spark_isolate";
|
||||
Spark._ANNOTATION_EVENTS = "_spark_events";
|
||||
Spark._ANNOTATION_WATCH = "_spark_watch";
|
||||
Spark._ANNOTATIONS = [Spark._ANNOTATION_DATA, Spark._ANNOTATION_ISOLATE,
|
||||
Spark._ANNOTATION_EVENTS, Spark._ANNOTATION_WATCH];
|
||||
Spark._ANNOTATION_NOTIFY = "notify";
|
||||
Spark._ANNOTATION_DATA = "data";
|
||||
Spark._ANNOTATION_ISOLATE = "isolate";
|
||||
Spark._ANNOTATION_EVENTS = "events";
|
||||
Spark._ANNOTATION_WATCH = "watch";
|
||||
|
||||
// Set in tests to turn on extra UniversalEventListener sanity checks
|
||||
Spark._checkIECompliance = false;
|
||||
|
||||
var makeRange = function(type, start, end, inner) {
|
||||
var range = new LiveRange(Spark._TAG, start, end, inner);
|
||||
range.type = type;
|
||||
return range;
|
||||
};
|
||||
|
||||
var findRangeOfType = function(type, node) {
|
||||
var range = LiveRange.findRange(Spark._TAG, node);
|
||||
while (range && range.type !== type)
|
||||
range = range.findParent();
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
var findParentOfType = function (type, range) {
|
||||
do {
|
||||
range = range.findParent();
|
||||
} while (range && range.type !== type);
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
|
||||
Spark._Renderer = function () {
|
||||
// Map from annotation ID to an annotation function, which is called
|
||||
@@ -39,12 +65,12 @@ _.extend(Spark._Renderer.prototype, {
|
||||
// `what` can be a function that takes a LiveRange, or just a set of
|
||||
// attributes to add to the liverange. tag and what are optional.
|
||||
// if no tag is passed, no liverange will be created.
|
||||
annotate: function (html, tag, what) {
|
||||
var id = tag + "-" + this.createId();
|
||||
annotate: function (html, type, what) {
|
||||
var id = type + "-" + this.createId();
|
||||
this.annotations[id] = function (start, end) {
|
||||
if (! tag)
|
||||
if (! type)
|
||||
return;
|
||||
var range = new LiveRange(tag, start, end);
|
||||
var range = makeRange(type, start, end);
|
||||
if (what instanceof Function)
|
||||
what(range);
|
||||
else
|
||||
@@ -60,7 +86,34 @@ _.extend(Spark._Renderer.prototype, {
|
||||
Spark.render = function (htmlFunc) {
|
||||
var renderer = new Spark._Renderer;
|
||||
var html = Spark._currentRenderer.withValue(renderer, function () {
|
||||
return renderer.annotate(htmlFunc());
|
||||
return renderer.annotate(
|
||||
htmlFunc(), Spark._ANNOTATION_NOTIFY, function (range) {
|
||||
// This is a heuristic to benefit users that manually insert
|
||||
// nodes into regions of the DOM that were rendered by Spark
|
||||
// and have event maps. The heuristic is: at every flush, look
|
||||
// for any nodes that were rendered by Spark since the last
|
||||
// flush, and call notifyWatchers on them so that any
|
||||
// *enclosing* event maps can wire up event handlers on the
|
||||
// newly inserted nodes.
|
||||
//
|
||||
// This heuristic is only of relevance on IE6-8 and only if
|
||||
// the user manually inserts nodes in the DOM (eg, through
|
||||
// jQuery or appendChild). We're not sure it's the right thing
|
||||
// and it could be removed at any time.
|
||||
var finalized = false;
|
||||
range.finalize = function () {
|
||||
finalized = true;
|
||||
};
|
||||
|
||||
var ctx = new Meteor.deps.Context;
|
||||
ctx.on_invalidate(function () {
|
||||
if (!finalized) {
|
||||
notifyWatchers(range.firstNode(), range.lastNode());
|
||||
range.destroy();
|
||||
}
|
||||
});
|
||||
ctx.invalidate();
|
||||
});
|
||||
});
|
||||
|
||||
var fragById = {};
|
||||
@@ -145,8 +198,7 @@ Spark.setDataContext = withRenderer(function (dataContext, html, _renderer) {
|
||||
});
|
||||
|
||||
Spark.getDataContext = function (node) {
|
||||
var range = LiveRange.findRange(
|
||||
Spark._ANNOTATION_DATA, node);
|
||||
var range = findRangeOfType(Spark._ANNOTATION_DATA, node);
|
||||
return range && range.data;
|
||||
};
|
||||
|
||||
@@ -155,20 +207,24 @@ var getListener = function () {
|
||||
if (!universalListener)
|
||||
universalListener = new UniversalEventListener(function (event) {
|
||||
// Handle a currently-propagating event on a particular node.
|
||||
// We walk all enclosing liveranges of the node, from the inside
|
||||
// out, looking for matching handlers. If the app calls
|
||||
// stopPropagation(), we still call all handlers in all event
|
||||
// maps for the current node. If the app calls
|
||||
// "stopImmediatePropagation()", we don't call any more
|
||||
// handlers.
|
||||
// We walk each enclosing liverange of the node and offer it the
|
||||
// chance to handle the event. It's range.handler's
|
||||
// responsibility to check isImmediatePropagationStopped()
|
||||
// before delivering events to the user. We precompute the list
|
||||
// of enclosing liveranges to defend against the case where user
|
||||
// event handlers change the DOM.
|
||||
|
||||
var range = LiveRange.findRange(Spark._ANNOTATION_EVENTS,
|
||||
event.currentTarget);
|
||||
while (range && !event.isImmediatePropagationStopped()) {
|
||||
range.handler(event);
|
||||
range = range.findParent();
|
||||
var ranges = [];
|
||||
var walk = findRangeOfType(Spark._ANNOTATION_EVENTS,
|
||||
event.currentTarget);
|
||||
while (walk) {
|
||||
ranges.push(walk);
|
||||
walk = findParentOfType(Spark._ANNOTATION_EVENTS, walk);
|
||||
}
|
||||
});
|
||||
_.each(ranges, function (r) {
|
||||
r.handler(event);
|
||||
});
|
||||
}, Spark._checkIECompliance);
|
||||
|
||||
return universalListener;
|
||||
};
|
||||
@@ -214,6 +270,8 @@ Spark.attachEvents = withRenderer(function (eventMap, html, _renderer) {
|
||||
}
|
||||
});
|
||||
|
||||
var finalized = false;
|
||||
|
||||
html = _renderer.annotate(
|
||||
html, Spark._ANNOTATION_EVENTS, function (range) {
|
||||
_.each(eventTypes, function (t) {
|
||||
@@ -221,10 +279,17 @@ Spark.attachEvents = withRenderer(function (eventMap, html, _renderer) {
|
||||
});
|
||||
installHandlers(range);
|
||||
|
||||
range.finalize = function () {
|
||||
finalized = true;
|
||||
};
|
||||
|
||||
range.handler = function (event) {
|
||||
var handlers = handlerMap[event.type] || [];
|
||||
|
||||
for (var i = 0; i < handlers.length; i++) {
|
||||
if (finalized || event.isImmediatePropagationStopped())
|
||||
return;
|
||||
|
||||
var handler = handlers[i];
|
||||
var callback = handler.callback;
|
||||
var selector = handler.selector;
|
||||
@@ -246,8 +311,12 @@ Spark.attachEvents = withRenderer(function (eventMap, html, _renderer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found a matching handler.
|
||||
// Found a matching handler. Call it.
|
||||
var eventData = Spark.getDataContext(event.currentTarget);
|
||||
// Note that the handler can do arbitrary things, like call
|
||||
// Meteor.flush() or otherwise remove and finalize parts of
|
||||
// the DOM. We can't assume `range` is valid past this point,
|
||||
// and we'll check the `finalized` flag at the top of the loop.
|
||||
var returnValue = callback.call(eventData, event);
|
||||
|
||||
// allow app to `return false` from event handler, just like
|
||||
@@ -256,8 +325,6 @@ Spark.attachEvents = withRenderer(function (eventMap, html, _renderer) {
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if (event.isImmediatePropagationStopped())
|
||||
break; // don't let any other handlers in this event map fire
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -309,8 +376,8 @@ Spark.isolate = function (htmlFunc) {
|
||||
return Spark.isolate(htmlFunc);
|
||||
});
|
||||
|
||||
var tempRange = new LiveRange(Spark._ANNOTATION_ISOLATE, frag, null,
|
||||
true /* inner */);
|
||||
var tempRange = makeRange(Spark._ANNOTATION_ISOLATE, frag, null,
|
||||
true /* inner */);
|
||||
tempRange.operate(function (start, end) {
|
||||
// Wrap contents of frag, *inside* the ISOLATE annotation,
|
||||
// as appropriate for insertion into `range`. We want the
|
||||
@@ -324,7 +391,7 @@ Spark.isolate = function (htmlFunc) {
|
||||
|
||||
var oldContents = range.replace_contents(frag); // XXX should patch
|
||||
Spark.finalize(oldContents);
|
||||
notifyWatchers(range);
|
||||
notifyWatchers(range.firstNode(), range.lastNode());
|
||||
range.destroy();
|
||||
});
|
||||
});
|
||||
@@ -332,18 +399,12 @@ Spark.isolate = function (htmlFunc) {
|
||||
return html;
|
||||
};
|
||||
|
||||
var notifyWatchers = function (range) {
|
||||
// find the innermost WATCH annotation containing the nodes in `range`
|
||||
var tempRange = new LiveRange(Spark._ANNOTATION_WATCH, range.firstNode(),
|
||||
range.lastNode(), true /* innermost */);
|
||||
var walk = tempRange.findParent();
|
||||
var notifyWatchers = function (start, end) {
|
||||
var tempRange = new LiveRange(Spark._TAG, start, end, true /* innermost */);
|
||||
for (var walk = tempRange; walk; walk = walk.findParent())
|
||||
if (walk.type === Spark._ANNOTATION_WATCH)
|
||||
walk.notify();
|
||||
tempRange.destroy();
|
||||
|
||||
// tell all enclosing WATCH annotations that their contents changed
|
||||
while (walk) {
|
||||
walk.notify();
|
||||
walk = walk.findParent();
|
||||
}
|
||||
};
|
||||
|
||||
// Delete all of the liveranges in the range of nodes between `start`
|
||||
@@ -358,13 +419,11 @@ Spark.finalize = function (start, end) {
|
||||
start = frag;
|
||||
end = null;
|
||||
}
|
||||
_.each(Spark._ANNOTATIONS, function (tag) {
|
||||
var wrapper = new LiveRange(tag, start, end);
|
||||
wrapper.visit(function (isStart, range) {
|
||||
isStart && range.finalize && range.finalize();
|
||||
});
|
||||
wrapper.destroy(true /* recursive */);
|
||||
var wrapper = new LiveRange(Spark._TAG, start, end);
|
||||
wrapper.visit(function (isStart, range) {
|
||||
isStart && range.finalize && range.finalize();
|
||||
});
|
||||
wrapper.destroy(true /* recursive */);
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
// XXX when testing spark, set the checkIECompliance flag on universal-events somehow
|
||||
// XXX make sure that when tests use id="..." to trigger patching, "preserve" happens
|
||||
|
||||
Spark._checkIECompliance = true;
|
||||
|
||||
(function () {
|
||||
|
||||
Tinytest.add("spark - assembly", function (test) {
|
||||
|
||||
var doTest = function(calc) {
|
||||
var frag = Spark.render(function() {
|
||||
return calc(function(str, expected) {
|
||||
var doTest = function (calc) {
|
||||
var frag = Spark.render(function () {
|
||||
return calc(function (str, expected) {
|
||||
return Spark.setDataContext(null, str);
|
||||
});
|
||||
});
|
||||
var groups = [];
|
||||
var html = calc(function(str, expected, noRange) {
|
||||
var html = calc(function (str, expected, noRange) {
|
||||
if (arguments.length > 1)
|
||||
str = expected;
|
||||
if (! noRange)
|
||||
@@ -20,41 +24,41 @@ Tinytest.add("spark - assembly", function (test) {
|
||||
test.equal(f.html(), html);
|
||||
|
||||
var actualGroups = [];
|
||||
var tempRange = new LiveRange(Spark._ANNOTATION_DATA, frag);
|
||||
tempRange.visit(function(isStart, rng) {
|
||||
if (! isStart)
|
||||
var tempRange = new LiveRange(Spark._TAG, frag);
|
||||
tempRange.visit(function (isStart, rng) {
|
||||
if (! isStart && rng.type === Spark._ANNOTATION_DATA)
|
||||
actualGroups.push(rangeToHtml(rng));
|
||||
});
|
||||
test.equal(actualGroups.join(','), groups.join(','));
|
||||
};
|
||||
|
||||
doTest(function(A) { return "<p>Hello</p>"; });
|
||||
doTest(function(A) { return "<td>Hello</td><td>World</td>"; });
|
||||
doTest(function(A) { return "<td>"+A("Hello")+"</td>"; });
|
||||
doTest(function(A) { return A("<td>"+A("Hello")+"</td>"); });
|
||||
doTest(function(A) { return A(A(A(A(A(A("foo")))))); });
|
||||
doTest(function (A) { return "<p>Hello</p>"; });
|
||||
doTest(function (A) { return "<td>Hello</td><td>World</td>"; });
|
||||
doTest(function (A) { return "<td>"+A("Hello")+"</td>"; });
|
||||
doTest(function (A) { return A("<td>"+A("Hello")+"</td>"); });
|
||||
doTest(function (A) { return A(A(A(A(A(A("foo")))))); });
|
||||
doTest(
|
||||
function(A) { return "<div>Yo"+A("<p>Hello "+A(A("World")),"<p>Hello World</p>")+
|
||||
function (A) { return "<div>Yo"+A("<p>Hello "+A(A("World")),"<p>Hello World</p>")+
|
||||
"</div>"; });
|
||||
doTest(function(A) {
|
||||
doTest(function (A) {
|
||||
return A("<ul>"+A("<li>one","<li>one</li>")+
|
||||
A("<li>two","<li>two</li>")+
|
||||
A("<li>three","<li>three</li>"),
|
||||
"<ul><li>one</li><li>two</li><li>three</li></ul>"); });
|
||||
|
||||
doTest(function(A) {
|
||||
doTest(function (A) {
|
||||
return A("<table>"+A("<tr>"+A("<td>"+A("Hi")+"</td>")+"</tr>")+"</table>",
|
||||
"<table><tbody><tr><td>Hi</td></tr></tbody></table>");
|
||||
});
|
||||
|
||||
test.throws(function() {
|
||||
doTest(function(A) {
|
||||
test.throws(function () {
|
||||
doTest(function (A) {
|
||||
var z = A("Hello");
|
||||
return z+z;
|
||||
});
|
||||
});
|
||||
|
||||
var frag = Spark.render(function() {
|
||||
var frag = Spark.render(function () {
|
||||
return '<div foo="abc' +
|
||||
Spark.setDataContext(null, "bar") +
|
||||
'xyz">Hello</div>';
|
||||
@@ -67,12 +71,118 @@ Tinytest.add("spark - assembly", function (test) {
|
||||
});
|
||||
|
||||
|
||||
Tinytest.add("spark - basic tag contents", function (test) {
|
||||
|
||||
// adapted from nateps / metamorph
|
||||
|
||||
var do_onscreen = function (f) {
|
||||
var div = OnscreenDiv();
|
||||
var stuff = {
|
||||
div: div,
|
||||
node: _.bind(div.node, div),
|
||||
render: function (rfunc) {
|
||||
div.node().appendChild(Meteor.render(rfunc));
|
||||
}
|
||||
};
|
||||
|
||||
f.call(stuff);
|
||||
|
||||
div.kill();
|
||||
};
|
||||
|
||||
var R, div;
|
||||
|
||||
// basic text replace
|
||||
|
||||
do_onscreen(function () {
|
||||
R = ReactiveVar("one two three");
|
||||
this.render(function () {
|
||||
return R.get();
|
||||
});
|
||||
R.set("three four five six");
|
||||
Meteor.flush();
|
||||
test.equal(this.div.html(), "three four five six");
|
||||
});
|
||||
|
||||
// work inside a table
|
||||
|
||||
do_onscreen(function () {
|
||||
R = ReactiveVar("<tr><td>HI!</td></tr>");
|
||||
this.render(function () {
|
||||
return "<table id='morphing'>" + R.get() + "</table>";
|
||||
});
|
||||
|
||||
test.equal($(this.node()).find("#morphing td").text(), "HI!");
|
||||
R.set("<tr><td>BUH BYE!</td></tr>");
|
||||
Meteor.flush();
|
||||
test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!");
|
||||
});
|
||||
|
||||
// work inside a tbody
|
||||
|
||||
do_onscreen(function () {
|
||||
R = ReactiveVar("<tr><td>HI!</td></tr>");
|
||||
this.render(function () {
|
||||
return "<table id='morphing'><tbody>" + R.get() + "</tbody></table>";
|
||||
});
|
||||
|
||||
test.equal($(this.node()).find("#morphing td").text(), "HI!");
|
||||
R.set("<tr><td>BUH BYE!</td></tr>");
|
||||
Meteor.flush();
|
||||
test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!");
|
||||
});
|
||||
|
||||
// work inside a tr
|
||||
|
||||
do_onscreen(function () {
|
||||
R = ReactiveVar("<td>HI!</td>");
|
||||
this.render(function () {
|
||||
return "<table id='morphing'><tr>" + R.get() + "</tr></table>";
|
||||
});
|
||||
|
||||
test.equal($(this.node()).find("#morphing td").text(), "HI!");
|
||||
R.set("<td>BUH BYE!</td>");
|
||||
Meteor.flush();
|
||||
test.equal($(this.node()).find("#morphing td").text(), "BUH BYE!");
|
||||
});
|
||||
|
||||
// work inside a ul
|
||||
|
||||
do_onscreen(function () {
|
||||
R = ReactiveVar("<li>HI!</li>");
|
||||
this.render(function () {
|
||||
return "<ul id='morphing'>" + R.get() + "</ul>";
|
||||
});
|
||||
|
||||
test.equal($(this.node()).find("#morphing li").text(), "HI!");
|
||||
R.set("<li>BUH BYE!</li>");
|
||||
Meteor.flush();
|
||||
test.equal($(this.node()).find("#morphing li").text(), "BUH BYE!");
|
||||
});
|
||||
|
||||
// work inside a select
|
||||
|
||||
do_onscreen(function () {
|
||||
R = ReactiveVar("<option>HI!</option>");
|
||||
this.render(function () {
|
||||
return "<select id='morphing'>" + R.get() + "</select>";
|
||||
});
|
||||
|
||||
test.equal($(this.node()).find("#morphing option").text(), "HI!");
|
||||
R.set("<option>BUH BYE!</option>");
|
||||
Meteor.flush();
|
||||
test.equal($(this.node()).find("#morphing option").text(), "BUH BYE!");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
Tinytest.add("spark - basic isolate", function (test) {
|
||||
|
||||
var R = ReactiveVar('foo');
|
||||
|
||||
var div = OnscreenDiv(Spark.render(function() {
|
||||
return '<div>' + Spark.isolate(function() {
|
||||
var div = OnscreenDiv(Spark.render(function () {
|
||||
return '<div>' + Spark.isolate(function () {
|
||||
return '<span>' + R.get() + '</span>';
|
||||
}) + '</div>';
|
||||
}));
|
||||
@@ -88,11 +198,11 @@ Tinytest.add("spark - basic isolate", function (test) {
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("spark - one render", function(test) {
|
||||
Tinytest.add("spark - one render", function (test) {
|
||||
|
||||
var R = ReactiveVar("foo");
|
||||
|
||||
var frag = WrappedFrag(Meteor.render(function() {
|
||||
var frag = WrappedFrag(Meteor.render(function () {
|
||||
return R.get();
|
||||
})).hold();
|
||||
|
||||
@@ -117,14 +227,14 @@ Tinytest.add("spark - one render", function(test) {
|
||||
test.equal(R.numListeners(), 0);
|
||||
|
||||
// empty return value should work, and show up as a comment
|
||||
frag = WrappedFrag(Meteor.render(function() {
|
||||
frag = WrappedFrag(Meteor.render(function () {
|
||||
return "";
|
||||
}));
|
||||
test.equal(frag.html(), "<!---->");
|
||||
|
||||
// nodes coming and going at top level of fragment
|
||||
R.set(true);
|
||||
frag = WrappedFrag(Meteor.render(function() {
|
||||
frag = WrappedFrag(Meteor.render(function () {
|
||||
return R.get() ? "<div>hello</div><div>world</div>" : "";
|
||||
})).hold();
|
||||
test.equal(frag.html(), "<div>hello</div><div>world</div>");
|
||||
@@ -141,7 +251,7 @@ Tinytest.add("spark - one render", function(test) {
|
||||
|
||||
// more complicated changes
|
||||
R.set(1);
|
||||
frag = WrappedFrag(Meteor.render(function() {
|
||||
frag = WrappedFrag(Meteor.render(function () {
|
||||
var result = [];
|
||||
for(var i=0; i<R.get(); i++) {
|
||||
result.push('<div id="x'+i+'" class="foo" name="bar"><p><b>'+
|
||||
@@ -170,11 +280,11 @@ Tinytest.add("spark - one render", function(test) {
|
||||
test.equal(WrappedFrag(Meteor.render("foo")).html(), "foo");
|
||||
});
|
||||
|
||||
Tinytest.add("spark - slow path GC", function(test) {
|
||||
Tinytest.add("spark - slow path GC", function (test) {
|
||||
|
||||
var R = ReactiveVar(123);
|
||||
|
||||
var div = OnscreenDiv(Meteor.render(function() {
|
||||
var div = OnscreenDiv(Meteor.render(function () {
|
||||
return "<p>The number is "+R.get()+".</p><hr><br><br><u>underlined</u>";
|
||||
}));
|
||||
|
||||
@@ -194,9 +304,9 @@ Tinytest.add("spark - slow path GC", function(test) {
|
||||
test.equal(R.numListeners(), 0);
|
||||
});
|
||||
|
||||
Tinytest.add("spark - isolate", function(test) {
|
||||
Tinytest.add("spark - isolate", function (test) {
|
||||
|
||||
var inc = function(v) {
|
||||
var inc = function (v) {
|
||||
v.set(v.get() + 1); };
|
||||
|
||||
var R1 = ReactiveVar(0);
|
||||
@@ -204,11 +314,11 @@ Tinytest.add("spark - isolate", function(test) {
|
||||
var R3 = ReactiveVar(0);
|
||||
var count1 = 0, count2 = 0, count3 = 0;
|
||||
|
||||
var frag = WrappedFrag(Meteor.render(function() {
|
||||
var frag = WrappedFrag(Meteor.render(function () {
|
||||
return R1.get() + "," + (count1++) + " " +
|
||||
Spark.isolate(function() {
|
||||
Spark.isolate(function () {
|
||||
return R2.get() + "," + (count2++) + " " +
|
||||
Spark.isolate(function() {
|
||||
Spark.isolate(function () {
|
||||
return R3.get() + "," + (count3++);
|
||||
});
|
||||
});
|
||||
@@ -241,13 +351,13 @@ Tinytest.add("spark - isolate", function(test) {
|
||||
R2.set(0);
|
||||
R3.set(0);
|
||||
|
||||
frag = WrappedFrag(Meteor.render(function() {
|
||||
frag = WrappedFrag(Meteor.render(function () {
|
||||
var buf = [];
|
||||
buf.push('<div class="foo', R1.get(), '">');
|
||||
buf.push(Spark.isolate(function() {
|
||||
buf.push(Spark.isolate(function () {
|
||||
var buf = [];
|
||||
for(var i=0; i<R2.get(); i++) {
|
||||
buf.push(Spark.isolate(function() {
|
||||
buf.push(Spark.isolate(function () {
|
||||
return '<div>'+R3.get()+'</div>';
|
||||
}));
|
||||
}
|
||||
@@ -296,12 +406,12 @@ Tinytest.add("spark - isolate", function(test) {
|
||||
frag.release();
|
||||
|
||||
// calling isolate() outside of render mode
|
||||
test.equal(Spark.isolate(function() { return "foo"; }), "foo");
|
||||
test.equal(Spark.isolate(function () { return "foo"; }), "foo");
|
||||
|
||||
// caller violating preconditions
|
||||
|
||||
test.throws(function() {
|
||||
Meteor.render(function() {
|
||||
test.throws(function () {
|
||||
Meteor.render(function () {
|
||||
return Spark.isolate("foo");
|
||||
});
|
||||
});
|
||||
@@ -310,10 +420,10 @@ Tinytest.add("spark - isolate", function(test) {
|
||||
// unused isolate
|
||||
|
||||
var Q = ReactiveVar("foo");
|
||||
Meteor.render(function() {
|
||||
Meteor.render(function () {
|
||||
// create an isolate, in render mode,
|
||||
// but don't use it.
|
||||
Spark.isolate(function() {
|
||||
Spark.isolate(function () {
|
||||
return Q.get();
|
||||
});
|
||||
return "";
|
||||
@@ -328,9 +438,9 @@ Tinytest.add("spark - isolate", function(test) {
|
||||
// nesting
|
||||
|
||||
var stuff = ReactiveVar(true);
|
||||
var div = OnscreenDiv(Meteor.render(function() {
|
||||
return Spark.isolate(function() {
|
||||
return "x"+(stuff.get() ? 'y' : '') + Spark.isolate(function() {
|
||||
var div = OnscreenDiv(Meteor.render(function () {
|
||||
return Spark.isolate(function () {
|
||||
return "x"+(stuff.get() ? 'y' : '') + Spark.isolate(function () {
|
||||
return "hi";
|
||||
});
|
||||
});
|
||||
@@ -347,19 +457,19 @@ Tinytest.add("spark - isolate", function(test) {
|
||||
var num1 = ReactiveVar(false);
|
||||
var num2 = ReactiveVar(false);
|
||||
var num3 = ReactiveVar(false);
|
||||
var numset = function(n) {
|
||||
_.each([num1, num2, num3], function(v, i) {
|
||||
var numset = function (n) {
|
||||
_.each([num1, num2, num3], function (v, i) {
|
||||
v.set((i+1) === n);
|
||||
});
|
||||
};
|
||||
numset(1);
|
||||
|
||||
var div = OnscreenDiv(Meteor.render(function() {
|
||||
return Spark.isolate(function() {
|
||||
var div = OnscreenDiv(Meteor.render(function () {
|
||||
return Spark.isolate(function () {
|
||||
return (num1.get() ? '1' : '')+
|
||||
Spark.isolate(function() {
|
||||
Spark.isolate(function () {
|
||||
return (num2.get() ? '2' : '')+
|
||||
Spark.isolate(function() {
|
||||
Spark.isolate(function () {
|
||||
return (num3.get() ? '3' : '')+'x';
|
||||
});
|
||||
});
|
||||
@@ -482,7 +592,7 @@ Tinytest.add("spark - data context", function (test) {
|
||||
Tinytest.add("spark - tables", function (test) {
|
||||
var R = ReactiveVar(0);
|
||||
|
||||
var table = OnscreenDiv(Meteor.render(function() {
|
||||
var table = OnscreenDiv(Meteor.render(function () {
|
||||
var buf = [];
|
||||
buf.push("<table>");
|
||||
for(var i=0; i<R.get(); i++)
|
||||
@@ -520,7 +630,7 @@ Tinytest.add("spark - tables", function (test) {
|
||||
|
||||
var div = OnscreenDiv();
|
||||
div.node().appendChild(document.createElement("TABLE"));
|
||||
div.node().firstChild.appendChild(Meteor.render(function() {
|
||||
div.node().firstChild.appendChild(Meteor.render(function () {
|
||||
var buf = [];
|
||||
for(var i=0; i<R.get(); i++)
|
||||
buf.push("<tr><td>"+(i+1)+"</td></tr>");
|
||||
@@ -547,7 +657,7 @@ Tinytest.add("spark - tables", function (test) {
|
||||
div.node().appendChild(DomUtils.htmlToFragment("<table><tr></tr></table>"));
|
||||
R.set(3);
|
||||
div.node().getElementsByTagName("tr")[0].appendChild(Meteor.render(
|
||||
function() {
|
||||
function () {
|
||||
var buf = [];
|
||||
for(var i=0; i<R.get(); i++)
|
||||
buf.push("<td>"+(i+1)+"</td>");
|
||||
@@ -563,4 +673,303 @@ Tinytest.add("spark - tables", function (test) {
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
test.equal(R.numListeners(), 0);
|
||||
});
|
||||
});
|
||||
|
||||
var eventmap = function (/*args*/) {
|
||||
// support event_buf as final argument
|
||||
var event_buf = null;
|
||||
if (arguments.length && _.isArray(arguments[arguments.length-1])) {
|
||||
event_buf = arguments[arguments.length-1];
|
||||
arguments.length--;
|
||||
}
|
||||
var events = {};
|
||||
_.each(arguments, function (esel) {
|
||||
var etyp = esel.split(' ')[0];
|
||||
events[esel] = function (evt) {
|
||||
if (evt.type !== etyp)
|
||||
throw new Error(etyp+" event arrived as "+evt.type);
|
||||
(event_buf || this).push(esel);
|
||||
};
|
||||
});
|
||||
return events;
|
||||
};
|
||||
|
||||
Tinytest.add("spark - event handling", function (test) {
|
||||
var event_buf = [];
|
||||
var getid = function (id) {
|
||||
return document.getElementById(id);
|
||||
};
|
||||
|
||||
var div;
|
||||
|
||||
var chunk = function (htmlFunc, options) {
|
||||
var html = Spark.isolate(htmlFunc);
|
||||
options = options || {};
|
||||
if (options.events)
|
||||
html = Spark.attachEvents(options.events, html);
|
||||
if (options.event_data)
|
||||
html = Spark.setDataContext(options.event_data, html);
|
||||
return html;
|
||||
};
|
||||
|
||||
var render = function (htmlFunc, options) {
|
||||
return Spark.render(function () {
|
||||
return chunk(htmlFunc, options);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// clicking on a div at top level
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(render(function () {
|
||||
return '<div id="foozy">Foo</div>';
|
||||
}, {events: eventmap("click"), event_data:event_buf}));
|
||||
clickElement(getid("foozy"));
|
||||
test.equal(event_buf, ['click']);
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// selector that specifies a top-level div
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(render(function () {
|
||||
return '<div id="foozy">Foo</div>';
|
||||
}, {events: eventmap("click div"), event_data:event_buf}));
|
||||
clickElement(getid("foozy"));
|
||||
test.equal(event_buf, ['click div']);
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// selector that specifies a second-level span
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(render(function () {
|
||||
return '<div id="foozy"><span>Foo</span></div>';
|
||||
}, {events: eventmap("click span"), event_data:event_buf}));
|
||||
clickElement(getid("foozy").firstChild);
|
||||
test.equal(event_buf, ['click span']);
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// replaced top-level elements still have event handlers
|
||||
// even if replaced by an isolate above the handlers in the DOM
|
||||
var R = ReactiveVar("p");
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(render(function () {
|
||||
return chunk(function () {
|
||||
return '<'+R.get()+' id="foozy">Hello</'+R.get()+'>';
|
||||
});
|
||||
}, {events: eventmap("click"), event_data:event_buf}));
|
||||
clickElement(getid("foozy"));
|
||||
test.equal(event_buf, ['click']);
|
||||
event_buf.length = 0;
|
||||
R.set("div"); // change tag, which is sure to replace element
|
||||
Meteor.flush();
|
||||
clickElement(getid("foozy")); // still clickable?
|
||||
test.equal(event_buf, ['click']);
|
||||
event_buf.length = 0;
|
||||
R.set("p");
|
||||
Meteor.flush();
|
||||
clickElement(getid("foozy"));
|
||||
test.equal(event_buf, ['click']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// bubbling from event on descendent of element matched
|
||||
// by selector
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(render(function () {
|
||||
return '<div id="foozy"><span><u><b>Foo</b></u></span>'+
|
||||
'<span>Bar</span></div>';
|
||||
}, {events: eventmap("click span"), event_data:event_buf}));
|
||||
clickElement(
|
||||
getid("foozy").firstChild.firstChild.firstChild);
|
||||
test.equal(event_buf, ['click span']);
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// bubbling order (for same event, same render node, different selector nodes)
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(render(function () {
|
||||
return '<div id="foozy"><span><u><b>Foo</b></u></span>'+
|
||||
'<span>Bar</span></div>';
|
||||
}, {events: eventmap("click span", "click b"), event_data:event_buf}));
|
||||
clickElement(
|
||||
getid("foozy").firstChild.firstChild.firstChild);
|
||||
test.equal(event_buf, ['click b', 'click span']);
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// "bubbling" order for handlers at same level
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(render(function () {
|
||||
return chunk(function () {
|
||||
return chunk(function () {
|
||||
return '<span id="foozy" class="a b c">Hello</span>';
|
||||
}, {events: eventmap("click .c"), event_data:event_buf});
|
||||
}, {events: eventmap("click .b"), event_data:event_buf});
|
||||
}, {events: eventmap("click .a"), event_data:event_buf}));
|
||||
clickElement(getid("foozy"));
|
||||
test.equal(event_buf, ['click .c', 'click .b', 'click .a']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// stopPropagation doesn't prevent other event maps from
|
||||
// handling same node
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(render(function () {
|
||||
return chunk(function () {
|
||||
return chunk(function () {
|
||||
return '<span id="foozy" class="a b c">Hello</span>';
|
||||
}, {events: eventmap("click .c"), event_data:event_buf});
|
||||
}, {events: {"click .b": function (evt) {
|
||||
event_buf.push("click .b"); evt.stopPropagation();}}});
|
||||
}, {events: eventmap("click .a"), event_data:event_buf}));
|
||||
clickElement(getid("foozy"));
|
||||
test.equal(event_buf, ['click .c', 'click .b', 'click .a']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// stopImmediatePropagation DOES
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(render(function () {
|
||||
return chunk(function () {
|
||||
return chunk(function () {
|
||||
return '<span id="foozy" class="a b c">Hello</span>';
|
||||
}, {events: eventmap("click .c"), event_data:event_buf});
|
||||
}, {events: {"click .b": function (evt) {
|
||||
event_buf.push("click .b");
|
||||
evt.stopImmediatePropagation();}}});
|
||||
}, {events: eventmap("click .a"), event_data:event_buf}));
|
||||
clickElement(getid("foozy"));
|
||||
test.equal(event_buf, ['click .c', 'click .b']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// bubbling continues even with DOM change
|
||||
event_buf.length = 0;
|
||||
R = ReactiveVar(true);
|
||||
div = OnscreenDiv(render(function () {
|
||||
return chunk(function () {
|
||||
return '<div id="blarn">'+(R.get()?'<span id="foozy">abcd</span>':'')+'</div>';
|
||||
}, {events: { 'click span': function () {
|
||||
event_buf.push('click span');
|
||||
R.set(false);
|
||||
Meteor.flush(); // kill the span
|
||||
}, 'click div': function (evt) {
|
||||
event_buf.push('click div');
|
||||
}}});
|
||||
}));
|
||||
// click on span
|
||||
clickElement(getid("foozy"));
|
||||
test.expect_fail(); // doesn't seem to work in old IE
|
||||
test.equal(event_buf, ['click span', 'click div']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// "deep reach" from high node down to replaced low node.
|
||||
// Tests that attach_secondary_events actually does the
|
||||
// right thing in IE. Also tests change event bubbling
|
||||
// and proper interpretation of event maps.
|
||||
event_buf.length = 0;
|
||||
R = ReactiveVar('foo');
|
||||
div = OnscreenDiv(render(function () {
|
||||
return '<div><p><span><b>'+
|
||||
chunk(function () {
|
||||
return '<input type="checkbox">'+R.get();
|
||||
}, {events: eventmap('click input'), event_data:event_buf}) +
|
||||
'</b></span></p></div>';
|
||||
}, { events: eventmap('change b', 'change input'), event_data:event_buf }));
|
||||
R.set('bar');
|
||||
Meteor.flush();
|
||||
// click on input
|
||||
clickElement(div.node().getElementsByTagName('input')[0]);
|
||||
event_buf.sort(); // don't care about order
|
||||
test.equal(event_buf, ['change b', 'change input', 'click input']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// test that 'click *' fires on bubble
|
||||
event_buf.length = 0;
|
||||
R = ReactiveVar('foo');
|
||||
div = OnscreenDiv(render(function () {
|
||||
return '<div><p><span><b>'+
|
||||
chunk(function () {
|
||||
return '<input type="checkbox">'+R.get();
|
||||
}, {events: eventmap('click input'), event_data:event_buf}) +
|
||||
'</b></span></p></div>';
|
||||
}, { events: eventmap('click *'), event_data:event_buf }));
|
||||
R.set('bar');
|
||||
Meteor.flush();
|
||||
// click on input
|
||||
clickElement(div.node().getElementsByTagName('input')[0]);
|
||||
test.equal(
|
||||
event_buf,
|
||||
['click input', 'click *', 'click *', 'click *', 'click *', 'click *']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// clicking on a div in a nested chunk (without patching)
|
||||
event_buf.length = 0;
|
||||
R = ReactiveVar('foo');
|
||||
div = OnscreenDiv(render(function () {
|
||||
return R.get() + chunk(function () {
|
||||
return '<span>ism</span>';
|
||||
}, {events: eventmap("click"), event_data:event_buf});
|
||||
}));
|
||||
test.equal(div.text(), 'fooism');
|
||||
clickElement(div.node().getElementsByTagName('SPAN')[0]);
|
||||
test.equal(event_buf, ['click']);
|
||||
event_buf.length = 0;
|
||||
R.set('bar');
|
||||
Meteor.flush();
|
||||
test.equal(div.text(), 'barism');
|
||||
clickElement(div.node().getElementsByTagName('SPAN')[0]);
|
||||
test.equal(event_buf, ['click']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// Test that reactive fragments manually inserted inside
|
||||
// a reactive fragment eventually get wired.
|
||||
event_buf.length = 0;
|
||||
div = OnscreenDiv(render(function () {
|
||||
return "<div></div>";
|
||||
}, { events: eventmap("click span", event_buf) }));
|
||||
Meteor.flush();
|
||||
div.node().firstChild.appendChild(render(function () {
|
||||
return '<span id="foozy">hello</span>';
|
||||
}));
|
||||
clickElement(getid("foozy"));
|
||||
// implementation has no way to know we've inserted the fragment
|
||||
test.equal(event_buf, []);
|
||||
event_buf.length = 0;
|
||||
Meteor.flush();
|
||||
clickElement(getid("foozy"));
|
||||
// now should be wired up
|
||||
test.equal(event_buf, ['click span']);
|
||||
event_buf.length = 0;
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// Event data comes from event.currentTarget, not event.target
|
||||
var data_buf = [];
|
||||
div = OnscreenDiv(render(function () {
|
||||
return "<ul>"+chunk(function () {
|
||||
return '<li id="funyard">Hello</li>';
|
||||
}, { event_data: {x:'listuff'} })+"</ul>";
|
||||
}, { event_data: {x:'ulstuff'},
|
||||
events: { 'click ul': function () { data_buf.push(this); }}}));
|
||||
clickElement(getid("funyard"));
|
||||
test.equal(data_buf, [{x:'ulstuff'}]);
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
});
|
||||
|
||||
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user