diff --git a/js/examples/textfield/index.html b/js/examples/textfield/index.html new file mode 100644 index 0000000..42c64ad --- /dev/null +++ b/js/examples/textfield/index.html @@ -0,0 +1,22 @@ + + + + Syndicate: Textfield Example + + + + + + + +

Textfield Example

+

+ After Hesam + Samimi's paper. +

+

Field contents:

+

Search

+ +

+  
+
diff --git a/js/examples/textfield/index.js b/js/examples/textfield/index.js
new file mode 100644
index 0000000..4ee1f4e
--- /dev/null
+++ b/js/examples/textfield/index.js
@@ -0,0 +1,197 @@
+///////////////////////////////////////////////////////////////////////////
+// GUI
+
+var Network = Syndicate.Network;
+var Route = Syndicate.Route;
+var Patch = Syndicate.Patch;
+var __ = Syndicate.__;
+var _$ = Syndicate._$;
+
+function piece(text, pos, lo, hi, cls) {
+  return ""+
+    ((pos >= lo && pos < hi)
+     ? text.substring(lo, pos) + "" + text.substring(pos, hi)
+     : text.substring(lo, hi))
+    + "";
+}
+
+function spawnGui() {
+  Network.spawn({
+    field: { text: '', pos: 0 },
+    highlight: { state: false },
+
+    boot: function () {
+      return Patch.sub(["jQuery", "#inputRow", "+keypress", __])
+	.andThen(Patch.sub(["fieldContents", __, __]))
+	.andThen(Patch.sub(["highlight", __]));
+    },
+
+    fieldContentsProjection: Route.compileProjection(["fieldContents", _$("text"), _$("pos")]),
+    highlightProjection: Route.compileProjection(["highlight", _$("state")]),
+    handleEvent: function (e) {
+      var self = this;
+      switch (e.type) {
+      case "message":
+	var event = e.message[3];
+	var keycode = event.keyCode;
+	var character = String.fromCharCode(event.charCode);
+	if (keycode === 37 /* left */) {
+	  Network.send(["fieldCommand", "cursorLeft"]);
+	} else if (keycode === 39 /* right */) {
+	  Network.send(["fieldCommand", "cursorRight"]);
+	} else if (keycode === 9 /* tab */) {
+	  // ignore
+	} else if (keycode === 8 /* backspace */) {
+	  Network.send(["fieldCommand", "backspace"]);
+	} else if (character) {
+	  Network.send(["fieldCommand", ["insert", character]]);
+	}
+	break;
+      case "stateChange":
+	Route.projectObjects(e.patch.added, this.fieldContentsProjection).forEach(function (c) {
+	  self.field = c;
+	});
+	Route.projectObjects(e.patch.added, this.highlightProjection).forEach(function (c) {
+	  self.highlight = c;
+	});
+	this.updateDisplay();
+	break;
+      }
+    },
+
+    updateDisplay: function () {
+      // BUG: escape text!
+      var text = this.field ? this.field.text : "";
+      var pos = this.field ? this.field.pos : 0;
+      var highlight = this.highlight ? this.highlight.state : false;
+      var hLeft = highlight ? highlight.get(0) : 0;
+      var hRight = highlight ? highlight.get(1) : 0;
+      $("#fieldContents")[0].innerHTML = highlight
+	? piece(text, pos, 0, hLeft, "normal") +
+	piece(text, pos, hLeft, hRight, "highlight") +
+	piece(text, pos, hRight, text.length + 1, "normal")
+	: piece(text, pos, 0, text.length + 1, "normal");
+    }
+  });
+}
+
+///////////////////////////////////////////////////////////////////////////
+// Textfield Model
+
+function spawnModel() {
+  var initialContents = "initial";
+  Network.spawn({
+    fieldContents: initialContents,
+    cursorPos: initialContents.length, /* positions address gaps between characters */
+
+    boot: function () {
+      this.publishState();
+      return Patch.sub(["fieldCommand", __]);
+    },
+
+    handleEvent: function (e) {
+      if (e.type === "message" && e.message[0] === "fieldCommand") {
+	var command = e.message[1];
+	if (command === "cursorLeft") {
+	  this.cursorPos--;
+	  if (this.cursorPos < 0)
+	    this.cursorPos = 0;
+	} else if (command === "cursorRight") {
+	  this.cursorPos++;
+	  if (this.cursorPos > this.fieldContents.length)
+	    this.cursorPos = this.fieldContents.length;
+	} else if (command === "backspace" && this.cursorPos > 0) {
+	  this.fieldContents =
+	    this.fieldContents.substring(0, this.cursorPos - 1) +
+	    this.fieldContents.substring(this.cursorPos);
+	  this.cursorPos--;
+	} else if (command.constructor === Array && command[0] === "insert") {
+	  var newText = command[1];
+	  this.fieldContents =
+	    this.fieldContents.substring(0, this.cursorPos) +
+	    newText +
+	    this.fieldContents.substring(this.cursorPos);
+	  this.cursorPos += newText.length;
+	}
+	this.publishState();
+      }
+    },
+
+    publishState: function () {
+      Network.stateChange(
+	Patch.retract(["fieldContents", __, __])
+	  .andThen(Patch.assert(["fieldContents", this.fieldContents, this.cursorPos])));
+    }
+  });
+}
+
+///////////////////////////////////////////////////////////////////////////
+// Search engine
+
+function spawnSearch() {
+  Network.spawn({
+    fieldContents: "",
+    highlight: false,
+
+    boot: function () {
+      this.publishState();
+      return Patch.sub(["jQuery", "#searchBox", "input", __])
+	.andThen(Patch.sub(["fieldContents", __, __]));
+    },
+
+    fieldContentsProjection: Route.compileProjection(["fieldContents", _$("text"), _$("pos")]),
+    handleEvent: function (e) {
+      var self = this;
+      if (e.type === "message" && e.message[0] === "jQuery") {
+	this.search();
+      }
+      if (e.type === "stateChange") {
+	Route.projectObjects(e.patch.added, this.fieldContentsProjection).forEach(function (c) {
+	  self.fieldContents = c.text;
+	});
+	this.search();
+      }
+    },
+
+    publishState: function () {
+      Network.stateChange(
+	Patch.retract(["highlight", __])
+	  .andThen(Patch.assert(["highlight", this.highlight])));
+    },
+
+    search: function () {
+      var searchtext = $("#searchBox")[0].value;
+      var oldHighlight = this.highlight;
+      if (searchtext) {
+	var pos = this.fieldContents.indexOf(searchtext);
+	this.highlight = (pos !== -1) && [pos, pos + searchtext.length];
+      } else {
+	this.highlight = false;
+      }
+      if (JSON.stringify(oldHighlight) !== JSON.stringify(this.highlight)) {
+	this.publishState();
+      }
+    }
+  });
+}
+
+///////////////////////////////////////////////////////////////////////////
+// Main
+
+var G;
+$(document).ready(function () {
+  G = new Syndicate.Ground(function () {
+    Syndicate.JQuery.spawnJQueryDriver();
+    Syndicate.DOM.spawnDOMDriver();
+
+    spawnGui();
+    spawnModel();
+    spawnSearch();
+  });
+
+  G.network.onStateChange = function (mux, patch) {
+    $("#spy-holder").text(Syndicate.prettyTrie(mux.routingTable));
+  };
+
+  G.startStepping();
+});
diff --git a/js/examples/textfield/style.css b/js/examples/textfield/style.css
new file mode 100644
index 0000000..d612510
--- /dev/null
+++ b/js/examples/textfield/style.css
@@ -0,0 +1,12 @@
+#fieldContents {
+    font-family: monospace;
+}
+
+.cursor {
+    border-left: solid red 1px;
+    border-right: solid red 1px;
+}
+
+.highlight {
+    background-color: yellow;
+}