From 7db8e5a0748a38cfdb320dca6115051b0fc58221 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Mon, 21 Apr 2014 21:00:41 -0700 Subject: [PATCH 001/223] Import of "minimal, stand-alone Blaze" experiment --- .../unfinished/blaze-test/.meteor/.gitignore | 1 + .../unfinished/blaze-test/.meteor/packages | 9 + .../unfinished/blaze-test/.meteor/release | 1 + .../blaze-test/client/blaze-test.css | 1 + .../blaze-test/client/blaze-test.html | 7 + .../blaze-test/client/blaze-test.js | 132 +++ packages/blaze/.gitignore | 1 + packages/blaze/attrs.js | 240 +++++ packages/blaze/blaze.js | 912 ++++++++++++++++++ packages/blaze/html.js | 415 ++++++++ packages/blaze/microscore.js | 110 +++ packages/blaze/package.js | 27 + 12 files changed, 1856 insertions(+) create mode 100644 examples/unfinished/blaze-test/.meteor/.gitignore create mode 100644 examples/unfinished/blaze-test/.meteor/packages create mode 100644 examples/unfinished/blaze-test/.meteor/release create mode 100644 examples/unfinished/blaze-test/client/blaze-test.css create mode 100644 examples/unfinished/blaze-test/client/blaze-test.html create mode 100644 examples/unfinished/blaze-test/client/blaze-test.js create mode 100644 packages/blaze/.gitignore create mode 100644 packages/blaze/attrs.js create mode 100644 packages/blaze/blaze.js create mode 100644 packages/blaze/html.js create mode 100644 packages/blaze/microscore.js create mode 100644 packages/blaze/package.js diff --git a/examples/unfinished/blaze-test/.meteor/.gitignore b/examples/unfinished/blaze-test/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/examples/unfinished/blaze-test/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/examples/unfinished/blaze-test/.meteor/packages b/examples/unfinished/blaze-test/.meteor/packages new file mode 100644 index 0000000000..9ea858d8c3 --- /dev/null +++ b/examples/unfinished/blaze-test/.meteor/packages @@ -0,0 +1,9 @@ +# 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. + +standard-app-packages +autopublish +insecure +blaze diff --git a/examples/unfinished/blaze-test/.meteor/release b/examples/unfinished/blaze-test/.meteor/release new file mode 100644 index 0000000000..621e94f0ec --- /dev/null +++ b/examples/unfinished/blaze-test/.meteor/release @@ -0,0 +1 @@ +none diff --git a/examples/unfinished/blaze-test/client/blaze-test.css b/examples/unfinished/blaze-test/client/blaze-test.css new file mode 100644 index 0000000000..b6b4052b43 --- /dev/null +++ b/examples/unfinished/blaze-test/client/blaze-test.css @@ -0,0 +1 @@ +/* CSS declarations go here */ diff --git a/examples/unfinished/blaze-test/client/blaze-test.html b/examples/unfinished/blaze-test/client/blaze-test.html new file mode 100644 index 0000000000..4cbb558b2e --- /dev/null +++ b/examples/unfinished/blaze-test/client/blaze-test.html @@ -0,0 +1,7 @@ + + blaze-test + + + + + diff --git a/examples/unfinished/blaze-test/client/blaze-test.js b/examples/unfinished/blaze-test/client/blaze-test.js new file mode 100644 index 0000000000..8e3da769f5 --- /dev/null +++ b/examples/unfinished/blaze-test/client/blaze-test.js @@ -0,0 +1,132 @@ +Meteor.startup(function () { + +Blaze._onAutorun = function (c) { + console.log('Created #' + c._id); + var callback = function () { + if (c.stopped) { + console.log('Stopped #' + c._id); + } else { + console.log('Invalidated #' + c._id); + Deps.afterFlush(function () { + c.onInvalidate(callback); + }); + } + }; + c.onInvalidate(callback); +}; + +theNumber = Blaze.Var(0); +theColor = Blaze.Var('yellow'); + +If = function (conditionVar, contentFunc, elseFunc) { + return Blaze.Isolate(function () { + return conditionVar.get() ? contentFunc() : + (elseFunc ? elseFunc() : null); + }); +}; + +With = function (dataVar, func) { + if (! (this instanceof With)) + // called without new + return new With(dataVar, func); + + this.data = dataVar; + this.func = func; +}; +Blaze.__extends(With, Blaze.Controller); + +_.extend(With.prototype, { + render: function () { + var func = this.func; + return func(); + } +}); + +Repeat = function (countVar, contentFunc) { + var seq, count; + var comp = Deps.autorun(function () { + if (! seq) { + count = countVar.get(); + if (typeof count !== 'number') + throw new Error("Expected number"); + var funcs = new Array(count); + for (var i = 0; i < count; i++) + funcs[i] = contentFunc; + seq = new Blaze.Sequence(funcs); + } else { + var targetCount = countVar.get(); + while (count < targetCount) { + seq.addItem(contentFunc, count); + count++; + } + while (count > targetCount) { + seq.removeItem(count-1); + count--; + } + } + }); + Blaze._onAutorun(comp); + return Blaze.List(seq); +}; + +Ticker = function () { + var self = this; + Blaze.Component.call(self); + self.time = Blaze.Var(new Date); + self.timer = setInterval(function () { + self.time.set(new Date); + }, 1000); +}; +Blaze.__extends(Ticker, Blaze.Component); +_.extend(Ticker.prototype, { + render: function () { + var self = this; + return Blaze.Isolate(function () { + return String(self.time.get()); + }); + }, + finalize: function () { + clearInterval(this.timer); + } +}); + +HTML = Blaze.HTML; + +outerRange = Blaze.render(function () { + return [HTML.DIV( + {style: If(Blaze.Var(function () { return theNumber.get() % 3 === 0; }), + function () { return ['background:', theColor.get()]; })}, + "The number ", Blaze.Isolate(function () { return theNumber.get(); }), " is ", + If(Blaze.Var(function () { + return theNumber.get() % 2 === 0; + }), function () { + return "even"; + }, function () { + return "odd"; + }), "."), + HTML.UL(Repeat(theNumber, + function () { + return With(Blaze.Var(123), function () { + return HTML.LI( + Blaze.Isolate(function () { + console.log('Context:', Blaze.currentController.data.get()); + return theNumber.get(); }), + " - ", new Ticker + ); + }); + }))]; + +}); +outerRange.attach(document.body); + + +// Now, run: +// +// ``` +// theNumber.set(1); +// theNumber.set(2); +// +// outerRange.stop(); +// ``` + +}); \ No newline at end of file diff --git a/packages/blaze/.gitignore b/packages/blaze/.gitignore new file mode 100644 index 0000000000..677a6fc263 --- /dev/null +++ b/packages/blaze/.gitignore @@ -0,0 +1 @@ +.build* diff --git a/packages/blaze/attrs.js b/packages/blaze/attrs.js new file mode 100644 index 0000000000..7025c88ccf --- /dev/null +++ b/packages/blaze/attrs.js @@ -0,0 +1,240 @@ + +// An AttributeHandler object is responsible for updating a particular attribute +// of a particular element. AttributeHandler subclasses implement +// browser-specific logic for dealing with particular attributes across +// different browsers. +// +// To define a new type of AttributeHandler, use +// `var FooHandler = AttributeHandler.extend({ update: function ... })` +// where the `update` function takes arguments `(element, oldValue, value)`. +// The `element` argument is always the same between calls to `update` on +// the same instance. `oldValue` and `value` are each either `null` or +// a Unicode string of the type that might be passed to the value argument +// of `setAttribute` (i.e. not an HTML string with character references). +// When an AttributeHandler is installed, an initial call to `update` is +// always made with `oldValue = null`. The `update` method can access +// `this.name` if the AttributeHandler class is a generic one that applies +// to multiple attribute names. +// +// AttributeHandlers can store custom properties on `this`, as long as they +// don't use the names `element`, `name`, `value`, and `oldValue`. +// +// AttributeHandlers can't influence how attributes appear in rendered HTML, +// only how they are updated after materialization as DOM. + +AttributeHandler = function (name, value) { + this.name = name; + this.value = value; +}; + +AttributeHandler.prototype.update = function (element, oldValue, value) { + if (value === null) { + if (oldValue !== null) + element.removeAttribute(this.name); + } else { + element.setAttribute(this.name, value); + } +}; + +AttributeHandler.extend = function (options) { + var curType = this; + var subType = function AttributeHandlerSubtype(/*arguments*/) { + AttributeHandler.apply(this, arguments); + }; + subType.prototype = new curType; + subType.extend = curType.extend; + if (options) + _.extend(subType.prototype, options); + return subType; +}; + +// Extended below to support both regular and SVG elements +var BaseClassHandler = AttributeHandler.extend({ + update: function (element, oldValue, value) { + if (!this.getCurrentValue || !this.setValue) + throw new Error("Missing methods in subclass of 'BaseClassHandler'"); + + var oldClasses = oldValue ? _.compact(oldValue.split(' ')) : []; + var newClasses = value ? _.compact(value.split(' ')) : []; + + // the current classes on the element, which we will mutate. + var classes = _.compact(this.getCurrentValue(element).split(' ')); + + // optimize this later (to be asymptotically faster) if necessary + for (var i = 0; i < oldClasses.length; i++) { + var c = oldClasses[i]; + if (! _.contains(newClasses, c)) + classes = _.without(classes, c); + } + for (var i = 0; i < newClasses.length; i++) { + var c = newClasses[i]; + if ((! _.contains(oldClasses, c)) && + (! _.contains(classes, c))) + classes.push(c); + } + + this.setValue(element, classes.join(' ')); + } +}); + +var ClassHandler = BaseClassHandler.extend({ + // @param rawValue {String} + getCurrentValue: function (element) { + return element.className; + }, + setValue: function (element, className) { + element.className = className; + } +}); + +var SVGClassHandler = BaseClassHandler.extend({ + getCurrentValue: function (element) { + return element.className.baseVal; + }, + setValue: function (element, className) { + element.setAttribute('class', className); + } +}); + +var BooleanHandler = AttributeHandler.extend({ + update: function (element, oldValue, value) { + var focused = this.focused(element); + + if (!focused) { + var name = this.name; + if (value == null) { + if (oldValue != null) + element[name] = false; + } else { + element[name] = true; + } + } + }, + // is the element part of a control which is focused? + focused: function (element) { + if (element.tagName === 'INPUT') { + return element === document.activeElement; + + } else if (element.tagName === 'OPTION') { + // find the containing SELECT element, on which focus + // is actually set + var selectEl = element; + while (selectEl && selectEl.tagName !== 'SELECT') + selectEl = selectEl.parentNode; + + if (selectEl) + return selectEl === document.activeElement; + else + return false; + } else { + throw new Error("Expected INPUT or OPTION element"); + } + } +}); + +var ValueHandler = AttributeHandler.extend({ + update: function (element, oldValue, value) { + var focused = (element === document.activeElement); + + if (!focused) + element.value = value; + } +}); + +// attributes of the type 'xlink:something' should be set using +// the correct namespace in order to work +var XlinkHandler = AttributeHandler.extend({ + update: function(element, oldValue, value) { + var NS = 'http://www.w3.org/1999/xlink'; + if (value === null) { + if (oldValue !== null) + element.removeAttributeNS(NS, this.name); + } else { + element.setAttributeNS(NS, this.name, this.value); + } + } +}); + +// cross-browser version of `instanceof SVGElement` +var isSVGElement = function (elem) { + return 'ownerSVGElement' in elem; +}; + +// XXX make it possible for users to register attribute handlers! +makeAttributeHandler = function (elem, name, value) { + // generally, use setAttribute but certain attributes need to be set + // by directly setting a JavaScript property on the DOM element. + if (name === 'class') { + if (isSVGElement(elem)) { + return new SVGClassHandler(name, value); + } else { + return new ClassHandler(name, value); + } + } else if ((elem.tagName === 'OPTION' && name === 'selected') || + (elem.tagName === 'INPUT' && name === 'checked')) { + return new BooleanHandler(name, value); + } else if ((elem.tagName === 'TEXTAREA' || elem.tagName === 'INPUT') + && name === 'value') { + // internally, TEXTAREAs tracks their value in the 'value' + // attribute just like INPUTs. + return new ValueHandler(name, value); + } else if (name.substring(0,6) === 'xlink:') { + return new XlinkHandler(name.substring(6), value); + } else { + return new AttributeHandler(name, value); + } + + // XXX will need one for 'style' on IE, though modern browsers + // seem to handle setAttribute ok. +}; + + +ElementAttributesUpdater = function (elem) { + this.elem = elem; + this.handlers = {}; +}; + +// Update attributes on `elem` to the dictionary `attrs`, whose +// values are strings. +ElementAttributesUpdater.prototype.update = function(newAttrs) { + var elem = this.elem; + var handlers = this.handlers; + + for (var k in handlers) { + if (! newAttrs.hasOwnProperty(k)) { + // remove attributes (and handlers) for attribute names + // that don't exist as keys of `newAttrs` and so won't + // be visited when traversing it. (Attributes that + // exist in the `newAttrs` object but are `null` + // are handled later.) + var handler = handlers[k]; + var oldValue = handler.value; + handler.value = null; + handler.update(elem, oldValue, null); + delete handlers[k]; + } + } + + for (var k in newAttrs) { + var handler = null; + var oldValue; + var value = newAttrs[k]; + if (! handlers.hasOwnProperty(k)) { + if (value !== null) { + // make new handler + handler = makeAttributeHandler(elem, k, value); + handlers[k] = handler; + oldValue = null; + } + } else { + handler = handlers[k]; + oldValue = handler.value; + } + if (oldValue !== value) { + handler.value = value; + handler.update(elem, oldValue, value); + if (value === null) + delete handlers[k]; + } + } +}; diff --git a/packages/blaze/blaze.js b/packages/blaze/blaze.js new file mode 100644 index 0000000000..6d68697653 --- /dev/null +++ b/packages/blaze/blaze.js @@ -0,0 +1,912 @@ +Blaze = {}; + +Blaze.HTML = HTML; + +Blaze._onAutorun = function () {}; // replace this for debugging :) + +// A constant empty array (frozen if the JS engine supports it). +var _emptyArray = Object.freeze ? Object.freeze([]) : []; + +// Adapted from CoffeeScript's `__extends`. +var __extends = function(child, parent) { + _.extend(child, parent); + if (Object.create) { + child.prototype = Object.create(parent.prototype); + } else { + var ctor = function () {}; + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + } + child.prototype.constructor = child; + child.__super__ = parent.prototype; + return child; +}; +Blaze.__extends = __extends; + +// splice out one element of array that is `=== element` (if present) +var spliceOut = function (array, element) { + for (var i = array.length - 1; i >= 0; i--) { + if (array[i] === element) { + array.splice(i, 1); + break; + } + } +}; + +Blaze.Sequence = function (array) { + if (! (this instanceof Blaze.Sequence)) + // called without new + return new Blaze.Sequence(array); + + // clone `array` + this.items = (array ? Array.prototype.slice.call(array) : []); + this.observers = []; + this.dep = new Deps.Dependency; +}; + +_.extend(Blaze.Sequence.prototype, { + get: function (k) { + var items = this.items; + if (! (k >= 0 && k < items.length)) + throw new Error("Bad index in Blaze.Sequence#get: " + k); + return items[k]; + }, + size: function () { + return this.items.length; + }, + addItem: function (item, k) { + var self = this; + var items = self.items; + if (! (k >= 0 && k <= items.length)) + throw new Error("Bad index in Blaze.Sequence#addItem: " + k); + + items.splice(k, 0, item); + this.dep.changed(); + + var observers = self.observers; + for (var i = 0; i < observers.length; i++) + observers[i].addItem(item, k); + }, + removeItem: function (k) { + var self = this; + var items = self.items; + if (! (k >= 0 && k < items.length)) + throw new Error("Bad index in Blaze.Sequence#removeItem: " + k); + + items.splice(k, 1); + this.dep.changed(); + + var observers = self.observers; + for (var i = 0; i < observers.length; i++) + observers[i].removeItem(k); + }, + observeMutations: function (callbacks) { + var self = this; + self.observers.push(callbacks); + + var handle = { + stop: function () { + spliceOut(self.observers, callbacks); + } + }; + + if (Deps.active) { + Deps.onInvalidate(function () { + handle.stop(); + }); + } + + return handle; + }, + depend: function () { + this.dep.depend(); + } +}); + +// RenderPoints must support being evaluated and/or createDOMRanged multiple +// times. They must not contain per-instance state. +Blaze.RenderPoint = function () {}; + +_.extend(Blaze.RenderPoint.prototype, { + render: function () { + return null; + }, + // Subclasses can override evaluate, toText, toHTML, and createDOMRange + // as they see fit. + evaluate: function () { + return Blaze.evaluate(this.render()); + }, + toText: function (textMode) { + return Blaze.toText(this.evaluate(), textMode); + }, + toHTML: function () { + return Blaze.toHTML(this.evaluate()); + }, + createDOMRange: function () { + return new Blaze.DOMRange(Blaze.toDOM(this.render())); + } +}); + +Blaze.Isolate = function (func) { + if (! (this instanceof Blaze.Isolate)) + // called without new + return new Blaze.Isolate(func); + + this.func = func; +}; +__extends(Blaze.Isolate, Blaze.RenderPoint); + +_.extend(Blaze.Isolate.prototype, { + render: function () { + var func = this.func; + return func(); + }, + createDOMRange: function () { + return Blaze.render(this.func); + } +}); + +Blaze.Controller = function () { + this.parentController = Blaze.currentController; +}; +__extends(Blaze.Controller, Blaze.RenderPoint); + +_.extend(Blaze.Controller.prototype, { + evaluate: function () { + var self = this; + return Blaze.withCurrentController(self, function () { + return Blaze.evaluate(self.render()); + }); + }, + createDOMRange: function () { + var self = this; + var range = Blaze.withCurrentController(self, function () { + return self.renderToDOM(); + }); + if (! range) + debugger; + range.controller = self; + self.domrange = range; + return range; + }, + renderToDOM: function () { + return new Blaze.DOMRange(Blaze.toDOM(this.render())); + } +}); + +Blaze.currentController = null; + +Blaze.withCurrentController = function (controller, func) { + var oldController = Blaze.currentController; + try { + Blaze.currentController = controller; + return func(); + } finally { + Blaze.currentController = oldController; + } +}; + +Blaze.Component = function () { + Blaze.Controller.call(this); +}; +__extends(Blaze.Component, Blaze.Controller); + +_.extend(Blaze.Component.prototype, { + renderToDOM: function () { + var self = this; + if (self.domrange) + throw new Error("Can't render a Component twice!"); + + var range = Blaze.render(function () { + return self.render(); + }); + range.onstop(function () { + self.finalize(); + }); + return range; + }, + finalize: function () {} +}); + + // ------------------------------ DOMBACKEND ------------------------------ + + +var DOMBackend = {}; + +var $jq = jQuery; + +DOMBackend.parseHTML = function (html) { + // Return an array of nodes. + // + // jQuery does fancy stuff like creating an appropriate + // container element and setting innerHTML on it, as well + // as working around various IE quirks. + return $jq.parseHTML(html) || []; +}; + + + + //////////////////// Blaze.toText + + // Escaping modes for outputting text when generating HTML. + Blaze.TEXTMODE = { + ATTRIBUTE: 1, + RCDATA: 2, + STRING: 3 + }; + +var ToTextVisitor = HTML.Visitor.extend({ + visitNull: function (nullOrUndefined) { + return ''; + }, + visitPrimitive: function (stringBooleanOrNumber) { + return String(stringBooleanOrNumber); + }, + visitArray: function (array) { + var parts = []; + for (var i = 0; i < array.length; i++) + parts.push(this.visit(array[i])); + return parts.join(''); + }, + visitComment: function (comment) { + throw new Error("Can't have a comment here"); + }, + visitCharRef: function (charRef) { + return charRef.str; + }, + visitRaw: function (raw) { + return raw.value; + }, + visitTag: function (tag) { + // Really we should just disallow Tags here. However, at the + // moment it's useful to stringify any HTML we find. In + // particular, when you include a template within `{{#markdown}}`, + // we render the template as text, and since there's currently + // no way to make the template be *parsed* as text (e.g. `