mirror of
https://github.com/keepassxreboot/keepassxc-browser.git
synced 2026-03-11 08:54:43 +00:00
parent
2a78057eb5
commit
a2576e19eb
5 changed files with 144 additions and 152 deletions
|
|
@ -9,19 +9,20 @@ class Autocomplete {
|
|||
this.elements = [];
|
||||
this.index = -1;
|
||||
this.input = undefined;
|
||||
this.shadowRoot = undefined;
|
||||
this.wrapper = undefined;
|
||||
this.shadowRoot = undefined;
|
||||
this.container = undefined;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.elements = [];
|
||||
}
|
||||
|
||||
async click(e, input) {
|
||||
async click(e) {
|
||||
|
||||
}
|
||||
|
||||
async itemClick(e, item, input, uuid) {
|
||||
async itemClick(e, input, uuid) {
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -37,8 +38,9 @@ class Autocomplete {
|
|||
this.autoSubmit = autoSubmit;
|
||||
|
||||
if (!this.autocompleteList.includes(input)) {
|
||||
input.addEventListener('click', ev => this.click(ev, input));
|
||||
input.addEventListener('keydown', ev => this.keyPress(ev));
|
||||
input.addEventListener('click', e => this.click(e));
|
||||
input.addEventListener('keydown', e => this.keyDown(e));
|
||||
input.addEventListener('keyup', e => this.keyUp(e));
|
||||
input.setAttribute('autocomplete', 'off');
|
||||
this.autocompleteList.push(input);
|
||||
}
|
||||
|
|
@ -48,17 +50,21 @@ class Autocomplete {
|
|||
}
|
||||
}
|
||||
|
||||
mouseOver(e, div, item) {
|
||||
this.removeItem(this.getAllItems());
|
||||
item.classList.add('kpxcAutocomplete-active');
|
||||
this.index = Array.from(div.childNodes).indexOf(item);
|
||||
}
|
||||
|
||||
mouseOut(e, item) {
|
||||
item.classList.remove('kpxcAutocomplete-active');
|
||||
mouseMove(e) {
|
||||
if (e.movementX === 0 && e.movementY === 0) {
|
||||
return;
|
||||
}
|
||||
this.deselectItem();
|
||||
e.target.classList.add('kpxcAutocomplete-active');
|
||||
const items = this.getAllItems();
|
||||
this.index = Array.from(items).indexOf(e.target);
|
||||
}
|
||||
|
||||
async showList(inputField) {
|
||||
if (this.input === inputField) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeList();
|
||||
|
||||
// Return if there are no credentials
|
||||
|
|
@ -67,104 +73,80 @@ class Autocomplete {
|
|||
}
|
||||
|
||||
this.input = inputField;
|
||||
this.input.select();
|
||||
|
||||
const div = kpxcUI.createElement('div', 'kpxcAutocomplete-items', { 'id': 'kpxcAutocomplete-list' });
|
||||
initColorTheme(div);
|
||||
if (!this.wrapper) {
|
||||
const styleSheet = createStylesheet('css/autocomplete.css');
|
||||
const colorStyleSheet = createStylesheet('css/colors.css');
|
||||
this.wrapper = kpxcUI.createElement('div');
|
||||
this.container = kpxcUI.createElement('div', 'kpxcAutocomplete-container', { 'id': 'kpxcAutocomplete-container' });
|
||||
|
||||
this.updatePosition(inputField, div);
|
||||
div.style.zIndex = '2147483646';
|
||||
this.shadowRoot = this.wrapper.attachShadow({ mode: 'closed' });
|
||||
this.shadowRoot.append(colorStyleSheet);
|
||||
this.shadowRoot.append(styleSheet);
|
||||
|
||||
const styleSheet = createStylesheet('css/autocomplete.css');
|
||||
const colorStyleSheet = createStylesheet('css/colors.css');
|
||||
const wrapper = kpxcUI.createElement('div');
|
||||
this.list = kpxcUI.createElement('div', 'kpxcAutocomplete-items', { 'id': 'kpxcAutocomplete-list' });
|
||||
initColorTheme(this.list);
|
||||
|
||||
this.shadowRoot = wrapper.attachShadow({ mode: 'closed' });
|
||||
this.shadowRoot.append(colorStyleSheet);
|
||||
this.shadowRoot.append(styleSheet);
|
||||
this.shadowRoot.append(div);
|
||||
this.wrapper = wrapper;
|
||||
document.body.append(wrapper);
|
||||
this.container.append(this.list);
|
||||
this.shadowRoot.append(this.container);
|
||||
document.body.append(this.wrapper);
|
||||
|
||||
await kpxc.updateTOTPList();
|
||||
for (const c of this.elements) {
|
||||
const item = document.createElement('div');
|
||||
item.textContent += c.label;
|
||||
const itemInput = kpxcUI.createElement('input', '', { 'type': 'hidden', 'value': c.value });
|
||||
item.append(itemInput);
|
||||
item.addEventListener('click', ev => this.itemClick(ev, item, inputField, c.uuid));
|
||||
await kpxc.updateTOTPList();
|
||||
for (const c of this.elements) {
|
||||
const item = document.createElement('div');
|
||||
item.textContent = c.label;
|
||||
const itemInput = kpxcUI.createElement('input', '', { 'type': 'hidden', 'value': c.value });
|
||||
item.append(itemInput);
|
||||
item.addEventListener('click', e => this.itemClick(e, this.input, c.uuid));
|
||||
|
||||
// These events prevent the double hover effect if both keyboard and mouse are used
|
||||
item.addEventListener('mouseover', ev => this.mouseOver(ev, div, item));
|
||||
item.addEventListener('mouseout', ev => this.mouseOut(ev, item));
|
||||
// These events prevent the double hover effect if both keyboard and mouse are used
|
||||
item.addEventListener('mousemove', e => this.mouseMove(e));
|
||||
|
||||
item.addEventListener('mousedown', ev => ev.stopPropagation());
|
||||
item.addEventListener('mouseup', ev => ev.stopPropagation());
|
||||
item.addEventListener('mousedown', e => e.stopPropagation());
|
||||
item.addEventListener('mouseup', e => e.stopPropagation());
|
||||
|
||||
div.appendChild(item);
|
||||
this.list.appendChild(item);
|
||||
}
|
||||
|
||||
// Add a footer message for auto-submit
|
||||
if (this.autoSubmit) {
|
||||
const footer = kpxcUI.createElement('footer', '', {}, tr('autocompleteSubmitMessage'));
|
||||
this.container.appendChild(footer);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a footer message for auto-submit
|
||||
if (this.autoSubmit) {
|
||||
const footer = kpxcUI.createElement('footer', '', {}, tr('autocompleteSubmitMessage'));
|
||||
div.appendChild(footer);
|
||||
}
|
||||
this.updatePosition();
|
||||
this.container.style.display = 'block';
|
||||
}
|
||||
|
||||
// Activate the first item automatically
|
||||
selectItem() {
|
||||
this.deselectItem();
|
||||
const items = this.getAllItems();
|
||||
this.index = 0;
|
||||
this.activateItem(items);
|
||||
}
|
||||
|
||||
activateItem(item) {
|
||||
if (!item || item.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeItem(item);
|
||||
if (this.index >= item.length) {
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
if (this.index < 0) {
|
||||
this.index = item.length - 1;
|
||||
}
|
||||
|
||||
if (item[this.index] !== undefined) {
|
||||
item[this.index].classList.add('kpxcAutocomplete-active');
|
||||
const item = items[this.index];
|
||||
if (item !== undefined) {
|
||||
item.classList.add('kpxcAutocomplete-active');
|
||||
item.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
removeItem(items) {
|
||||
deselectItem() {
|
||||
const items = this.list.querySelectorAll('div.kpxcAutocomplete-active');
|
||||
for (const item of items) {
|
||||
item.classList.remove('kpxcAutocomplete-active');
|
||||
}
|
||||
}
|
||||
|
||||
closeList(elem) {
|
||||
closeList() {
|
||||
this.input = undefined;
|
||||
if (!this.shadowRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = this.shadowSelectorAll('.kpxcAutocomplete-items');
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
if (elem !== item && this.input) {
|
||||
item.parentNode.removeChild(item);
|
||||
}
|
||||
}
|
||||
this.container.style.display = 'none';
|
||||
}
|
||||
|
||||
getAllItems() {
|
||||
const list = this.shadowSelector('#kpxcAutocomplete-list');
|
||||
if (!list) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return list.getElementsByTagName('div');
|
||||
return this.list.getElementsByTagName('div');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -174,43 +156,46 @@ class Autocomplete {
|
|||
* - Enter or Tab selects the item
|
||||
* - Backspace and Delete shows the list if input field is empty. First item is activated
|
||||
*/
|
||||
keyPress(e) {
|
||||
async keyDown(e) {
|
||||
if (!e.isTrusted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = this.getAllItems();
|
||||
const inputField = e.target;
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
// If the list is not visible, show it
|
||||
if (items.length === 0) {
|
||||
this.index = -1;
|
||||
this.showList(inputField);
|
||||
if (!this.input) {
|
||||
await this.showList(inputField);
|
||||
this.index = 0;
|
||||
if (inputField.value !== '') {
|
||||
this.updateSearch();
|
||||
}
|
||||
requestAnimationFrame(() => this.selectItem());
|
||||
} else {
|
||||
// Activate next item
|
||||
++this.index;
|
||||
this.activateItem(items);
|
||||
const items = this.getAllItems();
|
||||
this.index = (this.index+1) % items.length;
|
||||
this.selectItem();
|
||||
}
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
--this.index;
|
||||
this.activateItem(items);
|
||||
} else if (e.key === 'Enter') {
|
||||
} else if (e.key === 'ArrowUp' && this.list) {
|
||||
e.preventDefault();
|
||||
const items = this.getAllItems();
|
||||
this.index = (this.index > 0 ? this.index : items.length) - 1;
|
||||
this.selectItem();
|
||||
} else if (e.key === 'Enter' && this.input) {
|
||||
if (inputField.value === '') {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (this.index >= 0 && items && items[this.index] !== undefined) {
|
||||
const items = this.getAllItems();
|
||||
if (this.index >= 0 && items[this.index] !== undefined) {
|
||||
e.preventDefault();
|
||||
|
||||
this.itemEnter(this.index, this.elements);
|
||||
await this.itemEnter(this.index, this.elements);
|
||||
this.closeList();
|
||||
}
|
||||
} else if (e.key === 'Tab') {
|
||||
// Return if the list is not open
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (e.key === 'Tab' && this.input) {
|
||||
// Return if value is not in the list
|
||||
if (inputField.value !== '' && !this.elements.some(c => c.value === inputField.value)) {
|
||||
this.closeList();
|
||||
|
|
@ -227,26 +212,45 @@ class Autocomplete {
|
|||
this.closeList();
|
||||
} else if ((e.key === 'Backspace' || e.key === 'Delete') && inputField.value === '') {
|
||||
// Show menu when input field has no value and backspace is pressed
|
||||
this.index = -1;
|
||||
this.showList(inputField);
|
||||
this.index = 0;
|
||||
this.selectItem();
|
||||
}
|
||||
}
|
||||
|
||||
updatePosition(inputField, elem) {
|
||||
const div = elem || this.shadowSelector('.kpxcAutocomplete-items');
|
||||
if (!div) {
|
||||
keyUp(e) {
|
||||
if (!this.input || !e.isTrusted || e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = inputField.getBoundingClientRect();
|
||||
div.style.minWidth = Pixels(inputField.offsetWidth);
|
||||
this.updateSearch();
|
||||
this.selectItem();
|
||||
}
|
||||
|
||||
updateSearch() {
|
||||
if (this.index !== -1 && this.elements[this.index]?.value?.includes(this.input.value)) {
|
||||
return;
|
||||
}
|
||||
this.index = this.elements.findIndex(c => c.value.startsWith(this.input.value));
|
||||
if (this.index === -1) {
|
||||
this.index = this.elements.findIndex(c => c.value.includes(this.input.value));
|
||||
}
|
||||
}
|
||||
|
||||
updatePosition() {
|
||||
if (!this.container || !this.input) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = this.input.getBoundingClientRect();
|
||||
this.container.style.minWidth = Pixels(this.input.offsetWidth);
|
||||
|
||||
if (kpxcUI.bodyStyle.position.toLowerCase() === 'relative') {
|
||||
div.style.top = Pixels(rect.top - kpxcUI.bodyRect.top + document.scrollingElement.scrollTop + inputField.offsetHeight);
|
||||
div.style.left = Pixels(rect.left - kpxcUI.bodyRect.left + document.scrollingElement.scrollLeft);
|
||||
this.container.style.top = Pixels(rect.top - kpxcUI.bodyRect.top + document.scrollingElement.scrollTop + this.input.offsetHeight);
|
||||
this.container.style.left = Pixels(rect.left - kpxcUI.bodyRect.left + document.scrollingElement.scrollLeft);
|
||||
} else {
|
||||
div.style.top = Pixels(rect.top + document.scrollingElement.scrollTop + inputField.offsetHeight);
|
||||
div.style.left = Pixels(rect.left + document.scrollingElement.scrollLeft);
|
||||
this.container.style.top = Pixels(rect.top + document.scrollingElement.scrollTop + this.input.offsetHeight);
|
||||
this.container.style.left = Pixels(rect.left + document.scrollingElement.scrollLeft);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -254,25 +258,10 @@ class Autocomplete {
|
|||
|
||||
// Global handlers
|
||||
const handleOutsideClick = function(e, autocompleteMenu) {
|
||||
const list = autocompleteMenu.shadowRoot ? autocompleteMenu.shadowSelector('#kpxcAutocomplete-list') : undefined;
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target !== autocompleteMenu.input
|
||||
&& !e.target.classList.contains('kpxc-username-icon')
|
||||
&& e.target.nodeName !== autocompleteMenu.input.nodeName) {
|
||||
autocompleteMenu.closeList(e.target);
|
||||
|
||||
if (autocompleteMenu.wrapper) {
|
||||
document.body.removeChild(autocompleteMenu.wrapper);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updatePosition = function(autocompleteMenu) {
|
||||
if (autocompleteMenu.input) {
|
||||
autocompleteMenu.updatePosition(autocompleteMenu.input);
|
||||
&& e.target !== autocompleteMenu.input) {
|
||||
autocompleteMenu.closeList();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -292,8 +281,8 @@ window.addEventListener('resize', function() {
|
|||
return;
|
||||
}
|
||||
|
||||
updatePosition(kpxcUserAutocomplete);
|
||||
updatePosition(kpxcTOTPAutocomplete);
|
||||
kpxcUserAutocomplete.updatePosition();
|
||||
kpxcTOTPAutocomplete.updatePosition();
|
||||
});
|
||||
|
||||
// Handle autocomplete position on scroll
|
||||
|
|
@ -302,6 +291,6 @@ window.addEventListener('scroll', function() {
|
|||
return;
|
||||
}
|
||||
|
||||
updatePosition(kpxcUserAutocomplete);
|
||||
updatePosition(kpxcTOTPAutocomplete);
|
||||
kpxcUserAutocomplete.updatePosition();
|
||||
kpxcTOTPAutocomplete.updatePosition();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,24 +1,21 @@
|
|||
'use strict';
|
||||
|
||||
class CredentialAutocomplete extends Autocomplete {}
|
||||
CredentialAutocomplete.prototype.click = async function(e, input) {
|
||||
CredentialAutocomplete.prototype.click = async function(e) {
|
||||
if (!e.isTrusted) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
if (input.value !== '') {
|
||||
input.select();
|
||||
}
|
||||
|
||||
const field = this.autocompleteList.find(a => a === input);
|
||||
const field = this.autocompleteList.find(a => a === e.target);
|
||||
if (field) {
|
||||
this.showList(field);
|
||||
await this.showList(field);
|
||||
this.updateSearch();
|
||||
}
|
||||
};
|
||||
|
||||
CredentialAutocomplete.prototype.itemClick = async function(e, item, input, uuid) {
|
||||
CredentialAutocomplete.prototype.itemClick = async function(e, input, uuid) {
|
||||
if (!e.isTrusted) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -26,7 +23,7 @@ CredentialAutocomplete.prototype.itemClick = async function(e, item, input, uuid
|
|||
e.stopPropagation();
|
||||
|
||||
const index = Array.prototype.indexOf.call(e.currentTarget.parentElement.childNodes, e.currentTarget);
|
||||
const usernameValue = item.getElementsByTagName('input')[0].value;
|
||||
const usernameValue = e.target.getElementsByTagName('input')[0].value;
|
||||
await this.fillPassword(usernameValue, index, uuid);
|
||||
|
||||
this.closeList();
|
||||
|
|
@ -46,7 +43,6 @@ CredentialAutocomplete.prototype.fillPassword = async function(value, index, uui
|
|||
|
||||
const manualFill = await sendMessage('page_get_manual_fill');
|
||||
await kpxc.fillInCredentials(combination, value, uuid, manualFill === ManualFill.PASSWORD);
|
||||
this.input.setAttribute('fetched', true);
|
||||
};
|
||||
|
||||
const kpxcUserAutocomplete = new CredentialAutocomplete();
|
||||
|
|
|
|||
|
|
@ -1110,6 +1110,8 @@ kpxc.fillInCredentials = async function(combination, predefinedUsername, uuid, p
|
|||
} else if (combination.form) {
|
||||
combination.form.submit();
|
||||
}
|
||||
} else {
|
||||
(combination.username || combination.password).focus();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ TOTPAutocomplete.prototype.click = async function(e, input) {
|
|||
this.showList(input, true);
|
||||
};
|
||||
|
||||
TOTPAutocomplete.prototype.itemClick = async function(e, item, input, uuid) {
|
||||
TOTPAutocomplete.prototype.itemClick = async function(e, input, uuid) {
|
||||
if (!e.isTrusted) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
.kpxcAutocomplete-items {
|
||||
#kpxcAutocomplete-container {
|
||||
display: none;
|
||||
z-index: 2147483646;
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
.kpxcAutocomplete-items {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
|
@ -23,7 +28,12 @@
|
|||
border-bottom-left-radius: 4px !important;
|
||||
}
|
||||
|
||||
.kpxcAutocomplete-items footer {
|
||||
.kpxcAutocomplete-active {
|
||||
background-color: #28a745 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
#kpxcAutocomplete-container footer {
|
||||
padding: 5px;
|
||||
background-color: #ddd;
|
||||
border: 1px solid rgba(0,0,0,.125) !important;
|
||||
|
|
@ -33,11 +43,6 @@
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
.kpxcAutocomplete-active {
|
||||
background-color: #28a745 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.kpxcAutocomplete-items {
|
||||
background: var(--kpxc-background-color);
|
||||
|
|
|
|||
Loading…
Reference in a new issue