mirror of
https://github.com/keepassxreboot/keepassxc-browser.git
synced 2026-03-11 08:54:43 +00:00
Add support for draggable banners (#2359)
Add support for draggable banners
This commit is contained in:
parent
3f4825cc4c
commit
d517b12bd5
10 changed files with 140 additions and 22 deletions
|
|
@ -92,6 +92,7 @@
|
|||
"AuthenticatorAssertionResponse": "readonly",
|
||||
"AuthenticatorAttestationResponse": "readonly",
|
||||
"Autocomplete": "readonly",
|
||||
"BannerPosition": "readonly",
|
||||
"BLUE_BUTTON": "readonly",
|
||||
"bootstrap": "readonly",
|
||||
"browser": "readonly",
|
||||
|
|
|
|||
|
|
@ -237,6 +237,8 @@ kpxcEvent.sendBackToTabs = async function(tab, args = []) {
|
|||
kpxcEvent.messageHandlers = {
|
||||
'add_credentials': keepass.addCredentials,
|
||||
'associate': keepass.associate,
|
||||
'banner_get_position': page.getBannerPosition,
|
||||
'banner_set_position': page.setBannerPosition,
|
||||
'check_database_hash': keepass.checkDatabaseHash,
|
||||
'check_update_keepassxc': kpxcEvent.onCheckUpdateKeePassXC,
|
||||
'compare_version': kpxcEvent.compareVersion,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const defaultSettings = {
|
|||
autoReconnect: false,
|
||||
autoRetrieveCredentials: true,
|
||||
autoSubmit: false,
|
||||
bannerPosition: BannerPosition.TOP,
|
||||
checkUpdateKeePassXC: CHECK_UPDATE_NEVER,
|
||||
clearCredentialsTimeout: 10,
|
||||
colorTheme: 'system',
|
||||
|
|
@ -253,6 +254,15 @@ page.setManualFill = async function(tab, manualFill) {
|
|||
page.manualFill = manualFill;
|
||||
};
|
||||
|
||||
page.getBannerPosition = async function(tab) {
|
||||
return page.settings.bannerPosition;
|
||||
};
|
||||
|
||||
page.setBannerPosition = async function(tab, position) {
|
||||
page.settings.bannerPosition = position;
|
||||
await browser.storage.local.set({ 'settings': page.settings });
|
||||
};
|
||||
|
||||
page.getSubmitted = async function(tab) {
|
||||
// Do not return any credentials if the tab ID does not match.
|
||||
if (tab.id !== page.submittedCredentials.tabId) {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ const AssociatedAction = {
|
|||
CANCELED: 3
|
||||
};
|
||||
|
||||
const BannerPosition = {
|
||||
BOTTOM: 0,
|
||||
TOP: 1
|
||||
};
|
||||
|
||||
const ManualFill = {
|
||||
NONE: 0,
|
||||
PASSWORD: 1,
|
||||
|
|
|
|||
|
|
@ -324,7 +324,7 @@ class Autocomplete {
|
|||
}
|
||||
}
|
||||
|
||||
updatePosition() {
|
||||
updatePosition() {
|
||||
if (!this.container || !this.input) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -336,7 +336,7 @@ class Autocomplete {
|
|||
this.list.style.maxWidth = `max(${Pixels(this.input.offsetWidth)}, 600px)`;
|
||||
|
||||
// Get body zoom radio
|
||||
const zoom = kpxcUI.bodyStyle.zoom || 1;
|
||||
const zoom = kpxcUI.bodyStyle.zoom || 1;
|
||||
|
||||
// Calculate Y offset if menu does not fit to the bottom of the screen -> show it at the top of the input field
|
||||
const menuRect = this.container.getBoundingClientRect();
|
||||
|
|
@ -352,7 +352,7 @@ class Autocomplete {
|
|||
);
|
||||
this.container.style.left = Pixels((rect.left - kpxcUI.bodyRect.left) / zoom + scrollLeft);
|
||||
} else {
|
||||
this.container.style.top = Pixels(rect.top / zoom + scrollTop + this.input.offsetHeight - menuOffset)
|
||||
this.container.style.top = Pixels(rect.top / zoom + scrollTop + this.input.offsetHeight - menuOffset);
|
||||
this.container.style.left = Pixels(rect.left / zoom + scrollLeft);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,10 @@ kpxcBanner.create = async function(credentials = {}) {
|
|||
credentials.username = credentials.username.trim();
|
||||
kpxcBanner.credentials = credentials;
|
||||
|
||||
const banner = kpxcUI.createElement('div', 'kpxc-banner', { 'id': 'kpxc-banner-container' });
|
||||
const bannerPosition = await sendMessage('banner_get_position');
|
||||
const bannerClass =
|
||||
bannerPosition === BannerPosition.TOP ? 'kpxc-banner kpxc-banner-on-top' : 'kpxc-banner kpxc-banner-on-bottom';
|
||||
const banner = kpxcUI.createElement('div', bannerClass, { 'id': 'kpxc-banner-container' });
|
||||
initColorTheme(banner);
|
||||
banner.style.zIndex = '2147483646';
|
||||
|
||||
|
|
@ -114,6 +117,8 @@ kpxcBanner.create = async function(credentials = {}) {
|
|||
dismissButton.textContent = tr('popupButtonBack');
|
||||
});
|
||||
|
||||
kpxcUI.makeBannerDraggable(banner);
|
||||
|
||||
dismissButton.addEventListener('click', function(e) {
|
||||
if (!e.isTrusted) {
|
||||
return;
|
||||
|
|
@ -411,7 +416,7 @@ kpxcBanner.createCredentialDialog = async function() {
|
|||
const connectedDatabase = await sendMessage('get_connected_database');
|
||||
const databaseName = connectedDatabase.count > 0 ? connectedDatabase.identifier : '';
|
||||
|
||||
const dialog = kpxcUI.createElement('div', 'kpxc-banner-dialog');
|
||||
const dialog = kpxcUI.createElement('div', 'kpxc-banner-dialog kpxc-banner-dialog-top');
|
||||
const databaseText = kpxcUI.createElement('p');
|
||||
const spanDatabaseText = kpxcUI.createElement('span', '', {}, tr('rememberSaving'));
|
||||
const usernameNew = kpxcUI.createElement('p', 'username-new');
|
||||
|
|
@ -421,11 +426,9 @@ kpxcBanner.createCredentialDialog = async function() {
|
|||
const spanNewUsername = kpxcUI.createElement('span', '', {}, tr('rememberNewUsername'));
|
||||
const spanUsernameExists = kpxcUI.createElement('span', '', {}, tr('rememberUsernameExists'));
|
||||
|
||||
// Set dialog position
|
||||
dialog.style.top = Pixels(kpxcBanner.banner.offsetHeight);
|
||||
dialog.style.right = '0';
|
||||
|
||||
setDialogPosition(dialog);
|
||||
databaseText.appendChild(spanDatabaseText);
|
||||
|
||||
const spanName = kpxcUI.createElement('span', 'strong', {}, databaseName);
|
||||
databaseText.append(spanName);
|
||||
|
||||
|
|
@ -447,10 +450,25 @@ kpxcBanner.createGroupDialog = function() {
|
|||
const chooseGroup = kpxcUI.createElement('p', '', {}, tr('rememberChooseGroup'));
|
||||
const list = kpxcUI.createElement('ul', 'list-group', { 'id': 'list' });
|
||||
|
||||
// Set dialog position
|
||||
dialog.style.top = Pixels(kpxcBanner.banner.offsetHeight);
|
||||
dialog.style.right = Pixels(0);
|
||||
|
||||
setDialogPosition(dialog);
|
||||
dialog.appendMultiple(chooseGroup, list);
|
||||
kpxcBanner.banner.appendChild(dialog);
|
||||
};
|
||||
|
||||
const setDialogPosition = function(dialog) {
|
||||
if (!dialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (kpxcBanner.banner.classList.contains('kpxc-banner-on-bottom')) {
|
||||
dialog.style.bottom = Pixels(kpxcBanner.banner.offsetHeight);
|
||||
dialog.classList.remove('kpxc-banner-dialog-top');
|
||||
dialog.classList.add('kpxc-banner-dialog-bottom');
|
||||
} else {
|
||||
dialog.style.top = Pixels(kpxcBanner.banner.offsetHeight);
|
||||
dialog.classList.remove('kpxc-banner-dialog-bottom');
|
||||
dialog.classList.add('kpxc-banner-dialog-top');
|
||||
}
|
||||
|
||||
dialog.style.right = Pixels(0);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -72,7 +72,10 @@ kpxcCustomLoginFieldsBanner.create = async function() {
|
|||
return;
|
||||
}
|
||||
|
||||
const banner = kpxcUI.createElement('div', 'kpxc-banner', { 'id': 'container' });
|
||||
const bannerPosition = await sendMessage('banner_get_position');
|
||||
const bannerClass =
|
||||
bannerPosition === BannerPosition.TOP ? 'kpxc-banner kpxc-banner-on-top' : 'kpxc-banner kpxc-banner-on-bottom';
|
||||
const banner = kpxcUI.createElement('div', bannerClass, { 'id': 'container' });
|
||||
banner.style.zIndex = '2147483646';
|
||||
kpxcCustomLoginFieldsBanner.chooser = kpxcUI.createElement('div', '', { 'id': 'kpxcDefine-fields' });
|
||||
|
||||
|
|
@ -111,6 +114,7 @@ kpxcCustomLoginFieldsBanner.create = async function() {
|
|||
bannerButtons.appendMultiple(resetButton, separator, usernameButton,
|
||||
passwordButton, totpButton, stringFieldsButton, secondSeparator, clearDataButton, confirmButton, closeButton);
|
||||
banner.appendMultiple(bannerInfo, bannerButtons);
|
||||
kpxcUI.makeBannerDraggable(banner);
|
||||
|
||||
const location = kpxc.getDocumentLocation();
|
||||
kpxcCustomLoginFieldsBanner.buttons.clearData.style.display
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const createAttestationResponse = function(publicKey) {
|
|||
getPublicKeyAlgorithm: () => publicKey.response?.publicKeyAlgorithm,
|
||||
getTransports: () => [ 'internal' ]
|
||||
};
|
||||
return Object.setPrototypeOf(response, AuthenticatorAttestationResponse.prototype);
|
||||
return Object.setPrototypeOf(response, AuthenticatorAttestationResponse.prototype);
|
||||
};
|
||||
|
||||
// Wraps response to AuthenticatorAssertionResponse object
|
||||
|
|
|
|||
|
|
@ -237,6 +237,59 @@ kpxcUI.isRTL = function(field) {
|
|||
f => ({ 'ltr': false, 'rtl': true })[f.getLowerCaseAttribute('dir')]);
|
||||
};
|
||||
|
||||
kpxcUI.makeBannerDraggable = function(banner) {
|
||||
if (!banner) {
|
||||
return;
|
||||
}
|
||||
|
||||
banner.draggable = true;
|
||||
|
||||
banner.addEventListener('dragstart', (e) => {
|
||||
if (!e.isTrusted) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.dataTransfer.effectAllowed = 'copyMove';
|
||||
document.addEventListener('dragover', preventDefaultDragEnd);
|
||||
});
|
||||
|
||||
banner.addEventListener('dragend', async (e) => {
|
||||
if (!e.isTrusted || !e.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If dragged to last third of the screen, move banner to bottom.
|
||||
// If dragged to first third of the screen, move banner to top.
|
||||
// If credential/group dialog is open, move it as well.
|
||||
const bannerDialog = e.target.querySelector('.kpxc-banner-dialog');
|
||||
if (e.y > e.view.innerHeight * (2 / 3) && e.target.classList.contains('kpxc-banner-on-top')) {
|
||||
e.target.classList.remove('kpxc-banner-on-top');
|
||||
e.target.classList.add('kpxc-banner-on-bottom');
|
||||
|
||||
if (bannerDialog) {
|
||||
bannerDialog.style.top = '';
|
||||
bannerDialog.style.bottom = Pixels(e.target.offsetHeight);
|
||||
bannerDialog.classList.remove('kpxc-banner-dialog-top');
|
||||
bannerDialog.classList.add('kpxc-banner-dialog-bottom');
|
||||
}
|
||||
await sendMessage('banner_set_position', BannerPosition.BOTTOM);
|
||||
} else if (e.y < e.view.innerHeight * (1 / 3) && e.target.classList.contains('kpxc-banner-on-bottom')) {
|
||||
e.target.classList.remove('kpxc-banner-on-bottom');
|
||||
e.target.classList.add('kpxc-banner-on-top');
|
||||
|
||||
if (bannerDialog) {
|
||||
bannerDialog.style.bottom = '';
|
||||
bannerDialog.style.top = Pixels(e.target.offsetHeight);
|
||||
bannerDialog.classList.remove('kpxc-banner-dialog-bottom');
|
||||
bannerDialog.classList.add('kpxc-banner-dialog-top');
|
||||
}
|
||||
await sendMessage('banner_set_position', BannerPosition.TOP);
|
||||
}
|
||||
|
||||
document.removeEventListener('dragover', preventDefaultDragEnd);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Detects if the input field appears or disappears -> show/hide the icon
|
||||
* - boundingClientRect with slightly (< -10) negative values -> hidden
|
||||
|
|
@ -360,6 +413,10 @@ const createStylesheet = function(file) {
|
|||
return stylesheet;
|
||||
};
|
||||
|
||||
const preventDefaultDragEnd = function(e) {
|
||||
e?.preventDefault();
|
||||
};
|
||||
|
||||
const logDebug = function(message, extra) {
|
||||
if (kpxc.settings.debugLogging) {
|
||||
debugLogMessage(message, extra);
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ div.kpxc-banner {
|
|||
animation-duration: 0.2s;
|
||||
animation-name: kpxc-slidein;
|
||||
background-color: var(--kpxc-background-color);
|
||||
border-bottom: 1px solid #38383d !important;
|
||||
box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2) !important;
|
||||
box-sizing: border-box !important;
|
||||
color: var(--kpxc-text-color) !important;
|
||||
display: flex !important;
|
||||
font-size: 1em !important;
|
||||
height: auto !important;
|
||||
justify-content: space-between !important;
|
||||
left: 0px !important;
|
||||
line-height: 1 !important;
|
||||
margin: 0 auto !important;
|
||||
|
|
@ -16,10 +16,23 @@ div.kpxc-banner {
|
|||
overflow-x: hidden !important;
|
||||
padding: 4px !important;
|
||||
position: fixed !important;
|
||||
top: 0px !important;
|
||||
width: 100% !important;
|
||||
display: flex !important;
|
||||
justify-content: space-between !important;
|
||||
}
|
||||
|
||||
div.kpxc-banner:hover {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.kpxc-banner-on-bottom {
|
||||
border-bottom: 1px solid #38383d !important;
|
||||
bottom: 0px !important;
|
||||
box-shadow: 0 -4px 6px 0 hsla(0, 0%, 0%, 0.2) !important;
|
||||
}
|
||||
|
||||
.kpxc-banner-on-top {
|
||||
border-top: 1px solid #38383d !important;
|
||||
box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2) !important;
|
||||
top: 0px !important;
|
||||
}
|
||||
|
||||
@keyframes kpxc-slidein {
|
||||
|
|
@ -105,8 +118,6 @@ div.kpxc-banner label {
|
|||
div.kpxc-banner-dialog {
|
||||
background-color: var(--kpxc-background-color);
|
||||
border: var(--kpxc-container-border);
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2);
|
||||
color: var(--kpxc-text-color);
|
||||
display: block !important;
|
||||
margin: 0 auto;
|
||||
|
|
@ -120,6 +131,16 @@ div.kpxc-banner-dialog {
|
|||
z-index: 10000000;
|
||||
}
|
||||
|
||||
div.kpxc-banner-dialog-bottom {
|
||||
border-radius: 4px 4px 0 0;
|
||||
box-shadow: 0 -4px 6px 0 hsla(0, 0%, 0%, 0.2);
|
||||
}
|
||||
|
||||
div.kpxc-banner-dialog-top {
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2);
|
||||
}
|
||||
|
||||
div.kpxc-banner-dialog > ul {
|
||||
list-style-type: disc !important;
|
||||
margin-bottom: 0 !important;
|
||||
|
|
|
|||
Loading…
Reference in a new issue