view background.js @ 28:8279a650da6b

Preserve active and highlighted properties of moved tabs This is consistent with the equivalent built-in drag-and-drop operations.
author Guido Berhoerster <guido+tab-mover@berhoerster.name>
date Sat, 27 Feb 2021 09:14:34 +0100
parents f418a6305f17
children aaed574396b8
line wrap: on
line source

/*
 * Copyright (C) 2018 Guido Berhoerster <guido+tab-mover@berhoerster.name>
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

'use strict';

const ALLOWED_PROTOCOLS = new Set(['http:', 'https:', 'ftp:']);
var windowMenuIds = [];
var lastMenuInstanceId = 0;
var nextMenuInstanceId = 1;

function createMenuItem(createProperties) {
    return new Promise((resolve, reject) => {
        let id = browser.menus.create(createProperties, () => {
            if (browser.runtime.lastError) {
                reject(browser.runtime.lastError);
            } else {
                resolve(id);
            }
        });
    });
}

async function moveTabs(tab, targetWindowId) {
    // if the current tab is part of a highlighted group then move the whole
    // group
    let selectedTabs = (tab.highlighted) ? await browser.tabs.query({
        highlighted: true,
        windowId: tab.windowId
    }) : [tab];
    let activeTab = selectedTabs.find(tab => tab.active);
    await browser.tabs.move(selectedTabs.map(selectedTab => selectedTab.id),
            {windowId: targetWindowId, index: -1});

    // mark the previously active tab active again before highlighting other
    // tabs since this resets the selected tabs
    await browser.tabs.update(activeTab.id, {active: true});
    for (let tab of selectedTabs) {
        if (tab.id !== activeTab.id) {
            browser.tabs.update(tab.id, {active: false, highlighted: true});
        }
    }
}

async function reopenTabs(tab, targetWindowId) {
    // if the current tab is part of a highlighted group then reopen the whole
    // group
    let selectedTabs = (tab.highlighted) ? await browser.tabs.query({
        highlighted: true,
        windowId: tab.windowId
    }) : [tab];
    // filter out privileged tabs which cannot be reopened
    selectedTabs = selectedTabs.filter(selectedTab =>
            ALLOWED_PROTOCOLS.has(new URL(selectedTab.url).protocol));
    if (selectedTabs.length === 0) {
        return;
    }
    let activeTab = selectedTabs.find(tab => tab.active);
    // the actually active tab may have been filtered out above, fall back to
    // the first highlighted one
    if (typeof activeTab === 'undefined') {
        activeTab = selectedTabs[0];
        activeTab.active = true;
    }

    let newTabs = await Promise.all(selectedTabs.map(selectedTab => {
        return browser.tabs.create({
            url: selectedTab.url,
            windowId: targetWindowId,
            active: selectedTab.active
        });
    }));
    // tabs can only be highlighted after they have been created
    for (let tab of newTabs) {
        if (!tab.active) {
            browser.tabs.update(tab.id, {active: false, highlighted: true});
        }
    }
    browser.tabs.remove(selectedTabs.map(selectedTab => selectedTab.id));
}

async function onMenuShown(info, tab)  {
    let menuInstanceId = nextMenuInstanceId++;
    lastMenuInstanceId = menuInstanceId;
    let targetWindows = await browser.windows.getAll({
        populate: true,
        windowTypes: ['normal']
    });
    let creatingMenus = [];
    let moveMenuItems = 0;
    let reopenMenuItems = 0;
    for (let targetWindow of targetWindows) {
        if (targetWindow.id === tab.windowId) {
            // ignore active window
            continue;
        }
        if (tab.incognito === targetWindow.incognito) {
            creatingMenus.push(createMenuItem({
                onclick: (info, tab) => moveTabs(tab, targetWindow.id),
                parentId: 'move-menu',
                title: targetWindow.title
            }));
            moveMenuItems++;
        } else {
            creatingMenus.push(createMenuItem({
                onclick: (info, tab) => reopenTabs(tab, targetWindow.id),
                parentId: 'reopen-menu',
                title: targetWindow.title
            }));
            reopenMenuItems++;
        }
    }
    let updatingMenus = [
        browser.menus.update('move-menu', {enabled: moveMenuItems > 0}),
        browser.menus.update('reopen-menu', {enabled: reopenMenuItems > 0})
    ];
    await Promise.all([...creatingMenus, ...updatingMenus]);
    let newWindowMenuIds = await Promise.all(creatingMenus);
    if (menuInstanceId !== lastMenuInstanceId) {
        // menu has been closed and opened again, remove the items of this
        // instance again
        for (let menuId of newWindowMenuIds) {
            browser.menus.remove(menuId);
        }
        return;
    }
    windowMenuIds = newWindowMenuIds;
    browser.menus.refresh();
}

async function onMenuHidden() {
    lastMenuInstanceId = 0;
    browser.menus.update('move-menu', {enabled: false});
    browser.menus.update('reopen-menu', {enabled: false});
    for (let menuId of windowMenuIds) {
        browser.menus.remove(menuId);
    }
}

(async () => {
    await Promise.all([
        // create submenus
        createMenuItem({
            id: 'move-menu',
            title: browser.i18n.getMessage('moveToWindowMenu'),
            enabled: false,
            contexts: ['tab']
        }),
        createMenuItem({
            id: 'reopen-menu',
            title: browser.i18n.getMessage('reopenInWindowMenu'),
            enabled: false,
            contexts: ['tab']
        })
    ]);
    browser.menus.onShown.addListener(onMenuShown);
    browser.menus.onHidden.addListener(onMenuHidden);
})();