@manuscripts/body-editor 3.12.66 → 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,438 @@
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
+ import {
17
+ TriangleCollapsedIcon,
18
+ TriangleExpandedIcon,
19
+ } from '@manuscripts/style-guide'
20
+ import {
21
+ isAttachmentsNode,
22
+ isBibliographySectionNode,
23
+ isSupplementsNode,
24
+ isElementNodeType,
25
+ isHeroImageNode,
26
+ ManuscriptEditorView,
27
+ ManuscriptNode,
28
+ ManuscriptNodeType,
29
+ nodeTitle,
30
+ nodeTitlePlaceholder,
31
+ schema,
32
+ } from '@manuscripts/transform'
33
+ import { Fragment } from 'prosemirror-model'
34
+ import React, { MouseEvent, useRef, useState } from 'react'
35
+ import { useDrag, useDrop } from 'react-dnd'
36
+
37
+ import { Capabilities } from '../../lib/capabilities'
38
+ import { ContextMenu } from '../../lib/context-menu'
39
+ import { DropSide, getDropSide } from '../../lib/dnd'
40
+ import { isBodyLocked } from '../../lib/utils'
41
+ import { nodeTypeIcon } from '../../node-type-icons'
42
+ import { PluginState, sectionTitleKey } from '../../plugins/section_title'
43
+ import { FileAttachment } from '../../lib/files'
44
+ import {
45
+ Outline,
46
+ OutlineItem,
47
+ OutlineItemArrow,
48
+ OutlineItemIcon,
49
+ OutlineItemLink,
50
+ OutlineItemLinkText,
51
+ OutlineItemNoArrow,
52
+ OutlineItemPlaceholder,
53
+ } from './Outline'
54
+ import { isDeleted } from '@manuscripts/track-changes-plugin'
55
+
56
+ const excludedTypes = [
57
+ schema.nodes.table,
58
+ schema.nodes.figure,
59
+ schema.nodes.footnotes_element,
60
+ schema.nodes.bibliography_element,
61
+ schema.nodes.keywords,
62
+ schema.nodes.affiliations,
63
+ schema.nodes.contributors,
64
+ schema.nodes.author_notes,
65
+ schema.nodes.title,
66
+ schema.nodes.alt_titles,
67
+ schema.nodes.alt_title,
68
+ schema.nodes.alt_text,
69
+ schema.nodes.long_desc,
70
+ schema.nodes.trans_abstract,
71
+ schema.nodes.trans_graphical_abstract,
72
+ schema.nodes.subtitles,
73
+ schema.nodes.subtitle,
74
+ schema.nodes.supplement,
75
+ ]
76
+
77
+ const childrenExcludedTypes = [
78
+ schema.nodes.pullquote_element,
79
+ schema.nodes.blockquote_element,
80
+ ]
81
+
82
+ export interface TreeItem {
83
+ index: number
84
+ items: TreeItem[]
85
+ node: ManuscriptNode
86
+ pos: number
87
+ endPos: number
88
+ parent?: ManuscriptNode
89
+ }
90
+
91
+ const isExcluded = (nodeType: ManuscriptNodeType) => {
92
+ return excludedTypes.includes(nodeType)
93
+ }
94
+
95
+ const isChildrenExcluded = (nodeType: ManuscriptNodeType) => {
96
+ return childrenExcludedTypes.includes(nodeType)
97
+ }
98
+
99
+ interface TreeBuilderOptions {
100
+ node: ManuscriptNode
101
+ pos: number
102
+ index: number
103
+ parent?: ManuscriptNode
104
+ }
105
+
106
+ type TreeBuilder = (options: TreeBuilderOptions) => TreeItem
107
+
108
+ const isAbstractOrBackmatter = (item: TreeItem) => {
109
+ return (
110
+ item.parent &&
111
+ (item.parent.type === schema.nodes.abstracts ||
112
+ item.parent.type === schema.nodes.backmatter)
113
+ )
114
+ }
115
+
116
+ const isManuscriptNode = (node: ManuscriptNode | undefined) => {
117
+ return node?.type === schema.nodes.manuscript
118
+ }
119
+
120
+ type EditorPropsWithFiles = ManuscriptEditorView['props'] & {
121
+ getFiles?: () => FileAttachment[]
122
+ }
123
+
124
+ const isPdfFile = (file?: FileAttachment) => {
125
+ if (!file?.name) {
126
+ return false
127
+ }
128
+ const name = file.name.toLowerCase()
129
+ return name.endsWith('.pdf')
130
+ }
131
+
132
+ const getEditorFiles = (view?: ManuscriptEditorView) => {
133
+ const props = view?.props as EditorPropsWithFiles | undefined
134
+ return props?.getFiles?.()
135
+ }
136
+
137
+ export const buildTree: TreeBuilder = ({
138
+ node,
139
+ pos,
140
+ index,
141
+ parent,
142
+ }): TreeItem => {
143
+ const items: TreeItem[] = []
144
+ const startPos = pos + 1 // TODO: don't increment this?
145
+ const endPos = pos + node.nodeSize
146
+
147
+ if (!isChildrenExcluded(node.type)) {
148
+ node.forEach((childNode, offset, childIndex) => {
149
+ if (isExcluded(childNode.type)) {
150
+ return
151
+ }
152
+ if (
153
+ isManuscriptNode(node) ||
154
+ ((!childNode.isAtom || isElementNodeType(childNode.type)) &&
155
+ childNode.attrs.id &&
156
+ !isDeleted(childNode))
157
+ ) {
158
+ items.push(
159
+ buildTree({
160
+ node: childNode,
161
+ pos: startPos + offset,
162
+ index: childIndex,
163
+ parent: node,
164
+ })
165
+ )
166
+ }
167
+ })
168
+ }
169
+
170
+ return { node, index, items, pos, endPos, parent }
171
+ }
172
+
173
+ export interface DraggableTreeProps {
174
+ depth: number
175
+ tree: TreeItem
176
+ view?: ManuscriptEditorView
177
+ can?: Capabilities
178
+ getFiles?: () => FileAttachment[]
179
+ }
180
+
181
+ export const DraggableTree: React.FC<DraggableTreeProps> = ({
182
+ tree,
183
+ view,
184
+ depth,
185
+ can,
186
+ getFiles,
187
+ }) => {
188
+ const [dropSide, setDropSide] = useState<DropSide>()
189
+ const [isOpen, setOpen] = useState(depth === 0)
190
+ const ref = useRef<HTMLDivElement>(null)
191
+ // Disable drag-and-drop functionality when the body is locked
192
+ const disableDragAndDrop = view
193
+ ? isBodyLocked(view.state) || !can?.editArticle
194
+ : true
195
+
196
+ const { node, items, parent } = tree
197
+ const sectionTitleState: PluginState | undefined = view
198
+ ? sectionTitleKey.getState(view.state)
199
+ : undefined
200
+
201
+ const itemText = (node: ManuscriptNode) => {
202
+ const text = nodeTitle(node)
203
+ let sectionNumber =
204
+ node.type.name === 'section' && sectionTitleState
205
+ ? (sectionTitleState.get(node.attrs.id) ?? '')
206
+ : ''
207
+ sectionNumber = sectionNumber ? `${sectionNumber}.` : ''
208
+
209
+ if (text) {
210
+ return `${sectionNumber}${sectionNumber ? ' ' : ''}${text.trim()}`
211
+ }
212
+
213
+ const placeholder = nodeTitlePlaceholder(node.type)
214
+
215
+ return (
216
+ <OutlineItemPlaceholder>
217
+ {sectionNumber && `${sectionNumber} `}
218
+ {placeholder}
219
+ </OutlineItemPlaceholder>
220
+ )
221
+ }
222
+
223
+ const toggleOpen = () => {
224
+ setOpen(!isOpen)
225
+ }
226
+
227
+ const [{ isDragging }, dragRef] = useDrag({
228
+ type: 'outline',
229
+ item: tree,
230
+ canDrag: () => {
231
+ // Prevent dragging if the node is deleted, the body is locked, or editing is not allowed
232
+ return depth !== 0 && !disableDragAndDrop && !isDeleted(node)
233
+ },
234
+ collect: (monitor) => ({
235
+ isDragging: monitor.isDragging(),
236
+ }),
237
+ })
238
+
239
+ const [{ isOver }, dropRef] = useDrop({
240
+ accept: 'outline',
241
+ canDrop(item: TreeItem, monitor) {
242
+ if (disableDragAndDrop) {
243
+ return false
244
+ }
245
+ if (isAbstractOrBackmatter(item) || isAbstractOrBackmatter(tree)) {
246
+ return false
247
+ }
248
+ if (!ref.current) {
249
+ return false
250
+ }
251
+ if (!tree.parent) {
252
+ return false
253
+ }
254
+ // can't drop on itself
255
+ if (item.node.attrs.id === tree.node.attrs.id) {
256
+ return false
257
+ }
258
+ // can't drop within itself
259
+ if (item.pos <= tree.pos && item.endPos >= tree.endPos) {
260
+ return false
261
+ }
262
+ // can't drop immediately before/after itself
263
+ if (tree.pos === item.endPos || item.pos === tree.endPos) {
264
+ return false
265
+ }
266
+
267
+ const side = getDropSide(ref.current, monitor)
268
+ const index = side === 'before' ? tree.index : tree.index + 1
269
+
270
+ return tree.parent.canReplace(index, index, Fragment.from(item.node))
271
+ },
272
+ hover(item, monitor) {
273
+ if (disableDragAndDrop || !ref.current || !monitor.canDrop()) {
274
+ return
275
+ }
276
+ const side = getDropSide(ref.current, monitor)
277
+ setDropSide(side)
278
+ },
279
+ drop(item: TreeItem, monitor) {
280
+ if (disableDragAndDrop || !ref.current || !view) {
281
+ return
282
+ }
283
+ const side = getDropSide(ref.current, monitor)
284
+ const pos = side === 'before' ? tree.pos - 1 : tree.endPos - 1
285
+ let sourcePos = item.pos - 1
286
+
287
+ const node = item.node.type.schema.nodes[item.node.type.name].create(
288
+ {
289
+ ...item.node.attrs,
290
+ },
291
+ item.node.content
292
+ )
293
+
294
+ const tr = view.state.tr.insert(pos, node)
295
+ sourcePos = tr.mapping.map(sourcePos)
296
+ tr.delete(sourcePos, sourcePos + item.node.nodeSize)
297
+ view.dispatch(tr)
298
+ },
299
+ collect: (monitor) => ({
300
+ isOver: monitor.isOver({ shallow: true }),
301
+ }),
302
+ })
303
+
304
+ const isDeletedItem = isDeleted(node)
305
+ const isHeroImage = isHeroImageNode(node)
306
+ const isSupplements = isSupplementsNode(node)
307
+ const isMainDocument = isAttachmentsNode(node)
308
+ const editorFiles = isMainDocument
309
+ ? (getFiles?.() ?? getEditorFiles(view))
310
+ : undefined
311
+
312
+ const isTop =
313
+ isManuscriptNode(parent) &&
314
+ !isHeroImage &&
315
+ !isSupplements &&
316
+ !isMainDocument
317
+
318
+ // Hide attachments node from outline when it's empty
319
+ if (isMainDocument && node.childCount === 0) {
320
+ return null
321
+ }
322
+
323
+ if (isMainDocument) {
324
+ const attachmentIds = new Set<string>()
325
+ node.forEach((childNode) => {
326
+ const href = childNode.attrs?.href
327
+ if (href) {
328
+ attachmentIds.add(href.replace(/^attachment:/, ''))
329
+ }
330
+ })
331
+
332
+ const mainDocumentFile =
333
+ editorFiles?.find((file) => {
334
+ const fileId = file.id?.replace(/^attachment:/, '')
335
+ return !!fileId && attachmentIds.has(fileId)
336
+ }) ?? editorFiles?.find((file) => isPdfFile(file))
337
+ const isMainDocumentPDF = isPdfFile(mainDocumentFile)
338
+
339
+ // Hide main document in outline if it is not a PDF
340
+ if (!isMainDocumentPDF) {
341
+ return null
342
+ }
343
+ }
344
+
345
+ const handleContextMenu = (e: MouseEvent) => {
346
+ e.preventDefault()
347
+ e.stopPropagation()
348
+
349
+ if (!can?.editArticle) {
350
+ return false
351
+ }
352
+ if (!view) {
353
+ return null
354
+ }
355
+
356
+ const menu = new ContextMenu(tree.node, view, () => tree.pos - 1)
357
+ menu.showEditMenu(e.currentTarget as HTMLAnchorElement)
358
+ }
359
+
360
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
361
+ if (e.key === 'Enter') {
362
+ e.preventDefault()
363
+ e.stopPropagation()
364
+
365
+ if (items.length > 0) {
366
+ // Has children: expand/collapse
367
+ toggleOpen()
368
+ } else {
369
+ // No children: navigate to the link
370
+ const link = (e.currentTarget as HTMLElement).querySelector('a')
371
+ if (link) {
372
+ link.click()
373
+ }
374
+ }
375
+ }
376
+ }
377
+
378
+ dragRef(dropRef(ref))
379
+
380
+ const classNames = [
381
+ isDragging && 'dragging',
382
+ isOver && dropSide && `drop-${dropSide}`,
383
+ isDeletedItem && 'deleted',
384
+ isHeroImage && 'hero-image',
385
+ isSupplements && 'supplements',
386
+ isBibliographySectionNode(node) && 'references',
387
+ isMainDocument && 'main-document',
388
+ ]
389
+ .filter(Boolean)
390
+ .join(' ') // .filter(Boolean) removes all falsy values (false, '', null, etc.)
391
+ return (
392
+ <Outline ref={ref} className={classNames}>
393
+ {!isTop && node.type.name != 'manuscript' && (
394
+ <OutlineItem
395
+ depth={isHeroImage || isSupplements || isMainDocument ? 1 : depth}
396
+ onContextMenu={handleContextMenu}
397
+ onKeyDown={handleKeyDown}
398
+ tabIndex={-1}
399
+ data-outline-item
400
+ >
401
+ {items.length ? (
402
+ <OutlineItemArrow
403
+ aria-label={`${isOpen ? 'Collapse' : 'Expand'} ${node.type.name}`}
404
+ onClick={toggleOpen}
405
+ tabIndex={-1}
406
+ >
407
+ {isOpen ? <TriangleExpandedIcon /> : <TriangleCollapsedIcon />}
408
+ </OutlineItemArrow>
409
+ ) : (
410
+ <OutlineItemNoArrow />
411
+ )}
412
+
413
+ <OutlineItemLink to={`#${node.attrs.id}`} tabIndex={-1}>
414
+ <OutlineItemIcon>{nodeTypeIcon(node.type)}</OutlineItemIcon>
415
+ <OutlineItemLinkText className={`outline-text-${node.type.name}`}>
416
+ {itemText(node)}
417
+ </OutlineItemLinkText>
418
+ </OutlineItemLink>
419
+ </OutlineItem>
420
+ )}
421
+
422
+ {items.length ? (
423
+ <div className={`subtree ${isOpen ? '' : 'collapsed'}`}>
424
+ {items.map((subtree, index) => (
425
+ <DraggableTree
426
+ key={subtree.node.attrs.id || 'subtree-' + index}
427
+ tree={subtree}
428
+ view={view}
429
+ depth={!tree.parent ? depth : depth + 1}
430
+ can={can}
431
+ getFiles={getFiles}
432
+ />
433
+ ))}
434
+ </div>
435
+ ) : null}
436
+ </Outline>
437
+ )
438
+ }
@@ -0,0 +1,127 @@
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 { ManuscriptEditorView, ManuscriptNode } from '@manuscripts/transform'
18
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
19
+
20
+ import { Capabilities } from '../../lib/capabilities'
21
+ import { FileAttachment } from '../../lib/files'
22
+ import { useDebounce } from '../hooks/use-debounce'
23
+ import { buildTree, DraggableTree, TreeItem } from './DraggableTree'
24
+
25
+ export interface ManuscriptOutlineProps {
26
+ doc: ManuscriptNode | null
27
+ can?: Capabilities
28
+ view?: ManuscriptEditorView
29
+ getFiles?: () => FileAttachment[]
30
+ }
31
+
32
+ export const ManuscriptOutline: React.FC<ManuscriptOutlineProps> = (props) => {
33
+ const [values, setValues] = useState<{
34
+ tree: TreeItem
35
+ view?: ManuscriptEditorView
36
+ can?: Capabilities
37
+ getFiles?: () => FileAttachment[]
38
+ }>()
39
+ const containerRef = useRef<HTMLDivElement>(null)
40
+
41
+ const debouncedProps = useDebounce(props, 500)
42
+
43
+ useEffect(() => {
44
+ const { doc, view } = debouncedProps
45
+
46
+ if (doc) {
47
+ const tree = buildTree({
48
+ node: doc,
49
+ pos: 0,
50
+ index: 0,
51
+ })
52
+
53
+ setValues({ tree, view, can: props.can, getFiles: props.getFiles })
54
+ } else {
55
+ setValues(undefined)
56
+ }
57
+ }, [debouncedProps, props.can, props.getFiles])
58
+
59
+ // Get all visible outline items (excluding those in collapsed subtrees)
60
+ const getOutlineItems = useCallback(() => {
61
+ if (!containerRef.current) return []
62
+ const allItems = Array.from(
63
+ containerRef.current.querySelectorAll<HTMLElement>('[data-outline-item]')
64
+ )
65
+
66
+ // Filter out items that are inside a collapsed subtree
67
+ return allItems.filter((item) => {
68
+ let parent = item.parentElement
69
+ while (parent && parent !== containerRef.current) {
70
+ if (
71
+ parent.classList.contains('subtree') &&
72
+ parent.classList.contains('collapsed')
73
+ ) {
74
+ return false
75
+ }
76
+ parent = parent.parentElement
77
+ }
78
+ return true
79
+ })
80
+ }, [])
81
+
82
+ // Set roving tabindex: only first item is tabbable
83
+ useEffect(() => {
84
+ const items = getOutlineItems()
85
+ items.forEach((item, index) => {
86
+ item.tabIndex = index === 0 ? 0 : -1
87
+ })
88
+ }, [getOutlineItems, values])
89
+
90
+ const handleKeyDown = useCallback(
91
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
92
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
93
+ return
94
+ }
95
+
96
+ const target = event.target as HTMLElement
97
+ // Only handle if an outline item is focused
98
+ if (!target.hasAttribute('data-outline-item')) {
99
+ return
100
+ }
101
+
102
+ const items = getOutlineItems()
103
+ const currentIndex = items.indexOf(target)
104
+
105
+ if (currentIndex === -1) return
106
+
107
+ event.preventDefault()
108
+
109
+ let nextIndex: number
110
+ if (event.key === 'ArrowDown') {
111
+ nextIndex = (currentIndex + 1) % items.length
112
+ } else {
113
+ nextIndex = (currentIndex - 1 + items.length) % items.length
114
+ }
115
+
116
+ const nextItem = items[nextIndex]
117
+ nextItem?.focus()
118
+ },
119
+ [getOutlineItems]
120
+ )
121
+
122
+ return values && values.view ? (
123
+ <div ref={containerRef} onKeyDown={handleKeyDown}>
124
+ <DraggableTree {...values} depth={0} />
125
+ </div>
126
+ ) : null
127
+ }
@@ -0,0 +1,141 @@
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 { Link } from 'react-router-dom'
18
+ import styled from 'styled-components'
19
+
20
+ export const OutlineItemIcon = styled.span`
21
+ display: inline-flex;
22
+ width: 1.2em;
23
+ height: 1.3em;
24
+ justify-content: center;
25
+ align-items: center;
26
+ padding: 2px;
27
+ flex-shrink: 0;
28
+ `
29
+
30
+ export const OutlineItem = styled.div<{
31
+ depth: number
32
+ }>`
33
+ align-items: center;
34
+ color: ${(props) => props.theme.colors.text.primary};
35
+ box-sizing: border-box;
36
+ cursor: pointer;
37
+ display: flex;
38
+ font-size: ${(props) =>
39
+ props.depth === 0
40
+ ? props.theme.font.size.large
41
+ : props.theme.font.size.medium};
42
+ line-height: 30px;
43
+ margin-left: -${(props) => props.theme.grid.unit * 10}px;
44
+ margin-right: -${(props) => props.theme.grid.unit * 5}px;
45
+ overflow-x: hidden;
46
+ padding-right: ${(props) => props.theme.grid.unit * 5}px;
47
+ padding-left: ${(props) => 20 + props.depth * 20}px;
48
+ white-space: nowrap;
49
+
50
+ &:hover {
51
+ background: ${(props) => props.theme.colors.background.fifth};
52
+ }
53
+
54
+ &:focus-visible {
55
+ background: ${(props) => props.theme.colors.background.fifth};
56
+ outline: 2px solid #bce7f6;
57
+ outline-offset: -2px;
58
+ }
59
+ `
60
+
61
+ export const OutlineItemArrow = styled.button`
62
+ display: inline-block;
63
+ cursor: pointer;
64
+ border: none;
65
+ background: transparent;
66
+ padding: 0 ${(props) => props.theme.grid.unit}px;
67
+ flex-shrink: 0;
68
+ font-size: ${(props) => props.theme.font.size.normal};
69
+
70
+ &:hover use {
71
+ fill: #ccc;
72
+ }
73
+
74
+ &:focus {
75
+ outline: none;
76
+ }
77
+ `
78
+
79
+ export const OutlineItemNoArrow = styled.span`
80
+ display: inline-block;
81
+ width: 25px;
82
+ flex-shrink: 0;
83
+ `
84
+
85
+ export const OutlineItemLink = styled(Link)`
86
+ flex: 1;
87
+ display: inline-flex;
88
+ align-items: center;
89
+ overflow-x: hidden;
90
+ color: inherit;
91
+ text-decoration: none;
92
+ height: 100%;
93
+ border-bottom: 1px solid transparent;
94
+ border-top: 1px solid transparent;
95
+
96
+ &:focus,
97
+ &:active {
98
+ outline: none;
99
+ }
100
+ `
101
+
102
+ export const OutlineItemLinkText = styled.span`
103
+ flex: 1;
104
+ display: inline-block;
105
+ overflow-x: hidden;
106
+ text-overflow: ellipsis;
107
+ margin-left: ${(props) => props.theme.grid.unit * 2}px;
108
+ `
109
+
110
+ export const Outline = styled.div`
111
+ font-size: ${(props) => props.theme.font.size.medium};
112
+ position: relative;
113
+
114
+ & .outline-text-title {
115
+ font-size: ${(props) => props.theme.font.size.large};
116
+ }
117
+
118
+ &.dragging {
119
+ opacity: 0.5;
120
+ }
121
+
122
+ &.drop-before > ${OutlineItem} > ${OutlineItemLink} {
123
+ border-top-color: ${(props) => props.theme.colors.brand.dark};
124
+ }
125
+
126
+ &.drop-after > ${OutlineItem} > ${OutlineItemLink} {
127
+ border-bottom-color: ${(props) => props.theme.colors.brand.dark};
128
+ }
129
+
130
+ & .subtree.collapsed {
131
+ display: none;
132
+ }
133
+
134
+ & div:has(.supplements, .hero-image, .main-document) .references {
135
+ margin-bottom: 5px;
136
+ padding-bottom: 5px;
137
+ border-bottom: 1px dashed #ddd;
138
+ }
139
+ `
140
+
141
+ export const OutlineItemPlaceholder = styled.span``