mirror of
https://github.com/keepassxreboot/keepassxc-browser.git
synced 2026-03-11 08:54:43 +00:00
parent
f3e0111acc
commit
e444eab33c
11 changed files with 323 additions and 317 deletions
3
dist/manifest_chromium.json
vendored
3
dist/manifest_chromium.json
vendored
|
|
@ -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",
|
||||
|
|
|
|||
3
dist/manifest_firefox.json
vendored
3
dist/manifest_firefox.json
vendored
|
|
@ -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",
|
||||
|
|
|
|||
242
keepassxc-browser/content/icon-handler.js
Normal file
242
keepassxc-browser/content/icon-handler.js
Normal 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,
|
||||
];
|
||||
};
|
||||
67
keepassxc-browser/content/icon.js
Normal file
67
keepassxc-browser/content/icon.js
Normal 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%)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue