From da7cd1668aa321599799b1d8f4c59f5a31d61994 Mon Sep 17 00:00:00 2001 From: Michael Rowley Date: Sat, 13 Aug 2022 12:16:21 +0100 Subject: [PATCH] Added Header Aliases --- src/web/Manager.mjs | 2 + src/web/waiters/InputWaiter.mjs | 82 ++++++++++++++++++++++- src/web/waiters/TabWaiter.mjs | 112 +++++++++++++++++++++++++++----- 3 files changed, 175 insertions(+), 21 deletions(-) diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index e1e07dfd..c4ce6e62 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -167,6 +167,8 @@ class Manager { document.getElementById("btn-go-to-input-tab").addEventListener("click", this.input.goToTab.bind(this.input)); document.getElementById("btn-find-input-tab").addEventListener("click", this.input.findTab.bind(this.input)); this.addDynamicListener("#input-tabs li .input-tab-content", "click", this.input.changeTabClick, this.input); + this.addDynamicListener("#input-tabs-wrapper #input-tabs .input-tab-content", "dblclick", this.input.renameTabClick, this.input); + this.addDynamicListener("#input-tabs-wrapper #input-tabs .input-tab-content span input", "focusout", this.input.confirmTabRename, this.input); document.getElementById("input-show-pending").addEventListener("change", this.input.filterTabSearch.bind(this.input)); document.getElementById("input-show-loading").addEventListener("change", this.input.filterTabSearch.bind(this.input)); document.getElementById("input-show-loaded").addEventListener("change", this.input.filterTabSearch.bind(this.input)); diff --git a/src/web/waiters/InputWaiter.mjs b/src/web/waiters/InputWaiter.mjs index b421d8d8..fcc40179 100644 --- a/src/web/waiters/InputWaiter.mjs +++ b/src/web/waiters/InputWaiter.mjs @@ -1001,6 +1001,11 @@ class InputWaiter { */ changeTab(inputNum, changeOutput) { if (this.manager.tabs.getInputTabItem(inputNum) !== null) { + if (this.manager.tabs.getActiveTab("input") === inputNum) { + if (changeOutput && this.manager.tabs.getActiveTab("output") !== inputNum) + this.manager.output.changeTab(inputNum, false); + return; + } this.manager.tabs.changeInputTab(inputNum); this.inputWorker.postMessage({ action: "setInput", @@ -1090,6 +1095,7 @@ class InputWaiter { const inputNum = this.manager.tabs.getActiveInputTab(); if (inputNum === -1) return; + this.manager.tabs.removeTabHeaderAlias(inputNum); this.manager.highlighter.removeHighlights(); getSelection().removeAllRanges(); @@ -1228,6 +1234,7 @@ class InputWaiter { removeChefWorker: true } }); + this.manager.tabs.removeTabHeaderAlias(inputNum); this.manager.output.removeTab(inputNum); } @@ -1241,9 +1248,10 @@ class InputWaiter { if (!mouseEvent.target) { return; } - const tabNum = mouseEvent.target.closest("button").parentElement.getAttribute("inputNum"); - if (tabNum) { - this.removeInput(parseInt(tabNum, 10)); + const tabNumStr = mouseEvent.target.closest("button").parentElement.getAttribute("inputNum"); + if (tabNumStr) { + const tabNum = parseInt(tabNumStr, 10); + this.removeInput(tabNum); } } @@ -1438,6 +1446,74 @@ class InputWaiter { this.app.updateTitle(urlData.includeInput, urlData.input, true); } + /** + * Handler for renaming tabs. + * Opens the tab-renaming dialogue. + * + * @param {event} mouseEvent - The mouse event that this call was triggered by + */ + async renameTabClick(mouseEvent) { + const targetElement = mouseEvent.target; + if (!targetElement) return; + + const editingElement = document.createElement("input"); + editingElement.classList.add("form-control"); + let renameContents = targetElement.textContent; + const renameContentsColon = renameContents.indexOf(":"); + + // Calling 'getInputValue()' might take a long time for large datasets, + // it could be beneficial to modify the API to allow for querying whether + // there is any input rather than getting the full string just to access its length. + const inputLength = (await this.getInputValue(this.manager.tabs.getActiveInputTab())).length; + + // Remove the data from the renaming section + if (renameContentsColon !== -1 && inputLength !== 0) { + renameContents = renameContents.substring(0, renameContentsColon); + } + + // Remove the single quotation marks from the renaming section + renameContents = renameContents.replaceAll("'", ""); + + if (renameContents.length < 3 && !isNaN(parseInt(renameContents, 10))) { + renameContents = `Tab ${renameContents.toString()}`; + } + + editingElement.setAttribute("value", renameContents); + editingElement.setAttribute("minlength", "3"); // Delimiting between shortened tab headers and custom ones. + targetElement.textContent = ""; + editingElement.style.height = "1.5em"; + editingElement.style.textAlign = "center"; + targetElement.appendChild(editingElement); + // A delay is required between appending and focusing. + await new Promise(r => setTimeout(r, 100)); + editingElement.focus(); + } + + /** + * Assigns the current user-entered text to + * the tab's new name. + * Resets the DOM of the tab to the default non-editable state. + */ + async confirmTabRename() { + const activeInputTabNum = this.manager.tabs.getActiveInputTab(); + if (activeInputTabNum === -1) { + return; + } + const activeInputTabElement = this.manager.tabs.getTabItem(activeInputTabNum, "input"); + if (activeInputTabElement == null || activeInputTabElement.children.size < 1) { + return; + } + const tabContent = activeInputTabElement.children[0]; + const tabHeader = tabContent.children[0].children[0].value; + const inputContents = await this.getInputValue(activeInputTabNum); + + if (tabHeader.length === 0) + this.manager.tabs.removeTabHeaderAlias(activeInputTabNum, false); + else + this.manager.tabs.addTabHeaderAlias(activeInputTabNum, tabHeader); + this.manager.tabs.updateInputTabHeader(activeInputTabNum, inputContents); + this.manager.tabs.updateOutputTabHeader(activeInputTabNum, inputContents); + } } diff --git a/src/web/waiters/TabWaiter.mjs b/src/web/waiters/TabWaiter.mjs index 384b1ab7..fe9219d8 100644 --- a/src/web/waiters/TabWaiter.mjs +++ b/src/web/waiters/TabWaiter.mjs @@ -18,6 +18,7 @@ class TabWaiter { constructor(app, manager) { this.app = app; this.manager = manager; + this.tabHeaderAliases = []; // Mapping custom tab headers to indexes/numbers. } /** @@ -251,21 +252,20 @@ class TabWaiter { tabsList.appendChild(this.createTabElement(nums[i], active, io)); } - // Display shadows if there are tabs left / right of the displayed tabs - if (tabsLeft) { - tabsList.classList.add("tabs-left"); - } else { - tabsList.classList.remove("tabs-left"); - } - if (tabsRight) { - tabsList.classList.add("tabs-right"); - } else { - tabsList.classList.remove("tabs-right"); - } - // Show or hide the tab bar depending on how many tabs we have if (nums.length > 1) { this.showTabBar(); + // Display shadows if there are tabs left / right of the displayed tabs + if (tabsLeft) { + tabsList.classList.add("tabs-left"); + } else { + tabsList.classList.remove("tabs-left"); + } + if (tabsRight) { + tabsList.classList.add("tabs-right"); + } else { + tabsList.classList.remove("tabs-right"); + } } else { this.hideTabBar(); } @@ -349,16 +349,35 @@ class TabWaiter { * @param {string} data - The data to display in the tab header * @param {string} io - Either "input" or "output" */ - updateTabHeader(inputNum, data, io) { + async updateTabHeader(inputNum, data, io) { const tab = this.getTabItem(inputNum, io); - if (tab === null) return; + if (tab == null) return; - let headerData = `Tab ${inputNum}`; - if (data.length > 0) { - headerData = data.slice(0, 100); - headerData = `${inputNum}: ${headerData}`; + const customHeaderData = this.getTabHeaderAlias(inputNum); + + // When 'customHeaderData === `Tab ${inputNum}`' is true, it's usually due to + // a user having opened the rename textbox but then closing it without change. + const isStandardHeader = customHeaderData === null || customHeaderData === `Tab ${inputNum}`; + + let headerData = isStandardHeader ? `Tab ${inputNum}` : customHeaderData; + const dataIsFile = data instanceof ArrayBuffer; + const includeData = data.length > 0 || dataIsFile; + + if (includeData) { + const inputObj = await this.manager.input.getInputObj(inputNum); + + const dataPreview = dataIsFile ? inputObj.data.name : data.slice(0, 100); + + if (isStandardHeader) + headerData = inputNum.toString(); + else + headerData = `'${customHeaderData}'`; + + headerData += `: ${dataPreview}`; } tab.firstElementChild.innerText = headerData; + if (!isStandardHeader && !includeData) + tab.firstElementChild.innerText = `'${headerData}'`; } /** @@ -423,6 +442,63 @@ class TabWaiter { this.updateTabProgress(inputNum, progress, total, "output"); } + /** + * Adds an alias between a custom tab header and a tab number so that + * mapping between the two is possible if DOM element is removed. + * + * @param {number} tabNum - The index of the tab being aliased + * @param {string} tabHeader - The custom tab header + */ + addTabHeaderAlias(tabNum, tabHeader) { + // First, we try to overwrite an existing alias. + for (let i = 0; i < this.tabHeaderAliases.length; i++) { + if (this.tabHeaderAliases.at(i).tabNumber === tabNum) { + this.tabHeaderAliases.at(i).customHeader = tabHeader; + return; + } + } + this.tabHeaderAliases.push({tabNumber: tabNum, customHeader: tabHeader}); + } + + /** + * Removes a previously-assigned header alias. + * + * @param {number} tabNum - The index of the tab that should be removed. + * @param {boolean} shouldThrow - A boolean representing whether the function should throw an exception or return silently if it cannot locate the tab header. + */ + removeTabHeaderAlias(tabNum, shouldThrow) { + for (let i = 0; i < this.tabHeaderAliases.length; i++) { + + if (this.tabHeaderAliases.at(i).tabNumber === tabNum) { + this.tabHeaderAliases.splice(i, 1); + return; + } + + } + if (shouldThrow) + throw `Unable to locate header alias at tab index ${tabNum.toString()}.`; + } + + /** + * Retrieves the custom header for a given tab. + * + * @param {number} tabNum - The index of the tab whose alias should be retrieved. + * @param {boolean} shouldThrow - Whether the function should throw an exception (instead of returning null) in the event of it being unable to locate the tab. + * @returns {string} customHeader - The custom header for the requested tab. + */ + getTabHeaderAlias(tabNum, shouldThrow) { + for (let i = 0; i < this.tabHeaderAliases.length; i++) { + + if (this.tabHeaderAliases.at(i).tabNumber === tabNum) + return this.tabHeaderAliases.at(i).customHeader; + + } + if (shouldThrow) + throw `Unable to locate header alias at tab index ${tabNum.toString()}.`; + return null; + } + } export default TabWaiter; +