@bayonai/rich-text-editor 0.1.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/CHANGELOG.md +38 -0
- package/README.md +37 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/dist/react/BlockActionTool.d.ts +15 -0
- package/dist/react/BlockActionTool.js +37 -0
- package/dist/react/EditorSessionProvider.d.ts +28 -0
- package/dist/react/EditorSessionProvider.js +74 -0
- package/dist/react/RichTextBody.d.ts +18 -0
- package/dist/react/RichTextBody.js +66 -0
- package/dist/react/RichTextDocumentSurface.d.ts +6 -0
- package/dist/react/RichTextDocumentSurface.js +5 -0
- package/dist/react/RichTextEditor.d.ts +45 -0
- package/dist/react/RichTextEditor.js +1096 -0
- package/dist/react/RichTextIcons.d.ts +21 -0
- package/dist/react/RichTextIcons.js +55 -0
- package/dist/react/RichTextRenderedBlock.d.ts +4 -0
- package/dist/react/RichTextRenderedBlock.js +36 -0
- package/dist/react/RichTextRenderer.d.ts +4 -0
- package/dist/react/RichTextRenderer.js +8 -0
- package/dist/react/RichTextStyles.d.ts +1 -0
- package/dist/react/RichTextStyles.js +719 -0
- package/dist/react/RichTextTitleInput.d.ts +20 -0
- package/dist/react/RichTextTitleInput.js +39 -0
- package/dist/react/SelectionFormatToolbar.d.ts +34 -0
- package/dist/react/SelectionFormatToolbar.js +18 -0
- package/dist/react/SpecialBlockOption.d.ts +7 -0
- package/dist/react/SpecialBlockOption.js +7 -0
- package/dist/react/SpecialBlockTool.d.ts +42 -0
- package/dist/react/SpecialBlockTool.js +50 -0
- package/dist/react/TranscriptionControl.d.ts +19 -0
- package/dist/react/TranscriptionControl.js +129 -0
- package/dist/react/UnsavedChangesDialog.d.ts +9 -0
- package/dist/react/UnsavedChangesDialog.js +13 -0
- package/dist/react/blockActionToolState.d.ts +18 -0
- package/dist/react/blockActionToolState.js +53 -0
- package/dist/react/blockActions.d.ts +8 -0
- package/dist/react/blockActions.js +111 -0
- package/dist/react/editorNavigation.d.ts +19 -0
- package/dist/react/editorNavigation.js +39 -0
- package/dist/react/editorShortcuts.d.ts +20 -0
- package/dist/react/editorShortcuts.js +25 -0
- package/dist/react/index.d.ts +9 -0
- package/dist/react/index.js +6 -0
- package/dist/react/richTextBlockStyles.d.ts +7 -0
- package/dist/react/richTextBlockStyles.js +7 -0
- package/dist/react/specialBlockStyles.d.ts +15 -0
- package/dist/react/specialBlockStyles.js +9 -0
- package/dist/richText.d.ts +15 -0
- package/dist/richText.js +297 -0
- package/dist/saveControl.d.ts +8 -0
- package/dist/saveControl.js +9 -0
- package/dist/session.d.ts +27 -0
- package/dist/session.js +78 -0
- package/dist/sessionRegistry.d.ts +24 -0
- package/dist/sessionRegistry.js +87 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.js +1 -0
- package/dist/writingStats.d.ts +5 -0
- package/dist/writingStats.js +9 -0
- package/dist-cjs/index.js +22 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/react/BlockActionTool.js +40 -0
- package/dist-cjs/react/EditorSessionProvider.js +79 -0
- package/dist-cjs/react/RichTextBody.js +69 -0
- package/dist-cjs/react/RichTextDocumentSurface.js +8 -0
- package/dist-cjs/react/RichTextEditor.js +1108 -0
- package/dist-cjs/react/RichTextIcons.js +74 -0
- package/dist-cjs/react/RichTextRenderedBlock.js +39 -0
- package/dist-cjs/react/RichTextRenderer.js +11 -0
- package/dist-cjs/react/RichTextStyles.js +722 -0
- package/dist-cjs/react/RichTextTitleInput.js +44 -0
- package/dist-cjs/react/SelectionFormatToolbar.js +22 -0
- package/dist-cjs/react/SpecialBlockOption.js +10 -0
- package/dist-cjs/react/SpecialBlockTool.js +54 -0
- package/dist-cjs/react/TranscriptionControl.js +133 -0
- package/dist-cjs/react/UnsavedChangesDialog.js +16 -0
- package/dist-cjs/react/blockActionToolState.js +58 -0
- package/dist-cjs/react/blockActions.js +119 -0
- package/dist-cjs/react/editorNavigation.js +45 -0
- package/dist-cjs/react/editorShortcuts.js +28 -0
- package/dist-cjs/react/index.js +17 -0
- package/dist-cjs/react/richTextBlockStyles.js +10 -0
- package/dist-cjs/react/specialBlockStyles.js +12 -0
- package/dist-cjs/richText.js +307 -0
- package/dist-cjs/saveControl.js +12 -0
- package/dist-cjs/session.js +83 -0
- package/dist-cjs/sessionRegistry.js +90 -0
- package/dist-cjs/types.js +2 -0
- package/dist-cjs/writingStats.js +12 -0
- package/package.json +45 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createEmptyRichTextBlocks = createEmptyRichTextBlocks;
|
|
4
|
+
exports.createRichTextBlockId = createRichTextBlockId;
|
|
5
|
+
exports.sanitizeRichTextBlocks = sanitizeRichTextBlocks;
|
|
6
|
+
exports.isRichTextBlocksEmpty = isRichTextBlocksEmpty;
|
|
7
|
+
exports.richTextBlocksToPlainText = richTextBlocksToPlainText;
|
|
8
|
+
exports.editorHtmlToMarkdown = editorHtmlToMarkdown;
|
|
9
|
+
exports.markdownToEditorHtml = markdownToEditorHtml;
|
|
10
|
+
exports.sanitizeRichTextHtml = sanitizeRichTextHtml;
|
|
11
|
+
const allowedInlineTags = new Set(["a", "br", "code", "em", "strong"]);
|
|
12
|
+
const textBlockTypes = new Set(["paragraph", "heading", "quote", "checkbox"]);
|
|
13
|
+
function createEmptyRichTextBlocks() {
|
|
14
|
+
return [{ id: createRichTextBlockId(), type: "paragraph", markdown: "" }];
|
|
15
|
+
}
|
|
16
|
+
function createRichTextBlockId() {
|
|
17
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
18
|
+
return crypto.randomUUID();
|
|
19
|
+
}
|
|
20
|
+
return `block-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Sanitizes an array of RichTextBlock objects by:
|
|
24
|
+
* - Filtering out invalid blocks
|
|
25
|
+
* - Ensuring each block has a unique ID
|
|
26
|
+
* - Returning an empty array if no valid blocks remain
|
|
27
|
+
*/
|
|
28
|
+
function sanitizeRichTextBlocks(value) {
|
|
29
|
+
const blocks = Array.isArray(value) ? value : [];
|
|
30
|
+
const seenBlockIds = new Set();
|
|
31
|
+
const sanitized = blocks
|
|
32
|
+
.flatMap((block) => sanitizeRichTextBlock(block))
|
|
33
|
+
.map((block) => {
|
|
34
|
+
if (!seenBlockIds.has(block.id)) {
|
|
35
|
+
seenBlockIds.add(block.id);
|
|
36
|
+
return block;
|
|
37
|
+
}
|
|
38
|
+
const blockWithUniqueId = {
|
|
39
|
+
...block,
|
|
40
|
+
id: createUniqueRichTextBlockId(seenBlockIds),
|
|
41
|
+
};
|
|
42
|
+
seenBlockIds.add(blockWithUniqueId.id);
|
|
43
|
+
return blockWithUniqueId;
|
|
44
|
+
});
|
|
45
|
+
return sanitized.length > 0 ? sanitized : createEmptyRichTextBlocks();
|
|
46
|
+
}
|
|
47
|
+
function isRichTextBlocksEmpty(blocks) {
|
|
48
|
+
return sanitizeRichTextBlocks(blocks).every((block) => {
|
|
49
|
+
if (block.type === "divider") {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (block.type === "checkbox") {
|
|
53
|
+
return markdownToPlainText(block.markdown).trim() === "";
|
|
54
|
+
}
|
|
55
|
+
if (block.type === "image") {
|
|
56
|
+
return !block.assetId && !block.alt?.trim();
|
|
57
|
+
}
|
|
58
|
+
const text = block.type === "code" ? block.text : block.markdown;
|
|
59
|
+
return markdownToPlainText(text).trim() === "";
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function richTextBlocksToPlainText(blocks) {
|
|
63
|
+
return sanitizeRichTextBlocks(blocks)
|
|
64
|
+
.map((block) => {
|
|
65
|
+
if (block.type === "divider") {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
if (block.type === "checkbox") {
|
|
69
|
+
return markdownToPlainText(block.markdown);
|
|
70
|
+
}
|
|
71
|
+
if (block.type === "image") {
|
|
72
|
+
return block.alt ?? "";
|
|
73
|
+
}
|
|
74
|
+
return block.type === "code"
|
|
75
|
+
? block.text
|
|
76
|
+
: markdownToPlainText(block.markdown);
|
|
77
|
+
})
|
|
78
|
+
.join(" ")
|
|
79
|
+
.replace(/\s+/g, " ")
|
|
80
|
+
.trim();
|
|
81
|
+
}
|
|
82
|
+
function editorHtmlToMarkdown(html) {
|
|
83
|
+
const sanitized = normalizeTypography(decodeHtmlText(sanitizeRichTextHtml(html)
|
|
84
|
+
.replace(/<br>/g, " \n")
|
|
85
|
+
.replace(/<strong>([\s\S]*?)<\/strong>/g, (_, value) => {
|
|
86
|
+
return `**${decodeHtmlText(stripTags(value))}**`;
|
|
87
|
+
})
|
|
88
|
+
.replace(/<em>([\s\S]*?)<\/em>/g, (_, value) => {
|
|
89
|
+
return `_${decodeHtmlText(stripTags(value))}_`;
|
|
90
|
+
})
|
|
91
|
+
.replace(/<code>([\s\S]*?)<\/code>/g, (_, value) => {
|
|
92
|
+
return `\`${decodeHtmlText(stripTags(value)).replace(/`/g, "\\`")}\``;
|
|
93
|
+
})
|
|
94
|
+
.replace(/<a href="([^"]*)">([\s\S]*?)<\/a>/g, (_, href, value) => {
|
|
95
|
+
return `[${decodeHtmlText(stripTags(value))}](${decodeHtmlText(href)})`;
|
|
96
|
+
})
|
|
97
|
+
.replace(/<a>([\s\S]*?)<\/a>/g, (_, value) => {
|
|
98
|
+
return decodeHtmlText(stripTags(value));
|
|
99
|
+
})
|
|
100
|
+
.replace(/<[^>]+>/g, "")))
|
|
101
|
+
.replace(/\u00a0/g, " ")
|
|
102
|
+
.trim();
|
|
103
|
+
return sanitized;
|
|
104
|
+
}
|
|
105
|
+
function markdownToEditorHtml(markdown) {
|
|
106
|
+
const tokens = [];
|
|
107
|
+
let html = escapeHtmlText(normalizeTypography(markdown));
|
|
108
|
+
html = html.replace(/`([^`\n]+)`/g, (_, value) => {
|
|
109
|
+
const token = pushToken(tokens, `<code>${value}</code>`);
|
|
110
|
+
return token;
|
|
111
|
+
});
|
|
112
|
+
html = html.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, (_, label, href) => {
|
|
113
|
+
const safeHref = getSafeUrl(decodeHtmlText(href));
|
|
114
|
+
return safeHref ? `<a href="${safeHref}">${label}</a>` : label;
|
|
115
|
+
});
|
|
116
|
+
html = html
|
|
117
|
+
.replace(/\*\*([^*\n]+)\*\*/g, "<strong>$1</strong>")
|
|
118
|
+
.replace(/_([^_\n]+)_/g, "<em>$1</em>")
|
|
119
|
+
.replace(/ {2}\n/g, "<br>")
|
|
120
|
+
.replace(/\n/g, "<br>");
|
|
121
|
+
tokens.forEach((value, index) => {
|
|
122
|
+
html = html.replace(tokenPlaceholder(index), value);
|
|
123
|
+
});
|
|
124
|
+
return html;
|
|
125
|
+
}
|
|
126
|
+
function sanitizeRichTextHtml(html) {
|
|
127
|
+
return html
|
|
128
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
129
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
130
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
131
|
+
.replace(/<b(\s[^>]*)?>/gi, "<strong>")
|
|
132
|
+
.replace(/<\/b>/gi, "</strong>")
|
|
133
|
+
.replace(/<i(\s[^>]*)?>/gi, "<em>")
|
|
134
|
+
.replace(/<\/i>/gi, "</em>")
|
|
135
|
+
.replace(/<\/?([a-z0-9]+)([^>]*)>/gi, (match, rawTag, rawAttrs) => {
|
|
136
|
+
const tag = String(rawTag).toLowerCase();
|
|
137
|
+
const closing = match.startsWith("</");
|
|
138
|
+
if (!allowedInlineTags.has(tag)) {
|
|
139
|
+
return "";
|
|
140
|
+
}
|
|
141
|
+
if (closing) {
|
|
142
|
+
return tag === "br" ? "" : `</${tag}>`;
|
|
143
|
+
}
|
|
144
|
+
if (tag === "a") {
|
|
145
|
+
const href = getSafeAttribute(rawAttrs, "href");
|
|
146
|
+
return href ? `<a href="${href}">` : "<a>";
|
|
147
|
+
}
|
|
148
|
+
return tag === "br" ? "<br>" : `<${tag}>`;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function sanitizeRichTextBlock(block) {
|
|
152
|
+
if (!isRecord(block)) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
const id = readBlockId(block);
|
|
156
|
+
const type = block.type;
|
|
157
|
+
if (type === "code") {
|
|
158
|
+
return [{ id, type, text: String(block.text ?? "") }];
|
|
159
|
+
}
|
|
160
|
+
if (type === "divider") {
|
|
161
|
+
return [{ id, type }];
|
|
162
|
+
}
|
|
163
|
+
if (type === "image") {
|
|
164
|
+
return [
|
|
165
|
+
{
|
|
166
|
+
id,
|
|
167
|
+
type,
|
|
168
|
+
...(typeof block.assetId === "string" && block.assetId.trim()
|
|
169
|
+
? { assetId: block.assetId.trim() }
|
|
170
|
+
: {}),
|
|
171
|
+
...(typeof block.alt === "string" && block.alt.trim()
|
|
172
|
+
? { alt: markdownToPlainText(editorHtmlToMarkdown(block.alt)) }
|
|
173
|
+
: {}),
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
if (typeof type === "string" && textBlockTypes.has(type)) {
|
|
178
|
+
const markdown = typeof block.markdown === "string"
|
|
179
|
+
? block.markdown
|
|
180
|
+
: typeof block.text === "string"
|
|
181
|
+
? block.text
|
|
182
|
+
: typeof block.html === "string"
|
|
183
|
+
? editorHtmlToMarkdown(block.html)
|
|
184
|
+
: "";
|
|
185
|
+
if (type === "checkbox") {
|
|
186
|
+
return [
|
|
187
|
+
{
|
|
188
|
+
id,
|
|
189
|
+
type,
|
|
190
|
+
checked: Boolean(block.checked),
|
|
191
|
+
markdown: sanitizeMarkdown(markdown),
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
return [
|
|
196
|
+
{
|
|
197
|
+
id,
|
|
198
|
+
type: type,
|
|
199
|
+
markdown: sanitizeMarkdown(markdown),
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
function readBlockId(block) {
|
|
206
|
+
return typeof block.id === "string" && block.id.trim()
|
|
207
|
+
? block.id.trim()
|
|
208
|
+
: createRichTextBlockId();
|
|
209
|
+
}
|
|
210
|
+
function createUniqueRichTextBlockId(existingIds) {
|
|
211
|
+
let id = createRichTextBlockId();
|
|
212
|
+
while (existingIds.has(id)) {
|
|
213
|
+
id = createRichTextBlockId();
|
|
214
|
+
}
|
|
215
|
+
return id;
|
|
216
|
+
}
|
|
217
|
+
function markdownToPlainText(value) {
|
|
218
|
+
return value
|
|
219
|
+
.replace(/`([^`]*)`/g, "$1")
|
|
220
|
+
.replace(/\*\*([^*]*)\*\*/g, "$1")
|
|
221
|
+
.replace(/_([^_]*)_/g, "$1")
|
|
222
|
+
.replace(/\[([^\]]*)\]\([^)]+\)/g, "$1")
|
|
223
|
+
.replace(/[#>*`_[\]()]/g, " ")
|
|
224
|
+
.replace(/\s+/g, " ")
|
|
225
|
+
.trim();
|
|
226
|
+
}
|
|
227
|
+
function sanitizeMarkdown(value) {
|
|
228
|
+
return normalizeTypography(value)
|
|
229
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
230
|
+
.trim();
|
|
231
|
+
}
|
|
232
|
+
function normalizeTypography(value) {
|
|
233
|
+
const tokens = [];
|
|
234
|
+
let normalized = value.replace(/`([^`\n]+)`/g, (match) => {
|
|
235
|
+
return pushToken(tokens, match);
|
|
236
|
+
});
|
|
237
|
+
normalized = normalized.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, (_match, label, href) => {
|
|
238
|
+
return `[${label}](${pushToken(tokens, href)})`;
|
|
239
|
+
});
|
|
240
|
+
normalized = normalized
|
|
241
|
+
.replace(/––/g, "—")
|
|
242
|
+
.replace(/–-/g, "—")
|
|
243
|
+
.replace(/---/g, "—")
|
|
244
|
+
.replace(/--/g, "–");
|
|
245
|
+
tokens.forEach((token, index) => {
|
|
246
|
+
normalized = normalized.replace(tokenPlaceholder(index), token);
|
|
247
|
+
});
|
|
248
|
+
return normalized;
|
|
249
|
+
}
|
|
250
|
+
function stripTags(value) {
|
|
251
|
+
return value.replace(/<[^>]+>/g, "");
|
|
252
|
+
}
|
|
253
|
+
function getSafeAttribute(attrs, name) {
|
|
254
|
+
const value = getPlainAttribute(attrs, name);
|
|
255
|
+
const safeValue = getSafeUrl(value);
|
|
256
|
+
if (!safeValue) {
|
|
257
|
+
return "";
|
|
258
|
+
}
|
|
259
|
+
return escapeAttribute(safeValue);
|
|
260
|
+
}
|
|
261
|
+
function getSafeUrl(value) {
|
|
262
|
+
const trimmed = value.trim();
|
|
263
|
+
if (!trimmed || /^javascript:/i.test(trimmed)) {
|
|
264
|
+
return "";
|
|
265
|
+
}
|
|
266
|
+
return trimmed;
|
|
267
|
+
}
|
|
268
|
+
function getPlainAttribute(attrs, name) {
|
|
269
|
+
const pattern = new RegExp(`${name}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`, "i");
|
|
270
|
+
const match = attrs.match(pattern);
|
|
271
|
+
return match?.[2] ?? match?.[3] ?? match?.[4] ?? "";
|
|
272
|
+
}
|
|
273
|
+
function escapeAttribute(value) {
|
|
274
|
+
return value
|
|
275
|
+
.replace(/&/g, "&")
|
|
276
|
+
.replace(/</g, "<")
|
|
277
|
+
.replace(/>/g, ">")
|
|
278
|
+
.replace(/"/g, """);
|
|
279
|
+
}
|
|
280
|
+
function escapeHtmlText(value) {
|
|
281
|
+
return value
|
|
282
|
+
.replace(/&/g, "&")
|
|
283
|
+
.replace(/</g, "<")
|
|
284
|
+
.replace(/>/g, ">");
|
|
285
|
+
}
|
|
286
|
+
function decodeHtmlText(value) {
|
|
287
|
+
return value
|
|
288
|
+
.replace(/ /g, " ")
|
|
289
|
+
.replace(/ /g, " ")
|
|
290
|
+
.replace(/ /gi, " ")
|
|
291
|
+
.replace(/&/g, "&")
|
|
292
|
+
.replace(/</g, "<")
|
|
293
|
+
.replace(/>/g, ">")
|
|
294
|
+
.replace(/"/g, '"')
|
|
295
|
+
.replace(/'/g, "'")
|
|
296
|
+
.replace(/'/g, "'");
|
|
297
|
+
}
|
|
298
|
+
function pushToken(tokens, value) {
|
|
299
|
+
const index = tokens.push(value) - 1;
|
|
300
|
+
return tokenPlaceholder(index);
|
|
301
|
+
}
|
|
302
|
+
function tokenPlaceholder(index) {
|
|
303
|
+
return `@@RICHTEXTTOKEN${index}@@`;
|
|
304
|
+
}
|
|
305
|
+
function isRecord(value) {
|
|
306
|
+
return typeof value === "object" && value !== null;
|
|
307
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getEditorSaveControlState = getEditorSaveControlState;
|
|
4
|
+
function getEditorSaveControlState({ isDirty, isSaving, saveLabel, }) {
|
|
5
|
+
if (isSaving) {
|
|
6
|
+
return { disabled: true, label: "Saving..." };
|
|
7
|
+
}
|
|
8
|
+
if (!isDirty) {
|
|
9
|
+
return { disabled: true, label: "No changes to save" };
|
|
10
|
+
}
|
|
11
|
+
return { disabled: false, label: saveLabel };
|
|
12
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createEditorSession = createEditorSession;
|
|
4
|
+
exports.defaultEditorSessionEquals = defaultEditorSessionEquals;
|
|
5
|
+
exports.canonicalSerialize = canonicalSerialize;
|
|
6
|
+
function createEditorSession({ dirtyScope = "all", equals, initialBaselineValue, initialValue, onSave, }) {
|
|
7
|
+
let baseline = cloneValue(initialBaselineValue ?? initialValue);
|
|
8
|
+
let currentValue = cloneValue(initialValue);
|
|
9
|
+
let saving = false;
|
|
10
|
+
let saveHandler = onSave;
|
|
11
|
+
const listeners = new Set();
|
|
12
|
+
const valuesEqual = equals ??
|
|
13
|
+
((left, right) => defaultEditorSessionEquals(left, right, dirtyScope));
|
|
14
|
+
function emitChange() {
|
|
15
|
+
listeners.forEach((listener) => listener());
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
getValue: () => currentValue,
|
|
19
|
+
isDirty: () => !valuesEqual(currentValue, baseline),
|
|
20
|
+
isSaving: () => saving,
|
|
21
|
+
markClean: (value) => {
|
|
22
|
+
baseline = cloneValue(value ?? currentValue);
|
|
23
|
+
emitChange();
|
|
24
|
+
},
|
|
25
|
+
reset: () => {
|
|
26
|
+
currentValue = cloneValue(baseline);
|
|
27
|
+
emitChange();
|
|
28
|
+
return currentValue;
|
|
29
|
+
},
|
|
30
|
+
save: async () => {
|
|
31
|
+
if (saving) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
saving = true;
|
|
35
|
+
emitChange();
|
|
36
|
+
try {
|
|
37
|
+
const valueToSave = cloneValue(currentValue);
|
|
38
|
+
const result = await saveHandler(valueToSave);
|
|
39
|
+
baseline = cloneValue(valueToSave);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
saving = false;
|
|
44
|
+
emitChange();
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
setOnSave: (nextOnSave) => {
|
|
48
|
+
saveHandler = nextOnSave;
|
|
49
|
+
},
|
|
50
|
+
subscribe: (listener) => {
|
|
51
|
+
listeners.add(listener);
|
|
52
|
+
return () => listeners.delete(listener);
|
|
53
|
+
},
|
|
54
|
+
update: (value) => {
|
|
55
|
+
currentValue = cloneValue(value);
|
|
56
|
+
emitChange();
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function defaultEditorSessionEquals(left, right, dirtyScope = "all") {
|
|
61
|
+
if (dirtyScope === "content") {
|
|
62
|
+
return (canonicalSerialize(left.document) === canonicalSerialize(right.document));
|
|
63
|
+
}
|
|
64
|
+
return canonicalSerialize(left) === canonicalSerialize(right);
|
|
65
|
+
}
|
|
66
|
+
function canonicalSerialize(value) {
|
|
67
|
+
return JSON.stringify(sortValue(value));
|
|
68
|
+
}
|
|
69
|
+
function sortValue(value) {
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
return value.map(sortValue);
|
|
72
|
+
}
|
|
73
|
+
if (value && typeof value === "object") {
|
|
74
|
+
return Object.fromEntries(Object.entries(value)
|
|
75
|
+
.filter(([, entryValue]) => entryValue !== undefined)
|
|
76
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
77
|
+
.map(([key, entryValue]) => [key, sortValue(entryValue)]));
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
function cloneValue(value) {
|
|
82
|
+
return JSON.parse(JSON.stringify(value));
|
|
83
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createEditorSessionRegistry = createEditorSessionRegistry;
|
|
4
|
+
const cleanSnapshot = {
|
|
5
|
+
dirty: false,
|
|
6
|
+
error: null,
|
|
7
|
+
pendingExit: false,
|
|
8
|
+
saving: false,
|
|
9
|
+
};
|
|
10
|
+
function createEditorSessionRegistry() {
|
|
11
|
+
const sessions = new Set();
|
|
12
|
+
const sessionUnsubscribers = new Map();
|
|
13
|
+
const listeners = new Set();
|
|
14
|
+
let pendingAction = null;
|
|
15
|
+
let snapshot = cleanSnapshot;
|
|
16
|
+
function emitChange(changes = {}) {
|
|
17
|
+
snapshot = {
|
|
18
|
+
...snapshot,
|
|
19
|
+
...changes,
|
|
20
|
+
dirty: Array.from(sessions).some((session) => session.isDirty()),
|
|
21
|
+
};
|
|
22
|
+
listeners.forEach((listener) => listener());
|
|
23
|
+
}
|
|
24
|
+
function clearPendingExit() {
|
|
25
|
+
pendingAction = null;
|
|
26
|
+
emitChange({ error: null, pendingExit: false, saving: false });
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
discardAndExit: () => {
|
|
30
|
+
const action = pendingAction;
|
|
31
|
+
sessions.forEach((session) => {
|
|
32
|
+
if (session.isDirty()) {
|
|
33
|
+
session.reset();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
clearPendingExit();
|
|
37
|
+
void action?.();
|
|
38
|
+
},
|
|
39
|
+
getSnapshot: () => snapshot,
|
|
40
|
+
register: (session) => {
|
|
41
|
+
sessions.add(session);
|
|
42
|
+
sessionUnsubscribers.set(session, session.subscribe(() => emitChange()));
|
|
43
|
+
emitChange();
|
|
44
|
+
return () => {
|
|
45
|
+
sessionUnsubscribers.get(session)?.();
|
|
46
|
+
sessionUnsubscribers.delete(session);
|
|
47
|
+
sessions.delete(session);
|
|
48
|
+
emitChange();
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
requestExit: async (action) => {
|
|
52
|
+
if (!Array.from(sessions).some((session) => session.isDirty())) {
|
|
53
|
+
await action();
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
pendingAction = action;
|
|
57
|
+
emitChange({ error: null, pendingExit: true });
|
|
58
|
+
return false;
|
|
59
|
+
},
|
|
60
|
+
saveAndExit: async () => {
|
|
61
|
+
const action = pendingAction;
|
|
62
|
+
if (!action || snapshot.saving) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
emitChange({ error: null, saving: true });
|
|
66
|
+
try {
|
|
67
|
+
for (const session of sessions) {
|
|
68
|
+
if (session.isDirty()) {
|
|
69
|
+
await session.save();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
clearPendingExit();
|
|
73
|
+
await action();
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
emitChange({
|
|
77
|
+
error: error instanceof Error ? error.message : "Unable to save changes.",
|
|
78
|
+
pendingExit: true,
|
|
79
|
+
saving: false,
|
|
80
|
+
});
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
stay: clearPendingExit,
|
|
85
|
+
subscribe: (listener) => {
|
|
86
|
+
listeners.add(listener);
|
|
87
|
+
return () => listeners.delete(listener);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getWritingStats = getWritingStats;
|
|
4
|
+
const richText_1 = require("./richText");
|
|
5
|
+
function getWritingStats(value) {
|
|
6
|
+
const plainText = (0, richText_1.richTextBlocksToPlainText)(value);
|
|
7
|
+
const wordCount = plainText ? plainText.split(" ").length : 0;
|
|
8
|
+
return {
|
|
9
|
+
readingMinutes: Math.max(1, Math.ceil(wordCount / 225)),
|
|
10
|
+
wordCount,
|
|
11
|
+
};
|
|
12
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bayonai/rich-text-editor",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Reusable rich text editing, rendering, and unsaved-change session controls.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist-cjs/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./react": {
|
|
17
|
+
"types": "./dist/react/index.d.ts",
|
|
18
|
+
"import": "./dist/react/index.js",
|
|
19
|
+
"require": "./dist-cjs/react/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"dist-cjs",
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -p tsconfig.json && node scripts/write-esm-extensions.mjs && tsc -p tsconfig.cjs.json && node scripts/write-cjs.mjs",
|
|
30
|
+
"lint": "tsc --noEmit -p tsconfig.json",
|
|
31
|
+
"test": "vitest run"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": "^19.0.0",
|
|
35
|
+
"react-dom": "^19.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/react": "^19",
|
|
39
|
+
"@types/react-dom": "^19",
|
|
40
|
+
"react": "19.2.4",
|
|
41
|
+
"react-dom": "19.2.4",
|
|
42
|
+
"typescript": "^5",
|
|
43
|
+
"vitest": "^4.1.6"
|
|
44
|
+
}
|
|
45
|
+
}
|