1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 01:33:33 +00:00

[PM-5189] Separating the inline menu UI elements from the base AutofillOverlayContentService and setting up messaging to allow for propagation of those elements

This commit is contained in:
Cesar Gonzalez
2024-03-20 12:36:25 -05:00
parent a3b12581f7
commit 0af95bb2be
13 changed files with 3213 additions and 2716 deletions

View File

@@ -45,7 +45,8 @@ type OverlayBackgroundExtensionMessage = {
sender?: string; sender?: string;
details?: AutofillPageDetails; details?: AutofillPageDetails;
overlayElement?: string; overlayElement?: string;
display?: string; forceCloseOverlay?: boolean;
isOverlayHidden?: boolean;
data?: LockedVaultPendingNotificationsData; data?: LockedVaultPendingNotificationsData;
} & OverlayAddNewItemMessage; } & OverlayAddNewItemMessage;
@@ -59,6 +60,8 @@ type OverlayPortMessage = {
type FocusedFieldData = { type FocusedFieldData = {
focusedFieldStyles: Partial<CSSStyleDeclaration>; focusedFieldStyles: Partial<CSSStyleDeclaration>;
focusedFieldRects: Partial<DOMRect>; focusedFieldRects: Partial<DOMRect>;
tabId?: number;
frameId?: number;
}; };
type OverlayCipherData = { type OverlayCipherData = {
@@ -83,13 +86,17 @@ type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSende
type OverlayBackgroundExtensionMessageHandlers = { type OverlayBackgroundExtensionMessageHandlers = {
[key: string]: CallableFunction; [key: string]: CallableFunction;
openAutofillOverlay: () => void; openAutofillOverlay: () => void;
closeAutofillOverlay: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
autofillOverlayElementClosed: ({ message }: BackgroundMessageParam) => void; autofillOverlayElementClosed: ({ message }: BackgroundMessageParam) => void;
autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
getAutofillOverlayVisibility: () => void; getAutofillOverlayVisibility: () => void;
checkAutofillOverlayFocused: () => void; checkAutofillOverlayFocused: () => void;
focusAutofillOverlayList: () => void; focusAutofillOverlayList: () => void;
updateAutofillOverlayPosition: ({ message }: BackgroundMessageParam) => void; updateAutofillOverlayPosition: ({
updateAutofillOverlayHidden: ({ message }: BackgroundMessageParam) => void; message,
sender,
}: BackgroundOnMessageHandlerParams) => Promise<void>;
updateAutofillOverlayHidden: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
unlockCompleted: ({ message }: BackgroundMessageParam) => void; unlockCompleted: ({ message }: BackgroundMessageParam) => void;

View File

@@ -783,17 +783,24 @@ describe("OverlayBackground", () => {
}); });
}); });
it("will post a message to the overlay list facilitating an update of the list's position", () => { it("will post a message to the overlay list facilitating an update of the list's position", async () => {
const sender = mock<chrome.runtime.MessageSender>({ tab: { id: 1 } });
const focusedFieldData = createFocusedFieldDataMock(); const focusedFieldData = createFocusedFieldDataMock();
sendExtensionRuntimeMessage({ command: "updateFocusedFieldData", focusedFieldData }); sendExtensionRuntimeMessage({ command: "updateFocusedFieldData", focusedFieldData });
overlayBackground["updateOverlayPosition"]({ await overlayBackground["updateOverlayPosition"](
{
overlayElement: AutofillOverlayElement.List, overlayElement: AutofillOverlayElement.List,
}); },
sendExtensionRuntimeMessage({ sender,
);
sendExtensionRuntimeMessage(
{
command: "updateAutofillOverlayPosition", command: "updateAutofillOverlayPosition",
overlayElement: AutofillOverlayElement.List, overlayElement: AutofillOverlayElement.List,
}); },
sender,
);
expect(listPortSpy.postMessage).toHaveBeenCalledWith({ expect(listPortSpy.postMessage).toHaveBeenCalledWith({
command: "updateIframePosition", command: "updateIframePosition",

View File

@@ -56,17 +56,21 @@ class OverlayBackground implements OverlayBackgroundInterface {
private overlayButtonPort: chrome.runtime.Port; private overlayButtonPort: chrome.runtime.Port;
private overlayListPort: chrome.runtime.Port; private overlayListPort: chrome.runtime.Port;
private focusedFieldData: FocusedFieldData; private focusedFieldData: FocusedFieldData;
private isFieldCurrentlyFocused: boolean;
private isCurrentlyFilling: boolean;
private overlayPageTranslations: Record<string, string>; private overlayPageTranslations: Record<string, string>;
private readonly iconsServerUrl: string; private readonly iconsServerUrl: string;
private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = {
openAutofillOverlay: () => this.openOverlay(false), openAutofillOverlay: () => this.openOverlay(false),
closeAutofillOverlay: ({ message, sender }) => this.closeOverlay(sender, message),
autofillOverlayElementClosed: ({ message }) => this.overlayElementClosed(message), autofillOverlayElementClosed: ({ message }) => this.overlayElementClosed(message),
autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender), autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender),
getAutofillOverlayVisibility: () => this.getOverlayVisibility(), getAutofillOverlayVisibility: () => this.getOverlayVisibility(),
checkAutofillOverlayFocused: () => this.checkOverlayFocused(), checkAutofillOverlayFocused: () => this.checkOverlayFocused(),
focusAutofillOverlayList: () => this.focusOverlayList(), focusAutofillOverlayList: () => this.focusOverlayList(),
updateAutofillOverlayPosition: ({ message }) => this.updateOverlayPosition(message), updateAutofillOverlayPosition: ({ message, sender }) =>
updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message), this.updateOverlayPosition(message, sender),
updateAutofillOverlayHidden: ({ message, sender }) => this.updateOverlayHidden(message, sender),
updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender), updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender),
collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
unlockCompleted: ({ message }) => this.unlockCompleted(message), unlockCompleted: ({ message }) => this.unlockCompleted(message),
@@ -75,14 +79,16 @@ class OverlayBackground implements OverlayBackgroundInterface {
}; };
private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = { private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = {
overlayButtonClicked: ({ port }) => this.handleOverlayButtonClicked(port), overlayButtonClicked: ({ port }) => this.handleOverlayButtonClicked(port),
closeAutofillOverlay: ({ port }) => this.closeOverlay(port), closeAutofillOverlay: ({ port }) => this.closeOverlay(port.sender),
forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), forceCloseAutofillOverlay: ({ port }) =>
this.closeOverlay(port.sender, { forceCloseOverlay: true }),
overlayPageBlurred: () => this.checkOverlayListFocused(), overlayPageBlurred: () => this.checkOverlayListFocused(),
redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port),
}; };
private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = { private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = {
checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(), checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(),
forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), forceCloseAutofillOverlay: ({ port }) =>
this.closeOverlay(port.sender, { forceCloseOverlay: true }),
overlayPageBlurred: () => this.checkOverlayButtonFocused(), overlayPageBlurred: () => this.checkOverlayButtonFocused(),
unlockVault: ({ port }) => this.unlockVault(port), unlockVault: ({ port }) => this.unlockVault(port),
fillSelectedListItem: ({ message, port }) => this.fillSelectedOverlayListItem(message, port), fillSelectedListItem: ({ message, port }) => this.fillSelectedOverlayListItem(message, port),
@@ -216,7 +222,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
}; };
if (pageDetails.frameId !== 0 && pageDetails.details.fields.length) { if (pageDetails.frameId !== 0 && pageDetails.details.fields.length) {
void this.buildSubFrameOffset(pageDetails); void this.buildSubFrameOffsets(pageDetails);
} }
const pageDetailsMap = this.pageDetailsForTab[sender.tab.id]; const pageDetailsMap = this.pageDetailsForTab[sender.tab.id];
@@ -228,7 +234,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
pageDetailsMap.set(sender.frameId, pageDetails); pageDetailsMap.set(sender.frameId, pageDetails);
} }
private async buildSubFrameOffset({ tab, frameId, details }: PageDetail) { private async buildSubFrameOffsets({ tab, frameId, details }: PageDetail) {
const tabId = tab.id; const tabId = tab.id;
let subFrameOffsetsForTab = this.subFrameOffsetsForTab[tabId]; let subFrameOffsetsForTab = this.subFrameOffsetsForTab[tabId];
if (!subFrameOffsetsForTab) { if (!subFrameOffsetsForTab) {
@@ -335,12 +341,42 @@ class OverlayBackground implements OverlayBackgroundInterface {
* Sends a message to the sender tab to close the autofill overlay. * Sends a message to the sender tab to close the autofill overlay.
* *
* @param sender - The sender of the port message * @param sender - The sender of the port message
* @param forceCloseOverlay - Identifies whether the overlay should be force closed * @param forceCloseOverlay - Identifies whether the overlay should be forced closed
* @param overlayElement - The overlay element to close, either the list or button
*/ */
private closeOverlay({ sender }: chrome.runtime.Port, forceCloseOverlay = false) { private closeOverlay(
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. sender: chrome.runtime.MessageSender,
// eslint-disable-next-line @typescript-eslint/no-floating-promises {
BrowserApi.tabSendMessageData(sender.tab, "closeAutofillOverlay", { forceCloseOverlay }); forceCloseOverlay,
overlayElement,
}: { forceCloseOverlay?: boolean; overlayElement?: string } = {},
) {
if (forceCloseOverlay) {
void BrowserApi.tabSendMessage(sender.tab, { command: "closeInlineMenu" }, { frameId: 0 });
return;
}
if (this.isFieldCurrentlyFocused) {
return;
}
if (this.isCurrentlyFilling) {
void BrowserApi.tabSendMessage(
sender.tab,
{
command: "closeInlineMenu",
overlayElement: AutofillOverlayElement.List,
},
{ frameId: 0 },
);
return;
}
void BrowserApi.tabSendMessage(
sender.tab,
{ command: "closeInlineMenu", overlayElement },
{ frameId: 0 },
);
} }
/** /**
@@ -366,16 +402,32 @@ class OverlayBackground implements OverlayBackgroundInterface {
* is based on the focused field's position and dimensions. * is based on the focused field's position and dimensions.
* *
* @param overlayElement - The overlay element to update, either the list or button * @param overlayElement - The overlay element to update, either the list or button
* @param sender - The sender of the extension message
*/ */
private updateOverlayPosition({ overlayElement }: { overlayElement?: string }) { private async updateOverlayPosition(
{ overlayElement }: { overlayElement?: string },
sender: chrome.runtime.MessageSender,
) {
if (!overlayElement) { if (!overlayElement) {
return; return;
} }
await BrowserApi.tabSendMessage(
sender.tab,
{ command: "updateInlineMenuElementsPosition" },
{ frameId: 0 },
);
const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id];
let subFrameOffsets: SubFrameOffsetData;
if (subFrameOffsetsForTab) {
subFrameOffsets = subFrameOffsetsForTab.get(sender.frameId);
}
if (overlayElement === AutofillOverlayElement.Button) { if (overlayElement === AutofillOverlayElement.Button) {
this.overlayButtonPort?.postMessage({ this.overlayButtonPort?.postMessage({
command: "updateIframePosition", command: "updateIframePosition",
styles: this.getOverlayButtonPosition(), styles: this.getOverlayButtonPosition(subFrameOffsets),
}); });
return; return;
@@ -383,7 +435,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
this.overlayListPort?.postMessage({ this.overlayListPort?.postMessage({
command: "updateIframePosition", command: "updateIframePosition",
styles: this.getOverlayListPosition(), styles: this.getOverlayListPosition(subFrameOffsets),
}); });
} }
@@ -391,11 +443,14 @@ class OverlayBackground implements OverlayBackgroundInterface {
* Gets the position of the focused field and calculates the position * Gets the position of the focused field and calculates the position
* of the overlay button based on the focused field's position and dimensions. * of the overlay button based on the focused field's position and dimensions.
*/ */
private getOverlayButtonPosition() { private getOverlayButtonPosition(subFrameOffsets: SubFrameOffsetData) {
if (!this.focusedFieldData) { if (!this.focusedFieldData) {
return; return;
} }
const subFrameTopOffset = subFrameOffsets?.top || 0;
const subFrameLeftOffset = subFrameOffsets?.left || 0;
const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; const { top, left, width, height } = this.focusedFieldData.focusedFieldRects;
const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles; const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles;
let elementOffset = height * 0.37; let elementOffset = height * 0.37;
@@ -403,15 +458,15 @@ class OverlayBackground implements OverlayBackgroundInterface {
elementOffset = height >= 50 ? height * 0.47 : height * 0.42; elementOffset = height >= 50 ? height * 0.47 : height * 0.42;
} }
const elementHeight = height - elementOffset;
const elementTopPosition = top + elementOffset / 2;
let elementLeftPosition = left + width - height + elementOffset / 2;
const fieldPaddingRight = parseInt(paddingRight, 10); const fieldPaddingRight = parseInt(paddingRight, 10);
const fieldPaddingLeft = parseInt(paddingLeft, 10); const fieldPaddingLeft = parseInt(paddingLeft, 10);
if (fieldPaddingRight > fieldPaddingLeft) { const elementHeight = height - elementOffset;
elementLeftPosition = left + width - height - (fieldPaddingRight - elementOffset + 2);
} const elementTopPosition = subFrameTopOffset + top + elementOffset / 2;
const elementLeftPosition =
fieldPaddingRight > fieldPaddingLeft
? subFrameLeftOffset + left + width - height - (fieldPaddingRight - elementOffset + 2)
: subFrameLeftOffset + left + width - height + elementOffset / 2;
return { return {
top: `${Math.round(elementTopPosition)}px`, top: `${Math.round(elementTopPosition)}px`,
@@ -425,16 +480,19 @@ class OverlayBackground implements OverlayBackgroundInterface {
* Gets the position of the focused field and calculates the position * Gets the position of the focused field and calculates the position
* of the overlay list based on the focused field's position and dimensions. * of the overlay list based on the focused field's position and dimensions.
*/ */
private getOverlayListPosition() { private getOverlayListPosition(subFrameOffsets: SubFrameOffsetData) {
if (!this.focusedFieldData) { if (!this.focusedFieldData) {
return; return;
} }
const subFrameTopOffset = subFrameOffsets?.top || 0;
const subFrameLeftOffset = subFrameOffsets?.left || 0;
const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; const { top, left, width, height } = this.focusedFieldData.focusedFieldRects;
return { return {
width: `${Math.round(width)}px`, width: `${Math.round(width)}px`,
top: `${Math.round(top + height)}px`, top: `${Math.round(top + height + subFrameTopOffset)}px`,
left: `${Math.round(left)}px`, left: `${Math.round(left + subFrameLeftOffset)}px`,
}; };
} }
@@ -442,26 +500,34 @@ class OverlayBackground implements OverlayBackgroundInterface {
* Sets the focused field data to the data passed in the extension message. * Sets the focused field data to the data passed in the extension message.
* *
* @param focusedFieldData - Contains the rects and styles of the focused field. * @param focusedFieldData - Contains the rects and styles of the focused field.
* @param sender - The sender of the extension message
*/ */
private setFocusedFieldData( private setFocusedFieldData(
{ focusedFieldData }: OverlayBackgroundExtensionMessage, { focusedFieldData }: OverlayBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender, sender: chrome.runtime.MessageSender,
) { ) {
this.focusedFieldData = focusedFieldData; this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id, frameId: sender.frameId };
} }
/** /**
* Updates the overlay's visibility based on the display property passed in the extension message. * Updates the overlay's visibility based on the display property passed in the extension message.
* *
* @param display - The display property of the overlay, either "block" or "none" * @param display - The display property of the overlay, either "block" or "none"
* @param sender - The sender of the extension message
*/ */
private updateOverlayHidden({ display }: OverlayBackgroundExtensionMessage) { private updateOverlayHidden(
if (!display) { { isOverlayHidden }: OverlayBackgroundExtensionMessage,
return; sender: chrome.runtime.MessageSender,
} ) {
const display = isOverlayHidden ? "none" : "block";
const portMessage = { command: "updateOverlayHidden", styles: { display } }; const portMessage = { command: "updateOverlayHidden", styles: { display } };
void BrowserApi.tabSendMessage(
sender.tab,
{ command: "toggleInlineMenuHidden", isInlineMenuHidden: isOverlayHidden },
{ frameId: 0 },
);
this.overlayButtonPort?.postMessage(portMessage); this.overlayButtonPort?.postMessage(portMessage);
this.overlayListPort?.postMessage(portMessage); this.overlayListPort?.postMessage(portMessage);
} }
@@ -547,7 +613,7 @@ class OverlayBackground implements OverlayBackgroundInterface {
private async unlockVault(port: chrome.runtime.Port) { private async unlockVault(port: chrome.runtime.Port) {
const { sender } = port; const { sender } = port;
this.closeOverlay(port); this.closeOverlay(port.sender);
const retryMessage: LockedVaultPendingNotificationsData = { const retryMessage: LockedVaultPendingNotificationsData = {
commandToRetry: { message: { command: "openAutofillOverlay" }, sender }, commandToRetry: { message: { command: "openAutofillOverlay" }, sender },
target: "overlay.background", target: "overlay.background",
@@ -761,11 +827,14 @@ class OverlayBackground implements OverlayBackgroundInterface {
translations: this.getTranslations(), translations: this.getTranslations(),
ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null, ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null,
}); });
this.updateOverlayPosition({ void this.updateOverlayPosition(
{
overlayElement: isOverlayListPort overlayElement: isOverlayListPort
? AutofillOverlayElement.List ? AutofillOverlayElement.List
: AutofillOverlayElement.Button, : AutofillOverlayElement.Button,
}); },
port.sender,
);
}; };
/** /**

View File

@@ -3,7 +3,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
import { SubFrameOffsetData } from "../../background/abstractions/overlay.background"; import { SubFrameOffsetData } from "../../background/abstractions/overlay.background";
import AutofillScript from "../../models/autofill-script"; import AutofillScript from "../../models/autofill-script";
type AutofillExtensionMessage = { export type AutofillExtensionMessage = {
command: string; command: string;
tab?: chrome.tabs.Tab; tab?: chrome.tabs.Tab;
sender?: string; sender?: string;
@@ -12,6 +12,7 @@ type AutofillExtensionMessage = {
subFrameUrl?: string; subFrameUrl?: string;
pageDetailsUrl?: string; pageDetailsUrl?: string;
ciphers?: any; ciphers?: any;
isInlineMenuHidden?: boolean;
data?: { data?: {
authStatus?: AuthenticationStatus; authStatus?: AuthenticationStatus;
isFocusingFieldElement?: boolean; isFocusingFieldElement?: boolean;
@@ -23,15 +24,14 @@ type AutofillExtensionMessage = {
}; };
}; };
type AutofillExtensionMessageParam = { message: AutofillExtensionMessage }; export type AutofillExtensionMessageParam = { message: AutofillExtensionMessage };
type AutofillExtensionMessageHandlers = { export type AutofillExtensionMessageHandlers = {
[key: string]: CallableFunction; [key: string]: CallableFunction;
collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void; collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void;
collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void; collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void;
fillForm: ({ message }: AutofillExtensionMessageParam) => void; fillForm: ({ message }: AutofillExtensionMessageParam) => void;
openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void;
closeAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void;
addNewVaultItemFromOverlay: () => void; addNewVaultItemFromOverlay: () => void;
redirectOverlayFocusOut: ({ message }: AutofillExtensionMessageParam) => void; redirectOverlayFocusOut: ({ message }: AutofillExtensionMessageParam) => void;
updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void; updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void;
@@ -41,9 +41,7 @@ type AutofillExtensionMessageHandlers = {
getSubFrameOffsets: ({ message }: AutofillExtensionMessageParam) => Promise<SubFrameOffsetData>; getSubFrameOffsets: ({ message }: AutofillExtensionMessageParam) => Promise<SubFrameOffsetData>;
}; };
interface AutofillInit { export interface AutofillInit {
init(): void; init(): void;
destroy(): void; destroy(): void;
} }
export { AutofillExtensionMessage, AutofillExtensionMessageHandlers, AutofillInit };

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import { SubFrameOffsetData } from "../background/abstractions/overlay.background"; import { SubFrameOffsetData } from "../background/abstractions/overlay.background";
import AutofillPageDetails from "../models/autofill-page-details"; import AutofillPageDetails from "../models/autofill-page-details";
import { InlineMenuElements } from "../overlay/abstractions/inline-menu-elements";
import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service";
import CollectAutofillContentService from "../services/collect-autofill-content.service"; import CollectAutofillContentService from "../services/collect-autofill-content.service";
import DomElementVisibilityService from "../services/dom-element-visibility.service"; import DomElementVisibilityService from "../services/dom-element-visibility.service";
@@ -14,6 +15,7 @@ import {
class AutofillInit implements AutofillInitInterface { class AutofillInit implements AutofillInitInterface {
private readonly autofillOverlayContentService: AutofillOverlayContentService | undefined; private readonly autofillOverlayContentService: AutofillOverlayContentService | undefined;
private readonly inlineMenuElements: InlineMenuElements | undefined;
private readonly domElementVisibilityService: DomElementVisibilityService; private readonly domElementVisibilityService: DomElementVisibilityService;
private readonly collectAutofillContentService: CollectAutofillContentService; private readonly collectAutofillContentService: CollectAutofillContentService;
private readonly insertAutofillContentService: InsertAutofillContentService; private readonly insertAutofillContentService: InsertAutofillContentService;
@@ -22,7 +24,7 @@ class AutofillInit implements AutofillInitInterface {
collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true), collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true),
fillForm: ({ message }) => this.fillForm(message), fillForm: ({ message }) => this.fillForm(message),
openAutofillOverlay: ({ message }) => this.openAutofillOverlay(message), openAutofillOverlay: ({ message }) => this.openAutofillOverlay(message),
closeAutofillOverlay: ({ message }) => this.removeAutofillOverlay(message), // closeAutofillOverlay: ({ message }) => this.removeAutofillOverlay(message),
addNewVaultItemFromOverlay: () => this.addNewVaultItemFromOverlay(), addNewVaultItemFromOverlay: () => this.addNewVaultItemFromOverlay(),
redirectOverlayFocusOut: ({ message }) => this.redirectOverlayFocusOut(message), redirectOverlayFocusOut: ({ message }) => this.redirectOverlayFocusOut(message),
updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message), updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message),
@@ -37,9 +39,28 @@ class AutofillInit implements AutofillInitInterface {
* CollectAutofillContentService and InsertAutofillContentService classes. * CollectAutofillContentService and InsertAutofillContentService classes.
* *
* @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined.
* @param inlineMenuElements - The inline menu elements, potentially undefined.
*/ */
constructor(autofillOverlayContentService?: AutofillOverlayContentService) { constructor(
autofillOverlayContentService?: AutofillOverlayContentService,
inlineMenuElements?: InlineMenuElements,
) {
this.autofillOverlayContentService = autofillOverlayContentService; this.autofillOverlayContentService = autofillOverlayContentService;
if (this.autofillOverlayContentService) {
this.extensionMessageHandlers = Object.assign(
this.extensionMessageHandlers,
this.autofillOverlayContentService.extensionMessageHandlers,
);
}
this.inlineMenuElements = inlineMenuElements;
if (this.inlineMenuElements) {
this.extensionMessageHandlers = Object.assign(
this.extensionMessageHandlers,
this.inlineMenuElements.extensionMessageHandlers,
);
}
this.domElementVisibilityService = new DomElementVisibilityService(); this.domElementVisibilityService = new DomElementVisibilityService();
this.collectAutofillContentService = new CollectAutofillContentService( this.collectAutofillContentService = new CollectAutofillContentService(
this.domElementVisibilityService, this.domElementVisibilityService,
@@ -169,33 +190,7 @@ class AutofillInit implements AutofillInitInterface {
} }
this.autofillOverlayContentService.blurMostRecentOverlayField(); this.autofillOverlayContentService.blurMostRecentOverlayField();
this.removeAutofillOverlay(); void sendExtensionMessage("closeAutofillOverlay");
}
/**
* Removes the autofill overlay if the field is not currently focused.
* If the autofill is currently filling, only the overlay list will be
* removed.
*/
private removeAutofillOverlay(message?: AutofillExtensionMessage) {
if (message?.data?.forceCloseOverlay) {
this.autofillOverlayContentService?.removeAutofillOverlay();
return;
}
if (
!this.autofillOverlayContentService ||
this.autofillOverlayContentService.isFieldCurrentlyFocused
) {
return;
}
if (this.autofillOverlayContentService.isCurrentlyFilling) {
this.autofillOverlayContentService.removeAutofillOverlayList();
return;
}
this.autofillOverlayContentService.removeAutofillOverlay();
} }
/** /**
@@ -257,7 +252,6 @@ class AutofillInit implements AutofillInitInterface {
const { subFrameUrl } = message; const { subFrameUrl } = message;
const subFrameUrlWithoutTrailingSlash = subFrameUrl?.replace(/\/$/, ""); const subFrameUrlWithoutTrailingSlash = subFrameUrl?.replace(/\/$/, "");
// query iframe based on src attribute
const iframeElement = document.querySelector( const iframeElement = document.querySelector(
`iframe[src^="${subFrameUrlWithoutTrailingSlash}"]`, `iframe[src^="${subFrameUrlWithoutTrailingSlash}"]`,
); );

View File

@@ -1,3 +1,4 @@
import { InlineMenuElements } from "../overlay/content/inline-menu-elements";
import AutofillOverlayContentService from "../services/autofill-overlay-content.service"; import AutofillOverlayContentService from "../services/autofill-overlay-content.service";
import { setupAutofillInitDisconnectAction } from "../utils"; import { setupAutofillInitDisconnectAction } from "../utils";
@@ -6,7 +7,14 @@ import AutofillInit from "./autofill-init";
(function (windowContext) { (function (windowContext) {
if (!windowContext.bitwardenAutofillInit) { if (!windowContext.bitwardenAutofillInit) {
const autofillOverlayContentService = new AutofillOverlayContentService(); const autofillOverlayContentService = new AutofillOverlayContentService();
windowContext.bitwardenAutofillInit = new AutofillInit(autofillOverlayContentService); let inlineMenuElements: InlineMenuElements;
if (globalThis.parent === globalThis.top) {
inlineMenuElements = new InlineMenuElements();
}
windowContext.bitwardenAutofillInit = new AutofillInit(
autofillOverlayContentService,
inlineMenuElements,
);
setupAutofillInitDisconnectAction(windowContext); setupAutofillInitDisconnectAction(windowContext);
windowContext.bitwardenAutofillInit.init(); windowContext.bitwardenAutofillInit.init();

View File

@@ -0,0 +1,12 @@
import { AutofillExtensionMessageParam } from "../../content/abstractions/autofill-init";
export type InlineMenuExtensionMessageHandlers = {
[key: string]: CallableFunction;
closeInlineMenu: ({ message }: AutofillExtensionMessageParam) => void;
updateInlineMenuElementsPosition: () => Promise<[void, void]>;
toggleInlineMenuHidden: ({ message }: AutofillExtensionMessageParam) => void;
};
export interface InlineMenuElements {
extensionMessageHandlers: InlineMenuExtensionMessageHandlers;
}

View File

@@ -0,0 +1,408 @@
import {
sendExtensionMessage,
generateRandomCustomElementName,
setElementStyles,
} from "../../utils";
import { AutofillOverlayElement } from "../../utils/autofill-overlay.enum";
import {
InlineMenuExtensionMessageHandlers,
InlineMenuElements as InlineMenuElementsInterface,
} from "../abstractions/inline-menu-elements";
import AutofillOverlayButtonIframe from "../iframe-content/autofill-overlay-button-iframe";
import AutofillOverlayListIframe from "../iframe-content/autofill-overlay-list-iframe";
export class InlineMenuElements implements InlineMenuElementsInterface {
private readonly sendExtensionMessage = sendExtensionMessage;
private readonly generateRandomCustomElementName = generateRandomCustomElementName;
private readonly setElementStyles = setElementStyles;
private isFirefoxBrowser =
globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 ||
globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1;
private buttonElement: HTMLElement;
private listElement: HTMLElement;
private isButtonVisible = false;
private isListVisible = false;
private overlayElementsMutationObserver: MutationObserver;
private bodyElementMutationObserver: MutationObserver;
private documentElementMutationObserver: MutationObserver;
private mutationObserverIterations = 0;
private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout;
private readonly customElementDefaultStyles: Partial<CSSStyleDeclaration> = {
all: "initial",
position: "fixed",
display: "block",
zIndex: "2147483647",
};
private readonly _extensionMessageHandlers: InlineMenuExtensionMessageHandlers = {
closeInlineMenu: ({ message }) => this.removeInlineMenu(),
updateInlineMenuElementsPosition: () => this.updateInlineMenuElementsPosition(),
toggleInlineMenuHidden: ({ message }) =>
this.toggleInlineMenuHidden(message.isInlineMenuHidden),
};
constructor() {
this.setupMutationObserver();
}
get extensionMessageHandlers() {
return this._extensionMessageHandlers;
}
/**
* Sends a message that facilitates hiding the overlay elements.
*
* @param isHidden - Indicates if the overlay elements should be hidden.
*/
private toggleInlineMenuHidden(isHidden: boolean) {
this.isButtonVisible = !!this.buttonElement && !isHidden;
this.isListVisible = !!this.listElement && !isHidden;
}
/**
* Removes the autofill overlay from the page. This will initially
* unobserve the body element to ensure the mutation observer no
* longer triggers.
*/
private removeInlineMenu = () => {
this.removeBodyElementObserver();
this.removeInlineMenuButton();
this.removeInlineMenuList();
};
/**
* Removes the overlay button from the DOM if it is currently present. Will
* also remove the overlay reposition event listeners.
*/
private removeInlineMenuButton() {
if (!this.buttonElement) {
return;
}
this.buttonElement.remove();
this.isButtonVisible = false;
void this.sendExtensionMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.Button,
});
}
/**
* Removes the overlay list from the DOM if it is currently present.
*/
private removeInlineMenuList() {
if (!this.listElement) {
return;
}
this.listElement.remove();
this.isListVisible = false;
void this.sendExtensionMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.List,
});
}
/**
* Updates the position of both the overlay button and overlay list.
*/
private async updateInlineMenuElementsPosition() {
return Promise.all([this.updateButtonPosition(), this.updateListPosition()]);
}
/**
* Updates the position of the overlay button.
*/
private async updateButtonPosition(): Promise<void> {
return new Promise((resolve) => {
if (!this.buttonElement) {
this.createButton();
this.updateCustomElementDefaultStyles(this.buttonElement);
}
if (!this.isButtonVisible) {
this.appendOverlayElementToBody(this.buttonElement);
this.isButtonVisible = true;
}
resolve();
});
}
/**
* Updates the position of the overlay list.
*/
private async updateListPosition(): Promise<void> {
return new Promise((resolve) => {
if (!this.listElement) {
this.createList();
this.updateCustomElementDefaultStyles(this.listElement);
}
if (!this.isListVisible) {
this.appendOverlayElementToBody(this.listElement);
this.isListVisible = true;
}
resolve();
});
}
/**
* Appends the overlay element to the body element. This method will also
* observe the body element to ensure that the overlay element is not
* interfered with by any DOM changes.
*
* @param element - The overlay element to append to the body element.
*/
private appendOverlayElementToBody(element: HTMLElement) {
this.observeBodyElement();
globalThis.document.body.appendChild(element);
}
/**
* Creates the autofill overlay button element. Will not attempt
* to create the element if it already exists in the DOM.
*/
private createButton() {
if (this.buttonElement) {
return;
}
if (this.isFirefoxBrowser) {
this.buttonElement = globalThis.document.createElement("div");
new AutofillOverlayButtonIframe(this.buttonElement);
return;
}
const customElementName = this.generateRandomCustomElementName();
globalThis.customElements?.define(
customElementName,
class extends HTMLElement {
constructor() {
super();
new AutofillOverlayButtonIframe(this);
}
},
);
this.buttonElement = globalThis.document.createElement(customElementName);
}
/**
* Creates the autofill overlay list element. Will not attempt
* to create the element if it already exists in the DOM.
*/
private createList() {
if (this.listElement) {
return;
}
if (this.isFirefoxBrowser) {
this.listElement = globalThis.document.createElement("div");
new AutofillOverlayListIframe(this.listElement);
return;
}
const customElementName = this.generateRandomCustomElementName();
globalThis.customElements?.define(
customElementName,
class extends HTMLElement {
constructor() {
super();
new AutofillOverlayListIframe(this);
}
},
);
this.listElement = globalThis.document.createElement(customElementName);
}
/**
* Updates the default styles for the custom element. This method will
* remove any styles that are added to the custom element by other methods.
*
* @param element - The custom element to update the default styles for.
*/
private updateCustomElementDefaultStyles(element: HTMLElement) {
this.unobserveCustomElements();
setElementStyles(element, this.customElementDefaultStyles, true);
this.observeCustomElements();
}
/**
* Sets up mutation observers for the overlay elements, the body element, and the
* document element. The mutation observers are used to remove any styles that are
* added to the overlay elements by the website. They are also used to ensure that
* the overlay elements are always present at the bottom of the body element.
*/
private setupMutationObserver = () => {
this.overlayElementsMutationObserver = new MutationObserver(
this.handleOverlayElementMutationObserverUpdate,
);
this.bodyElementMutationObserver = new MutationObserver(
this.handleBodyElementMutationObserverUpdate,
);
};
/**
* Sets up mutation observers to verify that the overlay
* elements are not modified by the website.
*/
private observeCustomElements() {
if (this.buttonElement) {
this.overlayElementsMutationObserver?.observe(this.buttonElement, {
attributes: true,
});
}
if (this.listElement) {
this.overlayElementsMutationObserver?.observe(this.listElement, { attributes: true });
}
}
/**
* Disconnects the mutation observers that are used to verify that the overlay
* elements are not modified by the website.
*/
private unobserveCustomElements() {
this.overlayElementsMutationObserver?.disconnect();
}
/**
* Sets up a mutation observer for the body element. The mutation observer is used
* to ensure that the overlay elements are always present at the bottom of the body
* element.
*/
private observeBodyElement() {
this.bodyElementMutationObserver?.observe(globalThis.document.body, { childList: true });
}
/**
* Disconnects the mutation observer for the body element.
*/
private removeBodyElementObserver() {
this.bodyElementMutationObserver?.disconnect();
}
/**
* Handles the mutation observer update for the overlay elements. This method will
* remove any attributes or styles that might be added to the overlay elements by
* a separate process within the website where this script is injected.
*
* @param mutationRecord - The mutation record that triggered the update.
*/
private handleOverlayElementMutationObserverUpdate = (mutationRecord: MutationRecord[]) => {
if (this.isTriggeringExcessiveMutationObserverIterations()) {
return;
}
for (let recordIndex = 0; recordIndex < mutationRecord.length; recordIndex++) {
const record = mutationRecord[recordIndex];
if (record.type !== "attributes") {
continue;
}
const element = record.target as HTMLElement;
if (record.attributeName !== "style") {
this.removeModifiedElementAttributes(element);
continue;
}
element.removeAttribute("style");
this.updateCustomElementDefaultStyles(element);
}
};
/**
* Removes all elements from a passed overlay
* element except for the style attribute.
*
* @param element - The element to remove the attributes from.
*/
private removeModifiedElementAttributes(element: HTMLElement) {
const attributes = Array.from(element.attributes);
for (let attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) {
const attribute = attributes[attributeIndex];
if (attribute.name === "style") {
continue;
}
element.removeAttribute(attribute.name);
}
}
/**
* Handles the mutation observer update for the body element. This method will
* ensure that the overlay elements are always present at the bottom of the body
* element.
*/
private handleBodyElementMutationObserverUpdate = () => {
if (
(!this.buttonElement && !this.listElement) ||
this.isTriggeringExcessiveMutationObserverIterations()
) {
return;
}
const lastChild = globalThis.document.body.lastElementChild;
const secondToLastChild = lastChild?.previousElementSibling;
const lastChildIsOverlayList = lastChild === this.listElement;
const lastChildIsOverlayButton = lastChild === this.buttonElement;
const secondToLastChildIsOverlayButton = secondToLastChild === this.buttonElement;
if (
(lastChildIsOverlayList && secondToLastChildIsOverlayButton) ||
(lastChildIsOverlayButton && !this.isListVisible)
) {
return;
}
if (
(lastChildIsOverlayList && !secondToLastChildIsOverlayButton) ||
(lastChildIsOverlayButton && this.isListVisible)
) {
globalThis.document.body.insertBefore(this.buttonElement, this.listElement);
return;
}
globalThis.document.body.insertBefore(lastChild, this.buttonElement);
};
/**
* Identifies if the mutation observer is triggering excessive iterations.
* Will trigger a blur of the most recently focused field and remove the
* autofill overlay if any set mutation observer is triggering
* excessive iterations.
*/
private isTriggeringExcessiveMutationObserverIterations() {
if (this.mutationObserverIterationsResetTimeout) {
clearTimeout(this.mutationObserverIterationsResetTimeout);
}
this.mutationObserverIterations++;
this.mutationObserverIterationsResetTimeout = setTimeout(
() => (this.mutationObserverIterations = 0),
2000,
);
if (this.mutationObserverIterations > 100) {
clearTimeout(this.mutationObserverIterationsResetTimeout);
this.mutationObserverIterations = 0;
void this.sendExtensionMessage("blurMostRecentOverlayField");
this.removeInlineMenu();
return true;
}
return false;
}
destroy() {
this.documentElementMutationObserver?.disconnect();
}
}

View File

@@ -245,7 +245,7 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
} }
this.updateElementStyles(this.iframe, position); this.updateElementStyles(this.iframe, position);
setTimeout(() => this.updateElementStyles(this.iframe, { opacity: "1" }), 0); setTimeout(() => this.updateElementStyles(this.iframe, { opacity: "1" }), 75);
this.announceAriaAlert(); this.announceAriaAlert();
} }

View File

@@ -3,32 +3,36 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio
import AutofillField from "../../models/autofill-field"; import AutofillField from "../../models/autofill-field";
import { ElementWithOpId, FormFieldElement } from "../../types"; import { ElementWithOpId, FormFieldElement } from "../../types";
type OpenAutofillOverlayOptions = { export type OpenAutofillOverlayOptions = {
isFocusingFieldElement?: boolean; isFocusingFieldElement?: boolean;
isOpeningFullOverlay?: boolean; isOpeningFullOverlay?: boolean;
authStatus?: AuthenticationStatus; authStatus?: AuthenticationStatus;
}; };
interface AutofillOverlayContentService { export type AutofillOverlayContentExtensionMessageHandlers = {
[key: string]: CallableFunction;
blurMostRecentOverlayField: () => void;
};
export interface AutofillOverlayContentService {
isFieldCurrentlyFocused: boolean; isFieldCurrentlyFocused: boolean;
isCurrentlyFilling: boolean; isCurrentlyFilling: boolean;
isOverlayCiphersPopulated: boolean; isOverlayCiphersPopulated: boolean;
pageDetailsUpdateRequired: boolean; pageDetailsUpdateRequired: boolean;
autofillOverlayVisibility: number; autofillOverlayVisibility: number;
extensionMessageHandlers: any;
init(): void; init(): void;
setupAutofillOverlayListenerOnField( setupAutofillOverlayListenerOnField(
autofillFieldElement: ElementWithOpId<FormFieldElement>, autofillFieldElement: ElementWithOpId<FormFieldElement>,
autofillFieldData: AutofillField, autofillFieldData: AutofillField,
): Promise<void>; ): Promise<void>;
openAutofillOverlay(options: OpenAutofillOverlayOptions): void; openAutofillOverlay(options: OpenAutofillOverlayOptions): void;
removeAutofillOverlay(): void; // removeAutofillOverlay(): void;
removeAutofillOverlayButton(): void; // removeAutofillOverlayButton(): void;
removeAutofillOverlayList(): void; // removeAutofillOverlayList(): void;
addNewVaultItem(): void; addNewVaultItem(): void;
redirectOverlayFocusOut(direction: "previous" | "next"): void; redirectOverlayFocusOut(direction: "previous" | "next"): void;
focusMostRecentOverlayField(): void; focusMostRecentOverlayField(): void;
blurMostRecentOverlayField(): void; blurMostRecentOverlayField(): void;
destroy(): void; destroy(): void;
} }
export { OpenAutofillOverlayOptions, AutofillOverlayContentService };

View File

@@ -7,18 +7,19 @@ import { EVENTS, AutofillOverlayVisibility } from "@bitwarden/common/autofill/co
import { FocusedFieldData } from "../background/abstractions/overlay.background"; import { FocusedFieldData } from "../background/abstractions/overlay.background";
import AutofillField from "../models/autofill-field"; import AutofillField from "../models/autofill-field";
import AutofillOverlayButtonIframe from "../overlay/iframe-content/autofill-overlay-button-iframe"; // import AutofillOverlayButtonIframe from "../overlay/iframe-content/autofill-overlay-button-iframe";
import AutofillOverlayListIframe from "../overlay/iframe-content/autofill-overlay-list-iframe"; // import AutofillOverlayListIframe from "../overlay/iframe-content/autofill-overlay-list-iframe";
import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types"; import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types";
import { import {
elementIsFillableFormField, elementIsFillableFormField,
generateRandomCustomElementName, // generateRandomCustomElementName,
sendExtensionMessage, sendExtensionMessage,
setElementStyles, // setElementStyles,
} from "../utils"; } from "../utils";
import { AutofillOverlayElement, RedirectFocusDirection } from "../utils/autofill-overlay.enum"; import { AutofillOverlayElement, RedirectFocusDirection } from "../utils/autofill-overlay.enum";
import { import {
AutofillOverlayContentExtensionMessageHandlers,
AutofillOverlayContentService as AutofillOverlayContentServiceInterface, AutofillOverlayContentService as AutofillOverlayContentServiceInterface,
OpenAutofillOverlayOptions, OpenAutofillOverlayOptions,
} from "./abstractions/autofill-overlay-content.service"; } from "./abstractions/autofill-overlay-content.service";
@@ -30,10 +31,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
isOverlayCiphersPopulated = false; isOverlayCiphersPopulated = false;
pageDetailsUpdateRequired = false; pageDetailsUpdateRequired = false;
autofillOverlayVisibility: number; autofillOverlayVisibility: number;
private isFirefoxBrowser = // private isFirefoxBrowser =
globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 || // globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 ||
globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1; // globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1;
private readonly generateRandomCustomElementName = generateRandomCustomElementName; // private readonly generateRandomCustomElementName = generateRandomCustomElementName;
private readonly findTabs = tabbable; private readonly findTabs = tabbable;
private readonly sendExtensionMessage = sendExtensionMessage; private readonly sendExtensionMessage = sendExtensionMessage;
private formFieldElements: Set<ElementWithOpId<FormFieldElement>> = new Set([]); private formFieldElements: Set<ElementWithOpId<FormFieldElement>> = new Set([]);
@@ -43,24 +44,27 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
private focusableElements: FocusableElement[] = []; private focusableElements: FocusableElement[] = [];
private isOverlayButtonVisible = false; private isOverlayButtonVisible = false;
private isOverlayListVisible = false; private isOverlayListVisible = false;
private overlayButtonElement: HTMLElement; // private overlayButtonElement: HTMLElement;
private overlayListElement: HTMLElement; // private overlayListElement: HTMLElement;
private mostRecentlyFocusedField: ElementWithOpId<FormFieldElement>; private mostRecentlyFocusedField: ElementWithOpId<FormFieldElement>;
private focusedFieldData: FocusedFieldData; private focusedFieldData: FocusedFieldData;
private userInteractionEventTimeout: number | NodeJS.Timeout; private userInteractionEventTimeout: number | NodeJS.Timeout;
private overlayElementsMutationObserver: MutationObserver; // private overlayElementsMutationObserver: MutationObserver;
private bodyElementMutationObserver: MutationObserver; // private bodyElementMutationObserver: MutationObserver;
private documentElementMutationObserver: MutationObserver; // private documentElementMutationObserver: MutationObserver;
private mutationObserverIterations = 0; // private mutationObserverIterations = 0;
private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout; // private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout;
private autofillFieldKeywordsMap: WeakMap<AutofillField, string> = new WeakMap(); private autofillFieldKeywordsMap: WeakMap<AutofillField, string> = new WeakMap();
private eventHandlersMemo: { [key: string]: EventListener } = {}; private eventHandlersMemo: { [key: string]: EventListener } = {};
private readonly customElementDefaultStyles: Partial<CSSStyleDeclaration> = { readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = {
all: "initial", blurMostRecentOverlayField: () => this.blurMostRecentOverlayField,
position: "fixed",
display: "block",
zIndex: "2147483647",
}; };
// private readonly customElementDefaultStyles: Partial<CSSStyleDeclaration> = {
// all: "initial",
// position: "fixed",
// display: "block",
// zIndex: "2147483647",
// };
/** /**
* Initializes the autofill overlay content service by setting up the mutation observers. * Initializes the autofill overlay content service by setting up the mutation observers.
@@ -123,9 +127,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
} }
if (this.pageDetailsUpdateRequired) { if (this.pageDetailsUpdateRequired) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. void this.sendExtensionMessage("bgCollectPageDetails", {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendExtensionMessage("bgCollectPageDetails", {
sender: "autofillOverlayContentService", sender: "autofillOverlayContentService",
}); });
this.pageDetailsUpdateRequired = false; this.pageDetailsUpdateRequired = false;
@@ -164,52 +166,52 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
this.mostRecentlyFocusedField?.blur(); this.mostRecentlyFocusedField?.blur();
} }
/** // /**
* Removes the autofill overlay from the page. This will initially // * Removes the autofill overlay from the page. This will initially
* unobserve the body element to ensure the mutation observer no // * unobserve the body element to ensure the mutation observer no
* longer triggers. // * longer triggers.
*/ // */
removeAutofillOverlay = () => { // removeAutofillOverlay = () => {
this.removeBodyElementObserver(); // this.removeBodyElementObserver();
this.removeAutofillOverlayButton(); // this.removeAutofillOverlayButton();
this.removeAutofillOverlayList(); // this.removeAutofillOverlayList();
}; // };
/** // /**
* Removes the overlay button from the DOM if it is currently present. Will // * Removes the overlay button from the DOM if it is currently present. Will
* also remove the overlay reposition event listeners. // * also remove the overlay reposition event listeners.
*/ // */
removeAutofillOverlayButton() { // removeAutofillOverlayButton() {
if (!this.overlayButtonElement) { // if (!this.overlayButtonElement) {
return; // return;
} // }
//
this.overlayButtonElement.remove(); // this.overlayButtonElement.remove();
this.isOverlayButtonVisible = false; // this.isOverlayButtonVisible = false;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendExtensionMessage("autofillOverlayElementClosed", { // this.sendExtensionMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.Button, // overlayElement: AutofillOverlayElement.Button,
}); // });
this.removeOverlayRepositionEventListeners(); // this.removeOverlayRepositionEventListeners();
} // }
//
/** // /**
* Removes the overlay list from the DOM if it is currently present. // * Removes the overlay list from the DOM if it is currently present.
*/ // */
removeAutofillOverlayList() { // removeAutofillOverlayList() {
if (!this.overlayListElement) { // if (!this.overlayListElement) {
return; // return;
} // }
//
this.overlayListElement.remove(); // this.overlayListElement.remove();
this.isOverlayListVisible = false; // this.isOverlayListVisible = false;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendExtensionMessage("autofillOverlayElementClosed", { // this.sendExtensionMessage("autofillOverlayElementClosed", {
overlayElement: AutofillOverlayElement.List, // overlayElement: AutofillOverlayElement.List,
}); // });
} // }
/** /**
* Formats any found user filled fields for a login cipher and sends a message * Formats any found user filled fields for a login cipher and sends a message
@@ -227,9 +229,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
hostname: globalThis.document.location.hostname, hostname: globalThis.document.location.hostname,
}; };
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. void this.sendExtensionMessage("autofillOverlayAddNewVaultItem", { login });
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendExtensionMessage("autofillOverlayAddNewVaultItem", { login });
} }
/** /**
@@ -246,7 +246,8 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
if (direction === RedirectFocusDirection.Current) { if (direction === RedirectFocusDirection.Current) {
this.focusMostRecentOverlayField(); this.focusMostRecentOverlayField();
setTimeout(this.removeAutofillOverlay, 100); // setTimeout(this.removeAutofillOverlay, 100);
setTimeout(() => void this.sendExtensionMessage("closeAutofillOverlay"), 100);
return; return;
} }
@@ -340,9 +341,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
*/ */
private handleFormFieldBlurEvent = () => { private handleFormFieldBlurEvent = () => {
this.isFieldCurrentlyFocused = false; this.isFieldCurrentlyFocused = false;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. void this.sendExtensionMessage("checkAutofillOverlayFocused");
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendExtensionMessage("checkAutofillOverlayFocused");
}; };
/** /**
@@ -356,7 +355,8 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
private handleFormFieldKeyupEvent = (event: KeyboardEvent) => { private handleFormFieldKeyupEvent = (event: KeyboardEvent) => {
const eventCode = event.code; const eventCode = event.code;
if (eventCode === "Escape") { if (eventCode === "Escape") {
this.removeAutofillOverlay(); // this.removeAutofillOverlay();
void this.sendExtensionMessage("closeAutofillOverlay");
return; return;
} }
@@ -420,7 +420,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
this.storeModifiedFormElement(formFieldElement); this.storeModifiedFormElement(formFieldElement);
if (formFieldElement.value && (this.isOverlayCiphersPopulated || !this.isUserAuthed())) { if (formFieldElement.value && (this.isOverlayCiphersPopulated || !this.isUserAuthed())) {
this.removeAutofillOverlayList(); // this.removeAutofillOverlayList();
void this.sendExtensionMessage("closeAutofillOverlay", {
overlayElement: AutofillOverlayElement.List,
});
return; return;
} }
@@ -508,7 +511,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
this.autofillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick || this.autofillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick ||
(formElementHasValue && initiallyFocusedField !== this.mostRecentlyFocusedField) (formElementHasValue && initiallyFocusedField !== this.mostRecentlyFocusedField)
) { ) {
this.removeAutofillOverlayList(); // this.removeAutofillOverlayList();
void this.sendExtensionMessage("closeAutofillOverlay", {
overlayElement: AutofillOverlayElement.List,
});
} }
if (!formElementHasValue || (!this.isOverlayCiphersPopulated && this.isUserAuthed())) { if (!formElementHasValue || (!this.isOverlayCiphersPopulated && this.isUserAuthed())) {
@@ -595,19 +601,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
* Updates the position of the overlay button. * Updates the position of the overlay button.
*/ */
private updateOverlayButtonPosition() { private updateOverlayButtonPosition() {
if (!this.overlayButtonElement) { void this.sendExtensionMessage("updateAutofillOverlayPosition", {
this.createAutofillOverlayButton();
this.updateCustomElementDefaultStyles(this.overlayButtonElement);
}
if (!this.isOverlayButtonVisible) {
this.appendOverlayElementToBody(this.overlayButtonElement);
this.isOverlayButtonVisible = true;
this.setOverlayRepositionEventListeners();
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendExtensionMessage("updateAutofillOverlayPosition", {
overlayElement: AutofillOverlayElement.Button, overlayElement: AutofillOverlayElement.Button,
}); });
} }
@@ -616,34 +610,22 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
* Updates the position of the overlay list. * Updates the position of the overlay list.
*/ */
private updateOverlayListPosition() { private updateOverlayListPosition() {
if (!this.overlayListElement) { void this.sendExtensionMessage("updateAutofillOverlayPosition", {
this.createAutofillOverlayList();
this.updateCustomElementDefaultStyles(this.overlayListElement);
}
if (!this.isOverlayListVisible) {
this.appendOverlayElementToBody(this.overlayListElement);
this.isOverlayListVisible = true;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendExtensionMessage("updateAutofillOverlayPosition", {
overlayElement: AutofillOverlayElement.List, overlayElement: AutofillOverlayElement.List,
}); });
} }
/** // /**
* Appends the overlay element to the body element. This method will also // * Appends the overlay element to the body element. This method will also
* observe the body element to ensure that the overlay element is not // * observe the body element to ensure that the overlay element is not
* interfered with by any DOM changes. // * interfered with by any DOM changes.
* // *
* @param element - The overlay element to append to the body element. // * @param element - The overlay element to append to the body element.
*/ // */
private appendOverlayElementToBody(element: HTMLElement) { // private appendOverlayElementToBody(element: HTMLElement) {
this.observeBodyElement(); // this.observeBodyElement();
globalThis.document.body.appendChild(element); // globalThis.document.body.appendChild(element);
} // }
/** /**
* Sends a message that facilitates hiding the overlay elements. * Sends a message that facilitates hiding the overlay elements.
@@ -651,11 +633,9 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
* @param isHidden - Indicates if the overlay elements should be hidden. * @param isHidden - Indicates if the overlay elements should be hidden.
*/ */
private toggleOverlayHidden(isHidden: boolean) { private toggleOverlayHidden(isHidden: boolean) {
const displayValue = isHidden ? "none" : "block"; void this.sendExtensionMessage("updateAutofillOverlayHidden", {
void this.sendExtensionMessage("updateAutofillOverlayHidden", { display: displayValue }); isOverlayHidden: isHidden,
});
this.isOverlayButtonVisible = !!this.overlayButtonElement && !isHidden;
this.isOverlayListVisible = !!this.overlayListElement && !isHidden;
} }
/** /**
@@ -676,9 +656,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
focusedFieldRects: { width, height, top, left }, focusedFieldRects: { width, height, top, left },
}; };
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. void this.sendExtensionMessage("updateFocusedFieldData", {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.sendExtensionMessage("updateFocusedFieldData", {
focusedFieldData: this.focusedFieldData, focusedFieldData: this.focusedFieldData,
}); });
} }
@@ -762,77 +740,77 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
return !isLoginCipherField; return !isLoginCipherField;
} }
/** // /**
* Creates the autofill overlay button element. Will not attempt // * Creates the autofill overlay button element. Will not attempt
* to create the element if it already exists in the DOM. // * to create the element if it already exists in the DOM.
*/ // */
private createAutofillOverlayButton() { // private createAutofillOverlayButton() {
if (this.overlayButtonElement) { // if (this.overlayButtonElement) {
return; // return;
} // }
//
// if (this.isFirefoxBrowser) {
// this.overlayButtonElement = globalThis.document.createElement("div");
// new AutofillOverlayButtonIframe(this.overlayButtonElement);
//
// return;
// }
//
// const customElementName = this.generateRandomCustomElementName();
// globalThis.customElements?.define(
// customElementName,
// class extends HTMLElement {
// constructor() {
// super();
// new AutofillOverlayButtonIframe(this);
// }
// },
// );
// this.overlayButtonElement = globalThis.document.createElement(customElementName);
// }
//
// /**
// * Creates the autofill overlay list element. Will not attempt
// * to create the element if it already exists in the DOM.
// */
// private createAutofillOverlayList() {
// if (this.overlayListElement) {
// return;
// }
//
// if (this.isFirefoxBrowser) {
// this.overlayListElement = globalThis.document.createElement("div");
// new AutofillOverlayListIframe(this.overlayListElement);
//
// return;
// }
//
// const customElementName = this.generateRandomCustomElementName();
// globalThis.customElements?.define(
// customElementName,
// class extends HTMLElement {
// constructor() {
// super();
// new AutofillOverlayListIframe(this);
// }
// },
// );
// this.overlayListElement = globalThis.document.createElement(customElementName);
// }
if (this.isFirefoxBrowser) { // /**
this.overlayButtonElement = globalThis.document.createElement("div"); // * Updates the default styles for the custom element. This method will
new AutofillOverlayButtonIframe(this.overlayButtonElement); // * remove any styles that are added to the custom element by other methods.
// *
return; // * @param element - The custom element to update the default styles for.
} // */
// private updateCustomElementDefaultStyles(element: HTMLElement) {
const customElementName = this.generateRandomCustomElementName(); // this.unobserveCustomElements();
globalThis.customElements?.define( //
customElementName, // setElementStyles(element, this.customElementDefaultStyles, true);
class extends HTMLElement { //
constructor() { // this.observeCustomElements();
super(); // }
new AutofillOverlayButtonIframe(this);
}
},
);
this.overlayButtonElement = globalThis.document.createElement(customElementName);
}
/**
* Creates the autofill overlay list element. Will not attempt
* to create the element if it already exists in the DOM.
*/
private createAutofillOverlayList() {
if (this.overlayListElement) {
return;
}
if (this.isFirefoxBrowser) {
this.overlayListElement = globalThis.document.createElement("div");
new AutofillOverlayListIframe(this.overlayListElement);
return;
}
const customElementName = this.generateRandomCustomElementName();
globalThis.customElements?.define(
customElementName,
class extends HTMLElement {
constructor() {
super();
new AutofillOverlayListIframe(this);
}
},
);
this.overlayListElement = globalThis.document.createElement(customElementName);
}
/**
* Updates the default styles for the custom element. This method will
* remove any styles that are added to the custom element by other methods.
*
* @param element - The custom element to update the default styles for.
*/
private updateCustomElementDefaultStyles(element: HTMLElement) {
this.unobserveCustomElements();
setElementStyles(element, this.customElementDefaultStyles, true);
this.observeCustomElements();
}
/** /**
* Queries the background script for the autofill overlay visibility setting. * Queries the background script for the autofill overlay visibility setting.
@@ -890,7 +868,8 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
private triggerOverlayRepositionUpdates = async () => { private triggerOverlayRepositionUpdates = async () => {
if (!this.recentlyFocusedFieldIsCurrentlyFocused()) { if (!this.recentlyFocusedFieldIsCurrentlyFocused()) {
this.toggleOverlayHidden(false); this.toggleOverlayHidden(false);
this.removeAutofillOverlay(); // this.removeAutofillOverlay();
void this.sendExtensionMessage("closeAutofillOverlay");
return; return;
} }
@@ -906,7 +885,8 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
return; return;
} }
this.removeAutofillOverlay(); // this.removeAutofillOverlay();
void this.sendExtensionMessage("closeAutofillOverlay");
}; };
/** /**
@@ -927,7 +907,8 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
private setupGlobalEventListeners = () => { private setupGlobalEventListeners = () => {
globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent); globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent);
globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent); globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
this.setupMutationObserver(); this.setOverlayRepositionEventListeners();
// this.setupMutationObserver();
}; };
/** /**
@@ -940,178 +921,179 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
} }
this.mostRecentlyFocusedField = null; this.mostRecentlyFocusedField = null;
this.removeAutofillOverlay(); // this.removeAutofillOverlay();
void this.sendExtensionMessage("closeAutofillOverlay");
}; };
/** // /**
* Sets up mutation observers for the overlay elements, the body element, and the // * Sets up mutation observers for the overlay elements, the body element, and the
* document element. The mutation observers are used to remove any styles that are // * document element. The mutation observers are used to remove any styles that are
* added to the overlay elements by the website. They are also used to ensure that // * added to the overlay elements by the website. They are also used to ensure that
* the overlay elements are always present at the bottom of the body element. // * the overlay elements are always present at the bottom of the body element.
*/ // */
private setupMutationObserver = () => { // private setupMutationObserver = () => {
this.overlayElementsMutationObserver = new MutationObserver( // this.overlayElementsMutationObserver = new MutationObserver(
this.handleOverlayElementMutationObserverUpdate, // this.handleOverlayElementMutationObserverUpdate,
); // );
//
// this.bodyElementMutationObserver = new MutationObserver(
// this.handleBodyElementMutationObserverUpdate,
// );
// };
this.bodyElementMutationObserver = new MutationObserver( // /**
this.handleBodyElementMutationObserverUpdate, // * Sets up mutation observers to verify that the overlay
); // * elements are not modified by the website.
}; // */
// private observeCustomElements() {
// if (this.overlayButtonElement) {
// this.overlayElementsMutationObserver?.observe(this.overlayButtonElement, {
// attributes: true,
// });
// }
//
// if (this.overlayListElement) {
// this.overlayElementsMutationObserver?.observe(this.overlayListElement, { attributes: true });
// }
// }
/** // /**
* Sets up mutation observers to verify that the overlay // * Disconnects the mutation observers that are used to verify that the overlay
* elements are not modified by the website. // * elements are not modified by the website.
*/ // */
private observeCustomElements() { // private unobserveCustomElements() {
if (this.overlayButtonElement) { // this.overlayElementsMutationObserver?.disconnect();
this.overlayElementsMutationObserver?.observe(this.overlayButtonElement, { // }
attributes: true,
});
}
if (this.overlayListElement) { // /**
this.overlayElementsMutationObserver?.observe(this.overlayListElement, { attributes: true }); // * Sets up a mutation observer for the body element. The mutation observer is used
} // * to ensure that the overlay elements are always present at the bottom of the body
} // * element.
// */
// private observeBodyElement() {
// this.bodyElementMutationObserver?.observe(globalThis.document.body, { childList: true });
// }
/** // /**
* Disconnects the mutation observers that are used to verify that the overlay // * Disconnects the mutation observer for the body element.
* elements are not modified by the website. // */
*/ // private removeBodyElementObserver() {
private unobserveCustomElements() { // this.bodyElementMutationObserver?.disconnect();
this.overlayElementsMutationObserver?.disconnect(); // }
}
/** // /**
* Sets up a mutation observer for the body element. The mutation observer is used // * Handles the mutation observer update for the overlay elements. This method will
* to ensure that the overlay elements are always present at the bottom of the body // * remove any attributes or styles that might be added to the overlay elements by
* element. // * a separate process within the website where this script is injected.
*/ // *
private observeBodyElement() { // * @param mutationRecord - The mutation record that triggered the update.
this.bodyElementMutationObserver?.observe(globalThis.document.body, { childList: true }); // */
} // private handleOverlayElementMutationObserverUpdate = (mutationRecord: MutationRecord[]) => {
// if (this.isTriggeringExcessiveMutationObserverIterations()) {
// return;
// }
//
// for (let recordIndex = 0; recordIndex < mutationRecord.length; recordIndex++) {
// const record = mutationRecord[recordIndex];
// if (record.type !== "attributes") {
// continue;
// }
//
// const element = record.target as HTMLElement;
// if (record.attributeName !== "style") {
// this.removeModifiedElementAttributes(element);
//
// continue;
// }
//
// element.removeAttribute("style");
// this.updateCustomElementDefaultStyles(element);
// }
// };
/** // /**
* Disconnects the mutation observer for the body element. // * Removes all elements from a passed overlay
*/ // * element except for the style attribute.
private removeBodyElementObserver() { // *
this.bodyElementMutationObserver?.disconnect(); // * @param element - The element to remove the attributes from.
} // */
// private removeModifiedElementAttributes(element: HTMLElement) {
// const attributes = Array.from(element.attributes);
// for (let attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) {
// const attribute = attributes[attributeIndex];
// if (attribute.name === "style") {
// continue;
// }
//
// element.removeAttribute(attribute.name);
// }
// }
/** // /**
* Handles the mutation observer update for the overlay elements. This method will // * Handles the mutation observer update for the body element. This method will
* remove any attributes or styles that might be added to the overlay elements by // * ensure that the overlay elements are always present at the bottom of the body
* a separate process within the website where this script is injected. // * element.
* // */
* @param mutationRecord - The mutation record that triggered the update. // private handleBodyElementMutationObserverUpdate = () => {
*/ // if (
private handleOverlayElementMutationObserverUpdate = (mutationRecord: MutationRecord[]) => { // (!this.overlayButtonElement && !this.overlayListElement) ||
if (this.isTriggeringExcessiveMutationObserverIterations()) { // this.isTriggeringExcessiveMutationObserverIterations()
return; // ) {
} // return;
// }
//
// const lastChild = globalThis.document.body.lastElementChild;
// const secondToLastChild = lastChild?.previousElementSibling;
// const lastChildIsOverlayList = lastChild === this.overlayListElement;
// const lastChildIsOverlayButton = lastChild === this.overlayButtonElement;
// const secondToLastChildIsOverlayButton = secondToLastChild === this.overlayButtonElement;
//
// if (
// (lastChildIsOverlayList && secondToLastChildIsOverlayButton) ||
// (lastChildIsOverlayButton && !this.isOverlayListVisible)
// ) {
// return;
// }
//
// if (
// (lastChildIsOverlayList && !secondToLastChildIsOverlayButton) ||
// (lastChildIsOverlayButton && this.isOverlayListVisible)
// ) {
// globalThis.document.body.insertBefore(this.overlayButtonElement, this.overlayListElement);
// return;
// }
//
// globalThis.document.body.insertBefore(lastChild, this.overlayButtonElement);
// };
for (let recordIndex = 0; recordIndex < mutationRecord.length; recordIndex++) { // /**
const record = mutationRecord[recordIndex]; // * Identifies if the mutation observer is triggering excessive iterations.
if (record.type !== "attributes") { // * Will trigger a blur of the most recently focused field and remove the
continue; // * autofill overlay if any set mutation observer is triggering
} // * excessive iterations.
// */
const element = record.target as HTMLElement; // private isTriggeringExcessiveMutationObserverIterations() {
if (record.attributeName !== "style") { // if (this.mutationObserverIterationsResetTimeout) {
this.removeModifiedElementAttributes(element); // clearTimeout(this.mutationObserverIterationsResetTimeout);
// }
continue; //
} // this.mutationObserverIterations++;
// this.mutationObserverIterationsResetTimeout = setTimeout(
element.removeAttribute("style"); // () => (this.mutationObserverIterations = 0),
this.updateCustomElementDefaultStyles(element); // 2000,
} // );
}; //
// if (this.mutationObserverIterations > 100) {
/** // clearTimeout(this.mutationObserverIterationsResetTimeout);
* Removes all elements from a passed overlay // this.mutationObserverIterations = 0;
* element except for the style attribute. // this.blurMostRecentOverlayField();
* // this.removeAutofillOverlay();
* @param element - The element to remove the attributes from. //
*/ // return true;
private removeModifiedElementAttributes(element: HTMLElement) { // }
const attributes = Array.from(element.attributes); //
for (let attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) { // return false;
const attribute = attributes[attributeIndex]; // }
if (attribute.name === "style") {
continue;
}
element.removeAttribute(attribute.name);
}
}
/**
* Handles the mutation observer update for the body element. This method will
* ensure that the overlay elements are always present at the bottom of the body
* element.
*/
private handleBodyElementMutationObserverUpdate = () => {
if (
(!this.overlayButtonElement && !this.overlayListElement) ||
this.isTriggeringExcessiveMutationObserverIterations()
) {
return;
}
const lastChild = globalThis.document.body.lastElementChild;
const secondToLastChild = lastChild?.previousElementSibling;
const lastChildIsOverlayList = lastChild === this.overlayListElement;
const lastChildIsOverlayButton = lastChild === this.overlayButtonElement;
const secondToLastChildIsOverlayButton = secondToLastChild === this.overlayButtonElement;
if (
(lastChildIsOverlayList && secondToLastChildIsOverlayButton) ||
(lastChildIsOverlayButton && !this.isOverlayListVisible)
) {
return;
}
if (
(lastChildIsOverlayList && !secondToLastChildIsOverlayButton) ||
(lastChildIsOverlayButton && this.isOverlayListVisible)
) {
globalThis.document.body.insertBefore(this.overlayButtonElement, this.overlayListElement);
return;
}
globalThis.document.body.insertBefore(lastChild, this.overlayButtonElement);
};
/**
* Identifies if the mutation observer is triggering excessive iterations.
* Will trigger a blur of the most recently focused field and remove the
* autofill overlay if any set mutation observer is triggering
* excessive iterations.
*/
private isTriggeringExcessiveMutationObserverIterations() {
if (this.mutationObserverIterationsResetTimeout) {
clearTimeout(this.mutationObserverIterationsResetTimeout);
}
this.mutationObserverIterations++;
this.mutationObserverIterationsResetTimeout = setTimeout(
() => (this.mutationObserverIterations = 0),
2000,
);
if (this.mutationObserverIterations > 100) {
clearTimeout(this.mutationObserverIterationsResetTimeout);
this.mutationObserverIterations = 0;
this.blurMostRecentOverlayField();
this.removeAutofillOverlay();
return true;
}
return false;
}
/** /**
* Gets the root node of the passed element and returns the active element within that root node. * Gets the root node of the passed element and returns the active element within that root node.
@@ -1132,7 +1114,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
* disconnect the mutation observers and remove all event listeners. * disconnect the mutation observers and remove all event listeners.
*/ */
destroy() { destroy() {
this.documentElementMutationObserver?.disconnect(); // this.documentElementMutationObserver?.disconnect();
this.clearUserInteractionEventTimeout(); this.clearUserInteractionEventTimeout();
this.formFieldElements.forEach((formFieldElement) => { this.formFieldElements.forEach((formFieldElement) => {
this.removeCachedFormFieldEventListeners(formFieldElement); this.removeCachedFormFieldEventListeners(formFieldElement);
@@ -1145,7 +1127,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
this.handleVisibilityChangeEvent, this.handleVisibilityChangeEvent,
); );
globalThis.removeEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent); globalThis.removeEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent);
this.removeAutofillOverlay(); // this.removeAutofillOverlay();
this.removeOverlayRepositionEventListeners(); this.removeOverlayRepositionEventListeners();
} }
} }