@manuscripts/body-editor 3.12.67 → 3.13.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.
Files changed (289) hide show
  1. package/dist/cjs/components/toolbar/type-selector/styles.js +2 -1
  2. package/dist/cjs/versions.js +1 -1
  3. package/dist/es/components/toolbar/type-selector/styles.js +2 -1
  4. package/dist/es/versions.js +1 -1
  5. package/dist/types/components/toolbar/type-selector/styles.d.ts +2 -2
  6. package/dist/types/lib/footnotes.d.ts +4 -2
  7. package/dist/types/lib/utils.d.ts +5 -4
  8. package/dist/types/versions.d.ts +1 -1
  9. package/package.json +37 -36
  10. package/{dist/es/lib/statistics.js → src/__mocks__/styleMock.ts} +2 -14
  11. package/src/assets.ts +20 -0
  12. package/src/classes/collabProvider.ts +38 -0
  13. package/src/clipboard.ts +54 -0
  14. package/src/commands.ts +2109 -0
  15. package/src/components/ChangeHandlingForm.tsx +54 -0
  16. package/src/components/LanguageDropdown/index.tsx +343 -0
  17. package/src/components/affiliations/AffiliationForm.tsx +238 -0
  18. package/src/components/affiliations/AffiliationItem.tsx +151 -0
  19. package/src/components/affiliations/AffiliationList.tsx +71 -0
  20. package/src/components/affiliations/AffiliationsModal.tsx +676 -0
  21. package/src/components/affiliations/AffiliationsPanel.tsx +76 -0
  22. package/src/components/affiliations/CreateAffiliationModal.tsx +135 -0
  23. package/src/components/authors/AuthorDetailsForm.tsx +438 -0
  24. package/src/components/authors/AuthorList.tsx +81 -0
  25. package/src/components/authors/AuthorsModal.tsx +642 -0
  26. package/src/components/authors/AuthorsPanel.tsx +69 -0
  27. package/src/components/authors/CreateAuthorModal.tsx +172 -0
  28. package/src/components/authors/CreditDrawer.tsx +71 -0
  29. package/src/components/authors/DraggableAuthor.tsx +228 -0
  30. package/src/components/authors/useManageAffiliations.tsx +77 -0
  31. package/src/components/authors/useManageCredit.tsx +63 -0
  32. package/src/components/authors-affiliations/AuthorsAndAffiliationsModals.tsx +170 -0
  33. package/src/components/authors-affiliations/GenericPanel.tsx +164 -0
  34. package/src/components/authors-affiliations/ModalTabs.tsx +100 -0
  35. package/src/components/awards/AwardForm.tsx +338 -0
  36. package/src/components/awards/AwardModal.tsx +120 -0
  37. package/src/components/awards/DeleteAwardDiaolog.tsx +52 -0
  38. package/src/components/dialog/ConfirmationDialog.tsx +131 -0
  39. package/src/components/form/CreateModalStyles.tsx +37 -0
  40. package/src/components/form/FormFooter.tsx +54 -0
  41. package/src/components/form/FormPlaceholder.tsx +71 -0
  42. package/src/components/form/ModalFormActions.tsx +88 -0
  43. package/src/components/form/UnsavedLabel.tsx +49 -0
  44. package/src/components/hooks/use-debounce.ts +56 -0
  45. package/src/components/hooks/useAffiliationShowsErrorIndicator.ts +51 -0
  46. package/src/components/hooks/useAuthorShowsErrorIndicator.ts +73 -0
  47. package/src/components/keyboard-shortcuts-modal/FormattedShortcut.tsx +56 -0
  48. package/src/components/keyboard-shortcuts-modal/KeyboardShortcutsModal.tsx +283 -0
  49. package/src/components/keyboard-shortcuts-modal/keyboard-shortcuts.ts +304 -0
  50. package/src/components/keywords/AddKeywordInline.tsx +284 -0
  51. package/src/components/keywords/DeleteKeywordDialog.tsx +52 -0
  52. package/src/components/modal-drawer/GenericDrawer.tsx +65 -0
  53. package/src/components/modal-drawer/GenericDrawerGroup.tsx +125 -0
  54. package/src/components/outline/DraggableTree.tsx +438 -0
  55. package/src/components/outline/ManuscriptOutline.tsx +127 -0
  56. package/src/components/outline/Outline.tsx +141 -0
  57. package/src/components/references/BibliographyItemSource.ts +80 -0
  58. package/src/components/references/CitationEditor.tsx +301 -0
  59. package/src/components/references/CitationViewer.tsx +62 -0
  60. package/src/components/references/ImportBibliographyForm.tsx +232 -0
  61. package/src/components/references/ImportBibliographyModal.tsx +159 -0
  62. package/src/components/references/ReferenceForm/PersonDropDown.tsx +93 -0
  63. package/src/components/references/ReferenceForm/ReferenceForm.tsx +701 -0
  64. package/src/components/references/ReferenceForm/config.ts +133 -0
  65. package/src/components/references/ReferenceForm/styled-components.ts +156 -0
  66. package/src/components/references/ReferenceLine.tsx +43 -0
  67. package/src/components/references/ReferenceSearch.tsx +163 -0
  68. package/src/components/references/ReferenceSearchResults.tsx +113 -0
  69. package/src/components/references/ReferenceSearchResultsPlaceholder.tsx +71 -0
  70. package/src/components/references/ReferenceSearchSection.tsx +124 -0
  71. package/src/components/references/ReferencesEditor.tsx +61 -0
  72. package/src/components/references/ReferencesModal.tsx +352 -0
  73. package/src/components/references/SearchInput.tsx +98 -0
  74. package/src/components/toolbar/InsertEmbedDialog.tsx +222 -0
  75. package/src/components/toolbar/InsertTableDialog.tsx +208 -0
  76. package/src/components/toolbar/ListMenuItem.tsx +203 -0
  77. package/src/components/toolbar/helpers.ts +547 -0
  78. package/src/components/toolbar/type-selector/OptionComponent.tsx +89 -0
  79. package/src/components/toolbar/type-selector/TypeSelector.tsx +157 -0
  80. package/src/components/toolbar/type-selector/styles.ts +79 -0
  81. package/src/components/views/CrossReferenceItems.tsx +199 -0
  82. package/src/components/views/DeleteEmbedDialog.tsx +83 -0
  83. package/src/components/views/DeleteFootnoteDialog.tsx +54 -0
  84. package/src/components/views/FigureDropdown.tsx +423 -0
  85. package/src/components/views/FootnotesSelector.tsx +255 -0
  86. package/src/components/views/InsertSpecialCharacter.tsx +245 -0
  87. package/src/components/views/LinkForm.tsx +220 -0
  88. package/src/components/views/TableCellContextMenu.tsx +241 -0
  89. package/src/configs/ManuscriptsEditor.ts +140 -0
  90. package/src/configs/editor-plugins.ts +119 -0
  91. package/src/configs/editor-views.ts +139 -0
  92. package/src/icons.ts +62 -0
  93. package/src/index.ts +65 -0
  94. package/src/keys/highlight.ts +65 -0
  95. package/src/keys/index.ts +33 -0
  96. package/src/keys/keyword.ts +69 -0
  97. package/src/keys/list.ts +156 -0
  98. package/src/keys/misc.ts +112 -0
  99. package/src/keys/title.ts +221 -0
  100. package/src/lib/__tests__/__fixtures__/doc.json +388 -0
  101. package/src/lib/__tests__/__snapshots__/paste.test.ts.snap +24 -0
  102. package/src/lib/__tests__/authors.test.ts +202 -0
  103. package/src/lib/__tests__/helpers.test.ts +46 -0
  104. package/src/lib/__tests__/paste.test.ts +57 -0
  105. package/src/lib/__tests__/plugins.test.ts +176 -0
  106. package/src/lib/__tests__/url.test.ts +31 -0
  107. package/src/lib/array-reducer.ts +68 -0
  108. package/src/lib/authors-and-affiliations.ts +81 -0
  109. package/src/lib/authors.ts +67 -0
  110. package/src/lib/capabilities.ts +55 -0
  111. package/src/lib/codemirror.ts +37 -0
  112. package/src/lib/comments.ts +123 -0
  113. package/src/lib/context-menu.ts +745 -0
  114. package/src/lib/copy.ts +30 -0
  115. package/src/lib/crossref.ts +90 -0
  116. package/src/lib/dnd.ts +29 -0
  117. package/src/lib/doc.ts +167 -0
  118. package/src/lib/dompurify.ts +28 -0
  119. package/src/lib/files.ts +180 -0
  120. package/src/lib/footnotes.ts +83 -0
  121. package/src/lib/get-media-type.ts +105 -0
  122. package/src/lib/helpers.ts +199 -0
  123. package/src/lib/hierarchy.ts +117 -0
  124. package/src/lib/languages.ts +37 -0
  125. package/src/lib/lists.ts +56 -0
  126. package/src/lib/math.ts +111 -0
  127. package/src/lib/media.ts +334 -0
  128. package/src/lib/navigation-utils.ts +235 -0
  129. package/src/lib/normalize.ts +70 -0
  130. package/src/lib/paste.ts +304 -0
  131. package/src/lib/placeholder.ts +32 -0
  132. package/src/lib/platform.ts +18 -0
  133. package/src/lib/plugins.ts +22 -0
  134. package/src/lib/popper.ts +180 -0
  135. package/src/lib/position-menu.ts +184 -0
  136. package/src/lib/references.ts +94 -0
  137. package/src/lib/template.ts +29 -0
  138. package/src/lib/track-changes-utils.ts +34 -0
  139. package/{dist/types/lib/statistics.d.ts → src/lib/url.ts} +11 -8
  140. package/src/lib/use-do-with-debounce.ts +39 -0
  141. package/src/lib/utils.ts +323 -0
  142. package/src/lib/view.ts +100 -0
  143. package/src/menus.tsx +684 -0
  144. package/src/node-type-icons.tsx +83 -0
  145. package/src/plugins/__tests__/__fixtures__/deleted-footnote.json +140 -0
  146. package/src/plugins/__tests__/__fixtures__/inserted-footnote.json +153 -0
  147. package/src/plugins/__tests__/footnotes.test.ts +67 -0
  148. package/src/plugins/accessibility_element.ts +219 -0
  149. package/src/plugins/add-subtitle.ts +102 -0
  150. package/src/plugins/affiliations.ts +136 -0
  151. package/src/plugins/alt-titles.ts +219 -0
  152. package/src/plugins/bibliography.ts +235 -0
  153. package/src/plugins/comments.ts +328 -0
  154. package/src/plugins/cross-references.ts +70 -0
  155. package/src/plugins/detect-inconsistency/detect-inconsistency-utils.ts +102 -0
  156. package/src/plugins/detect-inconsistency/index.ts +55 -0
  157. package/src/plugins/detect-inconsistency/validators.ts +283 -0
  158. package/src/plugins/doi.ts +66 -0
  159. package/src/plugins/editor-props.ts +39 -0
  160. package/src/plugins/elements.ts +60 -0
  161. package/src/plugins/footnotes.ts +298 -0
  162. package/src/plugins/link.ts +53 -0
  163. package/src/plugins/lock-body.ts +136 -0
  164. package/src/plugins/move-node.ts +55 -0
  165. package/src/plugins/objects.ts +125 -0
  166. package/src/plugins/paragraphs.ts +90 -0
  167. package/src/plugins/persist.ts +64 -0
  168. package/src/plugins/persistent-cursor.ts +66 -0
  169. package/src/plugins/placeholder.ts +219 -0
  170. package/src/plugins/prevent-empty.ts +50 -0
  171. package/src/plugins/search-replace/index.ts +157 -0
  172. package/src/plugins/search-replace/lib.ts +95 -0
  173. package/src/plugins/section_category.ts +212 -0
  174. package/src/plugins/section_title/autocompletion.ts +87 -0
  175. package/src/plugins/section_title/index.ts +105 -0
  176. package/src/plugins/selected-suggestion.ts +243 -0
  177. package/src/plugins/tables-cursor-fix.ts +143 -0
  178. package/src/plugins/translations.ts +263 -0
  179. package/src/rules.ts +60 -0
  180. package/src/selection.ts +112 -0
  181. package/src/testing/default-capabilities.ts +48 -0
  182. package/src/testing/default-editor-data.ts +73 -0
  183. package/src/testing/empty-editor-doc.json +62 -0
  184. package/src/testing/polyfill-dom.ts +43 -0
  185. package/src/testing/section-categories.json +161 -0
  186. package/src/testing/setup-editor.ts +55 -0
  187. package/src/tests.ts +42 -0
  188. package/src/toolbar.tsx +259 -0
  189. package/src/types/@citation-js/core.d.ts +16 -0
  190. package/src/types/@citation-js/index.d.ts +16 -0
  191. package/src/types/@citation-js/plugin-bibtex.d.ts +16 -0
  192. package/src/types/@citation-js/plugin-csl.d.ts +16 -0
  193. package/src/types/@citation-js/plugin-doi.d.ts +16 -0
  194. package/src/types/@citation-js/plugin-enw.d.ts +16 -0
  195. package/src/types/@citation-js/plugin-pubmed.d.ts +16 -0
  196. package/src/types/@citation-js/plugin-ris.d.ts +16 -0
  197. package/src/types/citeproc.d.ts +124 -0
  198. package/src/types/css.d.ts +16 -0
  199. package/src/types/json.d.ts +17 -0
  200. package/src/types/prosemirror-dev-tools.d.ts +16 -0
  201. package/src/types/prosemirror-dropcursor.d.ts +16 -0
  202. package/src/types/styled-components.d.ts +23 -0
  203. package/src/types.ts +49 -0
  204. package/src/useEditor.ts +244 -0
  205. package/src/versions.ts +3 -0
  206. package/src/views/ReactSubView.tsx +145 -0
  207. package/src/views/abstracts.ts +46 -0
  208. package/src/views/accessibility_element.ts +86 -0
  209. package/src/views/affiliations.ts +254 -0
  210. package/src/views/alt_title.ts +85 -0
  211. package/src/views/alt_titles_section.ts +97 -0
  212. package/src/views/attachment.ts +160 -0
  213. package/src/views/attachments.ts +72 -0
  214. package/src/views/author_notes.ts +96 -0
  215. package/src/views/award.ts +194 -0
  216. package/src/views/awards.ts +59 -0
  217. package/src/views/base_node_view.ts +85 -0
  218. package/src/views/bibliography_element.ts +313 -0
  219. package/src/views/block_view.ts +100 -0
  220. package/src/views/blockquote_element.ts +26 -0
  221. package/src/views/blockquote_element_editable.ts +24 -0
  222. package/src/views/box_element.ts +33 -0
  223. package/src/views/caption.ts +37 -0
  224. package/src/views/caption_title.ts +33 -0
  225. package/src/views/citation.ts +67 -0
  226. package/src/views/citation_editable.ts +312 -0
  227. package/src/views/contributors.ts +341 -0
  228. package/src/views/creators.ts +82 -0
  229. package/src/views/cross_reference.ts +74 -0
  230. package/src/views/cross_reference_editable.ts +113 -0
  231. package/src/views/editable_block.ts +141 -0
  232. package/src/views/embed.ts +345 -0
  233. package/src/views/empty.ts +26 -0
  234. package/src/views/equation.ts +47 -0
  235. package/src/views/equation_editable.ts +84 -0
  236. package/src/views/equation_element.ts +29 -0
  237. package/src/views/equation_element_editable.ts +21 -0
  238. package/src/views/figure.ts +84 -0
  239. package/src/views/figure_editable.ts +361 -0
  240. package/src/views/figure_element.ts +197 -0
  241. package/src/views/figure_element_editable.ts +21 -0
  242. package/src/views/footnote.ts +236 -0
  243. package/src/views/footnotes_element.ts +54 -0
  244. package/src/views/general_table_footnote.ts +140 -0
  245. package/src/views/hero_image.ts +76 -0
  246. package/src/views/hero_image_editable.ts +21 -0
  247. package/src/views/image_element.ts +373 -0
  248. package/src/views/image_element_editable.ts +21 -0
  249. package/src/views/inline_equation.ts +47 -0
  250. package/src/views/inline_equation_editable.ts +70 -0
  251. package/src/views/inline_footnote.ts +264 -0
  252. package/src/views/inline_footnote_editable.ts +20 -0
  253. package/src/views/keyword.ts +127 -0
  254. package/src/views/keyword_group.ts +109 -0
  255. package/src/views/link.ts +27 -0
  256. package/src/views/link_editable.ts +169 -0
  257. package/src/views/list.ts +46 -0
  258. package/src/views/list_item.ts +26 -0
  259. package/src/views/paragraph.ts +26 -0
  260. package/src/views/paragraph_editable.ts +21 -0
  261. package/src/views/placeholder.ts +35 -0
  262. package/src/views/placeholder_element.ts +38 -0
  263. package/src/views/placeholder_element_editable.ts +21 -0
  264. package/src/views/pullquote_element.ts +106 -0
  265. package/src/views/pullquote_element_editable.ts +24 -0
  266. package/src/views/quote_image_editable.ts +84 -0
  267. package/src/views/section.ts +64 -0
  268. package/src/views/section_label.ts +20 -0
  269. package/src/views/section_title.ts +87 -0
  270. package/src/views/section_title_editable.ts +21 -0
  271. package/src/views/subtitle.ts +42 -0
  272. package/src/views/subtitle_editable.ts +20 -0
  273. package/src/views/subtitles.ts +34 -0
  274. package/src/views/subtitles_editable.ts +21 -0
  275. package/src/views/supplement.ts +236 -0
  276. package/src/views/supplement_editable.ts +20 -0
  277. package/src/views/supplements.ts +65 -0
  278. package/src/views/supplements_editable.ts +21 -0
  279. package/src/views/table_cell.ts +150 -0
  280. package/src/views/table_element.ts +33 -0
  281. package/src/views/table_element_editable.ts +21 -0
  282. package/src/views/table_element_footer.ts +41 -0
  283. package/src/views/title.ts +46 -0
  284. package/src/views/title_editable.ts +20 -0
  285. package/src/views/trans_graphical_abstract.ts +55 -0
  286. package/src/views/trans_graphical_abstract_editable.ts +21 -0
  287. package/src/views/translated_abstract.ts +53 -0
  288. package/src/views/translated_abstract_editable.ts +21 -0
  289. package/dist/cjs/lib/statistics.js +0 -38
@@ -0,0 +1,423 @@
1
+ /*!
2
+ * © 2022 Atypon Systems LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import {
17
+ DotsIcon,
18
+ DropdownList,
19
+ getFileIcon,
20
+ IconButton,
21
+ IconTextButton,
22
+ TriangleCollapsedIcon,
23
+ UploadIcon,
24
+ useDropdown,
25
+ } from '@manuscripts/style-guide'
26
+ import { FigureNode } from '@manuscripts/transform'
27
+ import { Node as ManuscriptNode } from 'prosemirror-model'
28
+ import React, { SyntheticEvent, useCallback, useEffect, useRef } from 'react'
29
+ import styled from 'styled-components'
30
+
31
+ import { Capabilities } from '../../lib/capabilities'
32
+ import {
33
+ FileAttachment,
34
+ ManuscriptFiles,
35
+ memoGroupFiles,
36
+ } from '../../lib/files'
37
+ import { getMediaTypeInfo } from '../../lib/get-media-type'
38
+ import { createKeyboardInteraction } from '../../lib/navigation-utils'
39
+ import { ReactViewComponentProps } from '../../views/ReactSubView'
40
+
41
+ // Custom hook for figure dropdown keyboard navigation
42
+ function useDropdownKeyboardNav(
43
+ isOpen: boolean,
44
+ containerRef: React.RefObject<HTMLDivElement | null>,
45
+ onEscape: () => void,
46
+ onArrowLeft?: () => void
47
+ ) {
48
+ useEffect(() => {
49
+ const container = containerRef.current
50
+ if (!isOpen || !container) {
51
+ return
52
+ }
53
+
54
+ const buttons = Array.from(
55
+ container.querySelectorAll('button:not([disabled])')
56
+ ) as HTMLElement[]
57
+
58
+ if (buttons.length === 0) {
59
+ return
60
+ }
61
+
62
+ const removeKeydownListener = createKeyboardInteraction({
63
+ container: container,
64
+ navigation: {
65
+ getItems: () =>
66
+ Array.from(container.querySelectorAll('button:not([disabled])')),
67
+ arrowKeys: {
68
+ forward: 'ArrowDown',
69
+ backward: 'ArrowUp',
70
+ },
71
+ },
72
+ additionalKeys: {
73
+ Enter: (e) => (e.target as HTMLElement).click(),
74
+ Escape: () => onEscape(),
75
+ ...(onArrowLeft ? { ArrowLeft: () => onArrowLeft() } : {}),
76
+ },
77
+ })
78
+
79
+ // Focus first button when dropdown opens
80
+ window.requestAnimationFrame(() => {
81
+ buttons[0]?.focus()
82
+ })
83
+
84
+ return () => {
85
+ removeKeydownListener()
86
+ }
87
+ }, [isOpen, containerRef, onEscape, onArrowLeft])
88
+ }
89
+
90
+ export interface FigureDropdownProps {
91
+ can: Capabilities
92
+ getFiles: () => FileAttachment[]
93
+ }
94
+
95
+ export interface FigureOptionsProps extends FigureDropdownProps {
96
+ onDownload?: () => void
97
+ onUpload?: () => void
98
+ onDetach?: () => void
99
+ onReplace?: (file: FileAttachment, isSupplement?: boolean) => void
100
+ onReplaceEmbed?: () => void
101
+ getDoc: () => ManuscriptNode
102
+ onDelete?: () => void
103
+ isEmbed: boolean
104
+ hasSiblings: () => boolean
105
+ }
106
+
107
+ export interface FigureElementOptionsProps extends FigureDropdownProps {
108
+ onAdd: (file: FileAttachment) => Promise<void>
109
+ onUpload: () => void
110
+ hasUploadedImage: boolean
111
+ }
112
+
113
+ function getSupplements(
114
+ getFiles: () => FileAttachment[],
115
+ getDoc: () => ManuscriptNode,
116
+ groupFiles: (doc: ManuscriptNode, files: FileAttachment[]) => ManuscriptFiles,
117
+ isEmbed: boolean
118
+ ) {
119
+ return groupFiles(getDoc(), getFiles())
120
+ .supplements.map((s) => s.file)
121
+ .filter((f) => {
122
+ const mediaInfo = getMediaTypeInfo(f.name)
123
+ return isEmbed
124
+ ? mediaInfo.isVideo || mediaInfo.isAudio
125
+ : mediaInfo.isImage
126
+ })
127
+ }
128
+
129
+ function getOtherFiles(
130
+ getFiles: () => FileAttachment[],
131
+ getDoc: () => ManuscriptNode,
132
+ groupFiles: (doc: ManuscriptNode, files: FileAttachment[]) => ManuscriptFiles,
133
+ isEmbed: boolean
134
+ ) {
135
+ return groupFiles(getDoc(), getFiles()).others.filter((f) => {
136
+ const mediaInfo = getMediaTypeInfo(f.name)
137
+ return isEmbed ? mediaInfo.isVideo || mediaInfo.isAudio : mediaInfo.isImage
138
+ })
139
+ }
140
+
141
+ type WrappedProps = FigureOptionsProps & ReactViewComponentProps<FigureNode>
142
+
143
+ export const FigureOptions: React.FC<WrappedProps> = ({
144
+ can,
145
+ getDoc,
146
+ getFiles,
147
+ onDownload,
148
+ onUpload,
149
+ onDetach,
150
+ onReplace,
151
+ onReplaceEmbed,
152
+ onDelete,
153
+ isEmbed,
154
+ hasSiblings,
155
+ container,
156
+ }) => {
157
+ const { isOpen, toggleOpen, wrapperRef } = useDropdown()
158
+ const dropdownRef = useRef<HTMLDivElement>(null)
159
+
160
+ const showDownload = onDownload && can.downloadFiles
161
+ const showUpload = onUpload && can.uploadFile
162
+ const showDetach = onDetach && can.detachFile
163
+ const showReplace =
164
+ (onReplace && can.replaceFile) || (onReplaceEmbed && can.editArticle)
165
+ const replaceBtnText = onDownload ? 'Replace' : 'Choose file'
166
+ const showDelete = () => {
167
+ if (!hasSiblings()) {
168
+ return false
169
+ }
170
+ if (onDelete && can.detachFile) {
171
+ return true
172
+ }
173
+ return false
174
+ }
175
+
176
+ useEffect(() => {
177
+ const activeClass = 'figure-dropdown-active'
178
+ if (isOpen) {
179
+ container.classList.add(activeClass)
180
+ } else {
181
+ container.classList.remove(activeClass)
182
+ }
183
+ }, [isOpen, container.classList])
184
+
185
+ useDropdownKeyboardNav(isOpen, dropdownRef, toggleOpen)
186
+
187
+ const isEmbedMode = !!onReplaceEmbed
188
+
189
+ const groupFiles = memoGroupFiles()
190
+
191
+ return (
192
+ <DropdownWrapper ref={wrapperRef}>
193
+ <OptionsButton
194
+ className={'options-button'}
195
+ onClick={toggleOpen}
196
+ onKeyDown={(e) => {
197
+ if (e.key === 'Enter') {
198
+ e.preventDefault()
199
+ toggleOpen()
200
+ }
201
+ }}
202
+ >
203
+ <DotsIcon />
204
+ </OptionsButton>
205
+ {isOpen && (
206
+ <OptionsDropdownList
207
+ direction={'right'}
208
+ width={128}
209
+ top={5}
210
+ ref={dropdownRef}
211
+ >
212
+ {showReplace && isEmbedMode && (
213
+ <ListItemButton onClick={() => onReplaceEmbed && onReplaceEmbed()}>
214
+ Edit Link
215
+ </ListItemButton>
216
+ )}
217
+ {showReplace && !isEmbedMode && (
218
+ <NestedDropdown
219
+ disabled={!showReplace}
220
+ parentToggleOpen={toggleOpen}
221
+ buttonText={replaceBtnText}
222
+ moveLeft
223
+ list={
224
+ <>
225
+ {getSupplements(getFiles, getDoc, groupFiles, isEmbed).map(
226
+ (file, index) => (
227
+ <ListItemButton
228
+ key={file.id}
229
+ id={index.toString()}
230
+ onClick={() => onReplace && onReplace(file, true)}
231
+ >
232
+ {getFileIcon(file.name)}
233
+ <ListItemText>{file.name}</ListItemText>
234
+ </ListItemButton>
235
+ )
236
+ )}
237
+ {getOtherFiles(getFiles, getDoc, groupFiles, isEmbed).map(
238
+ (file, index) => (
239
+ <ListItemButton
240
+ key={file.id}
241
+ id={index.toString()}
242
+ onClick={() => onReplace && onReplace(file)}
243
+ >
244
+ {getFileIcon(file.name)}
245
+ <ListItemText>{file.name}</ListItemText>
246
+ </ListItemButton>
247
+ )
248
+ )}
249
+ {showUpload && (
250
+ <UploadButton onClick={onUpload} disabled={!showUpload}>
251
+ <UploadIcon /> Upload new...
252
+ </UploadButton>
253
+ )}
254
+ </>
255
+ }
256
+ />
257
+ )}
258
+ {showDownload && (
259
+ <ListItemButton onClick={onDownload} disabled={!showDownload}>
260
+ Download
261
+ </ListItemButton>
262
+ )}
263
+ {showDetach && (
264
+ <ListItemButton onClick={onDetach} disabled={!showDetach}>
265
+ Detach
266
+ </ListItemButton>
267
+ )}
268
+ {showDelete() && (
269
+ <ListItemButton onClick={onDelete}>Delete</ListItemButton>
270
+ )}
271
+ </OptionsDropdownList>
272
+ )}
273
+ </DropdownWrapper>
274
+ )
275
+ }
276
+
277
+ const NestedDropdown: React.FC<{
278
+ parentToggleOpen: (e: SyntheticEvent) => void
279
+ buttonText: string
280
+ disabled: boolean
281
+ list: React.ReactNode
282
+ moveLeft?: boolean
283
+ }> = ({ parentToggleOpen, buttonText, disabled, list, moveLeft }) => {
284
+ const { isOpen, toggleOpen, wrapperRef } = useDropdown()
285
+ const nestedListRef = useRef<HTMLDivElement>(null)
286
+
287
+ // Handle ArrowLeft to close nested dropdown and focus parent
288
+ const handleArrowLeft = useCallback(() => {
289
+ toggleOpen()
290
+ const parentButton = wrapperRef.current?.querySelector(
291
+ '.nested-list-button'
292
+ ) as HTMLElement
293
+ parentButton?.focus()
294
+ }, [toggleOpen, wrapperRef])
295
+
296
+ // For nested dropdown: Escape and ArrowLeft both close nested menu and return to parent
297
+ useDropdownKeyboardNav(
298
+ isOpen,
299
+ nestedListRef,
300
+ handleArrowLeft,
301
+ handleArrowLeft
302
+ )
303
+
304
+ return (
305
+ <DropdownWrapper ref={wrapperRef}>
306
+ <NestedListButton
307
+ className="nested-list-button"
308
+ onClick={toggleOpen}
309
+ disabled={disabled}
310
+ onKeyDown={(e) => {
311
+ if (e.key === 'ArrowRight') {
312
+ e.preventDefault()
313
+ toggleOpen()
314
+ }
315
+ }}
316
+ >
317
+ <div>{buttonText}</div>
318
+ <TriangleCollapsedIcon />
319
+ </NestedListButton>
320
+ {isOpen && (
321
+ <NestedListDropdownList
322
+ direction={'right'}
323
+ moveLeft={moveLeft}
324
+ width={192}
325
+ ref={nestedListRef}
326
+ onClick={(e) => {
327
+ toggleOpen()
328
+ parentToggleOpen(e)
329
+ }}
330
+ >
331
+ {list}
332
+ </NestedListDropdownList>
333
+ )}
334
+ </DropdownWrapper>
335
+ )
336
+ }
337
+
338
+ const DropdownWrapper = styled.div``
339
+
340
+ const OptionsDropdownList = styled(DropdownList)`
341
+ right: 4%;
342
+ `
343
+
344
+ const OptionsButton = styled(IconButton)`
345
+ border: 1px solid #e2e2e2 !important;
346
+ box-sizing: border-box;
347
+ border-radius: 50%;
348
+ width: ${(props) => props.theme.grid.unit * 6}px;
349
+ height: ${(props) => props.theme.grid.unit * 6}px;
350
+ margin: ${(props) => props.theme.grid.unit}px;
351
+ visibility: hidden;
352
+ background: #ffffff;
353
+
354
+ &:hover {
355
+ background: #f2fbfc !important;
356
+ border: 1px solid #e2e2e2 !important;
357
+ }
358
+
359
+ &:active,
360
+ &:focus {
361
+ background: #f2fbfc !important;
362
+ border: 1px solid #e2e2e2 !important;
363
+
364
+ circle {
365
+ fill: #1a9bc7;
366
+ }
367
+ }
368
+
369
+ circle {
370
+ fill: #6e6e6e;
371
+ }
372
+ `
373
+
374
+ const ListItemButton = styled(IconTextButton)`
375
+ padding: 16px 14px;
376
+ justify-content: space-between;
377
+ align-items: center;
378
+
379
+ :hover,
380
+ :focus-visible {
381
+ background: #f2fbfc;
382
+ }
383
+
384
+ &:is([disabled]) {
385
+ background: #ffffff;
386
+ color: #353535;
387
+ opacity: 0.4;
388
+ }
389
+ `
390
+
391
+ const ListItemText = styled.div`
392
+ width: 141px;
393
+ overflow: hidden;
394
+ text-overflow: ellipsis;
395
+ text-align: start;
396
+ `
397
+
398
+ const NestedListButton = styled(ListItemButton)`
399
+ width: 100%;
400
+ &:active,
401
+ &:focus,
402
+ &:focus-visible {
403
+ background: #f2fbfc;
404
+ }
405
+ svg {
406
+ margin-right: 0;
407
+ }
408
+ `
409
+
410
+ const NestedListDropdownList = styled(DropdownList)<{ moveLeft?: boolean }>`
411
+ top: 0;
412
+ ${(props) => (props.moveLeft && 'right: 30%;') || 'left: 100%;'}
413
+ `
414
+
415
+ const UploadButton = styled(IconTextButton)`
416
+ border-top: 1px solid #f2f2f2;
417
+ padding: ${(props) => props.theme.grid.unit * 4}px;
418
+ justify-content: flex-start;
419
+
420
+ &:focus-visible {
421
+ background: #f2fbfc;
422
+ }
423
+ `
@@ -0,0 +1,255 @@
1
+ /*!
2
+ * © 2023 Atypon Systems LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import {
18
+ AddedIcon,
19
+ AddIcon,
20
+ AddNewIcon,
21
+ ButtonGroup,
22
+ IconTextButton,
23
+ PrimaryButton,
24
+ SecondaryButton,
25
+ withFocusTrap,
26
+ withListNavigation,
27
+ withNavigableListItem,
28
+ } from '@manuscripts/style-guide'
29
+ import { isDeleted } from '@manuscripts/track-changes-plugin'
30
+ import { FootnoteNode, InlineFootnoteNode } from '@manuscripts/transform'
31
+ import React, { useState } from 'react'
32
+ import styled from 'styled-components'
33
+
34
+ const NotesContainer = styled.div`
35
+ height: 90vh;
36
+ max-height: 400px;
37
+ overflow-y: auto;
38
+ `
39
+
40
+ const Actions = styled(ButtonGroup)`
41
+ align-items: center;
42
+ box-shadow: 0 -2px 12px 0 rgba(216, 216, 216, 0.26);
43
+ display: flex;
44
+ justify-content: space-between;
45
+ padding: ${(props) => props.theme.grid.unit * 4}px;
46
+ `
47
+
48
+ const Container = withFocusTrap(styled.div`
49
+ flex: 1;
50
+ font-family: ${(props) => props.theme.font.family.sans};
51
+ `)
52
+
53
+ const AddNewFootnote = styled(ButtonGroup)`
54
+ button {
55
+ margin-right: ${(props) => props.theme.grid.unit * 8}px;
56
+ }
57
+
58
+ button:hover,
59
+ button:active {
60
+ path {
61
+ stroke: ${(props) => props.theme.colors.brand.medium};
62
+ }
63
+ rect {
64
+ stroke: ${(props) => props.theme.colors.brand.medium};
65
+ }
66
+ }
67
+ `
68
+
69
+ export interface FootnotesSelectorProps {
70
+ footnotes: FootnoteNode[]
71
+ inlineFootnote?: InlineFootnoteNode
72
+ labels: Map<string, string>
73
+ onAdd: () => void
74
+ onInsert: (notes: FootnoteNode[]) => void
75
+ onCancel: () => void
76
+ }
77
+
78
+ export const FootnotesSelector: React.FC<FootnotesSelectorProps> = ({
79
+ footnotes,
80
+ inlineFootnote,
81
+ labels,
82
+ onAdd,
83
+ onInsert,
84
+ onCancel,
85
+ }) => {
86
+ let selectedNotesMap
87
+
88
+ if (inlineFootnote) {
89
+ const rids = inlineFootnote.attrs.rids
90
+ const selectedNotes = footnotes.filter((node) =>
91
+ rids.includes(node.attrs.id)
92
+ )
93
+ selectedNotesMap = new Map(
94
+ selectedNotes.map((node) => [node.attrs.id, node])
95
+ )
96
+ }
97
+
98
+ const [selections, setSelections] = useState(
99
+ new Map<string, FootnoteNode>(selectedNotesMap)
100
+ )
101
+
102
+ const toggleSelection = (item: FootnoteNode) => {
103
+ const id = item.attrs.id
104
+ if (selections.has(id)) {
105
+ selections.delete(id)
106
+ setSelections(new Map([...selections]))
107
+ } else {
108
+ selections.set(id, item)
109
+ setSelections(new Map([...selections]))
110
+ }
111
+ }
112
+
113
+ const isSelected = (item: FootnoteNode) => {
114
+ return selections.has(item.attrs.id)
115
+ }
116
+
117
+ const handleClick = () => {
118
+ return onInsert(footnotes.filter((node) => selections.has(node.attrs.id)))
119
+ }
120
+
121
+ return (
122
+ <Container>
123
+ <NotesContainer>
124
+ <FootnotesList
125
+ footnotes={footnotes}
126
+ inlineFootnote={inlineFootnote}
127
+ labels={labels}
128
+ isSelected={isSelected}
129
+ onSelect={toggleSelection}
130
+ />
131
+ </NotesContainer>
132
+ <Actions>
133
+ <AddNewFootnote>
134
+ <IconTextButton onClick={onAdd}>
135
+ <AddNewIcon />
136
+ Add new
137
+ </IconTextButton>
138
+ </AddNewFootnote>
139
+ <ButtonGroup>
140
+ <SecondaryButton onClick={onCancel}>Cancel</SecondaryButton>
141
+ <PrimaryButton
142
+ onClick={handleClick}
143
+ disabled={selections.size === 0 && !inlineFootnote}
144
+ >
145
+ {inlineFootnote ? 'Update' : 'Insert'}
146
+ </PrimaryButton>
147
+ </ButtonGroup>
148
+ </Actions>
149
+ </Container>
150
+ )
151
+ }
152
+
153
+ const FootnotesList: React.FC<{
154
+ footnotes: FootnoteNode[]
155
+ inlineFootnote?: InlineFootnoteNode
156
+ labels: Map<string, string>
157
+ isSelected: (item: FootnoteNode) => boolean
158
+ onSelect: (item: FootnoteNode) => void
159
+ }> = ({ footnotes, inlineFootnote, labels, isSelected, onSelect }) => {
160
+ const rids = inlineFootnote?.attrs.rids
161
+ const selectedNotes: FootnoteNode[] = []
162
+ const remainingNotes: FootnoteNode[] = []
163
+
164
+ footnotes.forEach((footnote) => {
165
+ const isNoteSelected = rids?.includes(footnote.attrs.id)
166
+ if (isDeleted(footnote)) {
167
+ return
168
+ }
169
+ isNoteSelected
170
+ ? selectedNotes.push(footnote)
171
+ : remainingNotes.push(footnote)
172
+ })
173
+
174
+ return (
175
+ <NotesListContainer>
176
+ {selectedNotes.map((footnote) => (
177
+ <FootnoteItem
178
+ key={footnote.attrs.id}
179
+ footnote={footnote}
180
+ label={labels.get(footnote.attrs.id)}
181
+ isSelected={isSelected}
182
+ onSelect={onSelect}
183
+ />
184
+ ))}
185
+ {selectedNotes.length > 0 && remainingNotes.length > 0 && <Separator />}
186
+ {remainingNotes.map((footnote) => (
187
+ <FootnoteItem
188
+ key={footnote.attrs.id}
189
+ footnote={footnote}
190
+ label={labels.get(footnote.attrs.id)}
191
+ isSelected={isSelected}
192
+ onSelect={onSelect}
193
+ />
194
+ ))}
195
+ </NotesListContainer>
196
+ )
197
+ }
198
+
199
+ const FootnoteItem: React.FC<{
200
+ footnote: FootnoteNode
201
+ label?: string
202
+ isSelected: (item: FootnoteNode) => boolean
203
+ onSelect: (item: FootnoteNode) => void
204
+ }> = ({ footnote, label, isSelected, onSelect }) => {
205
+ return (
206
+ <FootnoteItemContainer
207
+ onClick={() => onSelect(footnote)}
208
+ data-cy="footnote-item"
209
+ >
210
+ <StatusIcon>
211
+ {isSelected(footnote) ? (
212
+ <AddedIcon data-cy={'plus-icon-ok'} />
213
+ ) : (
214
+ <AddIcon data-cy={'plus-icon'} />
215
+ )}
216
+ </StatusIcon>
217
+ <NoteText>{(label ? label + '. ' : '') + footnote.textContent}</NoteText>
218
+ </FootnoteItemContainer>
219
+ )
220
+ }
221
+
222
+ const Separator = styled.div`
223
+ height: 0;
224
+ border-bottom: 1px solid #e2e2e2;
225
+ margin: 4px 0;
226
+ `
227
+ const NotesListContainer = withListNavigation(styled.div`
228
+ padding: ${(props) => props.theme.grid.unit * 6}px
229
+ ${(props) => props.theme.grid.unit * 5}px;
230
+ flex: 1;
231
+ overflow-y: auto;
232
+ `)
233
+
234
+ const FootnoteItemContainer = withNavigableListItem(styled.div`
235
+ cursor: pointer;
236
+ padding: ${(props) => props.theme.grid.unit * 2}px 0;
237
+ display: flex;
238
+ `)
239
+ const StatusIcon = styled.div`
240
+ flex-shrink: 1;
241
+ margin-right: ${(props) => props.theme.grid.unit * 3}px;
242
+ margin-left: ${(props) => props.theme.grid.unit}px;
243
+ height: ${(props) => props.theme.grid.unit * 6}px;
244
+ width: ${(props) => props.theme.grid.unit * 6}px;
245
+ display: inline-flex;
246
+ justify-content: center;
247
+ align-items: center;
248
+ cursor: pointer;
249
+ `
250
+ const NoteText = styled.div`
251
+ color: ${(props) => props.theme.colors.text.primary};
252
+ flex: 1;
253
+ font-weight: ${(props) => props.theme.font.weight.normal};
254
+ margin-top: 2px;
255
+ `