Refactor icon handling (#2791)

Refactor icon handling
This commit is contained in:
Sami Vänttinen 2025-12-16 09:08:17 +02:00 committed by GitHub
parent f3e0111acc
commit e444eab33c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 323 additions and 317 deletions

View file

@ -53,7 +53,8 @@
"content/fields.js",
"content/fill.js",
"content/form.js",
"content/icons.js",
"content/icon.js",
"content/icon-handler.js",
"content/keepassxc-browser.js",
"content/observer-helper.js",
"content/pwgen.js",

View file

@ -66,7 +66,8 @@
"content/fields.js",
"content/fill.js",
"content/form.js",
"content/icons.js",
"content/icon.js",
"content/icon-handler.js",
"content/keepassxc-browser.js",
"content/observer-helper.js",
"content/pwgen.js",

View file

@ -0,0 +1,242 @@
'use strict';
/**
* @Object kpxcIcons
* Icon handling.
*/
const kpxcIcons = {};
kpxcIcons.icons = [];
kpxcIcons.iconTypes = {
DEFAULT: 0, // Username icon
PASSWORD: 1,
TOTP: 2
};
// Adds an icon to input field
kpxcIcons.addIcon = async function(field, iconType) {
if (!field || !Object.values(kpxcIcons.iconTypes).includes(iconType)) {
return;
}
let iconSet = false;
if (iconType === kpxcIcons.iconTypes.DEFAULT && kpxcUsernameIcons.isValid(field)) {
kpxcUsernameIcons.newIcon(field, kpxc.databaseState);
iconSet = true;
} else if (iconType === kpxcIcons.iconTypes.PASSWORD && kpxcPasswordIcons.isValid(field)) {
kpxcPasswordIcons.newIcon(field, kpxc.databaseState);
iconSet = true;
} else if (iconType === kpxcIcons.iconTypes.TOTP && kpxcTOTPIcons.isValid(field)) {
kpxcTOTPIcons.newIcon(field, kpxc.databaseState);
iconSet = true;
}
if (iconSet) {
kpxcIcons.icons.push({
field: field,
iconType: iconType
});
}
};
// Adds all icons from a form struct
kpxcIcons.addIconsFromForm = async function(form) {
const addUsernameIcons = async function(c) {
if (kpxc.settings.showLoginFormIcon && await kpxc.passwordFilledWithExceptions(c) === false) {
// Special case where everything else has been hidden, but a single password field is now displayed.
// For example PayPal and Amazon is handled like this.
if (c.username && !c.password && c.passwordInputs.length === 1) {
kpxcIcons.addIcon(c.passwordInputs[0], kpxcIcons.iconTypes.DEFAULT);
}
if (c.username && !c.username.readOnly) {
kpxcIcons.addIcon(c.username, kpxcIcons.iconTypes.DEFAULT);
} else if (c.password && (!c.username || (c.username && c.username.readOnly))) {
// Single password field
kpxcIcons.addIcon(c.password, kpxcIcons.iconTypes.DEFAULT);
}
}
};
const addPasswordIcons = async function(c) {
// Show password icons also with forms without any username field
if (kpxc.settings.usePasswordGeneratorIcons
&& ((c.username && c.password) || (!c.username && c.passwordInputs.length > 0))) {
for (const input of c.passwordInputs) {
kpxcIcons.addIcon(input, kpxcIcons.iconTypes.PASSWORD);
}
}
};
const addTOTPIcons = async function(c) {
if (c.totp && kpxc.settings.showOTPIcon) {
kpxcIcons.addIcon(c.totp, kpxcIcons.iconTypes.TOTP);
}
};
await Promise.all([
await addUsernameIcons(form),
await addPasswordIcons(form),
await addTOTPIcons(form)
]);
};
kpxcIcons.calculateIconOffset = function(field, size) {
const offset = Math.floor((field.offsetHeight / 2) - (size / 2) - 1);
return (offset < 0) ? 0 : offset;
};
// Delete all icons that have been hidden from the page view
kpxcIcons.deleteAllHiddenIcons = function() {
kpxcIcons.deleteIcons(kpxcUsernameIcons.icons);
kpxcIcons.deleteIcons(kpxcPasswordIcons.icons);
kpxcIcons.deleteIcons(kpxcTOTPIcons.icons);
};
// Delete hidden icons from the list
kpxcIcons.deleteIcons = function(iconList) {
const deletedIcons = [];
for (const icon of iconList) {
if (icon.inputField && !kpxcFields.isVisible(icon.inputField)) {
const index = iconList.indexOf(icon);
icon.removeIcon();
iconList.splice(index, 1);
deletedIcons.push(icon.inputField);
// Delete the input field from detected fields so the icon can be detected again
const inputFieldIndex = kpxc.inputs.indexOf(icon.inputField);
if (inputFieldIndex >= 0) {
kpxc.inputs.splice(inputFieldIndex, 1);
}
}
}
// Remove the same icons from kpxcIcons.icons array
for (const input of deletedIcons) {
const index = kpxcIcons.icons.findIndex(e => e.field === input);
if (index >= 0) {
kpxcIcons.icons.splice(index, 1);
}
}
};
// Initializes all icons needed to be shown
kpxcIcons.initIcons = async function(combinations = []) {
if (combinations.length === 0) {
return;
}
for (const form of kpxcForm.savedForms) {
await kpxcIcons.addIconsFromForm(form);
}
// Check for other combinations that are not in any form,
// or there's a form that wasn't present in savedForms (and it's not null)
for (const c of combinations) {
if (!c.form || (c.form && !kpxcForm.savedForms.some(sf => sf.form === c.form))) {
await kpxcIcons.addIconsFromForm(c);
}
}
};
kpxcIcons.hasIcon = function(field) {
return !field ? false : kpxcIcons.icons.some(i => i.field === field);
};
kpxcIcons.monitorIconPosition = function(iconClass) {
// Handle icon position on resize
window.addEventListener('resize', function(e) {
kpxcIcons.updateIconPosition(iconClass);
});
// Handle icon position on scroll
window.addEventListener('scroll', function(e) {
kpxcIcons.updateIconPosition(iconClass);
});
window.addEventListener('transitionend', function(e) {
if (matchesWithNodeName(e.target, 'INPUT') || matchesWithNodeName(e.target, 'TEXTAREA')) {
kpxcIcons.updateIconPosition(iconClass);
}
});
};
kpxcIcons.setIconPosition = function(icon, field, rtl = false, segmented = false) {
const rect = field.getBoundingClientRect();
const size = Number(icon.getAttribute('size'));
const offset = kpxcIcons.calculateIconOffset(field, size);
const zoom = kpxcUI.bodyStyle.zoom || 1;
let left = kpxcUI.getRelativeLeftPosition(rect) / zoom;
let top = kpxcUI.getRelativeTopPosition(rect) / zoom;
// Add more space for the icon to show it at the right side of the field if TOTP fields are segmented
if (segmented) {
left += size + 10;
}
// Adjusts the icon offset for certain sites
const iconOffset = kpxcSites.iconOffset(left, top, size, field?.getLowerCaseAttribute('type'));
if (iconOffset) {
left = iconOffset[0];
top = iconOffset[1];
}
const scrollTop = kpxcUI.getScrollTop() / zoom;
const scrollLeft = kpxcUI.getScrollLeft() / zoom;
icon.style.top = Pixels(top + scrollTop + offset + 1);
icon.style.left = rtl
? Pixels(left + scrollLeft + offset)
: Pixels(left + scrollLeft + field.offsetWidth - size - offset);
};
// Sets the icons to corresponding database lock status
kpxcIcons.switchIcons = async function() {
const uuid = await sendMessage('page_get_login_id');
kpxcUsernameIcons.switchIcon(kpxc.databaseState, uuid);
kpxcPasswordIcons.switchIcon(kpxc.databaseState, uuid);
kpxcTOTPIcons.switchIcon(kpxc.databaseState, uuid);
};
/**
* Detects if the input field appears or disappears -> show/hide the icon
* - boundingClientRect with slightly (< -10) negative values -> hidden
* - intersectionRatio === 0 -> hidden
* - isIntersecting === false -> hidden
* - intersectionRatio > 0 -> shown
* - isIntersecting === true -> shown
*/
kpxcIcons.updateFromIntersectionObserver = function(iconClass, entries) {
for (const entry of entries) {
const rect = DOMRectToArray(entry.boundingClientRect);
if ((entry.intersectionRatio === 0 && !entry.isIntersecting) || (rect.some(x => x < -10))) {
iconClass.icon.style.display = 'none';
} else if (entry.intersectionRatio > 0 && entry.isIntersecting) {
iconClass.icon.style.display = 'block';
// Wait for possible DOM animations
setTimeout(() => {
kpxcIcons.setIconPosition(iconClass.icon, entry.target, iconClass.rtl, iconClass.segmented);
}, 400);
}
}
};
kpxcIcons.updateIconPosition = function(iconClass) {
if (iconClass.inputField && iconClass.icon) {
kpxcIcons.setIconPosition(iconClass.icon, iconClass.inputField, iconClass.rtl, iconClass.segmented);
}
};
const DOMRectToArray = function (domRect) {
return [
domRect.bottom,
domRect.height,
domRect.left,
domRect.right,
domRect.top,
domRect.width,
domRect.x,
domRect.y,
];
};

View file

@ -0,0 +1,67 @@
'use strict';
const MIN_ICON_SIZE = 14;
const MAX_ICON_SIZE = 24;
// Basic icon class
class Icon {
constructor(field, databaseState = DatabaseState.DISCONNECTED, segmented = false) {
this.databaseState = databaseState;
this.icon = null;
this.inputField = null;
this.rtl = kpxcUI.isRTL(field);
this.segmented = segmented;
try {
this.observer = new IntersectionObserver((entries) => {
kpxcIcons.updateFromIntersectionObserver(this, entries);
});
} catch (err) {
logError(err);
}
}
// Size the icon dynamically, but not greater than 24 or smaller than 14
calculateIconSize(field) {
return Math.max(Math.min(MAX_ICON_SIZE, field.offsetHeight - 4), MIN_ICON_SIZE);
}
// Creates a wrapper div that has the icon in Shadow DOM
createWrapper(styleSheetFilename) {
const styleSheet = createStylesheet(styleSheetFilename);
const wrapper = document.createElement('div');
wrapper.style.all = 'unset';
wrapper.style.display = 'none';
// Make sure the wrapper is positioned correctly without CSS styles affecting to it
wrapper.style.position = 'absolute';
wrapper.style.top = Pixels(0);
wrapper.style.left = Pixels(0);
// Waits for stylesheet to load before displaying the element
styleSheet.addEventListener('load', () => wrapper.style.display = 'block');
this.shadowRoot = wrapper.attachShadow({ mode: 'closed' });
this.shadowRoot.append(styleSheet);
this.shadowRoot.append(this.icon);
document.body.append(wrapper);
kpxcUI.observeWrapper(wrapper);
}
removeIcon() {
this.shadowRoot.removeChild(this.icon);
document.body.removeChild(this.shadowRoot.host);
}
switchIcon(state, uuid) {
if (!this.icon) {
return;
}
if (state === DatabaseState.UNLOCKED) {
this.icon.style.filter = kpxc.credentials.length === 0 && !uuid ? 'saturate(0%)' : 'saturate(100%)';
} else {
this.icon.style.filter = 'saturate(0%)';
}
}
}

View file

@ -1,116 +0,0 @@
'use strict';
/**
* @Object kpxcIcons
* Icon handling.
*/
const kpxcIcons = {};
kpxcIcons.icons = [];
kpxcIcons.iconTypes = { USERNAME: 0, PASSWORD: 1, TOTP: 2 };
// Adds an icon to input field
kpxcIcons.addIcon = async function(field, iconType) {
if (!field || iconType < 0 || iconType > 2) {
return;
}
let iconSet = false;
if (iconType === kpxcIcons.iconTypes.USERNAME && kpxcUsernameIcons.isValid(field)) {
kpxcUsernameIcons.newIcon(field, kpxc.databaseState);
iconSet = true;
} else if (iconType === kpxcIcons.iconTypes.PASSWORD && kpxcPasswordIcons.isValid(field)) {
kpxcPasswordIcons.newIcon(field, kpxc.databaseState);
iconSet = true;
} else if (iconType === kpxcIcons.iconTypes.TOTP && kpxcTOTPIcons.isValid(field)) {
kpxcTOTPIcons.newIcon(field, kpxc.databaseState);
iconSet = true;
}
if (iconSet) {
kpxcIcons.icons.push({
field: field,
iconType: iconType
});
}
};
// Adds all icons from a form struct
kpxcIcons.addIconsFromForm = async function(form) {
const addUsernameIcons = async function(c) {
if (kpxc.settings.showLoginFormIcon && await kpxc.passwordFilledWithExceptions(c) === false) {
// Special case where everything else has been hidden, but a single password field is now displayed.
// For example PayPal and Amazon is handled like this.
if (c.username && !c.password && c.passwordInputs.length === 1) {
kpxcIcons.addIcon(c.passwordInputs[0], kpxcIcons.iconTypes.USERNAME);
}
if (c.username && !c.username.readOnly) {
kpxcIcons.addIcon(c.username, kpxcIcons.iconTypes.USERNAME);
} else if (c.password && (!c.username || (c.username && c.username.readOnly))) {
// Single password field
kpxcIcons.addIcon(c.password, kpxcIcons.iconTypes.USERNAME);
}
}
};
const addPasswordIcons = async function(c) {
// Show password icons also with forms without any username field
if (kpxc.settings.usePasswordGeneratorIcons
&& ((c.username && c.password) || (!c.username && c.passwordInputs.length > 0))) {
for (const input of c.passwordInputs) {
kpxcIcons.addIcon(input, kpxcIcons.iconTypes.PASSWORD);
}
}
};
const addTOTPIcons = async function(c) {
if (c.totp && kpxc.settings.showOTPIcon) {
kpxcIcons.addIcon(c.totp, kpxcIcons.iconTypes.TOTP);
}
};
await Promise.all([
await addUsernameIcons(form),
await addPasswordIcons(form),
await addTOTPIcons(form)
]);
};
// Delete all icons that have been hidden from the page view
kpxcIcons.deleteHiddenIcons = function() {
kpxcUsernameIcons.deleteHiddenIcons();
kpxcPasswordIcons.deleteHiddenIcons();
kpxcTOTPIcons.deleteHiddenIcons();
};
// Initializes all icons needed to be shown
kpxcIcons.initIcons = async function(combinations = []) {
if (combinations.length === 0) {
return;
}
for (const form of kpxcForm.savedForms) {
await kpxcIcons.addIconsFromForm(form);
}
// Check for other combinations that are not in any form,
// or there's a form that wasn't present in savedForms (and it's not null)
for (const c of combinations) {
if (!c.form || (c.form && !kpxcForm.savedForms.some(sf => sf.form === c.form))) {
await kpxcIcons.addIconsFromForm(c);
}
}
};
kpxcIcons.hasIcon = function(field) {
return !field ? false : kpxcIcons.icons.some(i => i.field === field);
};
// Sets the icons to corresponding database lock status
kpxcIcons.switchIcons = async function() {
const uuid = await sendMessage('page_get_login_id');
kpxcUsernameIcons.switchIcon(kpxc.databaseState, uuid);
kpxcPasswordIcons.switchIcon(kpxc.databaseState, uuid);
kpxcTOTPIcons.switchIcon(kpxc.databaseState, uuid);
};

View file

@ -266,7 +266,7 @@ kpxcObserverHelper.handleObserverAdd = async function(target) {
kpxc.prepareCredentials();
}
kpxcIcons.deleteHiddenIcons();
kpxcIcons.deleteAllHiddenIcons();
};
// Removes monitored elements
@ -280,7 +280,7 @@ kpxcObserverHelper.handleObserverRemove = function(target) {
return;
}
kpxcIcons.deleteHiddenIcons();
kpxcIcons.deleteAllHiddenIcons();
};
// Handles CSS transitionend event

View file

@ -11,10 +11,6 @@ kpxcPasswordIcons.switchIcon = function(state) {
kpxcPasswordIcons.icons.forEach(u => u.switchIcon(state));
};
kpxcPasswordIcons.deleteHiddenIcons = function() {
kpxcUI.deleteHiddenIcons(kpxcPasswordIcons.icons);
};
kpxcPasswordIcons.isValid = function(field) {
if (!field
|| field.readOnly
@ -34,7 +30,7 @@ class PasswordIcon extends Icon {
this.nextFieldExists = false;
this.initField(field);
kpxcUI.monitorIconPosition(this);
kpxcIcons.monitorIconPosition(this);
}
}
@ -93,7 +89,7 @@ PasswordIcon.prototype.createIcon = function(field) {
icon.addEventListener('mousedown', ev => ev.stopPropagation());
icon.addEventListener('mouseup', ev => ev.stopPropagation());
kpxcUI.setIconPosition(icon, field, this.rtl);
kpxcIcons.setIconPosition(icon, field, this.rtl);
this.icon = icon;
this.createWrapper('css/pwgen.css');
if (kpxcFields.popoverSupported) {

View file

@ -36,10 +36,6 @@ kpxcTOTPIcons.switchIcon = function(state, uuid) {
kpxcTOTPIcons.icons.forEach(u => u.switchIcon(state, uuid));
};
kpxcTOTPIcons.deleteHiddenIcons = function() {
kpxcUI.deleteHiddenIcons(kpxcTOTPIcons.icons);
};
kpxcTOTPIcons.autoCompleteIsOneTimeCode = function(field) {
if (!field) {
return false;
@ -109,7 +105,7 @@ class TOTPFieldIcon extends Icon {
super(field, databaseState, segmented);
this.initField(field, segmented);
kpxcUI.monitorIconPosition(this);
kpxcIcons.monitorIconPosition(this);
}
}
@ -185,7 +181,7 @@ TOTPFieldIcon.prototype.createIcon = function(field, segmented = false) {
icon.addEventListener('mousedown', ev => ev.stopPropagation());
icon.addEventListener('mouseup', ev => ev.stopPropagation());
kpxcUI.setIconPosition(icon, field, this.rtl, segmented);
kpxcIcons.setIconPosition(icon, field, this.rtl, segmented);
this.icon = icon;
this.createWrapper('css/totp.css');
if (kpxcFields.popoverSupported) {

View file

@ -7,9 +7,6 @@ const MIN_INPUT_FIELD_OFFSET_WIDTH = 60;
const MIN_OPACITY = 0.7;
const MAX_OPACITY = 1;
const MIN_ICON_SIZE = 14;
const MAX_ICON_SIZE = 24;
const BLUE_BUTTON = 'kpxc-button kpxc-blue-button';
const GREEN_BUTTON = 'kpxc-button kpxc-green-button';
const ORANGE_BUTTON = 'kpxc-button kpxc-orange-button';
@ -32,69 +29,6 @@ const $ = function(elem) {
return document.querySelector(elem);
};
// Basic icon class
class Icon {
constructor(field, databaseState = DatabaseState.DISCONNECTED, segmented = false) {
this.databaseState = databaseState;
this.icon = null;
this.inputField = null;
this.rtl = kpxcUI.isRTL(field);
this.segmented = segmented;
try {
this.observer = new IntersectionObserver((entries) => {
kpxcUI.updateFromIntersectionObserver(this, entries);
});
} catch (err) {
logError(err);
}
}
// Size the icon dynamically, but not greater than 24 or smaller than 14
calculateIconSize(field) {
return Math.max(Math.min(MAX_ICON_SIZE, field.offsetHeight - 4), MIN_ICON_SIZE);
}
// Creates a wrapper div that has the icon in Shadow DOM
createWrapper(styleSheetFilename) {
const styleSheet = createStylesheet(styleSheetFilename);
const wrapper = document.createElement('div');
wrapper.style.all = 'unset';
wrapper.style.display = 'none';
// Make sure the wrapper is positioned correctly without CSS styles affecting to it
wrapper.style.position = 'absolute';
wrapper.style.top = Pixels(0);
wrapper.style.left = Pixels(0);
// Waits for stylesheet to load before displaying the element
styleSheet.addEventListener('load', () => wrapper.style.display = 'block');
this.shadowRoot = wrapper.attachShadow({ mode: 'closed' });
this.shadowRoot.append(styleSheet);
this.shadowRoot.append(this.icon);
document.body.append(wrapper);
kpxcUI.observeWrapper(wrapper);
}
switchIcon(state, uuid) {
if (!this.icon) {
return;
}
if (state === DatabaseState.UNLOCKED) {
this.icon.style.filter = kpxc.credentials.length === 0 && !uuid ? 'saturate(0%)' : 'saturate(100%)';
} else {
this.icon.style.filter = 'saturate(0%)';
}
}
removeIcon() {
this.shadowRoot.removeChild(this.icon);
document.body.removeChild(this.shadowRoot.host);
}
}
const kpxcUI = {};
kpxcUI.mouseDown = false;
@ -131,63 +65,6 @@ kpxcUI.createElement = function(type, classes, attributes, textContent) {
return element;
};
kpxcUI.monitorIconPosition = function(iconClass) {
// Handle icon position on resize
window.addEventListener('resize', function(e) {
kpxcUI.updateIconPosition(iconClass);
});
// Handle icon position on scroll
window.addEventListener('scroll', function(e) {
kpxcUI.updateIconPosition(iconClass);
});
window.addEventListener('transitionend', function(e) {
if (matchesWithNodeName(e.target, 'INPUT') || matchesWithNodeName(e.target, 'TEXTAREA')) {
kpxcUI.updateIconPosition(iconClass);
}
});
};
kpxcUI.updateIconPosition = function(iconClass) {
if (iconClass.inputField && iconClass.icon) {
kpxcUI.setIconPosition(iconClass.icon, iconClass.inputField, iconClass.rtl, iconClass.segmented);
}
};
kpxcUI.calculateIconOffset = function(field, size) {
const offset = Math.floor((field.offsetHeight / 2) - (size / 2) - 1);
return (offset < 0) ? 0 : offset;
};
kpxcUI.setIconPosition = function(icon, field, rtl = false, segmented = false) {
const rect = field.getBoundingClientRect();
const size = Number(icon.getAttribute('size'));
const offset = kpxcUI.calculateIconOffset(field, size);
const zoom = kpxcUI.bodyStyle.zoom || 1;
let left = kpxcUI.getRelativeLeftPosition(rect) / zoom;
let top = kpxcUI.getRelativeTopPosition(rect) / zoom;
// Add more space for the icon to show it at the right side of the field if TOTP fields are segmented
if (segmented) {
left += size + 10;
}
// Adjusts the icon offset for certain sites
const iconOffset = kpxcSites.iconOffset(left, top, size, field?.getLowerCaseAttribute('type'));
if (iconOffset) {
left = iconOffset[0];
top = iconOffset[1];
}
const scrollTop = kpxcUI.getScrollTop() / zoom;
const scrollLeft = kpxcUI.getScrollLeft() / zoom;
icon.style.top = Pixels(top + scrollTop + offset + 1);
icon.style.left = rtl
? Pixels(left + scrollLeft + offset)
: Pixels(left + scrollLeft + field.offsetWidth - size - offset);
};
kpxcUI.getScrollTop = function() {
return document.defaultView?.scrollY ?? document.scrollingElement?.scrollTop ?? 0;
};
@ -204,32 +81,6 @@ kpxcUI.getRelativeTopPosition = function(rect) {
return kpxcUI.bodyStyle.position.toLowerCase() === 'relative' ? rect.top - kpxcUI.bodyRect.top : rect.top;
};
kpxcUI.deleteHiddenIcons = function(iconList) {
const deletedIcons = [];
for (const icon of iconList) {
if (icon.inputField && !kpxcFields.isVisible(icon.inputField)) {
const index = iconList.indexOf(icon);
icon.removeIcon();
iconList.splice(index, 1);
deletedIcons.push(icon.inputField);
// Delete the input field from detected fields so the icon can be detected again
const inputFieldIndex = kpxc.inputs.indexOf(icon.inputField);
if (inputFieldIndex >= 0) {
kpxc.inputs.splice(inputFieldIndex, 1);
}
}
}
// Remove the same icons from kpxcIcons.icons array
for (const input of deletedIcons) {
const index = kpxcIcons.icons.findIndex(e => e.field === input);
if (index >= 0) {
kpxcIcons.icons.splice(index, 1);
}
}
};
kpxcUI.isRTL = function(field) {
if (!field) {
return false;
@ -300,31 +151,6 @@ kpxcUI.makeBannerDraggable = function(banner) {
});
};
/**
* Detects if the input field appears or disappears -> show/hide the icon
* - boundingClientRect with slightly (< -10) negative values -> hidden
* - intersectionRatio === 0 -> hidden
* - isIntersecting === false -> hidden
* - intersectionRatio > 0 -> shown
* - isIntersecting === true -> shown
*/
kpxcUI.updateFromIntersectionObserver = function(iconClass, entries) {
for (const entry of entries) {
const rect = DOMRectToArray(entry.boundingClientRect);
if ((entry.intersectionRatio === 0 && !entry.isIntersecting) || (rect.some(x => x < -10))) {
iconClass.icon.style.display = 'none';
} else if (entry.intersectionRatio > 0 && entry.isIntersecting) {
iconClass.icon.style.display = 'block';
// Wait for possible DOM animations
setTimeout(() => {
kpxcUI.setIconPosition(iconClass.icon, entry.target, iconClass.rtl, iconClass.segmented);
}, 400);
}
}
};
/**
* Creates a self-disappearing notification banner to DOM
* @param {string} type Notification type: (success, info, warning, error)
@ -445,10 +271,6 @@ kpxcUI.createPageObserver = function() {
}
};
const DOMRectToArray = function(domRect) {
return [ domRect.bottom, domRect.height, domRect.left, domRect.right, domRect.top, domRect.width, domRect.x, domRect.y ];
};
const initColorTheme = function(elem) {
let theme = kpxc.settings['colorTheme'];
if (theme === 'system') {

View file

@ -12,10 +12,6 @@ kpxcUsernameIcons.switchIcon = function(state) {
kpxcUsernameIcons.icons.forEach(u => u.switchIcon(state));
};
kpxcUsernameIcons.deleteHiddenIcons = function() {
kpxcUI.deleteHiddenIcons(kpxcUsernameIcons.icons);
};
kpxcUsernameIcons.isValid = function(field) {
if (!field
|| field.offsetWidth < MIN_INPUT_FIELD_OFFSET_WIDTH
@ -34,7 +30,7 @@ class UsernameFieldIcon extends Icon {
super(field, databaseState);
this.initField(field);
kpxcUI.monitorIconPosition(this);
kpxcIcons.monitorIconPosition(this);
}
switchIcon(state) {
@ -113,7 +109,7 @@ UsernameFieldIcon.prototype.createIcon = function(field) {
icon.addEventListener('mousedown', ev => ev.stopPropagation());
icon.addEventListener('mouseup', ev => ev.stopPropagation());
kpxcUI.setIconPosition(icon, field, this.rtl);
kpxcIcons.setIconPosition(icon, field, this.rtl);
this.icon = icon;
this.createWrapper('css/username.css');
if (kpxcFields.popoverSupported) {

View file

@ -53,7 +53,8 @@
"content/fields.js",
"content/fill.js",
"content/form.js",
"content/icons.js",
"content/icon.js",
"content/icon-handler.js",
"content/keepassxc-browser.js",
"content/observer-helper.js",
"content/pwgen.js",