@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.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.prettierrc +1 -0
- package/.storybook/main.ts +12 -0
- package/.storybook/preview.tsx +83 -0
- package/CHANGELOG.md +7 -0
- package/README.md +15 -0
- package/dist/byline/Byline.cjs +375 -0
- package/dist/byline/Byline.js +273 -0
- package/dist/byline/Preview.cjs +52 -0
- package/dist/byline/Preview.js +26 -0
- package/dist/byline/lib.cjs +240 -0
- package/dist/byline/lib.js +181 -0
- package/dist/byline/placeholder.cjs +29 -0
- package/dist/byline/placeholder.js +27 -0
- package/dist/byline/plugins.cjs +144 -0
- package/dist/byline/plugins.js +123 -0
- package/dist/byline/schema.cjs +66 -0
- package/dist/byline/schema.js +59 -0
- package/dist/byline/styles.cjs +244 -0
- package/dist/byline/styles.js +234 -0
- package/dist/index.cjs +4 -4
- package/dist/index.js +1 -5
- package/dist/types/.storybook/main.d.ts +3 -0
- package/dist/types/.storybook/preview.d.ts +3 -0
- package/dist/types/jest-setup-after-env.d.ts +1 -0
- package/dist/types/src/byline/Byline.d.ts +17 -0
- package/dist/types/src/byline/Byline.stories.d.ts +206 -0
- package/dist/types/src/byline/Byline.test.d.ts +1 -0
- package/dist/types/src/byline/Preview.d.ts +4 -0
- package/dist/types/src/byline/contributors-fixture.d.ts +1 -0
- package/dist/types/src/byline/lib.d.ts +48 -0
- package/dist/types/src/byline/lib.test.d.ts +1 -0
- package/dist/types/src/byline/placeholder.d.ts +2 -0
- package/dist/types/src/byline/plugins.d.ts +4 -0
- package/dist/types/src/byline/schema.d.ts +2 -0
- package/dist/types/src/byline/styles.d.ts +11 -0
- package/dist/types/src/byline/theme.d.ts +44 -0
- package/dist/types/src/byline/util.d.ts +3 -0
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/mocks/prosemirror-view.d.ts +10 -0
- package/eslint.config.js +14 -0
- package/jest-setup-after-env.ts +1 -0
- package/jest.config.js +12 -0
- package/package.json +60 -129
- package/rollup.config.js +49 -0
- package/src/byline/Byline.stories.tsx +186 -0
- package/src/byline/Byline.test.tsx +450 -0
- package/src/byline/Byline.tsx +524 -0
- package/src/byline/Preview.tsx +59 -0
- package/src/byline/contributors-fixture.ts +1006 -0
- package/src/byline/lib.test.ts +179 -0
- package/src/byline/lib.ts +426 -0
- package/src/byline/placeholder.ts +30 -0
- package/src/byline/plugins.ts +186 -0
- package/src/byline/schema.ts +62 -0
- package/src/byline/styles.ts +246 -0
- package/src/byline/theme.ts +45 -0
- package/src/byline/util.ts +5 -0
- package/src/index.ts +2 -0
- package/src/mocks/prosemirror-view.ts +19 -0
- package/tsconfig.json +19 -0
- package/LICENSE +0 -201
- 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;
|