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));