X-Git-Url: http://git.scottworley.com/vopamoi/blobdiff_plain/c2226333bbb69b5110aa5feaf839347321dca9ee..9db534f6ab9420b8fd5836e791dbe09abb0a92d8:/vopamoi.ts diff --git a/vopamoi.ts b/vopamoi.ts index 7777f36..f550fa1 100644 --- a/vopamoi.ts +++ b/vopamoi.ts @@ -67,7 +67,7 @@ const Model = { return tag; } } - task.appendChild(tag); + task.insertBefore(tag, task.getElementsByClassName("desc")[0]!); return tag; }, @@ -76,7 +76,7 @@ const Model = { 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; + const input = target.getElementsByTagName("input")[0]!; if ( input.value === target.getAttribute("data-description") && input.selectionStart === input.value.length && @@ -120,6 +120,17 @@ const Model = { } }, + 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; @@ -133,13 +144,7 @@ const Model = { 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; - } - } - document.getElementById("tasks")!.appendChild(target); + this.insertInPriorityOrder(target, target.parentElement!); return target; }, @@ -202,6 +207,7 @@ function Log(prefix: string = "vp-") { }, replay: function () { + document.getElementById("tasks")!.style.display = "none"; while (true) { const entry = window.localStorage.getItem(`${prefix}${next_log_index}`); if (entry === null) { @@ -210,6 +216,7 @@ function Log(prefix: string = "vp-") { this.apply(entry); next_log_index++; } + document.getElementById("tasks")!.style.display = ""; }, }; } @@ -276,6 +283,7 @@ function BrowserUI() { todo: "White", waiting: "MediumOrchid", }; + var currentTagView: string | null = null; var currentViewState = "todo"; var taskFocusedBeforeJumpingToInput: HTMLElement | null = null; var lastTagNameEntered = ""; @@ -297,9 +305,8 @@ function BrowserUI() { }, beginEdit: function (event: Event) { - var task = document.activeElement; + const task = this.currentTask(); if (!task) return; - if (task.classList.contains("tag")) task = task.parentElement!; const input = document.createElement("input"); const desc = task.getElementsByClassName("desc")[0]; const oldDescription = desc.textContent!; @@ -313,7 +320,7 @@ function BrowserUI() { }, beginTagEdit: function (event: Event) { - const task = document.activeElement; + const task = this.currentTask(); if (!task) return; const input = document.createElement("input"); input.classList.add("tag"); @@ -355,19 +362,40 @@ function BrowserUI() { } }, - 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")) { const state = task.getAttribute("data-state"); - if (task instanceof HTMLElement && (state === currentViewState || (currentViewState === "all" && state !== "deleted"))) { + if ( + task instanceof HTMLElement && + (state === currentViewState || (currentViewState === "all" && state !== "deleted")) && + !task.classList.contains("hide") + ) { return task; } } }, focusTaskNameInput: function (event: Event) { - if (document.activeElement instanceof HTMLElement) { - taskFocusedBeforeJumpingToInput = document.activeElement; - } + taskFocusedBeforeJumpingToInput = this.currentTask(); document.getElementById("taskName")!.focus(); window.scroll(0, 0); event.preventDefault(); @@ -381,7 +409,10 @@ function BrowserUI() { cursor = increment > 0 ? cursor.nextElementSibling : cursor.previousElementSibling; if (!cursor || !(cursor instanceof HTMLElement)) break; const state = cursor.getAttribute("data-state")!; - if (state === currentViewState || (currentViewState === "all" && state !== "deleted")) { + if ( + (state === currentViewState || (currentViewState === "all" && state !== "deleted")) && + !cursor.classList.contains("hide") + ) { offset -= increment; valid_cursor = cursor; } @@ -398,20 +429,34 @@ function BrowserUI() { }, makeBottomPriority: function (task: Element | null = null) { - if (!task) task = document.activeElement; + if (!task) task = this.currentTask(); if (!task) return; this.setPriority(task, document.getElementById("tasks")!.lastElementChild, null); }, makeTopPriority: function (task: Element | null = null) { - if (!task) task = document.activeElement; + if (!task) task = this.currentTask(); if (!task) return; ui.setPriority(task.getAttribute("data-created")!, clock.now(), Model.getPriority(task)); task instanceof HTMLElement && task.focus(); }, - 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(); if (!active) return false; const dest = this.visibleTaskAtOffset(active, offset); if (dest !== active && dest instanceof HTMLElement) { @@ -422,7 +467,7 @@ function BrowserUI() { }, moveTask: function (offset: number) { - const active = document.activeElement; + const active = this.currentTask(); if (!active) return; const dest = this.visibleTaskAtOffset(active, offset); if (dest === active) return; // Already extremal @@ -436,16 +481,27 @@ function BrowserUI() { }, removeTag: function () { - var target = document.activeElement; + const target = this.currentTag(); if (!target) return; - if (target.classList.contains("task")) { - const tags = target.getElementsByClassName("tag"); - target = tags[tags.length - 1]; - } - if (!target || !target.classList.contains("tag")) return; ui.removeTag(target.parentElement!.getAttribute("data-created")!, target.textContent!); }, + resetTagView: function () { + currentTagView = null; + const taskList = document.getElementById("tasks")!; + for (const task of Array.from(document.getElementsByClassName("task"))) { + task.classList.remove("hide"); + if (task.parentElement !== taskList) { + Model.insertInPriorityOrder(task, taskList); + } + } + }, + + resetView: function () { + this.setView("todo"); + this.resetTagView(); + }, + returnFocusAfterInput: function (): boolean { if (taskFocusedBeforeJumpingToInput) { taskFocusedBeforeJumpingToInput.focus(); @@ -469,17 +525,64 @@ function BrowserUI() { }, setState: function (newState: string) { - const task = document.activeElement; + const task = this.currentTask(); if (!task) return; const oldState = task.getAttribute("data-state")!; if (newState === oldState) return; const createTimestamp = task.getAttribute("data-created")!; if (currentViewState !== "all" || newState == "deleted") { - this.moveCursor(1) || this.moveCursor(-1); + this.moveCursorVertically(1) || this.moveCursorVertically(-1); } return ui.setState(createTimestamp, newState, oldState); }, + 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) { const sheet = (document.getElementById("viewStyle") as HTMLStyleElement).sheet!; if (state === "all") { @@ -491,11 +594,24 @@ function BrowserUI() { sheet.removeRule(2); sheet.removeRule(2); currentViewState = state; - if (document.activeElement?.getAttribute("data-state") !== state) { + if (this.currentTask()?.getAttribute("data-state") !== state) { this.firstVisibleTask()?.focus(); } }, + setUntaggedView: function () { + if (currentTagView !== null) { + this.resetTagView(); + } + for (const task of document.getElementsByClassName("task")) { + if (task.getElementsByClassName("tag").length === 0) { + task.classList.remove("hide"); + } else { + task.classList.add("hide"); + } + } + }, + undo: function () { const ret = ui.undo(); if (ret && ret instanceof HTMLElement) ret.focus(); @@ -541,8 +657,10 @@ function handleKey(event: any) { if (event.key == "e") return window.scrollBy(0, (inputCount ?? 1) * scrollIncrement); if (event.key == "y") return window.scrollBy(0, (inputCount ?? 1) * -scrollIncrement); } else { - if (event.key == "j") return browserUI.moveCursor(inputCount ?? 1); - if (event.key == "k") return browserUI.moveCursor(-(inputCount ?? 1)); + if (event.key == "h") return browserUI.moveCursorLeft(); + if (event.key == "l") return browserUI.moveCursorRight(); + if (event.key == "j") return browserUI.moveCursorVertically(inputCount ?? 1); + if (event.key == "k") return browserUI.moveCursorVertically(-(inputCount ?? 1)); if (event.key == "J") return browserUI.moveTask(inputCount ?? 1); if (event.key == "K") return browserUI.moveTask(-(inputCount ?? 1)); if (event.key == "G") return browserUI.jumpCursor(inputCount ?? MAX_SAFE_INTEGER); @@ -572,9 +690,14 @@ function handleKey(event: any) { 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 == "s") return (inputState = InputState.VS); - if (event.key == "v") return browserUI.setView("todo"); + 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"); } else if (inputState === InputState.VS) {