1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-03 10:13:31 +00:00

create inline menu exclusively on page targeted elements

This commit is contained in:
Jonathan Prusik
2025-10-06 14:59:30 -04:00
parent 44a80b6a2b
commit 4a71e7a65a
4 changed files with 85 additions and 3 deletions

View File

@@ -36,7 +36,11 @@ export interface AutofillOverlayContentService {
setupOverlayListeners(
autofillFieldElement: ElementWithOpId<FormFieldElement>,
autofillFieldData: AutofillField,
pageDetails: AutofillPageDetails,
pageDetails?: AutofillPageDetails,
): Promise<void>;
setupOverlayListenersOnQualifiedField(
autofillFieldElement: ElementWithOpId<FormFieldElement>,
autofillFieldData: AutofillField,
): Promise<void>;
blurMostRecentlyFocusedField(isClosingInlineMenu?: boolean): void;
getOwnedInlineMenuTagNames(): string[];

View File

@@ -18,6 +18,7 @@ interface CollectAutofillContentService {
autofillFormElements: AutofillFormElements;
getPageDetails(): Promise<AutofillPageDetails>;
getAutofillFieldElementByOpid(opid: string): HTMLElement | null;
getTargetedFields(): {[key: string]: Element} | null;
destroy(): void;
}

View File

@@ -197,8 +197,17 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
async setupOverlayListeners(
formFieldElement: ElementWithOpId<FormFieldElement>,
autofillFieldData: AutofillField,
pageDetails: AutofillPageDetails,
pageDetails?: AutofillPageDetails,
) {
// This should get targeted fields only
if (formFieldElement && !pageDetails) {
this.formFieldElements.set(formFieldElement, autofillFieldData);
await this.setupFormFieldElementEventListeners(formFieldElement);
await this.triggerFormFieldFocusedAction(formFieldElement);
return;
}
if (
currentlyInSandboxedIframe() ||
this.formFieldElements.has(formFieldElement) ||
@@ -1265,7 +1274,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
* @param formFieldElement - The form field element to set up the inline menu on.
* @param autofillFieldData - Autofill field data captured from the form field element.
*/
private async setupOverlayListenersOnQualifiedField(
async setupOverlayListenersOnQualifiedField(
formFieldElement: ElementWithOpId<FormFieldElement>,
autofillFieldData: AutofillField,
) {

View File

@@ -1,5 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CipherType } from "@bitwarden/common/vault/enums";
import { AutofillFieldQualifierType } from "../enums/autofill-field.enums";
import AutofillField from "../models/autofill-field";
import AutofillForm from "../models/autofill-form";
import AutofillPageDetails from "../models/autofill-page-details";
@@ -35,6 +38,8 @@ import {
import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service";
import { DomQueryService } from "./abstractions/dom-query.service";
type TargetedFields = { [type in AutofillFieldQualifierType]?: Element };
export class CollectAutofillContentService implements CollectAutofillContentServiceInterface {
private readonly sendExtensionMessage = sendExtensionMessage;
private readonly getAttributeBoolean = getAttributeBoolean;
@@ -44,6 +49,10 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
private _autofillFormElements: AutofillFormElements = new Map();
private autofillFieldElements: AutofillFieldElements = new Map();
private currentLocationHref = "";
/**
* A value of `null` indicates the configuration has not been initialized, whereas an empty object indicates no set rules.
*/
private readonly pageTargetingRules: null | { [type in AutofillFieldQualifierType]?: string } = {};
private intersectionObserver: IntersectionObserver;
private elementInitializingIntersectionObserver: Set<Element> = new Set();
private mutationObserver: MutationObserver;
@@ -89,6 +98,33 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
// Set up listeners on top-layer candidates that predate Mutation Observer setup
this.setupInitialTopLayerListeners();
const targetedFields = this.getTargetedFields();
const targetedFieldNames = Object.keys(targetedFields) as AutofillFieldQualifierType[];
if (targetedFieldNames.length) {
for (const fieldName of targetedFieldNames) {
const fieldNode = targetedFields[fieldName] as ElementWithOpId<FormFieldElement>;
void this.autofillOverlayContentService.setupOverlayListenersOnQualifiedField(
fieldNode,
{
// @TODO needs an opid and added to the list in order for autofill to work
opid: fieldNode.opid || `targeted_field_${fieldName}`,
elementNumber: 0, // Not relevant when using targeting rules
viewable: true,
htmlID: fieldNode.getAttribute('id'),
htmlName: fieldNode.getAttribute('name'),
htmlClass: fieldNode.getAttribute('class'),
tabindex: fieldNode.getAttribute('tabindex'),
title: fieldNode.getAttribute('title'),
inlineMenuFillType: CipherType.Login,
fieldQualifier: fieldName,
},
);
}
return {} as AutofillPageDetails;
}
if (!this.mutationObserver) {
this.setupMutationObserver();
}
@@ -124,11 +160,40 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
this.domRecentlyMutated = false;
const pageDetails = this.getFormattedPageDetails(autofillFormsData, autofillFieldsData);
this.setupOverlayListeners(pageDetails);
return pageDetails;
}
getTargetedFields(): TargetedFields {
if (this.pageTargetingRules) {
const definedTargetingRuleFields = Object.keys(
this.pageTargetingRules,
) as AutofillFieldQualifierType[];
// Note - potential bottleneck at async lookup (alternatively, promise map)
const foundTargetedFields = definedTargetingRuleFields.reduce((foundFields, fieldName) => {
const targetingRule = this.pageTargetingRules[fieldName];
const fieldMatches = this.domQueryService.queryDeepSelector(
globalThis.document,
targetingRule,
);
return fieldMatches.length
? {
...foundFields,
[fieldName]: fieldMatches[0],
}
: foundFields;
}, {});
return foundTargetedFields;
}
return {};
}
/**
* Find an AutofillField element by its opid, will only return the first
* element if there are multiple elements with the same opid. If no
@@ -1248,6 +1313,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
cancelIdleCallbackPolyfill(this.updateAfterMutationIdleCallback);
}
// If page targeting rules are present, call `getFieldByQuery` for each field and bypass
this.updateAfterMutationIdleCallback = requestIdleCallbackPolyfill(
this.getPageDetails.bind(this),
{ timeout: this.updateAfterMutationTimeout },
@@ -1451,6 +1517,8 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
* @param pageDetails - The page details to use for the inline menu listeners
*/
private setupOverlayListeners(pageDetails: AutofillPageDetails) {
// if there are targeted rules for the page, get the nodes with those rules, and if successful, pass those to `autofillOverlayContentService.setupOverlayListeners`
if (this.autofillOverlayContentService) {
this.autofillFieldElements.forEach((autofillField, formFieldElement) => {
this.setupOverlayOnField(formFieldElement, autofillField, pageDetails);