diff --git a/CHANGELOG.md b/CHANGELOG.md index 30854bb..0cce833 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,19 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [0.1.29](https://github.com/eea/volto-forests-theme/compare/0.1.28...0.1.29) + +- Prettier fix [`fccfd6f`](https://github.com/eea/volto-forests-theme/commit/fccfd6f35ad1f525127c7eb22f8f3dc124daa423) +- Lint fix MD [`7a5d5bb`](https://github.com/eea/volto-forests-theme/commit/7a5d5bba075ded2c5b02562db7a3ab31a2f3133c) +- Document customization [`2eb3624`](https://github.com/eea/volto-forests-theme/commit/2eb36241380c8077a4f5a0614d395f3b22b745bd) +- Customized block container cleanup [`6907ae2`](https://github.com/eea/volto-forests-theme/commit/6907ae2f199a844b36bf2e171dc8e89d3a4b8a98) +- Custom Block & add stretch class in block wrapper [`7755c15`](https://github.com/eea/volto-forests-theme/commit/7755c157dab2a0a2e222c4440547b192daeda97d) + #### [0.1.28](https://github.com/eea/volto-forests-theme/compare/0.1.27...0.1.28) +> 9 December 2021 + +- Develop [`#38`](https://github.com/eea/volto-forests-theme/pull/38) - fallback for window.env [`f9ddb2d`](https://github.com/eea/volto-forests-theme/commit/f9ddb2ddb8fbe060ec498f9f3dbd346859ed4895) - Don't show header-image-content in Data catalogue [`3d2704f`](https://github.com/eea/volto-forests-theme/commit/3d2704fff57124f7bc80ba915add1d6e67331d18) diff --git a/package.json b/package.json index f670da7..997596a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eeacms/volto-forests-theme", - "version": "0.1.28", + "version": "0.1.29", "description": "@eeacms/volto-forests-theme: Volto add-on", "main": "src/index.js", "author": "European Environment Agency: IDM2 A-Team", diff --git a/src/components/theme/DefaultViewWide/DefaultViewWide.jsx b/src/components/theme/DefaultViewWide/DefaultViewWide.jsx index 364f31a..867982e 100644 --- a/src/components/theme/DefaultViewWide/DefaultViewWide.jsx +++ b/src/components/theme/DefaultViewWide/DefaultViewWide.jsx @@ -56,14 +56,6 @@ const DefaultViewWide = (props) => { - {/* {renderPortletManager('plone.leftcolumn', 2, { ...props })} - */} {hasBlocksData(content) ? (
@@ -82,11 +74,6 @@ const DefaultViewWide = (props) => { path={getBaseUrl(location?.pathname || '')} /> ) : ( - //
- // {intl.formatMessage(messages.unknownBlock, { - // block: content[blocksFieldname]?.[block]?.['@type'], - // })} - //
'' ); })} @@ -94,11 +81,6 @@ const DefaultViewWide = (props) => {
) : ( - {/* -

{content.title}

- {content.description && ( -

{content.description}

- )} */} {content.image && ( { + const { + pathname, + onChangeField, + properties, + onChangeFormData, + selectedBlock, + multiSelected, + onSelectBlock, + allowedBlocks, + showRestricted, + title, + description, + metadata, + manage, + children, + isMainForm = true, + blocksConfig = config.blocks.blocksConfig, + editable = true, + } = props; + + const blockList = getBlocks(properties); + + const dispatch = useDispatch(); + + const ClickOutsideListener = () => { + onSelectBlock(null); + dispatch(setSidebarTab(0)); + }; + + const ref = useDetectClickOutside({ + onTriggered: ClickOutsideListener, + triggerKeys: ['Escape'], + // Disabled feature for now https://github.com/plone/volto/pull/2389#issuecomment-830027413 + disableClick: true, + disableKeys: !isMainForm, + }); + + const handleKeyDown = ( + e, + index, + block, + node, + { + disableEnter = false, + disableArrowUp = false, + disableArrowDown = false, + } = {}, + ) => { + const isMultipleSelection = e.shiftKey; + if (e.key === 'ArrowUp' && !disableArrowUp) { + onFocusPreviousBlock(block, node, isMultipleSelection); + e.preventDefault(); + } + if (e.key === 'ArrowDown' && !disableArrowDown) { + onFocusNextBlock(block, node, isMultipleSelection); + e.preventDefault(); + } + if (e.key === 'Enter' && !disableEnter) { + onAddBlock(config.settings.defaultBlockType, index + 1); + e.preventDefault(); + } + }; + + const onFocusPreviousBlock = ( + currentBlock, + blockNode, + isMultipleSelection, + ) => { + const prev = previousBlockId(properties, currentBlock); + if (prev === null) return; + + blockNode.blur(); + + onSelectBlock(prev, isMultipleSelection); + }; + + const onFocusNextBlock = (currentBlock, blockNode, isMultipleSelection) => { + const next = nextBlockId(properties, currentBlock); + if (next === null) return; + + blockNode.blur(); + + onSelectBlock(next, isMultipleSelection); + }; + + const onMutateBlock = (id, value) => { + const newFormData = mutateBlock(properties, id, value); + onChangeFormData(newFormData); + }; + + const onInsertBlock = (id, value) => { + const [newId, newFormData] = insertBlock(properties, id, value); + onChangeFormData(newFormData); + return newId; + }; + + const onAddBlock = (type, index) => { + if (editable) { + const [id, newFormData] = addBlock(properties, type, index); + onChangeFormData(newFormData); + return id; + } + }; + + const onChangeBlock = (id, value) => { + const newFormData = changeBlock(properties, id, value); + onChangeFormData(newFormData); + }; + + const onDeleteBlock = (id, selectPrev) => { + const previous = previousBlockId(properties, id); + + const newFormData = deleteBlock(properties, id); + onChangeFormData(newFormData); + + onSelectBlock(selectPrev ? previous : null); + }; + + const onMoveBlock = (dragIndex, hoverIndex) => { + const newFormData = moveBlock(properties, dragIndex, hoverIndex); + onChangeFormData(newFormData); + }; + + const defaultBlockWrapper = ({ draginfo }, editBlock, blockProps) => ( + + {editBlock} + + ); + + const editBlockWrapper = children || defaultBlockWrapper; + + return ( +
+
+ { + const { source, destination } = result; + if (!destination) { + return; + } + const newFormData = moveBlock( + properties, + source.index, + destination.index, + ); + onChangeFormData(newFormData); + return true; + }} + > + {(dragProps) => { + const { child, childId, index } = dragProps; + const blockProps = { + allowedBlocks, + showRestricted, + block: childId, + data: child, + handleKeyDown, + id: childId, + formTitle: title, + formDescription: description, + index, + manage, + onAddBlock, + onInsertBlock, + onChangeBlock, + onChangeField, + onDeleteBlock, + onFocusNextBlock, + onFocusPreviousBlock, + onMoveBlock, + onMutateBlock, + onSelectBlock, + pathname, + metadata, + properties, + blocksConfig, + selected: selectedBlock === childId, + multiSelected: multiSelected?.includes(childId), + type: child['@type'], + editable, + }; + return editBlockWrapper( + dragProps, + , + blockProps, + ); + }} + +
+
+ ); +}; + +export default BlocksForm; diff --git a/src/customizations/volto/components/manage/Blocks/Block/Edit.jsx b/src/customizations/volto/components/manage/Blocks/Block/Edit.jsx new file mode 100644 index 0000000..14ac753 --- /dev/null +++ b/src/customizations/volto/components/manage/Blocks/Block/Edit.jsx @@ -0,0 +1,223 @@ +/** + * Edit block. + * @module components/manage/Blocks/Block/Edit + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { defineMessages, injectIntl } from 'react-intl'; +import cx from 'classnames'; +import { setSidebarTab } from '@plone/volto/actions'; +import config from '@plone/volto/registry'; +import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser'; + +import { + SidebarPortal, + BlockSettingsSidebar, + BlockSettingsSchema, +} from '@plone/volto/components'; + +const messages = defineMessages({ + unknownBlock: { + id: 'Unknown Block', + defaultMessage: 'Unknown Block {block}', + }, +}); + +/** + * Edit block class. + * @class Edit + * @extends Component + */ +export class Edit extends Component { + /** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ + static propTypes = { + type: PropTypes.string.isRequired, + data: PropTypes.objectOf(PropTypes.any).isRequired, + // properties is mapped to formData, so it's not connected to changes of the object + properties: PropTypes.objectOf(PropTypes.any).isRequired, + selected: PropTypes.bool.isRequired, + multiSelected: PropTypes.bool, + index: PropTypes.number.isRequired, + id: PropTypes.string.isRequired, + manage: PropTypes.bool, + onMoveBlock: PropTypes.func.isRequired, + onDeleteBlock: PropTypes.func.isRequired, + editable: PropTypes.bool, + }; + + /** + * Default properties. + * @property {Object} defaultProps Default properties. + * @static + */ + static defaultProps = { + manage: false, + editable: true, + }; + + componentDidMount() { + const { type } = this.props; + const { blocksConfig = config.blocks.blocksConfig } = this.props; + + const blockHasOwnFocusManagement = + blocksConfig?.[type]?.['blockHasOwnFocusManagement'] || null; + if ( + !blockHasOwnFocusManagement && + this.props.selected && + this.blockNode.current + ) { + this.blockNode.current.focus(); + } + const tab = this.props.manage ? 1 : blocksConfig?.[type]?.sidebarTab || 0; + if (this.props.selected && this.props.editable) { + this.props.setSidebarTab(tab); + } + } + + UNSAFE_componentWillReceiveProps(nextProps) { + const { blocksConfig = config.blocks.blocksConfig } = this.props; + const { selected, type } = this.props; + const blockHasOwnFocusManagement = + blocksConfig?.[type]?.['blockHasOwnFocusManagement'] || null; + if ( + !blockHasOwnFocusManagement && + nextProps.selected && + selected !== nextProps.selected && + this.blockNode.current + ) { + this.blockNode.current.focus(); + } + if ( + ((!this.props.selected && nextProps.selected) || + type !== nextProps.type) && + this.props.editable + ) { + const tab = this.props.manage + ? 1 + : blocksConfig?.[nextProps.type]?.sidebarTab || 0; + this.props.setSidebarTab(tab); + } + } + + blockNode = React.createRef(); + + /** + * Render method. + * @method render + * @returns {string} Markup for the component. + */ + render() { + const { blocksConfig = config.blocks.blocksConfig } = this.props; + const { editable, type } = this.props; + + const disableNewBlocks = this.props.data?.disableNewBlocks; + + let Block = blocksConfig?.[type]?.['edit'] || null; + if ( + this.props.data?.readOnly || + (!editable && !config.blocks.showEditBlocksInBabelView) + ) { + Block = blocksConfig?.[type]?.['view'] || null; + } + const schema = blocksConfig?.[type]?.['schema'] || BlockSettingsSchema; + const blockHasOwnFocusManagement = + blocksConfig?.[type]?.['blockHasOwnFocusManagement'] || null; + + return ( + <> + {Block !== null ? ( +
{ + const isMultipleSelection = e.shiftKey || e.ctrlKey || e.metaKey; + !this.props.selected && + this.props.onSelectBlock( + this.props.id, + this.props.selected ? false : isMultipleSelection, + e, + ); + }} + onKeyDown={ + !(blockHasOwnFocusManagement || disableNewBlocks) + ? (e) => + this.props.handleKeyDown( + e, + this.props.index, + this.props.id, + this.blockNode.current, + ) + : null + } + className={cx(`block ${type}`, { + //TODO: add stretched group trigger for block border + 'stretched-group': + this.props.data.styles && + this.props.type === 'group' && + this.props.data.styles.stretch && + this.props.data.styles.stretch === 'stretch', + selected: this.props.selected || this.props.multiSelected, + multiSelected: this.props.multiSelected, + })} + style={{ outline: 'none' }} + ref={this.blockNode} + // The tabIndex is required for the keyboard navigation + /* eslint-disable jsx-a11y/no-noninteractive-tabindex */ + tabIndex={!blockHasOwnFocusManagement ? -1 : null} + > + + {this.props.manage && ( + + + + )} +
+ ) : ( +
+ !this.props.selected && this.props.onSelectBlock(this.props.id) + } + onKeyDown={ + !(blockHasOwnFocusManagement || disableNewBlocks) + ? (e) => + this.props.handleKeyDown( + e, + this.props.index, + this.props.id, + this.blockNode.current, + ) + : null + } + className={cx(`block ${type}`, { + selected: this.props.selected, + })} + style={{ outline: 'none' }} + ref={this.blockNode} + // The tabIndex is required for the keyboard navigation + tabIndex={-1} + > + {this.props.intl.formatMessage(messages.unknownBlock, { + block: type, + })} +
+ )} + + ); + } +} + +export default compose( + injectIntl, + withObjectBrowser, + connect(null, { setSidebarTab }), +)(Edit); diff --git a/src/customizations/volto/components/manage/Blocks/Block/EditBlockWrapper.jsx b/src/customizations/volto/components/manage/Blocks/Block/EditBlockWrapper.jsx new file mode 100644 index 0000000..e5b37c8 --- /dev/null +++ b/src/customizations/volto/components/manage/Blocks/Block/EditBlockWrapper.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { Icon } from '@plone/volto/components'; +import { blockHasValue } from '@plone/volto/helpers'; +import dragSVG from '@plone/volto/icons/drag.svg'; +import { Button } from 'semantic-ui-react'; +import includes from 'lodash/includes'; +import isBoolean from 'lodash/isBoolean'; +import { defineMessages, injectIntl } from 'react-intl'; +import config from '@plone/volto/registry'; + +import trashSVG from '@plone/volto/icons/delete.svg'; + +const messages = defineMessages({ + delete: { + id: 'delete', + defaultMessage: 'delete', + }, +}); + +const EditBlockWrapper = (props) => { + const hideHandler = (data) => { + return !!data.fixed || !(blockHasValue(data) && props.blockProps.editable); + }; + + const { intl, blockProps, draginfo, children } = props; + const { block, selected, type, onDeleteBlock, data, editable } = blockProps; + const visible = selected && !hideHandler(data); + + const required = isBoolean(data.required) + ? data.required + : includes(config.blocks.requiredBlocks, type); + + return ( +
+
+
+ +
+
+ {children} + {selected && !required && editable && ( + + )} +
+
+
+ ); +}; + +export default injectIntl(EditBlockWrapper); diff --git a/src/customizations/volto/components/manage/Blocks/Block/README.md b/src/customizations/volto/components/manage/Blocks/Block/README.md new file mode 100644 index 0000000..3242ec2 --- /dev/null +++ b/src/customizations/volto/components/manage/Blocks/Block/README.md @@ -0,0 +1,7 @@ +# Local Customizations + +To have control over parent block style when stretch is applied for the whole +section. The section needs to detect when it's stretched and adapt it's UI to +it. + +Edit.jsx has 'stretched-group' class added on a container div for that. diff --git a/src/customizations/volto/components/manage/Blocks/Block/Schema.jsx b/src/customizations/volto/components/manage/Blocks/Block/Schema.jsx new file mode 100644 index 0000000..094f0f5 --- /dev/null +++ b/src/customizations/volto/components/manage/Blocks/Block/Schema.jsx @@ -0,0 +1,47 @@ +const Schema = { + title: 'Block settings', + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: [ + 'placeholder', + 'required', + 'fixed', + 'disableNewBlocks', + 'readOnly', + ], + }, + ], + properties: { + placeholder: { + title: 'Helper text', + description: + 'A short hint that describes the expected value within this block', + type: 'string', + }, + required: { + title: 'Required', + description: "Don't allow deletion of this block", + type: 'boolean', + }, + fixed: { + title: 'Fixed position', + description: 'Disable drag & drop on this block', + type: 'boolean', + }, + disableNewBlocks: { + title: 'Disable new blocks', + description: 'Disable creation of new blocks after this block', + type: 'boolean', + }, + readOnly: { + title: 'Read-only', + description: 'Disable editing on this block', + type: 'boolean', + }, + }, + required: [], +}; + +export default Schema; diff --git a/src/customizations/volto/components/manage/Blocks/Block/Settings.jsx b/src/customizations/volto/components/manage/Blocks/Block/Settings.jsx new file mode 100644 index 0000000..efca273 --- /dev/null +++ b/src/customizations/volto/components/manage/Blocks/Block/Settings.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl } from 'react-intl'; +import InlineForm from '@plone/volto/components/manage/Form/InlineForm'; + +const Settings = ({ data, block, onChangeBlock, schema }) => { + return ( + { + onChangeBlock(block, { + ...data, + [id]: value, + }); + }} + formData={data} + /> + ); +}; + +Settings.propTypes = { + data: PropTypes.objectOf(PropTypes.any).isRequired, + block: PropTypes.string.isRequired, + onChangeBlock: PropTypes.func.isRequired, + schema: PropTypes.objectOf(PropTypes.any).isRequired, +}; + +export default injectIntl(Settings); diff --git a/src/customizations/volto/components/manage/Blocks/Block/Style.jsx b/src/customizations/volto/components/manage/Blocks/Block/Style.jsx new file mode 100644 index 0000000..4ea8f09 --- /dev/null +++ b/src/customizations/volto/components/manage/Blocks/Block/Style.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import cx from 'classnames'; + +export default ({ data, detached, children }) => { + return ( +
+
+ {children} +
+
+ ); +};