Skip to content

Commit

Permalink
Refactor: Calculate texts once on route change and store in redux sta…
Browse files Browse the repository at this point in the history
…te. Use in wrapped leaf.

tests: location from state.router.location
  • Loading branch information
ksuess committed Oct 21, 2024
1 parent c3195ce commit ee7ba4b
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 147 deletions.
232 changes: 231 additions & 1 deletion packages/volto-slate-glossary/src/components/Tooltips.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import jwtDecode from 'jwt-decode';
import { atom, 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 config from '@plone/volto/registry';

// jotai store for tooltip enhanced slate leafs
export const tooltippedTextsAtom = atom({});

const Tooltips = () => {
return (
<>
<Fetch />
<CalculateTexts />
</>
);
};

const Fetch = () => {
const dispatch = useDispatch();
const token = useSelector((state) => state.userSession?.token);

Expand All @@ -19,7 +38,218 @@ const Tooltips = () => {
}
}, [dispatch, token]);

return <div className="hidden-helper"></div>;
return <div className="hidden-AppExtras-Fetch"></div>;
};

const CalculateTexts = () => {
const glossaryterms = useSelector(
(state) => state.glossarytooltipterms?.result?.items,
);
const blocks_layout = useSelector(
(state) => state.content.data?.blocks_layout,
);
const blocks = useSelector((state) => state.content.data?.blocks);
const setTooltippedTexts = useSetAtom(tooltippedTextsAtom);

useEffect(() => {
if (glossaryterms) {
let texts = calculateTexts(blocks, blocks_layout, glossaryterms);
// Update jotai atom
setTooltippedTexts(texts);
}
}, [blocks, blocks_layout, glossaryterms, setTooltippedTexts]);

return <div className="hidden-AppExtras-CalculateTexts"></div>;
};

/**
* import from @plone/volto-slate Leaf when ready there
* @param {String} children text to be decorated
*/
export const applyLineBreakSupport = (children) => {
const klass = undefined;

return typeof children === 'string'
? children.split('\n').map((t, i) => {
return (
<React.Fragment key={`${i}`}>
{children.indexOf('\n') > -1 &&
children.split('\n').length - 1 > i ? (
<>
{klass ? <span className={klass}>{t}</span> : t}
<br />
</>
) : klass ? (
<span className={klass}>{t}</span>
) : (
t
)}
</React.Fragment>
);
})
: children;
};

export const enhanceTextWithTooltips = (text, remainingGlossaryterms) => {
const caseSensitive = config.settings.glossary.caseSensitive;
const matchOnlyFirstOccurence =
config.settings.glossary.matchOnlyFirstOccurence;
let result = [{ type: 'text', val: text }];
let matchedGlossaryTerms = [];
if (remainingGlossaryterms.length > 0) {
remainingGlossaryterms.forEach((term) => {
result = result.map((chunk) => {
if (chunk.type === 'text') {
let new_chunk = [];
let regExpTerm;
// We pass the 'u' flag for unicode support
// And we use '\p{L}' to match any unicode from the 'letter' category.
// See https://javascript.info/regexp-unicode
let myre = `(?<!\\p{L})(${term.term})(?!\\p{L})`;
if (caseSensitive || term.term === term.term.toUpperCase()) {
// Search case sensitively: if term is 'REST', we don't want to highlight 'rest'.
// regExpTerm = RegExp(myre, 'gv');
regExpTerm = RegExp(myre, 'gu');
} else {
// Search case insensitively.
// regExpTerm = RegExp(myre, 'giv');
regExpTerm = RegExp(myre, 'giu');
}
let chunk_val = chunk.val;
let index = 0;
while (true) {
let res = regExpTerm.exec(chunk.val);
if (
res === null ||
(matchOnlyFirstOccurence &&
matchedGlossaryTerms.includes(term.term))
) {
new_chunk.push({ type: 'text', val: chunk_val.slice(index) });
break;
}
// Term matched. Update context!
if (matchOnlyFirstOccurence) {
matchedGlossaryTerms.push(term.term);
}
if (res.index > 0) {
new_chunk.push({
type: 'text',
val: chunk_val.slice(index, res.index),
});
}
new_chunk.push({
type: 'glossarytermtooltip',
val: res[0],
});
index = res.index + res[0].length;
}
chunk = new_chunk;
}
return chunk;
});
result = _.flatten(result);
});
}
result = _.flatten(result);

return [
result.map((el, j) => {
if (el.type === 'text') {
return applyLineBreakSupport(el.val);
} else {
let idx = remainingGlossaryterms.findIndex(
(variant) => variant.term.toLowerCase() === el.val.toLowerCase(),
);
let definition = remainingGlossaryterms[idx]?.definition || '';
switch (definition.length) {
case 0:
definition = '';
break;
case 1:
definition = definition[0];
break;
default:
let arrayOfListNodes = definition
.map((el) => `<li>${el}</li>`)
.join('');
definition = `<ol>${arrayOfListNodes}</ol>`;
}
return (
<Popup
wide
position="bottom left"
trigger={<span className="glossarytooltip">{el.val}</span>}
key={j}
className="tooltip"
>
<Popup.Content>
<div
className="tooltip_content"
dangerouslySetInnerHTML={{
__html: definition,
}}
/>
</Popup.Content>
</Popup>
);
}
}),
matchOnlyFirstOccurence
? remainingGlossaryterms.filter(
(term) => !matchedGlossaryTerms.includes(term.term),
)
: remainingGlossaryterms,
];
};

const ConcatenatedString = (node) => {
if (Text.isText(node)) {
// return node.text.trim();
return node.text;
} else {
return node.children.map(ConcatenatedString);
}
};

const serializeNodes = (nodes) => {
return nodes.map(ConcatenatedString);
};

const calculateTexts = (blocks, blocks_layout, 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(
serializeNodes(blocks[blockid].value),
);
arrayOfStrings.forEach((str) => {
if (str.length === 0) {
return;
}
let key = uuidv5(str, MY_NAMESPACE);
let [value, newTerms] = enhanceTextWithTooltips(
str,
remainingGlossaryterms,
);
result[key] = value;
remainingGlossaryterms = newTerms;
});
} else {
if (blocks[blockid].blocks && blocks[blockid].blocks_layout) {
iterateOverBlocks(
blocks[blockid].blocks,
blocks[blockid].blocks_layout,
);
}
}
});
}
iterateOverBlocks(blocks, blocks_layout);
return result;
};

export default Tooltips;
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
}
}

.hidden-helper {
.hidden-AppExtras {
display: none;
}

Expand Down
10 changes: 9 additions & 1 deletion packages/volto-slate-glossary/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@ import GlossaryView from './components/GlossaryView';
import TermView from './components/TermView';
import { glossarytermsReducer, glossarytooltiptermsReducer } from './reducers';
import { TextWithGlossaryTooltips } from './utils';
// import { Tooltips } from './components';

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

config.views.viewContext['volto-slate-glossary'] = [];
// DEBUG
// config.settings.appExtras = [
// ...config.settings.appExtras,
// {
// match: '/',
// component: Tooltips,
// },
// ];

config.settings.slate.leafs = {
text: ({ children }) => <TextWithGlossaryTooltips text={children} />,
Expand Down
Loading

0 comments on commit ee7ba4b

Please sign in to comment.