@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.
- package/README.md +19 -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/playwright/byline.mock.d.ts +3 -0
- package/dist/types/playwright/byline.spec.d.ts +1 -0
- package/dist/types/playwright-ct.config.d.ts +5 -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/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/package.json +60 -126
- package/LICENSE +0 -201
- package/dist/index.d.ts +0 -3
|
@@ -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;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { bylineEditorSchema } from './schema.js';
|
|
2
|
+
|
|
3
|
+
const detectNameInText = (text, cursorOffset, isTypingFromStartRange) => {
|
|
4
|
+
const namePattern = /[\p{Lu}*][\p{L}*]*(?:[-'.&’]+[\p{Lu}*][\p{L}*]*|(?!\.\s)\s+[\p{Lu}*][\p{L}*]*)*[ ]?/gu;
|
|
5
|
+
const searchText = isTypingFromStartRange ? text.substring(0, isTypingFromStartRange.maxReached) : text;
|
|
6
|
+
const matches = Array.from(searchText.matchAll(namePattern)).flat().map((match) => ({
|
|
7
|
+
name: match.trimEnd(),
|
|
8
|
+
startIndex: searchText.indexOf(match),
|
|
9
|
+
endIndex: searchText.indexOf(match) + match.length
|
|
10
|
+
}));
|
|
11
|
+
if (matches.length === 0) {
|
|
12
|
+
return void 0;
|
|
13
|
+
}
|
|
14
|
+
const nameContainingCursor = matches.find((match) => cursorOffset >= 0 && cursorOffset >= match.startIndex && cursorOffset <= match.endIndex);
|
|
15
|
+
if (nameContainingCursor) {
|
|
16
|
+
return nameContainingCursor;
|
|
17
|
+
}
|
|
18
|
+
return void 0;
|
|
19
|
+
};
|
|
20
|
+
function refocusEditor(viewRef) {
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
viewRef.current?.focus();
|
|
23
|
+
}, 0);
|
|
24
|
+
}
|
|
25
|
+
function insertChip(text, from, to, type, tagId, path, meta) {
|
|
26
|
+
const command = (state, dispatch) => {
|
|
27
|
+
const chipNode = bylineEditorSchema.nodes.chip.create({
|
|
28
|
+
label: text,
|
|
29
|
+
type,
|
|
30
|
+
tagId,
|
|
31
|
+
path,
|
|
32
|
+
meta
|
|
33
|
+
});
|
|
34
|
+
const tr = state.tr.replaceRangeWith(from, to, chipNode);
|
|
35
|
+
if (dispatch) {
|
|
36
|
+
dispatch(tr);
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
};
|
|
40
|
+
return command;
|
|
41
|
+
}
|
|
42
|
+
const getCurrentText = (doc, currentOffset, toOffset, isTypingFromStartRange) => {
|
|
43
|
+
const hasSelection = currentOffset !== toOffset;
|
|
44
|
+
const selectedText = hasSelection ? doc.textBetween(currentOffset, toOffset, " ") : "";
|
|
45
|
+
if (hasSelection) {
|
|
46
|
+
return {
|
|
47
|
+
currentTextNode: null,
|
|
48
|
+
startOffset: -1,
|
|
49
|
+
endOffset: -1,
|
|
50
|
+
selectedText,
|
|
51
|
+
hasSelection: true
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
let currentTextNode = null;
|
|
55
|
+
let startOffset = -1;
|
|
56
|
+
let endOffset = -1;
|
|
57
|
+
let lastTextContent = "";
|
|
58
|
+
doc.descendants((node, pos) => {
|
|
59
|
+
if (pos >= currentOffset) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (node.isText && node.textContent.trim()) {
|
|
63
|
+
const relativeCursorOffset = currentOffset - pos;
|
|
64
|
+
const detectedName = detectNameInText(node.textContent, relativeCursorOffset, isTypingFromStartRange);
|
|
65
|
+
if (detectedName) {
|
|
66
|
+
currentTextNode = node;
|
|
67
|
+
lastTextContent = detectedName.name;
|
|
68
|
+
startOffset = pos + detectedName.startIndex;
|
|
69
|
+
endOffset = pos + detectedName.endIndex;
|
|
70
|
+
}
|
|
71
|
+
} else if (node.type.name === "chip") {
|
|
72
|
+
currentTextNode = null;
|
|
73
|
+
startOffset = -1;
|
|
74
|
+
endOffset = -1;
|
|
75
|
+
lastTextContent = "";
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
currentTextNode,
|
|
81
|
+
startOffset,
|
|
82
|
+
endOffset,
|
|
83
|
+
selectedText: lastTextContent,
|
|
84
|
+
hasSelection: false
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
const hasHitContributorLimit = (doc, contributorLimit) => {
|
|
88
|
+
if (contributorLimit === void 0) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const numberOfContributors = doc.children.filter((c) => c.type.name === "chip").length;
|
|
92
|
+
return numberOfContributors >= contributorLimit;
|
|
93
|
+
};
|
|
94
|
+
const addUntaggedContributor = (viewRef, setShowDropdown, contributorLimit, isTypingFromStartRange) => {
|
|
95
|
+
if (!viewRef.current) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const { state, dispatch } = viewRef.current;
|
|
99
|
+
const doc = state.doc;
|
|
100
|
+
if (hasHitContributorLimit(doc, contributorLimit)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const { currentTextNode, startOffset, endOffset, selectedText, hasSelection } = getCurrentText(doc, state.selection.from, state.selection.to, isTypingFromStartRange);
|
|
104
|
+
if (hasSelection) {
|
|
105
|
+
setShowDropdown(false);
|
|
106
|
+
const result2 = insertChip(selectedText, state.selection.from, state.selection.to, "untagged")(state, dispatch);
|
|
107
|
+
refocusEditor(viewRef);
|
|
108
|
+
return result2;
|
|
109
|
+
}
|
|
110
|
+
if (!currentTextNode || startOffset === -1) {
|
|
111
|
+
console.warn("No text node found in the document");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
setShowDropdown(false);
|
|
115
|
+
const result = insertChip(selectedText, startOffset, endOffset, "untagged")(state, dispatch);
|
|
116
|
+
refocusEditor(viewRef);
|
|
117
|
+
return result;
|
|
118
|
+
};
|
|
119
|
+
const addTaggedContributor = (contributor, viewRef, setShowDropdown, contributorLimit, isTypingFromStartRange) => {
|
|
120
|
+
if (!viewRef.current) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const { state, dispatch } = viewRef.current;
|
|
124
|
+
const doc = state.doc;
|
|
125
|
+
if (hasHitContributorLimit(doc, contributorLimit)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const { currentTextNode, startOffset, endOffset, hasSelection } = getCurrentText(doc, state.selection.from, state.selection.to, isTypingFromStartRange);
|
|
129
|
+
if (hasSelection) {
|
|
130
|
+
setShowDropdown(false);
|
|
131
|
+
const result2 = insertChip(contributor.label, state.selection.from, state.selection.to, "tagged", contributor.tagId, contributor.path, contributor.meta)(state, dispatch);
|
|
132
|
+
refocusEditor(viewRef);
|
|
133
|
+
return result2;
|
|
134
|
+
}
|
|
135
|
+
if (!currentTextNode || startOffset === -1) {
|
|
136
|
+
console.warn("No text node found in the document");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
setShowDropdown(false);
|
|
140
|
+
const result = insertChip(contributor.label, startOffset, endOffset, "tagged", contributor.tagId, contributor.path, contributor.meta)(state, dispatch);
|
|
141
|
+
refocusEditor(viewRef);
|
|
142
|
+
return result;
|
|
143
|
+
};
|
|
144
|
+
const convertBylineModelToNode = (value) => {
|
|
145
|
+
const nodes = (value ?? []).map((part) => {
|
|
146
|
+
if (part.type === "contributor") {
|
|
147
|
+
return bylineEditorSchema.nodes.chip.create({
|
|
148
|
+
label: part.value,
|
|
149
|
+
type: part.tagId ? "tagged" : "untagged",
|
|
150
|
+
tagId: part.tagId,
|
|
151
|
+
path: part.path,
|
|
152
|
+
meta: part.meta
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
return bylineEditorSchema.text(part.value);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
return bylineEditorSchema.node("doc", null, nodes);
|
|
159
|
+
};
|
|
160
|
+
const convertNodeToBylineModel = (doc) => {
|
|
161
|
+
const model = [];
|
|
162
|
+
doc.forEach((node) => {
|
|
163
|
+
if (node.isText) {
|
|
164
|
+
model.push({
|
|
165
|
+
type: "text",
|
|
166
|
+
value: node.text ?? ""
|
|
167
|
+
});
|
|
168
|
+
} else if (node.type.name === "chip") {
|
|
169
|
+
model.push({
|
|
170
|
+
type: "contributor",
|
|
171
|
+
value: node.attrs.label,
|
|
172
|
+
tagId: node.attrs.tagId,
|
|
173
|
+
path: node.attrs.path,
|
|
174
|
+
meta: node.attrs.meta
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
return model;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export { addTaggedContributor, addUntaggedContributor, convertBylineModelToNode, convertNodeToBylineModel, detectNameInText, getCurrentText, hasHitContributorLimit, insertChip };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var prosemirrorState = require('prosemirror-state');
|
|
4
|
+
var prosemirrorView = require('prosemirror-view');
|
|
5
|
+
|
|
6
|
+
const getPlaceholder = (text) => {
|
|
7
|
+
const span = document.createElement("span");
|
|
8
|
+
span.innerHTML = text;
|
|
9
|
+
span.className = "placeholder";
|
|
10
|
+
return span;
|
|
11
|
+
};
|
|
12
|
+
const createPlaceholderPlugin = (text) => {
|
|
13
|
+
const shouldDisplayPlaceholder = (state) => text !== "" && state.doc.childCount < 1;
|
|
14
|
+
return new prosemirrorState.Plugin({
|
|
15
|
+
props: {
|
|
16
|
+
decorations: (editorState) => {
|
|
17
|
+
const { doc } = editorState;
|
|
18
|
+
if (shouldDisplayPlaceholder(editorState)) {
|
|
19
|
+
return prosemirrorView.DecorationSet.create(doc, [
|
|
20
|
+
prosemirrorView.Decoration.widget(0, getPlaceholder(text))
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
return prosemirrorView.DecorationSet.empty;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
exports.createPlaceholderPlugin = createPlaceholderPlugin;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
import { DecorationSet, Decoration } from 'prosemirror-view';
|
|
3
|
+
|
|
4
|
+
const getPlaceholder = (text) => {
|
|
5
|
+
const span = document.createElement("span");
|
|
6
|
+
span.innerHTML = text;
|
|
7
|
+
span.className = "placeholder";
|
|
8
|
+
return span;
|
|
9
|
+
};
|
|
10
|
+
const createPlaceholderPlugin = (text) => {
|
|
11
|
+
const shouldDisplayPlaceholder = (state) => text !== "" && state.doc.childCount < 1;
|
|
12
|
+
return new Plugin({
|
|
13
|
+
props: {
|
|
14
|
+
decorations: (editorState) => {
|
|
15
|
+
const { doc } = editorState;
|
|
16
|
+
if (shouldDisplayPlaceholder(editorState)) {
|
|
17
|
+
return DecorationSet.create(doc, [
|
|
18
|
+
Decoration.widget(0, getPlaceholder(text))
|
|
19
|
+
]);
|
|
20
|
+
}
|
|
21
|
+
return DecorationSet.empty;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export { createPlaceholderPlugin };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var prosemirrorHistory = require('prosemirror-history');
|
|
4
|
+
var prosemirrorKeymap = require('prosemirror-keymap');
|
|
5
|
+
var prosemirrorModel = require('prosemirror-model');
|
|
6
|
+
var prosemirrorState = require('prosemirror-state');
|
|
7
|
+
var schema = require('./schema.cjs');
|
|
8
|
+
|
|
9
|
+
const serializeNode = (node, output) => {
|
|
10
|
+
if (node.isText) {
|
|
11
|
+
output.push(node.text ?? "");
|
|
12
|
+
} else if (node.type.name === "chip") {
|
|
13
|
+
output.push(node.attrs.label);
|
|
14
|
+
} else if (node.isInline) {
|
|
15
|
+
node.content.forEach((child) => serializeNode(child, output));
|
|
16
|
+
} else {
|
|
17
|
+
node.content.forEach((child) => serializeNode(child, output));
|
|
18
|
+
output.push("\n");
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const transformPastedNode = (slice, allowUntaggedContributors, contributorLimit) => {
|
|
22
|
+
let contributorCount = 0;
|
|
23
|
+
const output = [];
|
|
24
|
+
const convertChipToPlaintext = (node, output2) => {
|
|
25
|
+
const label = node.attrs.label;
|
|
26
|
+
if (label) {
|
|
27
|
+
const textNode = schema.bylineEditorSchema.text(label);
|
|
28
|
+
output2.push(textNode);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
slice.content.forEach((node) => {
|
|
32
|
+
if (node.type.name === "chip") {
|
|
33
|
+
const type = node.attrs.type;
|
|
34
|
+
if (contributorLimit !== void 0 && contributorCount >= contributorLimit) {
|
|
35
|
+
convertChipToPlaintext(node, output);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (type === "untagged" && !allowUntaggedContributors) {
|
|
39
|
+
convertChipToPlaintext(node, output);
|
|
40
|
+
} else {
|
|
41
|
+
output.push(node);
|
|
42
|
+
contributorCount++;
|
|
43
|
+
}
|
|
44
|
+
} else if (node.isText) {
|
|
45
|
+
output.push(node);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
return output;
|
|
49
|
+
};
|
|
50
|
+
const clipboardPlugin = (allowUntaggedContributors, contributorLimit) => {
|
|
51
|
+
return new prosemirrorState.Plugin({
|
|
52
|
+
props: {
|
|
53
|
+
// Custom serializer for copying content from the editor
|
|
54
|
+
clipboardTextSerializer: (slice) => {
|
|
55
|
+
const parts = [];
|
|
56
|
+
slice.content.forEach((node) => {
|
|
57
|
+
serializeNode(node, parts);
|
|
58
|
+
});
|
|
59
|
+
return parts.join("");
|
|
60
|
+
},
|
|
61
|
+
// Transform pasted content after ProseMirror's default parsing
|
|
62
|
+
transformPasted: (slice) => {
|
|
63
|
+
const transformedNodes = transformPastedNode(
|
|
64
|
+
slice,
|
|
65
|
+
allowUntaggedContributors,
|
|
66
|
+
contributorLimit
|
|
67
|
+
);
|
|
68
|
+
if (transformedNodes.length > 0) {
|
|
69
|
+
const newFragment = schema.bylineEditorSchema.nodes.doc.create(
|
|
70
|
+
{},
|
|
71
|
+
transformedNodes
|
|
72
|
+
).content;
|
|
73
|
+
return new prosemirrorModel.Slice(
|
|
74
|
+
newFragment,
|
|
75
|
+
slice.openStart,
|
|
76
|
+
slice.openEnd
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return slice;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
const deleteSelectedChip = (state, dispatch) => {
|
|
85
|
+
const { selection } = state;
|
|
86
|
+
if (selection instanceof prosemirrorState.NodeSelection && selection.node.type.name === "chip") {
|
|
87
|
+
if (dispatch) {
|
|
88
|
+
const tr = state.tr.deleteSelection();
|
|
89
|
+
dispatch(tr);
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
};
|
|
95
|
+
const keybindings = () => prosemirrorKeymap.keymap({
|
|
96
|
+
"Mod-z": prosemirrorHistory.undo,
|
|
97
|
+
"Mod-y": prosemirrorHistory.redo,
|
|
98
|
+
"Mod-shift-z": prosemirrorHistory.redo,
|
|
99
|
+
Backspace: deleteSelectedChip,
|
|
100
|
+
Delete: deleteSelectedChip
|
|
101
|
+
});
|
|
102
|
+
const bylinePlugin = () => {
|
|
103
|
+
return new prosemirrorState.Plugin({
|
|
104
|
+
props: {
|
|
105
|
+
nodeViews: {
|
|
106
|
+
chip: (node, view, getPos) => {
|
|
107
|
+
const dom = document.createElement("chip");
|
|
108
|
+
dom.setAttribute("data-label", node.attrs.label);
|
|
109
|
+
dom.setAttribute("data-type", node.attrs.type);
|
|
110
|
+
dom.setAttribute("data-tag-id", node.attrs.tagId);
|
|
111
|
+
dom.setAttribute("data-path", node.attrs.path);
|
|
112
|
+
if (node.attrs.meta) {
|
|
113
|
+
dom.setAttribute(
|
|
114
|
+
"data-meta",
|
|
115
|
+
JSON.stringify(node.attrs.meta)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
dom.textContent = node.attrs.label;
|
|
119
|
+
const deleteHandle = document.createElement("span");
|
|
120
|
+
deleteHandle.innerHTML = "\xD7";
|
|
121
|
+
deleteHandle.title = `Delete ${node.attrs.label}`;
|
|
122
|
+
deleteHandle.addEventListener("click", (event) => {
|
|
123
|
+
event.stopPropagation();
|
|
124
|
+
const pos = getPos();
|
|
125
|
+
if (pos === void 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const tr = view.state.tr.deleteRange(
|
|
129
|
+
pos,
|
|
130
|
+
pos + node.nodeSize
|
|
131
|
+
);
|
|
132
|
+
view.dispatch(tr);
|
|
133
|
+
});
|
|
134
|
+
dom.appendChild(deleteHandle);
|
|
135
|
+
return { dom };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
exports.bylinePlugin = bylinePlugin;
|
|
143
|
+
exports.clipboardPlugin = clipboardPlugin;
|
|
144
|
+
exports.keybindings = keybindings;
|