Custom Login Fields banner selector (#1390)

New Custom Login Fields banner selector.
This commit is contained in:
Sami Vänttinen 2022-06-22 07:21:05 +03:00 committed by GitHub
parent 32ee510565
commit ba5a7141fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 924 additions and 638 deletions

View file

@ -99,7 +99,7 @@
"kpxc": true,
"kpActions": true,
"kpxcBanner": true,
"kpxcDefine": true,
"kpxcCustomLoginFieldsBanner": true,
"kpxcFields": true,
"kpxcFill": true,
"kpxcForm": true,

View file

@ -183,6 +183,10 @@
"message": "Username field icon",
"description": "Username field icon text."
},
"totp": {
"message": "TOTP",
"description": "TOTP text."
},
"totpFieldText": {
"message": "Fill TOTP from KeePassXC",
"description": "OTP field icon hover text."
@ -191,9 +195,9 @@
"message": "TOTP field icon",
"description": "OTP field icon text."
},
"defineDismiss": {
"message": "Dismiss",
"description": "Dismiss button text when choosing custom login fields."
"defineClose": {
"message": "Close",
"description": "Close button text when choosing custom login fields."
},
"defineSkip": {
"message": "Skip",
@ -203,9 +207,9 @@
"message": "Show more",
"description": "More button text when choosing custom login fields."
},
"defineAgain": {
"message": "Again",
"description": "Again button text when choosing custom login fields."
"defineReset": {
"message": "Reset",
"description": "Reset button text when choosing custom login fields."
},
"defineConfirm": {
"message": "Confirm",
@ -215,29 +219,29 @@
"message": "Login fields for this page are already selected and will be overwritten.",
"description": "A text shown when custom credentials fields are already set for the page."
},
"defineDiscard": {
"message": "Discard selection",
"description": "Discard selection button text when choosing custom login fields."
"defineClearData": {
"message": "Clear saved data",
"description": "Clear save data button text when choosing custom login fields."
},
"defineStringField": {
"message": "String field #",
"message": "String field",
"description": "Text for string field."
},
"defineChooseUsername": {
"message": "1. Choose a username field",
"message": "Choose a username field",
"description": "Choosing a username field text when choosing custom login fields."
},
"defineChoosePassword": {
"message": "2. Now choose a password field",
"message": "Choose a password field",
"description": "Choosing a password field text when choosing custom login fields."
},
"defineChooseTOTP": {
"message": "3. Choose a TOTP field",
"message": "Choose a TOTP field",
"description": "Choosing a TOTP field text when choosing custom login fields."
},
"defineConfirmSelection": {
"message": "4. Confirm selection",
"description": "Confirm a selection text when choosing custom login fields."
"defineChooseStringFields": {
"message": "Choose String Fields",
"description": "Choose String Fields a selection text when choosing custom login fields."
},
"defineHelpText": {
"message": "Please confirm your selection or choose more fields as String fields.",
@ -247,6 +251,10 @@
"message": "You can also use the numbers to choose the input fields from keyboard.",
"description": "Help text when choosing custom login fields."
},
"defineChooseCustomLoginFieldText": {
"message": "Choose a Custom Login Field",
"description": "Help text for Custom Login Field banner."
},
"username": {
"message": "Username",
"description": "General text for username."
@ -255,6 +263,10 @@
"message": "Password",
"description": "General text for password."
},
"stringFields": {
"message": "String Fields",
"description": "General text for a String Fields."
},
"credentialsNoUsername": {
"message": "- no username -",
"description": "Shown when no username is set in the credentials."

View file

@ -126,7 +126,6 @@ kpxcBanner.create = async function(credentials = {}) {
const colorStyleSheet = createStylesheet('css/colors.css');
const wrapper = document.createElement('div');
wrapper.setAttribute('id', 'kpxc-banner');
this.shadowRoot = wrapper.attachShadow({ mode: 'closed' });
this.shadowRoot.append(colorStyleSheet);
this.shadowRoot.append(styleSheet);

View file

@ -0,0 +1,767 @@
'use strict';
const STEP_NONE = 0;
const STEP_SELECT_USERNAME = 1;
const STEP_SELECT_PASSWORD = 2;
const STEP_SELECT_TOTP = 3;
const STEP_SELECT_STRING_FIELDS = 4;
const BLUE_BUTTON = 'kpxc-button kpxc-blue-button';
const GREEN_BUTTON = 'kpxc-button kpxc-green-button';
const ORANGE_BUTTON = 'kpxc-button kpxc-orange-button';
const RED_BUTTON = 'kpxc-button kpxc-red-button';
const GRAY_BUTTON_CLASS = 'kpxc-gray-button';
const DEFINED_CUSTOM_FIELDS = 'defined-custom-fields';
const FIXED_FIELD_CLASS = 'kpxcDefine-fixed-field';
const DARK_FIXED_FIELD_CLASS = 'kpxcDefine-fixed-field-dark';
const HOVER_FIELD_CLASS = 'kpxcDefine-fixed-hover-field';
const DARK_HOVER_FIELD_CLASS = 'kpxcDefine-fixed-hover-field-dark';
const DARK_TEXT_CLASS = 'kpxcDefine-dark-text';
const USERNAME_FIELD_CLASS = 'kpxcDefine-fixed-username-field';
const PASSWORD_FIELD_CLASS = 'kpxcDefine-fixed-password-field';
const TOTP_FIELD_CLASS = 'kpxcDefine-fixed-totp-field';
const STRING_FIELD_CLASS = 'kpxcDefine-fixed-string-field';
const kpxcCustomLoginFieldsBanner = {};
kpxcCustomLoginFieldsBanner.banner = undefined;
kpxcCustomLoginFieldsBanner.chooser = undefined;
kpxcCustomLoginFieldsBanner.created = false;
kpxcCustomLoginFieldsBanner.dataStep = STEP_NONE;
kpxcCustomLoginFieldsBanner.infoText = undefined;
kpxcCustomLoginFieldsBanner.wrapper = undefined;
kpxcCustomLoginFieldsBanner.inputQueryPattern = 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=file]):not([type=hidden]):not([type=image]):not([type=month]):not([type=range]):not([type=reset]):not([type=submit]):not([type=time]):not([type=week]), select, textarea';
kpxcCustomLoginFieldsBanner.markedFields = [];
kpxcCustomLoginFieldsBanner.nonSelectedElementsPattern = `div.${FIXED_FIELD_CLASS}:not(.${USERNAME_FIELD_CLASS}):not(.${PASSWORD_FIELD_CLASS}):not(.${TOTP_FIELD_CLASS}):not(.${STRING_FIELD_CLASS})`;
kpxcCustomLoginFieldsBanner.selection = {
username: undefined,
usernameElement: undefined,
password: undefined,
passwordElement: undefined,
totp: undefined,
totpElement: undefined,
fields: [],
fieldElements: [],
};
kpxcCustomLoginFieldsBanner.buttons = {
reset: undefined,
confirm: undefined,
clearData: undefined,
close: undefined,
};
kpxcCustomLoginFieldsBanner.destroy = async function() {
if (!kpxcCustomLoginFieldsBanner.created) {
return;
}
kpxcCustomLoginFieldsBanner.resetSelection();
kpxcCustomLoginFieldsBanner.created = false;
kpxcCustomLoginFieldsBanner.close();
if (kpxcCustomLoginFieldsBanner.wrapper && window.self.document.body.contains(kpxcCustomLoginFieldsBanner.wrapper)) {
window.self.document.body.removeChild(kpxcCustomLoginFieldsBanner.wrapper);
} else {
window.self.document.body.removeChild(window.parent.document.body.querySelector('#kpxc-banner'));
}
};
kpxcCustomLoginFieldsBanner.close = function() {
kpxcCustomLoginFieldsBanner.chooser.remove();
document.removeEventListener('keydown', kpxcCustomLoginFieldsBanner.keyDown);
};
kpxcCustomLoginFieldsBanner.create = async function() {
if (await kpxc.siteIgnored() || kpxcCustomLoginFieldsBanner.created) {
return;
}
const banner = kpxcUI.createElement('div', 'kpxc-banner', { 'id': 'container' });
banner.style.zIndex = '2147483646';
kpxcCustomLoginFieldsBanner.chooser = kpxcUI.createElement('div', '', { 'id': 'kpxcDefine-fields' });
const bannerInfo = kpxcUI.createElement('div', 'banner-info');
const bannerButtons = kpxcUI.createElement('div', 'banner-buttons');
const iconClassName = isFirefox() ? 'kpxc-banner-icon-moz' : 'kpxc-banner-icon';
const icon = kpxcUI.createElement('span', iconClassName);
const infoText = kpxcUI.createElement('span', '', {}, tr('defineChooseCustomLoginFieldText'));
const separator = kpxcUI.createElement('div', 'kpxc-separator');
const secondSeparator = kpxcUI.createElement('div', 'kpxc-separator');
const resetButton = kpxcUI.createButton(BLUE_BUTTON, tr('defineReset'), kpxcCustomLoginFieldsBanner.reset);
const usernameButton = kpxcUI.createButton(ORANGE_BUTTON, tr('username'), kpxcCustomLoginFieldsBanner.usernameButtonClicked);
const passwordButton = kpxcUI.createButton(RED_BUTTON, tr('password'), kpxcCustomLoginFieldsBanner.passwordButtonClicked);
const totpButton = kpxcUI.createButton(GREEN_BUTTON, 'TOTP', kpxcCustomLoginFieldsBanner.totpButtonClicked);
const stringFieldsButton = kpxcUI.createButton(BLUE_BUTTON, tr('stringFields'), kpxcCustomLoginFieldsBanner.stringFieldsButtonClicked);
const clearDataButton = kpxcUI.createButton(RED_BUTTON, tr('defineClearData'), kpxcCustomLoginFieldsBanner.clearData);
const confirmButton = kpxcUI.createButton(GREEN_BUTTON, tr('defineConfirm'), kpxcCustomLoginFieldsBanner.confirm);
const closeButton = kpxcUI.createButton(RED_BUTTON, tr('defineClose'), kpxcCustomLoginFieldsBanner.closeButtonClicked);
closeButton.style.minWidth = Pixels(64);
confirmButton.disabled = true;
kpxcCustomLoginFieldsBanner.banner = banner;
kpxcCustomLoginFieldsBanner.infoText = infoText;
kpxcCustomLoginFieldsBanner.buttons.reset = resetButton;
kpxcCustomLoginFieldsBanner.buttons.clearData = clearDataButton;
kpxcCustomLoginFieldsBanner.buttons.confirm = confirmButton;
kpxcCustomLoginFieldsBanner.buttons.close = closeButton;
kpxcCustomLoginFieldsBanner.buttons.username = usernameButton;
kpxcCustomLoginFieldsBanner.buttons.password = passwordButton;
kpxcCustomLoginFieldsBanner.buttons.totp = totpButton;
kpxcCustomLoginFieldsBanner.buttons.stringFields = stringFieldsButton;
bannerInfo.appendMultiple(icon, infoText);
bannerButtons.appendMultiple(resetButton, separator, usernameButton,
passwordButton, totpButton, stringFieldsButton, secondSeparator, clearDataButton, confirmButton, closeButton);
banner.appendMultiple(bannerInfo, bannerButtons);
const location = kpxc.getDocumentLocation();
kpxcCustomLoginFieldsBanner.buttons.clearData.style.display
= kpxc.settings[DEFINED_CUSTOM_FIELDS] && kpxc.settings[DEFINED_CUSTOM_FIELDS][location]
? 'inline-block' : 'none';
if (window.self !== window.top && kpxcCustomLoginFieldsBanner.buttons.clearData.style.display === 'inline-block') {
sendMessageToParent('enable_clear_data_button');
}
initColorTheme(banner);
const bannerStyleSheet = createStylesheet('css/banner.css');
const defineStyleSheet = createStylesheet('css/define.css');
const buttonStyleSheet = createStylesheet('css/button.css');
const colorStyleSheet = createStylesheet('css/colors.css');
const wrapper = document.createElement('div');
this.shadowRoot = wrapper.attachShadow({ mode: 'closed' });
this.shadowRoot.append(colorStyleSheet);
this.shadowRoot.append(bannerStyleSheet);
this.shadowRoot.append(defineStyleSheet);
this.shadowRoot.append(buttonStyleSheet);
// Only create the banner to top window
if (window.self === window.top) {
this.shadowRoot.append(banner);
}
this.shadowRoot.append(kpxcCustomLoginFieldsBanner.chooser);
kpxcCustomLoginFieldsBanner.wrapper = wrapper;
if (!kpxcCustomLoginFieldsBanner.created) {
window.self.document.body.appendChild(wrapper);
kpxcCustomLoginFieldsBanner.created = true;
if (window.self === window.top) {
// Listen messages from iframes
window.addEventListener('message', handleTopWindowMessage, false);
} else {
// Listen messages from top window
window.addEventListener('message', handleParentWindowMessage, false);
}
}
document.addEventListener('keydown', kpxcCustomLoginFieldsBanner.keyDown);
};
kpxcCustomLoginFieldsBanner.removeSelection = function(selection, fieldClass) {
const inputField = kpxcFields.getElementFromXPathId(selection[0]);
const index = kpxcCustomLoginFieldsBanner.markedFields.indexOf(inputField);
if (index >= 0) {
removeContent(fieldClass);
kpxcCustomLoginFieldsBanner.markedFields.splice(index, 1);
}
};
kpxcCustomLoginFieldsBanner.enableAllButtons = function() {
for (const button of Object.values(kpxcCustomLoginFieldsBanner.buttons)) {
button.classList.remove(GRAY_BUTTON_CLASS);
}
};
kpxcCustomLoginFieldsBanner.usernameButtonClicked = function(e) {
// Cancel the current selection if button is clicked again
if (!e.isTrusted || kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_USERNAME) {
kpxcCustomLoginFieldsBanner.backToStart();
return;
}
// Reset username field selection if already set
if (kpxcCustomLoginFieldsBanner.selection.username) {
kpxcCustomLoginFieldsBanner.removeSelection(kpxcCustomLoginFieldsBanner.selection.username, `div.${USERNAME_FIELD_CLASS}`);
kpxcCustomLoginFieldsBanner.selection.username = undefined;
}
kpxcCustomLoginFieldsBanner.prepareUsernameSelection();
kpxcCustomLoginFieldsBanner.buttons.confirm.disabled = true;
sendMessageToFrames('username_button_clicked');
};
kpxcCustomLoginFieldsBanner.passwordButtonClicked = function(e) {
if (!e.isTrusted || kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_PASSWORD) {
kpxcCustomLoginFieldsBanner.backToStart();
return;
}
// Reset password field selection if already set
if (kpxcCustomLoginFieldsBanner.selection.password) {
kpxcCustomLoginFieldsBanner.removeSelection(kpxcCustomLoginFieldsBanner.selection.password, `div.${PASSWORD_FIELD_CLASS}`);
kpxcCustomLoginFieldsBanner.selection.password = undefined;
}
kpxcCustomLoginFieldsBanner.preparePasswordSelection();
kpxcCustomLoginFieldsBanner.buttons.confirm.disabled = true;
sendMessageToFrames('password_button_clicked');
};
kpxcCustomLoginFieldsBanner.totpButtonClicked = function(e) {
if (!e.isTrusted || kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_TOTP) {
kpxcCustomLoginFieldsBanner.backToStart();
return;
}
// Reset TOTP field selection if already set
if (kpxcCustomLoginFieldsBanner.selection.totp) {
kpxcCustomLoginFieldsBanner.removeSelection(kpxcCustomLoginFieldsBanner.selection.totp, `div.${TOTP_FIELD_CLASS}`);
kpxcCustomLoginFieldsBanner.selection.totp = undefined;
}
kpxcCustomLoginFieldsBanner.prepareTOTPSelection();
kpxcCustomLoginFieldsBanner.buttons.confirm.disabled = true;
sendMessageToFrames('totp_button_clicked');
};
kpxcCustomLoginFieldsBanner.stringFieldsButtonClicked = function(e) {
if (!e.isTrusted || kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_STRING_FIELDS) {
kpxcCustomLoginFieldsBanner.backToStart();
return;
}
// Reset String Field selection if already set
if (kpxcCustomLoginFieldsBanner.selection.fields.length > 0) {
for (const field of kpxcCustomLoginFieldsBanner.selection.fields) {
kpxcCustomLoginFieldsBanner.removeSelection(field, `div.${STRING_FIELD_CLASS}`);
}
kpxcCustomLoginFieldsBanner.selection.fields = [];
}
kpxcCustomLoginFieldsBanner.prepareStringFieldSelection();
kpxcCustomLoginFieldsBanner.buttons.confirm.disabled = true;
sendMessageToFrames('string_field_button_clicked');
};
kpxcCustomLoginFieldsBanner.closeButtonClicked = function(e) {
if (!e.isTrusted) {
return;
}
kpxcCustomLoginFieldsBanner.destroy();
sendMessageToFrames('close_button_clicked');
};
// Updates the possible selections if the page content has been changed
kpxcCustomLoginFieldsBanner.updateFieldSelections = function() {
if (kpxcCustomLoginFieldsBanner.dataStep === STEP_NONE && kpxcCustomLoginFieldsBanner.markedFields.length === 0) {
return;
}
kpxcCustomLoginFieldsBanner.removeMarkedFields();
if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_USERNAME) {
kpxcCustomLoginFieldsBanner.prepareUsernameSelection();
} else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_PASSWORD) {
kpxcCustomLoginFieldsBanner.preparePasswordSelection();
} else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_TOTP) {
kpxcCustomLoginFieldsBanner.prepareTOTPSelection();
} else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_STRING_FIELDS) {
kpxcCustomLoginFieldsBanner.prepareStringFieldSelection();
}
};
// Reset selections
kpxcCustomLoginFieldsBanner.reset = function() {
kpxcCustomLoginFieldsBanner.resetSelection();
kpxcCustomLoginFieldsBanner.buttons.confirm.disabled = true;
kpxcCustomLoginFieldsBanner.infoText.textContent = tr('defineChooseCustomLoginFieldText');
kpxcCustomLoginFieldsBanner.buttons.close.textContent = tr('defineClose');
kpxcCustomLoginFieldsBanner.dataStep = STEP_NONE;
kpxcCustomLoginFieldsBanner.enableAllButtons();
sendMessageToFrames('reset_button_clicked');
};
// Confirm and save the selections
kpxcCustomLoginFieldsBanner.confirm = async function() {
if (!kpxc.settings[DEFINED_CUSTOM_FIELDS]) {
kpxc.settings[DEFINED_CUSTOM_FIELDS] = {};
}
// If the new selection is already used in some other field, clear it
const clearIdenticalField = function(path, location) {
const currentSite = kpxc.settings[DEFINED_CUSTOM_FIELDS][location];
if (currentSite.username && currentSite.username[0] === path[0]) {
kpxc.settings[DEFINED_CUSTOM_FIELDS][location].username = undefined;
} else if (currentSite.password && currentSite.password[0] === path[0]) {
kpxc.settings[DEFINED_CUSTOM_FIELDS][location].password = undefined;
} else if (currentSite.totp && currentSite.totp[0] === path[0]) {
kpxc.settings[DEFINED_CUSTOM_FIELDS][location].totp = undefined;
}
};
const usernamePath = kpxcCustomLoginFieldsBanner.selection.username;
const passwordPath = kpxcCustomLoginFieldsBanner.selection.password;
const totpPath = kpxcCustomLoginFieldsBanner.selection.totp;
const stringFieldsPaths = kpxcCustomLoginFieldsBanner.selection.fields;
const location = kpxc.getDocumentLocation();
const currentSettings = kpxc.settings[DEFINED_CUSTOM_FIELDS][location];
if (currentSettings) {
// Update the single selection to current settings
if (usernamePath) {
clearIdenticalField(usernamePath, location);
kpxc.settings[DEFINED_CUSTOM_FIELDS][location].username = usernamePath;
}
if (passwordPath) {
clearIdenticalField(passwordPath, location);
kpxc.settings[DEFINED_CUSTOM_FIELDS][location].password = passwordPath;
}
if (totpPath) {
clearIdenticalField(totpPath, location);
kpxc.settings[DEFINED_CUSTOM_FIELDS][location].totp = totpPath;
}
if (stringFieldsPaths.length > 0) {
kpxc.settings[DEFINED_CUSTOM_FIELDS][location].fields = stringFieldsPaths;
}
} else {
// Override all fields (default)
kpxc.settings[DEFINED_CUSTOM_FIELDS][location] = {
username: usernamePath,
password: passwordPath,
totp: totpPath,
fields: stringFieldsPaths
};
}
await sendMessage('save_settings', kpxc.settings);
kpxcCustomLoginFieldsBanner.destroy();
sendMessageToFrames('confirm_button_clicked');
};
// Clears the previously saved data from settings
kpxcCustomLoginFieldsBanner.clearData = async function() {
const location = kpxc.getDocumentLocation();
delete kpxc.settings[DEFINED_CUSTOM_FIELDS][location];
await sendMessage('save_settings', kpxc.settings);
await sendMessage('load_settings');
kpxcCustomLoginFieldsBanner.buttons.clearData.style.display = 'none';
sendMessageToFrames('clear_data_button_clicked');
};
// Resets all selections and marked fields
kpxcCustomLoginFieldsBanner.resetSelection = function() {
kpxcCustomLoginFieldsBanner.selection = {
username: undefined,
password: undefined,
totp: undefined,
fields: []
};
kpxcCustomLoginFieldsBanner.removeMarkedFields();
};
kpxcCustomLoginFieldsBanner.removeMarkedFields = function() {
while (kpxcCustomLoginFieldsBanner.chooser.firstChild) {
kpxcCustomLoginFieldsBanner.chooser.firstChild.remove();
}
kpxcCustomLoginFieldsBanner.markedFields = [];
};
kpxcCustomLoginFieldsBanner.prepareUsernameSelection = function() {
kpxcCustomLoginFieldsBanner.infoText.textContent = tr('defineChooseUsername');
kpxcCustomLoginFieldsBanner.dataStep = STEP_SELECT_USERNAME;
kpxcCustomLoginFieldsBanner.buttons.username.classList.remove(GRAY_BUTTON_CLASS);
kpxcCustomLoginFieldsBanner.selectField('username');
};
kpxcCustomLoginFieldsBanner.preparePasswordSelection = function() {
kpxcCustomLoginFieldsBanner.infoText.textContent = tr('defineChoosePassword');
kpxcCustomLoginFieldsBanner.dataStep = STEP_SELECT_PASSWORD;
kpxcCustomLoginFieldsBanner.buttons.password.classList.remove(GRAY_BUTTON_CLASS);
kpxcCustomLoginFieldsBanner.selectField('password');
};
kpxcCustomLoginFieldsBanner.prepareTOTPSelection = function() {
kpxcCustomLoginFieldsBanner.infoText.textContent = tr('defineChooseTOTP');
kpxcCustomLoginFieldsBanner.dataStep = STEP_SELECT_TOTP;
kpxcCustomLoginFieldsBanner.buttons.totp.classList.remove(GRAY_BUTTON_CLASS);
kpxcCustomLoginFieldsBanner.selectField('totp');
};
kpxcCustomLoginFieldsBanner.prepareStringFieldSelection = function() {
kpxcCustomLoginFieldsBanner.infoText.textContent = tr('defineChooseStringFields');
kpxcCustomLoginFieldsBanner.dataStep = STEP_SELECT_STRING_FIELDS;
kpxcCustomLoginFieldsBanner.buttons.stringFields.classList.remove(GRAY_BUTTON_CLASS);
kpxcCustomLoginFieldsBanner.selectStringFields();
};
kpxcCustomLoginFieldsBanner.isFieldSelected = function(field) {
const currentFieldId = kpxcFields.setId(field);
if (kpxcCustomLoginFieldsBanner.markedFields.some(f => f === field)) {
return (
(kpxcCustomLoginFieldsBanner.selection.username && kpxcCustomLoginFieldsBanner.selection.usernameElement === field)
|| (kpxcCustomLoginFieldsBanner.selection.password && kpxcCustomLoginFieldsBanner.selection.passwordElement === field)
|| (kpxcCustomLoginFieldsBanner.selection.totp && kpxcCustomLoginFieldsBanner.selection.totpElement === field)
|| kpxcCustomLoginFieldsBanner.selection.fields.some(f => f[0] === currentFieldId[0])
);
}
return false;
};
kpxcCustomLoginFieldsBanner.getSelectedField = function(e, elem) {
if (!e.isTrusted) {
return undefined;
}
const field = elem || e.currentTarget;
if (kpxcCustomLoginFieldsBanner.markedFields.includes(field.originalElement)) {
return undefined;
}
return field;
};
kpxcCustomLoginFieldsBanner.setSelectedField = function(elem) {
if (elem) {
kpxcCustomLoginFieldsBanner.markedFields.push(elem);
}
kpxcCustomLoginFieldsBanner.buttons.confirm.disabled = false;
kpxcCustomLoginFieldsBanner.buttons.close.textContent = tr('optionsButtonCancel');
};
// Expects 'username', 'password' or 'totp'
kpxcCustomLoginFieldsBanner.selectField = function(fieldType) {
kpxcCustomLoginFieldsBanner.eventFieldClick = function(e) {
const field = kpxcCustomLoginFieldsBanner.getSelectedField(e);
if (!field) {
return;
}
if (isLightThemeBackground(field.originalElement)) {
field.classList.add(DARK_TEXT_CLASS);
}
field.classList.add(`kpxcDefine-fixed-${fieldType}-field`);
field.textContent = tr(fieldType);
field.onclick = undefined;
kpxcCustomLoginFieldsBanner.selection[fieldType] = kpxcFields.setId(field.originalElement);
kpxcCustomLoginFieldsBanner.selection[`${fieldType}Element`] = field.originalElement;
kpxcCustomLoginFieldsBanner.setSelectedField(field.originalElement);
kpxcCustomLoginFieldsBanner.backToStart();
kpxcCustomLoginFieldsBanner.buttons[fieldType].classList.add(GRAY_BUTTON_CLASS);
sendMessageToParent(`${fieldType}_selected`, kpxcCustomLoginFieldsBanner.selection[fieldType]);
};
kpxcCustomLoginFieldsBanner.markFields();
};
kpxcCustomLoginFieldsBanner.selectStringFields = function() {
kpxcCustomLoginFieldsBanner.eventFieldClick = function(e) {
const field = kpxcCustomLoginFieldsBanner.getSelectedField(e);
if (!field) {
return;
}
if (isLightThemeBackground(field.originalElement)) {
field.classList.add(DARK_TEXT_CLASS);
}
kpxcCustomLoginFieldsBanner.selection.fields.push(kpxcFields.setId(field.originalElement));
kpxcCustomLoginFieldsBanner.setSelectedField(field.originalElement);
field.classList.add(STRING_FIELD_CLASS);
field.textContent = `${tr('defineStringField')} #${String(kpxcCustomLoginFieldsBanner.selection.fields.length)}`;
field.onclick = undefined;
kpxcCustomLoginFieldsBanner.buttons.stringFields.classList.add(GRAY_BUTTON_CLASS);
sendMessageToParent('string_field_selected', kpxcCustomLoginFieldsBanner.selection.fields);
};
kpxcCustomLoginFieldsBanner.markFields();
};
kpxcCustomLoginFieldsBanner.markFields = function() {
let index = 1;
let firstInput;
const inputs = document.querySelectorAll(kpxcCustomLoginFieldsBanner.inputQueryPattern);
for (const i of inputs) {
if (kpxcCustomLoginFieldsBanner.isFieldSelected(i) || inputFieldIsSelected(i)) {
// Switch texts to current selection type
for (const selection of kpxcCustomLoginFieldsBanner.getNonSelectedElements()) {
selection.textContent = dataStepToString();
}
continue;
}
const field = kpxcUI.createElement('div', FIXED_FIELD_CLASS);
field.originalElement = i;
const rect = i.getBoundingClientRect();
field.style.top = Pixels(rect.top);
field.style.left = Pixels(rect.left);
field.style.width = Pixels(rect.width);
field.style.height = Pixels(rect.height);
field.textContent = dataStepToString();
// Change selection theme if needed
const isLightTheme = isLightThemeBackground(i);
if (isLightTheme) {
field.classList.add(DARK_FIXED_FIELD_CLASS);
}
field.addEventListener('click', function(e) {
kpxcCustomLoginFieldsBanner.eventFieldClick(e);
});
field.addEventListener('mouseenter', function() {
field.classList.add(isLightTheme ? DARK_HOVER_FIELD_CLASS : HOVER_FIELD_CLASS);
});
field.addEventListener('mouseleave', function() {
field.classList.remove(HOVER_FIELD_CLASS, DARK_HOVER_FIELD_CLASS);
});
i.addEventListener('focus', function() {
field.classList.add(isLightTheme ? DARK_HOVER_FIELD_CLASS : HOVER_FIELD_CLASS);
});
i.addEventListener('blur', function() {
field.classList.remove(HOVER_FIELD_CLASS, DARK_HOVER_FIELD_CLASS);
});
if (kpxcCustomLoginFieldsBanner.chooser) {
kpxcCustomLoginFieldsBanner.setSelectionPosition(field);
kpxcCustomLoginFieldsBanner.monitorSelectionPosition(field);
kpxcCustomLoginFieldsBanner.chooser.append(field);
firstInput = field;
++index;
}
}
if (firstInput) {
firstInput.focus();
}
};
// Returns to start after a single selection
kpxcCustomLoginFieldsBanner.backToStart = function() {
removeContent(kpxcCustomLoginFieldsBanner.nonSelectedElementsPattern);
kpxcCustomLoginFieldsBanner.infoText.textContent = tr('defineChooseCustomLoginFieldText');
kpxcCustomLoginFieldsBanner.dataStep = STEP_NONE;
kpxcCustomLoginFieldsBanner.buttons.confirm.disabled = kpxcCustomLoginFieldsBanner.markedFields.length === 0;
};
// Handle keyboard events
kpxcCustomLoginFieldsBanner.keyDown = function(e) {
if (!e.isTrusted) {
return;
}
// Works as a cancel when selection process is active
if (e.key === 'Escape') {
if (kpxcCustomLoginFieldsBanner.dataStep === STEP_NONE) {
kpxcCustomLoginFieldsBanner.destroy();
} else {
kpxcCustomLoginFieldsBanner.backToStart();
}
}
};
// Detect page scroll or resize changes
kpxcCustomLoginFieldsBanner.monitorSelectionPosition = function(selection) {
// Handle icon position on resize
window.addEventListener('resize', function(e) {
kpxcCustomLoginFieldsBanner.setSelectionPosition(selection);
});
// Handle icon position on scroll
window.addEventListener('scroll', function(e) {
kpxcCustomLoginFieldsBanner.setSelectionPosition(selection);
});
};
// Set selection input field position dynamically including the scroll position
kpxcCustomLoginFieldsBanner.setSelectionPosition = function(field) {
const rect = field.originalElement.getBoundingClientRect();
const left = kpxcUI.getRelativeLeftPosition(rect);
const top = kpxcUI.getRelativeTopPosition(rect);
const scrollTop = document.scrollingElement ? document.scrollingElement.scrollTop : 0;
const scrollLeft = document.scrollingElement ? document.scrollingElement.scrollLeft : 0;
field.style.top = Pixels(top + scrollTop);
field.style.left = Pixels(left + scrollLeft);
};
kpxcCustomLoginFieldsBanner.getNonSelectedElements = function() {
return kpxcCustomLoginFieldsBanner.chooser.querySelectorAll(kpxcCustomLoginFieldsBanner.nonSelectedElementsPattern);
};
const removeContent = function(pattern) {
const elems = kpxcCustomLoginFieldsBanner.chooser.querySelectorAll(pattern);
for (const e of elems) {
e.remove();
}
};
const inputFieldIsSelected = function(field) {
for (const child of kpxcCustomLoginFieldsBanner.chooser.children) {
if (child.originalElement === field) {
return true;
}
}
return false;
};
// Checks if an element has a light background, using luminance. Luminance with >= 127 is considered 'light'.
const isLightThemeBackground = function(elem) {
const inputStyle = getComputedStyle(elem);
const bgColor = inputStyle.backgroundColor.match(/[\.\d]+/g).map(e => Number(e));
if (bgColor.length < 3) {
return false;
}
const luminance = 0.299 * bgColor[0] + 0.587 * bgColor[1] + 0.114 * bgColor[2];
return luminance >= 128;
};
const dataStepToString = function() {
if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_USERNAME) {
return tr('username');
} else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_PASSWORD) {
return tr('password');
} else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_TOTP) {
return tr('totp');
} else if (kpxcCustomLoginFieldsBanner.dataStep === STEP_SELECT_STRING_FIELDS) {
return tr('defineStringField');
}
}
//--------------------------------------------------------------------------
// IFrame support
//--------------------------------------------------------------------------
// A simple check for top-level-domain
const topLevelDomainMatches = function(host) {
if (!host) {
return false;
}
const originUrl = new URL(host);
const frameUrl = new URL(window.self.document.location.origin);
const urlParts = originUrl.host.split('.');
const dotCount = urlParts.length - 1;
// Simple host like google.com, check directly
if (dotCount < 1) {
return false;
} else if (dotCount === 1) {
return frameUrl.host.includes(originUrl.host);
}
// Get the top-level-domain using counts of '.' but backwards, max 3.
// A basic host is like idmsa.apple.com, a more complex one like www.bbva.com.ar.
const index = Math.min(dotCount, 3);
const subDomain = `${urlParts[dotCount - index]}.`;
const topLevelDomain = originUrl.host.substring(originUrl.host.indexOf(subDomain) + subDomain.length);
return frameUrl.host.includes(topLevelDomain);
};
// Handles messages sent from iframes to the top window
const handleTopWindowMessage = function(e) {
if (!topLevelDomainMatches(e.origin)) {
return;
}
if (e.data.message === 'username_selected') {
kpxcCustomLoginFieldsBanner.selection.username = e.data.selection;
kpxcCustomLoginFieldsBanner.setSelectedField();
} else if (e.data.message === 'password_selected') {
kpxcCustomLoginFieldsBanner.selection.password = e.data.selection;
kpxcCustomLoginFieldsBanner.setSelectedField();
} else if (e.data.message === 'totp_selected') {
kpxcCustomLoginFieldsBanner.selection.totp = e.data.selection;
kpxcCustomLoginFieldsBanner.setSelectedField();
} else if (e.data.message === 'string_field_selected') {
kpxcCustomLoginFieldsBanner.selection.stringFields = e.data.selection;
kpxcCustomLoginFieldsBanner.setSelectedField();
} else if (e.data.message === 'enable_clear_data_button') {
kpxcCustomLoginFieldsBanner.buttons.clearData.style.display = 'inline-block';
}
};
// Handle Banner button clicks from the top window
const handleParentWindowMessage = function(e) {
if (!topLevelDomainMatches(e.origin)) {
return;
}
if (e.data === 'username_button_clicked') {
kpxcCustomLoginFieldsBanner.usernameButtonClicked(e);
} else if (e.data === 'password_button_clicked') {
kpxcCustomLoginFieldsBanner.passwordButtonClicked(e);
} else if (e.data === 'totp_button_clicked') {
kpxcCustomLoginFieldsBanner.totpButtonClicked(e);
} else if (e.data === 'string_field_button_clicked') {
kpxcCustomLoginFieldsBanner.stringFieldsButtonClicked(e);
} else if (e.data === 'reset_button_clicked') {
kpxcCustomLoginFieldsBanner.reset();
} else if (e.data === 'close_button_clicked') {
kpxcCustomLoginFieldsBanner.closeButtonClicked(e);
} else if (e.data === 'confirm_button_clicked') {
kpxcCustomLoginFieldsBanner.confirm();
} else if (e.data === 'clear_data_button_clicked') {
kpxcCustomLoginFieldsBanner.clearData();
}
};
// Sends messages to all iframes. Works only from the top window.
const sendMessageToFrames = function(message) {
if (window.self === window.top) {
for (var i = 0; i < window.frames.length; i++) {
frames[i].postMessage(message, '*');
}
}
};
// Sends message to parent window. Works only from iframes.
const sendMessageToParent = function(message, selection) {
if (window.self !== window.top) {
window.top.postMessage({ message, selection }, '*');
}
}

View file

@ -1,496 +0,0 @@
'use strict';
var kpxcDefine = {};
kpxcDefine.selection = {
username: null,
password: null,
totp: null,
fields: []
};
kpxcDefine.buttons = {
again: undefined,
confirm: undefined,
discard: undefined,
dismiss: undefined,
more: undefined,
skip: undefined
};
kpxcDefine.backdrop = undefined;
kpxcDefine.chooser = undefined;
kpxcDefine.dialog = undefined;
kpxcDefine.diffX = 0;
kpxcDefine.diffY = 0;
kpxcDefine.discardSection = undefined;
kpxcDefine.eventFieldClick = undefined;
kpxcDefine.headline = undefined;
kpxcDefine.help = undefined;
kpxcDefine.inputQueryPattern = 'input[type=email], input[type=number], input[type=password], input[type=tel], input[type=text], input[type=username], input:not([type])';
kpxcDefine.keyboardSelectorPattern = 'div.kpxcDefine-fixed-field:not(.kpxcDefine-fixed-username-field):not(.kpxcDefine-fixed-password-field):not(.kpxcDefine-fixed-totp-field)';
kpxcDefine.moreInputQueryPattern = 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=file]):not([type=hidden]):not([type=image]):not([type=month]):not([type=range]):not([type=reset]):not([type=submit]):not([type=time]):not([type=week]), select, textarea';
kpxcDefine.markedFields = [];
kpxcDefine.keyDown = undefined;
kpxcDefine.startPosX = 0;
kpxcDefine.startPosY = 0;
kpxcDefine.init = async function() {
kpxcDefine.backdrop = kpxcUI.createElement('div', 'kpxcDefine-modal-backdrop', { 'id': 'kpxcDefine-backdrop' });
kpxcDefine.chooser = kpxcUI.createElement('div', '', { 'id': 'kpxcDefine-fields' });
kpxcDefine.dialog = kpxcUI.createElement('div', '', { 'id': 'kpxcDefine-description' });
kpxcDefine.backdrop.append(kpxcDefine.dialog);
const styleSheet = createStylesheet('css/define.css');
const buttonStyleSheet = createStylesheet('css/button.css');
const wrapper = document.createElement('div');
this.shadowRoot = wrapper.attachShadow({ mode: 'closed' });
this.shadowRoot.append(styleSheet);
this.shadowRoot.append(buttonStyleSheet);
this.shadowRoot.append(kpxcDefine.backdrop);
this.shadowRoot.append(kpxcDefine.chooser);
document.body.append(wrapper);
kpxcDefine.initDescription();
kpxcDefine.resetSelection();
kpxcDefine.prepareStep1();
kpxcDefine.markAllUsernameFields();
kpxcDefine.dialog.onmousedown = function(e) {
kpxcDefine.mouseDown(e);
};
document.addEventListener('keydown', kpxcDefine.keyDown);
};
kpxcDefine.close = function() {
kpxcDefine.backdrop.remove();
kpxcDefine.chooser.remove();
document.removeEventListener('keydown', kpxcDefine.keyDown);
};
kpxcDefine.mouseDown = function(e) {
kpxcDefine.selected = kpxcDefine.dialog;
kpxcDefine.startPosX = e.clientX;
kpxcDefine.startPosY = e.clientY;
kpxcDefine.diffX = kpxcDefine.startPosX - kpxcDefine.dialog.offsetLeft;
kpxcDefine.diffY = kpxcDefine.startPosY - kpxcDefine.dialog.offsetTop;
return false;
};
kpxcDefine.initDescription = function() {
const description = kpxcDefine.dialog;
kpxcDefine.headline = kpxcUI.createElement('div', '', { 'id': 'kpxcDefine-chooser-headline' });
kpxcDefine.help = kpxcUI.createElement('div', 'kpxcDefine-chooser-help', { 'id': 'kpxcDefine-help' });
// Show keyboard shortcuts help text
const keyboardHelp = kpxcUI.createElement('div', 'kpxcDefine-keyboardHelp', {}, `${tr('optionsKeyboardShortcutsHeader')}:`);
keyboardHelp.style.marginBottom = '5px';
keyboardHelp.appendMultiple(document.createElement('br'), kpxcUI.createElement('kbd', '', {}, 'Escape'), ' ' + tr('defineDismiss'));
keyboardHelp.appendMultiple(document.createElement('br'), kpxcUI.createElement('kbd', '', {}, 'S'), ' ' + tr('defineSkip'));
keyboardHelp.appendMultiple(document.createElement('br'), kpxcUI.createElement('kbd', '', {}, 'A'), ' ' + tr('defineAgain'));
keyboardHelp.appendMultiple(document.createElement('br'), kpxcUI.createElement('kbd', '', {}, 'C'), ' ' + tr('defineConfirm'));
keyboardHelp.appendMultiple(document.createElement('br'), kpxcUI.createElement('kbd', '', {}, 'M'), ' ' + tr('defineMore'));
keyboardHelp.appendMultiple(document.createElement('br'), kpxcUI.createElement('kbd', '', {}, 'D'), ' ' + tr('defineDiscard'));
description.appendMultiple(kpxcDefine.headline, kpxcDefine.help, keyboardHelp);
const buttonDismiss = kpxcUI.createElement('button', 'kpxc-button kpxc-red-button', { 'id': 'kpxcDefine-btn-dismiss' }, tr('defineDismiss'));
buttonDismiss.addEventListener('click', kpxcDefine.close);
const buttonSkip = kpxcUI.createElement('button', 'kpxc-button kpxc-orange-button', { 'id': 'kpxcDefine-btn-skip' }, tr('defineSkip'));
buttonSkip.style.marginRight = '5px';
buttonSkip.addEventListener('click', kpxcDefine.skip);
const buttonMore = kpxcUI.createElement('button', 'kpxc-button kpxc-orange-button', { 'id': 'kpxcDefine-btn-more' }, tr('defineMore'));
buttonMore.style.marginRight = '5px';
buttonMore.style.marginLeft = '5px';
buttonMore.addEventListener('click', kpxcDefine.more);
const buttonAgain = kpxcUI.createElement('button', 'kpxc-button kpxc-blue-button', { 'id': 'kpxcDefine-btn-again' }, tr('defineAgain'));
buttonAgain.style.marginRight = '5px';
buttonAgain.addEventListener('click', kpxcDefine.again);
const buttonConfirm = kpxcUI.createElement('button', 'kpxc-button kpxc-green-button', { 'id': 'kpxcDefine-btn-confirm' }, tr('defineConfirm'));
buttonConfirm.style.marginRight = '15px';
buttonConfirm.style.display = 'none';
buttonConfirm.addEventListener('click', kpxcDefine.confirm);
kpxcDefine.buttons.again = buttonAgain;
kpxcDefine.buttons.confirm = buttonConfirm;
kpxcDefine.buttons.dismiss = buttonDismiss;
kpxcDefine.buttons.more = buttonMore;
kpxcDefine.buttons.skip = buttonSkip;
description.appendMultiple(buttonConfirm, buttonSkip, buttonMore, buttonAgain, buttonDismiss);
const location = kpxc.getDocumentLocation();
if (kpxc.settings['defined-custom-fields'] && kpxc.settings['defined-custom-fields'][location]) {
const div = kpxcUI.createElement('div', 'alreadySelected', {});
const defineDiscard = kpxcUI.createElement('p', '', {}, tr('defineAlreadySelected'));
const buttonDiscard = kpxcUI.createElement('button', 'kpxc-button kpxc-red-button', { 'id': 'kpxcDefine-btn-discard' }, tr('defineDiscard'));
buttonDiscard.style.marginTop = '5px';
buttonDiscard.addEventListener('click', kpxcDefine.discard);
kpxcDefine.buttons.discard = buttonSkip;
kpxcDefine.discardSection = div;
div.appendMultiple(defineDiscard, buttonDiscard);
description.append(div);
}
};
kpxcDefine.resetSelection = function() {
kpxcDefine.selection = {
username: null,
password: null,
totp: null,
fields: []
};
kpxcDefine.markedFields = [];
if (kpxcDefine.chooser) {
kpxcDefine.chooser.textContent = '';
}
};
kpxcDefine.isFieldSelected = function(field) {
if (kpxcDefine.markedFields.some(f => f === field)) {
return (
(kpxcDefine.selection.username && kpxcDefine.selection.username.originalElement === field)
|| (kpxcDefine.selection.password && kpxcDefine.selection.password.originalElement === field)
|| (kpxcDefine.selection.totp && kpxcDefine.selection.totp.originalElement === field)
|| kpxcDefine.selection.fields.includes(field)
);
}
return false;
};
kpxcDefine.markAllUsernameFields = function() {
kpxcDefine.eventFieldClick = function(e, elem) {
if (!e.isTrusted) {
return;
}
const field = elem || e.currentTarget;
field.classList.add('kpxcDefine-fixed-username-field');
field.textContent = tr('username');
field.onclick = null;
kpxcDefine.selection.username = field;
kpxcDefine.markedFields.push(field.originalElement);
kpxcDefine.prepareStep2();
kpxcDefine.markAllPasswordFields();
};
kpxcDefine.markFields(kpxcDefine.inputQueryPattern);
};
kpxcDefine.markAllPasswordFields = function() {
kpxcDefine.eventFieldClick = function(e, elem) {
if (!e.isTrusted) {
return;
}
const field = elem || e.currentTarget;
field.classList.add('kpxcDefine-fixed-password-field');
field.textContent = tr('password');
field.onclick = null;
kpxcDefine.selection.password = field;
kpxcDefine.markedFields.push(field.originalElement);
kpxcDefine.prepareStep3();
kpxcDefine.markAllTOTPFields();
};
kpxcDefine.markFields('input[type=\'password\']');
};
kpxcDefine.markAllStringFields = function() {
kpxcDefine.eventFieldClick = function(e, elem) {
if (!e.isTrusted) {
return;
}
const field = elem || e.currentTarget;
if (kpxcDefine.isFieldSelected(field.originalElement)) {
return;
}
kpxcDefine.selection.fields.push(field.originalElement);
kpxcDefine.markedFields.push(field.originalElement);
field.classList.add('kpxcDefine-fixed-string-field');
field.textContent = tr('defineStringField') + String(kpxcDefine.selection.fields.length);
field.onclick = null;
};
kpxcDefine.markFields(kpxcDefine.inputQueryPattern + ', select');
};
kpxcDefine.markAllTOTPFields = function() {
kpxcDefine.eventFieldClick = function(e, elem) {
if (!e.isTrusted) {
return;
}
const field = elem || e.currentTarget;
field.classList.add('kpxcDefine-fixed-totp-field');
field.textContent = 'TOTP';
field.onclick = null;
kpxcDefine.selection.totp = field;
kpxcDefine.markedFields.push(field.originalElement);
kpxcDefine.prepareStep4();
kpxcDefine.markAllStringFields();
};
kpxcDefine.markFields(kpxcDefine.inputQueryPattern);
};
kpxcDefine.markFields = function(pattern) {
let index = 1;
let firstInput = null;
const inputs = document.querySelectorAll(pattern);
for (const i of inputs) {
if (kpxcDefine.isFieldSelected(i)) {
continue;
}
if (!kpxcFields.isVisible(i)) {
continue;
}
const field = kpxcUI.createElement('div', 'kpxcDefine-fixed-field');
field.originalElement = i;
const rect = i.getBoundingClientRect();
field.style.top = Pixels(rect.top);
field.style.left = Pixels(rect.left);
field.style.width = Pixels(rect.width);
field.style.height = Pixels(rect.height);
field.textContent = String(index);
field.addEventListener('click', function(e) {
kpxcDefine.eventFieldClick(e);
});
field.addEventListener('mouseenter', function() {
field.classList.add('kpxcDefine-fixed-hover-field');
});
field.addEventListener('mouseleave', function() {
field.classList.remove('kpxcDefine-fixed-hover-field');
});
i.addEventListener('focus', function() {
field.classList.add('kpxcDefine-fixed-hover-field');
});
i.addEventListener('blur', function() {
field.classList.remove('kpxcDefine-fixed-hover-field');
});
if (kpxcDefine.chooser) {
kpxcDefine.chooser.append(field);
firstInput = field;
++index;
}
}
if (firstInput) {
firstInput.focus();
}
};
kpxcDefine.prepareStep1 = function() {
kpxcDefine.help.style.marginBottom = '10px';
kpxcDefine.help.textContent = tr('defineKeyboardText');
removeContent('div#kpxcDefine-fixed-field');
kpxcDefine.headline.textContent = tr('defineChooseUsername');
kpxcDefine.dataStep = 1;
kpxcDefine.buttons.skip.style.display = 'inline-block';
kpxcDefine.buttons.confirm.style.display = 'none';
kpxcDefine.buttons.again.style.display = 'none';
kpxcDefine.buttons.more.style.display = 'none';
};
kpxcDefine.prepareStep2 = function() {
const help = kpxcDefine.help;
help.style.marginBottom = '10px';
help.textContent = tr('defineKeyboardText');
removeContent('div.kpxcDefine-fixed-field:not(.kpxcDefine-fixed-username-field)');
removeContent('div.kpxcDefine-fixed.field');
kpxcDefine.headline.textContent = tr('defineChoosePassword');
kpxcDefine.dataStep = 2;
kpxcDefine.buttons.again.style.display = 'inline-block';
kpxcDefine.buttons.more.style.display = 'inline-block';
};
kpxcDefine.prepareStep3 = function() {
kpxcDefine.help.style.marginBottom = '10px';
kpxcDefine.help.textContent = tr('defineHelpText');
removeContent('div.kpxcDefine-fixed-field:not(.kpxcDefine-fixed-username-field):not(.kpxcDefine-fixed-password-field)');
kpxcDefine.headline.textContent = tr('defineChooseTOTP');
kpxcDefine.dataStep = 3;
kpxcDefine.buttons.skip.style.display = 'inline-block';
kpxcDefine.buttons.again.style.display = 'inline-block';
kpxcDefine.buttons.more.style.display = 'none';
kpxcDefine.buttons.confirm.style.display = 'none';
};
kpxcDefine.prepareStep4 = function() {
kpxcDefine.help.style.marginBottom = '10px';
kpxcDefine.help.textContent = tr('defineHelpText');
removeContent('div.kpxcDefine-fixed-field:not(.kpxcDefine-fixed-username-field):not(.kpxcDefine-fixed-password-field):not(.kpxcDefine-fixed-totp-field):not(.kpxcDefine-fixed-string-field)');
kpxcDefine.headline.textContent = tr('defineConfirmSelection');
kpxcDefine.dataStep = 4;
kpxcDefine.buttons.skip.style.display = 'none';
kpxcDefine.buttons.more.style.display = 'none';
kpxcDefine.buttons.again.style.display = 'inline-block';
kpxcDefine.buttons.confirm.style.display = 'inline-block';
};
kpxcDefine.skip = function() {
if (kpxcDefine.dataStep === 1) {
kpxcDefine.selection.username = null;
kpxcDefine.prepareStep2();
kpxcDefine.markAllPasswordFields();
} else if (kpxcDefine.dataStep === 2) {
kpxcDefine.selection.password = null;
kpxcDefine.prepareStep3();
kpxcDefine.markAllTOTPFields();
} else if (kpxcDefine.dataStep === 3) {
kpxcDefine.selection.totp = null;
kpxcDefine.prepareStep4();
kpxcDefine.markAllStringFields();
}
};
kpxcDefine.again = function() {
kpxcDefine.resetSelection();
kpxcDefine.prepareStep1();
kpxcDefine.markAllUsernameFields();
};
kpxcDefine.more = function() {
if (kpxcDefine.dataStep === 1) {
kpxcDefine.prepareStep1();
// Reset previous marked fields when no usernames have been selected
if (kpxcDefine.markedFields.length === 0) {
kpxcDefine.resetSelection();
}
} else if (kpxcDefine.dataStep === 2) {
kpxcDefine.prepareStep2();
} else if (kpxcDefine.dataStep === 3) {
kpxcDefine.prepareStep3();
} else if (kpxcDefine.dataStep === 4) {
kpxcDefine.prepareStep4();
}
kpxcDefine.markFields(kpxcDefine.moreInputQueryPattern);
};
kpxcDefine.confirm = async function() {
if (kpxcDefine.dataStep !== 4) {
return;
}
if (!kpxc.settings['defined-custom-fields']) {
kpxc.settings['defined-custom-fields'] = {};
}
if (kpxcDefine.selection.username) {
kpxcDefine.selection.username = kpxcFields.setId(kpxcDefine.selection.username.originalElement);
}
if (kpxcDefine.selection.password) {
kpxcDefine.selection.password = kpxcFields.setId(kpxcDefine.selection.password.originalElement);
}
if (kpxcDefine.selection.totp) {
kpxcDefine.selection.totp = kpxcFields.setId(kpxcDefine.selection.totp.originalElement);
}
const fieldIds = [];
for (const i of kpxcDefine.selection.fields) {
fieldIds.push(kpxcFields.setId(i));
}
const location = kpxc.getDocumentLocation();
kpxc.settings['defined-custom-fields'][location] = {
username: kpxcDefine.selection.username,
password: kpxcDefine.selection.password,
totp: kpxcDefine.selection.totp,
fields: fieldIds
};
await sendMessage('save_settings', kpxc.settings);
kpxcDefine.close();
};
kpxcDefine.discard = async function() {
if (!kpxcDefine.buttons.discard) {
return;
}
const location = kpxc.getDocumentLocation();
delete kpxc.settings['defined-custom-fields'][location];
await sendMessage('save_settings', kpxc.settings);
await sendMessage('load_settings');
kpxcDefine.discardSection.remove();
};
// Handle keyboard events
kpxcDefine.keyDown = function(e) {
if (!e.isTrusted) {
return;
}
if (e.key === 'Escape') {
kpxcDefine.close();
} else if (e.key === 'Enter') {
e.preventDefault();
} else if (e.keyCode >= 49 && e.keyCode <= 57) {
// Select input field by number
e.preventDefault();
const index = e.keyCode - 48;
const inputFields = document.querySelectorAll(kpxcDefine.keyboardSelectorPattern);
if (inputFields.length >= index) {
kpxcDefine.eventFieldClick(e, inputFields[index - 1]);
}
} else if (e.key === 's') {
e.preventDefault();
kpxcDefine.skip();
} else if (e.key === 'a') {
e.preventDefault();
kpxcDefine.again();
} else if (e.key === 'c') {
e.preventDefault();
kpxcDefine.confirm();
} else if (e.key === 'm') {
e.preventDefault();
kpxcDefine.more();
} else if (e.key === 'd') {
e.preventDefault();
kpxcDefine.discard();
}
};
const removeContent = function(pattern) {
const elems = kpxcDefine.chooser.querySelectorAll(pattern);
for (const e of elems) {
e.remove();
}
};

View file

@ -62,6 +62,31 @@ kpxcFields.getAllCombinations = async function(inputs) {
return combinations;
};
// If there are multiple combinations, return the first one where input field can be found inside the document.
// Used with Custom Login Fields where selected input fields might not be visible on the page yet,
// and there's an extra combination for those. Only used from popup fill.
kpxcFields.getCombinationFromAllInputs = function() {
const inputs = kpxcObserverHelper.getInputs(document.body);
for (const combination of kpxc.combinations) {
for (const value of Object.values(combination)) {
if (Array.isArray(value)) {
for (const v of value) {
if (inputs.some(i => i === v)) {
return combination;
}
}
} else {
if (inputs.some(i => i === value)) {
return combination;
}
}
}
}
return kpxc.combinations[0];
};
// Adds segmented TOTP fields to the combination if found
kpxcFields.getSegmentedTOTPFields = function(inputs, combinations) {
if (!kpxc.settings.showOTPIcon) {
@ -397,12 +422,6 @@ kpxcFields.useCustomLoginFields = async function() {
kpxcTOTPIcons.newIcon(totp, kpxc.databaseState);
}
// If not all expected fields are identified, return an empty combination
if ((creds.username && !username) || (creds.password && !password) || (creds.totp && !totp)
|| (creds.fields.length !== stringFields.length)) {
return [];
}
const combinations = [];
combinations.push({
username: username,

View file

@ -111,7 +111,8 @@ kpxcFill.fillFromPopup = async function(id, uuid) {
combination = kpxc.combinations[1];
}
kpxcFill.fillInCredentials(combination, selectedCredentials.login, uuid);
const foundCombination = kpxcFields.getCombinationFromAllInputs();
kpxcFill.fillInCredentials(foundCombination, selectedCredentials.login, uuid);
kpxcUserAutocomplete.closeList();
};
@ -222,7 +223,7 @@ kpxcFill.fillInCredentials = async function(combination, predefinedUsername, uui
return;
}
if (!combination || (!combination.username && !combination.password)) {
if (!combination) {
logDebug('Error: Empty login combination.');
return;
}
@ -286,8 +287,12 @@ kpxcFill.fillInStringFields = function(fields, stringFields) {
const filledInFields = [];
if (fields && stringFields && fields.length > 0 && stringFields.length > 0) {
for (let i = 0; i < fields.length; i++) {
const currentField = fields[i];
if (i >= stringFields.length) {
continue;
}
const stringFieldValue = Object.values(stringFields[i]);
const currentField = fields[i];
if (currentField && stringFieldValue[0]) {
kpxc.setValue(currentField, stringFieldValue[0]);

View file

@ -255,11 +255,11 @@ kpxc.initAutocomplete = function() {
// Looks for any username & password combinations from the detected input fields
kpxc.initCombinations = async function(inputs = []) {
if (inputs.length === 0) {
const isCustomLoginFieldsUsed = kpxcFields.isCustomLoginFieldsUsed();
if (inputs.length === 0 && !isCustomLoginFieldsUsed) {
return [];
}
const isCustomLoginFieldsUsed = kpxcFields.isCustomLoginFieldsUsed();
const combinations = isCustomLoginFieldsUsed
? await kpxcFields.useCustomLoginFields()
: await kpxcFields.getAllCombinations(inputs);
@ -285,6 +285,11 @@ kpxc.initCombinations = async function(inputs = []) {
}
}
// Update the fields in Custom Login Fields banner if it's open
if (kpxcCustomLoginFieldsBanner.created) {
kpxcCustomLoginFieldsBanner.updateFieldSelections();
}
logDebug('Login field combinations identified:', combinations);
return combinations;
};
@ -296,7 +301,7 @@ kpxc.initCredentialFields = async function() {
// Search all remaining inputs from the page, ignore the previous input fields
const pageInputs = await kpxcFields.getAllPageInputs(formInputs);
if (formInputs.length === 0 && pageInputs.length === 0) {
if (formInputs.length === 0 && pageInputs.length === 0 && !kpxcFields.isCustomLoginFieldsUsed()) {
// Run 'redetect_credentials' manually if no fields are found after a page load
setTimeout(async function() {
if (_called.automaticRedetectCompleted) {
@ -821,7 +826,7 @@ browser.runtime.onMessage.addListener(async function(req, sender) {
} else if (req.action === 'check_database_hash' && 'hash' in req) {
kpxc.detectDatabaseChange(req);
} else if (req.action === 'choose_credential_fields') {
kpxcDefine.init();
kpxcCustomLoginFieldsBanner.create();
} else if (req.action === 'clear_credentials') {
kpxc.clearAllFromPage();
} else if (req.action === 'fill_user_pass_with_specific_login') {

View file

@ -7,15 +7,15 @@ const MIN_INPUT_FIELD_OFFSET_WIDTH = 60;
const MIN_OPACITY = 0.7;
const MAX_OPACITY = 1;
let notificationWrapper;
let notificationTimeout;
const DatabaseState = {
DISCONNECTED: 0,
LOCKED: 1,
UNLOCKED: 2
};
let notificationWrapper;
let notificationTimeout;
// jQuery style wrapper for querySelector()
const $ = function(elem) {
return document.querySelector(elem);
@ -122,8 +122,8 @@ 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);
let left = kpxcUI.bodyStyle.position.toLowerCase() === 'relative' ? rect.left - kpxcUI.bodyRect.left : rect.left;
let top = kpxcUI.bodyStyle.position.toLowerCase() === 'relative' ? rect.top - kpxcUI.bodyRect.top : rect.top;
let left = kpxcUI.getRelativeLeftPosition(rect);
let top = kpxcUI.getRelativeTopPosition(rect);
// Add more space for the icon to show it at the right side of the field if TOTP fields are segmented
if (segmented) {
@ -145,6 +145,14 @@ kpxcUI.setIconPosition = function(icon, field, rtl = false, segmented = false) {
: Pixels(left + scrollLeft + field.offsetWidth - size - offset);
};
kpxcUI.getRelativeLeftPosition = function(rect) {
return kpxcUI.bodyStyle.position.toLowerCase() === 'relative' ? rect.left - kpxcUI.bodyRect.left : rect.left;
};
kpxcUI.getRelativeTopPosition = function(rect) {
return kpxcUI.bodyStyle.position.toLowerCase() === 'relative' ? rect.top - kpxcUI.bodyRect.top : rect.top;
};
kpxcUI.deleteHiddenIcons = function(iconList, attr) {
const deletedIcons = [];
for (const icon of iconList) {
@ -260,6 +268,12 @@ kpxcUI.createNotification = function(type, message) {
}, 5000);
};
kpxcUI.createButton = function(color, textContent, callback) {
const button = kpxcUI.createElement('button', color, {}, textContent);
button.addEventListener('click', callback);
return button;
};
const DOMRectToArray = function(domRect) {
return [ domRect.bottom, domRect.height, domRect.left, domRect.right, domRect.top, domRect.width, domRect.x, domRect.y ];
};
@ -267,8 +281,11 @@ const DOMRectToArray = function(domRect) {
const initColorTheme = function(elem) {
const colorTheme = kpxc.settings['colorTheme'];
if (colorTheme === undefined || colorTheme === 'system') {
if (colorTheme === undefined) {
elem.removeAttribute('data-color-theme');
} else if (colorTheme === 'system') {
const theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
elem.setAttribute('data-color-theme', theme);
} else {
elem.setAttribute('data-color-theme', colorTheme);
}
@ -302,16 +319,6 @@ document.addEventListener('mousemove', function(e) {
kpxcPasswordDialog.dialog.style.top = Pixels(yPos);
}
}
if (kpxcDefine.selected === kpxcDefine.dialog) {
const xPos = e.clientX - kpxcDefine.diffX;
const yPos = e.clientY - kpxcDefine.diffY;
if (kpxcDefine.selected && kpxcDefine.dialog) {
kpxcDefine.dialog.style.left = Pixels(xPos);
kpxcDefine.dialog.style.top = Pixels(yPos);
}
}
});
document.addEventListener('mousedown', function(e) {
@ -328,7 +335,6 @@ document.addEventListener('mouseup', function(e) {
}
kpxcPasswordDialog.selected = null;
kpxcDefine.selected = null;
kpxcUI.mouseDown = false;
});

View file

@ -21,7 +21,7 @@ kpxcUsernameIcons.isValid = function(field) {
|| field.offsetWidth < MIN_INPUT_FIELD_OFFSET_WIDTH
|| field.readOnly
|| kpxcIcons.hasIcon(field)
|| !kpxcFields.isVisible(field)) {
|| (!kpxcFields.isCustomLoginFieldsUsed() && !kpxcFields.isVisible(field))) {
return false;
}
@ -117,7 +117,7 @@ UsernameFieldIcon.prototype.createIcon = function(field) {
};
const iconClicked = async function(field, icon) {
if (!kpxcFields.isVisible(field)) {
if (!kpxcFields.isCustomLoginFieldsUsed() && !kpxcFields.isVisible(field)) {
icon.parentNode.removeChild(icon);
field.removeAttribute('kpxc-username-field');
return;

View file

@ -64,12 +64,33 @@ div.kpxc-banner .kpxc-banner-icon-moz {
background-size: contain;
}
div.kpxc-banner .kpxc-help-icon {
width: 24px;
height: 24px;
overflow: hidden;
background: url('chrome-extension://__MSG_@@extension_id__/icons/help.svg') right no-repeat;
background-size: contain;
}
div.kpxc-banner .kpxc-help-icon-moz {
width: 24px;
height: 24px;
overflow: hidden;
background: url('moz-extension://__MSG_@@extension_id__/icons/help.svg') right no-repeat;
background-size: contain;
}
.kpxc-separator {
border-left: 1px solid #ccc;
height: 100% !important;
margin: 10px !important;
}
.kpxc-pick-info-text {
margin-left: 8px;
margin-right: 8px;
}
div.kpxc-banner .kpxc-checkbox {
margin: 2px !important;
}

View file

@ -46,10 +46,12 @@
transition: all .15s;
}
.kpxc-button:disabled {
border-color: #ccc !important;
.kpxc-button:disabled, .kpxc-gray-button {
background-color: #777777 !important;
border-color: #444444 !important;
color: #fff !important;
}
.kpxc-button:disabled:hover {
background-color: #fff !important;
background-color: #777777 !important;
}

View file

@ -1,117 +1,61 @@
.kpxcDefine-modal-backdrop {
bottom: 0;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 2147483645;
}
.kpxcDefine-modal-backdrop:after {
background-color: #000000;
bottom: 0;
content: '';
filter: alpha(opacity=50);
left: 0;
opacity: 0.5;
position: fixed;
right: 0;
top: 0;
}
#kpxcDefine-fields {
z-index: 2147483646;
}
#kpxcDefine-description {
background-color: rgba(0,0,0,0.8);
border: 2px solid #555555;
color: #efefef;
cursor: pointer;
font-size: 15px;
height: auto !important;
left: 150px;
padding: 7px 5px;
position: absolute;
text-align: left;
top: 100px;
user-select: none;
z-index: 2147483646;
}
#kpxcDefine-description div:first-of-type {
color: #efefef;
font-weight: bold;
font-size: 120%;
margin-top: 0;
padding-bottom: 8px;
padding-top: 0;
text-align: left;
}
#kpxcDefine-description p {
border-top: 2px solid #666666;
color: #efefef;
line-height: 110%;
margin-top: 10px;
padding-top: 10px;
}
#kpxcDefine-help {
margin-bottom: 3px;
}
.kpxcDefine-keyboardHelp {
font-size: 0.75em;
height: auto !important;
}
.kpxcDefine-keyboardHelp kbd {
background-color: #eee;
border-radius: 3px;
border: 1px solid #b4b4b4;
color: #333;
display: inline-block;
height: auto !important;
line-height: 1;
padding: 2px 4px;
white-space: nowrap;
}
.kpxcDefine-chooser-help {
height: auto !important;
}
.kpxcDefine-fixed-field {
align-items: center;
background-color: rgba(239,239,239,0.4);
border: 2px solid #efefef;
color: #fff;
cursor: pointer;
display: flex;
font-weight: bold;
justify-content: center;
position: absolute;
text-align: center;
z-index: 2147483646;
}
.kpxcDefine-fixed-field-dark {
background-color: rgba(45, 45, 45, 0.4);
border: 2px solid #0f0f0f;
color: #fff;
}
.kpxcDefine-fixed-hover-field {
background-color: rgba(255, 165, 238, 0.631);
border: 2px solid orange;
background-color: rgba(255,165,239,0.4);
}
.kpxcDefine-fixed-hover-field-dark {
background-color: rgba(255, 165, 238, 0.599);
border: 2px solid orange;
color: #505050;
}
.kpxcDefine-fixed-password-field {
border: 2px solid red;
background-color: rgba(255,0,0,0.4);
border: 2px solid red;
color: #efefef;
}
.kpxcDefine-fixed-username-field {
border: 2px solid limegreen;
background-color: rgba(50,205,50,0.4);
background-color: rgba(232, 252, 3, 0.4);
border: 2px solid yellow;
color: #efefef;
}
.kpxcDefine-fixed-string-field, .kpxcDefine-fixed-totp-field {
border: 2px solid deepskyblue;
background-color: rgba(30,144,255,0.4);
.kpxcDefine-fixed-totp-field {
background-color: rgba(50,205,50,0.4);
border: 2px solid limegreen;
color: #efefef;
}
.kpxcDefine-fixed-string-field {
background-color: rgba(30,144,255,0.4);
border: 2px solid deepskyblue;
color: #efefef;
}
.kpxcDefine-dark-text {
color: #505050;
}

View file

@ -0,0 +1 @@
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><g transform="matrix(0.983029,0,0,0.983029,-2.15274,-4.69129)"><circle cx="26.604" cy="29.187" r="20.345" style="fill:#0090ff"/></g><g transform="matrix(31.8224,0,0,31.8224,16.3008,35.374)"><path d="M.169-.218C.169-.263.175-.3.186-.327.197-.354.217-.38.247-.407.276-.433.296-.454.306-.471.315-.487.32-.505.32-.523.32-.578.295-.605.244-.605.22-.605.201-.598.186-.583.172-.568.164-.548.164-.522H.022C.023-.584.043-.633.082-.668.122-.703.176-.721.244-.721.313-.721.367-.704.405-.671.443-.637.462-.59.462-.529.462-.501.456-.475.443-.451.431-.426.409-.399.378-.369L.339-.331C.314-.307.3-.28.296-.248l-.002.03H.169zm-.014.15C.155-.09.163-.108.177-.122.192-.136.211-.143.234-.143S.276-.136.291-.122c.015.014.022.032.022.054C.313-.047.306-.029.292-.015.277-.001.258.006.234.006.211.006.191-.001.177-.015.163-.029.155-.047.155-.068z" style="fill:#fff;fill-rule:nonzero"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -58,7 +58,7 @@
"content/banner.js",
"content/autocomplete.js",
"content/credential-autocomplete.js",
"content/define.js",
"content/custom-fields-banner.js",
"content/fields.js",
"content/fill.js",
"content/form.js",
@ -124,6 +124,7 @@
},
"web_accessible_resources": [
"icons/disconnected.svg",
"icons/help.svg",
"icons/keepassxc.svg",
"icons/key.svg",
"icons/locked.svg",