Source: cpanel.js

/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2025 Rainbownapkin and the TTN Community

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.*/

/**
 * Class containing code for managing the Canopy Panel UX
 */
class cPanel{
    /**
     * Instantiates a new Canopy Panel Management object
     * @param {channel} client - Parent client Management Object
     */
    constructor(client){
        /**
         * Parent Client Management object
         */
        this.client = client;

        /**
         * Active Panel Object
         */
        this.activePanel = null;

        /**
         * Pinned Panel Object
         */
        this.pinnedPanel = null;

        /**
         * Popped Panel Objects
         */
        this.poppedPanels = [];

        /**
         * Click-Dragger object for re-sizable active panel
         */
        this.activePanelDragger = new canopyUXUtils.clickDragger("#cpanel-active-drag-handle", "#cpanel-active-div", false, null, false);

        /**
         * Click-Dragger object for re-sizable pinned panel
         */
        this.pinnedPanelDragger = new canopyUXUtils.clickDragger("#cpanel-pinned-drag-handle", "#cpanel-pinned-div", false, this.client.chatBox.clickDragger);

        //Element Nodes
        //Active Panel
        /**
         * Active Panel Container
         */
        this.activePanelDiv = document.querySelector("#cpanel-active-div");

        /**
         * Active Panel Title
         */
        this.activePanelTitle = document.querySelector("#cpanel-active-title");

        /**
         * Active Title Document Div
         */
        this.activePanelDoc = document.querySelector("#cpanel-active-doc");

        /**
         * Active Panel Pin Icon
         */
        this.activePanelPinIcon = document.querySelector("#cpanel-active-pin-icon");

        /**
         * Active Panel Pop-Out Icon
         */
        this.activePanelPopoutIcon = document.querySelector("#cpanel-active-popout-icon");

        /**
         * Active Panel Close Icon
         */
        this.activePanelCloseIcon = document.querySelector("#cpanel-active-close-icon");

        //Pinned Panel
        /**
         * Pinned Panel Contianer
         */
        this.pinnedPanelDiv = document.querySelector("#cpanel-pinned-div");

        /**
         * Pinned Panel Title
         */
        this.pinnedPanelTitle = document.querySelector("#cpanel-pinned-title");

        /**
         * Pinned Panel Document Div
         */
        this.pinnedPanelDoc = document.querySelector("#cpanel-pinned-doc");

        /**
         * Pinned Panel Un-Pin Icon
         */
        this.pinnedPanelUnpinIcon = document.querySelector("#cpanel-pinned-unpin-icon");

        /**
         * Pinned Panel Pop-Out Icon
         */
        this.pinnedPanelPopoutIcon = document.querySelector("#cpanel-pinned-popout-icon");

        /**
         * Pinned Panel Close Icon
         */
        this.pinnedPanelCloseIcon = document.querySelector("#cpanel-pinned-close-icon");

        this.setupInput();
    }

    /**
     * Defines input-related event listeners
     */
    setupInput(){
        this.activePanelCloseIcon.addEventListener("click", this.hideActivePanel.bind(this));
        this.activePanelPinIcon.addEventListener("click", this.pinPanel.bind(this));
        this.activePanelPopoutIcon.addEventListener("click", this.popActivePanel.bind(this));
        this.pinnedPanelCloseIcon.addEventListener("click", this.hidePinnedPanel.bind(this));
        this.pinnedPanelUnpinIcon.addEventListener("click", this.unpinPanel.bind(this));
        this.pinnedPanelPopoutIcon.addEventListener("click", this.popPinnedPanel.bind(this));
    }

    /**
     * Sets Active Panel
     * @param {panelObj} panel - Panel Object to set as active
     * @param {String} panelBody - innerHTML of Panel, pulls from panelObj.getPage() if empty
     */
    async setActivePanel(panel, panelBody){
        //Set active panel
        this.activePanel = panel;

        //Grab panel hypertext content and load it into div
        this.activePanelDoc.innerHTML = (panelBody == null || panelBody == "") ? await this.activePanel.getPage() : panelBody;
          

        //Display panel
        this.activePanelDiv.style.display = "flex";
        this.activePanelTitle.textContent = this.activePanel.name;

        //Call panel initialization function
        this.activePanel.panelDocument = this.activePanelDoc;
        this.activePanel.docSwitch();
    }

    /**
     * Hides active panel
     * @param {Event} event - Event passed down from Input Handler
     * @param {Boolean} keepAlive - Prevents closing panel if true
     */
    hideActivePanel(event, keepAlive = false){
        if(!keepAlive){
            this.activePanel.closer();
        }

        //Hide the panel
        this.activePanelDiv.style.display = "none";
        //Clear out the panel
        this.activePanelDoc.innerHTML = '';
        //Set active panel to null
        this.activePanel = null;
    }

    /**
     * Pins active panel
     */
    pinPanel(){
        this.setPinnedPanel(this.activePanel, this.activePanelDoc.innerHTML);
        this.hideActivePanel(null, true);
    }

    /**
     * Pop's out active panel
     */
    popActivePanel(){
        this.popPanel(this.activePanel, this.activePanelDoc.innerHTML);
        this.hideActivePanel(null, true);
    }

    /**
     * Sets pinned panel
     * @param {panelObj} panel - Panel Object to apply to panel
     * @param {String} panelBody - Raw HTML to inject into panel body, defaults to panel page if null
     */
    async setPinnedPanel(panel, panelBody){
        //Set pinned panel
        this.pinnedPanel = panel;

        //Set Title
        this.pinnedPanelTitle.textContent = this.pinnedPanel.name;

        //Grab panel hypertext content and load it into div
        this.pinnedPanelDoc.innerHTML = (panelBody == null || panelBody == "") ? await this.pinnedPanel.getPage() : panelBody;

        //Display panel
        this.pinnedPanelDiv.style.display = "flex";

        //Call panel initialization function
        this.pinnedPanel.panelDocument = this.pinnedPanelDoc;
        this.pinnedPanel.docSwitch();

        //Resize to window/content
        this.pinnedPanelDragger.fixCutoff();
    }

    /**
     * Hides pinned panel
     * @param {Event} event - Passed down input event
     * @param {Boolean} keepAlive - Prevents panel.closer() from running if true
     */
    hidePinnedPanel(event, keepAlive = false){
        this.pinnedPanelDiv.style.display = "none";

        if(!keepAlive){
            this.pinnedPanel.closer();
        }

        this.pinnedPanel = null;
    }

    /**
     * Sets pinned panel to active
     */
    unpinPanel(){
        this.setActivePanel(this.pinnedPanel, this.pinnedPanelDoc.innerHTML);
        this.hidePinnedPanel(null, true);
    }

    /**
     * Pops pinned panel
     */
    popPinnedPanel(){
        this.popPanel(this.pinnedPanel, this.pinnedPanelDoc.innerHTML);
        this.hidePinnedPanel(null, true);
    }

    /**
     * Pops a new pop-out panel
     * @param {panelObj} panel - panelObj to apply to the panel
     * @param {String} panelBody - Raw HTML to inject into panel body, injects panel default if left to null
     */
    popPanel(panel, panelBody){
        var newPanel = new poppedPanel(panel, panelBody, this)

        this.poppedPanels.push(newPanel);
    }

}

/**
 * Template Class for other Classes for Objects which represent a single Canopy Panel
 */
class panelObj{
    /**
     * Instantiates a new Panel Object
     * @param {channel} client - Parent client Management Object
     * @param {String} name - Panel Name
     * @param {String} pageURL - Panel Default Page URL
     * @param {Document} panelDocument - Panel Document
     */
    constructor(client, name = "Placeholder Panel", pageURL = "/panel/placeholder", panelDocument = window.document){
        /**
         * Panel Name
         */
        this.name = name;

        /**
         * Panel Default Page URL
         */
        this.pageURL = pageURL;

        /**
         * Panel Document
         */
        this.panelDocument = panelDocument;

        /**
         * Current root document panel doc lives within
         */
        this.ownerDoc = this.panelDocument.ownerDocument == null ? this.panelDocument : this.panelDocument.ownerDocument; 

        /**
         * Parent Client Management object
         */
        this.client = client;
    }

    /**
     * Fetches panel page from the server
     * @returns {String} Raw panel doc HTML
     */
    async getPage(){
        var response = await fetch(this.pageURL,{
            method: "GET",
        });

        return await response.text();
    }

    /**
     * Handles Document/Panel Changes
     */
    docSwitch(){
        //Set owner doc
        this.ownerDoc = this.panelDocument.ownerDocument == null ? this.panelDocument : this.panelDocument.ownerDocument; 
    }

    /**
     * Called upon panel close/exit
     */
    closer(){
    }
}

/**
 * Class which represents a single instance of a popped-out panel
 */
class poppedPanel{
    /**
     * Instantiates a new Popped Panel Object
     * @param {panelObj} panel - Panel Object to apply to Popped Panel
     * @param {String} panelBody - Raw HTML to inject into panel body, defaults to panel page if null
     * @param {cPanel} cPanel - Parent Canopy Panel Management Object
     */
    constructor(panel, panelBody, cPanel){
        /**
         * Panel Object to apply to Popped Panel
         */
        this.panel = panel;

        /**
         * Raw HTML to inject into panel body, defaults to panel page if null
         */
        this.panelBody = panelBody;

        /**
         * Browser Window taken up by the Popped Panel
         */
        this.window = null;

        /**
         * Popped Panel Container Div
         */
        this.pinnedPanelDiv = null;

        /**
         * Popped Panel Title
         */
        this.pinnedPanelTitle = null;

        /**
         * Popped Panel Document Div
         */
        this.pinnedPanelDoc = null;

        /**
         * Popped Panel Close Icon
         */
        this.pinnedPanelCloseIcon = null;

        /**
         * Parent Canopy Panel Management Object
         */
        this.cPanel = cPanel;

        /**
         * Disables this.panel.closer() calls from this.closer()
         */
        this.keepAlive = false;

        //Continue constructor asynchrnously
        this.asyncConstructor();
    }

    /**
     * Continuation of constructor method for asynchronous function calls
     */
    async asyncConstructor(){
        //Set panel body properly
        this.panelBody = (this.panelBody == null || this.panelBody == "") ? await this.panel.getPage() : this.panelBody;

        //Pop the panel
        this.popContainer();
    }

    /**
     * Pops/Opens container window upon start
     */
    popContainer(){
        //Set Window Object
        this.window = window.open("/panel/popoutContainer","",`menubar=no,height=850,width=600`);
        this.window.addEventListener("load", this.fillContainer.bind(this)); 
    }

    /**
     * Fills container window with Popped Panel container elements
     */
    fillContainer(){
        //Set Element Nodes
        this.panelDiv = this.window.document.querySelector("#cpanel-div");
        this.panelTitle = this.window.document.querySelector("#cpanel-title");
        this.panelDoc = this.window.document.querySelector("#cpanel-doc");
        this.panelPopinIcon = this.window.document.querySelector("#cpanel-popin-icon");
        this.panelPinIcon = this.window.document.querySelector("#cpanel-pin-icon");

        //Set Window Title
        this.window.document.title = this.window.document.title.replace("NULL_POPOUT", `${this.panel.name} (${client.channelName})`);

        //Set Panel Content
        this.panelTitle.innerText = this.panel.name;
        this.panelDoc.innerHTML = this.panelBody;

        //Set panel object document and call the related function
        this.panel.panelDocument = this.window.document;
        this.panel.docSwitch();

        this.setupInput();
    }

    /**
     * Defines default input-related popped-panel Event Listeners
     */
    setupInput(){
        this.panelPopinIcon.addEventListener("click", this.unpop.bind(this));
        this.panelPinIcon.addEventListener("click", this.pin.bind(this));
        this.window.addEventListener("unload", this.closer.bind(this));
    }

    /**
     * Called upon close/exit of panel
     */
    closer(){
        if(!this.keepAlive){
            this.panel.closer();
        }

        this.cPanel.poppedPanels.splice(this.cPanel.poppedPanels.indexOf(this),1);
    }

    /**
     * Un-pops panel into active-panel slot
     */
    unpop(){
        //Set active panel
        this.cPanel.setActivePanel(this.panel, this.panelDoc.innerHTML);

        this.keepAlive = true;

        //Close the popped window
        this.window.close();
    }

    /**
     * Pins panel next to chat
     */
    pin(){
        this.cPanel.setPinnedPanel(this.panel, this.panelDoc.innerHTML);

        this.keepAlive = true;

        this.window.close();
    }

}