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;