From aaa585c9926d412e46c606a8aef3bfecd3200c9b Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Mon, 17 Jun 2024 07:49:17 -0500 Subject: [PATCH] [PM-5189] Reworking how we handle updating ciphers within nested sub frames --- .../background/notification.background.ts | 4 +- .../background/overlay.background.spec.ts | 2 +- .../autofill/background/overlay.background.ts | 55 +- .../src/autofill/content/autofill-init.ts | 4 +- .../fido2/background/fido2.background.ts | 4 +- .../autofill-overlay-content.service.ts | 2 +- .../autofill-overlay-content.service.ts | 569 ++++++++---------- apps/browser/src/autofill/utils/index.ts | 11 - 8 files changed, 304 insertions(+), 347 deletions(-) diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 40224642fe..ae50d179ba 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -772,12 +772,12 @@ export default class NotificationBackground { ) => { const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; if (!handler) { - return; + return null; } const messageResponse = handler({ message, sender }); if (typeof messageResponse === "undefined") { - return; + return null; } Promise.resolve(messageResponse) diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index a257372f82..74368a1098 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -394,7 +394,7 @@ describe("OverlayBackground", () => { it("triggers an update of the inline menu position after rebuilding sub frames", async () => { jest.useFakeTimers(); - overlayBackground["updateInlineMenuPositionTimeout"] = setTimeout(jest.fn, 650); + overlayBackground["delayedUpdateInlineMenuPositionTimeout"] = setTimeout(jest.fn, 650); const sender = mock({ tab, frameId: middleFrameId }); jest.spyOn(overlayBackground as any, "updateInlineMenuPositionAfterSubFrameRebuild"); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index cc2f03e286..fc10533ab5 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -63,7 +63,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { private inlineMenuCiphers: Map = new Map(); private inlineMenuPageTranslations: Record; private inlineMenuFadeInTimeout: number | NodeJS.Timeout; - private updateInlineMenuPositionTimeout: number | NodeJS.Timeout; + private delayedUpdateInlineMenuPositionTimeout: number | NodeJS.Timeout; private delayedCloseTimeout: number | NodeJS.Timeout; private focusedFieldData: FocusedFieldData; private isFieldCurrentlyFocused: boolean = false; @@ -252,9 +252,9 @@ export class OverlayBackground implements OverlayBackgroundInterface { }; 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, { - command: "setupAutofillInlineMenuReflowObserver", + command: "setupRebuildSubFrameOffsetsListeners", }); } @@ -292,6 +292,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id]; if (subFrameOffsetsForTab) { 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 * to calculate the position of the inline menu list and button. * - * @param tab - The tab that the sub frame is associated with - * @param frameId - The frame ID of the sub frame + * @param sender - The sender of the message * @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; + const { tab, frameId } = sender; const tabId = tab.id; let subFrameOffsetsForTab = this.subFrameOffsetsForTab[tabId]; if (!subFrameOffsetsForTab) { @@ -358,6 +359,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { } subFrameOffsetsForTab.set(frameId, subFrameData); + this.delayedUpdateInlineMenuPosition(sender); } /** @@ -392,12 +394,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { if (subFrameOffsetsForTab) { const tabFrameIds = Array.from(subFrameOffsetsForTab.keys()); for (const frameId of tabFrameIds) { - // if (frameId === sender.frameId) { - // continue; - // } - 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; } - this.clearUpdateInlineMenuPositionTimeout(); - await this.rebuildSubFrameOffsets(sender); + } - this.updateInlineMenuPositionTimeout = globalThis.setTimeout( - () => this.updateInlineMenuPositionAfterSubFrameRebuild(sender), - 650, - ); + private delayedUpdateInlineMenuPosition(sender: chrome.runtime.MessageSender) { + this.clearDelayedUpdateInlineMenuPositionTimeout(); + this.delayedUpdateInlineMenuPositionTimeout = globalThis.setTimeout(async () => { + this.clearDelayedUpdateInlineMenuPositionTimeout(); + await this.updateInlineMenuPositionAfterSubFrameRebuild(sender); + }, 650); } /** @@ -583,9 +582,10 @@ export class OverlayBackground implements OverlayBackgroundInterface { } } - private clearUpdateInlineMenuPositionTimeout() { - if (this.updateInlineMenuPositionTimeout) { - clearTimeout(this.updateInlineMenuPositionTimeout); + private clearDelayedUpdateInlineMenuPositionTimeout() { + if (this.delayedUpdateInlineMenuPositionTimeout) { + clearTimeout(this.delayedUpdateInlineMenuPositionTimeout); + this.delayedUpdateInlineMenuPositionTimeout = null; } } @@ -628,7 +628,16 @@ export class OverlayBackground implements OverlayBackgroundInterface { { overlayElement }: { overlayElement?: string }, 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; } @@ -1159,12 +1168,12 @@ export class OverlayBackground implements OverlayBackgroundInterface { ) => { const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; if (!handler) { - return; + return null; } const messageResponse = handler({ message, sender }); if (typeof messageResponse === "undefined") { - return; + return null; } Promise.resolve(messageResponse) diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts index cdb0d99533..70f815d223 100644 --- a/apps/browser/src/autofill/content/autofill-init.ts +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -178,12 +178,12 @@ class AutofillInit implements AutofillInitInterface { const command: string = message.command; const handler: CallableFunction | undefined = this.getExtensionMessageHandler(command); if (!handler) { - return; + return null; } const messageResponse = handler({ message, sender }); if (typeof messageResponse === "undefined") { - return; + return null; } void Promise.resolve(messageResponse).then((response) => sendResponse(response)); diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.ts b/apps/browser/src/autofill/fido2/background/fido2.background.ts index 7066d7938c..ad4a9b0135 100644 --- a/apps/browser/src/autofill/fido2/background/fido2.background.ts +++ b/apps/browser/src/autofill/fido2/background/fido2.background.ts @@ -295,12 +295,12 @@ export class Fido2Background implements Fido2BackgroundInterface { ) => { const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; if (!handler) { - return; + return null; } const messageResponse = handler({ message, sender }); if (typeof messageResponse === "undefined") { - return; + return null; } Promise.resolve(messageResponse) diff --git a/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts index f259d6a63f..4619cd65fd 100644 --- a/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/abstractions/autofill-overlay-content.service.ts @@ -29,7 +29,7 @@ export type AutofillOverlayContentExtensionMessageHandlers = { getSubFrameOffsets: ({ message }: AutofillExtensionMessageParam) => Promise; getSubFrameOffsetsFromWindowMessage: ({ message }: AutofillExtensionMessageParam) => void; checkMostRecentlyFocusedFieldHasValue: () => boolean; - setupAutofillInlineMenuReflowObserver: () => void; + setupRebuildSubFrameOffsetsListeners: () => void; destroyAutofillInlineMenuListeners: () => void; }; diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 766d13aab0..41f86a7c74 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -18,12 +18,7 @@ import { import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types"; -import { - elementIsFillableFormField, - getAttributeBoolean, - sendExtensionMessage, - throttle, -} from "../utils"; +import { elementIsFillableFormField, getAttributeBoolean, sendExtensionMessage } from "../utils"; import { AutofillOverlayContentExtensionMessageHandlers, @@ -67,7 +62,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ getSubFrameOffsetsFromWindowMessage: ({ message }) => this.getSubFrameOffsetsFromWindowMessage(message), checkMostRecentlyFocusedFieldHasValue: () => this.mostRecentlyFocusedFieldHasValue(), - setupAutofillInlineMenuReflowObserver: () => this.setupPageReflowEventListeners(), + setupRebuildSubFrameOffsetsListeners: () => this.setupRebuildSubFrameOffsetsListeners(), destroyAutofillInlineMenuListeners: () => this.destroy(), }; @@ -759,46 +754,248 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ this.inlineMenuVisibility = inlineMenuVisibility || AutofillOverlayVisibility.OnFieldFocus; } - private setupPageReflowEventListeners() { - if (this.reflowPerformanceObserver || this.reflowMutationObserver) { - return; - } + /** + * 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 { + return ( + formFieldElement?.value && + ((await this.isInlineMenuCiphersPopulated()) || !this.isUserAuthed()) + ); + } - if ("PerformanceObserver" in window && "LayoutShift" in window) { - this.reflowPerformanceObserver = new PerformanceObserver( - throttle(this.updateSubFrameOffsetsFromLayoutShiftEvent.bind(this), 100), - ); - this.reflowPerformanceObserver.observe({ type: "layout-shift", buffered: true }); + /** + * Indicates whether the most recently focused field has a value. + */ + private mostRecentlyFocusedFieldHasValue() { + return Boolean((this.mostRecentlyFocusedField as FillableFormFieldElement)?.value); + } - return; - } - - if (globalThis.window.top !== globalThis.window && this.formFieldElements.size > 0) { - this.setupRebuildSubFrameOffsetsEventListeners(); + /** + * 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; } } - private updateSubFrameOffsetsFromLayoutShiftEvent = (list: any) => { - const entries: any[] = list.getEntries(); - for (let index = 0; index < entries.length; index++) { - const entry = entries[index]; - if (entry.sources?.length) { - this.updateSubFrameForReflow(); - return; + /** + * 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; + } + + /** + * 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 { + 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; + 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"); - if (this.userInteractionEventTimeout) { - this.clearUserInteractionEventTimeout(); - void this.toggleInlineMenuHidden(false, true); - void this.sendExtensionMessage("closeAutofillInlineMenu", { - forceCloseInlineMenu: true, - }); + /** + * 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; } - 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. * 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 { - 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 { - 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; - 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 * disconnect the mutation observers and remove all event listeners. diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index 829cb8a4ee..26f984d85a 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -330,14 +330,3 @@ export function getPropertyOrAttribute(element: HTMLElement, attributeName: stri 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); - } - }; -}