-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.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;
+ 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);
+ }
- }
- },
-
- 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}`);
- for (const task of document.getElementsByClassName("task")) {
- if (task !== target && this.getPriority(task) < priority) {
- task.parentElement!.insertBefore(target, task);
- return target;
+ },
+
+ 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;
+ }
- }
- document.getElementById("tasks")!.appendChild(target);
- 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")}`;
- },
-};
+ 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")}`;
+ },
+ };
+}
+const model = Model();
apply: function (entry: string) {
const [timestamp, command, data] = splitN(entry, " ", 2);
if (command == "Create") {
apply: function (entry: string) {
const [timestamp, command, data] = splitN(entry, " ", 2);
if (command == "Create") {
- firstVisibleTask: function () {
- for (const task of document.getElementsByClassName("task")) {
+ currentTag: function (): Element | null {
+ var target = document.activeElement;
+ if (!target) return null;
+ if (target.classList.contains("task")) {
+ const tags = target.getElementsByClassName("tag");
+ target = tags[tags.length - 1];
+ }
+ if (!target || !target.classList.contains("tag")) return null;
+ return target;
+ },
+
+ currentTask: function (): HTMLElement | null {
+ var target = document.activeElement;
+ if (!target) return null;
+ if (target.classList.contains("tag")) target = target.parentElement!;
+ return target as HTMLElement;
+ },
+
+ firstVisibleTask: function (root: Element | null = null) {
+ if (root === null) root = document.body;
+ for (const task of root.getElementsByClassName("task")) {
- moveCursor: function (offset: number): boolean {
- const active = document.activeElement;
+ moveCursorLeft: function () {
+ const active = this.currentTask();
+ if (!active) return false;
+ if (active.parentElement!.classList.contains("task")) {
+ active.parentElement!.focus();
+ }
+ },
+
+ moveCursorRight: function () {
+ const active = this.currentTask();
+ if (!active) return false;
+ (this.firstVisibleTask(active) as HTMLElement | null)?.focus();
+ },
+
+ moveCursorVertically: function (offset: number): boolean {
+ const active = this.currentTask();
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;
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;
- setView: function (state: string, color: string) {
+ setTagView: function (tag: string | null = null) {
+ if (tag === null) {
+ const target = this.currentTag();
+ if (!target) return;
+ tag = target.textContent!;
+ }
+
+ if (currentTagView !== null) {
+ this.resetTagView();
+ }
+
+ 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]);
+ }
+ }
+
+ function highestPrioritySuperTask(t: Element) {
+ var maxPriority = -1;
+ var superTask = null;
+ for (const child of t.getElementsByClassName("tag")) {
+ const e = tasksWithTag.get(child.textContent);
+ if (e !== undefined && e[0] > maxPriority) {
+ maxPriority = e[0];
+ superTask = e[1];
+ }
+ }
+ return superTask;
+ }
+
+ for (const task of Array.from(document.getElementsByClassName("task"))) {
+ if (model.hasTag(task, tag)) {
+ task.classList.remove("hide");
+ } else {
+ const superTask = highestPrioritySuperTask(task);
+ if (superTask !== null) {
+ model.insertInPriorityOrder(task, superTask);
+ } else {
+ task.classList.add("hide");
+ }
+ }
+ }
+
+ currentTagView = tag;
+ },
+
+ setView: function (state: string) {
- if (event.key == "a") return browserUI.setView("all", "Gold");
- if (event.key == "c") return browserUI.setView("cancelled", "Red");
- if (event.key == "d") return browserUI.setView("done", "LawnGreen");
- if (event.key == "q") return browserUI.setView("todo", "White");
+ if (event.key == "a") return browserUI.setView("all");
+ if (event.key == "c") return browserUI.setView("cancelled");
+ if (event.key == "d") return browserUI.setView("done");
+ if (event.key == "i") return browserUI.setUntaggedView();
+ if (event.key == "p") return browserUI.setTagView("Project");
+ if (event.key == "q") return browserUI.setView("todo");
- if (event.key == "v") return browserUI.setView("todo", "White");
- if (event.key == "w") return browserUI.setView("waiting", "MediumOrchid");
- if (event.key == "x") return browserUI.setView("deleted", "Black");
+ if (event.key == "T") return browserUI.resetTagView();
+ if (event.key == "t") return browserUI.setTagView();
+ if (event.key == "u") return browserUI.setUntaggedView();
+ if (event.key == "v") return browserUI.resetView();
+ if (event.key == "w") return browserUI.setView("waiting");
+ if (event.key == "x") return browserUI.setView("deleted");