@upstart.gg/vite-plugins 0.1.5 → 0.1.6
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/src/vite-plugin-upstart-editor/runtime/state.d.ts +10 -0
- package/dist/src/vite-plugin-upstart-editor/runtime/state.d.ts.map +1 -0
- package/dist/src/vite-plugin-upstart-editor/runtime/state.js +18 -0
- package/dist/src/vite-plugin-upstart-editor/runtime/state.js.map +1 -0
- package/dist/upstart-editor-api.d.ts +0 -3
- package/dist/upstart-editor-api.d.ts.map +1 -1
- package/dist/upstart-editor-api.js +1 -2
- package/dist/upstart-editor-api.js.map +1 -1
- 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 +22 -22
- package/dist/vite-plugin-upstart-attrs.js.map +1 -1
- package/dist/vite-plugin-upstart-branding/plugin.d.ts +3 -4
- package/dist/vite-plugin-upstart-branding/plugin.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-branding/plugin.js +1 -2
- package/dist/vite-plugin-upstart-branding/plugin.js.map +1 -1
- package/dist/vite-plugin-upstart-branding/runtime.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-branding/runtime.js +2 -2
- package/dist/vite-plugin-upstart-branding/runtime.js.map +1 -1
- package/dist/vite-plugin-upstart-branding/types.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-branding/types.js +1 -1
- package/dist/vite-plugin-upstart-editor/plugin.d.ts +3 -4
- package/dist/vite-plugin-upstart-editor/plugin.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/plugin.js +2 -3
- package/dist/vite-plugin-upstart-editor/plugin.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +84 -16
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.js +1 -2
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +78 -21
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts +3 -5
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.js +30 -15
- package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts +0 -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 +9 -37
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +7 -9
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/types.js +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/utils.d.ts +0 -1
- package/dist/vite-plugin-upstart-editor/runtime/utils.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/utils.js +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 -2
- package/dist/vite-plugin-upstart-theme.js.map +1 -1
- package/package.json +14 -13
- package/src/vite-plugin-upstart-attrs.ts +1 -0
- package/src/vite-plugin-upstart-branding/runtime.ts +2 -3
- package/src/vite-plugin-upstart-editor/plugin.ts +2 -2
- package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +94 -18
- package/src/vite-plugin-upstart-editor/runtime/hover-overlay.ts +94 -20
- package/src/vite-plugin-upstart-editor/runtime/index.ts +42 -13
- package/src/vite-plugin-upstart-editor/runtime/state.ts +17 -0
- package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +49 -81
- package/src/vite-plugin-upstart-editor/runtime/types.ts +6 -3
|
@@ -1,8 +1,31 @@
|
|
|
1
|
-
import { getCurrentMode } from "./
|
|
1
|
+
import { getCurrentMode } from "./state.js";
|
|
2
2
|
import { sendToParent } from "./utils.js";
|
|
3
3
|
|
|
4
4
|
let isInitialized = false;
|
|
5
5
|
|
|
6
|
+
const DAISY_VAR_NAMES = [
|
|
7
|
+
"base-100",
|
|
8
|
+
"base-200",
|
|
9
|
+
"base-300",
|
|
10
|
+
"base-content",
|
|
11
|
+
"primary",
|
|
12
|
+
"primary-content",
|
|
13
|
+
"secondary",
|
|
14
|
+
"secondary-content",
|
|
15
|
+
"accent",
|
|
16
|
+
"accent-content",
|
|
17
|
+
"neutral",
|
|
18
|
+
"neutral-content",
|
|
19
|
+
"info",
|
|
20
|
+
"info-content",
|
|
21
|
+
"success",
|
|
22
|
+
"success-content",
|
|
23
|
+
"warning",
|
|
24
|
+
"warning-content",
|
|
25
|
+
"error",
|
|
26
|
+
"error-content",
|
|
27
|
+
];
|
|
28
|
+
|
|
6
29
|
/**
|
|
7
30
|
* Initialize click handler for className editing.
|
|
8
31
|
*/
|
|
@@ -41,6 +64,11 @@ function handleClick(event: MouseEvent): void {
|
|
|
41
64
|
return;
|
|
42
65
|
}
|
|
43
66
|
|
|
67
|
+
if (target.closest(".upstart-editor-bubble-menu")) {
|
|
68
|
+
console.info("[Upstart Editor] Click ignored: target is inside the bubble menu");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
44
72
|
const activeLink = target.closest("a[data-upstart-editor-active]");
|
|
45
73
|
const isFormControlClick = Boolean(
|
|
46
74
|
(target as HTMLElement | null)?.closest("input, textarea, select, button"),
|
|
@@ -49,26 +77,39 @@ function handleClick(event: MouseEvent): void {
|
|
|
49
77
|
event.preventDefault();
|
|
50
78
|
}
|
|
51
79
|
|
|
80
|
+
// Clicks inside contenteditable (TipTap active) — still notify style panel if applicable
|
|
52
81
|
if (target.closest("[contenteditable='true']")) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
const editableEl = target.closest<HTMLElement>("[data-upstart-editable-text='true']");
|
|
83
|
+
if (editableEl) {
|
|
84
|
+
const classNameId =
|
|
85
|
+
editableEl.dataset.upstartClassnameId ||
|
|
86
|
+
editableEl.closest<HTMLElement>("[data-upstart-classname-id]")?.dataset.upstartClassnameId ||
|
|
87
|
+
"";
|
|
88
|
+
if (classNameId) {
|
|
89
|
+
const computedStyle = getComputedStyle(document.documentElement);
|
|
90
|
+
const themeColors: Record<string, string> = {};
|
|
91
|
+
for (const name of DAISY_VAR_NAMES) {
|
|
92
|
+
const val = computedStyle.getPropertyValue(`--color-${name}`).trim();
|
|
93
|
+
if (val) themeColors[name] = val;
|
|
94
|
+
}
|
|
95
|
+
sendToParent({
|
|
96
|
+
type: "element-clicked",
|
|
97
|
+
hash: editableEl.dataset.upstartHash ?? "",
|
|
98
|
+
componentName: editableEl.dataset.upstartComponent,
|
|
99
|
+
filePath: editableEl.dataset.upstartFile ?? "",
|
|
100
|
+
classNameId,
|
|
101
|
+
currentClassName: editableEl.className,
|
|
102
|
+
themeColors,
|
|
103
|
+
bounds: { top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0 },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
console.info("[Upstart Editor] Click inside contenteditable: TipTap handles text editing");
|
|
66
108
|
return;
|
|
67
109
|
}
|
|
68
110
|
|
|
111
|
+
// Find the closest element with data-upstart-hash
|
|
69
112
|
const element = target.closest<HTMLElement>("[data-upstart-hash]");
|
|
70
|
-
// const i18nKey = target.closest<HTMLElement>("[data-upstart-i18n]");
|
|
71
|
-
|
|
72
113
|
if (!element) {
|
|
73
114
|
console.info("[Upstart Editor] Click ignored: no ancestral element with data-upstart-hash found");
|
|
74
115
|
return;
|
|
@@ -81,7 +122,39 @@ function handleClick(event: MouseEvent): void {
|
|
|
81
122
|
const hash = element.dataset.upstartHash as string;
|
|
82
123
|
const componentName = element.dataset.upstartComponent;
|
|
83
124
|
const filePath = element.dataset.upstartFile ?? "";
|
|
84
|
-
|
|
125
|
+
|
|
126
|
+
// If this element has editable-text="true", let TipTap handle text editing
|
|
127
|
+
if (element.dataset.upstartEditableText === "true") {
|
|
128
|
+
const proseMirror = element.querySelector<HTMLElement>(".ProseMirror");
|
|
129
|
+
if (proseMirror) {
|
|
130
|
+
proseMirror.focus();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Find classNameId on the element itself, or walk up to find it on a parent
|
|
135
|
+
let classNameId = element.dataset.upstartClassnameId ?? "";
|
|
136
|
+
let currentClassName = element.className;
|
|
137
|
+
if (!classNameId) {
|
|
138
|
+
const parentWithClassname = element.closest<HTMLElement>("[data-upstart-classname-id]");
|
|
139
|
+
if (parentWithClassname) {
|
|
140
|
+
classNameId = parentWithClassname.dataset.upstartClassnameId ?? "";
|
|
141
|
+
currentClassName = parentWithClassname.className;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check if this element (or a nearby ancestor) is a datasource record
|
|
146
|
+
const datasourceEl = target.closest<HTMLElement>("[data-upstart-datasource][data-upstart-record-id]");
|
|
147
|
+
const datasourceId = datasourceEl?.dataset.upstartDatasource;
|
|
148
|
+
const recordId = datasourceEl?.dataset.upstartRecordId;
|
|
149
|
+
|
|
150
|
+
// Read DaisyUI CSS custom property values from the iframe's document
|
|
151
|
+
const computedStyle = getComputedStyle(document.documentElement);
|
|
152
|
+
const themeColors: Record<string, string> = {};
|
|
153
|
+
for (const name of DAISY_VAR_NAMES) {
|
|
154
|
+
const val = computedStyle.getPropertyValue(`--color-${name}`).trim();
|
|
155
|
+
if (val) themeColors[name] = val;
|
|
156
|
+
}
|
|
157
|
+
|
|
85
158
|
const rect = element.getBoundingClientRect();
|
|
86
159
|
|
|
87
160
|
sendToParent({
|
|
@@ -90,7 +163,10 @@ function handleClick(event: MouseEvent): void {
|
|
|
90
163
|
componentName,
|
|
91
164
|
filePath,
|
|
92
165
|
classNameId,
|
|
93
|
-
currentClassName
|
|
166
|
+
currentClassName,
|
|
167
|
+
datasourceId,
|
|
168
|
+
recordId,
|
|
169
|
+
themeColors,
|
|
94
170
|
bounds: {
|
|
95
171
|
top: rect.top,
|
|
96
172
|
left: rect.left,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { getCurrentMode } from "./
|
|
1
|
+
import { getCurrentMode } from "./state.js";
|
|
2
2
|
import { sendToParent } from "./utils.js";
|
|
3
3
|
|
|
4
4
|
let overlay: HTMLDivElement | null = null;
|
|
5
5
|
let currentTarget: HTMLElement | null = null;
|
|
6
|
+
let hoveredEl: HTMLElement | null = null;
|
|
6
7
|
let isInitialized = false;
|
|
7
8
|
let rafId: number | null = null;
|
|
8
9
|
|
|
@@ -20,8 +21,35 @@ export function initHoverOverlay(): void {
|
|
|
20
21
|
|
|
21
22
|
console.log("[Upstart Editor] Initializing hover overlay...");
|
|
22
23
|
|
|
24
|
+
const style = document.createElement("style");
|
|
25
|
+
style.id = "upstart-hover-edit-styles";
|
|
26
|
+
style.textContent = [
|
|
27
|
+
":root[data-upstart-edit-mode] [data-upstart-classname-id]:not([data-upstart-editable-text='true']),",
|
|
28
|
+
":root[data-upstart-edit-mode] [data-upstart-datasource][data-upstart-record-id]:not([data-upstart-editable-text='true']),",
|
|
29
|
+
":root[data-upstart-edit-mode] [data-upstart-editor-active] {",
|
|
30
|
+
" transition: outline 150ms, outline-offset 150ms;",
|
|
31
|
+
"}",
|
|
32
|
+
":root[data-upstart-edit-mode] [data-upstart-hovered] {",
|
|
33
|
+
" outline: 2px solid rgba(114, 112, 198, 0.6);",
|
|
34
|
+
" outline-offset: 4px;",
|
|
35
|
+
" cursor: pointer;",
|
|
36
|
+
"}",
|
|
37
|
+
":root[data-upstart-edit-mode] [data-upstart-editor-active][data-upstart-hovered] {",
|
|
38
|
+
" outline: 2px solid rgba(114, 112, 198, 0.6);",
|
|
39
|
+
" outline-offset: 4px;",
|
|
40
|
+
" cursor: text;",
|
|
41
|
+
"}",
|
|
42
|
+
":root[data-upstart-edit-mode] [data-upstart-hovered-ancestor] {",
|
|
43
|
+
" outline: 1px dashed rgba(114, 112, 198, 0.6);",
|
|
44
|
+
" cursor: pointer;",
|
|
45
|
+
"}",
|
|
46
|
+
].join("\n");
|
|
47
|
+
document.head.appendChild(style);
|
|
48
|
+
|
|
23
49
|
document.addEventListener("mouseover", handleMouseOver);
|
|
24
50
|
document.addEventListener("mouseout", handleMouseOut);
|
|
51
|
+
document.addEventListener("keydown", handleKeyChange);
|
|
52
|
+
document.addEventListener("keyup", handleKeyChange);
|
|
25
53
|
window.addEventListener("scroll", scheduleOverlayUpdate, { passive: true });
|
|
26
54
|
window.addEventListener("resize", scheduleOverlayUpdate, { passive: true });
|
|
27
55
|
|
|
@@ -36,6 +64,51 @@ export function hideOverlays(): void {
|
|
|
36
64
|
overlay.style.display = "none";
|
|
37
65
|
currentTarget = null;
|
|
38
66
|
}
|
|
67
|
+
clearEditableHover();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isEligible(el: HTMLElement): boolean {
|
|
71
|
+
if (el.dataset.upstartEditableText === "true") return false;
|
|
72
|
+
return !!(el.dataset.upstartClassnameId || (el.dataset.upstartDatasource && el.dataset.upstartRecordId));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clearEditableHover(): void {
|
|
76
|
+
for (const el of document.querySelectorAll("[data-upstart-hovered],[data-upstart-hovered-ancestor]")) {
|
|
77
|
+
(el as HTMLElement).removeAttribute("data-upstart-hovered");
|
|
78
|
+
(el as HTMLElement).removeAttribute("data-upstart-hovered-ancestor");
|
|
79
|
+
}
|
|
80
|
+
hoveredEl = null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function applyTextHover(el: HTMLElement): void {
|
|
84
|
+
clearEditableHover();
|
|
85
|
+
el.setAttribute("data-upstart-hovered", "");
|
|
86
|
+
hoveredEl = el;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function applyEditableHover(target: HTMLElement, withAncestors: boolean): void {
|
|
90
|
+
clearEditableHover();
|
|
91
|
+
|
|
92
|
+
// Find closest eligible element
|
|
93
|
+
let el: HTMLElement | null = target;
|
|
94
|
+
while (el && el !== document.documentElement) {
|
|
95
|
+
if (isEligible(el)) break;
|
|
96
|
+
el = el.parentElement;
|
|
97
|
+
}
|
|
98
|
+
if (!el || el === document.documentElement) return;
|
|
99
|
+
|
|
100
|
+
el.setAttribute("data-upstart-hovered", "");
|
|
101
|
+
hoveredEl = el;
|
|
102
|
+
|
|
103
|
+
if (withAncestors) {
|
|
104
|
+
let ancestor = el.parentElement;
|
|
105
|
+
while (ancestor && ancestor !== document.documentElement) {
|
|
106
|
+
if (isEligible(ancestor)) {
|
|
107
|
+
ancestor.setAttribute("data-upstart-hovered-ancestor", "");
|
|
108
|
+
}
|
|
109
|
+
ancestor = ancestor.parentElement;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
39
112
|
}
|
|
40
113
|
|
|
41
114
|
function handleMouseOver(event: MouseEvent): void {
|
|
@@ -48,6 +121,18 @@ function handleMouseOver(event: MouseEvent): void {
|
|
|
48
121
|
return;
|
|
49
122
|
}
|
|
50
123
|
|
|
124
|
+
// If hovering inside an active text editor, highlight only that element
|
|
125
|
+
// and suppress the component overlay box so parents don't get hover effects.
|
|
126
|
+
const textEl = target.closest<HTMLElement>("[data-upstart-editor-active]");
|
|
127
|
+
if (textEl) {
|
|
128
|
+
applyTextHover(textEl);
|
|
129
|
+
if (overlay) overlay.style.display = "none";
|
|
130
|
+
currentTarget = null;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
applyEditableHover(target, event.metaKey || event.ctrlKey);
|
|
135
|
+
|
|
51
136
|
const component = target.closest<HTMLElement>("[data-upstart-component]");
|
|
52
137
|
if (!component || component.dataset.upstartEditorActive) {
|
|
53
138
|
return;
|
|
@@ -59,23 +144,6 @@ function handleMouseOver(event: MouseEvent): void {
|
|
|
59
144
|
|
|
60
145
|
currentTarget = component;
|
|
61
146
|
positionOverlay(component);
|
|
62
|
-
|
|
63
|
-
const hash = component.dataset.upstartHash;
|
|
64
|
-
if (hash) {
|
|
65
|
-
const rect = component.getBoundingClientRect();
|
|
66
|
-
sendToParent({
|
|
67
|
-
type: "element-hovered",
|
|
68
|
-
hash,
|
|
69
|
-
bounds: {
|
|
70
|
-
top: rect.top,
|
|
71
|
-
left: rect.left,
|
|
72
|
-
width: rect.width,
|
|
73
|
-
height: rect.height,
|
|
74
|
-
right: rect.right,
|
|
75
|
-
bottom: rect.bottom,
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
147
|
}
|
|
80
148
|
|
|
81
149
|
function handleMouseOut(event: MouseEvent): void {
|
|
@@ -98,12 +166,18 @@ function handleMouseOut(event: MouseEvent): void {
|
|
|
98
166
|
hideOverlays();
|
|
99
167
|
}
|
|
100
168
|
|
|
169
|
+
function handleKeyChange(event: KeyboardEvent): void {
|
|
170
|
+
if (getCurrentMode() !== "edit" || !hoveredEl) return;
|
|
171
|
+
if (event.key !== "Meta" && event.key !== "Control") return;
|
|
172
|
+
applyEditableHover(hoveredEl, event.type === "keydown" && (event.metaKey || event.ctrlKey));
|
|
173
|
+
}
|
|
174
|
+
|
|
101
175
|
function createOverlay(): void {
|
|
102
176
|
overlay = document.createElement("div");
|
|
103
177
|
overlay.id = "upstart-hover-overlay";
|
|
104
178
|
overlay.style.cssText =
|
|
105
|
-
"position: absolute; pointer-events: none; border: 2px solid
|
|
106
|
-
"background: rgba(
|
|
179
|
+
"position: absolute; pointer-events: none; border: 2px solid rgba(114, 112, 198, 0.6); " +
|
|
180
|
+
"background: rgba(114, 112, 198, 0.05); border-radius: 4px; z-index: 9999; " +
|
|
107
181
|
"transition: all 0.1s ease; display: none;";
|
|
108
182
|
document.body.appendChild(overlay);
|
|
109
183
|
}
|
|
@@ -3,23 +3,18 @@ import { initHoverOverlay, hideOverlays } from "./hover-overlay.js";
|
|
|
3
3
|
import { initErrorHandler } from "./error-handler.js";
|
|
4
4
|
import { initTextEditor, activateAllEditors, destroyAllActiveEditors } from "./text-editor.js";
|
|
5
5
|
import { sendToParent } from "./utils.js";
|
|
6
|
+
import { getCurrentMode, setCurrentMode } from "./state.js";
|
|
6
7
|
import type { EditorMode, UpstartParentMessage } from "./types.js";
|
|
7
8
|
|
|
8
|
-
let currentMode: EditorMode = "preview";
|
|
9
9
|
let isInitialized = false;
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
* Get the current editor mode.
|
|
13
|
-
*/
|
|
14
|
-
export function getCurrentMode(): EditorMode {
|
|
15
|
-
return currentMode;
|
|
16
|
-
}
|
|
11
|
+
export { getCurrentMode };
|
|
17
12
|
|
|
18
13
|
/**
|
|
19
14
|
* Set the current editor mode.
|
|
20
15
|
*/
|
|
21
16
|
export function setMode(mode: EditorMode): void {
|
|
22
|
-
|
|
17
|
+
setCurrentMode(mode);
|
|
23
18
|
console.log(`[Upstart Editor] Setting mode to: ${mode}`);
|
|
24
19
|
if (mode === "edit") {
|
|
25
20
|
enableEditMode();
|
|
@@ -28,15 +23,42 @@ export function setMode(mode: EditorMode): void {
|
|
|
28
23
|
}
|
|
29
24
|
}
|
|
30
25
|
|
|
26
|
+
export function waitForHydration(callback: () => void): void {
|
|
27
|
+
// Remix/React 18 wraps hydrateRoot in startTransition, making hydration a
|
|
28
|
+
// concurrent (low-priority) operation that can span many frames after the
|
|
29
|
+
// load event. Instead of guessing a delay, we wait for the DOM to stabilise:
|
|
30
|
+
// once 200 ms pass without any childList mutations, hydration is done.
|
|
31
|
+
const STABILITY_MS = 200;
|
|
32
|
+
|
|
33
|
+
const onReady = () => {
|
|
34
|
+
let timer: number | null = null;
|
|
35
|
+
|
|
36
|
+
const settle = () => {
|
|
37
|
+
if (timer) clearTimeout(timer);
|
|
38
|
+
timer = window.setTimeout(() => {
|
|
39
|
+
observer.disconnect();
|
|
40
|
+
callback();
|
|
41
|
+
}, STABILITY_MS);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const observer = new MutationObserver(settle);
|
|
45
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
46
|
+
|
|
47
|
+
// Kick off the first timer (covers case where no mutations occur after load)
|
|
48
|
+
settle();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (document.readyState === "complete") {
|
|
52
|
+
onReady();
|
|
53
|
+
} else {
|
|
54
|
+
window.addEventListener("load", onReady, { once: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
31
58
|
/**
|
|
32
59
|
* Initialize the Upstart editor runtime.
|
|
33
60
|
*/
|
|
34
61
|
export function initUpstartEditor(): void {
|
|
35
|
-
if (typeof window === "undefined") {
|
|
36
|
-
console.warn("[Upstart Editor] Cannot initialize editor: not running in a browser environment");
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
62
|
if (isInitialized) {
|
|
41
63
|
console.log("[Upstart Editor] Editor is already initialized");
|
|
42
64
|
return;
|
|
@@ -105,16 +127,23 @@ function handleParentMessage(event: MessageEvent): void {
|
|
|
105
127
|
if (message.type === "set-mode") {
|
|
106
128
|
console.log("Setting editor mode to:", message.mode);
|
|
107
129
|
setMode(message.mode);
|
|
130
|
+
} else if (message.type === "preview-classname") {
|
|
131
|
+
const el = document.querySelector<HTMLElement>(`[data-upstart-classname-id="${message.classNameId}"]`);
|
|
132
|
+
if (el) {
|
|
133
|
+
el.className = message.className;
|
|
134
|
+
}
|
|
108
135
|
}
|
|
109
136
|
}
|
|
110
137
|
|
|
111
138
|
function enableEditMode(): void {
|
|
112
139
|
console.log("[Upstart Editor] Edit mode enabled");
|
|
140
|
+
document.documentElement.setAttribute("data-upstart-edit-mode", "");
|
|
113
141
|
activateAllEditors();
|
|
114
142
|
}
|
|
115
143
|
|
|
116
144
|
function disableEditMode(): void {
|
|
117
145
|
console.log("[Upstart Editor] Preview mode enabled");
|
|
146
|
+
document.documentElement.removeAttribute("data-upstart-edit-mode");
|
|
118
147
|
destroyAllActiveEditors();
|
|
119
148
|
hideOverlays();
|
|
120
149
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EditorMode } from "./types.js";
|
|
2
|
+
|
|
3
|
+
let currentMode: EditorMode = "preview";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get the current editor mode.
|
|
7
|
+
*/
|
|
8
|
+
export function getCurrentMode(): EditorMode {
|
|
9
|
+
return currentMode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Set the current editor mode (internal use only).
|
|
14
|
+
*/
|
|
15
|
+
export function setCurrentMode(mode: EditorMode): void {
|
|
16
|
+
currentMode = mode;
|
|
17
|
+
}
|
|
@@ -54,52 +54,13 @@ let reactivateTimer: number | null = null;
|
|
|
54
54
|
* Activation is deferred to avoid conflicting with React hydration.
|
|
55
55
|
*/
|
|
56
56
|
export function initTextEditor(options: UpstartEditorOptions = {}): void {
|
|
57
|
-
if (typeof window === "undefined") {
|
|
58
|
-
console.warn("[Upstart Editor] Cannot initialize text editor: not running in a browser environment");
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
57
|
resolvedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
63
58
|
registerKeyboardShortcuts();
|
|
64
59
|
|
|
65
60
|
// Defer activation to let React finish hydrating the server-rendered HTML.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
startDomObserver();
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function waitForHydration(callback: () => void): void {
|
|
74
|
-
// Remix/React 18 wraps hydrateRoot in startTransition, making hydration a
|
|
75
|
-
// concurrent (low-priority) operation that can span many frames after the
|
|
76
|
-
// load event. Instead of guessing a delay, we wait for the DOM to stabilise:
|
|
77
|
-
// once 200 ms pass without any childList mutations, hydration is done.
|
|
78
|
-
const STABILITY_MS = 200;
|
|
79
|
-
|
|
80
|
-
const onReady = () => {
|
|
81
|
-
let timer: number | null = null;
|
|
82
|
-
|
|
83
|
-
const settle = () => {
|
|
84
|
-
if (timer) clearTimeout(timer);
|
|
85
|
-
timer = window.setTimeout(() => {
|
|
86
|
-
observer.disconnect();
|
|
87
|
-
callback();
|
|
88
|
-
}, STABILITY_MS);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const observer = new MutationObserver(settle);
|
|
92
|
-
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
93
|
-
|
|
94
|
-
// Kick off the first timer (covers case where no mutations occur after load)
|
|
95
|
-
settle();
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
if (document.readyState === "complete") {
|
|
99
|
-
onReady();
|
|
100
|
-
} else {
|
|
101
|
-
window.addEventListener("load", onReady, { once: true });
|
|
102
|
-
}
|
|
61
|
+
injectEditorStyles();
|
|
62
|
+
// activateAllEditors();
|
|
63
|
+
startDomObserver();
|
|
103
64
|
}
|
|
104
65
|
|
|
105
66
|
/**
|
|
@@ -118,11 +79,6 @@ function injectEditorStyles(): void {
|
|
|
118
79
|
style.textContent = [
|
|
119
80
|
"[data-upstart-editor-active] {",
|
|
120
81
|
" cursor: text;",
|
|
121
|
-
" transition: outline 100ms;",
|
|
122
|
-
"}",
|
|
123
|
-
"[data-upstart-editor-active]:hover {",
|
|
124
|
-
" outline: 1px solid #7270c6;",
|
|
125
|
-
" outline-offset: 4px;",
|
|
126
82
|
"}",
|
|
127
83
|
// "[data-upstart-editor-active] > *:not(.ProseMirror) {",
|
|
128
84
|
// " display: none !important;",
|
|
@@ -212,6 +168,7 @@ function createPlainTextEditor(
|
|
|
212
168
|
options: Required<UpstartEditorOptions>,
|
|
213
169
|
): Editor {
|
|
214
170
|
const content = element.textContent ?? "";
|
|
171
|
+
element.textContent = ""; // Clear the element first! (same as rich text editors)
|
|
215
172
|
let hasChanged = false;
|
|
216
173
|
|
|
217
174
|
const editor = new Editor({
|
|
@@ -380,6 +337,11 @@ function createInlineRichTextEditor(
|
|
|
380
337
|
}
|
|
381
338
|
|
|
382
339
|
function getEditorMode(element: HTMLElement, options: Required<UpstartEditorOptions>): TextEditorMode {
|
|
340
|
+
const modeOverride = element.dataset.upstartEditableTextMode as TextEditorMode | undefined;
|
|
341
|
+
if (modeOverride === "plain" || modeOverride === "inline-rich" || modeOverride === "block-rich") {
|
|
342
|
+
return modeOverride;
|
|
343
|
+
}
|
|
344
|
+
|
|
383
345
|
const tagName = element.tagName.toLowerCase();
|
|
384
346
|
|
|
385
347
|
if (options.plainTextElements.includes(tagName)) {
|
|
@@ -721,40 +683,46 @@ function wireBubbleMenu(menu: HTMLDivElement, editor: Editor): void {
|
|
|
721
683
|
event.preventDefault();
|
|
722
684
|
});
|
|
723
685
|
|
|
724
|
-
menu.addEventListener(
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
686
|
+
menu.addEventListener(
|
|
687
|
+
"click",
|
|
688
|
+
(event) => {
|
|
689
|
+
console.log("menu button cliked");
|
|
690
|
+
event.stopPropagation();
|
|
691
|
+
const target = event.target as HTMLElement | null;
|
|
692
|
+
const command = target?.dataset.command;
|
|
693
|
+
if (!command) return;
|
|
694
|
+
|
|
695
|
+
const chain = editor.chain().focus();
|
|
696
|
+
|
|
697
|
+
switch (command) {
|
|
698
|
+
case "bold":
|
|
699
|
+
chain.toggleBold().run();
|
|
700
|
+
break;
|
|
701
|
+
case "italic":
|
|
702
|
+
chain.toggleItalic().run();
|
|
703
|
+
break;
|
|
704
|
+
case "strike":
|
|
705
|
+
chain.toggleStrike().run();
|
|
706
|
+
break;
|
|
707
|
+
case "code":
|
|
708
|
+
chain.toggleCode().run();
|
|
709
|
+
break;
|
|
710
|
+
case "heading":
|
|
711
|
+
chain.toggleHeading({ level: 1 }).run();
|
|
712
|
+
break;
|
|
713
|
+
case "blockquote":
|
|
714
|
+
chain.toggleBlockquote().run();
|
|
715
|
+
break;
|
|
716
|
+
case "bulletList":
|
|
717
|
+
chain.toggleBulletList().run();
|
|
718
|
+
break;
|
|
719
|
+
case "orderedList":
|
|
720
|
+
chain.toggleOrderedList().run();
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
{ capture: true },
|
|
725
|
+
);
|
|
758
726
|
|
|
759
727
|
const updateActiveStates = (): void => {
|
|
760
728
|
const buttons = menu.querySelectorAll<HTMLButtonElement>("button[data-command]");
|
|
@@ -41,8 +41,6 @@ export type EditorMessage =
|
|
|
41
41
|
| { type: "editor-ready" }
|
|
42
42
|
| { type: "editor-navigated" }
|
|
43
43
|
| { type: "editor-error"; error: string }
|
|
44
|
-
| { type: "element-hovered"; hash: string; bounds: ElementBounds }
|
|
45
|
-
| { type: "datasource-item-clicked"; datasourceId: string; recordId: string }
|
|
46
44
|
| {
|
|
47
45
|
type: "element-clicked";
|
|
48
46
|
hash: string;
|
|
@@ -50,13 +48,18 @@ export type EditorMessage =
|
|
|
50
48
|
filePath: string;
|
|
51
49
|
currentClassName: string;
|
|
52
50
|
classNameId: string;
|
|
51
|
+
datasourceId?: string;
|
|
52
|
+
recordId?: string;
|
|
53
|
+
themeColors?: Record<string, string>;
|
|
53
54
|
bounds: ElementBounds;
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* Messages sent from parent to iframe.
|
|
58
59
|
*/
|
|
59
|
-
export type ParentMessage =
|
|
60
|
+
export type ParentMessage =
|
|
61
|
+
| { type: "set-mode"; mode: EditorMode }
|
|
62
|
+
| { type: "preview-classname"; classNameId: string; className: string };
|
|
60
63
|
|
|
61
64
|
/**
|
|
62
65
|
* Message wrapper sent via postMessage from iframe.
|