Add support for draggable banners (#2359)

Add support for draggable banners
This commit is contained in:
Sami Vänttinen 2024-10-08 07:00:43 +03:00 committed by GitHub
parent 3f4825cc4c
commit d517b12bd5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 140 additions and 22 deletions

View file

@ -92,6 +92,7 @@
"AuthenticatorAssertionResponse": "readonly",
"AuthenticatorAttestationResponse": "readonly",
"Autocomplete": "readonly",
"BannerPosition": "readonly",
"BLUE_BUTTON": "readonly",
"bootstrap": "readonly",
"browser": "readonly",

View file

@ -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,

View file

@ -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) {

View file

@ -50,6 +50,11 @@ const AssociatedAction = {
CANCELED: 3
};
const BannerPosition = {
BOTTOM: 0,
TOP: 1
};
const ManualFill = {
NONE: 0,
PASSWORD: 1,

View file

@ -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);
}
}

View file

@ -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);
};

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -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;