@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,102 @@
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 { ManuscriptEditorView, schema } from '@manuscripts/transform'
18
+ import { Plugin, TextSelection } from 'prosemirror-state'
19
+ import { Decoration, DecorationSet } from 'prosemirror-view'
20
+ import { v4 as uuidv4 } from 'uuid'
21
+
22
+ import { EditorProps } from '../configs/ManuscriptsEditor'
23
+ import { addAuthorIcon } from '../icons'
24
+ import { handleEnterKey } from '../lib/navigation-utils'
25
+ import { findInsertionPosition } from '../lib/utils'
26
+
27
+ const createAddSubtitleButton = (handler: () => void) => {
28
+ const button = document.createElement('span')
29
+ button.className = 'add-subtitle'
30
+ button.innerHTML = `${addAuthorIcon} <span class="add-subtitle-text">Add subtitle</span>`
31
+
32
+ button.tabIndex = 0
33
+ const activate = (event: MouseEvent | KeyboardEvent) => {
34
+ event.preventDefault()
35
+ handler()
36
+ }
37
+ button.addEventListener('mousedown', (e) => {
38
+ activate(e)
39
+ })
40
+ button.addEventListener('keydown', handleEnterKey(activate))
41
+
42
+ return button
43
+ }
44
+
45
+ export default (props: EditorProps) =>
46
+ new Plugin<null>({
47
+ props: {
48
+ decorations: (state) => {
49
+ const can = props.getCapabilities()
50
+ if (!can.editArticle) {
51
+ return DecorationSet.empty
52
+ }
53
+
54
+ let titleHasContent = false
55
+ let titlePos = -1
56
+ let hasSubtitles = false
57
+
58
+ state.doc.descendants((node, pos) => {
59
+ if (node.type === schema.nodes.title && node.textContent.trim()) {
60
+ titleHasContent = true
61
+ titlePos = pos
62
+ } else if (node.type === schema.nodes.subtitles) {
63
+ hasSubtitles = true
64
+ return false // Stop early if subtitles found
65
+ }
66
+ })
67
+
68
+ // Show button only if title has content and no subtitles exist
69
+ if (titleHasContent && !hasSubtitles) {
70
+ const titleNode = state.doc.nodeAt(titlePos)
71
+ if (titleNode) {
72
+ const titleEndPos = titlePos + titleNode.nodeSize
73
+
74
+ return DecorationSet.create(state.doc, [
75
+ Decoration.widget(titleEndPos, (view: ManuscriptEditorView) => {
76
+ return createAddSubtitleButton(() => {
77
+ const subtitlesNode = schema.nodes.subtitles.create(
78
+ { id: uuidv4() },
79
+ [schema.nodes.subtitle.create({ id: uuidv4() })]
80
+ )
81
+ const pos = findInsertionPosition(
82
+ schema.nodes.subtitles,
83
+ state.doc
84
+ )
85
+ const tr = view.state.tr.insert(pos, subtitlesNode)
86
+ const subtitlePos = pos + 1
87
+ tr.setSelection(TextSelection.create(tr.doc, subtitlePos))
88
+
89
+ view.dispatch(tr)
90
+ view.focus()
91
+ })
92
+ },
93
+ { side: -1 }
94
+ ),
95
+ ])
96
+ }
97
+ }
98
+
99
+ return DecorationSet.empty
100
+ },
101
+ },
102
+ })
@@ -0,0 +1,136 @@
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
+ /**
18
+ * This plugin handles indexing of affiliations that are supposed to be index in accordance with their order in the document.
19
+ * It also provides that information to components that display that indexing. Additionally it deletes affiliations that have been detached and not used anymore.
20
+ */
21
+ import { isDeleted } from '@manuscripts/track-changes-plugin'
22
+ import {
23
+ isAffiliationNode,
24
+ isContributorNode,
25
+ ManuscriptNode,
26
+ } from '@manuscripts/transform'
27
+ import { isEqual } from 'lodash'
28
+ import { Plugin, PluginKey } from 'prosemirror-state'
29
+ import { Decoration, DecorationSet } from 'prosemirror-view'
30
+
31
+ import {
32
+ AffiliationAttrs,
33
+ authorComparator,
34
+ ContributorAttrs,
35
+ } from '../lib/authors'
36
+
37
+ export interface PluginState {
38
+ version: string
39
+ indexedAffiliationIds: Map<string, number> // key is affiliation id
40
+ contributors: ContributorAttrs[]
41
+ affiliations: AffiliationAttrs[]
42
+ decorations: DecorationSet
43
+ }
44
+
45
+ export const affiliationsKey = new PluginKey<PluginState>('affiliations')
46
+
47
+ let id = 1
48
+ export const buildPluginState = (
49
+ doc: ManuscriptNode,
50
+ $old?: PluginState
51
+ ): PluginState => {
52
+ const nodes: [ManuscriptNode, number][] = []
53
+ const contributors: ContributorAttrs[] = []
54
+ const affiliations: AffiliationAttrs[] = []
55
+ const deletedContribId = new Set<string>()
56
+
57
+ doc.descendants((node, pos) => {
58
+ const attrs = node.attrs
59
+ if (isAffiliationNode(node)) {
60
+ nodes.push([node, pos])
61
+ affiliations.push(attrs as AffiliationAttrs)
62
+ }
63
+ if (isContributorNode(node)) {
64
+ isDeleted(node) && deletedContribId.add(attrs.id)
65
+ nodes.push([node, pos])
66
+ contributors.push(attrs as ContributorAttrs)
67
+ }
68
+ })
69
+
70
+ if (
71
+ $old &&
72
+ isEqual(contributors, $old.contributors) &&
73
+ isEqual(affiliations, $old.affiliations)
74
+ ) {
75
+ return $old
76
+ }
77
+
78
+ // Only index affiliations that are assigned to active contributors
79
+ const iAffiliations = new Set<string>()
80
+ contributors
81
+ .filter((contrib) => !deletedContribId.has(contrib.id))
82
+ .sort(authorComparator)
83
+ .forEach((attrs) => {
84
+ attrs.affiliationIDs?.forEach((aff) => {
85
+ iAffiliations.add(aff)
86
+ })
87
+ })
88
+
89
+ // Create index map only for affiliated affiliations
90
+ const indexedAffiliationIds = new Map<string, number>(
91
+ [...iAffiliations].map((id, i) => [id, i + 1])
92
+ )
93
+
94
+ const version = String(id++)
95
+ const decorations: Decoration[] = []
96
+ nodes.forEach(([node, pos]) => {
97
+ decorations.push(
98
+ Decoration.node(pos, pos + node.nodeSize, {
99
+ version,
100
+ })
101
+ )
102
+ })
103
+
104
+ return {
105
+ version,
106
+ indexedAffiliationIds,
107
+ contributors,
108
+ affiliations,
109
+ decorations: DecorationSet.create(doc, decorations),
110
+ }
111
+ }
112
+
113
+ export default () => {
114
+ return new Plugin<PluginState>({
115
+ key: affiliationsKey,
116
+
117
+ state: {
118
+ init(config, instance): PluginState {
119
+ return buildPluginState(instance.doc)
120
+ },
121
+
122
+ apply(tr, value, oldState, newState): PluginState {
123
+ const $old = affiliationsKey.getState(oldState)
124
+ if (!tr.docChanged && $old) {
125
+ return $old
126
+ } else {
127
+ return buildPluginState(newState.doc, $old)
128
+ }
129
+ },
130
+ },
131
+
132
+ props: {
133
+ decorations: (state) => affiliationsKey.getState(state)?.decorations,
134
+ },
135
+ })
136
+ }
@@ -0,0 +1,219 @@
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
+ import { skipTracking } from '@manuscripts/track-changes-plugin'
17
+ import {
18
+ AltTitleNode,
19
+ AltTitlesSectionNode,
20
+ ManuscriptEditorView,
21
+ schema,
22
+ SubtitlesNode,
23
+ TitleNode,
24
+ } from '@manuscripts/transform'
25
+ import { Node as ProseMirrorNode } from 'prosemirror-model'
26
+ import { Plugin, PluginKey } from 'prosemirror-state'
27
+ import { Decoration, DecorationSet } from 'prosemirror-view'
28
+
29
+ import { createToggleButton, findInsertionPosition } from '../lib/utils'
30
+
31
+ export interface PluginState {
32
+ collapsed: boolean
33
+ title: [TitleNode, number] | undefined
34
+ subtitles: [SubtitlesNode, number] | undefined
35
+ runningTitle: [AltTitleNode, number] | undefined
36
+ shortTitle: [AltTitleNode, number] | undefined
37
+ altTitlesSection: [AltTitlesSectionNode, number] | undefined
38
+ }
39
+
40
+ function getTitlesData(doc: ProseMirrorNode) {
41
+ let title: [TitleNode, number] | undefined
42
+ let subtitles: [SubtitlesNode, number] | undefined
43
+ let runningTitle: [AltTitleNode, number] | undefined
44
+ let shortTitle: [AltTitleNode, number] | undefined
45
+ let altTitlesSection: [AltTitlesSectionNode, number] | undefined
46
+
47
+ doc.descendants((node, pos) => {
48
+ if (title && runningTitle && shortTitle && altTitlesSection) {
49
+ return false
50
+ }
51
+ if (node.type === schema.nodes.title) {
52
+ // if title is empty we don't allow to edit alt titles
53
+ title = [node as TitleNode, pos]
54
+ }
55
+ if (node.type === schema.nodes.subtitles) {
56
+ subtitles = [node as SubtitlesNode, pos]
57
+ }
58
+ if (node.type === schema.nodes.alt_titles) {
59
+ altTitlesSection = [node as AltTitlesSectionNode, pos]
60
+ }
61
+ if (node.type === schema.nodes.alt_title) {
62
+ if (node.attrs.type === 'running') {
63
+ runningTitle = [node as AltTitleNode, pos]
64
+ }
65
+ if (node.attrs.type === 'short') {
66
+ shortTitle = [node as AltTitleNode, pos]
67
+ }
68
+ }
69
+ })
70
+ return { title, subtitles, runningTitle, shortTitle, altTitlesSection }
71
+ }
72
+
73
+ function selectionInAltTitles(from: number, to: number, state: PluginState) {
74
+ if (state.runningTitle && state.shortTitle) {
75
+ const range = {
76
+ from: Math.min(state.runningTitle[1], state.shortTitle[1]),
77
+ to: Math.max(
78
+ state.runningTitle[1] + state.runningTitle[0].nodeSize,
79
+ state.shortTitle[1] + state.shortTitle[0].nodeSize
80
+ ),
81
+ }
82
+ // if selection overlaps with the range of alt titles, the selection is in the titles
83
+ return Math.max(from, range.from) <= Math.min(to, range.to)
84
+ }
85
+ return false
86
+ }
87
+
88
+ export const altTitlesKey = new PluginKey<PluginState>('altTitles')
89
+
90
+ export default () => {
91
+ return new Plugin<PluginState>({
92
+ key: altTitlesKey,
93
+ state: {
94
+ init: (_, state) => {
95
+ return { collapsed: true, ...getTitlesData(state.doc) }
96
+ },
97
+ apply: (tr, value) => {
98
+ let newState = value
99
+ if (tr.docChanged) {
100
+ newState = { ...newState, ...getTitlesData(tr.doc) }
101
+ }
102
+ if (
103
+ tr.selectionSet &&
104
+ selectionInAltTitles(tr.selection.from, tr.selection.to, newState)
105
+ ) {
106
+ // if selection was set in the titles - open them
107
+ newState = { ...newState, collapsed: false }
108
+ }
109
+ if (tr.getMeta(altTitlesKey)) {
110
+ newState = {
111
+ ...newState,
112
+ ...tr.getMeta(altTitlesKey),
113
+ ...getTitlesData(tr.doc),
114
+ }
115
+ }
116
+ return newState
117
+ },
118
+ },
119
+ appendTransaction: (transactions, _, newState) => {
120
+ // in appendTransaction we check if alt_titles nodes exist before opening them for the first time because they are optional
121
+ const tr = newState.tr
122
+ if (
123
+ !transactions.some((tr) => tr.getMeta(altTitlesKey)) ||
124
+ !altTitlesKey.getState(newState)
125
+ ) {
126
+ return null
127
+ }
128
+
129
+ const { title, runningTitle, shortTitle, altTitlesSection } =
130
+ //eslint-disable-next-line @typescript-eslint/no-non-null-assertion
131
+ altTitlesKey.getState(newState)!
132
+
133
+ const schema = newState.schema
134
+
135
+ if (!title) {
136
+ return null
137
+ }
138
+
139
+ if (!altTitlesSection) {
140
+ const pos = findInsertionPosition(schema.nodes.alt_titles, tr.doc)
141
+ const section = schema.nodes.alt_titles.create({}, [
142
+ schema.nodes.alt_title.create({
143
+ type: 'running',
144
+ }),
145
+ schema.nodes.alt_title.create({ type: 'short' }),
146
+ ])
147
+ tr.insert(pos, section)
148
+ } else {
149
+ const endPos = altTitlesSection[1] + altTitlesSection[0].nodeSize - 1
150
+ if (!runningTitle) {
151
+ const title = schema.nodes.alt_title.create({
152
+ type: 'running',
153
+ })
154
+ tr.insert(endPos, title)
155
+ }
156
+ if (!shortTitle) {
157
+ const title = schema.nodes.alt_title.create({ type: 'short' })
158
+ const newPos = tr.mapping.map(endPos)
159
+ tr.insert(newPos, title)
160
+ }
161
+ }
162
+ if (tr.docChanged) {
163
+ return tr.setMeta(altTitlesKey, 'created-missing-titles')
164
+ } else {
165
+ return null
166
+ }
167
+ },
168
+ props: {
169
+ decorations: (state) => {
170
+ const decorations: Decoration[] = []
171
+ const pState = altTitlesKey.getState(state)
172
+ if (!pState || !pState.title) {
173
+ return DecorationSet.empty
174
+ }
175
+
176
+ if (!pState.collapsed) {
177
+ state.doc.descendants((node, pos) => {
178
+ if (node.type === state.schema.nodes.alt_titles) {
179
+ decorations.push(
180
+ Decoration.node(pos, pos + node.nodeSize, {
181
+ class: 'alt-titles-section-open',
182
+ })
183
+ )
184
+ }
185
+ })
186
+ } else if (pState.title[0].textContent.length) {
187
+ // showing opening button only when titles are collapsed
188
+ const titleEnd = pState.title[0].nodeSize + pState.title[1]
189
+ decorations.push(
190
+ Decoration.widget(
191
+ titleEnd - 1,
192
+ (view: ManuscriptEditorView) => {
193
+ return createToggleButton((e) => {
194
+ const tr = view.state.tr.setMeta(altTitlesKey, {
195
+ collapsed: false,
196
+ })
197
+ view.dispatch(skipTracking(tr))
198
+ // Only auto-focus first alt title on keyboard interaction, not mouse clicks
199
+ const isKeyboardEvent = e.type === 'keydown'
200
+ if (isKeyboardEvent) {
201
+ const altTitle = view.dom.querySelector(
202
+ '.alt-title-text'
203
+ ) as HTMLElement
204
+ altTitle?.focus()
205
+ }
206
+ }, 'alternative titles')
207
+ },
208
+ {
209
+ side: -1,
210
+ key: 'title-' + titleEnd,
211
+ }
212
+ )
213
+ )
214
+ }
215
+ return DecorationSet.create(state.doc, decorations)
216
+ },
217
+ },
218
+ })
219
+ }
@@ -0,0 +1,235 @@
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
+ BibliographyItemAttrs,
19
+ buildCiteprocCitation,
20
+ CitationNode,
21
+ isBibliographyElementNode,
22
+ isBibliographyItemNode,
23
+ isCitationNode,
24
+ ManuscriptNode,
25
+ } from '@manuscripts/transform'
26
+ import * as Citeproc from 'citeproc'
27
+ import { isEqual } from 'lodash'
28
+ import { EditorState, Plugin, PluginKey } from 'prosemirror-state'
29
+ import { Decoration, DecorationSet } from 'prosemirror-view'
30
+
31
+ import { CSLProps } from '../configs/ManuscriptsEditor'
32
+ import { PopperManager } from '../lib/popper'
33
+
34
+ export const bibliographyKey = new PluginKey<PluginState>('bibliography')
35
+
36
+ export interface PluginState {
37
+ version: string
38
+ citationNodes: [CitationNode, number][]
39
+ citations: Citeproc.Citation[]
40
+ bibliographyItems: Map<string, BibliographyItemAttrs>
41
+ renderedCitations: Map<string, string>
42
+ citationCounts: Map<string, number>
43
+ engine: Citeproc.Engine
44
+ }
45
+
46
+ export interface BibliographyProps {
47
+ cslProps: CSLProps
48
+ popper: PopperManager
49
+ }
50
+
51
+ /**
52
+ * This plugin generates labels for inline citations using citeproc-js.
53
+ * The citation labels are regenerated when any relevant content changes.
54
+ */
55
+ export default (props: BibliographyProps) => {
56
+ return new Plugin<PluginState>({
57
+ key: bibliographyKey,
58
+ state: {
59
+ init(config, instance): PluginState {
60
+ return buildBibliographyPluginState(instance.doc, props.cslProps)
61
+ },
62
+ apply(tr, value): PluginState {
63
+ if (!tr.steps.length) {
64
+ return value
65
+ }
66
+ return buildBibliographyPluginState(tr.doc, props.cslProps, value)
67
+ },
68
+ },
69
+ props: {
70
+ decorations(state) {
71
+ const bib = getBibliographyPluginState(state)
72
+
73
+ return DecorationSet.create(
74
+ state.doc,
75
+ bib ? buildDecorations(bib, state.doc) : []
76
+ )
77
+ },
78
+ },
79
+ })
80
+ }
81
+
82
+ let version = 1
83
+ const buildBibliographyPluginState = (
84
+ doc: ManuscriptNode,
85
+ csl: CSLProps,
86
+ $old?: PluginState
87
+ ): PluginState => {
88
+ const nodesMap = new Map<string, [CitationNode, number]>()
89
+ const bibliographyItems = new Map<string, BibliographyItemAttrs>()
90
+ doc.descendants((node, pos) => {
91
+ if (isCitationNode(node)) {
92
+ nodesMap.set(node.attrs.id, [node, pos])
93
+ }
94
+ if (isBibliographyItemNode(node)) {
95
+ bibliographyItems.set(node.attrs.id, node.attrs)
96
+ }
97
+ })
98
+
99
+ const nodes = Array.from(nodesMap.values())
100
+ const citations = nodes.map(([node]) => buildCiteprocCitation(node.attrs))
101
+
102
+ const $new: Partial<PluginState> = {
103
+ citationNodes: nodes,
104
+ citations,
105
+ bibliographyItems,
106
+ }
107
+
108
+ //TODO remove. There should always be a csl style
109
+ if (!csl.style) {
110
+ return $new as PluginState
111
+ }
112
+
113
+ if (
114
+ $old &&
115
+ isEqual(citations, $old.citations) &&
116
+ isEqual(bibliographyItems, $old.bibliographyItems)
117
+ ) {
118
+ $new.version = $old.version
119
+ $new.citationCounts = $old.citationCounts
120
+ $new.engine = $old.engine
121
+ $new.renderedCitations = $old.renderedCitations
122
+ } else {
123
+ const citationCounts = new Map()
124
+ const rids = nodes.flatMap((e) => e[0].attrs.rids)
125
+ rids.forEach((rid) => {
126
+ const count = citationCounts.get(rid) || 0
127
+ citationCounts.set(rid, count + 1)
128
+ })
129
+
130
+ const engine = new Citeproc.Engine(
131
+ {
132
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
133
+ retrieveLocale: () => csl.locale!,
134
+ retrieveItem: (id: string) => {
135
+ const item = bibliographyItems.get(id)
136
+ if (!item) {
137
+ throw Error(`Missing bibliography item with id ${id}`)
138
+ }
139
+ return item as CSL.Data
140
+ },
141
+ },
142
+ csl.style,
143
+ 'en-US'
144
+ )
145
+
146
+ //create new citations since citeproc modifies the ones passed
147
+ const citationTexts = engine.rebuildProcessorState(
148
+ nodes.map(([node]) => buildCiteprocCitation(node.attrs))
149
+ )
150
+
151
+ $new.version = String(version++)
152
+ $new.citationCounts = citationCounts
153
+ $new.engine = engine
154
+ $new.renderedCitations = new Map(citationTexts.map((i) => [i[0], i[2]]))
155
+ }
156
+
157
+ return $new as PluginState
158
+ }
159
+
160
+ export const buildDecorations = (state: PluginState, doc: ManuscriptNode) => {
161
+ const decorations: Decoration[] = []
162
+
163
+ let hasMissingItems = false
164
+
165
+ for (const [node, pos] of state.citationNodes) {
166
+ const rids = node.attrs.rids
167
+ if (rids.length) {
168
+ for (const rid of rids) {
169
+ if (!state.bibliographyItems.has(rid)) {
170
+ decorations.push(
171
+ Decoration.node(pos, pos + node.nodeSize, {
172
+ class: 'citation-missing',
173
+ })
174
+ )
175
+
176
+ hasMissingItems = true
177
+ }
178
+ }
179
+ } else {
180
+ decorations.push(
181
+ Decoration.node(pos, pos + node.nodeSize, {
182
+ class: 'citation-empty',
183
+ })
184
+ )
185
+ }
186
+ decorations.push(
187
+ Decoration.node(pos, pos + node.nodeSize, {
188
+ version: state.version,
189
+ })
190
+ )
191
+ }
192
+
193
+ if (hasMissingItems) {
194
+ doc.descendants((node, pos) => {
195
+ if (isBibliographyElementNode(node)) {
196
+ decorations.push(
197
+ Decoration.node(pos, pos + node.nodeSize, {
198
+ missing: 'true',
199
+ })
200
+ )
201
+
202
+ decorations.push(
203
+ Decoration.widget(pos, () => {
204
+ const el = document.createElement('div')
205
+ el.className = 'bibliography-missing'
206
+ el.textContent = `The bibliography could not be generated, due to a missing library item.`
207
+ return el
208
+ })
209
+ )
210
+ }
211
+ })
212
+ }
213
+
214
+ /**
215
+ * Use decorations to trigger bibliography element update.
216
+ * This is a way to communicate from the plugin to the bibliography element node without actually changing the node.
217
+ * We had to do that due to the absence of an actual node change.
218
+ * @TODO Look for a neater solution (using non-trackable attributes on bibliography_element is a proposed solution)
219
+ */
220
+ doc.descendants((node, pos) => {
221
+ if (isBibliographyElementNode(node)) {
222
+ decorations.push(
223
+ Decoration.node(pos, pos + node.nodeSize, {
224
+ version: state.version,
225
+ })
226
+ )
227
+ }
228
+ })
229
+
230
+ return decorations
231
+ }
232
+
233
+ export const getBibliographyPluginState = (state: EditorState) => {
234
+ return bibliographyKey.getState(state)
235
+ }