UNPKG

@wordpress/components

Version:
428 lines (405 loc) 14.2 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import { createElement, Fragment } from "@wordpress/element"; /** * External dependencies */ import classnames from 'classnames'; import { paramCase as kebabCase } from 'change-case'; /** * WordPress dependencies */ import { useState, useRef, useEffect, useCallback, useMemo } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { lineSolid, moreVertical, plus } from '@wordpress/icons'; import { __experimentalUseFocusOutside as useFocusOutside, useDebounce } from '@wordpress/compose'; /** * Internal dependencies */ import Button from '../button'; import { ColorPicker } from '../color-picker'; import { FlexItem } from '../flex'; import { HStack } from '../h-stack'; import { ItemGroup } from '../item-group'; import { VStack } from '../v-stack'; import GradientPicker from '../gradient-picker'; import ColorPalette from '../color-palette'; import DropdownMenu from '../dropdown-menu'; import Popover from '../popover'; import { PaletteActionsContainer, PaletteEditStyles, PaletteHeading, PaletteHStackHeader, IndicatorStyled, PaletteItem, NameContainer, NameInputControl, DoneButton, RemoveButton } from './styles'; import { NavigableMenu } from '../navigable-container'; import { DEFAULT_GRADIENT } from '../custom-gradient-picker/constants'; import CustomGradientPicker from '../custom-gradient-picker'; const DEFAULT_COLOR = '#000'; function NameInput(_ref) { let { value, onChange, label } = _ref; return createElement(NameInputControl, { label: label, hideLabelFromVision: true, value: value, onChange: onChange }); } /** * Returns a temporary name for a palette item in the format "Color + id". * To ensure there are no duplicate ids, this function checks all slugs for temporary names. * It expects slugs to be in the format: slugPrefix + color- + number. * It then sets the id component of the new name based on the incremented id of the highest existing slug id. * * @param elements An array of color palette items. * @param slugPrefix The slug prefix used to match the element slug. * * @return A unique name for a palette item. */ export function getNameForPosition(elements, slugPrefix) { const temporaryNameRegex = new RegExp(`^${slugPrefix}color-([\\d]+)$`); const position = elements.reduce((previousValue, currentValue) => { if (typeof (currentValue === null || currentValue === void 0 ? void 0 : currentValue.slug) === 'string') { const matches = currentValue === null || currentValue === void 0 ? void 0 : currentValue.slug.match(temporaryNameRegex); if (matches) { const id = parseInt(matches[1], 10); if (id >= previousValue) { return id + 1; } } } return previousValue; }, 1); return sprintf( /* translators: %s: is a temporary id for a custom color */ __('Color %s'), position); } function ColorPickerPopover(_ref2) { let { isGradient, element, onChange, popoverProps: receivedPopoverProps, onClose = () => {} } = _ref2; const popoverProps = useMemo(() => ({ shift: true, offset: 20, placement: 'left-start', ...receivedPopoverProps, className: classnames('components-palette-edit__popover', receivedPopoverProps === null || receivedPopoverProps === void 0 ? void 0 : receivedPopoverProps.className) }), [receivedPopoverProps]); return createElement(Popover, _extends({}, popoverProps, { onClose: onClose }), !isGradient && createElement(ColorPicker, { color: element.color, enableAlpha: true, onChange: newColor => { onChange({ ...element, color: newColor }); } }), isGradient && createElement("div", { className: "components-palette-edit__popover-gradient-picker" }, createElement(CustomGradientPicker, { __nextHasNoMargin: true, __experimentalIsRenderedInSidebar: true, value: element.gradient, onChange: newGradient => { onChange({ ...element, gradient: newGradient }); } }))); } function Option(_ref3) { let { canOnlyChangeValues, element, onChange, isEditing, onStartEditing, onRemove, onStopEditing, popoverProps: receivedPopoverProps, slugPrefix, isGradient } = _ref3; const focusOutsideProps = useFocusOutside(onStopEditing); const value = isGradient ? element.gradient : element.color; // Use internal state instead of a ref to make sure that the component // re-renders when the popover's anchor updates. const [popoverAnchor, setPopoverAnchor] = useState(null); const popoverProps = useMemo(() => ({ ...receivedPopoverProps, // Use the custom palette color item as the popover anchor. anchor: popoverAnchor }), [popoverAnchor, receivedPopoverProps]); return createElement(PaletteItem, _extends({ className: isEditing ? 'is-selected' : undefined, as: "div", onClick: onStartEditing, ref: setPopoverAnchor }, isEditing ? { ...focusOutsideProps } : { style: { cursor: 'pointer' } }), createElement(HStack, { justify: "flex-start" }, createElement(FlexItem, null, createElement(IndicatorStyled, { style: { background: value, color: 'transparent' } })), createElement(FlexItem, null, isEditing && !canOnlyChangeValues ? createElement(NameInput, { label: isGradient ? __('Gradient name') : __('Color name'), value: element.name, onChange: nextName => onChange({ ...element, name: nextName, slug: slugPrefix + kebabCase(nextName !== null && nextName !== void 0 ? nextName : '') }) }) : createElement(NameContainer, null, element.name)), isEditing && !canOnlyChangeValues && createElement(FlexItem, null, createElement(RemoveButton, { isSmall: true, icon: lineSolid, label: __('Remove color'), onClick: onRemove }))), isEditing && createElement(ColorPickerPopover, { isGradient: isGradient, onChange: onChange, element: element, popoverProps: popoverProps })); } function isTemporaryElement(slugPrefix, _ref4) { let { slug, color, gradient } = _ref4; const regex = new RegExp(`^${slugPrefix}color-([\\d]+)$`); return regex.test(slug) && (!!color && color === DEFAULT_COLOR || !!gradient && gradient === DEFAULT_GRADIENT); } function PaletteEditListView(_ref5) { let { elements, onChange, editingElement, setEditingElement, canOnlyChangeValues, slugPrefix, isGradient, popoverProps } = _ref5; // When unmounting the component if there are empty elements (the user did not complete the insertion) clean them. const elementsReference = useRef(); useEffect(() => { elementsReference.current = elements; }, [elements]); useEffect(() => { return () => { var _elementsReference$cu; if ((_elementsReference$cu = elementsReference.current) !== null && _elementsReference$cu !== void 0 && _elementsReference$cu.some(element => isTemporaryElement(slugPrefix, element))) { const newElements = elementsReference.current.filter(element => !isTemporaryElement(slugPrefix, element)); onChange(newElements.length ? newElements : undefined); } }; // Disable reason: adding the missing dependency here would cause breaking changes that will require // a heavier refactor to avoid. See https://github.com/WordPress/gutenberg/pull/43911 // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const debounceOnChange = useDebounce(onChange, 100); return createElement(VStack, { spacing: 3 }, createElement(ItemGroup, { isRounded: true }, elements.map((element, index) => createElement(Option, { isGradient: isGradient, canOnlyChangeValues: canOnlyChangeValues, key: index, element: element, onStartEditing: () => { if (editingElement !== index) { setEditingElement(index); } }, onChange: newElement => { debounceOnChange(elements.map((currentElement, currentIndex) => { if (currentIndex === index) { return newElement; } return currentElement; })); }, onRemove: () => { setEditingElement(null); const newElements = elements.filter((_currentElement, currentIndex) => { if (currentIndex === index) { return false; } return true; }); onChange(newElements.length ? newElements : undefined); }, isEditing: index === editingElement, onStopEditing: () => { if (index === editingElement) { setEditingElement(null); } }, slugPrefix: slugPrefix, popoverProps: popoverProps })))); } const EMPTY_ARRAY = []; /** * Allows editing a palette of colors or gradients. * * ```jsx * import { PaletteEdit } from '@wordpress/components'; * const MyPaletteEdit = () => { * const [ controlledColors, setControlledColors ] = useState( colors ); * * return ( * <PaletteEdit * colors={ controlledColors } * onChange={ ( newColors?: Color[] ) => { * setControlledColors( newColors ); * } } * paletteLabel="Here is a label" * /> * ); * }; * ``` */ export function PaletteEdit(_ref6) { let { gradients, colors = EMPTY_ARRAY, onChange, paletteLabel, paletteLabelHeadingLevel = 2, emptyMessage, canOnlyChangeValues, canReset, slugPrefix = '', popoverProps } = _ref6; const isGradient = !!gradients; const elements = isGradient ? gradients : colors; const [isEditing, setIsEditing] = useState(false); const [editingElement, setEditingElement] = useState(null); const isAdding = isEditing && !!editingElement && elements[editingElement] && !elements[editingElement].slug; const elementsLength = elements.length; const hasElements = elementsLength > 0; const debounceOnChange = useDebounce(onChange, 100); const onSelectPaletteItem = useCallback((value, newEditingElementIndex) => { const selectedElement = newEditingElementIndex === undefined ? undefined : elements[newEditingElementIndex]; const key = isGradient ? 'gradient' : 'color'; // Ensures that the index returned matches a known element value. if (!!selectedElement && selectedElement[key] === value) { setEditingElement(newEditingElementIndex); } else { setIsEditing(true); } }, [isGradient, elements]); return createElement(PaletteEditStyles, null, createElement(PaletteHStackHeader, null, createElement(PaletteHeading, { level: paletteLabelHeadingLevel }, paletteLabel), createElement(PaletteActionsContainer, null, hasElements && isEditing && createElement(DoneButton, { isSmall: true, onClick: () => { setIsEditing(false); setEditingElement(null); } }, __('Done')), !canOnlyChangeValues && createElement(Button, { isSmall: true, isPressed: isAdding, icon: plus, label: isGradient ? __('Add gradient') : __('Add color'), onClick: () => { const tempOptionName = getNameForPosition(elements, slugPrefix); if (!!gradients) { onChange([...gradients, { gradient: DEFAULT_GRADIENT, name: tempOptionName, slug: slugPrefix + kebabCase(tempOptionName) }]); } else { onChange([...colors, { color: DEFAULT_COLOR, name: tempOptionName, slug: slugPrefix + kebabCase(tempOptionName) }]); } setIsEditing(true); setEditingElement(elements.length); } }), hasElements && (!isEditing || !canOnlyChangeValues || canReset) && createElement(DropdownMenu, { icon: moreVertical, label: isGradient ? __('Gradient options') : __('Color options'), toggleProps: { isSmall: true } }, _ref7 => { let { onClose } = _ref7; return createElement(Fragment, null, createElement(NavigableMenu, { role: "menu" }, !isEditing && createElement(Button, { variant: "tertiary", onClick: () => { setIsEditing(true); onClose(); }, className: "components-palette-edit__menu-button" }, __('Show details')), !canOnlyChangeValues && createElement(Button, { variant: "tertiary", onClick: () => { setEditingElement(null); setIsEditing(false); onChange(); onClose(); }, className: "components-palette-edit__menu-button" }, isGradient ? __('Remove all gradients') : __('Remove all colors')), canReset && createElement(Button, { variant: "tertiary", onClick: () => { setEditingElement(null); onChange(); onClose(); } }, isGradient ? __('Reset gradient') : __('Reset colors')))); }))), hasElements && createElement(Fragment, null, isEditing && createElement(PaletteEditListView, { canOnlyChangeValues: canOnlyChangeValues, elements: elements // @ts-expect-error TODO: Don't know how to resolve , onChange: onChange, editingElement: editingElement, setEditingElement: setEditingElement, slugPrefix: slugPrefix, isGradient: isGradient, popoverProps: popoverProps }), !isEditing && editingElement !== null && createElement(ColorPickerPopover, { isGradient: isGradient, onClose: () => setEditingElement(null), onChange: newElement => { debounceOnChange( // @ts-expect-error TODO: Don't know how to resolve elements.map((currentElement, currentIndex) => { if (currentIndex === editingElement) { return newElement; } return currentElement; })); }, element: elements[editingElement !== null && editingElement !== void 0 ? editingElement : -1], popoverProps: popoverProps }), !isEditing && (isGradient ? createElement(GradientPicker, { __nextHasNoMargin: true, gradients: gradients, onChange: onSelectPaletteItem, clearable: false, disableCustomGradients: true }) : createElement(ColorPalette, { colors: colors, onChange: onSelectPaletteItem, clearable: false, disableCustomColors: true }))), !hasElements && emptyMessage); } export default PaletteEdit; //# sourceMappingURL=index.js.map