Skip to content

Commit

Permalink
Fix accordion block. Prepare for description block and teaser block. …
Browse files Browse the repository at this point in the history
…Make TooltipPopup customizable via component registry.
  • Loading branch information
ksuess committed Dec 20, 2024
1 parent 65e203b commit 6878591
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 50 deletions.
92 changes: 89 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,6 @@ config.settings.glossary.matchOnlyFirstOccurence = true;
```


User can opt-out by setting glossarytooltips to false.
Add a boolean member field *glossarytooltips* for it.

## Further configurations

Hide alphabet navigation of glossary view:
Expand All @@ -69,3 +66,92 @@ Show glossary term in tooltips header:
```js
config.settings.glossary.mentionTermInTooltip = true;
```

Show tooltips also in text blocks of an [accordion block](https://github.com/eea/volto-accordion-block):

```js
config.settings.glossary.includeAccordionBlock = true;
```

### Show tooltips also in a description block:

Per default only text of slate blocks are equipped with tooltips.
`TextWithGlossaryTooltips` can be used to enhance other texts with tooltip markup.

Create a custom `DescriptionBlockView` in your project:

```js
import { TextWithGlossaryTooltips } from '@rohberg/volto-slate-glossary/utils';

const DescriptionBlockView = ({ properties, metadata, id }) => {
let description = (metadata || properties)['description'] || '';
description = TextWithGlossaryTooltips({ text: description });

return <p className="documentDescription">{description}</p>;
};

export default DescriptionBlockView;
````

Register `DescriptionBlockView`:

```js
config.blocks.blocksConfig.description.view = DescriptionBlockViewWithTooltips;
```

### Show tooltips also in a teaser block

Per default only text of slate blocks are equipped with tooltips.
`TextWithGlossaryTooltips` can be used to enhance other texts with tooltip markup.

Create a custom `TeaserView` in your project:

```js
import TeaserBody from '@plone/volto/components/manage/Blocks/Teaser/Body';
import { withBlockExtensions } from '@plone/volto/helpers/Extensions';
import { TextWithGlossaryTooltips } from '@rohberg/volto-slate-glossary/utils';
const TeaserView = (props) => {
return (
<TeaserBody
{...{
...props,
data: {
...props.data,
description: TextWithGlossaryTooltips({
text: props.data.description,
}),
},
}}
/>
);
};
export default withBlockExtensions(TeaserView);
```

Register `TeaserView`:

```js
// teaser block with tooltips
config.blocks.blocksConfig.teaser.view = TeaserViewWithTooltips;
// teaser block in grid block also with tooltips
config.blocks.blocksConfig.gridBlock.blocksConfig.teaser.view =
TeaserViewWithTooltips;
```

### Register Custom tooltip component

The tooltip component can be replaced by a custom one.

```js
config.registerComponent({
name: 'TooltipPopup',
component: CustomTooltipPopup,
});
````
## Opt-out for users
A user can opt-out by setting glossarytooltips to false.
Add a boolean member field `glossarytooltips` to provide this.
4 changes: 4 additions & 0 deletions packages/volto-slate-glossary/news/13.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Optionally show tooltips in accordion block panel. Default is false. @ksuess
Prepare for tooltips in description block. Add intructions to README. @ksuess
Prepare for tooltips in teaser block. Add intructions to README. @ksuess
Make TooltipPopup customizable via component registry. @ksuess
104 changes: 59 additions & 45 deletions packages/volto-slate-glossary/src/components/Tooltips.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import React, { useEffect } from 'react';
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import jwtDecode from 'jwt-decode';
import { useSetAtom } from 'jotai';
import _ from 'lodash';
import { Text } from 'slate';
import { v5 as uuidv5 } from 'uuid';
import { Popup } from 'semantic-ui-react';
import { getUser } from '@plone/volto/actions';
import { getTooltipTerms } from '../actions';
import { MY_NAMESPACE } from '../utils';
import { tooltippedTextsAtom } from '../utils';
import config from '@plone/volto/registry';

/**
* Add to config.settings.appExtras
*/
export const FetchTooltipTerms = ({ token }) => {
const dispatch = useDispatch();

Expand All @@ -29,15 +31,13 @@ export const FetchTooltipTerms = ({ token }) => {
return <div className="hidden-AppExtras-Fetch"></div>;
};

/**
* Add to config.settings.appExtras
* with restriction to the routes where tooltips are wanted.
*/
const Tooltips = (props) => {
return (
<>
<CalculateTexts {...props} />
</>
);
};

const CalculateTexts = ({ pathname }) => {
const pathname = props.pathname;
const description = props.content?.description;
const glossaryterms = useSelector(
(state) => state.glossarytooltipterms?.result?.items,
);
Expand All @@ -51,12 +51,9 @@ const CalculateTexts = ({ pathname }) => {

useEffect(() => {
if (glossaryterms) {
let texts = calculateTexts(blocks, blocks_layout, glossaryterms);
let texts = calculateTexts(blocks, blocks_layout, description, glossaryterms);
// Store texts and pathname in atom
setTooltippedTexts({ pathname: pathname, texts: texts });
// return () => {
// setTooltippedTexts({ pathname: undefined, texts: [] });
// };
}
}, [blocks, blocks_layout, glossaryterms, pathname, setTooltippedTexts]);

Expand Down Expand Up @@ -99,8 +96,7 @@ export const applyLineBreakSupport = (children) => {
*/
export const enhanceTextWithTooltips = (text, remainingGlossaryterms) => {
const caseSensitive = config.settings.glossary.caseSensitive;
const { matchOnlyFirstOccurence, mentionTermInTooltip } =
config.settings.glossary;
const { matchOnlyFirstOccurence } = config.settings.glossary;
let result = [{ type: 'text', val: text }];
let matchedGlossaryTerms = [];
if (remainingGlossaryterms.length > 0) {
Expand Down Expand Up @@ -182,27 +178,8 @@ export const enhanceTextWithTooltips = (text, remainingGlossaryterms) => {
.join('');
definition = `<ol>${arrayOfListNodes}</ol>`;
}
return (
<Popup
wide
position="bottom left"
trigger={<span className="glossarytooltip">{el.val}</span>}
key={j}
className="tooltip"
>
{mentionTermInTooltip ? (
<Popup.Header>{el.val}</Popup.Header>
) : null}
<Popup.Content>
<div
className="tooltip_content"
dangerouslySetInnerHTML={{
__html: definition,
}}
/>
</Popup.Content>
</Popup>
);
const TooltipPopup = config.getComponent('TooltipPopup').component
return (<TooltipPopup term={el.val} definition={definition} idx={j} />)
}
}),
matchOnlyFirstOccurence
Expand All @@ -226,40 +203,77 @@ const serializeNodes = (nodes) => {
return nodes.map(ConcatenatedString);
};

const calculateTexts = (blocks, blocks_layout, glossaryterms) => {
/**
* calculate all markup of all slate blocks text, teaser blocks description and content description
*/
const calculateTexts = (blocks, blocks_layout, description, glossaryterms) => {
let remainingGlossaryterms = glossaryterms;
let result = {};

function iterateOverBlocks(blocks, blocks_layout) {
blocks_layout?.items &&
blocks_layout.items.forEach((blockid) => {
if (blocks[blockid].value) {
let arrayOfStrings = _.flattenDeep(
if (blocks[blockid].value) { // Simple slate block
const arrayOfStrings = _.flattenDeep(
serializeNodes(blocks[blockid].value),
);
arrayOfStrings.forEach((str) => {
if (str.length === 0) {
return;
}
let key = uuidv5(str, MY_NAMESPACE);
const key = uuidv5(str, MY_NAMESPACE);
if (Object.keys(result).includes(key)) {
return;
}
let [value, newTerms] = enhanceTextWithTooltips(
const [value, newTerms] = enhanceTextWithTooltips(
str,
remainingGlossaryterms,
);
result[key] = value;
remainingGlossaryterms = newTerms;
});
} else {
// Handle nested blocks
} else if (blocks[blockid]["@type"]==='description') {
if (description) {
const key = uuidv5(description, MY_NAMESPACE);
if (Object.keys(result).includes(key)) {
return;
}
const [value, newTerms] = enhanceTextWithTooltips(
description,
remainingGlossaryterms,
);
result[key] = value;
remainingGlossaryterms = newTerms;
}
} else if (blocks[blockid].description) {
const teaser_description = blocks[blockid].description;
if (teaser_description) {
const key = uuidv5(teaser_description, MY_NAMESPACE);
if (Object.keys(result).includes(key)) {
return;
}
const [value, newTerms] = enhanceTextWithTooltips(
teaser_description,
remainingGlossaryterms,
);
result[key] = value;
remainingGlossaryterms = newTerms;
}
} else { // Nested blocks
// block type 'gridBlock' or '"accordionPanel"
if (blocks[blockid].blocks && blocks[blockid].blocks_layout) {
iterateOverBlocks(
blocks[blockid].blocks,
blocks[blockid].blocks_layout,
);
}
// block type 'accordion'
if (config.settings.glossary.includeAccordionBlock && blocks[blockid].data?.blocks && blocks[blockid].data?.blocks_layout) {
iterateOverBlocks(
blocks[blockid].data.blocks,
blocks[blockid].data.blocks_layout,
);
}
}
});
}
Expand Down
8 changes: 8 additions & 0 deletions packages/volto-slate-glossary/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import TermView from './components/TermView';
import { glossarytermsReducer, glossarytooltiptermsReducer } from './reducers';
import { TextWithGlossaryTooltips } from './utils';
import { FetchTooltipTerms } from './components/Tooltips';
import DescriptionBlockView from './components/DescriptionBlockView';
import TooltipPopup from './components/TooltipPopup'

const applyConfig = (config) => {
config.settings.glossary = {
caseSensitive: false,
matchOnlyFirstOccurence: false,
showAlphabetNavigation: true,
mentionTermInTooltip: false,
includeAccordionBlock: false,
};

config.settings.slate.leafs = {
Expand All @@ -33,6 +36,11 @@ const applyConfig = (config) => {
},
};

config.registerComponent({
name: 'TooltipPopup',
component: TooltipPopup,
});

config.addonReducers = {
...config.addonReducers,
glossaryterms: glossarytermsReducer,
Expand Down
15 changes: 13 additions & 2 deletions packages/volto-slate-glossary/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ export const tooltippedTextsAtom = atom({ pathname: undefined, texts: [] });

export const MY_NAMESPACE = '4549d0a3-5fc2-4a94-bf96-eb7ddf5363a4';

/**
* TextWithGlossaryTooltips
*
* returns enhanced markup from Jotai store if
* - location should show tooltips
* - current user has not opted out
* - page is in view mode
*
* @param {String} text
* @returns String
*/
export const TextWithGlossaryTooltips = ({ text }) => {
const location = useSelector((state) => state.router?.location);
const pathname = location?.pathname;
Expand Down Expand Up @@ -40,16 +51,16 @@ export const TextWithGlossaryTooltips = ({ text }) => {
return text;
}

let uid;
try {
uuidv5(text, MY_NAMESPACE);
uid = uuidv5(text, MY_NAMESPACE);
} catch (error) {
// "RangeError: offset is out of bounds"
// generateUUID
// node_modules/.pnpm/[email protected]/node_modules/uuid/dist/esm-browser/v35.js:36
// console.error(error);
return text;
}
let uid = uuidv5(text, MY_NAMESPACE);
// No match in store if this location is not configured for tooltips. Return text unchanged.
const newText = Object.keys(tooltippedTexts?.texts).includes(uid)
? tooltippedTexts.texts[uid]
Expand Down

0 comments on commit 6878591

Please sign in to comment.