@manuscripts/body-editor 3.19.1 → 3.19.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.
- package/dist/cjs/components/cross-ref-check-modal/CrossRefWarningModal.js +202 -52
- package/dist/cjs/components/cross-ref-check-modal/openModal.js +8 -2
- package/dist/cjs/lib/utils.js +12 -1
- package/dist/cjs/plugins/cross-references.js +37 -14
- package/dist/cjs/versions.js +1 -1
- package/dist/cjs/views/ReactSubView.js +1 -0
- package/dist/cjs/views/cross_reference.js +13 -2
- package/dist/cjs/views/cross_reference_editable.js +1 -4
- package/dist/es/components/cross-ref-check-modal/CrossRefWarningModal.js +202 -52
- package/dist/es/components/cross-ref-check-modal/openModal.js +9 -3
- package/dist/es/lib/utils.js +11 -1
- package/dist/es/plugins/cross-references.js +37 -15
- package/dist/es/versions.js +1 -1
- package/dist/es/views/ReactSubView.js +1 -0
- package/dist/es/views/cross_reference.js +13 -2
- package/dist/es/views/cross_reference_editable.js +1 -4
- package/dist/types/components/authors-affiliations/AuthorsAndAffiliationsModals.d.ts +1 -1
- package/dist/types/components/cross-ref-check-modal/CrossRefWarningModal.d.ts +4 -2
- package/dist/types/components/cross-ref-check-modal/openModal.d.ts +2 -2
- package/dist/types/lib/utils.d.ts +4 -0
- package/dist/types/plugins/cross-references.d.ts +2 -0
- package/dist/types/versions.d.ts +1 -1
- package/dist/types/views/ReactSubView.d.ts +5 -2
- package/dist/types/views/cross_reference.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/cross-ref-check-modal/CrossRefWarningModal.tsx +315 -83
- package/src/components/cross-ref-check-modal/openModal.ts +14 -4
- package/src/lib/utils.ts +24 -0
- package/src/plugins/cross-references.ts +41 -13
- package/src/versions.ts +1 -1
- package/src/views/ReactSubView.tsx +6 -3
- package/src/views/cross_reference.ts +14 -2
- package/src/views/cross_reference_editable.ts +1 -4
- 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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
+
WebLinkIcon,
|
|
28
|
+
withFocusTrap,
|
|
29
29
|
} from '@manuscripts/style-guide'
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
<
|
|
69
|
-
|
|
100
|
+
<AttentionRedIcon width={24} height={24} />
|
|
101
|
+
Confirm Deletion
|
|
70
102
|
</Title>
|
|
71
|
-
<p>
|
|
72
|
-
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
170
|
+
Delete
|
|
94
171
|
</PrimaryButton>
|
|
95
|
-
</
|
|
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
|
-
|
|
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
|
-
}> = ({
|
|
217
|
+
}> = ({ xrefsGroup, selectAndScrollTo }) => {
|
|
106
218
|
return (
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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:
|
|
128
|
-
|
|
129
|
-
max-height: calc(
|
|
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:
|
|
132
|
-
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
}
|