@eeacms/volto-n2k 1.0.16 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +1 -1
  3. package/src/components/manage/Blocks/CddaShape/View.jsx +5 -2
  4. package/src/components/manage/Blocks/HabitatDistribution/View.jsx +5 -2
  5. package/src/components/manage/Blocks/HabitatProtectedSites/View.jsx +5 -2
  6. package/src/components/manage/Blocks/HabitatsBanner/View.jsx +5 -5
  7. package/src/components/manage/Blocks/HabitatsBanner/style.less +9 -1
  8. package/src/components/manage/Blocks/Landing/DefalutView.jsx +4 -1
  9. package/src/components/manage/Blocks/SiteShape/View.jsx +5 -2
  10. package/src/components/manage/Blocks/SpeciesBanner/View.jsx +11 -7
  11. package/src/components/manage/Blocks/SpeciesBanner/style.less +8 -1
  12. package/src/components/manage/Blocks/SpeciesDistribution/View.jsx +12 -4
  13. package/src/components/manage/Blocks/SpeciesDistribution/index.js +7 -1
  14. package/src/components/manage/Blocks/SpeciesProtectedSites/View.jsx +6 -2
  15. package/src/components/theme/Navigation/Navigation.jsx +5 -4
  16. package/src/customizations/@eeacms/volto-group-block/components/manage/Blocks/Group/Edit.jsx +266 -0
  17. package/src/customizations/@eeacms/volto-group-block/components/manage/Blocks/Group/EditBlockWrapper.jsx +188 -0
  18. package/src/customizations/@eeacms/volto-group-block/components/manage/Blocks/Group/EditSchema.jsx +41 -0
  19. package/src/customizations/@eeacms/volto-group-block/components/manage/Blocks/Group/LayoutSchema.jsx +116 -0
  20. package/src/customizations/@eeacms/volto-group-block/components/manage/Blocks/Group/View.jsx +40 -0
  21. package/src/customizations/@eeacms/volto-group-block/components/manage/Blocks/Group/editor.less +99 -0
  22. package/src/index.js +34 -0
  23. package/src/less/styles.less +10 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [1.0.18](https://github.com/eea/volto-n2k/compare/1.0.17...1.0.18) - 25 February 2023
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat: conditional rendering for section group [Miu Razvan - [`6afd9a5`](https://github.com/eea/volto-n2k/commit/6afd9a536a520c51ad457127aa02f07fd135010e)]
12
+
13
+ #### :hammer_and_wrench: Others
14
+
15
+ - update [Miu Razvan - [`ab24f16`](https://github.com/eea/volto-n2k/commit/ab24f1624eb541d3ab492aa934c923f6cbdd245c)]
16
+ - uopdate label on habitats page [Claudia Ifrim - [`aa708e2`](https://github.com/eea/volto-n2k/commit/aa708e24b8f456407192cf1b889d893fd7b92f0e)]
17
+ ### [1.0.17](https://github.com/eea/volto-n2k/compare/1.0.16...1.0.17) - 24 February 2023
18
+
19
+ #### :hammer_and_wrench: Others
20
+
21
+ - eslint fix [Miu Razvan - [`417aa35`](https://github.com/eea/volto-n2k/commit/417aa3588b32920a3e378fad8dd72de6d0f02ca3)]
22
+ - species distribution map for birds group [Miu Razvan - [`7712d28`](https://github.com/eea/volto-n2k/commit/7712d28c0c5a79ffd33d4fc8d1d8e73af55937a2)]
7
23
  ### [1.0.16](https://github.com/eea/volto-n2k/compare/1.0.15...1.0.16) - 22 February 2023
8
24
 
9
25
  #### :hammer_and_wrench: Others
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-n2k",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "volto-n2k: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -9,6 +9,7 @@ import { getCddaShapeURL } from './index';
9
9
  import './style.less';
10
10
 
11
11
  const View = (props) => {
12
+ const dataFetched = React.useRef();
12
13
  const [options, setOptions] = React.useState({});
13
14
  const [vectorSource, setVectorSource] = useState(null);
14
15
  const [tileWMSSources, setTileWMSSources] = useState([]);
@@ -34,12 +35,14 @@ const View = (props) => {
34
35
  }, []);
35
36
 
36
37
  useEffect(() => {
37
- if (__SERVER__ || !vectorSource || !site_code[0]) return;
38
+ if (__SERVER__ || !vectorSource || !site_code[0] || dataFetched.current)
39
+ return;
38
40
  const esrijsonFormat = new format.EsriJSON();
39
41
  // Get site shape
40
42
  fetch(getCddaShapeURL(site_code[0])).then(function (response) {
41
43
  if (response.status !== 200) return;
42
44
  response.json().then(function (data) {
45
+ dataFetched.current = true;
43
46
  if (data.features && data.features.length > 0) {
44
47
  const features = esrijsonFormat.readFeatures(data);
45
48
  if (features.length > 0) {
@@ -55,7 +58,7 @@ const View = (props) => {
55
58
  });
56
59
  });
57
60
  /* eslint-disable-next-line */
58
- }, [site_code?.[0]]);
61
+ }, [vectorSource, site_code?.[0]]);
59
62
 
60
63
  if (__SERVER__ || !vectorSource) return '';
61
64
  return (
@@ -9,6 +9,7 @@ import { getHabitatDistributionURL } from './index';
9
9
  import './style.less';
10
10
 
11
11
  const View = (props) => {
12
+ const dataFetched = React.useRef();
12
13
  const [options, setOptions] = React.useState({});
13
14
  const [vectorSource, setVectorSource] = useState(null);
14
15
  const [tileWMSSources, setTileWMSSources] = useState([]);
@@ -34,12 +35,14 @@ const View = (props) => {
34
35
  }, []);
35
36
 
36
37
  useEffect(() => {
37
- if (__SERVER__ || !vectorSource || !code_2000[0]) return;
38
+ if (__SERVER__ || !vectorSource || !code_2000[0] || dataFetched.current)
39
+ return;
38
40
  const esrijsonFormat = new format.EsriJSON();
39
41
  // Get habitat location on sites
40
42
  fetch(getHabitatDistributionURL(code_2000[0])).then(function (response) {
41
43
  if (response.status !== 200) return;
42
44
  response.json().then(function (data) {
45
+ dataFetched.current = true;
43
46
  if (data.features && data.features.length > 0) {
44
47
  const features = esrijsonFormat.readFeatures(data);
45
48
  if (features.length > 0) {
@@ -55,7 +58,7 @@ const View = (props) => {
55
58
  });
56
59
  });
57
60
  /* eslint-disable-next-line */
58
- }, [code_2000?.[0]]);
61
+ }, [vectorSource, code_2000?.[0]]);
59
62
 
60
63
  if (__SERVER__ || !vectorSource) return '';
61
64
  return (
@@ -9,6 +9,7 @@ import { getHabitatProtectedSitesURL } from './index';
9
9
  import './style.less';
10
10
 
11
11
  const View = (props) => {
12
+ const dataFetched = React.useRef();
12
13
  const [options, setOptions] = React.useState({});
13
14
  const [vectorSource, setVectorSource] = useState(null);
14
15
  const [tileWMSSources, setTileWMSSources] = useState([]);
@@ -34,12 +35,14 @@ const View = (props) => {
34
35
  }, []);
35
36
 
36
37
  useEffect(() => {
37
- if (__SERVER__ || !vectorSource || !code_2000[0]) return;
38
+ if (__SERVER__ || !vectorSource || !code_2000[0] || dataFetched.current)
39
+ return;
38
40
  const esrijsonFormat = new format.EsriJSON();
39
41
  // Get habitat protected sites
40
42
  fetch(getHabitatProtectedSitesURL(code_2000[0])).then(function (response) {
41
43
  if (response.status !== 200) return;
42
44
  response.json().then(function (data) {
45
+ dataFetched.current = true;
43
46
  if (data.features && data.features.length > 0) {
44
47
  const features = esrijsonFormat.readFeatures(data);
45
48
  if (features.length > 0) {
@@ -55,7 +58,7 @@ const View = (props) => {
55
58
  });
56
59
  });
57
60
  /* eslint-disable-next-line */
58
- }, [code_2000?.[0]]);
61
+ }, [vectorSource, code_2000?.[0]]);
59
62
 
60
63
  if (__SERVER__ || !vectorSource) return '';
61
64
  return (
@@ -7,9 +7,9 @@ const View = (props) => {
7
7
  const {
8
8
  code_2000 = [],
9
9
  // habitat_description = [],
10
- habitat_type = [],
10
+ // habitat_type = [],
11
11
  // number_countries = [],
12
- number_sites = [],
12
+ // number_sites = [],
13
13
  scientific_name = [],
14
14
  } = provider_data;
15
15
 
@@ -21,17 +21,17 @@ const View = (props) => {
21
21
  <div className="habitat-metadata">
22
22
  <h2 className="name">{scientific_name[0]}</h2>
23
23
  <p className="info">
24
- {habitat_type[0]} habitat code {code_2000[0]}
24
+ Habitats Directive Annex I code&nbsp;&nbsp;&nbsp;{code_2000[0]}
25
25
  </p>
26
26
  <br />
27
- {number_sites[0] && (
27
+ {/* {number_sites[0] && (
28
28
  <>
29
29
  <h3 style={{ marginBottom: '0.15rem' }}>{number_sites[0]}</h3>
30
30
  <h4 className="radjhan-normal">
31
31
  NATURA 2000 SITES PROTECTING THIS HABITAT
32
32
  </h4>
33
33
  </>
34
- )}
34
+ )} */}
35
35
  </div>
36
36
  </div>
37
37
  </Container>
@@ -12,6 +12,7 @@ div#view .habitat-banner-details .ui.container > * {
12
12
  display: flex;
13
13
  align-items: flex-start;
14
14
  justify-content: space-between;
15
+ gap: 1rem;
15
16
 
16
17
  @media only screen and (max-width: 765px) {
17
18
  flex-flow: column;
@@ -27,9 +28,15 @@ div#view .habitat-banner-details .ui.container > * {
27
28
  margin-bottom: 0 !important;
28
29
  color: #fff !important;
29
30
  font-family: inherit;
31
+
32
+ font-family: RajdhaniB, 'Helvetica Neue', Arial, Helvetica, sans-serif;
30
33
  font-size: 54px;
31
34
  line-height: 54px;
32
35
  text-transform: uppercase;
36
+
37
+ > * {
38
+ font-family: RajdhaniB, 'Helvetica Neue', Arial, Helvetica, sans-serif;
39
+ }
33
40
  }
34
41
 
35
42
  h3 {
@@ -52,7 +59,8 @@ div#view .habitat-banner-details .ui.container > * {
52
59
  text-transform: uppercase;
53
60
  }
54
61
 
55
- .info {
62
+ .info,
63
+ .info > * {
56
64
  font-family: 'RajdhaniR', 'Helvetica Neue', Arial, Helvetica, sans-serif;
57
65
  font-size: 18px;
58
66
  font-weight: 600;
@@ -15,7 +15,10 @@ const DefaultView = (props) => {
15
15
  const currentLang = props.localStorage.get('N2K_LANGUAGE');
16
16
 
17
17
  useEffect(() => {
18
- if (removeTrailingSlash(props.location.pathname) === '/natura2000') {
18
+ if (
19
+ props.location?.pathname &&
20
+ removeTrailingSlash(props.location.pathname) === '/natura2000'
21
+ ) {
19
22
  props.history.push(`/natura2000/${currentLang || 'en'}`);
20
23
  }
21
24
  /* eslint-disable-next-line */
@@ -9,6 +9,7 @@ import { getSiteShapeURL } from './index';
9
9
  import './style.less';
10
10
 
11
11
  const View = (props) => {
12
+ const dataFetched = React.useRef();
12
13
  const [options, setOptions] = React.useState({});
13
14
  const [vectorSource, setVectorSource] = useState(null);
14
15
  const [tileWMSSources, setTileWMSSources] = useState([]);
@@ -34,12 +35,14 @@ const View = (props) => {
34
35
  }, []);
35
36
 
36
37
  useEffect(() => {
37
- if (__SERVER__ || !vectorSource || !site_code[0]) return;
38
+ if (__SERVER__ || !vectorSource || !site_code[0] || dataFetched.current)
39
+ return;
38
40
  const esrijsonFormat = new format.EsriJSON();
39
41
  // Get site shape
40
42
  fetch(getSiteShapeURL(site_code[0])).then(function (response) {
41
43
  if (response.status !== 200) return;
42
44
  response.json().then(function (data) {
45
+ dataFetched.current = true;
43
46
  if (data.features && data.features.length > 0) {
44
47
  const features = esrijsonFormat.readFeatures(data);
45
48
  if (features.length > 0) {
@@ -55,7 +58,7 @@ const View = (props) => {
55
58
  });
56
59
  });
57
60
  /* eslint-disable-next-line */
58
- }, [site_code?.[0]]);
61
+ }, [vectorSource, site_code?.[0]]);
59
62
 
60
63
  if (__SERVER__ || !vectorSource) return '';
61
64
  return (
@@ -19,7 +19,7 @@ const View = (props) => {
19
19
  code_2000 = [],
20
20
  id_eunis = [],
21
21
  license = [],
22
- number_sites = [],
22
+ // number_sites = [],
23
23
  picture_url = [],
24
24
  scientific_name = [],
25
25
  source = [],
@@ -51,26 +51,30 @@ const View = (props) => {
51
51
  <div className="species-metadata">
52
52
  <h2 className="name">
53
53
  {common_name[0] ? common_name[0] + ' - ' : ''}{' '}
54
- <span style={{ fontStyle: 'italic' }}>{scientific_name[0]}</span>
54
+ <span style={{ fontStyle: 'italic', textTransform: 'none' }}>
55
+ {scientific_name[0]}
56
+ </span>
55
57
  </h2>
56
58
  {author[0] && (
57
59
  <p
58
60
  className="info radjhan-bold"
59
- style={{ marginBottom: '0.15rem' }}
61
+ style={{ marginBottom: '0.5rem' }}
60
62
  >
61
63
  {author[0]}
62
64
  </p>
63
65
  )}
64
66
  {code_2000[0] && (
65
- <p className="info">Natura 2000 species code {code_2000[0]}</p>
67
+ <p className="info">
68
+ Natura 2000 species code&nbsp;&nbsp;&nbsp;{code_2000[0]}
69
+ </p>
66
70
  )}
67
- <br />
71
+ {/* <br />
68
72
  {number_sites[0] && (
69
- <h3 style={{ marginBottom: '0.15rem' }}>{number_sites[0]}</h3>
73
+ <h3 style={{ marginBottom: '0.5rem' }}>{number_sites[0]}</h3>
70
74
  )}
71
75
  <h4 className="radjhan-normal">
72
76
  NATURA 2000 SITES PROTECTING THIS SPECIES
73
- </h4>
77
+ </h4> */}
74
78
  </div>
75
79
  <div
76
80
  className={cx('species-pictures', {
@@ -14,6 +14,7 @@ div#view .species-banner-details .ui.container > * {
14
14
  flex-flow: row;
15
15
  align-items: center;
16
16
  justify-content: space-between;
17
+ gap: 2rem;
17
18
 
18
19
  @media only screen and (max-width: 800px) {
19
20
  flex-flow: column;
@@ -25,9 +26,14 @@ div#view .species-banner-details .ui.container > * {
25
26
  margin-bottom: 0 !important;
26
27
  color: #fff !important;
27
28
  font-family: inherit;
29
+ font-family: RajdhaniB, 'Helvetica Neue', Arial, Helvetica, sans-serif;
28
30
  font-size: 54px;
29
31
  line-height: 54px;
30
32
  text-transform: uppercase;
33
+
34
+ > * {
35
+ font-family: RajdhaniB, 'Helvetica Neue', Arial, Helvetica, sans-serif;
36
+ }
31
37
  }
32
38
 
33
39
  h3 {
@@ -114,7 +120,8 @@ div#view .species-banner-details .ui.container > * {
114
120
 
115
121
  &:not(.with-slider) {
116
122
  display: flex;
117
- align-items: center;
123
+ flex-flow: column;
124
+ justify-content: center;
118
125
 
119
126
  .picture-wrapper {
120
127
  height: fit-content;
@@ -9,12 +9,13 @@ import { getSpeciesDistributionURL } from './index';
9
9
  import './style.less';
10
10
 
11
11
  const View = (props) => {
12
+ const dataFetched = React.useRef();
12
13
  const [options, setOptions] = React.useState({});
13
14
  const [vectorSource, setVectorSource] = useState(null);
14
15
  const [tileWMSSources, setTileWMSSources] = useState([]);
15
16
  const { extent, format, proj, style, source } = openlayers;
16
17
  const provider_data = props.provider_data || {};
17
- const { code_2000 = [] } = provider_data;
18
+ const { code_2000 = [], species_group_name = [] } = provider_data;
18
19
 
19
20
  useEffect(() => {
20
21
  if (__SERVER__) return;
@@ -34,12 +35,19 @@ const View = (props) => {
34
35
  }, []);
35
36
 
36
37
  useEffect(() => {
37
- if (__SERVER__ || !vectorSource || !code_2000[0]) return;
38
+ if (__SERVER__ || !vectorSource || !code_2000[0] || dataFetched.current)
39
+ return;
38
40
  const esrijsonFormat = new format.EsriJSON();
39
41
  // Get species location on sites
40
- fetch(getSpeciesDistributionURL(code_2000[0])).then(function (response) {
42
+ fetch(
43
+ getSpeciesDistributionURL(
44
+ code_2000[0],
45
+ species_group_name[0] === 'Birds',
46
+ ),
47
+ ).then(function (response) {
41
48
  if (response.status !== 200) return;
42
49
  response.json().then(function (data) {
50
+ dataFetched.current = true;
43
51
  if (data.features && data.features.length > 0) {
44
52
  const features = esrijsonFormat.readFeatures(data);
45
53
  if (features.length > 0) {
@@ -55,7 +63,7 @@ const View = (props) => {
55
63
  });
56
64
  });
57
65
  /* eslint-disable-next-line */
58
- }, [code_2000?.[0]]);
66
+ }, [vectorSource, code_2000?.[0]]);
59
67
 
60
68
  if (__SERVER__ || !vectorSource) return '';
61
69
  return (
@@ -1,7 +1,13 @@
1
1
  import SpeciesDistributionView from './View';
2
2
  import getSchema from './schema';
3
3
 
4
- export function getSpeciesDistributionURL(code_2000) {
4
+ export function getSpeciesDistributionURL(code_2000, isBird) {
5
+ if (isBird) {
6
+ return encodeURI(
7
+ `https://bio.discomap.eea.europa.eu/arcgis/rest/services/Article_12/BirdsDirective_ART_12_version_2020_08_public_VM/MapServer/3/query?f=json&where=speciescode LIKE '%${code_2000.toUpperCase()}%'&returnGeometry=true&spatialRel=esriSpatialRelIntersects&outFields=speciescode&outSR=102100`,
8
+ );
9
+ }
10
+
5
11
  return encodeURI(
6
12
  `https://bio.discomap.eea.europa.eu/arcgis/rest/services/Article17/HabitatsDirective_ART_17_WMS_version_2020_08_public/MapServer/3/query?f=json&where=speciescode LIKE '%${code_2000.toUpperCase()}%'&returnGeometry=true&spatialRel=esriSpatialRelIntersects&outFields=speciescode&outSR=102100`,
7
13
  );
@@ -9,6 +9,7 @@ import { getSpeciesProtectedSitesURL } from './index';
9
9
  import './style.less';
10
10
 
11
11
  const View = (props) => {
12
+ const dataFetched = React.useRef();
12
13
  const [options, setOptions] = React.useState({});
13
14
  const [vectorSource, setVectorSource] = useState(null);
14
15
  const [tileWMSSources, setTileWMSSources] = useState([]);
@@ -34,12 +35,15 @@ const View = (props) => {
34
35
  }, []);
35
36
 
36
37
  useEffect(() => {
37
- if (__SERVER__ || !vectorSource || !code_2000[0]) return;
38
+ if (__SERVER__ || !vectorSource || !code_2000[0] || dataFetched.current)
39
+ return;
40
+
38
41
  const esrijsonFormat = new format.EsriJSON();
39
42
  // Get species protected sites
40
43
  fetch(getSpeciesProtectedSitesURL(code_2000[0])).then(function (response) {
41
44
  if (response.status !== 200) return;
42
45
  response.json().then(function (data) {
46
+ dataFetched.current = true;
43
47
  if (data.features && data.features.length > 0) {
44
48
  const features = esrijsonFormat.readFeatures(data);
45
49
  if (features.length > 0) {
@@ -55,7 +59,7 @@ const View = (props) => {
55
59
  });
56
60
  });
57
61
  /* eslint-disable-next-line */
58
- }, [code_2000?.[0]]);
62
+ }, [vectorSource, code_2000?.[0]]);
59
63
 
60
64
  if (__SERVER__ || !vectorSource) return '';
61
65
  return (
@@ -417,8 +417,7 @@ class Navigation extends Component {
417
417
  )}
418
418
  </Dropdown>
419
419
  ) : (
420
- <Link
421
- to={flatUrl === '' ? '/' : flatUrl}
420
+ <div
422
421
  key={flatUrl}
423
422
  className={
424
423
  this.isActive(flatUrl)
@@ -426,8 +425,10 @@ class Navigation extends Component {
426
425
  : 'item firstLevel'
427
426
  }
428
427
  >
429
- {item.title}
430
- </Link>
428
+ <Link to={flatUrl === '' ? '/' : flatUrl}>
429
+ {item.title}
430
+ </Link>
431
+ </div>
431
432
  );
432
433
  })}
433
434
  </>
@@ -0,0 +1,266 @@
1
+ import React, { useState } from 'react';
2
+ import { isEmpty } from 'lodash';
3
+ import {
4
+ BlocksForm,
5
+ SidebarPortal,
6
+ Icon,
7
+ BlockDataForm,
8
+ } from '@plone/volto/components';
9
+ import { emptyBlocksForm } from '@plone/volto/helpers';
10
+ import delightedSVG from '@plone/volto/icons/delighted.svg';
11
+ import dissatisfiedSVG from '@plone/volto/icons/dissatisfied.svg';
12
+ import PropTypes from 'prop-types';
13
+ import { Button, Segment } from 'semantic-ui-react';
14
+ import EditBlockWrapper from './EditBlockWrapper';
15
+ import EditSchema from './EditSchema';
16
+ import helpSVG from '@plone/volto/icons/help.svg';
17
+ import cx from 'classnames';
18
+ import './editor.less';
19
+
20
+ const Edit = (props) => {
21
+ const {
22
+ block,
23
+ data,
24
+ onChangeBlock,
25
+ onChangeField,
26
+ pathname,
27
+ selected,
28
+ manage,
29
+ formDescription,
30
+ } = props;
31
+
32
+ const metadata = props.metadata || props.properties;
33
+ const data_blocks = data?.data?.blocks;
34
+ const properties = isEmpty(data_blocks) ? emptyBlocksForm() : data.data;
35
+
36
+ const [selectedBlock, setSelectedBlock] = useState(
37
+ properties.blocks_layout.items[0],
38
+ );
39
+
40
+ React.useEffect(() => {
41
+ if (
42
+ isEmpty(data_blocks) &&
43
+ properties.blocks_layout.items[0] !== selectedBlock
44
+ ) {
45
+ setSelectedBlock(properties.blocks_layout.items[0]);
46
+ onChangeBlock(block, {
47
+ ...data,
48
+ data: properties,
49
+ });
50
+ }
51
+ }, [onChangeBlock, properties, selectedBlock, block, data, data_blocks]);
52
+
53
+ const blockState = {};
54
+ let charCount = 0;
55
+
56
+ /**
57
+ * Count the number of characters that are anything except using Regex
58
+ * @param {string} paragraph
59
+ * @returns
60
+ */
61
+ const countCharsWithoutSpaces = (paragraph) => {
62
+ const regex = /[^\s\\]/g;
63
+
64
+ return (paragraph.match(regex) || []).length;
65
+ };
66
+
67
+ /**
68
+ * Count the number of characters
69
+ * @param {string} paragraph
70
+ * @returns
71
+ */
72
+ const countCharsWithSpaces = (paragraph) => {
73
+ return paragraph?.length || 0;
74
+ };
75
+
76
+ /**
77
+ * Recursively look for any block that contains text or plaintext
78
+ * @param {Object} blocksObject
79
+ * @returns
80
+ */
81
+ const countTextInBlocks = (blocksObject) => {
82
+ let groupCharCount = 0;
83
+ if (!props.data.maxChars) {
84
+ return groupCharCount;
85
+ }
86
+
87
+ Object.keys(blocksObject).forEach((blockId) => {
88
+ const foundText = blocksObject[blockId]?.plaintext
89
+ ? blocksObject[blockId]?.plaintext
90
+ : blocksObject[blockId]?.text?.blocks[0]?.text
91
+ ? blocksObject[blockId].text.blocks[0].text
92
+ : blocksObject[blockId]?.data?.blocks
93
+ ? countTextInBlocks(blocksObject[blockId]?.data?.blocks)
94
+ : blocksObject[blockId]?.blocks
95
+ ? countTextInBlocks(blocksObject[blockId]?.blocks)
96
+ : '';
97
+ const resultText =
98
+ typeof foundText === 'string' || foundText instanceof String
99
+ ? foundText
100
+ : '';
101
+
102
+ groupCharCount += props.data.ignoreSpaces
103
+ ? countCharsWithoutSpaces(resultText)
104
+ : countCharsWithSpaces(resultText);
105
+ });
106
+
107
+ return groupCharCount;
108
+ };
109
+
110
+ const showCharCounter = () => {
111
+ if (data_blocks) {
112
+ charCount = countTextInBlocks(data_blocks);
113
+ }
114
+ };
115
+ showCharCounter();
116
+
117
+ const counterClass =
118
+ charCount < Math.ceil(props.data.maxChars / 1.05)
119
+ ? 'info'
120
+ : charCount < props.data.maxChars
121
+ ? 'warning'
122
+ : 'danger';
123
+
124
+ const counterComponent = props.data.maxChars ? (
125
+ <p
126
+ className={cx('counter', counterClass)}
127
+ onClick={() => {
128
+ setSelectedBlock();
129
+ props.setSidebarTab(1);
130
+ }}
131
+ aria-hidden="true"
132
+ >
133
+ {props.data.maxChars - charCount < 0 ? (
134
+ <>
135
+ <span>{`${
136
+ charCount - props.data.maxChars
137
+ } characters over the limit`}</span>
138
+ <Icon name={dissatisfiedSVG} size="24px" />
139
+ </>
140
+ ) : (
141
+ <>
142
+ <span>{`${
143
+ props.data.maxChars - charCount
144
+ } characters remaining out of ${props.data.maxChars}`}</span>
145
+ <Icon name={delightedSVG} size="24px" />
146
+ </>
147
+ )}
148
+ </p>
149
+ ) : null;
150
+
151
+ // Get editing instructions from block settings or props
152
+ let instructions = data?.instructions?.data || data?.instructions;
153
+ if (!instructions || instructions === '<p><br/></p>') {
154
+ instructions = formDescription;
155
+ }
156
+
157
+ return (
158
+ <fieldset className="section-block">
159
+ <legend
160
+ onClick={() => {
161
+ setSelectedBlock();
162
+ props.setSidebarTab(1);
163
+ }}
164
+ aria-hidden="true"
165
+ >
166
+ {data.title || 'Section'}
167
+ </legend>
168
+ <BlocksForm
169
+ metadata={metadata}
170
+ properties={properties}
171
+ manage={manage}
172
+ selectedBlock={selected ? selectedBlock : null}
173
+ allowedBlocks={data.allowedBlocks}
174
+ title={data.placeholder}
175
+ description={instructions}
176
+ onSelectBlock={(id) => {
177
+ setSelectedBlock(id);
178
+ }}
179
+ onChangeFormData={(newFormData) => {
180
+ onChangeBlock(block, {
181
+ ...data,
182
+ data: newFormData,
183
+ });
184
+ }}
185
+ onChangeField={(id, value) => {
186
+ if (['blocks', 'blocks_layout'].indexOf(id) > -1) {
187
+ blockState[id] = value;
188
+ onChangeBlock(block, {
189
+ ...data,
190
+ data: {
191
+ ...data.data,
192
+ ...blockState,
193
+ },
194
+ });
195
+ } else {
196
+ onChangeField(id, value);
197
+ }
198
+ }}
199
+ pathname={pathname}
200
+ >
201
+ {({ draginfo }, editBlock, blockProps) => (
202
+ <EditBlockWrapper
203
+ draginfo={draginfo}
204
+ blockProps={blockProps}
205
+ disabled={data.disableInnerButtons}
206
+ extraControls={
207
+ <>
208
+ {instructions && (
209
+ <>
210
+ <Button
211
+ icon
212
+ basic
213
+ title="Section help"
214
+ onClick={() => {
215
+ setSelectedBlock();
216
+ const tab = manage ? 0 : 1;
217
+ props.setSidebarTab(tab);
218
+ }}
219
+ >
220
+ <Icon name={helpSVG} className="" size="19px" />
221
+ </Button>
222
+ </>
223
+ )}
224
+ </>
225
+ }
226
+ >
227
+ {editBlock}
228
+ </EditBlockWrapper>
229
+ )}
230
+ </BlocksForm>
231
+
232
+ {counterComponent}
233
+ <SidebarPortal selected={selected && !selectedBlock}>
234
+ {instructions && (
235
+ <Segment attached>
236
+ <div dangerouslySetInnerHTML={{ __html: instructions }} />
237
+ </Segment>
238
+ )}
239
+ {!data?.readOnlySettings && (
240
+ <BlockDataForm
241
+ schema={EditSchema}
242
+ title="Section (Group) settings"
243
+ formData={data}
244
+ onChangeField={(id, value) => {
245
+ props.onChangeBlock(props.block, {
246
+ ...props.data,
247
+ [id]: value,
248
+ });
249
+ }}
250
+ />
251
+ )}
252
+ </SidebarPortal>
253
+ </fieldset>
254
+ );
255
+ };
256
+
257
+ Edit.propTypes = {
258
+ block: PropTypes.string.isRequired,
259
+ data: PropTypes.object.isRequired,
260
+ onChangeBlock: PropTypes.func.isRequired,
261
+ pathname: PropTypes.string.isRequired,
262
+ selected: PropTypes.bool.isRequired,
263
+ manage: PropTypes.bool.isRequired,
264
+ };
265
+
266
+ export default Edit;
@@ -0,0 +1,188 @@
1
+ import React from 'react';
2
+ import { Icon, BlockChooser } from '@plone/volto/components';
3
+ import { blockHasValue } from '@plone/volto/helpers';
4
+ import config from '@plone/volto/registry';
5
+ import { Button } from 'semantic-ui-react';
6
+ import includes from 'lodash/includes';
7
+ import isBoolean from 'lodash/isBoolean';
8
+ import { defineMessages, injectIntl } from 'react-intl';
9
+ import { doesNodeContainClick } from 'semantic-ui-react/dist/commonjs/lib';
10
+ import cx from 'classnames';
11
+
12
+ import dragSVG from '@plone/volto/icons/drag.svg';
13
+ import addSVG from '@plone/volto/icons/circle-plus.svg';
14
+ import trashSVG from '@plone/volto/icons/delete.svg';
15
+
16
+ const messages = defineMessages({
17
+ unknownBlock: {
18
+ id: 'Unknown Block',
19
+ defaultMessage: 'Unknown Block {block}',
20
+ },
21
+ delete: {
22
+ id: 'delete',
23
+ defaultMessage: 'delete',
24
+ },
25
+ });
26
+
27
+ class EditBlockWrapper extends React.Component {
28
+ constructor(props) {
29
+ super(props);
30
+ this.state = {
31
+ addNewBlockOpened: false,
32
+ };
33
+ }
34
+
35
+ componentDidMount() {
36
+ document.addEventListener('mousedown', this.handleClickOutside, false);
37
+ }
38
+
39
+ componentWillUnmount() {
40
+ document.removeEventListener('mousedown', this.handleClickOutside);
41
+ }
42
+
43
+ handleClickOutside = (e) => {
44
+ if (
45
+ this.blockNode.current &&
46
+ doesNodeContainClick(this.blockNode.current, e)
47
+ )
48
+ return;
49
+
50
+ if (this.state.addNewBlockOpened) {
51
+ this.setState({
52
+ addNewBlockOpened: false,
53
+ });
54
+ return true;
55
+ }
56
+ };
57
+
58
+ blockNode = React.createRef();
59
+
60
+ render() {
61
+ const {
62
+ intl,
63
+ blockProps,
64
+ draginfo,
65
+ extraControls,
66
+ disabled,
67
+ children,
68
+ } = this.props;
69
+
70
+ const {
71
+ allowedBlocks,
72
+ block,
73
+ data,
74
+ onSelectBlock,
75
+ onDeleteBlock,
76
+ onMutateBlock,
77
+ onInsertBlock,
78
+ selected,
79
+ } = blockProps;
80
+ const type = data['@type'];
81
+ const { disableNewBlocks } = data;
82
+ const dragVisible = !data.fixed;
83
+ const visible = selected;
84
+
85
+ const required = isBoolean(data.required)
86
+ ? data.required
87
+ : includes(config.blocks.requiredBlocks, type);
88
+
89
+ // Get editing instructions from block settings or props
90
+ let instructions = data?.instructions?.data || data?.instructions;
91
+ if (!instructions || instructions === '<p><br/></p>') {
92
+ instructions = '';
93
+ }
94
+
95
+ return (
96
+ <div ref={this.blockNode}>
97
+ <div
98
+ ref={draginfo?.innerRef}
99
+ {...(selected ? draginfo?.draggableProps : null)}
100
+ className={`block-editor-${data['@type']}`}
101
+ >
102
+ {(!selected || !visible || disabled) && (
103
+ <div
104
+ style={{
105
+ display: 'none',
106
+ // keep react-beautiful-dnd happy
107
+ }}
108
+ {...draginfo.dragHandleProps}
109
+ ></div>
110
+ )}
111
+ {visible && (
112
+ <div className="block-toolbar">
113
+ {instructions ? extraControls : ''}
114
+
115
+ {!disabled && (
116
+ <>
117
+ <div
118
+ style={{
119
+ display: dragVisible ? 'inline-block' : 'none',
120
+ }}
121
+ {...draginfo.dragHandleProps}
122
+ className="drag handle wrapper-group-block"
123
+ >
124
+ <Button icon basic title="Drag and drop">
125
+ <Icon name={dragSVG} size="19px" />
126
+ </Button>
127
+ </div>
128
+
129
+ {!disableNewBlocks && !blockHasValue(data) && (
130
+ <Button
131
+ icon
132
+ basic
133
+ title="Add block"
134
+ onClick={() => {
135
+ this.setState({
136
+ addNewBlockOpened: !this.state.addNewBlockOpened,
137
+ });
138
+ }}
139
+ className="group-block-add-button"
140
+ >
141
+ <Icon name={addSVG} className="" size="19px" />
142
+ </Button>
143
+ )}
144
+ {!required && (
145
+ <Button
146
+ icon
147
+ basic
148
+ title="Remove block"
149
+ onClick={() => onDeleteBlock(block)}
150
+ className="delete-button-group-block"
151
+ aria-label={intl.formatMessage(messages.delete)}
152
+ >
153
+ <Icon name={trashSVG} size="19px" />
154
+ </Button>
155
+ )}
156
+ {this.state.addNewBlockOpened && (
157
+ <BlockChooser
158
+ onMutateBlock={(id, value) => {
159
+ onMutateBlock(id, value);
160
+ this.setState({ addNewBlockOpened: false });
161
+ }}
162
+ onInsertBlock={(id, value) => {
163
+ onSelectBlock(onInsertBlock(id, value));
164
+ this.setState({ addNewBlockOpened: false });
165
+ }}
166
+ currentBlock={block}
167
+ allowedBlocks={allowedBlocks}
168
+ />
169
+ )}
170
+ </>
171
+ )}
172
+ </div>
173
+ )}
174
+
175
+ <div
176
+ className={cx('ui drag block wrapper inner', type, {
177
+ multiSelected: this.props.multiSelected,
178
+ })}
179
+ >
180
+ {children}
181
+ </div>
182
+ </div>
183
+ </div>
184
+ );
185
+ }
186
+ }
187
+
188
+ export default injectIntl(EditBlockWrapper);
@@ -0,0 +1,41 @@
1
+ const Schema = {
2
+ title: 'Section block',
3
+ fieldsets: [
4
+ {
5
+ id: 'default',
6
+ title: 'Default',
7
+ fields: ['title', 'as', 'condition'],
8
+ },
9
+ ],
10
+ properties: {
11
+ title: {
12
+ title: 'Title',
13
+ description: 'Section friendly name',
14
+ type: 'string',
15
+ },
16
+ as: {
17
+ title: 'HTML5 element',
18
+ description: 'Select HTML5 element to be used for this block',
19
+ type: 'string',
20
+ factory: 'Choice',
21
+ default: 'div',
22
+ choices: [
23
+ ['div', 'div'],
24
+ ['section', 'section'],
25
+ ['article', 'article'],
26
+ ['aside', 'aside'],
27
+ ['details', 'details'],
28
+ ],
29
+ },
30
+ condition: {
31
+ title: 'Condition',
32
+ choices: [
33
+ ['bird', 'Is bird'],
34
+ ['species', 'Is not bird'],
35
+ ],
36
+ },
37
+ },
38
+ required: [],
39
+ };
40
+
41
+ export default Schema;
@@ -0,0 +1,116 @@
1
+ const Schema = {
2
+ title: 'Section (Group) settings',
3
+ fieldsets: [
4
+ {
5
+ id: 'default',
6
+ title: 'Default',
7
+ fields: [
8
+ 'title',
9
+ 'placeholder',
10
+ 'instructions',
11
+ 'allowedBlocks',
12
+ 'as',
13
+ 'maxChars',
14
+ 'ignoreSpaces',
15
+ 'readOnlySettings',
16
+ 'disableInnerButtons',
17
+ 'required',
18
+ 'fixed',
19
+ 'fixedLayout',
20
+ 'disableNewBlocks',
21
+ 'readOnly',
22
+ ],
23
+ },
24
+ ],
25
+ properties: {
26
+ title: {
27
+ title: 'Title',
28
+ description: 'Section friendly name',
29
+ type: 'string',
30
+ },
31
+ allowedBlocks: {
32
+ title: 'Allowed blocks',
33
+ description: 'Allow only the following blocks types',
34
+ type: 'array',
35
+ items: {
36
+ choices: [],
37
+ },
38
+ },
39
+ placeholder: {
40
+ title: 'Helper text',
41
+ description:
42
+ 'A short hint that describes the expected value within this block',
43
+ type: 'string',
44
+ },
45
+ instructions: {
46
+ title: 'Instructions',
47
+ description: 'Detailed expected value within this block',
48
+ type: 'string',
49
+ widget: 'richtext',
50
+ },
51
+ as: {
52
+ title: 'HTML5 element',
53
+ description: 'Select HTML5 element to be used for this block',
54
+ type: 'string',
55
+ factory: 'Choice',
56
+ default: 'div',
57
+ choices: [
58
+ ['div', 'div'],
59
+ ['section', 'section'],
60
+ ['article', 'article'],
61
+ ['aside', 'aside'],
62
+ ['details', 'details'],
63
+ ],
64
+ },
65
+ maxChars: {
66
+ title: 'Maximum Characters',
67
+ description: 'The maximum number of characters.',
68
+ type: 'integer',
69
+ factory: 'Integer',
70
+ },
71
+ ignoreSpaces: {
72
+ title: 'Ignore spaces',
73
+ description: 'Ignore spaces while calculating maximum characters',
74
+ type: 'boolean',
75
+ },
76
+ required: {
77
+ title: 'Required',
78
+ description: "Don't allow deletion of this block",
79
+ type: 'boolean',
80
+ },
81
+ fixed: {
82
+ title: 'Fixed position',
83
+ description: 'Disable drag & drop on this block',
84
+ type: 'boolean',
85
+ },
86
+ fixedLayout: {
87
+ title: 'Fixed layout',
88
+ description:
89
+ 'Fixed layout, New blocks created by Editor within this block will be ignored',
90
+ type: 'boolean',
91
+ },
92
+ disableNewBlocks: {
93
+ title: 'Disable new blocks',
94
+ description: 'Disable creation of new blocks after this block',
95
+ type: 'boolean',
96
+ },
97
+ readOnly: {
98
+ title: 'Read-only',
99
+ description: 'Disable editing on this block',
100
+ type: 'boolean',
101
+ },
102
+ readOnlySettings: {
103
+ title: 'Read-only settings',
104
+ description: 'Disable editing on section block settings',
105
+ type: 'boolean',
106
+ },
107
+ disableInnerButtons: {
108
+ title: 'Disable inner buttons',
109
+ description: 'Hide all block related buttons within this block',
110
+ type: 'boolean',
111
+ },
112
+ },
113
+ required: [],
114
+ };
115
+
116
+ export default Schema;
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { RenderBlocks } from '@plone/volto/components';
3
+ import config from '@plone/volto/registry';
4
+
5
+ const View = (props) => {
6
+ const { data } = props;
7
+ const condition = data.condition;
8
+ const metadata = props.metadata || props.properties;
9
+ const CustomTag = `${data.as || 'div'}`;
10
+ const customId = data?.title
11
+ ?.toLowerCase()
12
+ ?.replace(/[^a-zA-Z-\s]/gi, '')
13
+ ?.trim()
14
+ ?.replace(/\s+/gi, '-');
15
+
16
+ const ConditionalRendering =
17
+ config.blocks.blocksConfig.group.conditions?.[condition] || null;
18
+
19
+ if (ConditionalRendering) {
20
+ return (
21
+ <ConditionalRendering>
22
+ <CustomTag id={customId}>
23
+ <RenderBlocks
24
+ {...props}
25
+ metadata={metadata}
26
+ content={data?.data || {}}
27
+ />
28
+ </CustomTag>
29
+ </ConditionalRendering>
30
+ );
31
+ }
32
+
33
+ return (
34
+ <CustomTag id={customId}>
35
+ <RenderBlocks {...props} metadata={metadata} content={data?.data || {}} />
36
+ </CustomTag>
37
+ );
38
+ };
39
+
40
+ export default View;
@@ -0,0 +1,99 @@
1
+ @type: 'extra';
2
+ @element: 'custom';
3
+
4
+ @import (multiple, reference, optional) '../../theme.config';
5
+
6
+ @borderColor: rgba(120, 192, 215, 0.75);
7
+
8
+ .block-editor-group {
9
+ [data-rbd-draggable-context-id] {
10
+ margin-bottom: 1rem;
11
+ }
12
+
13
+ .block-add-button {
14
+ display: none !important;
15
+ }
16
+
17
+ .block.group.selected::before,
18
+ .block.group:hover::before {
19
+ border-style: dashed;
20
+ }
21
+
22
+ fieldset {
23
+ border: none;
24
+
25
+ legend {
26
+ position: absolute;
27
+ z-index: 3;
28
+ top: -1.3em;
29
+ left: 0;
30
+ width: fit-content;
31
+ padding: 0 1rem;
32
+ margin-right: auto;
33
+ margin-left: auto;
34
+ background-color: @pageBackground;
35
+ color: @borderColor;
36
+ cursor: pointer;
37
+ text-align: center;
38
+ }
39
+ }
40
+
41
+ .section-block {
42
+ padding-top: 1rem;
43
+ padding-bottom: 0.1rem;
44
+ margin: 0;
45
+ }
46
+
47
+ .counter {
48
+ display: grid;
49
+ align-items: center;
50
+ font-size: 85%;
51
+ grid-gap: 0.5em;
52
+ grid-template-columns: 98% auto;
53
+ text-align: end;
54
+
55
+ &.info {
56
+ color: #ccc;
57
+ }
58
+
59
+ &.danger {
60
+ color: crimson;
61
+ }
62
+
63
+ &.warning {
64
+ color: darkorange;
65
+ }
66
+ }
67
+
68
+ .blocks-form {
69
+ margin-top: 0.5rem;
70
+ }
71
+
72
+ .blocks-chooser {
73
+ right: 0;
74
+ left: auto;
75
+ margin-top: 3rem;
76
+ }
77
+
78
+ .block-toolbar {
79
+ position: absolute;
80
+ z-index: 3;
81
+ right: -9px;
82
+ display: flex;
83
+ border: none;
84
+ border: 1px solid @borderColor;
85
+ border-bottom: 1px solid @pageBackground;
86
+ margin-top: -45px;
87
+ background-color: @pageBackground;
88
+ border-top-left-radius: 1rem;
89
+ border-top-right-radius: 1rem;
90
+
91
+ .ui.basic.button {
92
+ padding: 8px 5px;
93
+ }
94
+
95
+ .ui.basic.button:hover {
96
+ background: transparent !important;
97
+ }
98
+ }
99
+ }
package/src/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  /* eslint-disable no-extend-native */
2
2
  import React from 'react';
3
3
  import loadable from '@loadable/component';
4
+ import { compose } from 'redux';
5
+ import { connectToProviderData } from '@eeacms/volto-datablocks/hocs';
4
6
 
5
7
  import { hashlink, localStorage } from './reducers';
6
8
 
@@ -125,6 +127,38 @@ const applyConfig = (config) => {
125
127
  ],
126
128
  };
127
129
 
130
+ config.blocks.blocksConfig.group = {
131
+ ...(config.blocks.blocksConfig.group || {}),
132
+ conditions: {
133
+ bird: compose(
134
+ connectToProviderData((props) => ({
135
+ provider_url: '/data/natura-2000-species',
136
+ })),
137
+ )(({ children, provider_data }) => {
138
+ if (
139
+ provider_data?.species_group_name?.[0] &&
140
+ provider_data?.species_group_name?.[0] === 'Birds'
141
+ ) {
142
+ return children;
143
+ }
144
+ return null;
145
+ }),
146
+ species: compose(
147
+ connectToProviderData((props) => ({
148
+ provider_url: '/data/natura-2000-species',
149
+ })),
150
+ )(({ children, provider_data }) => {
151
+ if (
152
+ provider_data?.species_group_name?.[0] &&
153
+ provider_data?.species_group_name?.[0] !== 'Birds'
154
+ ) {
155
+ return children;
156
+ }
157
+ return null;
158
+ }),
159
+ },
160
+ };
161
+
128
162
  config.settings.slate.elements[LINK] = LinkElement;
129
163
 
130
164
  config.settings.loadables = {
@@ -331,6 +331,16 @@ body.grey-bg {
331
331
  .readmore-button {
332
332
  color: #00a390;
333
333
  }
334
+
335
+ .ui.basic.segment .header {
336
+ .logo-nav-wrapper {
337
+ .navigation {
338
+ > .ui.menu a {
339
+ font-size: 14px;
340
+ }
341
+ }
342
+ }
343
+ }
334
344
  }
335
345
 
336
346
  .sdf-nav {