فهرست منبع

Move tab logic into a new TabWaiter

j433866 6 سال پیش
والد
کامیت
c2087f6d5f

+ 2 - 2
src/web/App.mjs

@@ -157,7 +157,7 @@ class App {
             this.manager.input.inputWorker.postMessage({
                 action: "autobake",
                 data: {
-                    activeTab: this.manager.input.getActiveTab()
+                    activeTab: this.manager.tabs.getActiveInputTab()
                 }
             });
         } else {
@@ -194,7 +194,7 @@ class App {
     setInput(input) {
         // Get the currently active tab.
         // If there isn't one, assume there are no inputs so use inputNum of 1
-        let inputNum = this.manager.input.getActiveTab();
+        let inputNum = this.manager.tabs.getActiveInputTab();
         if (inputNum === -1) inputNum = 1;
         this.manager.input.updateInputValue(inputNum, input);
 

+ 2 - 0
src/web/Manager.mjs

@@ -16,6 +16,7 @@ import HighlighterWaiter from "./waiters/HighlighterWaiter";
 import SeasonalWaiter from "./waiters/SeasonalWaiter";
 import BindingsWaiter from "./waiters/BindingsWaiter";
 import BackgroundWorkerWaiter from "./waiters/BackgroundWorkerWaiter";
+import TabWaiter from "./waiters/TabWaiter";
 
 
 /**
@@ -70,6 +71,7 @@ class Manager {
         this.seasonal    = new SeasonalWaiter(this.app, this);
         this.bindings    = new BindingsWaiter(this.app, this);
         this.background  = new BackgroundWorkerWaiter(this.app, this);
+        this.tabs        = new TabWaiter(this.app, this);
 
         // Object to store dynamic handlers to fire on elements that may not exist yet
         this.dynamicHandlers = {};

+ 1 - 1
src/web/waiters/BindingsWaiter.mjs

@@ -126,7 +126,7 @@ class BindingsWaiter {
                     break;
                 case "KeyW": // Close tab
                     e.preventDefault();
-                    this.manager.input.removeInput(this.manager.input.getActiveTab());
+                    this.manager.input.removeInput(this.manager.tabs.getActiveInputTab());
                     break;
                 case "ArrowLeft": // Go to previous tab
                     e.preventDefault();

+ 1 - 1
src/web/waiters/ControlsWaiter.mjs

@@ -72,7 +72,7 @@ class ControlsWaiter {
         if (this.app.baking) return;
         // Reset status using cancelBake
         this.manager.worker.cancelBake(true, false);
-        const activeTab = this.manager.input.getActiveTab();
+        const activeTab = this.manager.tabs.getActiveInputTab();
         let progress = 0;
         if (this.manager.output.outputs[activeTab].progress !== false) {
             progress = this.manager.output.outputs[activeTab].progress;

+ 1 - 1
src/web/waiters/HighlighterWaiter.mjs

@@ -378,7 +378,7 @@ class HighlighterWaiter {
     displayHighlights(pos, direction) {
         if (!pos) return;
 
-        if (this.manager.input.getActiveTab() !== this.manager.output.getActiveTab()) return;
+        if (this.manager.tabs.getActiveInputTab() !== this.manager.tabs.getActiveOutputTab()) return;
 
         const io = direction === "forward" ? "output" : "input";
 

+ 59 - 233
src/web/waiters/InputWaiter.mjs

@@ -48,8 +48,8 @@ class InputWaiter {
         this.inputWorker = null;
         this.loaderWorkers = [];
         this.workerId = 0;
-        this.maxWorkers = navigator.hardwareConcurrency || 4;
         this.maxTabs = 4;
+        this.maxWorkers = navigator.hardwareConcurrency || 4;
         this.callbacks = {};
         this.callbackID = 0;
     }
@@ -58,14 +58,14 @@ class InputWaiter {
      * Calculates the maximum number of tabs to display
      */
     calcMaxTabs() {
-        const numTabs = Math.floor((document.getElementById("IO").offsetWidth - 75)  / 120);
-        this.maxTabs = (numTabs > 1) ? numTabs : 2;
-        if (this.inputWorker) {
+        const numTabs = this.manager.tabs.calcMaxTabs();
+        if (this.inputWorker && this.maxTabs !== numTabs) {
+            this.maxTabs = numTabs;
             this.inputWorker.postMessage({
                 action: "updateMaxTabs",
                 data: {
-                    maxTabs: this.maxTabs,
-                    activeTab: this.getActiveTab()
+                    maxTabs: numTabs,
+                    activeTab: this.manager.tabs.getActiveInputTab()
                 }
             });
         }
@@ -94,7 +94,7 @@ class InputWaiter {
             action: "updateMaxTabs",
             data: {
                 maxTabs: this.maxTabs,
-                activeTab: this.getActiveTab()
+                activeTab: this.manager.tabs.getActiveInputTab()
             }
         });
         this.inputWorker.postMessage({
@@ -258,7 +258,7 @@ class InputWaiter {
                 this.changeTab(r.data, this.app.options.syncTabs);
                 break;
             case "updateTabHeader":
-                this.updateTabHeader(r.data);
+                this.manager.tabs.updateInputTabHeader(r.data.inputNum, r.data.input);
                 break;
             case "loadingInfo":
                 this.showLoadingInfo(r.data, true);
@@ -327,7 +327,7 @@ class InputWaiter {
      */
     async set(inputData, silent=false) {
         return new Promise(function(resolve, reject) {
-            const activeTab = this.getActiveTab();
+            const activeTab = this.manager.tabs.getActiveInputTab();
             if (inputData.inputNum !== activeTab) return;
 
             const inputText = document.getElementById("input-text");
@@ -383,7 +383,7 @@ class InputWaiter {
      * @param {number} inputData.progress - The load progress of the input file
      */
     setFile(inputData) {
-        const activeTab = this.getActiveTab();
+        const activeTab = this.manager.tabs.getActiveInputTab();
         if (inputData.inputNum !== activeTab) return;
 
         const fileOverlay = document.getElementById("input-file"),
@@ -425,7 +425,7 @@ class InputWaiter {
      * @param {ArrayBuffer} inputData.input - The actual input to display
      */
     displayFilePreview(inputData) {
-        const activeTab = this.getActiveTab(),
+        const activeTab = this.manager.tabs.getActiveInputTab(),
             input = inputData.input,
             inputText = document.getElementById("input-text"),
             fileThumb = document.getElementById("input-file-thumbnail");
@@ -459,7 +459,7 @@ class InputWaiter {
      * @param {number | string} progress - Either a number or "error"
      */
     updateFileProgress(inputNum, progress) {
-        const activeTab = this.getActiveTab();
+        const activeTab = this.manager.tabs.getActiveInputTab();
         if (inputNum !== activeTab) return;
 
         const fileLoaded = document.getElementById("input-file-loaded");
@@ -595,11 +595,7 @@ class InputWaiter {
     async getInputNums() {
         return await new Promise(resolve => {
             this.getNums(r => {
-                resolve({
-                    inputNums: r.inputNums,
-                    min: r.min,
-                    max: r.max
-                });
+                resolve(r);
             });
         });
     }
@@ -670,7 +666,7 @@ class InputWaiter {
 
         const textArea = document.getElementById("input-text");
         const value = (textArea.value !== undefined) ? textArea.value : "";
-        const activeTab = this.getActiveTab();
+        const activeTab = this.manager.tabs.getActiveInputTab();
 
         this.app.progress = 0;
 
@@ -678,7 +674,7 @@ class InputWaiter {
             (value.count("\n") + 1) : null;
         this.setInputInfo(value.length, lines);
         this.updateInputValue(activeTab, value);
-        this.updateTabHeader({inputNum: activeTab, input: value});
+        this.manager.tabs.updateInputTabHeader(activeTab, value);
 
         if (e && this.badKeys.indexOf(e.keyCode) < 0) {
             // Fire the statechange event as the input has been modified
@@ -803,7 +799,7 @@ class InputWaiter {
      */
     loadUIFiles(files) {
         const numFiles = files.length;
-        const activeTab = this.getActiveTab();
+        const activeTab = this.manager.tabs.getActiveInputTab();
         log.debug(`Loading ${numFiles} files.`);
 
         // Display the number of files as pending so the user
@@ -938,103 +934,12 @@ class InputWaiter {
             setTimeout(function() {
                 this.inputWorker.postMessage({
                     action: "getLoadProgress",
-                    data: this.getActiveTab()
+                    data: this.manager.tabs.getActiveInputTab()
                 });
             }.bind(this), 100);
         }
     }
 
-    /**
-     * Create a tab element for the input tab bar
-     *
-     * @param {number} inputNum - The inputNum of the new tab
-     * @param {boolean} [active=false] - If true, sets the tab to active
-     * @returns {Element}
-     */
-    createTabElement(inputNum, active) {
-        const newTab = document.createElement("li");
-        newTab.setAttribute("inputNum", inputNum.toString());
-
-        if (active) newTab.classList.add("active-input-tab");
-
-        const newTabContent = document.createElement("div");
-        newTabContent.classList.add("input-tab-content");
-        newTabContent.innerText = `${inputNum.toString()}: New Tab`;
-
-        const newTabButton = document.createElement("button");
-        newTabButton.type = "button";
-        newTabButton.className = "btn btn-primary bmd-btn-icon btn-close-tab";
-
-        const newTabButtonIcon = document.createElement("i");
-        newTabButtonIcon.classList.add("material-icons");
-        newTabButtonIcon.innerText = "clear";
-
-        newTabButton.appendChild(newTabButtonIcon);
-        newTabButton.addEventListener("click", this.removeTabClick.bind(this));
-
-        newTab.appendChild(newTabContent);
-        newTab.appendChild(newTabButton);
-
-        return newTab;
-    }
-
-    /**
-     * Redraw the tab bar with an updated list of tabs.
-     * Then changes to the activeTab
-     *
-     * @param {number[]} nums - The inputNums of the tab bar to be drawn
-     * @param {number} activeTab - The inputNum of the active tab
-     * @param {boolean} tabsLeft - True if there are tabs to the left of the currently displayed tabs
-     * @param {boolean} tabsRight - True if there are tabs to the right of the currently displayed tabs
-     */
-    refreshTabs(nums, activeTab, tabsLeft, tabsRight) {
-        const tabsList = document.getElementById("input-tabs");
-
-        for (let i = tabsList.children.length - 1; i >= 0; i--) {
-            tabsList.children.item(i).remove();
-        }
-
-        for (let i = 0; i < nums.length; i++) {
-            let active = false;
-            if (nums[i] === activeTab) active = true;
-            tabsList.appendChild(this.createTabElement(nums[i], active));
-        }
-
-        const firstTabElement = document.getElementById("input-tabs").firstElementChild;
-        const lastTabElement = document.getElementById("input-tabs").lastElementChild;
-
-        if (firstTabElement) {
-            if (tabsLeft) {
-                firstTabElement.style.boxShadow = "15px 0px 15px -15px var(--primary-border-colour) inset";
-            } else {
-                firstTabElement.style.boxShadow = "";
-            }
-        }
-        if (lastTabElement) {
-            if (tabsRight) {
-                lastTabElement.style.boxShadow = "-15px 0px 15px -15px var(--primary-border-colour) inset";
-            } else {
-                lastTabElement.style.boxShadow = "";
-            }
-        }
-
-        if (nums.length > 1) {
-            tabsList.parentElement.style.display = "block";
-
-            document.getElementById("input-wrapper").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
-            document.getElementById("input-highlighter").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
-            document.getElementById("input-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
-        } else {
-            tabsList.parentElement.style.display = "none";
-
-            document.getElementById("input-wrapper").style.height = "calc(100% - var(--title-height))";
-            document.getElementById("input-highlighter").style.height = "calc(100% - var(--title-height))";
-            document.getElementById("input-file").style.height = "calc(100% - var(--title-height))";
-        }
-
-        this.changeTab(activeTab, this.app.options.syncTabs);
-    }
-
     /**
      * Change to a different tab.
      *
@@ -1042,43 +947,25 @@ class InputWaiter {
      * @param {boolean} [changeOutput=false] - If true, also changes the output
      */
     changeTab(inputNum, changeOutput) {
-        const tabsList = document.getElementById("input-tabs");
-
-        this.manager.highlighter.removeHighlights();
-        getSelection().removeAllRanges();
-
-        let found = false;
-        let minNum = Number.MAX_SAFE_INTEGER;
-        for (let i = 0; i < tabsList.children.length; i++) {
-            const tabNum = parseInt(tabsList.children.item(i).getAttribute("inputNum"), 10);
-            if (tabNum === inputNum) {
-                tabsList.children.item(i).classList.add("active-input-tab");
-                found = true;
-            } else {
-                tabsList.children.item(i).classList.remove("active-input-tab");
-            }
-            if (tabNum < minNum) {
-                minNum = tabNum;
-            }
-        }
-        if (!found) {
-            let direction = "right";
-            if (inputNum < minNum) {
-                direction = "left";
-            }
+        if (this.manager.tabs.changeInputTab(inputNum)) {
             this.inputWorker.postMessage({
-                action: "refreshTabs",
+                action: "setInput",
                 data: {
                     inputNum: inputNum,
-                    direction: direction
+                    silent: true
                 }
             });
         } else {
+            const minNum = Math.min(...this.manager.tabs.getInputTabList());
+            let direction = "right";
+            if (inputNum < minNum) {
+                direction = "left";
+            }
             this.inputWorker.postMessage({
-                action: "setInput",
+                action: "refreshTabs",
                 data: {
                     inputNum: inputNum,
-                    silent: true
+                    direction: direction
                 }
             });
         }
@@ -1102,70 +989,6 @@ class InputWaiter {
         }
     }
 
-
-    /**
-     * Updates the tab header to display the new input content
-     */
-    updateTabHeader(headerData) {
-        const tabsList = document.getElementById("input-tabs");
-        const inputNum = headerData.inputNum;
-        let inputData = "New Tab";
-        if (headerData.input.length > 0) {
-            inputData = headerData.input.slice(0, 100);
-        }
-        for (let i = 0; i < tabsList.children.length; i++) {
-            if (tabsList.children.item(i).getAttribute("inputNum") === inputNum.toString()) {
-                tabsList.children.item(i).firstElementChild.innerText = `${inputNum}: ${inputData}`;
-                break;
-            }
-        }
-    }
-
-    /**
-     * Gets the number of the current active tab
-     *
-     * @returns {number}
-     */
-    getActiveTab() {
-        const activeTabs = document.getElementsByClassName("active-input-tab");
-        if (activeTabs.length > 0) {
-            const activeTab = activeTabs.item(0);
-            const tabNum = activeTab.getAttribute("inputNum");
-            return parseInt(tabNum, 10);
-        }
-        return -1;
-    }
-
-    /**
-     * Gets the li element for the tab of an input number
-     *
-     * @param {number} inputNum - The inputNum of the tab we're trying to find
-     * @returns {Element}
-     */
-    getTabItem(inputNum) {
-        const tabs = document.getElementById("input-tabs").children;
-        for (let i = 0; i < tabs.length; i++) {
-            if (parseInt(tabs.item(i).getAttribute("inputNum"), 10) === inputNum) {
-                return tabs.item(i);
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Gets a list of tab numbers for the currently open tabs
-     *
-     * @returns {number[]}
-     */
-    getTabList() {
-        const nums = [];
-        const tabs = document.getElementById("input-tabs").children;
-        for (let i = 0; i < tabs.length; i++) {
-            nums.push(parseInt(tabs.item(i).getAttribute("inputNum"), 10));
-        }
-        return nums;
-    }
-
     /**
      * Handler for clear all IO events.
      * Resets the input, output and info areas, and creates a new inputWorker
@@ -1206,7 +1029,7 @@ class InputWaiter {
      * Resets the input for the current tab
      */
     clearIoClick() {
-        const inputNum = this.getActiveTab();
+        const inputNum = this.manager.tabs.getActiveInputTab();
         if (inputNum === -1) return;
 
         this.manager.highlighter.removeHighlights();
@@ -1219,7 +1042,7 @@ class InputWaiter {
             input: ""
         });
 
-        this.updateTabHeader({inputNum: inputNum, input: ""});
+        this.manager.tabs.updateInputTabHeader(inputNum, "");
     }
 
     /**
@@ -1283,25 +1106,17 @@ class InputWaiter {
      * @param {boolean} [changeTab=true] - If true, changes to the new tab once it's been added
      */
     addTab(inputNum, changeTab = true) {
-        const tabsWrapper = document.getElementById("input-tabs");
-        const numTabs = tabsWrapper.children.length;
+        const tabsWrapper = document.getElementById("input-tabs"),
+            numTabs = tabsWrapper.children.length;
 
-        if (!this.getTabItem(inputNum) && numTabs < this.maxTabs) {
-            const newTab = this.createTabElement(inputNum, false);
+        if (!this.manager.tabs.getInputTabItem(inputNum) && numTabs < this.maxTabs) {
+            const newTab = this.manager.tabs.createInputTabElement(inputNum, changeTab);
             tabsWrapper.appendChild(newTab);
 
             if (numTabs > 0) {
-                tabsWrapper.parentElement.style.display = "block";
-
-                document.getElementById("input-wrapper").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
-                document.getElementById("input-highlighter").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
-                document.getElementById("input-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
+                this.manager.tabs.showTabBar();
             } else {
-                tabsWrapper.parentElement.style.display = "none";
-
-                document.getElementById("input-wrapper").style.height = "calc(100% - var(--title-height))";
-                document.getElementById("input-highlighter").style.height = "calc(100% - var(--title-height))";
-                document.getElementById("input-file").style.height = "calc(100% - var(--title-height))";
+                this.manager.tabs.hideTabBar();
             }
 
             this.inputWorker.postMessage({
@@ -1313,9 +1128,19 @@ class InputWaiter {
             document.getElementById("input-tabs").lastElementChild.style.boxShadow = "-15px 0px 15px -15px var(--primary-border-colour) inset";
         }
 
-        if (changeTab) {
-            this.changeTab(inputNum, true);
-        }
+        if (changeTab) this.changeTab(inputNum, false);
+    }
+
+    /**
+     * Refreshes the input tabs, and changes to activeTab
+     *
+     * @param {number[]} nums - The inputNums to be displayed as tabs
+     * @param {number} activeTab - The tab to change to
+     * @param {boolean} tabsLeft - True if there are input tabs to the left of the displayed tabs
+     * @param {boolean} tabsRight - True if there are input tabs to the right of the displayed tabs
+     */
+    refreshTabs(nums, activeTab, tabsLeft, tabsRight) {
+        this.manager.tabs.refreshInputTabs(nums, activeTab, tabsLeft, tabsRight);
     }
 
     /**
@@ -1326,7 +1151,7 @@ class InputWaiter {
      */
     removeInput(inputNum) {
         let refresh = false;
-        if (this.getTabItem(inputNum) !== null) {
+        if (this.manager.tabs.getInputTabItem(inputNum) !== null) {
             refresh = true;
         }
         this.inputWorker.postMessage({
@@ -1385,7 +1210,7 @@ class InputWaiter {
                 setTimeout(func.bind(this, [newTime]), newTime);
             }
         };
-        setTimeout(func.bind(this, [time]), time);
+        this.tabTimeout = setTimeout(func.bind(this, [time]), time);
     }
 
     /**
@@ -1402,7 +1227,7 @@ class InputWaiter {
                 setTimeout(func.bind(this, [newTime]), newTime);
             }
         };
-        setTimeout(func.bind(this, [time]), time);
+        this.tabTimeout = setTimeout(func.bind(this, [time]), time);
     }
 
     /**
@@ -1410,18 +1235,20 @@ class InputWaiter {
      */
     tabMouseUp() {
         this.mousedown = false;
+
+        clearTimeout(this.tabTimeout);
+        this.tabTimeout = null;
     }
 
     /**
      * Changes to the next (right) tab
      */
     changeTabRight() {
-        const activeTab = this.getActiveTab();
+        const activeTab = this.manager.tabs.getActiveInputTab();
         this.inputWorker.postMessage({
             action: "changeTabRight",
             data: {
-                activeTab: activeTab,
-                nums: this.getTabList()
+                activeTab: activeTab
             }
         });
     }
@@ -1430,12 +1257,11 @@ class InputWaiter {
      * Changes to the previous (left) tab
      */
     changeTabLeft() {
-        const activeTab = this.getActiveTab();
+        const activeTab = this.manager.tabs.getActiveInputTab();
         this.inputWorker.postMessage({
             action: "changeTabLeft",
             data: {
-                activeTab: activeTab,
-                nums: this.getTabList()
+                activeTab: activeTab
             }
         });
     }
@@ -1445,7 +1271,7 @@ class InputWaiter {
      */
     async goToTab() {
         const inputNums = await this.getInputNums();
-        let tabNum = window.prompt(`Enter tab number (${inputNums.min} - ${inputNums.max}):`, this.getActiveTab().toString());
+        let tabNum = window.prompt(`Enter tab number (${inputNums.min} - ${inputNums.max}):`, this.manager.tabs.getActiveInputTab().toString());
 
         if (tabNum === null) return;
         tabNum = parseInt(tabNum, 10);

+ 37 - 199
src/web/waiters/OutputWaiter.mjs

@@ -25,21 +25,19 @@ class OutputWaiter {
         this.manager = manager;
 
         this.outputs = {};
-        this.activeTab = -1;
-
         this.zipWorker = null;
-
-        this.maxTabs = 4; // Calculate this
+        this.maxTabs = 4;
+        this.tabTimeout = null;
     }
 
     /**
      * Calculates the maximum number of tabs to display
      */
     calcMaxTabs() {
-        const numTabs = Math.floor((document.getElementById("IO").offsetWidth - 75)  / 120);
+        const numTabs = this.manager.tabs.calcMaxTabs();
         if (numTabs !== this.maxTabs) {
             this.maxTabs = numTabs;
-            this.refreshTabs(this.getActiveTab());
+            this.refreshTabs(this.manager.tabs.getActiveOutputTab());
         }
     }
 
@@ -97,7 +95,7 @@ class OutputWaiter {
      * @returns {string | ArrayBuffer}
      */
     getActive(raw=true) {
-        return this.getOutput(this.getActiveTab(), raw);
+        return this.getOutput(this.manager.tabs.getActiveOutputTab(), raw);
     }
 
     /**
@@ -249,7 +247,7 @@ class OutputWaiter {
             if (output === undefined || output === null) return;
             if (typeof inputNum !== "number") inputNum = parseInt(inputNum, 10);
 
-            if (inputNum !== this.getActiveTab()) return;
+            if (inputNum !== this.manager.tabs.getActiveOutputTab()) return;
 
             this.toggleLoader(true);
 
@@ -636,33 +634,10 @@ class OutputWaiter {
         const tabsWrapper = document.getElementById("output-tabs");
         const numTabs = tabsWrapper.children.length;
 
-        if (this.getTabItem(inputNum) === undefined && numTabs < this.maxTabs) {
+        if (!this.manager.tabs.getOutputTabItem(inputNum) && numTabs < this.maxTabs) {
             // Create a new tab element
-            const newTab = this.createTabElement(inputNum);
-
+            const newTab = this.manager.tabs.createOutputTabElement(inputNum, changeTab);
             tabsWrapper.appendChild(newTab);
-
-            if (numTabs > 0) {
-                tabsWrapper.parentElement.style.display = "block";
-
-                document.getElementById("output-wrapper").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
-                document.getElementById("output-highlighter").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
-                document.getElementById("output-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
-                document.getElementById("output-loader").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
-                document.getElementById("show-file-overlay").style.top = "calc(var(--tab-height) + var(--title-height) + 10px)";
-
-                document.getElementById("save-all-to-file").style.display = "inline-block";
-            } else {
-                tabsWrapper.parentElement.style.display = "none";
-
-                document.getElementById("output-wrapper").style.height = "calc(100% - var(--title-height))";
-                document.getElementById("output-highlighter").style.height = "calc(100% - var(--title-height))";
-                document.getElementById("output-file").style.height = "calc(100% - var(--title-height))";
-                document.getElementById("output-loader").style.height = "calc(100% - var(--title-height))";
-                document.getElementById("show-file-overlay").style.top = "calc(var(--title-height) + 10px)";
-
-                document.getElementById("save-all-to-file").style.display = "none";
-            }
         } else if (numTabs === this.maxTabs) {
             // Can't create a new tab
             document.getElementById("output-tabs").lastElementChild.style.boxShadow = "-15px 0px 15px -15px var(--primary-border-colour) inset";
@@ -681,63 +656,24 @@ class OutputWaiter {
      */
     changeTab(inputNum, changeInput = false) {
         if (!this.outputExists(inputNum)) return;
-        const currentNum = this.getActiveTab();
+        const currentNum = this.manager.tabs.getActiveOutputTab();
 
         this.hideMagicButton();
 
         this.manager.highlighter.removeHighlights();
         getSelection().removeAllRanges();
 
-        const tabsWrapper = document.getElementById("output-tabs");
-        const tabs = tabsWrapper.children;
-
-        let found = false;
-        for (let i = 0; i < tabs.length; i++) {
-            if (tabs.item(i).getAttribute("inputNum") === inputNum.toString()) {
-                tabs.item(i).classList.add("active-output-tab");
-                this.activeTab = inputNum;
-                found = true;
-            } else {
-                tabs.item(i).classList.remove("active-output-tab");
-            }
-        }
-        if (!found) {
+        if (!this.manager.tabs.changeOutputTab(inputNum)) {
             let direction = "right";
             if (currentNum > inputNum) {
                 direction = "left";
             }
-
             const newOutputs = this.getNearbyNums(inputNum, direction);
 
             const tabsLeft = (newOutputs[0] !== this.getSmallestInputNum());
             const tabsRight = (newOutputs[newOutputs.length - 1] !== this.getLargestInputNum());
 
-            const firstTabElement = document.getElementById("output-tabs").firstElementChild;
-            const lastTabElement = document.getElementById("output-tabs").lastElementChild;
-
-            if (firstTabElement) {
-                if (tabsLeft) {
-                    firstTabElement.style.boxShadow = "15px 0px 15px -15px var(--primary-border-colour) inset";
-                } else {
-                    firstTabElement.style.boxShadow = "";
-                }
-            }
-            if (lastTabElement) {
-                if (tabsRight) {
-                    lastTabElement.style.boxShadow = "-15px 0px 15px -15px var(--primary-border-colour) inset";
-                } else {
-                    lastTabElement.style.boxShadow = "";
-                }
-            }
-
-            for (let i = 0; i < newOutputs.length; i++) {
-                tabs.item(i).setAttribute("inputNum", newOutputs[i].toString());
-                this.displayTabInfo(newOutputs[i]);
-                if (newOutputs[i] === inputNum) {
-                    this.activeTab = inputNum;
-                    tabs.item(i).classList.add("active-output-tab");
-                }
-            }
+            this.manager.tabs.refreshOutputTabs(newOutputs, inputNum, tabsLeft, tabsRight);
         }
 
         this.app.debounce(this.set, 50, "setOutput", this, [inputNum])();
@@ -792,7 +728,7 @@ class OutputWaiter {
                 setTimeout(func.bind(this, [newTime]), newTime);
             }
         };
-        setTimeout(func.bind(this, [time]), time);
+        this.tabTimeout = setTimeout(func.bind(this, [time]), time);
     }
 
     /**
@@ -809,7 +745,7 @@ class OutputWaiter {
                 setTimeout(func.bind(this, [newTime]), newTime);
             }
         };
-        setTimeout(func.bind(this, [time]), time);
+        this.tabTimeout = setTimeout(func.bind(this, [time]), time);
     }
 
     /**
@@ -817,13 +753,16 @@ class OutputWaiter {
      */
     tabMouseUp() {
         this.mousedown = false;
+
+        clearTimeout(this.tabTimeout);
+        this.tabTimeout = null;
     }
 
     /**
      * Handler for changing to the left tab
      */
     changeTabLeft() {
-        const currentTab = this.getActiveTab();
+        const currentTab = this.manager.tabs.getActiveOutputTab();
         this.changeTab(this.getPreviousInputNum(currentTab), this.app.options.syncTabs);
     }
 
@@ -831,7 +770,7 @@ class OutputWaiter {
      * Handler for changing to the right tab
      */
     changeTabRight() {
-        const currentTab = this.getActiveTab();
+        const currentTab = this.manager.tabs.getActiveOutputTab();
         this.changeTab(this.getNextInputNum(currentTab), this.app.options.syncTabs);
     }
 
@@ -842,7 +781,7 @@ class OutputWaiter {
         const min = this.getSmallestInputNum(),
             max = this.getLargestInputNum();
 
-        let tabNum = window.prompt(`Enter tab number (${min} - ${max}):`, this.getActiveTab().toString());
+        let tabNum = window.prompt(`Enter tab number (${min} - ${max}):`, this.manager.tabs.getActiveOutputTab().toString());
         if (tabNum === null) return;
         tabNum = parseInt(tabNum, 10);
 
@@ -960,9 +899,9 @@ class OutputWaiter {
      */
     removeTab(inputNum) {
         if (!this.outputExists(inputNum)) return;
-        let activeTab = this.getActiveTab();
+        let activeTab = this.manager.tabs.getActiveOutputTab();
 
-        const tabElement = this.getTabItem(inputNum);
+        const tabElement = this.manager.tabs.getOutputTabItem(inputNum);
 
         this.removeOutput(inputNum);
 
@@ -970,7 +909,7 @@ class OutputWaiter {
             // find new tab number?
             if (inputNum === activeTab) {
                 activeTab = this.getPreviousInputNum(activeTab);
-                if (activeTab === this.getActiveTab()) {
+                if (activeTab === this.manager.tabs.getActiveOutputTab()) {
                     activeTab = this.getNextInputNum(activeTab);
                 }
             }
@@ -983,109 +922,15 @@ class OutputWaiter {
      * @param {number} activeTab
      */
     refreshTabs(activeTab) {
-        const tabsList = document.getElementById("output-tabs");
-        let newInputs = this.getNearbyNums(activeTab, "right");
-        if (newInputs.length < this.maxTabs) {
-            newInputs = this.getNearbyNums(activeTab, "left");
-        }
-
-        for (let i = tabsList.children.length - 1; i >= 0; i--) {
-            tabsList.children.item(i).remove();
-        }
-
-        for (let i = 0; i < newInputs.length; i++) {
-            tabsList.appendChild(this.createTabElement(newInputs[i]));
-            this.displayTabInfo(newInputs[i]);
-        }
-
-        const tabsLeft = (newInputs[0] !== this.getSmallestInputNum());
-        const tabsRight = (newInputs[newInputs.length - 1] !== this.getLargestInputNum());
-
-        const firstTabElement = document.getElementById("output-tabs").firstElementChild;
-        const lastTabElement = document.getElementById("output-tabs").lastElementChild;
-
-        if (firstTabElement) {
-            if (tabsLeft) {
-                firstTabElement.style.boxShadow = "15px 0px 15px -15px var(--primary-border-colour) inset";
-            } else {
-                firstTabElement.style.boxShadow = "";
-            }
-        }
-        if (lastTabElement) {
-            if (tabsRight) {
-                lastTabElement.style.boxShadow = "-15px 0px 15px -15px var(--primary-border-colour) inset";
-            } else {
-                lastTabElement.style.boxShadow = "";
-            }
-        }
-
-        if (newInputs.length > 1) {
-            tabsList.parentElement.style.display = "block";
-
-            document.getElementById("output-wrapper").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
-            document.getElementById("output-highlighter").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
-            document.getElementById("output-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
-            document.getElementById("output-loader").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
-            document.getElementById("show-file-overlay").style.top = "calc(var(--tab-height) + var(--title-height) + 10px)";
-
-            document.getElementById("save-all-to-file").style.display = "inline-block";
-
-        } else {
-            tabsList.parentElement.style.display = "none";
-
-            document.getElementById("output-wrapper").style.height = "calc(100% - var(--title-height))";
-            document.getElementById("output-highlighter").style.height = "calc(100% - var(--title-height))";
-            document.getElementById("output-file").style.height = "calc(100% - var(--title-height))";
-            document.getElementById("output-loader").style.height = "calc(100% - var(--title-height))";
-            document.getElementById("show-file-overlay").style.top = "calc(var(--title-height) + 10px)";
-
-            document.getElementById("save-all-to-file").style.display = "none";
-        }
-
-        this.changeTab(activeTab);
-
-    }
-
-    /**
-     * Creates a new tab element to be added to the tab bar
-     *
-     * @param {number} inputNum
-     */
-    createTabElement(inputNum) {
-        const newTab = document.createElement("li");
-        newTab.setAttribute("inputNum", inputNum.toString());
-
-        const newTabContent = document.createElement("div");
-        newTabContent.classList.add("output-tab-content");
-        newTabContent.innerText = `Tab ${inputNum.toString()}`;
-
-        // Do we want remove tab button on output?
-        newTab.appendChild(newTabContent);
-
-        return newTab;
-    }
-
-    /**
-     * Gets the number of the current active tab
-     *
-     * @returns {number}
-     */
-    getActiveTab() {
-        return this.activeTab;
-    }
-
-    /**
-     * Gets the li element for a tab
-     *
-     * @param {number} inputNum
-     */
-    getTabItem(inputNum) {
-        const tabs = document.getElementById("output-tabs").children;
-        for (let i = 0; i < tabs.length; i++) {
-            if (parseInt(tabs.item(i).getAttribute("inputNum"), 10) === inputNum) {
-                return tabs.item(i);
-            }
+        const minNum = Math.min(this.manager.tabs.getOutputTabList());
+        let direction = "right";
+        if (activeTab < minNum) {
+            direction = "left";
         }
+        const newNums = this.getNearbyNums(activeTab, direction),
+            tabsLeft = (newNums[0] !== this.getSmallestInputNum()),
+            tabsRight = (newNums[newNums.length] !== this.getLargestInputNum());
+        this.manager.tabs.refreshOutputTabs(newNums, activeTab, tabsLeft, tabsRight);
     }
 
     /**
@@ -1094,7 +939,7 @@ class OutputWaiter {
      * @param {number} inputNum
      */
     displayTabInfo(inputNum) {
-        const tabItem = this.getTabItem(inputNum);
+        const tabItem = this.manager.tabs.getOutputTabItem(inputNum);
 
         if (!tabItem) return;
 
@@ -1137,7 +982,7 @@ class OutputWaiter {
     async backgroundMagic() {
         this.hideMagicButton();
         if (!this.app.options.autoMagic || !this.getActive(true)) return;
-        const dish = this.outputs[this.getActiveTab()].data.dish;
+        const dish = this.outputs[this.manager.tabs.getActiveOutputTab()].data.dish;
         const buffer = await this.getDishBuffer(dish);
         const sample = buffer.slice(0, 1000) || "";
 
@@ -1219,7 +1064,7 @@ class OutputWaiter {
             sliceToEl = document.getElementById("output-file-slice-to"),
             sliceFrom = parseInt(sliceFromEl.value, 10),
             sliceTo = parseInt(sliceToEl.value, 10),
-            output = this.outputs[this.getActiveTab()].data;
+            output = this.outputs[this.manager.tabs.getActiveOutputTab()].data;
 
         let str;
         if (output.type === "ArrayBuffer") {
@@ -1252,7 +1097,7 @@ class OutputWaiter {
 
         document.getElementById("output-text").classList.add("blur");
         showFileOverlay.style.display = "none";
-        this.set(this.getActiveTab());
+        this.set(this.manager.tabs.getActiveOutputTab());
     }
 
     /**
@@ -1330,22 +1175,15 @@ class OutputWaiter {
      */
     switchClick() {
         const active = this.getActive(true);
+        const transferable = (typeof active === "string" ? undefined : [active]);
         if (typeof active === "string") {
             this.manager.input.inputWorker.postMessage({
                 action: "inputSwitch",
                 data: {
-                    inputNum: this.manager.input.getActiveTab(),
-                    outputData: active
-                }
-            });
-        } else {
-            this.manager.input.inputWorker.postMessage({
-                action: "inputSwitch",
-                data: {
-                    inputNum: this.manager.input.getActiveTab(),
+                    inputNum: this.manager.tabs.getActiveInputTab(),
                     outputData: active
                 }
-            }, [active]);
+            }, transferable);
         }
     }
 

+ 407 - 0
src/web/waiters/TabWaiter.mjs

@@ -0,0 +1,407 @@
+/**
+ * @author j433866 [j433866@gmail.com]
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+
+/**
+ * Waiter to handle events related to the input and output tabs
+ */
+class TabWaiter {
+
+    /**
+     * TabWaiter constructor.
+     *
+     * @param {App} app - The main view object for CyberChef.
+     * @param {Manager} manager - The CyberChef event manager
+     */
+    constructor(app, manager) {
+        this.app = app;
+        this.manager = manager;
+    }
+
+    /**
+     * Calculates the maximum number of tabs to display
+     *
+     * @returns {number}
+     */
+    calcMaxTabs() {
+        let numTabs = Math.floor((document.getElementById("IO").offsetWidth - 75)  / 120);
+        numTabs = (numTabs > 1) ? numTabs : 2;
+
+        return numTabs;
+    }
+
+    /**
+     * Gets the currently active input or active tab number
+     *
+     * @param {string} io - Either "input" or "output"
+     * @returns {number} - The currently active tab or -1
+     */
+    getActiveTab(io) {
+        const activeTabs = document.getElementsByClassName(`active-${io}-tab`);
+        if (activeTabs.length > 0) {
+            if (!activeTabs.item(0).hasAttribute("inputNum")) return -1;
+            const tabNum = activeTabs.item(0).getAttribute("inputNum");
+            return parseInt(tabNum, 10);
+        }
+        return -1;
+    }
+
+    /**
+     * Gets the currently active input tab number
+     *
+     * @returns {number}
+     */
+    getActiveInputTab() {
+        return this.getActiveTab("input");
+    }
+
+    /**
+     * Gets the currently active output tab number
+     *
+     * @returns {number}
+     */
+    getActiveOutputTab() {
+        return this.getActiveTab("output");
+    }
+
+    /**
+     * Gets the li element for the tab of a given input number
+     *
+     * @param {number} inputNum - The inputNum of the tab we're trying to get
+     * @param {string} io - Either "input" or "output"
+     * @returns {Element}
+     */
+    getTabItem(inputNum, io) {
+        const tabs = document.getElementById(`${io}-tabs`).children;
+        for (let i = 0; i < tabs.length; i++) {
+            if (parseInt(tabs.item(i).getAttribute("inputNum"), 10) === inputNum) {
+                return tabs.item(i);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Gets the li element for an input tab of the given input number
+     *
+     * @param {inputNum} - The inputNum of the tab we're trying to get
+     * @returns {Element}
+     */
+    getInputTabItem(inputNum) {
+        return this.getTabItem(inputNum, "input");
+    }
+
+    /**
+     * Gets the li element for an output tab of the given input number
+     *
+     * @param {number} inputNum
+     * @returns {Element}
+     */
+    getOutputTabItem(inputNum) {
+        return this.getTabItem(inputNum, "output");
+    }
+
+    /**
+     * Gets a list of tab numbers for the currently displayed tabs
+     *
+     * @param {string} io - Either "input" or "output"
+     * @returns {number[]}
+     */
+    getTabList(io) {
+        const nums = [],
+            tabs = document.getElementById(`${io}-tabs`).children;
+
+        for (let i = 0; i < tabs.length; i++) {
+            nums.push(parseInt(tabs.item(i).getAttribute("inputNum"), 10));
+        }
+
+        return nums;
+    }
+
+    /**
+     * Gets a list of tab numbers for the currently displayed input tabs
+     *
+     * @returns {number[]}
+     */
+    getInputTabList() {
+        return this.getTabList("input");
+    }
+
+    /**
+     * Gets a list of tab numbers for the currently displayed output tabs
+     *
+     * @returns {number[]}
+     */
+    getOutputTabList() {
+        return this.getTabList("output");
+    }
+
+    /**
+     * Creates a new tab element for the tab bar
+     *
+     * @param {number} inputNum - The inputNum of the new tab
+     * @param {boolean} active - If true, sets the tab to active
+     * @param {string} io - Either "input" or "output"
+     * @returns {Element}
+     */
+    createTabElement(inputNum, active, io) {
+        const newTab = document.createElement("li");
+        newTab.setAttribute("inputNum", inputNum.toString());
+
+        if (active) newTab.classList.add(`active-${io}-tab`);
+
+        const newTabContent = document.createElement("div");
+        newTabContent.classList.add(`${io}-tab-content`);
+
+        newTabContent.innerText = `Tab ${inputNum.toString()}`;
+
+        newTab.appendChild(newTabContent);
+
+        if (io === "input") {
+            const newTabButton = document.createElement("button");
+            newTabButton.type = "button";
+            newTabButton.className = "btn btn-primary bmd-btn-icon btn-close-tab";
+
+            const newTabButtonIcon = document.createElement("i");
+            newTabButtonIcon.classList.add("material-icons");
+            newTabButtonIcon.innerText = "clear";
+
+            newTabButton.appendChild(newTabButtonIcon);
+
+            newTabButton.addEventListener("click", this.manager.input.removeTabClick.bind(this.manager.input));
+
+            newTab.appendChild(newTabButton);
+        }
+
+        return newTab;
+    }
+
+    /**
+     * Creates a new tab element for the input tab bar
+     *
+     * @param {number} inputNum - The inputNum of the new input tab
+     * @param {boolean} [active=false] - If true, sets the tab to active
+     * @returns {Element}
+     */
+    createInputTabElement(inputNum, active=false) {
+        return this.createTabElement(inputNum, active, "input");
+    }
+
+    /**
+     * Creates a new tab element for the output tab bar
+     *
+     * @param {number} inputNum - The inputNum of the new output tab
+     * @param {boolean} [active=false] - If true, sets the tab to active
+     * @returns {Element}
+     */
+    createOutputTabElement(inputNum, active=false) {
+        return this.createTabElement(inputNum, active, "output");
+    }
+
+    /**
+     * Displays the tab bar for both the input and output
+     */
+    showTabBar() {
+        document.getElementById("input-tabs-wrapper").style.display = "block";
+        document.getElementById("output-tabs-wrapper").style.display = "block";
+
+        document.getElementById("input-wrapper").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
+        document.getElementById("input-highlighter").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
+        document.getElementById("input-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
+
+        document.getElementById("output-wrapper").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
+        document.getElementById("output-highlighter").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
+        document.getElementById("output-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
+        document.getElementById("output-loader").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
+        document.getElementById("show-file-overlay").style.top = "calc(var(--tab-height) + var(--title-height) + 10px)";
+
+        document.getElementById("save-all-to-file").style.display = "inline-block";
+    }
+
+    /**
+     * Hides the tab bar for both the input and output
+     */
+    hideTabBar() {
+        document.getElementById("input-tabs-wrapper").style.display = "none";
+        document.getElementById("output-tabs-wrapper").style.display = "none";
+
+        document.getElementById("input-wrapper").style.height = "calc(100% - var(--title-height))";
+        document.getElementById("input-highlighter").style.height = "calc(100% - var(--title-height))";
+        document.getElementById("input-file").style.height = "calc(100% - var(--title-height))";
+
+        document.getElementById("output-wrapper").style.height = "calc(100% - var(--title-height))";
+        document.getElementById("output-highlighter").style.height = "calc(100% - var(--title-height))";
+        document.getElementById("output-file").style.height = "calc(100% - var(--title-height))";
+        document.getElementById("output-loader").style.height = "calc(100% - var(--title-height))";
+        document.getElementById("show-file-overlay").style.top = "calc(var(--title-height) + 10px)";
+
+        document.getElementById("save-all-to-file").style.display = "none";
+    }
+
+    /**
+     * Redraws the tab bar with an updated list of tabs, then changes to activeTab
+     *
+     * @param {number[]} nums - The inputNums of the tab bar to be drawn
+     * @param {number} activeTab - The inputNum of the activeTab
+     * @param {boolean} tabsLeft - True if there are tabs to the left of the displayed tabs
+     * @param {boolean} tabsRight - True if there are tabs to the right of the displayed tabs
+     * @param {string} io - Either "input" or "output"
+     */
+    refreshTabs(nums, activeTab, tabsLeft, tabsRight, io) {
+        const tabsList = document.getElementById(`${io}-tabs`);
+
+        // Remove existing tab elements
+        for (let i = tabsList.children.length - 1; i >= 0; i--) {
+            tabsList.children.item(i).remove();
+        }
+
+        // Create and add new tab elements
+        for (let i = 0; i < nums.length; i++) {
+            const active = (nums[i] === activeTab);
+            tabsList.appendChild(this.createTabElement(nums[i], active, io));
+        }
+
+        const firstTab = tabsList.firstElementChild,
+            lastTab = tabsList.lastElementChild;
+
+        // Display shadows if there are tabs left / right of the displayed tabs
+        if (firstTab) {
+            if (tabsLeft) {
+                firstTab.style.boxShadow = "15px 0px 15px -15px var(--primary-border-colour) inset";
+            } else {
+                firstTab.style.boxShadow = "";
+            }
+        }
+        if (lastTab) {
+            if (tabsRight) {
+                lastTab.style.boxShadow = "-15px 0px 15px -15px var(--primary-border-colour) inset";
+            } else {
+                lastTab.style.boxShadow = "";
+            }
+        }
+
+        // Show or hide the tab bar depending on how many tabs we have
+        if (nums.length > 1) {
+            this.showTabBar();
+        } else {
+            this.hideTabBar();
+        }
+
+        this.changeTab(activeTab, io);
+    }
+
+    /**
+     * Refreshes the input tabs, and changes to activeTab
+     *
+     * @param {number[]} nums - The inputNums to be displayed as tabs
+     * @param {number} activeTab - The tab to change to
+     * @param {boolean} tabsLeft - True if there are input tabs to the left of the displayed tabs
+     * @param {boolean} tabsRight - True if there are input tabs to the right of the displayed tabs
+     */
+    refreshInputTabs(nums, activeTab, tabsLeft, tabsRight) {
+        this.refreshTabs(nums, activeTab, tabsLeft, tabsRight, "input");
+    }
+
+    /**
+     * Refreshes the output tabs, and changes to activeTab
+     *
+     * @param {number[]} nums - The inputNums to be displayed as tabs
+     * @param {number} activeTab - The tab to change to
+     * @param {boolean} tabsLeft - True if there are output tabs to the left of the displayed tabs
+     * @param {boolean} tabsRight - True if there are output tabs to the right of the displayed tabs
+     */
+    refreshOutputTabs(nums, activeTab, tabsLeft, tabsRight) {
+        this.refreshTabs(nums, activeTab, tabsLeft, tabsRight, "output");
+    }
+
+    /**
+     * Changes the active tab to a different tab
+     *
+     * @param {number} inputNum - The inputNum of the tab to change to
+     * @param {string} io - Either "input" or "output"
+     * @return {boolean} - False if the tab is not currently being displayed
+     */
+    changeTab(inputNum, io) {
+        const tabsList = document.getElementById(`${io}-tabs`);
+
+        this.manager.highlighter.removeHighlights();
+        getSelection().removeAllRanges();
+
+        let found = false;
+        for (let i = 0; i < tabsList.children.length; i++) {
+            const tabNum = parseInt(tabsList.children.item(i).getAttribute("inputNum"), 10);
+            if (tabNum === inputNum) {
+                tabsList.children.item(i).classList.add(`active-${io}-tab`);
+                found = true;
+            } else {
+                tabsList.children.item(i).classList.remove(`active-${io}-tab`);
+            }
+        }
+
+        return found;
+    }
+
+    /**
+     * Changes the active input tab to a different tab
+     *
+     * @param {number} inputNum
+     * @returns {boolean} - False if the tab is not currently being displayed
+     */
+    changeInputTab(inputNum) {
+        return this.changeTab(inputNum, "input");
+    }
+
+    /**
+     * Changes the active output tab to a different tab
+     *
+     * @param {number} inputNum
+     * @returns {boolean} - False if the tab is not currently being displayed
+     */
+    changeOutputTab(inputNum) {
+        return this.changeTab(inputNum, "output");
+    }
+
+    /**
+     * Updates the tab header to display a preview of the tab contents
+     *
+     * @param {number} inputNum - The inputNum of the tab to update the header of
+     * @param {string} data - The data to display in the tab header
+     * @param {string} io - Either "input" or "output"
+     */
+    updateTabHeader(inputNum, data, io) {
+        const tab = this.getTabItem(inputNum, io);
+        if (tab === null) return;
+
+        let headerData = `Tab ${inputNum}`;
+        if (data.length > 0) {
+            headerData = data.slice(0, 100);
+            headerData = `${inputNum}: ${headerData}`;
+        }
+        tab.firstElementChild.innerText = headerData;
+    }
+
+    /**
+     * Updates the input tab header to display a preview of the tab contents
+     *
+     * @param {number} inputNum - The inputNum of the tab to update the header of
+     * @param {string} data - The data to display in the tab header
+     */
+    updateInputTabHeader(inputNum, data) {
+        this.updateTabHeader(inputNum, data, "input");
+    }
+
+    /**
+     * Updates the output tab header to display a preview of the tab contents
+     *
+     * @param {number} inputNum - The inputNum of the tab to update the header of
+     * @param {string} data - The data to display in the tab header
+     */
+    updateOutputTabHeader(inputNum, data) {
+        this.updateTabHeader(inputNum, data, "output");
+    }
+
+}
+
+export default TabWaiter;

+ 1 - 1
src/web/waiters/WorkerWaiter.mjs

@@ -325,7 +325,7 @@ class WorkerWaiter {
         this.inputNums = [];
         this.totalOutputs = 0;
         this.loadingOutputs = 0;
-        if (!silent) this.manager.output.set(this.manager.output.getActiveTab());
+        if (!silent) this.manager.output.set(this.manager.tabs.getActiveOutputTab());
     }
 
     /**

+ 12 - 24
src/web/workers/InputWorker.mjs

@@ -79,10 +79,10 @@ self.addEventListener("message", function(e) {
             self.removeInput(r.data);
             break;
         case "changeTabRight":
-            self.changeTabRight(r.data.activeTab, r.data.nums);
+            self.changeTabRight(r.data.activeTab);
             break;
         case "changeTabLeft":
-            self.changeTabLeft(r.data.activeTab, r.data.nums);
+            self.changeTabLeft(r.data.activeTab);
             break;
         case "autobake":
             self.autoBake(r.data.activeTab, 0, false);
@@ -887,38 +887,26 @@ self.removeInput = function(removeInputData) {
  * Change to the next tab.
  *
  * @param {number} inputNum - The inputNum of the tab to change to
- * @param {number[]} tabNums - List of the currently displayed tabs
  */
-self.changeTabRight = function(inputNum, tabNums) {
+self.changeTabRight = function(inputNum) {
     const newInput = self.getNextInputNum(inputNum);
-    if (tabNums.includes(newInput)) {
-        self.postMessage({
-            action: "changeTab",
-            data: newInput
-        });
-    } else {
-        // If the tab is not displayed, refresh the tabs to display it
-        self.refreshTabs(newInput, "right");
-    }
+    self.postMessage({
+        action: "changeTab",
+        data: newInput
+    });
 };
 
 /**
  * Change to the previous tab.
  *
  * @param {number} inputNum - The inputNum of the tab to change to
- * @param {number[]} tabNums - List of the currently displayed tabs
  */
-self.changeTabLeft = function(inputNum, tabNums) {
+self.changeTabLeft = function(inputNum) {
     const newInput = self.getPreviousInputNum(inputNum);
-    if (tabNums.includes(newInput)) {
-        self.postMessage({
-            action: "changeTab",
-            data: newInput
-        });
-    } else {
-        // If the tab is not displayed, refresh the tabs to display it
-        self.refreshTabs(newInput, "left");
-    }
+    self.postMessage({
+        action: "changeTab",
+        data: newInput
+    });
 };
 
 /**