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";
var DOM = Syndicate.DOM.DOM;
var jQueryEvent = Syndicate.JQuery.jQueryEvent;
new Syndicate.Ground(function () {
Syndicate.UI.spawnUIDriver();
$(document).ready(function() {
new Syndicate.Ground(function () {
Syndicate.DOM.spawnDOMDriver();
Syndicate.JQuery.spawnJQueryDriver();
Syndicate.Actor.spawnActor(new Object(), function() {
this.counter = 0;
Syndicate.Actor.createFacet()
.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() {
this.counter++;
})).completeBuild();
});
}).startStepping();
});
Syndicate.Actor.spawnActor(new Object(), function() {
var counter = 0;
var ui = new Syndicate.UI.Anchor();
Syndicate.Actor.createFacet()
.addAssertion((function() { var _ = Syndicate.__; return Syndicate.Patch.assert(ui.html('#button-label',''+counter), 0); }))
.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() {
counter++;
})).completeBuild();
});
}).startStepping();

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@
<head>
<title>Syndicate: DOM 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>

View File

@ -1,5 +1,5 @@
var G;
$(document).ready(function () {
document.addEventListener('DOMContentLoaded', function () {
var Dataspace = Syndicate.Dataspace;
var sub = Syndicate.sub;
var assert = Syndicate.assert;
@ -10,18 +10,17 @@ $(document).ready(function () {
G = new Syndicate.Ground(function () {
console.log('starting ground boot');
Syndicate.DOM.spawnDOMDriver();
var DOM = Syndicate.DOM.DOM;
var jQueryEvent = Syndicate.JQuery.jQueryEvent;
Syndicate.UI.spawnUIDriver();
Dataspace.spawn({
boot: function () {
return assert(DOM("#clicker-holder", "clicker",
'<button><span style="font-style: italic">Click me!</span></button>'))
.andThen(sub(jQueryEvent("button.clicker", "click", __)));
var ui = new Syndicate.UI.Anchor();
return assert(ui.html("#clicker-holder",
'<button><span style="font-style: italic">Click me!</span></button>'))
.andThen(sub(Syndicate.UI.globalEvent("button", "click", __)));
},
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");
}
}
@ -29,15 +28,17 @@ $(document).ready(function () {
Dataspace.spawn({
counter: 0,
ui: new Syndicate.UI.Anchor(),
boot: function () {
this.updateState();
return sub("bump_count");
},
updateState: function () {
Dataspace.stateChange(retract(DOM.pattern)
.andThen(assert(DOM("#counter-holder", "counter",
'<div><p>The current count is: '+this.counter+
'</p></div>'))));
Dataspace.stateChange(retract(this.ui.htmlPattern)
.andThen(assert(this.ui.html(
"#counter-holder",
'<div><p>The current count is: '+this.counter+
'</p></div>'))));
},
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();

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

View File

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

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>
<title>Syndicate: Smoketest with DSL</title>
<meta charset="utf-8">
<script src="../../third-party/jquery-2.2.0.min.js"></script>
<script src="../../dist/syndicatecompiler.js"></script>
<script src="../../dist/syndicate.js"></script>
<script type="text/syndicate-js" src="index.js"></script>

View File

@ -3,7 +3,6 @@
<head>
<title>Syndicate: Smoketest</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>

View File

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

View File

@ -4,7 +4,6 @@
<title>Syndicate: SVG</title>
<meta charset="utf-8">
<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/syndicate.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 () {
ground dataspace G {
Syndicate.DOM.spawnDOMDriver();
Syndicate.Timer.spawnTimerDriver();
actor {
var ui = new Syndicate.UI.Anchor();
react {
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 {
react {
assert DOM('#clock', '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');
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);
}
}
}
});
}

View File

@ -4,7 +4,6 @@
<title>Syndicate: Textfield Example (DSL variation)</title>
<meta charset="utf-8">
<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/syndicate.js"></script>
<script type="text/syndicate-js" src="index.js"></script>

View File

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

View File

@ -4,7 +4,6 @@
<title>Syndicate: Textfield Example</title>
<meta charset="utf-8">
<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="index.js"></script>
</head>

View File

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

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.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");

View File

@ -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.');

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;