'))));
},
handleEvent: function (e) {
if (e.type === "message" && e.message === "bump_count") {
@@ -49,7 +50,7 @@ $(document).ready(function () {
});
G.dataspace.setOnStateChange(function (mux, patch) {
- $("#spy-holder").text(Syndicate.prettyTrie(mux.routingTable));
+ document.getElementById('spy-holder').innerText = Syndicate.prettyTrie(mux.routingTable);
});
G.startStepping();
diff --git a/js/examples/index.md b/js/examples/index.md
index 537b758..785187a 100644
--- a/js/examples/index.md
+++ b/js/examples/index.md
@@ -13,36 +13,25 @@ This is a simple clickable button; each time the button is clicked,
the number on the face of the button is incremented.
The actor maintaining the counter also maintains the button's label
-and listens to click events. It uses the Syndicate/js DOM driver to
-publish the button's label text based on its internal state, and the
-Syndicate/js jQuery driver to subscribe to button click events.
+and listens to click events. It uses the Syndicate/js UI driver to
+publish the button's label text based on its internal state and to
+subscribe to button click events.
- [DEMO](button/)
- [Source code](button/index.js) using the Syndicate/js DSL
## DOM example
-This example demonstrates two actors, each using the Syndicate/js DOM
-driver to display user interface, and the jQuery driver to receive
-events from it. The first actor presents a button to the user, which
-when clicked sends a message to the other actor. The second actor
-receives messages from the first, updates its internal state, and
-reflects its new internal state in its visible UI.
+This example demonstrates two actors, each using the Syndicate/js UI
+driver to display user interface and receive events from it. The first
+actor presents a button to the user, which when clicked sends a
+message to the other actor. The second actor receives messages from
+the first, updates its internal state, and reflects its new internal
+state in its visible UI.
- [DEMO](dom/)
- [Source code](dom/index.js) in plain JavaScript
-## jQuery Example
-
-This example is similar to the button example, but uses plain
-JavaScript instead of the Syndicate/js DSL, calling out to Syndicate
-as a library. It uses the Syndicate/js jQuery driver to receive click
-events from the button, but does not use the Syndicate/js DOM driver;
-instead, it updates the DOM directly.
-
- - [DEMO](jquery/)
- - [Source code](jquery/index.js) in plain JavaScript
-
## Text Entry Widget
This is a simple text entry GUI control, following a design of
diff --git a/js/examples/iot/index.js b/js/examples/iot/index.js
index 445698c..7504683 100644
--- a/js/examples/iot/index.js
+++ b/js/examples/iot/index.js
@@ -5,17 +5,15 @@ assertion type tvAlert(text);
assertion type switchAction(on);
assertion type componentPresent(name);
-var DOM = Syndicate.DOM.DOM;
-var jQueryEvent = Syndicate.JQuery.jQueryEvent;
-
///////////////////////////////////////////////////////////////////////////
// TV
function spawnTV() {
actor {
+ var ui = new Syndicate.UI.Anchor();
react {
during tvAlert($text) {
- assert DOM('#tv', 'alert', Mustache.render($('#alert_template').html(), { text: text }));
+ assert ui.context(text).html('#tv', Mustache.render($('#alert_template').html(), { text: text }));
}
}
}
@@ -28,7 +26,7 @@ function spawnRemoteControl() {
actor {
react {
assert componentPresent('remote control');
- on message jQueryEvent('#remote-control', 'click', _) {
+ on message Syndicate.UI.globalEvent('#remote-control', 'click', _) {
:: remoteClick();
}
}
@@ -63,23 +61,24 @@ function spawnRemoteListener() {
function spawnStoveSwitch() {
actor {
this.powerOn = false;
+ this.ui = new Syndicate.UI.Anchor();
react {
assert componentPresent('stove switch');
assert switchState(this.powerOn);
- assert DOM('#stove-switch', 'switch-state',
- Mustache.render($('#stove_element_template').html(),
- { imgurl: ("img/stove-coil-element-" +
- (this.powerOn ? "hot" : "cold") + ".jpg") }));
+ assert this.ui.html('#stove-switch',
+ Mustache.render($('#stove_element_template').html(),
+ { imgurl: ("img/stove-coil-element-" +
+ (this.powerOn ? "hot" : "cold") + ".jpg") }));
- on message jQueryEvent('#stove-switch-on', 'click', _) { this.powerOn = true; }
- on message jQueryEvent('#stove-switch-off', 'click', _) { this.powerOn = false; }
+ on message Syndicate.UI.globalEvent('#stove-switch-on', 'click', _) { this.powerOn = true; }
+ on message Syndicate.UI.globalEvent('#stove-switch-off', 'click', _) { this.powerOn = false; }
on message switchAction($newState) {
this.powerOn = newState;
}
} until {
- case message jQueryEvent('#kill-stove-switch', 'click', _);
+ case message Syndicate.UI.globalEvent('#kill-stove-switch', 'click', _);
}
}
}
@@ -87,18 +86,19 @@ function spawnStoveSwitch() {
function spawnPowerDrawMonitor() {
actor {
this.watts = 0;
+ this.ui = new Syndicate.UI.Anchor();
react {
assert componentPresent('power draw monitor');
assert powerDraw(this.watts);
- assert DOM('#power-draw-meter', 'power-draw',
- Mustache.render($('#power_draw_template').html(), { watts: this.watts }));
+ assert this.ui.html('#power-draw-meter',
+ Mustache.render($('#power_draw_template').html(), { watts: this.watts }));
on asserted switchState($on) {
this.watts = on ? 1500 : 0;
}
} until {
- case message jQueryEvent('#kill-power-draw-monitor', 'click', _);
+ case message Syndicate.UI.globalEvent('#kill-power-draw-monitor', 'click', _);
}
}
}
@@ -207,7 +207,7 @@ function spawnChaosMonkey() {
jKillButtons.prop('disabled', true);
}
}
- on message jQueryEvent(spawnButtonSelector, 'click', _) {
+ on message Syndicate.UI.globalEvent(spawnButtonSelector, 'click', _) {
spawnFunction();
}
}
@@ -219,8 +219,7 @@ function spawnChaosMonkey() {
$(document).ready(function () {
ground dataspace G {
- Syndicate.JQuery.spawnJQueryDriver();
- Syndicate.DOM.spawnDOMDriver();
+ Syndicate.UI.spawnUIDriver();
Syndicate.Timer.spawnTimerDriver();
spawnTV();
diff --git a/js/examples/jquery/index.html b/js/examples/jquery/index.html
deleted file mode 100644
index d5bea5b..0000000
--- a/js/examples/jquery/index.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
- Syndicate: jQuery Example
-
-
-
-
-
-
-
jQuery example
-
-
0
-
-
-
diff --git a/js/examples/jquery/index.js b/js/examples/jquery/index.js
deleted file mode 100644
index 891cc0b..0000000
--- a/js/examples/jquery/index.js
+++ /dev/null
@@ -1,34 +0,0 @@
-"use strict";
-
-var G;
-$(document).ready(function () {
- var Dataspace = Syndicate.Dataspace;
- var sub = Syndicate.sub;
- var __ = Syndicate.__;
- var _$ = Syndicate._$;
-
- G = new Syndicate.Ground(function () {
- console.log('starting ground boot');
-
- Syndicate.JQuery.spawnJQueryDriver();
-
- Dataspace.spawn({
- boot: function () {
- return sub(Syndicate.JQuery.jQueryEvent('#clicker', 'click', __));
- },
- handleEvent: function (e) {
- if (e.type === 'message'
- && Syndicate.JQuery.jQueryEvent.isClassOf(e.message)
- && e.message[0] === '#clicker')
- {
- var r = $('#result');
- r.html(Number(r.html()) + 1);
- }
- }
- });
- });
- G.dataspace.setOnStateChange(function (mux, patch) {
- $("#spy-holder").text(Syndicate.prettyTrie(mux.routingTable));
- });
- G.startStepping();
-});
diff --git a/js/examples/smoketest-dsl/index.html b/js/examples/smoketest-dsl/index.html
index b8208b7..b9a3fc6 100644
--- a/js/examples/smoketest-dsl/index.html
+++ b/js/examples/smoketest-dsl/index.html
@@ -3,7 +3,6 @@
Syndicate: Smoketest with DSL
-
diff --git a/js/examples/smoketest/index.html b/js/examples/smoketest/index.html
index f5a020e..e6853cf 100644
--- a/js/examples/smoketest/index.html
+++ b/js/examples/smoketest/index.html
@@ -3,7 +3,6 @@
Syndicate: Smoketest
-
diff --git a/js/examples/smoketest/index.js b/js/examples/smoketest/index.js
index a960659..a64f6c5 100644
--- a/js/examples/smoketest/index.js
+++ b/js/examples/smoketest/index.js
@@ -3,33 +3,33 @@
var beep = Syndicate.Struct.makeConstructor('beep', ['counter']);
var G;
-$(document).ready(function () {
- var Dataspace = Syndicate.Dataspace;
- var sub = Syndicate.sub;
- var __ = Syndicate.__;
- var _$ = Syndicate._$;
+document.addEventListener('DOMContentLoaded', function () {
+ var Dataspace = Syndicate.Dataspace;
+ var sub = Syndicate.sub;
+ var __ = Syndicate.__;
+ var _$ = Syndicate._$;
- G = new Syndicate.Ground(function () {
- console.log('starting ground boot');
+ G = new Syndicate.Ground(function () {
+ console.log('starting ground boot');
- Dataspace.spawn({
- counter: 0,
- boot: function () {},
- handleEvent: function (e) {},
- step: function () {
- Dataspace.send(beep(this.counter++));
- return this.counter <= 10;
- }
- });
-
- Dataspace.spawn({
- boot: function () { return sub(beep.pattern); },
- handleEvent: function (e) {
- if (e.type === 'message') {
- console.log("beep!", e.message[0]);
- }
- }
- });
+ Dataspace.spawn({
+ counter: 0,
+ boot: function () {},
+ handleEvent: function (e) {},
+ step: function () {
+ Dataspace.send(beep(this.counter++));
+ return this.counter <= 10;
+ }
});
- G.startStepping();
+
+ Dataspace.spawn({
+ boot: function () { return sub(beep.pattern); },
+ handleEvent: function (e) {
+ if (e.type === 'message') {
+ console.log("beep!", e.message[0]);
+ }
+ }
+ });
+ });
+ G.startStepping();
});
diff --git a/js/examples/svg/index.html b/js/examples/svg/index.html
index 243dd81..928f7d5 100644
--- a/js/examples/svg/index.html
+++ b/js/examples/svg/index.html
@@ -4,7 +4,6 @@
Syndicate: SVG
-
diff --git a/js/examples/svg/index.js b/js/examples/svg/index.js
index 153eeda..5811235 100644
--- a/js/examples/svg/index.js
+++ b/js/examples/svg/index.js
@@ -1,25 +1,22 @@
-var DOM = Syndicate.DOM.DOM;
+ground dataspace G {
+ Syndicate.UI.spawnUIDriver();
+ Syndicate.Timer.spawnTimerDriver();
-$(document).ready(function () {
- ground dataspace G {
- Syndicate.DOM.spawnDOMDriver();
- Syndicate.Timer.spawnTimerDriver();
+ actor {
+ var ui = new Syndicate.UI.Anchor();
+ react {
+ assert ui.html('#clock',
+ '')
+ when (typeof this.angle === 'number');
- actor {
- react {
- assert DOM('#clock', 'clock',
- '')
- when (typeof this.angle === 'number');
-
- on message Syndicate.Timer.periodicTick(1000) {
- this.angle = ((((Date.now() / 1000) % 60) / 60) - 0.25) * 2 * Math.PI;
- this.handX = 50 + 40 * Math.cos(this.angle);
- this.handY = 50 + 40 * Math.sin(this.angle);
- }
+ on message Syndicate.Timer.periodicTick(1000) {
+ this.angle = ((((Date.now() / 1000) % 60) / 60) - 0.25) * 2 * Math.PI;
+ this.handX = 50 + 40 * Math.cos(this.angle);
+ this.handY = 50 + 40 * Math.sin(this.angle);
}
}
}
-});
+}
diff --git a/js/examples/textfield-dsl/index.html b/js/examples/textfield-dsl/index.html
index 9684bd4..a8aa208 100644
--- a/js/examples/textfield-dsl/index.html
+++ b/js/examples/textfield-dsl/index.html
@@ -4,7 +4,6 @@
Syndicate: Textfield Example (DSL variation)
-
diff --git a/js/examples/textfield-dsl/index.js b/js/examples/textfield-dsl/index.js
index 747f709..45e47a5 100644
--- a/js/examples/textfield-dsl/index.js
+++ b/js/examples/textfield-dsl/index.js
@@ -1,7 +1,7 @@
///////////////////////////////////////////////////////////////////////////
// GUI
-var jQueryEvent = Syndicate.JQuery.jQueryEvent;
+var globalEvent = Syndicate.UI.globalEvent;
assertion type fieldCommand(detail);
assertion type fieldContents(text, pos);
assertion type highlight(state);
@@ -36,7 +36,7 @@ function spawnGui() {
var highlight = this.highlightState;
var hLeft = highlight ? highlight[0] : 0;
var hRight = highlight ? highlight[1] : 0;
- $("#fieldContents")[0].innerHTML = highlight
+ document.getElementById("fieldContents").innerHTML = highlight
? piece(text, pos, 0, hLeft, "normal") +
piece(text, pos, hLeft, hRight, "highlight") +
piece(text, pos, hRight, text.length + 1, "normal")
@@ -44,18 +44,22 @@ function spawnGui() {
};
react {
- on message jQueryEvent("#inputRow", "+keypress", $event) {
- var keycode = event.keyCode;
+ on message globalEvent("#inputRow", "+keydown", $event) {
+ switch (event.keyCode) {
+ case 37 /* left */: :: fieldCommand("cursorLeft"); break;
+ case 39 /* right */: :: fieldCommand("cursorRight"); break;
+ case 9 /* tab */: /* ignore */ break;
+ case 8 /* backspace */:
+ event.preventDefault(); // that this works here is a minor miracle
+ :: fieldCommand("backspace");
+ break;
+ default: break;
+ }
+ }
+
+ on message globalEvent("#inputRow", "+keypress", $event) {
var character = String.fromCharCode(event.charCode);
- if (keycode === 37 /* left */) {
- :: fieldCommand("cursorLeft");
- } else if (keycode === 39 /* right */) {
- :: fieldCommand("cursorRight");
- } else if (keycode === 9 /* tab */) {
- // ignore
- } else if (keycode === 8 /* backspace */) {
- :: fieldCommand("backspace");
- } else if (character) {
+ if (event.charCode && character) {
:: fieldCommand(["insert", character]);
}
}
@@ -126,7 +130,7 @@ function spawnSearch() {
this.highlight = false;
this.search = function () {
- var searchtext = $("#searchBox")[0].value;
+ var searchtext = document.getElementById("searchBox").value;
if (searchtext) {
var pos = this.fieldValue.indexOf(searchtext);
this.highlight = (pos !== -1) && [pos, pos + searchtext.length];
@@ -138,7 +142,7 @@ function spawnSearch() {
react {
assert highlight(this.highlight);
- on message jQueryEvent("#searchBox", "input", $event) {
+ on message globalEvent("#searchBox", "input", $event) {
this.search();
}
@@ -153,17 +157,14 @@ function spawnSearch() {
///////////////////////////////////////////////////////////////////////////
// Main
-$(document).ready(function () {
- ground dataspace G {
- Syndicate.JQuery.spawnJQueryDriver();
- Syndicate.DOM.spawnDOMDriver();
+ground dataspace G {
+ Syndicate.UI.spawnUIDriver();
- spawnGui();
- spawnModel();
- spawnSearch();
- }
+ spawnGui();
+ spawnModel();
+ spawnSearch();
+}
- G.dataspace.setOnStateChange(function (mux, patch) {
- $("#spy-holder").text(Syndicate.prettyTrie(mux.routingTable));
- });
+G.dataspace.setOnStateChange(function (mux, patch) {
+ document.getElementById("spy-holder").innerText = Syndicate.prettyTrie(mux.routingTable);
});
diff --git a/js/examples/textfield/index.html b/js/examples/textfield/index.html
index 42c64ad..be063ad 100644
--- a/js/examples/textfield/index.html
+++ b/js/examples/textfield/index.html
@@ -4,7 +4,6 @@
Syndicate: Textfield Example
-
diff --git a/js/examples/textfield/index.js b/js/examples/textfield/index.js
index ec8ff04..adf8a86 100644
--- a/js/examples/textfield/index.js
+++ b/js/examples/textfield/index.js
@@ -7,7 +7,7 @@ var Patch = Syndicate.Patch;
var __ = Syndicate.__;
var _$ = Syndicate._$;
-var jQueryEvent = Syndicate.JQuery.jQueryEvent;
+var globalEvent = Syndicate.UI.globalEvent;
var fieldContents = Syndicate.Struct.makeConstructor('fieldContents', ['text', 'pos']);
var highlight = Syndicate.Struct.makeConstructor('highlight', ['state']);
var fieldCommand = Syndicate.Struct.makeConstructor('fieldCommand', ['detail']);
@@ -36,7 +36,8 @@ function spawnGui() {
highlight: { state: false },
boot: function () {
- return Patch.sub(jQueryEvent("#inputRow", "+keypress", __))
+ return Patch.sub(globalEvent("#inputRow", "keypress", __))
+ .andThen(Patch.sub(globalEvent("#inputRow", "+keydown", __)))
.andThen(Patch.sub(fieldContents.pattern))
.andThen(Patch.sub(highlight.pattern));
},
@@ -46,31 +47,40 @@ function spawnGui() {
handleEvent: function (e) {
var self = this;
switch (e.type) {
- case "message":
- var event = e.message[2];
- var keycode = event.keyCode;
- var character = String.fromCharCode(event.charCode);
- if (keycode === 37 /* left */) {
- Dataspace.send(fieldCommand("cursorLeft"));
- } else if (keycode === 39 /* right */) {
- Dataspace.send(fieldCommand("cursorRight"));
- } else if (keycode === 9 /* tab */) {
- // ignore
- } else if (keycode === 8 /* backspace */) {
- Dataspace.send(fieldCommand("backspace"));
- } else if (character) {
- Dataspace.send(fieldCommand(["insert", character]));
- }
- break;
- case "stateChange":
- Trie.projectObjects(e.patch.added, this.fieldContentsProjection).forEach(function (c) {
- self.field = c;
- });
- Trie.projectObjects(e.patch.added, this.highlightProjection).forEach(function (c) {
- self.highlight = c;
- });
- this.updateDisplay();
- break;
+ case "message":
+ var event = e.message[2];
+ switch (event.type) {
+ case "keydown":
+ switch (event.keyCode) {
+ case 37 /* left */: Dataspace.send(fieldCommand("cursorLeft")); break;
+ case 39 /* right */: Dataspace.send(fieldCommand("cursorRight")); break;
+ case 9 /* tab */: /* ignore */ break;
+ case 8 /* backspace */:
+ event.preventDefault(); // that this works here is a minor miracle
+ Dataspace.send(fieldCommand("backspace"));
+ break;
+ default: break;
+ }
+ break;
+ case "keypress":
+ var character = String.fromCharCode(event.charCode);
+ if (event.charCode && character) {
+ Dataspace.send(fieldCommand(["insert", character]));
+ }
+ break;
+ default:
+ break;
+ }
+ break;
+ case "stateChange":
+ Trie.projectObjects(e.patch.added, this.fieldContentsProjection).forEach(function (c) {
+ self.field = c;
+ });
+ Trie.projectObjects(e.patch.added, this.highlightProjection).forEach(function (c) {
+ self.highlight = c;
+ });
+ this.updateDisplay();
+ break;
}
},
@@ -80,7 +90,7 @@ function spawnGui() {
var highlight = this.highlight ? this.highlight.state : false;
var hLeft = highlight ? highlight[0] : 0;
var hRight = highlight ? highlight[1] : 0;
- $("#fieldContents")[0].innerHTML = highlight
+ document.getElementById("fieldContents").innerHTML = highlight
? piece(text, pos, 0, hLeft, "normal") +
piece(text, pos, hLeft, hRight, "highlight") +
piece(text, pos, hRight, text.length + 1, "normal")
@@ -149,14 +159,14 @@ function spawnSearch() {
boot: function () {
this.publishState();
- return Patch.sub(jQueryEvent("#searchBox", "input", __))
+ return Patch.sub(globalEvent("#searchBox", "input", __))
.andThen(Patch.sub(fieldContents.pattern));
},
fieldContentsProjection: fieldContents(_$("text"), _$("pos")),
handleEvent: function (e) {
var self = this;
- if (jQueryEvent.isClassOf(e.message)) {
+ if (globalEvent.isClassOf(e.message)) {
this.search();
}
if (e.type === "stateChange") {
@@ -174,7 +184,7 @@ function spawnSearch() {
},
search: function () {
- var searchtext = $("#searchBox")[0].value;
+ var searchtext = document.getElementById("searchBox").value;
var oldHighlight = this.highlight;
if (searchtext) {
var pos = this.fieldValue.indexOf(searchtext);
@@ -193,10 +203,9 @@ function spawnSearch() {
// Main
var G;
-$(document).ready(function () {
+document.addEventListener('DOMContentLoaded', function () {
G = new Syndicate.Ground(function () {
- Syndicate.JQuery.spawnJQueryDriver();
- Syndicate.DOM.spawnDOMDriver();
+ Syndicate.UI.spawnUIDriver();
spawnGui();
spawnModel();
@@ -204,7 +213,7 @@ $(document).ready(function () {
});
G.dataspace.setOnStateChange(function (mux, patch) {
- $("#spy-holder").text(Syndicate.prettyTrie(mux.routingTable));
+ document.getElementById("spy-holder").innerText = Syndicate.prettyTrie(mux.routingTable);
});
G.startStepping();
diff --git a/js/src/dom-driver.js b/js/src/dom-driver.js
deleted file mode 100644
index 57c540c..0000000
--- a/js/src/dom-driver.js
+++ /dev/null
@@ -1,120 +0,0 @@
-// DOM fragment display driver
-var Patch = require("./patch.js");
-var DemandMatcher = require('./demand-matcher.js').DemandMatcher;
-var Struct = require('./struct.js');
-var Ack = require('./ack.js').Ack;
-
-var Dataspace_ = require("./dataspace.js");
-var Dataspace = Dataspace_.Dataspace;
-var __ = Dataspace_.__;
-var _$ = Dataspace_._$;
-
-var DOM = Struct.makeConstructor('DOM', ['selector', 'fragmentClass', 'fragmentSpec']);
-
-function spawnDOMDriver(domWrapFunction, jQueryWrapFunction) {
- domWrapFunction = domWrapFunction || DOM;
- var spec = domWrapFunction(_$('selector'), _$('fragmentClass'), _$('fragmentSpec'));
- Dataspace.spawn(
- new DemandMatcher([spec],
- [Patch.advertise(spec)],
- {
- onDemandIncrease: function (c) {
- Dataspace.spawn(new DOMFragment(c.selector,
- c.fragmentClass,
- c.fragmentSpec,
- domWrapFunction,
- jQueryWrapFunction));
- }
- }));
-}
-
-function DOMFragment(selector, fragmentClass, fragmentSpec, domWrapFunction, jQueryWrapFunction) {
- this.selector = selector;
- this.fragmentClass = fragmentClass;
- this.fragmentSpec = fragmentSpec;
- this.domWrapFunction = domWrapFunction;
- this.jQueryWrapFunction = jQueryWrapFunction;
- this.demandExists = false;
- this.subscriptionEstablished = new Ack();
- this.nodes = this.buildNodes();
-}
-
-DOMFragment.prototype.boot = function () {
- var self = this;
- var specification = self.domWrapFunction(self.selector, self.fragmentClass, self.fragmentSpec);
-
- Dataspace.spawn(new Dataspace(function () {
- Syndicate.JQuery.spawnJQueryDriver(self.selector+" > ."+self.fragmentClass,
- 1,
- self.jQueryWrapFunction);
- Dataspace.spawn({
- demandExists: false,
- subscriptionEstablished: new Ack(1),
- boot: function () {
- this.subscriptionEstablished.arm();
- return Patch.sub(Patch.advertise(specification), 1);
- },
- handleEvent: function (e) {
- this.subscriptionEstablished.check(e);
- if (e.type === "stateChange") {
- if (e.patch.hasAdded()) this.demandExists = true;
- if (e.patch.hasRemoved()) this.demandExists = false;
- }
- if (this.subscriptionEstablished.done && !this.demandExists) {
- Dataspace.exitDataspace();
- }
- }
- });
- }));
-
- this.subscriptionEstablished.arm();
- return Patch.sub(specification).andThen(Patch.pub(specification));
-};
-
-DOMFragment.prototype.handleEvent = function (e) {
- this.subscriptionEstablished.check(e);
- if (e.type === "stateChange") {
- if (e.patch.hasAdded()) this.demandExists = true;
- if (e.patch.hasRemoved()) this.demandExists = false;
- }
- if (this.subscriptionEstablished.done && !this.demandExists) {
- for (var i = 0; i < this.nodes.length; i++) {
- var n = this.nodes[i];
- n.parentNode.removeChild(n);
- }
- Dataspace.exit();
- }
-};
-
-///////////////////////////////////////////////////////////////////////////
-
-DOMFragment.prototype.buildNodes = function () {
- var self = this;
- var nodes = [];
- $(self.selector).each(function (index, domNode) {
- if (typeof self.fragmentSpec !== 'string') {
- throw new Error("DOM fragmentSpec not a string: " + JSON.stringify(self.fragmentSpec));
- }
- var newNodes = $('
' + self.fragmentSpec + '
')[0].childNodes;
- // This next loop looks SUPER SUSPICIOUS. What is happening is
- // that each time we call domNode.appendChild(n), where n is an
- // element of the NodeList newNodes, the DOM is **removing** n
- // from the NodeList in order to place it in its new parent. So,
- // each call to appendChild shrinks the NodeList by one node until
- // it is finally empty, and its length property yields zero.
- while (newNodes.length) {
- var n = newNodes[0];
- if ('classList' in n) {
- n.classList.add(self.fragmentClass);
- }
- domNode.appendChild(n);
- nodes.push(n);
- }
- });
- return nodes;
-};
-
-///////////////////////////////////////////////////////////////////////////
-
-module.exports.spawnDOMDriver = spawnDOMDriver;
-module.exports.DOM = DOM;
diff --git a/js/src/jquery-driver.js b/js/src/jquery-driver.js
deleted file mode 100644
index 8f6ab2d..0000000
--- a/js/src/jquery-driver.js
+++ /dev/null
@@ -1,90 +0,0 @@
-// JQuery event driver
-var Patch = require("./patch.js");
-var DemandMatcher = require('./demand-matcher.js').DemandMatcher;
-var Struct = require('./struct.js');
-
-var Dataspace_ = require("./dataspace.js");
-var Dataspace = Dataspace_.Dataspace;
-var __ = Dataspace_.__;
-var _$ = Dataspace_._$;
-
-var jQueryEvent = Struct.makeConstructor('jQueryEvent', ['selector', 'eventName', 'eventValue']);
-
-function spawnJQueryDriver(baseSelector, metaLevel, wrapFunction) {
- metaLevel = metaLevel || 0;
- wrapFunction = wrapFunction || jQueryEvent;
- Dataspace.spawn(
- new DemandMatcher([Patch.observe(wrapFunction(_$('selector'), _$('eventName'), __))],
- [Patch.advertise(wrapFunction(_$('selector'), _$('eventName'), __))],
- {
- metaLevel: metaLevel,
- onDemandIncrease: function (c) {
- Dataspace.spawn(new JQueryEventRouter(baseSelector,
- c.selector,
- c.eventName,
- metaLevel,
- wrapFunction));
- }
- }));
-}
-
-function JQueryEventRouter(baseSelector, selector, eventName, metaLevel, wrapFunction) {
- var self = this;
- this.baseSelector = baseSelector || null;
- this.selector = selector;
- this.eventName = eventName;
- this.metaLevel = metaLevel || 0;
- this.wrapFunction = wrapFunction || jQueryEvent;
- this.preventDefault = (this.eventName.charAt(0) !== "+");
- this.handler =
- Dataspace.wrap(function (e) {
- Dataspace.send(self.wrapFunction(self.selector, self.eventName, e), self.metaLevel);
- if (self.preventDefault) e.preventDefault();
- return !self.preventDefault;
- });
- this.computeNodes().on(this.preventDefault ? this.eventName : this.eventName.substring(1),
- this.handler);
-}
-
-JQueryEventRouter.prototype.boot = function () {
- return Patch.pub(this.wrapFunction(this.selector, this.eventName, __), this.metaLevel)
- .andThen(Patch.sub(Patch.observe(this.wrapFunction(this.selector, this.eventName, __)),
- this.metaLevel));
-};
-
-JQueryEventRouter.prototype.handleEvent = function (e) {
- if (e.type === "stateChange" && e.patch.hasRemoved()) {
- this.computeNodes().off(this.eventName, this.handler);
- Dataspace.exit();
- }
-};
-
-JQueryEventRouter.prototype.computeNodes = function () {
- if (this.baseSelector) {
- return $(this.baseSelector).children(this.selector).addBack(this.selector);
- } else {
- return $(this.selector);
- }
-};
-
-function simplifyDOMEvent(e) {
- var keys = [];
- for (var k in e) {
- var v = e[k];
- if (typeof v === 'object') continue;
- if (typeof v === 'function') continue;
- keys.push(k);
- }
- keys.sort();
- var simplified = [];
- for (var i = 0; i < keys.length; i++) {
- simplified.push([keys[i], e[keys[i]]]);
- }
- return simplified;
-}
-
-///////////////////////////////////////////////////////////////////////////
-
-module.exports.spawnJQueryDriver = spawnJQueryDriver;
-module.exports.simplifyDOMEvent = simplifyDOMEvent;
-module.exports.jQueryEvent = jQueryEvent;
diff --git a/js/src/main.js b/js/src/main.js
index 96a580a..53feb8c 100644
--- a/js/src/main.js
+++ b/js/src/main.js
@@ -27,8 +27,7 @@ module.exports.DemandMatcher = require('./demand-matcher.js').DemandMatcher;
module.exports.Ack = require('./ack.js').Ack;
module.exports.RandomID = require('./randomid.js');
-module.exports.DOM = require("./dom-driver.js");
-module.exports.JQuery = require("./jquery-driver.js");
+module.exports.UI = require('./ui.js');
module.exports.Timer = require("./timer-driver.js");
module.exports.Reflect = require("./reflect.js");
module.exports.WakeDetector = require("./wake-detector-driver.js");
diff --git a/js/src/randomid.js b/js/src/randomid.js
index 18b6061..52473e3 100644
--- a/js/src/randomid.js
+++ b/js/src/randomid.js
@@ -3,10 +3,19 @@ var randomId;
if ((typeof window !== 'undefined') &&
(typeof window.crypto !== 'undefined') &&
(typeof window.crypto.getRandomValues !== 'undefined')) {
- randomId = function (byteCount) {
+ randomId = function (byteCount, hexOutput) {
var buf = new Uint8Array(byteCount);
window.crypto.getRandomValues(buf);
- return btoa(String.fromCharCode.apply(null, buf)).replace(/=/g,'');
+ if (hexOutput) {
+ var encoded = [];
+ for (var i = 0; i < buf.length; i++) {
+ encoded.push("0123456789abcdef"[(buf[i] >> 4) & 15]);
+ encoded.push("0123456789abcdef"[buf[i] & 15]);
+ }
+ return encoded.join('');
+ } else {
+ return btoa(String.fromCharCode.apply(null, buf)).replace(/=/g,'');
+ }
};
} else {
var crypto;
@@ -15,8 +24,12 @@ if ((typeof window !== 'undefined') &&
} catch (e) {}
if ((typeof crypto !== 'undefined') &&
(typeof crypto.randomBytes !== 'undefined')) {
- randomId = function (byteCount) {
- return crypto.randomBytes(byteCount).base64Slice().replace(/=/g,'');
+ randomId = function (byteCount, hexOutput) {
+ if (hexOutput) {
+ return crypto.randomBytes(byteCount).hexSlice().replace(/=/g,'');
+ } else {
+ return crypto.randomBytes(byteCount).base64Slice().replace(/=/g,'');
+ }
};
} else {
console.warn('No suitable implementation for RandomID.randomId available.');
diff --git a/js/src/ui.js b/js/src/ui.js
new file mode 100644
index 0000000..3eed014
--- /dev/null
+++ b/js/src/ui.js
@@ -0,0 +1,360 @@
+"use strict";
+// UI (DOM + event) support for Syndicate
+//
+// The previous dom-driver.js + jquery-driver.js approach worked kind
+// of OK, but started to fall down in a couple of areas: Added UI
+// fragments lacked identity, so would sometimes move around the tree
+// unexpectedly as they were updated; and there was no convenient
+// means of scoping event selectors to within a particular UI
+// fragment, despite various attempts at this.
+//
+// The design of this module aims to take these lessons into account.
+
+var Patch = require("./patch.js");
+var Trie = require("./trie.js");
+var DemandMatcher = require('./demand-matcher.js').DemandMatcher;
+var Struct = require('./struct.js');
+var RandomID = require('./randomid.js');
+
+var Dataspace_ = require("./dataspace.js");
+var Dataspace = Dataspace_.Dataspace;
+var __ = Dataspace_.__;
+var _$ = Dataspace_._$;
+
+///////////////////////////////////////////////////////////////////////////
+// Protocol
+
+// Message. Interest in this causes event listeners to be added for
+// the given eventType to all nodes matching the given selector *at
+// the time of the subscription*. As nodes *from this library* come
+// and go, they will have event handlers installed and removed as
+// well. WARNING: The simple implementation below currently scans the
+// whole document anytime a change is signalled; in future, it may not
+// do such a scan.
+var globalEvent = Struct.makeConstructor('globalEvent', ['selector', 'eventType', 'event']);
+
+// Message. Like globalEvent, but applies only within the scope of the
+// UI fragment identified.
+var uiEvent = Struct.makeConstructor('uiEvent', ['fragmentId', 'selector', 'eventType', 'event']);
+
+// Assertion. Causes the setup of DOM nodes corresponding to the given
+// HTML fragment, as immediate children of all nodes named by the
+// given selector that exist at the time of assertion.
+var uiFragment = Struct.makeConstructor('uiFragment', ['fragmentId', 'selector', 'html']);
+
+// Assertion. Asserted by respondent to a given uiFragment.
+var uiFragmentExists = Struct.makeConstructor('uiFragmentExists', ['fragmentId']);
+
+///////////////////////////////////////////////////////////////////////////
+// ID allocators
+
+var moduleInstance = RandomID.randomId(16, true);
+
+var nextFragmentIdNumber = 0;
+function newFragmentId() {
+ return 'ui_' + moduleInstance + '_' + (nextFragmentIdNumber++);
+}
+
+///////////////////////////////////////////////////////////////////////////
+
+function spawnUIDriver() {
+ var globalEventProj = globalEvent(_$('selector'), _$('eventType'), __);
+ Dataspace.spawn(
+ new DemandMatcher([Patch.observe(globalEventProj)],
+ [Patch.advertise(globalEventProj)],
+ {
+ onDemandIncrease: function (c) {
+ Dataspace.spawn(new GlobalEventSupply(c.selector, c.eventType));
+ }
+ }));
+
+ Dataspace.spawn(
+ new DemandMatcher([uiFragment(_$('fragmentId'), __, __)],
+ [uiFragmentExists(_$('fragmentId'))],
+ {
+ onDemandIncrease: function (c) {
+ Dataspace.spawn(new UIFragment(c.fragmentId));
+ }
+ }));
+}
+
+///////////////////////////////////////////////////////////////////////////
+
+function GlobalEventSupply(selector, eventType) {
+ this.selector = selector;
+ this.eventType = eventType;
+ this.demandPat = Patch.observe(globalEvent(this.selector, this.eventType, __));
+}
+
+GlobalEventSupply.prototype.boot = function () {
+ var self = this;
+ this.handlerClosure = Dataspace.wrap(function(e) { return self.handleDomEvent(e); });
+ this.updateEventListeners(true);
+
+ return Patch.sub(this.demandPat) // track demand
+ .andThen(Patch.sub(uiFragmentExists(__))) // track new fragments
+ .andThen(Patch.pub(globalEvent(this.selector, this.eventType, __))) // indicate our presence
+ ;
+};
+
+GlobalEventSupply.prototype.updateEventListeners = function (install) {
+ var nodes = document.querySelectorAll(this.selector);
+ for (var i = 0; i < nodes.length; i++) {
+ var n = nodes[i];
+ // addEventListener and removeEventListener are apparently idempotent.
+ if (install) {
+ n.addEventListener(cleanEventType(this.eventType), this.handlerClosure);
+ } else {
+ n.removeEventListener(cleanEventType(this.eventType), this.handlerClosure);
+ }
+ }
+};
+
+GlobalEventSupply.prototype.trapexit = function () {
+ console.log('GlobalEventSupply trapexit running', this.selector, this.eventType);
+ this.updateEventListeners(false);
+};
+
+GlobalEventSupply.prototype.handleDomEvent = function (event) {
+ Dataspace.send(globalEvent(this.selector, this.eventType, event));
+ return dealWithPreventDefault(this.eventType, event);
+};
+
+GlobalEventSupply.prototype.handleEvent = function (e) {
+ this.updateEventListeners(true);
+ // TODO: don't be so crude about this ^. On the one hand, this lets
+ // us ignore uiFragmentExists records coming and going; on the other
+ // hand, we do potentially a lot of redundant work.
+ if (e.type === 'stateChange' && e.patch.project(this.demandPat).hasRemoved()) {
+ Dataspace.exit(); // trapexit will uninstall event listeners
+ }
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+function UIFragment(fragmentId) {
+ this.fragmentId = fragmentId;
+ this.demandProj = uiFragment(this.fragmentId, _$('selector'), _$('html'));
+ this.eventDemandProj =
+ Patch.observe(uiEvent(this.fragmentId, _$('selector'), _$('eventType'), __));
+
+ this.currentAnchorNodes = [];
+ this.currentSelector = null;
+ this.currentHtml = null;
+
+ this.eventClosures = {};
+}
+
+UIFragment.prototype.boot = function () {
+ return Patch.sub(Trie.projectionToPattern(this.demandProj)) // track demand
+ .andThen(Patch.assert(uiFragmentExists(this.fragmentId))) // assert presence
+ .andThen(Patch.sub(Trie.projectionToPattern(this.eventDemandProj)))
+ // ^ track demand for fragment-specific events
+ ;
+};
+
+UIFragment.prototype.trapexit = function () {
+ console.log('UIFragment trapexit running', this.fragmentId);
+ this.updateContent(null, null);
+};
+
+function brandNode(n, fragmentId, brandValue) {
+ if ('dataset' in n) {
+ // html element nodes etc.
+ n.dataset[fragmentId] = brandValue;
+ } else {
+ // text nodes, svg nodes, etc etc.
+ n[fragmentId] = brandValue;
+ }
+}
+
+function getBrand(n, fragmentId) {
+ if ('dataset' in n && n.dataset[fragmentId]) return n.dataset[fragmentId];
+ if (n[fragmentId]) return n[fragmentId];
+ return null;
+}
+
+function findInsertionPoint(n, fragmentId) {
+ for (var i = 0; i < n.childNodes.length; i++) {
+ var c = n.childNodes[i];
+ if (getBrand(c, fragmentId)) return c;
+ }
+ return null;
+}
+
+function htmlToNodes(html) {
+ var e = document.createElement('arbitrarycontainer');
+ e.innerHTML = html;
+ return Array.prototype.slice.call(e.childNodes);
+}
+
+UIFragment.prototype.updateContent = function (newSelector, newHtml) {
+ var self = this;
+ var newBrand = '' + (Date.now());
+
+ var newAnchors = (newSelector !== null)
+ ? Array.prototype.slice.call(document.querySelectorAll(newSelector))
+ : [];
+
+ newAnchors.forEach(function (anchorNode) {
+ var insertionPoint = findInsertionPoint(anchorNode, self.fragmentId);
+ htmlToNodes(newHtml).forEach(function (newNode) {
+ brandNode(newNode, self.fragmentId, newBrand);
+ anchorNode.insertBefore(newNode, insertionPoint);
+ });
+ });
+
+ self.currentAnchorNodes.forEach(function (anchorNode) {
+ var insertionPoint = findInsertionPoint(anchorNode, self.fragmentId);
+ while (insertionPoint) {
+ var nextNode = insertionPoint.nextSibling;
+ var b = getBrand(insertionPoint, self.fragmentId);
+ if (!b) break; // we know all our-brand nodes will be adjacent
+ if (b !== newBrand) {
+ insertionPoint.parentNode.removeChild(insertionPoint);
+ }
+ insertionPoint = nextNode;
+ }
+ });
+
+ self.currentAnchorNodes = newAnchors;
+ self.currentSelector = newSelector;
+ self.currentHtml = newHtml;
+};
+
+UIFragment.prototype.handleEvent = function (e) {
+ var self = this;
+
+ if (e.type === 'stateChange') {
+ var fragmentChanges = e.patch.projectObjects(self.demandProj);
+ fragmentChanges[0].forEach(function (c) { self.updateContent(c.selector, c.html); });
+ fragmentChanges[1].forEach(function (c) {
+ if (c.selector === self.currentSelector && c.html === self.currentHtml) {
+ Dataspace.exit(); // trapexit will remove nodes
+ }
+ });
+
+ var eventDemand = e.patch.projectObjects(self.eventDemandProj);
+ eventDemand[0].forEach(function (c) { self.updateEventListeners(c, true); })
+ eventDemand[1].forEach(function (c) { self.updateEventListeners(c, false); })
+ }
+};
+
+UIFragment.prototype.eventClosureKey = function (c) {
+ return c.selector + ' :: ' + c.eventType;
+};
+
+UIFragment.prototype.getEventClosure = function (c) {
+ var self = this;
+ var key = self.eventClosureKey(c);
+ if (!(key in self.eventClosures)) {
+ self.eventClosures[key] = Dataspace.wrap(function (e) { return self.handleDomEvent(c, e); });
+ }
+ return self.eventClosures[key];
+};
+
+UIFragment.prototype.clearEventClosure = function (c) {
+ delete this.eventClosures[this.eventClosureKey(c)];
+};
+
+UIFragment.prototype.updateEventListeners = function (c, install) {
+ var self = this;
+ var handlerClosure = self.getEventClosure(c);
+
+ self.currentAnchorNodes.forEach(function (anchorNode) {
+ var uiNode = findInsertionPoint(anchorNode, self.fragmentId);
+ while (uiNode && getBrand(uiNode, self.fragmentId)) {
+ var nodes = uiNode.querySelectorAll(c.selector);
+ for (var i = 0; i < nodes.length; i++) {
+ var n = nodes[i];
+ // addEventListener and removeEventListener are apparently idempotent.
+ if (install) {
+ n.addEventListener(cleanEventType(c.eventType), handlerClosure);
+ } else {
+ n.removeEventListener(cleanEventType(c.eventType), handlerClosure);
+ }
+ }
+ uiNode = uiNode.nextSibling;
+ }
+ });
+
+ if (!install) {
+ self.clearEventClosure(c);
+ }
+};
+
+UIFragment.prototype.handleDomEvent = function (c, e) {
+ Dataspace.send(uiEvent(this.fragmentId, c.selector, c.eventType, e));
+ return dealWithPreventDefault(this.eventType, event);
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+function escapeDataAttributeName(s) {
+ // Per https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset,
+ // the rules seem to be:
+ //
+ // 1. Must not contain a dash immediately followed by an ASCII lowercase letter
+ // 2. Must not contain anything other than:
+ // - letters
+ // - numbers
+ // - dash, dot, colon, underscore
+ //
+ // I'm not implementing this exactly - I'm escaping some things that
+ // don't absolutely need escaping, because it's simpler and I don't
+ // yet need to undo this transformation.
+
+ var result = '';
+ for (var i = 0; i < s.length; i++) {
+ var c = s[i];
+ if (c >= 'a' && c <= 'z') { result = result + c; continue; }
+ if (c >= 'A' && c <= 'Z') { result = result + c; continue; }
+ if (c >= '0' && c <= '9') { result = result + c; continue; }
+ if (c === '.' || c === ':') { result = result + c; continue; }
+
+ c = c.charCodeAt(0);
+ result = result + '_' + c + '_';
+ }
+ return result;
+}
+
+function dealWithPreventDefault(eventType, event) {
+ var shouldPreventDefault = eventType.charAt(0) !== '+';
+ if (shouldPreventDefault) event.preventDefault();
+ return !shouldPreventDefault;
+}
+
+function cleanEventType(eventType) {
+ return (eventType.charAt(0) === '+') ? eventType.slice(1) : eventType;
+}
+
+///////////////////////////////////////////////////////////////////////////
+
+function Anchor(explicitFragmentId) {
+ this.fragmentId =
+ (typeof explicitFragmentId === 'undefined') ? newFragmentId() : explicitFragmentId;
+ this.htmlPattern = uiFragment(this.fragmentId, __, __);
+ this.eventPattern = uiEvent(this.fragmentId, __, __, __);
+}
+
+Anchor.prototype.context = function (contextId) {
+ return new Anchor(this.fragmentId + '_' + escapeDataAttributeName(contextId));
+};
+
+Anchor.prototype.html = function (selector, html) {
+ return uiFragment(this.fragmentId, selector, html);
+};
+
+Anchor.prototype.event = function (selector, eventType, event) {
+ return uiEvent(this.fragmentId, selector, eventType, event);
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+module.exports.newFragmentId = newFragmentId;
+module.exports.spawnUIDriver = spawnUIDriver;
+module.exports.Anchor = Anchor;
+module.exports.globalEvent = globalEvent;
+module.exports.uiEvent = uiEvent;
+module.exports.uiFragment = uiFragment;
+module.exports.uiFragmentExists = uiFragmentExists;