[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:
parent
78235ef7cf
commit
54795b81ac
2 changed files with 124 additions and 32 deletions
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue