diff --git a/src/components/manage/Blocks/Group/CounterComponent.jsx b/src/components/manage/Blocks/Group/CounterComponent.jsx
new file mode 100644
index 0000000..7279f6c
--- /dev/null
+++ b/src/components/manage/Blocks/Group/CounterComponent.jsx
@@ -0,0 +1,84 @@
+import cx from 'classnames';
+import isString from 'lodash/isString';
+import isArray from 'lodash/isArray';
+import { Icon } from '@plone/volto/components';
+import config from '@plone/volto/registry';
+import { visitBlocks } from '@plone/volto/helpers/Blocks/Blocks';
+import { serializeNodesToText } from '@plone/volto-slate/editor/render';
+import delightedSVG from '@plone/volto/icons/delighted.svg';
+import dissatisfiedSVG from '@plone/volto/icons/dissatisfied.svg';
+
+const CounterComponent = ({ data, setSidebarTab, setSelectedBlock }) => {
+ const { maxChars } = data;
+ let charCount = 0;
+
+ const countCharsWithoutSpaces = (paragraph) => {
+ const regex = /[^\s\\]/g;
+
+ return (paragraph.match(regex) || []).length;
+ };
+
+ const countCharsWithSpaces = (paragraph) => {
+ return paragraph?.length || 0;
+ };
+
+ const countTextInBlocks = (blocksObject) => {
+ const { countTextIn } = config.blocks?.blocksConfig?.group;
+ let groupCharCount = 0;
+ if (!maxChars) {
+ return groupCharCount;
+ }
+ if (!blocksObject) return groupCharCount;
+
+ visitBlocks(blocksObject, ([id, data]) => {
+ let foundText;
+ if (data && countTextIn?.includes(data?.['@type'])) {
+ if (isString(data?.plaintext)) foundText = data?.plaintext;
+ else if (isArray(data?.value) && data?.value !== null)
+ foundText = serializeNodesToText(data?.value);
+ } else foundText = '';
+
+ groupCharCount += data?.ignoreSpaces
+ ? countCharsWithoutSpaces(foundText)
+ : countCharsWithSpaces(foundText);
+ });
+
+ return groupCharCount;
+ };
+
+ charCount = countTextInBlocks(data?.data);
+
+ const counterClass =
+ charCount < Math.ceil(maxChars / 1.05)
+ ? 'info'
+ : charCount < maxChars
+ ? 'warning'
+ : 'danger';
+
+ return (
+
{
+ setSelectedBlock();
+ setSidebarTab(1);
+ }}
+ aria-hidden="true"
+ >
+ {maxChars - charCount < 0 ? (
+ <>
+ {`${charCount - maxChars} characters over the limit`}
+
+ >
+ ) : (
+ <>
+ {`${
+ maxChars - charCount
+ } characters remaining out of ${maxChars}`}
+
+ >
+ )}
+
+ );
+};
+
+export default CounterComponent;
diff --git a/src/components/manage/Blocks/Group/Edit.jsx b/src/components/manage/Blocks/Group/Edit.jsx
index d6e5d42..3cbea44 100644
--- a/src/components/manage/Blocks/Group/Edit.jsx
+++ b/src/components/manage/Blocks/Group/Edit.jsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useCallback } from 'react';
import { isEmpty, without } from 'lodash';
import config from '@plone/volto/registry';
import {
@@ -12,14 +12,12 @@ import {
emptyBlocksForm,
getBlocksLayoutFieldname,
} from '@plone/volto/helpers';
-import delightedSVG from '@plone/volto/icons/delighted.svg';
-import dissatisfiedSVG from '@plone/volto/icons/dissatisfied.svg';
import PropTypes from 'prop-types';
import { Button, Segment } from 'semantic-ui-react';
import EditBlockWrapper from './EditBlockWrapper';
import EditSchema from './EditSchema';
+import CounterComponent from './CounterComponent';
import helpSVG from '@plone/volto/icons/help.svg';
-import cx from 'classnames';
import './editor.less';
const Edit = (props) => {
@@ -43,8 +41,6 @@ const Edit = (props) => {
);
const blockState = {};
- let charCount = 0;
-
const handleKeyDown = (
e,
index,
@@ -71,41 +67,44 @@ const Edit = (props) => {
}
};
- const onSelectBlock = (id, isMultipleSelection, event, activeBlock) => {
- let newMultiSelected = [];
- let selected = id;
+ const onSelectBlock = useCallback(
+ (id, isMultipleSelection, event, activeBlock) => {
+ let newMultiSelected = [];
+ let selected = id;
- if (isMultipleSelection) {
- selected = null;
- const blocksLayoutFieldname = getBlocksLayoutFieldname(data?.data);
- const blocks_layout = data?.data[blocksLayoutFieldname].items;
- if (event.shiftKey) {
- const anchor =
- multiSelected.length > 0
- ? blocks_layout.indexOf(multiSelected[0])
- : blocks_layout.indexOf(activeBlock);
- const focus = blocks_layout.indexOf(id);
- if (anchor === focus) {
- newMultiSelected = [id];
- } else if (focus > anchor) {
- newMultiSelected = [...blocks_layout.slice(anchor, focus + 1)];
- } else {
- newMultiSelected = [...blocks_layout.slice(focus, anchor + 1)];
+ if (isMultipleSelection) {
+ selected = null;
+ const blocksLayoutFieldname = getBlocksLayoutFieldname(data?.data);
+ const blocks_layout = data?.data[blocksLayoutFieldname].items;
+ if (event.shiftKey) {
+ const anchor =
+ multiSelected.length > 0
+ ? blocks_layout.indexOf(multiSelected[0])
+ : blocks_layout.indexOf(activeBlock);
+ const focus = blocks_layout.indexOf(id);
+ if (anchor === focus) {
+ newMultiSelected = [id];
+ } else if (focus > anchor) {
+ newMultiSelected = [...blocks_layout.slice(anchor, focus + 1)];
+ } else {
+ newMultiSelected = [...blocks_layout.slice(focus, anchor + 1)];
+ }
}
- }
- if ((event.ctrlKey || event.metaKey) && !event.shiftKey) {
- if (multiSelected.includes(id)) {
- selected = null;
- newMultiSelected = without(multiSelected, id);
- } else {
- newMultiSelected = [...(multiSelected || []), id];
+ if ((event.ctrlKey || event.metaKey) && !event.shiftKey) {
+ if (multiSelected.includes(id)) {
+ selected = null;
+ newMultiSelected = without(multiSelected, id);
+ } else {
+ newMultiSelected = [...(multiSelected || []), id];
+ }
}
}
- }
- setSelectedBlock(selected);
- setMultiSelected(newMultiSelected);
- };
+ setSelectedBlock(selected);
+ setMultiSelected(newMultiSelected);
+ },
+ [data.data, multiSelected],
+ );
const changeBlockData = (newBlockData) => {
let pastedBlocks = newBlockData.blocks_layout.items.filter((blockID) => {
@@ -142,101 +141,6 @@ const Edit = (props) => {
}
}, [onChangeBlock, properties, selectedBlock, block, data, data_blocks]);
- /**
- * Count the number of characters that are anything except using Regex
- * @param {string} paragraph
- * @returns
- */
- const countCharsWithoutSpaces = (paragraph) => {
- const regex = /[^\s\\]/g;
-
- return (paragraph.match(regex) || []).length;
- };
-
- /**
- * Count the number of characters
- * @param {string} paragraph
- * @returns
- */
- const countCharsWithSpaces = (paragraph) => {
- return paragraph?.length || 0;
- };
-
- /**
- * Recursively look for any block that contains text or plaintext
- * @param {Object} blocksObject
- * @returns
- */
- const countTextInBlocks = (blocksObject) => {
- let groupCharCount = 0;
- if (!props.data.maxChars) {
- return groupCharCount;
- }
-
- Object.keys(blocksObject).forEach((blockId) => {
- const foundText = blocksObject[blockId]?.plaintext
- ? blocksObject[blockId]?.plaintext
- : blocksObject[blockId]?.text?.blocks[0]?.text
- ? blocksObject[blockId].text.blocks[0].text
- : blocksObject[blockId]?.data?.blocks
- ? countTextInBlocks(blocksObject[blockId]?.data?.blocks)
- : blocksObject[blockId]?.blocks
- ? countTextInBlocks(blocksObject[blockId]?.blocks)
- : '';
- const resultText =
- typeof foundText === 'string' || foundText instanceof String
- ? foundText
- : '';
-
- groupCharCount += props.data.ignoreSpaces
- ? countCharsWithoutSpaces(resultText)
- : countCharsWithSpaces(resultText);
- });
-
- return groupCharCount;
- };
-
- const showCharCounter = () => {
- if (data_blocks) {
- charCount = countTextInBlocks(data_blocks);
- }
- };
- showCharCounter();
-
- const counterClass =
- charCount < Math.ceil(props.data.maxChars / 1.05)
- ? 'info'
- : charCount < props.data.maxChars
- ? 'warning'
- : 'danger';
-
- const counterComponent = props.data.maxChars ? (
- {
- setSelectedBlock();
- props.setSidebarTab(1);
- }}
- aria-hidden="true"
- >
- {props.data.maxChars - charCount < 0 ? (
- <>
- {`${
- charCount - props.data.maxChars
- } characters over the limit`}
-
- >
- ) : (
- <>
- {`${
- props.data.maxChars - charCount
- } characters remaining out of ${props.data.maxChars}`}
-
- >
- )}
-
- ) : null;
-
// Get editing instructions from block settings or props
let instructions = data?.instructions?.data || data?.instructions;
if (!instructions || instructions === '
') {
@@ -353,7 +257,9 @@ const Edit = (props) => {
)}
- {counterComponent}
+ {props.data.maxChars && (
+
+ )}
{instructions && (
diff --git a/src/index.js b/src/index.js
index fc0bcf1..3857e97 100644
--- a/src/index.js
+++ b/src/index.js
@@ -59,6 +59,7 @@ const applyConfig = (config) => {
});
return entries;
},
+ countTextIn: ['slate', 'description'], //id of the block whose text should be counted
};
return config;