@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,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;
@@ -0,0 +1,123 @@
1
+ import { redo, undo } from 'prosemirror-history';
2
+ import { keymap } from 'prosemirror-keymap';
3
+ import { Slice } from 'prosemirror-model';
4
+ import { Plugin, NodeSelection } from 'prosemirror-state';
5
+ import { bylineEditorSchema } from './schema.js';
6
+
7
+ const serializeNode = (node, output) => {
8
+ if (node.isText) {
9
+ output.push(node.text ?? "");
10
+ } else if (node.type.name === "chip") {
11
+ output.push(node.attrs.label);
12
+ } else if (node.isInline) {
13
+ node.content.forEach((child) => serializeNode(child, output));
14
+ } else {
15
+ node.content.forEach((child) => serializeNode(child, output));
16
+ output.push("\n");
17
+ }
18
+ };
19
+ const transformPastedNode = (slice, allowUntaggedContributors, contributorLimit) => {
20
+ let contributorCount = 0;
21
+ const output = [];
22
+ const convertChipToPlaintext = (node, output2) => {
23
+ const label = node.attrs.label;
24
+ if (label) {
25
+ const textNode = bylineEditorSchema.text(label);
26
+ output2.push(textNode);
27
+ }
28
+ };
29
+ slice.content.forEach((node) => {
30
+ if (node.type.name === "chip") {
31
+ const type = node.attrs.type;
32
+ if (contributorLimit !== void 0 && contributorCount >= contributorLimit) {
33
+ convertChipToPlaintext(node, output);
34
+ return;
35
+ }
36
+ if (type === "untagged" && !allowUntaggedContributors) {
37
+ convertChipToPlaintext(node, output);
38
+ } else {
39
+ output.push(node);
40
+ contributorCount++;
41
+ }
42
+ } else if (node.isText) {
43
+ output.push(node);
44
+ }
45
+ });
46
+ return output;
47
+ };
48
+ const clipboardPlugin = (allowUntaggedContributors, contributorLimit) => {
49
+ return new Plugin({
50
+ props: {
51
+ // Custom serializer for copying content from the editor
52
+ clipboardTextSerializer: (slice) => {
53
+ const parts = [];
54
+ slice.content.forEach((node) => {
55
+ serializeNode(node, parts);
56
+ });
57
+ return parts.join("");
58
+ },
59
+ // Transform pasted content after ProseMirror's default parsing
60
+ transformPasted: (slice) => {
61
+ const transformedNodes = transformPastedNode(slice, allowUntaggedContributors, contributorLimit);
62
+ if (transformedNodes.length > 0) {
63
+ const newFragment = bylineEditorSchema.nodes.doc.create({}, transformedNodes).content;
64
+ return new Slice(newFragment, slice.openStart, slice.openEnd);
65
+ }
66
+ return slice;
67
+ }
68
+ }
69
+ });
70
+ };
71
+ const deleteSelectedChip = (state, dispatch) => {
72
+ const { selection } = state;
73
+ if (selection instanceof NodeSelection && selection.node.type.name === "chip") {
74
+ if (dispatch) {
75
+ const tr = state.tr.deleteSelection();
76
+ dispatch(tr);
77
+ }
78
+ return true;
79
+ }
80
+ return false;
81
+ };
82
+ const keybindings = () => keymap({
83
+ "Mod-z": undo,
84
+ "Mod-y": redo,
85
+ "Mod-shift-z": redo,
86
+ Backspace: deleteSelectedChip,
87
+ Delete: deleteSelectedChip
88
+ });
89
+ const bylinePlugin = () => {
90
+ return new Plugin({
91
+ props: {
92
+ nodeViews: {
93
+ chip: (node, view, getPos) => {
94
+ const dom = document.createElement("chip");
95
+ dom.setAttribute("data-label", node.attrs.label);
96
+ dom.setAttribute("data-type", node.attrs.type);
97
+ dom.setAttribute("data-tag-id", node.attrs.tagId);
98
+ dom.setAttribute("data-path", node.attrs.path);
99
+ if (node.attrs.meta) {
100
+ dom.setAttribute("data-meta", JSON.stringify(node.attrs.meta));
101
+ }
102
+ dom.textContent = node.attrs.label;
103
+ const deleteHandle = document.createElement("span");
104
+ deleteHandle.innerHTML = "\xD7";
105
+ deleteHandle.title = `Delete ${node.attrs.label}`;
106
+ deleteHandle.addEventListener("click", (event) => {
107
+ event.stopPropagation();
108
+ const pos = getPos();
109
+ if (pos === void 0) {
110
+ return;
111
+ }
112
+ const tr = view.state.tr.deleteRange(pos, pos + node.nodeSize);
113
+ view.dispatch(tr);
114
+ });
115
+ dom.appendChild(deleteHandle);
116
+ return { dom };
117
+ }
118
+ }
119
+ }
120
+ });
121
+ };
122
+
123
+ export { bylinePlugin, clipboardPlugin, keybindings };
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ var prosemirrorModel = require('prosemirror-model');
4
+
5
+ const bylineEditorSchema = new prosemirrorModel.Schema({
6
+ nodes: {
7
+ doc: {
8
+ content: "inline*"
9
+ },
10
+ chip: {
11
+ group: "inline",
12
+ inline: true,
13
+ atom: false,
14
+ selectable: true,
15
+ // allows selection for deletion/drag and drop
16
+ attrs: {
17
+ label: { default: "" },
18
+ type: { default: "untagged" },
19
+ tagId: { default: "" },
20
+ // tagId is optional for untagged contributors
21
+ path: { default: "" },
22
+ // path is optional for untagged contributors
23
+ meta: { default: {} }
24
+ // meta is optional and can hold any additional data
25
+ },
26
+ parseDOM: [
27
+ {
28
+ tag: "chip[data-label][data-type][data-tag-id][data-path][data-meta]",
29
+ getAttrs(dom) {
30
+ return {
31
+ label: dom.getAttribute("data-label"),
32
+ type: dom.getAttribute("data-type"),
33
+ tagId: dom.getAttribute("data-tag-id") ?? "",
34
+ path: dom.getAttribute("data-path") ?? "",
35
+ meta: dom.getAttribute("data-meta") ? JSON.parse(
36
+ dom.getAttribute("data-meta")
37
+ ) : {}
38
+ };
39
+ }
40
+ }
41
+ ],
42
+ toDOM(node) {
43
+ return [
44
+ "chip",
45
+ {
46
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- label exists as string on chip node
47
+ "data-label": node.attrs.label,
48
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- type exists as string on chip node
49
+ "data-type": node.attrs.type,
50
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- tagId exists as string on chip node
51
+ "data-tag-id": node.attrs.tagId,
52
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- path exists as string on chip node
53
+ "data-path": node.attrs.path,
54
+ "data-meta": node.attrs.meta ? JSON.stringify(node.attrs.meta) : {}
55
+ },
56
+ node.attrs.label
57
+ ];
58
+ }
59
+ },
60
+ text: {
61
+ group: "inline"
62
+ }
63
+ }
64
+ });
65
+
66
+ exports.bylineEditorSchema = bylineEditorSchema;
@@ -0,0 +1,59 @@
1
+ import { Schema } from 'prosemirror-model';
2
+
3
+ const bylineEditorSchema = new Schema({
4
+ nodes: {
5
+ doc: {
6
+ content: "inline*"
7
+ },
8
+ chip: {
9
+ group: "inline",
10
+ inline: true,
11
+ atom: false,
12
+ selectable: true,
13
+ attrs: {
14
+ label: { default: "" },
15
+ type: { default: "untagged" },
16
+ tagId: { default: "" },
17
+ path: { default: "" },
18
+ meta: { default: {} }
19
+ // meta is optional and can hold any additional data
20
+ },
21
+ parseDOM: [
22
+ {
23
+ tag: "chip[data-label][data-type][data-tag-id][data-path][data-meta]",
24
+ getAttrs(dom) {
25
+ return {
26
+ label: dom.getAttribute("data-label"),
27
+ type: dom.getAttribute("data-type"),
28
+ tagId: dom.getAttribute("data-tag-id") ?? "",
29
+ path: dom.getAttribute("data-path") ?? "",
30
+ meta: dom.getAttribute("data-meta") ? JSON.parse(dom.getAttribute("data-meta")) : {}
31
+ };
32
+ }
33
+ }
34
+ ],
35
+ toDOM(node) {
36
+ return [
37
+ "chip",
38
+ {
39
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- label exists as string on chip node
40
+ "data-label": node.attrs.label,
41
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- type exists as string on chip node
42
+ "data-type": node.attrs.type,
43
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- tagId exists as string on chip node
44
+ "data-tag-id": node.attrs.tagId,
45
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- path exists as string on chip node
46
+ "data-path": node.attrs.path,
47
+ "data-meta": node.attrs.meta ? JSON.stringify(node.attrs.meta) : {}
48
+ },
49
+ node.attrs.label
50
+ ];
51
+ }
52
+ },
53
+ text: {
54
+ group: "inline"
55
+ }
56
+ }
57
+ });
58
+
59
+ export { bylineEditorSchema };