[web] Connect to server

The individual components are more-or-less working, but the code that glues them
together is still pretty ugly. I should probably revisit and clean up the
individual components too. Also, the cursor code is missing a few features, but
everything is usable for the first time :D
This commit is contained in:
Joscha 2020-03-18 18:08:04 +00:00
parent 78235ef7cf
commit 54795b81ac
2 changed files with 124 additions and 32 deletions

View file

@ -29,21 +29,21 @@
} }
/* Fancy tree lines */ /* Fancy tree lines */
.node, .node::before { .node-children > *, .node-children > *::before {
border-color: var(--bright-black); border-color: var(--bright-black);
border-width: 2px; border-width: 2px;
} }
.node-children > .node { .node-children > * {
position: relative; /* .node is containing block for its .node::before */ position: relative; /* .node is containing block for its .node::before */
margin-left: calc(0.5ch - 1px); margin-left: calc(0.5ch - 1px);
padding-left: calc(1.5ch - 1px); padding-left: calc(1.5ch - 1px);
border-left-style: solid; border-left-style: solid;
} }
.node-children > .node:last-child { .node-children > *:last-child {
padding-left: calc(1.5ch + 1px); padding-left: calc(1.5ch + 1px);
border-left-style: none; border-left-style: none;
} }
.node-children > .node::before { .node-children > *::before {
content: ""; content: "";
position: absolute; position: absolute;
left: 0; left: 0;
@ -52,9 +52,9 @@
height: calc(0.6em - 1px); height: calc(0.6em - 1px);
border-bottom-style: solid; border-bottom-style: solid;
} }
.node-children > .node:last-child::before { .node-children > *:last-child::before {
border-left-style: solid; border-left-style: solid;
transition: all 0.4s; transition: border-bottom-left-radius 0.4s;
} }
/* Curvy lines */ /* Curvy lines */

View file

@ -349,6 +349,7 @@ class Editor {
this.nodeTree = nodeTree; this.nodeTree = nodeTree;
this.textarea = newElement("textarea"); this.textarea = newElement("textarea");
this.element = newElement("div", "node-editor", this.textarea);
this.textarea.addEventListener("input", event => this._updateTextAreaHeight()); this.textarea.addEventListener("input", event => this._updateTextAreaHeight());
this.path = undefined; this.path = undefined;
@ -370,21 +371,22 @@ class Editor {
node.elements.main.classList.remove("has-editor"); node.elements.main.classList.remove("has-editor");
} }
this.textarea.parentNode.removeChild(this.textarea); this.element.parentNode.removeChild(this.element);
} }
_attachTo(node, asChild) { _attachTo(node, asChild) {
if (asChild) { if (asChild) {
node.elements.children.appendChild(this.textarea); node.elements.children.appendChild(this.element);
node.setFolded(false);
} else { } else {
node.elements.main.classList.add("has-editor"); node.elements.main.classList.add("has-editor");
node.elements.main.insertBefore(this.textarea, node.elements.children); node.elements.main.insertBefore(this.element, node.elements.children);
} }
this._updateTextAreaHeight(); this._updateTextAreaHeight();
} }
restore() { restore() {
if (this.textarea.parentNode !== null) return; // Already attached if (this.element.parentNode !== null) return; // Already attached
let node = this._getAttachedNode(); let node = this._getAttachedNode();
if (node === undefined) return; // Nowhere to attach if (node === undefined) return; // Nowhere to attach
this._attachTo(node, this.asChild); this._attachTo(node, this.asChild);
@ -417,6 +419,64 @@ class Editor {
} }
} }
class Connection {
constructor(nodeTree, cursor, editor, url) {
this.nodeTree = nodeTree;
this.cursor = cursor;
this.editor = editor;
this.url = url;
this.ws = new WebSocket(this.url);
this.ws.addEventListener("message", msg => this.onMessage(msg));
this.ws.addEventListener("open", _ => this.sendHello());
}
onMessage(msg) {
let content = JSON.parse(msg.data);
if (content.type === "hello") {
this.onHello(content);
} else if (content.type === "update") {
this.onUpdate(content);
}
}
onHello(content) {
this.nodeTree.updateAt(new Path(), new Node(content.node));
this.cursor.restore();
this.editor.restore();
}
onUpdate(content) {
this.nodeTree.updateAt(new Path(...content.path), new Node(content.node));
this.cursor.restore();
this.editor.restore();
}
_send(thing) {
this.ws.send(JSON.stringify(thing));
}
sendHello() {
this._send({type: "hello", extensions: []});
}
sendEdit(path, text) {
this._send({type: "edit", path: path.elements, text: text});
}
sendDelete(path) {
this._send({type: "delete", path: path.elements});
}
sendReply(path, text) {
this._send({type: "reply", path: path.elements, text: text});
}
sendAct(path) {
this._send({type: "act", path: path.elements});
}
}
/* /*
* The main application * The main application
*/ */
@ -426,29 +486,48 @@ const loadingNode = new Node({text: "Connecting...", children: {}, order: []});
const nodeTree = new NodeTree(rootNodeContainer, loadingNode); const nodeTree = new NodeTree(rootNodeContainer, loadingNode);
const cursor = new Cursor(nodeTree); const cursor = new Cursor(nodeTree);
const editor = new Editor(nodeTree); const editor = new Editor(nodeTree);
const conn = new Connection(nodeTree, cursor, editor, "ws://127.0.0.1:8080/");
// TODO Replace this testing node with the real websocket code function beginEdit() {
const testNode = new Node({text: "Forest", children: [ let node = cursor.getSelectedNode();
{text: "Test", children: [ editor.content = node.text;
{text: "Bla", children: [], order: []}, editor.attachTo(cursor.path, false);
], order: [0]}, }
{text: "Sandbox", edit: true, delete: true, reply: true, act: true, children: [], order: []},
{text: "About", children: [
{text: "This project is an experiment in tree-based interaction.", children: [], order: []},
{text: "Motivation", children: [], order: []},
{text: "Inspirations", children: [], order: []},
], order: [0, 1, 2]}
], order: [0, 1, 2]});
nodeTree.updateAt(new Path(), testNode);
document.addEventListener("keydown", event => { function beginDirectReply() {
console.log(event); editor.content = "";
if (event.code === "Escape") { editor.attachTo(cursor.path, true);
editor.detach(); }
} else if (document.activeElement === editor.textarea) {
if (event.code === "Enter" && !event.shiftKey) { function beginIndirectReply() {
let path = cursor.path.parent;
if (path === undefined) return;
editor.content = "";
editor.attachTo(path, true);
}
function cancelEdit() {
editor.detach(); editor.detach();
} }
function completeEdit() {
let path = editor.path;
let text = editor.textarea.value;
if (editor.asChild) {
conn.sendReply(path, text);
} else {
conn.sendEdit(path, text);
}
editor.detach();
}
document.addEventListener("keydown", event => {
if (event.code === "Escape") {
cancelEdit();
event.preventDefault();
} else if (event.code === "Enter" && !event.shiftKey) {
completeEdit();
event.preventDefault();
} else if (document.activeElement.tagName === "TEXTAREA") { } else if (document.activeElement.tagName === "TEXTAREA") {
return; // Do nothing special return; // Do nothing special
} else if (event.code === "Tab") { } else if (event.code === "Tab") {
@ -461,9 +540,22 @@ document.addEventListener("keydown", event => {
cursor.moveDown(); cursor.moveDown();
event.preventDefault(); event.preventDefault();
} else if (event.code === "KeyE") { } else if (event.code === "KeyE") {
let node = cursor.getSelectedNode(); beginEdit();
editor.content = node.text; event.preventDefault();
editor.attachTo(cursor.path, false); } else if (event.code === "KeyR") {
if (event.shiftKey) {
console.log("indirect");
beginIndirectReply();
} else {
console.log("direct");
beginDirectReply();
}
event.preventDefault();
} else if (event.code === "KeyD") {
conn.sendDelete(cursor.path);
event.preventDefault();
} else if (event.code === "KeyA") {
conn.sendAct(cursor.path);
event.preventDefault(); event.preventDefault();
} }
}); });