import type { ObjectDirective } from 'vue';
|
|
interface PasswordInputState {
|
activate: () => void;
|
input: HTMLInputElement;
|
unlock: () => void;
|
}
|
|
interface DirectiveState {
|
inputStates: Map<HTMLInputElement, PasswordInputState>;
|
observer: MutationObserver;
|
}
|
|
const directiveStates = new WeakMap<HTMLElement, DirectiveState>();
|
|
function disableFormAutocomplete(root: HTMLElement) {
|
if (root instanceof HTMLFormElement) root.autocomplete = 'off';
|
root.querySelectorAll('form').forEach((form) => (form.autocomplete = 'off'));
|
}
|
|
function protectInput(input: HTMLInputElement, state: DirectiveState) {
|
if (state.inputStates.has(input)) return;
|
|
input.autocomplete = 'off';
|
input.name = `credential-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
input.readOnly = true;
|
input.type = 'text';
|
input.style.setProperty('-webkit-text-security', 'disc');
|
Object.assign(input.dataset, { '1pIgnore': 'true', lpignore: 'true' });
|
|
const activate = () => {
|
input.type = 'password';
|
input.readOnly = false;
|
input.style.removeProperty('-webkit-text-security');
|
input.removeEventListener('pointerdown', unlock);
|
input.removeEventListener('keydown', activate);
|
input.removeEventListener('beforeinput', activate);
|
input.removeEventListener('paste', activate);
|
};
|
|
// Keep the field disguised as text while it receives focus; password managers commonly fill on focus.
|
const unlock = () => {
|
input.readOnly = false;
|
input.removeEventListener('pointerdown', unlock);
|
};
|
|
input.addEventListener('pointerdown', unlock);
|
input.addEventListener('keydown', activate);
|
input.addEventListener('beforeinput', activate);
|
input.addEventListener('paste', activate);
|
state.inputStates.set(input, { activate, input, unlock });
|
}
|
|
function protectPasswordInputs(root: HTMLElement, state: DirectiveState) {
|
disableFormAutocomplete(root);
|
if (root instanceof HTMLInputElement && root.type === 'password') protectInput(root, state);
|
root.querySelectorAll<HTMLInputElement>('input[type="password"]').forEach((input) => protectInput(input, state));
|
}
|
|
/** Keep password managers from identifying and filling password fields before the user interacts with them. */
|
export const vDisablePasswordAutofill: ObjectDirective<HTMLElement, boolean | undefined> = {
|
mounted(el, binding) {
|
if (binding.value === false) return;
|
|
const state: DirectiveState = {
|
inputStates: new Map(),
|
observer: new MutationObserver((mutations) => {
|
mutations.forEach((mutation) => {
|
mutation.addedNodes.forEach((node) => {
|
if (node instanceof HTMLElement) protectPasswordInputs(node, state);
|
});
|
});
|
}),
|
};
|
protectPasswordInputs(el, state);
|
state.observer.observe(el, { childList: true, subtree: true });
|
directiveStates.set(el, state);
|
},
|
unmounted(el) {
|
const state = directiveStates.get(el);
|
if (!state) return;
|
|
state.observer.disconnect();
|
state.inputStates.forEach(({ activate, input, unlock }) => {
|
input.removeEventListener('pointerdown', unlock);
|
input.removeEventListener('keydown', activate);
|
input.removeEventListener('beforeinput', activate);
|
input.removeEventListener('paste', activate);
|
});
|
directiveStates.delete(el);
|
},
|
};
|