@wordpress/block-editor
Version:
274 lines (271 loc) • 9.27 kB
JavaScript
/**
* 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