diff --git a/dist/manifest_chromium.json b/dist/manifest_chromium.json index 1da1cf0..23e7f88 100755 --- a/dist/manifest_chromium.json +++ b/dist/manifest_chromium.json @@ -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", diff --git a/dist/manifest_firefox.json b/dist/manifest_firefox.json index 6bbf771..31ffefb 100644 --- a/dist/manifest_firefox.json +++ b/dist/manifest_firefox.json @@ -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", diff --git a/keepassxc-browser/content/icon-handler.js b/keepassxc-browser/content/icon-handler.js new file mode 100644 index 0000000..7d5a1c0 --- /dev/null +++ b/keepassxc-browser/content/icon-handler.js @@ -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, + ]; +}; diff --git a/keepassxc-browser/content/icon.js b/keepassxc-browser/content/icon.js new file mode 100644 index 0000000..bde3437 --- /dev/null +++ b/keepassxc-browser/content/icon.js @@ -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%)'; + } + } +} diff --git a/keepassxc-browser/content/icons.js b/keepassxc-browser/content/icons.js deleted file mode 100644 index f8e03c1..0000000 --- a/keepassxc-browser/content/icons.js +++ /dev/null @@ -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); -}; diff --git a/keepassxc-browser/content/observer-helper.js b/keepassxc-browser/content/observer-helper.js index 5df11e5..ddc521e 100644 --- a/keepassxc-browser/content/observer-helper.js +++ b/keepassxc-browser/content/observer-helper.js @@ -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 diff --git a/keepassxc-browser/content/pwgen.js b/keepassxc-browser/content/pwgen.js index 522f073..4899d24 100644 --- a/keepassxc-browser/content/pwgen.js +++ b/keepassxc-browser/content/pwgen.js @@ -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) { diff --git a/keepassxc-browser/content/totp-field.js b/keepassxc-browser/content/totp-field.js index f471d2c..a6507f3 100644 --- a/keepassxc-browser/content/totp-field.js +++ b/keepassxc-browser/content/totp-field.js @@ -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) { diff --git a/keepassxc-browser/content/ui.js b/keepassxc-browser/content/ui.js index be2d43c..652ca0d 100644 --- a/keepassxc-browser/content/ui.js +++ b/keepassxc-browser/content/ui.js @@ -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') { diff --git a/keepassxc-browser/content/username-field.js b/keepassxc-browser/content/username-field.js index 3316bf6..3442585 100644 --- a/keepassxc-browser/content/username-field.js +++ b/keepassxc-browser/content/username-field.js @@ -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) { diff --git a/keepassxc-browser/manifest.json b/keepassxc-browser/manifest.json index 1da1cf0..23e7f88 100755 --- a/keepassxc-browser/manifest.json +++ b/keepassxc-browser/manifest.json @@ -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",