@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,2109 @@
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 { isDeleted, skipTracking, skipSelect } from '@manuscripts/track-changes-plugin'
18
+ import {
19
+ AttachmentNode,
20
+ AwardNode,
21
+ BoxElementNode,
22
+ FigureElementNode,
23
+ FigureNode,
24
+ FootnotesElementNode,
25
+ generateNodeID,
26
+ GraphicalAbstractSectionNode,
27
+ isElementNodeType,
28
+ isListNode,
29
+ isSectionNodeType,
30
+ isTableElementNode,
31
+ KeywordsNode,
32
+ ListNode,
33
+ ManuscriptEditorState,
34
+ ManuscriptEditorView,
35
+ ManuscriptMarkType,
36
+ ManuscriptNode,
37
+ ManuscriptNodeSelection,
38
+ ManuscriptNodeType,
39
+ ManuscriptResolvedPos,
40
+ ManuscriptTextSelection,
41
+ ManuscriptTransaction,
42
+ schema,
43
+ SectionCategory,
44
+ SectionNode,
45
+ SupplementNode,
46
+ TransGraphicalAbstractNode,
47
+ } from '@manuscripts/transform'
48
+ import {
49
+ Attrs,
50
+ Fragment,
51
+ NodeRange,
52
+ ResolvedPos,
53
+ Slice,
54
+ } from 'prosemirror-model'
55
+ import { wrapInList } from 'prosemirror-schema-list'
56
+ import {
57
+ EditorState,
58
+ NodeSelection,
59
+ Selection,
60
+ TextSelection,
61
+ Transaction,
62
+ } from 'prosemirror-state'
63
+ import {
64
+ addColumnAfter,
65
+ addColumnBefore,
66
+ addRow,
67
+ selectedRect,
68
+ } from 'prosemirror-tables'
69
+ import {
70
+ findWrapping,
71
+ ReplaceAroundStep,
72
+ ReplaceStep,
73
+ } from 'prosemirror-transform'
74
+ import {
75
+ findChildrenByType,
76
+ findParentNodeOfType,
77
+ findParentNodeOfTypeClosestToPos,
78
+ flatten,
79
+ hasParentNodeOfType,
80
+ NodeWithPos,
81
+ } from 'prosemirror-utils'
82
+ import { EditorView } from 'prosemirror-view'
83
+
84
+ import { CommentAttrs, getCommentKey, getCommentRange } from './lib/comments'
85
+ import {
86
+ findAbstractsNode,
87
+ findBackmatter,
88
+ findBibliographySection,
89
+ findBody,
90
+ findFootnotesSection,
91
+ insertAttachmentsNode,
92
+ insertAwardsNode,
93
+ insertFootnotesSection,
94
+ upsertSupplementsSection,
95
+ } from './lib/doc'
96
+ import { FileAttachment } from './lib/files'
97
+ import {
98
+ createFootnote,
99
+ findFootnotesContainerNode,
100
+ getFootnotesElementState,
101
+ } from './lib/footnotes'
102
+ import {
103
+ findWordBoundaries,
104
+ isNodeOfType,
105
+ nearestAncestor,
106
+ } from './lib/helpers'
107
+ import { templateAllows } from './lib/template'
108
+ import {
109
+ findInsertionPosition,
110
+ findParentNodeWithId,
111
+ getChildOfType,
112
+ getInsertPos,
113
+ isBodyLocked,
114
+ } from './lib/utils'
115
+ import { expandAccessibilitySection } from './plugins/accessibility_element'
116
+ import { setCommentSelection } from './plugins/comments'
117
+ import { getEditorProps } from './plugins/editor-props'
118
+ import { searchReplaceKey } from './plugins/search-replace'
119
+ import { checkForCompletion } from './plugins/section_title/autocompletion'
120
+ import { EditorAction } from './types'
121
+ import { persistentCursor } from './plugins/persistent-cursor'
122
+ import { AwardAttrs } from './views/award'
123
+ import { openAuthorsAndAffiliationsModals } from './components/authors-affiliations/AuthorsAndAffiliationsModals'
124
+
125
+ export type Dispatch = (tr: ManuscriptTransaction) => void
126
+
127
+ // enter at the start of paragraph will add node above
128
+ export const addToStart = (
129
+ state: ManuscriptEditorState,
130
+ dispatch?: Dispatch
131
+ ): boolean => {
132
+ const { selection } = state
133
+
134
+ const props = getEditorProps(state)
135
+ if (props.getCapabilities().editWithoutTracking) {
136
+ return false
137
+ }
138
+
139
+ if (
140
+ !dispatch ||
141
+ !(selection instanceof TextSelection) ||
142
+ (selection.$from.node().type !== schema.nodes.paragraph &&
143
+ selection.$from.node().type !== schema.nodes.text_block)
144
+ ) {
145
+ return false
146
+ }
147
+
148
+ const {
149
+ $anchor: { parentOffset: startOffset },
150
+ $head: { parentOffset: endOffset },
151
+ $from,
152
+ $to,
153
+ } = selection
154
+
155
+ // Check if the cursor is at the start of a paragraph or text block.
156
+ // This ensures that a new node is added above the current node only when
157
+ // the cursor is at the very beginning of the parent node.
158
+ if (startOffset === 0 && endOffset === 0) {
159
+ const side =
160
+ (!$from.parentOffset && $to.index() < $to.parent.childCount ? $from : $to)
161
+ .pos - (startOffset === 0 ? 1 : 0)
162
+
163
+ const tr = state.tr
164
+ const from = $from.node().type.createAndFill()
165
+ if (from) {
166
+ tr.insert(side, from)
167
+ dispatch(tr.scrollIntoView())
168
+ return true
169
+ }
170
+ }
171
+ return false
172
+ }
173
+
174
+ export const markActive =
175
+ (type: ManuscriptMarkType) =>
176
+ (state: ManuscriptEditorState): boolean => {
177
+ const { from, $from, to, empty } = state.selection
178
+
179
+ let hasMark = false
180
+
181
+ state.doc.nodesBetween(from, to, (node) => {
182
+ if (node.isText) {
183
+ node.marks.forEach((m) => {
184
+ if (m.type === type && !isDeleted(m)) {
185
+ hasMark = true
186
+ }
187
+ })
188
+ }
189
+ })
190
+
191
+ return empty
192
+ ? Boolean(type.isInSet(state.storedMarks || $from.marks()))
193
+ : state.doc.rangeHasMark(from, to, type) && hasMark
194
+ }
195
+
196
+ export const isNodeSelection = (
197
+ selection: Selection
198
+ ): selection is ManuscriptNodeSelection => selection instanceof NodeSelection
199
+
200
+ export const blockActive =
201
+ (type: ManuscriptNodeType) => (state: ManuscriptEditorState) => {
202
+ const { selection } = state
203
+
204
+ if (isNodeSelection(selection)) {
205
+ return selection.node.type === type
206
+ }
207
+
208
+ const { to, $from } = selection as ManuscriptTextSelection
209
+
210
+ if (to > $from.end()) {
211
+ return false
212
+ }
213
+
214
+ for (let d = $from.depth; d >= 0; d--) {
215
+ const ancestor = $from.node(d)
216
+
217
+ // only look at the closest parent with an id
218
+ if (ancestor.attrs.id) {
219
+ return ancestor.type === type
220
+ }
221
+ }
222
+
223
+ return false
224
+ }
225
+
226
+ export const canInsert =
227
+ (type: ManuscriptNodeType) => (state: ManuscriptEditorState) => {
228
+ const { $from, $to } = state.selection
229
+
230
+ if (!templateAllows(state, type)) {
231
+ return false
232
+ }
233
+
234
+ // disable block comment insertion just for title node, LEAN-2746
235
+ if (
236
+ ($from.node().type === schema.nodes.title ||
237
+ $from.node().type === schema.nodes.section_title) &&
238
+ $from.pos === $to.pos &&
239
+ type !== schema.nodes.text
240
+ ) {
241
+ return false
242
+ }
243
+
244
+ if (
245
+ isElementNodeType(type) &&
246
+ type !== schema.nodes.paragraph &&
247
+ findParentNodeOfType(schema.nodes.backmatter)(state.selection)
248
+ ) {
249
+ return false
250
+ }
251
+
252
+ if (type === schema.nodes.table_element) {
253
+ if (findParentNodeOfType(schema.nodes.list_item)(state.selection)) {
254
+ return false
255
+ }
256
+ }
257
+
258
+ // Prevent inserting a box_element if:
259
+ // it's not inside a body node (invalid context), OR
260
+ // it's already nested inside another box_element (disallowed nesting)
261
+ if (
262
+ type === schema.nodes.box_element &&
263
+ (!hasParentNodeOfType(schema.nodes.body)(state.selection) ||
264
+ hasParentNodeOfType(schema.nodes.box_element)(state.selection))
265
+ ) {
266
+ return false
267
+ }
268
+ // Prevent inserting a hero_image if it already exists
269
+ if (
270
+ type === schema.nodes.hero_image &&
271
+ getChildOfType(state.doc, schema.nodes.hero_image, true)
272
+ ) {
273
+ return false
274
+ }
275
+
276
+ const initDepth =
277
+ findParentNodeOfType(schema.nodes.box_element)(state.selection)?.depth ||
278
+ 0
279
+
280
+ for (let d = $from.depth; d >= initDepth; d--) {
281
+ const index = $from.index(d)
282
+
283
+ if ($from.node(d).canReplaceWith(index, index, type)) {
284
+ return true
285
+ }
286
+ }
287
+
288
+ return false
289
+ }
290
+
291
+ const findBlockInsertPosition = (state: ManuscriptEditorState) => {
292
+ const { $from } = state.selection
293
+
294
+ for (let d = $from.depth; d >= 0; d--) {
295
+ const node = $from.node(d)
296
+
297
+ if (isElementNodeType(node.type)) {
298
+ return $from.after(d)
299
+ }
300
+ }
301
+
302
+ return null
303
+ }
304
+
305
+ export const createSelection = (
306
+ nodeType: ManuscriptNodeType,
307
+ position: number,
308
+ doc: ManuscriptNode
309
+ ) => {
310
+ const { nodes } = nodeType.schema
311
+
312
+ switch (nodeType) {
313
+ case nodes.figure_element:
314
+ // select the figure caption
315
+ return TextSelection.near(doc.resolve(position + 5), 1)
316
+
317
+ case nodes.listing_element:
318
+ // select the listing
319
+ return NodeSelection.create(doc, position + 1)
320
+
321
+ default:
322
+ return nodeType.isAtom
323
+ ? NodeSelection.create(doc, position)
324
+ : TextSelection.near(doc.resolve(position + 1))
325
+ }
326
+ }
327
+
328
+ export const createBlock = (
329
+ nodeType: ManuscriptNodeType,
330
+ position: number,
331
+ state: ManuscriptEditorState,
332
+ dispatch?: Dispatch,
333
+ attrs?: Attrs
334
+ ) => {
335
+ let node: ManuscriptNode
336
+ switch (nodeType) {
337
+ case schema.nodes.table_element:
338
+ node = createAndFillTableElement(attrs)
339
+ break
340
+ case schema.nodes.figure_element:
341
+ node = createAndFillFigureElement(attrs)
342
+ break
343
+ case schema.nodes.image_element:
344
+ node = createImageElement(attrs)
345
+ break
346
+ case schema.nodes.hero_image:
347
+ node = createHeroImage(attrs)
348
+ break
349
+ case schema.nodes.listing_element:
350
+ node = schema.nodes.listing_element.create({}, [
351
+ schema.nodes.listing.create(),
352
+ ...createAndFillCaption(),
353
+ ])
354
+ break
355
+ case schema.nodes.equation_element:
356
+ node = schema.nodes.equation_element.create({}, [
357
+ schema.nodes.equation.create(),
358
+ ])
359
+ break
360
+ case schema.nodes.embed:
361
+ node = createEmbedElement(attrs)
362
+ break
363
+ case state.schema.nodes.pullquote_element:
364
+ node = state.schema.nodes.pullquote_element.create(attrs, [
365
+ state.schema.nodes.text_block.create({}),
366
+ state.schema.nodes.attribution.create({}),
367
+ ])
368
+ break
369
+ default:
370
+ node = nodeType.createAndFill(attrs) as ManuscriptNode
371
+ }
372
+
373
+ const tr = state.tr.insert(position, node)
374
+
375
+ if (dispatch) {
376
+ const selection = createSelection(nodeType, position, tr.doc)
377
+ expandAccessibilitySection(tr, node)
378
+ dispatch(tr.setSelection(selection).scrollIntoView())
379
+ }
380
+ }
381
+
382
+ export const insertInlineTableFootnote = (
383
+ state: ManuscriptEditorState,
384
+ dispatch?: Dispatch
385
+ ) => {
386
+ const $pos = state.selection.$to
387
+ const table = findParentNodeOfTypeClosestToPos($pos, schema.nodes.table)
388
+ if (!table) {
389
+ return false
390
+ }
391
+ if (!dispatch) {
392
+ return true
393
+ }
394
+ return insertInlineFootnote(state, dispatch)
395
+ }
396
+
397
+ export const insertGeneralTableFootnote = (
398
+ element: [ManuscriptNode, number],
399
+ state: ManuscriptEditorState,
400
+ dispatch?: Dispatch
401
+ ) => {
402
+ const existing = findChildrenByType(
403
+ element[0],
404
+ schema.nodes.general_table_footnote
405
+ )
406
+ if (existing.length) {
407
+ return false
408
+ }
409
+ if (!dispatch) {
410
+ return true
411
+ }
412
+
413
+ const tr = state.tr
414
+ const footer = insertTableElementFooter(tr, element)
415
+
416
+ const pos = footer.pos + 2
417
+ const node = schema.nodes.general_table_footnote.create({}, [
418
+ schema.nodes.paragraph.create(),
419
+ ])
420
+ tr.insert(pos, node)
421
+ const selection = TextSelection.create(tr.doc, pos + 1)
422
+ tr.setSelection(selection).scrollIntoView()
423
+ dispatch(tr)
424
+ }
425
+
426
+ export const insertFigure = (
427
+ file: FileAttachment,
428
+ state: ManuscriptEditorState,
429
+ dispatch?: Dispatch
430
+ ) => {
431
+ const position = findBlockInsertPosition(state)
432
+ if (position === null || !dispatch) {
433
+ return false
434
+ }
435
+ const figure = state.schema.nodes.figure.createAndFill({
436
+ label: file.name,
437
+ src: file.id,
438
+ }) as FigureNode
439
+
440
+ const element = state.schema.nodes.figure_element.createAndFill({}, [
441
+ figure,
442
+ schema.nodes.caption.create(undefined, schema.nodes.text_block.create()),
443
+ ]) as FigureElementNode
444
+ const tr = state.tr.insert(position, element)
445
+ dispatch(tr)
446
+ return true
447
+ }
448
+
449
+ export const insertEmbed = (
450
+ state: ManuscriptEditorState,
451
+ dispatch?: Dispatch,
452
+ attrs?: Attrs
453
+ ) => {
454
+ const position = findBlockInsertPosition(state)
455
+ if (position === null) {
456
+ return false
457
+ }
458
+
459
+ const embed = schema.nodes.embed.create(
460
+ {
461
+ ...attrs,
462
+ id: generateNodeID(schema.nodes.embed),
463
+ },
464
+ [
465
+ ...createAndFillCaption(),
466
+ schema.nodes.alt_text.create(),
467
+ schema.nodes.long_desc.create(),
468
+ ]
469
+ )
470
+
471
+ const tr = state.tr.insert(position, embed)
472
+ dispatch && dispatch(tr)
473
+
474
+ return true
475
+ }
476
+
477
+ export const insertTable = (
478
+ config: TableConfig,
479
+ state: ManuscriptEditorState,
480
+ dispatch?: Dispatch
481
+ ) => {
482
+ const pos = findBlockInsertPosition(state)
483
+ if (!pos) {
484
+ return false
485
+ }
486
+ const node = createAndFillTableElement(undefined, config)
487
+ const tr = state.tr.insert(pos, node)
488
+ expandAccessibilitySection(tr, node)
489
+ tr.setSelection(NodeSelection.create(tr.doc, pos)).scrollIntoView()
490
+ dispatch && dispatch(tr)
491
+ return true
492
+ }
493
+
494
+ export const insertSupplement = (
495
+ file: FileAttachment,
496
+ view: ManuscriptEditorView,
497
+ setSelection = true
498
+ ) => {
499
+ const supplement = schema.nodes.supplement.createAndFill(
500
+ {
501
+ id: generateNodeID(schema.nodes.supplement),
502
+ href: file.id,
503
+ },
504
+ createAndFillCaption()
505
+ ) as SupplementNode
506
+
507
+ let tr = view.state.tr
508
+ const { pos } = upsertSupplementsSection(tr, supplement)
509
+
510
+ if (setSelection) {
511
+ tr.setSelection(NodeSelection.create(tr.doc, pos))
512
+ } else {
513
+ tr = skipSelect(tr)
514
+ }
515
+ view.focus()
516
+ view.dispatch(tr.scrollIntoView())
517
+ return true
518
+ }
519
+
520
+ export const insertAttachment = (
521
+ file: FileAttachment,
522
+ state: ManuscriptEditorState,
523
+ type: string,
524
+ dispatch?: Dispatch
525
+ ) => {
526
+ const tr = state.tr
527
+ const attachments = insertAttachmentsNode(tr)
528
+ if (!attachments) {
529
+ return false
530
+ }
531
+
532
+ if (attachments.node.childCount > 0) {
533
+ const startPos = attachments.pos + 1
534
+ const endPos = attachments.pos + attachments.node.nodeSize - 1
535
+ tr.delete(startPos, endPos)
536
+ }
537
+
538
+ const attachment = schema.nodes.attachment.createAndFill({
539
+ id: generateNodeID(schema.nodes.attachment),
540
+ href: file.id,
541
+ type: type,
542
+ }) as AttachmentNode
543
+
544
+ tr.insert(attachments.pos + 1, attachment)
545
+
546
+ if (dispatch) {
547
+ dispatch(skipTracking(tr))
548
+ }
549
+ return true
550
+ }
551
+
552
+ export const insertBlock =
553
+ (nodeType: ManuscriptNodeType) =>
554
+ (state: ManuscriptEditorState, dispatch?: Dispatch) => {
555
+ if (!canInsert(nodeType)(state)) {
556
+ return false
557
+ }
558
+ const position = findBlockInsertPosition(state)
559
+ if (position === null) {
560
+ return false
561
+ }
562
+
563
+ createBlock(nodeType, position, state, dispatch, undefined)
564
+
565
+ return true
566
+ }
567
+
568
+ export const deleteBlock =
569
+ (typeToDelete: string) =>
570
+ (state: ManuscriptEditorState, dispatch?: Dispatch) => {
571
+ const { selection, tr } = state
572
+ const { $head } = selection
573
+ const depth = nearestAncestor(isNodeOfType(typeToDelete))($head)
574
+
575
+ if (!depth) {
576
+ return false
577
+ }
578
+
579
+ if (dispatch) {
580
+ const start = $head.start(depth)
581
+ const end = $head.end(depth)
582
+ tr.delete(start - 1, end + 1)
583
+ dispatch(tr)
584
+ }
585
+
586
+ return true
587
+ }
588
+
589
+ export const insertBreak: EditorAction = (state, dispatch) => {
590
+ const br = state.schema.nodes.hard_break.create()
591
+
592
+ const tr = state.tr.replaceSelectionWith(br)
593
+
594
+ if (dispatch) {
595
+ dispatch(tr.scrollIntoView())
596
+ }
597
+
598
+ return true
599
+ }
600
+
601
+ const selectedText = (): string => (window.getSelection() || '').toString()
602
+
603
+ export const findPosBeforeFirstSubsection = (
604
+ $pos: ManuscriptResolvedPos
605
+ ): number | null => {
606
+ let posBeforeFirstSubsection: number | null = null
607
+
608
+ for (let d = $pos.depth; d >= 0; d--) {
609
+ const parentNode = $pos.node(d)
610
+ if (isSectionNodeType(parentNode.type)) {
611
+ const parentStartPos = $pos.start(d) // Get the start position of the parent section
612
+ parentNode.descendants((node, pos, parent) => {
613
+ // Only consider direct children of the section
614
+ if (
615
+ node.type === schema.nodes.section &&
616
+ parent === parentNode &&
617
+ posBeforeFirstSubsection === null
618
+ ) {
619
+ // Found the first subsection, set the position before it
620
+ posBeforeFirstSubsection = parentStartPos + pos
621
+ }
622
+ // Stop descending if we've found the position
623
+ return posBeforeFirstSubsection === null
624
+ })
625
+ break // Stop iterating after finding the parent section
626
+ }
627
+ }
628
+
629
+ return posBeforeFirstSubsection
630
+ }
631
+
632
+ const findPosAfterParentSection = (
633
+ $pos: ManuscriptResolvedPos
634
+ ): number | null => {
635
+ for (let d = $pos.depth; d >= 0; d--) {
636
+ const node = $pos.node(d)
637
+
638
+ if (isSectionNodeType(node.type)) {
639
+ return $pos.after(d)
640
+ }
641
+ }
642
+
643
+ return null
644
+ }
645
+
646
+ const findParentSectionStartPosition = (
647
+ $pos: ManuscriptResolvedPos
648
+ ): number | null => {
649
+ for (let d = $pos.depth; d >= 0; d--) {
650
+ const node = $pos.node(d)
651
+
652
+ if (isSectionNodeType(node.type)) {
653
+ return $pos.start(d)
654
+ }
655
+ }
656
+
657
+ return null
658
+ }
659
+
660
+ export const insertSectionLabel = (
661
+ state: ManuscriptEditorState,
662
+ dispatch?: Dispatch,
663
+ currentTr?: Transaction
664
+ ) => {
665
+ const pos = findParentSectionStartPosition(state.selection.$from)
666
+ if (pos === null) {
667
+ return false
668
+ }
669
+ const node = state.schema.nodes.section_label.create(
670
+ {},
671
+ state.schema.text('Label')
672
+ )
673
+ const tr = (currentTr || state.tr).insert(pos, node)
674
+ if (dispatch) {
675
+ // place cursor inside section title
676
+ // const selection = TextSelection.create(tr.doc, pos + 2)
677
+ dispatch(tr)
678
+ }
679
+
680
+ return true
681
+ }
682
+
683
+ const isLink = (text: string): boolean => /^\s*(https?:\S+)/.test(text)
684
+
685
+ export const insertLink = (
686
+ state: ManuscriptEditorState,
687
+ dispatch?: Dispatch
688
+ ) => {
689
+ const tr = state.tr
690
+ const selection = state.selection
691
+
692
+ if (!selection.empty) {
693
+ const text = selectedText()
694
+ const attrs = {
695
+ href: isLink(text) ? text.trim() : '',
696
+ }
697
+ const range = new NodeRange(
698
+ selection.$from,
699
+ selection.$to,
700
+ selection.$from.depth
701
+ )
702
+ const wrapping = findWrapping(range, schema.nodes.link, attrs)
703
+
704
+ if (wrapping) {
705
+ tr.wrap(range, wrapping)
706
+ }
707
+ } else {
708
+ tr.insert(state.selection.anchor, schema.nodes.link.create())
709
+ }
710
+
711
+ if (dispatch) {
712
+ const selection = NodeSelection.create(tr.doc, state.tr.selection.from)
713
+ dispatch(tr.setSelection(selection).scrollIntoView())
714
+ }
715
+
716
+ return true
717
+ }
718
+
719
+ export const insertInlineCitation = (
720
+ state: ManuscriptEditorState,
721
+ dispatch?: Dispatch
722
+ ) => {
723
+ const node = schema.nodes.citation.create({
724
+ id: generateNodeID(schema.nodes.citation),
725
+ rids: [],
726
+ selectedText: selectedText(),
727
+ })
728
+
729
+ const pos = state.selection.to
730
+
731
+ const { tr } = state
732
+
733
+ tr.insert(pos, node)
734
+
735
+ if (dispatch) {
736
+ const selection = NodeSelection.create(tr.doc, pos)
737
+ dispatch(tr.setSelection(selection).scrollIntoView())
738
+ }
739
+
740
+ return true
741
+ }
742
+
743
+ export const insertCrossReference = (
744
+ state: ManuscriptEditorState,
745
+ dispatch?: Dispatch
746
+ ) => {
747
+ const node = state.schema.nodes.cross_reference.create({
748
+ rids: [],
749
+ })
750
+
751
+ const pos = state.selection.to
752
+
753
+ const tr = state.tr.insert(pos, node)
754
+
755
+ if (dispatch) {
756
+ const selection = NodeSelection.create(tr.doc, pos)
757
+ dispatch(tr.setSelection(selection).scrollIntoView())
758
+ }
759
+
760
+ return true
761
+ }
762
+
763
+ export const insertInlineEquation = (
764
+ state: ManuscriptEditorState,
765
+ dispatch?: Dispatch
766
+ ) => {
767
+ const sourcePos = state.selection.from - 1
768
+
769
+ const tr = state.tr.replaceSelectionWith(
770
+ state.schema.nodes.inline_equation.create({
771
+ format: 'tex',
772
+ contents: selectedText().replace(/^\$/, '').replace(/\$$/, ''),
773
+ })
774
+ )
775
+
776
+ if (dispatch) {
777
+ const selection = NodeSelection.create(
778
+ tr.doc,
779
+ tr.mapping.map(sourcePos) + 1
780
+ )
781
+ dispatch(tr.setSelection(selection).scrollIntoView())
782
+ }
783
+
784
+ return true
785
+ }
786
+
787
+ export const insertTableElementFooter = (
788
+ tr: Transaction,
789
+ table: [ManuscriptNode, number]
790
+ ) => {
791
+ const footer = findChildrenByType(
792
+ table[0],
793
+ schema.nodes.table_element_footer
794
+ )[0]
795
+ if (footer) {
796
+ const pos = tr.mapping.map(table[1] + footer.pos)
797
+ if (isDeleted(footer.node)) {
798
+ reinstateNode(tr, footer.node, pos)
799
+ }
800
+ return {
801
+ node: footer.node,
802
+ pos,
803
+ }
804
+ }
805
+ const pos = tr.mapping.map(
806
+ getInsertPos(schema.nodes.table_element_footer, table[0], table[1])
807
+ )
808
+ const node = schema.nodes.table_element_footer.create()
809
+ tr.insert(pos, node)
810
+ return {
811
+ node,
812
+ pos,
813
+ }
814
+ }
815
+
816
+ export const insertFootnotesElement = (
817
+ tr: Transaction,
818
+ container: [ManuscriptNode, number]
819
+ ) => {
820
+ let pos
821
+ const node = schema.nodes.footnotes_element.create()
822
+ if (isTableElementNode(container[0])) {
823
+ //table footnote
824
+ const footer = insertTableElementFooter(tr, container)
825
+ pos = footer.pos + footer.node.nodeSize - 1
826
+ } else {
827
+ //regular footnote
828
+ const section = insertFootnotesSection(tr)
829
+ pos = section.pos + section.node.nodeSize - 1
830
+ tr.insert(pos, node)
831
+ }
832
+ return [node, pos] as [FootnotesElementNode, number]
833
+ }
834
+
835
+ export const insertInlineFootnote = (
836
+ state: ManuscriptEditorState,
837
+ dispatch?: Dispatch
838
+ ) => {
839
+ const pos = state.selection.to
840
+ const tr = state.tr
841
+ const container = findFootnotesContainerNode(state.doc, pos)
842
+ const fn = getFootnotesElementState(state, container.node.attrs.id)
843
+ const hasFootnotes =
844
+ fn && fn.footnotes.filter((fn) => !isDeleted(fn[0])).length > 0
845
+
846
+ const footnote = !hasFootnotes && createFootnote()
847
+ const node = schema.nodes.inline_footnote.create({
848
+ rids: footnote ? [footnote.attrs.id] : [],
849
+ })
850
+
851
+ tr.insert(pos, node)
852
+
853
+ if (footnote) {
854
+ let element: [FootnotesElementNode, number]
855
+ if (fn) {
856
+ element = [fn.element[0], tr.mapping.map(fn.element[1])]
857
+ } else {
858
+ element = insertFootnotesElement(tr, [container.node, container.pos])
859
+ }
860
+
861
+ if (isDeleted(element[0])) {
862
+ reinstateNode(tr, element[0], element[1])
863
+ }
864
+
865
+ const fnPos = element[1] + element[0].nodeSize - 1
866
+ tr.insert(fnPos, footnote)
867
+ const selection = TextSelection.create(tr.doc, fnPos + 2)
868
+ tr.setSelection(selection).scrollIntoView()
869
+ } else {
870
+ const selection = NodeSelection.create(tr.doc, pos)
871
+ tr.setSelection(selection).scrollIntoView()
872
+ }
873
+
874
+ if (dispatch) {
875
+ dispatch(tr)
876
+ }
877
+ return true
878
+ }
879
+
880
+ const reinstateNode = (tr: Transaction, node: ManuscriptNode, pos: number) => {
881
+ const attrs = {
882
+ ...node.attrs,
883
+ dataTracked: null,
884
+ }
885
+ tr.setNodeMarkup(pos, null, attrs)
886
+ }
887
+
888
+ export const insertBoxElement = (
889
+ state: ManuscriptEditorState,
890
+ dispatch?: Dispatch
891
+ ) => {
892
+ const selection = state.selection
893
+ const { nodes } = schema
894
+
895
+ // Check if the selection is inside the body
896
+ const isBody = hasParentNodeOfType(nodes.body)(selection)
897
+ const isBoxText = hasParentNodeOfType(nodes.box_element)(selection)
898
+ // If selection is not in the body, disable the option
899
+ if (!isBody || isBoxText) {
900
+ return false
901
+ }
902
+
903
+ const position = findBlockInsertPosition(state)
904
+ const paragraph = nodes.paragraph.create({})
905
+
906
+ // Create a section node with a section title and a paragraph
907
+ const section = nodes.section.createAndFill({}, [
908
+ nodes.section_title.create(),
909
+ paragraph,
910
+ ]) as ManuscriptNode
911
+
912
+ // Create the BoxElement node with a caption_title and the section
913
+ const node = nodes.box_element.createAndFill({}, [
914
+ nodes.caption_title.create(),
915
+ section,
916
+ ]) as BoxElementNode
917
+
918
+ if (position && dispatch) {
919
+ const tr = state.tr.insert(position, node)
920
+ const sectionTitlePosition = position + 4
921
+ tr.setSelection(TextSelection.create(tr.doc, sectionTitlePosition))
922
+ dispatch(tr.scrollIntoView())
923
+ }
924
+
925
+ return true
926
+ }
927
+
928
+ export const insertSection =
929
+ (subsection = false) =>
930
+ (state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
931
+ const { nodes } = schema
932
+ const $pos = state.selection.$from
933
+
934
+ const isInBibliography = findParentNodeOfTypeClosestToPos(
935
+ $pos,
936
+ nodes.bibliography_section
937
+ )
938
+ const isInBox = findParentNodeOfTypeClosestToPos($pos, nodes.box_element)
939
+ const isInBody = findParentNodeOfTypeClosestToPos($pos, nodes.body)
940
+ const isInMainTitle = findParentNodeOfTypeClosestToPos($pos, nodes.title)
941
+
942
+ if (isInMainTitle && isBodyLocked(state)) {
943
+ return false
944
+ }
945
+
946
+ // Disallow inserting (sub)sections in bibliography
947
+ if (isInBibliography) {
948
+ return false
949
+ }
950
+ // Only subsections are allowed in box_element
951
+ if (isInBox && !subsection) {
952
+ return false
953
+ }
954
+
955
+ let insertPos: number | null = null
956
+
957
+ if (subsection) {
958
+ insertPos =
959
+ findPosBeforeFirstSubsection($pos) || findPosAfterParentSection($pos)
960
+ if (!insertPos) {
961
+ return false
962
+ }
963
+ // Move position inside the parent section for the subsection
964
+ insertPos -= 1
965
+ } else {
966
+ if (isInBody) {
967
+ insertPos = findPosAfterParentSection($pos)
968
+ }
969
+ }
970
+
971
+ if (insertPos == null) {
972
+ // Insert at the end of the body if:
973
+ // - selection is outside the body
974
+ // - selection is inside the body but not within a section
975
+ const body = findBody(state.doc)
976
+ insertPos = body.pos + body.node.content.size + 1
977
+ }
978
+
979
+ const section = nodes.section.createAndFill() as SectionNode
980
+ const tr = state.tr.insert(insertPos, section)
981
+
982
+ if (dispatch) {
983
+ const selection = TextSelection.create(tr.doc, insertPos + 2) // Place cursor inside section title
984
+ view?.focus()
985
+ dispatch(tr.setSelection(selection).scrollIntoView())
986
+ }
987
+
988
+ return true
989
+ }
990
+
991
+ export const insertAbstractSection =
992
+ (category?: SectionCategory) =>
993
+ (state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
994
+ if (!category) {
995
+ return false
996
+ }
997
+ const abstracts = findAbstractsNode(state.doc)
998
+ const sections = findChildrenByType(abstracts.node, schema.nodes.section)
999
+ // Check if the section already exists
1000
+ if (sections.some((s) => s.node.attrs.category === category.id)) {
1001
+ return false
1002
+ }
1003
+
1004
+ // check if graphical abstract node exist to insert before it.
1005
+ const ga = findChildrenByType(
1006
+ state.doc,
1007
+ schema.nodes.graphical_abstract_section
1008
+ )[0]
1009
+
1010
+ let pos = ga ? ga.pos : abstracts.pos + abstracts.node.content.size + 1
1011
+ if (category.id === 'abstract') {
1012
+ pos = abstracts.pos + 1
1013
+ }
1014
+
1015
+ const node = schema.nodes.section.create({ category: category.id }, [
1016
+ schema.nodes.section_title.create({}, schema.text(category.titles[0])),
1017
+ schema.nodes.paragraph.create({ placeholder: 'Type abstract here...' }),
1018
+ ])
1019
+
1020
+ const tr = state.tr.insert(pos, node)
1021
+ if (dispatch) {
1022
+ // place cursor inside section title
1023
+ const selection = TextSelection.create(tr.doc, pos)
1024
+ view?.focus()
1025
+ dispatch(tr.setSelection(selection).scrollIntoView())
1026
+ }
1027
+
1028
+ return true
1029
+ }
1030
+
1031
+ export const insertBackmatterSection =
1032
+ (category: SectionCategory) =>
1033
+ (state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
1034
+ const backmatter = findBackmatter(state.doc)
1035
+ const sections = findChildrenByType(backmatter.node, schema.nodes.section)
1036
+ // Check if the section already exists
1037
+ if (sections.some((s) => s.node.attrs.category === category.id)) {
1038
+ return false
1039
+ }
1040
+
1041
+ // check if reference node exist to insert before it.
1042
+ const bibliography = findBibliographySection(state.doc)
1043
+
1044
+ // check if footnotes node exist to insert before it.
1045
+ const footnotesSection = findFootnotesSection(state.doc)
1046
+
1047
+ const pos =
1048
+ footnotesSection?.pos ??
1049
+ bibliography?.pos ??
1050
+ backmatter.pos + backmatter.node.content.size + 1
1051
+
1052
+ const attrs = { category: category.id }
1053
+ const node = schema.nodes.section.create(attrs, [
1054
+ schema.nodes.section_title.create({}, schema.text(category.titles[0])),
1055
+ ])
1056
+
1057
+ const tr = state.tr.insert(pos, node)
1058
+ if (dispatch) {
1059
+ // place cursor inside section title
1060
+ const selection = TextSelection.create(tr.doc, pos)
1061
+ view?.focus()
1062
+ dispatch(tr.setSelection(selection).scrollIntoView())
1063
+ }
1064
+
1065
+ return true
1066
+ }
1067
+
1068
+ const findSelectedList = (selection: Selection) =>
1069
+ (selection instanceof NodeSelection &&
1070
+ selection.node.type === schema.nodes.list && {
1071
+ pos: selection.from,
1072
+ node: selection.node,
1073
+ }) ||
1074
+ findParentNodeOfType([schema.nodes.list])(selection)
1075
+
1076
+ export const insertGraphicalAbstract =
1077
+ (category?: SectionCategory) =>
1078
+ (state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
1079
+ if (!category) {
1080
+ return false
1081
+ }
1082
+ const abstracts = findAbstractsNode(state.doc)
1083
+ const sections = findChildrenByType(
1084
+ abstracts.node,
1085
+ schema.nodes.graphical_abstract_section
1086
+ )
1087
+
1088
+ // Check if the section already exists
1089
+ if (sections.some((s) => s.node.attrs.category === category.id)) {
1090
+ return false
1091
+ }
1092
+
1093
+ const ga = findChildrenByType(
1094
+ state.doc,
1095
+ schema.nodes.graphical_abstract_section
1096
+ )[0]
1097
+
1098
+ // insert at the end of abstracts section
1099
+ let pos = abstracts.pos + abstracts.node.content.size + 1
1100
+ // abstract-key-image insert before abstract-graphical
1101
+ pos = ga && category.id === 'abstract-key-image' ? ga.pos : pos
1102
+
1103
+ const node = schema.nodes.graphical_abstract_section.createAndFill(
1104
+ { category: category.id },
1105
+ [
1106
+ schema.nodes.section_title.create({}, schema.text(category.titles[0])),
1107
+ createAndFillFigureElement(state),
1108
+ ]
1109
+ ) as GraphicalAbstractSectionNode
1110
+
1111
+ const tr = state.tr.insert(pos, node)
1112
+ node.lastChild && expandAccessibilitySection(tr, node.lastChild)
1113
+ if (dispatch) {
1114
+ // place cursor inside section title
1115
+ const selection = TextSelection.create(tr.doc, pos + 1)
1116
+ if (view) {
1117
+ view.focus()
1118
+ }
1119
+ dispatch(tr.setSelection(selection).scrollIntoView())
1120
+ }
1121
+ return true
1122
+ }
1123
+
1124
+ export const insertContributors = (
1125
+ state: ManuscriptEditorState,
1126
+ dispatch?: Dispatch,
1127
+ view?: EditorView
1128
+ ) => {
1129
+ const tr = state.tr
1130
+
1131
+ let contributors = findChildrenByType(state.doc, schema.nodes.contributors)[0]
1132
+
1133
+ if (contributors?.node.childCount) {
1134
+ return false
1135
+ }
1136
+ if (!contributors) {
1137
+ const pos = findInsertionPosition(schema.nodes.contributors, state.doc)
1138
+ const contributorsNode = state.schema.nodes.contributors.create({
1139
+ id: generateNodeID(schema.nodes.contributors),
1140
+ })
1141
+ tr.insert(pos, contributorsNode)
1142
+ contributors = { node: contributorsNode, pos }
1143
+ }
1144
+
1145
+ if (dispatch) {
1146
+ const selection = NodeSelection.create(tr.doc, contributors.pos)
1147
+ dispatch(tr.setSelection(selection).scrollIntoView())
1148
+ openAuthorsAndAffiliationsModals(contributors.pos, view, 'authors')
1149
+ }
1150
+ return true
1151
+ }
1152
+
1153
+ export const insertAffiliation = (
1154
+ state: ManuscriptEditorState,
1155
+ dispatch?: Dispatch,
1156
+ view?: EditorView
1157
+ ) => {
1158
+ const tr = state.tr
1159
+
1160
+ let affiliations = findChildrenByType(state.doc, schema.nodes.affiliations)[0]
1161
+
1162
+ if (affiliations?.node.childCount) {
1163
+ return false
1164
+ }
1165
+ if (!affiliations) {
1166
+ const pos = findInsertionPosition(schema.nodes.affiliations, state.doc)
1167
+ const affiliationsNode = state.schema.nodes.affiliations.create({
1168
+ id: generateNodeID(schema.nodes.affiliations),
1169
+ })
1170
+ tr.insert(pos, affiliationsNode)
1171
+ affiliations = { node: affiliationsNode, pos }
1172
+ }
1173
+
1174
+ if (dispatch) {
1175
+ const selection = NodeSelection.create(tr.doc, affiliations.pos)
1176
+ dispatch(tr.setSelection(selection).scrollIntoView())
1177
+ openAuthorsAndAffiliationsModals(affiliations.pos, view, 'affiliations')
1178
+ }
1179
+ return true
1180
+ }
1181
+
1182
+ export const insertAward =
1183
+ (attrs?: AwardAttrs) =>
1184
+ (state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
1185
+ const award = schema.nodes.award.create(attrs) as AwardNode
1186
+ const tr = state.tr
1187
+ const awards = insertAwardsNode(tr)
1188
+ const pos = awards.pos + awards.node.nodeSize - 1
1189
+ tr.insert(pos, award)
1190
+ const selection = NodeSelection.create(tr.doc, pos)
1191
+ if (dispatch && view) {
1192
+ dispatch(tr.setSelection(selection).scrollIntoView())
1193
+ view.focus()
1194
+ }
1195
+ return true
1196
+ }
1197
+
1198
+ export const insertKeywords = (
1199
+ state: ManuscriptEditorState,
1200
+ dispatch?: Dispatch,
1201
+ view?: EditorView
1202
+ ) => {
1203
+ // Check if another keywords node already exists
1204
+ if (getChildOfType(state.doc, schema.nodes.keywords, true)) {
1205
+ return false
1206
+ }
1207
+ const pos = findInsertionPosition(schema.nodes.keywords, state.doc)
1208
+ const keywords = schema.nodes.keywords.createAndFill({}, [
1209
+ schema.nodes.section_title.create({}, schema.text('Keywords')),
1210
+ schema.nodes.keywords_element.create({}, [
1211
+ schema.nodes.keyword_group.create({}, []),
1212
+ ]),
1213
+ ]) as KeywordsNode
1214
+
1215
+ const tr = state.tr.insert(pos, keywords)
1216
+
1217
+ if (dispatch) {
1218
+ const selection = NodeSelection.create(tr.doc, pos)
1219
+ if (view) {
1220
+ view.focus()
1221
+ }
1222
+ dispatch(tr.setSelection(selection).scrollIntoView())
1223
+ }
1224
+
1225
+ return true
1226
+ }
1227
+
1228
+ const findRootList = ($pos: ResolvedPos) => {
1229
+ for (let i = 0; i < $pos.depth; i++) {
1230
+ const node = $pos.node(i)
1231
+ if (isListNode(node)) {
1232
+ const pos = $pos.start(i)
1233
+ return {
1234
+ node,
1235
+ pos,
1236
+ }
1237
+ }
1238
+ }
1239
+ }
1240
+
1241
+ //Somewhat expensive logic, should this be in a plugin?
1242
+ const findListsAtSameLevel = (doc: ManuscriptNode, list: NodeWithPos) => {
1243
+ const $pos = doc.resolve(list.pos + 1)
1244
+ // find the top-level list. This is an optimization to
1245
+ // avoid traversing the entire document looking for lists
1246
+ const root = findRootList($pos)
1247
+ if (!root) {
1248
+ return [list]
1249
+ }
1250
+ const target = $pos.depth
1251
+ const lists: NodeWithPos[] = []
1252
+ root.node.descendants((node, pos) => {
1253
+ const $pos = doc.resolve(root.pos + pos + 1)
1254
+ if ($pos.depth === target && isListNode(node)) {
1255
+ lists.push({ node, pos: $pos.before(target) })
1256
+ }
1257
+ return $pos.depth <= target
1258
+ })
1259
+ return lists
1260
+ }
1261
+
1262
+ function toggleOffList(
1263
+ state: EditorState,
1264
+ dispatch: (tr: ManuscriptTransaction) => void
1265
+ ) {
1266
+ const {
1267
+ selection: { $from },
1268
+ tr,
1269
+ } = state
1270
+
1271
+ let rootList = findRootList($from)
1272
+ rootList && rootList.pos--
1273
+
1274
+ if (
1275
+ state.selection instanceof NodeSelection &&
1276
+ state.selection.node.type === schema.nodes.list
1277
+ ) {
1278
+ rootList = {
1279
+ pos: state.selection.from,
1280
+ node: state.selection.node as ListNode,
1281
+ }
1282
+ }
1283
+
1284
+ if (rootList) {
1285
+ state.doc.nodesBetween(
1286
+ rootList.pos,
1287
+ rootList.pos + rootList.node.nodeSize,
1288
+ (node, pos) => {
1289
+ // remove all the nodes that are not fully in the range
1290
+ if (
1291
+ rootList &&
1292
+ (pos < rootList.pos ||
1293
+ node.nodeSize > rootList.pos + rootList.node.nodeSize)
1294
+ ) {
1295
+ return true
1296
+ }
1297
+ if (node.type === schema.nodes.paragraph) {
1298
+ tr.insert(tr.mapping.map(rootList.pos), node)
1299
+ return false
1300
+ }
1301
+ }
1302
+ )
1303
+ tr.delete(
1304
+ tr.mapping.map(rootList.pos),
1305
+ tr.mapping.map(rootList.pos + rootList.node.nodeSize)
1306
+ )
1307
+ dispatch(tr)
1308
+ return true
1309
+ } else {
1310
+ return false
1311
+ }
1312
+ }
1313
+
1314
+ export const insertList =
1315
+ (type: ManuscriptNodeType, style?: string) =>
1316
+ (state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
1317
+ const list = findSelectedList(state.selection)
1318
+
1319
+ if (list) {
1320
+ if (!dispatch) {
1321
+ return true
1322
+ }
1323
+
1324
+ if (list.node.attrs.listStyleType === style) {
1325
+ return toggleOffList(state, dispatch)
1326
+ }
1327
+
1328
+ // list was found: update the type and style
1329
+ // of every list at the same level
1330
+ const nodes = findListsAtSameLevel(state.doc, list)
1331
+ const tr = state.tr
1332
+ for (const { node, pos } of nodes) {
1333
+ tr.setNodeMarkup(
1334
+ pos,
1335
+ type,
1336
+ {
1337
+ ...node.attrs,
1338
+ listStyleType: style,
1339
+ },
1340
+ node.marks
1341
+ )
1342
+ }
1343
+ dispatch(tr)
1344
+ return true
1345
+ } else {
1346
+ // no list found, create new list
1347
+ const { selection } = state
1348
+ let tr = state.tr
1349
+ const startPosition = selection.$from.pos + 1
1350
+
1351
+ return wrapInList(type, { listStyleType: style })(state, (tempTr) => {
1352
+ // if we dispatch all steps in this transaction track-changes-plugin will not be able to revert ReplaceAroundStep
1353
+ // as we have another ReplaceStep that will make transaction more complicated, so to make it easy to tracker we dispatch first ReplaceAroundStep
1354
+ // then will dispatch reminder steps in one transaction
1355
+ const range = selection.$from.blockRange(selection.$to)
1356
+ if (range && dispatch) {
1357
+ tempTr.steps.map((step) => {
1358
+ if (step instanceof ReplaceAroundStep) {
1359
+ dispatch(tr.step(step))
1360
+ tr = view?.state.tr || tr
1361
+ } else {
1362
+ tr.step(step)
1363
+ }
1364
+ })
1365
+ if (startPosition) {
1366
+ const selection = createSelection(
1367
+ state.schema.nodes.paragraph,
1368
+ startPosition,
1369
+ tr.doc
1370
+ )
1371
+ view?.focus()
1372
+ tr.setSelection(selection)
1373
+ }
1374
+
1375
+ dispatch(tr)
1376
+ }
1377
+ })
1378
+ }
1379
+ }
1380
+
1381
+ export const insertBibliographySection = () => {
1382
+ return false
1383
+ }
1384
+
1385
+ export const insertTOCSection = () => {
1386
+ return false
1387
+ }
1388
+
1389
+ export const insertTransAbstract = (
1390
+ state: ManuscriptEditorState,
1391
+ dispatch?: Dispatch,
1392
+ category?: string,
1393
+ insertAfterPos?: number
1394
+ ) => {
1395
+ if (!templateAllows(state, schema.nodes.trans_abstract)) {
1396
+ return false
1397
+ }
1398
+ if (!dispatch) {
1399
+ return true
1400
+ }
1401
+
1402
+ // Get document's primary language or default to English
1403
+ const lang = state.doc.attrs.primaryLanguageCode || 'en'
1404
+
1405
+ // Create empty section title
1406
+ const title = schema.nodes.section_title.create()
1407
+ // Create empty paragraph
1408
+ const paragraph = schema.nodes.paragraph.create()
1409
+
1410
+ // Create trans_abstract node with section title and paragraph
1411
+ // Pass the current section's category to the trans_abstract
1412
+ const node = schema.nodes.trans_abstract.create(
1413
+ {
1414
+ lang,
1415
+ category,
1416
+ },
1417
+ [title, paragraph]
1418
+ )
1419
+
1420
+ const abstracts = findAbstractsNode(state.doc)
1421
+
1422
+ const pos =
1423
+ insertAfterPos != null
1424
+ ? insertAfterPos
1425
+ : abstracts.pos + abstracts.node.nodeSize - 1
1426
+ const tr = state.tr.insert(pos, node)
1427
+
1428
+ const selection = TextSelection.create(tr.doc, pos + 1)
1429
+ tr.setSelection(selection).scrollIntoView()
1430
+
1431
+ dispatch(tr)
1432
+ return true
1433
+ }
1434
+
1435
+ export const insertTransGraphicalAbstract =
1436
+ (category: SectionCategory, insertAfterPos?: number) =>
1437
+ (state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
1438
+ if (!templateAllows(state, schema.nodes.trans_graphical_abstract)) {
1439
+ return false
1440
+ }
1441
+ if (!dispatch) {
1442
+ return true
1443
+ }
1444
+
1445
+ const lang = state.doc.attrs.primaryLanguageCode || 'en'
1446
+
1447
+ const abstracts = findAbstractsNode(state.doc)
1448
+ const pos =
1449
+ insertAfterPos != null
1450
+ ? insertAfterPos
1451
+ : abstracts.pos + abstracts.node.content.size + 1
1452
+
1453
+ const node = schema.nodes.trans_graphical_abstract.createAndFill(
1454
+ {
1455
+ lang,
1456
+ category: category.id,
1457
+ },
1458
+ [
1459
+ schema.nodes.section_title.create({}, schema.text(category.titles[0])),
1460
+ createAndFillFigureElement(state),
1461
+ ]
1462
+ ) as TransGraphicalAbstractNode
1463
+
1464
+ const tr = state.tr.insert(pos, node)
1465
+ if (node.lastChild) {
1466
+ expandAccessibilitySection(tr, node.lastChild)
1467
+ }
1468
+
1469
+ const selection = TextSelection.create(tr.doc, pos + 1)
1470
+ if (view) {
1471
+ view.focus()
1472
+ }
1473
+ dispatch(tr.setSelection(selection).scrollIntoView())
1474
+ return true
1475
+ }
1476
+
1477
+ // Copied from prosemirror-commands
1478
+ const findCutBefore = ($pos: ResolvedPos) => {
1479
+ if (!$pos.parent.type.spec.isolating) {
1480
+ for (let i = $pos.depth - 1; i >= 0; i--) {
1481
+ if ($pos.index(i) > 0) {
1482
+ return $pos.doc.resolve($pos.before(i + 1))
1483
+ }
1484
+ if ($pos.node(i).type.spec.isolating) {
1485
+ break
1486
+ }
1487
+ }
1488
+ }
1489
+ return null
1490
+ }
1491
+
1492
+ export const isAtStartOfTextBlock = (
1493
+ state: ManuscriptEditorState,
1494
+ $cursor: ResolvedPos,
1495
+ view?: ManuscriptEditorView
1496
+ ) => (view ? view.endOfTextblock('backward', state) : $cursor.parentOffset <= 0)
1497
+
1498
+ export const isTextSelection = (
1499
+ selection: Selection
1500
+ ): selection is ManuscriptTextSelection => selection instanceof TextSelection
1501
+
1502
+ // Ignore atom blocks (as backspace handler), instead of deleting them.
1503
+ // Adapted from selectNodeBackward in prosemirror-commands
1504
+ export const ignoreAtomBlockNodeBackward = (
1505
+ state: ManuscriptEditorState,
1506
+ dispatch?: Dispatch,
1507
+ view?: ManuscriptEditorView
1508
+ ): boolean => {
1509
+ const { selection } = state
1510
+
1511
+ if (!isTextSelection(selection)) {
1512
+ return false
1513
+ }
1514
+
1515
+ const { $cursor } = selection
1516
+
1517
+ if (!$cursor) {
1518
+ return false
1519
+ }
1520
+
1521
+ // ignore empty blocks
1522
+ if ($cursor.parent.content.size === 0) {
1523
+ return false
1524
+ }
1525
+
1526
+ // handle cursor at start of textblock
1527
+ if (!isAtStartOfTextBlock(state, $cursor, view)) {
1528
+ return false
1529
+ }
1530
+
1531
+ const $cut = findCutBefore($cursor)
1532
+
1533
+ if (!$cut) {
1534
+ return false
1535
+ }
1536
+
1537
+ const node = $cut.nodeBefore
1538
+
1539
+ if (!node) {
1540
+ return false
1541
+ }
1542
+
1543
+ return node.isBlock && node.isAtom
1544
+ }
1545
+
1546
+ export const ignoreMetaNodeBackspaceCommand = (
1547
+ state: ManuscriptEditorState
1548
+ ) => {
1549
+ const { selection } = state
1550
+
1551
+ if (!isNodeSelection(selection)) {
1552
+ return false
1553
+ }
1554
+
1555
+ return (
1556
+ selection.node.type === schema.nodes.keyword_group ||
1557
+ selection.node.type === schema.nodes.keyword ||
1558
+ selection.node.type === schema.nodes.affiliations ||
1559
+ selection.node.type === schema.nodes.affiliation ||
1560
+ selection.node.type === schema.nodes.contributors ||
1561
+ selection.node.type === schema.nodes.contributor ||
1562
+ selection.node.type === schema.nodes.awards ||
1563
+ selection.node.type === schema.nodes.award
1564
+ )
1565
+ }
1566
+ // Copied from prosemirror-commands
1567
+ const findCutAfter = ($pos: ResolvedPos) => {
1568
+ if (!$pos.parent.type.spec.isolating) {
1569
+ for (let i = $pos.depth - 1; i >= 0; i--) {
1570
+ const parent = $pos.node(i)
1571
+ if ($pos.index(i) + 1 < parent.childCount) {
1572
+ return $pos.doc.resolve($pos.after(i + 1))
1573
+ }
1574
+ if (parent.type.spec.isolating) {
1575
+ break
1576
+ }
1577
+ }
1578
+ }
1579
+ return null
1580
+ }
1581
+
1582
+ export const isAtEndOfTextBlock = (
1583
+ state: ManuscriptEditorState,
1584
+ $cursor: ResolvedPos,
1585
+ view?: ManuscriptEditorView
1586
+ ) =>
1587
+ view
1588
+ ? view.endOfTextblock('forward', state)
1589
+ : $cursor.parentOffset >= $cursor.parent.content.size
1590
+
1591
+ // Ignore atom blocks (as delete handler), instead of deleting them.
1592
+ // Adapted from selectNodeForward in prosemirror-commands
1593
+ export const ignoreAtomBlockNodeForward = (
1594
+ state: ManuscriptEditorState,
1595
+ dispatch?: Dispatch,
1596
+ view?: ManuscriptEditorView
1597
+ ): boolean => {
1598
+ const { selection } = state
1599
+
1600
+ if (!isTextSelection(selection)) {
1601
+ return false
1602
+ }
1603
+
1604
+ const { $cursor } = selection
1605
+
1606
+ if (!$cursor) {
1607
+ return false
1608
+ }
1609
+
1610
+ // ignore empty blocks
1611
+ if ($cursor.parent.content.size === 0) {
1612
+ return false
1613
+ }
1614
+
1615
+ // handle cursor at start of textblock
1616
+ if (!isAtEndOfTextBlock(state, $cursor, view)) {
1617
+ return false
1618
+ }
1619
+
1620
+ const $cut = findCutAfter($cursor)
1621
+
1622
+ if (!$cut) {
1623
+ return false
1624
+ }
1625
+
1626
+ const node = $cut.nodeAfter
1627
+
1628
+ if (!node) {
1629
+ return false
1630
+ }
1631
+
1632
+ return node.isBlock && node.isAtom
1633
+ }
1634
+
1635
+ const selectIsolatingParent = (
1636
+ state: ManuscriptEditorState
1637
+ ): TextSelection | null => {
1638
+ const { $anchor } = state.selection
1639
+
1640
+ for (let d = $anchor.depth; d >= 0; d--) {
1641
+ const node = $anchor.node(d)
1642
+
1643
+ if (node.type.spec.isolating) {
1644
+ return TextSelection.create(
1645
+ state.tr.doc,
1646
+ $anchor.start(d),
1647
+ $anchor.end(d)
1648
+ )
1649
+ }
1650
+ }
1651
+
1652
+ return null
1653
+ }
1654
+
1655
+ /**
1656
+ * "Select All" the contents of an isolating block instead of the whole document
1657
+ */
1658
+ export const selectAllIsolating = (
1659
+ state: ManuscriptEditorState,
1660
+ dispatch?: Dispatch
1661
+ ): boolean => {
1662
+ const selection = selectIsolatingParent(state)
1663
+
1664
+ if (!selection) {
1665
+ return false
1666
+ }
1667
+
1668
+ if (dispatch) {
1669
+ dispatch(state.tr.setSelection(selection))
1670
+ }
1671
+
1672
+ return true
1673
+ }
1674
+
1675
+ export type TableConfig = {
1676
+ numberOfColumns: number
1677
+ numberOfRows: number
1678
+ includeHeader: boolean
1679
+ }
1680
+
1681
+ const DEFAULT_TABLE_CONFIG: TableConfig = {
1682
+ numberOfColumns: 2,
1683
+ numberOfRows: 2,
1684
+ includeHeader: true,
1685
+ }
1686
+
1687
+ /**
1688
+ * Create a table containing a configurable number of rows and columns.
1689
+ * The table can optionally include a header row.
1690
+ */
1691
+ export const createAndFillTableElement = (
1692
+ attrs?: Attrs,
1693
+ config = DEFAULT_TABLE_CONFIG
1694
+ ) => {
1695
+ const { numberOfColumns, numberOfRows, includeHeader } = config
1696
+ const createRow = (cellType: ManuscriptNodeType) => {
1697
+ const cells = Array.from({ length: numberOfColumns }, () =>
1698
+ cellType.create({}, schema.nodes.text_block.create())
1699
+ )
1700
+ return schema.nodes.table_row.create({}, cells)
1701
+ }
1702
+
1703
+ const tableRows = includeHeader ? [createRow(schema.nodes.table_header)] : []
1704
+
1705
+ for (let i = 0; i < numberOfRows; i++) {
1706
+ tableRows.push(createRow(schema.nodes.table_cell))
1707
+ }
1708
+
1709
+ return schema.nodes.table_element.createChecked(
1710
+ {
1711
+ ...attrs,
1712
+ id: generateNodeID(schema.nodes.table_element),
1713
+ },
1714
+ [
1715
+ schema.nodes.caption_title.create(),
1716
+ schema.nodes.table.create({}, tableRows),
1717
+ schema.nodes.alt_text.create(),
1718
+ schema.nodes.long_desc.create(),
1719
+ schema.nodes.listing.create(),
1720
+ ]
1721
+ )
1722
+ }
1723
+
1724
+ const createAndFillFigureElement = (attrs?: Attrs) =>
1725
+ schema.nodes.figure_element.create(
1726
+ {
1727
+ ...attrs,
1728
+ id: generateNodeID(schema.nodes.figure_element),
1729
+ },
1730
+ [
1731
+ schema.nodes.figure.create(),
1732
+ schema.nodes.caption.create(undefined, schema.nodes.text_block.create()),
1733
+ schema.nodes.alt_text.create(),
1734
+ schema.nodes.long_desc.create(),
1735
+ schema.nodes.listing.create(),
1736
+ ]
1737
+ )
1738
+
1739
+ const createAndFillCaption = () => [
1740
+ schema.nodes.caption_title.create(),
1741
+ schema.nodes.caption.create(undefined, schema.nodes.text_block.create()),
1742
+ ]
1743
+
1744
+ const createImageElement = (attrs?: Attrs) =>
1745
+ schema.nodes.image_element.create(
1746
+ {
1747
+ ...attrs,
1748
+ id: generateNodeID(schema.nodes.image_element),
1749
+ },
1750
+ [
1751
+ schema.nodes.figure.create(),
1752
+ schema.nodes.caption.create(undefined, schema.nodes.text_block.create()),
1753
+ schema.nodes.alt_text.create(),
1754
+ schema.nodes.long_desc.create(),
1755
+ ]
1756
+ )
1757
+
1758
+ const createEmbedElement = (attrs?: Attrs) =>
1759
+ schema.nodes.embed.create(
1760
+ {
1761
+ ...attrs,
1762
+ id: generateNodeID(schema.nodes.embed),
1763
+ },
1764
+ [
1765
+ ...createAndFillCaption(),
1766
+ schema.nodes.alt_text.create(),
1767
+ schema.nodes.long_desc.create(),
1768
+ ]
1769
+ )
1770
+ /**
1771
+ * This to make sure we get block node
1772
+ */
1773
+ const getParentNode = (selection: Selection) => {
1774
+ const parentNode = findParentNodeWithId(selection)
1775
+ const node = parentNode?.node
1776
+
1777
+ if (node?.type === schema.nodes.table) {
1778
+ return findParentNodeOfType(schema.nodes.table_element)(selection)?.node
1779
+ }
1780
+
1781
+ return node
1782
+ }
1783
+
1784
+ export const addNodeComment = (
1785
+ node: ManuscriptNode,
1786
+ state: ManuscriptEditorState,
1787
+ dispatch?: Dispatch
1788
+ ) => {
1789
+ const props = getEditorProps(state)
1790
+ const attrs = {
1791
+ id: generateNodeID(schema.nodes.comment),
1792
+ contents: '',
1793
+ target: node.attrs.id,
1794
+ userID: props.userID,
1795
+ timestamp: Date.now(),
1796
+ } as CommentAttrs
1797
+ const comment = schema.nodes.comment.create(attrs)
1798
+ const comments = findChildrenByType(state.doc, schema.nodes.comments)[0]
1799
+ if (comments) {
1800
+ const pos = comments.pos + 1
1801
+
1802
+ const tr = state.tr.insert(pos, comment)
1803
+ const key = getCommentKey(attrs, undefined, node)
1804
+ setCommentSelection(tr, key, attrs.id, true)
1805
+ if (dispatch) {
1806
+ dispatch(tr)
1807
+ }
1808
+ return true
1809
+ }
1810
+ return false
1811
+ }
1812
+
1813
+ export const addInlineComment = (
1814
+ state: ManuscriptEditorState,
1815
+ dispatch?: Dispatch
1816
+ ): boolean => {
1817
+ const selection = state.selection
1818
+ const node = getParentNode(selection)
1819
+ if (!node) {
1820
+ return false
1821
+ }
1822
+ let from = selection.from
1823
+ let to = selection.to
1824
+
1825
+ if (from === to) {
1826
+ // Use the current cursor position to determine the boundaries of the intended word
1827
+ const result = findWordBoundaries(state, from)
1828
+ from = result.from
1829
+ to = result.to
1830
+ }
1831
+
1832
+ const props = getEditorProps(state)
1833
+ const attrs = {
1834
+ id: generateNodeID(schema.nodes.comment),
1835
+ contents: '',
1836
+ target: node.attrs.id,
1837
+ userID: props.userID,
1838
+ timestamp: Date.now(),
1839
+ originalText: selectedText() || state.doc.textBetween(from, to),
1840
+ selector: {
1841
+ from,
1842
+ to,
1843
+ },
1844
+ } as CommentAttrs
1845
+ const comment = schema.nodes.comment.create(attrs)
1846
+ const comments = findChildrenByType(state.doc, schema.nodes.comments)[0]
1847
+ if (comments) {
1848
+ const pos = comments.pos + 1
1849
+
1850
+ const tr = state.tr.insert(pos, comment)
1851
+
1852
+ const start = schema.nodes.highlight_marker.create({
1853
+ id: comment.attrs.id,
1854
+ tid: node.attrs.id,
1855
+ position: 'start',
1856
+ })
1857
+ const end = schema.nodes.highlight_marker.create({
1858
+ id: comment.attrs.id,
1859
+ tid: node.attrs.id,
1860
+ position: 'end',
1861
+ })
1862
+ tr.insert(from, start).insert(to + 1, end)
1863
+
1864
+ const range = getCommentRange(attrs)
1865
+ const key = getCommentKey(attrs, range, node)
1866
+ setCommentSelection(tr, key, attrs.id, true)
1867
+ if (dispatch) {
1868
+ dispatch(tr)
1869
+ }
1870
+ return true
1871
+ }
1872
+ return false
1873
+ }
1874
+
1875
+ export const addRows =
1876
+ (direction: 'top' | 'bottom') =>
1877
+ (state: EditorState, dispatch?: (tr: Transaction) => void): boolean => {
1878
+ if (dispatch) {
1879
+ const { tr } = state
1880
+ let rect = selectedRect(state)
1881
+ const selectedRows = rect.bottom - rect.top
1882
+ for (let i = 0; i < selectedRows; i++) {
1883
+ addRow(tr, rect, rect[direction])
1884
+ // this to make sure next row has tracking attributes, so each step of addRow has a different location
1885
+ rect = {
1886
+ ...selectedRect(state.apply(tr)),
1887
+ top: rect.top + 1,
1888
+ bottom: rect.bottom + 1,
1889
+ }
1890
+ }
1891
+ dispatch(tr)
1892
+ }
1893
+ return true
1894
+ }
1895
+
1896
+ export const addHeaderRow =
1897
+ (direction: 'above' | 'below') =>
1898
+ (state: EditorState, dispatch?: (tr: Transaction) => void): boolean => {
1899
+ if (dispatch) {
1900
+ const { tr } = state
1901
+ const rect = selectedRect(state)
1902
+ const addRowStep = addRow(
1903
+ state.tr,
1904
+ rect,
1905
+ rect[direction === 'below' ? 'bottom' : 'top']
1906
+ ).steps.pop()
1907
+ if (addRowStep && addRowStep instanceof ReplaceStep) {
1908
+ const { from, to, slice } = addRowStep
1909
+ const cells = flatten(slice.content.firstChild as ManuscriptNode, false)
1910
+ const row = schema.nodes.table_row.create(
1911
+ undefined,
1912
+ cells.map((cell) =>
1913
+ schema.nodes.table_header.create(cell.node.attrs, cell.node.content)
1914
+ )
1915
+ )
1916
+ tr.step(
1917
+ new ReplaceStep(
1918
+ from,
1919
+ to,
1920
+ new Slice(Fragment.from(row), slice.openStart, slice.openEnd)
1921
+ )
1922
+ )
1923
+ dispatch(tr)
1924
+ }
1925
+ }
1926
+ return true
1927
+ }
1928
+
1929
+ export const addColumns =
1930
+ (direction: 'right' | 'left') =>
1931
+ (state: EditorState, dispatch?: (tr: Transaction) => void): boolean => {
1932
+ if (dispatch) {
1933
+ const { tr } = state
1934
+ const rect = selectedRect(state.apply(tr))
1935
+ const selectedRows = rect.right - rect.left
1936
+ for (let i = 0; i < selectedRows; i++) {
1937
+ const command = direction === 'right' ? addColumnAfter : addColumnBefore
1938
+ command(state.apply(tr), (t) => t.steps.map((s) => tr.step(s)))
1939
+ }
1940
+ dispatch(tr)
1941
+ }
1942
+ return true
1943
+ }
1944
+
1945
+ export const autoComplete = (
1946
+ state: ManuscriptEditorState,
1947
+ dispatch?: Dispatch
1948
+ ) => {
1949
+ const complete = checkForCompletion(state)
1950
+ if (complete && complete.suggestion.length > 0) {
1951
+ const tr = state.tr.insertText(complete.suggestion, state.selection.from)
1952
+ const inserted = complete.title.substring(
1953
+ 0,
1954
+ complete.title.length - complete.suggestion.length
1955
+ )
1956
+ if (inserted) {
1957
+ // replacing to provide text case as required
1958
+ tr.replaceWith(
1959
+ state.selection.from - inserted.length,
1960
+ state.selection.from,
1961
+ schema.text(inserted)
1962
+ )
1963
+ }
1964
+
1965
+ dispatch && dispatch(tr)
1966
+ return true
1967
+ }
1968
+ return false
1969
+ }
1970
+
1971
+ export const activateSearch = (
1972
+ state: ManuscriptEditorState,
1973
+ dispatch?: Dispatch
1974
+ ) => {
1975
+ const pluginState = searchReplaceKey.getState(state)
1976
+ const tr = state.tr.setMeta(searchReplaceKey, {
1977
+ active: !pluginState?.active,
1978
+ })
1979
+ dispatch && dispatch(tr)
1980
+ return true
1981
+ }
1982
+
1983
+ export const activateSearchReplace = (
1984
+ state: ManuscriptEditorState,
1985
+ dispatch?: Dispatch
1986
+ ) => {
1987
+ const pluginState = searchReplaceKey.getState(state)
1988
+ const tr = state.tr.setMeta(searchReplaceKey, {
1989
+ activeAdvanced: !pluginState?.activeAdvanced,
1990
+ })
1991
+ dispatch && dispatch(tr)
1992
+ return true
1993
+ }
1994
+
1995
+ const createHeroImage = (attrs?: Attrs) =>
1996
+ schema.nodes.hero_image.create(
1997
+ {
1998
+ ...attrs,
1999
+ id: generateNodeID(schema.nodes.hero_image),
2000
+ },
2001
+ [
2002
+ schema.nodes.figure.create(),
2003
+ schema.nodes.alt_text.create(),
2004
+ schema.nodes.long_desc.create(),
2005
+ ]
2006
+ )
2007
+
2008
+ export const insertHeroImage =
2009
+ () =>
2010
+ (state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
2011
+ const position = findInsertionPosition(schema.nodes.hero_image, state.doc)
2012
+ view?.focus()
2013
+ createBlock(schema.nodes.hero_image, position, state, dispatch)
2014
+
2015
+ return true
2016
+ }
2017
+
2018
+ export const ignoreEnterInSubtitles = (state: ManuscriptEditorState) => {
2019
+ const { selection } = state
2020
+
2021
+ if (!isTextSelection(selection)) {
2022
+ return false
2023
+ }
2024
+
2025
+ // Check where the cursor is positioned
2026
+ const cursorParent = selection.$from.node()
2027
+
2028
+ if (
2029
+ cursorParent.type === state.schema.nodes.subtitle ||
2030
+ cursorParent.type === state.schema.nodes.subtitles
2031
+ ) {
2032
+ return true // Prevent Enter in subtitle area
2033
+ }
2034
+
2035
+ return false
2036
+ }
2037
+
2038
+ // Command to exit editor and focus container
2039
+ export const exitEditorToContainer: EditorAction = (state, dispatch, view) => {
2040
+ const editorContainer = document.getElementById('editor')
2041
+ if (editorContainer && dispatch && view) {
2042
+ editorContainer.focus()
2043
+ const tr = view.state.tr.setMeta(persistentCursor, { on: true })
2044
+ view.dispatch(tr)
2045
+ return true
2046
+ }
2047
+
2048
+ return false
2049
+ }
2050
+
2051
+ export const copySelection = (
2052
+ state: ManuscriptEditorState,
2053
+ dispatch?: Dispatch,
2054
+ view?: EditorView
2055
+ ) => {
2056
+ const { selection } = state
2057
+ const clipboard = navigator?.clipboard
2058
+
2059
+ if (selection.content().size && clipboard) {
2060
+ view &&
2061
+ (async () => {
2062
+ try {
2063
+ const { dom, text } = view.serializeForClipboard(selection.content())
2064
+ await clipboard.write([
2065
+ new ClipboardItem({
2066
+ 'text/plain': new Blob([text], { type: 'text/plain' }),
2067
+ 'text/html': new Blob([dom.innerHTML], { type: 'text/html' }),
2068
+ }),
2069
+ ])
2070
+ } catch (e) {
2071
+ console.error('clipboard writer error:', e)
2072
+ }
2073
+ })()
2074
+ return true
2075
+ }
2076
+
2077
+ return false
2078
+ }
2079
+
2080
+ export const paste =
2081
+ (format: 'html' | 'text') =>
2082
+ (state: ManuscriptEditorState, dispatch?: Dispatch, view?: EditorView) => {
2083
+ const clipboard = navigator?.clipboard
2084
+
2085
+ if (clipboard) {
2086
+ view &&
2087
+ (async () => {
2088
+ try {
2089
+ const items = await clipboard.read()
2090
+ const htmlItem = await items.find(({ types }) =>
2091
+ types.includes('text/html')
2092
+ )
2093
+ if (format === 'html' && htmlItem) {
2094
+ const htmlBlob = await htmlItem.getType('text/html')
2095
+ const html = await htmlBlob.text()
2096
+ view.pasteHTML(html)
2097
+ } else {
2098
+ const text = await clipboard.readText()
2099
+ view.pasteText(text)
2100
+ }
2101
+ } catch (e) {
2102
+ console.error('clipboard reader error:', e)
2103
+ }
2104
+ })()
2105
+ return true
2106
+ }
2107
+
2108
+ return false
2109
+ }