-const Model = {
- addTask: function (timestamp: string, description: string): Element {
- const task = document.createElement("div");
- task.appendChild(document.createTextNode(description));
- task.classList.add("task");
- task.setAttribute("tabindex", "0");
- task.setAttribute("data-created", timestamp);
- task.setAttribute("data-state", "todo");
- document.getElementById("tasks")!.appendChild(task);
- return task;
- },
-
- addTag: function (createTimestamp: string, tagName: string): Element | null {
- const task = this.getTask(createTimestamp);
- if (!task) return null;
- const tag = document.createElement("span");
- tag.appendChild(document.createTextNode(tagName));
- tag.classList.add("tag");
- tag.setAttribute("tabindex", "0");
- task.appendChild(tag);
- return tag;
- },
-
- edit: function (createTimestamp: string, newDescription: string): Element | null {
- const target = this.getTask(createTimestamp);
- if (!target) return null;
- if (target.hasAttribute("data-description")) {
- // Oh no: An edit has arrived from a replica while a local edit is in progress.
- const input = target.firstChild as HTMLInputElement;
- if (
- input.value === target.getAttribute("data-description") &&
- input.selectionStart === input.value.length &&
- input.selectionEnd === input.value.length
- ) {
- // No local changes have actually been made yet. Change the contents of the edit box!
- input.value = newDescription;
+// Returns a promise for a hue based on a hash of the string
+function hashHue(str: string) {
+ // Using crypto for this is overkill
+ return crypto.subtle.digest("SHA-256", new TextEncoder().encode(str)).then((buf) => (new Uint16Array(buf)[0] * 360) / 2 ** 16);
+}
+
+function Model() {
+ return {
+ addTask: function (timestamp: string, description: string): Element {
+ const task = document.createElement("div");
+ const desc = document.createElement("span");
+ desc.textContent = description;
+ desc.classList.add("desc");
+ task.appendChild(desc);
+ task.classList.add("task");
+ task.setAttribute("tabindex", "0");
+ task.setAttribute("id", timestamp);
+ task.setAttribute("data-state", "todo");
+ const tasks = document.getElementById("tasks")!;
+ tasks.insertBefore(task, tasks.firstElementChild);
+ return task;
+ },
+
+ addTag: function (createTimestamp: string, tagName: string): Element | null {
+ const task = this.getTask(createTimestamp);
+ if (!task) return null;
+ const existingTag = this.hasTag(task, tagName);
+ if (existingTag) return existingTag;
+ const tag = document.createElement("span");
+ tag.appendChild(document.createTextNode(tagName));
+ tag.classList.add("tag");
+ tag.setAttribute("tabindex", "0");
+ hashHue(tagName).then((hue) => (tag.style.backgroundColor = `hsl(${hue},90%,45%)`));
+ for (const child of task.getElementsByClassName("tag")) {
+ if (tagName > child.textContent!) {
+ task.insertBefore(tag, child);
+ return tag;
+ }
+ }
+ task.insertBefore(tag, task.getElementsByClassName("desc")[0]!);
+ return tag;
+ },
+
+ edit: function (createTimestamp: string, newDescription: string): Element | null {
+ const target = this.getTask(createTimestamp);
+ if (!target) return null;
+ if (target.hasAttribute("data-description")) {
+ // Oh no: An edit has arrived from a replica while a local edit is in progress.
+ const input = target.getElementsByTagName("input")[0]!;
+ if (
+ input.value === target.getAttribute("data-description") &&
+ input.selectionStart === input.value.length &&
+ input.selectionEnd === input.value.length
+ ) {
+ // No local changes have actually been made yet. Change the contents of the edit box!
+ input.value = newDescription;
+ } else {
+ // No great options.
+ // Prefer not to interrupt the local user's edit.
+ // The remote edit is mostly lost; this mostly becomes last-write-wins.
+ target.setAttribute("data-description", newDescription);
+ }