@eeacms/volto-slate-footnote 7.2.0 → 7.2.1

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [7.2.1](https://github.com/eea/volto-slate-footnote/compare/7.2.0...7.2.1) - 30 January 2025
8
+
9
+ #### :bug: Bug Fixes
10
+
11
+ - fix: Improve the algorithm of link detection and html insertion to remove bugs - refs 282898 [dobri1408 - [`4e03b60`](https://github.com/eea/volto-slate-footnote/commit/4e03b6003df6e1fae89343bade9fc35425fb7de9)]
12
+
13
+ #### :hammer_and_wrench: Others
14
+
15
+ - i18n: Add german translations [Valentin Dumitru - [`aedee6f`](https://github.com/eea/volto-slate-footnote/commit/aedee6fab827de485137e703854f98d8a29b45e9)]
7
16
  ### [7.2.0](https://github.com/eea/volto-slate-footnote/compare/7.1.1...7.2.0) - 28 November 2024
8
17
 
9
18
  ### [7.1.1](https://github.com/eea/volto-slate-footnote/compare/7.1.0...7.1.1) - 16 September 2024
@@ -11,22 +11,57 @@ msgstr ""
11
11
  "Content-Transfer-Encoding: \n"
12
12
  "Plural-Forms: \n"
13
13
 
14
+ #. Default: "Edit footnote"
14
15
  #: editor/index
15
- # defaultMessage: Edit footnote
16
16
  msgid "Edit footnote"
17
- msgstr ""
17
+ msgstr "Fußnote bearbeiten"
18
+
19
+ #. Default: "Footnotes"
20
+ #: index
21
+ msgid "Footnotes"
22
+ msgstr "Fußnoten"
18
23
 
24
+ #. Default: "No options"
19
25
  #: editor/MultiSelectSearchWidget
20
- # defaultMessage: No options
21
26
  msgid "No options"
22
- msgstr ""
27
+ msgstr "Keine Optionen"
23
28
 
29
+ #. Default: "Remove footnote"
24
30
  #: editor/index
25
- # defaultMessage: Remove footnote
26
31
  msgid "Remove footnote"
27
- msgstr ""
32
+ msgstr "Fußnote entfernen"
28
33
 
34
+ #. Default: "Select…"
29
35
  #: editor/MultiSelectSearchWidget
30
- # defaultMessage: Select…
31
36
  msgid "Select…"
32
- msgstr ""
37
+ msgstr "Auswählen..."
38
+
39
+ #. Default: "Block title"
40
+ #: Blocks/Footnote/schema
41
+ msgid "blockTitle"
42
+ msgstr "Blocktitel"
43
+
44
+ #. Default: "Friendly name to be displayed as block title"
45
+ #: Blocks/Footnote/schema
46
+ msgid "blockTitleDescription"
47
+ msgstr "Name, der als Blocktitel angezeigt werden soll"
48
+
49
+ #. Default: "Default"
50
+ #: Blocks/Footnote/schema
51
+ msgid "default"
52
+ msgstr "Standard"
53
+
54
+ #. Default: "Entire document"
55
+ #: Blocks/Footnote/schema
56
+ msgid "entireDocument"
57
+ msgstr "Ganzes Dokument"
58
+
59
+ #. Default: "Lookup citation references on the entire document"
60
+ #: Blocks/Footnote/schema
61
+ msgid "entireDocumentDescription"
62
+ msgstr "Nachschlagen von Zitierangaben im gesamten Dokument"
63
+
64
+ #. Default: "Footnote block"
65
+ #: Blocks/Footnote/schema
66
+ msgid "footnoteBlock"
67
+ msgstr "Fußnoten-Block"
@@ -11,22 +11,57 @@ msgstr ""
11
11
  "Content-Transfer-Encoding: \n"
12
12
  "Plural-Forms: \n"
13
13
 
14
+ #. Default: "Edit footnote"
14
15
  #: editor/index
15
- # defaultMessage: Edit footnote
16
16
  msgid "Edit footnote"
17
17
  msgstr ""
18
18
 
19
+ #. Default: "Footnotes"
20
+ #: index
21
+ msgid "Footnotes"
22
+ msgstr ""
23
+
24
+ #. Default: "No options"
19
25
  #: editor/MultiSelectSearchWidget
20
- # defaultMessage: No options
21
26
  msgid "No options"
22
27
  msgstr ""
23
28
 
29
+ #. Default: "Remove footnote"
24
30
  #: editor/index
25
- # defaultMessage: Remove footnote
26
31
  msgid "Remove footnote"
27
32
  msgstr ""
28
33
 
34
+ #. Default: "Select…"
29
35
  #: editor/MultiSelectSearchWidget
30
- # defaultMessage: Select…
31
36
  msgid "Select…"
32
37
  msgstr ""
38
+
39
+ #. Default: "Block title"
40
+ #: Blocks/Footnote/schema
41
+ msgid "blockTitle"
42
+ msgstr ""
43
+
44
+ #. Default: "Friendly name to be displayed as block title"
45
+ #: Blocks/Footnote/schema
46
+ msgid "blockTitleDescription"
47
+ msgstr ""
48
+
49
+ #. Default: "Default"
50
+ #: Blocks/Footnote/schema
51
+ msgid "default"
52
+ msgstr ""
53
+
54
+ #. Default: "Entire document"
55
+ #: Blocks/Footnote/schema
56
+ msgid "entireDocument"
57
+ msgstr ""
58
+
59
+ #. Default: "Lookup citation references on the entire document"
60
+ #: Blocks/Footnote/schema
61
+ msgid "entireDocumentDescription"
62
+ msgstr ""
63
+
64
+ #. Default: "Footnote block"
65
+ #: Blocks/Footnote/schema
66
+ msgid "footnoteBlock"
67
+ msgstr ""
@@ -11,22 +11,57 @@ msgstr ""
11
11
  "Content-Transfer-Encoding: \n"
12
12
  "Plural-Forms: \n"
13
13
 
14
+ #. Default: "Edit footnote"
14
15
  #: editor/index
15
- # defaultMessage: Edit footnote
16
16
  msgid "Edit footnote"
17
17
  msgstr ""
18
18
 
19
+ #. Default: "Footnotes"
20
+ #: index
21
+ msgid "Footnotes"
22
+ msgstr ""
23
+
24
+ #. Default: "No options"
19
25
  #: editor/MultiSelectSearchWidget
20
- # defaultMessage: No options
21
26
  msgid "No options"
22
27
  msgstr ""
23
28
 
29
+ #. Default: "Remove footnote"
24
30
  #: editor/index
25
- # defaultMessage: Remove footnote
26
31
  msgid "Remove footnote"
27
32
  msgstr ""
28
33
 
34
+ #. Default: "Select…"
29
35
  #: editor/MultiSelectSearchWidget
30
- # defaultMessage: Select…
31
36
  msgid "Select…"
32
37
  msgstr ""
38
+
39
+ #. Default: "Block title"
40
+ #: Blocks/Footnote/schema
41
+ msgid "blockTitle"
42
+ msgstr ""
43
+
44
+ #. Default: "Friendly name to be displayed as block title"
45
+ #: Blocks/Footnote/schema
46
+ msgid "blockTitleDescription"
47
+ msgstr ""
48
+
49
+ #. Default: "Default"
50
+ #: Blocks/Footnote/schema
51
+ msgid "default"
52
+ msgstr ""
53
+
54
+ #. Default: "Entire document"
55
+ #: Blocks/Footnote/schema
56
+ msgid "entireDocument"
57
+ msgstr ""
58
+
59
+ #. Default: "Lookup citation references on the entire document"
60
+ #: Blocks/Footnote/schema
61
+ msgid "entireDocumentDescription"
62
+ msgstr ""
63
+
64
+ #. Default: "Footnote block"
65
+ #: Blocks/Footnote/schema
66
+ msgid "footnoteBlock"
67
+ msgstr ""
@@ -11,22 +11,57 @@ msgstr ""
11
11
  "Content-Transfer-Encoding: \n"
12
12
  "Plural-Forms: \n"
13
13
 
14
+ #. Default: "Edit footnote"
14
15
  #: editor/index
15
- # defaultMessage: Edit footnote
16
16
  msgid "Edit footnote"
17
17
  msgstr ""
18
18
 
19
+ #. Default: "Footnotes"
20
+ #: index
21
+ msgid "Footnotes"
22
+ msgstr ""
23
+
24
+ #. Default: "No options"
19
25
  #: editor/MultiSelectSearchWidget
20
- # defaultMessage: No options
21
26
  msgid "No options"
22
27
  msgstr ""
23
28
 
29
+ #. Default: "Remove footnote"
24
30
  #: editor/index
25
- # defaultMessage: Remove footnote
26
31
  msgid "Remove footnote"
27
32
  msgstr ""
28
33
 
34
+ #. Default: "Select…"
29
35
  #: editor/MultiSelectSearchWidget
30
- # defaultMessage: Select…
31
36
  msgid "Select…"
32
37
  msgstr ""
38
+
39
+ #. Default: "Block title"
40
+ #: Blocks/Footnote/schema
41
+ msgid "blockTitle"
42
+ msgstr ""
43
+
44
+ #. Default: "Friendly name to be displayed as block title"
45
+ #: Blocks/Footnote/schema
46
+ msgid "blockTitleDescription"
47
+ msgstr ""
48
+
49
+ #. Default: "Default"
50
+ #: Blocks/Footnote/schema
51
+ msgid "default"
52
+ msgstr ""
53
+
54
+ #. Default: "Entire document"
55
+ #: Blocks/Footnote/schema
56
+ msgid "entireDocument"
57
+ msgstr ""
58
+
59
+ #. Default: "Lookup citation references on the entire document"
60
+ #: Blocks/Footnote/schema
61
+ msgid "entireDocumentDescription"
62
+ msgstr ""
63
+
64
+ #. Default: "Footnote block"
65
+ #: Blocks/Footnote/schema
66
+ msgid "footnoteBlock"
67
+ msgstr ""
package/locales/volto.pot CHANGED
@@ -1,34 +1,69 @@
1
1
  msgid ""
2
2
  msgstr ""
3
3
  "Project-Id-Version: Plone\n"
4
- "POT-Creation-Date: 2023-08-29T17:14:46.877Z\n"
4
+ "POT-Creation-Date: 2025-01-14T11:22:56.136Z\n"
5
5
  "Last-Translator: Plone i18n <plone-i18n@lists.sourceforge.net>\n"
6
6
  "Language-Team: Plone i18n <plone-i18n@lists.sourceforge.net>\n"
7
- "MIME-Version: 1.0\n"
8
7
  "Content-Type: text/plain; charset=utf-8\n"
9
8
  "Content-Transfer-Encoding: 8bit\n"
10
9
  "Plural-Forms: nplurals=1; plural=0;\n"
10
+ "MIME-Version: 1.0\n"
11
11
  "Language-Code: en\n"
12
12
  "Language-Name: English\n"
13
13
  "Preferred-Encodings: utf-8\n"
14
14
  "Domain: volto\n"
15
15
 
16
+ #. Default: "Edit footnote"
16
17
  #: editor/index
17
- # defaultMessage: Edit footnote
18
18
  msgid "Edit footnote"
19
19
  msgstr ""
20
20
 
21
+ #. Default: "Footnotes"
22
+ #: index
23
+ msgid "Footnotes"
24
+ msgstr ""
25
+
26
+ #. Default: "No options"
21
27
  #: editor/MultiSelectSearchWidget
22
- # defaultMessage: No options
23
28
  msgid "No options"
24
29
  msgstr ""
25
30
 
31
+ #. Default: "Remove footnote"
26
32
  #: editor/index
27
- # defaultMessage: Remove footnote
28
33
  msgid "Remove footnote"
29
34
  msgstr ""
30
35
 
36
+ #. Default: "Select…"
31
37
  #: editor/MultiSelectSearchWidget
32
- # defaultMessage: Select…
33
38
  msgid "Select…"
34
39
  msgstr ""
40
+
41
+ #. Default: "Block title"
42
+ #: Blocks/Footnote/schema
43
+ msgid "blockTitle"
44
+ msgstr ""
45
+
46
+ #. Default: "Friendly name to be displayed as block title"
47
+ #: Blocks/Footnote/schema
48
+ msgid "blockTitleDescription"
49
+ msgstr ""
50
+
51
+ #. Default: "Default"
52
+ #: Blocks/Footnote/schema
53
+ msgid "default"
54
+ msgstr ""
55
+
56
+ #. Default: "Entire document"
57
+ #: Blocks/Footnote/schema
58
+ msgid "entireDocument"
59
+ msgstr ""
60
+
61
+ #. Default: "Lookup citation references on the entire document"
62
+ #: Blocks/Footnote/schema
63
+ msgid "entireDocumentDescription"
64
+ msgstr ""
65
+
66
+ #. Default: "Footnote block"
67
+ #: Blocks/Footnote/schema
68
+ msgid "footnoteBlock"
69
+ msgstr ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-slate-footnote",
3
- "version": "7.2.0",
3
+ "version": "7.2.1",
4
4
  "description": "volto-slate-footnote: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -18,6 +18,8 @@ const FootnotesBlockEdit = (props) => {
18
18
  instructions = props.formDescription;
19
19
  }
20
20
 
21
+ const blockSchema = schema(props.intl);
22
+
21
23
  return (
22
24
  <>
23
25
  <FootnotesBlockView {...props} properties={properties} />
@@ -29,8 +31,8 @@ const FootnotesBlockEdit = (props) => {
29
31
  )}
30
32
  {!data?.readOnlySettings && (
31
33
  <InlineForm
32
- schema={schema}
33
- title={schema.title}
34
+ schema={blockSchema}
35
+ title={blockSchema.title}
34
36
  onChangeField={(id, value) => {
35
37
  onChangeBlock(block, {
36
38
  ...data,
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { escapeRegExp } from 'lodash';
2
+
3
3
  import {
4
4
  openAccordionOrTabIfContainsFootnoteReference,
5
5
  getAllBlocksAndSlateFields,
@@ -8,10 +8,9 @@ import {
8
8
  import './less/public.less';
9
9
 
10
10
  import { UniversalLink } from '@plone/volto/components';
11
+ import { renderTextWithLinks } from '../../editor/utils';
11
12
 
12
13
  const alphabet = 'abcdefghijklmnopqrstuvwxyz';
13
- const urlRegex =
14
- /\b((http|https|ftp):\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(\/[^\s]*)?\b/g;
15
14
 
16
15
  /**
17
16
  * @summary The React component that displays the list of footnotes inserted
@@ -78,47 +77,6 @@ const FootnotesBlockView = (props) => {
78
77
  startList = citationIndice;
79
78
  }
80
79
 
81
- const renderTextWithLinks = (text) => {
82
- if (!text) return null;
83
- const links = text.match(urlRegex);
84
- if (!links) {
85
- return (
86
- <div
87
- dangerouslySetInnerHTML={{
88
- __html: text,
89
- }}
90
- />
91
- );
92
- }
93
- let result = [];
94
- const parts = text.split(
95
- new RegExp(`(${links.map((link) => escapeRegExp(link)).join('|')})`),
96
- );
97
- parts.forEach((part, index) => {
98
- if (links.includes(part)) {
99
- result.push(
100
- <UniversalLink
101
- key={`link-${index}`}
102
- href={part}
103
- openLinkInNewTab={false}
104
- >
105
- {part}
106
- </UniversalLink>,
107
- );
108
- return;
109
- }
110
-
111
- result.push(
112
- <span
113
- dangerouslySetInnerHTML={{
114
- __html: part,
115
- }}
116
- />,
117
- );
118
- });
119
-
120
- return <div>{result}</div>;
121
- };
122
80
  return (
123
81
  <div className="footnotes-listing-block">
124
82
  <h3 title={placeholder}>{title}</h3>
@@ -141,7 +99,7 @@ const FootnotesBlockView = (props) => {
141
99
  key={`footnote-${zoteroId || uid}`}
142
100
  id={`footnote-${zoteroId || uid}`}
143
101
  >
144
- <div>{renderTextWithLinks(footnoteText)}</div>
102
+ <div>{renderTextWithLinks(footnoteText, zoteroId)}</div>
145
103
  {refsList ? (
146
104
  <>
147
105
  {/** some footnotes are never parent so we need the parent to reference */}
@@ -0,0 +1,84 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import FootnotesBlockView from './FootnotesBlockView';
5
+
6
+ jest.mock('@plone/volto/components', () => ({
7
+ UniversalLink: ({ children, href }) => <a href={href}>{children}</a>,
8
+ }));
9
+
10
+ jest.mock('@eeacms/volto-slate-footnote/editor/utils', () => ({
11
+ openAccordionOrTabIfContainsFootnoteReference: jest.fn(),
12
+ renderTextWithLinks: jest.fn(),
13
+ getAllBlocksAndSlateFields: jest.fn(() => [
14
+ { id: 'block1', footnote: 'Footnote with no link' },
15
+ { id: 'block2', footnote: 'Footnote with link http://example.com' },
16
+ { id: 'block3', footnote: 'Footnote with <b>HTML</b>' },
17
+ ]),
18
+ makeFootnoteListOfUniqueItems: jest.fn((blocks) => ({
19
+ note1: {
20
+ uid: '1',
21
+ footnote: 'First note with a reference',
22
+ zoteroId: 'zotero1',
23
+ refs: { ref1: 'ref1' },
24
+ },
25
+ note2: {
26
+ uid: '2',
27
+ footnote: 'Second note with multiple references',
28
+ zoteroId: null,
29
+ refs: { ref2: 'ref2', ref3: 'ref3' },
30
+ },
31
+ note3: {
32
+ uid: '3',
33
+ footnote: '<i>Note with HTML</i>',
34
+ zoteroId: 'zotero3',
35
+ refs: {},
36
+ },
37
+ })),
38
+ }));
39
+
40
+ describe('FootnotesBlockView', () => {
41
+ const propsVariations = [
42
+ {
43
+ description: 'renders with global metadata',
44
+ props: {
45
+ data: { title: 'Global Metadata', global: true, placeholder: 'Global' },
46
+ properties: { test: 'metadata' },
47
+ tabData: null,
48
+ content: null,
49
+ metadata: { test: 'metadata' },
50
+ },
51
+ },
52
+ {
53
+ description: 'renders with tabData',
54
+ props: {
55
+ data: { title: 'Tab Data', global: false, placeholder: 'Tab' },
56
+ properties: { test: 'tabProperties' },
57
+ tabData: { test: 'tabData' },
58
+ content: null,
59
+ },
60
+ },
61
+ {
62
+ description: 'renders with content',
63
+ props: {
64
+ data: { title: 'Content Data', global: false, placeholder: 'Content' },
65
+ properties: { test: 'contentProperties' },
66
+ tabData: null,
67
+ content: { test: 'contentData' },
68
+ },
69
+ },
70
+ {
71
+ description: 'renders with no metadata',
72
+ props: {
73
+ data: { title: 'No Metadata', global: false, placeholder: 'Default' },
74
+ properties: { test: 'defaultProperties' },
75
+ tabData: null,
76
+ content: null,
77
+ },
78
+ },
79
+ ];
80
+
81
+ test.each(propsVariations)('$description', ({ props }) => {
82
+ render(<FootnotesBlockView {...props} />);
83
+ });
84
+ });
@@ -1,23 +1,52 @@
1
- export const FootnoteBlockSchema = {
2
- title: 'Footnotes block',
1
+ import { defineMessages } from 'react-intl';
2
+
3
+ const messages = defineMessages({
4
+ footnoteBlock: {
5
+ id: 'footnoteBlock',
6
+ defaultMessage: 'Footnote block',
7
+ },
8
+ default: {
9
+ id: 'default',
10
+ defaultMessage: 'Default',
11
+ },
12
+ blockTitle: {
13
+ id: 'blockTitle',
14
+ defaultMessage: 'Block title',
15
+ },
16
+ blockTitleDescription: {
17
+ id: 'blockTitleDescription',
18
+ defaultMessage: 'Friendly name to be displayed as block title',
19
+ },
20
+ entireDocument: {
21
+ id: 'entireDocument',
22
+ defaultMessage: 'Entire document',
23
+ },
24
+ entireDocumentDescription: {
25
+ id: 'entireDocumentDescription',
26
+ defaultMessage: 'Lookup citation references on the entire document',
27
+ },
28
+ });
29
+
30
+ export const FootnoteBlockSchema = (intl) => ({
31
+ title: intl.formatMessage(messages.footnoteBlock),
3
32
  fieldsets: [
4
33
  {
5
34
  id: 'default',
6
- title: 'Default',
35
+ title: intl.formatMessage(messages.default),
7
36
  fields: ['title', 'global'],
8
37
  },
9
38
  ],
10
39
  properties: {
11
40
  title: {
12
- title: 'Block title',
13
- description: 'Friendly name to be displayed as block title',
41
+ title: intl.formatMessage(messages.blockTitle),
42
+ description: intl.formatMessage(messages.blockTitleDescription),
14
43
  type: 'string',
15
44
  },
16
45
  global: {
17
- title: 'Entire document',
18
- description: 'Lookup citation references on the entire document',
46
+ title: intl.formatMessage(messages.entireDocument),
47
+ description: intl.formatMessage(messages.entireDocumentDescription),
19
48
  type: 'boolean',
20
49
  },
21
50
  },
22
51
  required: [],
23
- };
52
+ });
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { Popup, List } from 'semantic-ui-react';
3
3
  import { useEditorContext } from '@plone/volto-slate/hooks';
4
+
4
5
  import { getAllBlocksAndSlateFields } from '@eeacms/volto-slate-footnote/editor/utils';
5
6
  import {
6
7
  makeFootnoteListOfUniqueItems,
@@ -8,7 +9,9 @@ import {
8
9
  } from './utils';
9
10
  import { isEmpty } from 'lodash';
10
11
  import { useSelector } from 'react-redux';
11
- import { UniversalLink } from '@plone/volto/components';
12
+
13
+ import { renderTextWithLinks } from './utils';
14
+ import { useHistory } from 'react-router-dom';
12
15
 
13
16
  /**
14
17
  * Removes '<?xml version="1.0"?>' from footnote
@@ -16,14 +19,13 @@ import { UniversalLink } from '@plone/volto/components';
16
19
  * @returns {string} formatted footnote
17
20
  */
18
21
 
19
- const urlRegex = /https?:\/\/[^\s]+/g;
20
-
21
22
  export const FootnoteElement = (props) => {
22
23
  const { attributes, children, element, mode, extras } = props;
23
24
  const { data = {} } = element;
24
25
  const { uid, zoteroId } = data;
25
26
  const editor = useEditorContext();
26
27
  const ref = React.useRef();
28
+ const history = useHistory();
27
29
 
28
30
  const initialFormData = useSelector((state) => state?.content?.data || {});
29
31
  const blockProps = editor?.getBlockProps ? editor.getBlockProps() : null;
@@ -51,35 +53,6 @@ export const FootnoteElement = (props) => {
51
53
  : // no extra citations (no multiples)
52
54
  `[${Object.keys(notesObjResult).indexOf(zoteroId) + 1}]`;
53
55
 
54
- const renderTextWithLinks = (text) => {
55
- if (!text) return null;
56
- const parts = text.split(urlRegex);
57
- const links = text.match(urlRegex);
58
- let result = [];
59
-
60
- parts.forEach((part, index) => {
61
- result.push(
62
- <div
63
- dangerouslySetInnerHTML={{
64
- __html: part,
65
- }}
66
- />,
67
- );
68
-
69
- if (links && links[index]) {
70
- result.push(
71
- <UniversalLink
72
- key={`link-${index}`}
73
- href={links[index]}
74
- openLinkInNewTab={false}
75
- >
76
- {links[index]}
77
- </UniversalLink>,
78
- );
79
- }
80
- });
81
- return result;
82
- };
83
56
  const citationIndice = zoteroId // ZOTERO
84
57
  ? indiceIfZoteroId
85
58
  : // FOOTNOTES
@@ -142,16 +115,20 @@ export const FootnoteElement = (props) => {
142
115
  <List divided relaxed selection>
143
116
  <List.Item
144
117
  href={`#footnote-${citationRefId}`}
145
- onClick={() =>
118
+ onClick={(e) => {
146
119
  openAccordionOrTabIfContainsFootnoteReference(
147
120
  `#footnote-${citationRefId}`,
148
- )
149
- }
121
+ );
122
+ if (e.target.tagName !== 'A') {
123
+ e.preventDefault();
124
+ history.push(`#footnote-${citationRefId}`);
125
+ }
126
+ }}
150
127
  key={`#footnote-${citationRefId}`}
151
128
  >
152
129
  <List.Content>
153
130
  <List.Description>
154
- {renderTextWithLinks(footnoteText)}
131
+ {renderTextWithLinks(footnoteText, zoteroId)}
155
132
  </List.Description>
156
133
  </List.Content>
157
134
  </List.Item>
@@ -164,16 +141,22 @@ export const FootnoteElement = (props) => {
164
141
  return (
165
142
  <List.Item
166
143
  href={`#footnote-${item.zoteroId || item.uid}`}
167
- onClick={() =>
144
+ onClick={(e) => {
168
145
  openAccordionOrTabIfContainsFootnoteReference(
169
146
  `#footnote-${item.zoteroId || item.uid}`,
170
- )
171
- }
147
+ );
148
+ if (e.target.tagName !== 'A') {
149
+ e.preventDefault();
150
+ history.push(
151
+ `#footnote-${item.zoteroId || item.uid}`,
152
+ );
153
+ }
154
+ }}
172
155
  key={`#footnote-${item.zoteroId || item.uid}`}
173
156
  >
174
157
  <List.Content>
175
158
  <List.Description>
176
- {renderTextWithLinks(footnoteText)}
159
+ {renderTextWithLinks(footnoteText, item.zoteroId)}
177
160
  </List.Description>
178
161
  </List.Content>
179
162
  </List.Item>
@@ -202,11 +185,15 @@ export const FootnoteElement = (props) => {
202
185
  <List divided relaxed selection>
203
186
  <List.Item
204
187
  href={`#footnote-${citationRefId}`}
205
- onClick={() =>
188
+ onClick={(e) => {
206
189
  openAccordionOrTabIfContainsFootnoteReference(
207
190
  `#footnote-${citationRefId}`,
208
- )
209
- }
191
+ );
192
+ if (e.target.tagName !== 'A') {
193
+ e.preventDefault();
194
+ history.push(`#footnote-${citationRefId}`);
195
+ }
196
+ }}
210
197
  key={`#footnote-${citationRefId}`}
211
198
  >
212
199
  <List.Content>
@@ -219,11 +206,15 @@ export const FootnoteElement = (props) => {
219
206
  data.extra.map((item) => (
220
207
  <List.Item
221
208
  href={`#footnote-${item.zoteroId || item.uid}`}
222
- onClick={() =>
209
+ onClick={(e) => {
223
210
  openAccordionOrTabIfContainsFootnoteReference(
224
211
  `#footnote-${item.zoteroId || item.uid}`,
225
- )
226
- }
212
+ );
213
+ if (e.target.tagName !== 'A') {
214
+ e.preventDefault();
215
+ history.push(`#footnote-${citationRefId}`);
216
+ }
217
+ }}
227
218
  key={`#footnote-${item.zoteroId || item.uid}`}
228
219
  >
229
220
  <List.Content>
@@ -1,6 +1,11 @@
1
1
  import config from '@plone/volto/registry';
2
2
  import { Node } from 'slate';
3
3
  import { getAllBlocks } from '@plone/volto-slate/utils';
4
+ import { escapeRegExp } from 'lodash';
5
+ import { UniversalLink } from '@plone/volto/components';
6
+ const urlRegex =
7
+ /\b((http|https|ftp):\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(\/[^\s<>)]*)?(?=\s|$|<|>|\))/g;
8
+
4
9
  /**
5
10
  * retrive all slate children of nested objects
6
11
  * @param {object} path - the keys that we want to extract the slate children from
@@ -9,7 +14,7 @@ import { getAllBlocks } from '@plone/volto-slate/utils';
9
14
  * path:{items:'value'}
10
15
  * @returns string
11
16
  */
12
- const retriveValuesOfSlateFromNestedPath = (path, value) => {
17
+ export const retriveValuesOfSlateFromNestedPath = (path, value) => {
13
18
  if (Array.isArray(value)) {
14
19
  let allSlateValue = [];
15
20
  value.forEach((element) => {
@@ -276,3 +281,72 @@ const iterateFootnoteObj = (notesObjResultTemp, node, parentUid) => {
276
281
  };
277
282
  }
278
283
  };
284
+
285
+ export function isValidHTML(htmlString) {
286
+ if (
287
+ __CLIENT__ &&
288
+ typeof window !== 'undefined' &&
289
+ typeof DOMParser !== 'undefined'
290
+ ) {
291
+ // The environment is client-side, and DOMParser is available
292
+ const parser = new DOMParser();
293
+ const parsedDocument = parser.parseFromString(htmlString, 'text/html');
294
+ const errors = parsedDocument.querySelectorAll('parsererror');
295
+ return errors.length === 0;
296
+ }
297
+ return false;
298
+ }
299
+
300
+ export const renderTextWithLinks = (text, zoteroId) => {
301
+ if (!text) return null;
302
+
303
+ const links = text.match(urlRegex);
304
+ let isValid = false;
305
+ if (zoteroId && isValidHTML(text)) isValid = true;
306
+
307
+ if (!links) {
308
+ if (isValid)
309
+ return (
310
+ <span
311
+ dangerouslySetInnerHTML={{
312
+ __html: text,
313
+ }}
314
+ />
315
+ );
316
+ else return text;
317
+ }
318
+ let result = [];
319
+ const parts = text.split(
320
+ new RegExp(`(${links.map((link) => escapeRegExp(link)).join('|')})`),
321
+ );
322
+ parts.forEach((part, index) => {
323
+ if (links.includes(part) && zoteroId) {
324
+ result.push(`
325
+ <a key=link-${index} href=${part} rel="noopener">
326
+ ${part}
327
+ </a>`);
328
+ return;
329
+ } else if (links.includes(part)) {
330
+ result.push(
331
+ <UniversalLink
332
+ key={`link-${index}`}
333
+ href={part}
334
+ openLinkInNewTab={false}
335
+ >
336
+ {part}
337
+ </UniversalLink>,
338
+ );
339
+ return;
340
+ } else result.push(part);
341
+ });
342
+
343
+ if (isValid)
344
+ return (
345
+ <span
346
+ dangerouslySetInnerHTML={{
347
+ __html: result.reduce((acc, c) => acc + c, ''),
348
+ }}
349
+ />
350
+ );
351
+ else return <div>{result}</div>;
352
+ };
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  openAccordionOrTabIfContainsFootnoteReference,
3
3
  getAllBlocksAndSlateFields,
4
+ isValidHTML,
5
+ retriveValuesOfSlateFromNestedPath,
4
6
  } from './utils';
5
7
  import { getAllBlocks } from '@plone/volto-slate/utils';
6
8
 
@@ -8,6 +10,47 @@ jest.mock('@plone/volto-slate/utils', () => ({
8
10
  getAllBlocks: jest.fn(),
9
11
  }));
10
12
 
13
+ describe('retriveValuesOfSlateFromNestedPath', () => {
14
+ test('should return values for a given string path in an object', () => {
15
+ const obj = { key: ['value1', 'value2'] };
16
+ expect(retriveValuesOfSlateFromNestedPath('key', obj)).toEqual([
17
+ 'value1',
18
+ 'value2',
19
+ ]);
20
+ });
21
+
22
+ test('should return an empty array when the path is not found', () => {
23
+ const obj = { key: [] };
24
+ expect(retriveValuesOfSlateFromNestedPath('key', obj)).toEqual([]);
25
+ });
26
+
27
+ test('should return values from an array of objects', () => {
28
+ const objArray = [{ key: ['value1'] }, { key: ['value2'] }];
29
+ expect(retriveValuesOfSlateFromNestedPath('key', objArray)).toEqual([
30
+ 'value1',
31
+ 'value2',
32
+ ]);
33
+ });
34
+
35
+ test('should handle nested object paths', () => {
36
+ const obj = { level1: { level2: ['value'] } };
37
+ expect(
38
+ retriveValuesOfSlateFromNestedPath({ level1: 'level2' }, obj),
39
+ ).toEqual(['value']);
40
+ });
41
+
42
+ test('should return an empty array for invalid inputs', () => {
43
+ expect(retriveValuesOfSlateFromNestedPath('key', null)).toEqual([]);
44
+ expect(retriveValuesOfSlateFromNestedPath('key', undefined)).toEqual([]);
45
+ expect(retriveValuesOfSlateFromNestedPath({}, {})).toEqual([]);
46
+ });
47
+
48
+ test('should return empty array if given an empty path object', () => {
49
+ expect(retriveValuesOfSlateFromNestedPath({}, { key: 'value' })).toEqual(
50
+ [],
51
+ );
52
+ });
53
+ });
11
54
  describe('openAccordionOrTabIfContainsFootnoteReference', () => {
12
55
  it('should open accordion if it contains footnote reference', () => {
13
56
  document.body.innerHTML = `
@@ -197,3 +240,29 @@ describe('getAllBlocksAndSlateFields', () => {
197
240
  expect(result).toEqual(expected);
198
241
  });
199
242
  });
243
+
244
+ describe('isValidHTML', () => {
245
+ beforeAll(() => {
246
+ global.DOMParser = class {
247
+ parseFromString(str, type) {
248
+ const doc = {
249
+ querySelectorAll: (selector) => {
250
+ if (selector === 'parsererror' && str.includes('<error>')) {
251
+ return [{}]; // Simulate an error
252
+ }
253
+ return [];
254
+ },
255
+ };
256
+ return doc;
257
+ }
258
+ };
259
+ });
260
+
261
+ test('returns true for valid HTML', () => {
262
+ expect(isValidHTML('<div>Hello</div>')).toBe(true);
263
+ });
264
+
265
+ test('returns false for invalid HTML', () => {
266
+ expect(isValidHTML('<error>Invalid HTML</error>')).toBe(false);
267
+ });
268
+ });
package/src/index.js CHANGED
@@ -6,6 +6,14 @@ import FootnotesBlockSchema from './Blocks/Footnote/FootnotesBlockSchema';
6
6
  import { FOOTNOTE } from './constants';
7
7
  import installFootnoteEditor from './editor';
8
8
  import SearchWidget from '@eeacms/volto-slate-footnote/editor/MultiSelectSearchWidget';
9
+ import { defineMessages } from 'react-intl';
10
+
11
+ defineMessages({
12
+ footnotes: {
13
+ id: 'Footnotes',
14
+ defaultMessage: 'Footnotes',
15
+ },
16
+ });
9
17
 
10
18
  /**
11
19
  * @summary Called from Volto to configure new or existing Volto block types.