@eeacms/volto-slate-footnote 3.0.0 → 4.0.0
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 +39 -1
- package/Jenkinsfile +1 -1
- package/cypress/integration/01-slate-footnote-block.js +80 -0
- package/cypress/support/commands.js +171 -1
- package/cypress/support/index.js +79 -5
- package/cypress.json +6 -1
- package/package.json +1 -1
- package/src/Blocks/Footnote/FootnotesBlockView.jsx +34 -10
- package/src/editor/FootnoteEditor.jsx +48 -16
- package/src/editor/MultiSelectSearchWidget.jsx +150 -0
- package/src/editor/extensions.js +1 -2
- package/src/editor/render.jsx +219 -40
- package/src/editor/styles.less +3 -27
- package/src/editor/utils.js +126 -28
- package/src/index.js +1 -1
- package/cypress/integration/01-block-basics.js +0 -36
- package/cypress/integration/02-block-slate.js +0 -63
|
@@ -8,7 +8,7 @@ import briefcaseSVG from '@plone/volto/icons/briefcase.svg';
|
|
|
8
8
|
import checkSVG from '@plone/volto/icons/check.svg';
|
|
9
9
|
import clearSVG from '@plone/volto/icons/clear.svg';
|
|
10
10
|
import { Node } from 'slate';
|
|
11
|
-
import {
|
|
11
|
+
import { getAllBlocksAndSlateFields } from '@eeacms/volto-slate-footnote/editor/utils';
|
|
12
12
|
|
|
13
13
|
const FootnoteEditor = (props) => {
|
|
14
14
|
const {
|
|
@@ -22,10 +22,9 @@ const FootnoteEditor = (props) => {
|
|
|
22
22
|
hasValue,
|
|
23
23
|
onChangeValues,
|
|
24
24
|
} = props;
|
|
25
|
-
|
|
26
25
|
const dispatch = useDispatch();
|
|
26
|
+
const pid = `${editor.uid}-${pluginId}`;
|
|
27
27
|
const [formData, setFormData] = React.useState({});
|
|
28
|
-
|
|
29
28
|
const active = getActiveElement(editor);
|
|
30
29
|
|
|
31
30
|
if (!active) {
|
|
@@ -35,12 +34,15 @@ const FootnoteEditor = (props) => {
|
|
|
35
34
|
const [elementNode] = active;
|
|
36
35
|
const isElement = isActiveElement(editor);
|
|
37
36
|
|
|
38
|
-
const blockProps = editor.getBlockProps();
|
|
39
|
-
const metadata = blockProps.metadata || blockProps.properties;
|
|
40
|
-
const blocks =
|
|
37
|
+
const blockProps = editor?.getBlockProps ? editor.getBlockProps() : {};
|
|
38
|
+
const metadata = blockProps.metadata || blockProps.properties || {};
|
|
39
|
+
const blocks = getAllBlocksAndSlateFields(metadata);
|
|
41
40
|
const filteredBlocks = [];
|
|
42
41
|
|
|
43
42
|
// make a list of filtered footnotes that have unique title
|
|
43
|
+
// to be used as choices for the multi search widget
|
|
44
|
+
// add label and value for the multi search widget
|
|
45
|
+
// flatten blocks to add all extra in the list
|
|
44
46
|
blocks
|
|
45
47
|
.filter((b) => b['@type'] === 'slate')
|
|
46
48
|
.forEach(({ value }) => {
|
|
@@ -48,7 +50,30 @@ const FootnoteEditor = (props) => {
|
|
|
48
50
|
|
|
49
51
|
Array.from(Node.elements(value[0])).forEach(([block]) => {
|
|
50
52
|
block.children.forEach((node) => {
|
|
51
|
-
if (
|
|
53
|
+
if (node.data && node.type === 'footnote' && node.data.extra) {
|
|
54
|
+
if (
|
|
55
|
+
!filteredBlocks.find((item) => item.title === node.data.footnote)
|
|
56
|
+
) {
|
|
57
|
+
filteredBlocks.push({
|
|
58
|
+
...node.data,
|
|
59
|
+
title: node.data.footnote || node.data.value,
|
|
60
|
+
label: node.data.footnote || node.data.value,
|
|
61
|
+
value: node.data.footnote || node.data.value,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
node.data.extra.forEach((ftitem) => {
|
|
65
|
+
if (
|
|
66
|
+
!filteredBlocks.find((item) => item.title === ftitem.footnote)
|
|
67
|
+
) {
|
|
68
|
+
filteredBlocks.push({
|
|
69
|
+
...ftitem,
|
|
70
|
+
title: ftitem.footnote || ftitem.value,
|
|
71
|
+
label: ftitem.footnote || ftitem.value,
|
|
72
|
+
value: ftitem.footnote || ftitem.value,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
} else if (
|
|
52
77
|
node.data &&
|
|
53
78
|
node.type === 'footnote' &&
|
|
54
79
|
!filteredBlocks.find((item) => item.title === node.data.footnote)
|
|
@@ -56,6 +81,8 @@ const FootnoteEditor = (props) => {
|
|
|
56
81
|
filteredBlocks.push({
|
|
57
82
|
...node.data,
|
|
58
83
|
title: node.data.footnote,
|
|
84
|
+
label: node.data.footnote,
|
|
85
|
+
value: node.data.footnote,
|
|
59
86
|
});
|
|
60
87
|
}
|
|
61
88
|
});
|
|
@@ -65,17 +92,24 @@ const FootnoteEditor = (props) => {
|
|
|
65
92
|
// Update the form data based on the current element
|
|
66
93
|
const elRef = React.useRef(null);
|
|
67
94
|
|
|
95
|
+
// add label and value for the multi search widget to be able to show/filter current data
|
|
68
96
|
if (isElement && !isEqual(elementNode, elRef.current)) {
|
|
69
97
|
elRef.current = elementNode;
|
|
70
|
-
setFormData(
|
|
98
|
+
setFormData({
|
|
99
|
+
footnote: {
|
|
100
|
+
...elementNode.data,
|
|
101
|
+
label: elementNode.data.footnote,
|
|
102
|
+
value: elementNode.data.footnote,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
71
105
|
} else if (!isElement) {
|
|
72
106
|
elRef.current = null;
|
|
73
107
|
}
|
|
74
108
|
|
|
75
109
|
const saveDataToEditor = React.useCallback(
|
|
76
110
|
(formData) => {
|
|
77
|
-
if (hasValue(formData)) {
|
|
78
|
-
insertElement(editor, formData);
|
|
111
|
+
if (hasValue(formData.footnote)) {
|
|
112
|
+
insertElement(editor, formData.footnote);
|
|
79
113
|
} else {
|
|
80
114
|
unwrapElement(editor);
|
|
81
115
|
}
|
|
@@ -111,14 +145,12 @@ const FootnoteEditor = (props) => {
|
|
|
111
145
|
icon={<VoltoIcon size="24px" name={briefcaseSVG} />}
|
|
112
146
|
onChangeField={(value) => {
|
|
113
147
|
if (!onChangeValues) {
|
|
114
|
-
return setFormData(
|
|
115
|
-
...formData,
|
|
116
|
-
...value,
|
|
117
|
-
});
|
|
148
|
+
return setFormData(value);
|
|
118
149
|
}
|
|
119
150
|
return onChangeValues('footnote', value, formData, setFormData);
|
|
120
151
|
}}
|
|
121
152
|
formData={formData}
|
|
153
|
+
dataBoss={formData}
|
|
122
154
|
source={filteredBlocks}
|
|
123
155
|
headerActions={
|
|
124
156
|
<>
|
|
@@ -126,7 +158,7 @@ const FootnoteEditor = (props) => {
|
|
|
126
158
|
onClick={() => {
|
|
127
159
|
saveDataToEditor(formData);
|
|
128
160
|
dispatch(
|
|
129
|
-
setPluginOptions(
|
|
161
|
+
setPluginOptions(pid, {
|
|
130
162
|
show_sidebar_editor: false,
|
|
131
163
|
}),
|
|
132
164
|
);
|
|
@@ -139,7 +171,7 @@ const FootnoteEditor = (props) => {
|
|
|
139
171
|
onClick={() => {
|
|
140
172
|
checkForCancel();
|
|
141
173
|
dispatch(
|
|
142
|
-
setPluginOptions(
|
|
174
|
+
setPluginOptions(pid, {
|
|
143
175
|
show_sidebar_editor: false,
|
|
144
176
|
}),
|
|
145
177
|
);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArrayWidget component.
|
|
3
|
+
* @module components/manage/Widgets/ArrayWidget
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useState } from 'react';
|
|
7
|
+
import { defineMessages } from 'react-intl';
|
|
8
|
+
import {
|
|
9
|
+
Option,
|
|
10
|
+
DropdownIndicator,
|
|
11
|
+
selectTheme,
|
|
12
|
+
customSelectStyles,
|
|
13
|
+
} from '@plone/volto/components/manage/Widgets/SelectStyling';
|
|
14
|
+
import { escapeRegExp, filter } from 'lodash';
|
|
15
|
+
import { nanoid } from 'volto-slate/utils';
|
|
16
|
+
|
|
17
|
+
import { FormFieldWrapper } from '@plone/volto/components';
|
|
18
|
+
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
|
|
19
|
+
|
|
20
|
+
const messages = defineMessages({
|
|
21
|
+
select: {
|
|
22
|
+
id: 'Select…',
|
|
23
|
+
defaultMessage: 'Select…',
|
|
24
|
+
},
|
|
25
|
+
no_options: {
|
|
26
|
+
id: 'No options',
|
|
27
|
+
defaultMessage: 'No options',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const MultiSelectSearchWidget = injectLazyLibs('reactSelectAsyncCreateable')(
|
|
32
|
+
(props) => {
|
|
33
|
+
const parentFootnote = props.value;
|
|
34
|
+
const extraValues = props.value.extra ? props.value.extra : [];
|
|
35
|
+
const [selectedOption, setSelectedOption] = useState(
|
|
36
|
+
parentFootnote.value ? [...[parentFootnote], ...extraValues] : [],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* evaluate on Regex to filter results
|
|
41
|
+
* @param {Object} e - event
|
|
42
|
+
* @param {Object} data
|
|
43
|
+
*/
|
|
44
|
+
const loadOptions = (search) => {
|
|
45
|
+
const re = new RegExp(escapeRegExp(search), 'i');
|
|
46
|
+
const isMatch = (result) => re.test(result.value);
|
|
47
|
+
const resultsFiltered = filter(props.choices, isMatch);
|
|
48
|
+
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
resolve(resultsFiltered);
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* If the list is empty or the first is not parent, return true
|
|
56
|
+
* @param {Object[]} selectedOption list of objects - footnotes
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
const isParetFootnoteRemoved = (selectedOption) =>
|
|
60
|
+
!selectedOption[0] || selectedOption[0].value !== parentFootnote.value;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* replace all parentFootnote data except uid, with the first from the list
|
|
64
|
+
* @param {Object[]} selectedOption list of objects - footnotes
|
|
65
|
+
* @returns {Object}
|
|
66
|
+
*/
|
|
67
|
+
const setParentFootnoteFromExtra = (selectedOption) => {
|
|
68
|
+
const { footnote, label, value } = selectedOption[0] || [];
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
...parentFootnote,
|
|
72
|
+
footnote: footnote || selectedOption[0]?.value,
|
|
73
|
+
label,
|
|
74
|
+
value,
|
|
75
|
+
extra: selectedOption.slice(1),
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Will make the footnotes object, that will be saved as first from selectedOption
|
|
81
|
+
* the rest will be added to extra
|
|
82
|
+
* @param {*} selectedOption
|
|
83
|
+
* @returns
|
|
84
|
+
*/
|
|
85
|
+
const setFootnoteFromSelection = (selectedOption) => {
|
|
86
|
+
const extra = selectedOption.slice(1).map((item) => {
|
|
87
|
+
const obj = {
|
|
88
|
+
uid: nanoid(5),
|
|
89
|
+
...item,
|
|
90
|
+
footnote: item.value,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const { __isNew__: remove, ...rest } = obj;
|
|
94
|
+
return rest;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return { ...parentFootnote, extra };
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Handle the field change, will remake the result based on the new selected list
|
|
102
|
+
* @method handleChange
|
|
103
|
+
* @param {array} selectedOption The selected options (already aggregated).
|
|
104
|
+
* @returns {undefined}
|
|
105
|
+
*/
|
|
106
|
+
const handleChange = (selectedOption) => {
|
|
107
|
+
setSelectedOption(selectedOption);
|
|
108
|
+
|
|
109
|
+
// manage case if parent footnotes (first from the options) was removed
|
|
110
|
+
const resultSelected = isParetFootnoteRemoved(selectedOption)
|
|
111
|
+
? setParentFootnoteFromExtra(selectedOption)
|
|
112
|
+
: setFootnoteFromSelection(selectedOption);
|
|
113
|
+
|
|
114
|
+
props.onChange({
|
|
115
|
+
footnote: resultSelected,
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// from choices (list of all footnotes available including current in value) get all not found in current in value
|
|
120
|
+
// consider that new footnotes have value and footnote undefined
|
|
121
|
+
const defaultOptions = (props.choices || []).filter(
|
|
122
|
+
(item) =>
|
|
123
|
+
!selectedOption.find(({ label }) => label === item.label) && item.value,
|
|
124
|
+
);
|
|
125
|
+
const AsyncCreatableSelect = props.reactSelectAsyncCreateable.default;
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<FormFieldWrapper {...props}>
|
|
129
|
+
<AsyncCreatableSelect
|
|
130
|
+
isDisabled={props.isDisabled}
|
|
131
|
+
className="react-select-container"
|
|
132
|
+
classNamePrefix="react-select"
|
|
133
|
+
defaultOptions={defaultOptions}
|
|
134
|
+
styles={customSelectStyles}
|
|
135
|
+
theme={selectTheme}
|
|
136
|
+
components={{ DropdownIndicator, Option }}
|
|
137
|
+
isMulti
|
|
138
|
+
options={defaultOptions}
|
|
139
|
+
value={selectedOption || []}
|
|
140
|
+
loadOptions={loadOptions}
|
|
141
|
+
onChange={handleChange}
|
|
142
|
+
placeholder={props.intl.formatMessage(messages.select)}
|
|
143
|
+
noOptionsMessage={() => props.intl.formatMessage(messages.no_options)}
|
|
144
|
+
/>
|
|
145
|
+
</FormFieldWrapper>
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
export default MultiSelectSearchWidget;
|
package/src/editor/extensions.js
CHANGED
|
@@ -11,6 +11,7 @@ export const withFootnote = (editor) => {
|
|
|
11
11
|
|
|
12
12
|
editor.normalizeNode = (entry) => {
|
|
13
13
|
const [node, path] = entry;
|
|
14
|
+
|
|
14
15
|
if (node.type === FOOTNOTE && !node.data?.uid) {
|
|
15
16
|
Transforms.setNodes(
|
|
16
17
|
editor,
|
|
@@ -34,12 +35,10 @@ export const withFootnote = (editor) => {
|
|
|
34
35
|
// this will be usefull when copy/pase items have the same uid
|
|
35
36
|
export const withBeforeInsertFragment = (editor) => {
|
|
36
37
|
const { beforeInsertFragment } = editor;
|
|
37
|
-
|
|
38
38
|
editor.beforeInsertFragment = (parsed) => {
|
|
39
39
|
if (parsed?.[0]?.children?.[0]?.data?.uid) {
|
|
40
40
|
parsed[0].children[0].data.uid = nanoid(5);
|
|
41
41
|
}
|
|
42
|
-
|
|
43
42
|
return beforeInsertFragment ? beforeInsertFragment(parsed) : parsed;
|
|
44
43
|
};
|
|
45
44
|
|
package/src/editor/render.jsx
CHANGED
|
@@ -1,48 +1,149 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
|
-
import { Popup } from 'semantic-ui-react';
|
|
2
|
+
import { Popup, List } from 'semantic-ui-react';
|
|
3
3
|
import { useEditorContext } from 'volto-slate/hooks';
|
|
4
|
-
import {
|
|
4
|
+
import { getAllBlocksAndSlateFields } from '@eeacms/volto-slate-footnote/editor/utils';
|
|
5
5
|
import { makeFootnoteListOfUniqueItems } from './utils';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Removes '<?xml version="1.0"?>' from footnote
|
|
9
|
+
* @param {string} footnote
|
|
10
|
+
* @returns {string} formatted footnote
|
|
11
|
+
*/
|
|
7
12
|
const makeFootnote = (footnote) => {
|
|
8
13
|
const free = footnote ? footnote.replace('<?xml version="1.0"?>', '') : '';
|
|
9
14
|
|
|
10
15
|
return free;
|
|
11
16
|
};
|
|
12
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Will open accordion if contains footnote reference
|
|
20
|
+
* @param {string} footnoteId
|
|
21
|
+
*/
|
|
22
|
+
const openAccordionIfContainsFootnoteReference = (footnoteId) => {
|
|
23
|
+
if (typeof window !== 'undefined') {
|
|
24
|
+
const footnote = document.querySelector(footnoteId);
|
|
25
|
+
if (footnote !== null && footnote.closest('.accordion') !== null) {
|
|
26
|
+
const comp = footnote.closest('.accordion').querySelector('.title');
|
|
27
|
+
if (!comp.className.includes('active')) {
|
|
28
|
+
comp.click();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
|
|
13
36
|
export const FootnoteElement = (props) => {
|
|
14
37
|
const { attributes, children, element, mode, extras } = props;
|
|
15
38
|
const { data = {} } = element;
|
|
16
39
|
const { uid, zoteroId } = data;
|
|
17
40
|
const editor = useEditorContext();
|
|
18
|
-
const [citationIndice, setCitationIndice] = useState(null);
|
|
19
|
-
const [citationRefId, setCitationRefId] = useState(null);
|
|
41
|
+
const [citationIndice, setCitationIndice] = useState(null); // list of indices to reference
|
|
42
|
+
const [citationRefId, setCitationRefId] = useState(null); // indice of element to be referenced
|
|
20
43
|
|
|
21
44
|
useEffect(() => {
|
|
22
|
-
const blockProps = editor ? editor.getBlockProps() : null;
|
|
45
|
+
const blockProps = editor?.getBlockProps ? editor.getBlockProps() : null;
|
|
23
46
|
const metadata = blockProps
|
|
24
47
|
? blockProps.metadata || blockProps.properties
|
|
25
|
-
: extras
|
|
26
|
-
const blocks =
|
|
48
|
+
: extras?.metadata || {};
|
|
49
|
+
const blocks = getAllBlocksAndSlateFields(metadata);
|
|
27
50
|
const notesObjResult = makeFootnoteListOfUniqueItems(blocks);
|
|
28
51
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
?
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
(
|
|
45
|
-
|
|
52
|
+
// will cosider zotero citations and footnote
|
|
53
|
+
// notesObjResult contains all zotero/footnote as unique, and contain refs for other zotero/footnote
|
|
54
|
+
const indice = zoteroId // ZOTERO
|
|
55
|
+
? data.extra
|
|
56
|
+
? [
|
|
57
|
+
`[${Object.keys(notesObjResult).indexOf(zoteroId) + 1}]`, // parent footnote
|
|
58
|
+
...data.extra.map(
|
|
59
|
+
// citations from extra
|
|
60
|
+
(zoteroObj, index) =>
|
|
61
|
+
// all zotero citation are indexed by zoteroId in notesObjResult
|
|
62
|
+
`[${
|
|
63
|
+
Object.keys(notesObjResult).indexOf(zoteroObj.zoteroId) + 1
|
|
64
|
+
}]`,
|
|
65
|
+
),
|
|
66
|
+
].join('')
|
|
67
|
+
: // no extra citations (no multiples)
|
|
68
|
+
`[${Object.keys(notesObjResult).indexOf(zoteroId) + 1}]`
|
|
69
|
+
: // FOOTNOTES
|
|
70
|
+
// not all footnotes will be found in notesObjResult because they might have different uid
|
|
71
|
+
notesObjResult[data.uid]
|
|
72
|
+
? // footnotes from extra
|
|
73
|
+
data.extra
|
|
74
|
+
? [
|
|
75
|
+
// parent footnote
|
|
76
|
+
`[${Object.keys(notesObjResult).indexOf(data.uid) + 1}]`,
|
|
77
|
+
...data.extra.map((footnoteObj, index) => {
|
|
78
|
+
return notesObjResult[footnoteObj.uid]
|
|
79
|
+
? // take footnote if uid is found
|
|
80
|
+
`[${
|
|
81
|
+
Object.keys(notesObjResult).indexOf(footnoteObj.uid) + 1
|
|
82
|
+
}]`
|
|
83
|
+
: // if uid is not found look for it in other footnotes refs
|
|
84
|
+
`[${
|
|
85
|
+
Object.keys(notesObjResult).indexOf(
|
|
86
|
+
Object.keys(notesObjResult).find(
|
|
87
|
+
(noteKey) =>
|
|
88
|
+
notesObjResult[noteKey].refs &&
|
|
89
|
+
notesObjResult[noteKey].refs[data.uid],
|
|
90
|
+
),
|
|
91
|
+
) + 1
|
|
92
|
+
}]`;
|
|
93
|
+
}),
|
|
94
|
+
].join('')
|
|
95
|
+
: // no extra footnotes (no multiples)
|
|
96
|
+
`[${Object.keys(notesObjResult).indexOf(data.uid) + 1}]`
|
|
97
|
+
: // footnotes not found in notesObjResult
|
|
98
|
+
data.extra
|
|
99
|
+
? [
|
|
100
|
+
// look for it in other footnotes refs - parent
|
|
101
|
+
`[${
|
|
102
|
+
Object.keys(notesObjResult).indexOf(
|
|
103
|
+
Object.keys(notesObjResult).find(
|
|
104
|
+
(noteKey) =>
|
|
105
|
+
notesObjResult[noteKey].refs &&
|
|
106
|
+
notesObjResult[noteKey].refs[data.uid],
|
|
107
|
+
),
|
|
108
|
+
) + 1
|
|
109
|
+
}]`,
|
|
110
|
+
...data.extra.map((footnoteObj, index) => {
|
|
111
|
+
return notesObjResult[footnoteObj.uid]
|
|
112
|
+
? // footnotes from extra might be found in notesObjResult
|
|
113
|
+
`[${Object.keys(notesObjResult).indexOf(footnoteObj.uid) + 1}]`
|
|
114
|
+
: // if uid is not found look for it in other footnotes refs
|
|
115
|
+
`[${
|
|
116
|
+
Object.keys(notesObjResult).indexOf(
|
|
117
|
+
Object.keys(notesObjResult).find(
|
|
118
|
+
(noteKey) =>
|
|
119
|
+
notesObjResult[noteKey].refs &&
|
|
120
|
+
notesObjResult[noteKey].refs[data.uid],
|
|
121
|
+
),
|
|
122
|
+
) + 1
|
|
123
|
+
}]`;
|
|
124
|
+
}),
|
|
125
|
+
].join('')
|
|
126
|
+
: // no extra footnotes
|
|
127
|
+
`[${
|
|
128
|
+
Object.keys(notesObjResult).indexOf(
|
|
129
|
+
Object.keys(notesObjResult).find(
|
|
130
|
+
(noteKey) =>
|
|
131
|
+
notesObjResult[noteKey].refs &&
|
|
132
|
+
notesObjResult[noteKey].refs[data.uid],
|
|
133
|
+
),
|
|
134
|
+
) + 1
|
|
135
|
+
}]`;
|
|
136
|
+
const findReferenceId =
|
|
137
|
+
// search within parent citations first, otherwise the uid might be inside a refs obj that comes before
|
|
138
|
+
Object.keys(notesObjResult).find(
|
|
139
|
+
(noteKey) => notesObjResult[noteKey].uid === uid,
|
|
140
|
+
) ||
|
|
141
|
+
// if not found in parent, search in refs, it might be a footnote references multiple times
|
|
142
|
+
Object.keys(notesObjResult).find(
|
|
143
|
+
(noteKey) =>
|
|
144
|
+
notesObjResult[noteKey].uid === uid ||
|
|
145
|
+
(notesObjResult[noteKey].refs && notesObjResult[noteKey].refs[uid]),
|
|
146
|
+
);
|
|
46
147
|
|
|
47
148
|
setCitationIndice(indice);
|
|
48
149
|
setCitationRefId(findReferenceId);
|
|
@@ -51,11 +152,7 @@ export const FootnoteElement = (props) => {
|
|
|
51
152
|
return (
|
|
52
153
|
<>
|
|
53
154
|
{mode === 'view' ? (
|
|
54
|
-
<
|
|
55
|
-
href={`#footnote-${citationRefId}`}
|
|
56
|
-
id={`ref-${uid}`}
|
|
57
|
-
aria-describedby="footnote-label"
|
|
58
|
-
>
|
|
155
|
+
<span id={`ref-${uid}`} aria-describedby="footnote-label">
|
|
59
156
|
<Popup
|
|
60
157
|
position="bottom left"
|
|
61
158
|
trigger={
|
|
@@ -68,16 +165,57 @@ export const FootnoteElement = (props) => {
|
|
|
68
165
|
{children}
|
|
69
166
|
</span>
|
|
70
167
|
}
|
|
168
|
+
hoverable
|
|
71
169
|
>
|
|
72
170
|
<Popup.Content>
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
171
|
+
<List divided relaxed selection>
|
|
172
|
+
<List.Item
|
|
173
|
+
as="a"
|
|
174
|
+
href={`#footnote-${citationRefId}`}
|
|
175
|
+
onClick={() =>
|
|
176
|
+
openAccordionIfContainsFootnoteReference(
|
|
177
|
+
`#footnote-${citationRefId}`,
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
key={`#footnote-${citationRefId}`}
|
|
181
|
+
>
|
|
182
|
+
<List.Content>
|
|
183
|
+
<List.Description>
|
|
184
|
+
<div
|
|
185
|
+
dangerouslySetInnerHTML={{
|
|
186
|
+
__html: makeFootnote(data.footnote),
|
|
187
|
+
}}
|
|
188
|
+
/>{' '}
|
|
189
|
+
</List.Description>
|
|
190
|
+
</List.Content>
|
|
191
|
+
</List.Item>
|
|
192
|
+
{data.extra &&
|
|
193
|
+
data.extra.map((item) => (
|
|
194
|
+
<List.Item
|
|
195
|
+
as="a"
|
|
196
|
+
href={`#footnote-${item.zoteroId || item.uid}`}
|
|
197
|
+
onClick={() =>
|
|
198
|
+
openAccordionIfContainsFootnoteReference(
|
|
199
|
+
`#footnote-${item.zoteroId || item.uid}`,
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
key={`#footnote-${item.zoteroId || item.uid}`}
|
|
203
|
+
>
|
|
204
|
+
<List.Content>
|
|
205
|
+
<List.Description>
|
|
206
|
+
<div
|
|
207
|
+
dangerouslySetInnerHTML={{
|
|
208
|
+
__html: makeFootnote(item.footnote),
|
|
209
|
+
}}
|
|
210
|
+
/>{' '}
|
|
211
|
+
</List.Description>
|
|
212
|
+
</List.Content>
|
|
213
|
+
</List.Item>
|
|
214
|
+
))}
|
|
215
|
+
</List>
|
|
78
216
|
</Popup.Content>
|
|
79
217
|
</Popup>
|
|
80
|
-
</
|
|
218
|
+
</span>
|
|
81
219
|
) : (
|
|
82
220
|
<Popup
|
|
83
221
|
position="bottom left"
|
|
@@ -91,13 +229,54 @@ export const FootnoteElement = (props) => {
|
|
|
91
229
|
{children}
|
|
92
230
|
</span>
|
|
93
231
|
}
|
|
232
|
+
hoverable
|
|
94
233
|
>
|
|
95
234
|
<Popup.Content>
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
235
|
+
<List divided relaxed selection>
|
|
236
|
+
<List.Item
|
|
237
|
+
as="a"
|
|
238
|
+
href={`#footnote-${citationRefId}`}
|
|
239
|
+
onClick={() =>
|
|
240
|
+
openAccordionIfContainsFootnoteReference(
|
|
241
|
+
`#footnote-${citationRefId}`,
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
key={`#footnote-${citationRefId}`}
|
|
245
|
+
>
|
|
246
|
+
<List.Content>
|
|
247
|
+
<List.Description>
|
|
248
|
+
<div
|
|
249
|
+
dangerouslySetInnerHTML={{
|
|
250
|
+
__html: makeFootnote(data.footnote),
|
|
251
|
+
}}
|
|
252
|
+
/>{' '}
|
|
253
|
+
</List.Description>
|
|
254
|
+
</List.Content>
|
|
255
|
+
</List.Item>
|
|
256
|
+
{data.extra &&
|
|
257
|
+
data.extra.map((item) => (
|
|
258
|
+
<List.Item
|
|
259
|
+
as="a"
|
|
260
|
+
href={`#footnote-${item.zoteroId || item.uid}`}
|
|
261
|
+
onClick={() =>
|
|
262
|
+
openAccordionIfContainsFootnoteReference(
|
|
263
|
+
`#footnote-${item.zoteroId || item.uid}`,
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
key={`#footnote-${item.zoteroId || item.uid}`}
|
|
267
|
+
>
|
|
268
|
+
<List.Content>
|
|
269
|
+
<List.Description>
|
|
270
|
+
<div
|
|
271
|
+
dangerouslySetInnerHTML={{
|
|
272
|
+
__html: makeFootnote(item.footnote),
|
|
273
|
+
}}
|
|
274
|
+
/>{' '}
|
|
275
|
+
</List.Description>
|
|
276
|
+
</List.Content>
|
|
277
|
+
</List.Item>
|
|
278
|
+
))}
|
|
279
|
+
</List>
|
|
101
280
|
</Popup.Content>
|
|
102
281
|
</Popup>
|
|
103
282
|
)}
|
package/src/editor/styles.less
CHANGED
|
@@ -4,8 +4,8 @@ body {
|
|
|
4
4
|
counter-reset: footnotes;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
color:
|
|
7
|
+
span[aria-describedby='footnote-label'] {
|
|
8
|
+
color: #004b87;
|
|
9
9
|
cursor: default;
|
|
10
10
|
outline: none;
|
|
11
11
|
text-decoration: none;
|
|
@@ -18,30 +18,6 @@ a[aria-describedby='footnote-label'] {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
.citation-indice {
|
|
22
|
-
color: inherit;
|
|
23
|
-
counter-increment: footnotes;
|
|
24
|
-
cursor: default;
|
|
25
|
-
outline: none;
|
|
26
|
-
text-decoration: none;
|
|
27
|
-
|
|
28
|
-
&::after {
|
|
29
|
-
margin-left: 2px;
|
|
30
|
-
color: blue;
|
|
31
|
-
content: '[' counter(footnotes) ']';
|
|
32
|
-
cursor: pointer;
|
|
33
|
-
font-size: 0.8em;
|
|
34
|
-
text-decoration: underline;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
&:focus {
|
|
38
|
-
&::after {
|
|
39
|
-
outline: thin dotted;
|
|
40
|
-
outline-offset: 2px;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
21
|
.footnote-edit-node {
|
|
46
22
|
padding: 0px 4px;
|
|
47
23
|
background-color: #e6f3ff;
|
|
@@ -51,7 +27,7 @@ a[aria-describedby='footnote-label'] {
|
|
|
51
27
|
.footnote-edit-node::after,
|
|
52
28
|
.citation-item::after {
|
|
53
29
|
color: #0645ad;
|
|
54
|
-
content:
|
|
30
|
+
content: attr(data-footnote-indice);
|
|
55
31
|
font-size: 75%;
|
|
56
32
|
vertical-align: super;
|
|
57
33
|
}
|