From 6399117cc30fc94bc9c27413f6f7e797c8669201 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sun, 31 Jul 2022 22:39:52 +0300 Subject: [PATCH 1/6] Show a link for headline elements --- package.json | 1 + .../src/blocks/Text/TextBlockView.jsx | 28 +++++++------ packages/volto-slate/src/editor/config.jsx | 9 +++-- packages/volto-slate/src/editor/render.jsx | 39 +++++++++++++++---- yarn.lock | 5 +++ 5 files changed, 55 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index d749f11c8d..7b44f89db8 100644 --- a/package.json +++ b/package.json @@ -284,6 +284,7 @@ "eslint-plugin-react": "7.20.0", "eslint-plugin-react-hooks": "4.0.2", "express": "4.17.1", + "github-slugger": "1.4.0", "glob": "7.1.6", "hamburgers": "1.1.3", "handlebars": "4.7.7", diff --git a/packages/volto-slate/src/blocks/Text/TextBlockView.jsx b/packages/volto-slate/src/blocks/Text/TextBlockView.jsx index 5e8accb82c..56867e8989 100644 --- a/packages/volto-slate/src/blocks/Text/TextBlockView.jsx +++ b/packages/volto-slate/src/blocks/Text/TextBlockView.jsx @@ -1,26 +1,24 @@ import { serializeNodes } from '@plone/volto-slate/editor/render'; import config from '@plone/volto/registry'; +import { isEqual } from 'lodash'; const TextBlockView = (props) => { const { id, data, styling = {} } = props; const { value, override_toc } = data; const metadata = props.metadata || props.properties; - return serializeNodes( - value, - (node, path) => { - const res = { ...styling }; - if (node.type) { - if ( - config.settings.slate.topLevelTargetElements.includes(node.type) || - override_toc - ) { - res.id = id; - } + const { topLevelTargetElements } = config.settings.slate; + + const getAttributes = (node, path) => { + const res = { ...styling }; + if (node.type && isEqual(path, [0])) { + if (topLevelTargetElements.includes(node.type) || override_toc) { + res.id = id; } - return res; - }, - { metadata: metadata }, - ); + } + return res; + }; + + return serializeNodes(value, getAttributes, { metadata: metadata }); }; export default TextBlockView; diff --git a/packages/volto-slate/src/editor/config.jsx b/packages/volto-slate/src/editor/config.jsx index c9367cd2a6..3831a985e4 100644 --- a/packages/volto-slate/src/editor/config.jsx +++ b/packages/volto-slate/src/editor/config.jsx @@ -40,6 +40,7 @@ import { bTagDeserializer, codeTagDeserializer, } from './deserialize'; +import { renderLinkElement } from './render'; // Registry of available buttons export const buttons = { @@ -225,10 +226,10 @@ export const defaultBlockType = 'p'; export const elements = { default: ({ attributes, children }) =>

{children}

, - h1: ({ attributes, children }) =>

{children}

, - h2: ({ attributes, children }) =>

{children}

, - h3: ({ attributes, children }) =>

{children}

, - h4: ({ attributes, children }) =>

{children}

, + h1: renderLinkElement('h1'), + h2: renderLinkElement('h2'), + h3: renderLinkElement('h3'), + h4: renderLinkElement('h4'), li: ({ attributes, children }) =>
  • {children}
  • , ol: ({ attributes, children }) =>
      {children}
    , diff --git a/packages/volto-slate/src/editor/render.jsx b/packages/volto-slate/src/editor/render.jsx index 7fa0f5f173..dcb50dc079 100644 --- a/packages/volto-slate/src/editor/render.jsx +++ b/packages/volto-slate/src/editor/render.jsx @@ -2,8 +2,12 @@ import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { Node, Text } from 'slate'; import cx from 'classnames'; -import { isEmpty, isEqual, omit } from 'lodash'; +import { isEmpty, omit } from 'lodash'; +import Slugger from 'github-slugger'; import config from '@plone/volto/registry'; +import linkSVG from '@plone/volto/icons/link.svg'; + +import './less/slate.less'; const OMITTED = ['editor', 'path']; @@ -106,13 +110,7 @@ export const serializeNodes = (nodes, getAttributes, extras = {}) => { mode="view" key={path} data-slate-data={node.data ? serializeData(node) : null} - attributes={ - isEqual(path, [0]) - ? getAttributes - ? getAttributes(node, path) - : null - : null - } + attributes={getAttributes ? getAttributes(node, path) : null} extras={extras} > {_serializeNodes(Array.from(Node.children(editor, path)))} @@ -153,3 +151,28 @@ export const serializeNodesToText = (nodes) => { export const serializeNodesToHtml = (nodes) => renderToStaticMarkup(serializeNodes(nodes)); + +export const renderLinkElement = (tagName) => { + function LinkElement({ attributes, children, mode = 'edit' }) { + const Tag = tagName; + const slug = Slugger.slug('hello'); + + return ( + + {mode === 'view' && ( + + )} + {children} + + ); + } + LinkElement.displayName = `${tagName}LinkElement`; + return LinkElement; +}; diff --git a/yarn.lock b/yarn.lock index ac144cc3a7..c144ad588f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9156,6 +9156,11 @@ git-url-parse@11.6.0, git-url-parse@^11.6.0: dependencies: git-up "^4.0.0" +github-slugger@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.4.0.tgz#206eb96cdb22ee56fdc53a28d5a302338463444e" + integrity sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ== + github-slugger@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.3.0.tgz#9bd0a95c5efdfc46005e82a906ef8e2a059124c9" From 9b45a563c8068a59b7b7693318a2956ea78a6d57 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sun, 31 Jul 2022 22:54:48 +0300 Subject: [PATCH 2/6] Improve sluggify behavior --- .../src/blocks/Text/TextBlockView.jsx | 13 ++++++++--- .../volto-slate/src/editor/less/slate.less | 22 +++++++++++++++++++ packages/volto-slate/src/editor/render.jsx | 7 +++--- 3 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 packages/volto-slate/src/editor/less/slate.less diff --git a/packages/volto-slate/src/blocks/Text/TextBlockView.jsx b/packages/volto-slate/src/blocks/Text/TextBlockView.jsx index 56867e8989..d81c1b85c7 100644 --- a/packages/volto-slate/src/blocks/Text/TextBlockView.jsx +++ b/packages/volto-slate/src/blocks/Text/TextBlockView.jsx @@ -1,9 +1,13 @@ -import { serializeNodes } from '@plone/volto-slate/editor/render'; +import { + serializeNodes, + serializeNodesToText, +} from '@plone/volto-slate/editor/render'; import config from '@plone/volto/registry'; import { isEqual } from 'lodash'; +import Slugger from 'github-slugger'; const TextBlockView = (props) => { - const { id, data, styling = {} } = props; + const { data, styling = {} } = props; // id, const { value, override_toc } = data; const metadata = props.metadata || props.properties; const { topLevelTargetElements } = config.settings.slate; @@ -12,7 +16,10 @@ const TextBlockView = (props) => { const res = { ...styling }; if (node.type && isEqual(path, [0])) { if (topLevelTargetElements.includes(node.type) || override_toc) { - res.id = id; + // console.log('children', children); + const text = serializeNodesToText(node?.children || []); + const slug = Slugger.slug(text); + res.id = slug; } } return res; diff --git a/packages/volto-slate/src/editor/less/slate.less b/packages/volto-slate/src/editor/less/slate.less new file mode 100644 index 0000000000..86c5e3f504 --- /dev/null +++ b/packages/volto-slate/src/editor/less/slate.less @@ -0,0 +1,22 @@ +h1, +h2, +h3, +h4 { + &:hover { + a.anchor { + svg { + visibility: unset; + } + } + } + + a.anchor { + margin-left: -2ch; + float: left; + + svg { + width: 2ch; + visibility: hidden; + } + } +} diff --git a/packages/volto-slate/src/editor/render.jsx b/packages/volto-slate/src/editor/render.jsx index dcb50dc079..0af32974d0 100644 --- a/packages/volto-slate/src/editor/render.jsx +++ b/packages/volto-slate/src/editor/render.jsx @@ -3,7 +3,6 @@ import { renderToStaticMarkup } from 'react-dom/server'; import { Node, Text } from 'slate'; import cx from 'classnames'; import { isEmpty, omit } from 'lodash'; -import Slugger from 'github-slugger'; import config from '@plone/volto/registry'; import linkSVG from '@plone/volto/icons/link.svg'; @@ -155,12 +154,12 @@ export const serializeNodesToHtml = (nodes) => export const renderLinkElement = (tagName) => { function LinkElement({ attributes, children, mode = 'edit' }) { const Tag = tagName; - const slug = Slugger.slug('hello'); + const slug = attributes.id || ''; return ( - {mode === 'view' && ( -

    `; diff --git a/src/components/manage/Blocks/ToC/View.jsx b/src/components/manage/Blocks/ToC/View.jsx index 92f0a5f033..a0cd337f53 100644 --- a/src/components/manage/Blocks/ToC/View.jsx +++ b/src/components/manage/Blocks/ToC/View.jsx @@ -56,7 +56,14 @@ const View = (props) => { const items = []; if (!level || !levels.includes(level)) return; tocEntriesLayout.push(id); - tocEntries[id] = { level, title: title || block.plaintext, items, id }; + tocEntries[id] = { + level, + title: title || block.plaintext, + items, + id, + override_toc: block.override_toc, + plaintext: block.plaintext, + }; if (level < rootLevel) { rootLevel = level; } diff --git a/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.jsx b/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.jsx index 97fdbbcd10..148a8d88e3 100644 --- a/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.jsx +++ b/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.jsx @@ -8,15 +8,27 @@ import PropTypes from 'prop-types'; import { map } from 'lodash'; import { List } from 'semantic-ui-react'; import { FormattedMessage, injectIntl } from 'react-intl'; +import { useHistory } from 'react-router-dom'; import AnchorLink from 'react-anchor-link-smooth-scroll'; +import Slugger from 'github-slugger'; -const RenderListItems = ({ items, data }) => { +const RenderListItems = ({ items, data, history }) => { return map(items, (item) => { - const { id, level, title } = item; + const { id, level, title, override_toc, plaintext } = item; + const slug = override_toc + ? Slugger.slug(plaintext) + : Slugger.slug(title) || id; return ( item && ( - {title} + { + history.push({ hash: slug }); + }} + > + {title} + {item.items?.length > 0 && ( { * @extends Component */ const View = ({ data, tocEntries }) => { + const history = useHistory(); return ( <> {data.title && !data.hide_title ? ( @@ -57,7 +70,7 @@ const View = ({ data, tocEntries }) => { bulleted={!data.ordered} as={data.ordered ? 'ol' : 'ul'} > - + ); diff --git a/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx b/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx index 8239a96e97..0bd1ccf8b0 100644 --- a/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx +++ b/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx @@ -9,15 +9,19 @@ import { map } from 'lodash'; import { Menu } from 'semantic-ui-react'; import { FormattedMessage, injectIntl } from 'react-intl'; import AnchorLink from 'react-anchor-link-smooth-scroll'; +import Slugger from 'github-slugger'; const RenderMenuItems = ({ items }) => { return map(items, (item) => { - const { id, level, title } = item; + const { id, level, title, override_toc, plaintext } = item; + const slug = override_toc + ? Slugger.slug(plaintext) + : Slugger.slug(title) || id; return ( item && ( - {title} + {title} {item.items?.length > 0 && } diff --git a/src/helpers/MessageLabels/MessageLabels.js b/src/helpers/MessageLabels/MessageLabels.js index a341f329b7..2e1c4f6b90 100644 --- a/src/helpers/MessageLabels/MessageLabels.js +++ b/src/helpers/MessageLabels/MessageLabels.js @@ -260,6 +260,10 @@ export const messages = defineMessages({ id: 'Show groups of users below', defaultMessage: 'Show groups of users below', }, + urlClipboardCopy: { + id: 'Link copied to clipboard', + defaultMessage: 'Link copied to clipboard', + }, inspectRelations: { id: 'Inspect relations', defaultMessage: 'Inspect relations', diff --git a/src/helpers/ScrollToTop/ScrollToTop.jsx b/src/helpers/ScrollToTop/ScrollToTop.jsx index 6d4a87bcc7..8122fbea63 100644 --- a/src/helpers/ScrollToTop/ScrollToTop.jsx +++ b/src/helpers/ScrollToTop/ScrollToTop.jsx @@ -28,15 +28,17 @@ class ScrollToTop extends React.Component { * @memberof ScrollToTop */ componentDidUpdate(prevProps) { + const { location } = this.props; const noInitialBlocksFocus = // Do not scroll on /edit config.blocks?.initialBlocksFocus === null ? this.props.location?.pathname.slice(-5) !== '/edit' : true; + + const isHash = location?.hash || location?.pathname.hash; if ( - !this.props.location?.hash && - !this.props.location?.pathname.hash && + !isHash && noInitialBlocksFocus && - this.props.location?.pathname !== prevProps.location?.pathname + location?.pathname !== prevProps.location?.pathname ) { window.scrollTo(0, 0); } diff --git a/src/hooks/clipboard/useClipboard.js b/src/hooks/clipboard/useClipboard.js new file mode 100644 index 0000000000..eaabbf7a15 --- /dev/null +++ b/src/hooks/clipboard/useClipboard.js @@ -0,0 +1,26 @@ +import { useState, useRef, useEffect, useCallback } from 'react'; + +export default function useClipboard(clipboardText = '') { + const stringToCopy = useRef(clipboardText); + const [copied, setCopied] = useState(false); + + //synchronous: window.clipboardData.setData(options.format || "text", text); + const copyToClipboard = async (text) => { + if ('clipboard' in navigator) { + return await navigator.clipboard.writeText(text); + } else { + return document.execCommand('copy', true, text); + } + }; + + const copyAction = useCallback(() => { + const copiedString = copyToClipboard(stringToCopy.current); + setCopied(copiedString); + }, [stringToCopy]); + + useEffect(() => { + stringToCopy.current = clipboardText; + }, [clipboardText]); + + return [copied, copyAction, setCopied]; +} diff --git a/src/hooks/index.js b/src/hooks/index.js new file mode 100644 index 0000000000..ab9a86a472 --- /dev/null +++ b/src/hooks/index.js @@ -0,0 +1,2 @@ +export useClipboard from '@plone/volto/hooks/clipboard/useClipboard'; +export useToken from '@plone/volto/hooks/userSession/useToken';