mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-26 22:01:32 +01:00
[PM-5189] Reworking how we handle updating ciphers within nested sub frames
This commit is contained in:
parent
ede74bc72d
commit
aaa585c992
@ -772,12 +772,12 @@ export default class NotificationBackground {
|
|||||||
) => {
|
) => {
|
||||||
const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command];
|
const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command];
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageResponse = handler({ message, sender });
|
const messageResponse = handler({ message, sender });
|
||||||
if (typeof messageResponse === "undefined") {
|
if (typeof messageResponse === "undefined") {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.resolve(messageResponse)
|
Promise.resolve(messageResponse)
|
||||||
|
@ -394,7 +394,7 @@ describe("OverlayBackground", () => {
|
|||||||
|
|
||||||
it("triggers an update of the inline menu position after rebuilding sub frames", async () => {
|
it("triggers an update of the inline menu position after rebuilding sub frames", async () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
overlayBackground["updateInlineMenuPositionTimeout"] = setTimeout(jest.fn, 650);
|
overlayBackground["delayedUpdateInlineMenuPositionTimeout"] = setTimeout(jest.fn, 650);
|
||||||
const sender = mock<chrome.runtime.MessageSender>({ tab, frameId: middleFrameId });
|
const sender = mock<chrome.runtime.MessageSender>({ tab, frameId: middleFrameId });
|
||||||
jest.spyOn(overlayBackground as any, "updateInlineMenuPositionAfterSubFrameRebuild");
|
jest.spyOn(overlayBackground as any, "updateInlineMenuPositionAfterSubFrameRebuild");
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
private inlineMenuCiphers: Map<string, CipherView> = new Map();
|
private inlineMenuCiphers: Map<string, CipherView> = new Map();
|
||||||
private inlineMenuPageTranslations: Record<string, string>;
|
private inlineMenuPageTranslations: Record<string, string>;
|
||||||
private inlineMenuFadeInTimeout: number | NodeJS.Timeout;
|
private inlineMenuFadeInTimeout: number | NodeJS.Timeout;
|
||||||
private updateInlineMenuPositionTimeout: number | NodeJS.Timeout;
|
private delayedUpdateInlineMenuPositionTimeout: number | NodeJS.Timeout;
|
||||||
private delayedCloseTimeout: number | NodeJS.Timeout;
|
private delayedCloseTimeout: number | NodeJS.Timeout;
|
||||||
private focusedFieldData: FocusedFieldData;
|
private focusedFieldData: FocusedFieldData;
|
||||||
private isFieldCurrentlyFocused: boolean = false;
|
private isFieldCurrentlyFocused: boolean = false;
|
||||||
@ -252,9 +252,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (pageDetails.frameId !== 0 && pageDetails.details.fields.length) {
|
if (pageDetails.frameId !== 0 && pageDetails.details.fields.length) {
|
||||||
void this.buildSubFrameOffsets(pageDetails.tab, pageDetails.frameId, pageDetails.details.url);
|
void this.buildSubFrameOffsets(sender, pageDetails.details.url);
|
||||||
void BrowserApi.tabSendMessage(pageDetails.tab, {
|
void BrowserApi.tabSendMessage(pageDetails.tab, {
|
||||||
command: "setupAutofillInlineMenuReflowObserver",
|
command: "setupRebuildSubFrameOffsetsListeners",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,6 +292,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id];
|
const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id];
|
||||||
if (subFrameOffsetsForTab) {
|
if (subFrameOffsetsForTab) {
|
||||||
subFrameOffsetsForTab.set(message.subFrameData.frameId, message.subFrameData);
|
subFrameOffsetsForTab.set(message.subFrameData.frameId, message.subFrameData);
|
||||||
|
this.delayedUpdateInlineMenuPosition(sender);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,12 +300,12 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
* Builds the offset data for a sub frame of a tab. The offset data is used
|
* Builds the offset data for a sub frame of a tab. The offset data is used
|
||||||
* to calculate the position of the inline menu list and button.
|
* to calculate the position of the inline menu list and button.
|
||||||
*
|
*
|
||||||
* @param tab - The tab that the sub frame is associated with
|
* @param sender - The sender of the message
|
||||||
* @param frameId - The frame ID of the sub frame
|
|
||||||
* @param url - The URL of the sub frame
|
* @param url - The URL of the sub frame
|
||||||
*/
|
*/
|
||||||
private async buildSubFrameOffsets(tab: chrome.tabs.Tab, frameId: number, url: string) {
|
private async buildSubFrameOffsets(sender: chrome.runtime.MessageSender, url: string) {
|
||||||
let subFrameDepth = 0;
|
let subFrameDepth = 0;
|
||||||
|
const { tab, frameId } = sender;
|
||||||
const tabId = tab.id;
|
const tabId = tab.id;
|
||||||
let subFrameOffsetsForTab = this.subFrameOffsetsForTab[tabId];
|
let subFrameOffsetsForTab = this.subFrameOffsetsForTab[tabId];
|
||||||
if (!subFrameOffsetsForTab) {
|
if (!subFrameOffsetsForTab) {
|
||||||
@ -358,6 +359,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
subFrameOffsetsForTab.set(frameId, subFrameData);
|
subFrameOffsetsForTab.set(frameId, subFrameData);
|
||||||
|
this.delayedUpdateInlineMenuPosition(sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -392,12 +394,8 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
if (subFrameOffsetsForTab) {
|
if (subFrameOffsetsForTab) {
|
||||||
const tabFrameIds = Array.from(subFrameOffsetsForTab.keys());
|
const tabFrameIds = Array.from(subFrameOffsetsForTab.keys());
|
||||||
for (const frameId of tabFrameIds) {
|
for (const frameId of tabFrameIds) {
|
||||||
// if (frameId === sender.frameId) {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
|
|
||||||
subFrameOffsetsForTab.delete(frameId);
|
subFrameOffsetsForTab.delete(frameId);
|
||||||
await this.buildSubFrameOffsets(sender.tab, frameId, sender.url);
|
await this.buildSubFrameOffsets(sender, sender.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -416,14 +414,15 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clearUpdateInlineMenuPositionTimeout();
|
|
||||||
|
|
||||||
await this.rebuildSubFrameOffsets(sender);
|
await this.rebuildSubFrameOffsets(sender);
|
||||||
|
}
|
||||||
|
|
||||||
this.updateInlineMenuPositionTimeout = globalThis.setTimeout(
|
private delayedUpdateInlineMenuPosition(sender: chrome.runtime.MessageSender) {
|
||||||
() => this.updateInlineMenuPositionAfterSubFrameRebuild(sender),
|
this.clearDelayedUpdateInlineMenuPositionTimeout();
|
||||||
650,
|
this.delayedUpdateInlineMenuPositionTimeout = globalThis.setTimeout(async () => {
|
||||||
);
|
this.clearDelayedUpdateInlineMenuPositionTimeout();
|
||||||
|
await this.updateInlineMenuPositionAfterSubFrameRebuild(sender);
|
||||||
|
}, 650);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -583,9 +582,10 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearUpdateInlineMenuPositionTimeout() {
|
private clearDelayedUpdateInlineMenuPositionTimeout() {
|
||||||
if (this.updateInlineMenuPositionTimeout) {
|
if (this.delayedUpdateInlineMenuPositionTimeout) {
|
||||||
clearTimeout(this.updateInlineMenuPositionTimeout);
|
clearTimeout(this.delayedUpdateInlineMenuPositionTimeout);
|
||||||
|
this.delayedUpdateInlineMenuPositionTimeout = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -628,7 +628,16 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
{ overlayElement }: { overlayElement?: string },
|
{ overlayElement }: { overlayElement?: string },
|
||||||
sender: chrome.runtime.MessageSender,
|
sender: chrome.runtime.MessageSender,
|
||||||
) {
|
) {
|
||||||
if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) {
|
if (this.delayedUpdateInlineMenuPositionTimeout && this.isFieldCurrentlyFocused) {
|
||||||
|
this.closeInlineMenu(sender, { forceCloseInlineMenu: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!overlayElement ||
|
||||||
|
sender.tab.id !== this.focusedFieldData?.tabId ||
|
||||||
|
this.delayedUpdateInlineMenuPositionTimeout
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1159,12 +1168,12 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
|||||||
) => {
|
) => {
|
||||||
const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command];
|
const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command];
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageResponse = handler({ message, sender });
|
const messageResponse = handler({ message, sender });
|
||||||
if (typeof messageResponse === "undefined") {
|
if (typeof messageResponse === "undefined") {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.resolve(messageResponse)
|
Promise.resolve(messageResponse)
|
||||||
|
@ -178,12 +178,12 @@ class AutofillInit implements AutofillInitInterface {
|
|||||||
const command: string = message.command;
|
const command: string = message.command;
|
||||||
const handler: CallableFunction | undefined = this.getExtensionMessageHandler(command);
|
const handler: CallableFunction | undefined = this.getExtensionMessageHandler(command);
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageResponse = handler({ message, sender });
|
const messageResponse = handler({ message, sender });
|
||||||
if (typeof messageResponse === "undefined") {
|
if (typeof messageResponse === "undefined") {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Promise.resolve(messageResponse).then((response) => sendResponse(response));
|
void Promise.resolve(messageResponse).then((response) => sendResponse(response));
|
||||||
|
@ -295,12 +295,12 @@ export class Fido2Background implements Fido2BackgroundInterface {
|
|||||||
) => {
|
) => {
|
||||||
const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command];
|
const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command];
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageResponse = handler({ message, sender });
|
const messageResponse = handler({ message, sender });
|
||||||
if (typeof messageResponse === "undefined") {
|
if (typeof messageResponse === "undefined") {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.resolve(messageResponse)
|
Promise.resolve(messageResponse)
|
||||||
|
@ -29,7 +29,7 @@ export type AutofillOverlayContentExtensionMessageHandlers = {
|
|||||||
getSubFrameOffsets: ({ message }: AutofillExtensionMessageParam) => Promise<SubFrameOffsetData>;
|
getSubFrameOffsets: ({ message }: AutofillExtensionMessageParam) => Promise<SubFrameOffsetData>;
|
||||||
getSubFrameOffsetsFromWindowMessage: ({ message }: AutofillExtensionMessageParam) => void;
|
getSubFrameOffsetsFromWindowMessage: ({ message }: AutofillExtensionMessageParam) => void;
|
||||||
checkMostRecentlyFocusedFieldHasValue: () => boolean;
|
checkMostRecentlyFocusedFieldHasValue: () => boolean;
|
||||||
setupAutofillInlineMenuReflowObserver: () => void;
|
setupRebuildSubFrameOffsetsListeners: () => void;
|
||||||
destroyAutofillInlineMenuListeners: () => void;
|
destroyAutofillInlineMenuListeners: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,12 +18,7 @@ import {
|
|||||||
import AutofillField from "../models/autofill-field";
|
import AutofillField from "../models/autofill-field";
|
||||||
import AutofillPageDetails from "../models/autofill-page-details";
|
import AutofillPageDetails from "../models/autofill-page-details";
|
||||||
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
|
||||||
import {
|
import { elementIsFillableFormField, getAttributeBoolean, sendExtensionMessage } from "../utils";
|
||||||
elementIsFillableFormField,
|
|
||||||
getAttributeBoolean,
|
|
||||||
sendExtensionMessage,
|
|
||||||
throttle,
|
|
||||||
} from "../utils";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AutofillOverlayContentExtensionMessageHandlers,
|
AutofillOverlayContentExtensionMessageHandlers,
|
||||||
@ -67,7 +62,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
getSubFrameOffsetsFromWindowMessage: ({ message }) =>
|
getSubFrameOffsetsFromWindowMessage: ({ message }) =>
|
||||||
this.getSubFrameOffsetsFromWindowMessage(message),
|
this.getSubFrameOffsetsFromWindowMessage(message),
|
||||||
checkMostRecentlyFocusedFieldHasValue: () => this.mostRecentlyFocusedFieldHasValue(),
|
checkMostRecentlyFocusedFieldHasValue: () => this.mostRecentlyFocusedFieldHasValue(),
|
||||||
setupAutofillInlineMenuReflowObserver: () => this.setupPageReflowEventListeners(),
|
setupRebuildSubFrameOffsetsListeners: () => this.setupRebuildSubFrameOffsetsListeners(),
|
||||||
destroyAutofillInlineMenuListeners: () => this.destroy(),
|
destroyAutofillInlineMenuListeners: () => this.destroy(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -759,46 +754,248 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
this.inlineMenuVisibility = inlineMenuVisibility || AutofillOverlayVisibility.OnFieldFocus;
|
this.inlineMenuVisibility = inlineMenuVisibility || AutofillOverlayVisibility.OnFieldFocus;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupPageReflowEventListeners() {
|
/**
|
||||||
if (this.reflowPerformanceObserver || this.reflowMutationObserver) {
|
* Returns a value that indicates if we should hide the inline menu list due to a filled field.
|
||||||
return;
|
*
|
||||||
}
|
* @param formFieldElement - The form field element that triggered the focus event.
|
||||||
|
*/
|
||||||
|
private async hideInlineMenuListOnFilledField(
|
||||||
|
formFieldElement?: FillableFormFieldElement,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return (
|
||||||
|
formFieldElement?.value &&
|
||||||
|
((await this.isInlineMenuCiphersPopulated()) || !this.isUserAuthed())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ("PerformanceObserver" in window && "LayoutShift" in window) {
|
/**
|
||||||
this.reflowPerformanceObserver = new PerformanceObserver(
|
* Indicates whether the most recently focused field has a value.
|
||||||
throttle(this.updateSubFrameOffsetsFromLayoutShiftEvent.bind(this), 100),
|
*/
|
||||||
);
|
private mostRecentlyFocusedFieldHasValue() {
|
||||||
this.reflowPerformanceObserver.observe({ type: "layout-shift", buffered: true });
|
return Boolean((this.mostRecentlyFocusedField as FillableFormFieldElement)?.value);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
/**
|
||||||
}
|
* Updates the local reference to the inline menu visibility setting.
|
||||||
|
*
|
||||||
if (globalThis.window.top !== globalThis.window && this.formFieldElements.size > 0) {
|
* @param data - The data object from the extension message.
|
||||||
this.setupRebuildSubFrameOffsetsEventListeners();
|
*/
|
||||||
|
private updateInlineMenuVisibility({ data }: AutofillExtensionMessage) {
|
||||||
|
if (!isNaN(data?.inlineMenuVisibility)) {
|
||||||
|
this.inlineMenuVisibility = data.inlineMenuVisibility;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateSubFrameOffsetsFromLayoutShiftEvent = (list: any) => {
|
/**
|
||||||
const entries: any[] = list.getEntries();
|
* Checks if a field is currently filling within an frame in the tab.
|
||||||
for (let index = 0; index < entries.length; index++) {
|
*/
|
||||||
const entry = entries[index];
|
private async isFieldCurrentlyFilling() {
|
||||||
if (entry.sources?.length) {
|
return (await this.sendExtensionMessage("checkIsFieldCurrentlyFilling")) === true;
|
||||||
this.updateSubFrameForReflow();
|
}
|
||||||
return;
|
|
||||||
|
/**
|
||||||
|
* Checks if the inline menu button is visible at the top frame.
|
||||||
|
*/
|
||||||
|
private async isInlineMenuButtonVisible() {
|
||||||
|
return (await this.sendExtensionMessage("checkIsAutofillInlineMenuButtonVisible")) === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the inline menu list if visible at the top frame.
|
||||||
|
*/
|
||||||
|
private async isInlineMenuListVisible() {
|
||||||
|
return (await this.sendExtensionMessage("checkIsAutofillInlineMenuListVisible")) === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current tab contains ciphers that can be used to populate the inline menu.
|
||||||
|
*/
|
||||||
|
private async isInlineMenuCiphersPopulated() {
|
||||||
|
return (await this.sendExtensionMessage("checkIsInlineMenuCiphersPopulated")) === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers a validation to ensure that the inline menu is repositioned only when the
|
||||||
|
* current frame contains the focused field at any given depth level.
|
||||||
|
*/
|
||||||
|
private async checkShouldRepositionInlineMenu() {
|
||||||
|
return (await this.sendExtensionMessage("checkShouldRepositionInlineMenu")) === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the root node of the passed element and returns the active element within that root node.
|
||||||
|
*
|
||||||
|
* @param element - The element to get the root node active element for.
|
||||||
|
*/
|
||||||
|
private getRootNodeActiveElement(element: Element): Element {
|
||||||
|
if (!element) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentRoot = element.getRootNode() as ShadowRoot | Document;
|
||||||
|
return documentRoot?.activeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries all iframe elements within the document and returns the
|
||||||
|
* sub frame offsets for each iframe element.
|
||||||
|
*
|
||||||
|
* @param message - The message object from the extension.
|
||||||
|
*/
|
||||||
|
private async getSubFrameOffsets(
|
||||||
|
message: AutofillExtensionMessage,
|
||||||
|
): Promise<SubFrameOffsetData | null> {
|
||||||
|
const { subFrameUrl } = message;
|
||||||
|
const subFrameUrlWithoutTrailingSlash = subFrameUrl?.replace(/\/$/, "");
|
||||||
|
|
||||||
|
let iframeElement: HTMLIFrameElement | null = null;
|
||||||
|
const iframeElements = globalThis.document.querySelectorAll(
|
||||||
|
`iframe[src="${subFrameUrl}"], iframe[src="${subFrameUrlWithoutTrailingSlash}"]`,
|
||||||
|
) as NodeListOf<HTMLIFrameElement>;
|
||||||
|
if (iframeElements.length === 1) {
|
||||||
|
iframeElement = iframeElements[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!iframeElement) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.calculateSubFrameOffsets(iframeElement, subFrameUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posts a message to the parent frame to calculate the sub frame offset of the current frame.
|
||||||
|
*
|
||||||
|
* @param message - The message object from the extension.
|
||||||
|
*/
|
||||||
|
private getSubFrameOffsetsFromWindowMessage(message: any) {
|
||||||
|
globalThis.parent.postMessage(
|
||||||
|
{
|
||||||
|
command: "calculateSubFramePositioning",
|
||||||
|
subFrameData: {
|
||||||
|
url: window.location.href,
|
||||||
|
frameId: message.subFrameId,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
parentFrameIds: [],
|
||||||
|
subFrameDepth: 0,
|
||||||
|
} as SubFrameDataFromWindowMessage,
|
||||||
|
},
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the bounding rect for the queried frame and returns the
|
||||||
|
* offset data for the sub frame.
|
||||||
|
*
|
||||||
|
* @param iframeElement - The iframe element to calculate the sub frame offsets for.
|
||||||
|
* @param subFrameUrl - The URL of the sub frame.
|
||||||
|
* @param frameId - The frame ID of the sub frame.
|
||||||
|
*/
|
||||||
|
private calculateSubFrameOffsets(
|
||||||
|
iframeElement: HTMLIFrameElement,
|
||||||
|
subFrameUrl?: string,
|
||||||
|
frameId?: number,
|
||||||
|
): SubFrameOffsetData {
|
||||||
|
const iframeRect = iframeElement.getBoundingClientRect();
|
||||||
|
const iframeStyles = globalThis.getComputedStyle(iframeElement);
|
||||||
|
const paddingLeft = parseInt(iframeStyles.getPropertyValue("padding-left")) || 0;
|
||||||
|
const paddingTop = parseInt(iframeStyles.getPropertyValue("padding-top")) || 0;
|
||||||
|
const borderWidthLeft = parseInt(iframeStyles.getPropertyValue("border-left-width")) || 0;
|
||||||
|
const borderWidthTop = parseInt(iframeStyles.getPropertyValue("border-top-width")) || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: subFrameUrl,
|
||||||
|
frameId,
|
||||||
|
top: iframeRect.top + paddingTop + borderWidthTop,
|
||||||
|
left: iframeRect.left + paddingLeft + borderWidthLeft,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the sub frame positioning for the current frame
|
||||||
|
* through all parent frames until the top frame is reached.
|
||||||
|
*
|
||||||
|
* @param event - The message event.
|
||||||
|
*/
|
||||||
|
private calculateSubFramePositioning = async (event: MessageEvent) => {
|
||||||
|
const subFrameData: SubFrameDataFromWindowMessage = event.data.subFrameData;
|
||||||
|
|
||||||
|
subFrameData.subFrameDepth++;
|
||||||
|
if (subFrameData.subFrameDepth >= MAX_SUB_FRAME_DEPTH) {
|
||||||
|
void this.sendExtensionMessage("destroyAutofillInlineMenuListeners", { subFrameData });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let subFrameOffsets: SubFrameOffsetData;
|
||||||
|
const iframes = globalThis.document.querySelectorAll("iframe");
|
||||||
|
for (let i = 0; i < iframes.length; i++) {
|
||||||
|
if (iframes[i].contentWindow === event.source) {
|
||||||
|
const iframeElement = iframes[i];
|
||||||
|
subFrameOffsets = this.calculateSubFrameOffsets(
|
||||||
|
iframeElement,
|
||||||
|
subFrameData.url,
|
||||||
|
subFrameData.frameId,
|
||||||
|
);
|
||||||
|
|
||||||
|
subFrameData.top += subFrameOffsets.top;
|
||||||
|
subFrameData.left += subFrameOffsets.left;
|
||||||
|
|
||||||
|
const parentFrameId = await this.sendExtensionMessage("getCurrentTabFrameId");
|
||||||
|
if (typeof parentFrameId !== "undefined") {
|
||||||
|
subFrameData.parentFrameIds.push(parentFrameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (globalThis.window.self !== globalThis.window.top) {
|
||||||
|
globalThis.parent.postMessage({ command: "calculateSubFramePositioning", subFrameData }, "*");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void this.sendExtensionMessage("updateSubFrameData", { subFrameData });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up global event listeners and the mutation
|
||||||
|
* observer to facilitate required changes to the
|
||||||
|
* overlay elements.
|
||||||
|
*/
|
||||||
|
private setupGlobalEventListeners = () => {
|
||||||
|
globalThis.addEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent);
|
||||||
|
globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent);
|
||||||
|
globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
|
||||||
|
this.setOverlayRepositionEventListeners();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles window messages that are sent to the current frame. Will trigger a
|
||||||
|
* calculation of the sub frame offsets through the parent frame.
|
||||||
|
*
|
||||||
|
* @param event - The message event.
|
||||||
|
*/
|
||||||
|
private handleWindowMessageEvent = (event: MessageEvent) => {
|
||||||
|
if (event.data?.command === "calculateSubFramePositioning") {
|
||||||
|
void this.calculateSubFramePositioning(event);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private updateSubFrameForReflow = () => {
|
/**
|
||||||
// console.log("update sub frame reflow");
|
* Handles the visibility change event. This method will remove the
|
||||||
if (this.userInteractionEventTimeout) {
|
* autofill overlay if the document is not visible.
|
||||||
this.clearUserInteractionEventTimeout();
|
*/
|
||||||
void this.toggleInlineMenuHidden(false, true);
|
private handleVisibilityChangeEvent = () => {
|
||||||
void this.sendExtensionMessage("closeAutofillInlineMenu", {
|
if (!this.mostRecentlyFocusedField || globalThis.document.visibilityState === "visible") {
|
||||||
forceCloseInlineMenu: true,
|
return;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
void this.sendExtensionMessage("updateSubFrameOffsetsForReflowEvent");
|
|
||||||
|
this.unsetMostRecentlyFocusedField();
|
||||||
|
void this.sendExtensionMessage("closeAutofillInlineMenu", {
|
||||||
|
forceCloseInlineMenu: true,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -911,6 +1108,36 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setupRebuildSubFrameOffsetsListeners = () => {
|
||||||
|
if (globalThis.window.top === globalThis.window || this.formFieldElements.size < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.addEventListener(EVENTS.FOCUS, this.handleSubFrameFocusInEvent);
|
||||||
|
globalThis.document.body.addEventListener(EVENTS.MOUSEENTER, this.handleSubFrameFocusInEvent);
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleSubFrameFocusInEvent = () => {
|
||||||
|
this.updateSubFrameForReflow();
|
||||||
|
|
||||||
|
globalThis.removeEventListener(EVENTS.FOCUS, this.handleSubFrameFocusInEvent);
|
||||||
|
globalThis.document.body.removeEventListener(
|
||||||
|
EVENTS.MOUSEENTER,
|
||||||
|
this.handleSubFrameFocusInEvent,
|
||||||
|
);
|
||||||
|
globalThis.addEventListener(EVENTS.BLUR, this.setupRebuildSubFrameOffsetsListeners);
|
||||||
|
globalThis.document.body.addEventListener(
|
||||||
|
EVENTS.MOUSELEAVE,
|
||||||
|
this.setupRebuildSubFrameOffsetsListeners,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private updateSubFrameForReflow = () => {
|
||||||
|
this.clearUserInteractionEventTimeout();
|
||||||
|
this.clearRecalculateSubFrameOffsetsTimeout();
|
||||||
|
void this.sendExtensionMessage("updateSubFrameOffsetsForReflowEvent");
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the focused field is present within the bounds of the viewport.
|
* Checks if the focused field is present within the bounds of the viewport.
|
||||||
* If not present, the inline menu will be closed.
|
* If not present, the inline menu will be closed.
|
||||||
@ -924,274 +1151,6 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a value that indicates if we should hide the inline menu list due to a filled field.
|
|
||||||
*
|
|
||||||
* @param formFieldElement - The form field element that triggered the focus event.
|
|
||||||
*/
|
|
||||||
private async hideInlineMenuListOnFilledField(
|
|
||||||
formFieldElement?: FillableFormFieldElement,
|
|
||||||
): Promise<boolean> {
|
|
||||||
return (
|
|
||||||
formFieldElement?.value &&
|
|
||||||
((await this.isInlineMenuCiphersPopulated()) || !this.isUserAuthed())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates whether the most recently focused field has a value.
|
|
||||||
*/
|
|
||||||
private mostRecentlyFocusedFieldHasValue() {
|
|
||||||
return Boolean((this.mostRecentlyFocusedField as FillableFormFieldElement)?.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up global event listeners and the mutation
|
|
||||||
* observer to facilitate required changes to the
|
|
||||||
* overlay elements.
|
|
||||||
*/
|
|
||||||
private setupGlobalEventListeners = () => {
|
|
||||||
globalThis.addEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent);
|
|
||||||
globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent);
|
|
||||||
globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
|
|
||||||
this.setOverlayRepositionEventListeners();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the visibility change event. This method will remove the
|
|
||||||
* autofill overlay if the document is not visible.
|
|
||||||
*/
|
|
||||||
private handleVisibilityChangeEvent = () => {
|
|
||||||
if (!this.mostRecentlyFocusedField || globalThis.document.visibilityState === "visible") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.unsetMostRecentlyFocusedField();
|
|
||||||
void this.sendExtensionMessage("closeAutofillInlineMenu", {
|
|
||||||
forceCloseInlineMenu: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the root node of the passed element and returns the active element within that root node.
|
|
||||||
*
|
|
||||||
* @param element - The element to get the root node active element for.
|
|
||||||
*/
|
|
||||||
private getRootNodeActiveElement(element: Element): Element {
|
|
||||||
if (!element) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const documentRoot = element.getRootNode() as ShadowRoot | Document;
|
|
||||||
return documentRoot?.activeElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setupRebuildSubFrameOffsetsEventListeners = () => {
|
|
||||||
// console.log("setting up listeners");
|
|
||||||
globalThis.addEventListener(EVENTS.FOCUS, this.handleSubFrameFocusInEvent);
|
|
||||||
globalThis.document.body.addEventListener(EVENTS.MOUSEENTER, this.handleSubFrameFocusInEvent);
|
|
||||||
};
|
|
||||||
|
|
||||||
private handleSubFrameFocusInEvent = (event: FocusEvent) => {
|
|
||||||
// console.log("removing listeners", event);
|
|
||||||
this.updateSubFrameForReflow();
|
|
||||||
|
|
||||||
globalThis.removeEventListener(EVENTS.FOCUS, this.handleSubFrameFocusInEvent);
|
|
||||||
globalThis.document.body.removeEventListener(
|
|
||||||
EVENTS.MOUSEENTER,
|
|
||||||
this.handleSubFrameFocusInEvent,
|
|
||||||
);
|
|
||||||
globalThis.addEventListener(EVENTS.BLUR, this.handleSubFrameFocusOutEvent);
|
|
||||||
globalThis.document.body.addEventListener(EVENTS.MOUSELEAVE, this.handleSubFrameFocusOutEvent);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSubFrameFocusOutEvent = (event: FocusEvent) => {
|
|
||||||
// console.log(event);
|
|
||||||
this.setupRebuildSubFrameOffsetsEventListeners();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queries all iframe elements within the document and returns the
|
|
||||||
* sub frame offsets for each iframe element.
|
|
||||||
*
|
|
||||||
* @param message - The message object from the extension.
|
|
||||||
*/
|
|
||||||
private async getSubFrameOffsets(
|
|
||||||
message: AutofillExtensionMessage,
|
|
||||||
): Promise<SubFrameOffsetData | null> {
|
|
||||||
const { subFrameUrl } = message;
|
|
||||||
const subFrameUrlWithoutTrailingSlash = subFrameUrl?.replace(/\/$/, "");
|
|
||||||
|
|
||||||
let iframeElement: HTMLIFrameElement | null = null;
|
|
||||||
const iframeElements = globalThis.document.querySelectorAll(
|
|
||||||
`iframe[src="${subFrameUrl}"], iframe[src="${subFrameUrlWithoutTrailingSlash}"]`,
|
|
||||||
) as NodeListOf<HTMLIFrameElement>;
|
|
||||||
if (iframeElements.length === 1) {
|
|
||||||
iframeElement = iframeElements[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!iframeElement) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.calculateSubFrameOffsets(iframeElement, subFrameUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the bounding rect for the queried frame and returns the
|
|
||||||
* offset data for the sub frame.
|
|
||||||
*
|
|
||||||
* @param iframeElement - The iframe element to calculate the sub frame offsets for.
|
|
||||||
* @param subFrameUrl - The URL of the sub frame.
|
|
||||||
* @param frameId - The frame ID of the sub frame.
|
|
||||||
*/
|
|
||||||
private calculateSubFrameOffsets(
|
|
||||||
iframeElement: HTMLIFrameElement,
|
|
||||||
subFrameUrl?: string,
|
|
||||||
frameId?: number,
|
|
||||||
): SubFrameOffsetData {
|
|
||||||
const iframeRect = iframeElement.getBoundingClientRect();
|
|
||||||
const iframeStyles = globalThis.getComputedStyle(iframeElement);
|
|
||||||
const paddingLeft = parseInt(iframeStyles.getPropertyValue("padding-left")) || 0;
|
|
||||||
const paddingTop = parseInt(iframeStyles.getPropertyValue("padding-top")) || 0;
|
|
||||||
const borderWidthLeft = parseInt(iframeStyles.getPropertyValue("border-left-width")) || 0;
|
|
||||||
const borderWidthTop = parseInt(iframeStyles.getPropertyValue("border-top-width")) || 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: subFrameUrl,
|
|
||||||
frameId,
|
|
||||||
top: iframeRect.top + paddingTop + borderWidthTop,
|
|
||||||
left: iframeRect.left + paddingLeft + borderWidthLeft,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Posts a message to the parent frame to calculate the sub frame offset of the current frame.
|
|
||||||
*
|
|
||||||
* @param message - The message object from the extension.
|
|
||||||
*/
|
|
||||||
private getSubFrameOffsetsFromWindowMessage(message: any) {
|
|
||||||
globalThis.parent.postMessage(
|
|
||||||
{
|
|
||||||
command: "calculateSubFramePositioning",
|
|
||||||
subFrameData: {
|
|
||||||
url: window.location.href,
|
|
||||||
frameId: message.subFrameId,
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
parentFrameIds: [],
|
|
||||||
subFrameDepth: 0,
|
|
||||||
} as SubFrameDataFromWindowMessage,
|
|
||||||
},
|
|
||||||
"*",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles window messages that are sent to the current frame. Will trigger a
|
|
||||||
* calculation of the sub frame offsets through the parent frame.
|
|
||||||
*
|
|
||||||
* @param event - The message event.
|
|
||||||
*/
|
|
||||||
private handleWindowMessageEvent = (event: MessageEvent) => {
|
|
||||||
if (event.data?.command === "calculateSubFramePositioning") {
|
|
||||||
void this.calculateSubFramePositioning(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the sub frame positioning for the current frame
|
|
||||||
* through all parent frames until the top frame is reached.
|
|
||||||
*
|
|
||||||
* @param event - The message event.
|
|
||||||
*/
|
|
||||||
private calculateSubFramePositioning = async (event: MessageEvent) => {
|
|
||||||
const subFrameData: SubFrameDataFromWindowMessage = event.data.subFrameData;
|
|
||||||
|
|
||||||
subFrameData.subFrameDepth++;
|
|
||||||
if (subFrameData.subFrameDepth >= MAX_SUB_FRAME_DEPTH) {
|
|
||||||
void this.sendExtensionMessage("destroyAutofillInlineMenuListeners", { subFrameData });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let subFrameOffsets: SubFrameOffsetData;
|
|
||||||
const iframes = globalThis.document.querySelectorAll("iframe");
|
|
||||||
for (let i = 0; i < iframes.length; i++) {
|
|
||||||
if (iframes[i].contentWindow === event.source) {
|
|
||||||
const iframeElement = iframes[i];
|
|
||||||
subFrameOffsets = this.calculateSubFrameOffsets(
|
|
||||||
iframeElement,
|
|
||||||
subFrameData.url,
|
|
||||||
subFrameData.frameId,
|
|
||||||
);
|
|
||||||
|
|
||||||
subFrameData.top += subFrameOffsets.top;
|
|
||||||
subFrameData.left += subFrameOffsets.left;
|
|
||||||
|
|
||||||
const parentFrameId = await this.sendExtensionMessage("getCurrentTabFrameId");
|
|
||||||
if (typeof parentFrameId !== "undefined") {
|
|
||||||
subFrameData.parentFrameIds.push(parentFrameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (globalThis.window.self !== globalThis.window.top) {
|
|
||||||
globalThis.parent.postMessage({ command: "calculateSubFramePositioning", subFrameData }, "*");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void this.sendExtensionMessage("updateSubFrameData", { subFrameData });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the local reference to the inline menu visibility setting.
|
|
||||||
*
|
|
||||||
* @param data - The data object from the extension message.
|
|
||||||
*/
|
|
||||||
private updateInlineMenuVisibility({ data }: AutofillExtensionMessage) {
|
|
||||||
if (!isNaN(data?.inlineMenuVisibility)) {
|
|
||||||
this.inlineMenuVisibility = data.inlineMenuVisibility;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a field is currently filling within an frame in the tab.
|
|
||||||
*/
|
|
||||||
private async isFieldCurrentlyFilling() {
|
|
||||||
return (await this.sendExtensionMessage("checkIsFieldCurrentlyFilling")) === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the inline menu button is visible at the top frame.
|
|
||||||
*/
|
|
||||||
private async isInlineMenuButtonVisible() {
|
|
||||||
return (await this.sendExtensionMessage("checkIsAutofillInlineMenuButtonVisible")) === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the inline menu list if visible at the top frame.
|
|
||||||
*/
|
|
||||||
private async isInlineMenuListVisible() {
|
|
||||||
return (await this.sendExtensionMessage("checkIsAutofillInlineMenuListVisible")) === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the current tab contains ciphers that can be used to populate the inline menu.
|
|
||||||
*/
|
|
||||||
private async isInlineMenuCiphersPopulated() {
|
|
||||||
return (await this.sendExtensionMessage("checkIsInlineMenuCiphersPopulated")) === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Triggers a validation to ensure that the inline menu is repositioned only when the
|
|
||||||
* current frame contains the focused field at any given depth level.
|
|
||||||
*/
|
|
||||||
private async checkShouldRepositionInlineMenu() {
|
|
||||||
return (await this.sendExtensionMessage("checkShouldRepositionInlineMenu")) === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroys the autofill overlay content service. This method will
|
* Destroys the autofill overlay content service. This method will
|
||||||
* disconnect the mutation observers and remove all event listeners.
|
* disconnect the mutation observers and remove all event listeners.
|
||||||
|
@ -330,14 +330,3 @@ export function getPropertyOrAttribute(element: HTMLElement, attributeName: stri
|
|||||||
|
|
||||||
return element.getAttribute(attributeName);
|
return element.getAttribute(attributeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function throttle(callback: () => void, limit: number) {
|
|
||||||
let waitingDelay = false;
|
|
||||||
return function (...args: unknown[]) {
|
|
||||||
if (!waitingDelay) {
|
|
||||||
callback.apply(this, args);
|
|
||||||
waitingDelay = true;
|
|
||||||
globalThis.setTimeout(() => (waitingDelay = false), limit);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user