UNPKG

@wordpress/block-editor

Version:
274 lines (271 loc) 9.27 kB
/** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; import { getBlockBindingsSource, getBlockBindingsSources, getBlockType } from '@wordpress/blocks'; import { __experimentalItemGroup as ItemGroup, __experimentalItem as Item, __experimentalText as Text, __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, __experimentalVStack as VStack, privateApis as componentsPrivateApis } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { useContext, Fragment } from '@wordpress/element'; import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies */ import { canBindAttribute, getBindableAttributes, useBlockBindingsUtils } from '../utils/block-bindings'; import { unlock } from '../lock-unlock'; import InspectorControls from '../components/inspector-controls'; import BlockContext from '../components/block-context'; import { useBlockEditContext } from '../components/block-edit'; import { store as blockEditorStore } from '../store'; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; const { Menu } = unlock(componentsPrivateApis); const EMPTY_OBJECT = {}; const useToolsPanelDropdownMenuProps = () => { const isMobile = useViewportMatch('medium', '<'); return !isMobile ? { popoverProps: { placement: 'left-start', // For non-mobile, inner sidebar width (248px) - button width (24px) - border (1px) + padding (16px) + spacing (20px) offset: 259 } } : {}; }; function BlockBindingsPanelMenuContent({ fieldsList, attribute, binding }) { const { clientId } = useBlockEditContext(); const registeredSources = getBlockBindingsSources(); const { updateBlockBindings } = useBlockBindingsUtils(); const currentKey = binding?.args?.key; const attributeType = useSelect(select => { const { name: blockName } = select(blockEditorStore).getBlock(clientId); const _attributeType = getBlockType(blockName).attributes?.[attribute]?.type; return _attributeType === 'rich-text' ? 'string' : _attributeType; }, [clientId, attribute]); return /*#__PURE__*/_jsx(_Fragment, { children: Object.entries(fieldsList).map(([name, fields], i) => /*#__PURE__*/_jsxs(Fragment, { children: [/*#__PURE__*/_jsxs(Menu.Group, { children: [Object.keys(fieldsList).length > 1 && /*#__PURE__*/_jsx(Menu.GroupLabel, { children: registeredSources[name].label }), Object.entries(fields).filter(([, args]) => args?.type === attributeType).map(([key, args]) => /*#__PURE__*/_jsxs(Menu.RadioItem, { onChange: () => updateBlockBindings({ [attribute]: { source: name, args: { key } } }), name: attribute + '-binding', value: key, checked: key === currentKey, children: [/*#__PURE__*/_jsx(Menu.ItemLabel, { children: args?.label }), /*#__PURE__*/_jsx(Menu.ItemHelpText, { children: args?.value })] }, key))] }), i !== Object.keys(fieldsList).length - 1 && /*#__PURE__*/_jsx(Menu.Separator, {})] }, name)) }); } function BlockBindingsAttribute({ attribute, binding, fieldsList }) { const { source: sourceName, args } = binding || {}; const sourceProps = getBlockBindingsSource(sourceName); const isSourceInvalid = !sourceProps; return /*#__PURE__*/_jsxs(VStack, { className: "block-editor-bindings__item", spacing: 0, children: [/*#__PURE__*/_jsx(Text, { truncate: true, children: attribute }), !!binding && /*#__PURE__*/_jsx(Text, { truncate: true, variant: !isSourceInvalid && 'muted', isDestructive: isSourceInvalid, children: isSourceInvalid ? __('Invalid source') : fieldsList?.[sourceName]?.[args?.key]?.label || sourceProps?.label || sourceName })] }); } function ReadOnlyBlockBindingsPanelItems({ bindings, fieldsList }) { return /*#__PURE__*/_jsx(_Fragment, { children: Object.entries(bindings).map(([attribute, binding]) => /*#__PURE__*/_jsx(Item, { children: /*#__PURE__*/_jsx(BlockBindingsAttribute, { attribute: attribute, binding: binding, fieldsList: fieldsList }) }, attribute)) }); } function EditableBlockBindingsPanelItems({ attributes, bindings, fieldsList }) { const { updateBlockBindings } = useBlockBindingsUtils(); const isMobile = useViewportMatch('medium', '<'); return /*#__PURE__*/_jsx(_Fragment, { children: attributes.map(attribute => { const binding = bindings[attribute]; return /*#__PURE__*/_jsx(ToolsPanelItem, { hasValue: () => !!binding, label: attribute, onDeselect: () => { updateBlockBindings({ [attribute]: undefined }); }, children: /*#__PURE__*/_jsxs(Menu, { placement: isMobile ? 'bottom-start' : 'left-start', children: [/*#__PURE__*/_jsx(Menu.TriggerButton, { render: /*#__PURE__*/_jsx(Item, {}), children: /*#__PURE__*/_jsx(BlockBindingsAttribute, { attribute: attribute, binding: binding, fieldsList: fieldsList }) }), /*#__PURE__*/_jsx(Menu.Popover, { gutter: isMobile ? 8 : 36, children: /*#__PURE__*/_jsx(BlockBindingsPanelMenuContent, { fieldsList: fieldsList, attribute: attribute, binding: binding }) })] }) }, attribute); }) }); } export const BlockBindingsPanel = ({ name: blockName, metadata }) => { const blockContext = useContext(BlockContext); const { removeAllBlockBindings } = useBlockBindingsUtils(); const bindableAttributes = getBindableAttributes(blockName); const dropdownMenuProps = useToolsPanelDropdownMenuProps(); // `useSelect` is used purposely here to ensure `getFieldsList` // is updated whenever there are updates in block context. // `source.getFieldsList` may also call a selector via `select`. const _fieldsList = {}; const { fieldsList, canUpdateBlockBindings } = useSelect(select => { if (!bindableAttributes || bindableAttributes.length === 0) { return EMPTY_OBJECT; } const registeredSources = getBlockBindingsSources(); Object.entries(registeredSources).forEach(([sourceName, { getFieldsList, usesContext }]) => { if (getFieldsList) { // Populate context. const context = {}; if (usesContext?.length) { for (const key of usesContext) { context[key] = blockContext[key]; } } const sourceList = getFieldsList({ select, context }); // Only add source if the list is not empty. if (Object.keys(sourceList || {}).length) { _fieldsList[sourceName] = { ...sourceList }; } } }); return { fieldsList: Object.values(_fieldsList).length > 0 ? _fieldsList : EMPTY_OBJECT, canUpdateBlockBindings: select(blockEditorStore).getSettings().canUpdateBlockBindings }; }, [blockContext, bindableAttributes]); // Return early if there are no bindable attributes. if (!bindableAttributes || bindableAttributes.length === 0) { return null; } // Filter bindings to only show bindable attributes and remove pattern overrides. const { bindings } = metadata || {}; const filteredBindings = { ...bindings }; Object.keys(filteredBindings).forEach(key => { if (!canBindAttribute(blockName, key) || filteredBindings[key].source === 'core/pattern-overrides') { delete filteredBindings[key]; } }); // Lock the UI when the user can't update bindings or there are no fields to connect to. const readOnly = !canUpdateBlockBindings || !Object.keys(fieldsList).length; if (readOnly && Object.keys(filteredBindings).length === 0) { return null; } return /*#__PURE__*/_jsx(InspectorControls, { group: "bindings", children: /*#__PURE__*/_jsxs(ToolsPanel, { label: __('Attributes'), resetAll: () => { removeAllBlockBindings(); }, dropdownMenuProps: dropdownMenuProps, className: "block-editor-bindings__panel", children: [/*#__PURE__*/_jsx(ItemGroup, { isBordered: true, isSeparated: true, children: readOnly ? /*#__PURE__*/_jsx(ReadOnlyBlockBindingsPanelItems, { bindings: filteredBindings, fieldsList: fieldsList }) : /*#__PURE__*/_jsx(EditableBlockBindingsPanelItems, { attributes: bindableAttributes, bindings: filteredBindings, fieldsList: fieldsList }) }), /*#__PURE__*/_jsx(Text, { as: "div", variant: "muted", children: /*#__PURE__*/_jsx("p", { children: __('Attributes connected to custom fields or other dynamic data.') }) })] }) }); }; export default { edit: BlockBindingsPanel, attributeKeys: ['metadata'], hasSupport() { return true; } }; //# sourceMappingURL=block-bindings.js.map