diff --git a/src/components/manage/Blocks/List/View.jsx b/src/components/manage/Blocks/List/View.jsx new file mode 100644 index 00000000..a0be7481 --- /dev/null +++ b/src/components/manage/Blocks/List/View.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; +import cx from 'classnames'; +import qs from 'querystring'; +import { setQueryParam } from '@eeacms/volto-datablocks/actions'; +import './style.css'; + +const getLength = (length = 0, limit = 0) => { + if (!length) return 0; + return limit < length ? limit : length; +}; + +const View = (props) => { + const { data = {} } = props; + const provider_data = props.provider_data || {}; + const columns = getLength( + provider_data[Object.keys(provider_data)?.[0]]?.length, + data.limit, + ); + + return ( + <> + {props.mode === 'edit' ?

Connected list

: ''} +
+ {Array(Math.max(0, columns)) + .fill() + .map((_, column) => { + const queries = {}; + data.queries.forEach((query) => { + queries[query.paramToSet] = provider_data[query.param][column]; + }); + + return ( + { + props.setQueryParam({ + queryParam: { ...queries }, + }); + }} + > + {provider_data[data.value][column]} + + ); + })} +
+ + ); +}; + +export default compose( + connect( + (state) => ({ + query: state.router.location.search, + search: state.discodata_query.search, + }), + { setQueryParam }, + ), +)(View); diff --git a/src/components/manage/Blocks/List/index.js b/src/components/manage/Blocks/List/index.js new file mode 100644 index 00000000..6d7d66be --- /dev/null +++ b/src/components/manage/Blocks/List/index.js @@ -0,0 +1,17 @@ +import ListView from './View'; +import getSchema from './schema'; + +export default (config) => { + config.blocks.blocksConfig.custom_connected_block = { + ...config.blocks.blocksConfig.custom_connected_block, + blocks: { + ...config.blocks.blocksConfig.custom_connected_block.blocks, + list: { + view: ListView, + getSchema: getSchema, + title: 'List', + }, + }, + }; + return config; +}; diff --git a/src/components/manage/Blocks/List/schema.js b/src/components/manage/Blocks/List/schema.js new file mode 100644 index 00000000..7a766044 --- /dev/null +++ b/src/components/manage/Blocks/List/schema.js @@ -0,0 +1,76 @@ +const getQuerySchema = (data) => { + const choices = Object.keys(data).map((key) => [key, key]); + + return { + title: 'Query', + fieldsets: [ + { id: 'default', title: 'Default', fields: ['param', 'paramToSet'] }, + ], + properties: { + param: { + title: 'Param', + type: 'array', + choices, + }, + paramToSet: { + title: 'Param to set', + widget: 'textarea', + }, + }, + required: [], + }; +}; + +const getSchema = (props) => { + const data = props.provider_data || {}; + const choices = Object.keys(data).map((key) => [key, key]); + + return { + title: 'List', + + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: ['url', 'value', 'limit', 'className'], + }, + { + id: 'advanced', + title: 'Advanced', + fields: ['queries'], + }, + ], + + properties: { + url: { + title: 'Url', + widget: 'object_by_path', + }, + value: { + title: 'Value', + type: 'array', + choices, + }, + limit: { + title: 'Limit', + type: 'number', + minimum: '0', + onBlur: () => null, + onClick: () => null, + }, + className: { + title: 'Class', + widget: 'textarea', + }, + queries: { + title: 'Queries', + widget: 'object_list', + schema: getQuerySchema(data), + }, + }, + + required: [], + }; +}; + +export default getSchema; diff --git a/src/components/manage/Blocks/List/style.css b/src/components/manage/Blocks/List/style.css new file mode 100644 index 00000000..a2d248e6 --- /dev/null +++ b/src/components/manage/Blocks/List/style.css @@ -0,0 +1,8 @@ +.connected-list .column .label { + margin-bottom: 0; + font-weight: bold; +} + +.connected-list .column .value { + margin-bottom: 1rem; +} \ No newline at end of file diff --git a/src/components/manage/Blocks/Select/View.jsx b/src/components/manage/Blocks/Select/View.jsx new file mode 100644 index 00000000..46269bf8 --- /dev/null +++ b/src/components/manage/Blocks/Select/View.jsx @@ -0,0 +1,185 @@ +import React from 'react'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { Dropdown } from 'semantic-ui-react'; +import cx from 'classnames'; +import { setQueryParam } from '@eeacms/volto-datablocks/actions'; +import './style.css'; + +const items = [ + { + title: 'Item 1', + parentId: null, + }, + { + title: 'Item 2', + parentId: null, + items: [ + { + title: 'Item 2.1', + parentId: 'Item 2', + }, + { + title: 'Item 2.2', + parentId: 'Item 2', + }, + { + title: 'Item 2.3', + parentId: 'Item 2', + }, + ], + }, + { + title: 'Item 3', + parentId: null, + }, + { + title: 'Item 4', + parentId: null, + items: [ + { + title: 'Item 4.1', + parentId: 'Item 4', + items: [ + { + title: 'Item 4.1.1', + parentId: 'Item 4.1', + }, + ], + }, + { + title: 'Item 4.2', + parentId: 'Item 4', + }, + { + title: 'Item 4.3', + parentId: 'Item 4', + }, + ], + }, +]; + +const activeItems = ['Item 4', 'Item 4.1', 'Item 4.1.1', 'Item 2']; + +const getVisibility = (activeItems, item) => { + return activeItems.indexOf(item.parentId) !== -1; +}; + +let collapsing = { + title: 'Item 4', + parentId: null, + items: [ + { + title: 'Item 4.1', + parentId: 'Item 4', + items: [ + { + title: 'Item 4.1.1', + parentId: 'Item 4.1', + }, + ], + }, + { + title: 'Item 4.2', + parentId: 'Item 4', + }, + { + title: 'Item 4.3', + parentId: 'Item 4', + }, + ], +}; + +// let test = ['Item 4', 'Item 4.1', 'Item 4.1.1', 'Item 4.2', 'Item 4.3']; + +const getIds = (item) => { + if (!item.items) return [item.title]; + let children = []; + item.items.forEach((child) => { + children = [...children, ...getIds(child)]; + }); + return [item.title, ...children]; +}; + +const collapsingItems = getIds(collapsing); + +collapsingItems.forEach((item) => { + const index = activeItems.indexOf(item); + if (index !== -1) { + activeItems.splice(index, 1); + } +}); +console.log('HERE', activeItems); +// activeItems.splice(activeItems.indexOf(collapsing.title), 1); + +const View = (props) => { + const [value, setValue] = React.useState(null); + const { data = {} } = props; + const provider_data = props.provider_data || {}; + const columns = provider_data[Object.keys(provider_data)?.[0]]?.length || 0; + + const options = Array(Math.max(0, columns)) + .fill() + .map((_, column) => ({ + key: provider_data[data.value][column], + value: provider_data[data.value][column], + text: provider_data[data.text][column], + })); + + React.useEffect(() => { + if (!value && options.length) { + const cachedValue = data.queries.filter( + (query) => query.param === data.value, + )?.[0]?.paramToSet; + onChange(props.search[cachedValue] || options[0]?.value); + } + /* eslint-disable-next-line */ + }, [provider_data]); + + const onChange = (value) => { + let index; + const queries = {}; + for (let i = 0; i < options.length; i++) { + if (options[i].value === value) { + index = i; + break; + } + } + data.queries.forEach((query) => { + queries[query.paramToSet] = provider_data[query.param][index]; + }); + setValue(value); + props.setQueryParam({ + queryParam: { + ...queries, + }, + }); + }; + + return ( + <> + {props.mode === 'edit' ?

Connected select

: ''} +
+ { + onChange(data.value); + }} + placeholder={data.placeholder} + options={options} + value={value} + /> +
+ + ); +}; + +export default compose( + connect( + (state) => ({ + query: state.router.location.search, + search: state.discodata_query.search, + }), + { setQueryParam }, + ), +)(View); diff --git a/src/components/manage/Blocks/Select/index.js b/src/components/manage/Blocks/Select/index.js new file mode 100644 index 00000000..abd8b714 --- /dev/null +++ b/src/components/manage/Blocks/Select/index.js @@ -0,0 +1,17 @@ +import SelectView from './View'; +import getSchema from './schema'; + +export default (config) => { + config.blocks.blocksConfig.custom_connected_block = { + ...config.blocks.blocksConfig.custom_connected_block, + blocks: { + ...config.blocks.blocksConfig.custom_connected_block.blocks, + select: { + view: SelectView, + getSchema: getSchema, + title: 'Select', + }, + }, + }; + return config; +}; diff --git a/src/components/manage/Blocks/Select/schema.js b/src/components/manage/Blocks/Select/schema.js new file mode 100644 index 00000000..72a82735 --- /dev/null +++ b/src/components/manage/Blocks/Select/schema.js @@ -0,0 +1,78 @@ +const getQuerySchema = (data) => { + const choices = Object.keys(data).map((key) => [key, key]); + + return { + title: 'Query', + fieldsets: [ + { id: 'default', title: 'Default', fields: ['param', 'paramToSet'] }, + ], + properties: { + param: { + title: 'Param', + type: 'array', + choices, + }, + paramToSet: { + title: 'Param to set', + widget: 'textarea', + }, + }, + required: [], + }; +}; + +const getSchema = (props) => { + const data = props.provider_data || {}; + const choices = Object.keys(data).map((key) => [key, key]); + + return { + title: 'Select', + + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: ['url', 'value', 'text', 'placeholder', 'className'], + }, + { + id: 'advanced', + title: 'Advanced', + fields: ['queries'], + }, + ], + + properties: { + url: { + title: 'Url', + widget: 'object_by_path', + }, + value: { + title: 'Value', + type: 'array', + choices, + }, + text: { + title: 'Text', + type: 'array', + choices, + }, + placeholder: { + title: 'Placeholder', + widget: 'textarea', + }, + className: { + title: 'Class', + widget: 'textarea', + }, + queries: { + title: 'Queries', + widget: 'object_list', + schema: getQuerySchema(data), + }, + }, + + required: [], + }; +}; + +export default getSchema; diff --git a/src/components/manage/Blocks/Select/style.css b/src/components/manage/Blocks/Select/style.css new file mode 100644 index 00000000..a2d248e6 --- /dev/null +++ b/src/components/manage/Blocks/Select/style.css @@ -0,0 +1,8 @@ +.connected-list .column .label { + margin-bottom: 0; + font-weight: bold; +} + +.connected-list .column .value { + margin-bottom: 1rem; +} \ No newline at end of file diff --git a/src/components/manage/Blocks/SiteTableau/View.jsx b/src/components/manage/Blocks/SiteTableau/View.jsx index 211b929d..fae8f455 100644 --- a/src/components/manage/Blocks/SiteTableau/View.jsx +++ b/src/components/manage/Blocks/SiteTableau/View.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { compose } from 'redux'; +import { withRouter } from 'react-router'; import Tableau from '@eeacms/volto-tableau/Tableau/View'; import config from '@plone/volto/registry'; import { getLatestTableauVersion } from 'tableau-api-js'; @@ -24,7 +25,7 @@ const getDevice = (config, width) => { const View = (props) => { const [error, setError] = React.useState(null); - const [loaded, setLoaded] = React.useState(false); + const [loaded, setLoaded] = React.useState(null); const [mounted, setMounted] = React.useState(false); const [extraFilters, setExtraFilters] = React.useState({}); const { data = {}, query = {}, screen = {}, provider_data = null } = props; @@ -34,12 +35,13 @@ const View = (props) => { urlParameters = [], title = null, description = null, + autoScale = false, } = data; const version = props.data.version || config.settings.tableauVersion || getLatestTableauVersion(); - const device = getDevice(config, screen.page.width || Infinity); + const device = getDevice(config, screen.page?.width || Infinity); const breakpointUrl = breakpointUrls.filter( (breakpoint) => breakpoint.device === device, )[0]?.url; @@ -87,7 +89,7 @@ const View = (props) => { {...props} canUpdateUrl={!breakpointUrl} extraFilters={extraFilters} - extraOptions={{ device }} + extraOptions={{ device: autoScale ? 'desktop' : device }} error={error} loaded={loaded} setError={setError} @@ -106,7 +108,7 @@ const View = (props) => { }; export default compose( - connect((state, props) => ({ + connect((state) => ({ query: { ...(qs.parse(state.router.location?.search?.replace('?', '')) || {}), ...(state.discodata_query?.search || {}), @@ -114,4 +116,4 @@ export default compose( tableau: state.tableau, screen: state.screen, })), -)(connectBlockToProviderData(View)); +)(connectBlockToProviderData(withRouter(View))); diff --git a/src/config.js b/src/config.js index 86f4fea9..7ebf5d2d 100644 --- a/src/config.js +++ b/src/config.js @@ -102,6 +102,8 @@ import installRegulatorySiteDetails from '~/components/manage/Blocks/SiteBlocks/ import installRegulatoryPermits from '~/components/manage/Blocks/SiteBlocks/RegulatoryPermits'; import installRegulatoryBATConclusions from '~/components/manage/Blocks/SiteBlocks/RegulatoryBATConclusions'; import installExploreEprtr from '~/components/manage/Blocks/ExploreEprtr'; +import installList from '~/components/manage/Blocks/List'; +import installSelect from '~/components/manage/Blocks/Select'; import { installTableau, installExpendableList, @@ -392,6 +394,8 @@ export default function applyConfig(config) { installRegulatoryPermits, installRegulatoryBATConclusions, installExploreEprtr, + installList, + installSelect, installTableau, installExpendableList, installFolderListing, diff --git a/src/customizations/volto/components/manage/UniversalLink/UniversalLink.jsx b/src/customizations/volto/components/manage/UniversalLink/UniversalLink.jsx index df74a849..ae6fd78b 100644 --- a/src/customizations/volto/components/manage/UniversalLink/UniversalLink.jsx +++ b/src/customizations/volto/components/manage/UniversalLink/UniversalLink.jsx @@ -21,7 +21,7 @@ const UniversalLink = ({ children, className = null, title = null, - stripHash = true, + stripHash = false, delay = 0, offset = null, onClick = () => {}, @@ -57,7 +57,7 @@ const UniversalLink = ({ const isDownload = (!isExternal && url.includes('@@download')) || download; const appUrl = flattenToAppURL(url); - const path = !isDownload && stripHash ? appUrl.split('#')[0] : appUrl; + const path = stripHash ? appUrl.split('#')[0] : appUrl; const hash = appUrl.split('#')[1]; return isExternal ? ( @@ -74,7 +74,7 @@ const UniversalLink = ({ {children} ) : isDownload ? ( - + {children} ) : ( @@ -122,6 +122,10 @@ UniversalLink.propTypes = { '@id': PropTypes.string.isRequired, remoteUrl: PropTypes.string, //of plone @type 'Link' }), + stripHash: PropTypes.bool, + delay: PropTypes.number, // ms + offset: PropTypes.oneOfType([PropTypes.func, PropTypes.number]), + onClick: PropTypes.func, children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), PropTypes.node, diff --git a/src/customizations/volto/helpers/ScrollToTop/ScrollToTop.jsx b/src/customizations/volto/helpers/ScrollToTop/ScrollToTop.jsx index c87c7937..b35b2ebf 100644 --- a/src/customizations/volto/helpers/ScrollToTop/ScrollToTop.jsx +++ b/src/customizations/volto/helpers/ScrollToTop/ScrollToTop.jsx @@ -1,20 +1,118 @@ -import { memo } from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; +import config from '@plone/volto/registry'; -export const scrollTo = (scrollnumber = 0) => - window.requestAnimationFrame(() => { - window.scrollTo({ top: scrollnumber, left: 0, behavior: 'smooth' }); - }); +const isBrowser = typeof window !== 'undefined'; -const ScrollToTop = ({ children }) => { - return children; +if (isBrowser) { + // Handle scroll restoration manually + window.history.scrollRestoration = 'manual'; +} + +const DefaultKey = 'init-enter'; + +const TIMEOUT = 10000; + +const getScrollPage = () => { + let docScrollTop = 0; + if (document.documentElement && document.documentElement !== null) { + docScrollTop = document.documentElement.scrollTop; + } + return window.pageYOffset || docScrollTop; }; -export default withRouter( - memo(ScrollToTop, (prevProps, nextProps) => { - const { location: prevLocation, history } = prevProps; - const { location: nextLocation } = nextProps; - const hash = nextLocation.state?.volto_scroll_hash || ''; +/** + * @class ScrollToTop + * @extends {Component} + */ +class ScrollToTop extends React.Component { + clock; + timer = 0; + visitedUrl = new Map(); + /** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ + static propTypes = { + location: PropTypes.shape({ + pathname: PropTypes.string, + hash: PropTypes.string, + search: PropTypes.string, + }).isRequired, + history: PropTypes.object.isRequired, + content: PropTypes.object.isRequired, + children: PropTypes.node.isRequired, + }; + + constructor(props) { + super(props); + this.scrollTo = this.scrollTo.bind(this); + this.handleRouteChange = this.handleRouteChange.bind(this); + this.handlePopStateChange = this.handlePopStateChange.bind(this); + } + + componentDidMount() { + window.addEventListener('popstate', this.handlePopStateChange); + this.handleRouteChange(); + } + + componentWillUnmount() { + window.removeEventListener('popstate', this.handlePopStateChange); + clearInterval(this.clock); + } + /** + * @param {*} prevProps Previous Props + * @returns {null} Null + * @memberof ScrollToTop + */ + componentDidUpdate(prevProps) { + this.handleRouteChange(prevProps); + } + /** + * Scroll to top position triggered if content is loaded + * @method scrollTo + * @param {Number} top Top position + * @returns {null} Null + */ + scrollTo(top) { + clearInterval(this.clock); + this.timer = 0; + this.clock = setInterval(() => { + if (!this.props.content.get.loading || this.timer >= TIMEOUT) { + window.requestAnimationFrame(() => { + window.scrollTo({ + top, + left: 0, + behavior: config.settings.scrollBehavior, + }); + }); + clearInterval(this.clock); + this.timer = 0; + } + this.timer += 100; + }, 100); + } + /** + * Scroll to top position triggered if content is loaded + * @method handleRouteChange + * @param {Object} prevProps Previous Props + * @returns {null} Null + */ + handleRouteChange(prevProps) { + const { location: prevLocation = {}, history = {} } = prevProps || {}; + const { location: nextLocation } = this.props; + + if (prevLocation === nextLocation) return; + + const key = prevLocation.key || DefaultKey; + + const hash = + nextLocation.state?.volto_scroll_hash || + nextLocation.hash.substring(1) || + ''; const offset = nextLocation.state?.volto_scroll_offset || 0; const locationChanged = @@ -22,11 +120,42 @@ export default withRouter( const element = hash ? document.getElementById(hash) : null; + const scroll = getScrollPage(); + if (locationChanged && history.action !== 'POP') { - scrollTo(0); + this.scrollTo(0); + this.visitedUrl.set(key, scroll); } else if (element && history.action !== 'POP') { - scrollTo(element.offsetTop - offset); + this.scrollTo(element.offsetTop - offset); + this.visitedUrl.set(key, scroll); + } else { + this.visitedUrl.set(key, scroll); } - return false; - }), -); + return; + } + /** + * Scroll restoration on popstate event + * @method handlePopStateChange + * @param {PopStateEvent} e Pop state event + * @returns {null} Null + */ + handlePopStateChange(e) { + const { key = DefaultKey } = e.state || {}; + const existingRecord = this.visitedUrl.get(key); + + if (existingRecord !== undefined) { + this.scrollTo(existingRecord); + } + } + /** + * @returns {node} Children + * @memberof ScrollToTop + */ + render() { + return this.props.children; + } +} + +export default connect((state) => ({ + content: state.content, +}))(withRouter(ScrollToTop));