@guardian/stand 0.0.2 → 0.0.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.
- package/README.md +4 -0
- package/dist/types/playwright/byline.mock.d.ts +3 -0
- package/dist/types/playwright-ct.config.d.ts +5 -0
- package/package.json +8 -5
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.prettierrc +0 -1
- package/.storybook/main.ts +0 -12
- package/.storybook/preview.tsx +0 -83
- package/CHANGELOG.md +0 -7
- package/dist/types/src/mocks/prosemirror-view.d.ts +0 -10
- package/eslint.config.js +0 -14
- package/jest-setup-after-env.ts +0 -1
- package/jest.config.js +0 -12
- package/rollup.config.js +0 -49
- package/src/byline/Byline.stories.tsx +0 -186
- package/src/byline/Byline.test.tsx +0 -450
- package/src/byline/Byline.tsx +0 -524
- package/src/byline/Preview.tsx +0 -59
- package/src/byline/contributors-fixture.ts +0 -1006
- package/src/byline/lib.test.ts +0 -179
- package/src/byline/lib.ts +0 -426
- package/src/byline/placeholder.ts +0 -30
- package/src/byline/plugins.ts +0 -186
- package/src/byline/schema.ts +0 -62
- package/src/byline/styles.ts +0 -246
- package/src/byline/theme.ts +0 -45
- package/src/byline/util.ts +0 -5
- package/src/index.ts +0 -2
- package/src/mocks/prosemirror-view.ts +0 -19
- package/tsconfig.json +0 -19
- /package/dist/types/{src/byline/Byline.test.d.ts → playwright/byline.spec.d.ts} +0 -0
package/src/byline/Byline.tsx
DELETED
|
@@ -1,524 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createInvisiblesPlugin,
|
|
3
|
-
nbSpace,
|
|
4
|
-
space,
|
|
5
|
-
} from '@guardian/prosemirror-invisibles';
|
|
6
|
-
import { dropCursor } from 'prosemirror-dropcursor';
|
|
7
|
-
import { history } from 'prosemirror-history';
|
|
8
|
-
import type { Node } from 'prosemirror-model';
|
|
9
|
-
import type { Transaction } from 'prosemirror-state';
|
|
10
|
-
import { EditorState } from 'prosemirror-state';
|
|
11
|
-
import { EditorView } from 'prosemirror-view';
|
|
12
|
-
import type { FocusEventHandler} from 'react';
|
|
13
|
-
import { useEffect, useRef, useState } from 'react';
|
|
14
|
-
import type {
|
|
15
|
-
BylineModel,
|
|
16
|
-
TaggedContributor,
|
|
17
|
-
TypingFromStartRange,
|
|
18
|
-
} from './lib';
|
|
19
|
-
import {
|
|
20
|
-
addTaggedContributor,
|
|
21
|
-
addUntaggedContributor,
|
|
22
|
-
convertBylineModelToNode,
|
|
23
|
-
convertNodeToBylineModel,
|
|
24
|
-
getCurrentText,
|
|
25
|
-
hasHitContributorLimit,
|
|
26
|
-
} from './lib';
|
|
27
|
-
import { createPlaceholderPlugin } from './placeholder';
|
|
28
|
-
import { bylinePlugin, clipboardPlugin, keybindings } from './plugins';
|
|
29
|
-
import { Preview } from './Preview';
|
|
30
|
-
import { bylineEditorSchema } from './schema';
|
|
31
|
-
import {
|
|
32
|
-
bylineContainerStyles,
|
|
33
|
-
bylineEditorStyles,
|
|
34
|
-
dropdownContainerStyles,
|
|
35
|
-
dropdownLiStyles,
|
|
36
|
-
dropdownUlStyles,
|
|
37
|
-
selectedDropdownLiStyles,
|
|
38
|
-
} from './styles';
|
|
39
|
-
import type { PartialBylineTheme } from './theme';
|
|
40
|
-
|
|
41
|
-
type BylineProps = {
|
|
42
|
-
theme?: PartialBylineTheme;
|
|
43
|
-
allowUntaggedContributors?: boolean;
|
|
44
|
-
contributorLimit?: number;
|
|
45
|
-
enablePreview?: boolean;
|
|
46
|
-
placeholder?: string;
|
|
47
|
-
initialValue?: BylineModel;
|
|
48
|
-
readOnly?: boolean;
|
|
49
|
-
handleSave: (newValue: BylineModel) => void;
|
|
50
|
-
searchContributors?: (selectedText: string) => Promise<TaggedContributor[]>;
|
|
51
|
-
onBlur?: FocusEventHandler<HTMLDivElement>;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export const Byline = ({
|
|
55
|
-
theme,
|
|
56
|
-
allowUntaggedContributors,
|
|
57
|
-
contributorLimit,
|
|
58
|
-
enablePreview,
|
|
59
|
-
placeholder,
|
|
60
|
-
initialValue,
|
|
61
|
-
readOnly,
|
|
62
|
-
handleSave,
|
|
63
|
-
searchContributors,
|
|
64
|
-
onBlur,
|
|
65
|
-
}: BylineProps) => {
|
|
66
|
-
const editorRef = useRef<HTMLDivElement>(null);
|
|
67
|
-
const viewRef = useRef<EditorView | null>(null);
|
|
68
|
-
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
69
|
-
const [currentText, setCurrentText] = useState('');
|
|
70
|
-
const [currentOptionIndex, setCurrentOptionIndex] = useState(0);
|
|
71
|
-
const [taggedContributors, setTaggedContributors] = useState<
|
|
72
|
-
TaggedContributor[]
|
|
73
|
-
>([]);
|
|
74
|
-
const [currentDoc, setCurrentDoc] = useState<Node | null>(null);
|
|
75
|
-
const [showDropdown, setShowDropdown] = useState(false);
|
|
76
|
-
|
|
77
|
-
// we have to useRef for these as they are read/modified during prosemirror transactions
|
|
78
|
-
// rather than react lifecycle
|
|
79
|
-
const isTypingFromStartRange = useRef<TypingFromStartRange | undefined>(
|
|
80
|
-
undefined,
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
// track the range of positions typed from the start of the input field
|
|
84
|
-
const trackTypingFromStart = (tr: Transaction) => {
|
|
85
|
-
const isCursorSelection = tr.selection.from === tr.selection.to;
|
|
86
|
-
const currentPosition = tr.selection.from;
|
|
87
|
-
const hasChanges = tr.steps.length > 0;
|
|
88
|
-
|
|
89
|
-
// early return and reset if not a cursor selection (single cursor, no range selection)
|
|
90
|
-
if (!isCursorSelection) {
|
|
91
|
-
isTypingFromStartRange.current = undefined;
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// starting to type from the beginning, currentPosition is 1 as thats the index
|
|
96
|
-
// where the cursor ends up
|
|
97
|
-
if (
|
|
98
|
-
!isTypingFromStartRange.current &&
|
|
99
|
-
currentPosition === 1 &&
|
|
100
|
-
hasChanges
|
|
101
|
-
) {
|
|
102
|
-
isTypingFromStartRange.current = {
|
|
103
|
-
start: 1,
|
|
104
|
-
maxReached: 1,
|
|
105
|
-
lastPosition: 1,
|
|
106
|
-
};
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// check if we're continuing to type within or extending the range
|
|
111
|
-
if (isTypingFromStartRange.current) {
|
|
112
|
-
const { start, maxReached, lastPosition } =
|
|
113
|
-
isTypingFromStartRange.current;
|
|
114
|
-
|
|
115
|
-
// check if current position is within our tracked range (allows cursor movement without changes)
|
|
116
|
-
// or adjacent to extend the range (requires changes)
|
|
117
|
-
const isWithinRange =
|
|
118
|
-
currentPosition >= start && currentPosition <= maxReached;
|
|
119
|
-
const isExtendingRange =
|
|
120
|
-
hasChanges && currentPosition === maxReached + 1;
|
|
121
|
-
|
|
122
|
-
if (isWithinRange || isExtendingRange) {
|
|
123
|
-
// update the maximum reached position based on changes
|
|
124
|
-
if (hasChanges) {
|
|
125
|
-
// determine what happened based on position change from last transaction
|
|
126
|
-
const positionDelta = currentPosition - lastPosition;
|
|
127
|
-
|
|
128
|
-
if (positionDelta < 0) {
|
|
129
|
-
// backspace/delete: content was removed, decrease maxReached by 1
|
|
130
|
-
isTypingFromStartRange.current.maxReached =
|
|
131
|
-
maxReached - 1;
|
|
132
|
-
} else if (positionDelta > 0) {
|
|
133
|
-
// content was added: increase maxReached by 1
|
|
134
|
-
isTypingFromStartRange.current.maxReached =
|
|
135
|
-
maxReached + 1;
|
|
136
|
-
}
|
|
137
|
-
// if positionDelta === 0, content was replaced at same position, no maxReached change needed
|
|
138
|
-
|
|
139
|
-
// update lastPosition for next transaction
|
|
140
|
-
isTypingFromStartRange.current.lastPosition =
|
|
141
|
-
currentPosition;
|
|
142
|
-
}
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// reset if none of the above conditions are met, no longer typing from start
|
|
148
|
-
isTypingFromStartRange.current = undefined;
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const getShouldShowDropdown = (
|
|
152
|
-
view: EditorView,
|
|
153
|
-
selectedText: string,
|
|
154
|
-
): boolean => {
|
|
155
|
-
if (readOnly) {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Determines whether to hide or show the contributor dropdown based on component props,
|
|
161
|
-
* and should be used in combination with the selected/current text when calling setShowDropdown
|
|
162
|
-
* The logic evaluates to true when either:
|
|
163
|
-
* - There are search contributors available OR
|
|
164
|
-
* - Untagged contributors are allowed
|
|
165
|
-
* - The document state is under any contributor limit
|
|
166
|
-
* - The selected text is not empty
|
|
167
|
-
*/
|
|
168
|
-
const showDropdownBasedOnProps =
|
|
169
|
-
!!searchContributors || !!allowUntaggedContributors;
|
|
170
|
-
|
|
171
|
-
if (hasHitContributorLimit(view.state.doc, contributorLimit)) {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return selectedText.trim() !== '' && showDropdownBasedOnProps;
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const [enterHit, setEnterHit] = useState(false);
|
|
179
|
-
|
|
180
|
-
// We don't have access to the React state in the dom events handler
|
|
181
|
-
const checkDropdownVisibility = () => {
|
|
182
|
-
// This function is only newly available since 2024
|
|
183
|
-
if (dropdownRef.current?.checkVisibility) {
|
|
184
|
-
return dropdownRef.current.checkVisibility();
|
|
185
|
-
} else {
|
|
186
|
-
return dropdownRef.current?.offsetParent !== null;
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
// Setup the prosemirror editor
|
|
191
|
-
useEffect(() => {
|
|
192
|
-
if (!editorRef.current) {
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const initialDoc = convertBylineModelToNode(initialValue);
|
|
197
|
-
|
|
198
|
-
const state = EditorState.create({
|
|
199
|
-
schema: bylineEditorSchema,
|
|
200
|
-
plugins: [
|
|
201
|
-
dropCursor(),
|
|
202
|
-
clipboardPlugin(allowUntaggedContributors, contributorLimit),
|
|
203
|
-
history(),
|
|
204
|
-
keybindings(),
|
|
205
|
-
createInvisiblesPlugin([space, nbSpace], {
|
|
206
|
-
displayLineEndSelection: true,
|
|
207
|
-
shouldShowInvisibles: true,
|
|
208
|
-
}),
|
|
209
|
-
bylinePlugin(),
|
|
210
|
-
createPlaceholderPlugin(placeholder ?? 'Enter a byline...'),
|
|
211
|
-
],
|
|
212
|
-
doc: initialDoc,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
// Set the initial document in the preview
|
|
216
|
-
setCurrentDoc(initialDoc);
|
|
217
|
-
|
|
218
|
-
viewRef.current = new EditorView(editorRef.current, {
|
|
219
|
-
state,
|
|
220
|
-
editable: () => !readOnly,
|
|
221
|
-
attributes: {
|
|
222
|
-
role: 'combobox',
|
|
223
|
-
'aria-label': 'byline',
|
|
224
|
-
'aria-controls': 'byline-dropdown',
|
|
225
|
-
'aria-expanded': 'false',
|
|
226
|
-
'data-testid': 'byline-input',
|
|
227
|
-
...(readOnly && { 'aria-readonly': 'true' }),
|
|
228
|
-
},
|
|
229
|
-
handleDOMEvents: {
|
|
230
|
-
keydown: (view, event) => {
|
|
231
|
-
if (readOnly) {
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Handle escape key for dropdown
|
|
236
|
-
if (event.key === 'Escape') {
|
|
237
|
-
setShowDropdown(false);
|
|
238
|
-
|
|
239
|
-
// Always return true to prevent other escape handlers
|
|
240
|
-
return true;
|
|
241
|
-
}
|
|
242
|
-
if (event.key === 'ArrowDown') {
|
|
243
|
-
if (checkDropdownVisibility()) {
|
|
244
|
-
event.preventDefault();
|
|
245
|
-
setCurrentOptionIndex((currentOptionIndex) => {
|
|
246
|
-
return currentOptionIndex + 1;
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
return true;
|
|
250
|
-
}
|
|
251
|
-
return false;
|
|
252
|
-
}
|
|
253
|
-
if (event.key === 'ArrowUp') {
|
|
254
|
-
if (checkDropdownVisibility()) {
|
|
255
|
-
event.preventDefault();
|
|
256
|
-
|
|
257
|
-
setCurrentOptionIndex((currentOptionIndex) => {
|
|
258
|
-
return currentOptionIndex - 1;
|
|
259
|
-
});
|
|
260
|
-
return true;
|
|
261
|
-
}
|
|
262
|
-
return false;
|
|
263
|
-
}
|
|
264
|
-
if (event.key === 'Enter') {
|
|
265
|
-
if (checkDropdownVisibility()) {
|
|
266
|
-
event.preventDefault();
|
|
267
|
-
setEnterHit(true);
|
|
268
|
-
}
|
|
269
|
-
return false;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return false;
|
|
273
|
-
},
|
|
274
|
-
blur: (_view, event) => {
|
|
275
|
-
if (readOnly) {
|
|
276
|
-
return false;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (
|
|
280
|
-
!dropdownRef.current?.contains(
|
|
281
|
-
event.relatedTarget as HTMLElement,
|
|
282
|
-
)
|
|
283
|
-
) {
|
|
284
|
-
setShowDropdown(false);
|
|
285
|
-
}
|
|
286
|
-
return false;
|
|
287
|
-
},
|
|
288
|
-
},
|
|
289
|
-
dispatchTransaction(tr) {
|
|
290
|
-
if (readOnly) {
|
|
291
|
-
// In readOnly mode, only allow selection, not document changes
|
|
292
|
-
if (tr.docChanged) {
|
|
293
|
-
// Block any transaction that changes the document
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
// Allow selection-only changes for text selection
|
|
297
|
-
if (viewRef.current) {
|
|
298
|
-
const newState = viewRef.current.state.apply(tr);
|
|
299
|
-
viewRef.current.updateState(newState);
|
|
300
|
-
}
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (viewRef.current?.hasFocus()) {
|
|
305
|
-
trackTypingFromStart(tr);
|
|
306
|
-
|
|
307
|
-
const newState = viewRef.current.state.apply(tr);
|
|
308
|
-
viewRef.current.updateState(newState);
|
|
309
|
-
const { selectedText } = getCurrentText(
|
|
310
|
-
newState.doc,
|
|
311
|
-
newState.selection.from,
|
|
312
|
-
newState.selection.to,
|
|
313
|
-
isTypingFromStartRange.current,
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
setCurrentText(selectedText);
|
|
317
|
-
|
|
318
|
-
const shouldShowDropdown = getShouldShowDropdown(
|
|
319
|
-
viewRef.current,
|
|
320
|
-
selectedText,
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
if (shouldShowDropdown && searchContributors) {
|
|
324
|
-
// Fetch contributors based on the selected text
|
|
325
|
-
void searchContributors(selectedText)
|
|
326
|
-
.then((contributors) => {
|
|
327
|
-
setTaggedContributors(contributors);
|
|
328
|
-
})
|
|
329
|
-
.catch((error) => {
|
|
330
|
-
console.error(
|
|
331
|
-
'Error fetching tagged contributors:',
|
|
332
|
-
error,
|
|
333
|
-
);
|
|
334
|
-
setTaggedContributors([]);
|
|
335
|
-
});
|
|
336
|
-
} else {
|
|
337
|
-
setTaggedContributors([]);
|
|
338
|
-
}
|
|
339
|
-
setShowDropdown(shouldShowDropdown);
|
|
340
|
-
|
|
341
|
-
// Update the current document state after each transaction
|
|
342
|
-
// if a transform step has happened
|
|
343
|
-
if (tr.docChanged) {
|
|
344
|
-
setCurrentDoc(newState.doc);
|
|
345
|
-
handleSave(convertNodeToBylineModel(newState.doc));
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
},
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
return () => {
|
|
352
|
-
viewRef.current?.destroy();
|
|
353
|
-
};
|
|
354
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this once
|
|
355
|
-
}, []);
|
|
356
|
-
|
|
357
|
-
// Handle dropdown functionality
|
|
358
|
-
useEffect(() => {
|
|
359
|
-
const numberOfOptions =
|
|
360
|
-
taggedContributors.length + (allowUntaggedContributors ? 1 : 0);
|
|
361
|
-
if (numberOfOptions) {
|
|
362
|
-
// Going up from the first option
|
|
363
|
-
if (currentOptionIndex < 0) {
|
|
364
|
-
setCurrentOptionIndex(numberOfOptions - 1);
|
|
365
|
-
} else {
|
|
366
|
-
setCurrentOptionIndex(currentOptionIndex % numberOfOptions);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
if (showDropdown) {
|
|
370
|
-
const editor = document.querySelector('[role=combobox]');
|
|
371
|
-
editor?.setAttribute(
|
|
372
|
-
'aria-activedescendant',
|
|
373
|
-
`contributor-option-${currentOptionIndex}`,
|
|
374
|
-
);
|
|
375
|
-
editor?.setAttribute('aria-expanded', 'true');
|
|
376
|
-
}
|
|
377
|
-
}, [
|
|
378
|
-
currentOptionIndex,
|
|
379
|
-
showDropdown,
|
|
380
|
-
taggedContributors.length,
|
|
381
|
-
allowUntaggedContributors,
|
|
382
|
-
]);
|
|
383
|
-
|
|
384
|
-
// Handle dropdown scrolling when changing index
|
|
385
|
-
useEffect(() => {
|
|
386
|
-
if (showDropdown && currentOptionIndex >= 0) {
|
|
387
|
-
const selectedOption = document.getElementById(
|
|
388
|
-
`contributor-option-${currentOptionIndex}`,
|
|
389
|
-
);
|
|
390
|
-
if (selectedOption && dropdownRef.current) {
|
|
391
|
-
selectedOption.scrollIntoView({
|
|
392
|
-
behavior: 'smooth',
|
|
393
|
-
block: 'nearest',
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}, [currentOptionIndex, showDropdown]);
|
|
398
|
-
|
|
399
|
-
// Handle Enter key press in the dropdown
|
|
400
|
-
useEffect(() => {
|
|
401
|
-
if (enterHit) {
|
|
402
|
-
if (
|
|
403
|
-
allowUntaggedContributors &&
|
|
404
|
-
currentOptionIndex === taggedContributors.length
|
|
405
|
-
) {
|
|
406
|
-
addUntaggedContributor(
|
|
407
|
-
viewRef,
|
|
408
|
-
setShowDropdown,
|
|
409
|
-
contributorLimit,
|
|
410
|
-
isTypingFromStartRange.current,
|
|
411
|
-
);
|
|
412
|
-
} else {
|
|
413
|
-
const contributorToAdd = taggedContributors[currentOptionIndex];
|
|
414
|
-
|
|
415
|
-
if (contributorToAdd) {
|
|
416
|
-
addTaggedContributor(
|
|
417
|
-
contributorToAdd,
|
|
418
|
-
viewRef,
|
|
419
|
-
setShowDropdown,
|
|
420
|
-
contributorLimit,
|
|
421
|
-
isTypingFromStartRange.current,
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
setEnterHit(false);
|
|
427
|
-
}
|
|
428
|
-
}, [
|
|
429
|
-
currentOptionIndex,
|
|
430
|
-
enterHit,
|
|
431
|
-
taggedContributors,
|
|
432
|
-
contributorLimit,
|
|
433
|
-
allowUntaggedContributors,
|
|
434
|
-
]);
|
|
435
|
-
|
|
436
|
-
return (
|
|
437
|
-
<div css={bylineContainerStyles}>
|
|
438
|
-
<div css={bylineEditorStyles(theme?.editor)} ref={editorRef} onBlur={onBlur} />
|
|
439
|
-
<div
|
|
440
|
-
ref={dropdownRef}
|
|
441
|
-
tabIndex={0}
|
|
442
|
-
css={dropdownContainerStyles(
|
|
443
|
-
showDropdown &&
|
|
444
|
-
// show the dropdown if there are tagged contributors to select or untagged contributors are allowed
|
|
445
|
-
(taggedContributors.length > 0 ||
|
|
446
|
-
!!allowUntaggedContributors),
|
|
447
|
-
theme?.dropdown,
|
|
448
|
-
)}
|
|
449
|
-
>
|
|
450
|
-
<ul id="byline-dropdown" role="listbox" css={dropdownUlStyles}>
|
|
451
|
-
{taggedContributors.map((contributor, i) => (
|
|
452
|
-
<li
|
|
453
|
-
key={contributor.tagId}
|
|
454
|
-
id={`contributor-option-${i}`}
|
|
455
|
-
role="option"
|
|
456
|
-
aria-selected={i === currentOptionIndex}
|
|
457
|
-
css={[
|
|
458
|
-
dropdownLiStyles(theme),
|
|
459
|
-
i === currentOptionIndex &&
|
|
460
|
-
selectedDropdownLiStyles(theme),
|
|
461
|
-
]}
|
|
462
|
-
onMouseMove={() => {
|
|
463
|
-
if (currentOptionIndex !== i) {
|
|
464
|
-
setCurrentOptionIndex(i);
|
|
465
|
-
}
|
|
466
|
-
}}
|
|
467
|
-
onMouseDown={(e) => {
|
|
468
|
-
e.preventDefault(); // Prevent focus loss
|
|
469
|
-
addTaggedContributor(
|
|
470
|
-
contributor,
|
|
471
|
-
viewRef,
|
|
472
|
-
setShowDropdown,
|
|
473
|
-
contributorLimit,
|
|
474
|
-
isTypingFromStartRange.current,
|
|
475
|
-
);
|
|
476
|
-
}}
|
|
477
|
-
>
|
|
478
|
-
{contributor.internalLabel ?? contributor.label}
|
|
479
|
-
</li>
|
|
480
|
-
))}
|
|
481
|
-
{allowUntaggedContributors && (
|
|
482
|
-
<li
|
|
483
|
-
role="option"
|
|
484
|
-
id={`contributor-option-${taggedContributors.length}`}
|
|
485
|
-
aria-selected={
|
|
486
|
-
currentOptionIndex === taggedContributors.length
|
|
487
|
-
}
|
|
488
|
-
css={[
|
|
489
|
-
dropdownLiStyles(theme),
|
|
490
|
-
currentOptionIndex ===
|
|
491
|
-
taggedContributors.length &&
|
|
492
|
-
selectedDropdownLiStyles(theme),
|
|
493
|
-
]}
|
|
494
|
-
onMouseMove={() => {
|
|
495
|
-
if (
|
|
496
|
-
currentOptionIndex !==
|
|
497
|
-
taggedContributors.length
|
|
498
|
-
) {
|
|
499
|
-
setCurrentOptionIndex(
|
|
500
|
-
taggedContributors.length,
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
}}
|
|
504
|
-
onMouseDown={(e) => {
|
|
505
|
-
e.preventDefault(); // Prevent focus loss
|
|
506
|
-
addUntaggedContributor(
|
|
507
|
-
viewRef,
|
|
508
|
-
setShowDropdown,
|
|
509
|
-
contributorLimit,
|
|
510
|
-
isTypingFromStartRange.current,
|
|
511
|
-
);
|
|
512
|
-
}}
|
|
513
|
-
>
|
|
514
|
-
Add "{currentText}" as untagged
|
|
515
|
-
contributor
|
|
516
|
-
</li>
|
|
517
|
-
)}
|
|
518
|
-
</ul>
|
|
519
|
-
</div>
|
|
520
|
-
|
|
521
|
-
{enablePreview && <Preview doc={currentDoc} />}
|
|
522
|
-
</div>
|
|
523
|
-
);
|
|
524
|
-
};
|
package/src/byline/Preview.tsx
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import type { Node } from 'prosemirror-model';
|
|
2
|
-
import {
|
|
3
|
-
previewContributorStyles,
|
|
4
|
-
previewFreeTextStyles,
|
|
5
|
-
previewStyles,
|
|
6
|
-
} from './styles';
|
|
7
|
-
|
|
8
|
-
export const Preview = ({ doc }: { doc: Node | null }) => {
|
|
9
|
-
if (doc?.childCount === undefined || doc.childCount === 0) {
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const parts: Node[] = [];
|
|
14
|
-
|
|
15
|
-
doc.descendants((node) => {
|
|
16
|
-
parts.push(node);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<div css={previewStyles} data-testid="byline-preview">
|
|
21
|
-
{parts.map((node, i) => {
|
|
22
|
-
if (node.isText) {
|
|
23
|
-
return (
|
|
24
|
-
<span
|
|
25
|
-
key={`${node.text}${i}`}
|
|
26
|
-
css={previewFreeTextStyles}
|
|
27
|
-
>
|
|
28
|
-
{node.text}
|
|
29
|
-
</span>
|
|
30
|
-
);
|
|
31
|
-
} else if (node.type.name === 'chip') {
|
|
32
|
-
if (node.attrs.path) {
|
|
33
|
-
return (
|
|
34
|
-
<a
|
|
35
|
-
key={`${node.text}${i}`}
|
|
36
|
-
css={previewContributorStyles(node)}
|
|
37
|
-
href={`https://theguardian.com/${node.attrs.path}`}
|
|
38
|
-
target="_blank"
|
|
39
|
-
rel="noopener noreferrer"
|
|
40
|
-
>
|
|
41
|
-
{node.attrs.label as string}
|
|
42
|
-
</a>
|
|
43
|
-
);
|
|
44
|
-
} else {
|
|
45
|
-
return (
|
|
46
|
-
<span
|
|
47
|
-
key={`${node.text}${i}`}
|
|
48
|
-
css={previewContributorStyles(node)}
|
|
49
|
-
>
|
|
50
|
-
{node.attrs.label as string}
|
|
51
|
-
</span>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
})}
|
|
57
|
-
</div>
|
|
58
|
-
);
|
|
59
|
-
};
|