@guardian/stand 0.0.0 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.prettierrc +1 -0
  4. package/.storybook/main.ts +12 -0
  5. package/.storybook/preview.tsx +83 -0
  6. package/CHANGELOG.md +7 -0
  7. package/README.md +15 -0
  8. package/dist/byline/Byline.cjs +375 -0
  9. package/dist/byline/Byline.js +273 -0
  10. package/dist/byline/Preview.cjs +52 -0
  11. package/dist/byline/Preview.js +26 -0
  12. package/dist/byline/lib.cjs +240 -0
  13. package/dist/byline/lib.js +181 -0
  14. package/dist/byline/placeholder.cjs +29 -0
  15. package/dist/byline/placeholder.js +27 -0
  16. package/dist/byline/plugins.cjs +144 -0
  17. package/dist/byline/plugins.js +123 -0
  18. package/dist/byline/schema.cjs +66 -0
  19. package/dist/byline/schema.js +59 -0
  20. package/dist/byline/styles.cjs +244 -0
  21. package/dist/byline/styles.js +234 -0
  22. package/dist/index.cjs +4 -4
  23. package/dist/index.js +1 -5
  24. package/dist/types/.storybook/main.d.ts +3 -0
  25. package/dist/types/.storybook/preview.d.ts +3 -0
  26. package/dist/types/jest-setup-after-env.d.ts +1 -0
  27. package/dist/types/src/byline/Byline.d.ts +17 -0
  28. package/dist/types/src/byline/Byline.stories.d.ts +206 -0
  29. package/dist/types/src/byline/Byline.test.d.ts +1 -0
  30. package/dist/types/src/byline/Preview.d.ts +4 -0
  31. package/dist/types/src/byline/contributors-fixture.d.ts +1 -0
  32. package/dist/types/src/byline/lib.d.ts +48 -0
  33. package/dist/types/src/byline/lib.test.d.ts +1 -0
  34. package/dist/types/src/byline/placeholder.d.ts +2 -0
  35. package/dist/types/src/byline/plugins.d.ts +4 -0
  36. package/dist/types/src/byline/schema.d.ts +2 -0
  37. package/dist/types/src/byline/styles.d.ts +11 -0
  38. package/dist/types/src/byline/theme.d.ts +44 -0
  39. package/dist/types/src/byline/util.d.ts +3 -0
  40. package/dist/types/src/index.d.ts +2 -0
  41. package/dist/types/src/mocks/prosemirror-view.d.ts +10 -0
  42. package/eslint.config.js +14 -0
  43. package/jest-setup-after-env.ts +1 -0
  44. package/jest.config.js +12 -0
  45. package/package.json +60 -129
  46. package/rollup.config.js +49 -0
  47. package/src/byline/Byline.stories.tsx +186 -0
  48. package/src/byline/Byline.test.tsx +450 -0
  49. package/src/byline/Byline.tsx +524 -0
  50. package/src/byline/Preview.tsx +59 -0
  51. package/src/byline/contributors-fixture.ts +1006 -0
  52. package/src/byline/lib.test.ts +179 -0
  53. package/src/byline/lib.ts +426 -0
  54. package/src/byline/placeholder.ts +30 -0
  55. package/src/byline/plugins.ts +186 -0
  56. package/src/byline/schema.ts +62 -0
  57. package/src/byline/styles.ts +246 -0
  58. package/src/byline/theme.ts +45 -0
  59. package/src/byline/util.ts +5 -0
  60. package/src/index.ts +2 -0
  61. package/src/mocks/prosemirror-view.ts +19 -0
  62. package/tsconfig.json +19 -0
  63. package/LICENSE +0 -201
  64. package/dist/index.d.ts +0 -3
@@ -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 };
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('@emotion/react/jsx-runtime');
4
+ var styles = require('./styles.cjs');
5
+
6
+ const Preview = ({ doc }) => {
7
+ if (doc?.childCount === void 0 || doc.childCount === 0) {
8
+ return null;
9
+ }
10
+ const parts = [];
11
+ doc.descendants((node) => {
12
+ parts.push(node);
13
+ });
14
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { css: styles.previewStyles, "data-testid": "byline-preview", children: parts.map((node, i) => {
15
+ if (node.isText) {
16
+ return /* @__PURE__ */ jsxRuntime.jsx(
17
+ "span",
18
+ {
19
+ css: styles.previewFreeTextStyles,
20
+ children: node.text
21
+ },
22
+ `${node.text}${i}`
23
+ );
24
+ } else if (node.type.name === "chip") {
25
+ if (node.attrs.path) {
26
+ return /* @__PURE__ */ jsxRuntime.jsx(
27
+ "a",
28
+ {
29
+ css: styles.previewContributorStyles(node),
30
+ href: `https://theguardian.com/${node.attrs.path}`,
31
+ target: "_blank",
32
+ rel: "noopener noreferrer",
33
+ children: node.attrs.label
34
+ },
35
+ `${node.text}${i}`
36
+ );
37
+ } else {
38
+ return /* @__PURE__ */ jsxRuntime.jsx(
39
+ "span",
40
+ {
41
+ css: styles.previewContributorStyles(node),
42
+ children: node.attrs.label
43
+ },
44
+ `${node.text}${i}`
45
+ );
46
+ }
47
+ }
48
+ return null;
49
+ }) });
50
+ };
51
+
52
+ exports.Preview = Preview;
@@ -0,0 +1,26 @@
1
+ import { jsx } from '@emotion/react/jsx-runtime';
2
+ import { previewStyles, previewFreeTextStyles, previewContributorStyles } from './styles.js';
3
+
4
+ const Preview = ({ doc }) => {
5
+ if (doc?.childCount === void 0 || doc.childCount === 0) {
6
+ return null;
7
+ }
8
+ const parts = [];
9
+ doc.descendants((node) => {
10
+ parts.push(node);
11
+ });
12
+ return jsx("div", { css: previewStyles, "data-testid": "byline-preview", children: parts.map((node, i) => {
13
+ if (node.isText) {
14
+ return jsx("span", { css: previewFreeTextStyles, children: node.text }, `${node.text}${i}`);
15
+ } else if (node.type.name === "chip") {
16
+ if (node.attrs.path) {
17
+ return jsx("a", { css: previewContributorStyles(node), href: `https://theguardian.com/${node.attrs.path}`, target: "_blank", rel: "noopener noreferrer", children: node.attrs.label }, `${node.text}${i}`);
18
+ } else {
19
+ return jsx("span", { css: previewContributorStyles(node), children: node.attrs.label }, `${node.text}${i}`);
20
+ }
21
+ }
22
+ return null;
23
+ }) });
24
+ };
25
+
26
+ export { Preview };
@@ -0,0 +1,240 @@
1
+ 'use strict';
2
+
3
+ var schema = require('./schema.cjs');
4
+
5
+ const detectNameInText = (text, cursorOffset, isTypingFromStartRange) => {
6
+ const namePattern = /[\p{Lu}*][\p{L}*]*(?:[-'.&’]+[\p{Lu}*][\p{L}*]*|(?!\.\s)\s+[\p{Lu}*][\p{L}*]*)*[ ]?/gu;
7
+ const searchText = isTypingFromStartRange ? text.substring(0, isTypingFromStartRange.maxReached) : text;
8
+ const matches = Array.from(searchText.matchAll(namePattern)).flat().map((match) => ({
9
+ name: match.trimEnd(),
10
+ startIndex: searchText.indexOf(match),
11
+ endIndex: searchText.indexOf(match) + match.length
12
+ }));
13
+ if (matches.length === 0) {
14
+ return void 0;
15
+ }
16
+ const nameContainingCursor = matches.find(
17
+ (match) => cursorOffset >= 0 && cursorOffset >= match.startIndex && cursorOffset <= match.endIndex
18
+ );
19
+ if (nameContainingCursor) {
20
+ return nameContainingCursor;
21
+ }
22
+ return void 0;
23
+ };
24
+ function refocusEditor(viewRef) {
25
+ setTimeout(() => {
26
+ viewRef.current?.focus();
27
+ }, 0);
28
+ }
29
+ function insertChip(text, from, to, type, tagId, path, meta) {
30
+ const command = (state, dispatch) => {
31
+ const chipNode = schema.bylineEditorSchema.nodes.chip.create({
32
+ label: text,
33
+ type,
34
+ tagId,
35
+ path,
36
+ meta
37
+ });
38
+ const tr = state.tr.replaceRangeWith(from, to, chipNode);
39
+ if (dispatch) {
40
+ dispatch(tr);
41
+ }
42
+ return true;
43
+ };
44
+ return command;
45
+ }
46
+ const getCurrentText = (doc, currentOffset, toOffset, isTypingFromStartRange) => {
47
+ const hasSelection = currentOffset !== toOffset;
48
+ const selectedText = hasSelection ? doc.textBetween(currentOffset, toOffset, " ") : "";
49
+ if (hasSelection) {
50
+ return {
51
+ currentTextNode: null,
52
+ startOffset: -1,
53
+ endOffset: -1,
54
+ selectedText,
55
+ hasSelection: true
56
+ };
57
+ }
58
+ let currentTextNode = null;
59
+ let startOffset = -1;
60
+ let endOffset = -1;
61
+ let lastTextContent = "";
62
+ doc.descendants((node, pos) => {
63
+ if (pos >= currentOffset) {
64
+ return false;
65
+ }
66
+ if (node.isText && node.textContent.trim()) {
67
+ const relativeCursorOffset = currentOffset - pos;
68
+ const detectedName = detectNameInText(
69
+ node.textContent,
70
+ relativeCursorOffset,
71
+ isTypingFromStartRange
72
+ );
73
+ if (detectedName) {
74
+ currentTextNode = node;
75
+ lastTextContent = detectedName.name;
76
+ startOffset = pos + detectedName.startIndex;
77
+ endOffset = pos + detectedName.endIndex;
78
+ }
79
+ } else if (node.type.name === "chip") {
80
+ currentTextNode = null;
81
+ startOffset = -1;
82
+ endOffset = -1;
83
+ lastTextContent = "";
84
+ }
85
+ return true;
86
+ });
87
+ return {
88
+ currentTextNode,
89
+ startOffset,
90
+ endOffset,
91
+ selectedText: lastTextContent,
92
+ hasSelection: false
93
+ };
94
+ };
95
+ const hasHitContributorLimit = (doc, contributorLimit) => {
96
+ if (contributorLimit === void 0) {
97
+ return false;
98
+ }
99
+ const numberOfContributors = doc.children.filter(
100
+ (c) => c.type.name === "chip"
101
+ ).length;
102
+ return numberOfContributors >= contributorLimit;
103
+ };
104
+ const addUntaggedContributor = (viewRef, setShowDropdown, contributorLimit, isTypingFromStartRange) => {
105
+ if (!viewRef.current) {
106
+ return;
107
+ }
108
+ const { state, dispatch } = viewRef.current;
109
+ const doc = state.doc;
110
+ if (hasHitContributorLimit(doc, contributorLimit)) {
111
+ return;
112
+ }
113
+ const {
114
+ currentTextNode,
115
+ startOffset,
116
+ endOffset,
117
+ selectedText,
118
+ hasSelection
119
+ } = getCurrentText(
120
+ doc,
121
+ state.selection.from,
122
+ state.selection.to,
123
+ isTypingFromStartRange
124
+ );
125
+ if (hasSelection) {
126
+ setShowDropdown(false);
127
+ const result2 = insertChip(
128
+ selectedText,
129
+ state.selection.from,
130
+ state.selection.to,
131
+ "untagged"
132
+ )(state, dispatch);
133
+ refocusEditor(viewRef);
134
+ return result2;
135
+ }
136
+ if (!currentTextNode || startOffset === -1) {
137
+ console.warn("No text node found in the document");
138
+ return;
139
+ }
140
+ setShowDropdown(false);
141
+ const result = insertChip(
142
+ selectedText,
143
+ startOffset,
144
+ endOffset,
145
+ "untagged"
146
+ )(state, dispatch);
147
+ refocusEditor(viewRef);
148
+ return result;
149
+ };
150
+ const addTaggedContributor = (contributor, viewRef, setShowDropdown, contributorLimit, isTypingFromStartRange) => {
151
+ if (!viewRef.current) {
152
+ return;
153
+ }
154
+ const { state, dispatch } = viewRef.current;
155
+ const doc = state.doc;
156
+ if (hasHitContributorLimit(doc, contributorLimit)) {
157
+ return;
158
+ }
159
+ const { currentTextNode, startOffset, endOffset, hasSelection } = getCurrentText(
160
+ doc,
161
+ state.selection.from,
162
+ state.selection.to,
163
+ isTypingFromStartRange
164
+ );
165
+ if (hasSelection) {
166
+ setShowDropdown(false);
167
+ const result2 = insertChip(
168
+ contributor.label,
169
+ state.selection.from,
170
+ state.selection.to,
171
+ "tagged",
172
+ contributor.tagId,
173
+ contributor.path,
174
+ contributor.meta
175
+ )(state, dispatch);
176
+ refocusEditor(viewRef);
177
+ return result2;
178
+ }
179
+ if (!currentTextNode || startOffset === -1) {
180
+ console.warn("No text node found in the document");
181
+ return;
182
+ }
183
+ setShowDropdown(false);
184
+ const result = insertChip(
185
+ contributor.label,
186
+ startOffset,
187
+ endOffset,
188
+ "tagged",
189
+ contributor.tagId,
190
+ contributor.path,
191
+ contributor.meta
192
+ )(state, dispatch);
193
+ refocusEditor(viewRef);
194
+ return result;
195
+ };
196
+ const convertBylineModelToNode = (value) => {
197
+ const nodes = (value ?? []).map((part) => {
198
+ if (part.type === "contributor") {
199
+ return schema.bylineEditorSchema.nodes.chip.create({
200
+ label: part.value,
201
+ type: part.tagId ? "tagged" : "untagged",
202
+ tagId: part.tagId,
203
+ path: part.path,
204
+ meta: part.meta
205
+ });
206
+ } else {
207
+ return schema.bylineEditorSchema.text(part.value);
208
+ }
209
+ });
210
+ return schema.bylineEditorSchema.node("doc", null, nodes);
211
+ };
212
+ const convertNodeToBylineModel = (doc) => {
213
+ const model = [];
214
+ doc.forEach((node) => {
215
+ if (node.isText) {
216
+ model.push({
217
+ type: "text",
218
+ value: node.text ?? ""
219
+ });
220
+ } else if (node.type.name === "chip") {
221
+ model.push({
222
+ type: "contributor",
223
+ value: node.attrs.label,
224
+ tagId: node.attrs.tagId,
225
+ path: node.attrs.path,
226
+ meta: node.attrs.meta
227
+ });
228
+ }
229
+ });
230
+ return model;
231
+ };
232
+
233
+ exports.addTaggedContributor = addTaggedContributor;
234
+ exports.addUntaggedContributor = addUntaggedContributor;
235
+ exports.convertBylineModelToNode = convertBylineModelToNode;
236
+ exports.convertNodeToBylineModel = convertNodeToBylineModel;
237
+ exports.detectNameInText = detectNameInText;
238
+ exports.getCurrentText = getCurrentText;
239
+ exports.hasHitContributorLimit = hasHitContributorLimit;
240
+ exports.insertChip = insertChip;