@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,547 @@
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 {
17
+ isDeleted,
18
+ isValid,
19
+ setAction,
20
+ TrackChangesAction,
21
+ TrackedAttrs,
22
+ } from '@manuscripts/track-changes-plugin'
23
+ import {
24
+ generateNodeID,
25
+ isSectionNodeType,
26
+ ManuscriptEditorView,
27
+ ManuscriptNodeType,
28
+ nodeNames,
29
+ schema,
30
+ SectionTitleNode,
31
+ } from '@manuscripts/transform'
32
+ import { Fragment, Node } from 'prosemirror-model'
33
+ import { EditorState, TextSelection, Transaction } from 'prosemirror-state'
34
+ import { findChildrenByType, hasParentNodeOfType } from 'prosemirror-utils'
35
+ import { EditorView } from 'prosemirror-view'
36
+
37
+ import { Dispatch } from '../../commands'
38
+ import { filterBlockNodes } from '../../lib/utils'
39
+ import { Option } from './type-selector/TypeSelector'
40
+
41
+ export const optionName = (nodeType: ManuscriptNodeType) => {
42
+ switch (nodeType) {
43
+ case nodeType.schema.nodes.section:
44
+ return 'Heading'
45
+
46
+ default:
47
+ return nodeNames.get(nodeType) || nodeType.name
48
+ }
49
+ }
50
+
51
+ export const titleCase = (text: string) =>
52
+ text.replace(/\b([a-z])/g, (match) => match.toUpperCase())
53
+
54
+ export const findSelectedOption = (options: Option[]): Option | undefined => {
55
+ for (const option of options) {
56
+ if (option.isSelected) {
57
+ return option
58
+ }
59
+ }
60
+ }
61
+
62
+ // Helper function to find the deepest subsection (the last subsection that doesn't have subsections itself)
63
+ export const getDeepestSubsectionPosition = (
64
+ subsection: Node,
65
+ position: number
66
+ ) => {
67
+ while (subsection.lastChild && isSectionNodeType(subsection.lastChild.type)) {
68
+ if (isDeleted(subsection.lastChild)) {
69
+ return (
70
+ position +
71
+ jumpToPreviousValidNode(
72
+ subsection.nodeSize - subsection.lastChild.nodeSize - 2,
73
+ subsection
74
+ ) +
75
+ 1
76
+ )
77
+ }
78
+ position += subsection.nodeSize - subsection.lastChild.nodeSize
79
+ subsection = subsection.lastChild
80
+ }
81
+ return position + subsection.content.size
82
+ }
83
+
84
+ const getInsertDataTracked = (node: Node) => {
85
+ const dataTracked = node.attrs.dataTracked as TrackedAttrs[] | null
86
+ const change = dataTracked?.find((c) => c.operation === 'insert')
87
+ return change ? [change] : null
88
+ }
89
+
90
+ /** Look at current position if node is deleted move to the previous sibling and
91
+ * check if it's deleted until we find a valid node */
92
+ const jumpToPreviousValidNode = (beforeSection: number, doc: Node) => {
93
+ const $beforeSection = doc.resolve(beforeSection)
94
+ let pos = beforeSection
95
+ for (let i = $beforeSection.index() - 1; i >= 0; i--) {
96
+ pos = $beforeSection.posAtIndex(i)
97
+ const node = doc.resolve(pos + 1).node()
98
+ if (!(isDeleted(node) && node.type === schema.nodes.section) && i >= 0) {
99
+ pos = $beforeSection.posAtIndex(i + 1)
100
+ break
101
+ }
102
+ }
103
+
104
+ return pos
105
+ }
106
+
107
+ export const demoteSectionToParagraph = (
108
+ state: EditorState,
109
+ dispatch: (tr: Transaction) => void,
110
+ view?: ManuscriptEditorView
111
+ ) => {
112
+ const {
113
+ doc,
114
+ selection: { $from },
115
+ schema,
116
+ tr,
117
+ } = state
118
+ const { nodes } = schema
119
+
120
+ const sectionTitle = $from.node($from.depth)
121
+ const afterSectionTitle = $from.after($from.depth)
122
+ const $afterSectionTitle = doc.resolve(afterSectionTitle)
123
+ const afterSectionTitleOffset = $afterSectionTitle.parentOffset
124
+
125
+ const sectionDepth = $from.depth - 1
126
+ const section = $from.node(sectionDepth)
127
+ let beforeSection = $from.before(sectionDepth)
128
+ beforeSection = jumpToPreviousValidNode(beforeSection, doc)
129
+ const $beforeSection = doc.resolve(beforeSection)
130
+ const previousNode = $beforeSection.nodeBefore
131
+ const paragraph = nodes.paragraph.create(
132
+ {
133
+ id: generateNodeID(nodes.paragraph),
134
+ dataTracked: getInsertDataTracked(section),
135
+ },
136
+ sectionTitle.content
137
+ )
138
+
139
+ let anchor
140
+
141
+ const sectionContent = section.content.cut(afterSectionTitleOffset)
142
+
143
+ if (previousNode && isSectionNodeType(previousNode.type)) {
144
+ const hasSubsections =
145
+ previousNode.lastChild && isSectionNodeType(previousNode.lastChild.type)
146
+
147
+ if (hasSubsections) {
148
+ beforeSection = getDeepestSubsectionPosition(
149
+ previousNode,
150
+ beforeSection - previousNode.nodeSize
151
+ )
152
+ } else {
153
+ beforeSection = beforeSection - 1
154
+ }
155
+ anchor = beforeSection + 1
156
+ } else {
157
+ anchor = beforeSection + 1
158
+ }
159
+
160
+ // TC plugins is the one who has to drop shadow content and not body-editor has to do that
161
+ const content = filterBlockNodes(
162
+ Fragment.from(paragraph).append(sectionContent)
163
+ )
164
+ tr.insert(beforeSection, content)
165
+
166
+ const afterSection = tr.mapping.map($from.after(sectionDepth))
167
+ tr.delete(afterSection - section.nodeSize, afterSection)
168
+
169
+ tr.setSelection(TextSelection.create(tr.doc, anchor))
170
+
171
+ dispatch(
172
+ setAction(
173
+ tr,
174
+ TrackChangesAction.structuralChangeAction,
175
+ 'convert-to-paragraph'
176
+ )
177
+ )
178
+ view && view.focus()
179
+ }
180
+
181
+ export const promoteParagraphToSection = (
182
+ state: EditorState,
183
+ dispatch: (tr: Transaction) => void,
184
+ view?: ManuscriptEditorView
185
+ ) => {
186
+ const {
187
+ doc,
188
+ selection: { $from },
189
+ schema,
190
+ tr,
191
+ } = state
192
+ const { nodes } = schema
193
+ const paragraph = $from.node($from.depth)
194
+ const beforeParagraph = $from.before($from.depth)
195
+ const $beforeParagraph = doc.resolve(beforeParagraph)
196
+ const beforeParagraphOffset = $beforeParagraph.parentOffset
197
+ const afterParagraphOffset = beforeParagraphOffset + paragraph.nodeSize
198
+
199
+ const sectionDepth = $from.depth - 1
200
+ const parentSection = $from.node(sectionDepth)
201
+ const startIndex = $from.index(sectionDepth)
202
+ const endIndex = $from.indexAfter(sectionDepth)
203
+ let afterParentSection = $from.after(sectionDepth)
204
+ const items: Node[] = []
205
+
206
+ if (startIndex > 0) {
207
+ // add the original section with content up to the paragraph
208
+ const precedingSection = parentSection.cut(0, beforeParagraphOffset)
209
+ items.push(precedingSection)
210
+ }
211
+
212
+ const textContent = findChildrenByType(paragraph, schema.nodes.text).map(
213
+ ({ node }) => node
214
+ )
215
+ const sectionTitle: SectionTitleNode = textContent
216
+ ? nodes.section_title.create({}, Fragment.from(textContent))
217
+ : nodes.section_title.create()
218
+
219
+ let sectionContent = Fragment.from(sectionTitle)
220
+ if (endIndex < parentSection.childCount) {
221
+ sectionContent = sectionContent.append(
222
+ parentSection.content.cut(afterParagraphOffset)
223
+ )
224
+ } else {
225
+ sectionContent = sectionContent.append(Fragment.from(paragraph.copy()))
226
+ }
227
+
228
+ if (parentSection.type.name === 'body') {
229
+ sectionContent = Fragment.from(sectionTitle)
230
+ afterParentSection = beforeParagraph + paragraph.nodeSize
231
+ // get position before first section in body, starting from the selection node index
232
+ parentSection.forEach((node, offset, index) => {
233
+ if (
234
+ node.type !== nodes.section &&
235
+ index > tr.doc.resolve(beforeParagraph).index()
236
+ ) {
237
+ const pos = $from.start($from.depth - 1)
238
+ afterParentSection = pos + offset + node.nodeSize
239
+ sectionContent = sectionContent.append(Fragment.from(node))
240
+ }
241
+ })
242
+ items[0] = nodes.section.create(
243
+ { dataTracked: getInsertDataTracked(paragraph) },
244
+ sectionContent
245
+ )
246
+ } else {
247
+ items.push(
248
+ parentSection.type.create(
249
+ { dataTracked: getInsertDataTracked(paragraph) },
250
+ sectionContent
251
+ )
252
+ )
253
+ }
254
+ const content = filterBlockNodes(Fragment.from(items[items.length - 1]))
255
+ tr.insert(afterParentSection, content)
256
+ tr.delete(beforeParagraph, afterParentSection - 1)
257
+
258
+ const anchor = tr.mapping.map(afterParentSection) - content.size + 2
259
+ tr.setSelection(TextSelection.create(tr.doc, anchor))
260
+
261
+ dispatch(
262
+ setAction(
263
+ tr,
264
+ TrackChangesAction.structuralChangeAction,
265
+ 'convert-to-section'
266
+ )
267
+ )
268
+ view && view.focus()
269
+ }
270
+
271
+ export const isIndentationAllowed =
272
+ (action: string) => (state: EditorState) => {
273
+ const { $from } = state.selection
274
+ const nodeType = $from.node().type
275
+
276
+ const allowedNodeTypes = [
277
+ schema.nodes.section_title,
278
+ schema.nodes.paragraph,
279
+ ]
280
+ if (!allowedNodeTypes.includes(nodeType)) {
281
+ return false
282
+ }
283
+
284
+ const isInBody = hasParentNodeOfType(schema.nodes.body)(state.selection)
285
+ const isDeletedNode = isDeleted($from.node($from.depth))
286
+ if (!isInBody || isDeletedNode) {
287
+ return false
288
+ }
289
+
290
+ // It is not allowed to unindent the top level parent sections, headers and paragraphs
291
+ if (action === 'unindent') {
292
+ const grandparentNode = $from.node($from.depth - 2)
293
+ if (grandparentNode?.type === schema.nodes.body) {
294
+ return false
295
+ }
296
+ }
297
+
298
+ if (nodeType === schema.nodes.paragraph) {
299
+ const parentNode = $from.node($from.depth - 1)
300
+
301
+ const isIndentNotAllowed = ![
302
+ schema.nodes.section,
303
+ schema.nodes.body,
304
+ ].includes(parentNode?.type)
305
+
306
+ const isUnindentNotAllowed = parentNode?.type !== schema.nodes.section
307
+
308
+ if (action === 'indent' && isIndentNotAllowed) {
309
+ return false
310
+ }
311
+
312
+ if (action === 'unindent' && isUnindentNotAllowed) {
313
+ return false
314
+ }
315
+ }
316
+
317
+ return true
318
+ }
319
+
320
+ export const indentSection =
321
+ () => (state: EditorState, dispatch: Dispatch, view?: EditorView) => {
322
+ const {
323
+ selection: { $from },
324
+ schema,
325
+ tr,
326
+ } = state
327
+ const { nodes } = schema
328
+
329
+ const sectionDepth = $from.depth - 1
330
+ const section = $from.node(sectionDepth)
331
+ const beforeSection = $from.before(sectionDepth)
332
+ const afterSection = $from.after(sectionDepth)
333
+
334
+ const parentSectionDepth = sectionDepth - 1
335
+ const parentSection = $from.node(parentSectionDepth)
336
+ const startIndex = $from.index(parentSectionDepth)
337
+
338
+ // Check if there's a valid previous section to indent into
339
+ // Look backwards through siblings to find a valid (non-deleted, non-moved) section
340
+ let previousSection: Node | null = null
341
+ for (let i = startIndex - 1; i >= 0; i--) {
342
+ const candidate = parentSection.child(i)
343
+ if (candidate.type === nodes.section && isValid(candidate)) {
344
+ previousSection = candidate
345
+ break
346
+ }
347
+ }
348
+
349
+ let anchor
350
+ if (!previousSection) {
351
+ // No valid previous section, creating new parent section
352
+ const emptyTitle = nodes.section_title.create()
353
+ const newParentSectionContent = Fragment.fromArray([emptyTitle, section])
354
+ const newParentSection = nodes.section.create({}, newParentSectionContent)
355
+
356
+ tr.insert(beforeSection, newParentSection)
357
+
358
+ // Delete the original section (now at shifted position)
359
+ const newBeforeSection = beforeSection + newParentSection.nodeSize
360
+ const newAfterSection = afterSection + newParentSection.nodeSize
361
+
362
+ tr.delete(newBeforeSection, newAfterSection)
363
+
364
+ anchor = beforeSection + 1
365
+ } else {
366
+ // Moving section into previous section as subsection
367
+ // Find the actual position of the previous section by walking through the document
368
+ let beforePreviousSection: number | null = null
369
+
370
+ $from.doc.descendants((node, pos) => {
371
+ if (node === previousSection) {
372
+ beforePreviousSection = pos
373
+ return false // stop iteration
374
+ }
375
+ })
376
+
377
+ if (beforePreviousSection === null) {
378
+ return false
379
+ }
380
+
381
+ const insertPos = beforePreviousSection + previousSection.nodeSize - 1
382
+ tr.insert(insertPos, section)
383
+
384
+ // Delete the original section (positions have shifted due to insert)
385
+ const newBeforeSection = beforeSection + section.nodeSize
386
+ const newAfterSection = afterSection + section.nodeSize
387
+ tr.delete(newBeforeSection, newAfterSection)
388
+
389
+ anchor = insertPos + 1
390
+ }
391
+
392
+ tr.setSelection(TextSelection.create(tr.doc, anchor))
393
+
394
+ setAction(tr, TrackChangesAction.indentationAction, { action: 'indent' })
395
+
396
+ dispatch(tr)
397
+ view && view.focus()
398
+ }
399
+
400
+ export const indentParagraph =
401
+ () => (state: EditorState, dispatch: Dispatch, view?: EditorView) => {
402
+ const { $from } = state.selection
403
+ const { schema, tr } = state
404
+
405
+ const beforeParagraph = $from.before($from.depth)
406
+ const sectionDepth = $from.depth - 1
407
+ const parentSection = $from.node(sectionDepth)
408
+ const sectionStart = $from.start(sectionDepth)
409
+ const sectionEnd = $from.end(sectionDepth)
410
+
411
+ // Build section content
412
+ const sectionContent = Fragment.from(
413
+ schema.nodes.section_title.create()
414
+ ).append(parentSection.content.cut(beforeParagraph - sectionStart))
415
+
416
+ // Create new section
417
+ const newSection = schema.nodes.section.create(
418
+ { id: generateNodeID(schema.nodes.section) },
419
+ sectionContent
420
+ )
421
+
422
+ setAction(tr, TrackChangesAction.indentationAction, { action: 'indent' })
423
+
424
+ tr.delete(beforeParagraph, sectionEnd)
425
+ tr.insert(beforeParagraph, newSection)
426
+
427
+ // Set selection inside the title of the new section
428
+ tr.setSelection(
429
+ TextSelection.create(tr.doc, beforeParagraph + newSection.nodeSize - 2)
430
+ )
431
+
432
+ dispatch(tr)
433
+ view?.focus()
434
+ }
435
+
436
+ export const unindentSection =
437
+ () => (state: EditorState, dispatch: Dispatch, view?: EditorView) => {
438
+ const {
439
+ selection: { $from },
440
+ tr,
441
+ } = state
442
+ const sectionDepth = $from.depth - 1
443
+ const section = $from.node(sectionDepth)
444
+ const beforeSection = $from.before(sectionDepth)
445
+ const sectionTitle = $from.node($from.depth)
446
+
447
+ const $beforeSection = tr.doc.resolve(beforeSection)
448
+ const beforeSectionOffset = $beforeSection.parentOffset
449
+ const afterSectionOffset = beforeSectionOffset + section.nodeSize
450
+
451
+ const parentSectionDepth = $from.depth - 2
452
+ const parentSection = $from.node(parentSectionDepth)
453
+ const endIndex = $from.indexAfter(parentSectionDepth)
454
+ const afterParentSection = $from.after(parentSectionDepth)
455
+
456
+ const hasFollowingSiblings = endIndex < parentSection.childCount
457
+
458
+ let extendedSection = section
459
+
460
+ if (hasFollowingSiblings) {
461
+ // If there are following siblings, we need to extend the section
462
+ const siblingsContent = parentSection.content.cut(afterSectionOffset)
463
+ extendedSection = section.copy(section.content.append(siblingsContent))
464
+ }
465
+
466
+ // Insert the unindented section at the parent level
467
+ tr.insert(afterParentSection, Fragment.from(extendedSection))
468
+
469
+ // Delete the original section (positions inside parent haven't changed)
470
+ tr.delete(beforeSection, beforeSection + extendedSection.nodeSize)
471
+
472
+ const anchor = afterParentSection + 2
473
+
474
+ tr.setSelection(
475
+ TextSelection.create(tr.doc, anchor, anchor + sectionTitle.content.size)
476
+ )
477
+
478
+ setAction(tr, TrackChangesAction.indentationAction, { action: 'unindent' })
479
+
480
+ dispatch(tr)
481
+ view && view.focus()
482
+ }
483
+
484
+ export const unindentParagraph =
485
+ () => (state: EditorState, dispatch: Dispatch, view?: EditorView) => {
486
+ const {
487
+ selection: { $from },
488
+ schema,
489
+ tr,
490
+ } = state
491
+
492
+ const paragraphPos = $from.before($from.depth)
493
+
494
+ const sectionDepth = $from.depth - 1
495
+ const section = $from.node(sectionDepth)
496
+ const afterSection = $from.after(sectionDepth)
497
+ const sectionStart = $from.start(sectionDepth)
498
+
499
+ const parentSectionDepth = sectionDepth - 1
500
+ const parentSection = $from.node(parentSectionDepth)
501
+ const parentSectionStart = $from.start(parentSectionDepth)
502
+ const parentSectionEnd = $from.end(parentSectionDepth)
503
+
504
+ let sectionContent = Fragment.from(schema.nodes.section_title.create())
505
+
506
+ sectionContent = sectionContent
507
+ .append(section.content.cut(paragraphPos - sectionStart))
508
+ .append(parentSection.content.cut(afterSection - parentSectionStart))
509
+
510
+ const newSection = schema.nodes.section.create(
511
+ { id: generateNodeID(schema.nodes.section) },
512
+ sectionContent
513
+ )
514
+
515
+ tr.delete(paragraphPos, parentSectionEnd)
516
+ tr.insert(paragraphPos + 2, newSection)
517
+
518
+ tr.setSelection(
519
+ TextSelection.create(tr.doc, paragraphPos + newSection.nodeSize)
520
+ )
521
+
522
+ setAction(tr, TrackChangesAction.indentationAction, { action: 'unindent' })
523
+
524
+ dispatch(tr)
525
+ view?.focus()
526
+ }
527
+
528
+ const indentationHandlers = {
529
+ section_title: {
530
+ indent: indentSection,
531
+ unindent: unindentSection,
532
+ },
533
+ paragraph: {
534
+ indent: indentParagraph,
535
+ unindent: unindentParagraph,
536
+ },
537
+ }
538
+
539
+ export const changeIndentation =
540
+ (action: 'indent' | 'unindent') =>
541
+ (state: EditorState, dispatch: Dispatch, view?: EditorView) => {
542
+ const { $from } = state.selection
543
+ const nodeName = $from.node().type.name as 'section_title' | 'paragraph'
544
+
545
+ const handler = indentationHandlers[nodeName]?.[action]
546
+ handler()(state, dispatch, view)
547
+ }
@@ -0,0 +1,89 @@
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 { TickIcon } from '@manuscripts/style-guide'
17
+ import React, { useLayoutEffect, useRef } from 'react'
18
+ import { components, ControlProps, OptionProps } from 'react-select'
19
+ import styled from 'styled-components'
20
+
21
+ import { optionName, titleCase } from '../helpers'
22
+ import { Option } from './TypeSelector'
23
+
24
+ const OptionContainer = styled.div<{ isFocused?: boolean }>`
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: space-between;
28
+ font-size: 14px;
29
+ cursor: pointer;
30
+ padding: 8px;
31
+ background: ${(props) => (props.isFocused ? '#f2fbfc' : 'transparent')};
32
+
33
+ &:hover {
34
+ background: ${(props) => props.theme.colors.background.fifth};
35
+ }
36
+ `
37
+ const OptionLabel = styled.span``
38
+
39
+ const TickIconWrapper = styled.div``
40
+ export const OptionComponent: React.FC<OptionProps<Option, false>> = ({
41
+ innerProps,
42
+ data,
43
+ isFocused,
44
+ innerRef,
45
+ }) => {
46
+ return (
47
+ <OptionContainer {...innerProps} isFocused={isFocused} ref={innerRef}>
48
+ <OptionLabel>{titleCase(optionName(data.nodeType))}</OptionLabel>
49
+ {data.isSelected && (
50
+ <TickIconWrapper>
51
+ <TickIcon />
52
+ </TickIconWrapper>
53
+ )}
54
+ </OptionContainer>
55
+ )
56
+ }
57
+
58
+ export const CustomControl = (props: ControlProps<Option, false>) => {
59
+ const controlRef = useRef<HTMLDivElement>(null)
60
+
61
+ useLayoutEffect(() => {
62
+ if (controlRef.current) {
63
+ controlRef.current.setAttribute('data-toolbar-button', 'true')
64
+ }
65
+ }, [])
66
+
67
+ const handleKeyDown = (e: React.KeyboardEvent) => {
68
+ if (e.key === 'Enter') {
69
+ e.preventDefault()
70
+ const { selectProps } = props
71
+ if (selectProps.menuIsOpen) {
72
+ selectProps.onMenuClose?.()
73
+ } else {
74
+ selectProps.onMenuOpen?.()
75
+ }
76
+ }
77
+ }
78
+
79
+ return (
80
+ <components.Control
81
+ {...props}
82
+ innerRef={controlRef}
83
+ innerProps={{
84
+ ...props.innerProps,
85
+ onKeyDown: handleKeyDown,
86
+ }}
87
+ />
88
+ )
89
+ }