@manuscripts/body-editor 3.13.18 → 3.14.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 (42) hide show
  1. package/dist/cjs/commands.js +30 -2
  2. package/dist/cjs/components/views/DeleteSupplementDialog.js +75 -0
  3. package/dist/cjs/icons.js +7 -1
  4. package/dist/cjs/index.js +4 -1
  5. package/dist/cjs/lib/files.js +17 -5
  6. package/dist/cjs/lib/supplements.js +61 -0
  7. package/dist/cjs/versions.js +1 -1
  8. package/dist/cjs/views/caption_title.js +1 -0
  9. package/dist/cjs/views/hero_image.js +108 -1
  10. package/dist/cjs/views/supplement.js +21 -1
  11. package/dist/es/commands.js +26 -0
  12. package/dist/es/components/views/DeleteSupplementDialog.js +35 -0
  13. package/dist/es/icons.js +7 -1
  14. package/dist/es/index.js +2 -0
  15. package/dist/es/lib/files.js +17 -5
  16. package/dist/es/lib/supplements.js +55 -0
  17. package/dist/es/versions.js +1 -1
  18. package/dist/es/views/caption_title.js +1 -0
  19. package/dist/es/views/hero_image.js +109 -2
  20. package/dist/es/views/supplement.js +22 -2
  21. package/dist/types/commands.d.ts +2 -0
  22. package/dist/types/components/views/DeleteSupplementDialog.d.ts +2 -0
  23. package/dist/types/icons.d.ts +6 -0
  24. package/dist/types/index.d.ts +2 -0
  25. package/dist/types/lib/files.d.ts +2 -0
  26. package/dist/types/lib/supplements.d.ts +30 -0
  27. package/dist/types/versions.d.ts +1 -1
  28. package/dist/types/views/hero_image.d.ts +9 -2
  29. package/package.json +2 -2
  30. package/src/commands.ts +42 -0
  31. package/src/components/views/DeleteSupplementDialog.tsx +88 -0
  32. package/src/icons.ts +12 -0
  33. package/src/index.ts +2 -0
  34. package/src/lib/files.ts +20 -6
  35. package/src/lib/supplements.ts +97 -0
  36. package/src/versions.ts +1 -1
  37. package/src/views/caption_title.ts +2 -0
  38. package/src/views/hero_image.ts +144 -5
  39. package/src/views/supplement.ts +27 -2
  40. package/src/views/supplements.ts +2 -1
  41. package/styles/AdvancedEditor.css +57 -0
  42. package/styles/Editor.css +13 -0
@@ -0,0 +1,97 @@
1
+ /*!
2
+ * © 2026 Atypon Systems LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import {
18
+ ManuscriptEditorView,
19
+ ManuscriptNode,
20
+ schema,
21
+ SupplementNode,
22
+ } from '@manuscripts/transform'
23
+ import { findChildrenByType, findParentNodeClosestToPos } from 'prosemirror-utils'
24
+
25
+ import { allowedHref } from './url'
26
+
27
+ export type NodeWeblink = {
28
+ node: SupplementNode
29
+ pos: number
30
+ }
31
+
32
+ export const getSupplementDisplayLabel = (
33
+ node: SupplementNode,
34
+ files: { id: string; name: string }[]
35
+ ): string => {
36
+ const href = node.attrs.href
37
+ if (allowedHref(href)) {
38
+ return href
39
+ }
40
+
41
+ const file = files.find((f) => f.id === href)
42
+ return file?.name ?? 'Untitled supplement'
43
+ }
44
+
45
+ export const performDeleteSupplement = (
46
+ view: ManuscriptEditorView,
47
+ pos: number
48
+ ): boolean => {
49
+ const node = view.state.doc.nodeAt(pos)
50
+ if (!node || node.type !== schema.nodes.supplement) {
51
+ return false
52
+ }
53
+
54
+ const from = pos
55
+ const to = from + node.nodeSize
56
+ const { from: deleteFrom, to: deleteTo } = deleteSupplementAtPos(
57
+ view.state.doc,
58
+ from,
59
+ to
60
+ )
61
+ view.dispatch(view.state.tr.delete(deleteFrom, deleteTo))
62
+ return true
63
+ }
64
+
65
+ export const deleteSupplementAtPos = (
66
+ doc: ManuscriptNode,
67
+ from: number,
68
+ to: number
69
+ ) => {
70
+ const resolvedPos = doc.resolve(from)
71
+ const supplementsNodeWithPos = findParentNodeClosestToPos(
72
+ resolvedPos,
73
+ (node) => node.type === schema.nodes.supplements
74
+ )
75
+
76
+ if (!supplementsNodeWithPos) {
77
+ return { from, to, deleteWholeSection: false }
78
+ }
79
+
80
+ const { node: supplementsNode, pos: supplementsPos } = supplementsNodeWithPos
81
+
82
+ const supplements = findChildrenByType(
83
+ supplementsNode,
84
+ schema.nodes.supplement
85
+ )
86
+ const isDeletingLastSupplement = supplements.length === 1
87
+
88
+ if (isDeletingLastSupplement) {
89
+ return {
90
+ from: supplementsPos,
91
+ to: supplementsPos + supplementsNode.nodeSize,
92
+ deleteWholeSection: true,
93
+ }
94
+ }
95
+
96
+ return { from, to, deleteWholeSection: false }
97
+ }
package/src/versions.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  //THIS FILE WAS GENERATED BY versions.mjs
2
- export const VERSION = '3.13.18';
2
+ export const VERSION = '3.14.0';
3
3
  export const MATHJAX_VERSION = '3.2.2';
@@ -22,7 +22,9 @@ import { createNodeView } from './creators'
22
22
  export class CaptionTitleView extends BaseNodeView<CaptionTitleNode> {
23
23
  public initialise = () => {
24
24
  this.createDOM()
25
+ this.updateContents()
25
26
  }
27
+
26
28
  protected createDOM() {
27
29
  this.dom = document.createElement('label')
28
30
  this.dom.className = 'caption-title placeholder'
@@ -14,17 +14,55 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { FigureElementNode } from '@manuscripts/transform'
18
-
19
- import { arrowUp } from '../icons'
17
+ import { HeroImageNode } from '@manuscripts/transform'
18
+
19
+ import {
20
+ arrowUp,
21
+ leadingHalfLeftIcon,
22
+ leadingHeroImageIcon,
23
+ leadingLargeFloatIcon,
24
+ leadingSmallFloatIcon,
25
+ leadingWallpaperIcon,
26
+ } from '../icons'
20
27
  import { handleEnterKey } from '../lib/navigation-utils'
21
28
  import { Trackable } from '../types'
22
29
  import BlockView from './block_view'
23
30
  import { createNodeView } from './creators'
24
31
 
25
- export class HeroImageView extends BlockView<Trackable<FigureElementNode>> {
32
+ const LAYOUT_OPTIONS_CONFIG = [
33
+ {
34
+ label: 'Hero Image',
35
+ icon: leadingHeroImageIcon,
36
+ attr: 'leading',
37
+ },
38
+ {
39
+ label: 'Float Image - Small',
40
+ icon: leadingSmallFloatIcon,
41
+ attr: 'leading-small-float',
42
+ },
43
+ {
44
+ label: 'Float Image - Large',
45
+ icon: leadingLargeFloatIcon,
46
+ attr: 'leading-large-float',
47
+ },
48
+ {
49
+ label: 'Wallpaper Image',
50
+ icon: leadingWallpaperIcon,
51
+ attr: 'leading-wallpaper',
52
+ },
53
+ {
54
+ label: 'Half Image',
55
+ icon: leadingHalfLeftIcon,
56
+ attr: 'leading-half-left',
57
+ },
58
+ ]
59
+
60
+ export class HeroImageView extends BlockView<Trackable<HeroImageNode>> {
26
61
  private container: HTMLElement
62
+ private layoutOptions: HTMLElement
63
+ private abortController: AbortController
27
64
  private collapsed = false
65
+ private layoutPanelCollapsed = false
28
66
 
29
67
  public ignoreMutation = () => true
30
68
 
@@ -33,7 +71,17 @@ export class HeroImageView extends BlockView<Trackable<FigureElementNode>> {
33
71
  this.container.classList.add('block', 'hero-image-container')
34
72
  this.dom.appendChild(this.container)
35
73
 
74
+ this.abortController = new AbortController()
75
+
36
76
  this.container.appendChild(this.createPanel())
77
+ this.container.appendChild(this.createLayoutPanel())
78
+
79
+ this.layoutOptions = this.createLayoutOptions()
80
+ this.layoutOptions.addEventListener('change', this.onSelectOption, {
81
+ signal: this.abortController.signal,
82
+ })
83
+
84
+ this.container.appendChild(this.layoutOptions)
37
85
 
38
86
  this.contentDOM = document.createElement('figure')
39
87
  this.contentDOM.classList.add('figure-block', 'hero-image-figure')
@@ -65,12 +113,103 @@ export class HeroImageView extends BlockView<Trackable<FigureElementNode>> {
65
113
  }
66
114
 
67
115
  heroImageToggleBtn.onclick = handleToggle
68
- heroImageToggleBtn.addEventListener('keydown', handleEnterKey(handleToggle))
116
+ heroImageToggleBtn.addEventListener(
117
+ 'keydown',
118
+ handleEnterKey(handleToggle),
119
+ {
120
+ signal: this.abortController.signal,
121
+ }
122
+ )
69
123
 
70
124
  panel.appendChild(label)
71
125
  panel.appendChild(heroImageToggleBtn)
72
126
  return panel
73
127
  }
128
+
129
+ createLayoutPanel() {
130
+ const panel = document.createElement('div')
131
+ panel.classList.add('panel-header', 'layout-header')
132
+
133
+ const label = document.createElement('span')
134
+ label.textContent = 'Layout'
135
+ label.contentEditable = 'false'
136
+
137
+ const toggleBtn = document.createElement('button')
138
+ toggleBtn.classList.add('toggle-btn', 'button-reset')
139
+
140
+ toggleBtn.innerHTML = arrowUp
141
+ toggleBtn.classList.toggle('collapsed', this.layoutPanelCollapsed)
142
+
143
+ const handleToggle = () => {
144
+ this.layoutPanelCollapsed = !this.layoutPanelCollapsed
145
+ if (this.layoutOptions) {
146
+ this.layoutOptions.style.display = this.layoutPanelCollapsed
147
+ ? 'none'
148
+ : ''
149
+ }
150
+ toggleBtn.classList.toggle('collapsed', this.layoutPanelCollapsed)
151
+ }
152
+
153
+ toggleBtn.onclick = handleToggle
154
+ toggleBtn.addEventListener('keydown', handleEnterKey(handleToggle), {
155
+ signal: this.abortController.signal,
156
+ })
157
+
158
+ panel.appendChild(label)
159
+ panel.appendChild(toggleBtn)
160
+ return panel
161
+ }
162
+
163
+ createLayoutOptions() {
164
+ const can = this.props.getCapabilities()
165
+ const optionsGroup = document.createElement('div')
166
+ optionsGroup.className = 'layout-options-group'
167
+
168
+ LAYOUT_OPTIONS_CONFIG.forEach(({ label, icon, attr }) => {
169
+ const image = document.createElement('span')
170
+ image.className = 'layout-option-image'
171
+ image.innerHTML = icon
172
+
173
+ const input = document.createElement('input')
174
+ input.type = 'radio'
175
+ input.name = 'layout-option'
176
+ input.value = attr
177
+ input.disabled = !can.editMetadata
178
+ input.checked = attr === this.node.attrs.type
179
+
180
+ const inputLabel = document.createElement('span')
181
+ inputLabel.className = 'layout-option-label'
182
+ inputLabel.textContent = label
183
+
184
+ const wrapper = document.createElement('span')
185
+ wrapper.className = 'layout-option-label-wrapper'
186
+ wrapper.append(input, inputLabel)
187
+
188
+ const container = document.createElement('div')
189
+ container.className = 'layout-option'
190
+ container.append(image, wrapper)
191
+ optionsGroup.appendChild(container)
192
+ })
193
+ optionsGroup.contentEditable = 'false'
194
+ return optionsGroup
195
+ }
196
+
197
+ private onSelectOption = (e: Event) => {
198
+ const target = e.target as HTMLInputElement
199
+ if (target && target.type === 'radio' && target.name === 'layout-option') {
200
+ const { state, dispatch } = this.view
201
+ const tr = state.tr.setNodeMarkup(this.getPos(), undefined, {
202
+ ...this.node.attrs,
203
+ type: target.value,
204
+ })
205
+ dispatch(tr)
206
+ }
207
+ }
208
+
209
+ destroy() {
210
+ this.abortController?.abort()
211
+ super.destroy()
212
+ }
74
213
  }
75
214
 
76
215
  export default createNodeView(HeroImageView)
@@ -18,12 +18,14 @@ import { getFileIcon } from '@manuscripts/style-guide'
18
18
  import { SupplementNode } from '@manuscripts/transform'
19
19
  import { renderToStaticMarkup } from 'react-dom/server'
20
20
 
21
- import { draggableIcon } from '../icons'
21
+ import { draggableIcon, webLinkIcon } from '../icons'
22
22
  import { findNodeByID } from '../lib/doc'
23
+ import { allowedHref } from '../lib/url'
23
24
  import { Trackable } from '../types'
24
25
  import { BaseNodeView } from './base_node_view'
25
26
  import { createNodeView } from './creators'
26
27
 
28
+
27
29
  export class SupplementView extends BaseNodeView<Trackable<SupplementNode>> {
28
30
  private supplementInfoEl: HTMLDivElement
29
31
  private static currentDragSupplementId: string | null = null
@@ -188,9 +190,32 @@ export class SupplementView extends BaseNodeView<Trackable<SupplementNode>> {
188
190
  this.supplementInfoEl.classList.add('supplement-file-info')
189
191
  this.supplementInfoEl.contentEditable = 'false'
190
192
 
193
+ const href = this.node.attrs.href
194
+
195
+ if (allowedHref(href)) {
196
+ const iconElement = document.createElement('span')
197
+ iconElement.classList.add('supplement-file-icon')
198
+ iconElement.innerHTML = webLinkIcon
199
+ this.supplementInfoEl.appendChild(iconElement)
200
+
201
+ const urlLink = document.createElement('a')
202
+ urlLink.classList.add('supplement-weblink-url')
203
+ urlLink.textContent = href
204
+ if (allowedHref(href)) {
205
+ urlLink.href = href
206
+ urlLink.target = '_blank'
207
+ urlLink.rel = 'noopener noreferrer'
208
+ }
209
+ urlLink.addEventListener('mousedown', (e) => e.stopPropagation())
210
+ this.supplementInfoEl.appendChild(urlLink)
211
+
212
+ this.dom.appendChild(this.supplementInfoEl)
213
+ return
214
+ }
215
+
191
216
  // Get the file from the file management system
192
217
  const files = this.props.getFiles()
193
- const file = files.find((f) => f.id === this.node.attrs.href)
218
+ const file = files.find((f) => f.id === href)
194
219
 
195
220
  if (file) {
196
221
  const iconElement = document.createElement('span')
@@ -32,6 +32,7 @@ export class SupplementsView extends BlockView<Trackable<SupplementsNode>> {
32
32
  this.toggleButton = document.createElement('button')
33
33
  this.toggleButton.classList.add('supplements-toggle-btn', 'button-reset')
34
34
  this.toggleButton.innerHTML = arrowUp
35
+
35
36
  const handleToggle = () => {
36
37
  this.collapsed = !this.collapsed
37
38
  this.toggleContent()
@@ -62,4 +63,4 @@ export class SupplementsView extends BlockView<Trackable<SupplementsNode>> {
62
63
  }
63
64
  }
64
65
 
65
- export default createNodeView(SupplementsView)
66
+ export default createNodeView(SupplementsView)
@@ -1754,6 +1754,63 @@ th:hover > .table-context-menu-button,
1754
1754
  border-bottom: 1px solid #c9c9c9;
1755
1755
  }
1756
1756
 
1757
+ .ProseMirror .layout-header {
1758
+ font-size: 18px;
1759
+ font-weight: 400;
1760
+ padding: 4px 3px 4px 0;
1761
+ margin: 8px;
1762
+ background: unset;
1763
+ border: none;
1764
+ }
1765
+
1766
+ .ProseMirror .layout-options-group {
1767
+ display: flex;
1768
+ justify-content: center;
1769
+ gap: 16px;
1770
+ padding: 16px 0;
1771
+ margin: 0 8px;
1772
+ border-bottom: 1px solid #e2e2e2;
1773
+ }
1774
+
1775
+ .ProseMirror .layout-option-image {
1776
+ width: 120px;
1777
+ height: 114px;
1778
+ margin-bottom: 8px;
1779
+ border-radius: 5px 5px 9px 9px;
1780
+ border: 1px solid #e2e2e2;
1781
+ background: #fafafa;
1782
+ display: flex;
1783
+ align-items: center;
1784
+ justify-content: center;
1785
+ }
1786
+
1787
+ .ProseMirror .layout-options-group .layout-option-label-wrapper {
1788
+ display: flex;
1789
+ width: 120px;
1790
+ align-items: flex-start;
1791
+ gap: 6px;
1792
+ }
1793
+
1794
+ .ProseMirror .layout-options-group input[type="radio"] {
1795
+ accent-color: #1a9bc7;
1796
+ width: 16px;
1797
+ height: 16px;
1798
+ }
1799
+
1800
+ .ProseMirror .layout-options-group input[type="radio"]:focus-visible {
1801
+ outline: 1px solid var(--focus-outline-color);
1802
+ outline-offset: 2px;
1803
+ }
1804
+
1805
+ .ProseMirror .layout-option-label {
1806
+ color: #353535;
1807
+ font-size: 14px;
1808
+ font-style: normal;
1809
+ font-weight: 400;
1810
+ line-height: 24px;
1811
+ user-select: none;
1812
+ }
1813
+
1757
1814
  .hero-image-container .figure-block {
1758
1815
  border: none !important;
1759
1816
  padding: 8px;
package/styles/Editor.css CHANGED
@@ -1248,6 +1248,17 @@
1248
1248
  font-size: 14px;
1249
1249
  }
1250
1250
 
1251
+ .ProseMirror .supplement-weblink-url {
1252
+ font-size: 14px;
1253
+ color: #20aedf;
1254
+ text-decoration: none;
1255
+ word-break: break-all;
1256
+ }
1257
+
1258
+ .ProseMirror .supplement-weblink-url:hover {
1259
+ text-decoration: underline;
1260
+ }
1261
+
1251
1262
  .ProseMirror .supplements-toggle-btn {
1252
1263
  background: none;
1253
1264
  border: none;
@@ -1300,6 +1311,8 @@
1300
1311
  padding: 8px 12px !important;
1301
1312
  font-size: 14px;
1302
1313
  display: flex;
1314
+ align-items: center;
1315
+ gap: 8px;
1303
1316
  margin-top: -1px;
1304
1317
  color: #6e6e6e !important;
1305
1318
  }