A better UI library. Replaces DOM and jQuery support.

This commit is contained in:
Tony Garnock-Jones 2016-05-11 21:03:11 -04:00
parent 23f269fba6
commit 0208ae7a7d
24 changed files with 568 additions and 479 deletions

View File

@ -1,19 +1,14 @@
"use strict"; "use strict";
var DOM = Syndicate.DOM.DOM; new Syndicate.Ground(function () {
var jQueryEvent = Syndicate.JQuery.jQueryEvent; Syndicate.UI.spawnUIDriver();
$(document).ready(function() { Syndicate.Actor.spawnActor(new Object(), function() {
new Syndicate.Ground(function () { var counter = 0;
Syndicate.DOM.spawnDOMDriver(); var ui = new Syndicate.UI.Anchor();
Syndicate.JQuery.spawnJQueryDriver(); Syndicate.Actor.createFacet()
.addAssertion((function() { var _ = Syndicate.__; return Syndicate.Patch.assert(ui.html('#button-label',''+counter), 0); }))
Syndicate.Actor.spawnActor(new Object(), function() { .onEvent(false, "message", (function() { var _ = Syndicate.__; return Syndicate.Patch.sub(Syndicate.UI.globalEvent('#counter','click',_), 0); }), (function() { var _ = Syndicate.__; return { assertion: Syndicate.UI.globalEvent('#counter','click',_), metalevel: 0 }; }), (function() {
this.counter = 0; counter++;
Syndicate.Actor.createFacet() })).completeBuild();
.addAssertion((function() { var _ = Syndicate.__; return Syndicate.Patch.assert(DOM('#button-label','',''+this.counter), 0); })) });
.onEvent(false, "message", (function() { var _ = Syndicate.__; return Syndicate.Patch.sub(jQueryEvent('#counter','click',_), 0); }), (function() { var _ = Syndicate.__; return { assertion: jQueryEvent('#counter','click',_), metalevel: 0 }; }), (function() { }).startStepping();
this.counter++;
})).completeBuild();
});
}).startStepping();
});

View File

@ -3,7 +3,6 @@
<head> <head>
<title>Syndicate: Button Example</title> <title>Syndicate: Button Example</title>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="../../third-party/jquery-2.2.0.min.js"></script>
<script src="../../dist/syndicatecompiler.js"></script> <script src="../../dist/syndicatecompiler.js"></script>
<script src="../../dist/syndicate.js"></script> <script src="../../dist/syndicate.js"></script>
<script type="text/syndicate-js" src="index.js"></script> <script type="text/syndicate-js" src="index.js"></script>

View File

@ -1,19 +1,14 @@
var DOM = Syndicate.DOM.DOM; ground dataspace {
var jQueryEvent = Syndicate.JQuery.jQueryEvent; Syndicate.UI.spawnUIDriver();
$(document).ready(function() { actor {
ground dataspace { var counter = 0;
Syndicate.DOM.spawnDOMDriver(); var ui = new Syndicate.UI.Anchor();
Syndicate.JQuery.spawnJQueryDriver(); react {
assert ui.html('#button-label', '' + counter);
actor { on message Syndicate.UI.globalEvent('#counter', 'click', _) {
this.counter = 0; counter++;
react {
assert DOM('#button-label', '', '' + this.counter);
on message jQueryEvent('#counter', 'click', _) {
this.counter++;
}
} }
} }
} }
}); }

View File

@ -1,9 +1,6 @@
assertion type present(name, status); assertion type present(name, status);
assertion type says(who, message); assertion type says(who, message);
var DOM = Syndicate.DOM.DOM;
var jQueryEvent = Syndicate.JQuery.jQueryEvent;
var brokerConnected = Syndicate.Broker.brokerConnected; var brokerConnected = Syndicate.Broker.brokerConnected;
var brokerConnection = Syndicate.Broker.brokerConnection; var brokerConnection = Syndicate.Broker.brokerConnection;
var toBroker = Syndicate.Broker.toBroker; var toBroker = Syndicate.Broker.toBroker;
@ -19,14 +16,15 @@ function spawnChatApp() {
if (!($("#nym").val())) { $("#nym").val("nym" + Math.floor(Math.random() * 65536)); } if (!($("#nym").val())) { $("#nym").val("nym" + Math.floor(Math.random() * 65536)); }
actor { actor {
var ui = new Syndicate.UI.Anchor();
react { react {
on asserted jQueryInput('#nym', $v) { this.nym = v; } on asserted inputValue('#nym', $v) { this.nym = v; }
on asserted jQueryInput('#status', $v) { this.status = v; } on asserted inputValue('#status', $v) { this.status = v; }
on asserted brokerConnected($url) { outputState('connected to ' + url); } on asserted brokerConnected($url) { outputState('connected to ' + url); }
on retracted brokerConnected($url) { outputState('disconnected from ' + url); } on retracted brokerConnected($url) { outputState('disconnected from ' + url); }
during jQueryInput('#wsurl', $url) { during inputValue('#wsurl', $url) {
assert brokerConnection(url); assert brokerConnection(url);
on message Syndicate.WakeDetector.wakeEvent() { on message Syndicate.WakeDetector.wakeEvent() {
@ -35,11 +33,12 @@ function spawnChatApp() {
assert toBroker(url, present(this.nym, this.status)); assert toBroker(url, present(this.nym, this.status));
during fromBroker(url, present($who, $status)) { during fromBroker(url, present($who, $status)) {
assert DOM('#nymlist', 'present-nym', assert ui.context(who)
Mustache.render($('#nym_template').html(), { who: who, status: status })); .html('#nymlist',
Mustache.render($('#nym_template').html(), { who: who, status: status }));
} }
on message jQueryEvent('#send_chat', 'click', _) { on message Syndicate.UI.globalEvent('#send_chat', 'click', _) {
var inp = $("#chat_input"); var inp = $("#chat_input");
var utterance = inp.val(); var utterance = inp.val();
inp.val(""); inp.val("");
@ -79,21 +78,21 @@ function outputUtterance(who, what) {
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Input control value monitoring // Input control value monitoring
assertion type jQueryInput(selector, value); assertion type inputValue(selector, value);
function spawnInputChangeMonitor() { function spawnInputChangeMonitor() {
actor { actor {
react { react {
on asserted Syndicate.observe(jQueryInput($selector, _)) { on asserted Syndicate.observe(inputValue($selector, _)) {
actor { actor {
this.value = $(selector).val(); this.value = $(selector).val();
react { react {
assert jQueryInput(selector, this.value); assert inputValue(selector, this.value);
on message jQueryEvent(selector, 'change', $e) { on message Syndicate.UI.globalEvent(selector, 'change', $e) {
this.value = e.target.value; this.value = e.target.value;
} }
} until { } until {
case retracted Syndicate.observe(jQueryInput(selector, _)); case retracted Syndicate.observe(inputValue(selector, _));
} }
} }
} }
@ -106,8 +105,7 @@ function spawnInputChangeMonitor() {
$(document).ready(function () { $(document).ready(function () {
ground dataspace G { ground dataspace G {
Syndicate.JQuery.spawnJQueryDriver(); Syndicate.UI.spawnUIDriver();
Syndicate.DOM.spawnDOMDriver();
Syndicate.WakeDetector.spawnWakeDetector(); Syndicate.WakeDetector.spawnWakeDetector();
Syndicate.Broker.spawnBrokerClientDriver(); Syndicate.Broker.spawnBrokerClientDriver();
spawnInputChangeMonitor(); spawnInputChangeMonitor();

View File

@ -3,7 +3,6 @@
<head> <head>
<title>Syndicate: DOM Example</title> <title>Syndicate: DOM Example</title>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="../../third-party/jquery-2.2.0.min.js"></script>
<script src="../../dist/syndicate.js"></script> <script src="../../dist/syndicate.js"></script>
<script src="index.js"></script> <script src="index.js"></script>
</head> </head>

View File

@ -1,5 +1,5 @@
var G; var G;
$(document).ready(function () { document.addEventListener('DOMContentLoaded', function () {
var Dataspace = Syndicate.Dataspace; var Dataspace = Syndicate.Dataspace;
var sub = Syndicate.sub; var sub = Syndicate.sub;
var assert = Syndicate.assert; var assert = Syndicate.assert;
@ -10,18 +10,17 @@ $(document).ready(function () {
G = new Syndicate.Ground(function () { G = new Syndicate.Ground(function () {
console.log('starting ground boot'); console.log('starting ground boot');
Syndicate.DOM.spawnDOMDriver(); Syndicate.UI.spawnUIDriver();
var DOM = Syndicate.DOM.DOM;
var jQueryEvent = Syndicate.JQuery.jQueryEvent;
Dataspace.spawn({ Dataspace.spawn({
boot: function () { boot: function () {
return assert(DOM("#clicker-holder", "clicker", var ui = new Syndicate.UI.Anchor();
'<button><span style="font-style: italic">Click me!</span></button>')) return assert(ui.html("#clicker-holder",
.andThen(sub(jQueryEvent("button.clicker", "click", __))); '<button><span style="font-style: italic">Click me!</span></button>'))
.andThen(sub(Syndicate.UI.globalEvent("button", "click", __)));
}, },
handleEvent: function (e) { handleEvent: function (e) {
if (e.type === "message" && jQueryEvent.isClassOf(e.message)) { if (e.type === "message" && Syndicate.UI.globalEvent.isClassOf(e.message)) {
Dataspace.send("bump_count"); Dataspace.send("bump_count");
} }
} }
@ -29,15 +28,17 @@ $(document).ready(function () {
Dataspace.spawn({ Dataspace.spawn({
counter: 0, counter: 0,
ui: new Syndicate.UI.Anchor(),
boot: function () { boot: function () {
this.updateState(); this.updateState();
return sub("bump_count"); return sub("bump_count");
}, },
updateState: function () { updateState: function () {
Dataspace.stateChange(retract(DOM.pattern) Dataspace.stateChange(retract(this.ui.htmlPattern)
.andThen(assert(DOM("#counter-holder", "counter", .andThen(assert(this.ui.html(
'<div><p>The current count is: '+this.counter+ "#counter-holder",
'</p></div>')))); '<div><p>The current count is: '+this.counter+
'</p></div>'))));
}, },
handleEvent: function (e) { handleEvent: function (e) {
if (e.type === "message" && e.message === "bump_count") { if (e.type === "message" && e.message === "bump_count") {
@ -49,7 +50,7 @@ $(document).ready(function () {
}); });
G.dataspace.setOnStateChange(function (mux, patch) { G.dataspace.setOnStateChange(function (mux, patch) {
$("#spy-holder").text(Syndicate.prettyTrie(mux.routingTable)); document.getElementById('spy-holder').innerText = Syndicate.prettyTrie(mux.routingTable);
}); });
G.startStepping(); G.startStepping();

View File

@ -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 number on the face of the button is incremented.
The actor maintaining the counter also maintains the button's label The actor maintaining the counter also maintains the button's label
and listens to click events. It uses the Syndicate/js DOM driver to 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 the publish the button's label text based on its internal state and to
Syndicate/js jQuery driver to subscribe to button click events. subscribe to button click events.
- [DEMO](button/) - [DEMO](button/)
- [Source code](button/index.js) using the Syndicate/js DSL - [Source code](button/index.js) using the Syndicate/js DSL
## DOM example ## DOM example
This example demonstrates two actors, each using the Syndicate/js DOM This example demonstrates two actors, each using the Syndicate/js UI
driver to display user interface, and the jQuery driver to receive driver to display user interface and receive events from it. The first
events from it. The first actor presents a button to the user, which actor presents a button to the user, which when clicked sends a
when clicked sends a message to the other actor. The second actor message to the other actor. The second actor receives messages from
receives messages from the first, updates its internal state, and the first, updates its internal state, and reflects its new internal
reflects its new internal state in its visible UI. state in its visible UI.
- [DEMO](dom/) - [DEMO](dom/)
- [Source code](dom/index.js) in plain JavaScript - [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 ## Text Entry Widget
This is a simple text entry GUI control, following a design of This is a simple text entry GUI control, following a design of

View File

@ -5,17 +5,15 @@ assertion type tvAlert(text);
assertion type switchAction(on); assertion type switchAction(on);
assertion type componentPresent(name); assertion type componentPresent(name);
var DOM = Syndicate.DOM.DOM;
var jQueryEvent = Syndicate.JQuery.jQueryEvent;
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// TV // TV
function spawnTV() { function spawnTV() {
actor { actor {
var ui = new Syndicate.UI.Anchor();
react { react {
during tvAlert($text) { 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 { actor {
react { react {
assert componentPresent('remote control'); assert componentPresent('remote control');
on message jQueryEvent('#remote-control', 'click', _) { on message Syndicate.UI.globalEvent('#remote-control', 'click', _) {
:: remoteClick(); :: remoteClick();
} }
} }
@ -63,23 +61,24 @@ function spawnRemoteListener() {
function spawnStoveSwitch() { function spawnStoveSwitch() {
actor { actor {
this.powerOn = false; this.powerOn = false;
this.ui = new Syndicate.UI.Anchor();
react { react {
assert componentPresent('stove switch'); assert componentPresent('stove switch');
assert switchState(this.powerOn); assert switchState(this.powerOn);
assert DOM('#stove-switch', 'switch-state', assert this.ui.html('#stove-switch',
Mustache.render($('#stove_element_template').html(), Mustache.render($('#stove_element_template').html(),
{ imgurl: ("img/stove-coil-element-" + { imgurl: ("img/stove-coil-element-" +
(this.powerOn ? "hot" : "cold") + ".jpg") })); (this.powerOn ? "hot" : "cold") + ".jpg") }));
on message jQueryEvent('#stove-switch-on', 'click', _) { this.powerOn = true; } on message Syndicate.UI.globalEvent('#stove-switch-on', 'click', _) { this.powerOn = true; }
on message jQueryEvent('#stove-switch-off', 'click', _) { this.powerOn = false; } on message Syndicate.UI.globalEvent('#stove-switch-off', 'click', _) { this.powerOn = false; }
on message switchAction($newState) { on message switchAction($newState) {
this.powerOn = newState; this.powerOn = newState;
} }
} until { } 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() { function spawnPowerDrawMonitor() {
actor { actor {
this.watts = 0; this.watts = 0;
this.ui = new Syndicate.UI.Anchor();
react { react {
assert componentPresent('power draw monitor'); assert componentPresent('power draw monitor');
assert powerDraw(this.watts); assert powerDraw(this.watts);
assert DOM('#power-draw-meter', 'power-draw', assert this.ui.html('#power-draw-meter',
Mustache.render($('#power_draw_template').html(), { watts: this.watts })); Mustache.render($('#power_draw_template').html(), { watts: this.watts }));
on asserted switchState($on) { on asserted switchState($on) {
this.watts = on ? 1500 : 0; this.watts = on ? 1500 : 0;
} }
} until { } 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); jKillButtons.prop('disabled', true);
} }
} }
on message jQueryEvent(spawnButtonSelector, 'click', _) { on message Syndicate.UI.globalEvent(spawnButtonSelector, 'click', _) {
spawnFunction(); spawnFunction();
} }
} }
@ -219,8 +219,7 @@ function spawnChaosMonkey() {
$(document).ready(function () { $(document).ready(function () {
ground dataspace G { ground dataspace G {
Syndicate.JQuery.spawnJQueryDriver(); Syndicate.UI.spawnUIDriver();
Syndicate.DOM.spawnDOMDriver();
Syndicate.Timer.spawnTimerDriver(); Syndicate.Timer.spawnTimerDriver();
spawnTV(); spawnTV();

View File

@ -1,16 +0,0 @@
<!doctype html>
<html>
<head>
<title>Syndicate: jQuery Example</title>
<meta charset="utf-8">
<script src="../../third-party/jquery-2.2.0.min.js"></script>
<script src="../../dist/syndicate.js"></script>
<script src="index.js"></script>
</head>
<body>
<h1>jQuery example</h1>
<button id="clicker">Click me</button>
<div id="result">0</div>
<pre id="spy-holder"></pre>
</body>
</html>

View File

@ -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();
});

View File

@ -3,7 +3,6 @@
<head> <head>
<title>Syndicate: Smoketest with DSL</title> <title>Syndicate: Smoketest with DSL</title>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="../../third-party/jquery-2.2.0.min.js"></script>
<script src="../../dist/syndicatecompiler.js"></script> <script src="../../dist/syndicatecompiler.js"></script>
<script src="../../dist/syndicate.js"></script> <script src="../../dist/syndicate.js"></script>
<script type="text/syndicate-js" src="index.js"></script> <script type="text/syndicate-js" src="index.js"></script>

View File

@ -3,7 +3,6 @@
<head> <head>
<title>Syndicate: Smoketest</title> <title>Syndicate: Smoketest</title>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="../../third-party/jquery-2.2.0.min.js"></script>
<script src="../../dist/syndicate.js"></script> <script src="../../dist/syndicate.js"></script>
<script src="index.js"></script> <script src="index.js"></script>
</head> </head>

View File

@ -3,33 +3,33 @@
var beep = Syndicate.Struct.makeConstructor('beep', ['counter']); var beep = Syndicate.Struct.makeConstructor('beep', ['counter']);
var G; var G;
$(document).ready(function () { document.addEventListener('DOMContentLoaded', function () {
var Dataspace = Syndicate.Dataspace; var Dataspace = Syndicate.Dataspace;
var sub = Syndicate.sub; var sub = Syndicate.sub;
var __ = Syndicate.__; var __ = Syndicate.__;
var _$ = Syndicate._$; var _$ = Syndicate._$;
G = new Syndicate.Ground(function () { G = new Syndicate.Ground(function () {
console.log('starting ground boot'); console.log('starting ground boot');
Dataspace.spawn({ Dataspace.spawn({
counter: 0, counter: 0,
boot: function () {}, boot: function () {},
handleEvent: function (e) {}, handleEvent: function (e) {},
step: function () { step: function () {
Dataspace.send(beep(this.counter++)); Dataspace.send(beep(this.counter++));
return this.counter <= 10; 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]);
}
}
});
}); });
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();
}); });

View File

@ -4,7 +4,6 @@
<title>Syndicate: SVG</title> <title>Syndicate: SVG</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="../../third-party/jquery-2.2.0.min.js"></script>
<script src="../../dist/syndicatecompiler.js"></script> <script src="../../dist/syndicatecompiler.js"></script>
<script src="../../dist/syndicate.js"></script> <script src="../../dist/syndicate.js"></script>
<script type="text/syndicate-js" src="index.js"></script> <script type="text/syndicate-js" src="index.js"></script>

View File

@ -1,25 +1,22 @@
var DOM = Syndicate.DOM.DOM; ground dataspace G {
Syndicate.UI.spawnUIDriver();
Syndicate.Timer.spawnTimerDriver();
$(document).ready(function () { actor {
ground dataspace G { var ui = new Syndicate.UI.Anchor();
Syndicate.DOM.spawnDOMDriver(); react {
Syndicate.Timer.spawnTimerDriver(); assert ui.html('#clock',
'<svg width="300px" viewBox="0 0 100 100">'+
'<circle fill="#0B79CE" r=45 cx=50 cy=50 />'+
'<line stroke="#023963" x1=50 y1=50 x2='+this.handX+' y2='+this.handY+' />'+
'</svg>')
when (typeof this.angle === 'number');
actor { on message Syndicate.Timer.periodicTick(1000) {
react { this.angle = ((((Date.now() / 1000) % 60) / 60) - 0.25) * 2 * Math.PI;
assert DOM('#clock', 'clock', this.handX = 50 + 40 * Math.cos(this.angle);
'<svg width="300px" viewBox="0 0 100 100">'+ this.handY = 50 + 40 * Math.sin(this.angle);
'<circle fill="#0B79CE" r=45 cx=50 cy=50/>'+
'<line stroke="#023963" x1=50 y1=50 x2='+this.handX+' y2='+this.handY+' />'+
'</svg>')
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);
}
} }
} }
} }
}); }

View File

@ -4,7 +4,6 @@
<title>Syndicate: Textfield Example (DSL variation)</title> <title>Syndicate: Textfield Example (DSL variation)</title>
<meta charset="utf-8"> <meta charset="utf-8">
<link href="style.css" rel="stylesheet"> <link href="style.css" rel="stylesheet">
<script src="../../third-party/jquery-2.2.0.min.js"></script>
<script src="../../dist/syndicatecompiler.js"></script> <script src="../../dist/syndicatecompiler.js"></script>
<script src="../../dist/syndicate.js"></script> <script src="../../dist/syndicate.js"></script>
<script type="text/syndicate-js" src="index.js"></script> <script type="text/syndicate-js" src="index.js"></script>

View File

@ -1,7 +1,7 @@
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// GUI // GUI
var jQueryEvent = Syndicate.JQuery.jQueryEvent; var globalEvent = Syndicate.UI.globalEvent;
assertion type fieldCommand(detail); assertion type fieldCommand(detail);
assertion type fieldContents(text, pos); assertion type fieldContents(text, pos);
assertion type highlight(state); assertion type highlight(state);
@ -36,7 +36,7 @@ function spawnGui() {
var highlight = this.highlightState; var highlight = this.highlightState;
var hLeft = highlight ? highlight[0] : 0; var hLeft = highlight ? highlight[0] : 0;
var hRight = highlight ? highlight[1] : 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, 0, hLeft, "normal") +
piece(text, pos, hLeft, hRight, "highlight") + piece(text, pos, hLeft, hRight, "highlight") +
piece(text, pos, hRight, text.length + 1, "normal") piece(text, pos, hRight, text.length + 1, "normal")
@ -44,18 +44,22 @@ function spawnGui() {
}; };
react { react {
on message jQueryEvent("#inputRow", "+keypress", $event) { on message globalEvent("#inputRow", "+keydown", $event) {
var keycode = event.keyCode; 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); var character = String.fromCharCode(event.charCode);
if (keycode === 37 /* left */) { if (event.charCode && character) {
:: 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) {
:: fieldCommand(["insert", character]); :: fieldCommand(["insert", character]);
} }
} }
@ -126,7 +130,7 @@ function spawnSearch() {
this.highlight = false; this.highlight = false;
this.search = function () { this.search = function () {
var searchtext = $("#searchBox")[0].value; var searchtext = document.getElementById("searchBox").value;
if (searchtext) { if (searchtext) {
var pos = this.fieldValue.indexOf(searchtext); var pos = this.fieldValue.indexOf(searchtext);
this.highlight = (pos !== -1) && [pos, pos + searchtext.length]; this.highlight = (pos !== -1) && [pos, pos + searchtext.length];
@ -138,7 +142,7 @@ function spawnSearch() {
react { react {
assert highlight(this.highlight); assert highlight(this.highlight);
on message jQueryEvent("#searchBox", "input", $event) { on message globalEvent("#searchBox", "input", $event) {
this.search(); this.search();
} }
@ -153,17 +157,14 @@ function spawnSearch() {
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Main // Main
$(document).ready(function () { ground dataspace G {
ground dataspace G { Syndicate.UI.spawnUIDriver();
Syndicate.JQuery.spawnJQueryDriver();
Syndicate.DOM.spawnDOMDriver();
spawnGui(); spawnGui();
spawnModel(); spawnModel();
spawnSearch(); spawnSearch();
} }
G.dataspace.setOnStateChange(function (mux, patch) { G.dataspace.setOnStateChange(function (mux, patch) {
$("#spy-holder").text(Syndicate.prettyTrie(mux.routingTable)); document.getElementById("spy-holder").innerText = Syndicate.prettyTrie(mux.routingTable);
});
}); });

View File

@ -4,7 +4,6 @@
<title>Syndicate: Textfield Example</title> <title>Syndicate: Textfield Example</title>
<meta charset="utf-8"> <meta charset="utf-8">
<link href="style.css" rel="stylesheet"> <link href="style.css" rel="stylesheet">
<script src="../../third-party/jquery-2.2.0.min.js"></script>
<script src="../../dist/syndicate.js"></script> <script src="../../dist/syndicate.js"></script>
<script src="index.js"></script> <script src="index.js"></script>
</head> </head>

View File

@ -7,7 +7,7 @@ var Patch = Syndicate.Patch;
var __ = Syndicate.__; var __ = Syndicate.__;
var _$ = Syndicate._$; var _$ = Syndicate._$;
var jQueryEvent = Syndicate.JQuery.jQueryEvent; var globalEvent = Syndicate.UI.globalEvent;
var fieldContents = Syndicate.Struct.makeConstructor('fieldContents', ['text', 'pos']); var fieldContents = Syndicate.Struct.makeConstructor('fieldContents', ['text', 'pos']);
var highlight = Syndicate.Struct.makeConstructor('highlight', ['state']); var highlight = Syndicate.Struct.makeConstructor('highlight', ['state']);
var fieldCommand = Syndicate.Struct.makeConstructor('fieldCommand', ['detail']); var fieldCommand = Syndicate.Struct.makeConstructor('fieldCommand', ['detail']);
@ -36,7 +36,8 @@ function spawnGui() {
highlight: { state: false }, highlight: { state: false },
boot: function () { 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(fieldContents.pattern))
.andThen(Patch.sub(highlight.pattern)); .andThen(Patch.sub(highlight.pattern));
}, },
@ -46,31 +47,40 @@ function spawnGui() {
handleEvent: function (e) { handleEvent: function (e) {
var self = this; var self = this;
switch (e.type) { switch (e.type) {
case "message": case "message":
var event = e.message[2]; var event = e.message[2];
var keycode = event.keyCode; switch (event.type) {
var character = String.fromCharCode(event.charCode); case "keydown":
if (keycode === 37 /* left */) { switch (event.keyCode) {
Dataspace.send(fieldCommand("cursorLeft")); case 37 /* left */: Dataspace.send(fieldCommand("cursorLeft")); break;
} else if (keycode === 39 /* right */) { case 39 /* right */: Dataspace.send(fieldCommand("cursorRight")); break;
Dataspace.send(fieldCommand("cursorRight")); case 9 /* tab */: /* ignore */ break;
} else if (keycode === 9 /* tab */) { case 8 /* backspace */:
// ignore event.preventDefault(); // that this works here is a minor miracle
} else if (keycode === 8 /* backspace */) { Dataspace.send(fieldCommand("backspace"));
Dataspace.send(fieldCommand("backspace")); break;
} else if (character) { default: break;
Dataspace.send(fieldCommand(["insert", character])); }
} break;
break; case "keypress":
case "stateChange": var character = String.fromCharCode(event.charCode);
Trie.projectObjects(e.patch.added, this.fieldContentsProjection).forEach(function (c) { if (event.charCode && character) {
self.field = c; Dataspace.send(fieldCommand(["insert", character]));
}); }
Trie.projectObjects(e.patch.added, this.highlightProjection).forEach(function (c) { break;
self.highlight = c; default:
}); break;
this.updateDisplay(); }
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 highlight = this.highlight ? this.highlight.state : false;
var hLeft = highlight ? highlight[0] : 0; var hLeft = highlight ? highlight[0] : 0;
var hRight = highlight ? highlight[1] : 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, 0, hLeft, "normal") +
piece(text, pos, hLeft, hRight, "highlight") + piece(text, pos, hLeft, hRight, "highlight") +
piece(text, pos, hRight, text.length + 1, "normal") piece(text, pos, hRight, text.length + 1, "normal")
@ -149,14 +159,14 @@ function spawnSearch() {
boot: function () { boot: function () {
this.publishState(); this.publishState();
return Patch.sub(jQueryEvent("#searchBox", "input", __)) return Patch.sub(globalEvent("#searchBox", "input", __))
.andThen(Patch.sub(fieldContents.pattern)); .andThen(Patch.sub(fieldContents.pattern));
}, },
fieldContentsProjection: fieldContents(_$("text"), _$("pos")), fieldContentsProjection: fieldContents(_$("text"), _$("pos")),
handleEvent: function (e) { handleEvent: function (e) {
var self = this; var self = this;
if (jQueryEvent.isClassOf(e.message)) { if (globalEvent.isClassOf(e.message)) {
this.search(); this.search();
} }
if (e.type === "stateChange") { if (e.type === "stateChange") {
@ -174,7 +184,7 @@ function spawnSearch() {
}, },
search: function () { search: function () {
var searchtext = $("#searchBox")[0].value; var searchtext = document.getElementById("searchBox").value;
var oldHighlight = this.highlight; var oldHighlight = this.highlight;
if (searchtext) { if (searchtext) {
var pos = this.fieldValue.indexOf(searchtext); var pos = this.fieldValue.indexOf(searchtext);
@ -193,10 +203,9 @@ function spawnSearch() {
// Main // Main
var G; var G;
$(document).ready(function () { document.addEventListener('DOMContentLoaded', function () {
G = new Syndicate.Ground(function () { G = new Syndicate.Ground(function () {
Syndicate.JQuery.spawnJQueryDriver(); Syndicate.UI.spawnUIDriver();
Syndicate.DOM.spawnDOMDriver();
spawnGui(); spawnGui();
spawnModel(); spawnModel();
@ -204,7 +213,7 @@ $(document).ready(function () {
}); });
G.dataspace.setOnStateChange(function (mux, patch) { G.dataspace.setOnStateChange(function (mux, patch) {
$("#spy-holder").text(Syndicate.prettyTrie(mux.routingTable)); document.getElementById("spy-holder").innerText = Syndicate.prettyTrie(mux.routingTable);
}); });
G.startStepping(); G.startStepping();

View File

@ -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 = $('<div>' + self.fragmentSpec + '</div>')[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;

View File

@ -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;

View File

@ -27,8 +27,7 @@ module.exports.DemandMatcher = require('./demand-matcher.js').DemandMatcher;
module.exports.Ack = require('./ack.js').Ack; module.exports.Ack = require('./ack.js').Ack;
module.exports.RandomID = require('./randomid.js'); module.exports.RandomID = require('./randomid.js');
module.exports.DOM = require("./dom-driver.js"); module.exports.UI = require('./ui.js');
module.exports.JQuery = require("./jquery-driver.js");
module.exports.Timer = require("./timer-driver.js"); module.exports.Timer = require("./timer-driver.js");
module.exports.Reflect = require("./reflect.js"); module.exports.Reflect = require("./reflect.js");
module.exports.WakeDetector = require("./wake-detector-driver.js"); module.exports.WakeDetector = require("./wake-detector-driver.js");

View File

@ -3,10 +3,19 @@ var randomId;
if ((typeof window !== 'undefined') && if ((typeof window !== 'undefined') &&
(typeof window.crypto !== 'undefined') && (typeof window.crypto !== 'undefined') &&
(typeof window.crypto.getRandomValues !== 'undefined')) { (typeof window.crypto.getRandomValues !== 'undefined')) {
randomId = function (byteCount) { randomId = function (byteCount, hexOutput) {
var buf = new Uint8Array(byteCount); var buf = new Uint8Array(byteCount);
window.crypto.getRandomValues(buf); 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 { } else {
var crypto; var crypto;
@ -15,8 +24,12 @@ if ((typeof window !== 'undefined') &&
} catch (e) {} } catch (e) {}
if ((typeof crypto !== 'undefined') && if ((typeof crypto !== 'undefined') &&
(typeof crypto.randomBytes !== 'undefined')) { (typeof crypto.randomBytes !== 'undefined')) {
randomId = function (byteCount) { randomId = function (byteCount, hexOutput) {
return crypto.randomBytes(byteCount).base64Slice().replace(/=/g,''); if (hexOutput) {
return crypto.randomBytes(byteCount).hexSlice().replace(/=/g,'');
} else {
return crypto.randomBytes(byteCount).base64Slice().replace(/=/g,'');
}
}; };
} else { } else {
console.warn('No suitable implementation for RandomID.randomId available.'); console.warn('No suitable implementation for RandomID.randomId available.');

360
js/src/ui.js Normal file
View File

@ -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;