@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,361 @@
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 { ManuscriptNode, schema } from '@manuscripts/transform'
18
+ import { findParentNodeOfTypeClosestToPos } from 'prosemirror-utils'
19
+
20
+ import { draggableIcon } from '../icons'
21
+ import {
22
+ addInteractionHandlers,
23
+ createFileHandlers,
24
+ createFileUploader,
25
+ createMediaPlaceholder,
26
+ createReactTools,
27
+ createUnsupportedFormat,
28
+ MediaType,
29
+ } from '../lib/media'
30
+ import { createEditableNodeView } from './creators'
31
+ import { FigureView } from './figure'
32
+ import { isDeleted } from '@manuscripts/track-changes-plugin'
33
+
34
+ export class FigureEditableView extends FigureView {
35
+ public reactTools: HTMLDivElement | null = null
36
+ positionMenuWrapper: HTMLDivElement
37
+ figurePosition: string
38
+ private dragHandle: HTMLDivElement | undefined
39
+ private static currentDragFigureId: string | null = null
40
+ private dragAndDropInitialized = false
41
+
42
+ public initialise() {
43
+ this.upload = this.upload.bind(this)
44
+ this.createDOM()
45
+ this.updateContents()
46
+ const domElement = this.dom as HTMLElement & {
47
+ __figureView?: FigureEditableView
48
+ }
49
+ domElement.__figureView = this
50
+ }
51
+
52
+ public update(newNode: ManuscriptNode): boolean {
53
+ const handledBySuper = super.update(newNode)
54
+ if (handledBySuper) {
55
+ this.addTools()
56
+ }
57
+ return handledBySuper
58
+ }
59
+
60
+ private clearTargetClass(
61
+ target: Element,
62
+ classes: string[] = ['drop-target-above', 'drop-target-below']
63
+ ) {
64
+ target.classList.remove(...classes)
65
+ }
66
+
67
+ private handleDragStart() {
68
+ const figureId = this.node.attrs.id
69
+ FigureEditableView.currentDragFigureId = figureId
70
+ this.container.classList.add('dragging')
71
+ // Add drag-active to siblings only
72
+ const parent = this.container.parentElement
73
+ if (parent) {
74
+ const siblingFigures = parent.querySelectorAll('.figure')
75
+ siblingFigures.forEach((el) => {
76
+ if (el !== this.container) {
77
+ el.classList.add('drag-active')
78
+ }
79
+ })
80
+ }
81
+ }
82
+
83
+ private setupDragAndDrop() {
84
+ this.container.draggable = true
85
+
86
+ // Drag events for container
87
+ this.container.addEventListener('dragstart', () => {
88
+ // Only start figure dragging if we have a figure ID and figure is not deleted
89
+ if (this.node.attrs.id && !isDeleted(this.node)) {
90
+ this.handleDragStart()
91
+ }
92
+ })
93
+
94
+ // Drag events for drag handle (if present)
95
+ if (this.dragHandle) {
96
+ this.dragHandle.addEventListener('dragstart', () => {
97
+ this.handleDragStart()
98
+ })
99
+ }
100
+
101
+ this.container.addEventListener('dragend', () => {
102
+ // Clear the static variable when drag ends
103
+ FigureEditableView.currentDragFigureId = null
104
+ this.clearTargetClass(this.container, ['dragging'])
105
+ const parent = this.container.parentElement
106
+ if (parent) {
107
+ const figures = parent.querySelectorAll('.figure')
108
+ figures.forEach((el) => {
109
+ this.clearTargetClass(el, [
110
+ 'drag-active',
111
+ 'drop-target-above',
112
+ 'drop-target-below',
113
+ ])
114
+ })
115
+ }
116
+ // TODO: Check if setting decorations will work for this case
117
+ })
118
+
119
+ this.container.addEventListener('dragover', (e) => {
120
+ // Only handle figure drops when we're dragging a figure (to avoid conflicts with other drag events for image)
121
+ if (FigureEditableView.currentDragFigureId) {
122
+ e.preventDefault()
123
+ e.stopPropagation()
124
+ const rect = this.container.getBoundingClientRect()
125
+ const relativeY = e.clientY - rect.top
126
+ const isAbove = relativeY < rect.height / 2
127
+ this.clearTargetClass(this.container)
128
+ this.container.classList.add(
129
+ isAbove ? 'drop-target-above' : 'drop-target-below'
130
+ )
131
+ }
132
+ })
133
+
134
+ this.container.addEventListener('dragleave', (e) => {
135
+ if (!this.container.contains(e.relatedTarget as Node)) {
136
+ this.clearTargetClass(this.container)
137
+ }
138
+ })
139
+
140
+ this.container.addEventListener('drop', (e) => {
141
+ // Only handle figure drops when we're dragging a figure (to avoid conflicts with other drop events for image)
142
+ if (!FigureEditableView.currentDragFigureId) {
143
+ return
144
+ }
145
+
146
+ e.preventDefault()
147
+ e.stopPropagation()
148
+
149
+ // Get figure ID from static variable
150
+ const figureId = FigureEditableView.currentDragFigureId
151
+
152
+ if (!figureId) {
153
+ return
154
+ }
155
+
156
+ const figure = this.getFigureById(figureId)
157
+ if (!figure) {
158
+ return
159
+ }
160
+ const toPos = this.getPos()
161
+ if (figure.pos === toPos) {
162
+ return
163
+ } // prevent self-move
164
+
165
+ this.moveFigure(figure.pos, figure.node, toPos)
166
+ this.clearTargetClass(this.container)
167
+ })
168
+ }
169
+
170
+ private getFigureById(
171
+ figureId: string
172
+ ): { pos: number; node: ManuscriptNode } | null {
173
+ let result: { pos: number; node: ManuscriptNode } | null = null
174
+ this.view.state.doc.descendants((node, pos) => {
175
+ if (node.type === schema.nodes.figure && node.attrs.id === figureId) {
176
+ result = { pos, node }
177
+ return false // Stop descending into children
178
+ }
179
+ if (result) {
180
+ return false // Stop entire traversal since we found the result
181
+ }
182
+ })
183
+ return result
184
+ }
185
+
186
+ private moveFigure(
187
+ fromPos: number,
188
+ fromNode: ManuscriptNode,
189
+ targetPos: number
190
+ ) {
191
+ const { state } = this.view
192
+ const { tr } = state
193
+
194
+ tr.delete(fromPos, fromPos + fromNode.nodeSize)
195
+ tr.insert(this.view.state.tr.mapping.map(targetPos), fromNode)
196
+ this.view.dispatch(tr)
197
+ }
198
+
199
+ upload = async (file: File) => {
200
+ const result = await this.props.fileManagement.upload(file)
201
+ this.setSrc(result.id)
202
+ }
203
+
204
+ public updateContents() {
205
+ super.updateContents()
206
+ this.clearTargetClass(this.container, ['dragging'])
207
+
208
+ // Setup drag and drop if capabilities allow, not already initialized
209
+ if (
210
+ this.props.getCapabilities().editArticle &&
211
+ !this.dragAndDropInitialized
212
+ ) {
213
+ this.setupDragAndDrop()
214
+ this.dragAndDropInitialized = true
215
+ }
216
+
217
+ const src = this.node.attrs.src
218
+ const files = this.props.getFiles()
219
+ const file = src && files.filter((f) => f.id === src)[0]
220
+
221
+ this.container.innerHTML = ''
222
+
223
+ const can = this.props.getCapabilities()
224
+
225
+ const link = file && this.props.fileManagement.previewLink(file)
226
+ const img = link
227
+ ? this.createImg(link)
228
+ : file
229
+ ? this.createUnsupportedFormat(file.name)
230
+ : this.createPlaceholder()
231
+
232
+ if (can.uploadFile && !isDeleted(this.node)) {
233
+ addInteractionHandlers(img, this.upload, 'image/*')
234
+ }
235
+
236
+ this.container.innerHTML = ''
237
+ this.container.appendChild(img)
238
+
239
+ this.addTools()
240
+ }
241
+
242
+ public addTools() {
243
+ this.manageReactTools()
244
+
245
+ const existingDragHandlers =
246
+ this.container.querySelectorAll('.drag-handler')
247
+ existingDragHandlers.forEach((handler) => handler.remove())
248
+
249
+ const $pos = this.view.state.doc.resolve(this.getPos())
250
+
251
+ const parent = $pos.parent
252
+ // Create drag handle for figure elements with multiple figures (not simple image)
253
+ if (
254
+ this.props.getCapabilities()?.editArticle &&
255
+ parent.type === schema.nodes.figure_element &&
256
+ !isDeleted(this.node) &&
257
+ this.countFigures() > 1
258
+ ) {
259
+ const dragHandle = document.createElement('div')
260
+ dragHandle.className = 'drag-handler'
261
+ dragHandle.innerHTML = draggableIcon
262
+ dragHandle.draggable = true
263
+
264
+ dragHandle.addEventListener('mousedown', (e) => {
265
+ e.stopPropagation()
266
+ })
267
+
268
+ this.container.appendChild(dragHandle)
269
+ }
270
+ }
271
+
272
+ // Helper function to count non-deleted figures in current figure element
273
+ countFigures() {
274
+ const parent = findParentNodeOfTypeClosestToPos(
275
+ this.view.state.doc.resolve(this.getPos()),
276
+ schema.nodes.figure_element
277
+ )
278
+ let count = 0
279
+ parent?.node.descendants((node) => {
280
+ if (node.type === schema.nodes.figure && !isDeleted(node)) {
281
+ count++
282
+ }
283
+ })
284
+ return count
285
+ }
286
+
287
+ private manageReactTools() {
288
+ this.reactTools?.remove()
289
+
290
+ const handlers = createFileHandlers(
291
+ this.node.attrs,
292
+ 'src',
293
+ this.view,
294
+ this.props,
295
+ this.setSrc
296
+ )
297
+
298
+ const can = this.props.getCapabilities()
299
+ if (can.uploadFile) {
300
+ handlers.handleUpload = createFileUploader(this.upload, 'image/*')
301
+ }
302
+
303
+ if (can.detachFile) {
304
+ handlers.handleDelete = () => {
305
+ const pos = this.getPos()
306
+ const tr = this.view.state.tr
307
+ tr.delete(pos, pos + this.node.nodeSize)
308
+ this.view.dispatch(tr)
309
+ }
310
+ }
311
+ this.reactTools = createReactTools(
312
+ this.node,
313
+ this.view,
314
+ this.getPos,
315
+ this.props,
316
+ handlers,
317
+ false,
318
+ () => this.countFigures() > 1
319
+ )
320
+
321
+ if (this.reactTools) {
322
+ this.dom.insertBefore(this.reactTools, this.dom.firstChild)
323
+ }
324
+ }
325
+
326
+ protected setSrc = (src: string) => {
327
+ const { tr } = this.view.state
328
+ const pos = this.getPos()
329
+ tr.setNodeMarkup(pos, undefined, {
330
+ ...this.node.attrs,
331
+ src: src,
332
+ })
333
+ this.view.dispatch(tr)
334
+ }
335
+
336
+ private createUnsupportedFormat = (name: string) => {
337
+ return createUnsupportedFormat(
338
+ name,
339
+ this.props.getCapabilities().editArticle
340
+ )
341
+ }
342
+
343
+ protected createImg = (src: string) => {
344
+ const img = document.createElement('img')
345
+ img.classList.add('figure-image')
346
+ img.src = src
347
+ img.alt = 'figure image'
348
+ return img
349
+ }
350
+
351
+ protected createPlaceholder = () => {
352
+ return createMediaPlaceholder(
353
+ MediaType.Figure,
354
+ this.view,
355
+ this.getPos,
356
+ this.props
357
+ )
358
+ }
359
+ }
360
+
361
+ export default createEditableNodeView(FigureEditableView)
@@ -0,0 +1,197 @@
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 { schema } from '@manuscripts/transform'
18
+ import { Node } from 'prosemirror-model'
19
+ import { TextSelection } from 'prosemirror-state'
20
+
21
+ import { addAuthorIcon } from '../icons'
22
+ import { handleEnterKey } from '../lib/navigation-utils'
23
+ import { createNodeView } from './creators'
24
+ import { FigureEditableView } from './figure_editable'
25
+ import { ImageElementView } from './image_element'
26
+
27
+ export class FigureElementView extends ImageElementView {
28
+ public ignoreMutation = () => true
29
+ private addFigureBtn: HTMLButtonElement
30
+ private resizeObserver: ResizeObserver | null = null
31
+
32
+ public createElement = () => {
33
+ super.createElement()
34
+ this.addFigureElementButtons()
35
+ }
36
+
37
+ public initialise() {
38
+ super.initialise()
39
+ this.setupResizeObserver()
40
+ }
41
+
42
+ private setupResizeObserver() {
43
+ if (this.resizeObserver) {
44
+ this.resizeObserver.disconnect()
45
+ }
46
+
47
+ this.resizeObserver = new ResizeObserver(() => {
48
+ // Reposition button when figure dimensions change
49
+ requestAnimationFrame(() => this.updateButtonPosition())
50
+ })
51
+
52
+ // Observe all current figures
53
+ this.container.querySelectorAll('figure').forEach((figure) => {
54
+ this.resizeObserver?.observe(figure)
55
+ })
56
+ }
57
+
58
+ private addFigureElementButtons() {
59
+ if (this.props.getCapabilities()?.editArticle) {
60
+ this.addFigureBtn = Object.assign(document.createElement('button'), {
61
+ className: 'add-button',
62
+ innerHTML: addAuthorIcon,
63
+ title: 'Add figure',
64
+ })
65
+ this.addFigureBtn.addEventListener('click', () => this.addFigure())
66
+ this.addFigureBtn.addEventListener(
67
+ 'keydown',
68
+ handleEnterKey(() => this.addFigure())
69
+ )
70
+ this.addFigureBtn.tabIndex = 0
71
+ this.container.prepend(this.addFigureBtn)
72
+ }
73
+ }
74
+
75
+ private updateButtonPosition() {
76
+ if (!this.addFigureBtn) {
77
+ return
78
+ }
79
+
80
+ const figures = this.container.querySelectorAll('figure')
81
+ const lastFigure = figures[figures.length - 1] as HTMLElement
82
+
83
+ if (!lastFigure) {
84
+ return // No figures found
85
+ }
86
+
87
+ const lastFigureRect = lastFigure.getBoundingClientRect()
88
+ const containerRect = this.container.getBoundingClientRect()
89
+
90
+ // Calculate position relative to container
91
+ const relativeTop = lastFigureRect.bottom - containerRect.top + 20
92
+ this.addFigureBtn.style.top = `${relativeTop}px`
93
+ }
94
+
95
+ private updateAddButtonState() {
96
+ if (!this.addFigureBtn) {
97
+ return
98
+ }
99
+
100
+ // Check if there's already an empty figure placeholder
101
+ let hasEmptyFigure = false
102
+ this.node.forEach((node) => {
103
+ if (node.type === schema.nodes.figure) {
104
+ // Check if this figure is empty (no src or empty src)
105
+ const src = node.attrs.src || ''
106
+ if (src.trim().length === 0) {
107
+ hasEmptyFigure = true
108
+ }
109
+ }
110
+ })
111
+
112
+ // Disable button if there's already an empty figure
113
+ if (hasEmptyFigure) {
114
+ this.addFigureBtn.classList.add('disabled')
115
+ this.addFigureBtn.disabled = true
116
+ } else {
117
+ this.addFigureBtn.classList.remove('disabled')
118
+ this.addFigureBtn.disabled = false
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Updates button position and re-observes figures.
124
+ */
125
+ public update(node: Node): boolean {
126
+ const handledBySuper = super.update(node)
127
+
128
+ if (handledBySuper) {
129
+ this.setupResizeObserver() // Re-observe figures after node update
130
+ requestAnimationFrame(() => {
131
+ this.updateButtonPosition() // Reposition after DOM update
132
+ this.updateAddButtonState() // Update button state after DOM update
133
+ this.updateChildDragHandlers()
134
+ })
135
+ }
136
+
137
+ return handledBySuper
138
+ }
139
+
140
+ private updateChildDragHandlers() {
141
+ const dragHandlers = this.container.querySelectorAll('.drag-handler')
142
+ dragHandlers.forEach((handler) => handler.remove())
143
+
144
+ const figureElements = this.container.querySelectorAll('figure')
145
+ figureElements.forEach((figureElement) => {
146
+ const figureView = (
147
+ figureElement as HTMLElement & { __figureView?: FigureEditableView }
148
+ ).__figureView
149
+ figureView?.addTools()
150
+ })
151
+ }
152
+
153
+ public updateContents() {
154
+ super.updateContents()
155
+ requestAnimationFrame(() => {
156
+ this.updateButtonPosition()
157
+ this.updateAddButtonState()
158
+ })
159
+ }
160
+
161
+ private addFigure = () => {
162
+ const { state } = this.view
163
+ const { tr } = state
164
+ const figureElementPos = this.getPos()
165
+
166
+ let insertPos = figureElementPos + 1
167
+ let lastFigureEndPos = insertPos
168
+ let hasFigures = false
169
+
170
+ this.node.forEach((node) => {
171
+ if (node.type === schema.nodes.figure) {
172
+ lastFigureEndPos = insertPos + node.nodeSize
173
+ hasFigures = true
174
+ }
175
+ insertPos += node.nodeSize
176
+ })
177
+
178
+ const finalInsertPos = hasFigures ? lastFigureEndPos : figureElementPos + 1
179
+
180
+ const figureNode = state.schema.nodes.figure.create()
181
+
182
+ tr.insert(finalInsertPos, figureNode)
183
+ tr.setSelection(TextSelection.create(tr.doc, finalInsertPos + 1))
184
+ this.view.dispatch(tr.scrollIntoView())
185
+ }
186
+
187
+ public destroy() {
188
+ // Disconnect ResizeObserver on destroy
189
+ if (this.resizeObserver) {
190
+ this.resizeObserver.disconnect()
191
+ this.resizeObserver = null
192
+ }
193
+ super.destroy()
194
+ }
195
+ }
196
+
197
+ export default createNodeView(FigureElementView)
@@ -0,0 +1,21 @@
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 { createEditableNodeView } from './creators'
18
+ import { EditableBlock } from './editable_block'
19
+ import { FigureElementView } from './figure_element'
20
+
21
+ export default createEditableNodeView(EditableBlock(FigureElementView))