@manuscripts/body-editor 3.16.0 → 3.16.2

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 (99) hide show
  1. package/dist/cjs/commands.js +45 -10
  2. package/dist/cjs/components/form/FormFooter.js +2 -2
  3. package/dist/cjs/components/keyboard-shortcuts-modal/KeyboardShortcutsModal.js +4 -2
  4. package/dist/cjs/components/references/CitationEditor.js +13 -19
  5. package/dist/cjs/components/references/ImportBibliography.js +77 -0
  6. package/dist/cjs/components/references/ImportBibliographyForm.js +1 -1
  7. package/dist/cjs/components/references/ImportBibliographyModal.js +12 -81
  8. package/dist/cjs/components/references/ImportReferencesConfirmation.js +113 -0
  9. package/dist/cjs/components/references/ImportSuccessPlaceholder.js +56 -0
  10. package/dist/cjs/components/references/ReferenceSearch.js +2 -5
  11. package/dist/cjs/components/references/ReferencesEditor.js +18 -2
  12. package/dist/cjs/components/references/ReferencesModal.js +86 -33
  13. package/dist/cjs/components/views/CrossReferenceItems.js +139 -52
  14. package/dist/cjs/components/views/DeleteSupplementDialog.js +1 -1
  15. package/dist/cjs/lib/supplements.js +9 -1
  16. package/dist/cjs/lib/view.js +39 -1
  17. package/dist/cjs/menus.js +35 -0
  18. package/dist/cjs/plugins/objects.js +1 -1
  19. package/dist/cjs/toolbar.js +6 -0
  20. package/dist/cjs/versions.js +1 -1
  21. package/dist/cjs/views/bibliography_element.js +1 -2
  22. package/dist/cjs/views/citation_editable.js +2 -29
  23. package/dist/cjs/views/cross_reference.js +12 -2
  24. package/dist/cjs/views/cross_reference_editable.js +95 -4
  25. package/dist/cjs/views/figure_editable.js +3 -0
  26. package/dist/es/commands.js +43 -9
  27. package/dist/es/components/form/FormFooter.js +2 -2
  28. package/dist/es/components/keyboard-shortcuts-modal/KeyboardShortcutsModal.js +4 -2
  29. package/dist/es/components/references/CitationEditor.js +13 -19
  30. package/dist/es/components/references/ImportBibliography.js +70 -0
  31. package/dist/es/components/references/ImportBibliographyForm.js +1 -1
  32. package/dist/es/components/references/ImportBibliographyModal.js +13 -79
  33. package/dist/es/components/references/ImportReferencesConfirmation.js +73 -0
  34. package/dist/es/components/references/ImportSuccessPlaceholder.js +49 -0
  35. package/dist/es/components/references/ReferenceSearch.js +3 -6
  36. package/dist/es/components/references/ReferencesEditor.js +19 -3
  37. package/dist/es/components/references/ReferencesModal.js +88 -35
  38. package/dist/es/components/views/CrossReferenceItems.js +141 -54
  39. package/dist/es/components/views/DeleteSupplementDialog.js +2 -2
  40. package/dist/es/lib/supplements.js +8 -1
  41. package/dist/es/lib/view.js +36 -0
  42. package/dist/es/menus.js +36 -1
  43. package/dist/es/plugins/objects.js +1 -1
  44. package/dist/es/toolbar.js +8 -2
  45. package/dist/es/versions.js +1 -1
  46. package/dist/es/views/bibliography_element.js +2 -3
  47. package/dist/es/views/citation_editable.js +3 -30
  48. package/dist/es/views/cross_reference.js +12 -2
  49. package/dist/es/views/cross_reference_editable.js +96 -5
  50. package/dist/es/views/figure_editable.js +3 -0
  51. package/dist/types/commands.d.ts +1 -0
  52. package/dist/types/components/form/FormFooter.d.ts +2 -1
  53. package/dist/types/components/references/CitationEditor.d.ts +1 -1
  54. package/dist/types/components/references/ImportBibliography.d.ts +7 -0
  55. package/dist/types/components/references/ImportReferencesConfirmation.d.ts +8 -0
  56. package/dist/types/components/references/ImportSuccessPlaceholder.d.ts +5 -0
  57. package/dist/types/components/references/ReferenceSearch.d.ts +0 -1
  58. package/dist/types/components/references/ReferencesEditor.d.ts +4 -1
  59. package/dist/types/components/references/ReferencesModal.d.ts +1 -0
  60. package/dist/types/components/views/CrossReferenceItems.d.ts +3 -0
  61. package/dist/types/lib/supplements.d.ts +1 -0
  62. package/dist/types/lib/view.d.ts +3 -1
  63. package/dist/types/versions.d.ts +1 -1
  64. package/dist/types/views/citation_editable.d.ts +0 -1
  65. package/dist/types/views/cross_reference_editable.d.ts +6 -0
  66. package/package.json +4 -4
  67. package/src/commands.ts +52 -10
  68. package/src/components/ChangeHandlingForm.tsx +4 -2
  69. package/src/components/form/FormFooter.tsx +3 -1
  70. package/src/components/keyboard-shortcuts-modal/KeyboardShortcutsModal.tsx +6 -7
  71. package/src/components/references/CitationEditor.tsx +16 -25
  72. package/src/components/references/ImportBibliography.tsx +119 -0
  73. package/src/components/references/ImportBibliographyForm.tsx +1 -1
  74. package/src/components/references/ImportBibliographyModal.tsx +34 -110
  75. package/src/components/references/ImportReferencesConfirmation.tsx +133 -0
  76. package/src/components/references/ImportSuccessPlaceholder.tsx +93 -0
  77. package/src/components/references/ReferenceSearch.tsx +0 -7
  78. package/src/components/references/ReferencesEditor.tsx +35 -6
  79. package/src/components/references/ReferencesModal.tsx +164 -78
  80. package/src/components/toolbar/InsertEmbedDialog.tsx +6 -2
  81. package/src/components/toolbar/type-selector/styles.ts +2 -1
  82. package/src/components/views/CrossReferenceItems.tsx +227 -100
  83. package/src/components/views/DeleteSupplementDialog.tsx +3 -6
  84. package/src/lib/footnotes.ts +3 -1
  85. package/src/lib/supplements.ts +13 -1
  86. package/src/lib/utils.ts +15 -4
  87. package/src/lib/view.ts +65 -0
  88. package/src/menus.tsx +39 -0
  89. package/src/plugins/add-subtitle.ts +18 -16
  90. package/src/plugins/objects.ts +2 -6
  91. package/src/toolbar.tsx +9 -0
  92. package/src/versions.ts +1 -1
  93. package/src/views/bibliography_element.ts +3 -4
  94. package/src/views/citation_editable.ts +5 -50
  95. package/src/views/cross_reference.ts +17 -3
  96. package/src/views/cross_reference_editable.ts +123 -11
  97. package/src/views/figure_editable.ts +4 -0
  98. package/src/views/supplement.ts +0 -1
  99. package/src/views/supplements.ts +2 -2
@@ -15,96 +15,147 @@
15
15
  */
16
16
 
17
17
  import {
18
- ButtonGroup,
18
+ CloseButton,
19
+ ExpandableSection,
19
20
  PrimaryButton,
20
- SecondaryButton,
21
- TextArea,
22
- withFocusTrap,
23
21
  withListNavigation,
24
22
  withNavigableListItem,
23
+ FileUnknownIcon,
24
+ getFileIcon,
25
+ ArrowDownIcon,
26
+ StyledModal,
27
+ ModalContainer,
28
+ ModalHeader,
29
+ ModalBody,
30
+ WebLinkIcon,
25
31
  } from '@manuscripts/style-guide'
26
- import { Target } from '@manuscripts/transform'
27
- import React, { useEffect, useRef, useState } from 'react'
32
+ import { Target, schema } from '@manuscripts/transform'
33
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
28
34
  import styled from 'styled-components'
29
35
 
30
- const Container = withFocusTrap(styled.div`
31
- padding: ${(props) => props.theme.grid.unit * 3}px
32
- ${(props) => props.theme.grid.unit * 4}px;
33
- display: flex;
34
- flex-direction: column;
35
- max-height: 60vh;
36
- overflow: hidden;
37
- `)
38
-
39
- const Actions = styled.div`
40
- display: flex;
41
- align-items: center;
42
- justify-content: flex-end;
43
- flex-shrink: 0;
44
- `
36
+ import { FileAttachment } from '../../lib/files'
37
+ import { allowedHref } from '../../lib/url'
38
+ import { nodeTypeIcon } from '../../node-type-icons'
39
+ import FormFooter from '../form/FormFooter'
45
40
 
46
41
  const Items = withListNavigation(styled.div`
47
42
  flex: 1;
48
43
  overflow-y: auto;
49
- padding: ${(props) => props.theme.grid.unit * 3}px;
50
44
  `)
51
45
 
52
- const CrossReferenceItem = withNavigableListItem(styled.div<{
53
- $isSelected: boolean
54
- }>`
46
+ const CrossReferenceItem = withNavigableListItem(styled.div`
55
47
  cursor: pointer;
56
- padding: ${(props) => props.theme.grid.unit * 4}px;
57
- background-color: ${(props) =>
58
- props.$isSelected
59
- ? props.theme.colors.background.selected
60
- : props.theme.colors.background.primary};
48
+ padding: ${(props) => props.theme.grid.unit * 2}px;
49
+ background-color: ${(props) => props.theme.colors.background.primary};
61
50
  transition: background-color 0.1s;
62
- border: solid
63
- ${(props) =>
64
- props.$isSelected
65
- ? props.theme.colors.brand.medium
66
- : props.theme.colors.border.secondary};
67
- border-width: 1px 0;
68
- z-index: ${(props) => (props.$isSelected ? '1' : '0')};
51
+ border: 1px solid ${(props) => props.theme.colors.border.secondary};
52
+ overflow: hidden;
53
+ max-width: 100%;
54
+ box-sizing: border-box;
55
+ margin-bottom: ${(props) => props.theme.grid.unit * 2}px;
69
56
 
70
57
  &:hover {
71
- background-color: ${(props) => props.theme.colors.background.selected};
58
+ background-color: #f2f2f2;
59
+ }
60
+
61
+ &.active {
62
+ background-color: #f2f2f2;
63
+ z-index: 1;
72
64
  }
73
65
  `)
74
66
 
75
67
  const Label = styled.span`
76
68
  color: ${(props) => props.theme.colors.text.primary};
69
+ -webkit-line-clamp: 2;
70
+ -webkit-box-orient: vertical;
71
+ overflow: hidden;
72
+ word-break: break-all;
73
+ overflow-wrap: break-word;
74
+ min-width: fit-content;
77
75
  `
78
76
 
79
77
  const Caption = styled.span`
80
78
  color: ${(props) => props.theme.colors.text.secondary};
79
+ display: -webkit-box;
80
+ -webkit-line-clamp: 2;
81
+ -webkit-box-orient: vertical;
82
+ overflow: hidden;
83
+ word-break: break-all;
84
+ overflow-wrap: break-word;
85
+ margin-left: 3.2px;
81
86
  `
82
87
 
83
88
  const Heading = styled.div`
84
- display: flex;
85
- align-items: center;
86
- justify-content: space-between;
87
- flex-shrink: 0;
89
+ font-size: ${(props) => props.theme.font.size.medium};
88
90
  font-weight: ${(props) => props.theme.font.weight.bold};
91
+ color: ${(props) => props.theme.colors.text.primary};
92
+ margin-bottom: ${(props) => props.theme.grid.unit * 3}px;
89
93
  `
90
94
 
91
95
  const Empty = styled.div`
92
96
  margin-bottom: ${(props) => props.theme.grid.unit * 4}px;
93
97
  color: ${(props) => props.theme.colors.text.tertiary};
94
98
  `
99
+
100
+ const GroupExpandableSection = styled(ExpandableSection)`
101
+ & > div:first-child {
102
+ flex-direction: row-reverse;
103
+ justify-content: flex-end;
104
+ padding-left: 0;
105
+ gap: 10px;
106
+ font-weight: ${(props) => props.theme.font.weight.normal};
107
+ svg {
108
+ width: 20px;
109
+ height: 20px;
110
+ }
111
+ }
112
+ `
95
113
  const DefaultLabelWrapper = styled.div`
96
- margin-bottom: ${(props) => props.theme.grid.unit * 2}px;
114
+ display: flex;
115
+ align-items: center;
116
+ min-width: 0;
97
117
  `
98
118
 
99
- const CustomTextArea = styled(TextArea)`
100
- width: 100%;
101
- height: 75px;
119
+ const ItemIcon = styled.span`
120
+ flex-shrink: 0;
121
+ display: flex;
122
+ align-items: center;
123
+ color: ${(props) => props.theme.colors.text.secondary};
124
+ margin-right: ${(props) => props.theme.grid.unit * 2}px;
125
+ svg {
126
+ width: 20px;
127
+ height: 20px;
128
+ }
129
+ `
130
+
131
+ const FieldLabel = styled.label`
132
+ font-size: ${(props) => props.theme.font.size.normal};
133
+ color: ${(props) => props.theme.colors.text.secondary};
134
+ margin-top: ${(props) => props.theme.grid.unit * 4}px;
135
+ margin-bottom: ${(props) => props.theme.grid.unit}px;
136
+ `
137
+
138
+ const CustomTextInput = styled.input`
102
139
  color: ${(props) => props.theme.colors.text.secondary};
103
- background-color: ${(props) =>
104
- props.theme.colors.background.primary} !important;
140
+ background-color: ${(props) => props.theme.colors.background.primary};
105
141
  border: 1px solid ${(props) => props.theme.colors.border.secondary};
106
142
  border-radius: ${(props) => props.theme.grid.radius.small};
107
143
  padding: ${(props) => props.theme.grid.unit * 2}px;
144
+ &:disabled {
145
+ cursor: not-allowed;
146
+ background-color: ${(props) => props.theme.colors.background.disabled};
147
+ color: ${(props) => props.theme.colors.text.muted};
148
+ }
149
+ `
150
+ const StyledModalBody = styled(ModalBody)`
151
+ display: flex;
152
+ flex-direction: column;
153
+ padding: ${(props) => props.theme.grid.unit * 5}px
154
+ ${(props) => props.theme.grid.unit * 6}px;
155
+ max-height: 580px;
156
+ min-width: 600px;
157
+ max-width: 600px;
158
+ overflow: hidden;
108
159
  `
109
160
 
110
161
  // trim a caption, avoiding cutting words
@@ -118,23 +169,54 @@ const trimmedCaption = (caption: string, limit: number): string => {
118
169
  return caption.replace(captionSearch, '$1…')
119
170
  }
120
171
 
172
+ const getTargetIcon = (
173
+ target: Target,
174
+ files: FileAttachment[]
175
+ ): React.ReactNode => {
176
+ if (target.type === schema.nodes.supplement.name) {
177
+ if (target.href && allowedHref(target.href)) {
178
+ return <WebLinkIcon className="file-icon" />
179
+ }
180
+ const fileName =
181
+ files.find((f) => f.id === target.href)?.name ?? target.label ?? ''
182
+ return getFileIcon(fileName) ?? <FileUnknownIcon />
183
+ }
184
+
185
+ return nodeTypeIcon(schema.nodes[target.type])
186
+ }
187
+
188
+ const GROUP_LABELS: Record<string, string> = {
189
+ figure_element: 'Figures',
190
+ table_element: 'Tables',
191
+ equation_element: 'Equations',
192
+ listing_element: 'Listings',
193
+ box_element: 'Boxes',
194
+ embed: 'Media',
195
+ supplement: 'Supplementary files',
196
+ }
197
+
121
198
  interface Props {
122
199
  targets: Target[]
200
+ files: FileAttachment[]
123
201
  handleSelect: (id: string, customLabel?: string) => void
124
202
  handleCancel: () => void
125
203
  currentTargetId?: string
126
204
  currentCustomLabel?: string
205
+ isEdit?: boolean
127
206
  }
128
207
 
129
208
  export const CrossReferenceItems: React.FC<Props> = ({
130
209
  targets,
210
+ files,
131
211
  handleSelect,
132
212
  handleCancel,
133
213
  currentTargetId,
134
214
  currentCustomLabel,
215
+ isEdit = false,
135
216
  }) => {
217
+ const [isOpen, setIsOpen] = useState(true)
136
218
  const [selectedItem, setSelectedItem] = useState<string>('')
137
- const customTextRef = useRef<HTMLTextAreaElement>(null)
219
+ const customTextRef = useRef<HTMLInputElement>(null)
138
220
 
139
221
  useEffect(() => {
140
222
  if (currentTargetId) {
@@ -142,58 +224,103 @@ export const CrossReferenceItems: React.FC<Props> = ({
142
224
  }
143
225
  }, [currentTargetId])
144
226
 
227
+ const close = useCallback(() => {
228
+ setIsOpen(false)
229
+ handleCancel()
230
+ }, [handleCancel])
231
+
232
+ const grouped = targets.reduce<Record<string, Target[]>>((acc, t) => {
233
+ ;(acc[t.type] ??= []).push(t)
234
+ return acc
235
+ }, {})
236
+
145
237
  return (
146
- <Container>
147
- <Heading>Insert Cross-reference</Heading>
148
-
149
- <Items>
150
- {targets.length ? (
151
- targets.map((target) => (
152
- <CrossReferenceItem
153
- key={target.id}
154
- $isSelected={selectedItem === target.id}
155
- onClick={() => setSelectedItem(target.id)}
156
- >
157
- <DefaultLabelWrapper>
158
- <Label>{target.label}</Label>
159
- <Caption>
160
- {target.caption && ': ' + trimmedCaption(target.caption, 200)}
161
- </Caption>
162
- </DefaultLabelWrapper>
163
- {selectedItem === target.id && (
164
- <CustomTextArea
165
- ref={customTextRef}
166
- placeholder={'Or type custom text'}
167
- defaultValue={
168
- currentTargetId &&
169
- currentTargetId === selectedItem &&
170
- currentCustomLabel
171
- ? currentCustomLabel
172
- : ''
173
- }
174
- />
175
- )}
176
- </CrossReferenceItem>
177
- ))
178
- ) : (
179
- <Empty>No cross-reference targets available.</Empty>
180
- )}
181
- </Items>
182
-
183
- <Actions>
184
- <ButtonGroup>
185
- <SecondaryButton onClick={handleCancel}>Cancel</SecondaryButton>
186
- <PrimaryButton
187
- onClick={() =>
188
- selectedItem &&
189
- handleSelect(selectedItem, customTextRef.current?.value || '')
190
- }
238
+ <StyledModal
239
+ isOpen={isOpen}
240
+ onRequestClose={close}
241
+ shouldCloseOnOverlayClick={true}
242
+ >
243
+ <ModalContainer data-cy="cross-reference-editor">
244
+ <ModalHeader>
245
+ <CloseButton onClick={close} />
246
+ </ModalHeader>
247
+ <StyledModalBody>
248
+ <Heading>
249
+ {isEdit ? 'Edit Cross-reference' : 'Insert Cross-reference'}
250
+ </Heading>
251
+
252
+ <FieldLabel htmlFor="cross-reference-custom-text">
253
+ Custom display text
254
+ </FieldLabel>
255
+ <CustomTextInput
256
+ ref={customTextRef}
257
+ id="cross-reference-custom-text"
258
+ data-cy="cross-reference-custom-text"
259
+ type="text"
260
+ placeholder={'Custom display text...'}
191
261
  disabled={!selectedItem}
192
- >
193
- Insert
194
- </PrimaryButton>
195
- </ButtonGroup>
196
- </Actions>
197
- </Container>
262
+ defaultValue={currentCustomLabel ?? ''}
263
+ />
264
+
265
+ <Items>
266
+ {Object.keys(grouped).length ? (
267
+ Object.entries(grouped).map(([type, group]) => (
268
+ <GroupExpandableSection
269
+ key={type}
270
+ title={GROUP_LABELS[type]}
271
+ icon={ArrowDownIcon}
272
+ >
273
+ {group.map((target) => {
274
+ const icon = getTargetIcon(target, files)
275
+
276
+ return (
277
+ <CrossReferenceItem
278
+ key={target.id}
279
+ data-cy="cross-reference-item"
280
+ className={
281
+ selectedItem === target.id ? 'active' : undefined
282
+ }
283
+ onClick={() => setSelectedItem(target.id)}
284
+ >
285
+ <DefaultLabelWrapper>
286
+ {icon ? <ItemIcon>{icon}</ItemIcon> : null}
287
+ <Label>
288
+ {target.label}
289
+ {target.caption && `:`}
290
+ </Label>
291
+ <Caption>
292
+ {trimmedCaption(target.caption, 200)}
293
+ </Caption>
294
+ </DefaultLabelWrapper>
295
+ </CrossReferenceItem>
296
+ )
297
+ })}
298
+ </GroupExpandableSection>
299
+ ))
300
+ ) : (
301
+ <Empty>No cross-reference targets available.</Empty>
302
+ )}
303
+ </Items>
304
+ </StyledModalBody>
305
+ <FormFooter
306
+ onCancel={close}
307
+ cancelLabel="Cancel"
308
+ primaryAction={
309
+ <PrimaryButton
310
+ onClick={() => {
311
+ if (selectedItem) {
312
+ setIsOpen(false)
313
+ const customLabel = customTextRef.current?.value
314
+ handleSelect(selectedItem, customLabel || '')
315
+ }
316
+ }}
317
+ disabled={!selectedItem}
318
+ >
319
+ {isEdit ? 'Update' : 'Insert'}
320
+ </PrimaryButton>
321
+ }
322
+ />
323
+ </ModalContainer>
324
+ </StyledModal>
198
325
  )
199
326
  }
@@ -13,10 +13,7 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import {
17
- Category,
18
- Dialog,
19
- } from '@manuscripts/style-guide'
16
+ import { Category, Dialog } from '@manuscripts/style-guide'
20
17
  import { ManuscriptEditorView, SupplementNode } from '@manuscripts/transform'
21
18
  import React, { useState } from 'react'
22
19
 
@@ -37,8 +34,8 @@ const DeleteSupplementDialog: React.FC<{
37
34
  <Dialog
38
35
  isOpen={isOpen}
39
36
  category={Category.confirmation}
40
- header='Delete supplement'
41
- message='Are you sure you want to delete &ldquo;{label}&rdquo;?'
37
+ header="Delete supplement"
38
+ message="Are you sure you want to delete &ldquo;{label}&rdquo;?"
42
39
  actions={{
43
40
  primary: {
44
41
  action: () => {
@@ -43,7 +43,9 @@ export const findFootnotesContainerNode = (
43
43
  )
44
44
  }
45
45
 
46
- export const findParentFootnote: (selection: Selection) => ContentNodeWithPos | undefined = findParentNodeOfType([
46
+ export const findParentFootnote: (
47
+ selection: Selection
48
+ ) => ContentNodeWithPos | undefined = findParentNodeOfType([
47
49
  schema.nodes.footnote,
48
50
  schema.nodes.general_table_footnote,
49
51
  ])
@@ -20,7 +20,10 @@ import {
20
20
  schema,
21
21
  SupplementNode,
22
22
  } from '@manuscripts/transform'
23
- import { findChildrenByType, findParentNodeClosestToPos } from 'prosemirror-utils'
23
+ import {
24
+ findChildrenByType,
25
+ findParentNodeClosestToPos,
26
+ } from 'prosemirror-utils'
24
27
 
25
28
  import { allowedHref } from './url'
26
29
 
@@ -29,10 +32,19 @@ export type NodeWeblink = {
29
32
  pos: number
30
33
  }
31
34
 
35
+ export const getSupplementCaptionTitle = (node: SupplementNode): string => {
36
+ return node.firstChild?.textContent.trim() ?? ''
37
+ }
38
+
32
39
  export const getSupplementDisplayLabel = (
33
40
  node: SupplementNode,
34
41
  files: { id: string; name: string }[]
35
42
  ): string => {
43
+ const captionTitle = getSupplementCaptionTitle(node)
44
+ if (captionTitle) {
45
+ return captionTitle
46
+ }
47
+
36
48
  const href = node.attrs.href
37
49
  if (allowedHref(href)) {
38
50
  return href
package/src/lib/utils.ts CHANGED
@@ -89,15 +89,26 @@ export const getChildOfType = (
89
89
  ): boolean =>
90
90
  !!getMatchingChild(parent, (node) => node.type.name === nodeType.name, deep)
91
91
 
92
- export const findParentNodeWithId: (selection: Selection) => ContentNodeWithPos | undefined = findParentNode((node) => 'id' in node.attrs)
92
+ export const findParentNodeWithId: (
93
+ selection: Selection
94
+ ) => ContentNodeWithPos | undefined = findParentNode(
95
+ (node) => 'id' in node.attrs
96
+ )
93
97
 
94
- export const findParentNodeWithIdValue: (selection: Selection) => ContentNodeWithPos | undefined = findParentNode((node) => node.attrs.id)
98
+ export const findParentNodeWithIdValue: (
99
+ selection: Selection
100
+ ) => ContentNodeWithPos | undefined = findParentNode((node) => node.attrs.id)
95
101
 
96
- export const findParentSection: (selection: Selection) => ContentNodeWithPos | undefined = findParentNode((node) =>
102
+ export const findParentSection: (
103
+ selection: Selection
104
+ ) => ContentNodeWithPos | undefined = findParentNode((node) =>
97
105
  isSectionNodeType(node.type)
98
106
  )
99
107
 
100
- export const findParentElement = (selection: Selection, validIds?: string[]): ContentNodeWithPos | undefined =>
108
+ export const findParentElement = (
109
+ selection: Selection,
110
+ validIds?: string[]
111
+ ): ContentNodeWithPos | undefined =>
101
112
  findParentNode((node) => {
102
113
  // if validIds was passed and this element is not in it, then keep looking
103
114
  if (validIds && !validIds.includes(node.attrs.id)) {
package/src/lib/view.ts CHANGED
@@ -15,7 +15,9 @@
15
15
  */
16
16
 
17
17
  import {
18
+ BibliographyItemAttrs,
18
19
  ManuscriptEditorView,
20
+ ManuscriptNode,
19
21
  ManuscriptNodeType,
20
22
  schema,
21
23
  } from '@manuscripts/transform'
@@ -98,3 +100,66 @@ export const deleteNode = (view: ManuscriptEditorView, id: string) => {
98
100
  view.dispatch(view.state.tr.delete(pos, pos + node.nodeSize))
99
101
  }
100
102
  }
103
+
104
+ const createBibliographySection = (nodes: ManuscriptNode[]) =>
105
+ schema.nodes.bibliography_section.createAndFill({}, [
106
+ schema.nodes.section_title.create({}, schema.text('References')),
107
+ schema.nodes.bibliography_element.create({}, nodes),
108
+ ]) as ManuscriptNode
109
+
110
+ export const insertBibliographyItems = (
111
+ view: ManuscriptEditorView,
112
+ items: BibliographyItemAttrs[]
113
+ ) => {
114
+ const { doc } = view.state
115
+ let tr = view.state.tr
116
+
117
+ const biblioSection = utils.findChildrenByType(
118
+ doc,
119
+ schema.nodes.bibliography_element,
120
+ true
121
+ )
122
+
123
+ if (biblioSection.length) {
124
+ let insertPos = biblioSection[0].pos + 1
125
+ for (const attrs of items) {
126
+ const node = schema.nodes.bibliography_item.create(attrs)
127
+ tr = tr.insert(insertPos, node)
128
+ insertPos += node.nodeSize
129
+ }
130
+ } else {
131
+ const backmatter = utils.findChildrenByType(
132
+ doc,
133
+ schema.nodes.backmatter,
134
+ true
135
+ )
136
+ const backmatterEnd = backmatter[0]
137
+ ? backmatter[0].node.nodeSize + backmatter[0].pos
138
+ : 0
139
+
140
+ const nodes = items.map((attrs) =>
141
+ schema.nodes.bibliography_item.create(attrs)
142
+ )
143
+ tr = tr.insert(
144
+ backmatterEnd ? backmatterEnd - 1 : tr.doc.content.size,
145
+ createBibliographySection(nodes)
146
+ )
147
+ }
148
+ view.dispatch(tr)
149
+ }
150
+
151
+ export const saveBibliographyItem = (
152
+ view: ManuscriptEditorView,
153
+ attrs: BibliographyItemAttrs[]
154
+ ) => {
155
+ if (attrs.length === 1) {
156
+ const item = attrs[0]
157
+
158
+ if (findChildByID(view, item.id)) {
159
+ updateNodeAttrs(view, schema.nodes.bibliography_item, item)
160
+ return
161
+ }
162
+ }
163
+
164
+ insertBibliographyItems(view, attrs)
165
+ }
package/src/menus.tsx CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  insertBoxElement,
39
39
  insertContributors,
40
40
  insertCrossReference,
41
+ canInsertCrossReference,
41
42
  insertEmbed,
42
43
  insertGraphicalAbstract,
43
44
  insertHeadshotGrid,
@@ -400,6 +401,44 @@ export const getEditorMenus = (
400
401
  {
401
402
  role: 'separator',
402
403
  },
404
+ {
405
+ id: 'insert-citation',
406
+ label: 'Citation',
407
+ shortcut: {
408
+ mac: 'Option+CommandOrControl+C',
409
+ pc: 'CommandOrControl+Option+C',
410
+ },
411
+ isEnabled:
412
+ isEditAllowed(state) &&
413
+ isCommandValid(canInsert(schema.nodes.citation)),
414
+ run: doCommand(insertInlineCitation),
415
+ isHidden: !templateAllows(state, schema.nodes.citation),
416
+ },
417
+ {
418
+ id: 'insert-cross-reference',
419
+ label: 'Cross-reference',
420
+ shortcut: {
421
+ mac: 'Option+CommandOrControl+R',
422
+ pc: 'CommandOrControl+Option+R',
423
+ },
424
+ isEnabled:
425
+ isEditAllowed(state) && isCommandValid(canInsertCrossReference),
426
+ run: doCommand(insertCrossReference),
427
+ isHidden: !templateAllows(state, schema.nodes.cross_reference),
428
+ },
429
+ {
430
+ id: 'insert-footnote',
431
+ label: 'Footnote',
432
+ shortcut: {
433
+ mac: 'Option+CommandOrControl+F',
434
+ pc: 'CommandOrControl+Option+F',
435
+ },
436
+ isEnabled:
437
+ isEditAllowed(state) &&
438
+ isCommandValid(canInsert(schema.nodes.inline_footnote)),
439
+ run: doCommand(insertInlineFootnote),
440
+ isHidden: !templateAllows(state, schema.nodes.inline_footnote),
441
+ },
403
442
  {
404
443
  id: 'insert-special-character',
405
444
  label: 'Special Characters',
@@ -72,23 +72,25 @@ export default (props: EditorProps) =>
72
72
  const titleEndPos = titlePos + titleNode.nodeSize
73
73
 
74
74
  return DecorationSet.create(state.doc, [
75
- Decoration.widget(titleEndPos, (view: ManuscriptEditorView) => {
76
- return createAddSubtitleButton(() => {
77
- const subtitlesNode = schema.nodes.subtitles.create(
78
- { id: uuidv4() },
79
- [schema.nodes.subtitle.create({ id: uuidv4() })]
80
- )
81
- const pos = findInsertionPosition(
82
- schema.nodes.subtitles,
83
- state.doc
84
- )
85
- const tr = view.state.tr.insert(pos, subtitlesNode)
86
- const subtitlePos = pos + 1
87
- tr.setSelection(TextSelection.create(tr.doc, subtitlePos))
75
+ Decoration.widget(
76
+ titleEndPos,
77
+ (view: ManuscriptEditorView) => {
78
+ return createAddSubtitleButton(() => {
79
+ const subtitlesNode = schema.nodes.subtitles.create(
80
+ { id: uuidv4() },
81
+ [schema.nodes.subtitle.create({ id: uuidv4() })]
82
+ )
83
+ const pos = findInsertionPosition(
84
+ schema.nodes.subtitles,
85
+ state.doc
86
+ )
87
+ const tr = view.state.tr.insert(pos, subtitlesNode)
88
+ const subtitlePos = pos + 1
89
+ tr.setSelection(TextSelection.create(tr.doc, subtitlePos))
88
90
 
89
- view.dispatch(tr)
90
- view.focus()
91
- })
91
+ view.dispatch(tr)
92
+ view.focus()
93
+ })
92
94
  },
93
95
  { side: -1 }
94
96
  ),