return crypto.subtle.digest("SHA-256", new TextEncoder().encode(str)).then((buf) => (new Uint16Array(buf)[0] * 360) / 2 ** 16);
}
-const Model = {
- 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("data-created", 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;
+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("data-created", 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;
+ 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);
+ }
} 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);
+ target.getElementsByClassName("desc")[0].textContent = newDescription;
}
- } else {
- target.getElementsByClassName("desc")[0].textContent = newDescription;
- }
- return target;
- },
+ return target;
+ },
- hasTag: function (task: Element, tag: string): Element | null {
- for (const child of task.getElementsByClassName("tag")) {
- if (child.textContent === tag) {
- return child;
+ hasTag: function (task: Element, tag: string): Element | null {
+ for (const child of task.getElementsByClassName("tag")) {
+ if (child.textContent === tag) {
+ return child;
+ }
}
- }
- return null;
- },
+ return null;
+ },
- getPriority: function (task: Element): number {
- if (task.hasAttribute("data-priority")) {
- return parseFloat(task.getAttribute("data-priority")!);
- }
- return parseFloat(task.getAttribute("data-created")!);
- },
+ getPriority: function (task: Element): number {
+ if (task.hasAttribute("data-priority")) {
+ return parseFloat(task.getAttribute("data-priority")!);
+ }
+ return parseFloat(task.getAttribute("data-created")!);
+ },
- getTask: function (createTimestamp: string) {
- for (const task of document.getElementsByClassName("task")) {
- if (task.getAttribute("data-created") === createTimestamp) {
- return task;
+ getTask: function (createTimestamp: string) {
+ for (const task of document.getElementsByClassName("task")) {
+ if (task.getAttribute("data-created") === createTimestamp) {
+ return task;
+ }
}
- }
- },
+ },
- insertInPriorityOrder: function (task: Element, dest: Element) {
- const priority = this.getPriority(task);
- for (const t of dest.children) {
- if (t !== task && this.getPriority(t) < priority) {
- dest.insertBefore(task, t);
+ insertInPriorityOrder: function (task: Element, dest: Element) {
+ const priority = this.getPriority(task);
+ for (const t of dest.children) {
+ if (t !== task && this.getPriority(t) < priority) {
+ dest.insertBefore(task, t);
+ return;
+ }
+ }
+ dest.appendChild(task);
+ },
+
+ removeTag: function (createTimestamp: string, tagName: string) {
+ const task = this.getTask(createTimestamp);
+ if (!task) return null;
+ const tag = this.hasTag(task, tagName);
+ if (!tag) return;
+ task.removeChild(tag);
+ if (task instanceof HTMLElement) task.focus();
+ },
+
+ setPriority: function (createTimestamp: string, priority: number): Element | null {
+ const target = this.getTask(createTimestamp);
+ if (!target) return null;
+ target.setAttribute("data-priority", `${priority}`);
+ this.insertInPriorityOrder(target, target.parentElement!);
+ return target;
+ },
+
+ setState: function (stateTimestamp: string, createTimestamp: string, state: string) {
+ const task = this.getTask(createTimestamp);
+ if (!task) return;
+ task.setAttribute("data-state", state);
+ var date = task.getElementsByClassName("statedate")[0];
+ if (state === "todo") {
+ task.removeChild(date);
return;
}
- }
- dest.appendChild(task);
- },
-
- removeTag: function (createTimestamp: string, tagName: string) {
- const task = this.getTask(createTimestamp);
- if (!task) return null;
- const tag = this.hasTag(task, tagName);
- if (!tag) return;
- task.removeChild(tag);
- if (task instanceof HTMLElement) task.focus();
- },
-
- setPriority: function (createTimestamp: string, priority: number): Element | null {
- const target = this.getTask(createTimestamp);
- if (!target) return null;
- target.setAttribute("data-priority", `${priority}`);
- this.insertInPriorityOrder(target, target.parentElement!);
- return target;
- },
-
- setState: function (stateTimestamp: string, createTimestamp: string, state: string) {
- const task = this.getTask(createTimestamp);
- if (!task) return;
- task.setAttribute("data-state", state);
- var date = task.getElementsByClassName("statedate")[0];
- if (state === "todo") {
- task.removeChild(date);
- return;
- }
- if (!date) {
- date = document.createElement("span");
- date.classList.add("statedate");
- task.insertBefore(date, task.firstChild);
- }
- const d = new Date(parseInt(stateTimestamp));
- date.textContent = `${d.getFullYear()}-${`${d.getMonth() + 1}`.padStart(2, "0")}-${`${d.getDate()}`.padStart(2, "0")}`;
- },
-};
+ if (!date) {
+ date = document.createElement("span");
+ date.classList.add("statedate");
+ task.insertBefore(date, task.firstChild);
+ }
+ const d = new Date(parseInt(stateTimestamp));
+ date.textContent = `${d.getFullYear()}-${`${d.getMonth() + 1}`.padStart(2, "0")}-${`${d.getDate()}`.padStart(2, "0")}`;
+ },
+ };
+}
+const model = Model();
function Log(prefix: string = "vp-") {
var next_log_index = 0;
apply: function (entry: string) {
const [timestamp, command, data] = splitN(entry, " ", 2);
if (command == "Create") {
- return Model.addTask(timestamp, data);
+ return model.addTask(timestamp, data);
}
if (command == "Edit") {
const [createTimestamp, description] = splitN(data, " ", 1);
- return Model.edit(createTimestamp, description);
+ return model.edit(createTimestamp, description);
}
if (command == "Priority") {
const [createTimestamp, newPriority] = splitN(data, " ", 1);
- return Model.setPriority(createTimestamp, parseFloat(newPriority));
+ return model.setPriority(createTimestamp, parseFloat(newPriority));
}
if (command == "State") {
const [createTimestamp, state] = splitN(data, " ", 1);
- return Model.setState(timestamp, createTimestamp, state);
+ return model.setState(timestamp, createTimestamp, state);
}
if (command == "Tag") {
const [createTimestamp, tag] = splitN(data, " ", 1);
- return Model.addTag(createTimestamp, tag);
+ return model.addTag(createTimestamp, tag);
}
if (command == "Untag") {
const [createTimestamp, tag] = splitN(data, " ", 1);
- return Model.removeTag(createTimestamp, tag);
+ return model.removeTag(createTimestamp, tag);
}
},
input.removeEventListener("blur", this.completeTagEdit);
task.removeChild(input);
task.focus();
- if (resolution === CommitOrAbort.Commit && !newTagName.match(/^ *$/) && !Model.hasTag(task, newTagName)) {
+ if (resolution === CommitOrAbort.Commit && !newTagName.match(/^ *$/) && !model.hasTag(task, newTagName)) {
ui.addTag(task.getAttribute("data-created")!, newTagName);
lastTagNameEntered = newTagName;
}
makeTopPriority: function (task: Element | null = null) {
if (!task) task = this.currentTask();
if (!task) return;
- ui.setPriority(task.getAttribute("data-created")!, clock.now(), Model.getPriority(task));
+ ui.setPriority(task.getAttribute("data-created")!, clock.now(), model.getPriority(task));
task instanceof HTMLElement && task.focus();
},
for (const task of Array.from(document.getElementsByClassName("task"))) {
task.classList.remove("hide");
if (task.parentElement !== taskList) {
- Model.insertInPriorityOrder(task, taskList);
+ model.insertInPriorityOrder(task, taskList);
}
}
},
// Change task's priority to be between other tasks a and b.
setPriority: function (task: Element, a: Element | null, b: Element | null) {
- const aPriority = a === null ? clock.now() : Model.getPriority(a);
- const bPriority = b === null ? 0 : Model.getPriority(b);
+ const aPriority = a === null ? clock.now() : model.getPriority(a);
+ const bPriority = b === null ? 0 : model.getPriority(b);
console.assert(aPriority > bPriority, aPriority, ">", bPriority);
const span = aPriority - bPriority;
const newPriority = bPriority + 0.1 * span + 0.8 * span * Math.random();
console.assert(aPriority > newPriority && newPriority > bPriority, aPriority, ">", newPriority, ">", bPriority);
const newPriorityRounded = Math.round(newPriority);
const okToRound = aPriority > newPriorityRounded && newPriorityRounded > bPriority;
- ui.setPriority(task.getAttribute("data-created")!, okToRound ? newPriorityRounded : newPriority, Model.getPriority(task));
+ ui.setPriority(task.getAttribute("data-created")!, okToRound ? newPriorityRounded : newPriority, model.getPriority(task));
task instanceof HTMLElement && task.focus();
},
const tasksWithTag = new Map();
for (const task of document.getElementsByClassName("task")) {
- if (Model.hasTag(task, tag)) {
- tasksWithTag.set(task.getElementsByClassName("desc")[0].textContent, [Model.getPriority(task), task]);
+ if (model.hasTag(task, tag)) {
+ tasksWithTag.set(task.getElementsByClassName("desc")[0].textContent, [model.getPriority(task), task]);
}
}
}
for (const task of Array.from(document.getElementsByClassName("task"))) {
- if (Model.hasTag(task, tag)) {
+ if (model.hasTag(task, tag)) {
task.classList.remove("hide");
} else {
const superTask = highestPrioritySuperTask(task);
if (superTask !== null) {
- Model.insertInPriorityOrder(task, superTask);
+ model.insertInPriorityOrder(task, superTask);
} else {
task.classList.add("hide");
}