diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index 582ad7a8..d895f450 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -330,6 +330,28 @@ code { padding: 0 !important; } +.iddisplay > *:first-child { + min-width: 0; + flex: 0 1 auto; + max-width: 100%; + overflow: hidden; +} + +.iddisplay .ant-btn { + flex-shrink: 0; +} + +.miscid > *:first-child { + min-width: 0; + flex: 0 1 auto; + max-width: 100%; + overflow: hidden; +} + +.miscid .ant-btn { + flex-shrink: 0; +} + .ant-popover-inner:has(.spotlight-tooltip) { padding: 0 !important; } @@ -1139,3 +1161,133 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton { .filter-sidebar-field-select .ant-select-selector { width: max-content !important; } + +.elipsis-text-wrapper.ant-typography { + display: block; + min-width: 0; + max-width: 100%; + width: auto; + overflow: hidden; + flex-grow: 0; + flex-shrink: 1; + flex-basis: auto; +} + +.elipsis-text-wrapper.ant-typography .elipsis-text { + display: flex; + flex-direction: row; + align-items: baseline; + width: 100%; + max-width: 100%; + min-width: 0; + position: relative; + overflow: hidden; + white-space: pre; +} + +.elipsis-text-wrapper.ant-typography code.elipsis-text-measure, +.elipsis-text-wrapper.ant-typography code.elipsis-text-full, +.elipsis-text-wrapper.ant-typography code.elipsis-text-truncated { + margin: 0; + box-sizing: border-box; +} + +.elipsis-text-measure { + position: absolute; + visibility: hidden; + pointer-events: none; + white-space: pre; +} + +.elipsis-text-full { + display: none; + position: absolute; + left: 0; + top: 0; + width: max-content; + max-width: 100%; + min-width: 0; + overflow: hidden; + white-space: pre; +} + +.elipsis-text:not(.is-truncated) .elipsis-text-full { + display: inline-block; +} + +.elipsis-text.is-truncated .elipsis-text-full { + display: none; +} + +.elipsis-text:not(.is-truncated) .elipsis-text-truncated .elipsis-text-dots { + display: none; +} + +.elipsis-text:not(.is-truncated) .elipsis-text-truncated { + visibility: hidden; +} + +.elipsis-text-truncated { + display: flex; + flex: 1 1 0; + align-items: baseline; + min-width: 0; + width: 100%; + max-width: 100%; + overflow: hidden; + white-space: pre; +} + +.elipsis-text-wrapper.ant-typography code.elipsis-text-truncated { + display: flex; +} + +.elipsis-text-start, +.elipsis-text-end { + flex: 1 1 0; + min-width: 0; + overflow: hidden; + white-space: pre; +} + +.elipsis-text-start { + mask-image: linear-gradient( + to right, + #000 0%, + #000 calc(100% - 0.8em), + transparent 100% + ); + -webkit-mask-image: linear-gradient( + to right, + #000 0%, + #000 calc(100% - 0.8em), + transparent 100% + ); +} + +.elipsis-text-end { + display: flex; + justify-content: flex-end; + mask-image: linear-gradient( + to left, + #000 0%, + #000 calc(100% - 0.8em), + transparent 100% + ); + -webkit-mask-image: linear-gradient( + to left, + #000 0%, + #000 calc(100% - 0.8em), + transparent 100% + ); +} + +.elipsis-text-end-inner { + flex: 0 0 auto; + white-space: pre; +} + +.elipsis-text-dots { + flex: 0 0 auto; + user-select: none; +} diff --git a/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx b/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx index 65b5c3de..f875f576 100644 --- a/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx +++ b/src/components/Dashboard/Management/AppUpdates/AppUpdateProgress.jsx @@ -6,6 +6,7 @@ import CloudIcon from '../../../Icons/CloudIcon' import HostIcon from '../../../Icons/HostIcon' import ReloadIcon from '../../../Icons/ReloadIcon' import HProgress from '../../common/HProgress' +import ElipsisText from '../../common/ElipsisText' import CheckCircleIcon from '../../../Icons/CheckCircleIcon' import XMarkCircleIcon from '../../../Icons/XMarkCircleIcon' @@ -143,9 +144,9 @@ const UpdateStage = ({ stage, status, percent, detail }) => { style={{ flex: 1, margin: 0 }} /> {detail && ( - + {detail} - + )} )} diff --git a/src/components/Dashboard/common/AddressDisplay.jsx b/src/components/Dashboard/common/AddressDisplay.jsx index 1d4bf526..39f733a1 100644 --- a/src/components/Dashboard/common/AddressDisplay.jsx +++ b/src/components/Dashboard/common/AddressDisplay.jsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types' import { Flex, Typography, Input } from 'antd' import CountryDisplay from './CountryDisplay' import CountrySelect from './CountrySelect' +import ElipsisText from './ElipsisText' const { Text } = Typography @@ -50,9 +51,9 @@ const AddressDisplay = ({ {isCountry ? ( ) : ( - + {fieldValue} - + )} ) diff --git a/src/components/Dashboard/common/CountryDisplay.jsx b/src/components/Dashboard/common/CountryDisplay.jsx index a8449a78..bcd7d57c 100644 --- a/src/components/Dashboard/common/CountryDisplay.jsx +++ b/src/components/Dashboard/common/CountryDisplay.jsx @@ -1,9 +1,8 @@ import PropTypes from 'prop-types' -import { Flex, Typography } from 'antd' +import { Flex } from 'antd' import Flag from './Flag' import countries from '../../../database/Countries' - -const { Text } = Typography +import ElipsisText from './ElipsisText' const CountryDisplay = ({ countryCode }) => { const country = countries.find((c) => c.code === countryCode) @@ -17,7 +16,7 @@ const CountryDisplay = ({ countryCode }) => { gap={'small'} align='center' className='country-display' - style={{ minWidth: 0 }} + style={{ minWidth: 0, width: '100%' }} > { hasBorder={true} borderRadius={5} /> -
- {country.name} +
+ {country.name}
) diff --git a/src/components/Dashboard/common/CountrySelect.jsx b/src/components/Dashboard/common/CountrySelect.jsx index abb1bd85..f16bcf6c 100644 --- a/src/components/Dashboard/common/CountrySelect.jsx +++ b/src/components/Dashboard/common/CountrySelect.jsx @@ -1,9 +1,8 @@ -import { Select, Flex, Typography } from 'antd' +import { Select, Flex } from 'antd' import PropTypes from 'prop-types' import Flag from './Flag' import countries from '../../../database/Countries' - -const { Text } = Typography +import ElipsisText from './ElipsisText' const CountrySelect = ({ value, @@ -30,7 +29,7 @@ const CountrySelect = ({ label: ( - {country.name} + {country.name} ) }))} diff --git a/src/components/Dashboard/common/DashboardNavigation.jsx b/src/components/Dashboard/common/DashboardNavigation.jsx index f0ddfb1e..ef4b7653 100644 --- a/src/components/Dashboard/common/DashboardNavigation.jsx +++ b/src/components/Dashboard/common/DashboardNavigation.jsx @@ -22,6 +22,7 @@ import { Header } from 'antd/es/layout/layout' import { useMediaQuery } from 'react-responsive' import KeyboardShortcut from './KeyboardShortcut' import UserProfilePopover from './UserProfilePopover' +import ElipsisText from './ElipsisText' import FarmControlLogo from '../../Logos/FarmControlLogo' import FarmControlLogoSmall from '../../Logos/FarmControlLogoSmall' @@ -194,19 +195,17 @@ const DashboardNavigation = () => { }} > {selectedMenuItem.icon} - {selectedMenuItem.label} - + )}
diff --git a/src/components/Dashboard/common/ElipsisText.jsx b/src/components/Dashboard/common/ElipsisText.jsx new file mode 100644 index 00000000..2fa75343 --- /dev/null +++ b/src/components/Dashboard/common/ElipsisText.jsx @@ -0,0 +1,110 @@ +import { useLayoutEffect, useRef, useState } from 'react' +import PropTypes from 'prop-types' +import { Typography } from 'antd' + +const { Text } = Typography + +const childrenToString = (children) => { + if (children == null || typeof children === 'boolean') return '' + if (typeof children === 'string' || typeof children === 'number') { + return String(children) + } + if (Array.isArray(children)) { + return children.map(childrenToString).join('') + } + if (typeof children === 'object' && children.props) { + return childrenToString(children.props.children) + } + return '' +} + +const splitMiddle = (text) => { + const mid = Math.ceil(text.length / 2) + return { + start: text.slice(0, mid), + end: text.slice(mid) + } +} + +const ElipsisText = ({ children, style, className, title, code, ...rest }) => { + const text = childrenToString(children) + const { start, end } = splitMiddle(text) + + const containerRef = useRef(null) + const measureRef = useRef(null) + const truncatedRef = useRef(false) + const [truncated, setTruncated] = useState(false) + + useLayoutEffect(() => { + const container = containerRef.current + const measure = measureRef.current + if (!container || !measure) return + + const update = () => { + const available = container.getBoundingClientRect().width + if (available < 1) return + + const textWidth = measure.getBoundingClientRect().width + const next = Boolean(end) && textWidth > available + if (truncatedRef.current === next) return + truncatedRef.current = next + setTruncated(next) + } + + update() + const frame = requestAnimationFrame(update) + + const resizeObserver = new ResizeObserver(update) + resizeObserver.observe(container) + + const intersectionObserver = new IntersectionObserver(update) + intersectionObserver.observe(container) + + return () => { + cancelAnimationFrame(frame) + resizeObserver.disconnect() + intersectionObserver.disconnect() + } + }, [text, end]) + + const showTruncated = truncated && Boolean(end) + const RootTag = code ? 'code' : 'span' + + return ( + + + + {text} + + {text} + + {start} + ... + + {end} + + + + + ) +} + +ElipsisText.propTypes = { + children: PropTypes.node, + style: PropTypes.object, + className: PropTypes.string, + title: PropTypes.string, + code: PropTypes.bool +} + +export default ElipsisText diff --git a/src/components/Dashboard/common/EmailDisplay.jsx b/src/components/Dashboard/common/EmailDisplay.jsx index 58b22bcb..60550649 100644 --- a/src/components/Dashboard/common/EmailDisplay.jsx +++ b/src/components/Dashboard/common/EmailDisplay.jsx @@ -3,6 +3,7 @@ import { Typography, Flex, Button, Tooltip } from 'antd' import NewMailIcon from '../../Icons/NewMailIcon' // import CopyIcon from './CopyIcon' import CopyButton from './CopyButton' +import ElipsisText from './ElipsisText' const { Text, Link } = Typography @@ -18,9 +19,9 @@ const EmailDisplay = ({ email, showCopy = true, showLink = false }) => { ) : ( <> - + {email} - +