A better UI library. Replaces DOM and jQuery support.
This commit is contained in:
parent
23f269fba6
commit
0208ae7a7d
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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");
|
||||
|
|
|
@ -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.');
|
||||
|
|
|
@ -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;
|
Loading…
Reference in New Issue