mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Instead of a general client-side sub de-duping mechanism (which mostly existed
for the sake of autosubscribe, and causes issues with server-driven
unsubscribes), make Meteor.subscribe explicitly aware of reactivity.
Expose an "invalidated" flag on Meteor.deps.Context.
Guarantee that invalidation callbacks from different contexts will not be
interleaved at flush time. This has the implication that if you do
context1.onInvalidate(function () {
context2.invalidate();
});
and this is the only way to invalidate context2, then context2's invalidation
callbacks will not be called until after *ALL* of context1's callbacks are
called. This allows us to be sure that the "unsubscribe, unless autorun tried to
re-create an identical sub" logic runs after the autorun function is rerun.
525 lines
16 KiB
JavaScript
525 lines
16 KiB
JavaScript
METEOR_VERSION = "0.5.4";
|
|
|
|
Meteor.startup(function () {
|
|
// XXX this is broken by the new multi-page layout. Also, it was
|
|
// broken before the multi-page layout because it had illegible
|
|
// colors. Just turn it off for now. We'll fix it and turn it on
|
|
// later.
|
|
// prettyPrint();
|
|
|
|
// returns a jQuery object suitable for setting scrollTop to
|
|
// scroll the page, either directly for via animate()
|
|
var scroller = function() {
|
|
return $("html, body").stop();
|
|
};
|
|
|
|
var sections = [];
|
|
_.each($('#main h1, #main h2, #main h3'), function (elt) {
|
|
var classes = (elt.getAttribute('class') || '').split(/\s+/);
|
|
if (_.indexOf(classes, "nosection") === -1)
|
|
sections.push(elt);
|
|
});
|
|
|
|
for (var i = 0; i < sections.length; i++) {
|
|
var classes = (sections[i].getAttribute('class') || '').split(/\s+/);
|
|
if (_.indexOf(classes, "nosection") !== -1)
|
|
continue;
|
|
sections[i].prev = sections[i-1] || sections[i];
|
|
sections[i].next = sections[i+1] || sections[i];
|
|
$(sections[i]).waypoint({offset: 30});
|
|
}
|
|
var section = document.location.hash.substr(1) || sections[0].id;
|
|
Session.set('section', section);
|
|
if (section) {
|
|
// WebKit will scroll down to the #id in the URL asynchronously
|
|
// after the page is rendered, but Firefox won't.
|
|
Meteor.setTimeout(function() {
|
|
var elem = $('#'+section);
|
|
if (elem.length)
|
|
scroller().scrollTop(elem.offset().top);
|
|
}, 0);
|
|
}
|
|
|
|
var ignore_waypoints = false;
|
|
$('body').delegate('h1, h2, h3', 'waypoint.reached', function (evt, dir) {
|
|
if (!ignore_waypoints) {
|
|
var active = (dir === "up") ? this.prev : this;
|
|
Session.set("section", active.id);
|
|
}
|
|
});
|
|
|
|
window.onhashchange = function () {
|
|
scrollToSection(location.hash);
|
|
};
|
|
|
|
var scrollToSection = function (section) {
|
|
ignore_waypoints = true;
|
|
Session.set("section", section.substr(1));
|
|
scroller().animate({
|
|
scrollTop: $(section).offset().top
|
|
}, 500, 'swing', function () {
|
|
window.location.hash = section;
|
|
ignore_waypoints = false;
|
|
});
|
|
};
|
|
|
|
$('#main, #nav').delegate("a[href^='#']", 'click', function (evt) {
|
|
evt.preventDefault();
|
|
var sel = $(this).attr('href');
|
|
scrollToSection(sel);
|
|
});
|
|
|
|
// Make external links open in a new tab.
|
|
$('a:not([href^="#"])').attr('target', '_blank');
|
|
});
|
|
|
|
var toc = [
|
|
{name: "Meteor " + METEOR_VERSION, id: "top"}, [
|
|
"Quick start",
|
|
"Seven principles",
|
|
"Resources"
|
|
],
|
|
"Concepts", [
|
|
"Structuring your app",
|
|
"Data and security",
|
|
"Reactivity",
|
|
"Live HTML",
|
|
"Templates",
|
|
"Smart packages",
|
|
"Deploying"
|
|
],
|
|
|
|
"API", [
|
|
"Core", [
|
|
"Meteor.isClient",
|
|
"Meteor.isServer",
|
|
"Meteor.startup",
|
|
"Meteor.absoluteUrl",
|
|
"Meteor.settings"
|
|
],
|
|
|
|
"Publish and subscribe", [
|
|
"Meteor.publish", [
|
|
{instance: "this", name: "userId", id: "publish_userId"},
|
|
{instance: "this", name: "set", id: "publish_set"},
|
|
{instance: "this", name: "unset", id: "publish_unset"},
|
|
{instance: "this", name: "complete", id: "publish_complete"},
|
|
{instance: "this", name: "flush", id: "publish_flush"},
|
|
{instance: "this", name: "onStop", id: "publish_onstop"},
|
|
{instance: "this", name: "stop", id: "publish_stop"}
|
|
],
|
|
"Meteor.subscribe"
|
|
],
|
|
|
|
{name: "Methods", id: "methods_header"}, [
|
|
"Meteor.methods", [
|
|
{instance: "this", name: "userId", id: "method_userId"},
|
|
{instance: "this", name: "setUserId", id: "method_setUserId"},
|
|
{instance: "this", name: "isSimulation", id: "method_issimulation"},
|
|
{instance: "this", name: "unblock", id: "method_unblock"}
|
|
],
|
|
"Meteor.Error",
|
|
"Meteor.call",
|
|
"Meteor.apply"
|
|
],
|
|
|
|
{name: "Server connections", id: "connections"}, [
|
|
"Meteor.status",
|
|
"Meteor.reconnect",
|
|
"Meteor.connect"
|
|
],
|
|
|
|
{name: "Collections", id: "collections"}, [
|
|
"Meteor.Collection", [
|
|
{instance: "collection", name: "find"},
|
|
{instance: "collection", name: "findOne"},
|
|
{instance: "collection", name: "insert"},
|
|
{instance: "collection", name: "update"},
|
|
{instance: "collection", name: "remove"},
|
|
{instance: "collection", name: "allow"},
|
|
{instance: "collection", name: "deny"}
|
|
],
|
|
|
|
"Meteor.Collection.Cursor", [
|
|
{instance: "cursor", name: "forEach"},
|
|
{instance: "cursor", name: "map"},
|
|
{instance: "cursor", name: "fetch"},
|
|
{instance: "cursor", name: "count"},
|
|
{instance: "cursor", name: "rewind"},
|
|
{instance: "cursor", name: "observe"}
|
|
],
|
|
{type: "spacer"},
|
|
"Meteor.uuid",
|
|
{type: "spacer"},
|
|
{name: "Selectors", style: "noncode"},
|
|
{name: "Modifiers", style: "noncode"},
|
|
{name: "Sort specifiers", style: "noncode"},
|
|
{name: "Field specifiers", style: "noncode"}
|
|
],
|
|
|
|
"Session", [
|
|
"Session.set",
|
|
"Session.get",
|
|
"Session.equals"
|
|
],
|
|
|
|
{name: "Accounts", id: "accounts_api"}, [
|
|
"Meteor.user",
|
|
"Meteor.userId",
|
|
"Meteor.users",
|
|
"Meteor.loggingIn",
|
|
"Meteor.logout",
|
|
"Meteor.loginWithPassword",
|
|
{name: "Meteor.loginWithFacebook", id: "meteor_loginwithexternalservice"},
|
|
{name: "Meteor.loginWithGithub", id: "meteor_loginwithexternalservice"},
|
|
{name: "Meteor.loginWithGoogle", id: "meteor_loginwithexternalservice"},
|
|
{name: "Meteor.loginWithTwitter", id: "meteor_loginwithexternalservice"},
|
|
{name: "Meteor.loginWithWeibo", id: "meteor_loginwithexternalservice"},
|
|
{type: "spacer"},
|
|
|
|
{name: "{{currentUser}}", id: "template_currentuser"},
|
|
{name: "{{loggingIn}}", id: "template_loggingin"},
|
|
{type: "spacer"},
|
|
|
|
"Accounts.config",
|
|
"Accounts.ui.config",
|
|
"Accounts.validateNewUser",
|
|
"Accounts.onCreateUser"
|
|
],
|
|
|
|
{name: "Passwords", id: "accounts_passwords"}, [
|
|
"Accounts.createUser",
|
|
"Accounts.changePassword",
|
|
"Accounts.forgotPassword",
|
|
"Accounts.resetPassword",
|
|
"Accounts.setPassword",
|
|
"Accounts.verifyEmail",
|
|
{type: "spacer"},
|
|
|
|
"Accounts.sendResetPasswordEmail",
|
|
"Accounts.sendEnrollmentEmail",
|
|
"Accounts.sendVerificationEmail",
|
|
"Accounts.emailTemplates"
|
|
],
|
|
|
|
{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: "Constant regions", style: "noncode", id: "constant"},
|
|
{name: "Reactivity isolation", style: "noncode", id: "isolate"}
|
|
],
|
|
|
|
"Timers", [
|
|
"Meteor.setTimeout",
|
|
"Meteor.setInterval",
|
|
"Meteor.clearTimeout",
|
|
"Meteor.clearInterval"
|
|
],
|
|
|
|
"Meteor.deps", [
|
|
{name: "Meteor.deps.Context", id: "context"}, [
|
|
{instance: "context", name: "run"},
|
|
{instance: "context", name: "onInvalidate", id: "oninvalidate"},
|
|
{instance: "context", name: "invalidate"}
|
|
],
|
|
{name: "Meteor.deps.Context.current", id: "current"},
|
|
"Meteor.autorun",
|
|
"Meteor.flush"
|
|
// ],
|
|
|
|
// "Environment Variables", [
|
|
// "Meteor.EnvironmentVariable", [
|
|
// {instance: "env_var", name: "get", id: "env_var_get"},
|
|
// {instance: "env_var", name: "withValue", id: "env_var_withvalue"},
|
|
// {instance: "env_var", name: "bindEnvironment", id: "env_var_bindenvironment"}
|
|
// ]
|
|
],
|
|
|
|
"Meteor.http", [
|
|
"Meteor.http.call",
|
|
{name: "Meteor.http.get", id: "meteor_http_get"},
|
|
{name: "Meteor.http.post", id: "meteor_http_post"},
|
|
{name: "Meteor.http.put", id: "meteor_http_put"},
|
|
{name: "Meteor.http.del", id: "meteor_http_del"}
|
|
],
|
|
"Email", [
|
|
"Email.send"
|
|
]
|
|
],
|
|
|
|
"Packages", [ [
|
|
"accounts-ui",
|
|
"amplify",
|
|
"backbone",
|
|
"bootstrap",
|
|
"coffeescript",
|
|
"d3",
|
|
"force-ssl",
|
|
"jquery",
|
|
"less",
|
|
"spiderable",
|
|
"stylus",
|
|
"showdown",
|
|
"underscore"
|
|
] ],
|
|
|
|
"Command line", [ [
|
|
"meteor help",
|
|
"meteor run",
|
|
"meteor create",
|
|
"meteor deploy",
|
|
"meteor logs",
|
|
"meteor update",
|
|
"meteor add",
|
|
"meteor remove",
|
|
"meteor list",
|
|
"meteor mongo",
|
|
"meteor reset",
|
|
"meteor bundle"
|
|
] ]
|
|
];
|
|
|
|
var name_to_id = function (name) {
|
|
var x = name.toLowerCase().replace(/[^a-z0-9_,.]/g, '').replace(/[,.]/g, '_');
|
|
return x;
|
|
};
|
|
|
|
Template.nav.sections = function () {
|
|
var ret = [];
|
|
var walk = function (items, depth) {
|
|
_.each(items, function (item) {
|
|
if (item instanceof Array)
|
|
walk(item, depth + 1);
|
|
else {
|
|
if (typeof(item) === "string")
|
|
item = {name: item};
|
|
ret.push(_.extend({
|
|
type: "section",
|
|
id: item.name && name_to_id(item.name) || undefined,
|
|
depth: depth,
|
|
style: ''
|
|
}, item));
|
|
}
|
|
});
|
|
};
|
|
|
|
walk(toc, 1);
|
|
return ret;
|
|
};
|
|
|
|
Template.nav.type = function (what) {
|
|
return this.type === what;
|
|
}
|
|
|
|
Template.nav.maybe_current = function () {
|
|
return Session.equals("section", this.id) ? "current" : "";
|
|
};
|
|
|
|
Handlebars.registerHelper('warning', function(fn) {
|
|
return Template.warning_helper(fn(this));
|
|
});
|
|
|
|
Handlebars.registerHelper('note', function(fn) {
|
|
return Template.note_helper(fn(this));
|
|
});
|
|
|
|
// "name" argument may be provided as part of options.hash instead.
|
|
Handlebars.registerHelper('dtdd', function(name, options) {
|
|
if (options && options.hash) {
|
|
// {{#dtdd name}}
|
|
options.hash.name = name;
|
|
} else {
|
|
// {{#dtdd name="foo" type="bar"}}
|
|
options = name;
|
|
}
|
|
|
|
return Template.dtdd_helper({descr: options.fn(this),
|
|
name: options.hash.name,
|
|
type: options.hash.type});
|
|
});
|
|
|
|
Handlebars.registerHelper('better_markdown', function(fn) {
|
|
var converter = new Showdown.converter();
|
|
var input = fn(this);
|
|
|
|
///////
|
|
// Make Markdown *actually* skip over block-level elements when
|
|
// processing a string.
|
|
//
|
|
// Official Markdown doesn't descend into
|
|
// block elements written out as HTML (divs, tables, etc.), BUT
|
|
// it doesn't skip them properly either. It assumes they are
|
|
// either pretty-printed with their contents indented, or, failing
|
|
// that, it just scans for a close tag with the same name, and takes
|
|
// it regardless of whether it is the right one. As a hack to work
|
|
// around Markdown's hacks, we find the block-level elements
|
|
// using a proper recursive method and rewrite them to be indented
|
|
// with the final close tag on its own line.
|
|
///////
|
|
|
|
// Open-block tag should be at beginning of line,
|
|
// and not, say, in a string literal in example code, or in a pre block.
|
|
// Tag must be followed by a non-word-char so that we match whole tag, not
|
|
// eg P for PRE. All regexes we wish to use when scanning must have
|
|
// 'g' flag so that they respect (and set) lastIndex.
|
|
// Assume all tags are lowercase.
|
|
var rOpenBlockTag = /^\s{0,2}<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)(?=\W)/mg;
|
|
var rTag = /<(\/?\w+)/g;
|
|
var idx = 0;
|
|
var newParts = [];
|
|
var blockBuf = [];
|
|
// helper function to execute regex `r` starting at idx and putting
|
|
// the end index back into idx; accumulate the intervening string
|
|
// into an array; and return the regex's first capturing group.
|
|
var rcall = function(r, inBlock) {
|
|
var lastIndex = idx;
|
|
r.lastIndex = lastIndex;
|
|
var match = r.exec(input);
|
|
var result = null;
|
|
if (! match) {
|
|
idx = input.length;
|
|
} else {
|
|
idx = r.lastIndex;
|
|
result = match[1];
|
|
}
|
|
(inBlock ? blockBuf : newParts).push(input.substring(lastIndex, idx));
|
|
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 = {};
|
|
var numHashedBlocks = 0;
|
|
|
|
var nestedTags = [];
|
|
while (idx < input.length) {
|
|
var blockTag = rcall(rOpenBlockTag, false);
|
|
if (blockTag) {
|
|
nestedTags.push(blockTag);
|
|
while (nestedTags.length) {
|
|
var tag = rcall(rTag, true);
|
|
if (! tag) {
|
|
throw new Error("Expected </"+nestedTags[nestedTags.length-1]+
|
|
"> but found end of string");
|
|
} else if (tag.charAt(0) === '/') {
|
|
// close tag
|
|
var tagToPop = tag.substring(1);
|
|
var tagPopped = nestedTags.pop();
|
|
if (tagPopped !== tagToPop)
|
|
throw new Error(("Mismatched close tag, expected </"+tagPopped+
|
|
"> but found </"+tagToPop+">: "+
|
|
input.substr(idx-50,50)+"{HERE}"+
|
|
input.substr(idx,50)).replace(/\n/g,'\\n'));
|
|
} else {
|
|
// open tag
|
|
nestedTags.push(tag);
|
|
}
|
|
}
|
|
var newBlock = blockBuf.join('');
|
|
var openTagFinish = newBlock.indexOf('>') + 1;
|
|
var closeTagLoc = newBlock.lastIndexOf('<');
|
|
|
|
var key = ++numHashedBlocks;
|
|
hashedBlocks[key] = newBlock.slice(openTagFinish, closeTagLoc);
|
|
newParts.push(newBlock.slice(0, openTagFinish),
|
|
'!!!!HTML:'+key+'!!!!',
|
|
newBlock.slice(closeTagLoc));
|
|
blockBuf.length = 0;
|
|
}
|
|
}
|
|
|
|
var newInput = newParts.join('');
|
|
var output = converter.makeHtml(newInput);
|
|
|
|
output = output.replace(/!!!!HTML:(.*?)!!!!/g, function(z, a) {
|
|
return hashedBlocks[a];
|
|
});
|
|
|
|
output = output.replace(/<!--(\/?\$.*?)-->/g, '<$1>');
|
|
|
|
return output;
|
|
});
|
|
|
|
Handlebars.registerHelper('dstache', function() {
|
|
return '{{';
|
|
});
|
|
|
|
Handlebars.registerHelper('tstache', function() {
|
|
return '{{{';
|
|
});
|
|
|
|
Handlebars.registerHelper('api_section', function(id, nameFn) {
|
|
return Template.api_section_helper(
|
|
{name: nameFn(this), id:id}, true);
|
|
});
|
|
|
|
Handlebars.registerHelper('api_box_inline', function(box, fn) {
|
|
return Template.api_box(_.extend(box, {body: fn(this)}), true);
|
|
});
|
|
|
|
Template.api_box.bare = function() {
|
|
return ((this.descr && this.descr.length) ||
|
|
(this.args && this.args.length) ||
|
|
(this.options && this.options.length)) ? "" : "bareapi";
|
|
};
|
|
|
|
var check_links = function() {
|
|
var body = document.body.innerHTML;
|
|
|
|
var id_set = {};
|
|
|
|
body.replace(/id\s*=\s*"(.*?)"/g, function(match, id) {
|
|
if (! id) return;
|
|
if (id_set['$'+id]) {
|
|
console.log("ERROR: Duplicate id: "+id);
|
|
} else {
|
|
id_set['$'+id] = true;
|
|
}
|
|
});
|
|
|
|
body.replace(/"#(.*?)"/g, function(match, frag) {
|
|
if (! frag) return;
|
|
if (! id_set['$'+frag]) {
|
|
var suggestions = [];
|
|
_.each(_.keys(id_set), function(id) {
|
|
id = id.slice(1);
|
|
if (id.slice(-frag.length) === frag ||
|
|
frag.slice(-id.length) === id) {
|
|
suggestions.push(id);
|
|
}
|
|
});
|
|
var msg = "ERROR: id not found: "+frag;
|
|
if (suggestions.length > 0) {
|
|
msg += " -- suggest "+suggestions.join(', ');
|
|
}
|
|
console.log(msg);
|
|
}
|
|
});
|
|
|
|
return "DONE";
|
|
};
|