@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 +9 -0
- package/locales/de/LC_MESSAGES/volto.po +43 -8
- package/locales/en/LC_MESSAGES/volto.po +39 -4
- package/locales/it/LC_MESSAGES/volto.po +39 -4
- package/locales/ro/LC_MESSAGES/volto.po +39 -4
- package/locales/volto.pot +41 -6
- package/package.json +1 -1
- package/src/Blocks/Footnote/FootnotesBlockEdit.jsx +4 -2
- package/src/Blocks/Footnote/FootnotesBlockView.jsx +3 -45
- package/src/Blocks/Footnote/FootnotesBlockView.test.jsx +84 -0
- package/src/Blocks/Footnote/schema.js +37 -8
- package/src/editor/render.jsx +37 -46
- package/src/editor/utils.js +75 -1
- package/src/editor/utils.test.js +69 -0
- package/src/index.js +8 -0
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:
|
|
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
|
@@ -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={
|
|
33
|
-
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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:
|
|
35
|
+
title: intl.formatMessage(messages.default),
|
|
7
36
|
fields: ['title', 'global'],
|
|
8
37
|
},
|
|
9
38
|
],
|
|
10
39
|
properties: {
|
|
11
40
|
title: {
|
|
12
|
-
title:
|
|
13
|
-
description:
|
|
41
|
+
title: intl.formatMessage(messages.blockTitle),
|
|
42
|
+
description: intl.formatMessage(messages.blockTitleDescription),
|
|
14
43
|
type: 'string',
|
|
15
44
|
},
|
|
16
45
|
global: {
|
|
17
|
-
title:
|
|
18
|
-
description:
|
|
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
|
+
});
|
package/src/editor/render.jsx
CHANGED
|
@@ -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
|
-
|
|
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>
|
package/src/editor/utils.js
CHANGED
|
@@ -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
|
+
};
|
package/src/editor/utils.test.js
CHANGED
|
@@ -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.
|