@upstart.gg/vite-plugins 0.0.38 → 0.0.40
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/dist/upstart-editor-api.d.ts +79 -0
- package/dist/upstart-editor-api.d.ts.map +1 -0
- package/dist/upstart-editor-api.js +208 -0
- package/dist/upstart-editor-api.js.map +1 -0
- package/dist/vite-plugin-upstart-attrs.d.ts +3 -3
- package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-attrs.js +227 -25
- package/dist/vite-plugin-upstart-attrs.js.map +1 -1
- package/dist/vite-plugin-upstart-branding/plugin.d.ts +17 -0
- package/dist/vite-plugin-upstart-branding/plugin.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-branding/plugin.js +41 -0
- package/dist/vite-plugin-upstart-branding/plugin.js.map +1 -0
- package/dist/vite-plugin-upstart-branding/runtime.d.ts +10 -0
- package/dist/vite-plugin-upstart-branding/runtime.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-branding/runtime.js +118 -0
- package/dist/vite-plugin-upstart-branding/runtime.js.map +1 -0
- package/dist/vite-plugin-upstart-branding/types.d.ts +14 -0
- package/dist/vite-plugin-upstart-branding/types.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-branding/types.js +1 -0
- package/dist/vite-plugin-upstart-editor/plugin.d.ts +3 -3
- package/dist/vite-plugin-upstart-editor/plugin.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/plugin.js +3 -16
- package/dist/vite-plugin-upstart-editor/plugin.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +25 -11
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.d.ts +5 -0
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.js +16 -0
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts +2 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.js +42 -7
- package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts +6 -1
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +423 -129
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +18 -10
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-theme.d.ts +3 -3
- package/dist/vite-plugin-upstart-theme.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-theme.js +1 -3
- package/dist/vite-plugin-upstart-theme.js.map +1 -1
- package/package.json +12 -4
- package/src/tests/upstart-editor-api.test.ts +98 -174
- package/src/tests/vite-plugin-upstart-attrs.test.ts +408 -105
- package/src/tests/vite-plugin-upstart-branding.test.ts +90 -0
- package/src/tests/vite-plugin-upstart-editor.test.ts +1 -2
- package/src/upstart-editor-api.ts +90 -29
- package/src/vite-plugin-upstart-attrs.ts +376 -38
- package/src/vite-plugin-upstart-branding/plugin.ts +59 -0
- package/src/vite-plugin-upstart-branding/runtime.ts +128 -0
- package/src/vite-plugin-upstart-branding/types.ts +10 -0
- package/src/vite-plugin-upstart-editor/plugin.ts +4 -19
- package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +25 -12
- package/src/vite-plugin-upstart-editor/runtime/error-handler.ts +12 -0
- package/src/vite-plugin-upstart-editor/runtime/hover-overlay.ts +1 -1
- package/src/vite-plugin-upstart-editor/runtime/index.ts +39 -5
- package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +518 -141
- package/src/vite-plugin-upstart-editor/runtime/types.ts +18 -4
- package/src/vite-plugin-upstart-theme.ts +0 -3
- package/src/vite-plugin-upstart-editor/PLAN.md +0 -1391
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
import { sendToParent } from "./utils.js";
|
|
2
|
-
import {
|
|
3
|
-
import { Editor } from "@tiptap/core";
|
|
2
|
+
import { Editor, Extension, Node } from "@tiptap/core";
|
|
4
3
|
import { BubbleMenu } from "@tiptap/extension-bubble-menu";
|
|
5
4
|
import Placeholder from "@tiptap/extension-placeholder";
|
|
6
5
|
import StarterKit from "@tiptap/starter-kit";
|
|
7
6
|
|
|
8
7
|
//#region src/vite-plugin-upstart-editor/runtime/text-editor.ts
|
|
8
|
+
/**
|
|
9
|
+
* Custom Document node for inline elements (e.g. <a>, <span>, headings).
|
|
10
|
+
* Uses `inline*` content instead of the default `block+` to prevent
|
|
11
|
+
* TipTap from wrapping text in <p> tags inside inline elements.
|
|
12
|
+
*/
|
|
13
|
+
const InlineDocument = Node.create({
|
|
14
|
+
name: "doc",
|
|
15
|
+
topNode: true,
|
|
16
|
+
content: "inline*"
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Remaps Enter to insert a <br> (hard break) instead of creating a new paragraph.
|
|
20
|
+
* Used in inline-rich mode where block nodes are not allowed.
|
|
21
|
+
*/
|
|
22
|
+
const EnterHardBreak = Extension.create({
|
|
23
|
+
name: "enterHardBreak",
|
|
24
|
+
addKeyboardShortcuts() {
|
|
25
|
+
return { Enter: () => this.editor.commands.setHardBreak() };
|
|
26
|
+
}
|
|
27
|
+
});
|
|
9
28
|
const DEFAULT_OPTIONS = {
|
|
10
29
|
richTextElements: [
|
|
11
30
|
"p",
|
|
@@ -13,8 +32,7 @@ const DEFAULT_OPTIONS = {
|
|
|
13
32
|
"article",
|
|
14
33
|
"section"
|
|
15
34
|
],
|
|
16
|
-
|
|
17
|
-
"button",
|
|
35
|
+
inlineRichTextElements: [
|
|
18
36
|
"a",
|
|
19
37
|
"span",
|
|
20
38
|
"h1",
|
|
@@ -22,35 +40,106 @@ const DEFAULT_OPTIONS = {
|
|
|
22
40
|
"h3",
|
|
23
41
|
"h4",
|
|
24
42
|
"h5",
|
|
25
|
-
"h6"
|
|
26
|
-
"label"
|
|
43
|
+
"h6"
|
|
27
44
|
],
|
|
45
|
+
plainTextElements: ["button", "label"],
|
|
28
46
|
bubbleMenu: true,
|
|
29
47
|
placeholder: "Start typing...",
|
|
30
48
|
autoSaveDelay: 1e3
|
|
31
49
|
};
|
|
32
50
|
const activeEditors = /* @__PURE__ */ new Map();
|
|
33
|
-
|
|
51
|
+
let i18nSyncInProgress = false;
|
|
34
52
|
const styleCache = /* @__PURE__ */ new WeakMap();
|
|
35
|
-
let
|
|
53
|
+
let resolvedOptions = DEFAULT_OPTIONS;
|
|
36
54
|
let shortcutsRegistered = false;
|
|
55
|
+
let domObserver = null;
|
|
56
|
+
let reactivateTimer = null;
|
|
37
57
|
/**
|
|
38
58
|
* Initialize TipTap text editing for elements marked as editable.
|
|
59
|
+
* Activation is deferred to avoid conflicting with React hydration.
|
|
39
60
|
*/
|
|
40
61
|
function initTextEditor(options = {}) {
|
|
41
|
-
if (typeof window === "undefined")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
62
|
+
if (typeof window === "undefined") {
|
|
63
|
+
console.warn("[Upstart Editor] Cannot initialize text editor: not running in a browser environment");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
resolvedOptions = {
|
|
67
|
+
...DEFAULT_OPTIONS,
|
|
68
|
+
...options
|
|
69
|
+
};
|
|
70
|
+
registerKeyboardShortcuts();
|
|
71
|
+
waitForHydration(() => {
|
|
72
|
+
injectEditorStyles();
|
|
73
|
+
startDomObserver();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function waitForHydration(callback) {
|
|
77
|
+
const STABILITY_MS = 200;
|
|
78
|
+
const onReady = () => {
|
|
79
|
+
let timer = null;
|
|
80
|
+
const settle = () => {
|
|
81
|
+
if (timer) clearTimeout(timer);
|
|
82
|
+
timer = window.setTimeout(() => {
|
|
83
|
+
observer.disconnect();
|
|
84
|
+
callback();
|
|
85
|
+
}, STABILITY_MS);
|
|
47
86
|
};
|
|
87
|
+
const observer = new MutationObserver(settle);
|
|
88
|
+
observer.observe(document.documentElement, {
|
|
89
|
+
childList: true,
|
|
90
|
+
subtree: true
|
|
91
|
+
});
|
|
92
|
+
settle();
|
|
93
|
+
};
|
|
94
|
+
if (document.readyState === "complete") onReady();
|
|
95
|
+
else window.addEventListener("load", onReady, { once: true });
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Inject CSS rules that visually hide original content when an editor is active.
|
|
99
|
+
* This avoids moving DOM nodes (which breaks React hydration).
|
|
100
|
+
*
|
|
101
|
+
* - `font-size: 0` + `color: transparent` hides direct text nodes
|
|
102
|
+
* - `> *:not(.ProseMirror)` hides child elements
|
|
103
|
+
* - ProseMirror gets explicit inline styles to restore text rendering
|
|
104
|
+
*/
|
|
105
|
+
function injectEditorStyles() {
|
|
106
|
+
if (document.getElementById("upstart-editor-styles")) return;
|
|
107
|
+
const style = document.createElement("style");
|
|
108
|
+
style.id = "upstart-editor-styles";
|
|
109
|
+
style.textContent = [
|
|
110
|
+
"[data-upstart-editor-active] {",
|
|
111
|
+
" cursor: text;",
|
|
112
|
+
" transition: outline 100ms;",
|
|
113
|
+
"}",
|
|
114
|
+
"[data-upstart-editor-active]:hover {",
|
|
115
|
+
" outline: 1px solid #7270c6;",
|
|
116
|
+
" outline-offset: 4px;",
|
|
117
|
+
"}",
|
|
118
|
+
"[data-upstart-editor-active] .ProseMirror {",
|
|
119
|
+
" outline: none !important;",
|
|
120
|
+
" font-size: inherit !important;",
|
|
121
|
+
" line-height: inherit !important;",
|
|
122
|
+
" color: inherit !important;",
|
|
123
|
+
" letter-spacing: inherit !important;",
|
|
124
|
+
" font-weight: inherit !important;",
|
|
125
|
+
" white-space: inherit !important;",
|
|
126
|
+
"}",
|
|
127
|
+
"[data-upstart-editor-active] .ProseMirror:focus {",
|
|
128
|
+
" outline: none !important;",
|
|
129
|
+
"}"
|
|
130
|
+
].join("\n");
|
|
131
|
+
document.head.appendChild(style);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Activate editors on all editable elements. Safe to call multiple times.
|
|
135
|
+
*/
|
|
136
|
+
function activateAllEditors() {
|
|
137
|
+
try {
|
|
138
|
+
cleanupOrphanedEditors();
|
|
48
139
|
document.querySelectorAll("[data-upstart-editable-text=\"true\"]").forEach((element) => setupEditableElement(element, resolvedOptions));
|
|
49
|
-
|
|
50
|
-
isInitialized = true;
|
|
51
|
-
console.log("[Upstart Editor] Text editor initialized");
|
|
140
|
+
console.log("[Upstart Editor] Text editors activated");
|
|
52
141
|
} catch (error) {
|
|
53
|
-
console.error("[Upstart Editor] Failed to
|
|
142
|
+
console.error("[Upstart Editor] Failed to activate text editors:", error);
|
|
54
143
|
sendToParent({
|
|
55
144
|
type: "editor-error",
|
|
56
145
|
error: error instanceof Error ? error.message : "Unknown error"
|
|
@@ -61,118 +150,190 @@ function initTextEditor(options = {}) {
|
|
|
61
150
|
* Destroy all active editors.
|
|
62
151
|
*/
|
|
63
152
|
function destroyAllActiveEditors() {
|
|
153
|
+
stopDomObserver();
|
|
64
154
|
for (const hash of activeEditors.keys()) destroyEditor(hash);
|
|
65
155
|
}
|
|
66
156
|
function setupEditableElement(element, options) {
|
|
67
157
|
const hash = getEditableHash(element);
|
|
68
|
-
if (!hash) return;
|
|
158
|
+
if (!hash || activeEditors.has(hash)) return;
|
|
69
159
|
cacheStyles(element);
|
|
70
|
-
element
|
|
71
|
-
element.addEventListener("dblclick", (event) => {
|
|
72
|
-
if (getCurrentMode() !== "edit") return;
|
|
73
|
-
event.preventDefault();
|
|
74
|
-
event.stopPropagation();
|
|
75
|
-
if (activeEditors.has(hash)) return;
|
|
76
|
-
activateEditor(element, hash, options);
|
|
77
|
-
});
|
|
78
|
-
element.addEventListener("mouseenter", () => {
|
|
79
|
-
if (getCurrentMode() !== "edit") return;
|
|
80
|
-
if (!activeEditors.has(hash)) applyHoverStyles(element);
|
|
81
|
-
});
|
|
82
|
-
element.addEventListener("mouseleave", () => {
|
|
83
|
-
if (!activeEditors.has(hash)) restoreStyles(element);
|
|
84
|
-
});
|
|
160
|
+
activateEditor(element, hash, options);
|
|
85
161
|
}
|
|
86
162
|
function activateEditor(element, hash, options) {
|
|
87
|
-
const
|
|
163
|
+
const mode = getEditorMode(element, options);
|
|
164
|
+
let editor;
|
|
165
|
+
switch (mode) {
|
|
166
|
+
case "block-rich":
|
|
167
|
+
editor = createRichTextEditor(element, hash, options);
|
|
168
|
+
break;
|
|
169
|
+
case "inline-rich":
|
|
170
|
+
editor = createInlineRichTextEditor(element, hash, options);
|
|
171
|
+
break;
|
|
172
|
+
case "plain":
|
|
173
|
+
editor = createPlainTextEditor(element, hash, options);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
element.dataset.upstartEditorActive = "true";
|
|
88
177
|
applyActiveStyles(element);
|
|
89
178
|
activeEditors.set(hash, {
|
|
90
179
|
editor,
|
|
91
180
|
element,
|
|
92
|
-
hash
|
|
181
|
+
hash,
|
|
182
|
+
mode
|
|
93
183
|
});
|
|
94
184
|
}
|
|
95
185
|
function createPlainTextEditor(element, hash, options) {
|
|
186
|
+
const content = element.textContent ?? "";
|
|
187
|
+
let hasChanged = false;
|
|
96
188
|
return new Editor({
|
|
97
189
|
element,
|
|
98
|
-
extensions: [
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
190
|
+
extensions: [
|
|
191
|
+
InlineDocument,
|
|
192
|
+
StarterKit.configure({
|
|
193
|
+
document: false,
|
|
194
|
+
heading: false,
|
|
195
|
+
bold: false,
|
|
196
|
+
italic: false,
|
|
197
|
+
strike: false,
|
|
198
|
+
blockquote: false,
|
|
199
|
+
bulletList: false,
|
|
200
|
+
orderedList: false,
|
|
201
|
+
listItem: false,
|
|
202
|
+
codeBlock: false,
|
|
203
|
+
horizontalRule: false
|
|
204
|
+
}),
|
|
205
|
+
Placeholder.configure({ placeholder: "Click to edit..." })
|
|
206
|
+
],
|
|
207
|
+
content,
|
|
208
|
+
editorProps: { attributes: { class: "upstart-editor-active" } },
|
|
209
|
+
onUpdate: ({ editor: e }) => {
|
|
210
|
+
if (i18nSyncInProgress) return;
|
|
211
|
+
hasChanged = true;
|
|
212
|
+
syncI18nSiblings(hash);
|
|
121
213
|
},
|
|
122
|
-
|
|
123
|
-
|
|
214
|
+
onBlur: ({ editor: e }) => {
|
|
215
|
+
if (!hasChanged) return;
|
|
216
|
+
hasChanged = false;
|
|
217
|
+
saveText(hash, e.getText());
|
|
124
218
|
}
|
|
125
219
|
});
|
|
126
220
|
}
|
|
127
221
|
function createRichTextEditor(element, hash, options) {
|
|
222
|
+
const content = element.innerHTML;
|
|
223
|
+
element.innerHTML = "";
|
|
224
|
+
let hasChanged = false;
|
|
128
225
|
const bubbleMenuElement = createBubbleMenuElement();
|
|
129
226
|
const extensions = [StarterKit, Placeholder.configure({ placeholder: options.placeholder })];
|
|
130
227
|
if (options.bubbleMenu) extensions.push(BubbleMenu.configure({ element: bubbleMenuElement }));
|
|
131
|
-
|
|
228
|
+
const editor = new Editor({
|
|
132
229
|
element,
|
|
133
230
|
extensions,
|
|
134
|
-
content
|
|
135
|
-
editorProps: { attributes: {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
debouncedSave(hash, updatedEditor.getHTML(), options.autoSaveDelay);
|
|
231
|
+
content,
|
|
232
|
+
editorProps: { attributes: { class: "upstart-editor-active" } },
|
|
233
|
+
onUpdate: ({ editor: e }) => {
|
|
234
|
+
if (i18nSyncInProgress) return;
|
|
235
|
+
hasChanged = true;
|
|
236
|
+
syncI18nSiblings(hash);
|
|
141
237
|
},
|
|
142
|
-
onBlur: ({ editor:
|
|
143
|
-
|
|
144
|
-
|
|
238
|
+
onBlur: ({ editor: e }) => {
|
|
239
|
+
if (!hasChanged) return;
|
|
240
|
+
hasChanged = false;
|
|
241
|
+
saveText(hash, e.getHTML());
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
if (options.bubbleMenu) wireBubbleMenu(bubbleMenuElement, editor);
|
|
245
|
+
return editor;
|
|
246
|
+
}
|
|
247
|
+
function createInlineRichTextEditor(element, hash, options) {
|
|
248
|
+
const content = element.innerHTML;
|
|
249
|
+
element.innerHTML = "";
|
|
250
|
+
let hasChanged = false;
|
|
251
|
+
const bubbleMenuElement = createBubbleMenuElement("inline");
|
|
252
|
+
const extensions = [
|
|
253
|
+
InlineDocument,
|
|
254
|
+
EnterHardBreak,
|
|
255
|
+
StarterKit.configure({
|
|
256
|
+
document: false,
|
|
257
|
+
heading: false,
|
|
258
|
+
blockquote: false,
|
|
259
|
+
bulletList: false,
|
|
260
|
+
orderedList: false,
|
|
261
|
+
listItem: false,
|
|
262
|
+
codeBlock: false,
|
|
263
|
+
horizontalRule: false
|
|
264
|
+
}),
|
|
265
|
+
Placeholder.configure({ placeholder: "Click to edit..." })
|
|
266
|
+
];
|
|
267
|
+
if (options.bubbleMenu) extensions.push(BubbleMenu.configure({ element: bubbleMenuElement }));
|
|
268
|
+
const editor = new Editor({
|
|
269
|
+
element,
|
|
270
|
+
extensions,
|
|
271
|
+
content,
|
|
272
|
+
editorProps: { attributes: { class: "upstart-editor-active" } },
|
|
273
|
+
onUpdate: ({ editor: e }) => {
|
|
274
|
+
if (i18nSyncInProgress) return;
|
|
275
|
+
hasChanged = true;
|
|
276
|
+
syncI18nSiblings(hash);
|
|
145
277
|
},
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
278
|
+
onBlur: ({ editor: e }) => {
|
|
279
|
+
if (!hasChanged) return;
|
|
280
|
+
hasChanged = false;
|
|
281
|
+
saveText(hash, e.getHTML());
|
|
149
282
|
}
|
|
150
283
|
});
|
|
284
|
+
if (options.bubbleMenu) wireBubbleMenu(bubbleMenuElement, editor);
|
|
285
|
+
return editor;
|
|
151
286
|
}
|
|
152
|
-
function
|
|
287
|
+
function getEditorMode(element, options) {
|
|
153
288
|
const tagName = element.tagName.toLowerCase();
|
|
154
|
-
if (options.plainTextElements.includes(tagName)) return
|
|
155
|
-
if (options.
|
|
156
|
-
return
|
|
289
|
+
if (options.plainTextElements.includes(tagName)) return "plain";
|
|
290
|
+
if (options.inlineRichTextElements.includes(tagName)) return "inline-rich";
|
|
291
|
+
if (options.richTextElements.includes(tagName)) return "block-rich";
|
|
292
|
+
return "block-rich";
|
|
157
293
|
}
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
294
|
+
function syncI18nSiblings(sourceHash) {
|
|
295
|
+
console.log("[Upstart Editor] Syncing i18n siblings for", sourceHash);
|
|
296
|
+
const instance = activeEditors.get(sourceHash);
|
|
297
|
+
if (!instance) {
|
|
298
|
+
console.warn("[Upstart Editor] No editor instance found for hash:", sourceHash);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const i18nKey = instance.element.dataset.upstartI18n;
|
|
302
|
+
if (!i18nKey) {
|
|
303
|
+
console.warn("[Upstart Editor] No sibling i18n key found for element:", instance.element);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const siblings = document.querySelectorAll(`[data-upstart-i18n="${CSS.escape(i18nKey)}"]`);
|
|
307
|
+
console.log(`[Upstart Editor] Found ${siblings.length} sibling(s) with i18n key "${i18nKey}"`);
|
|
308
|
+
const plainContent = instance.editor.getText();
|
|
309
|
+
i18nSyncInProgress = true;
|
|
310
|
+
try {
|
|
311
|
+
for (const sibling of siblings) {
|
|
312
|
+
if (sibling === instance.element) continue;
|
|
313
|
+
const siblingHash = sibling.dataset.upstartHash;
|
|
314
|
+
const siblingInstance = siblingHash ? activeEditors.get(siblingHash) : null;
|
|
315
|
+
if (siblingInstance) {
|
|
316
|
+
console.log(`[Upstart Editor] Updating sibling editor (hash: ${siblingHash}) with new content`, siblingInstance);
|
|
317
|
+
sibling.innerText = plainContent;
|
|
318
|
+
siblingInstance.editor.chain().selectAll().insertContent(plainContent).run();
|
|
319
|
+
} else sibling.textContent = plainContent;
|
|
320
|
+
}
|
|
321
|
+
} finally {
|
|
322
|
+
i18nSyncInProgress = false;
|
|
323
|
+
}
|
|
169
324
|
}
|
|
170
325
|
function saveText(hash, newText) {
|
|
171
326
|
try {
|
|
327
|
+
const [namespace, key] = (activeEditors.get(hash)?.element.dataset ?? {}).upstartI18n?.split(":") ?? [];
|
|
172
328
|
sendToParent({
|
|
173
|
-
type: "text-
|
|
174
|
-
|
|
175
|
-
|
|
329
|
+
type: "text-edit",
|
|
330
|
+
payload: {
|
|
331
|
+
action: "editText",
|
|
332
|
+
content: newText,
|
|
333
|
+
namespace,
|
|
334
|
+
key,
|
|
335
|
+
language: document.documentElement.lang
|
|
336
|
+
}
|
|
176
337
|
});
|
|
177
338
|
console.log("[Upstart Editor] Text save message sent:", hash);
|
|
178
339
|
} catch (error) {
|
|
@@ -186,13 +347,66 @@ function saveText(hash, newText) {
|
|
|
186
347
|
function destroyEditor(hash) {
|
|
187
348
|
const instance = activeEditors.get(hash);
|
|
188
349
|
if (!instance) return;
|
|
350
|
+
const finalContent = instance.mode === "plain" ? instance.editor.getText() : instance.editor.getHTML();
|
|
189
351
|
instance.editor.destroy();
|
|
352
|
+
const editorDom = instance.element.querySelector(".ProseMirror");
|
|
353
|
+
if (editorDom) editorDom.remove();
|
|
354
|
+
delete instance.element.dataset.upstartEditorActive;
|
|
355
|
+
if (instance.mode === "plain") instance.element.textContent = finalContent;
|
|
356
|
+
else instance.element.innerHTML = finalContent;
|
|
190
357
|
restoreStyles(instance.element);
|
|
191
358
|
activeEditors.delete(hash);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
359
|
+
}
|
|
360
|
+
function startDomObserver() {
|
|
361
|
+
if (domObserver) return;
|
|
362
|
+
domObserver = new MutationObserver((mutations) => {
|
|
363
|
+
let needsReactivation = false;
|
|
364
|
+
for (const mutation of mutations) {
|
|
365
|
+
if (mutation.type !== "childList") continue;
|
|
366
|
+
for (const node of mutation.addedNodes) if (node instanceof HTMLElement && (node.matches?.("[data-upstart-editable-text=\"true\"]") || node.querySelector?.("[data-upstart-editable-text=\"true\"]"))) {
|
|
367
|
+
needsReactivation = true;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
if (!needsReactivation) {
|
|
371
|
+
for (const node of mutation.removedNodes) if (node instanceof HTMLElement && (node.matches?.("[data-upstart-editable-text=\"true\"]") || node.querySelector?.("[data-upstart-editable-text=\"true\"]"))) {
|
|
372
|
+
needsReactivation = true;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (needsReactivation) break;
|
|
377
|
+
}
|
|
378
|
+
if (needsReactivation) scheduleReactivation();
|
|
379
|
+
});
|
|
380
|
+
domObserver.observe(document.body, {
|
|
381
|
+
childList: true,
|
|
382
|
+
subtree: true
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
function stopDomObserver() {
|
|
386
|
+
if (domObserver) {
|
|
387
|
+
domObserver.disconnect();
|
|
388
|
+
domObserver = null;
|
|
389
|
+
}
|
|
390
|
+
if (reactivateTimer) {
|
|
391
|
+
clearTimeout(reactivateTimer);
|
|
392
|
+
reactivateTimer = null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function scheduleReactivation() {
|
|
396
|
+
if (reactivateTimer) clearTimeout(reactivateTimer);
|
|
397
|
+
reactivateTimer = window.setTimeout(() => {
|
|
398
|
+
reactivateTimer = null;
|
|
399
|
+
activateAllEditors();
|
|
400
|
+
}, 50);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Remove editors whose host element has been detached from the document
|
|
404
|
+
* (e.g. React replaced the subtree during a re-render).
|
|
405
|
+
*/
|
|
406
|
+
function cleanupOrphanedEditors() {
|
|
407
|
+
for (const [hash, instance] of activeEditors) if (!document.contains(instance.element)) {
|
|
408
|
+
instance.editor.destroy();
|
|
409
|
+
activeEditors.delete(hash);
|
|
196
410
|
}
|
|
197
411
|
}
|
|
198
412
|
function registerKeyboardShortcuts() {
|
|
@@ -220,17 +434,8 @@ function cacheStyles(element) {
|
|
|
220
434
|
transition: element.style.transition || ""
|
|
221
435
|
});
|
|
222
436
|
}
|
|
223
|
-
function applyHoverStyles(element) {
|
|
224
|
-
cacheStyles(element);
|
|
225
|
-
element.style.outline = "1px dashed #3b82f6";
|
|
226
|
-
element.style.outlineOffset = "2px";
|
|
227
|
-
element.style.cursor = "text";
|
|
228
|
-
}
|
|
229
437
|
function applyActiveStyles(element) {
|
|
230
438
|
cacheStyles(element);
|
|
231
|
-
element.style.outline = "2px solid #3b82f6";
|
|
232
|
-
element.style.outlineOffset = "2px";
|
|
233
|
-
element.style.cursor = "text";
|
|
234
439
|
}
|
|
235
440
|
function restoreStyles(element) {
|
|
236
441
|
const cached = styleCache.get(element);
|
|
@@ -246,31 +451,82 @@ function restoreStyles(element) {
|
|
|
246
451
|
element.style.cursor = cached.cursor;
|
|
247
452
|
element.style.transition = cached.transition;
|
|
248
453
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
454
|
+
const BUBBLE_MENU_GROUPS = [
|
|
455
|
+
[
|
|
456
|
+
{
|
|
457
|
+
command: "bold",
|
|
458
|
+
icon: "format-bold",
|
|
459
|
+
title: "Bold"
|
|
460
|
+
},
|
|
254
461
|
{
|
|
255
|
-
|
|
256
|
-
|
|
462
|
+
command: "italic",
|
|
463
|
+
icon: "format-italic",
|
|
464
|
+
title: "Italic"
|
|
257
465
|
},
|
|
258
466
|
{
|
|
259
|
-
|
|
260
|
-
|
|
467
|
+
command: "strike",
|
|
468
|
+
icon: "format-strikethrough",
|
|
469
|
+
title: "Strikethrough"
|
|
261
470
|
},
|
|
262
471
|
{
|
|
263
|
-
|
|
264
|
-
|
|
472
|
+
command: "code",
|
|
473
|
+
icon: "code-tags",
|
|
474
|
+
title: "Inline Code"
|
|
265
475
|
}
|
|
266
|
-
]
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
476
|
+
],
|
|
477
|
+
[{
|
|
478
|
+
command: "heading",
|
|
479
|
+
icon: "format-header-1",
|
|
480
|
+
title: "Heading"
|
|
481
|
+
}, {
|
|
482
|
+
command: "blockquote",
|
|
483
|
+
icon: "format-quote-close",
|
|
484
|
+
title: "Blockquote"
|
|
485
|
+
}],
|
|
486
|
+
[{
|
|
487
|
+
command: "bulletList",
|
|
488
|
+
icon: "format-list-bulleted",
|
|
489
|
+
title: "Bullet List"
|
|
490
|
+
}, {
|
|
491
|
+
command: "orderedList",
|
|
492
|
+
icon: "format-list-numbered",
|
|
493
|
+
title: "Ordered List"
|
|
494
|
+
}]
|
|
495
|
+
];
|
|
496
|
+
function getIconifyUrl(iconName) {
|
|
497
|
+
return `https://api.iconify.design/mdi/${iconName}.svg?height=none&color=%23fff`;
|
|
498
|
+
}
|
|
499
|
+
function createBubbleMenuElement(mode = "block") {
|
|
500
|
+
const groups = mode === "inline" ? [BUBBLE_MENU_GROUPS[0]] : BUBBLE_MENU_GROUPS;
|
|
501
|
+
const menu = document.createElement("div");
|
|
502
|
+
menu.className = "upstart-editor-bubble-menu";
|
|
503
|
+
menu.style.cssText = "display: flex; align-items: center; gap: 2px; padding: 4px; background: #111827; color: #ffffff; border-radius: 8px; font-size: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.25);";
|
|
504
|
+
groups.forEach((group, groupIndex) => {
|
|
505
|
+
if (groupIndex > 0) {
|
|
506
|
+
const separator = document.createElement("div");
|
|
507
|
+
separator.style.cssText = "width: 1px; height: 16px; background: rgba(255,255,255,0.2); margin: 0 4px; flex-shrink: 0;";
|
|
508
|
+
menu.appendChild(separator);
|
|
509
|
+
}
|
|
510
|
+
for (const { command, icon, title } of group) {
|
|
511
|
+
const button = document.createElement("button");
|
|
512
|
+
button.type = "button";
|
|
513
|
+
button.dataset.command = command;
|
|
514
|
+
button.title = title;
|
|
515
|
+
button.style.cssText = "background: transparent; border: none; color: inherit; cursor: pointer; display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 4px; border-radius: 4px; transition: background 0.15s ease;";
|
|
516
|
+
const img = document.createElement("img");
|
|
517
|
+
img.src = getIconifyUrl(icon);
|
|
518
|
+
img.alt = title;
|
|
519
|
+
img.style.cssText = "width: 18px; height: 18px; display: block; pointer-events: none;";
|
|
520
|
+
button.appendChild(img);
|
|
521
|
+
button.addEventListener("mouseenter", () => {
|
|
522
|
+
if (!button.dataset.active) button.style.background = "rgba(255,255,255,0.12)";
|
|
523
|
+
});
|
|
524
|
+
button.addEventListener("mouseleave", () => {
|
|
525
|
+
button.style.background = button.dataset.active ? "rgba(255,255,255,0.2)" : "transparent";
|
|
526
|
+
});
|
|
527
|
+
menu.appendChild(button);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
274
530
|
return menu;
|
|
275
531
|
}
|
|
276
532
|
function wireBubbleMenu(menu, editor) {
|
|
@@ -281,12 +537,50 @@ function wireBubbleMenu(menu, editor) {
|
|
|
281
537
|
const command = event.target?.dataset.command;
|
|
282
538
|
if (!command) return;
|
|
283
539
|
const chain = editor.chain().focus();
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
540
|
+
switch (command) {
|
|
541
|
+
case "bold":
|
|
542
|
+
chain.toggleBold().run();
|
|
543
|
+
break;
|
|
544
|
+
case "italic":
|
|
545
|
+
chain.toggleItalic().run();
|
|
546
|
+
break;
|
|
547
|
+
case "strike":
|
|
548
|
+
chain.toggleStrike().run();
|
|
549
|
+
break;
|
|
550
|
+
case "code":
|
|
551
|
+
chain.toggleCode().run();
|
|
552
|
+
break;
|
|
553
|
+
case "heading":
|
|
554
|
+
chain.toggleHeading({ level: 1 }).run();
|
|
555
|
+
break;
|
|
556
|
+
case "blockquote":
|
|
557
|
+
chain.toggleBlockquote().run();
|
|
558
|
+
break;
|
|
559
|
+
case "bulletList":
|
|
560
|
+
chain.toggleBulletList().run();
|
|
561
|
+
break;
|
|
562
|
+
case "orderedList":
|
|
563
|
+
chain.toggleOrderedList().run();
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
287
566
|
});
|
|
567
|
+
const updateActiveStates = () => {
|
|
568
|
+
const buttons = menu.querySelectorAll("button[data-command]");
|
|
569
|
+
for (const button of buttons) {
|
|
570
|
+
const command = button.dataset.command;
|
|
571
|
+
if (command === "heading" ? editor.isActive("heading", { level: 1 }) : editor.isActive(command)) {
|
|
572
|
+
button.dataset.active = "true";
|
|
573
|
+
button.style.background = "rgba(255,255,255,0.2)";
|
|
574
|
+
} else {
|
|
575
|
+
delete button.dataset.active;
|
|
576
|
+
button.style.background = "transparent";
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
editor.on("transaction", updateActiveStates);
|
|
581
|
+
updateActiveStates();
|
|
288
582
|
}
|
|
289
583
|
|
|
290
584
|
//#endregion
|
|
291
|
-
export { destroyAllActiveEditors, initTextEditor };
|
|
585
|
+
export { activateAllEditors, destroyAllActiveEditors, initTextEditor };
|
|
292
586
|
//# sourceMappingURL=text-editor.js.map
|