@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,334 @@
1
+ /*!
2
+ * © 2019 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
+ ManuscriptEditorView,
19
+ ManuscriptNode,
20
+ schema,
21
+ } from '@manuscripts/transform'
22
+
23
+ import { openEmbedDialog } from '../components/toolbar/InsertEmbedDialog'
24
+ import { handleEnterKey } from './navigation-utils'
25
+ import {
26
+ FigureOptions,
27
+ FigureOptionsProps,
28
+ } from '../components/views/FigureDropdown'
29
+ import { EditorProps } from '../configs/ManuscriptsEditor'
30
+ import { fileCorruptedIcon } from '../icons'
31
+ import { Trackable } from '../types'
32
+ import ReactSubView from '../views/ReactSubView'
33
+ import { FileAttachment } from './files'
34
+
35
+ export const createUnsupportedFormat = (
36
+ filename: string,
37
+ canEdit: boolean
38
+ ): HTMLElement => {
39
+ const element = document.createElement('div')
40
+ element.classList.add('figure', 'placeholder')
41
+
42
+ const instructions = document.createElement('div')
43
+ instructions.classList.add('instructions')
44
+
45
+ const iconHtml = fileCorruptedIcon
46
+
47
+ instructions.innerHTML = `
48
+ <div>
49
+ <div class="unsupported-icon-wrapper">${iconHtml}</div>
50
+ <div>${filename}</div>
51
+ <div class="unsupported-format-label">
52
+ Unsupported file format
53
+ </div>
54
+ <div>
55
+ ${canEdit ? 'Click to add media' : 'No media here yet…'}
56
+ </div>
57
+ </div>
58
+ `
59
+
60
+ element.appendChild(instructions)
61
+ return element
62
+ }
63
+
64
+ export enum MediaType {
65
+ Media = 'media',
66
+ Figure = 'figure',
67
+ ExternalLink = 'external_link',
68
+ }
69
+
70
+ const MediaLabels: Record<MediaType, string> = {
71
+ [MediaType.Media]: 'media',
72
+ [MediaType.Figure]: 'figure',
73
+ [MediaType.ExternalLink]: 'a file to link',
74
+ }
75
+
76
+ export const createMediaPlaceholder = (
77
+ mediaType: MediaType = MediaType.Media,
78
+ view?: ManuscriptEditorView,
79
+ getPos?: () => number,
80
+ props?: EditorProps
81
+ ): HTMLElement => {
82
+ const element = document.createElement('div')
83
+ element.classList.add('figure', 'placeholder')
84
+ element.tabIndex = 0
85
+ const instructions = document.createElement('div')
86
+ instructions.classList.add('instructions')
87
+
88
+ instructions.innerHTML = `
89
+ <div class="placeholder-content">
90
+ <p>Drag or click here to upload ${MediaLabels[mediaType]} <br>
91
+ ${
92
+ mediaType === MediaType.ExternalLink
93
+ ? ` or drag items here from the file <a data-action='open-other-files'>'Other files'</a> in the inspector.</p>`
94
+ : `or drag items here from the file inspector tabs <br>
95
+ <a data-action='open-other-files'>'Other files'</a> |
96
+ <a data-action='open-supplement-files'>'Supplements'</a>`
97
+ }
98
+
99
+ ${
100
+ mediaType === MediaType.Media && view && getPos
101
+ ? "| <a data-action='add-external-link'>'External link'</a>"
102
+ : ''
103
+ }
104
+ </p>
105
+ </div>
106
+ `
107
+
108
+ if (mediaType === MediaType.Media && view && getPos) {
109
+ const embedLink = instructions.querySelector(
110
+ "[data-action='add-external-link']"
111
+ ) as HTMLElement
112
+ embedLink.tabIndex = 0
113
+ if (embedLink) {
114
+ embedLink.addEventListener('click', (e) => {
115
+ e.stopPropagation()
116
+ e.preventDefault()
117
+ openEmbedDialog(view, getPos())
118
+ })
119
+ embedLink.addEventListener(
120
+ 'keydown',
121
+ handleEnterKey((e) => {
122
+ e.stopPropagation()
123
+ e.preventDefault()
124
+ openEmbedDialog(view, getPos())
125
+ })
126
+ )
127
+ }
128
+ }
129
+
130
+ const links = instructions.querySelectorAll<HTMLElement>(
131
+ `[data-action='open-other-files'], [data-action='open-supplement-files'] `
132
+ )
133
+ links.forEach((link) => {
134
+ link.tabIndex = 0
135
+ link.addEventListener(
136
+ 'keydown',
137
+ handleEnterKey((event) => {
138
+ event.preventDefault()
139
+ event.stopPropagation()
140
+ props?.onEditorClick(event)
141
+ })
142
+ )
143
+ })
144
+
145
+ element.appendChild(instructions)
146
+ return element
147
+ }
148
+
149
+ export interface FileHandlers {
150
+ handleDownload?: () => void
151
+ handleUpload?: () => void
152
+ handleReplace?: (file: FileAttachment, isSupplement?: boolean) => void
153
+ handleReplaceEmbed?: () => void
154
+ handleDetach?: () => void
155
+ handleDelete?: () => void
156
+ }
157
+
158
+ type Handleable = { href?: string; src?: string }
159
+
160
+ export const createFileHandlers = <T extends Handleable>(
161
+ attrs: T,
162
+ fileAttrKey: keyof Handleable,
163
+ view: ManuscriptEditorView,
164
+ props: EditorProps,
165
+ setAttr: (value: string) => void
166
+ ): FileHandlers => {
167
+ const handlers: FileHandlers = {}
168
+
169
+ const controlAttr = attrs[fileAttrKey]
170
+ const files = props.getFiles()
171
+ const file = controlAttr && files.find((f) => f.id === controlAttr)
172
+ const can = props.getCapabilities()
173
+
174
+ if (controlAttr && file) {
175
+ handlers.handleDownload = () => {
176
+ props.fileManagement.download(file)
177
+ }
178
+ handlers.handleDetach = () => {
179
+ setAttr('')
180
+ }
181
+ }
182
+
183
+ if (can.replaceFile) {
184
+ handlers.handleReplace = (file: FileAttachment, isSupplement = false) => {
185
+ setAttr(file.id)
186
+ if (isSupplement) {
187
+ const tr = view.state.tr
188
+ view.state.doc.descendants((node, pos) => {
189
+ if (node.type === schema.nodes.supplement) {
190
+ const attr = node.attrs[fileAttrKey]
191
+ if (attr === file.id) {
192
+ tr.delete(pos, pos + node.nodeSize)
193
+ view.dispatch(tr)
194
+ }
195
+ }
196
+
197
+ if (
198
+ node.type !== schema.nodes.supplements &&
199
+ node.type !== schema.nodes.manuscript
200
+ ) {
201
+ return false
202
+ }
203
+ })
204
+ }
205
+ }
206
+ }
207
+
208
+ return handlers
209
+ }
210
+
211
+ export const createReactTools = <T extends ManuscriptNode>(
212
+ node: Trackable<T>,
213
+ view: ManuscriptEditorView,
214
+ getPos: () => number,
215
+ props: EditorProps,
216
+ handlers: FileHandlers,
217
+ isEmbed: boolean,
218
+ hasSiblings: () => boolean
219
+ ): HTMLDivElement | null => {
220
+ if (!props.dispatch || !props.theme) {
221
+ return null
222
+ }
223
+
224
+ const can = props.getCapabilities()
225
+
226
+ const componentProps: FigureOptionsProps = {
227
+ can,
228
+ getDoc: () => view.state.doc,
229
+ getFiles: props.getFiles,
230
+ onDownload: handlers.handleDownload,
231
+ onUpload: handlers.handleUpload,
232
+ onDetach: handlers.handleDetach,
233
+ onReplace: handlers.handleReplace,
234
+ onReplaceEmbed: handlers.handleReplaceEmbed,
235
+ onDelete: handlers.handleDelete,
236
+ isEmbed,
237
+ hasSiblings,
238
+ }
239
+
240
+ return ReactSubView(props, FigureOptions, componentProps, node, getPos, view)
241
+ }
242
+
243
+ export const createFileUploader = (
244
+ handler: (file: File) => Promise<void>,
245
+ accept = '*/*'
246
+ ) => {
247
+ const handleFileChange = async (e: Event) => {
248
+ const target = e.target as HTMLInputElement
249
+ if (target && target.files && target.files.length) {
250
+ await handler(target.files[0])
251
+ }
252
+ }
253
+
254
+ const input = document.createElement('input')
255
+ input.accept = accept
256
+ input.type = 'file'
257
+ input.addEventListener('change', handleFileChange)
258
+
259
+ return () => input.click()
260
+ }
261
+
262
+ export const addInteractionHandlers = (
263
+ element: HTMLElement,
264
+ uploadFn: (file: File) => Promise<void>,
265
+ accept = '*/*'
266
+ ): void => {
267
+ const handlePlaceholderInteraction = (target: HTMLElement) => {
268
+ if (target.dataset && target.dataset.action) {
269
+ return
270
+ }
271
+ const input = document.createElement('input')
272
+ input.type = 'file'
273
+ input.accept = accept
274
+ input.addEventListener('change', async (e) => {
275
+ const files = (e.target as HTMLInputElement).files
276
+ if (files && files.length > 0) {
277
+ await uploadFn(files[0])
278
+ }
279
+ })
280
+ input.click()
281
+ }
282
+
283
+ const handlePlaceholderClick = (event: Event) => {
284
+ const target = event.target as HTMLElement
285
+ handlePlaceholderInteraction(target)
286
+ }
287
+
288
+ element.addEventListener('click', handlePlaceholderClick)
289
+ element.addEventListener(
290
+ 'keydown',
291
+ handleEnterKey(() => {
292
+ const target = document.activeElement as HTMLElement
293
+ handlePlaceholderInteraction(target)
294
+ })
295
+ )
296
+
297
+ element.addEventListener('mouseenter', () => {
298
+ element.classList.toggle('over', true)
299
+ })
300
+
301
+ element.addEventListener('mouseleave', () => {
302
+ element.classList.toggle('over', false)
303
+ })
304
+
305
+ element.addEventListener('dragenter', (event) => {
306
+ event.preventDefault()
307
+ element.classList.toggle('over', true)
308
+ })
309
+
310
+ element.addEventListener('dragleave', (event) => {
311
+ event.preventDefault()
312
+ element.classList.toggle('over', false)
313
+ })
314
+
315
+ element.addEventListener('dragover', (e) => {
316
+ const dragEvent = e as DragEvent
317
+ if (dragEvent.dataTransfer && dragEvent.dataTransfer.items) {
318
+ for (const item of dragEvent.dataTransfer.items) {
319
+ if (item.kind === 'file') {
320
+ e.preventDefault()
321
+ dragEvent.dataTransfer.dropEffect = 'copy'
322
+ break
323
+ }
324
+ }
325
+ }
326
+ })
327
+
328
+ element.addEventListener('drop', async (e) => {
329
+ if (e.dataTransfer && e.dataTransfer.files.length) {
330
+ e.preventDefault()
331
+ await uploadFn(e.dataTransfer.files[0])
332
+ }
333
+ })
334
+ }
@@ -0,0 +1,235 @@
1
+ /*!
2
+ * © 2025 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
+ ManuscriptEditorState,
19
+ ManuscriptEditorView,
20
+ } from '@manuscripts/transform'
21
+ import { EditorView } from 'prosemirror-view'
22
+ import { Selection } from 'prosemirror-state'
23
+
24
+ import { Dispatch } from '../commands'
25
+ import { findParentNodeWithIdValue } from './utils'
26
+
27
+ /**
28
+ * TODO: Reuse this utility in other areas related to navigation implemented in LEAN-5090
29
+ * (e.g., inspector components, toolbar and other components with similar navigation patterns)
30
+ */
31
+ export function focusNextElement(
32
+ elements: Element[] | HTMLElement[],
33
+ currentIndex: number,
34
+ direction: 'forward' | 'backward'
35
+ ): void {
36
+ const nextIndex =
37
+ direction === 'forward'
38
+ ? (currentIndex + 1) % elements.length
39
+ : (currentIndex - 1 + elements.length) % elements.length
40
+ const nextElement = elements[nextIndex] as HTMLElement
41
+ nextElement?.focus()
42
+ }
43
+
44
+ /**
45
+ * Creates a keydown handler that triggers an action when Enter key is pressed.
46
+ * Automatically prevents default behavior.
47
+ */
48
+ export function handleEnterKey(action: (event: KeyboardEvent) => void) {
49
+ return (event: KeyboardEvent) => {
50
+ if (event.key === 'Enter') {
51
+ event.preventDefault()
52
+ action(event)
53
+ }
54
+ }
55
+ }
56
+
57
+ // Handles arrow key navigation between elements.
58
+ export function handleArrowNavigation(
59
+ event: KeyboardEvent,
60
+ elements: HTMLElement[],
61
+ currentElement: HTMLElement,
62
+ keys: {
63
+ forward: 'ArrowDown' | 'ArrowRight'
64
+ backward: 'ArrowUp' | 'ArrowLeft'
65
+ }
66
+ ): void {
67
+ const { forward, backward } = keys
68
+ if (event.key !== forward && event.key !== backward) {
69
+ return
70
+ }
71
+
72
+ event.preventDefault()
73
+ if (elements.length === 0) {
74
+ return
75
+ }
76
+ const currentIndex = elements.indexOf(currentElement)
77
+ if (currentIndex === -1) {
78
+ // Focus first element when current element not found
79
+ elements[0]?.focus()
80
+ return
81
+ }
82
+
83
+ const direction = event.key === forward ? 'forward' : 'backward'
84
+ focusNextElement(elements, currentIndex, direction)
85
+ }
86
+
87
+ type ArrowKey = 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight'
88
+ type SupportedKey = ArrowKey | 'Enter' | 'Escape' | 'Tab'
89
+
90
+ type KeyHandlers = Partial<Record<SupportedKey, (event: KeyboardEvent) => void>>
91
+
92
+ type NavigationConfig = {
93
+ /** Array of focusable items within the container */
94
+ getItems: () => HTMLElement[]
95
+ /** Arrow key configuration for navigation */
96
+ arrowKeys: {
97
+ forward: 'ArrowDown' | 'ArrowRight'
98
+ backward: 'ArrowUp' | 'ArrowLeft'
99
+ }
100
+ /** Optional override for the current focused element */
101
+ getCurrentElement?: (event: KeyboardEvent) => HTMLElement | null
102
+ }
103
+
104
+ export type KeyboardInteractionOptions = {
105
+ /** The container element (menu, dropdown, list, etc.) or window/document */
106
+ container: HTMLElement | Window | Document
107
+ /** Optional keyboard navigation capability (within items) */
108
+ navigation?: NavigationConfig
109
+ /** Optional custom key handlers */
110
+ additionalKeys?: KeyHandlers
111
+ }
112
+
113
+ export function createKeyboardInteraction(
114
+ options: KeyboardInteractionOptions
115
+ ): () => void {
116
+ const { container, additionalKeys, navigation } = options
117
+
118
+ const handleKeydown: EventListener = (event) => {
119
+ const e = event as KeyboardEvent
120
+
121
+ const key = e.key as SupportedKey
122
+ const handler = additionalKeys?.[key]
123
+
124
+ if (handler) {
125
+ e.preventDefault()
126
+ handler(e)
127
+ return
128
+ }
129
+
130
+ // Handle navigation
131
+ if (!navigation) {
132
+ return
133
+ }
134
+ const { getItems, arrowKeys, getCurrentElement } = navigation
135
+
136
+ const currentElement = getCurrentElement
137
+ ? getCurrentElement(e)
138
+ : (e.target as HTMLElement)
139
+
140
+ if (!currentElement) {
141
+ return
142
+ }
143
+
144
+ const list = getItems()
145
+ if (!list.length) {
146
+ return
147
+ }
148
+ handleArrowNavigation(e, list, currentElement, arrowKeys)
149
+ }
150
+
151
+ container.addEventListener('keydown', handleKeydown)
152
+
153
+ return () => {
154
+ container.removeEventListener('keydown', handleKeydown)
155
+ }
156
+ }
157
+
158
+ export const focusNearestElement = (
159
+ state: ManuscriptEditorState,
160
+ dispatch?: Dispatch,
161
+ view?: ManuscriptEditorView
162
+ ) => {
163
+ if (!view) {
164
+ return false
165
+ }
166
+ // Only intercept when editor text is the current focus context
167
+ const active = document.activeElement
168
+ if (!active || !active.classList.contains('manuscript-editor')) {
169
+ return false // let browser handle Tab
170
+ }
171
+
172
+ const { from } = view.state.selection
173
+ const coords = view.coordsAtPos(from)
174
+ const container = getCursorContainer(view)
175
+ let target = findNearestTabbable(container, coords.top)
176
+ // TODO: Consider a more generic solution based on hierarchy-walking to find the nearest tabbable element.
177
+ // Currently only looks one level up, which is sufficient to resolve focus issue when inside supplement item children.
178
+ if (!target) {
179
+ const parent = findParentNodeWithIdValue(view.state.selection)
180
+
181
+ const grandparent = parent
182
+ ? findParentNodeWithIdValue({
183
+ $from: view.state.doc.resolve(parent.pos),
184
+ } as Selection)
185
+ : null
186
+ if (grandparent) {
187
+ const dom = view.nodeDOM(grandparent.pos)
188
+
189
+ if (dom instanceof HTMLElement) {
190
+ target = findNearestTabbable(dom, coords.top)
191
+ }
192
+ }
193
+ }
194
+
195
+ if (!target) {
196
+ return false
197
+ }
198
+ target.focus()
199
+ return true
200
+ }
201
+
202
+ export function getCursorContainer(view: EditorView): HTMLElement {
203
+ const scoped = findParentNodeWithIdValue(view.state.selection)
204
+ if (scoped) {
205
+ const dom = view.nodeDOM(scoped.pos)
206
+ if (dom instanceof HTMLElement) {
207
+ return dom
208
+ }
209
+ }
210
+
211
+ return view.dom as HTMLElement
212
+ }
213
+
214
+ export function findNearestTabbable(
215
+ container: HTMLElement,
216
+ verticalPosition: number
217
+ ): HTMLElement | null {
218
+ const tabbables = container.querySelectorAll<HTMLElement>(
219
+ 'a[href], button, [tabindex]:not([tabindex="-1"])'
220
+ )
221
+
222
+ let target: HTMLElement | null = null
223
+ let minDistance: number | null = null
224
+
225
+ tabbables.forEach((el) => {
226
+ const rect = el.getBoundingClientRect()
227
+ const distance = Math.abs(rect.top - verticalPosition)
228
+ if (minDistance === null || distance < minDistance) {
229
+ minDistance = distance
230
+ target = el
231
+ }
232
+ })
233
+
234
+ return target
235
+ }
@@ -0,0 +1,70 @@
1
+ /*!
2
+ * © 2025 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 { generateNodeID, Nodes, schema } from '@manuscripts/transform'
18
+
19
+ import { AffiliationAttrs, ContributorAttrs } from './authors'
20
+
21
+ const trim = (value: string | undefined) => (value ?? '').trim()
22
+
23
+ export const normalizeAffiliation = (
24
+ a: AffiliationAttrs
25
+ ): AffiliationAttrs => ({
26
+ ...a,
27
+ institution: trim(a.institution),
28
+ department: trim(a.department),
29
+ addressLine1: trim(a.addressLine1),
30
+ addressLine2: trim(a.addressLine2),
31
+ addressLine3: trim(a.addressLine3),
32
+ postCode: trim(a.postCode),
33
+ country: trim(a.country),
34
+ county: trim(a.county),
35
+ city: trim(a.city),
36
+ email: a.email ?? { href: '', text: '' },
37
+ })
38
+
39
+ export const normalizeAuthor = (author: ContributorAttrs) => {
40
+ const basic: ContributorAttrs = {
41
+ id: author.id,
42
+ role: trim(author.role),
43
+ affiliationIDs: (author.affiliationIDs || []).sort(),
44
+ given: trim(author.given),
45
+ family: trim(author.family),
46
+ email: trim(author.email),
47
+ isCorresponding: author.isCorresponding || false,
48
+ ORCID: trim(author.ORCID),
49
+ isAuthenticated: author.isAuthenticated || false,
50
+ priority: author.priority,
51
+ isJointContributor: author.isJointContributor || false,
52
+ degrees: Array.isArray(author.degrees) ? author.degrees : [],
53
+ footnoteIDs: author.footnoteIDs || [],
54
+ correspIDs: author.correspIDs || [],
55
+ prefix: trim(author.prefix),
56
+ suffix: trim(author.suffix),
57
+ creditRoles: Array.isArray(author.creditRoles) ? author.creditRoles : [],
58
+ }
59
+
60
+ return basic
61
+ }
62
+
63
+ type WithID = { id: string | undefined }
64
+
65
+ export const checkID = <T extends WithID>(attrs: T, nodeType: Nodes) => {
66
+ return {
67
+ ...attrs,
68
+ id: attrs.id || generateNodeID(schema.nodes[nodeType]),
69
+ }
70
+ }