Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

v2.0

  • Loading branch information...
commit f849fa78da0d720cfeef497cc68651fa719884dd 2 parents 074809a + 5a020c3
@thejohnfreeman thejohnfreeman authored
Showing with 2,970 additions and 756 deletions.
  1. +1 −1  .gitignore
  2. +8 −1 Makefile
  3. +3 −0  README.md
  4. +6 −6 lib/bindings/behavior/value.js
  5. +37 −25 lib/bindings/binders/dom/foreach.js
  6. +16 −4 lib/bindings/binders/dom/if.js
  7. +5 −1 lib/bindings/binders/event/click.js
  8. +7 −2 lib/bindings/binders/html/attr.js
  9. +25 −0 lib/bindings/binders/html/enable.js
  10. +29 −0 lib/bindings/binders/html/error.js
  11. +29 −0 lib/bindings/binders/html/klass.js
  12. +10 −2 lib/bindings/binders/html/text.js
  13. +24 −9 lib/bindings/binders/value/betterTextbox.js
  14. +21 −2 lib/bindings/binders/value/common.js
  15. +21 −0 lib/bindings/binders/value/date.js
  16. +6 −2 lib/bindings/binders/value/focused.js
  17. +11 −19 lib/bindings/binders/value/number.js
  18. +7 −3 lib/bindings/binders/value/textbox.js
  19. +137 −24 lib/bindings/controller.js
  20. +2 −1  lib/hd.js
  21. +10 −6 lib/model/behavior/activation.js
  22. +16 −10 lib/model/behavior/enablement.js
  23. +11 −9 lib/model/behavior/precondition.js
  24. +23 −14 lib/model/factory/constraint.js
  25. +87 −0 lib/model/factory/dirty.js
  26. +3 −2 lib/model/factory/factory.js
  27. +20 −12 lib/model/factory/internal.js
  28. +83 −32 lib/model/factory/model.js
  29. +26 −19 lib/model/factory/solver.js
  30. +20 −0 lib/model/factory/translation.js
  31. +21 −14 lib/model/factory/variable.js
  32. +40 −29 lib/model/model.js
  33. +190 −44 lib/model/proxies/array.js
  34. +13 −8 lib/model/proxies/common.js
  35. +10 −10 lib/model/proxies/scalar.js
  36. +65 −0 lib/model/proxies/translation.js
  37. +218 −0 lib/model/proxies/util.js
  38. +78 −0 lib/model/proxies/validators.js
  39. +129 −120 lib/model/runtime/evaluator.js
  40. +47 −39 lib/model/runtime/runtime.js
  41. +4 −2 lib/model/solver/helpers.js
  42. +3 −5 lib/model/solver/model.js
  43. +18 −12 lib/model/solver/quickplan.js
  44. +16 −8 lib/model/solver/solver.js
  45. +0 −54 lib/utility/debug.js
  46. +15 −5 lib/utility/debug.m4.js
  47. +55 −0 lib/utility/diag.js
  48. +1 −1  lib/utility/json.js
  49. +44 −20 lib/utility/publisher.js
  50. +56 −63 lib/utility/stdlib.js
  51. +2 −2 make/Makefile.common
  52. +2 −0  make/Makefile.js
  53. +9 −1 make/Makefile.js.base
  54. +10 −1 make/Makefile.lib
  55. +8 −0 make/Makefile.test
  56. +4 −4 setup.sh
  57. +137 −0 test/examples/applytexas/index.html
  58. +97 −0 test/examples/applytexas/main.css
  59. +59 −0 test/examples/applytexas/model.js
  60. +0 −55 test/examples/betterTextbox.html
  61. +71 −12 test/examples/bindings.html
  62. +26 −5 test/examples/btb.html
  63. +1 −1  test/examples/grouped_options.html
  64. +99 −0 test/examples/hotel.html
  65. +81 −0 test/examples/image_scaling.html
  66. +71 −0 test/examples/save_file.html
  67. +25 −0 test/examples/template.html
  68. +1 −0  test/jslint/options.txt
  69. +27 −0 test/qunit/bindings/binders/html/attr.js
  70. +34 −0 test/qunit/bindings/binders/html/css.js
  71. +31 −0 test/qunit/bindings/binders/html/html.js
  72. +27 −0 test/qunit/bindings/binders/html/style.js
  73. +43 −0 test/qunit/bindings/binders/html/text.js
  74. +52 −0 test/qunit/bindings/binders/html/visible.js
  75. +51 −0 test/qunit/bindings/expressions.js
  76. +20 −1 test/qunit/common/hottest.js
  77. +1 −0  test/qunit/index.html
  78. +62 −3 test/todomvc/index.html
  79. +40 −31 test/todomvc/js/app.js
  80. +152 −0 test/todomvc/js/ko.js
View
2  .gitignore
@@ -3,6 +3,6 @@
hotdrink.js
hotdrink.js.gz
hotdrink-test.js
+hotdrink-debug.js
build
doc
-
View
9 Makefile
@@ -15,7 +15,7 @@ export BUILDDIR
##################################################
# targets
-.PHONY : all debug release doc test
+.PHONY : all debug release doc test lint
all :
@$(call defer,$(MAKEDIR)/Makefile.$(PRIMARY))
@@ -23,6 +23,9 @@ all :
debug :
@$(call defer,$(MAKEDIR)/Makefile.$(PRIMARY))
+raw :
+ @$(call defer,$(MAKEDIR)/Makefile.$(PRIMARY))
+
release :
@$(call defer,$(MAKEDIR)/Makefile.$(PRIMARY))
@@ -32,6 +35,10 @@ doc :
test :
@$(MAKE) -f $(MAKEDIR)/Makefile.test
+lint :
+ @$(call defer,$(MAKEDIR)/Makefile.$(PRIMARY))
+
+
##################################################
# cleaning
View
3  README.md
@@ -0,0 +1,3 @@
+Check the [wiki](https://github.com/HotDrink/hotdrink/wiki) for
+documentation.
+
View
12 lib/bindings/behavior/value.js
@@ -27,9 +27,9 @@ HOTDRINK_DEBOUNCE_THRESHOLD = 20;
var readListener = function readListener() {
var maybe = read(view);
- if ("value" in maybe) {
+ if (maybe.hasOwnProperty("value")) {
variable(maybe.value);
- } else if ("error" in maybe) {
+ } else if (maybe.hasOwnProperty("error")) {
WARNING("validation error: " + maybe.error);
} else {
ERROR("expected error monad from read");
@@ -55,17 +55,17 @@ HOTDRINK_DEBOUNCE_THRESHOLD = 20;
/* changeEvent may contain information on incremental changes. write
* should understand how to interpret (or ignore) it. */
- var writeListener = function writeListener(changeEvent) {
+ var writeListener = function writeListener(/*changes...*/) {
/* TODO: Unbind elements when removing them. */
- write(view, value(), changeEvent);
+ write(view, value(), Array.prototype.slice.call(arguments));
};
value.subscribe("value", writeListener);
- write(view, value(), { set: true });
+ write(view, value(), [{ set: true }]);
} else {
/* The option is a constant value. */
- write(view, value, { set: true });
+ write(view, value, [{ set: true }]);
}
};
View
62 lib/bindings/binders/dom/foreach.js
@@ -9,7 +9,7 @@
view.data(hdMirrorName, [stub]);
};
- var add = function add(view, list, index, howMany) {
+ var add = function add(view, index, items) {
var render = view.data(hdRenderName);
/* `hdMirror` is a JavaScript array mirroring `list`, but with an extra
* placeholder at the end.
@@ -22,15 +22,17 @@
* index.
*/
var mirror = view.data(hdMirrorName);
- var slot = mirror[index];
+ var slot = mirror[index].first();
var copies = [];
+ var i;
- for (var i = 0; i < howMany; ++i) {
- var copy = render(list[index + i]);
- copies[i] = copy;
- slot.before(copy);
+ for (i = 0; i < items.length; ++i) {
+ var copy = render(items[i]);
+ copies.push(copy);
}
+ slot.before.apply(slot, copies);
+
copies.unshift(index, 0);
Array.prototype.splice.apply(mirror, copies);
};
@@ -44,30 +46,40 @@
var set = function set(view, list) {
clear(view);
- add(view, list, 0, list.length);
+ add(view, 0, list);
};
- var write = function writeForEach(view, list, changeEvent) {
+ var swap = function swap(view, i, j) {
+ var mirror = view.data(hdMirrorName);
+ var ie = mirror[i], je = mirror[j];
+ /* The order of this manipulation matters. Remember i comes before j. */
+ ie.detach();
+ je.detach();
+ mirror[j + 1].first().before(ie);
+ mirror[j] = ie;
+ mirror[i + 1].first().before(je);
+ mirror[i] = je;
+ };
+
+ var write = function writeForEach(view, list, changes) {
ASSERT(view instanceof jQuery, "expected jQuery object");
ASSERT(Array.isArray(list), "expected array");
- if (changeEvent.set) {
- set(view, list);
- return;
- }
-
- /* Order is important here. */
- if (changeEvent.removes) {
- changeEvent.removes.forEach(function (args) {
- remove(view, args.index, args.howMany);
- });
- }
-
- if (changeEvent.adds) {
- changeEvent.adds.forEach(function (args) {
- add(view, list, args.index, args.howMany);
- });
- }
+ /* Use `some` so that we can break iteration by returning `true`, which
+ * indicates we saw all the changes we care about, i.e. we encountered a
+ * fully destructive change. */
+ changes.some(function (change) {
+ if (change.set) {
+ set(view, list);
+ return true;
+ } else if (change.remove) {
+ remove(view, change.remove.index, change.remove.howMany);
+ } else if (change.add) {
+ add(view, change.add.index, change.add.items);
+ } else if (change.swap) {
+ swap(view, change.swap[0], change.swap[1]);
+ }
+ });
};
hd.binders["foreach"] = function bindForEach(view, variable, context) {
View
20 lib/bindings/binders/dom/if.js
@@ -2,7 +2,7 @@
var hdRenderName = "hdRender";
- var write = function write(view, truthy) {
+ var writeIf = function writeIf(view, truthy) {
ASSERT(view instanceof jQuery, "expected jQuery object");
if (!!truthy) {
@@ -16,8 +16,12 @@
view.empty();
}
};
-
- hd.binders["if"] = function bindIf(view, variable, context) {
+
+ var writeIfNot = function writeIfNot(view, truthy) {
+ return writeIf(view, !truthy);
+ };
+
+ var bindIfOrIfNot = function bindIfOrIfNot(view, variable, context, writer) {
ASSERT(view instanceof jQuery, "expected jQuery object");
var template = view.contents().detach();
@@ -28,11 +32,19 @@
return copy;
});
- hd.bindWrite(variable, view, { write: write });
+ hd.bindWrite(variable, view, { write: writer });
/* Stop recursion. */
return true;
};
+ hd.binders["if"] = function bindIf(view, variable, context) {
+ return bindIfOrIfNot(view, variable, context, writeIf);
+ };
+
+ hd.binders["ifnot"] = function bindIfNot(view, variable, context) {
+ return bindIfOrIfNot(view, variable, context, writeIfNot);
+ };
+
}());
View
6 lib/bindings/binders/event/click.js
@@ -2,8 +2,12 @@
hd.binders["click"] = function bindClick(view, fn, context) {
hd.binders["event"](view, { "click": fn }, context);
- if (hd.isCommand(fn)) hd.bindEnablement(fn, view);
+ if (hd.isCommand(fn)) {
+ hd.bindEnablement(fn, view);
+ }
};
+ hd.binders["command"] = hd.binders["click"];
+
}());
View
9 lib/bindings/binders/html/attr.js
@@ -6,12 +6,17 @@
var write = function writeAttr(view, value) {
DEBUG_BEGIN;
ASSERT(view instanceof jQuery, "expected jQuery object");
- if (typeof value !== "string") {
+ if (typeof value !== "string" && value != null) {
WARNING("be careful setting attribute " + attrName +
" to a non-string value");
}
DEBUG_END;
- view.attr(attrName, value);
+ /* We intentionally use converting equality comparison (==) here */
+ if (value == null) {
+ view.removeAttr(attrName);
+ } else {
+ view.attr(attrName, value);
+ }
};
var value = attrs[attrName];
View
25 lib/bindings/binders/html/enable.js
@@ -0,0 +1,25 @@
+(function () {
+
+ var writeEnable = function writeEnable(view, truthy) {
+ ASSERT(view instanceof jQuery, "expected jQuery object");
+ if (!!truthy) {
+ view.removeAttr("disabled");
+ } else {
+ view.attr("disabled", "disabled");
+ }
+ };
+
+ hd.binders["enable"] = function bindEnable(view, truthy) {
+ hd.bindWrite(truthy, view, { write: writeEnable });
+ };
+
+ var writeDisable = function writeDisable(view, truthy) {
+ return writeEnable(view, !truthy);
+ };
+
+ hd.binders["disable"] = function bindDisable(view, truthy) {
+ hd.bindWrite(truthy, view, { write: writeDisable });
+ };
+
+}());
+
View
29 lib/bindings/binders/html/error.js
@@ -0,0 +1,29 @@
+(function () {
+
+ var makeWrite = function makeWrite(msgDefault) {
+ return function writeText(view, value) {
+ ASSERT(view instanceof jQuery, "expected jQuery object");
+ /* Use `==` to catch both undefined and null. */
+ if (value == null) {
+ view.text(msgDefault);
+ } else {
+ if (typeof value !== "string") {
+ value = JSON.stringify(value);
+ }
+ view.text(value);
+ }
+ };
+ };
+
+ /* @param option { hd.variable | String } */
+ hd.binders["error"] = function bindText(view, value) {
+ ASSERT(hd.isProxy(value), "expected proxy");
+ var error = value.unwrap().error;
+ ASSERT(hd.isProxy(error), "expected proxy to have an error variable");
+ ASSERT(view instanceof jQuery, "expected jQuery object");
+ var msgDefault = view.text();
+ hd.bindWrite(error, view, { write: makeWrite(msgDefault) });
+ };
+
+}());
+
View
29 lib/bindings/binders/html/klass.js
@@ -0,0 +1,29 @@
+(function () {
+
+ var write = function writeCss(view, className) {
+ ASSERT(view instanceof jQuery, "expected jQuery object");
+
+ /* Remove the old class if there is one. */
+ var old = view.data("hd-klass");
+ if (old) {
+ view.removeClass(old);
+ }
+
+ /* Add the new class and remember it. */
+ view.addClass(className);
+ view.data("hd-klass", className);
+ };
+
+ hd.binders["klass"] = function bindKlass(view, classes) {
+ /* Allow short-hand for a single class. */
+ if (!Array.isArray(classes)) {
+ classes = [classes];
+ }
+
+ classes.forEach(function (className) {
+ hd.bindWrite(className, view, { write: write });
+ });
+ };
+
+}());
+
View
12 lib/bindings/binders/html/text.js
@@ -2,8 +2,16 @@
var write = function writeText(view, value) {
ASSERT(view instanceof jQuery, "expected jQuery object");
- if (typeof value !== "string") value = JSON.stringify(value);
- view.text(value);
+ if (typeof value !== "string") {
+ value = JSON.stringify(value);
+ }
+ /* Passing undefined to JSON.stringify will return undefined. */
+ if (typeof value !== "string") {
+ value = "";
+ }
+ /* Encode HTML entities. */
+ value = view.text(value).html();
+ view.html(value.replace(/\n/g,"<br />"));
};
/* @param option { hd.variable | String } */
View
33 lib/bindings/binders/value/betterTextbox.js
@@ -66,17 +66,19 @@
* http://stackoverflow.com/a/2897510/618906
*/
var input = this.tbox.get(0);
- if ('selectionStart' in input) {
+ var index = 0;
+ if (input.hasOwnProperty("selectionStart")) {
// Standard-compliant browsers
- return input.selectionStart;
+ index = input.selectionStart;
} else if (document.selection) {
// IE
input.focus();
var sel = document.selection.createRange();
var selLen = document.selection.createRange().text.length;
sel.moveStart('character', -input.value.length);
- return sel.text.length - selLen;
+ index = sel.text.length - selLen;
}
+ return index;
};
pt.setCaret = function setCaret(index) {
@@ -105,7 +107,9 @@
};
pt.setText = function setText(text) {
- if (this.getText() === text) return;
+ if (this.getText() === text) {
+ return;
+ }
/* TODO: What to do with caret? */
this.tbox.val(text);
};
@@ -116,7 +120,9 @@
/* Insert a string before a position. */
pt.insertText = function insertText(index, insertion) {
/* Check for null operation. */
- if (!insertion) return;
+ if (!insertion) {
+ return;
+ }
/* Since the underlying widget does not separate text mutation from caret
* movement, we must remember the current caret location and restore it
@@ -132,14 +138,18 @@
/* If the caret was past the insertion point, we must adjust it by the
* length of the insertion. */
- if (pos > index) pos += insertion.length;
+ if (pos > index) {
+ pos += insertion.length;
+ }
this.setCaret(pos);
};
/* Remove and return the substring in a half-open range. */
pt.removeText = function removeText(begin, end) {
/* Check for null operation. */
- if (begin === end) return;
+ if (begin === end) {
+ return;
+ }
var pos = this.getCaret();
@@ -198,7 +208,9 @@
var write = function write(view, value) {
ASSERT(view instanceof BetterTextbox, "expected a better textbox");
- if (typeof value !== "string") value = JSON.stringify(value);
+ if (typeof value !== "string") {
+ value = JSON.stringify(value);
+ }
view.setText(value);
};
@@ -231,7 +243,10 @@
if (typeof options === "object") {
value = options.value;
- if (options.editing) hd.binders["focused"](view.tbox, options.editing);
+ value = common.maybeTranslate(value, options);
+ if (options.editing) {
+ hd.binders["focused"](view.tbox, options.editing);
+ }
if (options.focus) {
view.onFocus(wrapCallback(options.focus, context));
}
View
23 lib/bindings/binders/value/common.js
@@ -14,11 +14,30 @@
var bind = binder();
+ var maybeTranslate = function maybeTranslate(variable, options) {
+ if (options.toView || options.toModel || options.validate) {
+ if (!hd.isTranslation(variable)) {
+ variable = hd.translation(variable);
+ }
+ if (options.validate) {
+ variable.validate.prependOutgoing(options.validate);
+ }
+ if (options.toModel) {
+ variable.validate.prependOutgoing(options.toModel);
+ }
+ if (options.toView) {
+ variable.validate.incoming(options.toView);
+ }
+ }
+ return variable;
+ };
+
/* Export: */
hd.__private.bindings = {
- binder: binder,
- bind: bind
+ binder: binder,
+ bind: bind,
+ maybeTranslate: maybeTranslate
};
}());
View
21 lib/bindings/binders/value/date.js
@@ -0,0 +1,21 @@
+(function () {
+
+ var validators = hd.validators;
+
+ hd.binders["date"] = function bindDate(view, options) {
+ if (typeof options !== "object") {
+ options = { value: options };
+ }
+
+ if (!options.toModel) {
+ options.toModel = hd.util.toDate();
+ }
+ if (!options.toView) {
+ options.toView = hd.util.dateToString();
+ }
+
+ hd.binders["btb"](view, options);
+ }
+
+}());
+
View
8 lib/bindings/binders/value/focused.js
@@ -3,9 +3,13 @@
var writeFocused = function writeFocused(view, truthy) {
ASSERT(view instanceof jQuery, "expected jQuery object");
if (truthy) {
- if (!view.is(":focus")) view.focus();
+ if (!view.is(":focus")) {
+ view.focus();
+ }
} else {
- if (view.is(":focus")) view.blur();
+ if (view.is(":focus")) {
+ view.blur();
+ }
}
};
View
30 lib/bindings/binders/value/number.js
@@ -1,27 +1,19 @@
(function () {
- var common = hd.__private.bindings;
+ hd.binders["number"] = function bindNumber(view, options) {
+ if (typeof options !== "object") {
+ options = { value: options };
+ }
- var onChange = function onChangeNumber(view, listener) {
- ASSERT(view instanceof jQuery, "expected jQuery object");
- /* keyup instead of keypress, otherwise we'll read the
- * value before the user's edit. */
- view.bind("keyup", listener);
- };
-
- var convertNumber = function convertNumber(vv) {
- var mv = (typeof vv === "string") ? parseFloat(vv) : vv;
- return (typeof mv !== "number" || isNaN(mv))
- ? { error : "could not convert to number: " + JSON.stringify(vv) }
- : { value : mv };
- };
+ if (!options.toModel) {
+ options.toModel = hd.util.toNum();
+ }
+ if (!options.toView) {
+ options.toView = hd.util.toString();
+ }
- var read = function readNumber(view) {
- ASSERT(view instanceof jQuery, "expected jQuery object");
- return convertNumber(view.val());
+ hd.binders["btb"](view, options);
};
- hd.binders["number"] = common.binder({ onChange: onChange, read: read });
-
}());
View
10 lib/bindings/binders/value/textbox.js
@@ -13,12 +13,16 @@
hd.binders["textbox"] = function bindTextbox(view, options) {
var hdtSaved = HOTDRINK_DEBOUNCE_THRESHOLD;
+ var value;
if (typeof options === "object") {
- var value = options.value;
- if (options.debounce) HOTDRINK_DEBOUNCE_THRESHOLD = options.debounce;
+ value = options.value;
+ value = common.maybeTranslate(value, options);
+ if (options.debounce) {
+ HOTDRINK_DEBOUNCE_THRESHOLD = options.debounce;
+ }
} else {
- var value = options;
+ value = options;
}
subbind(view, value);
View
161 lib/bindings/controller.js
@@ -20,18 +20,102 @@
var binder = hd.binders[binderName];
if (!binder) {
- ERROR("No binder for " + binderName);
+ ERROR("no binder for " + binderName);
return;
}
+ LOG("calling " + binderName + " binder...");
+
if (binder(view, bindings[binderName], context)) {
doNotRecurse = true;
}
}, this);
+ LOG("finished binding this element");
+
return doNotRecurse;
};
+ /* Compilers manipulate source code consisting of "sections" (alluding to
+ * assembly programming). Each `sections` variable should be an object with
+ * two properties:
+ *
+ * data :: String = declarations to be evaluated in the binding context
+ *
+ * code :: String = binding list (like would be found in a `data-bind`
+ * attribute), to be evaluated in the binding context, that refers to
+ * the declarations in `data`
+ *
+ * `transform` is a function that takes the sections and an expression from
+ * the `code`, might add a declaration to the `data`, and returns a
+ * replacement expression.
+ *
+ * Expressions in the `code` are delimited by delim.
+ */
+ var makeCompiler = function makeCompiler(delim, transform) {
+
+ return function compiler(sections) {
+ /* Literal delimiters must be escaped. To assist with parsing, replace
+ * literal backquotes with a character sequence ("\um") that cannot
+ * appear inside or outside of strings in JavaScript. */
+ var code = sections.code.replace("\\" + delim, "\\um");
+
+ var exprs = code.split(delim);
+
+ /* If the delimiters are balanced, there will be an odd number of
+ * splits. */
+ if ((exprs.length % 2) === 0) {
+ ERROR("unbalanced delimiters (" + delim + ") in binding");
+ return null;
+ }
+
+ /* Delimited expressions will occur at every odd index. */
+ var i = 1;
+ for (; i < exprs.length; i += 2) {
+ exprs[i] = transform(sections, exprs[i].replace("\\um", delim));
+ }
+
+ sections.code = exprs.join("").replace("\\um", delim);
+ return sections;
+ };
+
+ };
+
+ var exprCompiler = makeCompiler("`", function (sections, expr) {
+ /* TODO: We could destroy this variable when the view it serves is
+ * removed from the DOM. */
+ var vvid = hd.__private.makeName("bindexpr");
+ sections.data += "var " + vvid +
+ " = hd.computed(function () { return (" + expr + "); }); ";
+ return vvid;
+ });
+
+ var cmdCompiler = makeCompiler("@", function (sections, expr) {
+ var fid = hd.__private.makeName("bindcmd");
+ sections.data += "var " + fid +
+ " = function " + fid + "() { return (" + expr + "); }; ";
+ return fid;
+ });
+
+ var compile = function compile(bindingString) {
+ var sections = {
+ data: "",
+ code: bindingString
+ };
+
+ sections = exprCompiler(sections);
+ if (!sections) return;
+ sections = cmdCompiler(sections);
+ if (!sections) return;
+
+ if (sections.data) {
+ sections.data += "hd.update(); ";
+ }
+
+ return "with ($context) { " + sections.data +
+ "return ({ " + sections.code + " }); }";
+ };
+
/* @returns {Boolean}
* True if we should not recurse into the view's descendants, e.g., in
* the presence of a binder like foreach that handles the binding of
@@ -40,18 +124,40 @@
var bindElement = function bindElement(elt, context) {
ASSERT(elt instanceof jQuery, "expected jQuery object");
ASSERT(elt.length === 1, "expected a single element");
- LOG("Trying to bind #" + elt.attr("id"));
/* Parse its bindings string. */
var bindingString = elt.attr("data-bind");
- if (!bindingString) return false;
+ if (!bindingString) {
+ return;
+ }
+
+ var functionBody = compile(bindingString);
+ if (!functionBody) {
+ return;
+ }
+
+ DEBUG_BEGIN;
+ var id = elt.attr("id");
+ if (!id) {
+ id = "element with bindings \"" + bindingString + "\"";
+ } else {
+ id = "#" + id;
+ }
+ DEBUG_END;
+
+ LOG("trying to bind " + id);
+ INSPECT(elt.get(0));
/* Credit to Knockout.js for this. */
- var functionBody = "with ($context) { return ({ " + bindingString + " }); } ";
+ var i;
+ for (i = context.$parents.length; i > 0; --i) {
+ functionBody
+ = "with ($context.$parents[" + (i - 1) + "]) { " + functionBody + " }";
+ }
LOG("functionBody = " + functionBody);
try {
var bindingMonad = new Function("$context", functionBody);
- } catch (e) {
+ } catch (meh) {
ERROR("expected execution (not construction) of function to throw");
}
@@ -69,11 +175,10 @@
* 2.b. Pass the computed variable to the binder named by the key.
*/
try {
+ LOG("running binding monad...");
var bindings = bindingMonad(context);
} catch (e) {
- var id = elt.attr("id");
- ERROR("cannot parse bindings on " +
- (id ? ("#" + id) : "(unidentified element)") + ":\n \"" +
+ ERROR("cannot parse bindings on " + id + ":\n \"" +
bindingString + "\"\n " +
e);
return true;
@@ -84,34 +189,39 @@
};
var Context = function Context($this, $parent, extras) {
- this.$this = $this;
+ var ctx = (typeof $this === "object")
+ ? Object.create($this.constructor.prototype)
+ : this;
+
+ ctx.$this = $this;
if ($parent) {
- this.$parent = $parent;
- this.$parents = $parent.$parents.slice();
- this.$parents.push($parent);
+ ctx.$parent = $parent;
+ ctx.$parents = $parent.$parents.slice();
+ ctx.$parents.unshift($parent);
} else {
- this.$root = $this;
- this.$parents = [];
+ ctx.$root = $this;
+ ctx.$parents = [];
}
//if (typeof extras === "object") {
- //Object.extend(this, extras);
+ //Object.extend(ctx, extras);
//}
- if (typeof $this === "object") {
- //Object.extend(this, $this);
- Object.keys($this).forEach(function (key) {
- this[key] = $this[key];
- }, this);
+ if (Object.canHaveProperties($this)) {
+ Object.extend(ctx, $this);
}
+
+ return ctx;
};
var bindTree = function bindTree(elts, context) {
ASSERT(elts instanceof jQuery, "expected jQuery object");
elts.each(function () {
var elt = $(this);
- if (bindElement(elt, context)) return;
+ if (bindElement(elt, context)) {
+ return;
+ }
bindTree(elt.children(), context);
});
};
@@ -124,9 +234,12 @@
/* Have to take our parameters in the wrong conceptual order because we
* have a default for the view. */
var bind = function bind(model, elts) {
- if (!elts) elts = $('body');
- if (!(elts instanceof jQuery)) elts = $(elts);
- LOG("Binding " + elts.attr("id"));
+ if (!elts) {
+ elts = $('body');
+ }
+ if (!(elts instanceof jQuery)) {
+ elts = $(elts);
+ }
subbind(elts, model);
};
View
3  lib/hd.js
@@ -1,4 +1,5 @@
hd = {
- __private: {}
+ __private: {},
+ util: {}
};
View
16 lib/model/behavior/activation.js
@@ -63,7 +63,7 @@
{
LOG("Starting analysis for activation behavior...");
- var isFirstUpdate = (this.timestamp === undefined)
+ var isFirstUpdate = (this.timestamp === undefined);
this.timestamp = timestamp;
/* We don't need to check outputs if no invariants changed. */
@@ -136,23 +136,27 @@
*/
Activation.prototype.isPoisoned = function isPoisoned(vv) {
/* If this variable has already been checked, then we can go home early. */
- if (vv.lastPoisoned === this.timestamp) return vv.isPoisoned;
+ if (vv.lastPoisoned === this.timestamp) {
+ return vv.isPoisoned;
+ }
vv.lastPoisoned = this.timestamp;
/* Base case: a blamed variable is poisoned. */
if (vv.blamedBy > 0) {
LOG(vv + " is a source of poison");
- return vv.isPoisoned = true;
+ return (vv.isPoisoned = true);
}
/* Recursion: a variable can be poisoned by its ancestors. */
var mm = vv.writtenBy;
if (mm) {
- return vv.isPoisoned = mm.inputsUsed.some(function (uu) {
+ return (vv.isPoisoned = mm.inputsUsed.some(function (uu) {
var found = this.isPoisoned(uu);
- if (found) LOG(vv + " was poisoned by " + uu);
+ if (found) {
+ LOG(vv + " was poisoned by " + uu);
+ }
return found;
- }, this);
+ }, this));
}
/* If we got this far, then it must be healthy. */
View
26 lib/model/behavior/enablement.js
@@ -54,11 +54,15 @@
= function maybeMarkCanBeDisabled(vv)
{
/* If this variable has already been checked, then we can go home early. */
- if (vv.lastCanBeDisabled === this.timestamp) return;
+ if (vv.lastCanBeDisabled === this.timestamp) {
+ return;
+ }
vv.lastCanBeDisabled = this.timestamp;
/* Only interface variables can be disabled. */
- if (vv.cellType !== "interface") return;
+ if (vv.cellType !== "interface") {
+ return;
+ }
var canBeDisabledPrev = vv.canBeDisabled;
/* TODO: Should we enable if it is violating a precondition? */
@@ -94,7 +98,7 @@
/* Base case: relevant variables can be relevant. */
if (this.isRelevant(vv)) {
LOG(vv + " can be relevant");
- return vv.canBeRelevant = true;
+ return (vv.canBeRelevant = true);
}
/* Recursion: if I am relevant, or after being touched, I can change the
@@ -104,7 +108,7 @@
* me will not change the solution. */
if (!(mm && mm.constraint)) {
LOG(vv + " cannot be relevant");
- return vv.canBeRelevant = false;
+ return (vv.canBeRelevant = false);
}
LOG("finding ways to be relevant by changing the constraint writing it...");
@@ -112,7 +116,9 @@
vv.canBeRelevant = mmPeers.some(function (nn) {
/* Only methods that do not write to us could be selected after we are
* touched. */
- if (nn.outputs.has(vv)) return false;
+ if (nn.outputs.has(vv)) {
+ return false;
+ }
/* Otherwise, if it has outputs that can be relevant, then we can be
* relevant too. */
@@ -126,7 +132,7 @@
};
/**
- * Any variable that can reach an output in the current solution is relevant.
+ * Any variable that can reach a command in the current solution is relevant.
*/
Enablement.prototype.isRelevant = function isRelevant(vv) {
LOG("is " + vv + " relevant now?");
@@ -138,10 +144,10 @@
}
vv.lastIsRelevant = this.timestamp;
- /* Base case: outputs are relevant. */
- if (vv.cellType === "output") {
+ /* Base case: commands are relevant. */
+ if (vv.cellType === "command") {
LOG(vv + " is relevant");
- return vv.isRelevant = true;
+ return (vv.isRelevant = true);
}
/* Recursion: if I reach a relevant variable in the current evaluation
@@ -156,7 +162,7 @@
return vv.isRelevant;
};
- hd.behaviors.push(new Enablement);
+ hd.behaviors.push(new Enablement());
}());
View
20 lib/model/behavior/precondition.js
@@ -4,7 +4,9 @@
hd.precondition = function precondition(commands, fn) {
/* Provide short-cut for single command. */
- if (!Array.isArray(commands)) commands = [commands];
+ if (!Array.isArray(commands)) {
+ commands = [commands];
+ }
/* The user has access to proxies, but we want the variables. */
commands = commands.map(function (proxy) {
ASSERT(hd.isCommand(proxy),
@@ -25,6 +27,7 @@
* </p>
*/
var Precondition = function Precondition() {
+ this.isFirstUpdate = true;
};
/**
@@ -34,7 +37,7 @@
* The commands guarded by this precondition.
* }
*
- * /output/ :: {
+ * /command/ :: {
* numPreconFailed :: number
* The number of preconditions guarding this command that are in a
* failed state.
@@ -45,7 +48,7 @@
* </pre>
*/
Precondition.prototype.variable = function variable(vv) {
- if (vv.cellType === "output") {
+ if (vv.cellType === "command") {
vv.numPreconFailed = 0;
vv.canBeDisabled = false;
}
@@ -56,22 +59,21 @@
{
LOG("Examining preconditions...");
- var isFirstUpdate = (this.wasUpdated === undefined);
- this.wasUpdated = true;
-
var changedCommands = [];
changedSet.forEach(function (vv) {
if (vv.cellType === "precondition") {
/* Add to the number of failed preconditions if its value is false.
* Subtract if true, unless this is our first update. */
- var weight = (vv.value ? (this.wasUpdated ? 0 : -1) : 1);
+ var weight = (vv.value ? (this.isFirstUpdate ? 0 : -1) : 1);
vv.guarded.forEach(function (ww) {
ww.numPreconFailed += weight;
changedCommands.setInsert(ww);
});
}
- });
+ }, this);
+
+ this.isFirstUpdate = false;
changedCommands.forEach(function (vv) {
var canBeDisabledPrev = vv.canBeDisabled;
@@ -84,7 +86,7 @@
LOG("Examined preconditions.");
};
- hd.behaviors.push(new Precondition);
+ hd.behaviors.push(new Precondition());
}());
View
37 lib/model/factory/constraint.js
@@ -10,12 +10,15 @@
this.methods = [];
this.cc = undefined;
/* We might not ever use this. Oh well. :/ */
- this.solver = new hd.__private.Solver;
+ this.solver = new hd.__private.Solver();
};
- ConstraintFactory.prototype.method = function method(outputs, fn) {
+ ConstraintFactory.prototype.method = function method(outputs, fn, context)
+ {
/* Provide short-cut for single output. */
- if (!Array.isArray(outputs)) outputs = [outputs];
+ if (!Array.isArray(outputs)) {
+ outputs = [outputs];
+ }
/* The user has access to proxies, but we want the variables. */
outputs = outputs.map(function (proxy) {
ASSERT(hd.isVariable(proxy),
@@ -30,18 +33,12 @@
ASSERT(!isConflict,
"cannot add a method that outputs to computed variable");
- /* The first method makes us a one-way constraint unless some of our
- * outputs already belong to constraint graphs. We need to find out now if
- * we have a one-way constraint in case we need to make it multi-way
- * later. */
- var wasOneWayConstraint = (this.solver.constraints.length === 0);
-
outputs.forEach(function (ww) {
this.solver.merge(ww.solver);
}, this);
/* Create the method. */
- var mm = factory.addMethod(outputs, fn);
+ var mm = factory.addMethod(outputs, fn, context);
/* This will add mm to this.cc.methods (if this.cc exists) since it shares
* the same methods array. */
this.methods.push(mm);
@@ -58,6 +55,8 @@
* them. Otherwise, start a new one. */
if (this.methods.length === 1) {
+ factory.setOneWayConstraint(mm);
+
/* If we already have a constraint graph, then this method writes to
* variables that can be written by another method in the graph. If we
* leave this as a one-way constraint, then that other method will never
@@ -71,8 +70,6 @@
}, 0);
}
- factory.setOneWayConstraint(mm);
-
} else {
if (this.methods.length === 2) {
@@ -105,9 +102,13 @@
/* This parameter is optional in most cases. It is used to inform the
* solver of variables that are input-only to this constraint. If the
* solver does not know about them, it may choose a cyclic solution. */
- if (!variables) variables = [];
+ if (!variables) {
+ variables = [];
+ }
/* Provide short-cut for single variable. */
- if (!Array.isArray(variables)) variables = [variables];
+ if (!Array.isArray(variables)) {
+ variables = [variables];
+ }
variables = variables.map(function (proxy) {
ASSERT(hd.isVariable(proxy),
"expected variable as subject of constraint");
@@ -117,5 +118,13 @@
return new ConstraintFactory(variables);
};
+ /* Syntactic sugar for creating a two-variable two-way constraint, with both
+ methods being identity functions */
+ hd.equalityConstraint = function equalityConstraint(v1, v2) {
+ return hd.constraint([v1, v2])
+ .method(v1, function () { return v2(); })
+ .method(v2, function () { return v1(); });
+ };
+
}());
View
87 lib/model/factory/dirty.js
@@ -0,0 +1,87 @@
+(function () {
+
+ var factory = hd.__private.factory;
+ var PROTO_NAME = hd.PROTO_NAME;
+
+ var isDirty = function isDirty() {
+ return !this.isClean();
+ };
+
+ /***************************************************************/
+ /* Each saved variable should know how to save and reset itself. */
+
+ var isIdentical = function isIdentical(a, b) { return a === b; };
+
+ var CleanScalar = function CleanScalar(now, cleaned, isEqual) {
+ this.now = now;
+ this.cleaned = cleaned;
+ this.isEqual = isEqual || isIdentical;
+ };
+
+ CleanScalar.prototype.isClean = function isClean() {
+ return this.isEqual(this.now(), this.cleaned());
+ };
+
+ CleanScalar.prototype.isDirty = isDirty;
+
+ CleanScalar.prototype.save = function save() {
+ this.cleaned(this.now());
+ };
+
+ CleanScalar.prototype.reset = function reset() {
+ this.now(this.cleaned());
+ };
+
+ /* Is there an intelligent way to do lists? */
+
+ /***************************************************************/
+
+ var dirtyBehavior = hd.dirty = {};
+
+ var reset = function reset() {
+ this[PROTO_NAME].cleaners.forEach(function (cleaner) {
+ cleaner.reset();
+ });
+ };
+
+ dirtyBehavior.mixin = function mixin(Ctor) {
+ Ctor.prototype.reset = reset;
+ };
+
+ var save = function save(truthy) {
+ if (truthy) {
+ this[PROTO_NAME].cleaners.forEach(function (cleaner) {
+ cleaner.save();
+ });
+ }
+ };
+
+ dirtyBehavior.enterCtor = function enterCtor(model) {
+ var cleaners = model[PROTO_NAME].cleaners = [];
+
+ model.isClean = factory.computed(function () {
+ return cleaners.every(function (cleaner) {
+ return cleaner.isClean();
+ });
+ }, save);
+
+ model.isDirty = isDirty;
+ };
+
+ hd.proxy.save = function save(isEqual) {
+ var model = factory.contexts[0];
+ ASSERT(model, "expected context for clean variable");
+ var proto = model[PROTO_NAME];
+ ASSERT(proto && proto.cleaners,
+ "expected dirty behavior to be enabled");
+
+ var now = this;
+ var cleaned = factory.variable(now());
+ var cleaner = new CleanScalar(now, cleaned, isEqual);
+ proto.cleaners.push(cleaner);
+
+ return this;
+ };
+
+}());
+
View
5 lib/model/factory/factory.js
@@ -4,8 +4,9 @@
/* Initialization. */
hd.__private.factory = {
- /* Set a sensible value for 'this' within methods. */
- contexts: []
+ /* Set a sensible value for 'this' within methods, with a default context
+ * for globals. */
+ contexts: [{}]
};
/***************************************************************/
View
32 lib/model/factory/internal.js
@@ -10,14 +10,18 @@
var vv = new hd.__private.Variable(cellType, initialValue);
runtime.touch(vv);
hd.behaviors.forEach(function (behavior) {
- if (behavior.variable) behavior.variable(vv);
+ if (behavior.variable) {
+ behavior.variable(vv);
+ }
});
+ LOG("added " + vv);
+ INSPECT(vv);
return vv;
};
- factory.addMethod = function addMethod(outputs, fn) {
- var mm = new hd.__private.Method(outputs, fn);
- mm.context = factory.contexts[0];
+ factory.addMethod = function addMethod(outputs, fn, context) {
+ var mm = new hd.__private.Method(outputs, fn);
+ mm.context = context ? context : factory.contexts[0];
return mm;
};
@@ -31,20 +35,24 @@
/* Does everything but create the proxy. Allows us to create different
* proxies for different types of variables. */
- factory.addComputedVariable = function addComputedVariable(cellType, fn) {
- var initialValue = undefined;
- var vv = this.addVariable(cellType, initialValue);
- vv.dependsOnSelf = true;
- this.addOneWayConstraint([vv], fn);
+ factory.addComputedVariable
+ = function addComputedVariable(cellType, fn, context)
+ {
+ var initialValue;
+ var vv = factory.addVariable(cellType, initialValue);
+ var mm = factory.addOneWayConstraint([vv], fn, context);
return vv;
};
/* Create a new one-way constraint.
*
* Note: There is no actual constraint, just a single method. */
- factory.addOneWayConstraint = function addOneWayConstraint(outputs, fn) {
- var mm = this.addMethod(outputs, fn);
- this.setOneWayConstraint(mm);
+ factory.addOneWayConstraint
+ = function addOneWayConstraint(outputs, fn, context)
+ {
+ var mm = factory.addMethod(outputs, fn, context);
+ factory.setOneWayConstraint(mm);
+ runtime.enqueue(mm);
return mm;
};
View
115 lib/model/factory/model.js
@@ -1,26 +1,21 @@
(function () {
- var factory = hd.__private.factory;
+ var factory = hd.__private.factory;
+ var PROTO_NAME = hd.PROTO_NAME;
/***************************************************************/
- /* Final processing. */
+ /* Final processing */
var submit = function submit() {
/* In here, we can pretend that we are a client with access to hd. */
- var data = {};
- Object.keys(this).forEach(function (v) {
- var value = this[v];
- if (hd.isVariable(value)) value = value();
- /* Reject any constants or variables that are functions. */
- if (typeof value !== "function") data[v] = value;
- }, this);
- var url = location.protocol + location.hostname + location.pathname;
- return hd.fn(submitForm)(url, data);
+ hd.toJS(this);
};
var sink = function sink() {
Object.keys(this).forEach(function (v) {
- if (hd.isVariable(this[v])) this[v]();
+ if (hd.isVariable(this[v])) {
+ this[v]();
+ }
}, this);
};
@@ -36,7 +31,7 @@
if (hd.isProxy(value)) {
var vv = value.unwrap();
- if (vv.cellType === "output") {
+ if (vv.cellType === "command") {
hasCommand = true;
}
@@ -50,41 +45,97 @@
}
});
- /* The default output variable will access every variable so that they are
- * all relevant. It will not be visible to users. */
+ /* The default command is a no-op that accesses every variable so that
+ * they are all relevant. It is invisible to users. */
if (!hasCommand) {
hd.command(sink);
}
};
- factory.model = function model(Ctor) {
+ /***************************************************************/
+ /* Model constructor "prototype" */
- /* The general case for a Model is a constructor. We support plain
- * objects as a convenience. */
+ var call = function call(context/*, args...*/) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ return this[PROTO_NAME].Ctor.apply(context, args);
+ };
- if (typeof Ctor === "function") {
- return function CtorWrapped(/*...*/) {
- var model = Object.create(Ctor.prototype);
+ var apply = function apply(context, args) {
+ return this[PROTO_NAME].Ctor.apply(context, args);
+ };
- factory.contexts.unshift(model);
- Ctor.apply(model, arguments);
- maybeAddCommand(model);
- factory.contexts.shift();
+ var behaviors = function behaviors(/*behaviors...*/) {
+ Array.prototype.forEach.call(arguments, function (behavior) {
+ var bs = this[PROTO_NAME].behaviors;
+ if (bs.has(behavior)) return;
+ behavior.mixin(this);
+ bs.push(behavior);
+ }, this);
+ };
- hd.update();
- return model;
- };
+ /***************************************************************/
+ /* hd.model */
+ var finishModelConstructor = function finishModelConstructor(Base, Ctor) {
+ if (typeof Ctor === "undefined") {
+ Ctor = Base;
+ Base = undefined;
} else {
- ASSERT(typeof Ctor === "object",
- "expected object or constructor for model");
- var model = Ctor;
+ Ctor.prototype = Object.create(Base.prototype);
+ }
+
+ var CtorHd = function CtorHd(/*...*/) {
+ var model = this;
+ var bs = CtorHd[PROTO_NAME].behaviors;
+
+ factory.contexts.unshift(model);
+ model[PROTO_NAME] = {};
+ bs.forEach(function (behavior) {
+ if (behavior.enterCtor) {
+ behavior.enterCtor(model);
+ }
+ });
+
+ Ctor.apply(model, arguments);
+
maybeAddCommand(model);
+ bs.forEach(function (behavior) {
+ if (behavior.exitCtor) {
+ behavior.exitCtor(model);
+ }
+ });
+ factory.contexts.shift();
hd.update();
- return model;
+ };
+
+ CtorHd.prototype = Ctor.prototype;
+ CtorHd.call = call;
+ CtorHd.apply = apply;
+ CtorHd.behaviors = behaviors;
+
+ CtorHd[PROTO_NAME] = {
+ Ctor: Ctor,
+ behaviors: (Base && Base[PROTO_NAME])
+ ? Base[PROTO_NAME].behaviors.slice()
+ : []
+ };
+
+ return CtorHd;
+ };
+
+ factory.model = function model(one, two) {
+ if (typeof one === "function") {
+ return finishModelConstructor(one, two);
}
+ /* The general case for a Model is a constructor. We support plain
+ * objects as a convenience. */
+ ASSERT(typeof one === "object",
+ "expected object or constructor for model");
+ maybeAddCommand(one);
+ hd.update();
+ return one;
};
hd.model = factory.model;
View
45 lib/model/factory/solver.js
@@ -25,6 +25,9 @@
this.variables.push(vvv);
this.priority.push(vvv);
this.priority.sort(definedLater);
+ /* A weird bug will sometimes leave this "sorted" in a weird order. Try
+ * again to make sure it's right. */
+ this.priority.sort(definedLater);
LOG(vv + " added to solver");
} else {
@@ -58,7 +61,9 @@
if (!ccc) {
/* Default for user-defined constraints. */
- if (strength === undefined) strength = Strength.REQUIRED;
+ if (strength === undefined) {
+ strength = Strength.REQUIRED;
+ }
ccc = new Solver.Constraint(cc, strength);
this.constraints.push(ccc);
@@ -82,24 +87,26 @@
/* Eat another solver if it exists. */
Solver.prototype.merge = function merge(other) {
- if (other && other !== this) {
- Array.prototype.push.apply(this.variables, other.variables);
- /* We want a priority that reflects the variables' definition order. */
- this.priority = this.variables.slice();
- this.priority.sort(definedLater);
- other.variables.forEach(function (vvv) {
- vvv.outer.solver = this;
- }, this);
- other.variables = [];
-
- Array.prototype.push.apply(this.constraints, other.constraints);
- other.constraints = [];
-
- /* Have to do it this way so we don't add stay constraints. They will be
- * added on solve. */
- Array.prototype.push.apply(this.unenforcedCnsQueue, other.unenforcedCnsQueue);
- LOG("Merged two constraint graphs.")
- };
+ if (!other || other === this) {
+ return;
+ }
+
+ Array.prototype.push.apply(this.variables, other.variables);
+ /* We want a priority that reflects the variables' definition order. */
+ this.priority = this.variables.slice();
+ this.priority.sort(definedLater);
+ other.variables.forEach(function (vvv) {
+ vvv.outer.solver = this;
+ }, this);
+ other.variables = [];
+
+ Array.prototype.push.apply(this.constraints, other.constraints);
+ other.constraints = [];
+
+ /* Have to do it this way so we don't add stay constraints. They will be
+ * added on solve. */
+ Array.prototype.push.apply(this.unenforcedCnsQueue, other.unenforcedCnsQueue);
+ LOG("Merged two constraint graphs.");
};
}());
View
20 lib/model/factory/translation.js
@@ -0,0 +1,20 @@
+(function () {
+
+ var proxies = hd.__private.proxies;
+ var factory = hd.__private.factory;
+
+ factory.translation = function translation(target, error) {
+ var vv = factory.addVariable("interface");
+
+ if (!error) {
+ error = factory.variable();
+ }
+
+ return proxies.makeTranslationProxy(vv, target, error);
+ };
+
+ hd.translation = factory.translation;
+ hd.translator = factory.translation;
+
+}());
+
View
35 lib/model/factory/variable.js
@@ -7,12 +7,16 @@
/* Variables. */
factory.variable = function variable(initialValue) {
- var vv = this.addVariable("interface", initialValue);
- return proxies.makeVariableProxy(vv);
+ var vv = factory.addVariable("interface", initialValue);
+ return proxies.makeInterfaceProxy(vv);
+ };
+
+ factory.number = function number(initialValue) {
+ return factory.variable(parseFloat(initialValue));
};
factory.list = function list(initialValue) {
- var vv = this.addVariable("interface", initialValue);
+ var vv = factory.addVariable("interface", initialValue);
return proxies.makeArrayProxy(vv);
};
@@ -22,16 +26,16 @@
* only a single output, then it can be given without wrapping it in an
* array.
*/
- factory.computed = function computed(fn, set) {
- var vv = this.addComputedVariable("logic", fn);
- return proxies.makeComputedProxy(vv, set);
+ factory.computed = function computed(fn, set, context) {
+ var vv = factory.addComputedVariable("logic", fn, context);
+ return proxies.makeLogicProxy(vv, set);
};
/***************************************************************/
/* Commands. */
factory.command = function command(fn) {
- var vv = this.addComputedVariable("output", fn);
+ var vv = factory.addComputedVariable("command", fn);
return proxies.makeCommandProxy(vv);
};
@@ -40,13 +44,13 @@
factory.fn = function fn(fnToWrap) {
/* The outer function takes the arguments to be passed to the wrapped
* function. It will be called inside a method. It will return a wrapped
- * function to be stored as the value of the output variable.
+ * function to be stored as the value of the command variable.
*
* The inner function will call the wrapped function with both the stored
* arguments from the method and any new arguments. It may be called by the
* user. */
return function () {
- var context = this;
+ var context = this;
var argsToPass = [].slice.call(arguments);
return function () {
return fnToWrap.apply(context, argsToPass.concat(arguments));
@@ -54,11 +58,14 @@
};
};
- hd.variable = factory.variable.bind(factory);
- hd.list = factory.list.bind(factory);
- hd.computed = factory.computed.bind(factory);
- hd.command = factory.command.bind(factory);
- hd.fn = factory.fn.bind(factory);
+ hd.variable = factory.variable;
+ hd.number = factory.number;
+ hd.list = factory.list;
+ hd.computed = factory.computed;
+ hd.command = factory.command;
+ hd.fn = factory.fn;
+
+ proxies.addArrayProxyMethods(hd.list);
}());
View
69 lib/model/model.js
@@ -6,7 +6,9 @@
var idNo = 0;
var makeName = function makeName(kind) {
- if (idNo === Number.MAX_VALUE) idNo = 0;
+ if (idNo === Number.MAX_VALUE) {
+ idNo = 0;
+ }
return "__" + kind + (idNo++);
};
@@ -30,14 +32,12 @@
* If this is a function, then the variable will have a function value.
*/
var Variable = function Variable(cellType, initialValue) {
- this.orderNo = idNo;
- this.id = makeName("variable");
- this.cellType = cellType;
- this.value = initialValue;
- /* TODO: Do we want to track this for resetting purposes? */
- //this.initialValue = initialValue;
+ this.orderNo = idNo;
+ this.id = makeName("variable");
+ this.cellType = cellType;
+ this.value = initialValue;
this.hasBeenEdited = false;
- this.usedBy = [];
+ this.usedBy = [];
publisher.initialize(this);
};
@@ -45,30 +45,35 @@
publisher.mixin(Variable);
Variable.prototype.isChanged = function isChanged() {
- return this.changeEvent;
+ return this.hasDraft("value");
};
/* The most basic mutation is an overwriting assignment. Richer operations
* will need to decide for themselves (1) what constitutes a change and (2)
* what information is needed to reproduce it. */
Variable.prototype.set = function set(value) {
- if (value === this.value) return;
- ASSERT(!this.isChanged(), "overlapping writes");
- this.changeEvent = { set: true,
- log : "set " + this + " : " +
+ if (value === this.value) {
+ return;
+ }
+ DEBUG_BEGIN;
+ if (this.isChanged()) {
+ WARNING("overlapping writes");
+ }
+ DEBUG_END;
+ this.draft("value", {
+ set: true,
+ log: "set " + this + " : " +
((typeof this.value === "function")
? "<function>" : JSON.stringify(this.value)) + " ==> " +
((typeof value === "function")
? "<function>" : JSON.stringify(value))
- };
+ });
this.value = value;
};
Variable.prototype.publishChange = function publishChange() {
ASSERT(this.isChanged(), "expected a change to publish");
- LOG(this.changeEvent.log);
- this.publish("value", this.changeEvent);
- delete this.changeEvent;
+ this.publish("value");
};
/**
@@ -100,27 +105,30 @@
* @param {Function :: () -> [concept.model.Value]} fn
* The function that computes new values for the variables.
* <br />
- * Methods may not set values for any variable in their function body; such
- * values must be returned by the method, in the order matching that given
- * for the outputs parameter.
+ * Methods may not set values for any variable in their function body;
+ * such values must be returned by the method, in the order matching
+ * that given for the outputs parameter.
* <br />
* Methods may use 'this' to access variables defined in their
* {@link concept.model.Model}.
*/
var Method = function Method(outputs, fn) {
- this.id = makeName("method");
- this.outputs = outputs;
- this.fn = fn;
- this.inputsUsed = [];
+ this.id = makeName("method");
+ this.outputs = outputs;
+ this.fn = fn;
+ this.inputsUsed = [];
this.inputsUsedPrev = [];
};
+ Method.prototype.toString = toId;
+
/**
* @methodOf hd.__private.Method#
* @returns {String} Shows inputs and outputs.
*/
- Method.prototype.toString = function toString() {
- return "[" + this.inputsUsed + "] -> [" + this.outputs + "]";
+ Method.prototype.toSignature = function toSignature() {
+ return this.id +
+ " :: [" + this.inputsUsed + "] -> [" + this.outputs + "]";
};
/**
@@ -142,8 +150,10 @@
* @param {[hd.__private.Method]} methods
*/
var Constraint = function Constraint(methods) {
- this.id = makeName("constraint");
- this.methods = methods;
+ this.id = makeName("constraint");
+ this.methods = methods;
+ this.selectedMethod = null;
+ this.selectedMethodPrev = null;
};
/**
@@ -159,12 +169,13 @@
*/
Constraint.prototype.toJSON = function toJSON() {
return Object.extract(this, ["id", "methods"]);
- }
+ };
/**
* @name hd.__private
* @namespace For model data structures and algorithms.
*/
+ hd.__private.makeName = makeName;
hd.__private.Variable = Variable;
hd.__private.Method = Method;
hd.__private.Constraint = Constraint;
View
234 lib/model/proxies/array.js
@@ -4,74 +4,205 @@
var runtime = hd.__private.runtime;
var evaluator = hd.__private.evaluator;
+ var clear = function clear() {
+ var vv = this.unwrap();
+
+ vv.draft("value", { set: true });
+
+ runtime.touch(vv);
+
+ vv.value = [];
+ return this;
+ };
+
+ var pop = function pop() {
+ var vv = this.unwrap();
+
+ /* Abort when no change. */
+ if (vv.value.length === 0) {
+ return;
+ }
+
+ /* If there is a `set` change, subscribers should ignore everything
+ * following. That way, we don't have to check here. This goes for every
+ * change. */
+ vv.draft("value").push({
+ remove: { index: vv.value.length - 1, howMany: 1 }
+ });
+
+ runtime.touch(vv);
+
+ return vv.value.pop();
+ };
+
var push = function push() {
var vv = this.unwrap();
- ASSERT(!vv.isChanged(), "folding change events not supported");
- vv.changeEvent = {
- adds: [{ index: vv.value.length, howMany: arguments.length }]
- };
- Array.prototype.push.apply(vv.value, arguments);
+ vv.draft("value").push({
+ add: {
+ index: vv.value.length,
+ items: Array.prototype.slice.call(arguments)
+ }
+ });
+
+ runtime.touch(vv);
+
+ return Array.prototype.push.apply(vv.value, arguments);
+ };
+
+ var reverse = function reverse() {
+ var vv = this.unwrap();
+
+ vv.draft("value", { set: true });
runtime.touch(vv);
+
+ vv.value.reverse();
+ return this;
};
- var remove = function remove(item) {
+ var shift = function shift() {
var vv = this.unwrap();
- ASSERT(!vv.isChanged(), "folding change events not supported");
/* Abort when no change. */
- var index = vv.value.indexOf(item);
- if (index < 0) return;
- vv.changeEvent = { removes: [{ index: index, howMany: 1 }] };
- vv.value.splice(index, 1);
+ if (vv.value.length === 0) {
+ return;
+ }
+ vv.draft("value").push({
+ remove: { index: 0, howMany: 1 }
+ });
runtime.touch(vv);
- };
- var pop = function pop() {
+ return vv.value.shift();
+ };
+
+ var sort = function sort(f) {
var vv = this.unwrap();
ASSERT(!vv.isChanged(), "folding change events not supported");
- /* Abort when no change. */
- if (vv.value.length === 0) return;
- vv.changeEvent = {
- removes: [{ index: vv.value.length - 1, howMany: 1 }]
- };
- vv.value.pop();
+ vv.draft("value", { set: true });
runtime.touch(vv);
+
+ vv.value.sort(f);
+ return this;
};
- var filter = function filter(p) {
+ var splice = function splice(index, howMany/*, ...*/) {
+ var vv = this.unwrap();
+ var list = vv.value;
+
+ if (index < 0) {
+ index = ((list.length + index) >= 0) ? (list.length + index) : 0;
+ }
+
+ if ((arguments.length === 1) || (howMany > list.length - index)) {
+ howMany = list.length - index;
+ }
+
+ if (howMany > 0) {
+ vv.draft("value").push({
+ remove: { index: index, howMany: howMany }
+ });
+ }
+
+ if (arguments.length > 2) {
+ vv.draft("value").push({
+ add: {
+ index: index,
+ items: Array.prototype.slice.call(arguments, 2)
+ }
+ });
+ }
+
+ runtime.touch(vv);
+
+ return Array.prototype.splice.apply(vv.value, arguments);
+ };
+
+ var unshift = function unshift() {
var vv = this.unwrap();
- var list = vv.value;
- var removes = [];
- var i = list.length;
- var howMany = 0;
- /* Instead of burdening each consumer with the task of sorting the
- * removals, we guarantee that removes are in back-to-front order. */
+ vv.draft("value").push({
+ add: {
+ index: 0,
+ items: Array.prototype.slice.call(arguments)
+ }
+ });
+
+ runtime.touch(vv);
+
+ return Array.prototype.unshift.apply(vv.value, arguments);
+ };
+
+ /* Instead of burdening each consumer with the task of sorting the
+ * removals, we guarantee that removes are in back-to-front order. */
+ var subranges = function subranges(list, pred, f) {
+ var howMany = 0;
+ var i = list.length;
for (; i > 0; --i) {
- /* Filter removes items that do *not* meet the predicate. */
- if (!p(list[i - 1])) {
+ if (pred(list[i - 1])) {
++howMany;
- } else {
- if (howMany > 0) {
- removes.push({ index: i, howMany: howMany });
- howMany = 0;
- }
+ } else if (howMany > 0) {
+ f(i, howMany);
+ howMany = 0;
}
}
if (howMany > 0) {
- removes.push({ index: i, howMany: howMany });
+ f(i, howMany);
}
+ };
+
+ var prune = function prune(pred) {
+ var vv = this.unwrap();
- vv.changeEvent = { removes: removes };
- vv.value = list.filter(p);
+ var list = vv.value;
+ var draft = vv.draft("value");
+ subranges(list, pred, function (index, howMany) {
+ draft.push({
+ remove: { index: index, howMany: howMany }
+ });
+ list.splice(index, howMany);
+ });
runtime.touch(vv);
+
+ /* No further action necessary; spliced earlier. */
+ return this;
+ };
+
+ var remove = function remove(/*...*/) {
+ var args = Array.prototype.slice.call(arguments);
+ return prune.call(this, function (item) { return args.has(item); });
+ };
+
+ var swap = function swap(i, j) {
+ var vv = this.unwrap();
+
+ var list = vv.value;
+ /* Out-of-bounds indices will be ignored. */
+ if (i === j || i < 0 || list.length <= i || j < 0 || list.length <= j) {
+ return;
+ }
+
+ /* We want to guarantee to subscribers that i < j. */
+ var tmp = i;
+ if (i > j) {
+ i = j;
+ j = tmp;
+ }
+
+ vv.draft("value").push({ swap: [i, j] });
+
+ runtime.touch(vv);
+
+ tmp = list[i];
+ list[i] = list[j];
+ list[j] = tmp;
+
+ return this;
};
proxies.makeArrayProxy = function makeArrayProxy(vv) {
@@ -88,24 +219,39 @@
} else if (arguments.length === 2) {
/* Element assignment: proxy(index, elt) */
vv.value[a0] = a1;
- vv.changeEvent = {
- removes: [{ index: a0, howMany: 1 }],
- adds: [{ index: a0, howMany: 1 }]
- };
+ vv.draft("value").push({
+ remove: { index: a0, howMany: 1 }
+ }, {
+ add: { index: a0, items: [a1] }
+ });
runtime.touch(vv);
} else {
return evaluator.get(vv);
}
};
- proxy.push = push;
- proxy.pop = pop;
- proxy.remove = remove;
- proxy.filter = filter;
+ addArrayProxyMethods(proxy);
return proxies.finishProxy(vv, proxy);
};
+ var addArrayProxyMethods
+ = proxies.addArrayProxyMethods
+ = function addArrayProxyMethods(o)
+ {
+ o.clear = clear;
+ o.pop = pop;
+ o.push = push;
+ o.reverse = reverse;
+ o.shift = shift;
+ o.sort = sort;
+ o.splice = splice;
+ o.unshift = unshift;
+ o.prune = prune;
+ o.remove = remove;
+ o.swap = swap;
+ };
+