bitwarden-estensione-browser/apps/browser/src/platform/popup/browser-popup-utils.ts

266 lines
8.4 KiB
TypeScript

import { BrowserApi } from "../browser/browser-api";
import { ScrollOptions } from "./abstractions/browser-popup-utils.abstractions";
class BrowserPopupUtils {
/**
* Identifies if the popup is within the sidebar.
*
* @param win - The passed window object.
*/
static inSidebar(win: Window): boolean {
return BrowserPopupUtils.urlContainsSearchParams(win, "uilocation", "sidebar");
}
/**
* Identifies if the popup is within the popout.
*
* @param win - The passed window object.
*/
static inPopout(win: Window): boolean {
return BrowserPopupUtils.urlContainsSearchParams(win, "uilocation", "popout");
}
/**
* Identifies if the popup is within the single action popout.
*
* @param win - The passed window object.
* @param popoutKey - The single action popout key used to identify the popout.
*/
static inSingleActionPopout(win: Window, popoutKey: string): boolean {
return BrowserPopupUtils.urlContainsSearchParams(win, "singleActionPopout", popoutKey);
}
/**
* Identifies if the popup is within the popup.
*
* @param win - The passed window object.
*/
static inPopup(win: Window): boolean {
return (
win.location.href.indexOf("uilocation=") === -1 ||
win.location.href.indexOf("uilocation=popup") > -1
);
}
/**
* Gets the scroll position of the popup.
*
* @param win - The passed window object.
* @param scrollingContainer - Element tag name of the scrolling container.
*/
static getContentScrollY(win: Window, scrollingContainer = "main"): number {
const content = win.document.getElementsByTagName(scrollingContainer)[0];
return content.scrollTop;
}
/**
* Sets the scroll position of the popup.
*
* @param win - The passed window object.
* @param scrollYAmount - The amount to scroll the popup.
* @param options - Allows for setting the delay in ms to wait before scrolling the popup and the scrolling container tag name.
*/
static async setContentScrollY(
win: Window,
scrollYAmount: number | undefined,
options: ScrollOptions = {
delay: 0,
containerSelector: "main",
},
) {
const { delay, containerSelector } = options;
return new Promise<void>((resolve) =>
win.setTimeout(() => {
const container = win.document.querySelector(containerSelector);
if (!isNaN(scrollYAmount) && container) {
container.scrollTop = scrollYAmount;
}
resolve();
}, delay),
);
}
/**
* Identifies if the background page needs to be initialized.
*/
static backgroundInitializationRequired() {
return !BrowserApi.getBackgroundPage();
}
/**
* Identifies if the popup is loading in private mode.
*/
static inPrivateMode() {
return BrowserPopupUtils.backgroundInitializationRequired() && !BrowserApi.isManifestVersion(3);
}
/**
* Opens a popout window of any extension page. If the popout window is already open, it will be focused.
*
* @param extensionUrlPath - A relative path to the extension page. Example: "popup/index.html#/tabs/vault"
* @param options - Options for the popout window that overrides the default options.
*/
static async openPopout(
extensionUrlPath: string,
options: {
senderWindowId?: number;
singleActionKey?: string;
forceCloseExistingWindows?: boolean;
windowOptions?: Partial<chrome.windows.CreateData>;
} = {},
) {
const { senderWindowId, singleActionKey, forceCloseExistingWindows, windowOptions } = options;
const defaultPopoutWindowOptions: chrome.windows.CreateData = {
type: "popup",
focused: true,
width: 380,
height: 630,
};
const offsetRight = 15;
const offsetTop = 90;
const popupWidth = defaultPopoutWindowOptions.width;
const senderWindow = await BrowserApi.getWindow(senderWindowId);
const popoutWindowOptions = {
left: senderWindow.left + senderWindow.width - popupWidth - offsetRight,
top: senderWindow.top + offsetTop,
...defaultPopoutWindowOptions,
...windowOptions,
url: BrowserPopupUtils.buildPopoutUrl(extensionUrlPath, singleActionKey),
};
if (
(await BrowserPopupUtils.isSingleActionPopoutOpen(
singleActionKey,
popoutWindowOptions,
forceCloseExistingWindows,
)) &&
!forceCloseExistingWindows
) {
return;
}
return await BrowserApi.createWindow(popoutWindowOptions);
}
/**
* Closes the single action popout window.
*
* @param popoutKey - The single action popout key used to identify the popout.
* @param delayClose - The amount of time to wait before closing the popout. Defaults to 0.
*/
static async closeSingleActionPopout(popoutKey: string, delayClose = 0): Promise<void> {
const extensionUrl = chrome.runtime.getURL("popup/index.html");
const tabs = await BrowserApi.tabsQuery({ url: `${extensionUrl}*` });
for (const tab of tabs) {
if (!tab.url.includes(`singleActionPopout=${popoutKey}`)) {
continue;
}
setTimeout(() => BrowserApi.removeWindow(tab.windowId), delayClose);
}
}
/**
* Opens a popout window for the current page.
* If the current page is set for the current tab, then the
* popout window will be set for the vault items listing tab.
*
* @param win - The passed window object.
* @param href - The href to open in the popout window.
*/
static async openCurrentPagePopout(win: Window, href: string = null) {
const popoutUrl = href || win.location.href;
const parsedUrl = new URL(popoutUrl);
let hashRoute = parsedUrl.hash;
if (hashRoute.startsWith("#/tabs/current")) {
hashRoute = "#/tabs/vault";
}
await BrowserPopupUtils.openPopout(`${parsedUrl.pathname}${hashRoute}`);
if (BrowserPopupUtils.inPopup(win)) {
BrowserApi.closePopup(win);
}
}
/**
* Identifies if a single action window is open based on the passed popoutKey.
* Will focus the existing window, and close any other windows that might exist
* with the same popout key.
*
* @param popoutKey - The single action popout key used to identify the popout.
* @param windowInfo - The window info to use to update the existing window.
* @param forceCloseExistingWindows - Identifies if the existing windows should be closed.
*/
private static async isSingleActionPopoutOpen(
popoutKey: string | undefined,
windowInfo: chrome.windows.CreateData,
forceCloseExistingWindows = false,
) {
if (!popoutKey) {
return false;
}
const extensionUrl = chrome.runtime.getURL("popup/index.html");
const popoutTabs = (await BrowserApi.tabsQuery({ url: `${extensionUrl}*` })).filter((tab) =>
tab.url.includes(`singleActionPopout=${popoutKey}`),
);
if (popoutTabs.length === 0) {
return false;
}
if (!forceCloseExistingWindows) {
// Update first, remove it from list
const tab = popoutTabs.shift();
await BrowserApi.updateWindowProperties(tab.windowId, {
focused: true,
width: windowInfo.width,
height: windowInfo.height,
top: windowInfo.top,
left: windowInfo.left,
});
}
popoutTabs.forEach((tab) => BrowserApi.removeWindow(tab.windowId));
return true;
}
/**
* Identifies if the url contains the specified search param and value.
*
* @param win - The passed window object.
* @param searchParam - The search param to identify.
* @param searchValue - The search value to identify.
*/
private static urlContainsSearchParams(
win: Window,
searchParam: string,
searchValue: string,
): boolean {
return win.location.href.indexOf(`${searchParam}=${searchValue}`) > -1;
}
/**
* Builds the popout url path. Ensures that the uilocation param is set to
* `popout` and that the singleActionPopout param is set to the passed singleActionKey.
*
* @param extensionUrlPath - A relative path to the extension page. Example: "popup/index.html#/tabs/vault"
* @param singleActionKey - The single action popout key used to identify the popout.
*/
private static buildPopoutUrl(extensionUrlPath: string, singleActionKey: string) {
const parsedUrl = new URL(chrome.runtime.getURL(extensionUrlPath));
parsedUrl.searchParams.set("uilocation", "popout");
if (singleActionKey) {
parsedUrl.searchParams.set("singleActionPopout", singleActionKey);
}
return parsedUrl.toString();
}
}
export default BrowserPopupUtils;