keepassxc-browser/keepassxc-browser/content/autocomplete.js
2020-01-26 15:57:54 +02:00

251 lines
8.3 KiB
JavaScript

'use strict';
const kpxcAutocomplete = {};
kpxcAutocomplete.autoSubmit = false;
kpxcAutocomplete.elements = [];
kpxcAutocomplete.started = false;
kpxcAutocomplete.index = -1;
kpxcAutocomplete.input = undefined;
kpxcAutocomplete.create = function(input, showListInstantly = false, autoSubmit = false) {
kpxcAutocomplete.autoSubmit = autoSubmit;
kpxcAutocomplete.input = input;
kpxcAutocomplete.started = true;
input.addEventListener('click', function(e) {
if (!e.isTrusted) {
return;
}
if (input.value !== '') {
input.select();
}
kpxcAutocomplete.showList(input);
});
input.addEventListener('keydown', kpxcAutocomplete.keyPress);
input.setAttribute('autocomplete', 'off');
if (showListInstantly) {
kpxcAutocomplete.showList(input);
}
};
kpxcAutocomplete.showList = function(inputField) {
kpxcAutocomplete.closeList();
kpxcAutocomplete.input = inputField;
const div = kpxcUI.createElement('div', 'kpxcAutocomplete-items', { 'id': 'kpxcAutocomplete-list' });
kpxcAutocomplete.updatePosition(inputField, div);
div.style.zIndex = '2147483646';
initColorTheme(div);
document.body.append(div);
for (const c of kpxcAutocomplete.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', function(e) {
if (!e.isTrusted) {
return;
}
// Save index for combination.loginId
const index = Array.prototype.indexOf.call(e.currentTarget.parentElement.childNodes, e.currentTarget);
browser.runtime.sendMessage({
action: 'page_set_login_id', args: index
});
inputField.value = this.getElementsByTagName('input')[0].value;
kpxcAutocomplete.fillPassword(inputField.value, index);
kpxcAutocomplete.closeList();
inputField.focus();
});
// These events prevent the double hover effect if both keyboard and mouse are used
item.addEventListener('mouseover', function(e) {
kpxcAutocomplete.removeItem(kpxcAutocomplete.getAllItems());
item.classList.add('kpxcAutocomplete-active');
kpxcAutocomplete.index = Array.from(div.childNodes).indexOf(item);
});
item.addEventListener('mouseout', function(e) {
item.classList.remove('kpxcAutocomplete-active');
});
div.appendChild(item);
}
// Add a footer message for auto-submit
if (kpxcAutocomplete.autoSubmit) {
const footer = kpxcUI.createElement('footer', '', {}, tr('autocompleteSubmitMessage'));
div.appendChild(footer);
}
// Activate the first item automatically
const items = kpxcAutocomplete.getAllItems();
kpxcAutocomplete.index = 0;
kpxcAutocomplete.activateItem(items);
};
kpxcAutocomplete.activateItem = function(item) {
if (!item || item.length === 0) {
return;
}
kpxcAutocomplete.removeItem(item);
if (kpxcAutocomplete.index >= item.length) {
kpxcAutocomplete.index = 0;
}
if (kpxcAutocomplete.index < 0) {
kpxcAutocomplete.index = item.length - 1;
}
if (item[kpxcAutocomplete.index] !== undefined) {
item[kpxcAutocomplete.index].classList.add('kpxcAutocomplete-active');
}
};
kpxcAutocomplete.removeItem = function(items) {
for (const item of items) {
item.classList.remove('kpxcAutocomplete-active');
}
};
kpxcAutocomplete.closeList = function(elem) {
const items = document.getElementsByClassName('kpxcAutocomplete-items');
for (const item of items) {
if (elem !== item && kpxcAutocomplete.input) {
item.parentNode.removeChild(item);
}
}
};
kpxcAutocomplete.getAllItems = function() {
const list = document.getElementById('kpxcAutocomplete-list');
if (!list) {
return [];
}
return list.getElementsByTagName('div');
};
/**
* Keyboard shortcuts for autocomplete menu:
* - ArrowDown shows the list or selects item below, or the first item (last is active)
* - ArrowUp selects item above, or the last item (first is active)
* - Enter or Tab selects the item
* - Backspace and Delete shows the list if input field is empty. First item is activated
*/
kpxcAutocomplete.keyPress = function(e) {
if (!e.isTrusted) {
return;
}
const items = kpxcAutocomplete.getAllItems();
if (e.key === 'ArrowDown') {
// If the list is not visible, show it
if (items.length === 0) {
kpxcAutocomplete.index = -1;
kpxcAutocomplete.showList(kpxcAutocomplete.input);
} else {
// Activate next item
++kpxcAutocomplete.index;
kpxcAutocomplete.activateItem(items);
}
} else if (e.key === 'ArrowUp') {
--kpxcAutocomplete.index;
kpxcAutocomplete.activateItem(items);
} else if (e.key === 'Enter') {
if (kpxcAutocomplete.input.value === '') {
e.preventDefault();
}
if (kpxcAutocomplete.index >= 0 && items && items[kpxcAutocomplete.index] !== undefined) {
e.preventDefault();
kpxcAutocomplete.input.value = e.currentTarget.value;
kpxcAutocomplete.fillPassword(kpxcAutocomplete.input.value, kpxcAutocomplete.index);
kpxcAutocomplete.closeList();
}
} else if (e.key === 'Tab') {
// Return if value is not in the list
if (kpxcAutocomplete.input.value !== '' && !kpxcAutocomplete.elements.some(c => c.value === kpxcAutocomplete.input.value)) {
kpxcAutocomplete.closeList();
return;
}
kpxcAutocomplete.index = kpxcAutocomplete.elements.findIndex(c => c.value === kpxcAutocomplete.input.value);
kpxcAutocomplete.fillPassword(kpxcAutocomplete.input.value, kpxcAutocomplete.index);
kpxcAutocomplete.closeList();
} else if (e.key === 'Escape') {
kpxcAutocomplete.closeList();
} else if ((e.key === 'Backspace' || e.key === 'Delete') && kpxcAutocomplete.input.value === '') {
// Show menu when input field has no value and backspace is pressed
kpxcAutocomplete.index = -1;
kpxcAutocomplete.showList(kpxcAutocomplete.input);
}
};
kpxcAutocomplete.fillPassword = function(value, index) {
const fieldId = kpxcAutocomplete.input.getAttribute('data-kpxc-id');
kpxcFields.prepareId(fieldId);
const givenType = kpxcAutocomplete.input.type === 'password' ? 'password' : 'username';
const combination = kpxcFields.getCombination(givenType, fieldId);
combination.loginId = index;
kpxc.fillInCredentials(combination, givenType === 'password', false);
kpxcAutocomplete.input.setAttribute('fetched', true);
};
kpxcAutocomplete.updatePosition = function(inputField, elem) {
const div = elem || $('.kpxcAutocomplete-items');
if (!div) {
return;
}
const rect = inputField.getBoundingClientRect();
div.style.top = Pixels((rect.top + document.scrollingElement.scrollTop) + inputField.offsetHeight);
div.style.left = Pixels((rect.left + document.scrollingElement.scrollLeft));
div.style.minWidth = Pixels(inputField.offsetWidth);
};
// Detect click outside autocomplete
document.addEventListener('click', function(e) {
if (!e.isTrusted) {
return;
}
const list = document.getElementById('kpxcAutocomplete-list');
if (!list) {
return;
}
if (e.target !== kpxcAutocomplete.input &&
!e.target.classList.contains('kpxc-username-icon') &&
e.target.nodeName !== kpxcAutocomplete.input.nodeName) {
kpxcAutocomplete.closeList(e.target);
}
});
// Handle autocomplete position on window resize
window.addEventListener('resize', function() {
if (!kpxc.settings.autoCompleteUsernames) {
return;
}
if (kpxcAutocomplete.input) {
kpxcAutocomplete.updatePosition(kpxcAutocomplete.input);
}
});
// Handle autocomplete position on scroll
window.addEventListener('scroll', function() {
if (!kpxc.settings.autoCompleteUsernames) {
return;
}
if (kpxcAutocomplete.input) {
kpxcAutocomplete.updatePosition(kpxcAutocomplete.input);
}
});