@guardian/stand 0.0.0 → 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.
Files changed (39) hide show
  1. package/README.md +19 -0
  2. package/dist/byline/Byline.cjs +375 -0
  3. package/dist/byline/Byline.js +273 -0
  4. package/dist/byline/Preview.cjs +52 -0
  5. package/dist/byline/Preview.js +26 -0
  6. package/dist/byline/lib.cjs +240 -0
  7. package/dist/byline/lib.js +181 -0
  8. package/dist/byline/placeholder.cjs +29 -0
  9. package/dist/byline/placeholder.js +27 -0
  10. package/dist/byline/plugins.cjs +144 -0
  11. package/dist/byline/plugins.js +123 -0
  12. package/dist/byline/schema.cjs +66 -0
  13. package/dist/byline/schema.js +59 -0
  14. package/dist/byline/styles.cjs +244 -0
  15. package/dist/byline/styles.js +234 -0
  16. package/dist/index.cjs +4 -4
  17. package/dist/index.js +1 -5
  18. package/dist/types/.storybook/main.d.ts +3 -0
  19. package/dist/types/.storybook/preview.d.ts +3 -0
  20. package/dist/types/jest-setup-after-env.d.ts +1 -0
  21. package/dist/types/playwright/byline.mock.d.ts +3 -0
  22. package/dist/types/playwright/byline.spec.d.ts +1 -0
  23. package/dist/types/playwright-ct.config.d.ts +5 -0
  24. package/dist/types/src/byline/Byline.d.ts +17 -0
  25. package/dist/types/src/byline/Byline.stories.d.ts +206 -0
  26. package/dist/types/src/byline/Preview.d.ts +4 -0
  27. package/dist/types/src/byline/contributors-fixture.d.ts +1 -0
  28. package/dist/types/src/byline/lib.d.ts +48 -0
  29. package/dist/types/src/byline/lib.test.d.ts +1 -0
  30. package/dist/types/src/byline/placeholder.d.ts +2 -0
  31. package/dist/types/src/byline/plugins.d.ts +4 -0
  32. package/dist/types/src/byline/schema.d.ts +2 -0
  33. package/dist/types/src/byline/styles.d.ts +11 -0
  34. package/dist/types/src/byline/theme.d.ts +44 -0
  35. package/dist/types/src/byline/util.d.ts +3 -0
  36. package/dist/types/src/index.d.ts +2 -0
  37. package/package.json +60 -126
  38. package/LICENSE +0 -201
  39. package/dist/index.d.ts +0 -3
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # Stand - a tools component library
2
+
3
+ _Find what you need on the (news)stand!_
4
+
5
+ Stand is component library for Guardian editorial tools. It is co-located within flexible-content as Composer is expected to be the main consumer of the UI components within Stand. But any editorial tool should be able to make use of the components as an npm package - `@guardian/stand` - and developers should feel comfortable contributing.
6
+
7
+ ## Setup
8
+
9
+ - Run `./setup.sh` in the project root (flexible-content) directory to set up pnpm, install dependencies, and build the project.
10
+
11
+ ## Tasks
12
+
13
+ - Run `pnpm install` to install dependencies.
14
+ - Run `pnpm build` to build, this makes any changes available to flexible-frontend
15
+ - Run `pnpm storybook` to run Storybook
16
+
17
+ ## Contributing
18
+
19
+ See the [Contributing to Stand](./CONTRIBUTING.md) documentation
@@ -0,0 +1,375 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('@emotion/react/jsx-runtime');
4
+ var prosemirrorInvisibles = require('@guardian/prosemirror-invisibles');
5
+ var prosemirrorDropcursor = require('prosemirror-dropcursor');
6
+ var prosemirrorHistory = require('prosemirror-history');
7
+ var prosemirrorState = require('prosemirror-state');
8
+ var prosemirrorView = require('prosemirror-view');
9
+ var react = require('react');
10
+ var lib = require('./lib.cjs');
11
+ var placeholder = require('./placeholder.cjs');
12
+ var plugins = require('./plugins.cjs');
13
+ var Preview = require('./Preview.cjs');
14
+ var schema = require('./schema.cjs');
15
+ var styles = require('./styles.cjs');
16
+
17
+ const Byline = ({
18
+ theme,
19
+ allowUntaggedContributors,
20
+ contributorLimit,
21
+ enablePreview,
22
+ placeholder: placeholder$1,
23
+ initialValue,
24
+ readOnly,
25
+ handleSave,
26
+ searchContributors,
27
+ onBlur
28
+ }) => {
29
+ const editorRef = react.useRef(null);
30
+ const viewRef = react.useRef(null);
31
+ const dropdownRef = react.useRef(null);
32
+ const [currentText, setCurrentText] = react.useState("");
33
+ const [currentOptionIndex, setCurrentOptionIndex] = react.useState(0);
34
+ const [taggedContributors, setTaggedContributors] = react.useState([]);
35
+ const [currentDoc, setCurrentDoc] = react.useState(null);
36
+ const [showDropdown, setShowDropdown] = react.useState(false);
37
+ const isTypingFromStartRange = react.useRef(
38
+ void 0
39
+ );
40
+ const trackTypingFromStart = (tr) => {
41
+ const isCursorSelection = tr.selection.from === tr.selection.to;
42
+ const currentPosition = tr.selection.from;
43
+ const hasChanges = tr.steps.length > 0;
44
+ if (!isCursorSelection) {
45
+ isTypingFromStartRange.current = void 0;
46
+ return;
47
+ }
48
+ if (!isTypingFromStartRange.current && currentPosition === 1 && hasChanges) {
49
+ isTypingFromStartRange.current = {
50
+ start: 1,
51
+ maxReached: 1,
52
+ lastPosition: 1
53
+ };
54
+ return;
55
+ }
56
+ if (isTypingFromStartRange.current) {
57
+ const { start, maxReached, lastPosition } = isTypingFromStartRange.current;
58
+ const isWithinRange = currentPosition >= start && currentPosition <= maxReached;
59
+ const isExtendingRange = hasChanges && currentPosition === maxReached + 1;
60
+ if (isWithinRange || isExtendingRange) {
61
+ if (hasChanges) {
62
+ const positionDelta = currentPosition - lastPosition;
63
+ if (positionDelta < 0) {
64
+ isTypingFromStartRange.current.maxReached = maxReached - 1;
65
+ } else if (positionDelta > 0) {
66
+ isTypingFromStartRange.current.maxReached = maxReached + 1;
67
+ }
68
+ isTypingFromStartRange.current.lastPosition = currentPosition;
69
+ }
70
+ return;
71
+ }
72
+ }
73
+ isTypingFromStartRange.current = void 0;
74
+ };
75
+ const getShouldShowDropdown = (view, selectedText) => {
76
+ if (readOnly) {
77
+ return false;
78
+ }
79
+ const showDropdownBasedOnProps = !!searchContributors || !!allowUntaggedContributors;
80
+ if (lib.hasHitContributorLimit(view.state.doc, contributorLimit)) {
81
+ return false;
82
+ }
83
+ return selectedText.trim() !== "" && showDropdownBasedOnProps;
84
+ };
85
+ const [enterHit, setEnterHit] = react.useState(false);
86
+ const checkDropdownVisibility = () => {
87
+ if (dropdownRef.current?.checkVisibility) {
88
+ return dropdownRef.current.checkVisibility();
89
+ } else {
90
+ return dropdownRef.current?.offsetParent !== null;
91
+ }
92
+ };
93
+ react.useEffect(() => {
94
+ if (!editorRef.current) {
95
+ return;
96
+ }
97
+ const initialDoc = lib.convertBylineModelToNode(initialValue);
98
+ const state = prosemirrorState.EditorState.create({
99
+ schema: schema.bylineEditorSchema,
100
+ plugins: [
101
+ prosemirrorDropcursor.dropCursor(),
102
+ plugins.clipboardPlugin(allowUntaggedContributors, contributorLimit),
103
+ prosemirrorHistory.history(),
104
+ plugins.keybindings(),
105
+ prosemirrorInvisibles.createInvisiblesPlugin([prosemirrorInvisibles.space, prosemirrorInvisibles.nbSpace], {
106
+ displayLineEndSelection: true,
107
+ shouldShowInvisibles: true
108
+ }),
109
+ plugins.bylinePlugin(),
110
+ placeholder.createPlaceholderPlugin(placeholder$1 ?? "Enter a byline...")
111
+ ],
112
+ doc: initialDoc
113
+ });
114
+ setCurrentDoc(initialDoc);
115
+ viewRef.current = new prosemirrorView.EditorView(editorRef.current, {
116
+ state,
117
+ editable: () => !readOnly,
118
+ attributes: {
119
+ role: "combobox",
120
+ "aria-label": "byline",
121
+ "aria-controls": "byline-dropdown",
122
+ "aria-expanded": "false",
123
+ "data-testid": "byline-input",
124
+ ...readOnly && { "aria-readonly": "true" }
125
+ },
126
+ handleDOMEvents: {
127
+ keydown: (view, event) => {
128
+ if (readOnly) {
129
+ return false;
130
+ }
131
+ if (event.key === "Escape") {
132
+ setShowDropdown(false);
133
+ return true;
134
+ }
135
+ if (event.key === "ArrowDown") {
136
+ if (checkDropdownVisibility()) {
137
+ event.preventDefault();
138
+ setCurrentOptionIndex((currentOptionIndex2) => {
139
+ return currentOptionIndex2 + 1;
140
+ });
141
+ return true;
142
+ }
143
+ return false;
144
+ }
145
+ if (event.key === "ArrowUp") {
146
+ if (checkDropdownVisibility()) {
147
+ event.preventDefault();
148
+ setCurrentOptionIndex((currentOptionIndex2) => {
149
+ return currentOptionIndex2 - 1;
150
+ });
151
+ return true;
152
+ }
153
+ return false;
154
+ }
155
+ if (event.key === "Enter") {
156
+ if (checkDropdownVisibility()) {
157
+ event.preventDefault();
158
+ setEnterHit(true);
159
+ }
160
+ return false;
161
+ }
162
+ return false;
163
+ },
164
+ blur: (_view, event) => {
165
+ if (readOnly) {
166
+ return false;
167
+ }
168
+ if (!dropdownRef.current?.contains(
169
+ event.relatedTarget
170
+ )) {
171
+ setShowDropdown(false);
172
+ }
173
+ return false;
174
+ }
175
+ },
176
+ dispatchTransaction(tr) {
177
+ if (readOnly) {
178
+ if (tr.docChanged) {
179
+ return;
180
+ }
181
+ if (viewRef.current) {
182
+ const newState = viewRef.current.state.apply(tr);
183
+ viewRef.current.updateState(newState);
184
+ }
185
+ return;
186
+ }
187
+ if (viewRef.current?.hasFocus()) {
188
+ trackTypingFromStart(tr);
189
+ const newState = viewRef.current.state.apply(tr);
190
+ viewRef.current.updateState(newState);
191
+ const { selectedText } = lib.getCurrentText(
192
+ newState.doc,
193
+ newState.selection.from,
194
+ newState.selection.to,
195
+ isTypingFromStartRange.current
196
+ );
197
+ setCurrentText(selectedText);
198
+ const shouldShowDropdown = getShouldShowDropdown(
199
+ viewRef.current,
200
+ selectedText
201
+ );
202
+ if (shouldShowDropdown && searchContributors) {
203
+ void searchContributors(selectedText).then((contributors) => {
204
+ setTaggedContributors(contributors);
205
+ }).catch((error) => {
206
+ console.error(
207
+ "Error fetching tagged contributors:",
208
+ error
209
+ );
210
+ setTaggedContributors([]);
211
+ });
212
+ } else {
213
+ setTaggedContributors([]);
214
+ }
215
+ setShowDropdown(shouldShowDropdown);
216
+ if (tr.docChanged) {
217
+ setCurrentDoc(newState.doc);
218
+ handleSave(lib.convertNodeToBylineModel(newState.doc));
219
+ }
220
+ }
221
+ }
222
+ });
223
+ return () => {
224
+ viewRef.current?.destroy();
225
+ };
226
+ }, []);
227
+ react.useEffect(() => {
228
+ const numberOfOptions = taggedContributors.length + (allowUntaggedContributors ? 1 : 0);
229
+ if (numberOfOptions) {
230
+ if (currentOptionIndex < 0) {
231
+ setCurrentOptionIndex(numberOfOptions - 1);
232
+ } else {
233
+ setCurrentOptionIndex(currentOptionIndex % numberOfOptions);
234
+ }
235
+ }
236
+ if (showDropdown) {
237
+ const editor = document.querySelector("[role=combobox]");
238
+ editor?.setAttribute(
239
+ "aria-activedescendant",
240
+ `contributor-option-${currentOptionIndex}`
241
+ );
242
+ editor?.setAttribute("aria-expanded", "true");
243
+ }
244
+ }, [
245
+ currentOptionIndex,
246
+ showDropdown,
247
+ taggedContributors.length,
248
+ allowUntaggedContributors
249
+ ]);
250
+ react.useEffect(() => {
251
+ if (showDropdown && currentOptionIndex >= 0) {
252
+ const selectedOption = document.getElementById(
253
+ `contributor-option-${currentOptionIndex}`
254
+ );
255
+ if (selectedOption && dropdownRef.current) {
256
+ selectedOption.scrollIntoView({
257
+ behavior: "smooth",
258
+ block: "nearest"
259
+ });
260
+ }
261
+ }
262
+ }, [currentOptionIndex, showDropdown]);
263
+ react.useEffect(() => {
264
+ if (enterHit) {
265
+ if (allowUntaggedContributors && currentOptionIndex === taggedContributors.length) {
266
+ lib.addUntaggedContributor(
267
+ viewRef,
268
+ setShowDropdown,
269
+ contributorLimit,
270
+ isTypingFromStartRange.current
271
+ );
272
+ } else {
273
+ const contributorToAdd = taggedContributors[currentOptionIndex];
274
+ if (contributorToAdd) {
275
+ lib.addTaggedContributor(
276
+ contributorToAdd,
277
+ viewRef,
278
+ setShowDropdown,
279
+ contributorLimit,
280
+ isTypingFromStartRange.current
281
+ );
282
+ }
283
+ }
284
+ setEnterHit(false);
285
+ }
286
+ }, [
287
+ currentOptionIndex,
288
+ enterHit,
289
+ taggedContributors,
290
+ contributorLimit,
291
+ allowUntaggedContributors
292
+ ]);
293
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { css: styles.bylineContainerStyles, children: [
294
+ /* @__PURE__ */ jsxRuntime.jsx("div", { css: styles.bylineEditorStyles(theme?.editor), ref: editorRef, onBlur }),
295
+ /* @__PURE__ */ jsxRuntime.jsx(
296
+ "div",
297
+ {
298
+ ref: dropdownRef,
299
+ tabIndex: 0,
300
+ css: styles.dropdownContainerStyles(
301
+ showDropdown && // show the dropdown if there are tagged contributors to select or untagged contributors are allowed
302
+ (taggedContributors.length > 0 || !!allowUntaggedContributors),
303
+ theme?.dropdown
304
+ ),
305
+ children: /* @__PURE__ */ jsxRuntime.jsxs("ul", { id: "byline-dropdown", role: "listbox", css: styles.dropdownUlStyles, children: [
306
+ taggedContributors.map((contributor, i) => /* @__PURE__ */ jsxRuntime.jsx(
307
+ "li",
308
+ {
309
+ id: `contributor-option-${i}`,
310
+ role: "option",
311
+ "aria-selected": i === currentOptionIndex,
312
+ css: [
313
+ styles.dropdownLiStyles(theme),
314
+ i === currentOptionIndex && styles.selectedDropdownLiStyles(theme)
315
+ ],
316
+ onMouseMove: () => {
317
+ if (currentOptionIndex !== i) {
318
+ setCurrentOptionIndex(i);
319
+ }
320
+ },
321
+ onMouseDown: (e) => {
322
+ e.preventDefault();
323
+ lib.addTaggedContributor(
324
+ contributor,
325
+ viewRef,
326
+ setShowDropdown,
327
+ contributorLimit,
328
+ isTypingFromStartRange.current
329
+ );
330
+ },
331
+ children: contributor.internalLabel ?? contributor.label
332
+ },
333
+ contributor.tagId
334
+ )),
335
+ allowUntaggedContributors && /* @__PURE__ */ jsxRuntime.jsxs(
336
+ "li",
337
+ {
338
+ role: "option",
339
+ id: `contributor-option-${taggedContributors.length}`,
340
+ "aria-selected": currentOptionIndex === taggedContributors.length,
341
+ css: [
342
+ styles.dropdownLiStyles(theme),
343
+ currentOptionIndex === taggedContributors.length && styles.selectedDropdownLiStyles(theme)
344
+ ],
345
+ onMouseMove: () => {
346
+ if (currentOptionIndex !== taggedContributors.length) {
347
+ setCurrentOptionIndex(
348
+ taggedContributors.length
349
+ );
350
+ }
351
+ },
352
+ onMouseDown: (e) => {
353
+ e.preventDefault();
354
+ lib.addUntaggedContributor(
355
+ viewRef,
356
+ setShowDropdown,
357
+ contributorLimit,
358
+ isTypingFromStartRange.current
359
+ );
360
+ },
361
+ children: [
362
+ 'Add "',
363
+ currentText,
364
+ '" as untagged contributor'
365
+ ]
366
+ }
367
+ )
368
+ ] })
369
+ }
370
+ ),
371
+ enablePreview && /* @__PURE__ */ jsxRuntime.jsx(Preview.Preview, { doc: currentDoc })
372
+ ] });
373
+ };
374
+
375
+ exports.Byline = Byline;
@@ -0,0 +1,273 @@
1
+ import { jsxs, jsx } from '@emotion/react/jsx-runtime';
2
+ import { createInvisiblesPlugin, space, nbSpace } from '@guardian/prosemirror-invisibles';
3
+ import { dropCursor } from 'prosemirror-dropcursor';
4
+ import { history } from 'prosemirror-history';
5
+ import { EditorState } from 'prosemirror-state';
6
+ import { EditorView } from 'prosemirror-view';
7
+ import { useRef, useState, useEffect } from 'react';
8
+ import { convertBylineModelToNode, getCurrentText, convertNodeToBylineModel, addUntaggedContributor, addTaggedContributor, hasHitContributorLimit } from './lib.js';
9
+ import { createPlaceholderPlugin } from './placeholder.js';
10
+ import { clipboardPlugin, keybindings, bylinePlugin } from './plugins.js';
11
+ import { Preview } from './Preview.js';
12
+ import { bylineEditorSchema } from './schema.js';
13
+ import { bylineContainerStyles, bylineEditorStyles, dropdownContainerStyles, dropdownUlStyles, dropdownLiStyles, selectedDropdownLiStyles } from './styles.js';
14
+
15
+ const Byline = ({ theme, allowUntaggedContributors, contributorLimit, enablePreview, placeholder, initialValue, readOnly, handleSave, searchContributors, onBlur }) => {
16
+ const editorRef = useRef(null);
17
+ const viewRef = useRef(null);
18
+ const dropdownRef = useRef(null);
19
+ const [currentText, setCurrentText] = useState("");
20
+ const [currentOptionIndex, setCurrentOptionIndex] = useState(0);
21
+ const [taggedContributors, setTaggedContributors] = useState([]);
22
+ const [currentDoc, setCurrentDoc] = useState(null);
23
+ const [showDropdown, setShowDropdown] = useState(false);
24
+ const isTypingFromStartRange = useRef(void 0);
25
+ const trackTypingFromStart = (tr) => {
26
+ const isCursorSelection = tr.selection.from === tr.selection.to;
27
+ const currentPosition = tr.selection.from;
28
+ const hasChanges = tr.steps.length > 0;
29
+ if (!isCursorSelection) {
30
+ isTypingFromStartRange.current = void 0;
31
+ return;
32
+ }
33
+ if (!isTypingFromStartRange.current && currentPosition === 1 && hasChanges) {
34
+ isTypingFromStartRange.current = {
35
+ start: 1,
36
+ maxReached: 1,
37
+ lastPosition: 1
38
+ };
39
+ return;
40
+ }
41
+ if (isTypingFromStartRange.current) {
42
+ const { start, maxReached, lastPosition } = isTypingFromStartRange.current;
43
+ const isWithinRange = currentPosition >= start && currentPosition <= maxReached;
44
+ const isExtendingRange = hasChanges && currentPosition === maxReached + 1;
45
+ if (isWithinRange || isExtendingRange) {
46
+ if (hasChanges) {
47
+ const positionDelta = currentPosition - lastPosition;
48
+ if (positionDelta < 0) {
49
+ isTypingFromStartRange.current.maxReached = maxReached - 1;
50
+ } else if (positionDelta > 0) {
51
+ isTypingFromStartRange.current.maxReached = maxReached + 1;
52
+ }
53
+ isTypingFromStartRange.current.lastPosition = currentPosition;
54
+ }
55
+ return;
56
+ }
57
+ }
58
+ isTypingFromStartRange.current = void 0;
59
+ };
60
+ const getShouldShowDropdown = (view, selectedText) => {
61
+ if (readOnly) {
62
+ return false;
63
+ }
64
+ const showDropdownBasedOnProps = !!searchContributors || !!allowUntaggedContributors;
65
+ if (hasHitContributorLimit(view.state.doc, contributorLimit)) {
66
+ return false;
67
+ }
68
+ return selectedText.trim() !== "" && showDropdownBasedOnProps;
69
+ };
70
+ const [enterHit, setEnterHit] = useState(false);
71
+ const checkDropdownVisibility = () => {
72
+ if (dropdownRef.current?.checkVisibility) {
73
+ return dropdownRef.current.checkVisibility();
74
+ } else {
75
+ return dropdownRef.current?.offsetParent !== null;
76
+ }
77
+ };
78
+ useEffect(() => {
79
+ if (!editorRef.current) {
80
+ return;
81
+ }
82
+ const initialDoc = convertBylineModelToNode(initialValue);
83
+ const state = EditorState.create({
84
+ schema: bylineEditorSchema,
85
+ plugins: [
86
+ dropCursor(),
87
+ clipboardPlugin(allowUntaggedContributors, contributorLimit),
88
+ history(),
89
+ keybindings(),
90
+ createInvisiblesPlugin([space, nbSpace], {
91
+ displayLineEndSelection: true,
92
+ shouldShowInvisibles: true
93
+ }),
94
+ bylinePlugin(),
95
+ createPlaceholderPlugin(placeholder ?? "Enter a byline...")
96
+ ],
97
+ doc: initialDoc
98
+ });
99
+ setCurrentDoc(initialDoc);
100
+ viewRef.current = new EditorView(editorRef.current, {
101
+ state,
102
+ editable: () => !readOnly,
103
+ attributes: {
104
+ role: "combobox",
105
+ "aria-label": "byline",
106
+ "aria-controls": "byline-dropdown",
107
+ "aria-expanded": "false",
108
+ "data-testid": "byline-input",
109
+ ...readOnly && { "aria-readonly": "true" }
110
+ },
111
+ handleDOMEvents: {
112
+ keydown: (view, event) => {
113
+ if (readOnly) {
114
+ return false;
115
+ }
116
+ if (event.key === "Escape") {
117
+ setShowDropdown(false);
118
+ return true;
119
+ }
120
+ if (event.key === "ArrowDown") {
121
+ if (checkDropdownVisibility()) {
122
+ event.preventDefault();
123
+ setCurrentOptionIndex((currentOptionIndex2) => {
124
+ return currentOptionIndex2 + 1;
125
+ });
126
+ return true;
127
+ }
128
+ return false;
129
+ }
130
+ if (event.key === "ArrowUp") {
131
+ if (checkDropdownVisibility()) {
132
+ event.preventDefault();
133
+ setCurrentOptionIndex((currentOptionIndex2) => {
134
+ return currentOptionIndex2 - 1;
135
+ });
136
+ return true;
137
+ }
138
+ return false;
139
+ }
140
+ if (event.key === "Enter") {
141
+ if (checkDropdownVisibility()) {
142
+ event.preventDefault();
143
+ setEnterHit(true);
144
+ }
145
+ return false;
146
+ }
147
+ return false;
148
+ },
149
+ blur: (_view, event) => {
150
+ if (readOnly) {
151
+ return false;
152
+ }
153
+ if (!dropdownRef.current?.contains(event.relatedTarget)) {
154
+ setShowDropdown(false);
155
+ }
156
+ return false;
157
+ }
158
+ },
159
+ dispatchTransaction(tr) {
160
+ if (readOnly) {
161
+ if (tr.docChanged) {
162
+ return;
163
+ }
164
+ if (viewRef.current) {
165
+ const newState = viewRef.current.state.apply(tr);
166
+ viewRef.current.updateState(newState);
167
+ }
168
+ return;
169
+ }
170
+ if (viewRef.current?.hasFocus()) {
171
+ trackTypingFromStart(tr);
172
+ const newState = viewRef.current.state.apply(tr);
173
+ viewRef.current.updateState(newState);
174
+ const { selectedText } = getCurrentText(newState.doc, newState.selection.from, newState.selection.to, isTypingFromStartRange.current);
175
+ setCurrentText(selectedText);
176
+ const shouldShowDropdown = getShouldShowDropdown(viewRef.current, selectedText);
177
+ if (shouldShowDropdown && searchContributors) {
178
+ void searchContributors(selectedText).then((contributors) => {
179
+ setTaggedContributors(contributors);
180
+ }).catch((error) => {
181
+ console.error("Error fetching tagged contributors:", error);
182
+ setTaggedContributors([]);
183
+ });
184
+ } else {
185
+ setTaggedContributors([]);
186
+ }
187
+ setShowDropdown(shouldShowDropdown);
188
+ if (tr.docChanged) {
189
+ setCurrentDoc(newState.doc);
190
+ handleSave(convertNodeToBylineModel(newState.doc));
191
+ }
192
+ }
193
+ }
194
+ });
195
+ return () => {
196
+ viewRef.current?.destroy();
197
+ };
198
+ }, []);
199
+ useEffect(() => {
200
+ const numberOfOptions = taggedContributors.length + (allowUntaggedContributors ? 1 : 0);
201
+ if (numberOfOptions) {
202
+ if (currentOptionIndex < 0) {
203
+ setCurrentOptionIndex(numberOfOptions - 1);
204
+ } else {
205
+ setCurrentOptionIndex(currentOptionIndex % numberOfOptions);
206
+ }
207
+ }
208
+ if (showDropdown) {
209
+ const editor = document.querySelector("[role=combobox]");
210
+ editor?.setAttribute("aria-activedescendant", `contributor-option-${currentOptionIndex}`);
211
+ editor?.setAttribute("aria-expanded", "true");
212
+ }
213
+ }, [
214
+ currentOptionIndex,
215
+ showDropdown,
216
+ taggedContributors.length,
217
+ allowUntaggedContributors
218
+ ]);
219
+ useEffect(() => {
220
+ if (showDropdown && currentOptionIndex >= 0) {
221
+ const selectedOption = document.getElementById(`contributor-option-${currentOptionIndex}`);
222
+ if (selectedOption && dropdownRef.current) {
223
+ selectedOption.scrollIntoView({
224
+ behavior: "smooth",
225
+ block: "nearest"
226
+ });
227
+ }
228
+ }
229
+ }, [currentOptionIndex, showDropdown]);
230
+ useEffect(() => {
231
+ if (enterHit) {
232
+ if (allowUntaggedContributors && currentOptionIndex === taggedContributors.length) {
233
+ addUntaggedContributor(viewRef, setShowDropdown, contributorLimit, isTypingFromStartRange.current);
234
+ } else {
235
+ const contributorToAdd = taggedContributors[currentOptionIndex];
236
+ if (contributorToAdd) {
237
+ addTaggedContributor(contributorToAdd, viewRef, setShowDropdown, contributorLimit, isTypingFromStartRange.current);
238
+ }
239
+ }
240
+ setEnterHit(false);
241
+ }
242
+ }, [
243
+ currentOptionIndex,
244
+ enterHit,
245
+ taggedContributors,
246
+ contributorLimit,
247
+ allowUntaggedContributors
248
+ ]);
249
+ return jsxs("div", { css: bylineContainerStyles, children: [jsx("div", { css: bylineEditorStyles(theme?.editor), ref: editorRef, onBlur }), jsx("div", { ref: dropdownRef, tabIndex: 0, css: dropdownContainerStyles(showDropdown && // show the dropdown if there are tagged contributors to select or untagged contributors are allowed
250
+ (taggedContributors.length > 0 || !!allowUntaggedContributors), theme?.dropdown), children: jsxs("ul", { id: "byline-dropdown", role: "listbox", css: dropdownUlStyles, children: [taggedContributors.map((contributor, i) => jsx("li", { id: `contributor-option-${i}`, role: "option", "aria-selected": i === currentOptionIndex, css: [
251
+ dropdownLiStyles(theme),
252
+ i === currentOptionIndex && selectedDropdownLiStyles(theme)
253
+ ], onMouseMove: () => {
254
+ if (currentOptionIndex !== i) {
255
+ setCurrentOptionIndex(i);
256
+ }
257
+ }, onMouseDown: (e) => {
258
+ e.preventDefault();
259
+ addTaggedContributor(contributor, viewRef, setShowDropdown, contributorLimit, isTypingFromStartRange.current);
260
+ }, children: contributor.internalLabel ?? contributor.label }, contributor.tagId)), allowUntaggedContributors && jsxs("li", { role: "option", id: `contributor-option-${taggedContributors.length}`, "aria-selected": currentOptionIndex === taggedContributors.length, css: [
261
+ dropdownLiStyles(theme),
262
+ currentOptionIndex === taggedContributors.length && selectedDropdownLiStyles(theme)
263
+ ], onMouseMove: () => {
264
+ if (currentOptionIndex !== taggedContributors.length) {
265
+ setCurrentOptionIndex(taggedContributors.length);
266
+ }
267
+ }, onMouseDown: (e) => {
268
+ e.preventDefault();
269
+ addUntaggedContributor(viewRef, setShowDropdown, contributorLimit, isTypingFromStartRange.current);
270
+ }, children: ['Add "', currentText, '" as untagged contributor'] })] }) }), enablePreview && jsx(Preview, { doc: currentDoc })] });
271
+ };
272
+
273
+ export { Byline };