From c39d4854fbd2556063d2d5be251e83a167f270ba Mon Sep 17 00:00:00 2001 From: Aleksandra Bozek Date: Tue, 23 Sep 2025 16:27:27 +0200 Subject: [PATCH 1/4] IBX-10533: Quick action btn manager for handling & displaying multiple btns --- .../Resources/encore/ibexa.js.config.js | 1 + .../public/js/scripts/admin.back.to.top.js | 28 +++++++++- .../public/js/scripts/core/backdrop.js | 1 + .../public/js/scripts/quick.action.manager.js | 54 +++++++++++++++++++ .../public/js/scripts/sidebar/side.panel.js | 44 ++++++++++----- .../Resources/public/scss/_back-to-top.scss | 4 -- src/bundle/Resources/public/scss/_tabs.scss | 7 +++ .../admin/ui/component/tab/tabs.html.twig | 1 + .../ui/component/tab/tabs_header.html.twig | 1 + .../admin/ui/component/tab/tabs_tab.html.twig | 4 +- .../views/themes/admin/ui/layout.html.twig | 32 +++++------ 11 files changed, 144 insertions(+), 33 deletions(-) create mode 100644 src/bundle/Resources/public/js/scripts/quick.action.manager.js diff --git a/src/bundle/Resources/encore/ibexa.js.config.js b/src/bundle/Resources/encore/ibexa.js.config.js index c7c09b3e1f..0539b9fe80 100644 --- a/src/bundle/Resources/encore/ibexa.js.config.js +++ b/src/bundle/Resources/encore/ibexa.js.config.js @@ -40,6 +40,7 @@ const layout = [ path.resolve(__dirname, '../public/js/scripts/admin.picker.js'), path.resolve(__dirname, '../public/js/scripts/admin.notifications.modal.js'), path.resolve(__dirname, '../public/js/scripts/sidebar/side.panel.js'), + path.resolve(__dirname, '../public/js/scripts/quick.action.manager.js'), path.resolve(__dirname, '../public/js/scripts/admin.location.add.translation.js'), path.resolve(__dirname, '../public/js/scripts/admin.form.autosubmit.js'), path.resolve(__dirname, '../public/js/scripts/admin.anchor.navigation'), diff --git a/src/bundle/Resources/public/js/scripts/admin.back.to.top.js b/src/bundle/Resources/public/js/scripts/admin.back.to.top.js index 6aec1a432d..efa6088cf7 100644 --- a/src/bundle/Resources/public/js/scripts/admin.back.to.top.js +++ b/src/bundle/Resources/public/js/scripts/admin.back.to.top.js @@ -1,5 +1,6 @@ (function (global, doc) { const backToTopBtn = doc.querySelector('.ibexa-back-to-top__btn'); + const backToTop = doc.querySelector('.ibexa-back-to-top'); const backToTopAnchor = doc.querySelector('.ibexa-back-to-top-anchor'); const backToTopScrollContainer = doc.querySelector('.ibexa-back-to-top-scroll-container'); @@ -7,12 +8,29 @@ return; } + const checkIsVisible = () => { + if (!backToTop) { + return false; + } + + return backToTopBtn.classList.contains('ibexa-back-to-top__btn--visible'); + }; + const backToTopBtnTitle = backToTopBtn.querySelector('.ibexa-back-to-top__title'); let currentBackToTopAnchorHeight = backToTopAnchor.offsetHeight; const setBackToTopBtnTextVisibility = (container) => { const isTitleVisible = Math.abs(container.scrollHeight - container.scrollTop - container.clientHeight) <= 2; + const shouldBeVisible = container.scrollTop !== 0; + + if (backToTopBtn.classList.contains('ibexa-back-to-top__btn--visible') && !shouldBeVisible) { + backToTopBtn.classList.remove('ibexa-back-to-top__btn--visible'); + } + + if (!backToTopBtn.classList.contains('ibexa-back-to-top__btn--visible') && shouldBeVisible) { + backToTopBtn.classList.add('ibexa-back-to-top__btn--visible'); + global.ibexa.adminUiConfig.quickActionManager.recalculateButtonsLayout(); + } - backToTopBtn.classList.toggle('ibexa-back-to-top__btn--visible', container.scrollTop !== 0); backToTopBtn.classList.toggle('ibexa-btn--no-text', !isTitleVisible); backToTopBtnTitle.classList.toggle('ibexa-back-to-top__title--visible', isTitleVisible); }; @@ -37,6 +55,14 @@ setBackToTopBtnTextVisibility(backToTopScrollContainer); }); + const config = { + name: 'back-to-top', + zIndex: 10, + selector: backToTop, + priority: 100, + checkVisibility: checkIsVisible, + }; + global.ibexa.adminUiConfig.quickActionManager.registerButton(config); resizeObserver.observe(backToTopAnchor); })(window, window.document); diff --git a/src/bundle/Resources/public/js/scripts/core/backdrop.js b/src/bundle/Resources/public/js/scripts/core/backdrop.js index 2982ad4277..3daefd3e3e 100644 --- a/src/bundle/Resources/public/js/scripts/core/backdrop.js +++ b/src/bundle/Resources/public/js/scripts/core/backdrop.js @@ -28,6 +28,7 @@ if (this.backdrop) { this.backdrop.remove(); this.backdrop = null; + this.extraClasses = []; } } diff --git a/src/bundle/Resources/public/js/scripts/quick.action.manager.js b/src/bundle/Resources/public/js/scripts/quick.action.manager.js new file mode 100644 index 0000000000..95b08ed28b --- /dev/null +++ b/src/bundle/Resources/public/js/scripts/quick.action.manager.js @@ -0,0 +1,54 @@ +(function (global) { + let registeredActionButtons = []; + + const QuickActionManager = (() => { + const registerButton = (config) => { + if (!config || !config.selector || registeredActionButtons.some((btn) => btn.name === config.name)) { + return; + } + + registeredActionButtons = [...registeredActionButtons, config]; + recalculateButtonsLayout(); + }; + const unregisterButton = (name) => { + registeredActionButtons = registeredActionButtons.filter((btn) => btn.name !== name); + recalculateButtonsLayout(); + }; + + const recalculateButtonsLayout = () => { + const sortedButtons = registeredActionButtons.sort((a, b) => a.priority - b.priority); + const buttonsToRender = sortedButtons.filter((el) => { + if (el.checkVisibility && typeof el.checkVisibility === 'function') { + const isVisible = el.checkVisibility(); + + return isVisible; + } + + return false; + }); + + buttonsToRender.forEach((buttonConfig, index) => { + const { selector } = buttonConfig; + + if (!selector.style.transition) { + selector.style.transition = 'all 0.3s ease-in-out'; + } + + selector.style.position = 'fixed'; + selector.style.right = '2rem'; + selector.style.zIndex = buttonConfig.zIndex || 1040; + + const bottomPosition = `${index === 0 ? 2 : (index + 1) * 3.2}rem`; + selector.style.bottom = bottomPosition; + }); + }; + return { + registerButton, + unregisterButton, + recalculateButtonsLayout, + }; + })(); + + window.ibexa = window.ibexa || {}; + global.ibexa.adminUiConfig.quickActionManager = QuickActionManager; +})(window); diff --git a/src/bundle/Resources/public/js/scripts/sidebar/side.panel.js b/src/bundle/Resources/public/js/scripts/sidebar/side.panel.js index 08123d75b4..fcb624fffe 100644 --- a/src/bundle/Resources/public/js/scripts/sidebar/side.panel.js +++ b/src/bundle/Resources/public/js/scripts/sidebar/side.panel.js @@ -4,22 +4,42 @@ '.ibexa-side-panel .ibexa-btn--close, .ibexa-side-panel .ibexa-side-panel__btn--cancel', ); const sidePanelTriggers = [...doc.querySelectorAll('.ibexa-side-panel-trigger')]; - const backdrop = new ibexa.core.Backdrop(); - const removeBackdrop = () => { - backdrop.hide(); + const panelBackdrops = new Map(); // Mapa przechowująca powiązania panel -> backdrop + const defaultBackdrop = new ibexa.core.Backdrop(); + const removeBackdrop = (sidePanel) => { + const backdrop = panelBackdrops.get(sidePanel) || defaultBackdrop; + + backdrop.remove(); doc.body.classList.remove('ibexa-scroll-disabled'); + + if (panelBackdrops.has(sidePanel)) { + panelBackdrops.delete(sidePanel); + } }; - const showBackdrop = () => { - backdrop.show(); + const showBackdrop = (sidePanel) => { + if (sidePanel.dataset.backdropClasses) { + const extraClasses = sidePanel.dataset.backdropClasses.split(' ').filter(Boolean); + const newBackdrop = new ibexa.core.Backdrop({ extraClasses }); + + newBackdrop.show(); + panelBackdrops.set(sidePanel, newBackdrop); + } else { + defaultBackdrop.show(); + panelBackdrops.set(sidePanel, defaultBackdrop); + } + doc.body.classList.add('ibexa-scroll-disabled'); }; const toggleSidePanelVisibility = (sidePanel) => { const shouldBeVisible = sidePanel.classList.contains(CLASS_HIDDEN); const handleClickOutside = (event) => { - if (event.target.classList.contains('ibexa-backdrop')) { + const currentBackdrop = panelBackdrops.get(sidePanel); + + if (event.target.classList.contains('ibexa-backdrop') && event.target === currentBackdrop.get()) { + event.stopPropagation(); sidePanel.classList.add(CLASS_HIDDEN); - doc.body.removeEventListener('click', handleClickOutside, false); - removeBackdrop(); + doc.body.removeEventListener('click', handleClickOutside, { capture: true }); + removeBackdrop(sidePanel); if (sidePanel.dataset?.closeReload === 'true') { global.location.reload(); @@ -30,11 +50,11 @@ sidePanel.classList.toggle(CLASS_HIDDEN, !shouldBeVisible); if (shouldBeVisible) { - doc.body.addEventListener('click', handleClickOutside, false); - showBackdrop(); + doc.body.addEventListener('click', handleClickOutside, { capture: true }); + showBackdrop(sidePanel); } else { - doc.body.removeEventListener('click', handleClickOutside, false); - removeBackdrop(); + doc.body.removeEventListener('click', handleClickOutside, { capture: true }); + removeBackdrop(sidePanel); } }; diff --git a/src/bundle/Resources/public/scss/_back-to-top.scss b/src/bundle/Resources/public/scss/_back-to-top.scss index 764785efef..7a0ef57c8b 100644 --- a/src/bundle/Resources/public/scss/_back-to-top.scss +++ b/src/bundle/Resources/public/scss/_back-to-top.scss @@ -1,8 +1,4 @@ .ibexa-back-to-top { - position: fixed; - bottom: calculateRem(16px); - right: calculateRem(32px); - .btn.ibexa-back-to-top__btn { height: calculateRem(62px); min-width: calculateRem(62px); diff --git a/src/bundle/Resources/public/scss/_tabs.scss b/src/bundle/Resources/public/scss/_tabs.scss index 3476684911..361e5a8fe4 100644 --- a/src/bundle/Resources/public/scss/_tabs.scss +++ b/src/bundle/Resources/public/scss/_tabs.scss @@ -30,6 +30,13 @@ } } + &--no-corner { + .ibexa-tabs__link { + font-weight: 600; + background-color: $ibexa-color-light-400; + } + } + &--hidden { display: none; } diff --git a/src/bundle/Resources/views/themes/admin/ui/component/tab/tabs.html.twig b/src/bundle/Resources/views/themes/admin/ui/component/tab/tabs.html.twig index 6bb38abbb1..2c9ea158ad 100644 --- a/src/bundle/Resources/views/themes/admin/ui/component/tab/tabs.html.twig +++ b/src/bundle/Resources/views/themes/admin/ui/component/tab/tabs.html.twig @@ -16,6 +16,7 @@ active_tab, hide_toggler: hide_toggler|default(false), include_tab_more: include_tab_more|default(false), + tab_corner_disabled: tab_corner_disabled|default(false), } %} {% block tabs_list_after %} {{ tabs_list_after_content }} diff --git a/src/bundle/Resources/views/themes/admin/ui/component/tab/tabs_header.html.twig b/src/bundle/Resources/views/themes/admin/ui/component/tab/tabs_header.html.twig index a323d19957..ff0131ff06 100644 --- a/src/bundle/Resources/views/themes/admin/ui/component/tab/tabs_header.html.twig +++ b/src/bundle/Resources/views/themes/admin/ui/component/tab/tabs_header.html.twig @@ -17,6 +17,7 @@ label: tab.label, active: tab == active_tab, has_error: tab.has_error|default(false), + tab_corner_disabled: tab_corner_disabled|default(false), } %} {% endfor %} {% endblock %} diff --git a/src/bundle/Resources/views/themes/admin/ui/component/tab/tabs_tab.html.twig b/src/bundle/Resources/views/themes/admin/ui/component/tab/tabs_tab.html.twig index 3d1b9bc742..42d9045721 100644 --- a/src/bundle/Resources/views/themes/admin/ui/component/tab/tabs_tab.html.twig +++ b/src/bundle/Resources/views/themes/admin/ui/component/tab/tabs_tab.html.twig @@ -10,5 +10,7 @@ label: tab.label, } %} {% endblock %} - {% include '@ibexadesign/ui/component/tab/tab_corner.html.twig' %} + {% if not tab_corner_disabled|default(false) %} + {% include '@ibexadesign/ui/component/tab/tab_corner.html.twig' %} + {% endif %} diff --git a/src/bundle/Resources/views/themes/admin/ui/layout.html.twig b/src/bundle/Resources/views/themes/admin/ui/layout.html.twig index e3ca18743f..35e0fb11eb 100644 --- a/src/bundle/Resources/views/themes/admin/ui/layout.html.twig +++ b/src/bundle/Resources/views/themes/admin/ui/layout.html.twig @@ -217,21 +217,23 @@ })|e('html_attr') }}">
- {% if not is_back_to_top_disabled|default(false) %} - {% block back_to_top %} -
- -
- {% endblock %} - {% endif %} - +
+ {% if not is_back_to_top_disabled|default(false) %} + {% block back_to_top %} +
+ +
+ {% endblock %} + {% endif %} + {{ ibexa_twig_component_group('admin-ui-quick-action-menu') }} +
{{ ibexa_twig_component_group('admin-ui-layout-content-after') }} {{ encore_entry_script_tags('ibexa-admin-ui-layout-js', null, 'ibexa') }} From 4812a81ffdf42217b9a223a3cf3cd41658d9787c Mon Sep 17 00:00:00 2001 From: Aleksandra Bozek Date: Wed, 24 Sep 2025 15:22:01 +0200 Subject: [PATCH 2/4] Code cleanup --- .../Resources/public/js/scripts/quick.action.manager.js | 4 ++-- src/bundle/Resources/public/js/scripts/sidebar/side.panel.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bundle/Resources/public/js/scripts/quick.action.manager.js b/src/bundle/Resources/public/js/scripts/quick.action.manager.js index 95b08ed28b..2e4f8f0fa6 100644 --- a/src/bundle/Resources/public/js/scripts/quick.action.manager.js +++ b/src/bundle/Resources/public/js/scripts/quick.action.manager.js @@ -14,7 +14,6 @@ registeredActionButtons = registeredActionButtons.filter((btn) => btn.name !== name); recalculateButtonsLayout(); }; - const recalculateButtonsLayout = () => { const sortedButtons = registeredActionButtons.sort((a, b) => a.priority - b.priority); const buttonsToRender = sortedButtons.filter((el) => { @@ -39,9 +38,11 @@ selector.style.zIndex = buttonConfig.zIndex || 1040; const bottomPosition = `${index === 0 ? 2 : (index + 1) * 3.2}rem`; + selector.style.bottom = bottomPosition; }); }; + return { registerButton, unregisterButton, @@ -49,6 +50,5 @@ }; })(); - window.ibexa = window.ibexa || {}; global.ibexa.adminUiConfig.quickActionManager = QuickActionManager; })(window); diff --git a/src/bundle/Resources/public/js/scripts/sidebar/side.panel.js b/src/bundle/Resources/public/js/scripts/sidebar/side.panel.js index fcb624fffe..48fa8891db 100644 --- a/src/bundle/Resources/public/js/scripts/sidebar/side.panel.js +++ b/src/bundle/Resources/public/js/scripts/sidebar/side.panel.js @@ -4,7 +4,7 @@ '.ibexa-side-panel .ibexa-btn--close, .ibexa-side-panel .ibexa-side-panel__btn--cancel', ); const sidePanelTriggers = [...doc.querySelectorAll('.ibexa-side-panel-trigger')]; - const panelBackdrops = new Map(); // Mapa przechowująca powiązania panel -> backdrop + const panelBackdrops = new Map(); const defaultBackdrop = new ibexa.core.Backdrop(); const removeBackdrop = (sidePanel) => { const backdrop = panelBackdrops.get(sidePanel) || defaultBackdrop; From 45b9413608e0b67582311f164aa850654467ce9d Mon Sep 17 00:00:00 2001 From: Aleksandra Bozek Date: Wed, 1 Oct 2025 11:58:11 +0200 Subject: [PATCH 3/4] After review --- .../public/js/scripts/admin.back.to.top.js | 13 ++- .../public/js/scripts/quick.action.manager.js | 93 +++++++++---------- .../views/themes/admin/ui/layout.html.twig | 2 +- 3 files changed, 51 insertions(+), 57 deletions(-) diff --git a/src/bundle/Resources/public/js/scripts/admin.back.to.top.js b/src/bundle/Resources/public/js/scripts/admin.back.to.top.js index efa6088cf7..763bef1484 100644 --- a/src/bundle/Resources/public/js/scripts/admin.back.to.top.js +++ b/src/bundle/Resources/public/js/scripts/admin.back.to.top.js @@ -1,4 +1,4 @@ -(function (global, doc) { +(function (global, doc, ibexa) { const backToTopBtn = doc.querySelector('.ibexa-back-to-top__btn'); const backToTop = doc.querySelector('.ibexa-back-to-top'); const backToTopAnchor = doc.querySelector('.ibexa-back-to-top-anchor'); @@ -15,7 +15,6 @@ return backToTopBtn.classList.contains('ibexa-back-to-top__btn--visible'); }; - const backToTopBtnTitle = backToTopBtn.querySelector('.ibexa-back-to-top__title'); let currentBackToTopAnchorHeight = backToTopAnchor.offsetHeight; const setBackToTopBtnTextVisibility = (container) => { @@ -28,7 +27,7 @@ if (!backToTopBtn.classList.contains('ibexa-back-to-top__btn--visible') && shouldBeVisible) { backToTopBtn.classList.add('ibexa-back-to-top__btn--visible'); - global.ibexa.adminUiConfig.quickActionManager.recalculateButtonsLayout(); + ibexa.quickAction.recalculateButtonsLayout(); } backToTopBtn.classList.toggle('ibexa-btn--no-text', !isTitleVisible); @@ -56,13 +55,13 @@ setBackToTopBtnTextVisibility(backToTopScrollContainer); }); const config = { - name: 'back-to-top', + id: 'back-to-top', zIndex: 10, - selector: backToTop, + container: backToTop, priority: 100, checkVisibility: checkIsVisible, }; - global.ibexa.adminUiConfig.quickActionManager.registerButton(config); + ibexa.quickAction.registerButton(config); resizeObserver.observe(backToTopAnchor); -})(window, window.document); +})(window, window.document, window.ibexa); diff --git a/src/bundle/Resources/public/js/scripts/quick.action.manager.js b/src/bundle/Resources/public/js/scripts/quick.action.manager.js index 2e4f8f0fa6..df1df26579 100644 --- a/src/bundle/Resources/public/js/scripts/quick.action.manager.js +++ b/src/bundle/Resources/public/js/scripts/quick.action.manager.js @@ -1,54 +1,49 @@ (function (global) { - let registeredActionButtons = []; + let actionButtonConfigs = []; + + const registerButton = (config) => { + if (!config || !config.container || actionButtonConfigs.some((btn) => btn.id === config.id)) { + return; + } + + actionButtonConfigs = [...actionButtonConfigs, config].sort((a, b) => a.priority - b.priority); + recalculateButtonsLayout(); + }; + const unregisterButton = (id) => { + actionButtonConfigs = actionButtonConfigs.filter((btn) => btn.id !== id); + recalculateButtonsLayout(); + }; + const recalculateButtonsLayout = () => { + const buttonsToRender = actionButtonConfigs.filter((btn) => { + if (typeof btn.checkVisibility === 'function') { + const isVisible = btn.checkVisibility(); + + return isVisible; + } + + return false; + }); + + buttonsToRender.forEach((buttonConfig, index) => { + const { container } = buttonConfig; - const QuickActionManager = (() => { - const registerButton = (config) => { - if (!config || !config.selector || registeredActionButtons.some((btn) => btn.name === config.name)) { - return; + if (!container.style.transition) { + container.style.transition = 'all 0.3s ease-in-out'; } - registeredActionButtons = [...registeredActionButtons, config]; - recalculateButtonsLayout(); - }; - const unregisterButton = (name) => { - registeredActionButtons = registeredActionButtons.filter((btn) => btn.name !== name); - recalculateButtonsLayout(); - }; - const recalculateButtonsLayout = () => { - const sortedButtons = registeredActionButtons.sort((a, b) => a.priority - b.priority); - const buttonsToRender = sortedButtons.filter((el) => { - if (el.checkVisibility && typeof el.checkVisibility === 'function') { - const isVisible = el.checkVisibility(); - - return isVisible; - } - - return false; - }); - - buttonsToRender.forEach((buttonConfig, index) => { - const { selector } = buttonConfig; - - if (!selector.style.transition) { - selector.style.transition = 'all 0.3s ease-in-out'; - } - - selector.style.position = 'fixed'; - selector.style.right = '2rem'; - selector.style.zIndex = buttonConfig.zIndex || 1040; - - const bottomPosition = `${index === 0 ? 2 : (index + 1) * 3.2}rem`; - - selector.style.bottom = bottomPosition; - }); - }; - - return { - registerButton, - unregisterButton, - recalculateButtonsLayout, - }; - })(); - - global.ibexa.adminUiConfig.quickActionManager = QuickActionManager; + container.style.position = 'fixed'; + container.style.right = '2rem'; + container.style.zIndex = buttonConfig.zIndex || 1040; + + const bottomPosition = `${index === 0 ? 2 : index * 3.8 + 2 + index * 0.5}rem`; + + container.style.bottom = bottomPosition; + }); + }; + + global.ibexa.quickAction = { + registerButton, + unregisterButton, + recalculateButtonsLayout, + }; })(window); diff --git a/src/bundle/Resources/views/themes/admin/ui/layout.html.twig b/src/bundle/Resources/views/themes/admin/ui/layout.html.twig index 35e0fb11eb..38bd4c4d45 100644 --- a/src/bundle/Resources/views/themes/admin/ui/layout.html.twig +++ b/src/bundle/Resources/views/themes/admin/ui/layout.html.twig @@ -220,7 +220,7 @@
{% if not is_back_to_top_disabled|default(false) %} {% block back_to_top %} -
+