Refactor Site Preferences options (#2666)

Refactor Site Preferences options
This commit is contained in:
Sami Vänttinen 2025-09-15 06:54:58 +03:00 committed by GitHub
parent 39c86225e3
commit a6d11091d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 224 additions and 51 deletions

View file

@ -175,6 +175,7 @@
"sendMessage": "readonly",
"showNotification": "readonly",
"siteMatch": "readonly",
"SitePreferences": "readonly",
"slashNeededForUrl": "readonly",
"SORT_BY_GROUP_AND_TITLE": "readonly",
"SORT_BY_GROUP_AND_USERNAME": "readonly",

View file

@ -678,6 +678,10 @@
"message": "Site Preferences",
"description": "Site Preferences page header."
},
"optionsSitePreferencesSettings": {
"message": "Settings",
"description": "Site Preferences settings button text."
},
"optionsMenuAbout": {
"message": "About",
"description": "About page header."

View file

@ -61,6 +61,17 @@ const ManualFill = {
BOTH: 2
};
const SitePreferences = {
ALLOW_IFRAMES: 'allowIframes',
IMPROVED_FIELD_DETECTION: 'improvedFieldDetection',
USERNAME_ONLY: 'usernameOnly',
};
// Returns a string with 'px' for CSS styles
const Pixels = function(value) {
return String(value) + 'px';
};
const compareVersion = function(minimum, current, canBeEqual = true) {
if (!minimum || !current || minimum?.indexOf('.') === -1 || current?.indexOf('.') === -1) {
return false;

View file

@ -62,10 +62,10 @@ kpxc.addToSitePreferences = async function(optionName, addWildcard = false) {
await sendMessage('save_settings', kpxc.settings);
if (optionName === 'allowIframes') {
if (optionName === SitePreferences.ALLOW_IFRAMES) {
await sendMessage('page_set_allow_iframes', [ true, site ]);
await sendMessage('iframe_detected', false);
} else if (optionName === 'usernameOnly') {
} else if (optionName === SitePreferences.USERNAME_ONLY) {
await sendMessage('username_field_detected', false);
}
};
@ -952,9 +952,9 @@ browser.runtime.onMessage.addListener(async function(req, sender) {
if (req.action === 'activated_tab') {
kpxc.triggerActivatedTab();
} else if (req.action === 'add_allow_iframes_option') {
kpxc.addToSitePreferences('allowIframes');
kpxc.addToSitePreferences(SitePreferences.ALLOW_IFRAMES);
} else if (req.action === 'add_username_only_option') {
kpxc.addToSitePreferences('usernameOnly', true);
kpxc.addToSitePreferences(SitePreferences.USERNAME_ONLY, true);
} else if (req.action === 'check_database_hash' && 'hash' in req) {
kpxc.detectDatabaseChange(req);
} else if (req.action === 'choose_credential_fields') {

View file

@ -32,11 +32,6 @@ const $ = function(elem) {
return document.querySelector(elem);
};
// Returns a string with 'px' for CSS styles
const Pixels = function(value) {
return String(value) + 'px';
};
// Basic icon class
class Icon {
constructor(field, databaseState = DatabaseState.DISCONNECTED, segmented = false) {

View file

@ -4,6 +4,7 @@
--kpxc-autocomplete-menu-border: 1px solid #ddd;
--kpxc-background-color: #fff;
--kpxc-box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--kpxc-box-shadow-strong: 0 20px 25px -5px rgb(0 0 0 / 0.8), 0 8px 10px -6px rgb(0 0 0 / 0.8);
--kpxc-card-background-color: #fff;
--kpxc-card-border-color: rgba(0, 0, 0, .125);
--kpxc-card-header-color: #fafafa;
@ -29,7 +30,6 @@
}
@media (prefers-color-scheme: dark) {
:root,
:host {
--kpxc-autocomplete-footer-color: #2b2a2a;
@ -114,4 +114,3 @@
--kpxc-lighter-text-color: rgba(0, 0, 0, 0.8);
--kpxc-light-text-color: rgba(0, 0, 0, 0.5);
}
}

View file

@ -105,12 +105,6 @@ tbody {
color: var(--kpxc-text-color);
}
.table-striped > tbody > tr:nth-of-type(odd) input[type="checkbox"] {
background-color: var(--kpxc-card-background-color) !important;
border-color: var(--kpxc-input-main-border-color) !important;
color: var(--kpxc-text-color) !important;
}
.table-striped > tbody > tr:nth-of-type(odd) .form-check-input:checked {
background-color: var(--kpxc-checkbox-background-color) !important;
}
@ -136,14 +130,14 @@ table tbody tr.empty:not(:nth-last-child(2)) {
display: none;
}
#tab-site-preferences td:nth-of-type(2),
#tab-site-preferences td:nth-of-type(2) select {
#tab-site-preferences td:nth-of-type(3),
#tab-site-preferences td:nth-of-type(3) select {
width: 200px;
}
#tab-site-preferences td:nth-of-type(2),
#tab-site-preferences td:nth-of-type(3),
#tab-site-preferences td:nth-of-type(4),
#tab-site-preferences td:nth-of-type(5),
table td:last-of-type {
width: 1px;
}
@ -233,6 +227,26 @@ table td:last-of-type {
color: var(--kpxc-text-color);
}
.non-clickable {
pointer-events: none;
}
.settings-dropdown {
box-shadow: var(--kpxc-box-shadow-strong);
overflow: hidden;
position: absolute;
}
.settings-dropdown-body {
background-color: var(--bs-table-bg);
}
.dropdown-divider {
border-top: 1px solid var(--kpxc-table-odd-color);
height: 0.5rem;
margin-top: 0.3rem;
}
.site-preferences-input {
border: 0;
border-bottom-right-radius: 4px !important;

View file

@ -260,7 +260,7 @@
<input class="form-check-input" type="checkbox" name="autoFillSingleEntry" id="autoFillSingleEntry" value="true">
<label class="form-check-label" for="autoFillSingleEntry" data-i18n="optionsCheckboxAutoFillSingleEntry"></label>
<div class="form-text" data-i18n="optionsAutoFillSingleEntryHelpText"></div>
<div class="alert alert-warning mt-3 col-lg-9" role="alert">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
<span data-i18n="optionsAutoFillSingleEntryWarning"></span>
@ -761,16 +761,14 @@
<thead>
<tr>
<th scope="col"><span data-i18n="optionsColumnPageURL"></span></th>
<th scope="col"></th>
<th scope="col"><span data-i18n="optionsColumnIgnore"></span></th>
<th scope="col"><span data-i18n="optionsColumnUsernameOnly"></span></th>
<th scope="col"><span data-i18n="optionsColumnImprovedInputFieldDetection"></span></th>
<th scope="col"><span data-i18n="optionsColumnAllowIframes"></span></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr class="empty">
<td colspan="5"><span data-i18n="optionsSitePreferencesNotFound"></span></td>
<td colspan="4"><span data-i18n="optionsSitePreferencesNotFound"></span></td>
</tr>
<tr class="clone d-none">
<td class="text-nowrap">
@ -790,17 +788,19 @@
</button>
</div>
</td>
<td>
<select class="form-select form-select-sm" name="ignore" data-i18n="[title]optionsSitePreferencesSelect">
<td class="text-nowrap">
<button type="button" class="btn btn-sm btn-primary container-fluid" id="settings-button">
<span class="non-clickable" data-i18n="optionsSitePreferencesSettings"></span>
</button>
</td>
<td>
<select class="form-select form-select-sm" id="ignore-select" name="ignore" data-i18n="[title]optionsSitePreferencesSelect">
<option value="ignoreNothing" data-i18n="optionsSelectionNothing"></option>
<option value="ignoreNormal" data-i18n="optionsSelectionNormal"></option>
<option value="ignoreAutoSubmit" data-i18n="optionsSelectionAutoSubmit"></option>
<option value="ignoreFull" data-i18n="optionsSelectionFull"></option>
</select>
</td>
<td><input class="form-check-input" type="checkbox" name="usernameOnly" value="false" data-i18n="[title]optionsColumnUsernameOnly"></td>
<td><input class="form-check-input" type="checkbox" name="improvedFieldDetection" value="false" data-i18n="[title]optionsColumnImprovedInputFieldDetection"></td>
<td><input class="form-check-input" type="checkbox" name="allowIframes" value="false" data-i18n="[title]optionsColumnAllowIframes"></td>
<td class="text-nowrap">
<button class="btn btn-sm delete btn-danger" data-i18n="[title]removeSitePreferencesButtonTitle">
<i class="fa fa-remove" aria-hidden="true"></i>
@ -838,6 +838,32 @@
</div>
</div>
</div>
<!-- Settings dropdown -->
<div class="card settings-dropdown" style="display: none;">
<div class="card-body settings-dropdown-body">
<!-- Username only detection -->
<div class="form-check">
<input class="form-check-input" type="checkbox" id="usernameOnly" name="usernameOnly" value="false" data-i18n="[title]optionsColumnUsernameOnly">
<label class="form-check-label" for="usernameOnly" data-i18n="optionsColumnUsernameOnly"></label>
</div>
<!-- Improved input field detection -->
<div class="form-check">
<input class="form-check-input" type="checkbox" id="improvedFieldDetection" name="improvedFieldDetection" value="false" data-i18n="[title]optionsColumnImprovedInputFieldDetection">
<label class="form-check-label" for="improvedFieldDetection" data-i18n="optionsColumnImprovedInputFieldDetection"></label>
</div>
<!-- Allow iFrames -->
<div class="form-check">
<input class="form-check-input" type="checkbox" id="allowIframes" name="allowIframes" value="false" data-i18n="[title]optionsColumnAllowIframes">
<label class="form-check-label" for="allowIframes" data-i18n="optionsColumnAllowIframes"></label>
</div>
<!-- Add new options below -->
<!--<hr class="dropdown-divider">-->
</div>
</div>
</div>
<!-- About -->

View file

@ -1,6 +1,7 @@
'use strict';
const options = {};
options.dropdownButton = null;
const $ = function(elem) {
return document.querySelector(elem);
@ -536,6 +537,43 @@ options.initSitePreferences = function() {
modalEvent.currentTarget.querySelector('.modal-footer button.yes').focus();
});
const settingsButtonClicked = function(e) {
e.preventDefault();
const closestTr = e.target.closest('tr');
const url = closestTr.getAttribute('url');
const sitePreferences = options.settings['sitePreferences']?.find((pref) => pref?.url === url);
const usernameOnly = sitePreferences.usernameOnly;
const improvedFieldDetection = sitePreferences.improvedFieldDetection;
const allowIframes = sitePreferences.allowIframes;
const dropdown = $('.settings-dropdown');
if (dropdown?.style?.display !== 'block'
|| (dropdown?.style?.display === 'block' && e.target !== options.dropdownButton)) {
if (options.dropdownButton) {
options.dropdownButton.classList.remove('active');
// Update number of enabled settings to the old Settings button
const checkboxValues = Array.from(dropdown?.querySelectorAll('input[type=checkbox]')).map(c => c.checked);
updateSettingsButtonText(options.dropdownButton, checkboxValues);
}
// Apply current settings to the dropdown
dropdown.querySelector('#usernameOnly').checked = usernameOnly;
dropdown.querySelector('#improvedFieldDetection').checked = improvedFieldDetection;
dropdown.querySelector('#allowIframes').checked = allowIframes;
dropdown?.show();
updateDropdownPosition(e, dropdown);
options.dropdownButton = e.target;
options.dropdownButton.classList.add('active');
} else {
dropdown?.hide();
options.dropdownButton.classList.remove('active');
updateSettingsButtonText(options.dropdownButton, [ usernameOnly, improvedFieldDetection, allowIframes ]);
options.dropdownButton = null;
}
};
const removeButtonClicked = function(e) {
e.preventDefault();
@ -601,18 +639,18 @@ options.initSitePreferences = function() {
}
};
const checkboxClicked = function() {
const closestTr = this.closest('tr');
const checkboxClicked = async function(e) {
const closestTr = options?.dropdownButton?.closest('tr');
const url = closestTr.getAttribute('url');
for (const site of options.settings['sitePreferences']) {
if (site.url === url) {
if (this.name === 'usernameOnly') {
site.usernameOnly = this.checked;
} else if (this.name === 'improvedFieldDetection') {
site.improvedFieldDetection = this.checked;
} else if (this.name === 'allowIframes') {
site.allowIframes = this.checked;
if (e.target.name === SitePreferences.USERNAME_ONLY) {
site.usernameOnly = e.target.checked;
} else if (e.target.name === SitePreferences.IMPROVED_FIELD_DETECTION) {
site.improvedFieldDetection = e.target.checked;
} else if (e.target.name === SitePreferences.ALLOW_IFRAMES) {
site.allowIframes = e.target.checked;
}
}
}
@ -633,6 +671,11 @@ options.initSitePreferences = function() {
options.saveSettings();
};
const dropdown = $('.settings-dropdown');
dropdown.querySelector('#usernameOnly').addEventListener('change', checkboxClicked);
dropdown.querySelector('#improvedFieldDetection').addEventListener('change',checkboxClicked);
dropdown.querySelector('#allowIframes').addEventListener('change', checkboxClicked);
const addNewRow = function(rowClone, newIndex, url, ignore, usernameOnly, improvedFieldDetection, allowIframes) {
const row = rowClone.cloneNode(true);
row.setAttribute('url', url);
@ -662,19 +705,25 @@ options.initSitePreferences = function() {
saveModifiedUrl(e, row, inputField, editButton, cancelButton, saveButton)
);
// Page URL
row.children[0].children[0].children[0].value = url;
row.children[0].children[0]?.addEventListener('dblclick', (e) =>
enterEditMode(e, row, inputField, editButton, cancelButton, saveButton)
);
row.children[1].children[0].value = ignore;
row.children[1].children[0].addEventListener('change', selectionChanged);
row.children[2].children['usernameOnly'].checked = usernameOnly;
row.children[2].children['usernameOnly'].addEventListener('change', checkboxClicked);
row.children[3].children['improvedFieldDetection'].checked = improvedFieldDetection;
row.children[3].children['improvedFieldDetection'].addEventListener('change', checkboxClicked);
row.children[4].children['allowIframes'].checked = allowIframes;
row.children[4].children['allowIframes'].addEventListener('change', checkboxClicked);
row.children[5].addEventListener('click', removeButtonClicked);
// Settings
const settings = row.children[1];
updateSettingsButtonText(settings.querySelector('#settings-button'),
[ usernameOnly, improvedFieldDetection, allowIframes ]);
settings.querySelector('#settings-button').addEventListener('click', (e) => settingsButtonClicked(e));
// Ignore
const ignoreSelect = row.children[2];
ignoreSelect.querySelector('#ignore-select').value = ignore;
ignoreSelect.querySelector('#ignore-select').addEventListener('change', selectionChanged);
// Remove button
row.children[3].addEventListener('click', removeButtonClicked);
$('#tab-site-preferences table tbody').append(row);
};
@ -738,11 +787,11 @@ options.initSitePreferences = function() {
$('#tab-site-preferences table tbody tr.empty').hide();
options.settings['sitePreferences'].push({
url: value,
ignore: IGNORE_NOTHING,
usernameOnly: false,
improvedFieldDetection: false,
allowIframes: false,
ignore: IGNORE_NOTHING,
improvedFieldDetection: false,
url: value,
usernameOnly: false,
});
options.saveSettings();
manualUrl.value = '';
@ -823,6 +872,80 @@ const getBrowserId = function() {
return 'Other/Unknown';
};
// Update the number of enabled settings to the button text
const updateSettingsButtonText = function(buttonElement, enabledOptions = []) {
const numberOfEnabledOptions = enabledOptions.filter(o => o === true).length;
const buttonText = buttonElement.querySelector('span');
if (numberOfEnabledOptions > 0) {
buttonText.textContent =
`${browser.i18n.getMessage('optionsSitePreferencesSettings')} (${numberOfEnabledOptions})`;
} else {
buttonText.textContent = browser.i18n.getMessage('optionsSitePreferencesSettings');
}
};
// Updates settings dropdown menu position
const updateDropdownPosition = function(e, dropdown) {
if (!dropdown) {
dropdown = $('.settings-dropdown');
}
const settingsButton = e?.target ?? options.dropdownButton;
const rect = settingsButton?.getClientRects()?.[0];
if (!rect) {
return;
}
const zoom = getComputedStyle(document.body).zoom || 1;
const scrollTop = document.defaultView.scrollY / zoom;
const scrollLeft = document.defaultView?.scrollX / zoom;
// If dropdown does not fit to the bottom of the screen -> show it at the top of the settings button
const dropdownRect = dropdown.getBoundingClientRect();
const totalHeight = dropdownRect.height + rect.height;
const offset = (totalHeight + rect.y) / zoom > window.self.visualViewport.height ? totalHeight / zoom : 0;
dropdown.style.left = Pixels(rect.left / zoom + scrollLeft);
dropdown.style.top = Pixels(rect.bottom / zoom + scrollTop - offset);
};
// Hides the settings dropdown when clicked outside of it
document.addEventListener('mouseup', function(e) {
if (!e.isTrusted) {
return;
}
const dropdown = $('.settings-dropdown');
if (dropdown?.style?.display !== 'block') {
return;
}
const rect = dropdown?.getClientRects()?.[0];
if (!rect) {
return;
}
if ((e.x > rect.right || e.x < rect.x) || (e.y > rect.bottom || e.y < rect.y)
&& e.target.nodeName !== 'BUTTON') {
dropdown?.hide();
const checkboxValues = Array.from(dropdown?.querySelectorAll('input[type=checkbox]')).map(c => c.checked);
updateSettingsButtonText(options.dropdownButton, checkboxValues);
options.dropdownButton.classList.remove('active');
options.dropdownButton = null;
}
});
// Handle dropdown position on window resize
window.addEventListener('resize', function() {
updateDropdownPosition();
});
// Handle dropdown position on scroll
window.addEventListener('scroll', function() {
updateDropdownPosition();
});
(async() => {
try {
// We eagerly load the theme here to avoid a white flash