@plone/volto 17.0.0-alpha.13 → 17.0.0-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/CHANGELOG.md +51 -0
  3. package/README.md +2 -2
  4. package/docker-compose.yml +1 -1
  5. package/locales/ca/LC_MESSAGES/volto.po +5 -0
  6. package/locales/ca.json +1 -1
  7. package/locales/de/LC_MESSAGES/volto.po +5 -0
  8. package/locales/de.json +1 -1
  9. package/locales/en/LC_MESSAGES/volto.po +5 -0
  10. package/locales/en.json +1 -1
  11. package/locales/es/LC_MESSAGES/volto.po +49 -44
  12. package/locales/es.json +1 -1
  13. package/locales/eu/LC_MESSAGES/volto.po +5 -0
  14. package/locales/eu.json +1 -1
  15. package/locales/fi/LC_MESSAGES/volto.po +5 -0
  16. package/locales/fi.json +1 -1
  17. package/locales/fr/LC_MESSAGES/volto.po +5 -0
  18. package/locales/fr.json +1 -1
  19. package/locales/it/LC_MESSAGES/volto.po +5 -0
  20. package/locales/it.json +1 -1
  21. package/locales/ja/LC_MESSAGES/volto.po +5 -0
  22. package/locales/ja.json +1 -1
  23. package/locales/nl/LC_MESSAGES/volto.po +5 -0
  24. package/locales/nl.json +1 -1
  25. package/locales/pt/LC_MESSAGES/volto.po +5 -0
  26. package/locales/pt.json +1 -1
  27. package/locales/pt_BR/LC_MESSAGES/volto.po +5 -0
  28. package/locales/pt_BR.json +1 -1
  29. package/locales/ro/LC_MESSAGES/volto.po +5 -0
  30. package/locales/ro.json +1 -1
  31. package/locales/volto.pot +5 -0
  32. package/locales/zh_CN/LC_MESSAGES/volto.po +5 -0
  33. package/locales/zh_CN.json +1 -1
  34. package/package.json +2 -1
  35. package/packages/volto-slate/package.json +1 -1
  36. package/packages/volto-slate/src/blocks/Text/TextBlockView.jsx +20 -16
  37. package/packages/volto-slate/src/editor/config.jsx +5 -4
  38. package/packages/volto-slate/src/editor/less/slate.less +28 -0
  39. package/packages/volto-slate/src/editor/render.jsx +68 -8
  40. package/src/components/manage/Blocks/Listing/ListingBody.jsx +30 -8
  41. package/src/components/manage/Blocks/Title/View.jsx +15 -5
  42. package/src/components/manage/Blocks/Title/View.test.jsx +16 -1
  43. package/src/components/manage/Blocks/ToC/View.jsx +8 -1
  44. package/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.jsx +17 -4
  45. package/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx +6 -2
  46. package/src/components/theme/Anontools/Anontools.jsx +45 -72
  47. package/src/components/theme/Anontools/Anontools.test.jsx +16 -2
  48. package/src/helpers/MessageLabels/MessageLabels.js +4 -0
  49. package/src/helpers/ScrollToTop/ScrollToTop.jsx +5 -3
  50. package/src/hooks/clipboard/useClipboard.js +26 -0
  51. package/src/hooks/content/useContent.js +31 -0
  52. package/src/hooks/index.js +2 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plone/volto-slate",
3
- "version": "17.0.0-alpha.13",
3
+ "version": "17.0.0-alpha.14",
4
4
  "description": "Slate.js integration with Volto",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -1,26 +1,30 @@
1
- import { serializeNodes } from '@plone/volto-slate/editor/render';
1
+ import {
2
+ serializeNodes,
3
+ serializeNodesToText,
4
+ } from '@plone/volto-slate/editor/render';
2
5
  import config from '@plone/volto/registry';
6
+ import { isEqual } from 'lodash';
7
+ import Slugger from 'github-slugger';
3
8
 
4
9
  const TextBlockView = (props) => {
5
10
  const { id, data, styling = {} } = props;
6
11
  const { value, override_toc } = data;
7
12
  const metadata = props.metadata || props.properties;
8
- return serializeNodes(
9
- value,
10
- (node, path) => {
11
- const res = { ...styling };
12
- if (node.type) {
13
- if (
14
- config.settings.slate.topLevelTargetElements.includes(node.type) ||
15
- override_toc
16
- ) {
17
- res.id = id;
18
- }
13
+ const { topLevelTargetElements } = config.settings.slate;
14
+
15
+ const getAttributes = (node, path) => {
16
+ const res = { ...styling };
17
+ if (node.type && isEqual(path, [0])) {
18
+ if (topLevelTargetElements.includes(node.type) || override_toc) {
19
+ const text = serializeNodesToText(node?.children || []);
20
+ const slug = Slugger.slug(text);
21
+ res.id = slug || id;
19
22
  }
20
- return res;
21
- },
22
- { metadata: metadata },
23
- );
23
+ }
24
+ return res;
25
+ };
26
+
27
+ return serializeNodes(value, getAttributes, { metadata: metadata });
24
28
  };
25
29
 
26
30
  export default TextBlockView;
@@ -43,6 +43,7 @@ import {
43
43
  bTagDeserializer,
44
44
  codeTagDeserializer,
45
45
  } from './deserialize';
46
+ import { renderLinkElement } from './render';
46
47
 
47
48
  // Registry of available buttons
48
49
  export const buttons = {
@@ -234,10 +235,10 @@ export const defaultBlockType = 'p';
234
235
  export const elements = {
235
236
  default: ({ attributes, children }) => <p {...attributes}>{children}</p>,
236
237
 
237
- h1: ({ attributes, children }) => <h1 {...attributes}>{children}</h1>,
238
- h2: ({ attributes, children }) => <h2 {...attributes}>{children}</h2>,
239
- h3: ({ attributes, children }) => <h3 {...attributes}>{children}</h3>,
240
- h4: ({ attributes, children }) => <h4 {...attributes}>{children}</h4>,
238
+ h1: renderLinkElement('h1'),
239
+ h2: renderLinkElement('h2'),
240
+ h3: renderLinkElement('h3'),
241
+ h4: renderLinkElement('h4'),
241
242
 
242
243
  li: ({ attributes, children }) => <li {...attributes}>{children}</li>,
243
244
  ol: ({ attributes, children }) => <ol {...attributes}>{children}</ol>,
@@ -0,0 +1,28 @@
1
+ h1,
2
+ h2,
3
+ h3,
4
+ h4 {
5
+ &:hover {
6
+ a.anchor {
7
+ svg {
8
+ opacity: 1;
9
+ transform: rotate(15deg);
10
+ }
11
+ }
12
+ }
13
+
14
+ a.anchor {
15
+ position: absolute;
16
+ display: inline-block;
17
+ margin-left: 5px;
18
+ vertical-align: middle;
19
+
20
+ svg {
21
+ width: 1.6ch;
22
+ fill: #42526e;
23
+ opacity: 0;
24
+ transform: rotate(15deg) translate(-8px, 2px);
25
+ transition: opacity 0.2s ease 0s, transform 0.2s ease 0s;
26
+ }
27
+ }
28
+ }
@@ -1,9 +1,18 @@
1
1
  import React from 'react';
2
2
  import { renderToStaticMarkup } from 'react-dom/server';
3
+ import { useLocation } from 'react-router-dom';
4
+ import { toast } from 'react-toastify';
5
+ import { useIntl } from 'react-intl';
3
6
  import { Node, Text } from 'slate';
4
7
  import cx from 'classnames';
5
- import { isEmpty, isEqual, omit } from 'lodash';
8
+ import { isEmpty, omit } from 'lodash';
9
+ import { UniversalLink, Toast } from '@plone/volto/components';
10
+ import { messages, addAppURL } from '@plone/volto/helpers';
11
+ import useClipboard from '@plone/volto/hooks/clipboard/useClipboard';
6
12
  import config from '@plone/volto/registry';
13
+ import linkSVG from '@plone/volto/icons/link.svg';
14
+
15
+ import './less/slate.less';
7
16
 
8
17
  const OMITTED = ['editor', 'path'];
9
18
 
@@ -106,13 +115,7 @@ export const serializeNodes = (nodes, getAttributes, extras = {}) => {
106
115
  mode="view"
107
116
  key={path}
108
117
  data-slate-data={node.data ? serializeData(node) : null}
109
- attributes={
110
- isEqual(path, [0])
111
- ? getAttributes
112
- ? getAttributes(node, path)
113
- : null
114
- : null
115
- }
118
+ attributes={getAttributes ? getAttributes(node, path) : null}
116
119
  extras={extras}
117
120
  >
118
121
  {_serializeNodes(Array.from(Node.children(editor, path)))}
@@ -153,3 +156,60 @@ export const serializeNodesToText = (nodes) => {
153
156
 
154
157
  export const serializeNodesToHtml = (nodes) =>
155
158
  renderToStaticMarkup(serializeNodes(nodes));
159
+
160
+ export const renderLinkElement = (tagName) => {
161
+ function LinkElement({
162
+ attributes,
163
+ children,
164
+ mode = 'edit',
165
+ className = null,
166
+ }) {
167
+ const { slate = {} } = config.settings;
168
+ const Tag = tagName;
169
+ const slug = attributes.id || '';
170
+ const location = useLocation();
171
+ const appPathname = addAppURL(location.pathname);
172
+ // eslint-disable-next-line no-unused-vars
173
+ const [copied, copy, setCopied] = useClipboard(
174
+ appPathname.concat(`#${slug}`),
175
+ );
176
+ const intl = useIntl();
177
+
178
+ return slate.useLinkedHeadings === false ? (
179
+ <Tag {...attributes} className={className}>
180
+ {children}
181
+ </Tag>
182
+ ) : (
183
+ <Tag {...attributes} className={className}>
184
+ {children}
185
+ {mode === 'view' && slug && (
186
+ <UniversalLink
187
+ className="anchor"
188
+ aria-hidden="true"
189
+ tabIndex={-1}
190
+ href={`#${slug}`}
191
+ >
192
+ <svg
193
+ {...linkSVG.attributes}
194
+ dangerouslySetInnerHTML={{ __html: linkSVG.content }}
195
+ height={null}
196
+ onClick={() => {
197
+ copy();
198
+
199
+ toast.info(
200
+ <Toast
201
+ info
202
+ title={intl.formatMessage(messages.success)}
203
+ content={intl.formatMessage(messages.urlClipboardCopy)}
204
+ />,
205
+ );
206
+ }}
207
+ ></svg>
208
+ </UniversalLink>
209
+ )}
210
+ </Tag>
211
+ );
212
+ }
213
+ LinkElement.displayName = `${tagName}LinkElement`;
214
+ return LinkElement;
215
+ };
@@ -1,14 +1,35 @@
1
- import React, { createRef } from 'react';
1
+ import React, { createRef, useMemo } from 'react';
2
2
  import { FormattedMessage, injectIntl } from 'react-intl';
3
3
  import cx from 'classnames';
4
4
  import { Pagination, Dimmer, Loader } from 'semantic-ui-react';
5
+ import Slugger from 'github-slugger';
5
6
  import { Icon } from '@plone/volto/components';
7
+ import { renderLinkElement } from '@plone/volto-slate/editor/render';
6
8
  import config from '@plone/volto/registry';
7
9
  import withQuerystringResults from './withQuerystringResults';
8
10
 
9
11
  import paginationLeftSVG from '@plone/volto/icons/left-key.svg';
10
12
  import paginationRightSVG from '@plone/volto/icons/right-key.svg';
11
13
 
14
+ const Headline = ({ headlineTag, id, data = {}, listingItems, isEditMode }) => {
15
+ let attr = { id };
16
+ const slug = Slugger.slug(data.headline);
17
+ attr.id = slug || id;
18
+ const LinkedHeadline = useMemo(() => renderLinkElement(headlineTag), [
19
+ headlineTag,
20
+ ]);
21
+ return (
22
+ <LinkedHeadline
23
+ mode={!isEditMode && 'view'}
24
+ children={data.headline}
25
+ attributes={attr}
26
+ className={cx('headline', {
27
+ emptyListing: !listingItems?.length > 0,
28
+ })}
29
+ />
30
+ );
31
+ };
32
+
12
33
  const ListingBody = withQuerystringResults((props) => {
13
34
  const {
14
35
  data = {},
@@ -22,6 +43,7 @@ const ListingBody = withQuerystringResults((props) => {
22
43
  nextBatch,
23
44
  isFolderContentsListing,
24
45
  hasLoaded,
46
+ id,
25
47
  } = props;
26
48
 
27
49
  let ListingBodyTemplate;
@@ -50,13 +72,13 @@ const ListingBody = withQuerystringResults((props) => {
50
72
  return (
51
73
  <>
52
74
  {data.headline && (
53
- <HeadlineTag
54
- className={cx('headline', {
55
- emptyListing: !listingItems?.length > 0,
56
- })}
57
- >
58
- {data.headline}
59
- </HeadlineTag>
75
+ <Headline
76
+ headlineTag={HeadlineTag}
77
+ id={id}
78
+ listingItems={listingItems}
79
+ data={data}
80
+ isEditMode={isEditMode}
81
+ />
60
82
  )}
61
83
  {listingItems?.length > 0 ? (
62
84
  <div ref={listingRef}>
@@ -3,19 +3,29 @@
3
3
  * @module volto-slate/blocks/Title/TitleBlockView
4
4
  */
5
5
 
6
- import React from 'react';
6
+ import React, { useMemo } from 'react';
7
7
  import PropTypes from 'prop-types';
8
+ import Slugger from 'github-slugger';
9
+ import { renderLinkElement } from '@plone/volto-slate/editor/render';
8
10
 
9
11
  /**
10
12
  * View title block component.
11
13
  * @class View
12
14
  * @extends Component
13
15
  */
14
- const TitleBlockView = ({ properties, metadata }) => {
16
+ const TitleBlockView = ({ properties, metadata, id, children }) => {
17
+ let attr = { id };
18
+ const title = (properties || metadata)['title'];
19
+ const slug = Slugger.slug(title);
20
+ attr.id = slug || id;
21
+ const LinkedTitle = useMemo(() => renderLinkElement('h1'), []);
15
22
  return (
16
- <h1 className="documentFirstHeading">
17
- {(metadata || properties)['title'] || ''}
18
- </h1>
23
+ <LinkedTitle
24
+ mode="view"
25
+ children={title ?? children}
26
+ attributes={attr}
27
+ className={'documentFirstHeading'}
28
+ />
19
29
  );
20
30
  };
21
31
 
@@ -1,10 +1,25 @@
1
1
  import React from 'react';
2
2
  import renderer from 'react-test-renderer';
3
+ import configureStore from 'redux-mock-store';
4
+ import { Provider } from 'react-intl-redux';
5
+ import { MemoryRouter } from 'react-router-dom';
3
6
  import View from './View';
4
7
 
8
+ const mockStore = configureStore();
9
+
5
10
  test('renders a view title component', () => {
11
+ const store = mockStore({
12
+ intl: {
13
+ locale: 'en',
14
+ messages: {},
15
+ },
16
+ });
6
17
  const component = renderer.create(
7
- <View properties={{ title: 'My Title' }} />,
18
+ <Provider store={store}>
19
+ <MemoryRouter>
20
+ <View properties={{ title: 'My Title' }} id="a123" />
21
+ </MemoryRouter>
22
+ </Provider>,
8
23
  );
9
24
  const json = component.toJSON();
10
25
  expect(json).toMatchSnapshot();
@@ -56,7 +56,14 @@ const View = (props) => {
56
56
  const items = [];
57
57
  if (!level || !levels.includes(level)) return;
58
58
  tocEntriesLayout.push(id);
59
- tocEntries[id] = { level, title: title || block.plaintext, items, id };
59
+ tocEntries[id] = {
60
+ level,
61
+ title: title || block.plaintext,
62
+ items,
63
+ id,
64
+ override_toc: block.override_toc,
65
+ plaintext: block.plaintext,
66
+ };
60
67
  if (level < rootLevel) {
61
68
  rootLevel = level;
62
69
  }
@@ -8,15 +8,27 @@ import PropTypes from 'prop-types';
8
8
  import { map } from 'lodash';
9
9
  import { List } from 'semantic-ui-react';
10
10
  import { FormattedMessage, injectIntl } from 'react-intl';
11
+ import { useHistory } from 'react-router-dom';
11
12
  import AnchorLink from 'react-anchor-link-smooth-scroll';
13
+ import Slugger from 'github-slugger';
12
14
 
13
- const RenderListItems = ({ items, data }) => {
15
+ const RenderListItems = ({ items, data, history }) => {
14
16
  return map(items, (item) => {
15
- const { id, level, title } = item;
17
+ const { id, level, title, override_toc, plaintext } = item;
18
+ const slug = override_toc
19
+ ? Slugger.slug(plaintext)
20
+ : Slugger.slug(title) || id;
16
21
  return (
17
22
  item && (
18
23
  <List.Item key={id} className={`item headline-${level}`} as="li">
19
- <AnchorLink href={`#${id}`}>{title}</AnchorLink>
24
+ <AnchorLink
25
+ href={`#${slug}`}
26
+ onClick={(e) => {
27
+ history.push({ hash: slug });
28
+ }}
29
+ >
30
+ {title}
31
+ </AnchorLink>
20
32
  {item.items?.length > 0 && (
21
33
  <List
22
34
  ordered={data.ordered}
@@ -38,6 +50,7 @@ const RenderListItems = ({ items, data }) => {
38
50
  * @extends Component
39
51
  */
40
52
  const View = ({ data, tocEntries }) => {
53
+ const history = useHistory();
41
54
  return (
42
55
  <>
43
56
  {data.title && !data.hide_title ? (
@@ -57,7 +70,7 @@ const View = ({ data, tocEntries }) => {
57
70
  bulleted={!data.ordered}
58
71
  as={data.ordered ? 'ol' : 'ul'}
59
72
  >
60
- <RenderListItems items={tocEntries} data={data} />
73
+ <RenderListItems items={tocEntries} data={data} history={history} />
61
74
  </List>
62
75
  </>
63
76
  );
@@ -9,15 +9,19 @@ import { map } from 'lodash';
9
9
  import { Menu } from 'semantic-ui-react';
10
10
  import { FormattedMessage, injectIntl } from 'react-intl';
11
11
  import AnchorLink from 'react-anchor-link-smooth-scroll';
12
+ import Slugger from 'github-slugger';
12
13
 
13
14
  const RenderMenuItems = ({ items }) => {
14
15
  return map(items, (item) => {
15
- const { id, level, title } = item;
16
+ const { id, level, title, override_toc, plaintext } = item;
17
+ const slug = override_toc
18
+ ? Slugger.slug(plaintext)
19
+ : Slugger.slug(title) || id;
16
20
  return (
17
21
  item && (
18
22
  <React.Fragment key={id}>
19
23
  <Menu.Item className={`headline-${level}`}>
20
- <AnchorLink href={`#${id}`}>{title}</AnchorLink>
24
+ <AnchorLink href={`#${slug}`}>{title}</AnchorLink>
21
25
  </Menu.Item>
22
26
  {item.items?.length > 0 && <RenderMenuItems items={item.items} />}
23
27
  </React.Fragment>
@@ -1,83 +1,56 @@
1
- /**
2
- * Anontools component.
3
- * @module components/theme/Anontools/Anontools
4
- */
5
-
6
- import React, { Component } from 'react';
7
1
  import PropTypes from 'prop-types';
8
- import { connect } from 'react-redux';
9
2
  import { Link } from 'react-router-dom';
10
3
  import { Menu } from 'semantic-ui-react';
11
4
  import { FormattedMessage } from 'react-intl';
5
+ import { flattenToAppURL } from '@plone/volto/helpers';
6
+ import { useToken } from '@plone/volto/hooks/userSession/useToken';
7
+ import { useContent } from '@plone/volto/hooks/content/useContent';
12
8
  import config from '@plone/volto/registry';
13
9
 
14
- /**
15
- * Anontools container class.
16
- */
17
- export class Anontools extends Component {
18
- /**
19
- * Property types.
20
- * @property {Object} propTypes Property types.
21
- * @static
22
- */
23
- static propTypes = {
24
- token: PropTypes.string,
25
- content: PropTypes.shape({
26
- '@id': PropTypes.string,
27
- }),
28
- };
29
-
30
- /**
31
- * Default properties.
32
- * @property {Object} defaultProps Default properties.
33
- * @static
34
- */
35
- static defaultProps = {
36
- token: null,
37
- content: {
38
- '@id': null,
39
- },
40
- };
10
+ const Anontools = () => {
11
+ const token = useToken();
12
+ const { data: content } = useContent();
41
13
 
42
- /**
43
- * Render method.
44
- * @method render
45
- * @returns {string} Markup for the component.
46
- */
47
- render() {
48
- const { settings } = config;
49
- return (
50
- !this.props.token && (
51
- <Menu pointing secondary floated="right">
14
+ const { settings } = config;
15
+ return (
16
+ !token && (
17
+ <Menu pointing secondary floated="right">
18
+ <Menu.Item>
19
+ <Link
20
+ aria-label="login"
21
+ to={`/login${
22
+ content?.['@id']
23
+ ? `?return_url=${flattenToAppURL(content['@id'])}`
24
+ : ''
25
+ }`}
26
+ >
27
+ <FormattedMessage id="Log in" defaultMessage="Log in" />
28
+ </Link>
29
+ </Menu.Item>
30
+ {settings.showSelfRegistration && (
52
31
  <Menu.Item>
53
- <Link
54
- aria-label="login"
55
- to={`/login${
56
- this.props.content?.['@id']
57
- ? `?return_url=${this.props.content['@id'].replace(
58
- settings.apiPath,
59
- '',
60
- )}`
61
- : ''
62
- }`}
63
- >
64
- <FormattedMessage id="Log in" defaultMessage="Log in" />
32
+ <Link aria-label="register" to="/register">
33
+ <FormattedMessage id="Register" defaultMessage="Register" />
65
34
  </Link>
66
35
  </Menu.Item>
67
- {settings.showSelfRegistration && (
68
- <Menu.Item>
69
- <Link aria-label="register" to="/register">
70
- <FormattedMessage id="Register" defaultMessage="Register" />
71
- </Link>
72
- </Menu.Item>
73
- )}
74
- </Menu>
75
- )
76
- );
77
- }
78
- }
36
+ )}
37
+ </Menu>
38
+ )
39
+ );
40
+ };
41
+
42
+ export default Anontools;
43
+
44
+ Anontools.propTypes = {
45
+ token: PropTypes.string,
46
+ content: PropTypes.shape({
47
+ '@id': PropTypes.string,
48
+ }),
49
+ };
79
50
 
80
- export default connect((state) => ({
81
- token: state.userSession.token,
82
- content: state.content.data,
83
- }))(Anontools);
51
+ Anontools.defaultProps = {
52
+ token: null,
53
+ content: {
54
+ '@id': null,
55
+ },
56
+ };
@@ -12,7 +12,14 @@ describe('Anontools', () => {
12
12
  it('renders an anontools component when no token is specified', () => {
13
13
  const store = mockStore({
14
14
  userSession: { token: null },
15
- content: { data: { '@id': 'myid' } },
15
+ content: {
16
+ data: { '@id': 'myid' },
17
+ get: {
18
+ loading: false,
19
+ loaded: true,
20
+ error: null,
21
+ },
22
+ },
16
23
  intl: {
17
24
  locale: 'en',
18
25
  messages: {},
@@ -32,7 +39,14 @@ describe('Anontools', () => {
32
39
  it('should not render an anontools component when a token is specified', () => {
33
40
  const store = mockStore({
34
41
  userSession: { token: '1234' },
35
- content: { data: {} },
42
+ content: {
43
+ data: {},
44
+ get: {
45
+ loading: false,
46
+ loaded: true,
47
+ error: null,
48
+ },
49
+ },
36
50
  intl: {
37
51
  locale: 'en',
38
52
  messages: {},
@@ -260,6 +260,10 @@ export const messages = defineMessages({
260
260
  id: 'Show groups of users below',
261
261
  defaultMessage: 'Show groups of users below',
262
262
  },
263
+ urlClipboardCopy: {
264
+ id: 'Link copied to clipboard',
265
+ defaultMessage: 'Link copied to clipboard',
266
+ },
263
267
  inspectRelations: {
264
268
  id: 'Inspect relations',
265
269
  defaultMessage: 'Inspect relations',
@@ -28,15 +28,17 @@ class ScrollToTop extends React.Component {
28
28
  * @memberof ScrollToTop
29
29
  */
30
30
  componentDidUpdate(prevProps) {
31
+ const { location } = this.props;
31
32
  const noInitialBlocksFocus = // Do not scroll on /edit
32
33
  config.blocks?.initialBlocksFocus === null
33
34
  ? this.props.location?.pathname.slice(-5) !== '/edit'
34
35
  : true;
36
+
37
+ const isHash = location?.hash || location?.pathname.hash;
35
38
  if (
36
- !this.props.location?.hash &&
37
- !this.props.location?.pathname.hash &&
39
+ !isHash &&
38
40
  noInitialBlocksFocus &&
39
- this.props.location?.pathname !== prevProps.location?.pathname
41
+ location?.pathname !== prevProps.location?.pathname
40
42
  ) {
41
43
  window.scrollTo(0, 0);
42
44
  }
@@ -0,0 +1,26 @@
1
+ import { useState, useRef, useEffect, useCallback } from 'react';
2
+
3
+ export default function useClipboard(clipboardText = '') {
4
+ const stringToCopy = useRef(clipboardText);
5
+ const [copied, setCopied] = useState(false);
6
+
7
+ //synchronous: window.clipboardData.setData(options.format || "text", text);
8
+ const copyToClipboard = async (text) => {
9
+ if ('clipboard' in navigator) {
10
+ return await navigator.clipboard.writeText(text);
11
+ } else {
12
+ return document.execCommand('copy', true, text);
13
+ }
14
+ };
15
+
16
+ const copyAction = useCallback(() => {
17
+ const copiedString = copyToClipboard(stringToCopy.current);
18
+ setCopied(copiedString);
19
+ }, [stringToCopy]);
20
+
21
+ useEffect(() => {
22
+ stringToCopy.current = clipboardText;
23
+ }, [clipboardText]);
24
+
25
+ return [copied, copyAction, setCopied];
26
+ }
@@ -0,0 +1,31 @@
1
+ import { useSelector, shallowEqual } from 'react-redux';
2
+
3
+ /**
4
+ * useContent hook
5
+ *
6
+ * This hook returns the current content that is stored in the Redux store in the
7
+ * `content` reducer, and returns it along with the related state (loading/loaded/error).
8
+ *
9
+ * @export
10
+ * @return {{ data: ContentData, loading: boolean, loaded: boolean, error: Error }}
11
+ */
12
+ export function useContent() {
13
+ const data = useSelector((state) => state.content.data, shallowEqual);
14
+ const loading = useSelector((state) => state.content.get.loading);
15
+ const loaded = useSelector((state) => state.content.get.loaded);
16
+ const error = useSelector((state) => state.content.get.error, shallowEqual);
17
+
18
+ return { data, loading, loaded, error };
19
+ }
20
+
21
+ // For reference purposes: Potential future useQuery version
22
+ // export function useContent() {
23
+ // // the cache will need to know the current location
24
+ // const pathname = useLocation();
25
+ // const query = useQuery(getContentQuery({ path }))
26
+
27
+ // // This might not be needed if we rename the properties
28
+ // const {isLoading: loading, isSuccess: loaded, ...rest} = query;
29
+
30
+ // return { loading, loaded, ...rest };
31
+ // }