mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'master' into auth
Conflicts: .gitignore examples/todos/client/todos.js
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@
|
||||
/dev_bundle*.tar.gz
|
||||
/dist
|
||||
\#*\#
|
||||
.\#*
|
||||
.\#*
|
||||
.idea
|
||||
|
||||
36
History.md
36
History.md
@@ -1,6 +1,42 @@
|
||||
|
||||
## vNEXT
|
||||
|
||||
## v0.4.0
|
||||
|
||||
* Merge Spark, a new live page update engine
|
||||
* Breaking API changes
|
||||
* Input elements no longer preserved based on `id` and `name` attributes. Use [`preserve`](http://docs.meteor.com/#template_preserve) instead.
|
||||
|
||||
* All `Meteor.ui` functions removed. Use `Meteor.render`, `Meteor.renderList`, and [Spark](https://github.com/meteor/meteor/wiki/Spark) functions instead.
|
||||
|
||||
* New template functions (eg. `created`, `rendered`, etc) may collide with existing helpers. Use `Template.foo.helpers()` to avoid conflicts.
|
||||
|
||||
* New syntax for declaring event maps. Use `Template.foo.events({...})`. For backwards compatibility, both syntaxes are allowed for now.
|
||||
|
||||
* New Template features
|
||||
|
||||
* Allow embedding non-Meteor widgets (eg. Google Maps) using [`{{#constant}}`](http://docs.meteor.com/#constant)
|
||||
|
||||
* Callbacks when templates are rendered. See http://docs.meteor.com/#template_rendered
|
||||
|
||||
* Explicit control of which nodes are preserved during re-rendering. See http://docs.meteor.com/#template_preserve
|
||||
|
||||
* Easily find nodes within a template in event handlers and callbacks. See http://docs.meteor.com/#template_find
|
||||
|
||||
* Allow parts of a template to be independently reactive with the [`{{#isolate}}`](http://docs.meteor.com/#isolate) block helper.
|
||||
|
||||
|
||||
* Use PACKAGE_DIRS environment variable to override package location. #227
|
||||
|
||||
* Add `absolute-url` package to construct URLs pointing to the application.
|
||||
|
||||
* Allow modifying documents returned by `observe` callbacks. #209
|
||||
|
||||
* Fix periodic crash after client disconnect. #212
|
||||
|
||||
* Fix minimingo crash on dotted queries with undefined keys. #126
|
||||
|
||||
|
||||
## v0.3.9
|
||||
|
||||
* Add `spiderable` package to allow web crawlers to index Meteor apps.
|
||||
|
||||
32
LICENSE.txt
32
LICENSE.txt
@@ -581,6 +581,38 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
----------
|
||||
d3: https://github.com/mbostock/d3
|
||||
----------
|
||||
|
||||
Copyright (c) 2012, Michael Bostock
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* The name Michael Bostock may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
|
||||
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
||||
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
|
||||
|
||||
=============
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
meteor (0.3.9-1) unstable; urgency=low
|
||||
meteor (0.4.0-1) unstable; urgency=low
|
||||
|
||||
* Automated debian build.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
## example.
|
||||
|
||||
URLBASE="https://d3sqy0vbqsdhku.cloudfront.net"
|
||||
VERSION="0.3.9"
|
||||
VERSION="0.4.0"
|
||||
PKGVERSION="${VERSION}-1"
|
||||
|
||||
UNAME=`uname`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "0.3.9",
|
||||
"deb_version": "0.3.9-1",
|
||||
"rpm_version": "0.3.9-1",
|
||||
"version": "0.4.0",
|
||||
"deb_version": "0.4.0-1",
|
||||
"rpm_version": "0.4.0-1",
|
||||
"urlbase": "https://d3sqy0vbqsdhku.cloudfront.net"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
Summary: Meteor platform and JavaScript application server
|
||||
Vendor: Meteor
|
||||
Name: meteor
|
||||
Version: 0.3.9
|
||||
Version: 0.4.0
|
||||
Release: 1
|
||||
License: MIT
|
||||
Group: Networking/WWW
|
||||
|
||||
@@ -191,10 +191,32 @@ var files = module.exports = {
|
||||
else
|
||||
return path.join(__dirname, '../..');
|
||||
},
|
||||
|
||||
// Return where the packages are stored
|
||||
get_package_dir: function () {
|
||||
return path.join(__dirname, '../../packages');
|
||||
|
||||
// returns a list of places where packages can be found.
|
||||
// 1. directories set via process.env.PACKAGES_DIRS
|
||||
// 2. default is packages/ in the meteor directory
|
||||
// XXX: 3. a per project directory? (vendor/packages in rails parlance?)
|
||||
get_package_dirs: function() {
|
||||
var package_dirs = [path.join(__dirname, '../../packages')];
|
||||
if (process.env.PACKAGE_DIRS)
|
||||
package_dirs = process.env.PACKAGE_DIRS.split(':').concat(package_dirs);
|
||||
|
||||
return package_dirs;
|
||||
},
|
||||
|
||||
// search package dirs for a package named name.
|
||||
// undefined if the package isn't in any dir
|
||||
get_package_dir: function (name) {
|
||||
var ret;
|
||||
_.find(this.get_package_dirs(), function(package_dir) {
|
||||
var dir = path.join(package_dir, name);
|
||||
if (path.existsSync(dir)) {
|
||||
ret = dir;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
// Return the directory that contains the core tool (the top-level
|
||||
|
||||
@@ -84,10 +84,10 @@ _.extend(Package.prototype, {
|
||||
init_from_library: function (name) {
|
||||
var self = this;
|
||||
self.name = name;
|
||||
self.source_root = path.join(__dirname, '../../packages', name);
|
||||
self.source_root = files.get_package_dir(name);
|
||||
self.serve_root = path.join('/packages', name);
|
||||
|
||||
var fullpath = path.join(files.get_package_dir(), name, 'package.js');
|
||||
|
||||
var fullpath = path.join(self.source_root, 'package.js');
|
||||
var code = fs.readFileSync(fullpath).toString();
|
||||
// \n is necessary in case final line is a //-comment
|
||||
var wrapped = "(function(Package,require){" + code + "\n})";
|
||||
@@ -128,7 +128,7 @@ _.extend(Package.prototype, {
|
||||
// stack -- has to come before user packages, because we don't
|
||||
// (presently) require packages to declare dependencies on
|
||||
// 'standard meteor stuff' like minimongo.
|
||||
api.use(['deps', 'session', 'livedata', 'mongo-livedata', 'liveui',
|
||||
api.use(['deps', 'session', 'livedata', 'mongo-livedata', 'spark',
|
||||
'templating', 'startup', 'past']);
|
||||
api.use(require('./project.js').get_packages(app_dir));
|
||||
|
||||
@@ -266,12 +266,14 @@ var packages = module.exports = {
|
||||
// a package object.
|
||||
list: function () {
|
||||
var ret = {};
|
||||
var dir = files.get_package_dir();
|
||||
_.each(fs.readdirSync(dir), function (name) {
|
||||
// skip .meteor directory
|
||||
if (path.existsSync(path.join(dir, name, 'package.js')))
|
||||
ret[name] = packages.get(name);
|
||||
});
|
||||
|
||||
_.each(files.get_package_dirs(), function(dir) {
|
||||
_.each(fs.readdirSync(dir), function (name) {
|
||||
// skip .meteor directory
|
||||
if (path.existsSync(path.join(dir, name, 'package.js')))
|
||||
ret[name] = packages.get(name);
|
||||
});
|
||||
})
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
exports.CURRENT_VERSION = "0.3.9";
|
||||
exports.CURRENT_VERSION = "0.4.0";
|
||||
|
||||
var fs = require("fs");
|
||||
var http = require("http");
|
||||
|
||||
@@ -152,7 +152,7 @@ Commands.push({
|
||||
|
||||
var example_dir = path.join(__dirname, '../../examples');
|
||||
var examples = _.reject(fs.readdirSync(example_dir), function (e) {
|
||||
return (e === 'unfinished');
|
||||
return (e === 'unfinished' || e === 'other');
|
||||
});
|
||||
|
||||
if (argv._.length === 1) {
|
||||
|
||||
@@ -2,7 +2,7 @@ try {
|
||||
// XXX can't get this from updater.js because in 0.3.7 and before the
|
||||
// updater didn't have the right NODE_PATH set. At some point we can
|
||||
// remove this and just use updater.CURRENT_VERSION.
|
||||
var VERSION = "0.3.9";
|
||||
var VERSION = "0.4.0";
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
@@ -274,7 +274,7 @@ var DependencyWatcher = function (deps, app_dir, on_change) {
|
||||
self.specific_files = {};
|
||||
for (var pkg in (deps.packages || {})) {
|
||||
_.each(deps.packages[pkg], function (file) {
|
||||
self.specific_files[path.join(files.get_package_dir(), pkg, file)]
|
||||
self.specific_files[path.join(files.get_package_dir(pkg), file)]
|
||||
= true;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ put on the screen.
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
<h2 id="publishandsubscribe"><span>Publish and subscribe</span></h2>
|
||||
|
||||
These functions control how Meteor servers publish sets of records and
|
||||
@@ -514,7 +515,7 @@ those changes may or may not appear in the result set.
|
||||
|
||||
Cursors are a reactive data source. The first time you retrieve a
|
||||
cursor's documents with `fetch`, `map`, or `forEach` inside a
|
||||
reactive context (eg, [`Meteor.ui.render`](#meteor_ui_render),
|
||||
reactive context (eg, [`Meteor.render`](#meteor_render),
|
||||
[`Meteor.autosubscribe`](#meteor_autosubscribe), Meteor will register a
|
||||
dependency on the underlying data. Any change to the collection that
|
||||
changes the documents in a cursor will trigger a recomputation. To
|
||||
@@ -620,23 +621,6 @@ Example:
|
||||
// Delete all the log entries
|
||||
Logs.remove({});
|
||||
|
||||
// Show a list of posts that have been flagged, updating in realtime.
|
||||
// Put a link next to each post that deletes the post if clicked.
|
||||
var frag = Meteor.ui.render(function() {
|
||||
return Meteor.ui.listChunk(
|
||||
Posts.find({flagged: true}),
|
||||
function (post) {
|
||||
// In real code it'd be necessary to sanitize post.name
|
||||
return "<div>" + post.name +
|
||||
" <span class='delete'>Delete</span></div>";
|
||||
},
|
||||
{ events: {
|
||||
'click .delete': function () {
|
||||
Posts.remove(this._id);
|
||||
}
|
||||
}});
|
||||
});
|
||||
document.body.appendChild(frag);
|
||||
|
||||
<h2 id="meteor_collection_cursor"><span>Cursors</span></h2>
|
||||
|
||||
@@ -672,7 +656,7 @@ the matching documents.
|
||||
|
||||
// Display a count of posts matching certain criteria. Automatically
|
||||
// keep it updated as the database changes.
|
||||
var frag = Meteor.ui.render(function () {
|
||||
var frag = Meteor.render(function () {
|
||||
var high_scoring = Posts.find({score: {$gt: 10}});
|
||||
return "<p>There are " + high_scoring.count() + " posts with " +
|
||||
"scores greater than 10</p>";
|
||||
@@ -869,7 +853,7 @@ See [`Meteor.deps`](#meteor_deps) for another example.
|
||||
Example:
|
||||
|
||||
Session.set("enemy", "Eastasia");
|
||||
var frag = Meteor.ui.render(function () {
|
||||
var frag = Meteor.render(function () {
|
||||
return "<p>We've always been at war with " +
|
||||
Session.get("enemy") + "</p>";
|
||||
});
|
||||
@@ -916,11 +900,11 @@ Example:
|
||||
"selected" : "";
|
||||
};
|
||||
|
||||
Template.post_item.events = {
|
||||
Template.post_item.events({
|
||||
'click': function() {
|
||||
Session.set("selected_post", this._id);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Using Session.equals here means that when the user clicks
|
||||
// on an item and changes the selection, only the newly selected
|
||||
@@ -929,85 +913,260 @@ Example:
|
||||
// If Session.get had been used instead of Session.equals, then
|
||||
// when the selection changed, all the items would be re-rendered.
|
||||
|
||||
<h2 id="meteor_ui"><span>Meteor.ui</span></h2>
|
||||
<h2 id="templates_api"><span>Templates</span></h2>
|
||||
|
||||
`Meteor.ui` provides building blocks for creating reactive UIs out of strings of
|
||||
HTML, making it easy to create DOM elements that update
|
||||
automatically as data changes in
|
||||
[`Session`](#session) variables or in a
|
||||
[`Meteor.Collection`](#meteor_collection). Meteor's built-in templates already use these functions, but if you prefer a different way of generating HTML,
|
||||
are integrating a new template language with Meteor, or need to compose a reactive
|
||||
snippet of HTML on the fly, then this package has what you need.
|
||||
A template that you declare as `<{{! }}template name="foo"> ... </{{!
|
||||
}}template>` can be accessed as the function `Template.foo`, which
|
||||
returns a string of HTML when called.
|
||||
|
||||
The same template may occur many times on the page, and these
|
||||
occurrences are called template instances. Template instances have a
|
||||
life cycle of being created, put into the document, and later taken
|
||||
out of the document and destroyed. Meteor manages these stages for
|
||||
you, including determining when a template instance has been removed
|
||||
or replaced and should be cleaned up. You can associate data with a
|
||||
template instance, and you can access its DOM nodes when it is in the
|
||||
document.
|
||||
|
||||
Additionally, Meteor will maintain a template instance and its state
|
||||
even if its surrounding HTML is re-rendered into new DOM nodes. As
|
||||
long as the structure of template invocations is the same, Meteor will
|
||||
not consider any instances to have been created or destroyed. You can
|
||||
request that the same DOM nodes be retained as well using `preserve`
|
||||
and `constant`.
|
||||
|
||||
There are a number of callbacks and directives that you can specify on
|
||||
a named template and that apply to all instances of the template.
|
||||
They are described below.
|
||||
|
||||
{{> api_box template_call}}
|
||||
|
||||
When called inside a template helper, the body of `Meteor.render`, or
|
||||
other settings where reactive HTML is being generated, the resulting
|
||||
HTML is annotated so that it renders as reactive DOM elements.
|
||||
Otherwise, the HTML is unadorned and static.
|
||||
|
||||
|
||||
{{> api_box template_rendered}}
|
||||
|
||||
This callback is called once when an instance of Template.*myTemplate* is rendered into DOM nodes and put into the document for the first time, and again each time any part of the template is re-rendered.
|
||||
|
||||
In the body of the callback, `this` is a [template
|
||||
instance](#template_inst) object that is unique to this occurrence of
|
||||
the template and persists across re-renderings. Use the `created` and
|
||||
`destroyed` callbacks to perform initialization or clean-up on the
|
||||
object.
|
||||
|
||||
{{> api_box template_created}}
|
||||
|
||||
This callback is called when an invocation of *myTemplate* represents
|
||||
a new occurrence of the template and not a re-rendering of an existing
|
||||
template instance. Inside the callback, `this` is the new [template
|
||||
instance](#template_inst) object. Properties you set on this object
|
||||
will be visible from the `rendered` and `destroyed` callbacks and from
|
||||
event handlers.
|
||||
|
||||
This callback fires once and is the first callback to fire. Every
|
||||
`created` has a corresponding `destroyed`; that is, if you get a
|
||||
`created` callback with a certain template instance object in `this`,
|
||||
you will eventually get a `destroyed` callback for the same object.
|
||||
|
||||
{{> api_box template_destroyed}}
|
||||
|
||||
This callback is called when an occurrence of a template is taken off
|
||||
the page for any reason and not replaced with a re-rendering. Inside
|
||||
the callback, `this` is the [template instance](#template_inst) object
|
||||
being destroyed.
|
||||
|
||||
This callback is most useful for cleaning up or undoing any external
|
||||
effects of `created`. It fires once and is the last callback to fire.
|
||||
|
||||
|
||||
{{> api_box template_events}}
|
||||
|
||||
Declare event handers for instances of this template. Multiple calls add
|
||||
new event handlers in addition to the existing ones.
|
||||
|
||||
See [Event Maps](#eventmaps) for a detailed description of the event
|
||||
map format and how event handling works in Meteor.
|
||||
|
||||
{{#note}}
|
||||
This syntax replaces the previous syntax: `Template.myTemplate.events = {...}`, but for now, the
|
||||
old syntax still works.
|
||||
{{/note}}
|
||||
|
||||
|
||||
{{> api_box template_helpers}}
|
||||
|
||||
Each template has a local dictionary of helpers that are made available to it,
|
||||
and this call specifies helpers to add to the template's dictionary.
|
||||
|
||||
Example:
|
||||
|
||||
Template.myTemplate.helpers({
|
||||
foo: function () {
|
||||
return Session.get("foo");
|
||||
}
|
||||
});
|
||||
|
||||
In Handlebars, this helper would then be invoked as `{{dstache}}foo}}`.
|
||||
|
||||
The following syntax is equivalent, but won't work for reserved property
|
||||
names:
|
||||
|
||||
Template.myTemplate.foo = function () {
|
||||
return Session.get("foo");
|
||||
};
|
||||
|
||||
{{> api_box template_preserve}}
|
||||
|
||||
You can "preserve" a DOM element during re-rendering, leaving the
|
||||
existing element in place in the document while replacing the
|
||||
surrounding HTML. This means that re-rendering a template need not
|
||||
disturb text fields, iframes, and other sensitive elements it
|
||||
contains. The elements to preserve must be present both as nodes in
|
||||
the old DOM and as tags in the new HTML. Meteor will patch the DOM
|
||||
around the preserved elements.
|
||||
|
||||
Preservation is useful in a variety of cases where replacing a DOM
|
||||
element with an identical or modified element would not have the same
|
||||
effect as retaining the original element. These include:
|
||||
|
||||
* Input text fields and other form controls
|
||||
* Elements with CSS animations
|
||||
* Iframes
|
||||
* Nodes with references kept in JavaScript code
|
||||
|
||||
If you want to preserve a whole region of the DOM, an element and its
|
||||
children, or nodes not rendered by Meteor, use a [constant
|
||||
region](#constant) instead.
|
||||
|
||||
To preserve nodes, pass a list of selectors each of which should match
|
||||
at most one element in the template. When the template is re-rendered,
|
||||
the selector is run on the old DOM and the new DOM, and Meteor will
|
||||
reuse the old element in place while working in any HTML changes around
|
||||
it.
|
||||
|
||||
A second form of `preserve` takes a labeling function for each selector
|
||||
and allows the selectors to match multiple nodes. The node-labeling
|
||||
function takes a node and returns a label string that is unique for each
|
||||
node, or `false` to exclude the node from preservation.
|
||||
|
||||
For example, to preserve all `<input>` elements with ids in template 'foo', use:
|
||||
|
||||
Template.foo.preserve({
|
||||
'input[id]': function (node) { return node.id; }
|
||||
});
|
||||
|
||||
Selectors are interpreted as rooted at the top level of the template.
|
||||
Each occurrence of the template operates independently, so the selectors
|
||||
do not have to be unique on the entire page, only within one occurrence
|
||||
of the template. Selectors will match nodes even if they are in
|
||||
sub-templates.
|
||||
|
||||
Preserving a node does *not* preserve its attributes or contents. They
|
||||
will be updated to reflect the new HTML. Text in input fields is not
|
||||
preserved unless the input field has focus, in which case the cursor and
|
||||
selection are left intact. Iframes retain their navigation state and
|
||||
animations continue to run as long as their parameters haven't changed.
|
||||
|
||||
There are some cases where nodes can not be preserved because of
|
||||
constraints inherent in the DOM API. For example, an element's tag name
|
||||
can't be changed, and it can't be moved relative to its parent or other
|
||||
preserved nodes. For this reason, nodes that are re-ordered or
|
||||
re-parented by an update will not be preserved.
|
||||
|
||||
{{#note}}
|
||||
Previous versions of Meteor had an implicit page-wide `preserve`
|
||||
directive that labeled nodes by their "id" and "name" attributes.
|
||||
This has been removed in favor of the explicit, opt-in mechanism.
|
||||
{{/note}}
|
||||
|
||||
|
||||
<h2 id="template_inst"><span>Template instances</span></h2>
|
||||
|
||||
A template instance object represents an occurrence of a template in
|
||||
the document. It can be used to access the DOM and it can be
|
||||
assigned properties that persist across page re-renderings.
|
||||
|
||||
Template instance objects are found as the value of `this` in the
|
||||
`created`, `rendered`, and `destroyed` template callbacks and as an
|
||||
argument to event handlers.
|
||||
|
||||
In addition to the properties and functions described below, you can
|
||||
assign additional properties of your choice to the object. Property names
|
||||
starting with `_` are guaranteed to be available for your use. Use
|
||||
the `created` and `destroyed` callbacks to perform initialization or
|
||||
clean-up on the object.
|
||||
|
||||
You can only access `findAll`, `find`, `firstNode`, and `lastNode`
|
||||
from the `rendered` callback and event handlers, not from `created`
|
||||
and `destroyed`, because they require the template instance to be
|
||||
in the DOM.
|
||||
|
||||
{{> api_box template_findAll}}
|
||||
|
||||
Returns an array of DOM elements matching `selector`.
|
||||
|
||||
The template instance serves as the document root for the selector. Only
|
||||
elements inside the template and its sub-templates can match parts of
|
||||
the selector.
|
||||
|
||||
{{> api_box template_find}}
|
||||
|
||||
Returns one DOM element matching `selector`, or `null` if there are no
|
||||
such elements.
|
||||
|
||||
The template instance serves as the document root for the selector. Only
|
||||
elements inside the template and its sub-templates can match parts of
|
||||
the selector.
|
||||
|
||||
{{> api_box template_firstNode}}
|
||||
|
||||
The two nodes `firstNode` and `lastNode` indicate the extent of the
|
||||
rendered template in the DOM. The rendered template includes these
|
||||
nodes, their intervening siblings, and their descendents. These two
|
||||
nodes are siblings (they have the same parent), and `lastNode` comes
|
||||
after `firstNode`, or else they are the same node.
|
||||
|
||||
{{> api_box template_lastNode}}
|
||||
|
||||
{{> api_box template_data}}
|
||||
|
||||
This property provides access to the data context at the top level of
|
||||
the template. It is updated each time the template is re-rendered.
|
||||
Access is read-only and non-reactive.
|
||||
|
||||
This package is implemented on top of [`Meteor.deps`](#meteor_deps), which provides the
|
||||
data dependency tracking and invalidation system, while `Meteor.ui` contributes
|
||||
the ability to turn HTML into DOM elements, keep track of regions of the DOM that
|
||||
need updating, and patch old DOM content with new, recalculated content.
|
||||
|
||||
{{> api_box render}}
|
||||
|
||||
`Meteor.ui.render` creates a `DocumentFragment` (a sequence of DOM
|
||||
nodes) that automatically updates in realtime. You pass in
|
||||
`html_func`, a function that returns an HTML
|
||||
string. `Meteor.ui.render` calls your function and turns the output
|
||||
into DOM nodes. Meanwhile, it tracks the data that was used when
|
||||
`html_func` ran, and automatically wires up callbacks so that whenever
|
||||
any of the data changes, `html_func` is re-run and the DOM nodes
|
||||
are updated in place.
|
||||
`Meteor.render` creates a `DocumentFragment` (a sequence of DOM nodes)
|
||||
that automatically updates in realtime. Most Meteor apps don't need to
|
||||
call this directly, they use templates and Meteor handles the rendering.
|
||||
|
||||
Insert the returned `DocumentFragment` directly into the DOM wherever
|
||||
you would like it to appear. The inserted nodes will continue to
|
||||
update until they are taken off the screen. Then they will be
|
||||
automatically cleaned up. For the details, see
|
||||
Pass in `htmlFunc`, a function that returns an HTML
|
||||
string. `Meteor.render` calls the function and turns the output into
|
||||
DOM nodes. Meanwhile, it tracks the data that was used when `htmlFunc`
|
||||
ran, and automatically wires up callbacks so that whenever any of the
|
||||
data changes, `htmlFunc` is re-run and the DOM nodes are updated in
|
||||
place.
|
||||
|
||||
You may insert the returned `DocumentFragment` directly into the DOM
|
||||
wherever you would like it to appear. The inserted nodes will continue
|
||||
to update until they are taken off the screen. Then they will be
|
||||
automatically cleaned up. For more details about clean-up, see
|
||||
[`Meteor.flush`](#meteor_flush).
|
||||
|
||||
You can also hook up events to the rendered DOM nodes using the
|
||||
`events` option. If you provide `event_data`, it will be passed to
|
||||
event handlers in `this`. (See [Event Maps](#eventmaps).)
|
||||
|
||||
When render replaces DOM elements because data changed, it can leave
|
||||
input elements undisturbed so that focus is preserved, text entered
|
||||
into fields isn't lost, and so forth. To activate this feature, give
|
||||
each such element a unique `id`, or give it a unique `name` attribute
|
||||
inside the nearest enclosing element with an `id`.
|
||||
|
||||
If you want a region of your HTML to be able to update independently
|
||||
of the other HTML around it, wrap it in [`Meteor.ui.chunk`](#meteor_ui_chunk).
|
||||
|
||||
`Meteor.ui.render` tracks the data dependencies of `html_func` by
|
||||
running it in a reactive context, so it can respond to changes in any
|
||||
reactive data sources used by that function. For more information, or
|
||||
to learn how to make your own reactive data sources, see
|
||||
`Meteor.render` tracks the data dependencies of `htmlFunc` by running
|
||||
it in a reactive context, so it can respond to changes in any reactive
|
||||
data sources used by that function. For more information, or to learn
|
||||
how to make your own reactive data sources, see
|
||||
[Reactivity](#reactivity).
|
||||
|
||||
{{!
|
||||
Meteor.ui.render runs html_func in a reactive context, then returns a
|
||||
DocumentFragment that can be inserted anywhere in a Document and that
|
||||
will automatically update itself in place whenever the context is
|
||||
invalidated. The updating will stop if the nodes in the fragment are
|
||||
ever not on the screen (that is, children of `window.document`) when
|
||||
Meteor.flush runs.
|
||||
|
||||
During an update, if a node has a unique id, or if it has a name that
|
||||
is unique among the descendants of the nearest enclosing parent that
|
||||
has an id, then it will be "patched" (updated in place, rather than
|
||||
replaced), meaning that focus will be preserved, the text in <input>
|
||||
elements will be not be lost, etc.
|
||||
|
||||
|
||||
By default, Meteor.ui.render puts the entire output of `html_func` in
|
||||
a single invalidation context. For finer control of rerendering, you
|
||||
can use Meteor.ui.chunk to create a nested tree of invalidation
|
||||
contexts.
|
||||
[events]
|
||||
[more?]
|
||||
}}
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
// Show the number of users online.
|
||||
var frag = Meteor.ui.render(function () {
|
||||
var frag = Meteor.render(function () {
|
||||
return "<p>There are " + Users.find({online: true}).count() +
|
||||
" users online.</p>";
|
||||
});
|
||||
@@ -1017,146 +1176,47 @@ Example:
|
||||
// offline. The count on the screen will automatically update.
|
||||
Users.update({idleTime: {$gt: 30}}, {online: false});
|
||||
|
||||
// Show a counter, and let the user click to increase or decrease it.
|
||||
Session.set("counter", 0);
|
||||
{{> api_box renderList}}
|
||||
|
||||
var frag = Meteor.ui.render(function () {
|
||||
return '<div>Counter: ' + Session.get("counter") + ' ' +
|
||||
'<span class="inc">Increase</span>' +
|
||||
'<span class="dec">Decrease</span></div>';
|
||||
}, { events:
|
||||
{
|
||||
'click .inc': function (event) {
|
||||
Session.set("counter", Session.get("counter") + 1);
|
||||
},
|
||||
'click .dec': function (event) {
|
||||
Session.set("counter", Session.get("counter") - 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
document.body.appendChild(frag);
|
||||
Creates a `DocumentFragment` that automatically updates as the results
|
||||
of a database query change. Most Meteor apps use `{{dstache}}#each}}` in
|
||||
a template instead of calling this directly.
|
||||
|
||||
{{> api_box chunk}}
|
||||
|
||||
When generating HTML from a function passed to [`Meteor.ui.render`](#meteor_ui_render), you can use `Meteor.ui.chunk` to mark a substring of the HTML as separately reactive. If the data used to generate that substring changes, only the elements corresponding to that substring will be updated, not the elements before and after it.
|
||||
|
||||
Like `render`, `Meteor.ui.chunk` takes a function `html_func` that returns a HTML string. It calls that function, records the data that the function used (using a [reactive context](#meteor_ui_deps)), and arranges to rerun the function as necessary whenever the data changes. What's different from `render` is that it returns another HTML string, not a DocumentFragment. So, unlike `render`, `chunk` may be nested as deeply as you like, for example to render nested views or subtemplates.
|
||||
|
||||
`chunk` can also be used to attach events to part of an HTML string, in much the same way that they could be attached to DOM elements. When the string is parsed into DOM elements by `render`, the event handlers will automatically be hooked up. If `event_data` is provided, it sets the event data for _all_ events that occur that within the chunk, even when those events are handled by handlers declared in enclosing chunks.
|
||||
|
||||
{{#note}}
|
||||
Note: In Internet Explorer 8 and
|
||||
earlier, if you manually add elements to a chunk after it has been
|
||||
rendered — for example, using jQuery or the DOM API — then handlers
|
||||
in event maps may not fire on these elements. This is a limitation of the implementation.
|
||||
{{/note}}
|
||||
|
||||
`chunk` works by creating a unique ID for the chunk, wrapping the HTML string in a comment that calls out that ID, and adding an entry to the chunk table for the current invocation of `render`. As `render` turns the HTML string into DOM nodes, it pulls out the comments and wires up the appropriate callbacks and pointers. On the other hand, if there is no current invocation of `render`, `chunk` just passes the string through unchanged. (In this case, if an event map is provided, it is ignored.)
|
||||
|
||||
The contents of a chunk must be balanced HTML tags; the string returned by
|
||||
`html_func` cannot start or end inside a tag or tag attribute.
|
||||
|
||||
Example:
|
||||
|
||||
Meteor.startup(function() {
|
||||
|
||||
Session.set("greeting", "Hello");
|
||||
Session.set("target", "World");
|
||||
|
||||
// Render two chunks that will be tracked and updated independently.
|
||||
document.body.appendChild(
|
||||
Meteor.ui.render(function() {
|
||||
return "<div>" +
|
||||
Meteor.ui.chunk(function() { return Session.get("greeting"); }) +
|
||||
" " +
|
||||
Meteor.ui.chunk(function() { return Session.get("target"); }) +
|
||||
"</div>";
|
||||
}));
|
||||
|
||||
// Updates "Hello" to "Goodbye" without touching "World"
|
||||
Session.set("greeting", "Goodbye");
|
||||
// Updates "World" to "Ralph" without touching "Goodbye"
|
||||
Session.set("target", "Ralph");
|
||||
|
||||
});
|
||||
|
||||
// Every two seconds, alternates between Goodbye Ralph and Goodbye World.
|
||||
// If you select the word Goodbye or part of it, the selection stays on
|
||||
// update, because the text node is not being replaced.
|
||||
Meteor.setInterval(function() {
|
||||
if (Session.get("target") === "Ralph") {
|
||||
Session.set("target", "World");
|
||||
} else {
|
||||
Session.set("target", "Ralph");
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
|
||||
{{> api_box listChunk}}
|
||||
|
||||
`listChunk` is like `chunk`, but instead of creating one chunk, it creates several, one for each record in the results of a database query.
|
||||
|
||||
It keeps the chunks updated as the results of the database query change. For example, if a new record is created in the database that matches the query, a new chunk is inserted. If the query is sorted, and a database record changes, and the change causes it to move in the sort order, then the chunk is moved appropriately.
|
||||
|
||||
If you provide `else_func`, then whenever the query returns no results, it will be called to render alternative content. You might use this to show a message like "No records match your query."
|
||||
|
||||
You can provide an `events` option to attach a set of event handlers to each chunk that `listChunk` creates. `event_data` is set to the corresponding database record in each chunk, meaning that when any event fires within a chunk, `this` in the event handler will contain the appropriate database record.
|
||||
`renderList` is more efficient than using `Meteor.render` to render HTML
|
||||
for a list of documents. For example, if a new document is created in
|
||||
the database that matches the query, a new item will be rendered and
|
||||
inserted at the appropriate place in the DOM without re-rendering the
|
||||
other elements. Similarly, if a document changes position in a sorted
|
||||
query, the DOM nodes will simply be moved and not re-rendered.
|
||||
|
||||
`docFunc` is called as needed to generate HTML for each document. If
|
||||
you provide `elseFunc`, then whenever the query returns no results, it
|
||||
will be called to render alternate content. You might use this to show
|
||||
a message like "No records match your query."
|
||||
|
||||
Each call to `docFunc` or `elseFunc` is run in its own reactive
|
||||
context so that if it has other external data dependencies, it will be
|
||||
individually re-run when the data changes.
|
||||
|
||||
Example:
|
||||
|
||||
// List the titles of all of the posts that have the tag
|
||||
// "frontpage". Keep the list updated as new posts are made, as tags
|
||||
// change, etc. Let the user click a post to select it.
|
||||
Session.set("selected", null);
|
||||
var frag = Meteor.ui.render(function() {
|
||||
return Meteor.ui.listChunk(Posts.find({tags: "frontpage"}),
|
||||
function(post) {
|
||||
var style = Session.equals("selected", post._id) ? "selected" : "";
|
||||
// A real app would need to quote/sanitize post.name
|
||||
return '<div class="' + style + '">' + post.name + '</div>';
|
||||
},
|
||||
{ events:
|
||||
{
|
||||
'click': function (event) {
|
||||
Session.set("selected", this._id);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
// change, etc. Display the selected post differently.
|
||||
var frag = Meteor.renderList(
|
||||
Posts.find({tags: "frontpage"}),
|
||||
function(post) {
|
||||
var style = Session.equals("selectedId", post._id) ? "selected" : "";
|
||||
// A real app would need to quote/sanitize post.name
|
||||
return '<div class="' + style + '">' + post.name + '</div>';
|
||||
});
|
||||
document.body.appendChild(frag);
|
||||
|
||||
{{> api_box flush }}
|
||||
// Select a post. This will cause only the selected item and the
|
||||
// previously selected item to update.
|
||||
var somePost = Posts.findOne({tags: "frontpage"});
|
||||
Session.set("selectedId", somePost._id);
|
||||
|
||||
Normally, when you make changes (like writing to the database),
|
||||
their impact (like updating the DOM) is delayed until the system is
|
||||
idle. This keeps things predictable — you can know that the DOM
|
||||
won't go changing out from under your code as it runs. It's also one
|
||||
of the things that makes Meteor fast.
|
||||
|
||||
`Meteor.flush` forces all of the pending reactive updates to complete
|
||||
(for example, it ensures the DOM has been updated with your recent
|
||||
database changes.) Call `flush` to apply those pending changes
|
||||
immediately. The main use for this is to make sure the DOM has been
|
||||
brought up to date with your latest changes, so you can manually
|
||||
manipulate it with jQuery or the like.
|
||||
|
||||
When you call `flush`, any auto-updating DOM elements that are not on
|
||||
the screen may be cleaned up (meaning that Meteor will stop tracking
|
||||
and updating the elements, so that the browser's garbage collector can
|
||||
delete them.) So, if you manually call `flush`, you need to make sure
|
||||
that any auto-updating elements that you have created by calling
|
||||
[`Meteor.ui.render`](#meteor_ui_render) have already been inserted in the main
|
||||
DOM tree.
|
||||
|
||||
Technically speaking, `flush` calls the [invalidation
|
||||
callbacks](#on_invalidate) on every [reactive context](#context) that
|
||||
has been [invalidated](#invalidate), but hasn't yet has its callbacks
|
||||
called. If the invalidation callbacks invalidate still more contexts,
|
||||
flush keeps flushing until everything is totally settled. The DOM
|
||||
elements are cleaned up because of logic in
|
||||
[`Meteor.ui.render`](#meteor_ui_render) that works through invalidations.
|
||||
|
||||
{{#api_box_inline eventmaps}}
|
||||
|
||||
@@ -1180,14 +1240,14 @@ To handle more than one type of event with the same function, use a comma-separa
|
||||
{{/dtdd}}
|
||||
</dl>
|
||||
|
||||
The handler function gets one argument, an object with information
|
||||
about the event. It will receive some additional context data in
|
||||
`this`, depending on the context of the current element handling the event.
|
||||
In a Handlebars template,
|
||||
an element's context is the Handlebars data context where that element
|
||||
occurs, which is set by block helpers such as `#with`
|
||||
and `#each`. When using [`Meteor.ui.chunk`](#meteor_ui_chunk), the
|
||||
data context is set using the `event_data` option.
|
||||
The handler function receives two arguments: `event`, an object with
|
||||
information about the event, and `template`, a [template
|
||||
instance](#template_inst) for the template where the handler is
|
||||
defined. The handler also receives some additional context data in
|
||||
`this`, depending on the context of the current element handling the
|
||||
event. In a Handlebars template, an element's context is the
|
||||
Handlebars data context where that element occurs, which is set by
|
||||
block helpers such as `#with` and `#each`.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -1325,6 +1385,61 @@ browsers.
|
||||
|
||||
{{/api_box_inline}}
|
||||
|
||||
|
||||
|
||||
{{#api_box_inline constant}}
|
||||
|
||||
You can mark a region of a template as "constant" and not subject to
|
||||
re-rendering using the
|
||||
`{{dstache}}#constant}}...{{dstache}}/constant}}` block helper.
|
||||
Content inside the `#constant` block helper is preserved exactly as-is
|
||||
even if the enclosing template is re-rendered. Changes to other parts
|
||||
of the template are patched in around it in the same manner as
|
||||
`preserve`. Unlike individual node preservation, a constant region
|
||||
retains not only the identities of its nodes but also their attributes
|
||||
and contents. The contents of the block will only be evaluated once
|
||||
per occurrence of the enclosing template.
|
||||
|
||||
Constant regions allow non-Meteor content to be embedded in a Meteor
|
||||
template. Many third-party widgets create and manage their own DOM
|
||||
nodes programmatically. Typically, you put an empty element in your
|
||||
template, which the widget or library will then populate with
|
||||
children. Normally, when Meteor re-renders the enclosing template it
|
||||
would remove the new children, since the template says it should be
|
||||
empty. If the container is wrapped in a `#constant` block, however, it
|
||||
is left alone; whatever content is currently in the DOM remains.
|
||||
|
||||
{{#note}}
|
||||
Constant regions are intended for embedding non-Meteor content.
|
||||
Event handlers and reactive dependencies don't currently work
|
||||
correctly inside constant regions.
|
||||
{{/note}}
|
||||
|
||||
|
||||
{{/api_box_inline}}
|
||||
|
||||
{{#api_box_inline isolate}}
|
||||
|
||||
Each template runs in its own reactive context. When the template
|
||||
accesses a reactive data source, such as by calling `Session.get` or
|
||||
making a database query, this establishes a data dependency that will
|
||||
cause the whole template to be re-rendered when the data changes.
|
||||
This means that the amount of re-rendering for a particular change
|
||||
is affected by how you've divided your HTML into templates.
|
||||
|
||||
Typically, the exact extent of re-rendering is not crucial, but if you
|
||||
want more control, such as for performance reasons, you can use the
|
||||
`{{dstache}}#isolate}}...{{dstache}}/isolate}}` helper. Data
|
||||
dependencies established inside an `#isolate` block are localized to
|
||||
the block and will not in themselves cause the parent template to be
|
||||
re-rendered. This block helper essentially conveys the reactivity
|
||||
benefits you would get by pulling the content out into a new
|
||||
sub-template.
|
||||
|
||||
{{/api_box_inline}}
|
||||
|
||||
|
||||
|
||||
<h2 id="timers"><span>Timers</span></h2>
|
||||
|
||||
Meteor uses global environment variables
|
||||
@@ -1513,6 +1628,40 @@ just means that [`run`](#run) sets it, runs some user-supplied code, and
|
||||
then restores its previous value.)
|
||||
|
||||
|
||||
{{> api_box flush }}
|
||||
|
||||
Normally, when you make changes (like writing to the database),
|
||||
their impact (like updating the DOM) is delayed until the system is
|
||||
idle. This keeps things predictable — you can know that the DOM
|
||||
won't go changing out from under your code as it runs. It's also one
|
||||
of the things that makes Meteor fast.
|
||||
|
||||
`Meteor.flush` forces all of the pending reactive updates to complete
|
||||
(for example, it ensures the DOM has been updated with your recent
|
||||
database changes.) Call `flush` to apply those pending changes
|
||||
immediately. The main use for this is to make sure the DOM has been
|
||||
brought up to date with your latest changes, so you can manually
|
||||
manipulate it with jQuery or the like.
|
||||
|
||||
When you call `flush`, any auto-updating DOM elements that are not on
|
||||
the screen may be cleaned up (meaning that Meteor will stop tracking
|
||||
and updating the elements, so that the browser's garbage collector can
|
||||
delete them.) So, if you manually call `flush`, you need to make sure
|
||||
that any auto-updating elements that you have created by calling
|
||||
[`Meteor.render`](#meteor_render) have already been inserted in the main
|
||||
DOM tree.
|
||||
|
||||
Technically speaking, `flush` calls the [invalidation
|
||||
callbacks](#on_invalidate) on every [reactive context](#context) that
|
||||
has been [invalidated](#invalidate), but hasn't yet has its callbacks
|
||||
called. If the invalidation callbacks invalidate still more contexts,
|
||||
flush keeps flushing until everything is totally settled. The DOM
|
||||
elements are cleaned up by logic that is triggered by context invalidations.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h2 id="meteor_http"><span>Meteor.http</span></h2>
|
||||
|
||||
`Meteor.http` provides an HTTP API on the client and server. To use
|
||||
|
||||
@@ -481,13 +481,6 @@ Template.api.Context = {
|
||||
descr: ["Create an invalidation context. Invalidation contexts are used to run a piece of code, and record its dependencies so it can be rerun later if one of its inputs changes.", "An invalidation context is basically just a list of callbacks for an event that can fire only once. The [`on_invalidate`](#on_invalidate) method adds a callback to the list, and the [`invalidate`](#invalidate) method fires the event."]
|
||||
};
|
||||
|
||||
Template.api.current = {
|
||||
id: "current",
|
||||
name: "Meteor.deps.Context.current",
|
||||
locus: "Client",
|
||||
descr: ["The current [`invalidation context`](#context), or `null` if not being called from inside [`run`](#run)."]
|
||||
};
|
||||
|
||||
Template.api.run = {
|
||||
id: "run",
|
||||
name: "<em>context</em>.run(func)",
|
||||
@@ -519,77 +512,11 @@ Template.api.invalidate = {
|
||||
descr: ["Add this context to the list of contexts that will have their `on_invalidate|on_invalidate` callbacks called by the next call to [`Meteor.flush`](#meteor_flush)."]
|
||||
};
|
||||
|
||||
|
||||
// writeFence
|
||||
// invalidationCrossbar
|
||||
|
||||
Template.api.render = {
|
||||
id: "meteor_ui_render",
|
||||
name: "Meteor.ui.render(html_func, [options])",
|
||||
Template.api.current = {
|
||||
id: "current",
|
||||
name: "Meteor.deps.Context.current",
|
||||
locus: "Client",
|
||||
descr: ["Create DOM nodes that automatically update themselves as data changes."],
|
||||
args: [
|
||||
{name: "html_func",
|
||||
type: "Function returning a string of HTML",
|
||||
descr: "Render function to be called, initially and whenever data changes"}
|
||||
],
|
||||
options: [
|
||||
{name: "events",
|
||||
type: "Object — event map",
|
||||
type_link: "eventmaps",
|
||||
descr: "Events to hook up to the rendered elements"},
|
||||
{name: "event_data",
|
||||
type: "Any value",
|
||||
descr: "Value to bind to `this` in event handlers"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.chunk = {
|
||||
id: "meteor_ui_chunk",
|
||||
name: "Meteor.ui.chunk(html_func, [options])",
|
||||
locus: "Client",
|
||||
descr: ["Inside [`Meteor.ui.render`](#meteor_ui_render), give special behavior to a range of HTML."],
|
||||
args: [
|
||||
{name: "html_func",
|
||||
type: "Function returning a string of HTML",
|
||||
descr: "Render function to be called, initially and whenever data changes"}
|
||||
],
|
||||
options: [
|
||||
{name: "events",
|
||||
type: "Object — event map",
|
||||
type_link: "eventmaps",
|
||||
descr: "Events to hook up to the rendered elements"},
|
||||
{name: "event_data",
|
||||
type: "Any value",
|
||||
descr: "Value to bind to `this` in event handlers"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.listChunk = {
|
||||
id: "meteor_ui_listchunk",
|
||||
name: "Meteor.ui.listChunk(observable, doc_func, [else_func], [options])",
|
||||
locus: "Client",
|
||||
descr: ["Observe a database query and create annotated HTML that will be reactively updated when rendered with [`Meteor.ui.render`](#meteor_ui_render)."],
|
||||
args: [
|
||||
{name: "observable",
|
||||
type: "Cursor",
|
||||
type_link: "meteor_collection_cursor",
|
||||
descr: "Query cursor to observe, as a reactive source of ordered documents"},
|
||||
{name: "doc_func",
|
||||
type: "Function taking a document and returning HTML",
|
||||
descr: "Render function to be called for each document"},
|
||||
{name: "else_func",
|
||||
type: "Function returning HTML",
|
||||
descr: "Render function to be called when query is empty"}
|
||||
],
|
||||
options: [
|
||||
{name: "events",
|
||||
type: "Object — event map",
|
||||
type_link: "eventmaps",
|
||||
descr: "Events to hook up to the rendered elements"}
|
||||
]
|
||||
descr: ["The current [`invalidation context`](#context), or `null` if not being called from inside [`run`](#run)."]
|
||||
};
|
||||
|
||||
Template.api.flush = {
|
||||
@@ -599,11 +526,59 @@ Template.api.flush = {
|
||||
descr: ["Ensure that any reactive updates have finished. Allow auto-updating DOM element to be cleaned up if they are offscreen."]
|
||||
};
|
||||
|
||||
|
||||
// writeFence
|
||||
// invalidationCrossbar
|
||||
|
||||
Template.api.render = {
|
||||
id: "meteor_render",
|
||||
name: "Meteor.render(htmlFunc)",
|
||||
locus: "Client",
|
||||
descr: ["Create DOM nodes that automatically update themselves as data changes."],
|
||||
args: [
|
||||
{name: "htmlFunc",
|
||||
type: "Function returning a string of HTML",
|
||||
descr: "Function that generates HTML to be rendered. Called immediately and re-run whenever data changes. May also be a string of HTML instead of a function."}
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.renderList = {
|
||||
id: "meteor_renderlist",
|
||||
name: "Meteor.renderList(observable, docFunc, [elseFunc])",
|
||||
locus: "Client",
|
||||
descr: ["Create DOM nodes that automatically update themselves based on the results of a database query."],
|
||||
args: [
|
||||
{name: "observable",
|
||||
type: "Cursor",
|
||||
type_link: "meteor_collection_cursor",
|
||||
descr: "Query cursor to observe as a reactive source of ordered documents."},
|
||||
{name: "docFunc",
|
||||
type: "Function taking a document and returning HTML",
|
||||
descr: "Render function to be called for each document."},
|
||||
{name: "elseFunc",
|
||||
type: "Function returning HTML",
|
||||
descr: "Optional. Render function to be called when query is empty."}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
Template.api.eventmaps = {
|
||||
id: "eventmaps",
|
||||
name: "Event Maps"
|
||||
};
|
||||
|
||||
Template.api.constant = {
|
||||
id: "constant",
|
||||
name: "Constant regions"
|
||||
};
|
||||
|
||||
Template.api.isolate = {
|
||||
id: "isolate",
|
||||
name: "Reactivity isolation"
|
||||
};
|
||||
|
||||
|
||||
|
||||
Template.api.setTimeout = {
|
||||
id: "meteor_settimeout",
|
||||
name: "Meteor.setTimeout",
|
||||
@@ -835,3 +810,118 @@ Template.api.http_del = {
|
||||
};
|
||||
|
||||
|
||||
// XXX move these up to right place
|
||||
Template.api.template_call = {
|
||||
id: "template_call",
|
||||
name: "Template.<em>myTemplate</em>([data])",
|
||||
locus: "Client",
|
||||
descr: ["Call a template function by name to produce HTML."],
|
||||
args: [
|
||||
{name: "data",
|
||||
type: "Object",
|
||||
descr: 'Optional. The data context object with which to call the template.'}
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.template_rendered = {
|
||||
id: "template_rendered",
|
||||
name: "Template.<em>myTemplate</em>.rendered = function ( ) { ... }",
|
||||
locus: "Client",
|
||||
descr: ["Provide a callback when an instance of a template is rendered."]
|
||||
};
|
||||
|
||||
Template.api.template_created = {
|
||||
id: "template_created",
|
||||
name: "Template.<em>myTemplate</em>.created = function ( ) { ... }",
|
||||
locus: "Client",
|
||||
descr: ["Provide a callback when an instance of a template is created."]
|
||||
};
|
||||
|
||||
Template.api.template_destroyed = {
|
||||
id: "template_destroyed",
|
||||
name: "Template.<em>myTemplate</em>.destroyed = function ( ) { ... }",
|
||||
locus: "Client",
|
||||
descr: ["Provide a callback when an instance of a template is destroyed."]
|
||||
};
|
||||
|
||||
Template.api.template_events = {
|
||||
id: "template_events",
|
||||
name: "Template.<em>myTemplate</em>.events(eventMap)",
|
||||
locus: "Client",
|
||||
descr: ["Specify event handlers for this template."],
|
||||
args: [
|
||||
{name: "eventMap",
|
||||
type: "Object: event map",
|
||||
type_link: "eventmaps",
|
||||
descr: "Event handlers to associate with this template."}
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.template_helpers = {
|
||||
id: "template_helpers",
|
||||
name: "Template.<em>myTemplate</em>.helpers(helpers)",
|
||||
locus: "Client",
|
||||
descr: ["Specify template helpers available to this template."],
|
||||
args: [
|
||||
{name: "helpers",
|
||||
type: "Object",
|
||||
descr: "Dictionary of helper functions by name."}
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.template_preserve = {
|
||||
id: "template_preserve",
|
||||
name: "Template.<em>myTemplate</em>.preserve(selectors)",
|
||||
locus: "Client",
|
||||
descr: ["Specify rules for preserving individual DOM elements on re-render."],
|
||||
args: [
|
||||
{name: "selectors",
|
||||
type: "Array or Object",
|
||||
descr: "Array of selectors that each match at most one element, such as `['.thing1', '.thing2']`, or, alternatively, a dictionary of selectors and node-labeling functions (see below)."}
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.template_findAll = {
|
||||
id: "template_findAll",
|
||||
name: "<em>this</em>.findAll(selector)",
|
||||
locus: "Client",
|
||||
descr: ["Find all elements matching `selector` in this template instance."],
|
||||
args: [
|
||||
{name: "selector",
|
||||
type: "String",
|
||||
descr: 'The CSS selector to match, scoped to the template contents.'}
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.template_find = {
|
||||
id: "template_find",
|
||||
name: "<em>this</em>.find(selector)",
|
||||
locus: "Client",
|
||||
descr: ["Find one element matching `selector` in this template instance."],
|
||||
args: [
|
||||
{name: "selector",
|
||||
type: "String",
|
||||
descr: 'The CSS selector to match, scoped to the template contents.'}
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.template_firstNode = {
|
||||
id: "template_firstNode",
|
||||
name: "<em>this</em>.firstNode",
|
||||
locus: "Client",
|
||||
descr: ["The first top-level DOM node in this template instance."]
|
||||
};
|
||||
|
||||
Template.api.template_lastNode = {
|
||||
id: "template_lastNode",
|
||||
name: "<em>this</em>.lastNode",
|
||||
locus: "Client",
|
||||
descr: ["The last top-level DOM node in this template instance."]
|
||||
};
|
||||
|
||||
Template.api.template_data = {
|
||||
id: "template_data",
|
||||
name: "<em>this</em>.data",
|
||||
locus: "Client",
|
||||
descr: ["The data context of this instance's latest invocation."]
|
||||
};
|
||||
|
||||
@@ -189,7 +189,7 @@ application with error-prone logic.
|
||||
|
||||
These Meteor functions run your code in a reactive context:
|
||||
|
||||
* [`Meteor.ui.render`](#meteor_ui_render) and [`Meteor.ui.chunk`](#meteor_ui_chunk)
|
||||
* [`Meteor.render`](#meteor_render) and [`Meteor.renderList`](#meteor_renderlist)
|
||||
* [`Meteor.autosubscribe`](#meteor_autosubscribe)
|
||||
* [Templates](#templates)
|
||||
|
||||
@@ -220,7 +220,7 @@ generate it.
|
||||
This optional feature works with any HTML templating library, or even
|
||||
with HTML you generate manually from Javascript. Here's an example:
|
||||
|
||||
var fragment = Meteor.ui.render(
|
||||
var fragment = Meteor.render(
|
||||
function () {
|
||||
var name = Session.get("name") || "Anonymous";
|
||||
return "<div>Hello, " + name + "</div>";
|
||||
@@ -229,25 +229,18 @@ with HTML you generate manually from Javascript. Here's an example:
|
||||
|
||||
Session.set("name", "Bob"); // page updates automatically!
|
||||
|
||||
[`Meteor.ui.render`](#meteor_ui_render) takes a rendering function,
|
||||
that is, a function that returns some HTML as a string. It returns an
|
||||
auto-updating `DocumentFragment`. When there is a change to data used
|
||||
by the rendering function, it is re-run. The DOM nodes in the
|
||||
`DocumentFragment` then update themselves in-place, no matter where
|
||||
they were inserted on the page.
|
||||
|
||||
It's completely automatic. [`Meteor.ui.render`](#meteor_ui_render)
|
||||
uses [reactive contexts](#reactivity) to discover what data is used by
|
||||
the rendering function. From within
|
||||
[`Meteor.ui.render`](#meteor_ui_render), you can use
|
||||
[`Meteor.ui.chunk`](#meteor_ui_chunk) to customize how much of the
|
||||
HTML is rerendered on a data change, or
|
||||
[`Meteor.ui.listChunk`](#meteor_ui_listchunk) to efficiently track a
|
||||
live database query.
|
||||
[`Meteor.render`](#meteor_render) takes a rendering function, that is, a
|
||||
function that returns some HTML as a string. It returns an auto-updating
|
||||
`DocumentFragment`. When there is a change to data used by the rendering
|
||||
function, it is re-run. The DOM nodes in the `DocumentFragment` then
|
||||
update themselves in-place, no matter where they were inserted on the
|
||||
page. It's completely automatic. [`Meteor.render`](#meteor_render) uses
|
||||
[reactive contexts](#reactivity) to discover what data is used by the
|
||||
rendering function.
|
||||
|
||||
Most of the time, though, you won't call these functions directly
|
||||
— you'll just use your favorite templating package, such as
|
||||
Handlebars or Jade. The `render` and `chunk` functions are intended
|
||||
Handlebars or Jade. The `render` and `renderList` functions are intended
|
||||
for people that are implementing new templating systems.
|
||||
|
||||
Meteor normally batches up any needed updates and executes them only
|
||||
@@ -276,12 +269,12 @@ redrawn. The user could be in for a bumpy ride, as the focus, the
|
||||
cursor position, the partially entered text, and the accented
|
||||
character input state will be lost when the `<input>` is recreated.
|
||||
|
||||
This is another problem that Meteor solves automatically. Just make
|
||||
sure that each of your focusable elements either has a unique `id`, or
|
||||
has a `name` that is unique within the closest parent that has an
|
||||
`id`. Meteor will preserve these elements even when their enclosing
|
||||
template is rerendered, but will still update their children and copy
|
||||
over any attribute changes.
|
||||
This is another problem that Meteor solves for you. You can specify
|
||||
elements to preserve when templates are re-rendered with the
|
||||
[`preserve`](#template_preserve) directive on the template. Meteor will
|
||||
preserve these elements even when their enclosing template is
|
||||
rerendered, but will still update their children and copy over any
|
||||
attribute changes.
|
||||
|
||||
{{/better_markdown}}
|
||||
</template>
|
||||
@@ -325,9 +318,9 @@ function `Template.hello`, passing any data for the template:
|
||||
|
||||
This returns a string. To use the template along with the [`Live
|
||||
HTML`](#livehtml) system, and get DOM elements that update
|
||||
automatically in place, use [`Meteor.ui.render`](#meteor_ui_render):
|
||||
automatically in place, use [`Meteor.render`](#meteor_render):
|
||||
|
||||
Meteor.ui.render(function () {
|
||||
Meteor.render(function () {
|
||||
return Template.hello({first: "Alyssa", last: "Hacker"});
|
||||
})
|
||||
=> automatically updating DOM elements
|
||||
@@ -387,7 +380,7 @@ Helpers can also be used to pass in constant data.
|
||||
// Works fine with {{dstache}}#each sections}}
|
||||
Template.report.sections = ["Situation", "Complication", "Resolution"];
|
||||
|
||||
Finally, you can set the `events` property of a template function to a
|
||||
Finally, you can use an `events` declaration on a template function to set up a
|
||||
table of event handlers. The format is documented at [Event
|
||||
Maps](#eventmaps). The `this` argument to the event handler will be
|
||||
the data context of the element that triggered the event.
|
||||
@@ -406,11 +399,11 @@ the data context of the element that triggered the event.
|
||||
</{{! }}template>
|
||||
|
||||
<!-- myapp.js -->
|
||||
Template.player_score.events = {
|
||||
Template.player_score.events({
|
||||
'click .give_points': function () {
|
||||
Users.update({_id: this._id}, {$inc: {score: 2}});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Putting it all together, here's an example of how you can inject
|
||||
arbitrary data into your templates, and have them update automatically
|
||||
@@ -430,7 +423,7 @@ discussion.
|
||||
|
||||
<!-- in the console -->
|
||||
> Session.set("weather", "cloudy");
|
||||
> document.body.appendChild(Meteor.ui.render(Template.forecast));
|
||||
> document.body.appendChild(Meteor.render(Template.forecast));
|
||||
In DOM: <div>It'll be cloudy tonight</div>
|
||||
|
||||
> Session.set("weather", "cool and dry");
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
<div id="main">
|
||||
<div id="top"></div>
|
||||
<h1 class="main-headline">Meteor 0.3.9</h1>
|
||||
<h1 class="main-headline">Meteor 0.4.0</h1>
|
||||
{{> introduction }}
|
||||
{{> concepts }}
|
||||
{{> api }}
|
||||
@@ -28,7 +28,7 @@
|
||||
{{/if}}
|
||||
{{#if type "section"}}
|
||||
<h{{depth}}><a href="#{{id}}" class="{{maybe_current}} {{style}}">
|
||||
{{#if instance}}<i>{{instance}}</i>.{{/if}}{{name}}
|
||||
{{#if prefix}}{{prefix}}.{{/if}}{{#if instance}}<i>{{instance}}</i>.{{/if}}{{name}}
|
||||
</a></h{{depth}}>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
METEOR_VERSION = "0.3.9";
|
||||
METEOR_VERSION = "0.4.0";
|
||||
|
||||
Meteor.startup(function () {
|
||||
// XXX this is broken by the new multi-page layout. Also, it was
|
||||
@@ -143,14 +143,29 @@ var toc = [
|
||||
"Session.equals"
|
||||
],
|
||||
|
||||
"Meteor.ui", [
|
||||
"Meteor.ui.render",
|
||||
"Meteor.ui.chunk",
|
||||
"Meteor.ui.listChunk",
|
||||
"Meteor.flush",
|
||||
{name: "Templates", id: "templates_api"}, [
|
||||
{prefix: "Template", instance: "myTemplate", id: "template_call"}, [
|
||||
{name: "rendered", id: "template_rendered"},
|
||||
{name: "created", id: "template_created"},
|
||||
{name: "destroyed", id: "template_destroyed"},
|
||||
{name: "events", id: "template_events"},
|
||||
{name: "helpers", id: "template_helpers"},
|
||||
{name: "preserve", id: "template_preserve"}
|
||||
],
|
||||
{name: "Template instances", id: "template_inst"}, [
|
||||
{instance: "this", name: "findAll", id: "template_findAll"},
|
||||
{instance: "this", name: "find", id: "template_find"},
|
||||
{instance: "this", name: "firstNode", id: "template_firstNode"},
|
||||
{instance: "this", name: "lastNode", id: "template_lastNode"},
|
||||
{instance: "this", name: "data", id: "template_data"}
|
||||
],
|
||||
"Meteor.render",
|
||||
"Meteor.renderList",
|
||||
{type: "spacer"},
|
||||
{name: "Event maps", style: "noncode"}
|
||||
],
|
||||
{name: "Event maps", style: "noncode"},
|
||||
{name: "Constant regions", style: "noncode", id: "constant"},
|
||||
{name: "Reactivity isolation", style: "noncode", id: "isolate"}
|
||||
],
|
||||
|
||||
"Timers", [
|
||||
"Meteor.setTimeout",
|
||||
@@ -165,7 +180,8 @@ var toc = [
|
||||
{instance: "context", name: "on_invalidate"},
|
||||
{instance: "context", name: "invalidate"}
|
||||
],
|
||||
{name: "Meteor.deps.Context.current", id: "current"}
|
||||
{name: "Meteor.deps.Context.current", id: "current"},
|
||||
"Meteor.flush"
|
||||
// ],
|
||||
|
||||
// "Environment Variables", [
|
||||
@@ -321,6 +337,13 @@ Handlebars.registerHelper('better_markdown', function(fn) {
|
||||
return result;
|
||||
};
|
||||
|
||||
// This is a tower of terrible hacks.
|
||||
// Replace Spark annotations <$...> ... </$...> with HTML comments, and
|
||||
// space out the comments on their own lines. This keeps them from
|
||||
// interfering with Markdown's paragraph parsing.
|
||||
// Really, running Markdown multiple times on the same string is just a
|
||||
// bad idea.
|
||||
input = input.replace(/<(\/?\$.*?)>/g, '<!--$1-->');
|
||||
input = input.replace(/<!--.*?-->/g, '\n\n$&\n\n');
|
||||
|
||||
var hashedBlocks = {};
|
||||
@@ -370,6 +393,8 @@ Handlebars.registerHelper('better_markdown', function(fn) {
|
||||
return hashedBlocks[a];
|
||||
});
|
||||
|
||||
output = output.replace(/<!--(\/?\$.*?)-->/g, '<$1>');
|
||||
|
||||
return output;
|
||||
});
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ our thinking. We'd love to hear your feedback.
|
||||
<!-- change colors on these. $ and command output in grey, rest in
|
||||
white -->
|
||||
|
||||
The following works on all <a target="_blank" href="https://github.com/meteor/meteor/wiki/Supported-Platforms">supported platforms</a>.
|
||||
|
||||
Install Meteor:
|
||||
|
||||
<pre>
|
||||
|
||||
@@ -17,17 +17,17 @@ if (Meteor.is_client) {
|
||||
return Session.equals("selected_player", this._id) ? "selected" : '';
|
||||
};
|
||||
|
||||
Template.leaderboard.events = {
|
||||
Template.leaderboard.events({
|
||||
'click input.inc': function () {
|
||||
Players.update(Session.get("selected_player"), {$inc: {score: 5}});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Template.player.events = {
|
||||
Template.player.events({
|
||||
'click': function () {
|
||||
Session.set("selected_player", this._id);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// On server startup, create some players if the database is empty.
|
||||
|
||||
1
examples/other/template-demo/.meteor/.gitignore
vendored
Normal file
1
examples/other/template-demo/.meteor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
local
|
||||
6
examples/other/template-demo/.meteor/packages
Normal file
6
examples/other/template-demo/.meteor/packages
Normal file
@@ -0,0 +1,6 @@
|
||||
# Meteor packages used by this project, one per line.
|
||||
#
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
|
||||
autopublish
|
||||
7034
examples/other/template-demo/client/d3.v2.js
vendored
Normal file
7034
examples/other/template-demo/client/d3.v2.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
44
examples/other/template-demo/client/template-demo.css
Normal file
44
examples/other/template-demo/client/template-demo.css
Normal file
@@ -0,0 +1,44 @@
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, san-serif;
|
||||
width: 600px;
|
||||
margin: auto;
|
||||
padding: 25px 50px;
|
||||
border: 5px dashed #ccc;
|
||||
border-style: none dashed;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 50px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.clearboth {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spinForward {
|
||||
from {-webkit-transform: rotate(0deg);}
|
||||
to {-webkit-transform: rotate(360deg);}
|
||||
}
|
||||
|
||||
@-webkit-keyframes spinBackward {
|
||||
from {-webkit-transform: rotate(360deg);}
|
||||
to {-webkit-transform: rotate(0deg);}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 100px;
|
||||
border: 2px solid black;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.circles {
|
||||
float: left;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.circles svg {
|
||||
border: 2px solid #333;
|
||||
}
|
||||
182
examples/other/template-demo/client/template-demo.html
Normal file
182
examples/other/template-demo/client/template-demo.html
Normal file
@@ -0,0 +1,182 @@
|
||||
<head>
|
||||
<title>Advanced Template Demo</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{> page}}
|
||||
</body>
|
||||
|
||||
<template name="page">
|
||||
<h1>Advanced Template Demo</h1>
|
||||
<p>
|
||||
This demo shows off the advanced features of Meteor's optional
|
||||
Spark-based templating system, including constant regions, node
|
||||
preservation, per-template state, and template lifecycle
|
||||
callbacks.
|
||||
</p>
|
||||
|
||||
{{> preserveDemo }}
|
||||
{{> constantDemo }}
|
||||
{{> stateDemo }}
|
||||
{{> d3Demo }}
|
||||
</template>
|
||||
|
||||
<template name="preserveDemo">
|
||||
<h2>Element preservation</h2>
|
||||
|
||||
<input type="button" value="X++" class="x">
|
||||
|
||||
<p>
|
||||
Elements can be <em>preserved</em>, meaning that they will not be
|
||||
disturbed even as their attributes, children, or siblings
|
||||
change. In this example, when you press the X++ button, the CSS
|
||||
animation continues uninterrupted.
|
||||
</p>
|
||||
|
||||
|
||||
X={{x}}<br>
|
||||
<div class="spinner" style="-webkit-animation: {{spinAnim}} 2s infinite linear">
|
||||
X={{x}}
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" class="spinforward" {{spinForwardChecked}}>
|
||||
Spin Forward
|
||||
</div>
|
||||
X={{x}}
|
||||
</template>
|
||||
|
||||
<template name="constantDemo">
|
||||
<h2>Constant regions</h2>
|
||||
|
||||
<div>
|
||||
<input type="button" value="X++" class="x"> <br>
|
||||
<input type="checkbox" class="remove" which="1" {{checked 1}}>
|
||||
Remove map 1<br>
|
||||
<input type="checkbox" class="remove" which="2" {{checked 2}}>
|
||||
Remove map 2
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<p>
|
||||
Parts of a template can be marked as <em>constant</em>, meaning
|
||||
that Meteor will leave the entire region alone (even as its
|
||||
siblings change.) This is great for embedding non-Meteor
|
||||
widgets. Try scrolling the two Google Maps embeds below. When you
|
||||
press X++, the maps stay where they are.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Try using the checkboxes to remove either or both of the
|
||||
maps. When you remove a map, Spark tracks the <em>identity</em> of
|
||||
the constant regions so that it knows which DOM nodes to keep and
|
||||
which DOM nodes to throw away. In the case of the Handlebars
|
||||
package, the identity is based on the actual template call stack
|
||||
that rendered the constant region.
|
||||
</p>
|
||||
|
||||
X={{x}}<br>
|
||||
|
||||
{{#if show 1}}
|
||||
{{#constant}}
|
||||
<div style="float: left; padding-right: 20px;">
|
||||
<iframe width="290" height="290" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="https://maps.google.com/maps?f=q&source=s_q&hl=en&geocode=&q=140+10th+Street,+San+Francisco,+CA&aq=0&oq=140+10th+s&sll=37.7577,-122.4376&sspn=0.166931,0.329247&ie=UTF8&hq=&hnear=140+10th+St,+San+Francisco,+California+94103&t=m&ll=37.774921,-122.415419&spn=0.013569,0.017252&z=14&iwloc=A&output=embed"></iframe><br /><small><a href="https://maps.google.com/maps?f=q&source=embed&hl=en&geocode=&q=140+10th+Street,+San+Francisco,+CA&aq=0&oq=140+10th+s&sll=37.7577,-122.4376&sspn=0.166931,0.329247&ie=UTF8&hq=&hnear=140+10th+St,+San+Francisco,+California+94103&t=m&ll=37.774921,-122.415419&spn=0.013569,0.017252&z=14&iwloc=A" style="color:#0000FF;text-align:left">View Larger Map</a></small>
|
||||
</div>
|
||||
{{/constant}}
|
||||
{{/if}}
|
||||
|
||||
{{#if show 2}}
|
||||
{{#constant}}
|
||||
<div>
|
||||
<iframe width="290" height="290" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="https://maps.google.com/maps?f=q&source=s_q&hl=en&geocode=&q=880+Harrison+Street,+San+Francisco,+CA&aq=0&oq=880+harrison&sll=37.7577,-122.4376&sspn=0.166931,0.329247&ie=UTF8&hq=&hnear=880+Harrison+St,+San+Francisco,+California+94107&t=m&ll=37.779534,-122.411213&spn=0.013568,0.01708&z=14&iwloc=A&output=embed"></iframe><br /><small><a href="https://maps.google.com/maps?f=q&source=embed&hl=en&geocode=&q=880+Harrison+Street,+San+Francisco,+CA&aq=0&oq=880+harrison&sll=37.7577,-122.4376&sspn=0.166931,0.329247&ie=UTF8&hq=&hnear=880+Harrison+St,+San+Francisco,+California+94107&t=m&ll=37.779534,-122.411213&spn=0.013568,0.01708&z=14&iwloc=A" style="color:#0000FF;text-align:left">View Larger Map</a></small>
|
||||
</div>
|
||||
{{/constant}}
|
||||
{{/if}}
|
||||
|
||||
<div class="clearboth"> </div>
|
||||
|
||||
X={{x}}
|
||||
</template>
|
||||
|
||||
<template name="stateDemo">
|
||||
<h2>Template callbacks</h2>
|
||||
|
||||
<p>
|
||||
<input type="button" value="X++" class="x">
|
||||
<input type="button" value="Y++" class="y">
|
||||
<input type="button" value="Z++" class="z">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can get a <em>created</em> callback when a template is
|
||||
initially rendered; a <em>rendered</em> when a template is placed on
|
||||
the screen and when any part of the template is redrawn; and
|
||||
a <em>destroyed</em> callback when a template is taken across the
|
||||
screen. All of these callbacks receive a common <em>template state
|
||||
object</em> in 'this' which allows you to attach data to each
|
||||
particular instance of a template.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In this case, <em>created</em> is used to create a new JavaScript
|
||||
timer that updates the text of a <span> element every
|
||||
second. <em>rendered</em> is used to find the <span> when it
|
||||
appears on the screen, and update the pointer when the
|
||||
<span> is redraw (say, when you press Y++ — since it
|
||||
is not marked to be preserved.) <em>destroyed</em> is used to cancel
|
||||
the timer when the template goes off the screen.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The template state is used to hold the current time count and a
|
||||
reference to the <span> object to update. That's why there
|
||||
can be multiple copies of the same template, each with a different
|
||||
value for the counter.
|
||||
</p>
|
||||
|
||||
X={{x}}<br>
|
||||
<input type="button" value="Create a timer" class="create"><br>
|
||||
{{#each timers}}
|
||||
<div>
|
||||
{{> timer}}
|
||||
Z={{z}}
|
||||
</div>
|
||||
{{/each}}
|
||||
X={{x}}
|
||||
</template>
|
||||
|
||||
<template name="timer">
|
||||
<span class="elapsed"></span>
|
||||
<input type="button" value="Reset" class="reset">
|
||||
<input type="button" value="Delete" class="delete">
|
||||
Y={{y}}
|
||||
</template>
|
||||
|
||||
<template name="d3Demo">
|
||||
<h2>Simple d3.js integration</h2>
|
||||
<p>
|
||||
Meteor fits naturally with the popular d3.js data visualization
|
||||
library by Michael Bostock. Just set up d3 from your
|
||||
template's <em>rendered</em> callback. With Meteor, you can pass
|
||||
data directly out of a Mongo query into d3, and your d3
|
||||
visualization will update in realtime, with no extra code! Try
|
||||
opening this page in two browser windows.
|
||||
</p>
|
||||
|
||||
{{> circles left}}
|
||||
{{> circles right}}
|
||||
<div class="clearboth"> </div>
|
||||
|
||||
</template>
|
||||
|
||||
<template name="circles">
|
||||
<div class="circles">
|
||||
{{#constant}}
|
||||
<svg width="272" height="272"></svg>
|
||||
{{/constant}}
|
||||
<br>
|
||||
{{count}} circles<br>
|
||||
<input type="button" value="Add" class="add">
|
||||
<input type="button" value="Remove" class="remove" {{{disabled}}}>
|
||||
<input type="button" value="Scram" class="scram">
|
||||
</div>
|
||||
</template>
|
||||
296
examples/other/template-demo/client/template-demo.js
Normal file
296
examples/other/template-demo/client/template-demo.js
Normal file
@@ -0,0 +1,296 @@
|
||||
Timers = new Meteor.Collection(null);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
if (! Session.get("x")) {
|
||||
Session.set("x", 1);
|
||||
}
|
||||
|
||||
if (! Session.get("y")) {
|
||||
Session.set("y", 1);
|
||||
}
|
||||
|
||||
if (! Session.get("z")) {
|
||||
Session.set("z", 1);
|
||||
}
|
||||
|
||||
Template.preserveDemo.x =
|
||||
Template.constantDemo.x =
|
||||
Template.stateDemo.x =
|
||||
function () {
|
||||
return Session.get("x");
|
||||
};
|
||||
|
||||
Template.timer.y = function () {
|
||||
return Session.get("y");
|
||||
};
|
||||
|
||||
Template.stateDemo.z =
|
||||
function () {
|
||||
return Session.get("z");
|
||||
};
|
||||
|
||||
Template.page.events({
|
||||
'click input.x': function () {
|
||||
Session.set("x", Session.get("x") + 1);
|
||||
},
|
||||
|
||||
'click input.y': function () {
|
||||
Session.set("y", Session.get("y") + 1);
|
||||
},
|
||||
|
||||
'click input.z': function () {
|
||||
Session.set("z", Session.get("z") + 1);
|
||||
}
|
||||
});
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
if (typeof Session.get("spinForward") !== 'boolean') {
|
||||
Session.set("spinForward", true);
|
||||
}
|
||||
|
||||
Template.preserveDemo.preserve([ '.spinner', '.spinforward' ]);
|
||||
|
||||
Template.preserveDemo.spinForwardChecked = function () {
|
||||
return Session.get('spinForward') ? 'checked="checked"' : '';
|
||||
};
|
||||
|
||||
Template.preserveDemo.spinAnim = function () {
|
||||
return Session.get('spinForward') ? 'spinForward' : 'spinBackward';
|
||||
};
|
||||
|
||||
Template.preserveDemo.events({
|
||||
'change .spinforward' : function (event) {
|
||||
Session.set('spinForward', event.currentTarget.checked);
|
||||
}
|
||||
});
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Template.constantDemo.checked = function (which) {
|
||||
return Session.get('mapchecked' + which) ? 'checked="checked"' : '';
|
||||
};
|
||||
|
||||
Template.constantDemo.show = function (which) {
|
||||
return ! Session.get('mapchecked' + which);
|
||||
};
|
||||
|
||||
Template.constantDemo.events({
|
||||
'change .remove' : function (event) {
|
||||
var tgt = event.currentTarget;
|
||||
Session.set('mapchecked' + tgt.getAttribute("which"), tgt.checked);
|
||||
}
|
||||
});
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Template.stateDemo.events({
|
||||
'click .create': function () {
|
||||
Timers.insert({});
|
||||
}
|
||||
});
|
||||
|
||||
Template.stateDemo.timers = function () {
|
||||
return Timers.find();
|
||||
};
|
||||
|
||||
Template.timer.events({
|
||||
'click .reset': function (event, template) {
|
||||
template.elapsed = 0;
|
||||
updateTimer(template);
|
||||
},
|
||||
'click .delete': function () {
|
||||
Timers.remove(this._id);
|
||||
}
|
||||
});
|
||||
|
||||
var updateTimer = function (timer) {
|
||||
timer.node.innerHTML = timer.elapsed + " second" +
|
||||
((timer.elapsed === 1) ? "" : "s");
|
||||
};
|
||||
|
||||
Template.timer.created = function () {
|
||||
var self = this;
|
||||
self.elapsed = 0;
|
||||
self.node = null;
|
||||
};
|
||||
|
||||
Template.timer.rendered = function () {
|
||||
var self = this;
|
||||
self.node = this.find(".elapsed");
|
||||
updateTimer(self);
|
||||
|
||||
if (! self.timer) {
|
||||
var tick = function () {
|
||||
self.elapsed++;
|
||||
self.timer = setTimeout(tick, 1000);
|
||||
updateTimer(self);
|
||||
};
|
||||
tick();
|
||||
}
|
||||
};
|
||||
|
||||
Template.timer.destroyed = function () {
|
||||
clearInterval(this.timer);
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Run f(). Record its dependencies. Rerun it whenever the
|
||||
// dependencies change.
|
||||
//
|
||||
// Returns an object with a stop() method. Call stop() to stop the
|
||||
// rerunning.
|
||||
//
|
||||
// XXX this should go into Meteor core as Meteor.autorun
|
||||
var autorun = function (f) {
|
||||
var ctx;
|
||||
var slain = false;
|
||||
var rerun = function () {
|
||||
if (slain)
|
||||
return;
|
||||
ctx = new Meteor.deps.Context;
|
||||
ctx.run(f);
|
||||
ctx.on_invalidate(rerun);
|
||||
};
|
||||
rerun();
|
||||
return {
|
||||
stop: function () {
|
||||
slain = true;
|
||||
ctx.invalidate();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Template.d3Demo.left = function () {
|
||||
return { group: "left" };
|
||||
};
|
||||
|
||||
Template.d3Demo.right = function () {
|
||||
return { group: "right" };
|
||||
};
|
||||
|
||||
Template.circles.events({
|
||||
'mousedown circle': function (evt, template) {
|
||||
Session.set("selectedCircle:" + this.group, evt.currentTarget.id);
|
||||
},
|
||||
'click .add': function () {
|
||||
Circles.insert({x: Meteor.random(), y: Meteor.random(),
|
||||
r: Meteor.random() * .1 + .02,
|
||||
color: {
|
||||
r: Meteor.random(),
|
||||
g: Meteor.random(),
|
||||
b: Meteor.random()
|
||||
},
|
||||
group: this.group
|
||||
});
|
||||
},
|
||||
'click .remove': function () {
|
||||
var selected = Session.get("selectedCircle:" + this.group);
|
||||
if (selected) {
|
||||
Circles.remove(selected);
|
||||
Session.set("selectedCircle:" + this.group, null);
|
||||
}
|
||||
},
|
||||
'click .scram': function () {
|
||||
Circles.find({group: this.group}).forEach(function (r) {
|
||||
Circles.update(r._id, {
|
||||
$set: {
|
||||
x: Meteor.random(), y: Meteor.random(), r: Meteor.random() * .1 + .02
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var colorToString = function (color) {
|
||||
var f = function (x) { return Math.floor(x * 256); };
|
||||
return "rgb(" + f(color.r) + "," +
|
||||
+ f(color.g) + "," + + f(color.b) + ")";
|
||||
};
|
||||
|
||||
Template.circles.count = function () {
|
||||
return Circles.find({group: this.group}).count();
|
||||
};
|
||||
|
||||
Template.circles.disabled = function () {
|
||||
return Session.get("selectedCircle:" + this.group) ?
|
||||
'' : 'disabled="disabled"';
|
||||
};
|
||||
|
||||
Template.circles.created = function () {
|
||||
};
|
||||
|
||||
Template.circles.rendered = function () {
|
||||
var self = this;
|
||||
self.node = self.find("svg");
|
||||
|
||||
var data = self.data;
|
||||
|
||||
if (! self.handle) {
|
||||
d3.select(self.node).append("rect");
|
||||
self.handle = autorun(function () {
|
||||
var circle = d3.select(self.node).selectAll("circle")
|
||||
.data(Circles.find({group: data.group}).fetch(),
|
||||
function (d) { return d._id; });
|
||||
|
||||
circle.enter().append("circle")
|
||||
.attr("id", function (d) {
|
||||
return d._id;
|
||||
})
|
||||
.attr("cx", function (d) {
|
||||
return d.x * 272;
|
||||
})
|
||||
.attr("cy", function (d) {
|
||||
return d.y * 272;
|
||||
})
|
||||
.attr("r", 50)
|
||||
.style("fill", function (d) {
|
||||
return colorToString(d.color);
|
||||
})
|
||||
.style("opacity", 0);
|
||||
|
||||
circle.transition()
|
||||
.duration(250)
|
||||
.attr("cx", function (d) {
|
||||
return d.x * 272;
|
||||
})
|
||||
.attr("cy", function (d) {
|
||||
return d.y * 272;
|
||||
})
|
||||
.attr("r", function (d) {
|
||||
return d.r * 272;
|
||||
})
|
||||
.style("fill", function (d) {
|
||||
return colorToString(d.color);
|
||||
})
|
||||
.style("opacity", .9)
|
||||
.ease("cubic-out");
|
||||
|
||||
circle.exit().transition()
|
||||
.duration(250)
|
||||
.attr("r", 0)
|
||||
.remove();
|
||||
|
||||
var selectionId = Session.get("selectedCircle:" + data.group);
|
||||
var s = selectionId && Circles.findOne(selectionId);
|
||||
var rect = d3.select(self.node).select("rect");
|
||||
if (s)
|
||||
rect.attr("x", (s.x - s.r) * 272)
|
||||
.attr("y", (s.y - s.r) * 272)
|
||||
.attr("width", s.r * 2 * 272)
|
||||
.attr("height", s.r * 2 * 272)
|
||||
.attr("display", '')
|
||||
.style("fill", "none")
|
||||
.style("stroke", "red")
|
||||
.style("stroke-width", 3);
|
||||
else
|
||||
rect.attr("display", 'none');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Template.circles.destroyed = function () {
|
||||
this.handle && this.handle.stop();
|
||||
};
|
||||
1
examples/other/template-demo/model.js
Normal file
1
examples/other/template-demo/model.js
Normal file
@@ -0,0 +1 @@
|
||||
Circles = new Meteor.Collection("circles");
|
||||
@@ -39,42 +39,36 @@ Meteor.autosubscribe(function () {
|
||||
|
||||
////////// Helpers for in-place editing //////////
|
||||
|
||||
// Returns an event_map key for attaching "ok/cancel" events to
|
||||
// a text input (given by selector)
|
||||
var okcancel_events = function (selector) {
|
||||
return 'keyup '+selector+', keydown '+selector+', focusout '+selector;
|
||||
};
|
||||
// Returns an event map that handles the "escape" and "return" keys and
|
||||
// "blur" events on a text input (given by selector) and interprets them
|
||||
// as "ok" or "cancel".
|
||||
var okCancelEvents = function (selector, callbacks) {
|
||||
var ok = callbacks.ok || function () {};
|
||||
var cancel = callbacks.cancel || function () {};
|
||||
|
||||
// Creates an event handler for interpreting "escape", "return", and "blur"
|
||||
// on a text field and calling "ok" or "cancel" callbacks.
|
||||
var make_okcancel_handler = function (options) {
|
||||
var ok = options.ok || function () {};
|
||||
var cancel = options.cancel || function () {};
|
||||
|
||||
return function (evt) {
|
||||
if (evt.type === "keydown" && evt.which === 27) {
|
||||
// escape = cancel
|
||||
cancel.call(this, evt);
|
||||
|
||||
} else if (evt.type === "keyup" && evt.which === 13 ||
|
||||
evt.type === "focusout") {
|
||||
// blur/return/enter = ok/submit if non-empty
|
||||
var value = String(evt.target.value || "");
|
||||
if (value)
|
||||
ok.call(this, value, evt);
|
||||
else
|
||||
var events = {};
|
||||
events['keyup '+selector+', keydown '+selector+', focusout '+selector] =
|
||||
function (evt) {
|
||||
if (evt.type === "keydown" && evt.which === 27) {
|
||||
// escape = cancel
|
||||
cancel.call(this, evt);
|
||||
}
|
||||
};
|
||||
|
||||
} else if (evt.type === "keyup" && evt.which === 13 ||
|
||||
evt.type === "focusout") {
|
||||
// blur/return/enter = ok/submit if non-empty
|
||||
var value = String(evt.target.value || "");
|
||||
if (value)
|
||||
ok.call(this, value, evt);
|
||||
else
|
||||
cancel.call(this, evt);
|
||||
}
|
||||
};
|
||||
return events;
|
||||
};
|
||||
|
||||
// Finds a text input in the DOM by id and focuses it.
|
||||
var focus_field_by_id = function (id) {
|
||||
var input = document.getElementById(id);
|
||||
if (input) {
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
var activateInput = function (input) {
|
||||
input.focus();
|
||||
input.select();
|
||||
};
|
||||
|
||||
////////// Lists //////////
|
||||
@@ -83,7 +77,7 @@ Template.lists.lists = function () {
|
||||
return Lists.find({}, {sort: {name: 1}});
|
||||
};
|
||||
|
||||
Template.lists.events = {
|
||||
Template.lists.events({
|
||||
'mousedown .list': function (evt) { // select list
|
||||
Router.setList(this._id);
|
||||
},
|
||||
@@ -91,15 +85,27 @@ Template.lists.events = {
|
||||
// prevent clicks on <a> from refreshing the page.
|
||||
evt.preventDefault();
|
||||
},
|
||||
'dblclick .list': function (evt) { // start editing list name
|
||||
'dblclick .list': function (evt, tmpl) { // start editing list name
|
||||
Session.set('editing_listname', this._id);
|
||||
Meteor.flush(); // force DOM redraw, so we can focus the edit field
|
||||
focus_field_by_id("list-name-input");
|
||||
activateInput(tmpl.find("#list-name-input"));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Template.lists.events[ okcancel_events('#list-name-input') ] =
|
||||
make_okcancel_handler({
|
||||
// Attach events to keydown, keyup, and blur on "New list" input box.
|
||||
Template.lists.events(okCancelEvents(
|
||||
'#new-list',
|
||||
{
|
||||
ok: function (text, evt) {
|
||||
var id = Lists.insert({name: text});
|
||||
Router.setList(id);
|
||||
evt.target.value = "";
|
||||
}
|
||||
}));
|
||||
|
||||
Template.lists.events(okCancelEvents(
|
||||
'#list-name-input',
|
||||
{
|
||||
ok: function (value) {
|
||||
Lists.update(this._id, {$set: {name: value}});
|
||||
Session.set('editing_listname', null);
|
||||
@@ -107,17 +113,7 @@ Template.lists.events[ okcancel_events('#list-name-input') ] =
|
||||
cancel: function () {
|
||||
Session.set('editing_listname', null);
|
||||
}
|
||||
});
|
||||
|
||||
// Attach events to keydown, keyup, and blur on "New list" input box.
|
||||
Template.lists.events[ okcancel_events('#new-list') ] =
|
||||
make_okcancel_handler({
|
||||
ok: function (text, evt) {
|
||||
var id = Lists.insert({name: text});
|
||||
Router.setList(id);
|
||||
evt.target.value = "";
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
Template.lists.selected = function () {
|
||||
return Session.equals('list_id', this._id) ? 'selected' : '';
|
||||
@@ -131,16 +127,19 @@ Template.lists.editing = function () {
|
||||
return Session.equals('editing_listname', this._id);
|
||||
};
|
||||
|
||||
// Preserve text input fields so that they aren't replaced
|
||||
// while the user is typing in them.
|
||||
Template.lists.preserve(['#list-name-input', '#new-list']);
|
||||
|
||||
////////// Todos //////////
|
||||
|
||||
Template.todos.any_list_selected = function () {
|
||||
return !Session.equals('list_id', null);
|
||||
};
|
||||
|
||||
Template.todos.events = {};
|
||||
|
||||
Template.todos.events[ okcancel_events('#new-todo') ] =
|
||||
make_okcancel_handler({
|
||||
Template.todos.events(okCancelEvents(
|
||||
'#new-todo',
|
||||
{
|
||||
ok: function (text, evt) {
|
||||
var tag = Session.get('tag_filter');
|
||||
Todos.insert({
|
||||
@@ -152,7 +151,7 @@ Template.todos.events[ okcancel_events('#new-todo') ] =
|
||||
});
|
||||
evt.target.value = '';
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
Template.todos.todos = function () {
|
||||
// Determine which todos to display in main pane,
|
||||
@@ -170,6 +169,8 @@ Template.todos.todos = function () {
|
||||
return Todos.find(sel, {sort: {timestamp: 1}});
|
||||
};
|
||||
|
||||
Template.todos.preserve(['#new-todo']);
|
||||
|
||||
Template.todo_item.tag_objs = function () {
|
||||
var todo_id = this._id;
|
||||
return _.map(this.tags || [], function (tag) {
|
||||
@@ -193,7 +194,7 @@ Template.todo_item.adding_tag = function () {
|
||||
return Session.equals('editing_addtag', this._id);
|
||||
};
|
||||
|
||||
Template.todo_item.events = {
|
||||
Template.todo_item.events({
|
||||
'click .check': function () {
|
||||
Todos.update(this._id, {$set: {done: !this.done}});
|
||||
},
|
||||
@@ -202,16 +203,16 @@ Template.todo_item.events = {
|
||||
Todos.remove(this._id);
|
||||
},
|
||||
|
||||
'click .addtag': function (evt) {
|
||||
'click .addtag': function (evt, tmpl) {
|
||||
Session.set('editing_addtag', this._id);
|
||||
Meteor.flush(); // update DOM before focus
|
||||
focus_field_by_id("edittag-input");
|
||||
activateInput(tmpl.find("#edittag-input"));
|
||||
},
|
||||
|
||||
'dblclick .display .todo-text': function (evt) {
|
||||
'dblclick .display .todo-text': function (evt, tmpl) {
|
||||
Session.set('editing_itemname', this._id);
|
||||
Meteor.flush(); // update DOM before focus
|
||||
focus_field_by_id("todo-input");
|
||||
activateInput(tmpl.find("#todo-input"));
|
||||
},
|
||||
|
||||
'click .remove': function (evt) {
|
||||
@@ -234,10 +235,12 @@ Template.todo_item.events = {
|
||||
privateTo: Meteor.user()._id
|
||||
}});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Template.todo_item.events[ okcancel_events('#todo-input') ] =
|
||||
make_okcancel_handler({
|
||||
|
||||
Template.todo_item.events(okCancelEvents(
|
||||
'#todo-input',
|
||||
{
|
||||
ok: function (value) {
|
||||
Todos.update(this._id, {$set: {text: value}});
|
||||
Session.set('editing_itemname', null);
|
||||
@@ -245,10 +248,11 @@ Template.todo_item.events[ okcancel_events('#todo-input') ] =
|
||||
cancel: function () {
|
||||
Session.set('editing_itemname', null);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
Template.todo_item.events[ okcancel_events('#edittag-input') ] =
|
||||
make_okcancel_handler({
|
||||
Template.todo_item.events(okCancelEvents(
|
||||
'#edittag-input',
|
||||
{
|
||||
ok: function (value) {
|
||||
Todos.update(this._id, {$addToSet: {tags: value}});
|
||||
Session.set('editing_addtag', null);
|
||||
@@ -256,7 +260,9 @@ Template.todo_item.events[ okcancel_events('#edittag-input') ] =
|
||||
cancel: function () {
|
||||
Session.set('editing_addtag', null);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
Template.todo_item.preserve(['#todo-input', '#edittag-input']);
|
||||
|
||||
////////// Tag Filter //////////
|
||||
|
||||
@@ -290,14 +296,14 @@ Template.tag_filter.selected = function () {
|
||||
return Session.equals('tag_filter', this.tag) ? 'selected' : '';
|
||||
};
|
||||
|
||||
Template.tag_filter.events = {
|
||||
Template.tag_filter.events({
|
||||
'mousedown .tag': function () {
|
||||
if (Session.equals('tag_filter', this.tag))
|
||||
Session.set('tag_filter', null);
|
||||
else
|
||||
Session.set('tag_filter', this.tag);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
////////// Tracking selected list in URL //////////
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{> page}}
|
||||
</body>
|
||||
|
||||
<template name="page">
|
||||
<div id="main">
|
||||
<div id="left">
|
||||
{{> board }}
|
||||
@@ -14,7 +18,7 @@
|
||||
{{> scores }}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</template>
|
||||
|
||||
<template name="board">
|
||||
<div id="clock">
|
||||
|
||||
@@ -51,6 +51,11 @@ var clear_selected_positions = function () {
|
||||
Session.set('selected_' + pos, false);
|
||||
};
|
||||
|
||||
Template.page.preserve({
|
||||
'input[id]': function (n) { return n.id; },
|
||||
'button[name=submit]': true
|
||||
});
|
||||
|
||||
//////
|
||||
////// lobby template: shows everyone not currently playing, and
|
||||
////// offers a button to start a fresh game.
|
||||
@@ -85,7 +90,7 @@ Template.lobby.disabled = function () {
|
||||
};
|
||||
|
||||
|
||||
Template.lobby.events = {
|
||||
Template.lobby.events({
|
||||
'keyup input#myname': function (evt) {
|
||||
var name = $('#lobby input#myname').val().trim();
|
||||
Players.update(Session.get('player_id'), {$set: {name: name}});
|
||||
@@ -93,7 +98,7 @@ Template.lobby.events = {
|
||||
'click button.startgame': function () {
|
||||
Meteor.call('start_new_game');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
//////
|
||||
////// board template: renders the board and the clock given the
|
||||
@@ -125,13 +130,13 @@ Template.board.clock = function () {
|
||||
return min + ':' + (sec < 10 ? ('0' + sec) : sec);
|
||||
};
|
||||
|
||||
Template.board.events = {
|
||||
Template.board.events({
|
||||
'click .square': function (evt) {
|
||||
var textbox = $('#scratchpad input');
|
||||
textbox.val(textbox.val() + evt.target.innerHTML);
|
||||
textbox.focus();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
//////
|
||||
////// scratchpad is where we enter new words.
|
||||
@@ -141,7 +146,7 @@ Template.scratchpad.show = function () {
|
||||
return game() && game().clock > 0;
|
||||
};
|
||||
|
||||
Template.scratchpad.events = {
|
||||
Template.scratchpad.events({
|
||||
'click button, keyup input': function (evt) {
|
||||
var textbox = $('#scratchpad input');
|
||||
// if we clicked the button or hit enter
|
||||
@@ -160,17 +165,17 @@ Template.scratchpad.events = {
|
||||
set_selected_positions(textbox.val());
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Template.postgame.show = function () {
|
||||
return game() && game().clock === 0;
|
||||
};
|
||||
|
||||
Template.postgame.events = {
|
||||
Template.postgame.events({
|
||||
'click button': function (evt) {
|
||||
Players.update(Session.get('player_id'), {$set: {game_id: null}});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//////
|
||||
////// scores shows everyone's score and word list.
|
||||
|
||||
346
packages/domutils/domutils.js
Normal file
346
packages/domutils/domutils.js
Normal file
@@ -0,0 +1,346 @@
|
||||
|
||||
|
||||
DomUtils = {};
|
||||
|
||||
(function() {
|
||||
|
||||
var Sizzle = window.Sizzle || $.find;
|
||||
|
||||
///// Common look-up tables used by htmlToFragment et al.
|
||||
|
||||
var testDiv = document.createElement("div");
|
||||
testDiv.innerHTML = " <link/><table></table>";
|
||||
|
||||
// Tests that, if true, indicate browser quirks present.
|
||||
var quirks = {
|
||||
// IE loses initial whitespace when setting innerHTML.
|
||||
leadingWhitespaceKilled: (testDiv.firstChild.nodeType !== 3),
|
||||
|
||||
// IE may insert an empty tbody tag in a table.
|
||||
tbodyInsertion: testDiv.getElementsByTagName("tbody").length > 0,
|
||||
|
||||
// IE loses some tags in some environments (requiring extra wrapper).
|
||||
tagsLost: testDiv.getElementsByTagName("link").length === 0
|
||||
};
|
||||
|
||||
// Set up map of wrappers for different nodes.
|
||||
var wrapMap = {
|
||||
option: [ 1, "<select multiple='multiple'>", "</select>" ],
|
||||
legend: [ 1, "<fieldset>", "</fieldset>" ],
|
||||
thead: [ 1, "<table>", "</table>" ],
|
||||
tr: [ 2, "<table><tbody>", "</tbody></table>" ],
|
||||
td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
|
||||
col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
|
||||
area: [ 1, "<map>", "</map>" ],
|
||||
_default: [ 0, "", "" ]
|
||||
};
|
||||
_.extend(wrapMap, {
|
||||
optgroup: wrapMap.option,
|
||||
tbody: wrapMap.thead,
|
||||
tfoot: wrapMap.thead,
|
||||
colgroup: wrapMap.thead,
|
||||
caption: wrapMap.thead,
|
||||
th: wrapMap.td
|
||||
});
|
||||
if (quirks.tagsLost) {
|
||||
// trick from jquery. initial text is ignored when we take lastChild.
|
||||
wrapMap._default = [ 1, "div<div>", "</div>" ];
|
||||
}
|
||||
|
||||
var rleadingWhitespace = /^\s+/,
|
||||
rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
|
||||
rtagName = /<([\w:]+)/,
|
||||
rtbody = /<tbody/i,
|
||||
rhtml = /<|&#?\w+;/,
|
||||
rnoInnerhtml = /<(?:script|style)/i;
|
||||
|
||||
|
||||
// Parse an HTML string, which may contain multiple top-level tags,
|
||||
// and return a DocumentFragment.
|
||||
DomUtils.htmlToFragment = function(html) {
|
||||
var doc = document; // node factory
|
||||
var frag = doc.createDocumentFragment();
|
||||
|
||||
if (! html.length) {
|
||||
// empty, do nothing
|
||||
} else if (! rhtml.test(html)) {
|
||||
// Just text.
|
||||
frag.appendChild(doc.createTextNode(html));
|
||||
} else {
|
||||
// General case.
|
||||
// Replace self-closing tags
|
||||
html = html.replace(rxhtmlTag, "<$1></$2>");
|
||||
// Use first tag to determine wrapping needed.
|
||||
var firstTagMatch = rtagName.exec(html);
|
||||
var firstTag = (firstTagMatch ? firstTagMatch[1].toLowerCase() : "");
|
||||
var wrapData = wrapMap[firstTag] || wrapMap._default;
|
||||
|
||||
var container = doc.createElement("div");
|
||||
// insert wrapped HTML into a DIV
|
||||
container.innerHTML = wrapData[1] + html + wrapData[2];
|
||||
// set "container" to inner node of wrapper
|
||||
var unwraps = wrapData[0];
|
||||
while (unwraps--) {
|
||||
container = container.lastChild;
|
||||
}
|
||||
|
||||
if (quirks.tbodyInsertion && ! rtbody.test(html)) {
|
||||
// Any tbody we find was created by the browser.
|
||||
var tbodies = container.getElementsByTagName("tbody");
|
||||
_.each(tbodies, function(n) {
|
||||
if (! n.firstChild) {
|
||||
// spurious empty tbody
|
||||
n.parentNode.removeChild(n);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (quirks.leadingWhitespaceKilled) {
|
||||
var wsMatch = rleadingWhitespace.exec(html);
|
||||
if (wsMatch) {
|
||||
container.insertBefore(doc.createTextNode(wsMatch[0]),
|
||||
container.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Reparent children of container to frag.
|
||||
while (container.firstChild)
|
||||
frag.appendChild(container.firstChild);
|
||||
}
|
||||
|
||||
return frag;
|
||||
};
|
||||
|
||||
// Return an HTML string representing the contents of frag,
|
||||
// a DocumentFragment. (This is what innerHTML would do if
|
||||
// it were defined on DocumentFragments.)
|
||||
DomUtils.fragmentToHtml = function(frag) {
|
||||
frag = frag.cloneNode(true); // deep copy, don't touch original!
|
||||
|
||||
return DomUtils.fragmentToContainer(frag).innerHTML;
|
||||
};
|
||||
|
||||
// Given a DocumentFragment, return a node whose children are the
|
||||
// reparented contents of the DocumentFragment. In most cases this
|
||||
// is as simple as creating a DIV, but in the case of a fragment
|
||||
// containing TRs, for example, it's necessary to create a TABLE and
|
||||
// a TBODY and return the TBODY.
|
||||
DomUtils.fragmentToContainer = function(frag) {
|
||||
var doc = document; // node factory
|
||||
|
||||
var firstElement = frag.firstChild;
|
||||
while (firstElement && firstElement.nodeType !== 1) {
|
||||
firstElement = firstElement.nextSibling;
|
||||
}
|
||||
|
||||
var container = doc.createElement("div");
|
||||
|
||||
if (! firstElement) {
|
||||
// no tags!
|
||||
container.appendChild(frag);
|
||||
} else {
|
||||
var firstTag = firstElement.nodeName;
|
||||
var wrapData = wrapMap[firstTag] || wrapMap._default;
|
||||
|
||||
container.innerHTML = wrapData[1] + wrapData[2];
|
||||
var unwraps = wrapData[0];
|
||||
while (unwraps--) {
|
||||
container = container.lastChild;
|
||||
}
|
||||
|
||||
container.appendChild(frag);
|
||||
}
|
||||
|
||||
return container;
|
||||
};
|
||||
|
||||
// Returns true if element a properly contains element b.
|
||||
// Only works on element nodes (e.g. not text nodes).
|
||||
DomUtils.elementContains = function(a, b) {
|
||||
// Note: Some special-casing would be required to implement this method
|
||||
// where a and b aren't necessarily elements, e.g. b is a text node,
|
||||
// because contains() doesn't seem to work reliably on some browsers
|
||||
// including IE.
|
||||
if (a.nodeType !== 1 || b.nodeType !== 1) {
|
||||
return false; // a and b are not both elements
|
||||
}
|
||||
if (a.compareDocumentPosition) {
|
||||
return a.compareDocumentPosition(b) & 0x10;
|
||||
} else {
|
||||
// Should be only old IE and maybe other old browsers here.
|
||||
// Modern Safari has both methods but seems to get contains() wrong.
|
||||
return a !== b && a.contains(b);
|
||||
}
|
||||
};
|
||||
|
||||
// Returns an array containing the children of contextNode that
|
||||
// match `selector`. Unlike querySelectorAll, `selector` is
|
||||
// interpreted as if the document were rooted at `contextNode` --
|
||||
// the only nodes that can be used to match components of the
|
||||
// selector are the descendents of `contextNode`. `contextNode`
|
||||
// itself is not included (it can't be used to match a component of
|
||||
// the selector, and it can never be included in the returned
|
||||
// array.)
|
||||
//
|
||||
// `contextNode` may be either a node, a document, or a DocumentFragment.
|
||||
DomUtils.findAll = function(contextNode, selector) {
|
||||
// Eventually, we will remove the dependency on Sizzle and
|
||||
// implement this in terms of querySelectorAll on modern browsers
|
||||
// and Sizzle in old IE. We'll use Sizzle's trick for scoped
|
||||
// querySelectorAll which involves temporarily assigning an ID to
|
||||
// contextNode (if it doesn't have one) and prepending the ID to
|
||||
// the selector.
|
||||
if (contextNode.nodeType === 11 /* DocumentFragment */) {
|
||||
// Sizzle doesn't work on a DocumentFragment, but it does work on
|
||||
// a descendent of one.
|
||||
var frag = contextNode;
|
||||
var container = DomUtils.fragmentToContainer(frag);
|
||||
var results = Sizzle(selector, container);
|
||||
// put nodes back into frag
|
||||
while (container.firstChild)
|
||||
frag.appendChild(container.firstChild);
|
||||
return results;
|
||||
} else {
|
||||
return Sizzle(selector, contextNode);
|
||||
}
|
||||
};
|
||||
|
||||
// Like `findAll` but finds one element (or returns null).
|
||||
DomUtils.find = function(contextNode, selector) {
|
||||
var results = DomUtils.findAll(contextNode, selector);
|
||||
return (results.length ? results[0] : null);
|
||||
};
|
||||
|
||||
// Like `findAll` but searches the nodes from `start` to `end`
|
||||
// inclusive. `start` and `end` must be siblings, and they participate
|
||||
// in the search (they can be used to match selector components, and
|
||||
// they can appear in the returned results). It's as if the parent of
|
||||
// `start` and `end` serves as contextNode, but matches from children
|
||||
// that aren't between `start` and `end` (inclusive) are ignored.
|
||||
//
|
||||
// If `selector` involves sibling selectors, child index selectors, or
|
||||
// the like, the results are undefined.
|
||||
//
|
||||
// precond: clipStart/clipEnd are descendents of contextNode
|
||||
// XXX document
|
||||
DomUtils.findAllClipped = function(contextNode, selector, clipStart, clipEnd) {
|
||||
|
||||
// Ensure the clip range starts and ends on element nodes. This is possible
|
||||
// to do without changing the result set because non-element nodes can't
|
||||
// be or contain matches.
|
||||
while (clipStart !== clipEnd && clipStart.nodeType !== 1)
|
||||
clipStart = clipStart.nextSibling;
|
||||
while (clipStart !== clipEnd && clipEnd.nodeType !== 1)
|
||||
clipEnd = clipEnd.previousSibling;
|
||||
if (clipStart.nodeType !== 1)
|
||||
return []; // no top-level elements! start === end and it's not an element
|
||||
|
||||
// resultsPlus includes matches all matches descended from contextNode,
|
||||
// including those that aren't in the clip range.
|
||||
var resultsPlus = DomUtils.findAll(contextNode, selector);
|
||||
|
||||
// Filter the list of nodes to remove nodes that occur before start
|
||||
// or after end.
|
||||
return _.reject(resultsPlus, function(n) {
|
||||
// reject node if it contains the clip range
|
||||
if (DomUtils.elementContains(n, clipStart))
|
||||
return true;
|
||||
// reject node if (n,start) are in order or (end,n) are in order
|
||||
return (DomUtils.elementOrder(n, clipStart) > 0) ||
|
||||
(DomUtils.elementOrder(clipEnd, n) > 0);
|
||||
});
|
||||
};
|
||||
|
||||
// Like `findAllClipped` but finds one element (or returns null).
|
||||
DomUtils.findClipped = function(contextNode, selector, clipStart, clipEnd) {
|
||||
var results = DomUtils.findAllClipped(contextNode, selector, clipStart, clipEnd);
|
||||
return (results.length ? results[0] : null);
|
||||
};
|
||||
|
||||
|
||||
// Returns 0 if the nodes are the same or either one contains the other;
|
||||
// otherwise, 1 if a comes before b, or else -1 if b comes before a in
|
||||
// document order.
|
||||
// Requires: `a` and `b` are element nodes in the same document tree.
|
||||
DomUtils.elementOrder = function(a, b) {
|
||||
// See http://ejohn.org/blog/comparing-document-position/
|
||||
if (a === b)
|
||||
return 0;
|
||||
if (a.compareDocumentPosition) {
|
||||
var n = a.compareDocumentPosition(b);
|
||||
return ((n & 0x18) ? 0 : ((n & 0x4) ? 1 : -1));
|
||||
} else {
|
||||
// Only old IE is known to not have compareDocumentPosition (though Safari
|
||||
// originally lacked it). Thankfully, IE gives us a way of comparing elements
|
||||
// via the "sourceIndex" property.
|
||||
if (a.contains(b) || b.contains(a))
|
||||
return 0;
|
||||
return (a.sourceIndex < b.sourceIndex ? 1 : -1);
|
||||
}
|
||||
};
|
||||
|
||||
// Wrap `frag` as necessary to prepare it for insertion in
|
||||
// `container`. For example, if `frag` has TR nodes at top level,
|
||||
// and `container` is a TABLE, then it's necessary to wrap `frag` in
|
||||
// a TBODY to avoid IE quirks.
|
||||
//
|
||||
// `frag` is a DocumentFragment and will be modified in
|
||||
// place. `container` is a DOM element.
|
||||
DomUtils.wrapFragmentForContainer = function(frag, container) {
|
||||
if (container && container.nodeName === "TABLE" &&
|
||||
_.any(frag.childNodes,
|
||||
function(n) { return n.nodeName === "TR"; })) {
|
||||
// Avoid putting a TR directly in a TABLE without an
|
||||
// intervening TBODY, because it doesn't work in IE. We do
|
||||
// the same thing on all browsers for ease of testing
|
||||
// and debugging.
|
||||
var tbody = document.createElement("TBODY");
|
||||
tbody.appendChild(frag);
|
||||
frag.appendChild(tbody);
|
||||
}
|
||||
};
|
||||
|
||||
// Return true if `node` is part of the global DOM document. Like
|
||||
// elementContains(document, node), except (1) it works for any node
|
||||
// (eg, text nodes), not just elements; (2) it works around browser
|
||||
// quirks that would otherwise come up when passing 'document' as
|
||||
// the first argument to elementContains.
|
||||
//
|
||||
// Returns true if node === document.
|
||||
DomUtils.isInDocument = function (node) {
|
||||
// Deal with all cases where node is not an element
|
||||
// node descending from the body first...
|
||||
if (node === document)
|
||||
return true;
|
||||
|
||||
if (node.nodeType !== 1 /* Element */)
|
||||
node = node.parentNode;
|
||||
if (! (node && node.nodeType === 1))
|
||||
return false;
|
||||
if (node === document.body)
|
||||
return true;
|
||||
|
||||
return DomUtils.elementContains(document.body, node);
|
||||
};
|
||||
|
||||
// Return an HTML string representation of the nodes from
|
||||
// firstNode to lastNode, which must be siblings.
|
||||
// The tags representing firstNode and lastNode are included,
|
||||
// but not their parent or outer siblings.
|
||||
DomUtils.rangeToHtml = function (firstNode, lastNode) {
|
||||
var frag = document.createDocumentFragment();
|
||||
for(var n = firstNode, after = lastNode.nextSibling;
|
||||
n && n !== after;
|
||||
n = n.nextSibling)
|
||||
frag.appendChild(n.cloneNode(true)); // deep copy
|
||||
return DomUtils.fragmentToHtml(frag);
|
||||
};
|
||||
|
||||
// Return an HTML string representation of node, including its
|
||||
// own open and close tag.
|
||||
DomUtils.outerHtml = function (node) {
|
||||
return DomUtils.rangeToHtml(node, node);
|
||||
};
|
||||
|
||||
|
||||
})();
|
||||
1
packages/domutils/domutils_tests.js
Normal file
1
packages/domutils/domutils_tests.js
Normal file
@@ -0,0 +1 @@
|
||||
// TESTS GO HERE
|
||||
18
packages/domutils/package.js
Normal file
18
packages/domutils/package.js
Normal file
@@ -0,0 +1,18 @@
|
||||
Package.describe({
|
||||
summary: "Utility functions for DOM manipulation",
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('jquery', 'client');
|
||||
api.add_files('domutils.js', 'client');
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use(['tinytest']);
|
||||
api.use(['domutils', 'test-helpers'], 'client');
|
||||
|
||||
api.add_files([
|
||||
'domutils_tests.js'
|
||||
], 'client');
|
||||
});
|
||||
@@ -21,19 +21,32 @@ Handlebars.json_ast_to_func = function (ast) {
|
||||
//
|
||||
// partials take one argument, data
|
||||
|
||||
// XXX handlebars' format for arguments is stupid. eg, options ===
|
||||
// options.fn. plow this stuff under. treat block arguments (fn,
|
||||
// inverse) as just another kind of argument, same as what is passed
|
||||
// in via named arguments.
|
||||
// XXX handlebars' format for arguments is not the clearest, likely
|
||||
// for backwards compatibility to mustache. eg, options ===
|
||||
// options.fn. take the opportunity to clean this up. treat block
|
||||
// arguments (fn, inverse) as just another kind of argument, same as
|
||||
// what is passed in via named arguments.
|
||||
Handlebars._default_helpers = {
|
||||
'with': function (data, options) {
|
||||
return options.fn(data);
|
||||
},
|
||||
'each': function (data, options) {
|
||||
var parentData = this;
|
||||
if (data && data.length > 0)
|
||||
return _.map(data, options.fn).join('');
|
||||
return _.map(data, function(x, i) {
|
||||
// infer a branch key from the data
|
||||
var branch = (x._id || (typeof x === 'string' ? x : null) ||
|
||||
Spark.UNIQUE_LABEL);
|
||||
return Spark.labelBranch(branch, function() {
|
||||
return options.fn(x);
|
||||
});
|
||||
}).join('');
|
||||
else
|
||||
return options.inverse(this);
|
||||
return Spark.labelBranch(
|
||||
'else',
|
||||
function () {
|
||||
return options.inverse(parentData);
|
||||
});
|
||||
},
|
||||
'if': function (data, options) {
|
||||
if (!data || (data instanceof Array && !data.length))
|
||||
@@ -124,7 +137,7 @@ Handlebars.evaluate = function (ast, data, options) {
|
||||
var dataThis = stack.data;
|
||||
|
||||
var data;
|
||||
if (id[0] === 0 && (id[1] in helpers) && ! scopedToContext) {
|
||||
if (id[0] === 0 && helpers.hasOwnProperty(id[1]) && ! scopedToContext) {
|
||||
// first path segment is a helper
|
||||
data = helpers[id[1]];
|
||||
} else {
|
||||
@@ -196,7 +209,7 @@ Handlebars.evaluate = function (ast, data, options) {
|
||||
var last = params[params.length - 1];
|
||||
var hash = {};
|
||||
if (typeof(last) === "object" && !(last instanceof Array)) {
|
||||
// evaluate hash values, which are currently invocations
|
||||
// evaluate hash values, which are found as invocations
|
||||
// like [0, "foo"]
|
||||
_.each(params.pop(), function(v,k) {
|
||||
var result = eval_value(stack, v);
|
||||
@@ -239,7 +252,7 @@ Handlebars.evaluate = function (ast, data, options) {
|
||||
return apply(values, extra);
|
||||
};
|
||||
|
||||
var template = function (stack, elts) {
|
||||
var template = function (stack, elts, basePCKey) {
|
||||
var buf = [];
|
||||
|
||||
var toString = function (x) {
|
||||
@@ -250,22 +263,19 @@ Handlebars.evaluate = function (ast, data, options) {
|
||||
return x.toString();
|
||||
};
|
||||
|
||||
// wrap `fn` and `inverse` blocks in liveranges
|
||||
// having event_data, if the data is different
|
||||
// from the enclosing data.
|
||||
// wrap `fn` and `inverse` blocks in chunks having `data`, if the data
|
||||
// is different from the enclosing data, so that the data is available
|
||||
// at runtime for events.
|
||||
var decorateBlockFn = function(fn, old_data) {
|
||||
return function(data) {
|
||||
var result = fn(data);
|
||||
// don't create spurious ranges when data is same as before
|
||||
// (or when transitioning between e.g. `window` and `undefined`)
|
||||
// don't create spurious annotations when data is same
|
||||
// as before (or when transitioning between e.g. `window` and
|
||||
// `undefined`)
|
||||
if ((data || Handlebars._defaultThis) ===
|
||||
(old_data || Handlebars._defaultThis)) {
|
||||
return result;
|
||||
} else {
|
||||
return Meteor.ui.chunk(
|
||||
function() { return result; },
|
||||
{ event_data: data });
|
||||
}
|
||||
(old_data || Handlebars._defaultThis))
|
||||
return fn(data);
|
||||
else
|
||||
return Spark.setDataContext(data, fn(data));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -280,32 +290,63 @@ Handlebars.evaluate = function (ast, data, options) {
|
||||
return Handlebars._escape(toString(x));
|
||||
};
|
||||
|
||||
_.each(elts, function (elt) {
|
||||
var curIndex;
|
||||
// Construct a unique key for the current position
|
||||
// in the AST. Since template(...) is invoked recursively,
|
||||
// the "PC" (program counter) key is hierarchical, consisting
|
||||
// of one or more numbers, for example '0' or '1.3.0.1'.
|
||||
var getPCKey = function() {
|
||||
return (basePCKey ? basePCKey+'.' : '') + curIndex;
|
||||
};
|
||||
var branch = function(name, func) {
|
||||
// Construct a unique branch identifier based on what partial
|
||||
// we're in, what partial or helper we're calling, and our index
|
||||
// into the template AST (essentially the program counter).
|
||||
// If "foo" calls "bar" at index 3, it looks like: bar@foo#3.
|
||||
return Spark.labelBranch(name + "@" + getPCKey(), func);
|
||||
};
|
||||
|
||||
_.each(elts, function (elt, index) {
|
||||
curIndex = index;
|
||||
if (typeof(elt) === "string")
|
||||
buf.push(elt);
|
||||
else if (elt[0] === '{')
|
||||
// {{double stache}}
|
||||
buf.push(maybeEscape(invoke(stack, elt[1])));
|
||||
buf.push(branch(elt[1], function () {
|
||||
return maybeEscape(invoke(stack, elt[1]));
|
||||
}));
|
||||
else if (elt[0] === '!')
|
||||
// {{{triple stache}}}
|
||||
buf.push(toString(invoke(stack, elt[1] || '')));
|
||||
buf.push(branch(elt[1], function () {
|
||||
return toString(invoke(stack, elt[1] || ''));
|
||||
}));
|
||||
else if (elt[0] === '#') {
|
||||
// {{#block helper}}
|
||||
var block = decorateBlockFn(
|
||||
function (data) {
|
||||
return template({parent: stack, data: data}, elt[2]);
|
||||
return template({parent: stack, data: data}, elt[2],
|
||||
getPCKey());
|
||||
}, stack.data);
|
||||
block.fn = block;
|
||||
block.inverse = decorateBlockFn(
|
||||
function (data) {
|
||||
return template({parent: stack, data: data}, elt[3] || []);
|
||||
return template({parent: stack, data: data}, elt[3] || [],
|
||||
getPCKey());
|
||||
}, stack.data);
|
||||
buf.push(toString(invoke(stack, elt[1], block, true)));
|
||||
var html = branch(elt[1], function () {
|
||||
return toString(invoke(stack, elt[1], block, true));
|
||||
});
|
||||
buf.push(html);
|
||||
} else if (elt[0] === '>') {
|
||||
// {{> partial}}
|
||||
if (!(elt[1] in partials))
|
||||
throw new Error("No such partial '" + elt[1] + "'");
|
||||
buf.push(toString(partials[elt[1]](stack.data)));
|
||||
var partialName = elt[1];
|
||||
if (!(partialName in partials))
|
||||
throw new Error("No such partial '" + partialName + "'");
|
||||
// call the partial
|
||||
var html = branch(partialName, function () {
|
||||
return toString(partials[partialName](stack.data));
|
||||
});
|
||||
buf.push(html);
|
||||
} else
|
||||
throw new Error("bad element in template");
|
||||
});
|
||||
@@ -313,7 +354,13 @@ Handlebars.evaluate = function (ast, data, options) {
|
||||
return buf.join('');
|
||||
};
|
||||
|
||||
return template({data: data, parent: null}, ast);
|
||||
// Set the prefix for PC keys, which identify call sites in the AST
|
||||
// for the purpose of chunk matching.
|
||||
// `options.name` will be null in the body, but otherwise have a value,
|
||||
// assuming `options` was assembled in templating/deftemplate.js.
|
||||
var rootPCKey = (options.name||"")+"#";
|
||||
|
||||
return template({data: data, parent: null}, ast, rootPCKey);
|
||||
};
|
||||
|
||||
Handlebars.SafeString = function(string) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
Tinytest.add("less - presence", function(test) {
|
||||
|
||||
var d = OnscreenDiv(Meteor.ui.render(function() {
|
||||
var d = OnscreenDiv(Meteor.render(function() {
|
||||
return '<p class="less-unlucky-left-border"></p>'; }));
|
||||
d.node().style.display = 'block';
|
||||
|
||||
|
||||
@@ -726,7 +726,7 @@ Meteor._LivedataServer = function () {
|
||||
// minutes. Also run result cache cleanup.
|
||||
// XXX at scale, we'll want to have a separate timer for each
|
||||
// session, and stagger them
|
||||
setInterval(function () {
|
||||
Meteor.setInterval(function () {
|
||||
var now = +(new Date);
|
||||
_.each(self.sessions, function (s) {
|
||||
s.cleanup();
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
// Stand back, I'm going to try SCIENCE.
|
||||
|
||||
Meteor.ui = Meteor.ui || {};
|
||||
|
||||
(function () {
|
||||
// XXX we should eventually move LiveRange off into its own
|
||||
// package. but it would be also be nice to keep it as a single,
|
||||
// self-contained file to make it easier to use outside of Meteor.
|
||||
|
||||
// Possible optimization: get rid of start_idx/end_idx and just search
|
||||
// Possible optimization: get rid of _startIndex/_endIndex 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
|
||||
|
||||
var canSetTextProps = (function() {
|
||||
var canSetTextProps = (function () {
|
||||
// IE8 and earlier don't support expando attributes on text nodes,
|
||||
// but fortunately they are allowed on comments.
|
||||
var test_elt = document.createTextNode("");
|
||||
var testElem = document.createTextNode("");
|
||||
var exception;
|
||||
try {
|
||||
test_elt.test = 123;
|
||||
testElem.test = 123;
|
||||
} catch (exception) { }
|
||||
|
||||
return (test_elt.test === 123);
|
||||
return (testElem.test === 123);
|
||||
})();
|
||||
|
||||
Meteor.ui._wrap_endpoints = function (start, end) {
|
||||
var wrapEndpoints = function (start, end) {
|
||||
if (canSetTextProps) {
|
||||
return [start, end];
|
||||
} else {
|
||||
@@ -47,7 +41,7 @@ Meteor.ui = Meteor.ui || {};
|
||||
};
|
||||
|
||||
|
||||
// This is a constructor (invoke it as 'new Meteor.ui._LiveRange').
|
||||
// This is a constructor (invoke it as 'new LiveRange').
|
||||
//
|
||||
// Create a range, tagged 'tag', that includes start, end, and all
|
||||
// the nodes between them, and the children of all of those nodes,
|
||||
@@ -61,7 +55,7 @@ Meteor.ui = Meteor.ui || {};
|
||||
// other ranges is uniquely determined.
|
||||
//
|
||||
// To track the range as it's relocated, some of the DOM nodes that
|
||||
// are part of the range will have an expando attribute set on
|
||||
// are part of the range will have an expando attribute 0Aset on
|
||||
// them. The name of the expando attribute will be 'tag', so pick
|
||||
// something that won't collide.
|
||||
//
|
||||
@@ -81,7 +75,10 @@ Meteor.ui = Meteor.ui || {};
|
||||
// and end are the first and last child of their parent respectively
|
||||
// or when caller is building up the range tree from the inside
|
||||
// out. Let's wait for the profiler to tell us to add this.
|
||||
Meteor.ui._LiveRange = function (tag, start, end, inner) {
|
||||
//
|
||||
// XXX Should eventually support LiveRanges where start === end
|
||||
// and start.parentNode is null.
|
||||
LiveRange = function (tag, start, end, inner) {
|
||||
if (start.nodeType === 11 /* DocumentFragment */) {
|
||||
end = start.lastChild;
|
||||
start = start.firstChild;
|
||||
@@ -91,11 +88,11 @@ Meteor.ui = Meteor.ui || {};
|
||||
}
|
||||
end = end || start;
|
||||
|
||||
this.tag = tag; // must be set before calling _ensure_tag
|
||||
this.tag = tag; // must be set before calling _ensureTag
|
||||
|
||||
var endpoints = Meteor.ui._wrap_endpoints(start, end);
|
||||
start = this._ensure_tag(endpoints[0]);
|
||||
end = this._ensure_tag(endpoints[1]);
|
||||
var endpoints = wrapEndpoints(start, end);
|
||||
start = this._ensureTag(endpoints[0]);
|
||||
end = this._ensureTag(endpoints[1]);
|
||||
|
||||
// Decide at what indices in start[tag][0] and end[tag][1] we
|
||||
// should insert the new range.
|
||||
@@ -114,25 +111,25 @@ Meteor.ui = Meteor.ui || {};
|
||||
// Liveranges technically start just before, and end just after, their
|
||||
// start and end nodes to which the liverange data is attached.
|
||||
|
||||
var start_index = findPosition(start[tag][0], true, end, start, inner);
|
||||
var end_index = findPosition(end[tag][1], false, start, end, inner);
|
||||
var startIndex = findPosition(start[tag][0], true, end, start, inner);
|
||||
var endIndex = findPosition(end[tag][1], false, start, end, inner);
|
||||
|
||||
// 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_idx is the value such that
|
||||
// this._start[this.tag][0][this._start_idx] === this.
|
||||
// finally, this._startIndex is the value such that
|
||||
// this._start[this.tag][0][this._startIndex] === this.
|
||||
//
|
||||
// Similarly for 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.
|
||||
|
||||
// Set this._start, this._end, this._start_idx, this._end_idx
|
||||
this._insert_entries(start, 0, start_index, [this]);
|
||||
this._insert_entries(end, 1, end_index, [this]);
|
||||
// Set this._start, this._end, this._startIndex, this._endIndex
|
||||
this._insertEntries(start, 0, startIndex, [this]);
|
||||
this._insertEntries(end, 1, endIndex, [this]);
|
||||
};
|
||||
|
||||
var findPosition = function(ranges, findEndNotStart, edge, otherEdge, inner) {
|
||||
@@ -160,9 +157,9 @@ Meteor.ui = Meteor.ui || {};
|
||||
// range starts.
|
||||
//
|
||||
// invariant: n >= edge ("n is after, or is, edge")
|
||||
var initial_n = (findEndNotStart ? edge.parentNode.lastChild : otherEdge);
|
||||
var take_first = (findEndNotStart ? ! inner : inner);
|
||||
for(var i=0, n=initial_n; i<=ranges.length; i++) {
|
||||
var initialN = (findEndNotStart ? edge.parentNode.lastChild : otherEdge);
|
||||
var takeFirst = (findEndNotStart ? ! inner : inner);
|
||||
for(var i=0, n=initialN; i<=ranges.length; i++) {
|
||||
var r = ranges[i];
|
||||
var curEdge = r && (findEndNotStart ? r._end : r._start);
|
||||
while (n !== curEdge && n !== edge) {
|
||||
@@ -170,7 +167,7 @@ Meteor.ui = Meteor.ui || {};
|
||||
}
|
||||
if (curEdge === edge) {
|
||||
index = i;
|
||||
if (take_first) break;
|
||||
if (takeFirst) break;
|
||||
} else if (n === edge) {
|
||||
index = i;
|
||||
break;
|
||||
@@ -179,13 +176,13 @@ Meteor.ui = Meteor.ui || {};
|
||||
return index;
|
||||
};
|
||||
|
||||
Meteor.ui._LiveRange.prototype._ensure_tag = function (node) {
|
||||
LiveRange.prototype._ensureTag = function (node) {
|
||||
if (!(this.tag in node))
|
||||
node[this.tag] = [[], []];
|
||||
return node;
|
||||
};
|
||||
|
||||
var can_delete_expandos = (function() {
|
||||
var canDeleteExpandos = (function() {
|
||||
// IE7 can't remove expando attributes from DOM nodes with
|
||||
// delete. Instead you must remove them with node.removeAttribute.
|
||||
var node = document.createElement("DIV");
|
||||
@@ -199,10 +196,10 @@ Meteor.ui = Meteor.ui || {};
|
||||
return result;
|
||||
})();
|
||||
|
||||
Meteor.ui._LiveRange._clean_node = function (tag, node, force) {
|
||||
LiveRange._cleanNode = function (tag, node, force) {
|
||||
var data = node[tag];
|
||||
if (data && (!(data[0].length + data[1].length) || force)) {
|
||||
if (can_delete_expandos)
|
||||
if (canDeleteExpandos)
|
||||
delete node[tag];
|
||||
else
|
||||
node.removeAttribute(tag);
|
||||
@@ -220,67 +217,67 @@ Meteor.ui = Meteor.ui || {};
|
||||
// through the DOM.
|
||||
//
|
||||
// Pass true for `recursive` to also destroy all descendent ranges.
|
||||
Meteor.ui._LiveRange.prototype.destroy = function (recursive) {
|
||||
LiveRange.prototype.destroy = function (recursive) {
|
||||
var self = this;
|
||||
|
||||
if (recursive) {
|
||||
// recursive case: destroy all descendent ranges too
|
||||
// (more efficient than actually recursing)
|
||||
|
||||
this.visit(function(is_start, range) {
|
||||
if (is_start) {
|
||||
this.visit(function(isStart, range) {
|
||||
if (isStart) {
|
||||
range._start = null;
|
||||
range._end = null;
|
||||
}
|
||||
}, function(is_start, node) {
|
||||
if (! is_start) {
|
||||
}, function(isStart, node) {
|
||||
if (! isStart) {
|
||||
// when leaving a node, force-clean its children
|
||||
for(var n = node.firstChild; n; n = n.nextSibling) {
|
||||
Meteor.ui._LiveRange._clean_node(self.tag, n, true);
|
||||
LiveRange._cleanNode(self.tag, n, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._remove_entries(this._start, 0, this._start_idx);
|
||||
this._remove_entries(this._end, 1, 0, this._end_idx + 1);
|
||||
|
||||
this._removeEntries(this._start, 0, this._startIndex);
|
||||
this._removeEntries(this._end, 1, 0, this._endIndex + 1);
|
||||
|
||||
if (this._start !== this._end) {
|
||||
// force-clean the top-level nodes in this, besides _start and _end
|
||||
for(var n = this._start.nextSibling;
|
||||
n !== this._end;
|
||||
n = n.nextSibling) {
|
||||
Meteor.ui._LiveRange._clean_node(self.tag, n, true);
|
||||
LiveRange._cleanNode(self.tag, n, true);
|
||||
}
|
||||
|
||||
// clean ends on this._start and starts on this._end
|
||||
if (this._start[self.tag])
|
||||
this._remove_entries(this._start, 1);
|
||||
this._removeEntries(this._start, 1);
|
||||
if (this._end[self.tag])
|
||||
this._remove_entries(this._end, 0);
|
||||
this._removeEntries(this._end, 0);
|
||||
}
|
||||
|
||||
this._start = this._end = null;
|
||||
|
||||
} else {
|
||||
this._remove_entries(this._start, 0, this._start_idx, this._start_idx + 1);
|
||||
this._remove_entries(this._end, 1, this._end_idx, this._end_idx + 1);
|
||||
this._removeEntries(this._start, 0, this._startIndex, this._startIndex + 1);
|
||||
this._removeEntries(this._end, 1, this._endIndex, this._endIndex + 1);
|
||||
this._start = this._end = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Return the first node in the range (in preorder traversal)
|
||||
Meteor.ui._LiveRange.prototype.firstNode = function () {
|
||||
LiveRange.prototype.firstNode = function () {
|
||||
return this._start;
|
||||
};
|
||||
|
||||
// Return the last node in the range (in postorder traversal)
|
||||
Meteor.ui._LiveRange.prototype.lastNode = function () {
|
||||
LiveRange.prototype.lastNode = function () {
|
||||
return this._end;
|
||||
};
|
||||
|
||||
// Return the node that immediately contains this LiveRange, that is,
|
||||
// the parentNode of firstNode and lastNode.
|
||||
Meteor.ui._LiveRange.prototype.containerNode = function() {
|
||||
LiveRange.prototype.containerNode = function() {
|
||||
return this._start.parentNode;
|
||||
};
|
||||
|
||||
@@ -288,57 +285,57 @@ Meteor.ui = Meteor.ui || {};
|
||||
// either the contained ranges (with the same tag as this range),
|
||||
// the contained elements, or both.
|
||||
//
|
||||
// visit_range(is_start, range) is invoked for each range
|
||||
// visitRange(isStart, 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. Both
|
||||
// visitNode(isStart, node) is similar but for nodes. Both
|
||||
// functions are optional.
|
||||
//
|
||||
// If you return false (i.e. a value === false) from visit_range
|
||||
// or visit_node when is_start is true, the children of that range
|
||||
// If you return false (i.e. a value === false) from visitRange
|
||||
// or visitNode when isStart is true, the children of that range
|
||||
// or node are skipped, and the next callback will be the same
|
||||
// range or node with is_start false.
|
||||
// range or node with isStart false.
|
||||
//
|
||||
// If you create or destroy ranges with this tag from a visitation
|
||||
// function, results are undefined!
|
||||
Meteor.ui._LiveRange.prototype.visit = function(visit_range, visit_node) {
|
||||
visit_range = visit_range || function() {};
|
||||
visit_node = visit_node || function() {};
|
||||
LiveRange.prototype.visit = function(visitRange, visitNode) {
|
||||
visitRange = visitRange || function() {};
|
||||
visitNode = visitNode || function() {};
|
||||
|
||||
var tag = this.tag;
|
||||
|
||||
var recurse = function(start, end, start_range_skip) {
|
||||
var startIndex = start_range_skip || 0;
|
||||
var recurse = function(start, end, startRangeSkip) {
|
||||
var startIndex = startRangeSkip || 0;
|
||||
var after = end.nextSibling;
|
||||
for(var n = start; n && n !== after; n = n.nextSibling) {
|
||||
var startData = n[tag] && n[tag][0];
|
||||
if (startData && startIndex < startData.length) {
|
||||
// immediate child range that starts with n
|
||||
var range = startData[startIndex];
|
||||
// be robust if visit_range mutates _start or _end;
|
||||
// be robust if visitRange mutates _start or _end;
|
||||
// useful in destroy(true)
|
||||
var range_start = range._start;
|
||||
var range_end = range._end;
|
||||
if (visit_range(true, range) !== false)
|
||||
recurse(range_start, range_end, startIndex+1);
|
||||
visit_range(false, range);
|
||||
n = range_end;
|
||||
var rangeStart = range._start;
|
||||
var rangeEnd = range._end;
|
||||
if (visitRange(true, range) !== false)
|
||||
recurse(rangeStart, rangeEnd, startIndex+1);
|
||||
visitRange(false, range);
|
||||
n = rangeEnd;
|
||||
}
|
||||
else {
|
||||
// bare node
|
||||
if (visit_node(true, n) !== false && n.firstChild)
|
||||
if (visitNode(true, n) !== false && n.firstChild)
|
||||
recurse(n.firstChild, n.lastChild);
|
||||
visit_node(false, n);
|
||||
visitNode(false, n);
|
||||
}
|
||||
startIndex = 0;
|
||||
}
|
||||
};
|
||||
|
||||
recurse(this._start, this._end, this._start_idx + 1);
|
||||
recurse(this._start, this._end, this._startIndex + 1);
|
||||
};
|
||||
|
||||
// startEnd === 0 for starts, 1 for ends
|
||||
Meteor.ui._LiveRange.prototype._remove_entries =
|
||||
LiveRange.prototype._removeEntries =
|
||||
function(node, startEnd, i, j)
|
||||
{
|
||||
var entries = node[this.tag][startEnd];
|
||||
@@ -347,19 +344,19 @@ Meteor.ui = Meteor.ui || {};
|
||||
var removed = entries.splice(i, j-i);
|
||||
// fix up remaining ranges (not removed ones)
|
||||
for(var a = i; a < entries.length; a++) {
|
||||
if (startEnd) entries[a]._end_idx = a;
|
||||
else entries[a]._start_idx = a;
|
||||
if (startEnd) entries[a]._endIndex = a;
|
||||
else entries[a]._startIndex = a;
|
||||
}
|
||||
|
||||
// potentially remove empty liverange data
|
||||
if (! entries.length) {
|
||||
Meteor.ui._LiveRange._clean_node(this.tag, node);
|
||||
LiveRange._cleanNode(this.tag, node);
|
||||
}
|
||||
|
||||
return removed;
|
||||
};
|
||||
|
||||
Meteor.ui._LiveRange.prototype._insert_entries =
|
||||
LiveRange.prototype._insertEntries =
|
||||
function(node, startEnd, i, newRanges)
|
||||
{
|
||||
// insert the new ranges and "adopt" them by setting node pointers
|
||||
@@ -368,10 +365,10 @@ Meteor.ui = Meteor.ui || {};
|
||||
for(var a=i; a < entries.length; a++) {
|
||||
if (startEnd) {
|
||||
entries[a]._end = node;
|
||||
entries[a]._end_idx = a;
|
||||
entries[a]._endIndex = a;
|
||||
} else {
|
||||
entries[a]._start = node;
|
||||
entries[a]._start_idx = a;
|
||||
entries[a]._startIndex = a;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -386,100 +383,103 @@ Meteor.ui = Meteor.ui || {};
|
||||
// - If the input DocumentFragment has LiveRanges, they will become
|
||||
// our children.
|
||||
//
|
||||
// It is illegal for new_frag to be empty.
|
||||
Meteor.ui._LiveRange.prototype.replace_contents = function (new_frag) {
|
||||
if (! new_frag.firstChild)
|
||||
throw new Error("replace_contents requires non-empty fragment");
|
||||
// It is illegal for newFrag to be empty.
|
||||
LiveRange.prototype.replaceContents = function (newFrag) {
|
||||
if (! newFrag.firstChild)
|
||||
throw new Error("replaceContents requires non-empty fragment");
|
||||
|
||||
return this.operate(function(old_start, old_end) {
|
||||
return this.operate(function(oldStart, oldEnd) {
|
||||
// Insert new fragment
|
||||
old_start.parentNode.insertBefore(new_frag, old_start);
|
||||
oldStart.parentNode.insertBefore(newFrag, oldStart);
|
||||
|
||||
// Pull out departing fragment
|
||||
// Possible optimization: use W3C Ranges on browsers that support them
|
||||
var ret_frag = old_start.ownerDocument.createDocumentFragment();
|
||||
var walk = old_start;
|
||||
var retFrag = oldStart.ownerDocument.createDocumentFragment();
|
||||
var walk = oldStart;
|
||||
while (true) {
|
||||
var next = walk.nextSibling;
|
||||
ret_frag.appendChild(walk);
|
||||
if (walk === old_end)
|
||||
retFrag.appendChild(walk);
|
||||
if (walk === oldEnd)
|
||||
break;
|
||||
walk = next;
|
||||
if (!walk)
|
||||
throw new Error("LiveRanges must begin and end on siblings in order");
|
||||
}
|
||||
|
||||
return ret_frag;
|
||||
return retFrag;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Perform a user-specified DOM mutation on the contents of this range.
|
||||
//
|
||||
// `func` is called with two parameters, `old_start` and `old_end`, equal
|
||||
// `func` is called with two parameters, `oldStart` and `oldEnd`, equal
|
||||
// to the original firstNode() and lastNode() of this range. `func` is allowed
|
||||
// to perform arbitrary operations on the sequence of nodes from `old_start`
|
||||
// to `old_end` and on child ranges of this range. `func` may NOT call methods
|
||||
// to perform arbitrary operations on the sequence of nodes from `oldStart`
|
||||
// to `oldEnd` and on child ranges of this range. `func` may NOT call methods
|
||||
// on this range itself or otherwise rely on the existence of this range and
|
||||
// enclosing ranges. `func` must leave at least one node to become the new
|
||||
// contents of this range.
|
||||
//
|
||||
// The return value of `func` is returned.
|
||||
//
|
||||
// This method is a generalization of replace_contents that works by
|
||||
// This method is a generalization of replaceContents that works by
|
||||
// temporarily removing this LiveRange from the DOM and restoring it after
|
||||
// `func` has been called.
|
||||
Meteor.ui._LiveRange.prototype.operate = function (func) {
|
||||
LiveRange.prototype.operate = function (func) {
|
||||
// boundary nodes of departing fragment
|
||||
var old_start = this._start;
|
||||
var old_end = this._end;
|
||||
var oldStart = this._start;
|
||||
var oldEnd = this._end;
|
||||
|
||||
// pull off outer liverange data
|
||||
var outer_starts =
|
||||
this._remove_entries(old_start, 0, 0, this._start_idx + 1);
|
||||
var outer_ends =
|
||||
this._remove_entries(old_end, 1, this._end_idx);
|
||||
var outerStarts =
|
||||
this._removeEntries(oldStart, 0, 0, this._startIndex + 1);
|
||||
var outerEnds =
|
||||
this._removeEntries(oldEnd, 1, this._endIndex);
|
||||
|
||||
var container_node = old_start.parentNode;
|
||||
var before_node = old_start.previousSibling;
|
||||
var after_node = old_end.nextSibling;
|
||||
var containerNode = oldStart.parentNode;
|
||||
var beforeNode = oldStart.previousSibling;
|
||||
var afterNode = oldEnd.nextSibling;
|
||||
|
||||
var ret = null;
|
||||
|
||||
// perform user-specifiedDOM manipulation
|
||||
ret = func(old_start, old_end);
|
||||
ret = func(oldStart, oldEnd);
|
||||
|
||||
// see what we've got...
|
||||
|
||||
var new_start =
|
||||
before_node ? before_node.nextSibling : container_node.firstChild;
|
||||
var new_end =
|
||||
after_node ? after_node.previousSibling : container_node.lastChild;
|
||||
var newStart =
|
||||
beforeNode ? beforeNode.nextSibling : containerNode.firstChild;
|
||||
var newEnd =
|
||||
afterNode ? afterNode.previousSibling : containerNode.lastChild;
|
||||
|
||||
if (! new_start || new_start === after_node) {
|
||||
if (! newStart || newStart === afterNode) {
|
||||
throw new Error("Ranges must contain at least one element");
|
||||
}
|
||||
|
||||
// wrap endpoints if necessary
|
||||
var new_endpoints = Meteor.ui._wrap_endpoints(new_start, new_end);
|
||||
new_start = this._ensure_tag(new_endpoints[0]);
|
||||
new_end = this._ensure_tag(new_endpoints[1]);
|
||||
var newEndpoints = wrapEndpoints(newStart, newEnd);
|
||||
newStart = this._ensureTag(newEndpoints[0]);
|
||||
newEnd = this._ensureTag(newEndpoints[1]);
|
||||
|
||||
// put the outer liveranges back
|
||||
|
||||
this._insert_entries(new_start, 0, 0, outer_starts);
|
||||
this._insert_entries(new_end, 1, new_end[this.tag][1].length, outer_ends);
|
||||
this._insertEntries(newStart, 0, 0, outerStarts);
|
||||
this._insertEntries(newEnd, 1, newEnd[this.tag][1].length, outerEnds);
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Move all liverange data represented in the DOM from sourceNode to
|
||||
// targetNode.
|
||||
// targetNode. targetNode must be capable of receiving liverange tags
|
||||
// (for example, a node that has been the first or last node of a liverange
|
||||
// before; not a text node in IE).
|
||||
//
|
||||
// This is a low-level operation suitable for moving liveranges en masse
|
||||
// from one DOM tree to another, where transplant_tag is called on every
|
||||
// from one DOM tree to another, where transplantTag is called on every
|
||||
// pair of nodes such that targetNode takes the place of sourceNode.
|
||||
Meteor.ui._LiveRange.transplant_tag = function(tag, targetNode, sourceNode) {
|
||||
LiveRange.transplantTag = function(tag, targetNode, sourceNode) {
|
||||
|
||||
if (! sourceNode[tag])
|
||||
return;
|
||||
|
||||
@@ -497,44 +497,66 @@ Meteor.ui = Meteor.ui || {};
|
||||
ends[i]._end = targetNode;
|
||||
};
|
||||
|
||||
// Takes two sibling nodes tgtStart and tgtEnd with no LiveRange data on them
|
||||
// and a LiveRange srcRange in a separate DOM tree. Transplants srcRange
|
||||
// to span from tgtStart to tgtEnd, and also copies info about enclosing ranges
|
||||
// starting on srcRange._start or ending on srcRange._end. tgtStart and tgtEnd
|
||||
// must be capable of receiving liverange tags (for example, nodes that have
|
||||
// held liverange data in the past; not text nodes in IE).
|
||||
//
|
||||
// This is a low-level operation suitable for moving liveranges en masse
|
||||
// from one DOM tree to another.
|
||||
LiveRange.transplantRange = function(tgtStart, tgtEnd, srcRange) {
|
||||
srcRange._ensureTag(tgtStart);
|
||||
if (tgtEnd !== tgtStart)
|
||||
srcRange._ensureTag(tgtEnd);
|
||||
|
||||
srcRange._insertEntries(
|
||||
tgtStart, 0, 0,
|
||||
srcRange._start[srcRange.tag][0].slice(0, srcRange._startIndex + 1));
|
||||
srcRange._insertEntries(
|
||||
tgtEnd, 1, 0,
|
||||
srcRange._end[srcRange.tag][1].slice(srcRange._endIndex));
|
||||
};
|
||||
|
||||
// Inserts a DocumentFragment immediately before this range.
|
||||
// The new nodes are outside this range but inside all
|
||||
// enclosing ranges.
|
||||
Meteor.ui._LiveRange.prototype.insert_before = function(frag) {
|
||||
var frag_start = frag.firstChild;
|
||||
LiveRange.prototype.insertBefore = function(frag) {
|
||||
var fragStart = frag.firstChild;
|
||||
|
||||
if (! frag_start) // empty frag
|
||||
if (! fragStart) // empty frag
|
||||
return;
|
||||
|
||||
// insert into DOM
|
||||
this._start.parentNode.insertBefore(frag, this._start);
|
||||
|
||||
// move starts of ranges that begin on this._start, but are
|
||||
// outside this, to beginning of frag_start
|
||||
this._ensure_tag(frag_start);
|
||||
this._insert_entries(frag_start, 0, 0,
|
||||
this._remove_entries(this._start, 0, 0,
|
||||
this._start_idx));
|
||||
// outside this, to beginning of fragStart
|
||||
this._ensureTag(fragStart);
|
||||
this._insertEntries(fragStart, 0, 0,
|
||||
this._removeEntries(this._start, 0, 0,
|
||||
this._startIndex));
|
||||
};
|
||||
|
||||
// Inserts a DocumentFragment immediately after this range.
|
||||
// The new nodes are outside this range but inside all
|
||||
// enclosing ranges.
|
||||
Meteor.ui._LiveRange.prototype.insert_after = function(frag) {
|
||||
var frag_end = frag.lastChild;
|
||||
LiveRange.prototype.insertAfter = function(frag) {
|
||||
var fragEnd = frag.lastChild;
|
||||
|
||||
if (! frag_end) // empty frag
|
||||
if (! fragEnd) // empty frag
|
||||
return;
|
||||
|
||||
// insert into DOM
|
||||
this._end.parentNode.insertBefore(frag, this._end.nextSibling);
|
||||
|
||||
// move ends of ranges that end on this._end, but are
|
||||
// outside this, to end of frag_end
|
||||
this._ensure_tag(frag_end);
|
||||
this._insert_entries(frag_end, 1, frag_end[this.tag][1].length,
|
||||
this._remove_entries(this._end, 1,
|
||||
this._end_idx + 1));
|
||||
// outside this, to end of fragEnd
|
||||
this._ensureTag(fragEnd);
|
||||
this._insertEntries(fragEnd, 1, fragEnd[this.tag][1].length,
|
||||
this._removeEntries(this._end, 1,
|
||||
this._endIndex + 1));
|
||||
};
|
||||
|
||||
// Extracts this range and its contents from the DOM and
|
||||
@@ -546,9 +568,9 @@ Meteor.ui = Meteor.ui || {};
|
||||
// it is illegal to perform `extract` if the immediately
|
||||
// enclosing range would become empty. If this precondition
|
||||
// is violated, no action is taken and null is returned.
|
||||
Meteor.ui._LiveRange.prototype.extract = function() {
|
||||
if (this._start_idx > 0 &&
|
||||
this._start[this.tag][0][this._start_idx - 1]._end === this._end) {
|
||||
LiveRange.prototype.extract = function() {
|
||||
if (this._startIndex > 0 &&
|
||||
this._start[this.tag][0][this._startIndex - 1]._end === this._end) {
|
||||
// immediately enclosing range wraps same nodes, so can't extract because
|
||||
// it would empty it.
|
||||
return null;
|
||||
@@ -558,22 +580,22 @@ Meteor.ui = Meteor.ui || {};
|
||||
var after = this._end.nextSibling;
|
||||
var parent = this._start.parentNode;
|
||||
|
||||
if (this._start_idx > 0) {
|
||||
if (this._startIndex > 0) {
|
||||
// must be a later node where outer ranges that start here end;
|
||||
// move their starts to after
|
||||
this._ensure_tag(after);
|
||||
this._insert_entries(after, 0, 0,
|
||||
this._remove_entries(this._start, 0, 0,
|
||||
this._start_idx));
|
||||
this._ensureTag(after);
|
||||
this._insertEntries(after, 0, 0,
|
||||
this._removeEntries(this._start, 0, 0,
|
||||
this._startIndex));
|
||||
}
|
||||
|
||||
if (this._end_idx < this._end[this.tag][1].length - 1) {
|
||||
if (this._endIndex < this._end[this.tag][1].length - 1) {
|
||||
// must be an earlier node where outer ranges that end here
|
||||
// start; move their ends to before
|
||||
this._ensure_tag(before);
|
||||
this._insert_entries(before, 1, before[this.tag][1].length,
|
||||
this._remove_entries(this._end, 1,
|
||||
this._end_idx + 1));
|
||||
this._ensureTag(before);
|
||||
this._insertEntries(before, 1, before[this.tag][1].length,
|
||||
this._removeEntries(this._end, 1,
|
||||
this._endIndex + 1));
|
||||
}
|
||||
|
||||
var result = document.createDocumentFragment();
|
||||
@@ -593,42 +615,42 @@ Meteor.ui = Meteor.ui || {};
|
||||
// this range's container node (the parent of its endpoints) and
|
||||
// only return liveranges whose first and last nodes are siblings
|
||||
// of this one's.
|
||||
Meteor.ui._LiveRange.prototype.findParent = function(withSameContainer) {
|
||||
var result = _enclosing_range_search(this.tag, this._end, this._end_idx);
|
||||
LiveRange.prototype.findParent = function(withSameContainer) {
|
||||
var result = enclosingRangeSearch(this.tag, this._end, this._endIndex);
|
||||
if (result)
|
||||
return result;
|
||||
|
||||
if (withSameContainer)
|
||||
return null;
|
||||
|
||||
return Meteor.ui._LiveRange.findRange(this.tag, this.containerNode());
|
||||
return LiveRange.findRange(this.tag, this.containerNode());
|
||||
};
|
||||
|
||||
// Find the nearest enclosing range containing `node`, if any.
|
||||
Meteor.ui._LiveRange.findRange = function(tag, node) {
|
||||
var result = _enclosing_range_search(tag, node);
|
||||
LiveRange.findRange = function(tag, node) {
|
||||
var result = enclosingRangeSearch(tag, node);
|
||||
if (result)
|
||||
return result;
|
||||
|
||||
if (! node.parentNode)
|
||||
return null;
|
||||
|
||||
return Meteor.ui._LiveRange.findRange(tag, node.parentNode);
|
||||
return LiveRange.findRange(tag, node.parentNode);
|
||||
};
|
||||
|
||||
var _enclosing_range_search = function(tag, end, end_idx) {
|
||||
var enclosingRangeSearch = function(tag, end, endIndex) {
|
||||
// Search for an enclosing range, at the same level,
|
||||
// starting at node `end` or after the range whose
|
||||
// position in the end array of `end` is `end_idx`.
|
||||
// position in the end array of `end` is `endIndex`.
|
||||
// The search works by scanning forwards for range ends
|
||||
// while skipping over ranges whose starts we encounter.
|
||||
|
||||
if (typeof end_idx === "undefined")
|
||||
end_idx = -1;
|
||||
if (typeof endIndex === "undefined")
|
||||
endIndex = -1;
|
||||
|
||||
if (end[tag] && end_idx + 1 < end[tag][1].length) {
|
||||
if (end[tag] && endIndex + 1 < end[tag][1].length) {
|
||||
// immediately enclosing range ends at same node as this one
|
||||
return end[tag][1][end_idx + 1];
|
||||
return end[tag][1][endIndex + 1];
|
||||
}
|
||||
|
||||
var node = end.nextSibling;
|
||||
@@ -639,7 +661,7 @@ Meteor.ui = Meteor.ui || {};
|
||||
// skip over sibling of this range
|
||||
var r = startData[0];
|
||||
node = r._end;
|
||||
endIndex = r._end_idx + 1;
|
||||
endIndex = r._endIndex + 1;
|
||||
}
|
||||
if (node[tag] && endIndex < node[tag][1].length)
|
||||
return node[tag][1][endIndex];
|
||||
@@ -10,25 +10,25 @@ var check_liverange_integrity = function (range) {
|
||||
for (var i = 0; i < data[0].length; i++) {
|
||||
if (data[0][i]._start !== node)
|
||||
throw new Error("integrity check failed - incorrect _start");
|
||||
if (data[0][i]._start_idx !== i)
|
||||
throw new Error("integrity check failed - incorrect _start_idx");
|
||||
if (data[0][i]._startIndex !== i)
|
||||
throw new Error("integrity check failed - incorrect _startIndex");
|
||||
}
|
||||
for (var i = 0; i < data[1].length; i++) {
|
||||
if (data[1][i]._end !== node)
|
||||
throw new Error("integrity check failed - incorrect _end");
|
||||
if (data[1][i]._end_idx !== i)
|
||||
throw new Error("integrity check failed - incorrect _end_idx");
|
||||
if (data[1][i]._endIndex !== i)
|
||||
throw new Error("integrity check failed - incorrect _endIndex");
|
||||
}
|
||||
};
|
||||
|
||||
range.visit(function (is_start, range) {
|
||||
if (is_start)
|
||||
range.visit(function (isStart, range) {
|
||||
if (isStart)
|
||||
stack.push(range);
|
||||
else
|
||||
if (range !== stack.pop())
|
||||
throw new Error("integrity check failed - unbalanced range");
|
||||
}, function (is_start, node) {
|
||||
if (is_start) {
|
||||
}, function (isStart, node) {
|
||||
if (isStart) {
|
||||
check_node(node);
|
||||
stack.push(node);
|
||||
}
|
||||
@@ -40,5 +40,3 @@ var check_liverange_integrity = function (range) {
|
||||
if (stack.length)
|
||||
throw new Error("integrity check failed - missing close tags");
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
/******************************************************************************/
|
||||
|
||||
var create = function (id, start, end, inner, tag) {
|
||||
var ret = new Meteor.ui._LiveRange(tag || 'a', start, end, inner);
|
||||
var ret = new LiveRange(tag || 'a', start, end, inner);
|
||||
ret.id = id;
|
||||
return ret;
|
||||
};
|
||||
@@ -21,13 +21,13 @@ var frag = function (html) {
|
||||
var dump = function (what, tag) {
|
||||
var ret = "";
|
||||
|
||||
var emit = function (is_start, obj) {
|
||||
ret += (is_start ? "<": "</") + obj.id + ">";
|
||||
var emit = function (isStart, obj) {
|
||||
ret += (isStart ? "<": "</") + obj.id + ">";
|
||||
};
|
||||
|
||||
if (typeof what === 'object' && what.nodeType === 11 /* DocumentFragment */) {
|
||||
if (what.firstChild) {
|
||||
var range = new Meteor.ui._LiveRange(tag || 'a', what);
|
||||
var range = new LiveRange(tag || 'a', what);
|
||||
range.visit(emit, emit);
|
||||
range.destroy();
|
||||
}
|
||||
@@ -43,11 +43,11 @@ var dump = function (what, tag) {
|
||||
// actual can be a range or a fragment
|
||||
var assert_dump = function (test, expected, actual, tag) {
|
||||
test.equal(dump(actual), expected, "Tree doesn't match");
|
||||
if (actual instanceof Meteor.ui._LiveRange)
|
||||
if (actual instanceof LiveRange)
|
||||
check_liverange_integrity(actual);
|
||||
else {
|
||||
if (actual.firstChild) {
|
||||
var range = new Meteor.ui._LiveRange(tag || 'a', actual);
|
||||
var range = new LiveRange(tag || 'a', actual);
|
||||
check_liverange_integrity(range);
|
||||
range.destroy();
|
||||
}
|
||||
@@ -58,8 +58,8 @@ var contained_ranges = function (range) {
|
||||
var result = {range: range, children: []};
|
||||
var stack = [result];
|
||||
|
||||
range.visit(function (is_start, range) {
|
||||
if (is_start) {
|
||||
range.visit(function (isStart, range) {
|
||||
if (isStart) {
|
||||
var record = {range: range, children: []};
|
||||
stack[stack.length - 1].children.push(record);
|
||||
stack.push(record);
|
||||
@@ -92,7 +92,7 @@ var assert_contained = function (r, expected) {
|
||||
Tinytest.add("liverange - single node", function (test) {
|
||||
var f = frag("<div id=1></div>");
|
||||
var r_a = create("a", f);
|
||||
test.instanceOf(r_a, Meteor.ui._LiveRange);
|
||||
test.instanceOf(r_a, LiveRange);
|
||||
assert_dump(test, "<a><1></1></a>", r_a);
|
||||
assert_dump(test, "<a><1></1></a>", f);
|
||||
assert_contained(r_a, {range: r_a, children: []});
|
||||
@@ -108,14 +108,14 @@ Tinytest.add("liverange - single node", function (test) {
|
||||
test.equal(r_b.firstNode(), f.firstChild);
|
||||
test.equal(r_b.lastNode(), f.lastChild);
|
||||
|
||||
var ret1 = r_a.replace_contents(frag("<div id=2></div>"), true);
|
||||
var ret1 = r_a.replaceContents(frag("<div id=2></div>"), true);
|
||||
test.equal(ret1.nodeType, 11 /* DocumentFragment */);
|
||||
assert_dump(test, "<1></1>", ret1);
|
||||
assert_dump(test, "<a><2></2></a>", r_a);
|
||||
assert_dump(test, "<b><a><2></2></a></b>", r_b);
|
||||
assert_dump(test, "<b><a><2></2></a></b>", f);
|
||||
|
||||
var ret2 = r_b.replace_contents(frag("<div id=3></div>"), true);
|
||||
var ret2 = r_b.replaceContents(frag("<div id=3></div>"), true);
|
||||
assert_dump(test, "<a><2></2></a>", ret2);
|
||||
assert_dump(test, "<a><2></2></a>", r_a);
|
||||
assert_dump(test, "<b><3></3></b>", r_b);
|
||||
@@ -166,14 +166,14 @@ Tinytest.add("liverange - empty replace", function (test) {
|
||||
f = frag("<div id=1></div>");
|
||||
r = create("z", f);
|
||||
test.throws(function() {
|
||||
r.replace_contents(frag(""));
|
||||
r.replaceContents(frag(""));
|
||||
});
|
||||
|
||||
f = frag("<div id=1></div><div id=2></div><div id=3></div>");
|
||||
r = create("z", f.childNodes[1]);
|
||||
assert_dump(test, "<1></1><z><2></2></z><3></3>", f);
|
||||
test.throws(function() {
|
||||
r.replace_contents(frag(""));
|
||||
r.replaceContents(frag(""));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -261,7 +261,7 @@ Tinytest.add("liverange - multiple nodes", function (test) {
|
||||
var r_o = create("o", f3.childNodes[0], f3.childNodes[0]);
|
||||
assert_dump(test, "<m><o><n><9></9></n></o><10></10><11></11></m>", f3);
|
||||
|
||||
var ret1 = r_i.replace_contents(f3, true);
|
||||
var ret1 = r_i.replaceContents(f3, true);
|
||||
assert_dump(test, "", f3);
|
||||
assert_dump(test, "<2></2><f><c><a><e><3></3></e><d><b><4></4></b></d></a></c></f>", ret1);
|
||||
assert_dump(test, "<l><k><6></6><j><7><h><g><1></1><i><m><o><n><9></9></n></o><10></10><11></11></m></i></g></h><5></5></7><8></8></j></k></l>", f2);
|
||||
@@ -481,7 +481,7 @@ var makeTestPattern = function(codedStr) {
|
||||
// close range
|
||||
var start = starts.pop();
|
||||
var range =
|
||||
new Meteor.ui._LiveRange(
|
||||
new LiveRange(
|
||||
self.tag, start[0].childNodes[start[1]],
|
||||
start[0].lastChild);
|
||||
range.letter = c.toUpperCase();
|
||||
@@ -506,18 +506,18 @@ var makeTestPattern = function(codedStr) {
|
||||
};
|
||||
|
||||
self.findRange = function(node) {
|
||||
return Meteor.ui._LiveRange.findRange(self.tag, node);
|
||||
return LiveRange.findRange(self.tag, node);
|
||||
};
|
||||
|
||||
self.currentString = function() {
|
||||
var buf = [];
|
||||
var tempRange = new Meteor.ui._LiveRange(self.tag, self.frag);
|
||||
tempRange.visit(function(is_start, range) {
|
||||
buf.push(is_start ?
|
||||
var tempRange = new LiveRange(self.tag, self.frag);
|
||||
tempRange.visit(function(isStart, range) {
|
||||
buf.push(isStart ?
|
||||
range.letter.toUpperCase() :
|
||||
range.letter.toLowerCase());
|
||||
}, function(is_start, node) {
|
||||
buf.push(is_start ? '[' : ']');
|
||||
}, function(isStart, node) {
|
||||
buf.push(isStart ? '[' : ']');
|
||||
});
|
||||
tempRange.destroy();
|
||||
|
||||
@@ -603,11 +603,11 @@ Tinytest.add("liverange - destroy", function(test) {
|
||||
var frag = document.createDocumentFragment();
|
||||
var txt = document.createComment("pudding");
|
||||
frag.appendChild(txt);
|
||||
var rng5 = new Meteor.ui._LiveRange('_pudding', txt);
|
||||
var rng4 = new Meteor.ui._LiveRange('_pudding', txt);
|
||||
var rng3 = new Meteor.ui._LiveRange('_pudding', txt);
|
||||
var rng2 = new Meteor.ui._LiveRange('_pudding', txt);
|
||||
var rng1 = new Meteor.ui._LiveRange('_pudding', txt);
|
||||
var rng5 = new LiveRange('_pudding', txt);
|
||||
var rng4 = new LiveRange('_pudding', txt);
|
||||
var rng3 = new LiveRange('_pudding', txt);
|
||||
var rng2 = new LiveRange('_pudding', txt);
|
||||
var rng1 = new LiveRange('_pudding', txt);
|
||||
rng1.num = 1;
|
||||
rng2.num = 2;
|
||||
rng3.num = 3;
|
||||
@@ -617,8 +617,8 @@ Tinytest.add("liverange - destroy", function(test) {
|
||||
rng4.destroy(true);
|
||||
// check that outer ranges are still there
|
||||
var buf = [];
|
||||
rng1.visit(function(is_start, r) {
|
||||
buf.push([is_start, r.num]);
|
||||
rng1.visit(function(isStart, r) {
|
||||
buf.push([isStart, r.num]);
|
||||
});
|
||||
test.equal(buf, [[true, 2], [true, 3], [false, 3], [false, 2]]);
|
||||
});
|
||||
18
packages/liverange/package.js
Normal file
18
packages/liverange/package.js
Normal file
@@ -0,0 +1,18 @@
|
||||
Package.describe({
|
||||
summary: "Mark, track, and update an arbitrary region in the DOM",
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.add_files('liverange.js', 'client');
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use(['tinytest']);
|
||||
api.use(['liverange', 'test-helpers', 'domutils'], 'client');
|
||||
|
||||
api.add_files([
|
||||
'liverange_test_helpers.js',
|
||||
'liverange_tests.js'
|
||||
], 'client');
|
||||
});
|
||||
@@ -1,154 +0,0 @@
|
||||
Meteor.ui = Meteor.ui || {};
|
||||
|
||||
// Define Meteor.ui._htmlToFragment and Meteor.ui._fragmentToHtml.
|
||||
// Adapted from jquery's html() and "clean" routines.
|
||||
//
|
||||
// _fragmentToHtml is only used in test code and could be moved
|
||||
// into a non-core package.
|
||||
_.extend(Meteor.ui, (function() {
|
||||
|
||||
// --- One-time set-up:
|
||||
|
||||
var testDiv = document.createElement("div");
|
||||
testDiv.innerHTML = " <link/><table></table>";
|
||||
|
||||
// Tests that, if true, indicate browser quirks present.
|
||||
var quirks = {
|
||||
// IE loses initial whitespace when setting innerHTML.
|
||||
leadingWhitespaceKilled: (testDiv.firstChild.nodeType !== 3),
|
||||
|
||||
// IE may insert an empty tbody tag in a table.
|
||||
tbodyInsertion: testDiv.getElementsByTagName("tbody").length > 0,
|
||||
|
||||
// IE loses some tags in some environments (requiring extra wrapper).
|
||||
tagsLost: testDiv.getElementsByTagName("link").length === 0
|
||||
};
|
||||
|
||||
// Set up map of wrappers for different nodes.
|
||||
var wrapMap = {
|
||||
option: [ 1, "<select multiple='multiple'>", "</select>" ],
|
||||
legend: [ 1, "<fieldset>", "</fieldset>" ],
|
||||
thead: [ 1, "<table>", "</table>" ],
|
||||
tr: [ 2, "<table><tbody>", "</tbody></table>" ],
|
||||
td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
|
||||
col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
|
||||
area: [ 1, "<map>", "</map>" ],
|
||||
_default: [ 0, "", "" ]
|
||||
};
|
||||
_.extend(wrapMap, {
|
||||
optgroup: wrapMap.option,
|
||||
tbody: wrapMap.thead,
|
||||
tfoot: wrapMap.thead,
|
||||
colgroup: wrapMap.thead,
|
||||
caption: wrapMap.thead,
|
||||
th: wrapMap.td
|
||||
});
|
||||
if (quirks.tagsLost) {
|
||||
// trick from jquery. initial text is ignored when we take lastChild.
|
||||
wrapMap._default = [ 1, "div<div>", "</div>" ];
|
||||
}
|
||||
|
||||
var rleadingWhitespace = /^\s+/,
|
||||
rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
|
||||
rtagName = /<([\w:]+)/,
|
||||
rtbody = /<tbody/i,
|
||||
rhtml = /<|&#?\w+;/,
|
||||
rnoInnerhtml = /<(?:script|style)/i;
|
||||
|
||||
|
||||
return {
|
||||
_htmlToFragment: function(html) {
|
||||
var doc = document; // node factory
|
||||
var frag = doc.createDocumentFragment();
|
||||
|
||||
if (! html.length) {
|
||||
// empty, do nothing
|
||||
} else if (! rhtml.test(html)) {
|
||||
// Just text.
|
||||
frag.appendChild(doc.createTextNode(html));
|
||||
} else {
|
||||
// General case.
|
||||
// Replace self-closing tags
|
||||
html = html.replace(rxhtmlTag, "<$1></$2>");
|
||||
// Use first tag to determine wrapping needed.
|
||||
var firstTagMatch = rtagName.exec(html);
|
||||
var firstTag = (firstTagMatch ? firstTagMatch[1].toLowerCase() : "");
|
||||
var wrapData = wrapMap[firstTag] || wrapMap._default;
|
||||
|
||||
var container = doc.createElement("div");
|
||||
// insert wrapped HTML into a DIV
|
||||
container.innerHTML = wrapData[1] + html + wrapData[2];
|
||||
// set "container" to inner node of wrapper
|
||||
var unwraps = wrapData[0];
|
||||
while (unwraps--) {
|
||||
container = container.lastChild;
|
||||
}
|
||||
|
||||
if (quirks.tbodyInsertion && ! rtbody.test(html)) {
|
||||
// Any tbody we find was created by the browser.
|
||||
var tbodies = container.getElementsByTagName("tbody");
|
||||
_.each(tbodies, function(n) {
|
||||
if (! n.firstChild) {
|
||||
// spurious empty tbody
|
||||
n.parentNode.removeChild(n);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (quirks.leadingWhitespaceKilled) {
|
||||
var wsMatch = rleadingWhitespace.exec(html);
|
||||
if (wsMatch) {
|
||||
container.insertBefore(doc.createTextNode(wsMatch[0]),
|
||||
container.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Reparent children of container to frag.
|
||||
while (container.firstChild)
|
||||
frag.appendChild(container.firstChild);
|
||||
}
|
||||
|
||||
return frag;
|
||||
},
|
||||
_fragmentToHtml: function(frag) {
|
||||
frag = frag.cloneNode(true); // deep copy, don't touch original!
|
||||
|
||||
var doc = document; // node factory
|
||||
|
||||
var firstElement = frag.firstChild;
|
||||
while (firstElement && firstElement.nodeType !== 1) {
|
||||
firstElement = firstElement.nextSibling;
|
||||
}
|
||||
|
||||
var container = doc.createElement("div");
|
||||
|
||||
if (! firstElement) {
|
||||
// no tags!
|
||||
container.appendChild(frag);
|
||||
} else {
|
||||
var firstTag = firstElement.nodeName;
|
||||
var wrapData = wrapMap[firstTag] || wrapMap._default;
|
||||
|
||||
container.innerHTML = wrapData[1] + wrapData[2];
|
||||
var unwraps = wrapData[0];
|
||||
while (unwraps--) {
|
||||
container = container.lastChild;
|
||||
}
|
||||
|
||||
container.appendChild(frag);
|
||||
}
|
||||
|
||||
return container.innerHTML;
|
||||
},
|
||||
_rangeToHtml: function(liverange) {
|
||||
var frag = document.createDocumentFragment();
|
||||
for(var n = liverange.firstNode(),
|
||||
after = liverange.lastNode().nextSibling;
|
||||
n && n !== after;
|
||||
n = n.nextSibling)
|
||||
frag.appendChild(n.cloneNode(true)); // deep copy
|
||||
return Meteor.ui._fragmentToHtml(frag);
|
||||
}
|
||||
};
|
||||
})());
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
Meteor.ui = Meteor.ui || {};
|
||||
Meteor.ui._event = Meteor.ui._event || {};
|
||||
|
||||
// LiveEvents -- Normalized cross-browser event handling library
|
||||
//
|
||||
// This module lets you set up a function f that will be called
|
||||
// whenever an event fires on any node in the DOM. Specifically, when
|
||||
// an event fires on node N, f will be called with N. Then, if the
|
||||
// event is a bubbling event, f will be called again with N's parent,
|
||||
// then called again with N's grandparent, etc, until the root of the
|
||||
// document is reached. This provides a good base on top of which
|
||||
// custom event handling semantics can be implemented.
|
||||
//
|
||||
// f also receives the event object for the event that fired. The
|
||||
// event object is normalized and extended to smooth over
|
||||
// cross-browser differences in event handling. See the details in
|
||||
// setHandler.
|
||||
//
|
||||
// To use, first call setHandler to set the handler function. (There
|
||||
// can be only one.) After that, it's necessary to call
|
||||
// registerEventType to indicate what events you'll be handling and
|
||||
// where in the document they could occur. setHandler and
|
||||
// registerEventType are the only public functions.
|
||||
//
|
||||
// Internally, there are two separate implementations, one for modern
|
||||
// browsers (in liveevents_w3c.js), and one for old browsers with no
|
||||
// event capturing support (in liveevents_now3c.js.) The correct
|
||||
// implementation will be chosen for you automatically at runtime.
|
||||
|
||||
(function() {
|
||||
|
||||
// Install the global event handler. After this function has been
|
||||
// done, handleEventFunc(event) will be called whenever a DOM event
|
||||
// fires or bubbles to a new node.
|
||||
//
|
||||
// 'event' will be a normalized version of the DOM event
|
||||
// object. Some of the properties that are normalized include:
|
||||
// - type
|
||||
// - target
|
||||
// - currentTarget
|
||||
// - stopPropagation()
|
||||
// - preventDefault()
|
||||
// - isPropagationStopped()
|
||||
// - isDefaultPrevented()
|
||||
//
|
||||
// This function should only be called once, ever, and must be
|
||||
// called before registerEventType.
|
||||
Meteor.ui._event.setHandler = function(handleEventFunc) {
|
||||
|
||||
Meteor.ui._event._handleEventFunc = function(event) {
|
||||
// When in unit test mode, wrap the given handleEventFunc to
|
||||
// block events we didn't register for explicitly.
|
||||
// See description of this flag in liveevents_tests.js.
|
||||
if (Meteor.ui._TEST_requirePreciseEventHandlers) {
|
||||
if (! event.currentTarget['_liveui_test_eventtype_'+event.type])
|
||||
return;
|
||||
}
|
||||
|
||||
handleEventFunc(event);
|
||||
};
|
||||
};
|
||||
|
||||
// After calling setHandler, this function must be called some
|
||||
// number of times to enable handling of different events at
|
||||
// different points in the document.
|
||||
//
|
||||
// Specifically, calling this function will ensure that events of
|
||||
// type eventType will be successfully caught when they occur within
|
||||
// the DOM subtree rooted at subtreeRoot (i.e. subtreeRoot and its
|
||||
// descendents). Only the current descendents are registered.
|
||||
// If new nodes are added to the subtree later, they must be
|
||||
// registered.
|
||||
//
|
||||
// If this function isn't called for a given event type T and
|
||||
// subtree S, and T fires within S, then it's unspecified whether
|
||||
// handleEventFunc will be called. (In browsers where we are able to
|
||||
// catch events for the entire document using a capturing handler,
|
||||
// it will be called. In browsers that don't support this, the event
|
||||
// will be lost.)
|
||||
Meteor.ui._event.registerEventType = function(eventType, subtreeRoot) {
|
||||
// Only work on element nodes, not e.g. text nodes or fragments
|
||||
if (subtreeRoot.nodeType !== 1)
|
||||
return;
|
||||
|
||||
Meteor.ui._event.registerEventTypeImpl(eventType, subtreeRoot);
|
||||
|
||||
// When in unit test mode, mark all the nodes in the current subtree.
|
||||
// We will later block events on nodes that weren't marked. This
|
||||
// tests that LiveUI is generating calls to registerEventType
|
||||
// with proper subtree information, even in browsers that don't need
|
||||
// it.
|
||||
// See description of this flag in liveevents_tests.js.
|
||||
if (Meteor.ui._TEST_requirePreciseEventHandlers) {
|
||||
var n = subtreeRoot, t = eventType;
|
||||
// set property to any non-primitive value (to prevent showing
|
||||
// up as an HTML attribute in IE)
|
||||
n['_liveui_test_eventtype_'+t] = n;
|
||||
if (n.firstChild) {
|
||||
_.each(n.getElementsByTagName('*'), function(x) {
|
||||
x['_liveui_test_eventtype_'+t] = x;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Meteor.ui._event.registerEventTypeImpl = null; // overridden by impls
|
||||
|
||||
|
||||
// inspired by jquery fix()
|
||||
Meteor.ui._event._fixEvent = function(event) {
|
||||
var originalStopPropagation = event.stopPropagation;
|
||||
var originalPreventDefault = event.preventDefault;
|
||||
event.isPropagationStopped = returnFalse;
|
||||
event.isImmediatePropagationStopped = returnFalse;
|
||||
event.isDefaultPrevented = returnFalse;
|
||||
event.stopPropagation = function() {
|
||||
event.isPropagationStopped = returnTrue;
|
||||
if (originalStopPropagation)
|
||||
originalStopPropagation.call(event);
|
||||
else
|
||||
event.cancelBubble = true; // IE
|
||||
};
|
||||
event.preventDefault = function() {
|
||||
event.isDefaultPrevented = returnTrue;
|
||||
if (originalPreventDefault)
|
||||
originalPreventDefault.call(event);
|
||||
else
|
||||
event.returnValue = false; // IE
|
||||
};
|
||||
event.stopImmediatePropagation = function() {
|
||||
event.stopPropagation();
|
||||
event.isImmediatePropagationStopped = returnTrue;
|
||||
};
|
||||
|
||||
var type = event.type;
|
||||
|
||||
// adapted from jquery
|
||||
if (event.metaKey === undefined)
|
||||
event.metaKey = event.ctrlKey;
|
||||
if (/^key/.test(type)) {
|
||||
// KEY EVENTS
|
||||
// Add which. Technically char codes and key codes are
|
||||
// different things; the former is ASCII/unicode/etc and the
|
||||
// latter is arbitrary. But browsers that lack charCode
|
||||
// seem to put character info in keyCode.
|
||||
// (foo == null) tests for null or undefined
|
||||
if (event.which == null)
|
||||
event.which = (event.charCode != null ? event.charCode : event.keyCode);
|
||||
} else if (/^(?:mouse|contextmenu)|click/.test(type)) {
|
||||
// MOUSE EVENTS
|
||||
// Add relatedTarget, if necessary
|
||||
if (! event.relatedTarget && event.fromElement)
|
||||
event.relatedTarget = (event.fromElement === event.target ?
|
||||
event.toElement : event.fromElement);
|
||||
// Add which for click: 1 === left; 2 === middle; 3 === right
|
||||
if (! event.which && event.button !== undefined ) {
|
||||
var button = event.button;
|
||||
event.which = (button & 1 ? 1 :
|
||||
(button & 2 ? 3 :
|
||||
(button & 4 ? 2 : 0 )));
|
||||
}
|
||||
}
|
||||
|
||||
return event;
|
||||
};
|
||||
|
||||
var returnFalse = function() { return false; };
|
||||
var returnTrue = function() { return true; };
|
||||
|
||||
if (! document.addEventListener)
|
||||
Meteor.ui._event._loadNoW3CImpl(); // IE 6-8
|
||||
else
|
||||
Meteor.ui._event._loadW3CImpl(); // IE 9-10, WebKit, Firefox, Opera
|
||||
|
||||
})();
|
||||
@@ -1,26 +0,0 @@
|
||||
|
||||
Meteor.ui = Meteor.ui || {};
|
||||
|
||||
// LiveEvents is unit-tested by the LiveUI tests, because it was
|
||||
// originally extracted from liveui.js.
|
||||
|
||||
|
||||
// TEST FLAG: requirePreciseEventHandlers
|
||||
//
|
||||
// This flag enables extra checking that LiveUI is correctly registering new
|
||||
// DOM nodes with LiveEvents, even in browsers that don't require it.
|
||||
// If the checks fail, it means the tests would
|
||||
// fail anyway in Old IE, but this way we get to find out sooner.
|
||||
//
|
||||
// The reason for this set-up is that the main (W3C) implementation of
|
||||
// LiveEvents doesn't need to know when nodes are added to the DOM
|
||||
// via the "subtreeRoot" information in registerEventType.
|
||||
// However, the Old IE implementation does, so it's important that LiveUI
|
||||
// tell us specifically what nodes need event handlers. When this
|
||||
// flag is true, we hold LiveUI to the same standard of specificity whether
|
||||
// or not we are running Old IE.
|
||||
//
|
||||
// This flag is set to `true` when running unit tests (via the inclusion
|
||||
// of this file). Of course, the tests are assumed to still pass even if
|
||||
// it is `false`, in which case the extra checks aren't done.
|
||||
Meteor.ui._TEST_requirePreciseEventHandlers = true;
|
||||
@@ -1,180 +0,0 @@
|
||||
Meteor.ui = Meteor.ui || {};
|
||||
Meteor.ui._event = Meteor.ui._event || {};
|
||||
|
||||
// LiveEvents implementation that depends on the W3C event model,
|
||||
// i.e. addEventListener and capturing. It's intended for all
|
||||
// browsers except IE <= 8.
|
||||
//
|
||||
// We take advantage of the fact that event handlers installed during
|
||||
// the capture phase are live during the bubbling phase. By installing
|
||||
// a capturing listener on the document, we bind the handler to the
|
||||
// event target and its ancestors "just in time".
|
||||
|
||||
Meteor.ui._event._loadW3CImpl = function() {
|
||||
var SIMULATE_NEITHER = 0;
|
||||
var SIMULATE_FOCUS_BLUR = 1;
|
||||
var SIMULATE_FOCUSIN_FOCUSOUT = 2;
|
||||
|
||||
// Focusin/focusout are the bubbling versions of focus/blur, and are
|
||||
// part of the W3C spec, but are absent from Firefox as of today
|
||||
// (v11), so we supply them.
|
||||
//
|
||||
// In addition, while most browsers fire these events sync in
|
||||
// response to a programmatic action (like .focus()), not all do.
|
||||
// IE 9+ fires focusin/focusout sync but focus/blur async. Opera
|
||||
// fires them all async. We don't do anything about this right now,
|
||||
// but simulating focus/blur on IE would make them sync.
|
||||
//
|
||||
// We have the capabiilty here to simulate focusin/focusout from
|
||||
// focus/blur, vice versa, or neither.
|
||||
//
|
||||
// We do a browser check that fails in old Firefox (3.6) but will
|
||||
// succeed if Firefox ever implements focusin/focusout. Old Firefox
|
||||
// fails all tests of the form ('onfoo' in node), while new Firefox
|
||||
// and all other known browsers will pass if 'foo' is a known event.
|
||||
var focusBlurMode = ('onfocusin' in document.createElement("DIV")) ?
|
||||
SIMULATE_NEITHER : SIMULATE_FOCUSIN_FOCUSOUT;
|
||||
|
||||
// mouseenter/mouseleave is non-bubbling mouseover/mouseout. It's
|
||||
// standard but only IE and Opera seem to support it,
|
||||
// so we simulate it (which works in IE but not in Opera for some reason).
|
||||
var simulateMouseEnterLeave = (! window.opera);
|
||||
|
||||
var universalCapturer = function(event) {
|
||||
if (event.target.nodeType === 3) // fix text-node target
|
||||
event.target = event.target.parentNode;
|
||||
|
||||
var type = event.type;
|
||||
var bubbles = event.bubbles;
|
||||
var target = event.target;
|
||||
|
||||
target.addEventListener(type, universalHandler, false);
|
||||
|
||||
// According to the DOM event spec, if the DOM is mutated during
|
||||
// event handling, the original bubbling order still applies.
|
||||
// So we can determine the chain of nodes that could possibly
|
||||
// be bubbled to right now.
|
||||
var ancestors;
|
||||
if (bubbles) {
|
||||
ancestors = [];
|
||||
for(var n = target.parentNode; n; n = n.parentNode) {
|
||||
n.addEventListener(type, universalHandler, false);
|
||||
ancestors.push(n);
|
||||
};
|
||||
}
|
||||
|
||||
// Unbind the handlers later.
|
||||
Meteor.defer(function() {
|
||||
target.removeEventListener(type, universalHandler);
|
||||
if (bubbles) {
|
||||
_.each(ancestors, function(n) {
|
||||
n.removeEventListener(type, universalHandler);
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
var sendUIEvent = function(type, target, bubbles, cancelable, detail) {
|
||||
var event = document.createEvent("UIEvents");
|
||||
event.initUIEvent(type, bubbles, cancelable, window, detail);
|
||||
event.synthetic = true;
|
||||
target.dispatchEvent(event);
|
||||
};
|
||||
|
||||
var universalHandler = function(event) {
|
||||
// fire synthetic focusin/focusout on blur/focus or vice versa
|
||||
if (event.currentTarget === event.target) {
|
||||
if (focusBlurMode === SIMULATE_FOCUS_BLUR) {
|
||||
if (event.type === 'focusin')
|
||||
sendUIEvent('focus', event.target, false);
|
||||
else if (event.type === 'focusout')
|
||||
sendUIEvent('blur', event.target, false);
|
||||
} else if (focusBlurMode === SIMULATE_FOCUSIN_FOCUSOUT) {
|
||||
if (event.type === 'focus')
|
||||
sendUIEvent('focusin', event.target, true);
|
||||
else if (event.type === 'blur')
|
||||
sendUIEvent('focusout', event.target, true);
|
||||
}
|
||||
}
|
||||
// only respond to synthetic events of the types we are faking
|
||||
if (focusBlurMode === SIMULATE_FOCUS_BLUR) {
|
||||
if (event.type === 'focus' || event.type === 'blur') {
|
||||
if (! event.synthetic)
|
||||
return;
|
||||
}
|
||||
} else if (focusBlurMode === SIMULATE_FOCUSIN_FOCUSOUT) {
|
||||
if (event.type === 'focusin' || event.type === 'focusout') {
|
||||
if (! event.synthetic)
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (simulateMouseEnterLeave) {
|
||||
if (event.type === 'mouseenter' || event.type === 'mouseleave') {
|
||||
if (! event.synthetic)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.ui._event._handleEventFunc(
|
||||
Meteor.ui._event._fixEvent(event));
|
||||
|
||||
// event ordering: fire mouseleave after mouseout
|
||||
if (simulateMouseEnterLeave &&
|
||||
// We respond to mouseover/mouseout here even on
|
||||
// bubble, i.e. when event.currentTarget !== event.target,
|
||||
// to ensure we see every enter and leave.
|
||||
// We ignore the case where the mouse enters from
|
||||
// a child or leaves to a child (by checking if
|
||||
// relatedTarget is present and a descendent).
|
||||
(! event.relatedTarget ||
|
||||
(event.currentTarget !== event.relatedTarget &&
|
||||
// XXX change this to call domutils.js when
|
||||
// davidchunks branch lands
|
||||
! Meteor.ui._Patcher._elementContains(
|
||||
event.currentTarget, event.relatedTarget)))) {
|
||||
if (event.type === 'mouseover'){
|
||||
sendUIEvent('mouseenter', event.currentTarget, false);
|
||||
}
|
||||
else if (event.type === 'mouseout') {
|
||||
sendUIEvent('mouseleave', event.currentTarget, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var installCapturer = function(eventType) {
|
||||
// install handlers for the events used to fake events of this type,
|
||||
// in addition to handlers for the real type
|
||||
if (focusBlurMode === SIMULATE_FOCUS_BLUR) {
|
||||
if (eventType === 'focus')
|
||||
installCapturer('focusin');
|
||||
else if (eventType === 'blur')
|
||||
installCapturer('focusout');
|
||||
} else if (focusBlurMode === SIMULATE_FOCUSIN_FOCUSOUT) {
|
||||
if (eventType === 'focusin')
|
||||
installCapturer('focus');
|
||||
else if (eventType === 'focusout')
|
||||
installCapturer('blur');
|
||||
}
|
||||
if (simulateMouseEnterLeave) {
|
||||
if (eventType === 'mouseenter')
|
||||
installCapturer('mouseover');
|
||||
else if (eventType === 'mouseleave')
|
||||
installCapturer('mouseout');
|
||||
}
|
||||
|
||||
if (! eventsCaptured[eventType]) {
|
||||
// only bind one event capturer per type
|
||||
eventsCaptured[eventType] = true;
|
||||
document.addEventListener(eventType, universalCapturer, true);
|
||||
}
|
||||
};
|
||||
|
||||
var eventsCaptured = {};
|
||||
|
||||
Meteor.ui._event.registerEventTypeImpl = function(eventType, subtreeRoot) {
|
||||
// We capture on the entire document, so don't actually care
|
||||
// about subtreeRoot!
|
||||
installCapturer(eventType);
|
||||
};
|
||||
|
||||
};
|
||||
@@ -1,680 +0,0 @@
|
||||
Meteor.ui = Meteor.ui || {};
|
||||
|
||||
(function() {
|
||||
|
||||
// In render mode (i.e. inside Meteor.ui.render), this is an
|
||||
// object, otherwise it is null.
|
||||
// callbacks: id -> func, where id ranges from 1 to callbacks._count.
|
||||
Meteor.ui._render_mode = null;
|
||||
|
||||
// `in_range` is a package-private argument used to render inside
|
||||
// an existing LiveRange on an update.
|
||||
Meteor.ui.render = function (html_func, react_data, in_range) {
|
||||
if (typeof html_func !== "function")
|
||||
throw new Error("Meteor.ui.render() requires a function as its first argument.");
|
||||
|
||||
if (Meteor.ui._render_mode)
|
||||
throw new Error("Can't nest Meteor.ui.render.");
|
||||
|
||||
var cx = new Meteor.deps.Context;
|
||||
|
||||
Meteor.ui._render_mode = {callbacks: {_count: 0}};
|
||||
var html, rangeCallbacks;
|
||||
try {
|
||||
html = cx.run(html_func); // run the caller's html_func
|
||||
} finally {
|
||||
rangeCallbacks = Meteor.ui._render_mode.callbacks;
|
||||
Meteor.ui._render_mode = null;
|
||||
}
|
||||
|
||||
if (typeof html !== "string")
|
||||
throw new Error("Render function must return a string");
|
||||
|
||||
var frag = Meteor.ui._htmlToFragment(html);
|
||||
if (! frag.firstChild)
|
||||
frag.appendChild(document.createComment("empty"));
|
||||
|
||||
|
||||
// Helper that invokes `f` on every comment node under `parent`.
|
||||
// If `f` returns a node, visit that node next.
|
||||
var each_comment = function(parent, f) {
|
||||
for (var n = parent.firstChild; n;) {
|
||||
if (n.nodeType === 8) { // comment
|
||||
n = f(n) || n.nextSibling;
|
||||
continue;
|
||||
} else if (n.nodeType === 1) { // element
|
||||
each_comment(n, f);
|
||||
}
|
||||
n = n.nextSibling;
|
||||
}
|
||||
};
|
||||
|
||||
// walk comments and create ranges
|
||||
var rangeStartNodes = {};
|
||||
var rangesCreated = []; // [[range, id], ...]
|
||||
each_comment(frag, function(n) {
|
||||
|
||||
var rangeCommentMatch = /^\s*(START|END)RANGE_(\S+)/.exec(n.nodeValue);
|
||||
if (! rangeCommentMatch)
|
||||
return null;
|
||||
|
||||
var which = rangeCommentMatch[1];
|
||||
var id = rangeCommentMatch[2];
|
||||
|
||||
if (which === "START") {
|
||||
if (rangeStartNodes[id])
|
||||
throw new Error("The return value of chunk can only be used once.");
|
||||
rangeStartNodes[id] = n;
|
||||
|
||||
return null;
|
||||
}
|
||||
// else: which === "END"
|
||||
|
||||
var startNode = rangeStartNodes[id];
|
||||
var endNode = n;
|
||||
var next = endNode.nextSibling;
|
||||
|
||||
// try to remove comments
|
||||
var a = startNode, b = endNode;
|
||||
if (a.nextSibling && b.previousSibling) {
|
||||
if (a.nextSibling === b) {
|
||||
// replace two adjacent comments with one
|
||||
endNode = startNode;
|
||||
b.parentNode.removeChild(b);
|
||||
startNode.nodeValue = 'placeholder';
|
||||
} else {
|
||||
// remove both comments
|
||||
startNode = startNode.nextSibling;
|
||||
endNode = endNode.previousSibling;
|
||||
a.parentNode.removeChild(a);
|
||||
b.parentNode.removeChild(b);
|
||||
}
|
||||
} else {
|
||||
/* shouldn't happen; invalid HTML? */
|
||||
}
|
||||
|
||||
if (startNode.parentNode !== endNode.parentNode) {
|
||||
// Try to fix messed-up comment ranges like
|
||||
// <!-- #1 --><tbody> ... <!-- /#1 --></tbody>,
|
||||
// which are extremely common with tables. Tests
|
||||
// fail in all browsers without this code.
|
||||
if (startNode === endNode.parentNode ||
|
||||
startNode === endNode.parentNode.previousSibling) {
|
||||
startNode = endNode.parentNode.firstChild;
|
||||
} else if (endNode === startNode.parentNode ||
|
||||
endNode === startNode.parentNode.nextSibling) {
|
||||
endNode = startNode.parentNode.lastChild;
|
||||
} else {
|
||||
var r = new RegExp('<!--\\s*STARTRANGE_'+id+'.*?-->', 'g');
|
||||
var match = r.exec(html);
|
||||
var help = "";
|
||||
if (match) {
|
||||
var comment_end = r.lastIndex;
|
||||
var comment_start = comment_end - match[0].length;
|
||||
var stripped_before = html.slice(0, comment_start).replace(
|
||||
/<!--\s*(START|END)RANGE.*?-->/g, '');
|
||||
var stripped_after = html.slice(comment_end).replace(
|
||||
/<!--\s*(START|END)RANGE.*?-->/g, '');
|
||||
var context_amount = 50;
|
||||
var context = stripped_before.slice(-context_amount) +
|
||||
stripped_after.slice(0, context_amount);
|
||||
help = " (possible unclosed near: "+context+")";
|
||||
}
|
||||
throw new Error("Could not create liverange in template. "+
|
||||
"Check for unclosed tags in your HTML."+help);
|
||||
}
|
||||
}
|
||||
|
||||
var range = new Meteor.ui._LiveRange(Meteor.ui._tag, startNode, endNode);
|
||||
rangesCreated.push([range, id]);
|
||||
|
||||
return next;
|
||||
});
|
||||
|
||||
|
||||
var range;
|
||||
if (in_range) {
|
||||
// Called to re-render a chunk; update that chunk in place.
|
||||
Meteor.ui._intelligent_replace(in_range, frag);
|
||||
range = in_range;
|
||||
} else {
|
||||
range = new Meteor.ui._LiveRange(Meteor.ui._tag, frag);
|
||||
}
|
||||
|
||||
// Call "added to DOM" callbacks to wire up all sub-chunks.
|
||||
_.each(rangesCreated, function(x) {
|
||||
var range = x[0];
|
||||
var id = x[1];
|
||||
if (rangeCallbacks[id])
|
||||
rangeCallbacks[id](range);
|
||||
});
|
||||
|
||||
Meteor.ui._wire_up(cx, range, html_func, react_data);
|
||||
|
||||
return (in_range ? null : frag);
|
||||
|
||||
};
|
||||
|
||||
Meteor.ui.chunk = function(html_func, react_data) {
|
||||
if (typeof html_func !== "function")
|
||||
throw new Error("Meteor.ui.chunk() requires a function as its first argument.");
|
||||
|
||||
if (! Meteor.ui._render_mode) {
|
||||
return html_func();
|
||||
}
|
||||
|
||||
var cx = new Meteor.deps.Context;
|
||||
var html = cx.run(html_func);
|
||||
|
||||
if (typeof html !== "string")
|
||||
throw new Error("Render function must return a string");
|
||||
|
||||
return Meteor.ui._ranged_html(html, function(range) {
|
||||
Meteor.ui._wire_up(cx, range, html_func, react_data);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Meteor.ui.listChunk = function (observable, doc_func, else_func, react_data) {
|
||||
if (arguments.length === 3 && typeof else_func === "object") {
|
||||
// support (observable, doc_func, react_data) form
|
||||
react_data = else_func;
|
||||
else_func = null;
|
||||
}
|
||||
|
||||
if (typeof doc_func !== "function")
|
||||
throw new Error("Meteor.ui.listChunk() requires a function as first argument");
|
||||
else_func = (typeof else_func === "function" ? else_func :
|
||||
function() { return ""; });
|
||||
react_data = react_data || {};
|
||||
|
||||
var buf = [];
|
||||
var receiver = new Meteor.ui._CallbackReceiver();
|
||||
|
||||
var handle = observable.observe(receiver);
|
||||
receiver.flush_to_array(buf);
|
||||
|
||||
var inner_html;
|
||||
if (buf.length === 0) {
|
||||
inner_html = Meteor.ui.chunk(else_func, react_data);
|
||||
} else {
|
||||
var doc_render = function(doc) {
|
||||
return Meteor.ui._ranged_html(
|
||||
Meteor.ui.chunk(function() { return doc_func(doc); },
|
||||
_.extend({}, react_data, {event_data: doc})));
|
||||
};
|
||||
inner_html = _.map(buf, doc_render).join('');
|
||||
}
|
||||
|
||||
if (! Meteor.ui._render_mode) {
|
||||
handle.stop();
|
||||
return inner_html;
|
||||
}
|
||||
|
||||
return Meteor.ui._ranged_html(inner_html, function(outer_range) {
|
||||
var range_list = [];
|
||||
// find immediate sub-ranges of range, and add to range_list
|
||||
if (buf.length > 0) {
|
||||
outer_range.visit(function(is_start, r) {
|
||||
if (is_start)
|
||||
range_list.push(r);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
Meteor.ui._wire_up_list(outer_range, range_list, receiver, handle,
|
||||
doc_func, else_func, react_data);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
var killContext = function(range) {
|
||||
var cx = range.context;
|
||||
if (cx && ! cx.killed) {
|
||||
cx.killed = true;
|
||||
cx.invalidate && cx.invalidate();
|
||||
delete range.context;
|
||||
}
|
||||
};
|
||||
|
||||
Meteor.ui._tag = "_liveui";
|
||||
|
||||
var _checkOffscreen = function(range) {
|
||||
var node = range.firstNode();
|
||||
|
||||
if (node.parentNode &&
|
||||
(Meteor.ui._onscreen(node) || Meteor.ui._is_held(node)))
|
||||
return false;
|
||||
|
||||
cleanup_range(range);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Internal facility, only used by tests, for holding onto
|
||||
// DocumentFragments across flush(). Reference counts
|
||||
// using hold() and release().
|
||||
Meteor.ui._is_held = function(node) {
|
||||
while (node.parentNode)
|
||||
node = node.parentNode;
|
||||
|
||||
return node.nodeType !== 3 /*TEXT_NODE*/ && node._liveui_refs;
|
||||
};
|
||||
Meteor.ui._hold = function(frag) {
|
||||
frag._liveui_refs = (frag._liveui_refs || 0) + 1;
|
||||
};
|
||||
Meteor.ui._release = function(frag) {
|
||||
// Clean up on flush, if hits 0.
|
||||
// Don't want to decrement
|
||||
// _liveui_refs to 0 now because someone else might
|
||||
// clean it up if it's not held.
|
||||
var cx = new Meteor.deps.Context;
|
||||
cx.on_invalidate(function() {
|
||||
--frag._liveui_refs;
|
||||
if (! frag._liveui_refs)
|
||||
cleanup_frag(frag);
|
||||
});
|
||||
cx.invalidate();
|
||||
};
|
||||
|
||||
Meteor.ui._onscreen = function (node) {
|
||||
// http://jsperf.com/is-element-in-the-dom
|
||||
|
||||
if (document.compareDocumentPosition)
|
||||
return document.compareDocumentPosition(node) & 16;
|
||||
else {
|
||||
if (node.nodeType !== 1 /* Element */)
|
||||
/* contains() doesn't work reliably on non-Elements. Fine on
|
||||
Chrome, not so much on Safari and IE. */
|
||||
node = node.parentNode;
|
||||
if (node.nodeType === 11 /* DocumentFragment */ ||
|
||||
node.nodeType === 9 /* Document */)
|
||||
/* contains() chokes on DocumentFragments on IE8 */
|
||||
return node === document;
|
||||
/* contains() exists on document on Chrome, but only on
|
||||
document.body on some other browsers. */
|
||||
return document.body.contains(node);
|
||||
}
|
||||
};
|
||||
|
||||
var CallbackReceiver = function() {
|
||||
var self = this;
|
||||
|
||||
self.queue = [];
|
||||
self.deps = {};
|
||||
|
||||
// attach these callback funcs to each instance, as they may
|
||||
// not be called as methods by livedata.
|
||||
_.each(["added", "removed", "moved", "changed"], function (name) {
|
||||
self[name] = function (/* arguments */) {
|
||||
self.queue.push([name].concat(_.toArray(arguments)));
|
||||
self.signal();
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
Meteor.ui._CallbackReceiver = CallbackReceiver;
|
||||
|
||||
CallbackReceiver.prototype.flush_to = function(t) {
|
||||
// fire all queued events on new target
|
||||
_.each(this.queue, function(x) {
|
||||
var name = x[0];
|
||||
var args = x.slice(1);
|
||||
t[name].apply(t, args);
|
||||
});
|
||||
this.queue.length = 0;
|
||||
};
|
||||
CallbackReceiver.prototype.flush_to_array = function(array) {
|
||||
// apply all queued events to array
|
||||
_.each(this.queue, function(x) {
|
||||
switch (x[0]) {
|
||||
case 'added': array.splice(x[2], 0, x[1]); break;
|
||||
case 'removed': array.splice(x[2], 1); break;
|
||||
case 'moved': array.splice(x[3], 0, array.splice(x[2], 1)[0]); break;
|
||||
case 'changed': array[x[2]] = x[1]; break;
|
||||
}
|
||||
});
|
||||
this.queue.length = 0;
|
||||
};
|
||||
CallbackReceiver.prototype.signal = function() {
|
||||
if (this.queue.length > 0) {
|
||||
for(var id in this.deps)
|
||||
this.deps[id].invalidate();
|
||||
}
|
||||
};
|
||||
CallbackReceiver.prototype.depend = function() {
|
||||
var context = Meteor.deps.Context.current;
|
||||
if (context && !(context.id in this.deps)) {
|
||||
this.deps[context.id] = context;
|
||||
var self = this;
|
||||
context.on_invalidate(function() {
|
||||
delete self.deps[context.id];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Performs a replacement by determining which nodes should
|
||||
// be preserved and invoking Meteor.ui._Patcher as appropriate.
|
||||
Meteor.ui._intelligent_replace = function(tgtRange, srcParent) {
|
||||
|
||||
// Table-body fix: if tgtRange is in a table and srcParent
|
||||
// contains a TR, wrap fragment in a TBODY on all browsers,
|
||||
// so that it will display properly in IE.
|
||||
if (tgtRange.containerNode().nodeName === "TABLE" &&
|
||||
_.any(srcParent.childNodes,
|
||||
function(n) { return n.nodeName === "TR"; })) {
|
||||
var tbody = document.createElement("TBODY");
|
||||
while (srcParent.firstChild)
|
||||
tbody.appendChild(srcParent.firstChild);
|
||||
srcParent.appendChild(tbody);
|
||||
}
|
||||
|
||||
var copyFunc = function(t, s) {
|
||||
Meteor.ui._LiveRange.transplant_tag(Meteor.ui._tag, t, s);
|
||||
};
|
||||
|
||||
//tgtRange.replace_contents(srcParent);
|
||||
|
||||
tgtRange.operate(function(start, end) {
|
||||
// clear all LiveRanges on target
|
||||
cleanup_range(new Meteor.ui._LiveRange(Meteor.ui._tag, start, end));
|
||||
|
||||
var patcher = new Meteor.ui._Patcher(
|
||||
start.parentNode, srcParent,
|
||||
start.previousSibling, end.nextSibling);
|
||||
patcher.diffpatch(copyFunc);
|
||||
});
|
||||
|
||||
attach_secondary_events(tgtRange);
|
||||
};
|
||||
|
||||
Meteor.ui._wire_up = function(cx, range, html_func, react_data) {
|
||||
// wire events
|
||||
var data = react_data || {};
|
||||
if (data.events)
|
||||
range.event_handlers = unpackEventMap(data.events);
|
||||
if (data.event_data)
|
||||
range.event_data = data.event_data;
|
||||
|
||||
attach_events(range);
|
||||
|
||||
// record that if we see this range offscreen during a flush,
|
||||
// we are to kill the context (mark it killed and invalidate it).
|
||||
// Kill old context from previous update.
|
||||
killContext(range);
|
||||
range.context = cx;
|
||||
|
||||
// wire update
|
||||
cx.on_invalidate(function(old_cx) {
|
||||
if (old_cx.killed)
|
||||
return; // context was invalidated as part of killing it
|
||||
if (_checkOffscreen(range))
|
||||
return;
|
||||
|
||||
Meteor.ui.render(html_func, react_data, range);
|
||||
});
|
||||
};
|
||||
|
||||
// Convert an event map from the developer into an internal
|
||||
// format for range.event_handlers. The internal format is
|
||||
// an array of objects with properties {type, selector, callback}.
|
||||
// The array has an expando property `types`, which is a list
|
||||
// of all the unique event types used (as an optimization for
|
||||
// code that needs this info).
|
||||
var unpackEventMap = function(events) {
|
||||
var handlers = [];
|
||||
|
||||
var eventTypeSet = {};
|
||||
|
||||
// iterate over `spec: callback` map
|
||||
_.each(events, function(callback, spec) {
|
||||
var clauses = spec.split(/,\s+/);
|
||||
// iterate over clauses of spec, e.g. ['click .foo', 'click .bar']
|
||||
_.each(clauses, function (clause) {
|
||||
var parts = clause.split(/\s+/);
|
||||
if (parts.length === 0)
|
||||
return;
|
||||
|
||||
var type = parts.shift();
|
||||
var selector = parts.join(' ');
|
||||
|
||||
handlers.push({type:type, selector:selector, callback:callback});
|
||||
eventTypeSet[type] = true;
|
||||
});
|
||||
});
|
||||
|
||||
handlers.types = _.keys(eventTypeSet);
|
||||
return handlers;
|
||||
};
|
||||
|
||||
Meteor.ui._wire_up_list =
|
||||
function(outer_range, range_list, receiver, handle_to_stop,
|
||||
doc_func, else_func, react_data)
|
||||
{
|
||||
react_data = react_data || {};
|
||||
|
||||
outer_range.context = new Meteor.deps.Context;
|
||||
outer_range.context.run(function() {
|
||||
receiver.depend();
|
||||
});
|
||||
outer_range.context.on_invalidate(function update(old_cx) {
|
||||
if (old_cx.killed || _checkOffscreen(outer_range)) {
|
||||
if (handle_to_stop)
|
||||
handle_to_stop.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
receiver.flush_to(callbacks);
|
||||
|
||||
Meteor.ui._wire_up_list(outer_range, range_list, receiver,
|
||||
handle_to_stop, doc_func, else_func,
|
||||
react_data);
|
||||
});
|
||||
|
||||
var renderItem = function(doc, in_range) {
|
||||
return Meteor.ui.render(
|
||||
_.bind(doc_func, null, doc),
|
||||
_.extend({}, react_data, {event_data: doc}),
|
||||
in_range);
|
||||
};
|
||||
|
||||
var renderElse = function() {
|
||||
return Meteor.ui.render(else_func, react_data);
|
||||
};
|
||||
|
||||
var callbacks = {
|
||||
added: function(doc, before_idx) {
|
||||
var frag = renderItem(doc);
|
||||
var range = new Meteor.ui._LiveRange(Meteor.ui._tag, frag);
|
||||
if (range_list.length === 0)
|
||||
cleanup_frag(outer_range.replace_contents(frag));
|
||||
else if (before_idx === range_list.length)
|
||||
range_list[range_list.length-1].insert_after(frag);
|
||||
else
|
||||
range_list[before_idx].insert_before(frag);
|
||||
|
||||
attach_secondary_events(range);
|
||||
|
||||
range_list.splice(before_idx, 0, range);
|
||||
},
|
||||
removed: function(doc, at_idx) {
|
||||
if (range_list.length === 1) {
|
||||
cleanup_frag(
|
||||
outer_range.replace_contents(renderElse()));
|
||||
attach_secondary_events(outer_range);
|
||||
} else {
|
||||
cleanup_frag(range_list[at_idx].extract());
|
||||
}
|
||||
|
||||
range_list.splice(at_idx, 1);
|
||||
},
|
||||
moved: function(doc, old_idx, new_idx) {
|
||||
if (old_idx === new_idx)
|
||||
return;
|
||||
|
||||
var range = range_list[old_idx];
|
||||
// We know the list has at least two items,
|
||||
// at old_idx and new_idx, so `extract` will succeed.
|
||||
var frag = range.extract(true);
|
||||
range_list.splice(old_idx, 1);
|
||||
|
||||
if (new_idx === range_list.length)
|
||||
range_list[range_list.length-1].insert_after(frag);
|
||||
else
|
||||
range_list[new_idx].insert_before(frag);
|
||||
range_list.splice(new_idx, 0, range);
|
||||
},
|
||||
changed: function(doc, at_idx) {
|
||||
var range = range_list[at_idx];
|
||||
|
||||
// replace the render in the immediately nested range
|
||||
range.visit(function(is_start, r) {
|
||||
if (is_start)
|
||||
renderItem(doc, r);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Meteor.ui._ranged_html = function(html, callback) {
|
||||
if (! Meteor.ui._render_mode)
|
||||
return html;
|
||||
|
||||
var callbacks = Meteor.ui._render_mode.callbacks;
|
||||
|
||||
var commentId = ++callbacks._count;
|
||||
callbacks[commentId] = callback;
|
||||
return "<!-- STARTRANGE_"+commentId+" -->" + html +
|
||||
"<!-- ENDRANGE_"+commentId+" -->";
|
||||
};
|
||||
|
||||
var cleanup_frag = function(frag) {
|
||||
// wrap the frag in a new LiveRange that will be destroyed
|
||||
cleanup_range(new Meteor.ui._LiveRange(Meteor.ui._tag, frag));
|
||||
};
|
||||
|
||||
// Cleans up a range and its descendant ranges by calling
|
||||
// killContext on them (which removes any associated context
|
||||
// from dependency tracking) and then destroy (which removes
|
||||
// the liverange data from the DOM).
|
||||
var cleanup_range = function(range) {
|
||||
range.visit(function(is_start, range) {
|
||||
if (is_start)
|
||||
killContext(range);
|
||||
});
|
||||
range.destroy(true);
|
||||
};
|
||||
|
||||
// Attach events specified by `range` to top-level nodes in `range`.
|
||||
// The nodes may still be in a DocumentFragment.
|
||||
var attach_events = function(range) {
|
||||
if (! range.event_handlers)
|
||||
return;
|
||||
|
||||
_.each(range.event_handlers.types, function(t) {
|
||||
for(var n = range.firstNode(), after = range.lastNode().nextSibling;
|
||||
n && n !== after;
|
||||
n = n.nextSibling)
|
||||
Meteor.ui._event.registerEventType(t, n);
|
||||
});
|
||||
};
|
||||
|
||||
// Attach events specified by enclosing ranges of `range`, at the
|
||||
// same DOM level, to nodes in `range`. This is necessary if
|
||||
// `range` has just been inserted (as in the case of list 'added'
|
||||
// events) or if it has been re-rendered but its enclosing ranges
|
||||
// haven't. In either case, the nodes in `range` have been rendered
|
||||
// without taking enclosing ranges into account, so additional event
|
||||
// handlers need to be attached.
|
||||
var attach_secondary_events = function(range) {
|
||||
// Implementations of LiveEvents that use whole-document event capture
|
||||
// (all except old IE) don't actually need any of this; this function
|
||||
// could be a no-op.
|
||||
for(var r = range; r; r = r.findParent()) {
|
||||
if (r === range)
|
||||
continue;
|
||||
if (! r.event_handlers)
|
||||
continue;
|
||||
|
||||
var eventTypes = r.event_handlers.types;
|
||||
_.each(eventTypes, function(t) {
|
||||
for(var n = range.firstNode(), after = range.lastNode().nextSibling;
|
||||
n && n !== after;
|
||||
n = n.nextSibling)
|
||||
Meteor.ui._event.registerEventType(t, n);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 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.
|
||||
Meteor.ui._handleEvent = function(event) {
|
||||
var curNode = event.currentTarget;
|
||||
if (! curNode)
|
||||
return;
|
||||
|
||||
var innerRange = Meteor.ui._LiveRange.findRange(Meteor.ui._tag, curNode);
|
||||
if (! innerRange)
|
||||
return;
|
||||
|
||||
var type = event.type;
|
||||
|
||||
for(var range = innerRange; range; range = range.findParent()) {
|
||||
var event_handlers = range.event_handlers;
|
||||
if (! event_handlers)
|
||||
continue;
|
||||
|
||||
for(var i=0, N=event_handlers.length; i<N; i++) {
|
||||
var h = event_handlers[i];
|
||||
|
||||
if (h.type !== type)
|
||||
continue;
|
||||
|
||||
var selector = h.selector;
|
||||
if (selector) {
|
||||
var contextNode = range.containerNode();
|
||||
var results = $(contextNode).find(selector);
|
||||
if (! _.contains(results, curNode))
|
||||
continue;
|
||||
} else {
|
||||
// if no selector, only match the event target
|
||||
if (curNode !== event.target)
|
||||
continue;
|
||||
}
|
||||
|
||||
var event_data = findEventData(event.currentTarget);
|
||||
|
||||
// Call the app's handler/callback
|
||||
var returnValue = h.callback.call(event_data, event);
|
||||
|
||||
// allow app to `return false` from event handler, just like
|
||||
// you can in a jquery event handler
|
||||
if (returnValue === false) {
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
if (event.isImmediatePropagationStopped())
|
||||
return; // stop handling by this and other event maps
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// find the innermost enclosing liverange that has event_data
|
||||
var findEventData = function(node) {
|
||||
var innerRange = Meteor.ui._LiveRange.findRange(Meteor.ui._tag, node);
|
||||
|
||||
for(var range = innerRange; range; range = range.findParent())
|
||||
if (range.event_data)
|
||||
return range.event_data;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
Meteor.ui._event.setHandler(Meteor.ui._handleEvent);
|
||||
})();
|
||||
@@ -1,13 +0,0 @@
|
||||
<template name="test_renderList_each">
|
||||
Before{{render_count}}
|
||||
{{#each data}}
|
||||
{{name}}{{weather "here"}}
|
||||
{{/each}}
|
||||
Middle
|
||||
{{#each data2}}
|
||||
{{name}}
|
||||
{{else}}
|
||||
Else
|
||||
{{/each}}
|
||||
After
|
||||
</template>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
||||
Package.describe({
|
||||
summary: "Meteor's machinery for making arbitrary templates reactive",
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('livedata');
|
||||
api.use(['underscore', 'session'], 'client');
|
||||
|
||||
// XXX Depends on jquery because we need a selector engine to resolve
|
||||
// event maps. What would be nice is, if you've included jquery or
|
||||
// zepto, use one of those; if not, ship our own copy of sizzle (but,
|
||||
// you still want the event object normalization that jquery provides?)
|
||||
api.use('jquery');
|
||||
|
||||
api.add_files(['liveevents_w3c.js', 'liveevents_now3c.js'], 'client');
|
||||
api.add_files(['liveevents.js'], 'client');
|
||||
api.add_files(['liverange.js', 'liveui.js', 'innerhtml.js', 'smartpatch.js'],
|
||||
'client');
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use(['tinytest', 'templating', 'htmljs']);
|
||||
api.use(['liveui', 'test-helpers'], 'client');
|
||||
|
||||
api.add_files('form_responder.js', 'server');
|
||||
|
||||
api.add_files([
|
||||
'liverange_test_helpers.js',
|
||||
'liveui_tests.js',
|
||||
'liveui_tests.html',
|
||||
'liverange_tests.js',
|
||||
'smartpatch_tests.js',
|
||||
'liveevents_tests.js'
|
||||
], 'client');
|
||||
});
|
||||
@@ -26,7 +26,7 @@
|
||||
return shortname;
|
||||
};
|
||||
|
||||
Template.madewith.events = {
|
||||
Template.madewith.events({
|
||||
'click .madewith_upvote': function(event) {
|
||||
var app = apps.findOne();
|
||||
if (app) {
|
||||
@@ -37,5 +37,5 @@
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
})();
|
||||
@@ -3,7 +3,7 @@ Package.describe({
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use(['livedata', 'underscore', 'liveui', 'templating'], 'client');
|
||||
api.use(['livedata', 'underscore', 'spark', 'templating'], 'client');
|
||||
|
||||
api.add_files([
|
||||
'madewith.css',
|
||||
|
||||
@@ -372,6 +372,8 @@ LocalCollection._deepcopy = function (v) {
|
||||
return v;
|
||||
if (v === null)
|
||||
return null; // null has typeof "object"
|
||||
if (v instanceof Date)
|
||||
return new Date(v.getTime());
|
||||
if (_.isArray(v)) {
|
||||
var ret = v.slice(0);
|
||||
for (var i = 0; i < v.length; i++)
|
||||
|
||||
@@ -199,7 +199,7 @@ Tinytest.add("minimongo - cursors", function (test) {
|
||||
Tinytest.add("minimongo - misc", function (test) {
|
||||
// deepcopy
|
||||
var a = {a: [1, 2, 3], b: "x", c: true, d: {x: 12, y: [12]},
|
||||
f: null};
|
||||
f: null, g: new Date()};
|
||||
var b = LocalCollection._deepcopy(a);
|
||||
test.isTrue(LocalCollection._f._equal(a, b));
|
||||
a.a.push(4);
|
||||
@@ -211,7 +211,10 @@ Tinytest.add("minimongo - misc", function (test) {
|
||||
test.equal(b.d.z, 15);
|
||||
a.d.y.push(88);
|
||||
test.length(b.d.y, 1);
|
||||
|
||||
test.equal(a.g, b.g)
|
||||
b.g.setDate(b.g.getDate() + 1);
|
||||
test.notEqual(a.g, b.g)
|
||||
|
||||
a = {x: function () {}};
|
||||
b = LocalCollection._deepcopy(a);
|
||||
a.x.a = 14;
|
||||
@@ -532,6 +535,14 @@ Tinytest.add("minimongo - selector_compiler", function (test) {
|
||||
match({"a.b": /a/}, {a: {b: "cat"}});
|
||||
nomatch({"a.b": /a/}, {a: {b: "dog"}});
|
||||
|
||||
// trying to access a dotted field that is undefined at some point
|
||||
// down the chain
|
||||
nomatch({"a.b": 1}, {x: 2});
|
||||
nomatch({"a.b.c": 1}, {a: {x: 2}});
|
||||
nomatch({"a.b.c": 1}, {a: {b: {x: 2}}});
|
||||
nomatch({"a.b.c": 1}, {a: {b: 1}});
|
||||
nomatch({"a.b.c": 1}, {a: {b: 0}});
|
||||
|
||||
// dotted keypaths: literal objects
|
||||
match({"a.b": {c: 1}}, {a: {b: {c: 1}}});
|
||||
nomatch({"a.b": {c: 1}}, {a: {b: {c: 2}}});
|
||||
|
||||
@@ -384,20 +384,22 @@ LocalCollection._exprForKeypathPredicate = function (keypath, value, literals) {
|
||||
}
|
||||
|
||||
// now, deal with the orthogonal concern of dotted.key.paths and the
|
||||
// (potentially multi-level) array searching they require
|
||||
// (potentially multi-level) array searching they require.
|
||||
// while at it, make sure to not throw an exception if we hit undefined while
|
||||
// drilling down through the dotted parts
|
||||
var ret = '';
|
||||
var innermost = true;
|
||||
while (keyparts.length) {
|
||||
var part = keyparts.pop();
|
||||
var formal = keyparts.length ? "x" : "doc";
|
||||
if (innermost) {
|
||||
ret = '(function(x){return ' + predcode + ';})(' + formal + '[' +
|
||||
ret = '(function(x){return ' + predcode + ';})(' + formal + '&&' + formal + '[' +
|
||||
JSON.stringify(part) + '])';
|
||||
innermost = false;
|
||||
} else {
|
||||
// for all but the innermost level of a dotted expression,
|
||||
// if the runtime type is an array, search it
|
||||
ret = 'f._matches(' + formal + '[' + JSON.stringify(part) +
|
||||
ret = 'f._matches(' + formal + '&&' + formal + '[' + JSON.stringify(part) +
|
||||
'], function(x){return ' + ret + ';})';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
Tinytest.add("sass - presence", function(test) {
|
||||
|
||||
var d = OnscreenDiv(Meteor.ui.render(function() {
|
||||
var d = OnscreenDiv(Meteor.render(function() {
|
||||
return '<p class="sass-unlucky-left-border"></p>'; }));
|
||||
d.node().style.display = 'block';
|
||||
|
||||
|
||||
21
packages/spark/convenience.js
Normal file
21
packages/spark/convenience.js
Normal file
@@ -0,0 +1,21 @@
|
||||
Meteor.render = function (htmlFunc) {
|
||||
return Spark.render(function () {
|
||||
return Spark.isolate(
|
||||
typeof htmlFunc === 'function' ? htmlFunc : function() {
|
||||
// non-function argument becomes a constant (non-reactive) string
|
||||
return String(htmlFunc);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Meteor.renderList = function (cursor, itemFunc, elseFunc) {
|
||||
return Spark.render(function () {
|
||||
return Spark.list(cursor, function (item) {
|
||||
return Spark.labelBranch(item._id || null, function () {
|
||||
return Spark.isolate(_.bind(itemFunc, null, item));
|
||||
});
|
||||
}, function () {
|
||||
return elseFunc ? Spark.isolate(elseFunc) : '';
|
||||
});
|
||||
});
|
||||
};
|
||||
29
packages/spark/package.js
Normal file
29
packages/spark/package.js
Normal file
@@ -0,0 +1,29 @@
|
||||
Package.describe({
|
||||
summary: "Toolkit for live-updating HTML interfaces",
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use(['underscore', 'uuid', 'domutils', 'liverange', 'universal-events'],
|
||||
'client');
|
||||
|
||||
// XXX Depends on jquery because we need a selector engine to resolve
|
||||
// event maps. What would be nice is, if you've included jquery or
|
||||
// zepto, use one of those; if not, ship our own copy of sizzle (but,
|
||||
// you still want the event object normalization that jquery provides?)
|
||||
api.use('jquery');
|
||||
|
||||
api.add_files(['spark.js', 'patch.js', 'convenience.js'], 'client');
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use('tinytest');
|
||||
api.use(['spark', 'test-helpers'], 'client');
|
||||
|
||||
api.add_files('test_form_responder.js', 'server');
|
||||
|
||||
api.add_files([
|
||||
'spark_tests.js',
|
||||
'patch_tests.js'
|
||||
], 'client');
|
||||
});
|
||||
@@ -1,4 +1,88 @@
|
||||
Meteor.ui = Meteor.ui || {};
|
||||
|
||||
Spark._patch = function(tgtParent, srcParent, tgtBefore, tgtAfter, preservations,
|
||||
results) {
|
||||
|
||||
var copyFunc = function(t, s) {
|
||||
LiveRange.transplantTag(Spark._TAG, t, s);
|
||||
};
|
||||
|
||||
var patcher = new Spark._Patcher(
|
||||
tgtParent, srcParent, tgtBefore, tgtAfter);
|
||||
|
||||
|
||||
var visitNodes = function(parent, before, after, func) {
|
||||
for(var n = before ? before.nextSibling : parent.firstChild;
|
||||
n && n !== after;
|
||||
n = n.nextSibling) {
|
||||
if (func(n) !== false && n.firstChild)
|
||||
visitNodes(n, null, null, func);
|
||||
}
|
||||
};
|
||||
|
||||
// results arg is optional; it is mutated if provided; returned either way
|
||||
results = (results || {});
|
||||
// array of LiveRanges that were successfully preserved from
|
||||
// the region preservations
|
||||
var regionPreservations = (results.regionPreservations =
|
||||
results.regionPreservations || []);
|
||||
|
||||
var lastTgtMatch = null;
|
||||
|
||||
visitNodes(srcParent, null, null, function(src) {
|
||||
// XXX inefficient to scan for match for every node!
|
||||
// We could at least skip non-element nodes, except for "range matches"
|
||||
// used for constant chunks, which may begin on a non-element.
|
||||
// But really this shouldn't be a linear search.
|
||||
var pres = _.find(preservations, function (p) {
|
||||
// find preserved region starting at `src`, if any
|
||||
return p.type === 'region' && p.newRange.firstNode() === src;
|
||||
}) || _.find(preservations, function (p) {
|
||||
// else, find preservation of `src`
|
||||
return p.type === 'node' && p.to === src;
|
||||
});
|
||||
|
||||
if (pres) {
|
||||
var tgt = (pres.type === 'region' ? pres.fromStart : pres.from);
|
||||
if (! lastTgtMatch ||
|
||||
DomUtils.elementOrder(lastTgtMatch, tgt) > 0) {
|
||||
if (pres.type === 'region') {
|
||||
// preserved region for constant landmark
|
||||
if (patcher.match(pres.fromStart, pres.newRange.firstNode(),
|
||||
copyFunc, true)) {
|
||||
patcher.skipToSiblings(pres.fromEnd, pres.newRange.lastNode());
|
||||
// without knowing or caring what DOM nodes are in pres.newRange,
|
||||
// transplant the range data to pres.fromStart and pres.fromEnd
|
||||
// (including references to enclosing ranges).
|
||||
LiveRange.transplantRange(
|
||||
pres.fromStart, pres.fromEnd, pres.newRange);
|
||||
regionPreservations.push(pres.newRange);
|
||||
}
|
||||
} else if (pres.type === 'node') {
|
||||
if (patcher.match(tgt, src, copyFunc)) {
|
||||
// match succeeded
|
||||
lastTgtMatch = tgt;
|
||||
if (tgt.firstChild || src.firstChild) {
|
||||
// Don't patch contents of TEXTAREA tag,
|
||||
// which are only the initial contents but
|
||||
// may affect the tag's .value in IE.
|
||||
if (tgt.nodeName !== "TEXTAREA") {
|
||||
// recurse!
|
||||
Spark._patch(tgt, src, null, null, preservations);
|
||||
}
|
||||
}
|
||||
return false; // tell visitNodes not to recurse
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
patcher.finish();
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
|
||||
// A Patcher manages the controlled replacement of a region of the DOM.
|
||||
// The target region is changed in place to match the source region.
|
||||
@@ -10,20 +94,18 @@ Meteor.ui = Meteor.ui || {};
|
||||
// of srcParent, which may be a DocumentFragment.
|
||||
//
|
||||
// To use a new Patcher, call `match` zero or more times followed by
|
||||
// `finish`. Alternatively, just call `diffpatch` to use the standard
|
||||
// matching strategy.
|
||||
// `finish`.
|
||||
//
|
||||
// A match is a correspondence between an old node in the target region
|
||||
// and a new node in the source region that will replace it. Based on
|
||||
// this correspondence, the target node is preserved and the attributes
|
||||
// and children of the source node are copied over it. The `match`
|
||||
// method declares such a correspondence. The `diffpatch` method
|
||||
// determines the correspondences and calls `match` and `finish`.
|
||||
// A Patcher that makes no matches just removes the target nodes
|
||||
// and inserts the source nodes in their place.
|
||||
// method declares such a correspondence. A Patcher that makes no matches,
|
||||
// for example, just removes the target nodes and inserts the source nodes
|
||||
// in their place.
|
||||
//
|
||||
// Constructor:
|
||||
Meteor.ui._Patcher = function(tgtParent, srcParent, tgtBefore, tgtAfter) {
|
||||
Spark._Patcher = function(tgtParent, srcParent, tgtBefore, tgtAfter) {
|
||||
this.tgtParent = tgtParent;
|
||||
this.srcParent = srcParent;
|
||||
|
||||
@@ -34,94 +116,6 @@ Meteor.ui._Patcher = function(tgtParent, srcParent, tgtBefore, tgtAfter) {
|
||||
this.lastKeptSrcNode = null;
|
||||
};
|
||||
|
||||
// Perform a complete patching where nodes with the same `id` or `name`
|
||||
// are matched.
|
||||
//
|
||||
// Any node that has either an "id" or "name" attribute is considered a
|
||||
// "labeled" node, and these labeled nodes are candidates for preservation.
|
||||
// For two labeled nodes, old and new, to match, they first must have the same
|
||||
// label (that is, they have the same id, or neither has an id and they have
|
||||
// the same name). Labels are considered relative to the nearest enclosing
|
||||
// labeled ancestor, and must be unique among the labeled nodes that share
|
||||
// this nearest labeled ancestor. Labeled nodes are also expected to stay
|
||||
// in the same order, or else some of them won't be matched.
|
||||
|
||||
Meteor.ui._Patcher.prototype.diffpatch = function(copyCallback) {
|
||||
var self = this;
|
||||
|
||||
var each_labeled_node = function(parent, before, after, func) {
|
||||
for(var n = before ? before.nextSibling : parent.firstChild;
|
||||
n && n !== after;
|
||||
n = n.nextSibling) {
|
||||
|
||||
var label = null;
|
||||
|
||||
if (n.nodeType === 1) {
|
||||
if (n.id) {
|
||||
label = '#'+n.id;
|
||||
} else if (n.getAttribute("name")) {
|
||||
label = n.getAttribute("name");
|
||||
// Radio button special case: radio buttons
|
||||
// in a group all have the same name. Their value
|
||||
// determines their identity.
|
||||
// Checkboxes with the same name and different
|
||||
// values are also sometimes used in apps, so
|
||||
// we treat them similarly.
|
||||
if (n.nodeName === 'INPUT' &&
|
||||
(n.type === 'radio' || n.type === 'checkbox') &&
|
||||
n.value)
|
||||
label = label + ':' + n.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (label)
|
||||
func(label, n);
|
||||
else
|
||||
// not a labeled node; recurse
|
||||
each_labeled_node(n, null, null, func);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var targetNodes = {};
|
||||
var targetNodeOrder = {};
|
||||
var targetNodeCounter = 0;
|
||||
|
||||
each_labeled_node(
|
||||
self.tgtParent, self.tgtBefore, self.tgtAfter,
|
||||
function(label, node) {
|
||||
targetNodes[label] = node;
|
||||
targetNodeOrder[label] = targetNodeCounter++;
|
||||
});
|
||||
|
||||
var lastPos = -1;
|
||||
each_labeled_node(
|
||||
self.srcParent, null, null,
|
||||
function(label, node) {
|
||||
var tgt = targetNodes[label];
|
||||
var src = node;
|
||||
if (tgt && targetNodeOrder[label] > lastPos) {
|
||||
if (self.match(tgt, src, copyCallback)) {
|
||||
// match succeeded
|
||||
if (tgt.firstChild || src.firstChild) {
|
||||
// Don't patch contents of TEXTAREA tag,
|
||||
// which are only the initial contents but
|
||||
// may affect the tag's .value in IE.
|
||||
if (tgt.nodeName !== "TEXTAREA") {
|
||||
// recurse with a new Patcher!
|
||||
var patcher = new Meteor.ui._Patcher(tgt, src);
|
||||
patcher.diffpatch(copyCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
lastPos = targetNodeOrder[label];
|
||||
}
|
||||
});
|
||||
|
||||
self.finish();
|
||||
|
||||
};
|
||||
|
||||
|
||||
// Advances the patching process up to tgtNode in the target tree,
|
||||
// and srcNode in the source tree. tgtNode will be preserved, with
|
||||
@@ -167,7 +161,8 @@ Meteor.ui._Patcher.prototype.diffpatch = function(copyCallback) {
|
||||
// copyCallback is called on every new matched (tgt, src) pair
|
||||
// right after copying attributes. It's a good time to transplant
|
||||
// liveranges and patch children.
|
||||
Meteor.ui._Patcher.prototype.match = function(tgtNode, srcNode, copyCallback) {
|
||||
Spark._Patcher.prototype.match = function(
|
||||
tgtNode, srcNode, copyCallback, onlyAdvance) {
|
||||
|
||||
// last nodes "kept" (matched/identified with each other)
|
||||
var lastKeptTgt = this.lastKeptTgtNode;
|
||||
@@ -182,7 +177,6 @@ Meteor.ui._Patcher.prototype.match = function(tgtNode, srcNode, copyCallback) {
|
||||
|
||||
var starting = ! lastKeptTgt;
|
||||
var finishing = ! tgt;
|
||||
var elementContains = Meteor.ui._Patcher._elementContains;
|
||||
|
||||
if (! starting) {
|
||||
// move lastKeptTgt/lastKeptSrc forward and out,
|
||||
@@ -190,7 +184,7 @@ Meteor.ui._Patcher.prototype.match = function(tgtNode, srcNode, copyCallback) {
|
||||
// replacing as we go. If tgt/src is falsy, we make it to the
|
||||
// top level.
|
||||
while (lastKeptTgt.parentNode !== this.tgtParent &&
|
||||
! (tgt && elementContains(lastKeptTgt.parentNode, tgt))) {
|
||||
! (tgt && DomUtils.elementContains(lastKeptTgt.parentNode, tgt))) {
|
||||
// Last-kept nodes are inside parents that are not
|
||||
// parents of the newly matched nodes. Must finish
|
||||
// replacing their contents and back out.
|
||||
@@ -216,9 +210,9 @@ Meteor.ui._Patcher.prototype.match = function(tgtNode, srcNode, copyCallback) {
|
||||
// ancestor of <c/> on the left rather than a sibling of an
|
||||
// ancestor.
|
||||
if (! finishing &&
|
||||
(elementContains(lastKeptSrc, src) ||
|
||||
(DomUtils.elementContains(lastKeptSrc, src) ||
|
||||
! (lastKeptSrc.parentNode === this.srcParent ||
|
||||
elementContains(lastKeptSrc.parentNode, src)))) {
|
||||
DomUtils.elementContains(lastKeptSrc.parentNode, src)))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -227,28 +221,36 @@ Meteor.ui._Patcher.prototype.match = function(tgtNode, srcNode, copyCallback) {
|
||||
this._replaceNodes(lastKeptTgt, null, lastKeptSrc, null,
|
||||
this.tgtParent, this.srcParent);
|
||||
} else {
|
||||
// Compare tag names and depths to make sure we can match nodes.
|
||||
// Compare tag names and depths to make sure we can match nodes...
|
||||
if (! onlyAdvance) {
|
||||
if (tgt.nodeName !== src.nodeName)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Look at tags of parents until we hit parent of last-kept,
|
||||
// which we know is ok.
|
||||
for(var a=tgt, b=src;
|
||||
for(var a=tgt.parentNode, b=src.parentNode;
|
||||
a !== (starting ? this.tgtParent : lastKeptTgt.parentNode);
|
||||
a = a.parentNode, b = b.parentNode) {
|
||||
if (b === (starting ? this.srcParent : lastKeptSrc.parentNode)) {
|
||||
if (b === (starting ? this.srcParent : lastKeptSrc.parentNode))
|
||||
return false; // src is shallower, b hit top first
|
||||
}
|
||||
if (a.nodeName !== b.nodeName) {
|
||||
if (a.nodeName !== b.nodeName)
|
||||
return false; // tag names don't match
|
||||
}
|
||||
}
|
||||
if (b !== (starting ? this.srcParent : lastKeptSrc.parentNode)) {
|
||||
return false; // src is deeper, b didn't hit top when a did
|
||||
}
|
||||
|
||||
var firstIter = true;
|
||||
// move tgt and src backwards and out, replacing as we go
|
||||
while (true) {
|
||||
Meteor.ui._Patcher._copyAttributes(tgt, src);
|
||||
if (copyCallback)
|
||||
copyCallback(tgt, src);
|
||||
if (! (firstIter && onlyAdvance)) {
|
||||
Spark._Patcher._copyAttributes(tgt, src);
|
||||
if (copyCallback)
|
||||
copyCallback(tgt, src);
|
||||
}
|
||||
|
||||
firstIter = false;
|
||||
|
||||
if ((starting ? this.tgtParent : lastKeptTgt.parentNode)
|
||||
=== tgt.parentNode) {
|
||||
@@ -270,11 +272,29 @@ Meteor.ui._Patcher.prototype.match = function(tgtNode, srcNode, copyCallback) {
|
||||
return true;
|
||||
};
|
||||
|
||||
// After a match, skip ahead to later siblings of the last kept nodes,
|
||||
// without performing any replacements.
|
||||
Spark._Patcher.prototype.skipToSiblings = function(tgt, src) {
|
||||
var lastTgt = this.lastKeptTgtNode;
|
||||
var lastSrc = this.lastKeptSrcNode;
|
||||
|
||||
if (! (lastTgt && lastTgt.parentNode === tgt.parentNode))
|
||||
return false;
|
||||
|
||||
if (! (lastSrc && lastSrc.parentNode === src.parentNode))
|
||||
return false;
|
||||
|
||||
this.lastKeptTgtNode = tgt;
|
||||
this.lastKeptSrcNode = src;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Completes patching assuming no more matches.
|
||||
//
|
||||
// Patchers are single-use, so no more methods can be called
|
||||
// on the Patcher.
|
||||
Meteor.ui._Patcher.prototype.finish = function() {
|
||||
Spark._Patcher.prototype.finish = function() {
|
||||
return this.match(null, null);
|
||||
};
|
||||
|
||||
@@ -284,7 +304,7 @@ Meteor.ui._Patcher.prototype.finish = function() {
|
||||
//
|
||||
// Precondition: tgtBefore and tgtAfter have same parent; either may be falsy,
|
||||
// but not both, unless optTgtParent is provided. Same with srcBefore/srcAfter.
|
||||
Meteor.ui._Patcher.prototype._replaceNodes = function(
|
||||
Spark._Patcher.prototype._replaceNodes = function(
|
||||
tgtBefore, tgtAfter, srcBefore, srcAfter, optTgtParent, optSrcParent)
|
||||
{
|
||||
var tgtParent = optTgtParent || (tgtBefore || tgtAfter).parentNode;
|
||||
@@ -326,7 +346,7 @@ Meteor.ui._Patcher.prototype._replaceNodes = function(
|
||||
//
|
||||
// This is complicated by form controls and the fact that old IE
|
||||
// can't keep the difference straight between properties and attributes.
|
||||
Meteor.ui._Patcher._copyAttributes = function(tgt, src) {
|
||||
Spark._Patcher._copyAttributes = function(tgt, src) {
|
||||
var srcAttrs = src.attributes;
|
||||
var tgtAttrs = tgt.attributes;
|
||||
|
||||
@@ -350,9 +370,12 @@ Meteor.ui._Patcher._copyAttributes = function(tgt, src) {
|
||||
// Record for later whether this is a radio button.
|
||||
isRadio = (tgt.type === 'radio');
|
||||
// Clearing the attributes of a checkbox won't necessarily
|
||||
// uncheck it, eg in FF12, so we uncheck explicitly.
|
||||
if (typeof tgt.checked === "boolean")
|
||||
// uncheck it, eg in FF12, so we uncheck explicitly
|
||||
// (if necessary; we don't want to generate spurious
|
||||
// propertychange events in old IE).
|
||||
if (tgt.checked === true && src.checked === false) {
|
||||
tgt.checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
for(var i=tgtAttrs.length-1; i>=0; i--) {
|
||||
@@ -422,8 +445,7 @@ Meteor.ui._Patcher._copyAttributes = function(tgt, src) {
|
||||
|
||||
tgt.mergeAttributes(src);
|
||||
|
||||
if (typeof tgt.checked !== "undefined" ||
|
||||
typeof src.checked !== "undefined")
|
||||
if (typeof tgt.checked !== "undefined" && src.checked)
|
||||
tgt.checked = src.checked;
|
||||
|
||||
if (src.name)
|
||||
@@ -467,17 +489,3 @@ Meteor.ui._Patcher._copyAttributes = function(tgt, src) {
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// returns true if element a properly contains element b
|
||||
Meteor.ui._Patcher._elementContains = function(a, b) {
|
||||
if (a.nodeType !== 1 || b.nodeType !== 1) {
|
||||
return false;
|
||||
}
|
||||
if (a.compareDocumentPosition) {
|
||||
return a.compareDocumentPosition(b) & 0x10;
|
||||
} else {
|
||||
// Should be only old IE and maybe other old browsers here.
|
||||
// Modern Safari has both methods but seems to get contains() wrong.
|
||||
return a !== b && a.contains(b);
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
Tinytest.add("smartpatch - basic", function(test) {
|
||||
Tinytest.add("spark - patch - basic", function(test) {
|
||||
|
||||
var Patcher = Meteor.ui._Patcher;
|
||||
var Patcher = Spark._Patcher;
|
||||
|
||||
var div = function(html) {
|
||||
var n = document.createElement("DIV");
|
||||
@@ -91,7 +91,6 @@ Tinytest.add("smartpatch - basic", function(test) {
|
||||
test.isTrue(ret);
|
||||
assert_html(x, "<i><u>bar</u><s>baz</s></i>");
|
||||
|
||||
var LiveRange = Meteor.ui._LiveRange;
|
||||
var t = "_foo";
|
||||
var liverange = function(start, end, inner) {
|
||||
return new LiveRange(t, start, end, inner);
|
||||
@@ -107,7 +106,7 @@ Tinytest.add("smartpatch - basic", function(test) {
|
||||
tgt.firstNode().previousSibling,
|
||||
tgt.lastNode().nextSibling);
|
||||
var copyCallback = function(tgt, src) {
|
||||
LiveRange.transplant_tag(t, tgt, src);
|
||||
LiveRange.transplantTag(t, tgt, src);
|
||||
};
|
||||
ret = p.match(tag(x, 'u'), tag(y, 'u'), copyCallback);
|
||||
test.isTrue(ret);
|
||||
@@ -121,7 +120,7 @@ Tinytest.add("smartpatch - basic", function(test) {
|
||||
_.each([["aaa","zzz"], ["",""], ["aaa",""], ["","zzz"]], rangeTest);
|
||||
});
|
||||
|
||||
Tinytest.add("smartpatch - copyAttributes", function(test) {
|
||||
Tinytest.add("spark - patch - copyAttributes", function(test) {
|
||||
|
||||
var attrTester = function(tagName, initial) {
|
||||
var node;
|
||||
@@ -137,12 +136,12 @@ Tinytest.add("smartpatch - copyAttributes", function(test) {
|
||||
});
|
||||
buf.push('></', tagName, '>');
|
||||
var nodeHtml = buf.join('');
|
||||
var frag = Meteor.ui._htmlToFragment(nodeHtml);
|
||||
var frag = DomUtils.htmlToFragment(nodeHtml);
|
||||
var n = frag.firstChild;
|
||||
if (! node) {
|
||||
node = n;
|
||||
} else {
|
||||
Meteor.ui._Patcher._copyAttributes(node, n);
|
||||
Spark._Patcher._copyAttributes(node, n);
|
||||
}
|
||||
lastAttrs = {};
|
||||
_.each(allAttrNames, function(v,k) {
|
||||
@@ -239,4 +238,3 @@ Tinytest.add("smartpatch - copyAttributes", function(test) {
|
||||
|
||||
|
||||
});
|
||||
|
||||
1122
packages/spark/spark.js
Normal file
1122
packages/spark/spark.js
Normal file
File diff suppressed because it is too large
Load Diff
3623
packages/spark/spark_tests.js
Normal file
3623
packages/spark/spark_tests.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
(function () {
|
||||
|
||||
var TEST_RESPONDER_ROUTE = "/liveui_test_responder";
|
||||
var TEST_RESPONDER_ROUTE = "/spark_test_responder";
|
||||
|
||||
var respond = function(req, res) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
Tinytest.add("stylus - presence", function(test) {
|
||||
|
||||
var d = OnscreenDiv(Meteor.ui.render(function() {
|
||||
var d = OnscreenDiv(Meteor.render(function() {
|
||||
return '<p class="stylus-unlucky-left-border"></p>'; }));
|
||||
d.node().style.display = 'block';
|
||||
|
||||
|
||||
@@ -2,38 +2,174 @@
|
||||
|
||||
Meteor._partials = {};
|
||||
|
||||
Meteor._hook_handlebars_each = function () {
|
||||
Meteor._hook_handlebars_each = function(){}; // install the hook only once
|
||||
// XXX Handlebars hooking is janky and gross
|
||||
|
||||
Meteor._hook_handlebars = function () {
|
||||
Meteor._hook_handlebars = function(){}; // install the hook only once
|
||||
|
||||
var orig = Handlebars._default_helpers.each;
|
||||
Handlebars._default_helpers.each = function (arg, options) {
|
||||
if (!(arg instanceof LocalCollection.Cursor))
|
||||
// if arg isn't an observable (like LocalCollection.Cursor),
|
||||
// don't use this reactive implementation of #each.
|
||||
if (!(arg && 'observe' in arg))
|
||||
return orig.call(this, arg, options);
|
||||
|
||||
return Meteor.ui.listChunk(arg, options.fn, options.inverse, null);
|
||||
return Spark.list(
|
||||
arg,
|
||||
function (item) {
|
||||
return Spark.labelBranch(item._id || null, function () {
|
||||
var html = Spark.isolate(_.bind(options.fn, null, item));
|
||||
return Spark.setDataContext(item, html);
|
||||
});
|
||||
},
|
||||
function () {
|
||||
return options.inverse ?
|
||||
Spark.isolate(options.inverse) : '';
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
_.extend(Handlebars._default_helpers, {
|
||||
isolate: function (options) {
|
||||
var data = this;
|
||||
return Spark.isolate(function () {
|
||||
return options.fn(data);
|
||||
});
|
||||
},
|
||||
constant: function (options) {
|
||||
var data = this;
|
||||
return Spark.createLandmark({ constant: true }, function () {
|
||||
return options.fn(data);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// map from landmark id, to the 'this' object for
|
||||
// created/rendered/destroyed callbacks on templates
|
||||
var templateInstanceData = {};
|
||||
|
||||
var templateObjFromLandmark = function (landmark) {
|
||||
var template = templateInstanceData[landmark.id] || (
|
||||
templateInstanceData[landmark.id] = {
|
||||
// set these once
|
||||
find: function (selector) {
|
||||
if (! landmark.hasDom())
|
||||
throw new Error("Template not in DOM");
|
||||
return landmark.find(selector);
|
||||
},
|
||||
findAll: function (selector) {
|
||||
if (! landmark.hasDom())
|
||||
throw new Error("Template not in DOM");
|
||||
return landmark.findAll(selector);
|
||||
}
|
||||
});
|
||||
// set these each time
|
||||
template.firstNode = landmark.hasDom() ? landmark.firstNode() : null;
|
||||
template.lastNode = landmark.hasDom() ? landmark.lastNode() : null;
|
||||
return template;
|
||||
};
|
||||
|
||||
// XXX forms hooks into this to add "bind"?
|
||||
Meteor._template_decl_methods = {
|
||||
// methods store data here (event map, etc.). initialized per template.
|
||||
_tmpl_data: null,
|
||||
// these functions must be generic (i.e. use `this`)
|
||||
events: function (eventMap) {
|
||||
var events =
|
||||
(this._tmpl_data.events = (this._tmpl_data.events || {}));
|
||||
_.extend(events, eventMap);
|
||||
},
|
||||
preserve: function (preserveMap) {
|
||||
var preserve =
|
||||
(this._tmpl_data.preserve = (this._tmpl_data.preserve || {}));
|
||||
|
||||
if (_.isArray(preserveMap))
|
||||
_.each(preserveMap, function (selector) {
|
||||
preserve[selector] = true;
|
||||
});
|
||||
else
|
||||
_.extend(preserve, preserveMap);
|
||||
},
|
||||
helpers: function (helperMap) {
|
||||
var helpers =
|
||||
(this._tmpl_data.helpers = (this._tmpl_data.helpers || {}));
|
||||
for(var h in helperMap)
|
||||
helpers[h] = helperMap[h];
|
||||
}
|
||||
};
|
||||
|
||||
Meteor._def_template = function (name, raw_func) {
|
||||
Meteor._hook_handlebars_each();
|
||||
Meteor._hook_handlebars();
|
||||
|
||||
window.Template = window.Template || {};
|
||||
|
||||
var partial = function(data) {
|
||||
var getHtml = function() {
|
||||
return raw_func(data, {
|
||||
helpers: partial,
|
||||
partials: Meteor._partials
|
||||
// Define the function assigned to Template.<name>.
|
||||
|
||||
var partial = function (data) {
|
||||
var tmpl = name && Template[name] || {};
|
||||
var tmplData = tmpl._tmpl_data || {};
|
||||
|
||||
var html = Spark.labelBranch("Template."+name, function () {
|
||||
var html = Spark.createLandmark({
|
||||
preserve: tmplData.preserve || {},
|
||||
created: function () {
|
||||
var template = templateObjFromLandmark(this);
|
||||
template.data = data;
|
||||
tmpl.created && tmpl.created.call(template);
|
||||
},
|
||||
rendered: function () {
|
||||
var template = templateObjFromLandmark(this);
|
||||
template.data = data;
|
||||
tmpl.rendered && tmpl.rendered.call(template);
|
||||
},
|
||||
destroyed: function () {
|
||||
// template.data is already set from previous callbacks
|
||||
tmpl.destroyed &&
|
||||
tmpl.destroyed.call(templateObjFromLandmark(this));
|
||||
delete templateInstanceData[this.id];
|
||||
}
|
||||
}, function (landmark) {
|
||||
var html = Spark.isolate(function () {
|
||||
// XXX Forms needs to run a hook before and after raw_func
|
||||
// (and receive 'landmark')
|
||||
return raw_func(data, {
|
||||
helpers: _.extend({}, partial, tmplData.helpers || {}),
|
||||
partials: Meteor._partials,
|
||||
name: name
|
||||
});
|
||||
});
|
||||
|
||||
// take an event map with `function (event, template)` handlers
|
||||
// and produce one with `function (event, landmark)` handlers
|
||||
// for Spark, by inserting logic to create the template object.
|
||||
var wrapEventMap = function (oldEventMap) {
|
||||
var newEventMap = {};
|
||||
_.each(oldEventMap, function (handler, key) {
|
||||
newEventMap[key] = function (event, landmark) {
|
||||
return handler.call(this, event,
|
||||
templateObjFromLandmark(landmark));
|
||||
};
|
||||
});
|
||||
return newEventMap;
|
||||
};
|
||||
|
||||
// support old Template.foo.events = {...} format
|
||||
var events =
|
||||
(tmpl.events !== Meteor._template_decl_methods.events ?
|
||||
tmpl.events : tmplData.events);
|
||||
// events need to be inside the landmark, not outside, so
|
||||
// that when an event fires, you can retrieve the enclosing
|
||||
// landmark to get the template data
|
||||
if (tmpl.events)
|
||||
html = Spark.attachEvents(wrapEventMap(events), html);
|
||||
return html;
|
||||
});
|
||||
};
|
||||
html = Spark.setDataContext(data, html);
|
||||
return html;
|
||||
});
|
||||
|
||||
|
||||
var react_data = { events: (name ? Template[name].events : {}),
|
||||
event_data: data,
|
||||
template_name: name };
|
||||
|
||||
return Meteor.ui.chunk(getHtml, react_data);
|
||||
return html;
|
||||
};
|
||||
|
||||
// XXX hack.. copy all of Handlebars' built in helpers over to
|
||||
@@ -49,6 +185,8 @@
|
||||
"'. Each template needs a unique name.");
|
||||
|
||||
Template[name] = partial;
|
||||
_.extend(partial, Meteor._template_decl_methods);
|
||||
partial._tmpl_data = {};
|
||||
|
||||
Meteor._partials[name] = partial;
|
||||
}
|
||||
@@ -58,7 +196,3 @@
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -111,7 +111,8 @@ var html_scanner = {
|
||||
|
||||
_handleTag: function (results, tag, attribs, contents, parseError) {
|
||||
|
||||
// trim the tag contents
|
||||
// trim the tag contents.
|
||||
// this is a courtesy and is also relied on by some unit tests.
|
||||
contents = contents.match(/^[ \t\r\n]*([\s\S]*?)[ \t\r\n]*$/)[1];
|
||||
|
||||
// do we have 1 or more attribs?
|
||||
@@ -146,7 +147,7 @@ var html_scanner = {
|
||||
if (hasAttribs)
|
||||
throw parseError("Attributes on <body> not supported");
|
||||
results.js += "Meteor.startup(function(){" +
|
||||
"document.body.appendChild(Meteor.ui.render(" +
|
||||
"document.body.appendChild(Spark.render(" +
|
||||
"Meteor._def_template(null," + code + ")));});";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ Package.on_use(function (api) {
|
||||
// is encountered.. shouldn't be very hard, we just need a way to
|
||||
// get at 'api' from a register_extension handler
|
||||
|
||||
api.use(['underscore', 'liveui'], 'client');
|
||||
api.use(['underscore', 'spark'], 'client');
|
||||
|
||||
// provides the runtime logic to instantiate our templates
|
||||
api.add_files('deftemplate.js', 'client');
|
||||
@@ -83,7 +83,7 @@ Package.register_extension(
|
||||
Package.on_test(function (api) {
|
||||
api.use('tinytest');
|
||||
api.use('htmljs');
|
||||
api.use('test-helpers', 'client');
|
||||
api.use(['test-helpers', 'domutils', 'session'], 'client');
|
||||
api.add_files([
|
||||
'templating_tests.js',
|
||||
'templating_tests.html'
|
||||
|
||||
@@ -24,7 +24,7 @@ Tinytest.add("templating - html scanner", function (test) {
|
||||
};
|
||||
|
||||
var BODY_PREAMBLE = "Meteor.startup(function(){" +
|
||||
"document.body.appendChild(Meteor.ui.render(" +
|
||||
"document.body.appendChild(Spark.render(" +
|
||||
"Meteor._def_template(null,";
|
||||
var BODY_POSTAMBLE = ")));});";
|
||||
var TEMPLATE_PREAMBLE = "Meteor._def_template(";
|
||||
|
||||
@@ -183,3 +183,125 @@
|
||||
(biggie={{#get_arg helperListFour platypus thisTest fancyhelper.currentFruit fancyhelper.currentCountry.unicorns a=platypus b=thisTest c=fancyhelper.currentFruit d=fancyhelper.currentCountry.unicorns}}{{/get_arg}})
|
||||
(twoArgBlock={{#two_args "foo" "foo"}}{{/two_args}})
|
||||
</template>
|
||||
|
||||
<template name="test_render_a">
|
||||
{{foo}}<br><hr>
|
||||
</template>
|
||||
|
||||
<template name="test_render_b">
|
||||
{{#with 200}}{{foo}}<br><hr>{{/with}}
|
||||
</template>
|
||||
|
||||
<template name="test_render_c">
|
||||
<br><hr>
|
||||
</template>
|
||||
|
||||
<template name="test_branches_a">
|
||||
<span>{{var}}</span>
|
||||
{{#myConstant}}<hr/>{{/myConstant}}
|
||||
{{#if 1}}
|
||||
{{#myConstant}}<hr/>{{/myConstant}}
|
||||
{{/if}}
|
||||
{{#myConstant}}<hr/>{{/myConstant}}
|
||||
</template>
|
||||
|
||||
<template name="test_listmatching_a0">
|
||||
<span>{{var}}</span>
|
||||
<p>
|
||||
{{#each c}}
|
||||
{{> test_listmatching_a1}}
|
||||
{{/each}}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template name="test_listmatching_a1">
|
||||
{{letter}}
|
||||
</template>
|
||||
|
||||
<template name="test_isolate_a">
|
||||
{{helper 1}}
|
||||
{{#isolate}}
|
||||
{{helper 2}}
|
||||
{{#isolate}}
|
||||
{{helper 3}}
|
||||
{{#isolate}}
|
||||
{{helper 4}}
|
||||
{{/isolate}}
|
||||
{{/isolate}}
|
||||
{{/isolate}}
|
||||
</template>
|
||||
|
||||
<template name="test_template_arg_a">
|
||||
<b>Foo</b> <i>Bar</i> <u>Baz</u>
|
||||
</template>
|
||||
|
||||
<template name="test_template_preserve_a">
|
||||
<span class="a">x</span>
|
||||
<span class="b">x</span>
|
||||
<span class="c">x</span>
|
||||
<span class="d">x</span>
|
||||
<span class="y">x</span>
|
||||
<span class="e">x</span>
|
||||
<span class="z">x</span>
|
||||
<span class="f">x</span>
|
||||
<u>{{var}}</u>
|
||||
</template>
|
||||
|
||||
<template name="test_template_helpers_a">
|
||||
{{foo}}{{bar}}{{baz}}
|
||||
</template>
|
||||
|
||||
<template name="test_template_helpers_b">
|
||||
{{name}}{{arity}}{{toString}}{{length}}{{var}}
|
||||
</template>
|
||||
|
||||
<template name="test_template_helpers_c">
|
||||
{{name}}{{arity}}{{toString}}{{length}}{{var}}x
|
||||
</template>
|
||||
|
||||
<template name="test_template_events_a">
|
||||
<b>foo</b><u>bar</u><i>baz</i>
|
||||
</template>
|
||||
|
||||
<template name="test_template_events_b">
|
||||
<b>foo</b><u>bar</u><i>baz</i>
|
||||
</template>
|
||||
|
||||
<template name="test_template_eachrender_a">
|
||||
{{#each entries}}
|
||||
<div>{{x}}</div>
|
||||
{{/each}}
|
||||
</template>
|
||||
|
||||
<template name="test_template_eachrender_b">
|
||||
{{#each entries}}
|
||||
<div>{{x}}</div>
|
||||
{{/each}}
|
||||
</template>
|
||||
|
||||
<template name="test_template_landmarks_a">
|
||||
{{LM}}{{{LM}}}{{LM}}{{{LM}}}{{v}}
|
||||
</template>
|
||||
|
||||
<template name="test_template_bare_each_a">
|
||||
{{#each abc}}
|
||||
{{LM}}
|
||||
{{/each}}
|
||||
{{v}}
|
||||
</template>
|
||||
|
||||
<template name="test_template_labels_a0">
|
||||
{{{stuff}}}
|
||||
</template>
|
||||
|
||||
<template name="test_template_labels_a1">
|
||||
<hr>
|
||||
</template>
|
||||
|
||||
<template name="test_template_labels_a2">
|
||||
<hr>
|
||||
</template>
|
||||
|
||||
<template name="test_template_labels_a3">
|
||||
<hr>
|
||||
</template>
|
||||
|
||||
@@ -3,8 +3,8 @@ Tinytest.add("templating - assembly", function (test) {
|
||||
|
||||
// Test for a bug that made it to production -- after a replacement,
|
||||
// we need to also check the newly replaced node for replacements
|
||||
var frag = Meteor.ui.render(Template.test_assembly_a0);
|
||||
test.equal(canonicalizeHtml(Meteor.ui._fragmentToHtml(frag)),
|
||||
var frag = Meteor.render(Template.test_assembly_a0);
|
||||
test.equal(canonicalizeHtml(DomUtils.fragmentToHtml(frag)),
|
||||
"Hi");
|
||||
|
||||
// Another production bug -- we must use LiveRange to replace the
|
||||
@@ -14,7 +14,7 @@ Tinytest.add("templating - assembly", function (test) {
|
||||
return Session.get("stuff");
|
||||
};
|
||||
var onscreen = DIV({style: "display: none"}, [
|
||||
Meteor.ui.render(Template.test_assembly_b0)]);
|
||||
Meteor.render(Template.test_assembly_b0)]);
|
||||
document.body.appendChild(onscreen);
|
||||
test.equal(canonicalizeHtml(onscreen.innerHTML), "xyhi");
|
||||
Session.set("stuff", false);
|
||||
@@ -41,7 +41,7 @@ Tinytest.add("templating - table assembly", function(test) {
|
||||
|
||||
var table;
|
||||
|
||||
table = childWithTag(Meteor.ui.render(Template.test_table_a0), "TABLE");
|
||||
table = childWithTag(Meteor.render(Template.test_table_a0), "TABLE");
|
||||
|
||||
// table.rows is a great test, as it fails not only when TR/TD tags are
|
||||
// stripped due to improper html-to-fragment, but also when they are present
|
||||
@@ -49,7 +49,7 @@ Tinytest.add("templating - table assembly", function(test) {
|
||||
test.equal(table.rows.length, 3);
|
||||
|
||||
// this time with an explicit TBODY
|
||||
table = childWithTag(Meteor.ui.render(Template.test_table_b0), "TABLE");
|
||||
table = childWithTag(Meteor.render(Template.test_table_b0), "TABLE");
|
||||
test.equal(table.rows.length, 3);
|
||||
|
||||
var c = new LocalCollection();
|
||||
@@ -58,7 +58,7 @@ Tinytest.add("templating - table assembly", function(test) {
|
||||
c.insert({bar:'c'});
|
||||
var onscreen = DIV({style: "display: none;"});
|
||||
onscreen.appendChild(
|
||||
Meteor.ui.render(_.bind(Template.test_table_each, null, {foo: c.find()})));
|
||||
Meteor.render(_.bind(Template.test_table_each, null, {foo: c.find()})));
|
||||
document.body.appendChild(onscreen);
|
||||
table = childWithTag(onscreen, "TABLE");
|
||||
|
||||
@@ -80,17 +80,20 @@ Tinytest.add("templating - event handler this", function(test) {
|
||||
Template.test_event_data_with.TWO = {str: "two"};
|
||||
Template.test_event_data_with.THREE = {str: "three"};
|
||||
|
||||
Template.test_event_data_with.events({
|
||||
'click': function(event, template) {
|
||||
test.isTrue(this.str);
|
||||
test.equal(template.data.str, "one");
|
||||
event_buf.push(this.str);
|
||||
}
|
||||
});
|
||||
|
||||
var event_buf = [];
|
||||
var tmpl = OnscreenDiv(
|
||||
Meteor.ui.render(
|
||||
function() {
|
||||
return Template.test_event_data_with(
|
||||
Template.test_event_data_with.ONE);
|
||||
},
|
||||
{ events: { 'click': function() {
|
||||
test.isTrue(this.str);
|
||||
event_buf.push(this.str);
|
||||
} }}));
|
||||
Meteor.render(function () {
|
||||
return Template.test_event_data_with(
|
||||
Template.test_event_data_with.ONE);
|
||||
}));
|
||||
|
||||
var divs = tmpl.node().getElementsByTagName("div");
|
||||
test.equal(3, divs.length);
|
||||
@@ -313,3 +316,633 @@ Tinytest.add("templating - helpers and dots", function(test) {
|
||||
test.equal(trials[5], "(twoArgBlock=true,false)");
|
||||
test.equal(trials.length, 6);
|
||||
});
|
||||
|
||||
|
||||
Tinytest.add("templating - rendered template", function(test) {
|
||||
var R = ReactiveVar('foo');
|
||||
Template.test_render_a.foo = function() {
|
||||
R.get();
|
||||
return this.x + 1;
|
||||
};
|
||||
|
||||
Template.test_render_a.preserve(['br']);
|
||||
|
||||
var div = OnscreenDiv(
|
||||
Meteor.render(function () {
|
||||
return Template.test_render_a({ x: 123 });
|
||||
}));
|
||||
|
||||
test.equal(div.text().match(/\S+/)[0], "124");
|
||||
|
||||
var br1 = div.node().getElementsByTagName('br')[0];
|
||||
var hr1 = div.node().getElementsByTagName('hr')[0];
|
||||
test.isTrue(br1);
|
||||
test.isTrue(hr1);
|
||||
|
||||
R.set('bar');
|
||||
Meteor.flush();
|
||||
var br2 = div.node().getElementsByTagName('br')[0];
|
||||
var hr2 = div.node().getElementsByTagName('hr')[0];
|
||||
test.isTrue(br2);
|
||||
test.isTrue(br1 === br2);
|
||||
test.isTrue(hr2);
|
||||
test.isFalse(hr1 === hr2);
|
||||
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
/////
|
||||
|
||||
R = ReactiveVar('foo');
|
||||
|
||||
Template.test_render_b.foo = function() {
|
||||
R.get();
|
||||
return (+this) + 1;
|
||||
};
|
||||
Template.test_render_b.preserve(['br']);
|
||||
|
||||
div = OnscreenDiv(
|
||||
Meteor.render(function () {
|
||||
return Template.test_render_b({ x: 123 });
|
||||
}));
|
||||
|
||||
test.equal(div.text().match(/\S+/)[0], "201");
|
||||
|
||||
var br1 = div.node().getElementsByTagName('br')[0];
|
||||
var hr1 = div.node().getElementsByTagName('hr')[0];
|
||||
test.isTrue(br1);
|
||||
test.isTrue(hr1);
|
||||
|
||||
R.set('bar');
|
||||
Meteor.flush();
|
||||
var br2 = div.node().getElementsByTagName('br')[0];
|
||||
var hr2 = div.node().getElementsByTagName('hr')[0];
|
||||
test.isTrue(br2);
|
||||
test.isTrue(br1 === br2);
|
||||
test.isTrue(hr2);
|
||||
test.isFalse(hr1 === hr2);
|
||||
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
/////
|
||||
|
||||
var stuff = new LocalCollection();
|
||||
stuff.insert({foo:'bar'});
|
||||
|
||||
Template.test_render_c.preserve(['br']);
|
||||
|
||||
div = OnscreenDiv(
|
||||
Meteor.renderList(
|
||||
stuff.find(), function (data) {
|
||||
return Template.test_render_c(data, 'blah');
|
||||
}));
|
||||
|
||||
var br1 = div.node().getElementsByTagName('br')[0];
|
||||
var hr1 = div.node().getElementsByTagName('hr')[0];
|
||||
test.isTrue(br1);
|
||||
test.isTrue(hr1);
|
||||
|
||||
stuff.update({foo:'bar'}, {$set: {foo: 'baz'}});
|
||||
Meteor.flush();
|
||||
var br2 = div.node().getElementsByTagName('br')[0];
|
||||
var hr2 = div.node().getElementsByTagName('hr')[0];
|
||||
test.isTrue(br2);
|
||||
test.isTrue(br1 === br2);
|
||||
test.isTrue(hr2);
|
||||
test.isFalse(hr1 === hr2);
|
||||
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
/////
|
||||
|
||||
var stuff = new LocalCollection();
|
||||
stuff.insert({foo:'bar'});
|
||||
|
||||
Template.test_render_c.preserve(['br']);
|
||||
|
||||
div = OnscreenDiv(Meteor.renderList(stuff.find(),
|
||||
Template.test_render_c));
|
||||
|
||||
var br1 = div.node().getElementsByTagName('br')[0];
|
||||
var hr1 = div.node().getElementsByTagName('hr')[0];
|
||||
test.isTrue(br1);
|
||||
test.isTrue(hr1);
|
||||
|
||||
stuff.update({foo:'bar'}, {$set: {foo: 'baz'}});
|
||||
Meteor.flush();
|
||||
var br2 = div.node().getElementsByTagName('br')[0];
|
||||
var hr2 = div.node().getElementsByTagName('hr')[0];
|
||||
test.isTrue(br2);
|
||||
test.isTrue(br1 === br2);
|
||||
test.isTrue(hr2);
|
||||
test.isFalse(hr1 === hr2);
|
||||
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("templating - branch labels", function(test) {
|
||||
var R = ReactiveVar('foo');
|
||||
Template.test_branches_a['var'] = function () {
|
||||
return R.get();
|
||||
};
|
||||
|
||||
var elems = [];
|
||||
|
||||
// use constant landmarks to test that each
|
||||
// block helper invocation gets a different label
|
||||
Template.test_branches_a.myConstant = function (options) {
|
||||
var data = this;
|
||||
var firstRender = true;
|
||||
return Spark.createLandmark({ constant: true,
|
||||
rendered: function () {
|
||||
if (! firstRender)
|
||||
return;
|
||||
firstRender = false;
|
||||
var hr = this.find('hr');
|
||||
hr.myIndex = elems.length;
|
||||
elems.push(this.find('hr'));
|
||||
}},
|
||||
function () {
|
||||
return options.fn(data);
|
||||
});
|
||||
};
|
||||
|
||||
var div = OnscreenDiv(Meteor.render(Template.test_branches_a));
|
||||
Meteor.flush();
|
||||
test.equal(DomUtils.find(div.node(), 'span').innerHTML, 'foo');
|
||||
test.equal(elems.length, 3);
|
||||
|
||||
R.set('bar');
|
||||
Meteor.flush();
|
||||
var elems2 = DomUtils.findAll(div.node(), 'hr');
|
||||
elems2.sort(function(a, b) { return a.myIndex - b.myIndex; });
|
||||
test.equal(elems[0], elems2[0]);
|
||||
test.equal(elems[1], elems2[1]);
|
||||
test.equal(elems[2], elems2[2]);
|
||||
test.equal(DomUtils.find(div.node(), 'span').innerHTML, 'bar');
|
||||
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
});
|
||||
|
||||
Tinytest.add("templating - matching in list", function (test) {
|
||||
var c = new LocalCollection();
|
||||
c.insert({letter:'a'});
|
||||
c.insert({letter:'b'});
|
||||
c.insert({letter:'c'});
|
||||
|
||||
_.extend(Template.test_listmatching_a0, {
|
||||
'var': function () { return R.get(); },
|
||||
c: function () { return c.find(); }
|
||||
});
|
||||
|
||||
var buf = [];
|
||||
_.extend(Template.test_listmatching_a1, {
|
||||
created: function () { buf.push('+'); },
|
||||
rendered: function () {
|
||||
var letter = canonicalizeHtml(
|
||||
DomUtils.rangeToHtml(this.firstNode,
|
||||
this.lastNode).match(/\S+/)[0]);
|
||||
buf.push('*'+letter);
|
||||
},
|
||||
destroyed: function () { buf.push('-'); }
|
||||
});
|
||||
|
||||
var R = ReactiveVar('foo');
|
||||
var div = OnscreenDiv(Spark.render(Template.test_listmatching_a0));
|
||||
Meteor.flush();
|
||||
|
||||
test.equal(DomUtils.find(div.node(), 'span').innerHTML, 'foo');
|
||||
test.equal(div.html().match(/<p>(.*?)<\/p>/)[1].match(/\S+/g), ['a','b','c']);
|
||||
test.equal(buf.join(''), '+++*a*b*c');
|
||||
|
||||
buf.length = 0;
|
||||
R.set('bar');
|
||||
Meteor.flush();
|
||||
test.equal(DomUtils.find(div.node(), 'span').innerHTML, 'bar');
|
||||
test.equal(div.html().match(/<p>(.*?)<\/p>/)[1].match(/\S+/g), ['a','b','c']);
|
||||
test.equal(buf.join(''), '*a*b*c');
|
||||
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("templating - isolate helper", function (test) {
|
||||
var Rs = _.map(_.range(4), function () { return ReactiveVar(1); });
|
||||
var touch = function (n) { Rs[n-1].get(); };
|
||||
var bump = function (n) { Rs[n-1].set(Rs[n-1].get() + 1); };
|
||||
var counts = _.map(_.range(4), function () { return 0; });
|
||||
var tally = function (n) { return ++counts[n-1]; };
|
||||
|
||||
_.extend(Template.test_isolate_a, {
|
||||
helper: function (n) {
|
||||
touch(n);
|
||||
return tally(n);
|
||||
}
|
||||
});
|
||||
|
||||
var div = OnscreenDiv(Meteor.render(Template.test_isolate_a));
|
||||
|
||||
var getTallies = function () {
|
||||
return _.map(div.html().match(/\S+/g), Number);
|
||||
};
|
||||
var expect = function(str) {
|
||||
test.equal(getTallies().join(','), str);
|
||||
};
|
||||
|
||||
Meteor.flush();
|
||||
expect("1,1,1,1");
|
||||
bump(1); Meteor.flush(); expect("2,2,2,2");
|
||||
bump(2); Meteor.flush(); expect("2,3,3,3");
|
||||
bump(3); Meteor.flush(); expect("2,3,4,4");
|
||||
bump(4); Meteor.flush(); expect("2,3,4,5");
|
||||
Meteor.flush(); expect("2,3,4,5");
|
||||
bump(3); Meteor.flush(); expect("2,3,5,6");
|
||||
bump(2); Meteor.flush(); expect("2,4,6,7");
|
||||
bump(1); Meteor.flush(); expect("3,5,7,8");
|
||||
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("templating - template arg", function (test) {
|
||||
Template.test_template_arg_a.events({
|
||||
click: function (event, template) {
|
||||
template.firstNode.innerHTML = 'Hello';
|
||||
template.lastNode.innerHTML = 'World';
|
||||
template.find('i').innerHTML =
|
||||
(template.findAll('*').length)+"-element";
|
||||
template.lastNode.innerHTML += ' (the secret is '+
|
||||
template.secret+')';
|
||||
}
|
||||
});
|
||||
|
||||
Template.test_template_arg_a.created = function() {
|
||||
var self = this;
|
||||
test.isFalse(self.firstNode);
|
||||
test.isFalse(self.lastNode);
|
||||
test.throws(function () { return self.find("*"); });
|
||||
test.throws(function () { return self.findAll("*"); });
|
||||
};
|
||||
|
||||
Template.test_template_arg_a.rendered = function () {
|
||||
var template = this;
|
||||
template.firstNode.innerHTML = 'Greetings';
|
||||
template.lastNode.innerHTML = 'Line';
|
||||
template.find('i').innerHTML =
|
||||
(template.findAll('b').length)+"-bold";
|
||||
template.secret = "strawberry "+template.data.food;
|
||||
};
|
||||
|
||||
Template.test_template_arg_a.destroyed = function() {
|
||||
var self = this;
|
||||
test.isFalse(self.firstNode);
|
||||
test.isFalse(self.lastNode);
|
||||
test.throws(function () { return self.find("*"); });
|
||||
test.throws(function () { return self.findAll("*"); });
|
||||
};
|
||||
|
||||
var div = OnscreenDiv(Spark.render(function () {
|
||||
return Template.test_template_arg_a({food: "pie"});
|
||||
}));
|
||||
|
||||
test.equal(div.text(), "Foo Bar Baz");
|
||||
Meteor.flush();
|
||||
test.equal(div.text(), "Greetings 1-bold Line");
|
||||
clickElement(DomUtils.find(div.node(), 'i'));
|
||||
test.equal(div.text(), "Hello 3-element World (the secret is strawberry pie)");
|
||||
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
});
|
||||
|
||||
Tinytest.add("templating - preserve", function (test) {
|
||||
var R = ReactiveVar('foo');
|
||||
|
||||
var tmpl = Template.test_template_preserve_a;
|
||||
tmpl.preserve(['.b']);
|
||||
tmpl.preserve(['.c']);
|
||||
tmpl.preserve({'.d': true});
|
||||
tmpl.preserve({'span': function (n) {
|
||||
return _.contains(['e','f'], n.className) && n.className;
|
||||
}});
|
||||
tmpl.preserve(['span.a']);
|
||||
tmpl['var'] = function () { return R.get(); };
|
||||
|
||||
var div = OnscreenDiv(Meteor.render(tmpl));
|
||||
Meteor.flush();
|
||||
test.equal(DomUtils.find(div.node(), 'u').firstChild.nodeValue.match(
|
||||
/\S+/)[0], 'foo');
|
||||
var spans1 = {};
|
||||
_.each(DomUtils.findAll(div.node(), 'span'), function (sp) {
|
||||
spans1[sp.className] = sp;
|
||||
});
|
||||
|
||||
R.set('bar');
|
||||
Meteor.flush();
|
||||
test.equal(DomUtils.find(div.node(), 'u').firstChild.nodeValue.match(
|
||||
/\S+/)[0], 'bar');
|
||||
var spans2 = {};
|
||||
_.each(DomUtils.findAll(div.node(), 'span'), function (sp) {
|
||||
spans2[sp.className] = sp;
|
||||
});
|
||||
|
||||
test.isTrue(spans1.a === spans2.a);
|
||||
test.isTrue(spans1.b === spans2.b);
|
||||
test.isTrue(spans1.c === spans2.c);
|
||||
test.isTrue(spans1.d === spans2.d);
|
||||
test.isTrue(spans1.e === spans2.e);
|
||||
test.isTrue(spans1.f === spans2.f);
|
||||
test.isFalse(spans1.y === spans2.y);
|
||||
test.isFalse(spans1.z === spans2.z);
|
||||
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
});
|
||||
|
||||
Tinytest.add("templating - helpers", function (test) {
|
||||
var tmpl = Template.test_template_helpers_a;
|
||||
|
||||
tmpl.foo = 'z';
|
||||
tmpl.helpers({bar: 'b'});
|
||||
// helpers(...) takes precendence of assigned helper
|
||||
tmpl.helpers({foo: 'a', baz: function() { return 'c'; }});
|
||||
|
||||
var div = OnscreenDiv(Meteor.render(tmpl));
|
||||
test.equal(div.text().match(/\S+/)[0], 'abc');
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
tmpl = Template.test_template_helpers_b;
|
||||
|
||||
tmpl.helpers({
|
||||
'name': 'A',
|
||||
'arity': 'B',
|
||||
'toString': 'C',
|
||||
'length': 4,
|
||||
'var': 'D'
|
||||
});
|
||||
|
||||
div = OnscreenDiv(Meteor.render(tmpl));
|
||||
var txt = div.text().match(/\S+/)[0];
|
||||
test.isTrue(txt.match(/^ABC?4D$/));
|
||||
// We don't get 'C' (the ability to name a helper {{toString}})
|
||||
// in IE < 9 because of the famed DontEnum bug. This could be
|
||||
// fixed but it would require making all the code that handles
|
||||
// the dictionary of helpers be DontEnum-aware. In practice,
|
||||
// the Object prototype method names (toString, hasOwnProperty,
|
||||
// isPropertyOf, ...) make poor helper names and are unlikely
|
||||
// to be used in apps.
|
||||
test.expect_fail();
|
||||
test.equal(txt, 'ABC4D');
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// test that helpers don't "leak"
|
||||
tmpl = Template.test_template_helpers_c;
|
||||
div = OnscreenDiv(Meteor.render(tmpl));
|
||||
test.equal(div.text(), 'x');
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
});
|
||||
|
||||
Tinytest.add("templating - events", function (test) {
|
||||
var tmpl = Template.test_template_events_a;
|
||||
|
||||
var buf = [];
|
||||
|
||||
// old style
|
||||
tmpl.events = {
|
||||
'click b': function () { buf.push('b'); }
|
||||
};
|
||||
|
||||
var div = OnscreenDiv(Meteor.render(tmpl));
|
||||
clickElement(DomUtils.find(div.node(), 'b'));
|
||||
test.equal(buf, ['b']);
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
///
|
||||
|
||||
tmpl = Template.test_template_events_b;
|
||||
buf = [];
|
||||
// new style
|
||||
tmpl.events({
|
||||
'click u': function () { buf.push('u'); }
|
||||
});
|
||||
tmpl.events({
|
||||
'click i': function () { buf.push('i'); }
|
||||
});
|
||||
|
||||
var div = OnscreenDiv(Meteor.render(tmpl));
|
||||
clickElement(DomUtils.find(div.node(), 'u'));
|
||||
clickElement(DomUtils.find(div.node(), 'i'));
|
||||
test.equal(buf, ['u', 'i']);
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
});
|
||||
|
||||
Tinytest.add("templating - #each rendered callback", function (test) {
|
||||
// test that any list modification triggers a rendered callback on the
|
||||
// enclosing template
|
||||
|
||||
var entries = new LocalCollection();
|
||||
entries.insert({x:'a'});
|
||||
entries.insert({x:'b'});
|
||||
entries.insert({x:'c'});
|
||||
|
||||
var buf = [];
|
||||
|
||||
var tmpl = Template.test_template_eachrender_a;
|
||||
tmpl.helpers({entries: function() {
|
||||
return entries.find({}, {sort: ['x']}); }});
|
||||
tmpl.rendered = function () {
|
||||
buf.push(canonicalizeHtml(
|
||||
DomUtils.rangeToHtml(this.firstNode, this.lastNode)).replace(/\s/g, ''));
|
||||
};
|
||||
var div = OnscreenDiv(Meteor.render(tmpl));
|
||||
Meteor.flush();
|
||||
test.equal(buf, ['<div>a</div><div>b</div><div>c</div>']);
|
||||
buf.length = 0;
|
||||
|
||||
// added
|
||||
entries.insert({x:'d'});
|
||||
test.equal(buf, []);
|
||||
Meteor.flush();
|
||||
test.equal(buf, ['<div>a</div><div>b</div><div>c</div><div>d</div>']);
|
||||
buf.length = 0;
|
||||
|
||||
// removed
|
||||
entries.remove({x:'a'});
|
||||
test.equal(buf, []);
|
||||
Meteor.flush();
|
||||
test.equal(buf, ['<div>b</div><div>c</div><div>d</div>']);
|
||||
buf.length = 0;
|
||||
|
||||
// moved/changed
|
||||
entries.update({x:'b'}, {$set: {x: 'z'}});
|
||||
test.equal(buf, []);
|
||||
Meteor.flush();
|
||||
test.equal(buf, ['<div>c</div><div>d</div><div>z</div>',
|
||||
'<div>c</div><div>d</div><div>z</div>']);
|
||||
buf.length = 0;
|
||||
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
|
||||
// test pure "moved"
|
||||
|
||||
tmpl = Template.test_template_eachrender_b;
|
||||
var cbks = [];
|
||||
var xs = ['a','b','c'];
|
||||
tmpl.helpers({entries: function() {
|
||||
return { observe: function (callbacks) {
|
||||
cbks.push(callbacks);
|
||||
_.each(xs, function(x, i) {
|
||||
callbacks.added({x:x}, i);
|
||||
});
|
||||
return {
|
||||
stop: function () {
|
||||
cbks = _.without(cbks, callbacks);
|
||||
}
|
||||
};
|
||||
}};
|
||||
}});
|
||||
tmpl.rendered = function () {
|
||||
buf.push(canonicalizeHtml(
|
||||
DomUtils.rangeToHtml(this.firstNode, this.lastNode)).replace(/\s/g, ''));
|
||||
};
|
||||
buf = [];
|
||||
var div = OnscreenDiv(Meteor.render(tmpl));
|
||||
test.equal(buf, []);
|
||||
Meteor.flush();
|
||||
test.equal(buf, ['<div>a</div><div>b</div><div>c</div>']);
|
||||
buf.length = 0;
|
||||
|
||||
_.each(cbks, function (callbacks) {
|
||||
callbacks.moved({x:'a'}, 0, 2);
|
||||
});
|
||||
test.equal(buf, []);
|
||||
Meteor.flush();
|
||||
test.equal(div.html().replace(/\s/g, ''),
|
||||
'<div>b</div><div>c</div><div>a</div>');
|
||||
test.equal(buf, ['<div>b</div><div>c</div><div>a</div>']);
|
||||
buf.length = 0;
|
||||
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
});
|
||||
|
||||
Tinytest.add("templating - landmarks in helpers", function (test) {
|
||||
var buf = [];
|
||||
|
||||
var R = ReactiveVar('foo');
|
||||
|
||||
var tmpl = Template.test_template_landmarks_a;
|
||||
tmpl.LM = function () {
|
||||
return new Handlebars.SafeString(
|
||||
Spark.createLandmark({created: function () { buf.push('c'); },
|
||||
rendered: function () { buf.push('r'); },
|
||||
destroyed: function () { buf.push('d'); }},
|
||||
function () { return 'x'; }));
|
||||
};
|
||||
tmpl.v = function () {
|
||||
return R.get();
|
||||
};
|
||||
|
||||
var div = OnscreenDiv(Meteor.render(tmpl));
|
||||
test.equal(div.text().match(/\S+/)[0], 'xxxxfoo');
|
||||
Meteor.flush();
|
||||
buf.sort();
|
||||
test.equal(buf.join(''), 'ccccrrrr');
|
||||
buf.length = 0;
|
||||
|
||||
R.set('bar');
|
||||
Meteor.flush();
|
||||
test.equal(div.text().match(/\S+/)[0], 'xxxxbar');
|
||||
test.equal(buf.join(''), 'rrrr');
|
||||
buf.length = 0;
|
||||
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
test.equal(buf.join(''), 'dddd');
|
||||
});
|
||||
|
||||
Tinytest.add("templating - bare each has no matching", function (test) {
|
||||
var buf = [];
|
||||
|
||||
var R = ReactiveVar('foo');
|
||||
|
||||
var tmpl = Template.test_template_bare_each_a;
|
||||
tmpl.abc = [{}, {}, {}];
|
||||
tmpl.LM = function () {
|
||||
return new Handlebars.SafeString(
|
||||
Spark.createLandmark({created: function () { buf.push('c'); },
|
||||
rendered: function () { buf.push('r'); },
|
||||
destroyed: function () { buf.push('d'); }},
|
||||
function () { return 'x'; }));
|
||||
};
|
||||
tmpl.v = function () {
|
||||
return R.get();
|
||||
};
|
||||
|
||||
var div = OnscreenDiv(Meteor.render(tmpl));
|
||||
Meteor.flush();
|
||||
buf.sort();
|
||||
test.equal(buf.join(''), 'cccrrr');
|
||||
buf.length = 0;
|
||||
|
||||
R.set('bar');
|
||||
Meteor.flush();
|
||||
buf.sort();
|
||||
test.equal(buf.join(''), 'cccdddrrr');
|
||||
buf.length = 0;
|
||||
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
test.equal(buf.join(''), 'ddd');
|
||||
});
|
||||
|
||||
Tinytest.add("templating - templates are labeled", function (test) {
|
||||
var buf = [];
|
||||
|
||||
var R = ReactiveVar('foo');
|
||||
|
||||
var tmpls = _.map([0,1,2,3], function (n) {
|
||||
return Template['test_template_labels_a'+n];
|
||||
});
|
||||
tmpls[0].stuff = function () {
|
||||
return tmpls[1]() + tmpls[2]() + tmpls[3]() + R.get();
|
||||
};
|
||||
_.each([tmpls[1], tmpls[2], tmpls[3]], function (tmpl) {
|
||||
tmpl.preserve(['hr']);
|
||||
tmpl.created = function () { buf.push('c'); };
|
||||
tmpl.rendered = function () { buf.push('r'); };
|
||||
tmpl.destroyed = function () { buf.push('d'); };
|
||||
});
|
||||
|
||||
var div = OnscreenDiv(Meteor.render(tmpls[0]));
|
||||
Meteor.flush();
|
||||
test.equal(div.html(), "<hr><hr><hr>foo");
|
||||
buf.sort();
|
||||
test.equal(buf.join(''), 'cccrrr');
|
||||
buf.length = 0;
|
||||
|
||||
R.set('bar');
|
||||
Meteor.flush();
|
||||
test.equal(div.html(), "<hr><hr><hr>bar");
|
||||
buf.sort();
|
||||
test.equal(buf.join(''), 'rrr');
|
||||
buf.length = 0;
|
||||
|
||||
div.kill();
|
||||
Meteor.flush();
|
||||
test.equal(buf.join(''), 'ddd');
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var canonicalizeHtml = function(html) {
|
||||
var h = html;
|
||||
// kill IE-specific comments inserted by Meteor liveui
|
||||
// kill IE-specific comments inserted by Spark
|
||||
h = h.replace(/<!--IE-->/g, '');
|
||||
// ignore exact text of comments
|
||||
h = h.replace(/<!--.*?-->/g, '<!---->');
|
||||
|
||||
@@ -13,7 +13,7 @@ var OnscreenDiv = function(optFrag) {
|
||||
if (! (this instanceof OnscreenDiv))
|
||||
return new OnscreenDiv(optFrag);
|
||||
|
||||
this.div = Meteor.ui._htmlToFragment(
|
||||
this.div = DomUtils.htmlToFragment(
|
||||
'<div class="OnscreenDiv" style="display: none"></div>').firstChild;
|
||||
document.body.appendChild(this.div);
|
||||
|
||||
@@ -46,12 +46,15 @@ OnscreenDiv.prototype.node = function() {
|
||||
// "fast GC" -- i.e., after the next Meteor.flush()
|
||||
// the DIV will be fully cleaned up by LiveUI.
|
||||
OnscreenDiv.prototype.kill = function() {
|
||||
// remove DIV from document by putting it in a fragment
|
||||
var frag = document.createDocumentFragment();
|
||||
frag.appendChild(this.div);
|
||||
// instigate clean-up on next flush()
|
||||
Meteor.ui._hold(frag);
|
||||
Meteor.ui._release(frag);
|
||||
var self = this;
|
||||
if (self.div.parentNode)
|
||||
self.div.parentNode.removeChild(self.div);
|
||||
|
||||
var cx = new Meteor.deps.Context;
|
||||
cx.on_invalidate(function() {
|
||||
Spark.finalize(self.div);
|
||||
});
|
||||
cx.invalidate();
|
||||
};
|
||||
|
||||
// remove the DIV from the document
|
||||
|
||||
@@ -6,6 +6,11 @@ Package.describe({
|
||||
Package.on_use(function (api, where) {
|
||||
where = where || ["client", "server"];
|
||||
|
||||
// XXX These files have various dependencies on other packages
|
||||
// that aren't specified here. :(
|
||||
// This package should probably get split into several packages,
|
||||
// each with correct dependencies.
|
||||
|
||||
api.add_files('try_all_permutations.js', where);
|
||||
api.add_files('async_multi.js', where);
|
||||
api.add_files('event_simulation.js', where);
|
||||
@@ -13,7 +18,9 @@ Package.on_use(function (api, where) {
|
||||
api.add_files('canonicalize_html.js', where);
|
||||
api.add_files('stub_stream.js', where);
|
||||
api.add_files('onscreendiv.js', where);
|
||||
api.add_files('wrappedfrag.js', where);
|
||||
api.add_files('current_style.js', where);
|
||||
api.add_files('reactivevar.js', where);
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
|
||||
53
packages/test-helpers/reactivevar.js
Normal file
53
packages/test-helpers/reactivevar.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// ReactiveVar is like a portable Session var. When you get it,
|
||||
// it registers a dependency, and when it's set, it invalidates
|
||||
// its dependencies.
|
||||
//
|
||||
// When set to a primitive value, invalidation
|
||||
// is only fired if the new value is !== the old one. When set
|
||||
// to an object value, invalidation always happens. Each behavior
|
||||
// may be desirable in different test scenarios.
|
||||
// body and keeps track of it, providing methods that query it,
|
||||
// mutate, and destroy it.
|
||||
//
|
||||
// Constructor, with optional 'new':
|
||||
// var R = [new] ReactiveVar([initialValue])
|
||||
|
||||
|
||||
var ReactiveVar = function(initialValue) {
|
||||
if (! (this instanceof ReactiveVar))
|
||||
return new ReactiveVar(initialValue);
|
||||
|
||||
this._value = (typeof initialValue === "undefined" ? null :
|
||||
initialValue);
|
||||
this._deps = {};
|
||||
};
|
||||
|
||||
ReactiveVar.prototype.get = function() {
|
||||
var context = Meteor.deps.Context.current;
|
||||
if (context && !(context.id in this._deps)) {
|
||||
this._deps[context.id] = context;
|
||||
var self = this;
|
||||
context.on_invalidate(function() {
|
||||
delete self._deps[context.id];
|
||||
});
|
||||
}
|
||||
|
||||
return this._value;
|
||||
};
|
||||
|
||||
ReactiveVar.prototype.set = function(newValue) {
|
||||
// detect equality and don't invalidate dependers
|
||||
// when value is a primitive.
|
||||
if ((typeof newValue !== 'object') && this._value === newValue)
|
||||
return;
|
||||
|
||||
this._value = newValue;
|
||||
|
||||
for(var id in this._deps)
|
||||
this._deps[id].invalidate();
|
||||
|
||||
};
|
||||
|
||||
ReactiveVar.prototype.numListeners = function() {
|
||||
return _.keys(this._deps).length;
|
||||
};
|
||||
46
packages/test-helpers/wrappedfrag.js
Normal file
46
packages/test-helpers/wrappedfrag.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// A WrappedFrag provides utility methods pertaining to a given
|
||||
// DocumentFragment that are helpful in tests. For example,
|
||||
// WrappedFrag(frag).html() constructs a sort of cross-browser
|
||||
// innerHTML for the fragment.
|
||||
|
||||
// Constructor, with optional 'new':
|
||||
// var f = [new] WrappedFrag([frag])
|
||||
WrappedFrag = function(frag) {
|
||||
if (! (this instanceof WrappedFrag))
|
||||
return new WrappedFrag(frag);
|
||||
|
||||
this.frag = frag;
|
||||
};
|
||||
|
||||
WrappedFrag.prototype.rawHtml = function() {
|
||||
return DomUtils.fragmentToHtml(this.frag);
|
||||
};
|
||||
|
||||
WrappedFrag.prototype.html = function() {
|
||||
return canonicalizeHtml(this.rawHtml());
|
||||
};
|
||||
|
||||
WrappedFrag.prototype.hold = function() {
|
||||
// increments frag's GC protection reference count
|
||||
this.frag["_protect"] = (this.frag["_protect"] || 0) + 1;
|
||||
return this;
|
||||
};
|
||||
|
||||
WrappedFrag.prototype.release = function() {
|
||||
var frag = this.frag;
|
||||
// decrement frag's GC protection reference count
|
||||
// Clean up on flush, if hits 0. Wait to decrement
|
||||
// so no one else cleans it up first.
|
||||
var cx = new Meteor.deps.Context;
|
||||
cx.on_invalidate(function() {
|
||||
if (! --frag["_protect"]) {
|
||||
Spark.finalize(frag);
|
||||
}
|
||||
});
|
||||
cx.invalidate();
|
||||
return this;
|
||||
};
|
||||
|
||||
WrappedFrag.prototype.node = function() {
|
||||
return this.frag;
|
||||
};
|
||||
@@ -116,12 +116,12 @@ Template.test.test_class = function() {
|
||||
return classes.join(' ');
|
||||
};
|
||||
|
||||
Template.test.events = {
|
||||
Template.test.events({
|
||||
'click .testname': function() {
|
||||
this.expanded = ! this.expanded;
|
||||
_resultsChanged();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Template.test.eventsArray = function() {
|
||||
var events = _.filter(this.events, function(e) {
|
||||
@@ -158,7 +158,7 @@ Template.test.eventsArray = function() {
|
||||
});
|
||||
};
|
||||
|
||||
Template.event.events = {
|
||||
Template.event.events({
|
||||
'click .debug': function () {
|
||||
// the way we manage groupPath, shortName, cookies, etc, is really
|
||||
// messy. needs to be aggressively refactored.
|
||||
@@ -166,7 +166,7 @@ Template.event.events = {
|
||||
test: this.cookie.shortName});
|
||||
Meteor._debugTest(this.cookie, reportResults);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Template.event.get_details = function() {
|
||||
var details = this.details;
|
||||
|
||||
@@ -9,7 +9,7 @@ Package.on_use(function (api) {
|
||||
// that tinytest and the driver both implement?
|
||||
api.use('tinytest');
|
||||
|
||||
api.use(['liveui', 'livedata', 'templating'], 'client');
|
||||
api.use(['spark', 'livedata', 'templating'], 'client');
|
||||
|
||||
api.add_files([
|
||||
'driver.css',
|
||||
|
||||
83
packages/universal-events/event_tests.js
Normal file
83
packages/universal-events/event_tests.js
Normal file
@@ -0,0 +1,83 @@
|
||||
|
||||
Tinytest.add("universal-events - basic", function(test) {
|
||||
|
||||
var runTest = function (testMissingHandlers) {
|
||||
var msgs = [];
|
||||
var listeners = [];
|
||||
|
||||
var createListener = function () {
|
||||
var out = [];
|
||||
msgs.push(out);
|
||||
var ret = new UniversalEventListener(function(event) {
|
||||
var node = event.currentTarget;
|
||||
if (DomUtils.elementContains(document.body, node)) {
|
||||
out.push(event.currentTarget.nodeName.toLowerCase());
|
||||
}
|
||||
}, testMissingHandlers);
|
||||
listeners.push(ret);
|
||||
return ret;
|
||||
};
|
||||
|
||||
var L1 = createListener();
|
||||
|
||||
var check = function (event, expected) {
|
||||
_.each(msgs, function (m) {
|
||||
m.length = 0;
|
||||
});
|
||||
simulateEvent(DomUtils.find(d.node(), "b"), event);
|
||||
for (var i = 0; i < listeners.length; i++)
|
||||
test.equal(msgs[i], testMissingHandlers ? [] : expected[i]);
|
||||
};
|
||||
|
||||
var d = OnscreenDiv(Meteor.render("<div><span><b>Hello</b></span></div>"));
|
||||
L1.addType('mousedown');
|
||||
if (!testMissingHandlers)
|
||||
L1.installHandler(d.node(), 'mousedown');
|
||||
var x = ['b', 'span', 'div', 'div'];
|
||||
check('mousedown', [x]);
|
||||
|
||||
check('mouseup', [[]]);
|
||||
|
||||
L1.removeType('mousedown');
|
||||
check('mousedown', [[]]);
|
||||
L1.removeType('mousedown');
|
||||
check('mousedown', [[]]);
|
||||
|
||||
L1.addType('mousedown');
|
||||
check('mousedown', [x]);
|
||||
L1.addType('mousedown');
|
||||
check('mousedown', [x]);
|
||||
L1.removeType('mousedown');
|
||||
check('mousedown', [[]]);
|
||||
|
||||
var L2 = createListener();
|
||||
if (!testMissingHandlers)
|
||||
L2.installHandler(d.node(), 'mousedown');
|
||||
|
||||
L1.addType('mousedown');
|
||||
check('mousedown', [x, []]);
|
||||
L2.addType('mousedown');
|
||||
check('mousedown', [x, x]);
|
||||
L2.addType('mousedown');
|
||||
check('mousedown', [x, x]);
|
||||
L1.removeType('mousedown');
|
||||
check('mousedown', [[], x]);
|
||||
L1.removeType('mousedown');
|
||||
check('mousedown', [[], x]);
|
||||
L2.removeType('mousedown');
|
||||
check('mousedown', [[], []]);
|
||||
L1.addType('mousedown');
|
||||
check('mousedown', [x, []]);
|
||||
L1.removeType('mousedown');
|
||||
check('mousedown', [[], []]);
|
||||
L2.addType('mousedown');
|
||||
check('mousedown', [[], x]);
|
||||
L2.removeType('mousedown');
|
||||
check('mousedown', [[], []]);
|
||||
|
||||
d.kill();
|
||||
};
|
||||
|
||||
runTest(false);
|
||||
runTest(true);
|
||||
});
|
||||
@@ -1,7 +1,4 @@
|
||||
Meteor.ui = Meteor.ui || {};
|
||||
Meteor.ui._event = Meteor.ui._event || {};
|
||||
|
||||
// LiveEvents implementation for "old IE" versions 6-8, which lack
|
||||
// Universal Events implementation for IE versions 6-8, which lack
|
||||
// addEventListener and event capturing.
|
||||
//
|
||||
// The strategy is very different. We walk the subtree in question
|
||||
@@ -22,56 +19,97 @@ Meteor.ui._event = Meteor.ui._event || {};
|
||||
// events on checkboxes and radio buttons immediately rather than
|
||||
// only when the user blurs them, another old IE quirk.
|
||||
|
||||
Meteor.ui._event._loadNoW3CImpl = function() {
|
||||
UniversalEventListener._impl = UniversalEventListener._impl || {};
|
||||
|
||||
// Singleton
|
||||
UniversalEventListener._impl.ie = function (deliver) {
|
||||
var self = this;
|
||||
this.deliver = deliver;
|
||||
this.curriedHandler = function () {
|
||||
self.handler.call(this, self);
|
||||
};
|
||||
|
||||
// The 'submit' event on IE doesn't bubble. We want to simulate
|
||||
// bubbling submit to match other browsers, and to do that we use
|
||||
// IE's own event machinery. We can't dispatch events with arbitrary
|
||||
// names in IE, so we appropriate the obscure "datasetcomplete" event
|
||||
// for this purpose.
|
||||
document.attachEvent('ondatasetcomplete', function () {
|
||||
var evt = window.event;
|
||||
var target = evt && evt.srcElement;
|
||||
if (evt.synthetic && target &&
|
||||
target.nodeName === 'FORM' &&
|
||||
evt.returnValue !== false)
|
||||
// No event handler called preventDefault on the simulated
|
||||
// submit event. That means the form should be submitted.
|
||||
target.submit();
|
||||
});
|
||||
};
|
||||
|
||||
_.extend(UniversalEventListener._impl.ie.prototype, {
|
||||
addType: function (type) {
|
||||
// not necessary for IE
|
||||
},
|
||||
|
||||
removeType: function (type) {
|
||||
// not necessary for IE
|
||||
},
|
||||
|
||||
installHandler: function (node, type) {
|
||||
// use old-school event binding, so that we can
|
||||
// access the currentTarget as `this` in the handler.
|
||||
// note: handler is never removed from node
|
||||
var prop = 'on' + type;
|
||||
|
||||
if (node.nodeType === 1) { // ELEMENT
|
||||
this._install(node, prop);
|
||||
|
||||
// hopefully fast traversal, since the browser is doing it
|
||||
var descendents = node.getElementsByTagName('*');
|
||||
|
||||
for(var i=0, N = descendents.length; i<N; i++)
|
||||
this._install(descendents[i], prop);
|
||||
}
|
||||
},
|
||||
|
||||
_install: function (node, prop) {
|
||||
var props = [prop];
|
||||
|
||||
var installHandler = function(node, prop) {
|
||||
// install handlers for faking focus/blur if necessary
|
||||
if (prop === 'onfocus')
|
||||
installHandler(node, 'onfocusin');
|
||||
props.push('onfocusin');
|
||||
else if (prop === 'onblur')
|
||||
installHandler(node, 'onfocusout');
|
||||
props.push('onfocusout');
|
||||
// install handlers for faking bubbling change/submit
|
||||
else if (prop === 'onchange') {
|
||||
installHandler(node, 'oncellchange');
|
||||
// if we're looking at a checkbox or radio button,
|
||||
// sign up for propertychange and NOT change
|
||||
if (node.nodeName === 'INPUT' &&
|
||||
(node.type === 'checkbox' || node.type === 'radio')) {
|
||||
installHandler(node, 'onpropertychange');
|
||||
return;
|
||||
}
|
||||
(node.type === 'checkbox' || node.type === 'radio'))
|
||||
props = ['onpropertychange'];
|
||||
props.push('oncellchange');
|
||||
} else if (prop === 'onsubmit')
|
||||
installHandler(node, 'ondatasetcomplete');
|
||||
props.push(node, 'ondatasetcomplete');
|
||||
|
||||
node[prop] = universalHandler;
|
||||
};
|
||||
|
||||
Meteor.ui._event.registerEventTypeImpl = function(eventType, subtreeRoot) {
|
||||
// use old-school event binding, so that we can
|
||||
// access the currentTarget as `this` in the handler.
|
||||
var prop = 'on'+eventType;
|
||||
|
||||
if (subtreeRoot.nodeType === 1) { // ELEMENT
|
||||
installHandler(subtreeRoot, prop);
|
||||
|
||||
// hopefully fast traversal, since the browser is doing it
|
||||
var descendents = subtreeRoot.getElementsByTagName('*');
|
||||
|
||||
for(var i=0, N = descendents.length; i<N; i++)
|
||||
installHandler(descendents[i], prop);
|
||||
}
|
||||
};
|
||||
|
||||
var sendEvent = function(ontype, target) {
|
||||
var e = document.createEventObject();
|
||||
e.synthetic = true;
|
||||
target.fireEvent(ontype, e);
|
||||
return e.returnValue;
|
||||
};
|
||||
for(var i = 0; i < props.length; i++)
|
||||
node[props[i]] = this.curriedHandler;
|
||||
},
|
||||
|
||||
// This is the handler we assign to DOM nodes, so it shouldn't close over
|
||||
// anything that would create a circular reference leading to a memory leak.
|
||||
var universalHandler = function() {
|
||||
//
|
||||
// This handler is called via this.curriedHandler. When it is called:
|
||||
// - 'this' is the node currently handling the event (set by IE)
|
||||
// - 'self' is what would normally be 'this'
|
||||
handler: function (self) {
|
||||
var sendEvent = function (ontype, target) {
|
||||
var e = document.createEventObject();
|
||||
e.synthetic = true;
|
||||
target.fireEvent(ontype, e);
|
||||
return e.returnValue;
|
||||
};
|
||||
|
||||
|
||||
var event = window.event;
|
||||
var type = event.type;
|
||||
var target = event.srcElement || document;
|
||||
@@ -102,7 +140,8 @@ Meteor.ui._event._loadNoW3CImpl = function() {
|
||||
}
|
||||
}
|
||||
// ignore non-simulated events of types we simulate
|
||||
if ((type === 'focus' || event.type === 'blur' || event.type === 'change' ||
|
||||
if ((type === 'focus' || event.type === 'blur'
|
||||
|| event.type === 'change' ||
|
||||
event.type === 'submit') && ! event.synthetic) {
|
||||
if (event.type === 'submit')
|
||||
event.returnValue = false; // block all native submits, we will submit
|
||||
@@ -117,18 +156,7 @@ Meteor.ui._event._loadNoW3CImpl = function() {
|
||||
type = event.type = 'submit';
|
||||
}
|
||||
|
||||
Meteor.ui._event._handleEventFunc(
|
||||
Meteor.ui._event._fixEvent(event));
|
||||
};
|
||||
self.deliver(event);
|
||||
}
|
||||
|
||||
// submit forms that aren't preventDefaulted
|
||||
document.attachEvent('ondatasetcomplete', function() {
|
||||
var evt = window.event;
|
||||
var target = evt && evt.srcElement;
|
||||
if (evt.synthetic && target &&
|
||||
target.nodeName === 'FORM' &&
|
||||
evt.returnValue !== false)
|
||||
target.submit();
|
||||
});
|
||||
|
||||
};
|
||||
});
|
||||
208
packages/universal-events/events-w3c.js
Normal file
208
packages/universal-events/events-w3c.js
Normal file
@@ -0,0 +1,208 @@
|
||||
// Universal Events implementation that depends on the W3C event
|
||||
// model, i.e. addEventListener and capturing. It's intended for all
|
||||
// browsers except IE <= 8.
|
||||
//
|
||||
// We take advantage of the fact that event handlers installed during
|
||||
// the capture phase are live during the bubbling phase. By installing
|
||||
// a capturing listener on the document, we bind the handler to the
|
||||
// event target and its ancestors "just in time".
|
||||
|
||||
(function () {
|
||||
var SIMULATE_NEITHER = 0;
|
||||
var SIMULATE_FOCUS_BLUR = 1;
|
||||
var SIMULATE_FOCUSIN_FOCUSOUT = 2;
|
||||
|
||||
UniversalEventListener._impl = UniversalEventListener._impl || {};
|
||||
|
||||
// Singleton
|
||||
UniversalEventListener._impl.w3c = function (deliver) {
|
||||
this.deliver = deliver;
|
||||
this.typeCounts = {}; // map from event type name to count
|
||||
|
||||
this.boundHandler = _.bind(this.handler, this);
|
||||
this.boundCapturer = _.bind(this.capturer, this);
|
||||
|
||||
// Focusin/focusout are the bubbling versions of focus/blur, and
|
||||
// are part of the W3C spec, but are absent from Firefox as of
|
||||
// today (v11), so we supply them.
|
||||
//
|
||||
// In addition, while most browsers fire these events sync in
|
||||
// response to a programmatic action (like .focus()), not all do.
|
||||
// IE 9+ fires focusin/focusout sync but focus/blur async. Opera
|
||||
// fires them all async. We don't do anything about this right
|
||||
// now, but simulating focus/blur on IE would make them sync.
|
||||
//
|
||||
// We have the capabiilty here to simulate focusin/focusout from
|
||||
// focus/blur, vice versa, or neither.
|
||||
//
|
||||
// We do a browser check that fails in old Firefox (3.6) but will
|
||||
// succeed if Firefox ever implements focusin/focusout. Old
|
||||
// Firefox fails all tests of the form ('onfoo' in node), while
|
||||
// new Firefox and all other known browsers will pass if 'foo' is
|
||||
// a known event.
|
||||
this.focusBlurMode = ('onfocusin' in document.createElement("DIV")) ?
|
||||
SIMULATE_NEITHER : SIMULATE_FOCUSIN_FOCUSOUT;
|
||||
|
||||
// mouseenter/mouseleave is non-bubbling mouseover/mouseout. It's
|
||||
// standard but only IE and Opera seem to support it,
|
||||
// so we simulate it (which works in IE but not in Opera for some reason).
|
||||
this.simulateMouseEnterLeave = (! window.opera);
|
||||
};
|
||||
|
||||
_.extend(UniversalEventListener._impl.w3c.prototype, {
|
||||
addType: function (eventType) {
|
||||
this._listen(this._expandEventType(eventType));
|
||||
},
|
||||
|
||||
removeType: function (type) {
|
||||
this._unlisten(this._expandEventType(type));
|
||||
},
|
||||
|
||||
installHandler: function (node, type) {
|
||||
// Unnecessary in w3c implementation
|
||||
},
|
||||
|
||||
_expandEventType: function (type) {
|
||||
var ret = [type];
|
||||
|
||||
// install handlers for the events used to fake events of this
|
||||
// type, in addition to handlers for the real type
|
||||
|
||||
if (this.focusBlurMode === SIMULATE_FOCUS_BLUR) {
|
||||
if (type === 'focus')
|
||||
ret.push('focusin');
|
||||
else if (type === 'blur')
|
||||
ret.push('focusout');
|
||||
} else if (this.focusBlurMode === SIMULATE_FOCUSIN_FOCUSOUT) {
|
||||
if (type === 'focusin')
|
||||
ret.push('focus');
|
||||
else if (type === 'focusout')
|
||||
ret.push('blur');
|
||||
}
|
||||
if (this.simulateMouseEnterLeave) {
|
||||
if (type === 'mouseenter')
|
||||
ret.push('mouseover');
|
||||
else if (type === 'mouseleave')
|
||||
ret.push('mouseout');
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
_listen: function (types) {
|
||||
var self = this;
|
||||
_.each(types, function (type) {
|
||||
if ((self.typeCounts[type] = (self.typeCounts[type] || 0) + 1) === 1)
|
||||
document.addEventListener(type, self.boundCapturer, true);
|
||||
});
|
||||
},
|
||||
|
||||
_unlisten: function (types) {
|
||||
var self = this;
|
||||
_.each(types, function (type) {
|
||||
if (!(--self.typeCounts[type])) {
|
||||
document.removeEventListener(type, self.boundCapturer, true);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
capturer: function (event) {
|
||||
if (event.target.nodeType === 3) // fix text-node target
|
||||
event.target = event.target.parentNode;
|
||||
|
||||
var type = event.type;
|
||||
var bubbles = event.bubbles;
|
||||
var target = event.target;
|
||||
|
||||
target.addEventListener(type, this.boundHandler, false);
|
||||
|
||||
// According to the DOM event spec, if the DOM is mutated during
|
||||
// event handling, the original bubbling order still applies.
|
||||
// So we can determine the chain of nodes that could possibly
|
||||
// be bubbled to right now.
|
||||
var ancestors;
|
||||
if (bubbles) {
|
||||
ancestors = [];
|
||||
for(var n = target.parentNode; n; n = n.parentNode) {
|
||||
n.addEventListener(type, this.boundHandler, false);
|
||||
ancestors.push(n);
|
||||
};
|
||||
}
|
||||
|
||||
// Unbind the handlers later.
|
||||
setTimeout(function() {
|
||||
target.removeEventListener(type, this.boundHandler, false);
|
||||
if (bubbles) {
|
||||
_.each(ancestors, function(n) {
|
||||
n.removeEventListener(type, this.boundHandler, false);
|
||||
});
|
||||
};
|
||||
}, 0);
|
||||
},
|
||||
|
||||
handler: function (event) {
|
||||
var sendUIEvent = function (type, target, bubbles, cancelable, detail) {
|
||||
var evt = document.createEvent("UIEvents");
|
||||
evt.initUIEvent(type, bubbles, cancelable, window, detail);
|
||||
evt.synthetic = true;
|
||||
target.dispatchEvent(evt);
|
||||
};
|
||||
|
||||
// fire synthetic focusin/focusout on blur/focus or vice versa
|
||||
if (event.currentTarget === event.target) {
|
||||
if (this.focusBlurMode === SIMULATE_FOCUS_BLUR) {
|
||||
if (event.type === 'focusin')
|
||||
sendUIEvent('focus', event.target, false);
|
||||
else if (event.type === 'focusout')
|
||||
sendUIEvent('blur', event.target, false);
|
||||
} else if (this.focusBlurMode === SIMULATE_FOCUSIN_FOCUSOUT) {
|
||||
if (event.type === 'focus')
|
||||
sendUIEvent('focusin', event.target, true);
|
||||
else if (event.type === 'blur')
|
||||
sendUIEvent('focusout', event.target, true);
|
||||
}
|
||||
}
|
||||
// only respond to synthetic events of the types we are faking
|
||||
if (this.focusBlurMode === SIMULATE_FOCUS_BLUR) {
|
||||
if (event.type === 'focus' || event.type === 'blur') {
|
||||
if (! event.synthetic)
|
||||
return;
|
||||
}
|
||||
} else if (this.focusBlurMode === SIMULATE_FOCUSIN_FOCUSOUT) {
|
||||
if (event.type === 'focusin' || event.type === 'focusout') {
|
||||
if (! event.synthetic)
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.simulateMouseEnterLeave) {
|
||||
if (event.type === 'mouseenter' || event.type === 'mouseleave') {
|
||||
if (! event.synthetic)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.deliver(event);
|
||||
|
||||
// event ordering: fire mouseleave after mouseout
|
||||
if (this.simulateMouseEnterLeave &&
|
||||
// We respond to mouseover/mouseout here even on
|
||||
// bubble, i.e. when event.currentTarget !== event.target,
|
||||
// to ensure we see every enter and leave.
|
||||
// We ignore the case where the mouse enters from
|
||||
// a child or leaves to a child (by checking if
|
||||
// relatedTarget is present and a descendent).
|
||||
(! event.relatedTarget ||
|
||||
(event.currentTarget !== event.relatedTarget &&
|
||||
! DomUtils.elementContains(
|
||||
event.currentTarget, event.relatedTarget)))) {
|
||||
if (event.type === 'mouseover'){
|
||||
sendUIEvent('mouseenter', event.currentTarget, false);
|
||||
}
|
||||
else if (event.type === 'mouseout') {
|
||||
sendUIEvent('mouseleave', event.currentTarget, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
254
packages/universal-events/listener.js
Normal file
254
packages/universal-events/listener.js
Normal file
@@ -0,0 +1,254 @@
|
||||
// Meteor Universal Events -- Normalized cross-browser event handling library
|
||||
//
|
||||
// This module lets you set up a function f that will be called
|
||||
// whenever an event fires on any element in the DOM. Specifically,
|
||||
// when an event fires on node N, f will be called with N. Then, if
|
||||
// the event is a bubbling event, f will be called again with N's
|
||||
// parent, then called again with N's grandparent, etc, until the root
|
||||
// of the document is reached. This provides a good base on top of
|
||||
// which custom event handling systems can be implemented.
|
||||
//
|
||||
// f also receives the event object for the event that fired. The
|
||||
// event object is normalized and extended to smooth over
|
||||
// cross-browser differences in event handling. See the details in
|
||||
// setHandler.
|
||||
//
|
||||
// Usage:
|
||||
// var listener = new UniversalEventListener(function (event) { ... });
|
||||
// listener.addType("click");
|
||||
//
|
||||
// If you want to support IE <= 8, you must also call installHandler
|
||||
// on each subtree of DOM nodes on which you wish to receive events,
|
||||
// eg, before inserting them into the document.
|
||||
//
|
||||
// Universal Events works reliably for events that fire on any DOM
|
||||
// element. It may not work consistently across browsers for events
|
||||
// that are intended to fire on non-element nodes (eg, text nodes).
|
||||
// We're not sure if it's possible to handle those events consistently
|
||||
// across browsers, but in any event, it's not a common use case.
|
||||
//
|
||||
// Implementation notes:
|
||||
//
|
||||
// Internally, there are two separate implementations, one for modern
|
||||
// browsers (in events-w3c.js), and one for old browsers with no
|
||||
// event capturing support (in events-ie.js.) The correct
|
||||
// implementation will be chosen for you automatically at runtime.
|
||||
|
||||
(function () {
|
||||
|
||||
var listeners = [];
|
||||
|
||||
var returnFalse = function() { return false; };
|
||||
var returnTrue = function() { return true; };
|
||||
|
||||
// inspired by jquery fix()
|
||||
var normalizeEvent = function (event) {
|
||||
var originalStopPropagation = event.stopPropagation;
|
||||
var originalPreventDefault = event.preventDefault;
|
||||
event.isPropagationStopped = returnFalse;
|
||||
event.isImmediatePropagationStopped = returnFalse;
|
||||
event.isDefaultPrevented = returnFalse;
|
||||
event.stopPropagation = function() {
|
||||
event.isPropagationStopped = returnTrue;
|
||||
if (originalStopPropagation)
|
||||
originalStopPropagation.call(event);
|
||||
else
|
||||
event.cancelBubble = true; // IE
|
||||
};
|
||||
event.preventDefault = function() {
|
||||
event.isDefaultPrevented = returnTrue;
|
||||
if (originalPreventDefault)
|
||||
originalPreventDefault.call(event);
|
||||
else
|
||||
event.returnValue = false; // IE
|
||||
};
|
||||
event.stopImmediatePropagation = function() {
|
||||
event.stopPropagation();
|
||||
event.isImmediatePropagationStopped = returnTrue;
|
||||
};
|
||||
|
||||
var type = event.type;
|
||||
|
||||
// adapted from jquery
|
||||
if (event.metaKey === undefined)
|
||||
event.metaKey = event.ctrlKey;
|
||||
if (/^key/.test(type)) {
|
||||
// KEY EVENTS
|
||||
// Add which. Technically char codes and key codes are
|
||||
// different things; the former is ASCII/unicode/etc and the
|
||||
// latter is arbitrary. But browsers that lack charCode
|
||||
// seem to put character info in keyCode.
|
||||
// (foo == null) tests for null or undefined
|
||||
if (event.which == null)
|
||||
event.which = (event.charCode != null ? event.charCode : event.keyCode);
|
||||
} else if (/^(?:mouse|contextmenu)|click/.test(type)) {
|
||||
// MOUSE EVENTS
|
||||
// Add relatedTarget, if necessary
|
||||
if (! event.relatedTarget && event.fromElement)
|
||||
event.relatedTarget = (event.fromElement === event.target ?
|
||||
event.toElement : event.fromElement);
|
||||
// Add which for click: 1 === left; 2 === middle; 3 === right
|
||||
if (! event.which && event.button !== undefined ) {
|
||||
var button = event.button;
|
||||
event.which = (button & 1 ? 1 :
|
||||
(button & 2 ? 3 :
|
||||
(button & 4 ? 2 : 0 )));
|
||||
}
|
||||
}
|
||||
|
||||
return event;
|
||||
};
|
||||
|
||||
var deliver = function (event) {
|
||||
event = normalizeEvent(event);
|
||||
_.each(listeners, function (listener) {
|
||||
if (listener.types[event.type]) {
|
||||
// if in debug mode, filter out events where the user forgot
|
||||
// to call installHandler, even if we're not on IE
|
||||
if (!(listener._checkIECompliance &&
|
||||
! event.currentTarget['_uevents_test_eventtype_' + event.type]))
|
||||
listener.handler.call(null, event);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// When IE8 is dead, we can remove this springboard logic.
|
||||
var impl;
|
||||
var getImpl = function () {
|
||||
if (! impl)
|
||||
impl = (document.addEventListener ?
|
||||
new UniversalEventListener._impl.w3c(deliver) :
|
||||
new UniversalEventListener._impl.ie(deliver));
|
||||
return impl;
|
||||
};
|
||||
|
||||
var typeCounts = {};
|
||||
|
||||
|
||||
////////// PUBLIC API
|
||||
|
||||
// Create a new universal event listener with a given handler.
|
||||
// Until some event types are turned on with `addType`, the handler
|
||||
// will not receive any events.
|
||||
//
|
||||
// Whenever an event of the appropriate type fires anywhere in the
|
||||
// document, `handler` will be called with one argument, the
|
||||
// event. If the event is a bubbling event (most events are
|
||||
// bubbling, eg, 'click'), then `handler` will be called not only
|
||||
// for the element that was the origin of the event (eg, the button
|
||||
// that was clicked), but for each parent element as the event
|
||||
// bubbles up to the top of the tree.
|
||||
//
|
||||
// The event object that's passed to `handler` will be normalized
|
||||
// across browsers so that it contains the following fields and
|
||||
// methods:
|
||||
//
|
||||
// - type (e.g. "click")
|
||||
// - target
|
||||
// - currentTarget
|
||||
// - stopPropagation()
|
||||
// - preventDefault()
|
||||
// - isPropagationStopped()
|
||||
// - isDefaultPrevented()
|
||||
//
|
||||
// NOTE: If you want compatibility with IE <= 8, you will need to
|
||||
// call `installHandler` to prepare each subtree of the DOM to receive
|
||||
// the events you are interested in.
|
||||
//
|
||||
// Debugging only:
|
||||
//
|
||||
// The _checkIECompliance flag enables extra checking that the user
|
||||
// is correctly registering new DOM nodes with installHandler, even
|
||||
// in browsers that don't require it. In other words, when the flag
|
||||
// is set, modern browsers will require the same API calls as IE <=
|
||||
// 8. This is only used for tests and is private for now.
|
||||
UniversalEventListener = function (handler, _checkIECompliance) {
|
||||
this.handler = handler;
|
||||
this.types = {}; // map from event type name to 'true'
|
||||
this.impl = getImpl();
|
||||
this._checkIECompliance = _checkIECompliance;
|
||||
listeners.push(this);
|
||||
};
|
||||
|
||||
_.extend(UniversalEventListener.prototype, {
|
||||
// Adds `type` to the set of event types that this listener will
|
||||
// listen to and deliver to the handler. Has no effect if `type`
|
||||
// is already in the set.
|
||||
addType: function (type) {
|
||||
if (!this.types[type]) {
|
||||
this.types[type] = true;
|
||||
typeCounts[type] = (typeCounts[type] || 0) + 1;
|
||||
if (typeCounts[type] === 1)
|
||||
this.impl.addType(type);
|
||||
}
|
||||
},
|
||||
|
||||
// Removes `type` from the set of event types that this listener
|
||||
// will listen to and deliver to the handler. Has no effect if `type`
|
||||
// is not in the set.
|
||||
removeType: function (type) {
|
||||
if (this.types[type]) {
|
||||
delete this.types[type];
|
||||
typeCounts[type]--;
|
||||
if (! typeCounts[type])
|
||||
this.impl.removeType(type);
|
||||
}
|
||||
},
|
||||
|
||||
// It is only necessary to call this method if you want to support
|
||||
// IE <= 8. On those browsers, you must call this method on each
|
||||
// set of nodes before adding them to the DOM (or at least, before
|
||||
// expecting to receive events on them), and you must specify the
|
||||
// types of events you'll be receiving.
|
||||
//
|
||||
// `node` and all of its descendents will be set up to handle
|
||||
// events of type `type` (eg, 'click'). Only current descendents
|
||||
// of `node` are affected; if new nodes are added to the subtree
|
||||
// later, installHandler must be called again to ensure events are
|
||||
// received on those nodes. To set up to handle multiple event
|
||||
// types, make multiple calls.
|
||||
//
|
||||
// It is safe to call installHandler any number of times on the same
|
||||
// arguments (it is idempotent).
|
||||
//
|
||||
// If you forget to call this function for a given node, it's
|
||||
// unspecified whether you'll receive events on IE <= 8 (you may,
|
||||
// you may not.) If you don't care about supporting IE <= 8 you
|
||||
// can ignore this function.
|
||||
installHandler: function (node, type) {
|
||||
// Only work on element nodes, not e.g. text nodes or fragments
|
||||
if (node.nodeType !== 1)
|
||||
return;
|
||||
this.impl.installHandler(node, type);
|
||||
|
||||
// When in checkIECompliance mode, mark all the nodes in the current subtree.
|
||||
// We will later block events on nodes that weren't marked. This
|
||||
// tests that Spark is generating calls to registerEventType
|
||||
// with proper subtree information, even in browsers that don't need
|
||||
// it.
|
||||
if (this._checkIECompliance) {
|
||||
// set flag to mark the node for this type, recording the
|
||||
// fact that installHandler was called for this node and type.
|
||||
// the property value can be any non-primitive value (to prevent
|
||||
// showing up as an HTML attribute in IE) so we use `node` itself.
|
||||
node['_uevents_test_eventtype_'+type] = node;
|
||||
if (node.firstChild) {
|
||||
_.each(node.getElementsByTagName('*'), function(x) {
|
||||
x['_uevents_test_eventtype_'+type] = x;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Tear down this UniversalEventListener so that no more events
|
||||
// are delivered.
|
||||
destroy: function () {
|
||||
var self = this;
|
||||
|
||||
listeners = _.without(listeners, self);
|
||||
_.each(_.keys(self.types), function (type) {
|
||||
self.removeType(type);
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
23
packages/universal-events/package.js
Normal file
23
packages/universal-events/package.js
Normal file
@@ -0,0 +1,23 @@
|
||||
Package.describe({
|
||||
summary: "Listen to events globally, and normalize them",
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use(['underscore'], 'client');
|
||||
|
||||
api.add_files(['listener.js',
|
||||
'events-w3c.js',
|
||||
'events-ie.js'], 'client');
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use('tinytest');
|
||||
api.use(['universal-events', 'test-helpers'], 'client');
|
||||
api.use('domutils');
|
||||
api.use('spark');
|
||||
|
||||
api.add_files([
|
||||
'event_tests.js'
|
||||
], 'client');
|
||||
});
|
||||
Reference in New Issue
Block a user