Advanced Panels Mod (with Sessions Panel)



  • What

    A mod that enabled an easy way to make advanced panels. A panel that runs in the same context as the bookmarks and notes panels.

    Why

    I wanted to try making a sessions panel, but found that there would be a lot of work to get the Advanced Panel created and running well. This effort would also be duplicated (and might conflict) if I wanted to make a second advanced panel later.

    How

    The mod hooks into dummy web panels (with URLs that go nowhere), and inserts the advanced panel into it. This way, Vivaldi will remember the size and position, as if it were a normal web panel, and the mod can manage its own state and HTML as needed without having to worry about loading and listeners and such nonsense.

    Demo

    Sessions Panel - Gives you an easy to access list of sessions. You can delete them, open them and create new ones from the panel instead of having to deal with the modal dialog. The mod requires that you first add your own web panel manually, with a URL set to vivaldi://sessions

    0_1529404049447_2018-06-19_11-26-26.gif

    Installation

    Comes in two parts: advancedPanels.js and advancedPanels.css. These should be installed as per the instructions here.

    /*
    * Advanced Panels (a mod for Vivaldi)
    * Written by LonM
    * No Copyright Reserved
    */
    
    (function advancedPanels(){
    "use strict";
    
        /**
         * Key is the ID of your advanced panel. This must be UNIQUE (across the whole vivaldi UI). If in doubt, append your name to ensure it is unique
         *     You can use this ID as a #selector in the advancedPanels.css file
         * title: String, self explanatory
         * url: String, a UNIQUE (amongst web panels) vivaldi:// url that points to a non-existent page. You must add this as a web panel
         * switch: String of HTML, this will be set as the html in the panel switch button. E.g. an SVG
         * initialHTML: String of HTML, this will be used to fill in the panel with a skeleton of HTML to use
         * module: () => {onInit, onActivate} All necessary javascript should be included here.
         *     onInit: () => void. This will be called AFTER the advanced panel DOM is added, but BEFORE onActivate is called.
         *     onActivate: () => void. This will be called each time the advanced panel is opened AND IMMEDIATELY AFTER onInit.
         */
        const CUSTOM_PANELS = {
            sessions_lonm: {
                title: "Sessions",
                url: "vivaldi://sessions",
                switch: `<span>
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="5 0 10 10">
                            <path d="M7 2h6v1h-6v-1zm0 2h6v1h-6v-1zm0 2h6v1h-6v-1z"></path>
                        </svg>
                    </span>`,
                initialHTML: `
                    <div class="newSession">
                        <h2>New Session</h2>
                        <input type="text" placeholder="Session Name" class="session-name">
                        <label><input type="checkbox" class="all-windows"><span>All Windows</span></label>
                        <label><input type="checkbox" class="selected-tabs"><span>Only Selected Tabs</span></label>
                        <input type="button" class="add-session" value="Add Session"></input>
                    </div>
                    <div class="sortselector sortselector-compact">
                        <select class="sortselector-dropdown" title="Sort by..." tabindex="-1">
                            <option value="visitTime">Sort by Date</option>
                            <option value="title">Sort by Name</option>
                        </select>
                        <button class="sortselector-button direction-descending" title="Sort Ascending" tabindex="-1">
                            <svg width="11" height="6" viewBox="0 0 11 6" xmlns="http://www.w3.org/2000/svg">
                                <path d="M5.5.133l.11-.11 4.456 4.456-1.498 1.497L5.5 2.91 2.432 5.976.934 4.48 5.39.022l.11.11z"></path>
                            </svg>
                        </button>
                        <button class="sortselector-button direction-ascending selected" title="Sort Descending" tabindex="-1">
                            <svg width="11" height="6" viewBox="0 0 11 6" xmlns="http://www.w3.org/2000/svg">
                                <path d="M5.5.133l.11-.11 4.456 4.456-1.498 1.497L5.5 2.91 2.432 5.976.934 4.48 5.39.022l.11.11z"></path>
                            </svg>
                        </button>
                    </div>
                    <section class="sessionslist">
                        <ul>
                        </ul>
                    </section>
                    <div class="modal-container">
                        <div class="confirm">
                            <p>Are you sure you want to delete <span class="title"></span>?</p>
                            <button class="yes">⚠ Yes, Delete</button>
                            <button class="no">No, don't.</button>
                        </div>
                    </div>
                    <template class="session_item">
                        <li>
                            <div>
                                <h3></h3>
                                <span>Created <time></time></span>
                            </div>
                            <button class="open_new" title="Open in new window">
                                <svg viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg">
                                <path d="M21 6h-16v14h16v-14zm-11 2h2v2h-2v-2zm-3 0h2v2h-2v-2zm12 10h-12v-7h12v7zm0-8h-6v-2h6v2z"></path>
                                </svg>
                            </button>
                            <button class="open_current" title="Open in current window">
                                <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
                                <path d="M0 9h16v2h-16v-2zm0-4h8v4h-8v-4z"></path>
                                <path opacity=".5" d="M9 5h7v3h-7z"></path>
                                </svg>
                            </button>
                            <button class="delete" title="Delete this session">
                                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">
                                <path d="M10.2.5l-.7-.7L5 4.3.5-.2l-.7.7L4.3 5-.2 9.5l.7.7L5 5.7l4.5 4.5.7-.7L5.7 5"></path>
                                </svg>
                            </button>
                        </li>
                    </template>`,
                module: function(){
    
                    /**
                     * Get selected session names
                     * @returns string array of names
                     */
                    function getSelectedSessionNames(){
                        const selections = Array.from(document.querySelectorAll("#sessions_lonm li.selected h3"));
                        return selections.map(x => x.innerText);
                    }
    
                    /**
                     * Open a session after its corresponding list item is clicked
                     * @param e click event
                     * REMARK: Hide the confirm box if it is open
                     * REMARK: If click was on a button, just ignore it
                     */
                    function listItemClick(e){
                        if(isButton(e.target)){
                            return;
                        }
                        if(e.ctrlKey){
                            e.currentTarget.classList.toggle("selected");
                        } else {
                            const oldselect = document.querySelectorAll("#sessions_lonm li.selected");
                            oldselect.forEach(item => item.classList.remove("selected"));
                            e.currentTarget.classList.add("selected");
                        }
                        document.querySelector("#sessions_lonm .confirm").classList.remove("show");
                    }
    
                    /**
                     * Check if the target of a click is a button
                     * @param target an event target
                     */
                    function isButton(target){
                        const tag = target.tagName.toLowerCase();
                        return (tag==="button" && target.className==="delete") || (tag==="svg" && target.parentElement.className==="delete");
                    }
    
                    /**
                     * Add a new session
                     * @param e button click event
                     */
                    function newSessionClick(e){
                        let name = document.querySelector('#sessions_lonm .newSession input.session-name').value;
                        const windows = document.querySelector('#sessions_lonm .newSession input.all-windows').checked;
                        const selectedTabs = document.querySelector('#sessions_lonm .newSession input.selected-tabs').checked;
                        const markedTabs = document.querySelectorAll(".tab.marked");
                        if(name===""){
                            name = new Date().toISOString().replace(":",".").replace(":",".");
                        }
                        vivaldi.windowPrivate.getCurrentId(window => {
                            const options = {
                                saveOnlyWindowId: windows ? 0 : window
                            };
                            if(selectedTabs && markedTabs && markedTabs.length>0){
                                options.ids = Array.from(markedTabs).map(tab => Number(tab.id.replace("tab-", "")));
                            }
                            vivaldi.sessionsPrivate.saveOpenTabs(name, options, ()=>{
                                document.querySelector('#sessions_lonm .newSession input.session-name').value = "";
                                document.querySelector('#sessions_lonm .newSession input.all-windows').checked = false;
                                document.querySelector('#sessions_lonm .newSession input.selected-tabs').checked = false;
                                updateList();
                            });
                        });
                    }
    
                    /**
                     * Change sort Order
                     * @param e click event
                     */
                    function sortOrderChange(e){
                        document.querySelectorAll("#sessions_lonm .sortselector-button").forEach(el => {
                            el.classList.toggle("selected");
                        });
                        updateList();
                    }
    
                    /**
                     * Change sort Method
                     * @param e click event
                     */
                    function sortMethodChange(e){
                        updateList();
                    }
    
                    /**
                     * User clicked remove button
                     * @param e click event
                     */
                    function deleteClick(e){
                        const selectedSessions = getSelectedSessionNames();
                        if(selectedSessions.length === 1){
                            confirmMsg(selectedSessions[0]);
                        } else {
                            confirmMsg(selectedSessions.length + " selected sessions");
                        }
                    }
    
                    /**
                     * Show the delete confirmation box with specified text
                     * @param msg string to use
                     */
                    function confirmMsg(msg){
                        document.querySelector("#sessions_lonm .confirm .title").innerText = msg;
                        document.querySelector("#sessions_lonm .modal-container").classList.add("show");
                    }
    
                    /**
                     * User confirmed remove
                     * @param e event
                     * REMARK: Want to remove all and only update UI after final removal
                     */
                    function deleteConfirmClick(e){
                        const selections = getSelectedSessionNames();
                        for (let i = 0; i < selections.length-1; i++) {
                            vivaldi.sessionsPrivate.delete(selections[i],() => {});
                        }
                        vivaldi.sessionsPrivate.delete(selections[selections.length-1], ()=>{
                            updateList();
                        });
                    }
    
                    /**
                     * User cancelled remove
                     * @param e event
                     */
                    function deleteCancelClick(e){
                        document.querySelector("#sessions_lonm .modal-container").classList.remove("show");
                    }
    
                    /**
                     * User clicked open (current) button
                     * @param e click event
                     */
                    function openInCurrentWindowClick(e){
                        const selections = getSelectedSessionNames();
                        selections.forEach(item => {
                            vivaldi.sessionsPrivate.open(
                                item,
                                {openInNewWindow: false}
                            );
                        });
                    }
    
                    /**
                     * User clicked open (new) button
                     * @param e click event
                     */
                    function oneInNewWindowClick(e){
                        const selections = getSelectedSessionNames();
                        selections.forEach(item => {
                            vivaldi.sessionsPrivate.open(
                                item,
                                {openInNewWindow: true}
                            );
                        });
                    }
    
                    /**
                     * Generate a list item for a session object
                     * @returns DOM list item
                     */
                    function createListItem(session){
                        const template = document.querySelector("#sessions_lonm template.session_item");
                        const el = document.importNode(template.content, true);
                        el.querySelector("h3").innerText = session.name;
                        const date = new Date(session.createDateJS);
                        el.querySelector("time").innerText = date.toLocaleString();
                        el.querySelector("time").setAttribute("datetime", date.toISOString());
                        el.querySelector("li").addEventListener("click", listItemClick);
                        el.querySelector(".open_new").addEventListener("click", oneInNewWindowClick);
                        el.querySelector(".open_current").addEventListener("click", openInCurrentWindowClick);
                        el.querySelector(".delete").addEventListener("click", deleteClick);
                        return el;
                    }
    
                    /**
                     * Sort the array of sessions
                     * @param sessions array of session objects - unsorted
                     * @returns sessions array of session objects - sorted
                     */
                    function sortSessions(sessions){
                        const sortRule = document.querySelector("#sessions_lonm .sortselector-dropdown").value;
                        const sortDescending = document.querySelector("#sessions_lonm .direction-descending.selected");
                        if(sortRule==="visitTime" && sortDescending){
                            sessions.sort((a,b) => {return a.createDateJS - b.createDateJS;});
                        } else if (sortRule==="visitTime" && !sortDescending) {
                            sessions.sort((a,b) => {return b.createDateJS - a.createDateJS;});
                        } else if (sortRule==="title" && sortDescending) {
                            sessions.sort((a,b) => {return a.name.localeCompare(b.name);});
                        } else if (sortRule==="title" && !sortDescending) {
                            sessions.sort((a,b) => {return b.name.localeCompare(a.name);});
                        }
                        return sessions;
                    }
    
                    /**
                     * Create the dom list for the sessions
                     * @param sessions The array of session objects (already sorted)
                     * @returns DOM list of session items
                     */
                    function createList(sessions){
                        const newList = document.createElement("ul");
                        sessions.forEach((session, index) => {
                            const li = createListItem(session, index);
                            newList.appendChild(li);
                        });
                        return newList;
                    }
    
                    /**
                     * Get the array of sessions and recreate the list in the panel
                     */
                    function updateList(){
                        document.querySelector("#sessions_lonm .modal-container").classList.remove("show");
                        const existingList = document.querySelector("#sessions_lonm .sessionslist ul");
                        existingList.parentElement.removeChild(existingList);
                        vivaldi.sessionsPrivate.getAll(items => {
                            const sorted = sortSessions(items);
                            const newList = createList(sorted);
                            document.querySelector("#sessions_lonm .sessionslist").appendChild(newList);
                        });
                    }
    
                    /**
                     * Update the session listing on activation of panel
                     */
                    function onActivate(){
                        updateList();
                    }
    
                    /**
                     * Add the sort listeners on creation of panel
                     */
                    function onInit(){
                        document.querySelectorAll("#sessions_lonm .sortselector-button").forEach(el => {
                            el.addEventListener("click", sortOrderChange);
                        });
                        document.querySelector("#sessions_lonm .sortselector-dropdown").addEventListener("change", sortMethodChange);
                        document.querySelector("#sessions_lonm .confirm .yes").addEventListener("click", deleteConfirmClick);
                        document.querySelector("#sessions_lonm .confirm .no").addEventListener("click", deleteCancelClick);
                        document.querySelector("#sessions_lonm .newSession .add-session").addEventListener("click", newSessionClick);
                    }
    
                    return {
                        onInit: onInit,
                        onActivate: onActivate
                    };
                }
            }
        };
    
    
        /**
         * Observe for changes to the UI, e.g. if panels are hidden by going in to fullscreen mode
         * This may require the panel buttons and panels to be re-converted
         * Also, observe panels container, if class changes to switcher, the webstack is removed
         */
        const UI_STATE_OBSERVER = new MutationObserver(records => {
            convertWebPanelButtonstoAdvancedPanelButtons();
            listenForNewPanelsAndConvertIfNecessary();
        });
    
        /**
         * Observe UI state changes
         */
        function observeUIState(){
            const classInit = {
                attributes: true,
                attributeFilter: ["class"]
            };
            UI_STATE_OBSERVER.observe(document.querySelector("#browser"), classInit);
            UI_STATE_OBSERVER.observe(document.querySelector("#panels-container"), classInit);
        }
    
        const PANEL_STACK_CREATION_OBSERVER = new MutationObserver((records, observer) => {
            observer.disconnect();
            listenForNewPanelsAndConvertIfNecessary();
        });
    
    
        /**
         * Start listening for new web panel stack children and convert any already open ones
         */
        function listenForNewPanelsAndConvertIfNecessary(){
            const panelStack = document.querySelector("#panels .webpanel-stack");
            if(panelStack){
                WEBPANEL_CREATE_OBSERVER.observe(panelStack, {childList: true});
                const currentlyOpen = document.querySelectorAll(".webpanel-stack .panel");
                currentlyOpen.forEach(convertWebPanelToAdvancedPanel);
            } else {
                const panels = document.querySelector("#panels");
                PANEL_STACK_CREATION_OBSERVER.observe(panels, {childList: true});
            }
        }
    
        /**
         * Observer that should check for child additions to web panel stack
         * When a new child is added (a web panel initialised), convert it appropriately
         */
        const WEBPANEL_CREATE_OBSERVER = new MutationObserver(records => {
            records.forEach(record => {
                record.addedNodes.forEach(convertWebPanelToAdvancedPanel);
            });
        });
    
        /**
         * Webview loaded a page. This means the src has been initially set.
         * @param e load event
         */
        function webviewLoaded(e){
            e.currentTarget.removeEventListener("contentload", webviewLoaded);
            convertWebPanelToAdvancedPanel(e.currentTarget.parentElement.parentElement);
        }
    
        /**
         * Attempt to convert a web panel to an advanced panel.
         * First check if the SRC matches a registered value.
         * If so, call the advanced Panel Created method
         * @param node DOM node representing the newly added web panel (child of .webpanel-stack)
         * REMARK: Webview.src can add a trailing "/" to URLs
         * REMARK: When initially created the webview may have no src,
         *     so we need to listen for the first src change
         */
        function convertWebPanelToAdvancedPanel(node){
            const addedWebview = node.querySelector("webview");
            if(!addedWebview){
                return;
            }
            if(!addedWebview.src){
                addedWebview.addEventListener("contentload", webviewLoaded);
                return;
            }
            for(const key in CUSTOM_PANELS){
                const panel = CUSTOM_PANELS[key];
                const expectedURL = panel.url;
                if(addedWebview.src.startsWith(expectedURL)){
                    advancedPanelCreated(node, panel, key);
                    break;
                }
            }
        }
    
        /**
         * Convert a web panel into an Advanced Panelβ„’
         * @param node the dom node added under web panel stack
         * @param panel the panel object descriptor
         * @param panelId the identifier (key) for the panel
         * REMARK: Vivaldi can instantiate some new windows with an
         *    "empty" web panel containing nothing but the webview
         * REMARK: Can't simply call node.innerHTML as otherwise the
         *    vivaldi UI will crash when attempting to hide the panel
         * REMARK: Check that the panel isn't already an advanced panel
         *    before convert as this could be called after state change
         * REMARK: it may take a while for vivaldi to update the title of
         *    a panel, therefore after it is terminated, the title may
         *    change to aborted. Title changing should be briefly delayed
         */
        function advancedPanelCreated(node, panel, panelID){
            if(node.getAttribute("advancedPanel")){
                return;
            }
            node.setAttribute("advancedPanel", "true");
            node.querySelector("webview").terminate();
            const newHTML = document.createElement("div");
            newHTML.innerHTML = panel.initialHTML;
            node.appendChild(newHTML);
            node.id = panelID;
            panel.module().onInit();
            ADVANCED_PANEL_ACTIVATION.observe(node, {attributes: true, attributeFilter: ["class"]});
            if(node.querySelector("header.webpanel-header")){
                advancedPanelOpened(node);
                setTimeout(() => {updateAdvancedPanelTitle(node);}, 500);
            }
        }
    
    
        /**
         * Observe attributes of an advanced panel to see when it becomes active
         */
        const ADVANCED_PANEL_ACTIVATION = new MutationObserver(records => {
            records.forEach(record => {
                if(record.target.classList.contains("visible")){
                    advancedPanelOpened(record.target);
                }
            });
        });
    
        /**
         * An advanced panel has been selected by the user and is now active
         * @param node DOM node of the advancedpanel activated
         */
        function advancedPanelOpened(node){
            updateAdvancedPanelTitle(node);
            const panel = CUSTOM_PANELS[node.id];
            panel.module().onActivate();
        }
    
        /**
         * Update the header title of a panel
         * @param node DOM node of the advancedpanel activated
         */
        function updateAdvancedPanelTitle(node){
            const panel = CUSTOM_PANELS[node.id];
            node.querySelector("header.webpanel-header h1").innerHTML = panel.title;
            node.querySelector("header.webpanel-header h1").title = panel.title;
        }
    
    
    
        /**
         * Go through each advanced panel descriptor and convert the associated button
         */
        function convertWebPanelButtonstoAdvancedPanelButtons(){
            for(const key in CUSTOM_PANELS){
                const panel = CUSTOM_PANELS[key];
                let switchBtn = document.querySelector(`#switch button[title~="${panel.url}"`);
                if(!switchBtn){
                    switchBtn = document.querySelector(`#switch button[advancedPanelSwitch="${key}"`);
                    if(!switchBtn){
                        console.warn(`Failed to find button for ${panel.title}`);
                        continue;
                    }
                }
                convertSingleButton(switchBtn, panel, key);
            }
        }
    
        /**
         * Set the appropriate values to convert a regular web panel switch into an advanced one
         * @param switchBtn DOM node for the #switch button being converted
         * @param panel The Advanced panel object description
         * @param id string id of the panel
         * REMARK: Check that the button isn't already an advanced panel button
         *    before convert as this could be called after state change
         */
        function convertSingleButton(switchBtn, panel, id){
            if(switchBtn.getAttribute("advancedPanelSwitch")){
                return;
            }
            switchBtn.title = panel.title;
            switchBtn.innerHTML = panel.switch;
            switchBtn.setAttribute("advancedPanelSwitch", id);
        }
    
    
        /**
         * Observe web panel switches.
         * REMARK: When one is added or removed, all of the web panels are recreated
         */
        const WEB_SWITCH_OBSERVER = new MutationObserver(records => {
            convertWebPanelButtonstoAdvancedPanelButtons();
            listenForNewPanelsAndConvertIfNecessary();
        });
    
        /**
         * Start observing for additions or removals of web panel switches
         */
        function observePanelSwitchChildren(){
            const panelSwitch = document.querySelector("#switch");
            WEB_SWITCH_OBSERVER.observe(panelSwitch, {childList: true});
        }
    
    
        /**
         * Initialise the mod. Checking to make sure that the relevant panel element exists first.
         */
        function initMod(){
            if(document.querySelector("#panels .webpanel-stack")){
                observeUIState();
                observePanelSwitchChildren();
                convertWebPanelButtonstoAdvancedPanelButtons();
                listenForNewPanelsAndConvertIfNecessary();
            } else {
                setTimeout(initMod, 500);
            }
        }
    
        initMod();
    })();
    
    
    /*
    * Advanced Panels (a mod for Vivaldi)
    * Written by LonM
    * No Copyright Reserved
    */
    .panel[advancedpanel="true"] header .toolbar .back, /* hide the web panel back/reload buttons */
    .panel[advancedpanel="true"] header .toolbar .forward,
    .panel[advancedpanel="true"] header .toolbar .reload,
    .panel[advancedpanel="true"] header .toolbar .home,
    .panel[advancedpanel="true"] footer /* this last one is to prevent collision with panel actions mod */ {
        display: none !important;
    }
    
    .panel[advancedpanel="true"] > div {
        height: 100%;
        display: flex;
        flex-direction: column;
    }
    
    .panel[advancedpanel="true"] button {
        background: var(--colorBg);
    }
    
    
    /*
    * CSS For Sessions Panel
    */
    #sessions_lonm .sessionslist li {
        display: flex;
        flex-direction: row;
        padding-top: 4px;
        padding-bottom: 4px;
        padding-left: 8px
    }
    
    #sessions_lonm .sessionslist li div {
        flex-grow: 1;
        padding-right: 4px;
    }
    
    #sessions_lonm .sessionslist li button {
        width: 24px;
        height: 24px;
    }
    
    #sessions_lonm .sessionslist li span {
        font-size: smaller;
    }
    
    #sessions_lonm li h3 , #sessions_lonm li div {
        text-overflow: ellipsis;
        overflow: hidden;
        white-space: nowrap;
    }
    
    #sessions_lonm li:hover {
        background: var(--colorHighlightBgAlpha);
    }
    #sessions_lonm li.selected {
        background: var(--colorHighlightBg);
        color: var(--colorHighlightFg);
    }
    
    #sessions_lonm li button {
        display: none;
    }
    #sessions_lonm li.selected button {
        display: block;
    }
    
    #sessions_lonm .sortselector-button {
        display: none;
    }
    #sessions_lonm .sortselector-button.selected {
        display: block;
    }
    
    #sessions_lonm .open_current svg {
        width: 16px;
        left: -3.5px;
        position: relative;
    }
    
    #sessions_lonm .open_new svg {
        width: 20px;
        position: relative;
        left: -5.5px;
    }
    #sessions_lonm .modal-container{
        display: none;
    }
    
    #sessions_lonm .show.modal-container {
        width: 100%;
        height: 100%;
        position: fixed;
        top: 0;
        left: 0;
        display: flex;
        justify-content: center;
        align-items: center;
    }
    
    #sessions_lonm .confirm {
        width: 90%;
        height: 130px;
        background: var(--colorBg);
        color: var(--colorFg);
        background-color: var(--colorBgLight);
        box-shadow: var(--shadowOverlay);
        border-radius: var(--radiusHalf);
    }
    
    #sessions_lonm .confirm p:nth-of-type(1) {
        margin: 20px;
    }
    
    #sessions_lonm .confirm p:nth-of-type(2) {
        margin-left: 20px;
    }
    
    #sessions_lonm .confirm button {
        margin: 20px;
        border: 1px solid var(--colorBorder);
        height: 28px;
        padding: 0 18px;
        -webkit-user-select: none;
        color: var(--colorFg);
        background-image: linear-gradient(var(--colorBgLightIntense) 0%, var(--colorBg) 100%);
    }
    
    #sessions_lonm .confirm button:hover {
        background-image: linear-gradient(var(--colorBg), var(--colorBg));
    }
    
    
    #sessions_lonm .newSession {
        padding: 4px;
        display: grid;
        grid-template-columns: 1fr 1fr;
        grid-template-rows: auto;
        grid-column-gap: 6px;
        grid-row-gap: 6px;
    }
    
    #sessions_lonm .newSession h2,
    #sessions_lonm .newSession input[type="text"]{
        grid-column: 1/3;
    }
    
    #sessions_lonm .newSession label input + span {
        margin-left: 10px;
    }
    
    

    Writing Panels

    See the big comment at the top of the file for an explanation of what you need to do to make a panel. You need to provide HTML and identifiers, and in your javascript module you need to provide an interface that offers onInit and onActivate functions.

    The vast majority of a panel is written in the JavaScript file, but you can use the key of a panel as an #id in the CSS file to add additional styles.

    The JavaScript functionality for a mod should be written entirely within the CUSTOM_PANELS dictionary. Everything outside that is just assisting with loading the panel.

    Additional Remarks

    I've tried to put the important remarks in the code. If I think of any additional ones, they'll be put here.

    I didn't bother to translate this mod because... that would be too much work.

    The sessions icon is just 3 lines because I have no idea what icon to give it.

    Something when clicking "open session in current window", if there are multiple windows in the session, Vivaldi will open only the primary window in the current window and still open other windows individually. There's nothing I can do about this, that's just the way Vivaldi works.

    Let me know what you think - if this is useful, or if it's just a load of nonsense.

    Changelog

    • Bugfix for floating panels
    • Bugfixes - Better handling of cases where panel is removed from DOM
    • Can now save selected tabs in a session, style improvements
    • Can now save a session in a single click - uses current timestamp as name
    • Can now select multiple sessions at once using Ctrl+Click
    • Sessions Panel Sorting Rules
    • Fix a bug on dark themes, and when adding or removing a regular web panel
    • Initial Release

  • Vivaldi Ambassador

    @lonm This looks great as a Vivaldi new feature and even better for a custom mod. Congrats!!

    One request (you don't have to bother, just a little detail). Since the "advanced panel" is the main component here and the "session panel" is your example of use case (again, well done), could you perhaps split the code between the 'core' - advanced panels - and your example?
    Like, every new 'panel' goes into a folder and the 'core' imports all of its contents

    Again, just an idea (I might do it myself over the weekend if you're too busy). Thanks in advance



  • @masterleo29 I suppose that could be possible. I'm not sure how Vivaldi would react to trying to import javascript on the fly.


  • Vivaldi Ambassador

    @lonm You have a point there.. Once I get back to my laptop I'll try something


  • Moderator

    I loved this too much! I dream with a session panel and I liked the idea. I'll test it when possible.



  • I think I will be able to put this to good use. Thanks for sharing



  • Hi, how to do this?
    I used this code to hide the buttons:

    .window-buttongroup, .window-buttongroup * {
    	display: none;
    }
    

    But the trash button is not at the end of the bar.
    Thank you.



  • @burbuja This isn't really the right place to post that request, it would be better in its own topic.

    But the code you want is:

    .window-buttongroup, .window-buttongroup * {
    	display: none;
    }
    #tabs-container.top {
        padding-right: 10px;
    }
    


  • Nice πŸ™‚
    I should test this on clean vivaldi as it throws a
    sespanel.js:451 Failed to find button for Sessions
    (probably conflictual css/js on my side. I'm sure about that)

    Oops.



  • @hadden89 The mod requires that you first add your own web panel manually, with a URL set to vivaldi://sessions.



  • Well it works, but the only panel I'd possibly want is working inbuilt mail πŸ˜•



  • @lonm said in Advanced Panels Mod (with Sessions Panel):

    @burbuja This isn't really the right place to post that request, it would be better in its own topic.

    But the code you want is:

    .window-buttongroup, .window-buttongroup * {
    	display: none;
    }
    #tabs-container.top {
        padding-right: 10px;
    }
    

    This doesn't work



  • @sjudenim Ah, that should have an !important to force it:

    .window-buttongroup, .window-buttongroup * {
    	display: none;
    }
    #tabs-container.top {
        padding-right: 10px !important;
    }
    

    But again, I must stress that this should be in it's own modding topic.



  • @lonm said in Advanced Panels Mod (with Sessions Panel):

    @sjudenim Ah, that should have an !important to force it:

    .window-buttongroup, .window-buttongroup * {
    	display: none;
    }
    #tabs-container.top {
        padding-right: 10px !important;
    }
    

    But again, I must stress that this should be in it's own modding topic.

    I agree, just pointing out code that does't work for those that may come across it in the future



  • An idea for a button could be something like these



  • @hadden89 I had an initial thought of using the clock, but thought it was too similar to the history icon.

    Instead I went with 3 bars, which is too similar to the menu icon 😎



  • @lonm Something like this would look OK-ish https://www.onlinewebfonts.com/icon/475555



  • [Sorry, false alertβ€”old post edited away.]



  • @valiowk On v2 RC2 it still works. πŸ˜›
    You mean on v2 stable, do you? (I don't have the mod there).



  • βš™ I've updated the sessions panel mod.

    You can now use the βœ”Only selected tabs check box to save tabs that you select with by Ctrl/Shift clicking them.

    As a consequence, the minimum required version of Vivaldi to run the mod is now 2.2.

    If you're on an older version, no To see the previous version, click the 3 dots on the first post above and view the forum post edit history.

    (oh and there are some style improvements. πŸ‘©β€πŸŽ¨)


 

Looks like your connection to Vivaldi Forum was lost, please wait while we try to reconnect.