@manuscripts/body-editor 3.12.67 → 3.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,54 @@
1
+ /*!
2
+ * © 2024 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 { Form, useFormikContext } from 'formik'
17
+ import React, { MutableRefObject, PropsWithChildren, useEffect } from 'react'
18
+ import styled from 'styled-components'
19
+
20
+ export interface ChangeHandlingFormProps<Values>
21
+ extends Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onChange'> {
22
+ onChange: (values: Values) => void
23
+ id?: string
24
+ formRef?: MutableRefObject<HTMLFormElement | null>
25
+ }
26
+
27
+ export const ChangeHandlingForm = <Values,>(
28
+ props: PropsWithChildren<ChangeHandlingFormProps<Values>>
29
+ ) => {
30
+ const { values } = useFormikContext<Values>()
31
+ const { onChange, id, formRef, children, ...formProps } = props
32
+
33
+ useEffect(() => {
34
+ // you might modify this to fit your use case
35
+ // like adding a `const prevValues = usePrevious(values)` above and checking for equality
36
+ // or debounce this whole function but make sure not to use stale values
37
+ onChange?.(values)
38
+ // eslint-disable-next-line react-hooks/exhaustive-deps
39
+ }, [onChange, values])
40
+
41
+ return (
42
+ <FlexForm id={id} ref={formRef} {...formProps}>
43
+ {children}
44
+ </FlexForm>
45
+ )
46
+ }
47
+
48
+ export const FlexForm = styled(Form)`
49
+ display: flex;
50
+ flex-direction: column;
51
+ min-height: 0;
52
+ width: 100%;
53
+ box-sizing: border-box;
54
+ `
@@ -0,0 +1,343 @@
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
+ DropdownContainer,
19
+ DropdownList,
20
+ TickIcon,
21
+ TriangleCollapsedIcon,
22
+ } from '@manuscripts/style-guide'
23
+ import React, { useEffect, useRef, useState } from 'react'
24
+ import styled from 'styled-components'
25
+
26
+ import { getLanguage, getLanguageLabel, Language } from '../../lib/languages'
27
+
28
+ interface LanguageDropdownProps {
29
+ onLanguageSelect: (languageCode: string) => void
30
+ onClose?: () => void
31
+ currentLanguage?: string
32
+ showButton?: boolean
33
+ buttonLabel?: string
34
+ selectedLanguageDisplay?: string
35
+ onCloseParent?: () => void
36
+ languages: Language[]
37
+ menuItemRef?: (el: HTMLDivElement | null) => void
38
+ }
39
+
40
+ const LanguageOptionItem: React.FC<{
41
+ language: Language
42
+ isSelected: boolean
43
+ onSelect: (
44
+ event: React.MouseEvent | React.KeyboardEvent,
45
+ languageCode: string
46
+ ) => void
47
+ }> = ({ language, isSelected, onSelect }) => {
48
+ const handleKeyDown = (event: React.KeyboardEvent) => {
49
+ if (event.key === 'Enter') {
50
+ event.preventDefault()
51
+ onSelect(event, language.code)
52
+ }
53
+ }
54
+
55
+ return (
56
+ <StyledLanguageOption
57
+ key={language.code}
58
+ onClick={(event) => onSelect(event, language.code)}
59
+ onKeyDown={handleKeyDown}
60
+ tabIndex={0}
61
+ role="submenuitem"
62
+ >
63
+ {language.name}
64
+ {language.nativeName && ` (${language.nativeName})`}
65
+ {isSelected && (
66
+ <TickIconWrapper>
67
+ <TickIcon />
68
+ </TickIconWrapper>
69
+ )}
70
+ </StyledLanguageOption>
71
+ )
72
+ }
73
+
74
+ const LanguageDropdown: React.FC<LanguageDropdownProps> = ({
75
+ onLanguageSelect,
76
+ onClose,
77
+ currentLanguage = 'en',
78
+ showButton = false,
79
+ selectedLanguageDisplay,
80
+ onCloseParent,
81
+ languages,
82
+ menuItemRef,
83
+ }) => {
84
+ const [isOpen, setIsOpen] = useState(!showButton)
85
+ const dropdownRef = useRef<HTMLDivElement>(null)
86
+ const dropdownMenuRef = useRef<HTMLDivElement>(null)
87
+ const languageButtonRef = useRef<HTMLDivElement | null>(null)
88
+
89
+ // Close dropdown when clicking outside
90
+ useEffect(() => {
91
+ const handleClickOutside = (event: MouseEvent) => {
92
+ if (
93
+ dropdownRef.current &&
94
+ !dropdownRef.current.contains(event.target as Element)
95
+ ) {
96
+ setIsOpen(false)
97
+ onClose?.()
98
+ onCloseParent?.()
99
+ }
100
+ }
101
+
102
+ if (isOpen) {
103
+ document.addEventListener('mousedown', handleClickOutside)
104
+ }
105
+
106
+ return () => {
107
+ document.removeEventListener('mousedown', handleClickOutside)
108
+ }
109
+ }, [isOpen, onClose, onCloseParent])
110
+
111
+ // Focus first option when dropdown opens
112
+ useEffect(() => {
113
+ if (isOpen && dropdownMenuRef.current) {
114
+ const firstOption = dropdownMenuRef.current.querySelector<HTMLElement>(
115
+ '[role="submenuitem"]'
116
+ )
117
+ firstOption?.focus()
118
+ }
119
+ }, [isOpen])
120
+
121
+ const toggleDropdown = (event: React.MouseEvent | React.KeyboardEvent) => {
122
+ event.stopPropagation()
123
+ setIsOpen(!isOpen)
124
+ }
125
+
126
+ const handleKeyDown = (event: React.KeyboardEvent) => {
127
+ if (event.key === 'Enter' || event.key === 'ArrowRight') {
128
+ event.preventDefault()
129
+ toggleDropdown(event)
130
+ }
131
+ }
132
+
133
+ const handleMenuKeyDown = (event: React.KeyboardEvent) => {
134
+ if (!dropdownMenuRef.current) return
135
+
136
+ const menuItems = Array.from(
137
+ dropdownMenuRef.current.querySelectorAll<HTMLElement>(
138
+ '[role="submenuitem"]'
139
+ )
140
+ )
141
+
142
+ if (menuItems.length === 0) return
143
+
144
+ const currentIndex = menuItems.findIndex(
145
+ (item) => item === document.activeElement
146
+ )
147
+
148
+ if (event.key === 'ArrowDown') {
149
+ event.preventDefault()
150
+ const nextIndex = (currentIndex + 1) % menuItems.length
151
+ menuItems[nextIndex]?.focus()
152
+ } else if (event.key === 'ArrowUp') {
153
+ event.preventDefault()
154
+ const prevIndex =
155
+ currentIndex <= 0 ? menuItems.length - 1 : currentIndex - 1
156
+ menuItems[prevIndex]?.focus()
157
+ } else if (event.key === 'Escape' || event.key === 'ArrowLeft') {
158
+ event.preventDefault()
159
+ setIsOpen(false)
160
+ setTimeout(() => languageButtonRef.current?.focus(), 0)
161
+ }
162
+ }
163
+
164
+ const handleSelect = (
165
+ event: React.MouseEvent | React.KeyboardEvent,
166
+ languageCode: string
167
+ ) => {
168
+ event.stopPropagation()
169
+ onLanguageSelect(languageCode)
170
+ }
171
+
172
+ const getDisplayName = (languageCode: string) => {
173
+ const lang = getLanguage(languageCode, languages)
174
+ return getLanguageLabel(lang)
175
+ }
176
+
177
+ return (
178
+ <DropdownContainer ref={dropdownRef}>
179
+ {showButton && (
180
+ <LanguageButton
181
+ ref={(el) => {
182
+ languageButtonRef.current = el
183
+ menuItemRef?.(el)
184
+ }}
185
+ onClick={toggleDropdown}
186
+ onKeyDown={handleKeyDown}
187
+ tabIndex={0}
188
+ role="menuitem"
189
+ >
190
+ <ButtonContent>
191
+ <ButtonLabel>
192
+ Document language <TriangleCollapsedIcon />
193
+ </ButtonLabel>
194
+ <SelectedLanguage>
195
+ {selectedLanguageDisplay || getDisplayName(currentLanguage)}
196
+ </SelectedLanguage>
197
+ </ButtonContent>
198
+ </LanguageButton>
199
+ )}
200
+
201
+ {isOpen && (
202
+ <DropdownMenu
203
+ ref={dropdownMenuRef}
204
+ direction="right"
205
+ width={231}
206
+ height={400}
207
+ top={18}
208
+ onKeyDown={handleMenuKeyDown}
209
+ role="menu"
210
+ >
211
+ {!showButton && <DropdownTitle>Choose language</DropdownTitle>}
212
+ {languages.map((language) => (
213
+ <LanguageOptionItem
214
+ key={language.code}
215
+ language={language}
216
+ isSelected={currentLanguage === language.code}
217
+ onSelect={handleSelect}
218
+ />
219
+ ))}
220
+ </DropdownMenu>
221
+ )}
222
+ </DropdownContainer>
223
+ )
224
+ }
225
+
226
+ const StyledDropdownList = styled(DropdownList)`
227
+ overflow-y: auto;
228
+ overflow-x: hidden;
229
+ border-radius: 8px;
230
+
231
+ /* Custom scrollbar styling */
232
+ &::-webkit-scrollbar {
233
+ width: 6px;
234
+ }
235
+
236
+ &::-webkit-scrollbar-track {
237
+ background: ${(props) => props.theme.colors.background.secondary};
238
+ border-radius: 3px;
239
+ }
240
+
241
+ &::-webkit-scrollbar-thumb {
242
+ background: #6e6e6e;
243
+ border-radius: 3px;
244
+ }
245
+
246
+ &::-webkit-scrollbar-thumb:hover {
247
+ background: #6e6e6e;
248
+ }
249
+ `
250
+
251
+ const LanguageButton = styled.div`
252
+ display: flex;
253
+ align-items: center;
254
+ justify-content: space-between;
255
+ font-family: ${(props) => props.theme.font.family.Lato};
256
+ cursor: pointer;
257
+ font-size: 14px;
258
+ line-height: 24px;
259
+ color: ${(props) => props.theme.colors.text.primary};
260
+ padding: 10px 16px;
261
+ width: 100%;
262
+
263
+ &:hover {
264
+ background: ${(props) => props.theme.colors.background.fifth};
265
+ }
266
+
267
+ &:focus-visible {
268
+ outline: 2px solid ${(props) => props.theme.colors.outline.focus};
269
+ outline-offset: -2px;
270
+ background: ${(props) => props.theme.colors.background.fifth};
271
+ }
272
+ `
273
+
274
+ const ButtonContent = styled.span`
275
+ display: flex;
276
+ flex-direction: column;
277
+ align-items: flex-start;
278
+ `
279
+
280
+ const ButtonLabel = styled.span`
281
+ display: flex;
282
+ align-items: center;
283
+ font-size: 16px;
284
+
285
+ svg {
286
+ top: 10px;
287
+ right: -10px;
288
+ position: relative;
289
+ }
290
+ `
291
+
292
+ const SelectedLanguage = styled.span`
293
+ font-size: 12px;
294
+ color: ${(props) => props.theme.colors.text.secondary};
295
+ margin-top: 2px;
296
+ `
297
+
298
+ const DropdownMenu = styled(StyledDropdownList)`
299
+ // Inherits all styles from StyledDropdownList
300
+ `
301
+
302
+ const DropdownTitle = styled.div`
303
+ font-family: 'PT Sans', sans-serif;
304
+ font-size: 16px;
305
+ font-style: italic;
306
+ font-weight: 400;
307
+ line-height: 32px;
308
+ color: #c9c9c9;
309
+ padding: 12px 16px 8px 16px;
310
+ border-bottom: 1px solid #c9c9c9;
311
+ margin-bottom: 4px;
312
+ `
313
+
314
+ const TickIconWrapper = styled.div`
315
+ svg path {
316
+ fill: #6e6e6e;
317
+ }
318
+ `
319
+
320
+ const StyledLanguageOption = styled.div`
321
+ display: flex;
322
+ align-items: center;
323
+ justify-content: space-between;
324
+ font-family: ${(props) => props.theme.font.family.Lato};
325
+ cursor: pointer;
326
+ font-size: 14px;
327
+ line-height: 24px;
328
+ color: ${(props) => props.theme.colors.text.primary};
329
+ padding: 10px 16px;
330
+ transition: background-color 0.2s ease;
331
+
332
+ &:hover {
333
+ background-color: #f2fbfc;
334
+ }
335
+
336
+ &:focus-visible {
337
+ outline: 2px solid ${(props) => props.theme.colors.outline.focus};
338
+ outline-offset: -2px;
339
+ background: ${(props) => props.theme.colors.background.fifth};
340
+ }
341
+ `
342
+
343
+ export default LanguageDropdown
@@ -0,0 +1,238 @@
1
+ /*!
2
+ * © 2024 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
+ FormGroup,
18
+ FormRow,
19
+ InputErrorText,
20
+ RequiredIndicator,
21
+ SelectField,
22
+ TextField,
23
+ } from '@manuscripts/style-guide'
24
+ import {
25
+ Field,
26
+ FieldProps,
27
+ FormikErrors,
28
+ FormikProps,
29
+ FormikProvider,
30
+ getIn,
31
+ useFormik,
32
+ } from 'formik'
33
+ import React, { MutableRefObject, useEffect } from 'react'
34
+ import styled from 'styled-components'
35
+
36
+ import { COUNTRY_SELECT_OPTIONS } from '@manuscripts/style-guide'
37
+ import { AffiliationAttrs } from '../../lib/authors'
38
+ import { normalizeAffiliation } from '../../lib/normalize'
39
+ import { ChangeHandlingForm } from '../ChangeHandlingForm'
40
+ import { UnsavedLabel } from '../form/UnsavedLabel'
41
+ import { isInstitutionError } from '../hooks/useAffiliationShowsErrorIndicator'
42
+
43
+ function isAffiliationFieldChanged(
44
+ formik: FormikProps<AffiliationAttrs>,
45
+ key: keyof AffiliationAttrs | string
46
+ ) {
47
+ const v = normalizeAffiliation(formik.values)
48
+ const i = normalizeAffiliation(formik.initialValues)
49
+ return getIn(v, key) !== getIn(i, key)
50
+ }
51
+
52
+ export { affiliationShowsErrorIndicator } from '../hooks/useAffiliationShowsErrorIndicator'
53
+
54
+ export interface FormActions {
55
+ reset: () => void
56
+ submitForm: () => Promise<void> | void
57
+ }
58
+
59
+ export interface AffiliationFormProps {
60
+ values: AffiliationAttrs
61
+ onSave: (values: AffiliationAttrs) => void
62
+ onChange: (values: AffiliationAttrs) => void
63
+ actionsRef?: MutableRefObject<FormActions | undefined>
64
+ newEntity?: boolean
65
+ onAffiliationErrorChange?: (hasError: boolean) => void
66
+ unsavedContinueActive?: boolean
67
+ }
68
+
69
+ export const AffiliationForm: React.FC<AffiliationFormProps> = ({
70
+ values,
71
+ onSave,
72
+ onChange,
73
+ actionsRef,
74
+ newEntity = false,
75
+ onAffiliationErrorChange,
76
+ unsavedContinueActive = false,
77
+ }) => {
78
+ const validateAffiliation = (vals: AffiliationAttrs) => {
79
+ const errors: FormikErrors<AffiliationAttrs> = {}
80
+ if (!vals.institution?.trim()) {
81
+ errors.institution = 'Institution Name is required'
82
+ }
83
+ return errors
84
+ }
85
+
86
+ const formik = useFormik<AffiliationAttrs>({
87
+ initialValues: values,
88
+ onSubmit: (attrs) => onSave(attrs),
89
+ enableReinitialize: true,
90
+ validate: validateAffiliation,
91
+ validateOnChange: true,
92
+ })
93
+
94
+ if (actionsRef) {
95
+ actionsRef.current = {
96
+ reset: () => formik.resetForm(),
97
+ submitForm: () => formik.submitForm(),
98
+ }
99
+ }
100
+
101
+ const showInstitutionError = isInstitutionError(formik, newEntity)
102
+
103
+ useEffect(() => {
104
+ onAffiliationErrorChange?.(showInstitutionError)
105
+ }, [showInstitutionError, onAffiliationErrorChange])
106
+
107
+ const showUnsavedDot = (key: keyof AffiliationAttrs | string) =>
108
+ unsavedContinueActive && isAffiliationFieldChanged(formik, key)
109
+
110
+ return (
111
+ <FormikProvider value={formik}>
112
+ <ChangeHandlingForm onChange={onChange} id="affiliation-form" noValidate>
113
+ <FormRow>
114
+ <Field name="institution">
115
+ {(props: FieldProps) => (
116
+ <>
117
+ <UnsavedLabel
118
+ htmlFor="institution"
119
+ showDot={showUnsavedDot('institution')}
120
+ >
121
+ Institution Name
122
+ <RequiredIndicator>*</RequiredIndicator>
123
+ </UnsavedLabel>
124
+ <TextField
125
+ id="institution"
126
+ {...props.field}
127
+ error={showInstitutionError}
128
+ />
129
+ {showInstitutionError && (
130
+ <InputErrorText>
131
+ {formik.errors.institution as string}
132
+ </InputErrorText>
133
+ )}
134
+ </>
135
+ )}
136
+ </Field>
137
+ </FormRow>
138
+ <FormRow>
139
+ <Field name="department" type="textarea">
140
+ {(props: FieldProps) => (
141
+ <>
142
+ <UnsavedLabel
143
+ htmlFor="department"
144
+ showDot={showUnsavedDot('department')}
145
+ >
146
+ Department
147
+ </UnsavedLabel>
148
+ <TextField id="department" {...props.field} />
149
+ </>
150
+ )}
151
+ </Field>
152
+ </FormRow>
153
+ <FormRow>
154
+ <Field name="addressLine1">
155
+ {(props: FieldProps) => (
156
+ <>
157
+ <UnsavedLabel
158
+ htmlFor="addressLine1"
159
+ showDot={showUnsavedDot('addressLine1')}
160
+ >
161
+ Street Address
162
+ </UnsavedLabel>
163
+ <TextField id="addressLine1" {...props.field} />
164
+ </>
165
+ )}
166
+ </Field>
167
+ </FormRow>
168
+ <StyledFormGroup>
169
+ <FormRow>
170
+ <UnsavedLabel htmlFor="country" showDot={showUnsavedDot('country')}>
171
+ Country
172
+ </UnsavedLabel>
173
+ <Field
174
+ id="country"
175
+ name="country"
176
+ component={SelectField}
177
+ options={COUNTRY_SELECT_OPTIONS}
178
+ isSearchable={true}
179
+ variant="large"
180
+ listMaxHeight="140px"
181
+ />
182
+ </FormRow>
183
+ <FormRow>
184
+ <Field name="city">
185
+ {(props: FieldProps) => (
186
+ <>
187
+ <UnsavedLabel htmlFor="city" showDot={showUnsavedDot('city')}>
188
+ City
189
+ </UnsavedLabel>
190
+ <TextField id="city" {...props.field} />
191
+ </>
192
+ )}
193
+ </Field>
194
+ </FormRow>
195
+ </StyledFormGroup>
196
+ <StyledFormGroup>
197
+ <FormRow>
198
+ <Field name="county">
199
+ {(props: FieldProps) => (
200
+ <>
201
+ <UnsavedLabel
202
+ htmlFor="county"
203
+ showDot={showUnsavedDot('county')}
204
+ >
205
+ State / Province
206
+ </UnsavedLabel>
207
+ <TextField id="county" {...props.field} />
208
+ </>
209
+ )}
210
+ </Field>
211
+ </FormRow>
212
+ <FormRow>
213
+ <Field name="postCode">
214
+ {(props: FieldProps) => (
215
+ <>
216
+ <UnsavedLabel
217
+ htmlFor="postCode"
218
+ showDot={showUnsavedDot('postCode')}
219
+ >
220
+ Postal Code
221
+ </UnsavedLabel>
222
+ <TextField id="postCode" {...props.field} />
223
+ </>
224
+ )}
225
+ </Field>
226
+ </FormRow>
227
+ </StyledFormGroup>
228
+ </ChangeHandlingForm>
229
+ </FormikProvider>
230
+ )
231
+ }
232
+
233
+ const StyledFormGroup = styled(FormGroup)`
234
+ > div {
235
+ flex: 1;
236
+ min-width: 0;
237
+ }
238
+ `