From fdcd5c439ac6c7f74391490b15e0a77386ef0644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Wed, 30 Oct 2024 15:14:01 +0100 Subject: [PATCH 01/11] AI Assistant --- .../js/scripts/helpers/config.loader.js | 2 + .../public/js/scripts/helpers/react.helper.js | 21 +++ .../public/scss/ui/modules/_common.scss | 2 + .../ui/modules/common/_draggable.dialog.scss | 9 + .../scss/ui/modules/common/_popup.menu.scss | 87 +++++++++ .../draggable-dialog/draggable.dialog.js | 121 ++++++++++++ .../modules/common/popup-menu/popup.menu.js | 172 ++++++++++++++++++ 7 files changed, 414 insertions(+) create mode 100644 src/bundle/Resources/public/js/scripts/helpers/react.helper.js create mode 100644 src/bundle/Resources/public/scss/ui/modules/common/_draggable.dialog.scss create mode 100644 src/bundle/Resources/public/scss/ui/modules/common/_popup.menu.scss create mode 100644 src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js create mode 100644 src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js diff --git a/src/bundle/Resources/public/js/scripts/helpers/config.loader.js b/src/bundle/Resources/public/js/scripts/helpers/config.loader.js index 2c2f3ff9b2..821121f1bf 100644 --- a/src/bundle/Resources/public/js/scripts/helpers/config.loader.js +++ b/src/bundle/Resources/public/js/scripts/helpers/config.loader.js @@ -12,6 +12,7 @@ import * as modal from './modal.helper'; import * as notification from './notification.helper'; import * as objectInstances from './object.instances'; import * as pagination from './pagination.helper'; +import * as react from './react.helper'; import * as request from './request.helper'; import * as system from './system.helper'; import * as table from './table.helper'; @@ -36,6 +37,7 @@ import * as user from './user.helper'; ibexa.addConfig('helpers.notification', notification); ibexa.addConfig('helpers.objectInstances', objectInstances); ibexa.addConfig('helpers.pagination', pagination); + ibexa.addConfig('helpers.react', react); ibexa.addConfig('helpers.request', request); ibexa.addConfig('helpers.system', system); ibexa.addConfig('helpers.table', table); diff --git a/src/bundle/Resources/public/js/scripts/helpers/react.helper.js b/src/bundle/Resources/public/js/scripts/helpers/react.helper.js new file mode 100644 index 0000000000..f3b7746c29 --- /dev/null +++ b/src/bundle/Resources/public/js/scripts/helpers/react.helper.js @@ -0,0 +1,21 @@ +const createDynamicRoot = (contextDOMElement = window.document.body, id) => { + const rootDOMElement = document.createElement('div'); + + rootDOMElement.classList.add('ibexa-react-root'); + + if (id) { + rootDOMElement.id = id; + } + + contextDOMElement.appendChild(rootDOMElement); + + const reactRoot = window.ReactDOM.createRoot(rootDOMElement); + + return { reactRoot, rootDOMElement }; +}; + +const removeDynamicRoot = (rootDOMElement) => { + rootDOMElement.remove(); +}; + +export { createDynamicRoot, removeDynamicRoot }; diff --git a/src/bundle/Resources/public/scss/ui/modules/_common.scss b/src/bundle/Resources/public/scss/ui/modules/_common.scss index d916cc360d..e9eee25a80 100644 --- a/src/bundle/Resources/public/scss/ui/modules/_common.scss +++ b/src/bundle/Resources/public/scss/ui/modules/_common.scss @@ -5,3 +5,5 @@ @import 'common/user.name'; @import 'common/taggify'; @import 'common/spinner'; +@import 'common/draggable.dialog'; +@import 'common/popup.menu'; diff --git a/src/bundle/Resources/public/scss/ui/modules/common/_draggable.dialog.scss b/src/bundle/Resources/public/scss/ui/modules/common/_draggable.dialog.scss new file mode 100644 index 0000000000..8aaf740a99 --- /dev/null +++ b/src/bundle/Resources/public/scss/ui/modules/common/_draggable.dialog.scss @@ -0,0 +1,9 @@ +.c-draggable-dialog { + position: fixed; + z-index: 10000; + + &__draggable { + cursor: grab; + user-select: none; + } +} diff --git a/src/bundle/Resources/public/scss/ui/modules/common/_popup.menu.scss b/src/bundle/Resources/public/scss/ui/modules/common/_popup.menu.scss new file mode 100644 index 0000000000..773aeea498 --- /dev/null +++ b/src/bundle/Resources/public/scss/ui/modules/common/_popup.menu.scss @@ -0,0 +1,87 @@ +.c-popup-menu { + display: none; + flex-direction: column; + gap: calculateRem(1px); + padding: calculateRem(8px) 0; + background: $ibexa-color-white; + border: calculateRem(1px) solid $ibexa-color-light; + border-radius: $ibexa-border-radius; + box-shadow: calculateRem(4px) calculateRem(22px) calculateRem(67px) 0 rgba($ibexa-color-info, 0.2); + position: fixed; + z-index: 1060; + + &--visible { + display: flex; + } + + &__search { + margin-bottom: calculateRem(4px); + padding: 0 calculateRem(8px); + + &--hidden { + display: none; + } + } + + &__search-input { + border-radius: $ibexa-border-radius; + } + + &__groups { + max-height: calculateRem(390px); + overflow-y: auto; + } + + &__group:not(:last-child) { + &::after { + content: ''; + border-top: calculateRem(1px) solid $ibexa-color-light; + display: flex; + width: calc(100% - calculateRem(16px)); + margin: calculateRem(1px) calculateRem(8px) 0; + } + } + + &__item { + display: flex; + align-items: center; + min-width: calculateRem(150px); + padding: 0 calculateRem(8px); + transition: all $ibexa-admin-transition-duration $ibexa-admin-transition; + } + + &__item-content { + position: relative; + display: flex; + align-items: center; + align-items: baseline; + width: 100%; + cursor: pointer; + padding: calculateRem(9px); + color: $ibexa-color-dark; + font-size: calculateRem(14px); + text-align: left; + text-decoration: none; + border: none; + border-radius: $ibexa-border-radius; + transition: all $ibexa-admin-transition-duration $ibexa-admin-transition; + + &:hover { + background-color: $ibexa-color-light-300; + color: $ibexa-color-black; + text-decoration: none; + } + + &[disabled], + &:disabled, + &--disabled { + pointer-events: none; + cursor: not-allowed; + opacity: 0.2; + + &:hover { + background-color: initial; + } + } + } +} diff --git a/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js b/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js new file mode 100644 index 0000000000..21f3a21ade --- /dev/null +++ b/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js @@ -0,0 +1,121 @@ +import React, { useRef, createContext, useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; + +import { getRootDOMElement } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; +import { createCssClassNames } from '../helpers/css.class.names'; + +export const DraggableContext = createContext(); + +const DraggableDialog = ({ children, initialCoords }) => { + const rootDOMElement = getRootDOMElement(); + const containerRef = useRef(); + const dragOffsetPosition = useRef({ x: 0, y: 0 }); + const containerSize = useRef({ width: 0, height: 0 }); + const [isDragging, setIsDragging] = useState(false); + const [coords, setCoords] = useState(initialCoords); + const containerAttrs = { + ref: containerRef, + className: 'c-draggable-dialog', + style: { + top: coords.y, + left: coords.x, + }, + }; + const getMousePosition = (event) => ({ x: event.x, y: event.y }); + const setContainerCoords = (event) => { + const mouseCoords = getMousePosition(event); + let x = mouseCoords.x - dragOffsetPosition.current.x; + let y = mouseCoords.y - dragOffsetPosition.current.y; + let newDragOffsetX; + let newDragOffsetY; + + if (x < 0) { + x = 0; + newDragOffsetX = mouseCoords.x; + } else if (x + containerSize.current.width > window.innerWidth) { + x = window.innerWidth - containerSize.current.width; + newDragOffsetX = mouseCoords.x - x; + } + + if (y < 0) { + y = 0; + newDragOffsetY = mouseCoords.y; + } else if (y + containerSize.current.height > window.innerHeight) { + y = window.innerHeight - containerSize.current.height; + newDragOffsetY = mouseCoords.y - y; + } + + if (newDragOffsetX) { + dragOffsetPosition.current.x = newDragOffsetX; + } + + if (newDragOffsetY) { + dragOffsetPosition.current.y = newDragOffsetY; + } + + setCoords({ + x, + y, + }); + }; + const startDragging = (event) => { + const { x: containerX, y: containerY, width, height } = containerRef.current.getBoundingClientRect(); + const mouseCoords = getMousePosition(event.nativeEvent); + + dragOffsetPosition.current = { + x: mouseCoords.x - containerX, + y: mouseCoords.y - containerY, + }; + + containerSize.current = { + width, + height, + }; + + setContainerCoords(event.nativeEvent); + + setIsDragging(true); + }; + const stopDragging = () => { + setIsDragging(false); + }; + const handleDragging = (event) => { + setContainerCoords(event); + }; + + useEffect(() => { + if (isDragging) { + rootDOMElement.addEventListener('mousemove', handleDragging); + rootDOMElement.addEventListener('mouseup', stopDragging); + } + + return () => { + rootDOMElement.removeEventListener('mousemove', handleDragging); + rootDOMElement.removeEventListener('mouseup', stopDragging); + }; + }, [isDragging]); + + return ( + +
{children}
+
+ ); +}; + +DraggableDialog.propTypes = { + initialCoords: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + }).isRequired, + children: PropTypes.node, +}; + +DraggableDialog.defaultProps = { + children: null, +}; + +export default DraggableDialog; diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js new file mode 100644 index 0000000000..f7c8ae6d51 --- /dev/null +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js @@ -0,0 +1,172 @@ +import React, { useState, useEffect, useRef, useLayoutEffect } from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; + +import { getRootDOMElement, getTranslator } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; +import Icon from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/common/icon/icon'; + +import { createCssClassNames } from '../helpers/css.class.names'; +// import Icon from '../icon/icon'; +// import { getTranslator } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; + +const { document } = window; +const MIN_SEARCH_ITEMS_DEFAULT = 5; +const MIN_ITEMS_LIST_HEIGHT = 150; +const ITEMS_LIST_WIDGET_MARGIN = 8; +const ITEMS_LIST_SITE_MARGIN = ITEMS_LIST_WIDGET_MARGIN + 4; +const RESTRICTED_AREA_ITEMS_CONTAINER = 190; + +const PopupMenu = ({ positionOffset, items, scrollContainer, referenceElement, onItemClick }) => { + const rootDOMElement = getRootDOMElement(); + const Translator = getTranslator(); + const containerRef = useRef(); + const [isVisible, setIsVisible] = useState(true); + const [itemsListStyles, setItemsListStyles] = useState({ + left: 0, + top: 0, + }); + const [filterText, setFilterText] = useState(''); + const popupMenuClassName = createCssClassNames({ + 'c-popup-menu': true, + 'c-popup-menu--visible': isVisible, + }); + const searchPlaceholder = Translator.trans(/*@Desc("Search...")*/ 'popup_menu.placeholder', {}, 'popup_menu'); + const updateFilterValue = (event) => setFilterText(event.target.value); + const resetInputValue = () => setFilterText(''); + const showItem = (item) => { + if (filterText.length < 3) { + return true; + } + + const itemLabelLowerCase = item.label.toLowerCase(); + const filterTextLowerCase = filterText.toLowerCase(); + + return itemLabelLowerCase.indexOf(filterTextLowerCase) === 0; + }; + const renderGroup = (group) => { + const groupClassName = createCssClassNames({ + 'c-popup-menu__group': true, + }); + + return
{group.items.map(renderItem)}
; + }; + const renderItem = (item) => { + if (!showItem(item)) { + return null; + } + + const itemClassName = createCssClassNames({ + 'c-popup-menu__item': true, + }); + + return ( +
+ +
+ ); + }; + const calculateAndSetItemsListStyles = () => { + const itemsStyles = {}; + const { top: referenceTop, left: referenceLeft } = referenceElement.getBoundingClientRect(); + const { height: containerHeight } = containerRef.current.getBoundingClientRect(); + const bottom = referenceTop + containerHeight; + + itemsStyles.top = referenceTop; + + if (window.innerHeight - bottom > MIN_ITEMS_LIST_HEIGHT) { + const { x: offsetX, y: offsetY } = positionOffset(referenceElement, 'bottom'); + + itemsStyles.top = referenceTop + offsetY; + itemsStyles.left = referenceLeft + offsetX; + itemsStyles.maxHeight = window.innerHeight - bottom; + } else { + const { x: offsetX, y: offsetY } = positionOffset(referenceElement, 'top'); + + console.log(offsetY); + + itemsStyles.top = referenceTop + offsetY; + itemsStyles.left = referenceLeft + offsetX; + itemsStyles.maxHeight = referenceTop; + itemsStyles.transform = 'translateY(-100%)'; + } + + setItemsListStyles(itemsStyles); + }; + + useEffect(() => { + calculateAndSetItemsListStyles(); + + if (!isVisible) { + return; + } + + const onInteractionOutside = (event) => { + if (containerRef.current.contains(event.target) || referenceElement.contains(event.target)) { + return; + } + + setIsVisible(false); + }; + + window.document.body.addEventListener('click', onInteractionOutside, false); + scrollContainer.addEventListener('scroll', calculateAndSetItemsListStyles, false); + + return () => { + window.document.body.removeEventListener('click', onInteractionOutside); + scrollContainer.removeEventListener('scroll', calculateAndSetItemsListStyles); + + setItemsListStyles({}); + }; + }, [isVisible]); + + return ( +
+
+
+ +
+ + +
+
+
+
{items.map(renderGroup)}
+
+ ); +}; + +PopupMenu.propTypes = { + referenceElement: PropTypes.isRequired, + positionOffset: PropTypes.func, + scrollContainer: PropTypes.node, + onItemClick: PropTypes.func, +}; + +PopupMenu.defaultProps = { + positionOffset: () => ({ x: 0, y: 0 }), + scrollContainer: window.document.body, + onItemClick: () => {}, +}; + +export default PopupMenu; From f3d997a9fbdd8bf515b73f5503234a6d61e1cb01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Wed, 30 Oct 2024 16:19:37 +0100 Subject: [PATCH 02/11] footer --- .../src/modules/common/popup-menu/popup.menu.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js index f7c8ae6d51..87479271bc 100644 --- a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js @@ -16,7 +16,7 @@ const ITEMS_LIST_WIDGET_MARGIN = 8; const ITEMS_LIST_SITE_MARGIN = ITEMS_LIST_WIDGET_MARGIN + 4; const RESTRICTED_AREA_ITEMS_CONTAINER = 190; -const PopupMenu = ({ positionOffset, items, scrollContainer, referenceElement, onItemClick }) => { +const PopupMenu = ({ positionOffset, items, scrollContainer, referenceElement, onItemClick, footer, extraClasses }) => { const rootDOMElement = getRootDOMElement(); const Translator = getTranslator(); const containerRef = useRef(); @@ -29,6 +29,7 @@ const PopupMenu = ({ positionOffset, items, scrollContainer, referenceElement, o const popupMenuClassName = createCssClassNames({ 'c-popup-menu': true, 'c-popup-menu--visible': isVisible, + [extraClasses]: true, }); const searchPlaceholder = Translator.trans(/*@Desc("Search...")*/ 'popup_menu.placeholder', {}, 'popup_menu'); const updateFilterValue = (event) => setFilterText(event.target.value); @@ -152,6 +153,11 @@ const PopupMenu = ({ positionOffset, items, scrollContainer, referenceElement, o
{items.map(renderGroup)}
+ { footer && ( + + ) } ); }; @@ -161,12 +167,16 @@ PopupMenu.propTypes = { positionOffset: PropTypes.func, scrollContainer: PropTypes.node, onItemClick: PropTypes.func, + footer: PropTypes.node, + extraClasses: PropTypes.string, }; PopupMenu.defaultProps = { positionOffset: () => ({ x: 0, y: 0 }), scrollContainer: window.document.body, onItemClick: () => {}, + footer: null, + extraClasses: '', }; export default PopupMenu; From 6c085470cc9fe5ef4b0aab8bd6181ea33b61e1ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Thu, 31 Oct 2024 11:56:28 +0100 Subject: [PATCH 03/11] improve search; fix linter --- .../modules/common/popup-menu/popup.menu.js | 121 ++++++++++-------- 1 file changed, 68 insertions(+), 53 deletions(-) diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js index 87479271bc..77ace73270 100644 --- a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js @@ -1,23 +1,14 @@ -import React, { useState, useEffect, useRef, useLayoutEffect } from 'react'; -import ReactDOM from 'react-dom'; +import React, { useState, useEffect, useRef, useMemo } from 'react'; import PropTypes from 'prop-types'; -import { getRootDOMElement, getTranslator } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; +import { getTranslator } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; +import { createCssClassNames } from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/common/helpers/css.class.names'; import Icon from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/common/icon/icon'; -import { createCssClassNames } from '../helpers/css.class.names'; -// import Icon from '../icon/icon'; -// import { getTranslator } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; - -const { document } = window; const MIN_SEARCH_ITEMS_DEFAULT = 5; const MIN_ITEMS_LIST_HEIGHT = 150; -const ITEMS_LIST_WIDGET_MARGIN = 8; -const ITEMS_LIST_SITE_MARGIN = ITEMS_LIST_WIDGET_MARGIN + 4; -const RESTRICTED_AREA_ITEMS_CONTAINER = 190; -const PopupMenu = ({ positionOffset, items, scrollContainer, referenceElement, onItemClick, footer, extraClasses }) => { - const rootDOMElement = getRootDOMElement(); +const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, referenceElement, scrollContainer }) => { const Translator = getTranslator(); const containerRef = useRef(); const [isVisible, setIsVisible] = useState(true); @@ -26,6 +17,7 @@ const PopupMenu = ({ positionOffset, items, scrollContainer, referenceElement, o top: 0, }); const [filterText, setFilterText] = useState(''); + const numberOfItems = useMemo(() => items.reduce((sum, group) => sum + group.items.length, 0), [items]); const popupMenuClassName = createCssClassNames({ 'c-popup-menu': true, 'c-popup-menu--visible': isVisible, @@ -45,6 +37,12 @@ const PopupMenu = ({ positionOffset, items, scrollContainer, referenceElement, o return itemLabelLowerCase.indexOf(filterTextLowerCase) === 0; }; const renderGroup = (group) => { + const isAnyItemVisible = group.items.some(showItem); + + if (!isAnyItemVisible) { + return null; + } + const groupClassName = createCssClassNames({ 'c-popup-menu__group': true, }); @@ -85,8 +83,6 @@ const PopupMenu = ({ positionOffset, items, scrollContainer, referenceElement, o } else { const { x: offsetX, y: offsetY } = positionOffset(referenceElement, 'top'); - console.log(offsetY); - itemsStyles.top = referenceTop + offsetY; itemsStyles.left = referenceLeft + offsetX; itemsStyles.maxHeight = referenceTop; @@ -95,35 +91,12 @@ const PopupMenu = ({ positionOffset, items, scrollContainer, referenceElement, o setItemsListStyles(itemsStyles); }; - - useEffect(() => { - calculateAndSetItemsListStyles(); - - if (!isVisible) { - return; + const renderSearch = () => { + if (numberOfItems < MIN_SEARCH_ITEMS_DEFAULT) { + return null; } - const onInteractionOutside = (event) => { - if (containerRef.current.contains(event.target) || referenceElement.contains(event.target)) { - return; - } - - setIsVisible(false); - }; - - window.document.body.addEventListener('click', onInteractionOutside, false); - scrollContainer.addEventListener('scroll', calculateAndSetItemsListStyles, false); - - return () => { - window.document.body.removeEventListener('click', onInteractionOutside); - scrollContainer.removeEventListener('scroll', calculateAndSetItemsListStyles); - - setItemsListStyles({}); - }; - }, [isVisible]); - - return ( -
+ return (
+ ); + }; + const renderFooter = () => { + if (!footer) { + return null; + } + + return
{footer}
; + }; + + useEffect(() => { + calculateAndSetItemsListStyles(); + + if (!isVisible) { + return; + } + + const onInteractionOutside = (event) => { + if (containerRef.current.contains(event.target) || referenceElement.contains(event.target)) { + return; + } + + setIsVisible(false); + }; + + window.document.body.addEventListener('click', onInteractionOutside, false); + scrollContainer.addEventListener('scroll', calculateAndSetItemsListStyles, false); + + return () => { + window.document.body.removeEventListener('click', onInteractionOutside); + scrollContainer.removeEventListener('scroll', calculateAndSetItemsListStyles); + + setItemsListStyles({}); + }; + }, [isVisible]); + + return ( +
+ {renderSearch()}
{items.map(renderGroup)}
- { footer && ( - - ) } + {renderFooter()}
); }; PopupMenu.propTypes = { referenceElement: PropTypes.isRequired, + extraClasses: PropTypes.string, + footer: PropTypes.node, + items: PropTypes.arrayOf({ + items: PropTypes.shape({ + value: PropTypes.oneOf([PropTypes.string, PropTypes.number]), + label: PropTypes.string, + }), + }), + onItemClick: PropTypes.func, positionOffset: PropTypes.func, scrollContainer: PropTypes.node, - onItemClick: PropTypes.func, - footer: PropTypes.node, - extraClasses: PropTypes.string, }; PopupMenu.defaultProps = { + extraClasses: '', + footer: null, + items: [], + onItemClick: () => {}, positionOffset: () => ({ x: 0, y: 0 }), scrollContainer: window.document.body, - onItemClick: () => {}, - footer: null, - extraClasses: '', }; export default PopupMenu; From 077ed1bc3628fa08eeba4815655ee222294f10eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Thu, 31 Oct 2024 16:10:36 +0100 Subject: [PATCH 04/11] dialog hook --- .../public/js/scripts/helpers/react.helper.js | 9 +++++--- .../translations/popup_menu.en.xliff | 16 ++++++++++++++ .../draggable-dialog/draggable.dialog.js | 21 +++++++++++++------ .../modules/common/popup-menu/popup.menu.js | 4 +--- 4 files changed, 38 insertions(+), 12 deletions(-) create mode 100644 src/bundle/Resources/translations/popup_menu.en.xliff diff --git a/src/bundle/Resources/public/js/scripts/helpers/react.helper.js b/src/bundle/Resources/public/js/scripts/helpers/react.helper.js index f3b7746c29..7b32f1f85b 100644 --- a/src/bundle/Resources/public/js/scripts/helpers/react.helper.js +++ b/src/bundle/Resources/public/js/scripts/helpers/react.helper.js @@ -11,11 +11,14 @@ const createDynamicRoot = (contextDOMElement = window.document.body, id) => { const reactRoot = window.ReactDOM.createRoot(rootDOMElement); - return { reactRoot, rootDOMElement }; + return reactRoot; }; -const removeDynamicRoot = (rootDOMElement) => { - rootDOMElement.remove(); +const removeDynamicRoot = (reactRoot) => { + const rootDOMElement = reactRoot._internalRoot?.containerInfo; + + reactRoot.unmount(); + rootDOMElement?.remove(); }; export { createDynamicRoot, removeDynamicRoot }; diff --git a/src/bundle/Resources/translations/popup_menu.en.xliff b/src/bundle/Resources/translations/popup_menu.en.xliff new file mode 100644 index 0000000000..fe8e1a80a8 --- /dev/null +++ b/src/bundle/Resources/translations/popup_menu.en.xliff @@ -0,0 +1,16 @@ + + + +
+ + The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message. +
+ + + Search... + Search... + key: popup_menu.placeholder + + +
+
diff --git a/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js b/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js index 21f3a21ade..a1550d0967 100644 --- a/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js +++ b/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js @@ -6,13 +6,13 @@ import { createCssClassNames } from '../helpers/css.class.names'; export const DraggableContext = createContext(); -const DraggableDialog = ({ children, initialCoords }) => { +const DraggableDialog = ({ children, referenceElement, positionOffset }) => { const rootDOMElement = getRootDOMElement(); const containerRef = useRef(); const dragOffsetPosition = useRef({ x: 0, y: 0 }); const containerSize = useRef({ width: 0, height: 0 }); const [isDragging, setIsDragging] = useState(false); - const [coords, setCoords] = useState(initialCoords); + const [coords, setCoords] = useState({ x: 0, y: 0 }); const containerAttrs = { ref: containerRef, className: 'c-draggable-dialog', @@ -95,6 +95,16 @@ const DraggableDialog = ({ children, initialCoords }) => { }; }, [isDragging]); + useEffect(() => { + const { top: referenceTop, left: referenceLeft } = referenceElement.getBoundingClientRect(); + const { x: offsetX, y: offsetY } = positionOffset(referenceElement); + + setCoords({ + top: referenceTop + offsetY, + left: referenceLeft + offsetX, + }); + }, [referenceElement]); + return ( { }; DraggableDialog.propTypes = { - initialCoords: PropTypes.shape({ - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired, - }).isRequired, + referenceElement: PropTypes.node.isRequired, children: PropTypes.node, + positionOffset: PropTypes.func, }; DraggableDialog.defaultProps = { children: null, + positionOffset: () => ({ x: 0, y: 0 }), }; export default DraggableDialog; diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js index 77ace73270..ca20ee0793 100644 --- a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js @@ -72,8 +72,6 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r const { height: containerHeight } = containerRef.current.getBoundingClientRect(); const bottom = referenceTop + containerHeight; - itemsStyles.top = referenceTop; - if (window.innerHeight - bottom > MIN_ITEMS_LIST_HEIGHT) { const { x: offsetX, y: offsetY } = positionOffset(referenceElement, 'bottom'); @@ -171,7 +169,7 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r }; PopupMenu.propTypes = { - referenceElement: PropTypes.isRequired, + referenceElement: PropTypes.node.isRequired, extraClasses: PropTypes.string, footer: PropTypes.node, items: PropTypes.arrayOf({ From 2ac20f74ebba9cda44a3cd73fd98a5f8bc18b841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Tue, 5 Nov 2024 17:22:11 +0100 Subject: [PATCH 05/11] translations --- .../Resources/config/bazinga_js_translation.yaml | 1 + .../translations/ibexa_popup_menu.en.xliff | 16 ++++++++++++++++ .../Resources/translations/popup_menu.en.xliff | 4 ++-- .../common/draggable-dialog/draggable.dialog.js | 1 - .../src/modules/common/popup-menu/popup.menu.js | 2 +- 5 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 src/bundle/Resources/translations/ibexa_popup_menu.en.xliff diff --git a/src/bundle/Resources/config/bazinga_js_translation.yaml b/src/bundle/Resources/config/bazinga_js_translation.yaml index 762dbc83ef..ad62c5d39e 100644 --- a/src/bundle/Resources/config/bazinga_js_translation.yaml +++ b/src/bundle/Resources/config/bazinga_js_translation.yaml @@ -12,4 +12,5 @@ active_domains: - 'ibexa_user_invitation' - 'ibexa_content_type' - 'ibexa_dropdown' + - 'ibexa_popup_menu' - 'messages' diff --git a/src/bundle/Resources/translations/ibexa_popup_menu.en.xliff b/src/bundle/Resources/translations/ibexa_popup_menu.en.xliff new file mode 100644 index 0000000000..02053674b7 --- /dev/null +++ b/src/bundle/Resources/translations/ibexa_popup_menu.en.xliff @@ -0,0 +1,16 @@ + + + +
+ + The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message. +
+ + + Search... + Search... + key: ibexa_popup_menu.search.placeholder + + +
+
diff --git a/src/bundle/Resources/translations/popup_menu.en.xliff b/src/bundle/Resources/translations/popup_menu.en.xliff index fe8e1a80a8..25e0baa413 100644 --- a/src/bundle/Resources/translations/popup_menu.en.xliff +++ b/src/bundle/Resources/translations/popup_menu.en.xliff @@ -6,10 +6,10 @@ The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message. - + Search... Search... - key: popup_menu.placeholder + key: popup_menu.search.placeholder diff --git a/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js b/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js index a1550d0967..2e6384992e 100644 --- a/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js +++ b/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js @@ -2,7 +2,6 @@ import React, { useRef, createContext, useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { getRootDOMElement } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; -import { createCssClassNames } from '../helpers/css.class.names'; export const DraggableContext = createContext(); diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js index ca20ee0793..b172d79dd4 100644 --- a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js @@ -23,7 +23,7 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r 'c-popup-menu--visible': isVisible, [extraClasses]: true, }); - const searchPlaceholder = Translator.trans(/*@Desc("Search...")*/ 'popup_menu.placeholder', {}, 'popup_menu'); + const searchPlaceholder = Translator.trans(/*@Desc("Search...")*/ 'ibexa_popup_menu.search.placeholder', {}, 'ibexa_popup_menu'); const updateFilterValue = (event) => setFilterText(event.target.value); const resetInputValue = () => setFilterText(''); const showItem = (item) => { From 40b5c007ef496613fec7fb9b572cf0b83ec3573c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Thu, 7 Nov 2024 17:30:48 +0100 Subject: [PATCH 06/11] minor fixes --- .../ui/modules/common/_draggable.dialog.scss | 4 +++ .../scss/ui/modules/common/_popup.menu.scss | 6 ++-- .../translations/popup_menu.en.xliff | 16 ----------- .../themes/admin/ui/form_fields.html.twig | 5 ++++ .../draggable-dialog/draggable.dialog.js | 28 ++++++++++++++++--- .../modules/common/popup-menu/popup.menu.js | 21 +++++++------- 6 files changed, 46 insertions(+), 34 deletions(-) delete mode 100644 src/bundle/Resources/translations/popup_menu.en.xliff diff --git a/src/bundle/Resources/public/scss/ui/modules/common/_draggable.dialog.scss b/src/bundle/Resources/public/scss/ui/modules/common/_draggable.dialog.scss index 8aaf740a99..9398d4a219 100644 --- a/src/bundle/Resources/public/scss/ui/modules/common/_draggable.dialog.scss +++ b/src/bundle/Resources/public/scss/ui/modules/common/_draggable.dialog.scss @@ -2,6 +2,10 @@ position: fixed; z-index: 10000; + &--hidden { + visibility: hidden; + } + &__draggable { cursor: grab; user-select: none; diff --git a/src/bundle/Resources/public/scss/ui/modules/common/_popup.menu.scss b/src/bundle/Resources/public/scss/ui/modules/common/_popup.menu.scss index 773aeea498..72ca05fad2 100644 --- a/src/bundle/Resources/public/scss/ui/modules/common/_popup.menu.scss +++ b/src/bundle/Resources/public/scss/ui/modules/common/_popup.menu.scss @@ -1,5 +1,5 @@ .c-popup-menu { - display: none; + display: flex; flex-direction: column; gap: calculateRem(1px); padding: calculateRem(8px) 0; @@ -10,8 +10,8 @@ position: fixed; z-index: 1060; - &--visible { - display: flex; + &--hidden { + visibility: hidden; } &__search { diff --git a/src/bundle/Resources/translations/popup_menu.en.xliff b/src/bundle/Resources/translations/popup_menu.en.xliff deleted file mode 100644 index 25e0baa413..0000000000 --- a/src/bundle/Resources/translations/popup_menu.en.xliff +++ /dev/null @@ -1,16 +0,0 @@ - - - -
- - The source node in most cases contains the sample message as written by the developer. If it looks like a dot-delimitted string such as "form.label.firstname", then the developer has not provided a default message. -
- - - Search... - Search... - key: popup_menu.search.placeholder - - -
-
diff --git a/src/bundle/Resources/views/themes/admin/ui/form_fields.html.twig b/src/bundle/Resources/views/themes/admin/ui/form_fields.html.twig index cc7adcf1ef..639a3ee78f 100644 --- a/src/bundle/Resources/views/themes/admin/ui/form_fields.html.twig +++ b/src/bundle/Resources/views/themes/admin/ui/form_fields.html.twig @@ -399,6 +399,11 @@ {% block content %} {{ input_html }} {% endblock %} + + {% block actions %} + {{ parent() }} + {{ extra_actions_after|default()}} + {% endblock %} {%- endembed -%} {%- else -%} {{ parent() }} diff --git a/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js b/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js index 2e6384992e..8f4f3a8afc 100644 --- a/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js +++ b/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js @@ -2,6 +2,7 @@ import React, { useRef, createContext, useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { getRootDOMElement } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; +import { createCssClassNames } from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/common/helpers/css.class.names'; export const DraggableContext = createContext(); @@ -11,10 +12,14 @@ const DraggableDialog = ({ children, referenceElement, positionOffset }) => { const dragOffsetPosition = useRef({ x: 0, y: 0 }); const containerSize = useRef({ width: 0, height: 0 }); const [isDragging, setIsDragging] = useState(false); - const [coords, setCoords] = useState({ x: 0, y: 0 }); + const [coords, setCoords] = useState({ x: null, y: null }); + const dialogClasses = createCssClassNames({ + 'c-draggable-dialog': true, + 'c-draggable-dialog--hidden': coords.x === null || coords.y === null, + }); const containerAttrs = { ref: containerRef, - className: 'c-draggable-dialog', + className: dialogClasses, style: { top: coords.y, left: coords.x, @@ -96,11 +101,26 @@ const DraggableDialog = ({ children, referenceElement, positionOffset }) => { useEffect(() => { const { top: referenceTop, left: referenceLeft } = referenceElement.getBoundingClientRect(); + const { width: containerWidth, height: containerHeight } = containerRef.current.getBoundingClientRect(); const { x: offsetX, y: offsetY } = positionOffset(referenceElement); + let x = referenceLeft + offsetX; + let y = referenceTop + offsetY; + + if (x < 0) { + x = 0; + } else if (x + containerWidth > window.innerWidth) { + x = window.innerWidth - containerWidth; + } + + if (y < 0) { + y = 0; + } else if (y + containerHeight > window.innerHeight) { + y = window.innerHeight - containerHeight; + } setCoords({ - top: referenceTop + offsetY, - left: referenceLeft + offsetX, + x, + y, }); }, [referenceElement]); diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js index b172d79dd4..a774e29cc3 100644 --- a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js @@ -8,10 +8,10 @@ import Icon from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/common/icon/icon const MIN_SEARCH_ITEMS_DEFAULT = 5; const MIN_ITEMS_LIST_HEIGHT = 150; -const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, referenceElement, scrollContainer }) => { +const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, referenceElement, scrollContainer, onClose }) => { const Translator = getTranslator(); const containerRef = useRef(); - const [isVisible, setIsVisible] = useState(true); + const [isRendered, setIsRendered] = useState(false); const [itemsListStyles, setItemsListStyles] = useState({ left: 0, top: 0, @@ -20,7 +20,7 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r const numberOfItems = useMemo(() => items.reduce((sum, group) => sum + group.items.length, 0), [items]); const popupMenuClassName = createCssClassNames({ 'c-popup-menu': true, - 'c-popup-menu--visible': isVisible, + 'c-popup-menu--hidden': !isRendered, [extraClasses]: true, }); const searchPlaceholder = Translator.trans(/*@Desc("Search...")*/ 'ibexa_popup_menu.search.placeholder', {}, 'ibexa_popup_menu'); @@ -77,13 +77,13 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r itemsStyles.top = referenceTop + offsetY; itemsStyles.left = referenceLeft + offsetX; - itemsStyles.maxHeight = window.innerHeight - bottom; + itemsStyles.maxHeight = window.innerHeight - itemsStyles.top; } else { const { x: offsetX, y: offsetY } = positionOffset(referenceElement, 'top'); itemsStyles.top = referenceTop + offsetY; itemsStyles.left = referenceLeft + offsetX; - itemsStyles.maxHeight = referenceTop; + itemsStyles.maxHeight = itemsStyles.top; itemsStyles.transform = 'translateY(-100%)'; } @@ -135,17 +135,14 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r useEffect(() => { calculateAndSetItemsListStyles(); - - if (!isVisible) { - return; - } + setIsRendered(true); const onInteractionOutside = (event) => { if (containerRef.current.contains(event.target) || referenceElement.contains(event.target)) { return; } - setIsVisible(false); + onClose(); }; window.document.body.addEventListener('click', onInteractionOutside, false); @@ -157,7 +154,7 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r setItemsListStyles({}); }; - }, [isVisible]); + }, [onClose, scrollContainer]); return (
@@ -178,6 +175,7 @@ PopupMenu.propTypes = { label: PropTypes.string, }), }), + onClose: PropTypes.func, onItemClick: PropTypes.func, positionOffset: PropTypes.func, scrollContainer: PropTypes.node, @@ -187,6 +185,7 @@ PopupMenu.defaultProps = { extraClasses: '', footer: null, items: [], + onClose: () => {}, onItemClick: () => {}, positionOffset: () => ({ x: 0, y: 0 }), scrollContainer: window.document.body, From eb53194af90a2842b0b693fd58cabee1f49128eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Mon, 18 Nov 2024 18:08:49 +0100 Subject: [PATCH 07/11] recalc actions width --- .../public/js/scripts/admin.input.text.js | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/src/bundle/Resources/public/js/scripts/admin.input.text.js b/src/bundle/Resources/public/js/scripts/admin.input.text.js index 826c294855..45e240176d 100644 --- a/src/bundle/Resources/public/js/scripts/admin.input.text.js +++ b/src/bundle/Resources/public/js/scripts/admin.input.text.js @@ -1,4 +1,5 @@ (function (global, doc) { + const INPUT_PADDING = 12; const togglePasswordVisibility = (event) => { const passwordTogglerBtn = event.currentTarget; const passwordShowIcon = passwordTogglerBtn.querySelector('.ibexa-input-text-wrapper__password-show'); @@ -40,35 +41,27 @@ passwordTogglerBtns.forEach((passwordTogglerBtn) => passwordTogglerBtn.addEventListener('click', togglePasswordVisibility, false)); recalculateStyling(); }; - const handleInputChange = ({ target: { value } }, btn) => { - btn.disabled = value.trim() === ''; - }; - const recalculateStyling = () => { - const extraBtns = doc.querySelectorAll('.ibexa-input-text-wrapper__action-btn--extra-btn'); - - extraBtns.forEach((btn) => { - const input = btn.closest('.ibexa-input-text-wrapper').querySelector('input'); - const clearButton = btn.previousElementSibling?.classList.contains('ibexa-input-text-wrapper__action-btn--clear') - ? btn.previousElementSibling - : null; + const recalculateInputStyling = (inputActionsContainer) => { + const input = inputActionsContainer.closest('.ibexa-input-text-wrapper').querySelector('input'); - if (!input) { - return; - } + if (!input) { + return; + } - btn.disabled = !input.value; - input.addEventListener('input', (inputEvent) => handleInputChange(inputEvent, btn), false); + const { width: actionsWidth } = inputActionsContainer.getBoundingClientRect(); - if (!clearButton) { - return; - } + input.style.paddingRight = `${actionsWidth + INPUT_PADDING}px`; + }; + const recalculateStyling = () => { + const inputActionsContainers = doc.querySelectorAll('.ibexa-input-text-wrapper__actions'); - const clearButtonStyles = global.getComputedStyle(clearButton); - const clearButtonMarginRight = parseInt(clearButtonStyles.getPropertyValue('margin-right'), 10); - const clearButtonWidth = parseInt(clearButtonStyles.getPropertyValue('width'), 10); - const paddingRight = `${btn.offsetWidth + clearButtonMarginRight + clearButtonWidth}px`; + inputActionsContainers.forEach((inputActionsContainer) => { + const inputActionsContainerObserver = new ResizeObserver(() => { + recalculateInputStyling(inputActionsContainer); + }); - input.style.paddingRight = paddingRight; + inputActionsContainerObserver.observe(inputActionsContainer); + recalculateInputStyling(inputActionsContainer); }); }; From 02c768c6da5b5855c2da42be0d423efcb472fb01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Tue, 19 Nov 2024 16:58:54 +0100 Subject: [PATCH 08/11] added BC fallback --- .../views/themes/admin/content/form_fields.html.twig | 2 +- .../Resources/views/themes/admin/ui/form_fields.html.twig | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/bundle/Resources/views/themes/admin/content/form_fields.html.twig b/src/bundle/Resources/views/themes/admin/content/form_fields.html.twig index fa32def56b..d19c08ef6f 100644 --- a/src/bundle/Resources/views/themes/admin/content/form_fields.html.twig +++ b/src/bundle/Resources/views/themes/admin/content/form_fields.html.twig @@ -68,7 +68,7 @@ {{- block('form_label') }}
- {{- form_widget(form, {'attr': attr}) -}} + {{- form_widget(form, { attr }) -}}
{{- block('form_errors') -}} diff --git a/src/bundle/Resources/views/themes/admin/ui/form_fields.html.twig b/src/bundle/Resources/views/themes/admin/ui/form_fields.html.twig index 639a3ee78f..073a8ead4b 100644 --- a/src/bundle/Resources/views/themes/admin/ui/form_fields.html.twig +++ b/src/bundle/Resources/views/themes/admin/ui/form_fields.html.twig @@ -389,6 +389,9 @@ {%- set type = type|default('text') -%} {%- set is_text_input = type == 'text' or type == 'number' or force_text|default(false) -%} {%- if is_text_input -%} + {# @deprecated extra_actions_after in attr will be removed in 5.0, used for BC in 4.6 #} + {%- set extra_actions_after_from_attr = attr.extra_actions_after|default(null) -%} + {%- set attr = attr|filter((value, key) => key != 'extra_actions_after') -%} {%- set attr = attr|merge({class: (attr.class|default('') ~ ' ibexa-input ibexa-input--text')|trim}) -%} {%- set empty_placeholder_for_hiding_clear_btn_with_css = ' ' -%} {%- set attr = attr|merge({placeholder: (attr.placeholder is defined and attr.placeholder is not null) ? attr.placeholder : empty_placeholder_for_hiding_clear_btn_with_css}) -%} @@ -402,7 +405,7 @@ {% block actions %} {{ parent() }} - {{ extra_actions_after|default()}} + {{ extra_actions_after|default(extra_actions_after_from_attr)}} {% endblock %} {%- endembed -%} {%- else -%} From f8b30d4a4dcc25978eb7ccf42908b34972df2cb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Wed, 27 Nov 2024 12:09:36 +0100 Subject: [PATCH 09/11] after cr --- .../scss/ui/modules/common/_popup.menu.scss | 15 +-------------- .../common/draggable-dialog/draggable.dialog.js | 4 ++-- .../src/modules/common/popup-menu/popup.menu.js | 4 ++-- 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/bundle/Resources/public/scss/ui/modules/common/_popup.menu.scss b/src/bundle/Resources/public/scss/ui/modules/common/_popup.menu.scss index 72ca05fad2..09427e8b16 100644 --- a/src/bundle/Resources/public/scss/ui/modules/common/_popup.menu.scss +++ b/src/bundle/Resources/public/scss/ui/modules/common/_popup.menu.scss @@ -53,13 +53,12 @@ &__item-content { position: relative; display: flex; - align-items: center; align-items: baseline; width: 100%; cursor: pointer; padding: calculateRem(9px); color: $ibexa-color-dark; - font-size: calculateRem(14px); + font-size: $ibexa-text-font-size-medium; text-align: left; text-decoration: none; border: none; @@ -71,17 +70,5 @@ color: $ibexa-color-black; text-decoration: none; } - - &[disabled], - &:disabled, - &--disabled { - pointer-events: none; - cursor: not-allowed; - opacity: 0.2; - - &:hover { - background-color: initial; - } - } } } diff --git a/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js b/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js index 8f4f3a8afc..7aa0b4631c 100644 --- a/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js +++ b/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js @@ -89,8 +89,8 @@ const DraggableDialog = ({ children, referenceElement, positionOffset }) => { useEffect(() => { if (isDragging) { - rootDOMElement.addEventListener('mousemove', handleDragging); - rootDOMElement.addEventListener('mouseup', stopDragging); + rootDOMElement.addEventListener('mousemove', handleDragging, false); + rootDOMElement.addEventListener('mouseup', stopDragging, false); } return () => { diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js index a774e29cc3..7625ba3120 100644 --- a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef, useMemo } from 'react'; import PropTypes from 'prop-types'; -import { getTranslator } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; +import { getTranslator, getRootDOMElement } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; import { createCssClassNames } from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/common/helpers/css.class.names'; import Icon from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/common/icon/icon'; @@ -188,7 +188,7 @@ PopupMenu.defaultProps = { onClose: () => {}, onItemClick: () => {}, positionOffset: () => ({ x: 0, y: 0 }), - scrollContainer: window.document.body, + scrollContainer: getRootDOMElement(), }; export default PopupMenu; From 2066b8d1a2931403ba0e138851a2bc168a904b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Thu, 28 Nov 2024 14:51:00 +0100 Subject: [PATCH 10/11] added some root handling --- .../public/js/scripts/helpers/react.helper.js | 8 +++++++- .../src/modules/common/popup-menu/popup.menu.js | 17 +++++------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/bundle/Resources/public/js/scripts/helpers/react.helper.js b/src/bundle/Resources/public/js/scripts/helpers/react.helper.js index 7b32f1f85b..df9511f5e2 100644 --- a/src/bundle/Resources/public/js/scripts/helpers/react.helper.js +++ b/src/bundle/Resources/public/js/scripts/helpers/react.helper.js @@ -1,4 +1,10 @@ -const createDynamicRoot = (contextDOMElement = window.document.body, id) => { +import { getRootDOMElement } from './context.helper'; + +const createDynamicRoot = ({ contextDOMElement = getRootDOMElement(), id } = {}) => { + if (id && contextDOMElement.querySelector(`#${id}`) !== null) { + console.warn(`You're creating second root element with ID "${id}". IDs should be unique inside a document.`); + } + const rootDOMElement = document.createElement('div'); rootDOMElement.classList.add('ibexa-react-root'); diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js index 7625ba3120..51ddae3e47 100644 --- a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js @@ -43,23 +43,15 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r return null; } - const groupClassName = createCssClassNames({ - 'c-popup-menu__group': true, - }); - - return
{group.items.map(renderItem)}
; + return
{group.items.map(renderItem)}
; }; const renderItem = (item) => { if (!showItem(item)) { return null; } - const itemClassName = createCssClassNames({ - 'c-popup-menu__item': true, - }); - return ( -
+
@@ -137,6 +129,7 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r calculateAndSetItemsListStyles(); setIsRendered(true); + const rootDOMElement = getRootDOMElement(); const onInteractionOutside = (event) => { if (containerRef.current.contains(event.target) || referenceElement.contains(event.target)) { return; @@ -145,11 +138,11 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r onClose(); }; - window.document.body.addEventListener('click', onInteractionOutside, false); + rootDOMElement.addEventListener('click', onInteractionOutside, false); scrollContainer.addEventListener('scroll', calculateAndSetItemsListStyles, false); return () => { - window.document.body.removeEventListener('click', onInteractionOutside); + rootDOMElement.removeEventListener('click', onInteractionOutside); scrollContainer.removeEventListener('scroll', calculateAndSetItemsListStyles); setItemsListStyles({}); From ea5dac74bb68fe914d4f958719485e162db22ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Grabowski?= Date: Thu, 5 Dec 2024 14:14:15 +0100 Subject: [PATCH 11/11] added dependency arrays; break popupmenu component to smaller ones --- .../public/js/scripts/helpers/react.helper.js | 2 +- .../draggable-dialog/draggable.dialog.js | 104 ++++++++++-------- .../common/popup-menu/popup.menu.group.js | 38 +++++++ .../common/popup-menu/popup.menu.helper.js | 12 ++ .../common/popup-menu/popup.menu.item.js | 32 ++++++ .../modules/common/popup-menu/popup.menu.js | 98 +++-------------- .../common/popup-menu/popup.menu.search.js | 61 ++++++++++ 7 files changed, 216 insertions(+), 131 deletions(-) create mode 100644 src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.group.js create mode 100644 src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.helper.js create mode 100644 src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.item.js create mode 100644 src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.search.js diff --git a/src/bundle/Resources/public/js/scripts/helpers/react.helper.js b/src/bundle/Resources/public/js/scripts/helpers/react.helper.js index df9511f5e2..2399378631 100644 --- a/src/bundle/Resources/public/js/scripts/helpers/react.helper.js +++ b/src/bundle/Resources/public/js/scripts/helpers/react.helper.js @@ -1,7 +1,7 @@ import { getRootDOMElement } from './context.helper'; const createDynamicRoot = ({ contextDOMElement = getRootDOMElement(), id } = {}) => { - if (id && contextDOMElement.querySelector(`#${id}`) !== null) { + if (id && window.document.getElementById(id) !== null) { console.warn(`You're creating second root element with ID "${id}". IDs should be unique inside a document.`); } diff --git a/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js b/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js index 7aa0b4631c..7dc3583f00 100644 --- a/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js +++ b/src/bundle/ui-dev/src/modules/common/draggable-dialog/draggable.dialog.js @@ -1,4 +1,4 @@ -import React, { useRef, createContext, useState, useEffect } from 'react'; +import React, { useRef, createContext, useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { getRootDOMElement } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; @@ -25,43 +25,46 @@ const DraggableDialog = ({ children, referenceElement, positionOffset }) => { left: coords.x, }, }; - const getMousePosition = (event) => ({ x: event.x, y: event.y }); - const setContainerCoords = (event) => { - const mouseCoords = getMousePosition(event); - let x = mouseCoords.x - dragOffsetPosition.current.x; - let y = mouseCoords.y - dragOffsetPosition.current.y; - let newDragOffsetX; - let newDragOffsetY; - - if (x < 0) { - x = 0; - newDragOffsetX = mouseCoords.x; - } else if (x + containerSize.current.width > window.innerWidth) { - x = window.innerWidth - containerSize.current.width; - newDragOffsetX = mouseCoords.x - x; - } - - if (y < 0) { - y = 0; - newDragOffsetY = mouseCoords.y; - } else if (y + containerSize.current.height > window.innerHeight) { - y = window.innerHeight - containerSize.current.height; - newDragOffsetY = mouseCoords.y - y; - } - - if (newDragOffsetX) { - dragOffsetPosition.current.x = newDragOffsetX; - } - - if (newDragOffsetY) { - dragOffsetPosition.current.y = newDragOffsetY; - } - - setCoords({ - x, - y, - }); - }; + const getMousePosition = useCallback((event) => ({ x: event.x, y: event.y }), []); + const setContainerCoords = useCallback( + (event) => { + const mouseCoords = getMousePosition(event); + let x = mouseCoords.x - dragOffsetPosition.current.x; + let y = mouseCoords.y - dragOffsetPosition.current.y; + let newDragOffsetX; + let newDragOffsetY; + + if (x < 0) { + x = 0; + newDragOffsetX = mouseCoords.x; + } else if (x + containerSize.current.width > window.innerWidth) { + x = window.innerWidth - containerSize.current.width; + newDragOffsetX = mouseCoords.x - x; + } + + if (y < 0) { + y = 0; + newDragOffsetY = mouseCoords.y; + } else if (y + containerSize.current.height > window.innerHeight) { + y = window.innerHeight - containerSize.current.height; + newDragOffsetY = mouseCoords.y - y; + } + + if (newDragOffsetX) { + dragOffsetPosition.current.x = newDragOffsetX; + } + + if (newDragOffsetY) { + dragOffsetPosition.current.y = newDragOffsetY; + } + + setCoords({ + x, + y, + }); + }, + [getMousePosition], + ); const startDragging = (event) => { const { x: containerX, y: containerY, width, height } = containerRef.current.getBoundingClientRect(); const mouseCoords = getMousePosition(event.nativeEvent); @@ -80,24 +83,29 @@ const DraggableDialog = ({ children, referenceElement, positionOffset }) => { setIsDragging(true); }; - const stopDragging = () => { + const stopDragging = useCallback(() => { setIsDragging(false); - }; - const handleDragging = (event) => { - setContainerCoords(event); - }; + }, []); + const handleDragging = useCallback( + (event) => { + setContainerCoords(event); + }, + [setContainerCoords], + ); useEffect(() => { - if (isDragging) { - rootDOMElement.addEventListener('mousemove', handleDragging, false); - rootDOMElement.addEventListener('mouseup', stopDragging, false); + if (!isDragging) { + return; } + rootDOMElement.addEventListener('mousemove', handleDragging, false); + rootDOMElement.addEventListener('mouseup', stopDragging, false); + return () => { rootDOMElement.removeEventListener('mousemove', handleDragging); rootDOMElement.removeEventListener('mouseup', stopDragging); }; - }, [isDragging]); + }, [isDragging, rootDOMElement, handleDragging, stopDragging]); useEffect(() => { const { top: referenceTop, left: referenceLeft } = referenceElement.getBoundingClientRect(); @@ -122,7 +130,7 @@ const DraggableDialog = ({ children, referenceElement, positionOffset }) => { x, y, }); - }, [referenceElement]); + }, [referenceElement, positionOffset]); return ( { + const isAnyItemVisible = items.some((item) => showItem(item, filterText)); + + if (!isAnyItemVisible) { + return null; + } + + return ( +
+ {items.map((item) => ( + + ))} +
+ ); +}; + +PopupMenuGroup.propTypes = { + items: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + }), + ), + onItemClick: PropTypes.func.isRequired, + filterText: PropTypes.string, +}; + +PopupMenuGroup.defaultProps = { + items: [], + filterText: '', +}; + +export default PopupMenuGroup; diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.helper.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.helper.js new file mode 100644 index 0000000000..17aef3e6cd --- /dev/null +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.helper.js @@ -0,0 +1,12 @@ +const MIN_SEARCH_LENGTH = 3; + +export const showItem = (item, filterText) => { + if (filterText.length < MIN_SEARCH_LENGTH) { + return true; + } + + const itemLabelLowerCase = item.label.toLowerCase(); + const filterTextLowerCase = filterText.toLowerCase(); + + return itemLabelLowerCase.indexOf(filterTextLowerCase) === 0; +}; diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.item.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.item.js new file mode 100644 index 0000000000..ece2c65b34 --- /dev/null +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.item.js @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { showItem } from './popup.menu.helper'; + +const PopupMenuItem = ({ item, filterText, onItemClick }) => { + if (!showItem(item, filterText)) { + return null; + } + + return ( +
+ +
+ ); +}; + +PopupMenuItem.propTypes = { + item: PropTypes.shape({ + label: PropTypes.string.isRequired, + }).isRequired, + onItemClick: PropTypes.func.isRequired, + filterText: PropTypes.string, +}; + +PopupMenuItem.defaultProps = { + filterText: '', +}; + +export default PopupMenuItem; diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js index 51ddae3e47..103b28a366 100644 --- a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.js @@ -1,15 +1,15 @@ -import React, { useState, useEffect, useRef, useMemo } from 'react'; +import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import PropTypes from 'prop-types'; -import { getTranslator, getRootDOMElement } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; +import { getRootDOMElement } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; import { createCssClassNames } from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/common/helpers/css.class.names'; -import Icon from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/common/icon/icon'; -const MIN_SEARCH_ITEMS_DEFAULT = 5; +import PopupMenuSearch from './popup.menu.search'; +import PopupMenuGroup from './popup.menu.group'; + const MIN_ITEMS_LIST_HEIGHT = 150; const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, referenceElement, scrollContainer, onClose }) => { - const Translator = getTranslator(); const containerRef = useRef(); const [isRendered, setIsRendered] = useState(false); const [itemsListStyles, setItemsListStyles] = useState({ @@ -23,42 +23,7 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r 'c-popup-menu--hidden': !isRendered, [extraClasses]: true, }); - const searchPlaceholder = Translator.trans(/*@Desc("Search...")*/ 'ibexa_popup_menu.search.placeholder', {}, 'ibexa_popup_menu'); - const updateFilterValue = (event) => setFilterText(event.target.value); - const resetInputValue = () => setFilterText(''); - const showItem = (item) => { - if (filterText.length < 3) { - return true; - } - - const itemLabelLowerCase = item.label.toLowerCase(); - const filterTextLowerCase = filterText.toLowerCase(); - - return itemLabelLowerCase.indexOf(filterTextLowerCase) === 0; - }; - const renderGroup = (group) => { - const isAnyItemVisible = group.items.some(showItem); - - if (!isAnyItemVisible) { - return null; - } - - return
{group.items.map(renderItem)}
; - }; - const renderItem = (item) => { - if (!showItem(item)) { - return null; - } - - return ( -
- -
- ); - }; - const calculateAndSetItemsListStyles = () => { + const calculateAndSetItemsListStyles = useCallback(() => { const itemsStyles = {}; const { top: referenceTop, left: referenceLeft } = referenceElement.getBoundingClientRect(); const { height: containerHeight } = containerRef.current.getBoundingClientRect(); @@ -80,43 +45,7 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r } setItemsListStyles(itemsStyles); - }; - const renderSearch = () => { - if (numberOfItems < MIN_SEARCH_ITEMS_DEFAULT) { - return null; - } - - return ( -
-
- -
- - -
-
-
- ); - }; + }, [referenceElement, positionOffset]); const renderFooter = () => { if (!footer) { return null; @@ -147,12 +76,16 @@ const PopupMenu = ({ extraClasses, footer, items, onItemClick, positionOffset, r setItemsListStyles({}); }; - }, [onClose, scrollContainer]); + }, [onClose, scrollContainer, referenceElement, calculateAndSetItemsListStyles]); return (
- {renderSearch()} -
{items.map(renderGroup)}
+ +
+ {items.map((group) => ( + + ))} +
{renderFooter()}
); @@ -163,8 +96,9 @@ PopupMenu.propTypes = { extraClasses: PropTypes.string, footer: PropTypes.node, items: PropTypes.arrayOf({ + id: PropTypes.string.isRequired, items: PropTypes.shape({ - value: PropTypes.oneOf([PropTypes.string, PropTypes.number]), + id: PropTypes.oneOf([PropTypes.string, PropTypes.number]), label: PropTypes.string, }), }), diff --git a/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.search.js b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.search.js new file mode 100644 index 0000000000..ba99b3f5f1 --- /dev/null +++ b/src/bundle/ui-dev/src/modules/common/popup-menu/popup.menu.search.js @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { getTranslator } from '@ibexa-admin-ui/src/bundle/Resources/public/js/scripts/helpers/context.helper'; +import Icon from '@ibexa-admin-ui/src/bundle/ui-dev/src/modules/common/icon/icon'; + +const MIN_SEARCH_ITEMS_DEFAULT = 5; + +const PopupMenuSearch = ({ numberOfItems, filterText, setFilterText }) => { + const Translator = getTranslator(); + const searchPlaceholder = Translator.trans(/*@Desc("Search...")*/ 'ibexa_popup_menu.search.placeholder', {}, 'ibexa_popup_menu'); + const updateFilterValue = (event) => setFilterText(event.target.value); + const resetInputValue = () => setFilterText(''); + + if (numberOfItems < MIN_SEARCH_ITEMS_DEFAULT) { + return null; + } + + return ( +
+
+ +
+ + +
+
+
+ ); +}; + +PopupMenuSearch.propTypes = { + numberOfItems: PropTypes.number.isRequired, + setFilterText: PropTypes.func.isRequired, + filterText: PropTypes.string, +}; + +PopupMenuSearch.defaultProps = { + filterText: '', +}; + +export default PopupMenuSearch;