@manuscripts/body-editor 3.19.1 → 3.19.3

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 (34) hide show
  1. package/dist/cjs/components/cross-ref-check-modal/CrossRefWarningModal.js +202 -52
  2. package/dist/cjs/components/cross-ref-check-modal/openModal.js +8 -2
  3. package/dist/cjs/lib/utils.js +12 -1
  4. package/dist/cjs/plugins/cross-references.js +37 -14
  5. package/dist/cjs/versions.js +1 -1
  6. package/dist/cjs/views/ReactSubView.js +1 -0
  7. package/dist/cjs/views/cross_reference.js +13 -2
  8. package/dist/cjs/views/cross_reference_editable.js +1 -4
  9. package/dist/es/components/cross-ref-check-modal/CrossRefWarningModal.js +202 -52
  10. package/dist/es/components/cross-ref-check-modal/openModal.js +9 -3
  11. package/dist/es/lib/utils.js +11 -1
  12. package/dist/es/plugins/cross-references.js +37 -15
  13. package/dist/es/versions.js +1 -1
  14. package/dist/es/views/ReactSubView.js +1 -0
  15. package/dist/es/views/cross_reference.js +13 -2
  16. package/dist/es/views/cross_reference_editable.js +1 -4
  17. package/dist/types/components/authors-affiliations/AuthorsAndAffiliationsModals.d.ts +1 -1
  18. package/dist/types/components/cross-ref-check-modal/CrossRefWarningModal.d.ts +4 -2
  19. package/dist/types/components/cross-ref-check-modal/openModal.d.ts +2 -2
  20. package/dist/types/lib/utils.d.ts +4 -0
  21. package/dist/types/plugins/cross-references.d.ts +2 -0
  22. package/dist/types/versions.d.ts +1 -1
  23. package/dist/types/views/ReactSubView.d.ts +5 -2
  24. package/dist/types/views/cross_reference.d.ts +1 -0
  25. package/package.json +2 -2
  26. package/src/components/cross-ref-check-modal/CrossRefWarningModal.tsx +315 -83
  27. package/src/components/cross-ref-check-modal/openModal.ts +14 -4
  28. package/src/lib/utils.ts +24 -0
  29. package/src/plugins/cross-references.ts +41 -13
  30. package/src/versions.ts +1 -1
  31. package/src/views/ReactSubView.tsx +6 -3
  32. package/src/views/cross_reference.ts +14 -2
  33. package/src/views/cross_reference_editable.ts +1 -4
  34. package/styles/Editor.css +12 -0
@@ -14,71 +14,148 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { ManuscriptNode } from '@manuscripts/transform'
18
- import React, { useState } from 'react'
19
-
20
17
  import {
21
- AttentionOrangeIcon,
22
- CloseButton,
18
+ ArrowUpIcon,
19
+ AttentionRedIcon,
20
+ ButtonGroup,
21
+ IconButton,
23
22
  ModalContainer,
24
- ModalHeader,
25
23
  PrimaryButton,
24
+ RadioButton,
26
25
  StyledModal,
27
26
  TertiaryButton,
28
- TextButton,
27
+ WebLinkIcon,
28
+ withFocusTrap,
29
29
  } from '@manuscripts/style-guide'
30
- import { ResolvedPos } from 'prosemirror-model'
31
- import { startCase } from 'lodash'
30
+ import { ManuscriptNode, schema } from '@manuscripts/transform'
31
+ import { NodeType, ResolvedPos } from 'prosemirror-model'
32
+ import React, { useState } from 'react'
33
+
34
+ import { getSurroundingText } from '../../lib/utils'
35
+ import { nodeTypeIcon } from '../../node-type-icons'
32
36
  import styled from 'styled-components'
33
37
 
34
38
  export type XrefGroup = {
35
39
  referenced: ManuscriptNode
36
40
  label: string
41
+ caption: string
37
42
  xrefs: [ManuscriptNode, ResolvedPos][]
38
43
  }
39
44
 
45
+ export type DeleteOption = 'delete-without-ref' | 'delete-with-ref'
46
+
40
47
  export const CrossRefWarningModal: React.FC<{
41
48
  onClose: () => void
42
49
  xrefs: XrefGroup[]
43
- onConfirm: () => void
50
+ onConfirm: (deleteOption: DeleteOption) => void
44
51
  selectAndScrollTo: ($pos: ResolvedPos) => void
45
52
  }> = ({ onClose, xrefs, onConfirm, selectAndScrollTo }) => {
46
53
  const [isOpen, setIsOpen] = useState(true)
54
+ const [deleteOption, setDeleteOption] =
55
+ useState<DeleteOption>('delete-without-ref')
56
+
47
57
  const handleClose = () => {
48
58
  setIsOpen(false)
49
59
  onClose()
50
60
  }
51
61
 
62
+ const [showRef, setShowRef] = useState(false)
63
+ const [isScrolling, setIsScrolling] = useState(false)
64
+
65
+ const handleSelectAndScrollTo = ($pos: ResolvedPos) => {
66
+ setIsScrolling(true)
67
+ selectAndScrollTo($pos)
68
+ }
69
+
70
+ const toggleReferenceList = () => setShowRef(!showRef)
71
+
72
+ const onChangeDeleteOption = (e: React.ChangeEvent<HTMLInputElement>) => {
73
+ const { value } = e.target
74
+ setDeleteOption(value as DeleteOption)
75
+ }
76
+
77
+ const references = xrefs.flatMap((xrefGroup) =>
78
+ xrefGroup.xrefs.map((xref) => [...xref])
79
+ ) as [ManuscriptNode, ResolvedPos][]
80
+
81
+ const elementLabel =
82
+ xrefs.length === 1 ? xrefs[0].label : `${xrefs.length} elements`
83
+ const locationWord = references.length === 1 ? 'location' : 'locations'
84
+ const pronoun = xrefs.length === 1 ? 'it' : 'them'
85
+
52
86
  return (
53
87
  <Modal
54
88
  isOpen={isOpen}
89
+ $isScrolling={isScrolling}
55
90
  onRequestClose={() => handleClose()}
56
91
  shouldCloseOnOverlayClick={false}
57
92
  hideOverlay={true}
58
93
  >
59
- <Container data-cy="cross-reference-warning-modal">
60
- <ModalHeader>
61
- <CloseButton
62
- onClick={() => handleClose()}
63
- data-cy="modal-close-button"
64
- />
65
- </ModalHeader>
94
+ <Container
95
+ $isScrolling={isScrolling}
96
+ data-cy="cross-reference-warning-modal"
97
+ >
66
98
  <Body>
67
99
  <Title>
68
- <AttentionOrangeIcon width={24} height={22} /> Delete referenced
69
- content?
100
+ <AttentionRedIcon width={24} height={24} />
101
+ Confirm Deletion
70
102
  </Title>
71
- <p>You are deleting content referenced elsewhere in the document:</p>
72
- <ScrolableItems>
73
- {xrefs.map((group, i) => (
74
- <XrefGroupDisplay
75
- key={i}
76
- group={group}
77
- selectAndScrollTo={selectAndScrollTo}
78
- />
79
- ))}
80
- </ScrolableItems>
81
- <Actions>
103
+ <p>
104
+ <b>{elementLabel}</b> {xrefs.length === 1 ? 'is' : 'are'} actively
105
+ referenced in{' '}
106
+ <b>
107
+ {references.length} {locationWord}
108
+ </b>{' '}
109
+ in your document. Deleting {pronoun} will break the following
110
+ cross-references:
111
+ </p>
112
+ <div>
113
+ <ToggleHeader>
114
+ <SecondaryBoldHeading>
115
+ {showRef
116
+ ? 'Hide locations'
117
+ : `Show ${references.length} location`}
118
+ </SecondaryBoldHeading>
119
+ <ToggleButton onClick={toggleReferenceList}>
120
+ {showRef ? <ArrowUpIcon /> : <ArrowDownIcon />}
121
+ </ToggleButton>
122
+ </ToggleHeader>
123
+ {showRef && (
124
+ <ListWrapper>
125
+ {xrefs.length === 1 ? (
126
+ <XrefGroupDisplay
127
+ label={xrefs[0].label}
128
+ xrefs={xrefs[0].xrefs}
129
+ selectAndScrollTo={handleSelectAndScrollTo}
130
+ />
131
+ ) : (
132
+ <XrefGroupsDisplay
133
+ xrefsGroup={xrefs}
134
+ selectAndScrollTo={handleSelectAndScrollTo}
135
+ />
136
+ )}
137
+ </ListWrapper>
138
+ )}
139
+ </div>
140
+ <SelectorContainer>
141
+ <RadioButton
142
+ name={'delete-option'}
143
+ id={'delete-without-ref-option'}
144
+ value={'delete-without-ref'}
145
+ label={'Delete but keep referenced text'}
146
+ checked={deleteOption === 'delete-without-ref'}
147
+ onChange={onChangeDeleteOption}
148
+ />
149
+ <RadioButton
150
+ name={'delete-option'}
151
+ id={'delete-with-ref-option'}
152
+ value={'delete-with-ref'}
153
+ label={'Delete & remove references'}
154
+ checked={deleteOption === 'delete-with-ref'}
155
+ onChange={onChangeDeleteOption}
156
+ />
157
+ </SelectorContainer>
158
+ <ButtonGroup>
82
159
  <TertiaryButton type="button" onClick={() => handleClose()}>
83
160
  Cancel
84
161
  </TertiaryButton>
@@ -86,13 +163,13 @@ export const CrossRefWarningModal: React.FC<{
86
163
  $danger={true}
87
164
  type="button"
88
165
  onClick={() => {
89
- onConfirm()
166
+ onConfirm(deleteOption)
90
167
  setIsOpen(false)
91
168
  }}
92
169
  >
93
- Delete & remove citation
170
+ Delete
94
171
  </PrimaryButton>
95
- </Actions>
172
+ </ButtonGroup>
96
173
  </Body>
97
174
  </Container>
98
175
  </Modal>
@@ -100,45 +177,95 @@ export const CrossRefWarningModal: React.FC<{
100
177
  }
101
178
 
102
179
  const XrefGroupDisplay: React.FC<{
103
- group: XrefGroup
180
+ label: string
181
+ xrefs: XrefGroup['xrefs']
182
+ selectAndScrollTo: ($pos: ResolvedPos) => void
183
+ }> = ({ label, xrefs, selectAndScrollTo }) => {
184
+ return (
185
+ <ReferencesList>
186
+ {xrefs.map(([xrefNode, pos], i) => {
187
+ const { leftHandText, rightHandText } = getSurroundingText(
188
+ pos,
189
+ xrefNode
190
+ )
191
+ const derivedLabel = xrefNode.attrs.label || label || '[cross-ref]'
192
+
193
+ return (
194
+ <ListItem key={i}>
195
+ <XRefTextContainer>
196
+ <XRefAdjacentText $direction={'rtl'}>
197
+ {leftHandText}
198
+ </XRefAdjacentText>
199
+ <XRefLabel>{derivedLabel}</XRefLabel>
200
+ <XRefAdjacentText $direction={'ltr'}>
201
+ {rightHandText}
202
+ </XRefAdjacentText>
203
+ </XRefTextContainer>
204
+ <ScrollButton $mini={true} onClick={() => selectAndScrollTo(pos)}>
205
+ Show
206
+ </ScrollButton>
207
+ </ListItem>
208
+ )
209
+ })}
210
+ </ReferencesList>
211
+ )
212
+ }
213
+
214
+ const XrefGroupsDisplay: React.FC<{
215
+ xrefsGroup: XrefGroup[]
104
216
  selectAndScrollTo: ($pos: ResolvedPos) => void
105
- }> = ({ group, selectAndScrollTo }) => {
217
+ }> = ({ xrefsGroup, selectAndScrollTo }) => {
106
218
  return (
107
- <div>
108
- <h3>{group.label}</h3>
109
- <ReferencesList>
110
- {group.xrefs.map(([, pos], i) => {
111
- return (
112
- <li key={i}>
113
- <TextButton onClick={() => selectAndScrollTo(pos)}>
114
- {`${startCase(pos.parent.type.name)} - ${pos.parent.textContent}`}
115
- </TextButton>
116
- </li>
117
- )
118
- })}
119
- </ReferencesList>
120
- </div>
219
+ <ReferencesList>
220
+ {xrefsGroup.map(({ referenced, label, caption, xrefs }, i) => {
221
+ const icon = getReferencedIcon(referenced.type)
222
+
223
+ return (
224
+ <div key={i}>
225
+ <DarkSecondaryBoldHeading>
226
+ {icon ? <ItemIcon>{icon}</ItemIcon> : null}
227
+ {`${label}:`}
228
+ <CaptionTitle $direction={'ltr'}>{caption}</CaptionTitle>
229
+ </DarkSecondaryBoldHeading>
230
+ <XrefGroupDisplay
231
+ label={label}
232
+ xrefs={xrefs}
233
+ selectAndScrollTo={selectAndScrollTo}
234
+ />
235
+ </div>
236
+ )
237
+ })}
238
+ </ReferencesList>
121
239
  )
122
240
  }
123
241
 
124
- const Container = styled(ModalContainer)`
242
+ const getReferencedIcon = (type: NodeType): React.ReactNode => {
243
+ if (type === schema.nodes.supplement) {
244
+ return <WebLinkIcon className="file-icon" />
245
+ }
246
+ return nodeTypeIcon(schema.nodes[type.name])
247
+ }
248
+
249
+ const Container = styled(ModalContainer)<{ $isScrolling: boolean }>`
125
250
  position: absolute;
126
- top: 1rem;
127
- left: 50%;
128
- right: 0;
129
- max-height: calc(50vh - 2rem);
251
+ top: ${({ $isScrolling }) => ($isScrolling ? '1rem' : '30%')};
252
+ left: ${({ $isScrolling }) =>
253
+ $isScrolling ? 'calc(100% - 556px - 0rem)' : '50%'};
254
+ max-height: calc(60vh - 2rem);
130
255
  min-height: 280px;
131
- transform: translate(-50%, 0);
132
- max-width: 480px;
256
+ transform: ${({ $isScrolling }) =>
257
+ $isScrolling ? 'translate(0, 0)' : 'translate(-50%, 0)'};
258
+ max-width: 556px;
133
259
  transition:
134
260
  top 0.2s,
261
+ left 0.2s,
135
262
  transform 0.2s;
136
263
  `
137
264
 
138
265
  // since we need to scroll inside the editor when this dialog is active, we can't use dialog.showModal()
139
266
  // so we recreate the appearance using classic position:fixed/after approach.
140
267
  // While showModal doesn't block scrolling - it doesn't allow to focus on the editor and that kills the scrollIntoView
141
- const Modal = styled(StyledModal)`
268
+ const Modal = styled(StyledModal)<{ $isScrolling: boolean }>`
142
269
  position: fixed;
143
270
  top: 0;
144
271
  left: 0;
@@ -163,6 +290,8 @@ const Modal = styled(StyledModal)`
163
290
  right: 0;
164
291
  bottom: 0;
165
292
  background: rgba(0, 0, 0, 0.2);
293
+ opacity: ${({ $isScrolling }) => ($isScrolling ? 0.15 : 1)};
294
+ transform: opacity 0.2s;
166
295
  }
167
296
  h3 {
168
297
  font-size: 16px;
@@ -173,11 +302,11 @@ const Modal = styled(StyledModal)`
173
302
  }
174
303
  `
175
304
 
176
- const Body = styled.div`
305
+ const Body = withFocusTrap(styled.div`
177
306
  margin: 1.5rem;
178
307
  display: flex;
179
308
  flex-flow: column;
180
- `
309
+ `)
181
310
 
182
311
  const Title = styled.h2`
183
312
  font-size: 18px;
@@ -187,37 +316,140 @@ const Title = styled.h2`
187
316
  color: #353535;
188
317
  svg {
189
318
  vertical-align: text-top;
319
+ margin-right: 8px;
190
320
  }
191
321
  `
192
322
 
193
- const ReferencesList = styled.ul`
194
- padding: 8px;
195
- margin-left: 0;
196
- list-style: none;
323
+ const ToggleHeader = styled.div`
324
+ display: flex;
325
+ justify-content: space-between;
326
+ align-items: center;
327
+ padding: 6px 3px 6px 8px;
328
+ border-radius: 4px 4px 0 0;
329
+ border: 1px solid #e2e2e2;
197
330
  background: #f2f2f2;
331
+ `
332
+
333
+ const ListWrapper = styled.div`
198
334
  border: 1px solid #e2e2e2;
199
- border-radius: 3px;
200
-
201
- ${TextButton} {
202
- margin-left: 0;
203
- text-decoration: underline;
204
- &:hover {
205
- text-decoration: none;
206
- }
207
- display: block;
208
- max-width: 100%;
209
- overflow: hidden;
210
- color: #353535;
211
- text-overflow: ellipsis;
335
+ border-top: none;
336
+ max-height: 21vh;
337
+ min-height: 100px;
338
+ overflow-y: auto;
339
+ padding: 12px;
340
+ `
341
+
342
+ const ToggleButton = styled(IconButton)`
343
+ height: 24px;
344
+ &&:not([disabled]):focus-visible {
345
+ outline: 2px solid ${(props) => props.theme.colors.outline.focus};
346
+ outline-offset: 2px;
347
+ }
348
+ svg {
349
+ width: 12px;
350
+ height: 7px;
212
351
  }
213
352
  `
214
353
 
215
- const Actions = styled.footer`
216
- text-align: right;
217
- padding-top: 1rem;
354
+ const SecondaryHeading = styled.div`
355
+ font-size: 14px;
356
+ font-style: normal;
357
+ font-weight: 700;
358
+ line-height: 24px;
359
+ color: #6e6e6e;
360
+ user-select: none;
218
361
  `
219
- const ScrolableItems = styled.div`
220
- max-height: 16vh;
221
- min-height: 100px;
222
- overflow-y: auto;
362
+
363
+ const SecondaryBoldHeading = styled(SecondaryHeading)`
364
+ font-weight: 700;
365
+ color: #6e6e6e;
366
+ `
367
+
368
+ const DarkSecondaryBoldHeading = styled(SecondaryBoldHeading)`
369
+ display: flex;
370
+ padding: 0 4px;
371
+ margin-bottom: 10px;
372
+ border-radius: 4px;
373
+ color: #353535;
374
+ background: #f2f2f2;
375
+ `
376
+
377
+ const ItemIcon = styled.span`
378
+ display: flex;
379
+ padding-right: 8px;
380
+ svg {
381
+ align-self: center;
382
+ }
383
+ `
384
+
385
+ const ReferencesList = styled.ul`
386
+ display: flex;
387
+ gap: 10px;
388
+ flex-direction: column;
389
+ padding: 0;
390
+ margin: 0;
391
+ list-style: none;
392
+ `
393
+
394
+ const ListItem = styled.li`
395
+ display: flex;
396
+ justify-content: space-between;
397
+ align-items: center;
398
+ border-bottom: solid 1px #e2e2e2;
399
+ &:last-child {
400
+ border-bottom: none;
401
+ padding: 0;
402
+ }
403
+ `
404
+
405
+ const ScrollButton = styled(TertiaryButton)`
406
+ margin: 0;
407
+ padding: 4px;
408
+ font-size: 14px;
409
+ line-height: 24px;
410
+ text-decoration-line: underline;
411
+ `
412
+
413
+ const XRefTextContainer = styled.div`
414
+ display: flex;
415
+ color: #6e6e6e;
416
+ font-size: 12px;
417
+ font-style: normal;
418
+ font-weight: 400;
419
+ line-height: 16px;
420
+ `
421
+
422
+ const XRefLabel = styled.span`
423
+ border-radius: 4px;
424
+ border: 1px solid #fe8f1f;
425
+ background: #fff9e5;
426
+ mix-blend-mode: darken;
427
+ `
428
+
429
+ const XRefAdjacentText = styled.span<{ $direction: 'rtl' | 'ltr' }>`
430
+ display: block;
431
+ max-width: 150px;
432
+ white-space: nowrap;
433
+ overflow: hidden;
434
+ text-overflow: ellipsis;
435
+ direction: ${(props) => props.$direction};
436
+ `
437
+
438
+ const CaptionTitle = styled(XRefAdjacentText)`
439
+ max-width: 120px;
440
+ margin-left: 4px;
441
+ `
442
+
443
+ const SelectorContainer = styled.div`
444
+ display: flex;
445
+ gap: 24px;
446
+ padding: 8px 12px 4px 12px;
447
+ margin: 24px 0;
448
+ border-radius: 4px;
449
+ border: 1px solid #fe8f1f;
450
+ background: #fff9e5;
451
+ `
452
+
453
+ const ArrowDownIcon = styled(ArrowUpIcon)`
454
+ transform: rotate(180deg);
223
455
  `
@@ -19,12 +19,16 @@ import { ResolvedPos } from 'prosemirror-model'
19
19
 
20
20
  import { getEditorProps } from '../../plugins/editor-props'
21
21
  import ReactSubView from '../../views/ReactSubView'
22
- import { CrossRefWarningModal, XrefGroup } from './CrossRefWarningModal'
22
+ import {
23
+ CrossRefWarningModal,
24
+ DeleteOption,
25
+ XrefGroup,
26
+ } from './CrossRefWarningModal'
23
27
 
24
28
  export const openCrossRefWarningModal = (
25
29
  view: ManuscriptEditorView,
26
30
  xrefGroups: XrefGroup[],
27
- onConfirm: () => void,
31
+ onConfirm: (deleteOption: DeleteOption) => void,
28
32
  onClose: () => void,
29
33
  selectAndScrollTo: ($pos: ResolvedPos) => void
30
34
  ): HTMLDivElement => {
@@ -32,8 +36,14 @@ export const openCrossRefWarningModal = (
32
36
  const props = getEditorProps(state)
33
37
  const componentProps = {
34
38
  xrefs: xrefGroups,
35
- onConfirm,
36
- onClose,
39
+ onConfirm: (deleteOption: DeleteOption) => {
40
+ dialog.unmount()
41
+ onConfirm(deleteOption)
42
+ },
43
+ onClose: () => {
44
+ dialog.unmount()
45
+ onClose()
46
+ },
37
47
  selectAndScrollTo,
38
48
  }
39
49
 
package/src/lib/utils.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  import { EditorState, Selection } from 'prosemirror-state'
34
34
  import {
35
35
  ContentNodeWithPos,
36
+ findChildren,
36
37
  findChildrenByType,
37
38
  findParentNode,
38
39
  findParentNodeOfTypeClosestToPos,
@@ -332,3 +333,26 @@ export const getLastTitleNode = (state: ManuscriptEditorState) => {
332
333
  const titleNode = findChildrenByType(state.doc, state.schema.nodes.title)[0]
333
334
  return titleNode
334
335
  }
336
+
337
+ export const getSurroundingText = (pos: ResolvedPos, node: ManuscriptNode) => {
338
+ const nodeContent = findChildren(
339
+ pos.node(),
340
+ (child) => child.attrs.id === node.attrs.id
341
+ )[0]
342
+
343
+ const content = pos.node().content
344
+ const MAX_LENGTH = 40
345
+
346
+ const nodeStart = nodeContent.pos
347
+ const nodeEnd = nodeContent.pos + node.nodeSize
348
+
349
+ const leftHandText = content.textBetween(
350
+ nodeStart > MAX_LENGTH ? nodeStart - MAX_LENGTH : 0,
351
+ nodeStart
352
+ )
353
+ const rightHandText = content.textBetween(
354
+ nodeEnd,
355
+ Math.min(nodeEnd + MAX_LENGTH, content.size)
356
+ )
357
+ return { leftHandText, rightHandText }
358
+ }
@@ -20,18 +20,28 @@ import {
20
20
  schema,
21
21
  Target,
22
22
  } from '@manuscripts/transform'
23
- import { trackChangesPluginKey } from '@manuscripts/track-changes-plugin'
23
+ import {
24
+ trackChangesPluginKey,
25
+ TrackChangesAction,
26
+ } from '@manuscripts/track-changes-plugin'
24
27
  import isEqual from 'lodash/isEqual'
25
28
  import { Node, ResolvedPos } from 'prosemirror-model'
26
29
  import { NodeSelection, Plugin, Transaction } from 'prosemirror-state'
27
30
  import { Decoration, DecorationSet, EditorView } from 'prosemirror-view'
28
31
 
29
- import { XrefGroup } from '../components/cross-ref-check-modal/CrossRefWarningModal'
32
+ import {
33
+ DeleteOption,
34
+ XrefGroup,
35
+ } from '../components/cross-ref-check-modal/CrossRefWarningModal'
30
36
  import { openCrossRefWarningModal } from '../components/cross-ref-check-modal/openModal'
31
37
  import { objectsKey } from './objects'
32
38
 
39
+ export const DELETE_WITHOUT_REF = 'delete-without-ref'
40
+ export const SELECTED_CROSS_REF_CLASS = 'selected-cross-reference'
41
+
33
42
  let modalActive = false
34
43
  let modalElement: HTMLDivElement | null = null
44
+ let scrollTargetEl: HTMLElement | null = null
35
45
 
36
46
  export default () => {
37
47
  let view: ManuscriptEditorView | null = null
@@ -55,7 +65,10 @@ export default () => {
55
65
  !view ||
56
66
  !tr.docChanged ||
57
67
  tr.getMeta(trackChangesPluginKey) ||
58
- tr.getMeta('addToHistory') === false
68
+ tr.getMeta(TrackChangesAction.refreshChanges) ||
69
+ tr.getMeta('addToHistory') === false ||
70
+ tr.getMeta('collab$') ||
71
+ tr.getMeta(DELETE_WITHOUT_REF)
59
72
  ) {
60
73
  return true
61
74
  }
@@ -81,6 +94,9 @@ export default () => {
81
94
  modalElement.remove()
82
95
  modalElement = null
83
96
  }
97
+ if (scrollTargetEl) {
98
+ scrollTargetEl.classList.remove(SELECTED_CROSS_REF_CLASS)
99
+ }
84
100
  }
85
101
 
86
102
  const deletedIds = new Set(
@@ -206,7 +222,8 @@ function createXrefGroups(
206
222
  const xrefs = xrefsByRid.get(id)
207
223
  if (xrefs?.length) {
208
224
  const label = targets.get(referenced.attrs.id)?.label || ''
209
- xrefGroups.push({ referenced, label, xrefs })
225
+ const caption = targets.get(referenced.attrs.id)?.caption || ''
226
+ xrefGroups.push({ referenced, label, caption, xrefs })
210
227
  }
211
228
  }
212
229
  return xrefGroups
@@ -219,7 +236,7 @@ const onConfirmCreator =
219
236
  deletedIds: Set<string>,
220
237
  tr: Transaction
221
238
  ) =>
222
- () => {
239
+ (deleteOption: DeleteOption) => {
223
240
  cleanup()
224
241
  if (!view) {
225
242
  return
@@ -237,21 +254,25 @@ const onConfirmCreator =
237
254
  }
238
255
  newTr.step(step)
239
256
  }
240
- // Remove cross-references that pointed to the now-deleted nodes.
241
- // Collect positions in reverse order so deletions don't shift
242
- // positions of earlier entries.
243
- const xrefPositions: { from: number; to: number }[] = []
257
+ if (deleteOption === 'delete-with-ref') {
258
+ // Remove cross-references that pointed to the now-deleted nodes.
259
+ // Collect positions in reverse order so deletions don't shift
260
+ // positions of earlier entries.
261
+ const xrefPositions: { from: number; to: number }[] = []
244
262
  newTr.doc.descendants((node, pos) => {
245
263
  if (node.type === schema.nodes.cross_reference) {
246
264
  const rids = node.attrs.rids as string[]
247
265
  if (rids.some((rid) => deletedIds.has(rid))) {
248
266
  xrefPositions.push({ from: pos, to: pos + node.nodeSize })
249
267
  }
250
- }
268
+ }
251
269
  })
252
270
  for (let i = xrefPositions.length - 1; i >= 0; i--) {
253
- const { from, to } = xrefPositions[i]
254
- newTr.delete(from, to)
271
+ const { from, to } = xrefPositions[i]
272
+ newTr.delete(from, to)
273
+ }
274
+ } else {
275
+ newTr.setMeta(DELETE_WITHOUT_REF, true)
255
276
  }
256
277
  view.dispatch(newTr)
257
278
  }
@@ -263,7 +284,6 @@ const selectAndScrollToCreator = (view: EditorView) => ($pos: ResolvedPos) => {
263
284
 
264
285
  const selTr = view.state.tr
265
286
  selTr.setSelection(NodeSelection.create(view.state.doc, $pos.pos))
266
- view.focus()
267
287
  view.dispatch(selTr)
268
288
  // Standard PM's scrollIntoView doesn't allow placement control - hence switching to native DOM's peer method.
269
289
  let scrollable = view.dom.parentElement
@@ -293,4 +313,12 @@ const selectAndScrollToCreator = (view: EditorView) => ($pos: ResolvedPos) => {
293
313
  modalElement?.classList.remove('modal-bottom')
294
314
  scrollable.scrollTo({ top: scrollTo, behavior: 'smooth' })
295
315
  }
316
+ const dom = view.nodeDOM($pos.pos)
317
+ if (dom && dom instanceof HTMLElement) {
318
+ dom.classList.add(SELECTED_CROSS_REF_CLASS)
319
+ if (scrollTargetEl && scrollTargetEl !== dom) {
320
+ scrollTargetEl?.classList.remove(SELECTED_CROSS_REF_CLASS)
321
+ }
322
+ scrollTargetEl = dom
323
+ }
296
324
  }