From d517b12bd539077fe408531f4f27ad6295a13a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20V=C3=A4nttinen?= Date: Tue, 8 Oct 2024 07:00:43 +0300 Subject: [PATCH] Add support for draggable banners (#2359) Add support for draggable banners --- .eslintrc | 1 + keepassxc-browser/background/event.js | 2 + keepassxc-browser/background/page.js | 10 ++++ keepassxc-browser/common/global.js | 5 ++ keepassxc-browser/content/autocomplete.js | 6 +- keepassxc-browser/content/banner.js | 38 +++++++++---- .../content/custom-fields-banner.js | 6 +- keepassxc-browser/content/passkeys.js | 2 +- keepassxc-browser/content/ui.js | 57 +++++++++++++++++++ keepassxc-browser/css/banner.css | 35 +++++++++--- 10 files changed, 140 insertions(+), 22 deletions(-) diff --git a/.eslintrc b/.eslintrc index 8bf1e16..cf89cf1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -92,6 +92,7 @@ "AuthenticatorAssertionResponse": "readonly", "AuthenticatorAttestationResponse": "readonly", "Autocomplete": "readonly", + "BannerPosition": "readonly", "BLUE_BUTTON": "readonly", "bootstrap": "readonly", "browser": "readonly", diff --git a/keepassxc-browser/background/event.js b/keepassxc-browser/background/event.js index 57f848a..10cb1ae 100755 --- a/keepassxc-browser/background/event.js +++ b/keepassxc-browser/background/event.js @@ -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, diff --git a/keepassxc-browser/background/page.js b/keepassxc-browser/background/page.js index 81f4308..12b378d 100755 --- a/keepassxc-browser/background/page.js +++ b/keepassxc-browser/background/page.js @@ -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) { diff --git a/keepassxc-browser/common/global.js b/keepassxc-browser/common/global.js index 45fcbfb..141a73c 100755 --- a/keepassxc-browser/common/global.js +++ b/keepassxc-browser/common/global.js @@ -50,6 +50,11 @@ const AssociatedAction = { CANCELED: 3 }; +const BannerPosition = { + BOTTOM: 0, + TOP: 1 +}; + const ManualFill = { NONE: 0, PASSWORD: 1, diff --git a/keepassxc-browser/content/autocomplete.js b/keepassxc-browser/content/autocomplete.js index 548a2b0..4c98d4b 100644 --- a/keepassxc-browser/content/autocomplete.js +++ b/keepassxc-browser/content/autocomplete.js @@ -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); } } diff --git a/keepassxc-browser/content/banner.js b/keepassxc-browser/content/banner.js index 78b1644..eb732a3 100644 --- a/keepassxc-browser/content/banner.js +++ b/keepassxc-browser/content/banner.js @@ -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); +}; diff --git a/keepassxc-browser/content/custom-fields-banner.js b/keepassxc-browser/content/custom-fields-banner.js index d29dd93..42b19a1 100644 --- a/keepassxc-browser/content/custom-fields-banner.js +++ b/keepassxc-browser/content/custom-fields-banner.js @@ -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 diff --git a/keepassxc-browser/content/passkeys.js b/keepassxc-browser/content/passkeys.js index 7e392f0..601462e 100644 --- a/keepassxc-browser/content/passkeys.js +++ b/keepassxc-browser/content/passkeys.js @@ -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 diff --git a/keepassxc-browser/content/ui.js b/keepassxc-browser/content/ui.js index 70e62e7..49379ce 100644 --- a/keepassxc-browser/content/ui.js +++ b/keepassxc-browser/content/ui.js @@ -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); diff --git a/keepassxc-browser/css/banner.css b/keepassxc-browser/css/banner.css index 47c082d..797952d 100644 --- a/keepassxc-browser/css/banner.css +++ b/keepassxc-browser/css/banner.css @@ -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;