
META-INF.resources.page_editor.common.components.SpacingBox.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com.liferay.layout.content.page.editor.web
Show all versions of com.liferay.layout.content.page.editor.web
Liferay Layout Content Page Editor Web
The newest version!
/**
* SPDX-FileCopyrightText: (c) 2000 Liferay, Inc. https://liferay.com
* SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06
*/
import ClayButton, {ClayButtonWithIcon} from '@clayui/button';
import {Text} from '@clayui/core';
import ClayDropDown from '@clayui/drop-down';
import Layout from '@clayui/layout';
import ClayTooltip from '@clayui/tooltip';
import {ReactPortal} from '@liferay/frontend-js-react-web';
import {
LengthInput,
isValidStyleValue,
} from '@liferay/layout-js-components-web';
import classNames from 'classnames';
import {useId} from 'frontend-js-components-web';
import {sub} from 'frontend-js-web';
import React, {useEffect, useMemo, useRef, useState} from 'react';
import {useGlobalContext} from '../../app/contexts/GlobalContext';
import {useSelector} from '../../app/contexts/StoreContext';
import {getResetLabelByViewport} from '../../app/utils/getResetLabelByViewport';
import {useStyleBook} from '../../plugins/page_design_options/hooks/useStyleBook';
/**
* These elements must be sorted from the most outer circle to the most inner
* circle to facilitate keyboard navigation.
* @type {string[]}
*/
const SPACING_TYPES = ['margin', 'padding'];
/**
* We want to show spacing options in a clockwise order, according
* to the standard of most CSS rules (top, right, bottom, left).
* @type {string[]}
*/
const SPACING_POSITIONS = ['top', 'right', 'bottom', 'left'];
const ARROW_TO_POSITION = {
ArrowDown: 'bottom',
ArrowLeft: 'left',
ArrowRight: 'right',
ArrowUp: 'top',
};
const REVERSED_POSITION = {
bottom: 'top',
left: 'right',
right: 'left',
top: 'bottom',
};
const BUTTON_CLASSNAME = 'page-editor__spacing-selector__button';
const DROPDOWN_CLASSNAME = 'page-editor__spacing-selector__dropdown';
const TOOLTIP_SHOW_DELAY = 600;
export default function SpacingBox({
canSetCustomValue,
fields,
onChange,
value,
}) {
const ref = useRef();
const focusButton = (type, position) => {
const button = document.querySelector(
`.${BUTTON_CLASSNAME}[data-type=${type}][data-position=${position}]`
);
button?.focus();
};
const handleKeyDown = (event) => {
if (
(event.key === 'Enter' || event.key === ' ') &&
document.activeElement === ref.current
) {
event.preventDefault();
focusButton('margin', 'top');
return;
}
if (
!document.activeElement?.classList.contains(BUTTON_CLASSNAME) ||
document.activeElement?.getAttribute('aria-expanded') === 'true' ||
!event.key.startsWith('Arrow')
) {
return;
}
event.preventDefault();
const {position: currentPosition, type: currentType} =
document.activeElement.dataset;
let nextPosition = ARROW_TO_POSITION[event.key];
let nextType = currentType;
if (nextPosition === currentPosition) {
// Move to the outer type.
// We try to update the type so we can move to the outer circle,
// or stay in position if it is not possible.
const currentTypeIndex = SPACING_TYPES.indexOf(currentType);
nextType = SPACING_TYPES[Math.max(0, currentTypeIndex - 1)];
}
else if (nextPosition === REVERSED_POSITION[currentPosition]) {
// Move to the inner type.
// We try to update the type so we can move to the inner circle,
// and keep currentPosition if it succeeds.
const currentTypeIndex = SPACING_TYPES.indexOf(currentType);
if (currentTypeIndex < SPACING_TYPES.length - 1) {
nextType = SPACING_TYPES[currentTypeIndex + 1];
nextPosition = currentPosition;
}
}
focusButton(nextType, nextPosition);
};
return (
{SPACING_TYPES.map((type) => (
{SPACING_POSITIONS.map((position) => {
const key = `${type}${capitalize(position)}`;
return (
);
})}
))}
);
}
function SpacingSelectorButton({
canSetCustomValue,
field,
onChange,
position,
type,
value,
}) {
const [active, setActive] = useState(false);
const disabled = !field || field.disabled;
const itemListRef = useRef();
const [labelElement, setLabelElement] = useState(null);
const {tokenValues} = useStyleBook();
const tooltipId = useId();
const triggerId = useId();
const headerId = useId();
const [triggerElement, setTriggerElement] = useState(null);
const selectedViewportSize = useSelector(
(state) => state.selectedViewportSize
);
const resetButtonLabel = useMemo(
() => getResetLabelByViewport(selectedViewportSize),
[selectedViewportSize]
);
useEffect(() => {
if (active && itemListRef.current) {
setTimeout(
() =>
itemListRef.current
?.querySelector(
`button[data-value="${
value || field?.defaultValue
}"]`
)
?.focus(),
10
);
}
}, [active, field, value]);
return (
setActive(!active)}
ref={setTriggerElement}
type="button"
>
{field ? (
{field.label} -{' '}
>
}
positionElement={labelElement}
/>
) : null}
}
>
{active && canSetCustomValue ? (
<>
>
) : null}
{field?.typeOptions?.validValues?.map((option) => (
{
onChange(field.name, option.value);
setActive(false);
triggerElement?.focus();
}}
>
{tokenValues[
`spacer${option.value}`
]?.label || option.label}
))}
);
}
function SpacingOptionValue({
position,
removeValueUnit = false,
tokenValues,
type,
value: optionValue,
}) {
const globalContext = useGlobalContext();
const [value, setValue] = useState(optionValue);
useEffect(() => {
if (tokenValues[`spacer${optionValue}`]) {
setValue(tokenValues[`spacer${optionValue}`].value);
return;
}
if (isValidStyleValue(`${type}-${position}`, optionValue)) {
setValue(optionValue);
return;
}
const element = globalContext.document.createElement('div');
element.style.display = 'none';
element.classList.add(`${type[0]}${position[0]}-${optionValue}`);
globalContext.document.body.appendChild(element);
const nextValue = globalContext.window
.getComputedStyle(element)
.getPropertyValue(`${type}-${position}`);
setValue(nextValue);
globalContext.document.body.removeChild(element);
}, [
globalContext,
optionValue,
position,
removeValueUnit,
type,
tokenValues,
]);
return value === undefined ? '' : value;
}
function Tooltip({hoverElement, id: tooltipId, label, positionElement}) {
const [tooltipStyle, setTooltipStyle] = useState(null);
useEffect(() => {
if (!hoverElement || !positionElement) {
return;
}
let showTimeoutId;
const handleMouseLeave = () => {
clearTimeout(showTimeoutId);
setTooltipStyle(null);
};
const handleMouseOver = () => {
clearTimeout(showTimeoutId);
showTimeoutId = setTimeout(() => {
const rect = positionElement.getBoundingClientRect();
setTooltipStyle({
left: rect.left + rect.width / 2,
top: rect.top,
});
}, TOOLTIP_SHOW_DELAY);
};
hoverElement.addEventListener('mouseleave', handleMouseLeave);
hoverElement.addEventListener('mouseover', handleMouseOver);
return () => {
clearTimeout(showTimeoutId);
hoverElement.removeEventListener('mouseleave', handleMouseLeave);
hoverElement.removeEventListener('mouseover', handleMouseOver);
};
}, [hoverElement, positionElement]);
return tooltipStyle ? (
{label}
) : null;
}
function capitalize(str) {
return `${str.substring(0, 1).toUpperCase()}${str.substring(1)}`;
}
function SpacingSelectorBackground() {
return (
);
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy