design-comuni-plone-theme
Version:
Volto Theme for Italia design guidelines
249 lines (225 loc) • 7.1 kB
JSX
// customization:
// - Element component to render heading tags correctly
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { useLocation } from 'react-router-dom';
import { toast } from 'react-toastify';
import { useIntl } from 'react-intl';
import { useSelector } from 'react-redux';
import { Node, Text } from 'slate';
import cx from 'classnames';
import { isEmpty, omit } from 'lodash';
import { UniversalLink, Toast } from '@plone/volto/components';
import { messages, addAppURL } from '@plone/volto/helpers';
import useClipboard from '@plone/volto/hooks/clipboard/useClipboard';
import config from '@plone/volto/registry';
import linkSVG from '@plone/volto/icons/link.svg';
import '@plone/volto-slate/editor/less/slate.less';
const OMITTED = ['editor', 'path'];
// TODO: read, see if relevant
// https://reactjs.org/docs/higher-order-components.html#dont-use-hocs-inside-the-render-method
// customization to
// fix: heading node does not receive Slate attributes
// solution: Element replaces headings with native DOM node that receives {...attributes}
// todo: remove after update to correct version of volto
export const Element = ({
element,
attributes = {},
extras,
children,
...rest
}) => {
const type = element?.type;
// Render native heading tag so Slate attrs (ref, data-*, contentEditable...) land on the actual DOM node
if (/^h[1-6]$/.test(type)) {
const Tag = type;
return <Tag {...attributes}>{children}</Tag>;
}
const { slate } = config.settings;
const { elements } = slate;
const El = elements[type] || elements['default'];
// preserve attributes (and merge className with element.styleName if any)
const attrs = {
...attributes,
className:
[attributes?.className, element?.styleName].filter(Boolean).join(' ') ||
undefined,
};
if (attributes?.ref) attrs.ref = attributes.ref;
return (
<El
element={element}
{...omit(rest, OMITTED)}
attributes={attrs}
extras={extras}
>
{children}
</El>
);
};
export const Leaf = ({ children, ...rest }) => {
const { attributes, leaf, mode } = rest;
let { leafs } = config.settings.slate;
children = Object.keys(leafs).reduce((acc, name) => {
return Object.keys(leaf).includes(name)
? leafs[name]({ children: acc })
: acc;
}, children);
const classNames = {
[`highlight-${leaf.highlightType}`]: mode !== 'view' && leaf.highlightType,
'highlight-selection': mode !== 'view' && leaf.isSelection,
};
// stylemenu support
for (const prop in leaf) {
if (prop.startsWith('style-')) {
classNames[prop.substring(6)] = true;
}
}
const klass = cx(classNames);
return mode === 'view' ? (
typeof children === 'string' ? (
children.split('\n').map((t, i) => {
// Softbreak support. Should do a plugin?
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>
);
})
) : (
<span className={klass}>{children}</span>
)
) : (
<span {...attributes} className={klass}>
{children}
</span>
);
};
const serializeData = (node) => {
return JSON.stringify({ type: node.type, data: node.data });
};
export const serializeNodes = (nodes, getAttributes, extras = {}) => {
const editor = { children: nodes || [] };
const _serializeNodes = (nodes) => {
return (nodes || []).map(([node, path], i) => {
return Text.isText(node) ? (
<Leaf path={path} leaf={node} text={node} mode="view" key={path}>
{node.text}
</Leaf>
) : (
<Element
path={path}
element={node}
mode="view"
key={path}
data-slate-data={node.data ? serializeData(node) : null}
attributes={getAttributes ? getAttributes(node, path) : null}
extras={extras}
>
{_serializeNodes(Array.from(Node.children(editor, path)))}
</Element>
);
});
};
return _serializeNodes(Array.from(Node.children(editor, [])));
};
/**
* Get the concatenated text string of a node's content.
*
* Note that this WILL include spaces between block node leafs in contrary to
* the original slate method. This function joins text of nodes with
* separating spaces to produce a string for indexing purposes.
*
*/
const ConcatenatedString = (node) => {
if (Text.isText(node)) {
return node.text.trim();
} else {
return node.children.map(ConcatenatedString).join(' ');
}
};
/**
* @function serializeNodesToText
*
* @param {Array[Node]} nodes
*
* @returns {string}
*/
export const serializeNodesToText = (nodes) => {
return nodes.map(ConcatenatedString).join('\n');
};
export const serializeNodesToHtml = (nodes) =>
renderToStaticMarkup(serializeNodes(nodes));
export const renderLinkElement = (tagName) => {
function LinkElement({
attributes,
children,
mode = 'edit',
className = null,
}) {
const { slate = {} } = config.settings;
const Tag = tagName;
const slug = attributes.id || '';
const location = useLocation();
const token = useSelector((state) => state.userSession.token);
const appPathname = addAppURL(location.pathname);
// eslint-disable-next-line no-unused-vars
const [copied, copy, setCopied] = useClipboard(
appPathname.concat(`#${slug}`),
);
const intl = useIntl();
return !token || slate.useLinkedHeadings === false ? (
<Tag {...attributes} className={className} tabIndex={0}>
{children}
</Tag>
) : (
<Tag {...attributes} className={className} tabIndex={0}>
{children}
{mode === 'view' && slug && (
<UniversalLink
className="anchor"
aria-hidden="true"
tabIndex={-1}
href={`#${slug}`}
>
<style>
{/* Prettify the unstyled flash of the link icon on development */}
{`
a.anchor svg {
height: var(--anchor-svg-height, 24px);
}
`}
</style>
<svg
{...linkSVG.attributes}
dangerouslySetInnerHTML={{ __html: linkSVG.content }}
height={null}
onClick={() => {
copy();
toast.info(
<Toast
info
title={intl.formatMessage(messages.success)}
content={intl.formatMessage(messages.urlClipboardCopy)}
/>,
);
}}
></svg>
</UniversalLink>
)}
</Tag>
);
}
LinkElement.displayName = `${tagName}LinkElement`;
return LinkElement;
};