@yoamigo.com/core 0.1.0
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/LICENSE +11 -0
- package/README.md +81 -0
- package/dist/MarkdownText-mylt-QX-.d.ts +106 -0
- package/dist/api-client-D8FkeBAI.d.ts +40 -0
- package/dist/asset-resolver-BnIvDkVv.d.ts +72 -0
- package/dist/builder-selection-CYP91nRu.d.ts +6 -0
- package/dist/index.css +372 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.js +2579 -0
- package/dist/lib-prod.d.ts +13 -0
- package/dist/lib-prod.js +108 -0
- package/dist/lib.d.ts +84 -0
- package/dist/lib.js +736 -0
- package/dist/plugin.d.ts +47 -0
- package/dist/plugin.js +96 -0
- package/dist/prod.d.ts +31 -0
- package/dist/prod.js +295 -0
- package/dist/router.d.ts +46 -0
- package/dist/router.js +42 -0
- package/package.json +115 -0
- package/src/styles/index.css +5 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2579 @@
|
|
|
1
|
+
// src/lib/content-registry.ts
|
|
2
|
+
var contentMap = /* @__PURE__ */ new Map();
|
|
3
|
+
function registerContent(content) {
|
|
4
|
+
contentMap = new Map(Object.entries(content));
|
|
5
|
+
}
|
|
6
|
+
function getContent(fieldId) {
|
|
7
|
+
return contentMap.get(fieldId) ?? "";
|
|
8
|
+
}
|
|
9
|
+
function getAllContent() {
|
|
10
|
+
return Object.fromEntries(contentMap);
|
|
11
|
+
}
|
|
12
|
+
function hasContent(fieldId) {
|
|
13
|
+
return contentMap.has(fieldId);
|
|
14
|
+
}
|
|
15
|
+
var contentRegistry = {
|
|
16
|
+
registerContent,
|
|
17
|
+
getContent,
|
|
18
|
+
getAllContent,
|
|
19
|
+
hasContent
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/lib/utils/content-loader.ts
|
|
23
|
+
function getAllContent2() {
|
|
24
|
+
return getAllContent();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/components/ContentStoreProvider.tsx
|
|
28
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
29
|
+
|
|
30
|
+
// src/lib/image-cache.ts
|
|
31
|
+
var cache = /* @__PURE__ */ new Map();
|
|
32
|
+
function setCachedImages(images) {
|
|
33
|
+
cache.clear();
|
|
34
|
+
const timestamp = Date.now();
|
|
35
|
+
for (const img of images) {
|
|
36
|
+
cache.set(img.key, {
|
|
37
|
+
blobUrl: img.blobUrl,
|
|
38
|
+
originalUrl: img.originalUrl,
|
|
39
|
+
timestamp
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function clearImageCache() {
|
|
44
|
+
cache.clear();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/lib/image-cache-listener.ts
|
|
48
|
+
var isListening = false;
|
|
49
|
+
function handleMessage(event) {
|
|
50
|
+
const data = event.data;
|
|
51
|
+
if (data?.type === "YA_IMAGE_CACHE_UPDATE") {
|
|
52
|
+
console.log("[ImageCache] Received cache update:", data.images.length, "images");
|
|
53
|
+
setCachedImages(data.images);
|
|
54
|
+
}
|
|
55
|
+
if (data?.type === "YA_IMAGE_CACHE_CLEAR") {
|
|
56
|
+
console.log("[ImageCache] Cache cleared by parent");
|
|
57
|
+
clearImageCache();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function initImageCacheListener() {
|
|
61
|
+
if (isListening) {
|
|
62
|
+
window.parent.postMessage({ type: "YA_IMAGE_CACHE_REQUEST" }, "*");
|
|
63
|
+
return () => {
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
window.addEventListener("message", handleMessage);
|
|
67
|
+
isListening = true;
|
|
68
|
+
window.parent.postMessage({ type: "YA_IMAGE_CACHE_REQUEST" }, "*");
|
|
69
|
+
return () => {
|
|
70
|
+
window.removeEventListener("message", handleMessage);
|
|
71
|
+
isListening = false;
|
|
72
|
+
clearImageCache();
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/components/ContentStoreProvider.tsx
|
|
77
|
+
import { jsx } from "react/jsx-runtime";
|
|
78
|
+
var ContentStoreContext = createContext(null);
|
|
79
|
+
function useContentStore() {
|
|
80
|
+
const context = useContext(ContentStoreContext);
|
|
81
|
+
if (!context) {
|
|
82
|
+
throw new Error("useContentStore must be used within a ContentStoreProvider");
|
|
83
|
+
}
|
|
84
|
+
return context;
|
|
85
|
+
}
|
|
86
|
+
function extractSessionId() {
|
|
87
|
+
const match = window.location.pathname.match(/\/session\/([^/]+)/);
|
|
88
|
+
return match ? match[1] : null;
|
|
89
|
+
}
|
|
90
|
+
function isBuilderPreview() {
|
|
91
|
+
return typeof window !== "undefined" && (window.__YOAMIGO_EDIT_MODE__ === true || import.meta.env?.YA_EDIT_MODE === "true");
|
|
92
|
+
}
|
|
93
|
+
function ContentStoreProvider({
|
|
94
|
+
children,
|
|
95
|
+
initialContent,
|
|
96
|
+
initialMode,
|
|
97
|
+
pages = []
|
|
98
|
+
}) {
|
|
99
|
+
const defaultContent = initialContent ?? getAllContent2();
|
|
100
|
+
const defaultMode = initialMode ?? (isBuilderPreview() ? "inline-edit" : "read-only");
|
|
101
|
+
const [content, setContent] = useState(
|
|
102
|
+
new Map(Object.entries(defaultContent))
|
|
103
|
+
);
|
|
104
|
+
const [mode, setModeState] = useState(defaultMode);
|
|
105
|
+
const [listeners, setListeners] = useState(/* @__PURE__ */ new Set());
|
|
106
|
+
const [activeFieldId, setActiveFieldId] = useState(null);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const handleContentUpdate = () => {
|
|
109
|
+
const newContent = getAllContent2();
|
|
110
|
+
setContent(new Map(Object.entries(newContent)));
|
|
111
|
+
};
|
|
112
|
+
window.addEventListener("content-updated", handleContentUpdate);
|
|
113
|
+
return () => window.removeEventListener("content-updated", handleContentUpdate);
|
|
114
|
+
}, []);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (mode !== "inline-edit") return;
|
|
117
|
+
return initImageCacheListener();
|
|
118
|
+
}, [mode]);
|
|
119
|
+
const activeCallbacksRef = useRef(null);
|
|
120
|
+
const notifyListeners = useCallback(() => {
|
|
121
|
+
listeners.forEach((listener) => listener());
|
|
122
|
+
}, [listeners]);
|
|
123
|
+
const getValue = useCallback(
|
|
124
|
+
(fieldId) => {
|
|
125
|
+
return content.get(fieldId) ?? "";
|
|
126
|
+
},
|
|
127
|
+
[content]
|
|
128
|
+
);
|
|
129
|
+
const setValue = useCallback(
|
|
130
|
+
(fieldId, value2) => {
|
|
131
|
+
setContent((prev) => {
|
|
132
|
+
const next = new Map(prev);
|
|
133
|
+
next.set(fieldId, value2);
|
|
134
|
+
return next;
|
|
135
|
+
});
|
|
136
|
+
notifyListeners();
|
|
137
|
+
},
|
|
138
|
+
[notifyListeners]
|
|
139
|
+
);
|
|
140
|
+
const setMode = useCallback(
|
|
141
|
+
(newMode) => {
|
|
142
|
+
setModeState(newMode);
|
|
143
|
+
notifyListeners();
|
|
144
|
+
},
|
|
145
|
+
[notifyListeners]
|
|
146
|
+
);
|
|
147
|
+
const subscribe = useCallback((listener) => {
|
|
148
|
+
setListeners((prev) => {
|
|
149
|
+
const next = new Set(prev);
|
|
150
|
+
next.add(listener);
|
|
151
|
+
return next;
|
|
152
|
+
});
|
|
153
|
+
return () => {
|
|
154
|
+
setListeners((prev) => {
|
|
155
|
+
const next = new Set(prev);
|
|
156
|
+
next.delete(listener);
|
|
157
|
+
return next;
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
}, []);
|
|
161
|
+
const clearActiveField = useCallback(() => {
|
|
162
|
+
if (activeCallbacksRef.current) {
|
|
163
|
+
activeCallbacksRef.current.close();
|
|
164
|
+
}
|
|
165
|
+
activeCallbacksRef.current = null;
|
|
166
|
+
setActiveFieldId(null);
|
|
167
|
+
}, []);
|
|
168
|
+
const setActiveField = useCallback((fieldId, callbacks) => {
|
|
169
|
+
if (activeFieldId && activeFieldId !== fieldId && activeCallbacksRef.current) {
|
|
170
|
+
activeCallbacksRef.current.close();
|
|
171
|
+
}
|
|
172
|
+
activeCallbacksRef.current = callbacks;
|
|
173
|
+
setActiveFieldId(fieldId);
|
|
174
|
+
}, [activeFieldId]);
|
|
175
|
+
const saveToWorker = useCallback(
|
|
176
|
+
async (fieldId, value2) => {
|
|
177
|
+
const sessionId = extractSessionId();
|
|
178
|
+
if (!sessionId) {
|
|
179
|
+
console.warn("saveToWorker: Not in builder preview mode, skipping");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetch(`/api/session/${sessionId}/inline-edit`, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: { "Content-Type": "application/json" },
|
|
186
|
+
credentials: "include",
|
|
187
|
+
body: JSON.stringify({ fieldId, value: value2 })
|
|
188
|
+
});
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
const error = await response.json();
|
|
191
|
+
console.error("saveToWorker failed:", error);
|
|
192
|
+
throw new Error(error.error || "Failed to save");
|
|
193
|
+
}
|
|
194
|
+
console.log(`saveToWorker: Saved ${fieldId}`);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error("saveToWorker error:", error);
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
[]
|
|
201
|
+
);
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (!activeFieldId) return;
|
|
204
|
+
const handleClickOutside = (event) => {
|
|
205
|
+
const target = event.target;
|
|
206
|
+
const isInsideYaText = target.closest("[data-field-id]");
|
|
207
|
+
const isInsideBubbleMenu = target.closest(".ya-bubble-menu");
|
|
208
|
+
const isInsideActions = target.closest(".ya-text-actions");
|
|
209
|
+
if (!isInsideYaText && !isInsideBubbleMenu && !isInsideActions) {
|
|
210
|
+
if (activeCallbacksRef.current) {
|
|
211
|
+
activeCallbacksRef.current.close();
|
|
212
|
+
}
|
|
213
|
+
activeCallbacksRef.current = null;
|
|
214
|
+
setActiveFieldId(null);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
218
|
+
return () => {
|
|
219
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
220
|
+
};
|
|
221
|
+
}, [activeFieldId]);
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
;
|
|
224
|
+
window.mpContentStore = {
|
|
225
|
+
getValue,
|
|
226
|
+
setValue,
|
|
227
|
+
getMode: () => mode,
|
|
228
|
+
setMode,
|
|
229
|
+
subscribe,
|
|
230
|
+
saveToWorker
|
|
231
|
+
};
|
|
232
|
+
return () => {
|
|
233
|
+
delete window.mpContentStore;
|
|
234
|
+
};
|
|
235
|
+
}, [getValue, setValue, mode, setMode, subscribe, saveToWorker]);
|
|
236
|
+
const getPages = useCallback(() => pages, [pages]);
|
|
237
|
+
const value = {
|
|
238
|
+
getValue,
|
|
239
|
+
setValue,
|
|
240
|
+
mode,
|
|
241
|
+
setMode,
|
|
242
|
+
subscribe,
|
|
243
|
+
saveToWorker,
|
|
244
|
+
activeFieldId,
|
|
245
|
+
setActiveField,
|
|
246
|
+
clearActiveField,
|
|
247
|
+
getPages
|
|
248
|
+
};
|
|
249
|
+
return /* @__PURE__ */ jsx(ContentStoreContext.Provider, { value, children });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/components/ContentStoreProvider.prod.tsx
|
|
253
|
+
import { createContext as createContext2, useContext as useContext2 } from "react";
|
|
254
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
255
|
+
var contentMap2 = new Map(Object.entries(getAllContent()));
|
|
256
|
+
var staticStore = {
|
|
257
|
+
getValue: (fieldId) => contentMap2.get(fieldId) ?? "",
|
|
258
|
+
setValue: () => {
|
|
259
|
+
},
|
|
260
|
+
// No-op in production
|
|
261
|
+
mode: "read-only",
|
|
262
|
+
setMode: () => {
|
|
263
|
+
},
|
|
264
|
+
// No-op in production
|
|
265
|
+
subscribe: () => () => {
|
|
266
|
+
},
|
|
267
|
+
// No-op, return empty unsubscribe
|
|
268
|
+
saveToWorker: void 0
|
|
269
|
+
};
|
|
270
|
+
var ContentStoreContext2 = createContext2(staticStore);
|
|
271
|
+
function useContentStore2() {
|
|
272
|
+
return useContext2(ContentStoreContext2);
|
|
273
|
+
}
|
|
274
|
+
function ContentStoreProvider2({ children }) {
|
|
275
|
+
return /* @__PURE__ */ jsx2(ContentStoreContext2.Provider, { value: staticStore, children });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/components/YaText.tsx
|
|
279
|
+
import { useEffect as useEffect3, useRef as useRef3, useState as useState3, useCallback as useCallback3 } from "react";
|
|
280
|
+
import { createPortal as createPortal2 } from "react-dom";
|
|
281
|
+
import { useEditor, EditorContent } from "@tiptap/react";
|
|
282
|
+
import { BubbleMenu } from "@tiptap/react/menus";
|
|
283
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
284
|
+
import Link from "@tiptap/extension-link";
|
|
285
|
+
import { TextStyle } from "@tiptap/extension-text-style";
|
|
286
|
+
import { Extension } from "@tiptap/core";
|
|
287
|
+
|
|
288
|
+
// src/components/SafeHtml.tsx
|
|
289
|
+
import { useEffect as useEffect2, useRef as useRef2, useState as useState2, useCallback as useCallback2 } from "react";
|
|
290
|
+
import { createPortal } from "react-dom";
|
|
291
|
+
import DOMPurify from "dompurify";
|
|
292
|
+
import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
293
|
+
var ALLOWED_TAGS = ["strong", "em", "a", "span", "br", "b", "i", "u"];
|
|
294
|
+
var ALLOWED_ATTR = ["href", "style", "class", "target", "rel"];
|
|
295
|
+
DOMPurify.addHook("uponSanitizeAttribute", (_node, data) => {
|
|
296
|
+
if (data.attrName === "style") {
|
|
297
|
+
const allowedStyles = ["font-size", "font-weight"];
|
|
298
|
+
const styleValue = data.attrValue;
|
|
299
|
+
const filteredStyles = styleValue.split(";").map((s) => s.trim()).filter((s) => {
|
|
300
|
+
const [prop] = s.split(":").map((p) => p.trim());
|
|
301
|
+
return allowedStyles.includes(prop);
|
|
302
|
+
}).join("; ");
|
|
303
|
+
data.attrValue = filteredStyles;
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
function extractLinkDisplayText(href) {
|
|
307
|
+
if (!href) return { text: "link", isExternal: false };
|
|
308
|
+
if (href.startsWith("#")) {
|
|
309
|
+
const section = href.slice(1).replace(/-/g, " ");
|
|
310
|
+
return { text: section || "section", isExternal: false };
|
|
311
|
+
}
|
|
312
|
+
if (href.startsWith("/")) {
|
|
313
|
+
const segments = href.split("/").filter(Boolean);
|
|
314
|
+
const lastSegment = segments[segments.length - 1] || "home";
|
|
315
|
+
const text = lastSegment.replace(/-/g, " ").replace(/^\w/, (c) => c.toUpperCase());
|
|
316
|
+
return { text, isExternal: false };
|
|
317
|
+
}
|
|
318
|
+
if (href.startsWith("mailto:")) {
|
|
319
|
+
return { text: "email", isExternal: true };
|
|
320
|
+
}
|
|
321
|
+
if (href.startsWith("tel:")) {
|
|
322
|
+
return { text: "phone", isExternal: true };
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const url = new URL(href);
|
|
326
|
+
return {
|
|
327
|
+
text: url.hostname.replace(/^www\./, ""),
|
|
328
|
+
isExternal: true
|
|
329
|
+
};
|
|
330
|
+
} catch {
|
|
331
|
+
return { text: href.slice(0, 20), isExternal: true };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function LinkIcon() {
|
|
335
|
+
return /* @__PURE__ */ jsxs(
|
|
336
|
+
"svg",
|
|
337
|
+
{
|
|
338
|
+
className: "mp-link-popover-icon",
|
|
339
|
+
viewBox: "0 0 24 24",
|
|
340
|
+
fill: "none",
|
|
341
|
+
stroke: "currentColor",
|
|
342
|
+
strokeWidth: "2",
|
|
343
|
+
strokeLinecap: "round",
|
|
344
|
+
strokeLinejoin: "round",
|
|
345
|
+
children: [
|
|
346
|
+
/* @__PURE__ */ jsx3("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }),
|
|
347
|
+
/* @__PURE__ */ jsx3("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
function LinkPopover({
|
|
353
|
+
displayText,
|
|
354
|
+
position,
|
|
355
|
+
onMouseEnter,
|
|
356
|
+
onMouseLeave,
|
|
357
|
+
onClick
|
|
358
|
+
}) {
|
|
359
|
+
return /* @__PURE__ */ jsxs(
|
|
360
|
+
"div",
|
|
361
|
+
{
|
|
362
|
+
className: "mp-link-popover",
|
|
363
|
+
style: {
|
|
364
|
+
top: position.top,
|
|
365
|
+
left: position.left
|
|
366
|
+
},
|
|
367
|
+
onMouseEnter,
|
|
368
|
+
onMouseLeave,
|
|
369
|
+
onClick,
|
|
370
|
+
role: "button",
|
|
371
|
+
tabIndex: 0,
|
|
372
|
+
onKeyDown: (e) => {
|
|
373
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
374
|
+
e.preventDefault();
|
|
375
|
+
onClick();
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
children: [
|
|
379
|
+
/* @__PURE__ */ jsx3(LinkIcon, {}),
|
|
380
|
+
/* @__PURE__ */ jsxs("span", { className: "mp-link-popover-text", children: [
|
|
381
|
+
/* @__PURE__ */ jsx3("span", { className: "mp-link-popover-prefix", children: "Go to " }),
|
|
382
|
+
/* @__PURE__ */ jsx3("span", { className: "mp-link-popover-name", children: displayText })
|
|
383
|
+
] })
|
|
384
|
+
]
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
function SafeHtml({ content, className, mode = "read-only" }) {
|
|
389
|
+
const containerRef = useRef2(null);
|
|
390
|
+
const showTimerRef = useRef2(void 0);
|
|
391
|
+
const hideTimerRef = useRef2(void 0);
|
|
392
|
+
const [popoverState, setPopoverState] = useState2({
|
|
393
|
+
isVisible: false,
|
|
394
|
+
href: "",
|
|
395
|
+
displayText: "",
|
|
396
|
+
isExternal: false,
|
|
397
|
+
position: { top: 0, left: 0 }
|
|
398
|
+
});
|
|
399
|
+
const sanitized = DOMPurify.sanitize(content, {
|
|
400
|
+
ALLOWED_TAGS,
|
|
401
|
+
ALLOWED_ATTR
|
|
402
|
+
});
|
|
403
|
+
const hidePopover = useCallback2(() => {
|
|
404
|
+
clearTimeout(showTimerRef.current);
|
|
405
|
+
setPopoverState((prev) => ({ ...prev, isVisible: false }));
|
|
406
|
+
}, []);
|
|
407
|
+
const scheduleHide = useCallback2(() => {
|
|
408
|
+
clearTimeout(showTimerRef.current);
|
|
409
|
+
hideTimerRef.current = window.setTimeout(() => {
|
|
410
|
+
hidePopover();
|
|
411
|
+
}, 100);
|
|
412
|
+
}, [hidePopover]);
|
|
413
|
+
const cancelHide = useCallback2(() => {
|
|
414
|
+
clearTimeout(hideTimerRef.current);
|
|
415
|
+
}, []);
|
|
416
|
+
const handlePopoverClick = useCallback2(() => {
|
|
417
|
+
if (popoverState.isExternal) {
|
|
418
|
+
window.open(popoverState.href, "_blank", "noopener,noreferrer");
|
|
419
|
+
} else {
|
|
420
|
+
window.location.href = popoverState.href;
|
|
421
|
+
}
|
|
422
|
+
hidePopover();
|
|
423
|
+
}, [popoverState.href, popoverState.isExternal, hidePopover]);
|
|
424
|
+
useEffect2(() => {
|
|
425
|
+
if (mode !== "inline-edit" || !containerRef.current) return;
|
|
426
|
+
const container = containerRef.current;
|
|
427
|
+
const handleMouseOver = (e) => {
|
|
428
|
+
const link = e.target.closest("a");
|
|
429
|
+
if (!link) return;
|
|
430
|
+
if (e.target.closest(".mp-text-editing, .mp-text-editable")) return;
|
|
431
|
+
const selectModeEnabled = window.__builderSelectModeEnabled;
|
|
432
|
+
if (selectModeEnabled) return;
|
|
433
|
+
clearTimeout(hideTimerRef.current);
|
|
434
|
+
showTimerRef.current = window.setTimeout(() => {
|
|
435
|
+
const href = link.getAttribute("href") || "";
|
|
436
|
+
const { text, isExternal } = extractLinkDisplayText(href);
|
|
437
|
+
const rect = link.getBoundingClientRect();
|
|
438
|
+
const top = rect.bottom + window.scrollY + 8;
|
|
439
|
+
const left = rect.left + rect.width / 2 + window.scrollX;
|
|
440
|
+
setPopoverState({
|
|
441
|
+
isVisible: true,
|
|
442
|
+
href,
|
|
443
|
+
displayText: text,
|
|
444
|
+
isExternal,
|
|
445
|
+
position: { top, left }
|
|
446
|
+
});
|
|
447
|
+
}, 150);
|
|
448
|
+
};
|
|
449
|
+
const handleMouseOut = (e) => {
|
|
450
|
+
const link = e.target.closest("a");
|
|
451
|
+
if (!link) return;
|
|
452
|
+
scheduleHide();
|
|
453
|
+
};
|
|
454
|
+
const handleClick = (e) => {
|
|
455
|
+
if (e.target.closest(".mp-text-editing, .mp-text-editable")) return;
|
|
456
|
+
const yaLink = e.target.closest(".ya-link-editable, .ya-link-editing");
|
|
457
|
+
if (yaLink) {
|
|
458
|
+
if (yaLink.classList.contains("ya-link-editing")) {
|
|
459
|
+
e.preventDefault();
|
|
460
|
+
e.stopPropagation();
|
|
461
|
+
}
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const link = e.target.closest("a");
|
|
465
|
+
if (link) {
|
|
466
|
+
e.preventDefault();
|
|
467
|
+
e.stopPropagation();
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
container.addEventListener("mouseover", handleMouseOver);
|
|
471
|
+
container.addEventListener("mouseout", handleMouseOut);
|
|
472
|
+
container.addEventListener("click", handleClick, true);
|
|
473
|
+
return () => {
|
|
474
|
+
container.removeEventListener("mouseover", handleMouseOver);
|
|
475
|
+
container.removeEventListener("mouseout", handleMouseOut);
|
|
476
|
+
container.removeEventListener("click", handleClick, true);
|
|
477
|
+
clearTimeout(showTimerRef.current);
|
|
478
|
+
clearTimeout(hideTimerRef.current);
|
|
479
|
+
};
|
|
480
|
+
}, [mode, scheduleHide]);
|
|
481
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
482
|
+
/* @__PURE__ */ jsx3(
|
|
483
|
+
"span",
|
|
484
|
+
{
|
|
485
|
+
ref: containerRef,
|
|
486
|
+
className,
|
|
487
|
+
dangerouslySetInnerHTML: { __html: sanitized }
|
|
488
|
+
}
|
|
489
|
+
),
|
|
490
|
+
mode === "inline-edit" && popoverState.isVisible && createPortal(
|
|
491
|
+
/* @__PURE__ */ jsx3(
|
|
492
|
+
LinkPopover,
|
|
493
|
+
{
|
|
494
|
+
displayText: popoverState.displayText,
|
|
495
|
+
position: popoverState.position,
|
|
496
|
+
onMouseEnter: cancelHide,
|
|
497
|
+
onMouseLeave: hidePopover,
|
|
498
|
+
onClick: handlePopoverClick
|
|
499
|
+
}
|
|
500
|
+
),
|
|
501
|
+
document.body
|
|
502
|
+
)
|
|
503
|
+
] });
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/components/YaText.tsx
|
|
507
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
508
|
+
var FontSize = Extension.create({
|
|
509
|
+
name: "fontSize",
|
|
510
|
+
addOptions() {
|
|
511
|
+
return {
|
|
512
|
+
types: ["textStyle"]
|
|
513
|
+
};
|
|
514
|
+
},
|
|
515
|
+
addGlobalAttributes() {
|
|
516
|
+
return [
|
|
517
|
+
{
|
|
518
|
+
types: this.options.types,
|
|
519
|
+
attributes: {
|
|
520
|
+
fontSize: {
|
|
521
|
+
default: null,
|
|
522
|
+
parseHTML: (element) => element.style.fontSize || null,
|
|
523
|
+
renderHTML: (attributes) => {
|
|
524
|
+
if (!attributes.fontSize) {
|
|
525
|
+
return {};
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
style: `font-size: ${attributes.fontSize}`
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
];
|
|
535
|
+
},
|
|
536
|
+
addCommands() {
|
|
537
|
+
return {
|
|
538
|
+
setFontSize: (fontSize) => ({ chain }) => {
|
|
539
|
+
return chain().setMark("textStyle", { fontSize }).run();
|
|
540
|
+
},
|
|
541
|
+
unsetFontSize: () => ({ chain }) => {
|
|
542
|
+
return chain().setMark("textStyle", { fontSize: null }).removeEmptyTextStyle().run();
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
var FontWeight = Extension.create({
|
|
548
|
+
name: "fontWeight",
|
|
549
|
+
addOptions() {
|
|
550
|
+
return {
|
|
551
|
+
types: ["textStyle"]
|
|
552
|
+
};
|
|
553
|
+
},
|
|
554
|
+
addGlobalAttributes() {
|
|
555
|
+
return [
|
|
556
|
+
{
|
|
557
|
+
types: this.options.types,
|
|
558
|
+
attributes: {
|
|
559
|
+
fontWeight: {
|
|
560
|
+
default: null,
|
|
561
|
+
parseHTML: (element) => element.style.fontWeight || null,
|
|
562
|
+
renderHTML: (attributes) => {
|
|
563
|
+
if (!attributes.fontWeight) {
|
|
564
|
+
return {};
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
style: `font-weight: ${attributes.fontWeight}`
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
];
|
|
574
|
+
},
|
|
575
|
+
addCommands() {
|
|
576
|
+
return {
|
|
577
|
+
setFontWeight: (fontWeight) => ({ chain }) => {
|
|
578
|
+
return chain().setMark("textStyle", { fontWeight }).run();
|
|
579
|
+
},
|
|
580
|
+
unsetFontWeight: () => ({ chain }) => {
|
|
581
|
+
return chain().setMark("textStyle", { fontWeight: null }).removeEmptyTextStyle().run();
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
var SIZE_PRESETS = {
|
|
587
|
+
S: "14px",
|
|
588
|
+
M: "16px",
|
|
589
|
+
L: "20px",
|
|
590
|
+
XL: "24px"
|
|
591
|
+
};
|
|
592
|
+
var WEIGHT_PRESETS = {
|
|
593
|
+
Regular: "400",
|
|
594
|
+
"Semi-bold": "600",
|
|
595
|
+
Bold: "700",
|
|
596
|
+
"Extra-bold": "800"
|
|
597
|
+
};
|
|
598
|
+
function YaText({ fieldId, className, as: Component = "span", children }) {
|
|
599
|
+
const { getValue, setValue, mode, saveToWorker, activeFieldId, setActiveField } = useContentStore();
|
|
600
|
+
const storeContent = getValue(fieldId);
|
|
601
|
+
const content = storeContent || (typeof children === "string" ? children : "");
|
|
602
|
+
const [isEditing, setIsEditing] = useState3(false);
|
|
603
|
+
const [originalContent, setOriginalContent] = useState3(content);
|
|
604
|
+
const containerRef = useRef3(null);
|
|
605
|
+
const originalContentRef = useRef3(content);
|
|
606
|
+
const editor = useEditor({
|
|
607
|
+
extensions: [
|
|
608
|
+
StarterKit.configure({
|
|
609
|
+
// Disable block-level features for inline editing
|
|
610
|
+
heading: false,
|
|
611
|
+
bulletList: false,
|
|
612
|
+
orderedList: false,
|
|
613
|
+
blockquote: false,
|
|
614
|
+
codeBlock: false,
|
|
615
|
+
horizontalRule: false,
|
|
616
|
+
// Disable Link since we configure it separately below
|
|
617
|
+
link: false
|
|
618
|
+
}),
|
|
619
|
+
Link.configure({
|
|
620
|
+
openOnClick: false,
|
|
621
|
+
HTMLAttributes: {
|
|
622
|
+
class: "text-[var(--color-primary)] underline"
|
|
623
|
+
}
|
|
624
|
+
}),
|
|
625
|
+
TextStyle,
|
|
626
|
+
FontSize,
|
|
627
|
+
FontWeight
|
|
628
|
+
],
|
|
629
|
+
content,
|
|
630
|
+
// Content is now HTML, no conversion needed
|
|
631
|
+
editable: true,
|
|
632
|
+
editorProps: {
|
|
633
|
+
attributes: {
|
|
634
|
+
class: "outline-none"
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
parseOptions: { preserveWhitespace: "full" }
|
|
638
|
+
});
|
|
639
|
+
useEffect3(() => {
|
|
640
|
+
if (editor && !isEditing) {
|
|
641
|
+
if (editor.getHTML() !== content) {
|
|
642
|
+
editor.commands.setContent(content, { parseOptions: { preserveWhitespace: "full" } });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}, [content, editor, isEditing]);
|
|
646
|
+
useEffect3(() => {
|
|
647
|
+
if (isEditing && activeFieldId !== null && activeFieldId !== fieldId) {
|
|
648
|
+
setIsEditing(false);
|
|
649
|
+
}
|
|
650
|
+
}, [activeFieldId, fieldId, isEditing]);
|
|
651
|
+
const handleSave = useCallback3(() => {
|
|
652
|
+
if (!editor) return;
|
|
653
|
+
let html = editor.getHTML();
|
|
654
|
+
html = html.replace(/<\/p><p>/g, "<br><br>").replace(/^<p>/, "").replace(/<\/p>$/, "");
|
|
655
|
+
setValue(fieldId, html);
|
|
656
|
+
saveToWorker?.(fieldId, html);
|
|
657
|
+
setIsEditing(false);
|
|
658
|
+
}, [editor, fieldId, setValue, saveToWorker]);
|
|
659
|
+
const handleCancel = useCallback3(() => {
|
|
660
|
+
if (editor) {
|
|
661
|
+
editor.commands.setContent(originalContent, { parseOptions: { preserveWhitespace: "full" } });
|
|
662
|
+
}
|
|
663
|
+
setIsEditing(false);
|
|
664
|
+
}, [editor, originalContent]);
|
|
665
|
+
const handleClose = useCallback3(() => {
|
|
666
|
+
if (!editor) {
|
|
667
|
+
setIsEditing(false);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
let currentHtml = editor.getHTML();
|
|
671
|
+
currentHtml = currentHtml.replace(/<\/p><p>/g, "<br><br>").replace(/^<p>/, "").replace(/<\/p>$/, "");
|
|
672
|
+
if (currentHtml !== originalContentRef.current) {
|
|
673
|
+
setValue(fieldId, currentHtml);
|
|
674
|
+
saveToWorker?.(fieldId, currentHtml);
|
|
675
|
+
}
|
|
676
|
+
setIsEditing(false);
|
|
677
|
+
}, [editor, fieldId, setValue, saveToWorker]);
|
|
678
|
+
const handleClick = useCallback3((e) => {
|
|
679
|
+
if (isEditing) {
|
|
680
|
+
e.preventDefault();
|
|
681
|
+
e.stopPropagation();
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const selectModeEnabled = window.__builderSelectModeEnabled;
|
|
685
|
+
if (selectModeEnabled) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const isInsideLink = e.currentTarget.closest("a") !== null;
|
|
689
|
+
if (isInsideLink) {
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (mode === "inline-edit" && !isEditing) {
|
|
693
|
+
e.preventDefault();
|
|
694
|
+
e.stopPropagation();
|
|
695
|
+
setOriginalContent(content);
|
|
696
|
+
originalContentRef.current = content;
|
|
697
|
+
setActiveField(fieldId, { close: handleClose });
|
|
698
|
+
setIsEditing(true);
|
|
699
|
+
setTimeout(() => {
|
|
700
|
+
editor?.chain().focus().selectAll().run();
|
|
701
|
+
}, 20);
|
|
702
|
+
}
|
|
703
|
+
}, [mode, isEditing, content, editor, fieldId, setActiveField, handleClose]);
|
|
704
|
+
const handleKeyDown = useCallback3(
|
|
705
|
+
(event) => {
|
|
706
|
+
if (!isEditing) return;
|
|
707
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
708
|
+
event.preventDefault();
|
|
709
|
+
handleSave();
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (event.key === "Escape") {
|
|
713
|
+
event.preventDefault();
|
|
714
|
+
handleCancel();
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "s") {
|
|
718
|
+
event.preventDefault();
|
|
719
|
+
handleSave();
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
[isEditing, handleSave, handleCancel]
|
|
724
|
+
);
|
|
725
|
+
const handleLink = useCallback3(() => {
|
|
726
|
+
if (!editor) return;
|
|
727
|
+
const previousUrl = editor.getAttributes("link").href;
|
|
728
|
+
const url = window.prompt("Enter URL:", previousUrl || "https://");
|
|
729
|
+
if (url === null) return;
|
|
730
|
+
if (url === "") {
|
|
731
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
732
|
+
} else {
|
|
733
|
+
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
|
734
|
+
}
|
|
735
|
+
}, [editor]);
|
|
736
|
+
const handleFontSizeChange = useCallback3(
|
|
737
|
+
(e) => {
|
|
738
|
+
if (!editor) return;
|
|
739
|
+
const size = e.target.value;
|
|
740
|
+
if (size === "") {
|
|
741
|
+
editor.chain().focus().unsetFontSize().run();
|
|
742
|
+
} else {
|
|
743
|
+
editor.chain().focus().setFontSize(size).run();
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
[editor]
|
|
747
|
+
);
|
|
748
|
+
const handleFontWeightChange = useCallback3(
|
|
749
|
+
(e) => {
|
|
750
|
+
if (!editor) return;
|
|
751
|
+
const weight = e.target.value;
|
|
752
|
+
if (weight === "") {
|
|
753
|
+
editor.chain().focus().unsetFontWeight().run();
|
|
754
|
+
} else {
|
|
755
|
+
editor.chain().focus().setFontWeight(weight).run();
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
[editor]
|
|
759
|
+
);
|
|
760
|
+
const getCurrentFontSize = () => {
|
|
761
|
+
if (!editor) return "";
|
|
762
|
+
const attrs = editor.getAttributes("textStyle");
|
|
763
|
+
return attrs.fontSize || "";
|
|
764
|
+
};
|
|
765
|
+
const getCurrentFontWeight = () => {
|
|
766
|
+
if (!editor) return "";
|
|
767
|
+
const attrs = editor.getAttributes("textStyle");
|
|
768
|
+
return attrs.fontWeight || "";
|
|
769
|
+
};
|
|
770
|
+
if (mode === "read-only") {
|
|
771
|
+
return /* @__PURE__ */ jsx4(
|
|
772
|
+
Component,
|
|
773
|
+
{
|
|
774
|
+
ref: containerRef,
|
|
775
|
+
className,
|
|
776
|
+
"data-ya-restricted": "true",
|
|
777
|
+
"data-field-id": fieldId,
|
|
778
|
+
children: /* @__PURE__ */ jsx4(SafeHtml, { content, mode })
|
|
779
|
+
}
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
return /* @__PURE__ */ jsx4(
|
|
783
|
+
Component,
|
|
784
|
+
{
|
|
785
|
+
ref: containerRef,
|
|
786
|
+
className: `${className || ""} ${isEditing ? "ya-text-editing" : "ya-text-editable"}`,
|
|
787
|
+
"data-ya-restricted": "true",
|
|
788
|
+
"data-field-id": fieldId,
|
|
789
|
+
onClick: handleClick,
|
|
790
|
+
onKeyDown: handleKeyDown,
|
|
791
|
+
children: editor ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
792
|
+
createPortal2(
|
|
793
|
+
/* @__PURE__ */ jsxs2(
|
|
794
|
+
BubbleMenu,
|
|
795
|
+
{
|
|
796
|
+
editor,
|
|
797
|
+
shouldShow: () => isEditing,
|
|
798
|
+
options: { offset: 6, placement: "top" },
|
|
799
|
+
className: "ya-bubble-menu",
|
|
800
|
+
children: [
|
|
801
|
+
/* @__PURE__ */ jsx4(
|
|
802
|
+
"button",
|
|
803
|
+
{
|
|
804
|
+
type: "button",
|
|
805
|
+
onClick: () => editor.chain().focus().toggleBold().run(),
|
|
806
|
+
className: `ya-bubble-btn ${editor.isActive("bold") ? "is-active" : ""}`,
|
|
807
|
+
title: "Bold",
|
|
808
|
+
children: /* @__PURE__ */ jsx4("strong", { children: "B" })
|
|
809
|
+
}
|
|
810
|
+
),
|
|
811
|
+
/* @__PURE__ */ jsx4(
|
|
812
|
+
"button",
|
|
813
|
+
{
|
|
814
|
+
type: "button",
|
|
815
|
+
onClick: () => editor.chain().focus().toggleItalic().run(),
|
|
816
|
+
className: `ya-bubble-btn ${editor.isActive("italic") ? "is-active" : ""}`,
|
|
817
|
+
title: "Italic",
|
|
818
|
+
children: /* @__PURE__ */ jsx4("em", { children: "I" })
|
|
819
|
+
}
|
|
820
|
+
),
|
|
821
|
+
/* @__PURE__ */ jsx4(
|
|
822
|
+
"button",
|
|
823
|
+
{
|
|
824
|
+
type: "button",
|
|
825
|
+
onClick: handleLink,
|
|
826
|
+
className: `ya-bubble-btn ${editor.isActive("link") ? "is-active" : ""}`,
|
|
827
|
+
title: "Link",
|
|
828
|
+
children: /* @__PURE__ */ jsx4("span", { children: "\u{1F517}" })
|
|
829
|
+
}
|
|
830
|
+
),
|
|
831
|
+
/* @__PURE__ */ jsx4("span", { className: "ya-bubble-divider" }),
|
|
832
|
+
/* @__PURE__ */ jsxs2(
|
|
833
|
+
"select",
|
|
834
|
+
{
|
|
835
|
+
value: getCurrentFontSize(),
|
|
836
|
+
onChange: handleFontSizeChange,
|
|
837
|
+
className: "ya-bubble-select",
|
|
838
|
+
title: "Font Size",
|
|
839
|
+
children: [
|
|
840
|
+
/* @__PURE__ */ jsx4("option", { value: "", children: "Size" }),
|
|
841
|
+
Object.entries(SIZE_PRESETS).map(([name, size]) => /* @__PURE__ */ jsx4("option", { value: size, children: name }, name))
|
|
842
|
+
]
|
|
843
|
+
}
|
|
844
|
+
),
|
|
845
|
+
/* @__PURE__ */ jsxs2(
|
|
846
|
+
"select",
|
|
847
|
+
{
|
|
848
|
+
value: getCurrentFontWeight(),
|
|
849
|
+
onChange: handleFontWeightChange,
|
|
850
|
+
className: "ya-bubble-select",
|
|
851
|
+
title: "Font Weight",
|
|
852
|
+
children: [
|
|
853
|
+
/* @__PURE__ */ jsx4("option", { value: "", children: "Weight" }),
|
|
854
|
+
Object.entries(WEIGHT_PRESETS).map(([name, weight]) => /* @__PURE__ */ jsx4("option", { value: weight, children: name }, name))
|
|
855
|
+
]
|
|
856
|
+
}
|
|
857
|
+
)
|
|
858
|
+
]
|
|
859
|
+
}
|
|
860
|
+
),
|
|
861
|
+
document.body
|
|
862
|
+
),
|
|
863
|
+
isEditing ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
864
|
+
/* @__PURE__ */ jsx4(EditorContent, { editor }),
|
|
865
|
+
/* @__PURE__ */ jsxs2("div", { className: "ya-text-actions", children: [
|
|
866
|
+
/* @__PURE__ */ jsx4(
|
|
867
|
+
"button",
|
|
868
|
+
{
|
|
869
|
+
type: "button",
|
|
870
|
+
onClick: handleCancel,
|
|
871
|
+
className: "ya-text-btn ya-text-btn-cancel",
|
|
872
|
+
children: "Cancel"
|
|
873
|
+
}
|
|
874
|
+
),
|
|
875
|
+
/* @__PURE__ */ jsx4(
|
|
876
|
+
"button",
|
|
877
|
+
{
|
|
878
|
+
type: "button",
|
|
879
|
+
onClick: handleSave,
|
|
880
|
+
className: "ya-text-btn ya-text-btn-save",
|
|
881
|
+
children: "Save"
|
|
882
|
+
}
|
|
883
|
+
)
|
|
884
|
+
] })
|
|
885
|
+
] }) : /* @__PURE__ */ jsx4(SafeHtml, { content, mode })
|
|
886
|
+
] }) : /* @__PURE__ */ jsx4(SafeHtml, { content, mode })
|
|
887
|
+
}
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/components/YaImage.tsx
|
|
892
|
+
import { useCallback as useCallback5, useEffect as useEffect5, useRef as useRef5, useState as useState5 } from "react";
|
|
893
|
+
|
|
894
|
+
// src/lib/asset-resolver.ts
|
|
895
|
+
var assetResolver = (path) => path;
|
|
896
|
+
function setAssetResolver(resolver) {
|
|
897
|
+
assetResolver = resolver;
|
|
898
|
+
}
|
|
899
|
+
function resolveAssetUrl(path) {
|
|
900
|
+
if (!path) return "";
|
|
901
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
902
|
+
return path;
|
|
903
|
+
}
|
|
904
|
+
return assetResolver(path);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// src/components/YaTooltip.tsx
|
|
908
|
+
import { useEffect as useEffect4, useRef as useRef4, useState as useState4, useCallback as useCallback4 } from "react";
|
|
909
|
+
import { createPortal as createPortal3 } from "react-dom";
|
|
910
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
911
|
+
function YaTooltip({
|
|
912
|
+
anchorRef,
|
|
913
|
+
children,
|
|
914
|
+
show,
|
|
915
|
+
preferredPosition = "bottom"
|
|
916
|
+
}) {
|
|
917
|
+
const [position, setPosition] = useState4(preferredPosition);
|
|
918
|
+
const [coords, setCoords] = useState4({ top: 0, left: 0 });
|
|
919
|
+
const [isPositioned, setIsPositioned] = useState4(false);
|
|
920
|
+
const tooltipRef = useRef4(null);
|
|
921
|
+
const calculatePosition = useCallback4(() => {
|
|
922
|
+
if (!anchorRef.current) return;
|
|
923
|
+
const anchor = anchorRef.current.getBoundingClientRect();
|
|
924
|
+
const tooltip = tooltipRef.current?.getBoundingClientRect();
|
|
925
|
+
const tooltipWidth = tooltip?.width || 120;
|
|
926
|
+
const tooltipHeight = tooltip?.height || 40;
|
|
927
|
+
const padding = 8;
|
|
928
|
+
const edgeMargin = 10;
|
|
929
|
+
const spaceBelow = window.innerHeight - anchor.bottom - edgeMargin;
|
|
930
|
+
const spaceAbove = anchor.top - edgeMargin;
|
|
931
|
+
const spaceRight = window.innerWidth - anchor.right - edgeMargin;
|
|
932
|
+
const spaceLeft = anchor.left - edgeMargin;
|
|
933
|
+
let pos = preferredPosition;
|
|
934
|
+
let top = 0;
|
|
935
|
+
let left = 0;
|
|
936
|
+
const positions = [preferredPosition, "bottom", "top", "right", "left"];
|
|
937
|
+
const uniquePositions = [...new Set(positions)];
|
|
938
|
+
for (const tryPos of uniquePositions) {
|
|
939
|
+
let fits = false;
|
|
940
|
+
switch (tryPos) {
|
|
941
|
+
case "bottom":
|
|
942
|
+
if (spaceBelow >= tooltipHeight + padding) {
|
|
943
|
+
pos = "bottom";
|
|
944
|
+
top = anchor.bottom + padding;
|
|
945
|
+
left = anchor.left + anchor.width / 2;
|
|
946
|
+
fits = true;
|
|
947
|
+
}
|
|
948
|
+
break;
|
|
949
|
+
case "top":
|
|
950
|
+
if (spaceAbove >= tooltipHeight + padding) {
|
|
951
|
+
pos = "top";
|
|
952
|
+
top = anchor.top - tooltipHeight - padding;
|
|
953
|
+
left = anchor.left + anchor.width / 2;
|
|
954
|
+
fits = true;
|
|
955
|
+
}
|
|
956
|
+
break;
|
|
957
|
+
case "right":
|
|
958
|
+
if (spaceRight >= tooltipWidth + padding) {
|
|
959
|
+
pos = "right";
|
|
960
|
+
top = anchor.top + anchor.height / 2;
|
|
961
|
+
left = anchor.right + padding;
|
|
962
|
+
fits = true;
|
|
963
|
+
}
|
|
964
|
+
break;
|
|
965
|
+
case "left":
|
|
966
|
+
if (spaceLeft >= tooltipWidth + padding) {
|
|
967
|
+
pos = "left";
|
|
968
|
+
top = anchor.top + anchor.height / 2;
|
|
969
|
+
left = anchor.left - tooltipWidth - padding;
|
|
970
|
+
fits = true;
|
|
971
|
+
}
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
if (fits) break;
|
|
975
|
+
}
|
|
976
|
+
if (pos === "top" || pos === "bottom") {
|
|
977
|
+
const halfWidth = tooltipWidth / 2;
|
|
978
|
+
if (left - halfWidth < edgeMargin) {
|
|
979
|
+
left = edgeMargin + halfWidth;
|
|
980
|
+
} else if (left + halfWidth > window.innerWidth - edgeMargin) {
|
|
981
|
+
left = window.innerWidth - edgeMargin - halfWidth;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (pos === "left" || pos === "right") {
|
|
985
|
+
const halfHeight = tooltipHeight / 2;
|
|
986
|
+
if (top - halfHeight < edgeMargin) {
|
|
987
|
+
top = edgeMargin + halfHeight;
|
|
988
|
+
} else if (top + halfHeight > window.innerHeight - edgeMargin) {
|
|
989
|
+
top = window.innerHeight - edgeMargin - halfHeight;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
setPosition(pos);
|
|
993
|
+
setCoords({ top, left });
|
|
994
|
+
setIsPositioned(true);
|
|
995
|
+
}, [anchorRef, preferredPosition]);
|
|
996
|
+
useEffect4(() => {
|
|
997
|
+
if (!show) {
|
|
998
|
+
setIsPositioned(false);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
calculatePosition();
|
|
1002
|
+
window.addEventListener("scroll", calculatePosition, true);
|
|
1003
|
+
window.addEventListener("resize", calculatePosition);
|
|
1004
|
+
return () => {
|
|
1005
|
+
window.removeEventListener("scroll", calculatePosition, true);
|
|
1006
|
+
window.removeEventListener("resize", calculatePosition);
|
|
1007
|
+
};
|
|
1008
|
+
}, [show, calculatePosition]);
|
|
1009
|
+
useEffect4(() => {
|
|
1010
|
+
if (show && tooltipRef.current) {
|
|
1011
|
+
calculatePosition();
|
|
1012
|
+
}
|
|
1013
|
+
}, [show, children, calculatePosition]);
|
|
1014
|
+
if (!show) return null;
|
|
1015
|
+
return createPortal3(
|
|
1016
|
+
/* @__PURE__ */ jsx5(
|
|
1017
|
+
"div",
|
|
1018
|
+
{
|
|
1019
|
+
ref: tooltipRef,
|
|
1020
|
+
className: `ya-tooltip ya-tooltip-${position}`,
|
|
1021
|
+
style: {
|
|
1022
|
+
top: coords.top,
|
|
1023
|
+
left: coords.left,
|
|
1024
|
+
opacity: isPositioned ? 1 : 0,
|
|
1025
|
+
pointerEvents: isPositioned ? "auto" : "none"
|
|
1026
|
+
},
|
|
1027
|
+
role: "tooltip",
|
|
1028
|
+
children
|
|
1029
|
+
}
|
|
1030
|
+
),
|
|
1031
|
+
document.body
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/components/YaImage.tsx
|
|
1036
|
+
import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1037
|
+
function parseImageValue(value) {
|
|
1038
|
+
if (!value) {
|
|
1039
|
+
return { src: "" };
|
|
1040
|
+
}
|
|
1041
|
+
try {
|
|
1042
|
+
const parsed = JSON.parse(value);
|
|
1043
|
+
if (typeof parsed === "object" && parsed.src) {
|
|
1044
|
+
return parsed;
|
|
1045
|
+
}
|
|
1046
|
+
} catch {
|
|
1047
|
+
}
|
|
1048
|
+
return { src: value };
|
|
1049
|
+
}
|
|
1050
|
+
function serializeImageValue(value) {
|
|
1051
|
+
return JSON.stringify(value);
|
|
1052
|
+
}
|
|
1053
|
+
function getObjectPosition(imageData) {
|
|
1054
|
+
if (imageData.focalPoint) {
|
|
1055
|
+
return `${imageData.focalPoint.x}% ${imageData.focalPoint.y}%`;
|
|
1056
|
+
}
|
|
1057
|
+
return imageData.objectPosition || "50% 50%";
|
|
1058
|
+
}
|
|
1059
|
+
var SMALL_IMAGE_THRESHOLD = 100;
|
|
1060
|
+
function YaImage({
|
|
1061
|
+
fieldId,
|
|
1062
|
+
className,
|
|
1063
|
+
alt,
|
|
1064
|
+
objectFit: propObjectFit,
|
|
1065
|
+
objectPosition: propObjectPosition,
|
|
1066
|
+
loading = "lazy",
|
|
1067
|
+
fallbackSrc,
|
|
1068
|
+
fallbackAlt
|
|
1069
|
+
}) {
|
|
1070
|
+
const { getValue, mode } = useContentStore();
|
|
1071
|
+
const containerRef = useRef5(null);
|
|
1072
|
+
const imgRef = useRef5(null);
|
|
1073
|
+
const [isSelected, setIsSelected] = useState5(false);
|
|
1074
|
+
const [isHovered, setIsHovered] = useState5(false);
|
|
1075
|
+
const [isSmallImage, setIsSmallImage] = useState5(false);
|
|
1076
|
+
const rawValue = getValue(fieldId);
|
|
1077
|
+
const imageData = parseImageValue(rawValue);
|
|
1078
|
+
const src = imageData.src || fallbackSrc || "";
|
|
1079
|
+
const altText = imageData.alt || alt || fallbackAlt || "";
|
|
1080
|
+
const objectFit = imageData.objectFit || propObjectFit || "cover";
|
|
1081
|
+
const objectPosition = getObjectPosition(imageData) || propObjectPosition || "50% 50%";
|
|
1082
|
+
const handleClick = useCallback5(() => {
|
|
1083
|
+
if (mode !== "inline-edit") return;
|
|
1084
|
+
if (document.body.classList.contains("builder-selector-active")) return;
|
|
1085
|
+
setIsSelected(true);
|
|
1086
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
1087
|
+
window.parent.postMessage(
|
|
1088
|
+
{
|
|
1089
|
+
type: "YA_IMAGE_EDIT_REQUEST",
|
|
1090
|
+
fieldId,
|
|
1091
|
+
currentValue: {
|
|
1092
|
+
...imageData,
|
|
1093
|
+
src,
|
|
1094
|
+
// Raw path, parent resolves display URL via asset cache
|
|
1095
|
+
alt: altText,
|
|
1096
|
+
objectFit,
|
|
1097
|
+
objectPosition
|
|
1098
|
+
},
|
|
1099
|
+
// Include element rect for floating popover positioning
|
|
1100
|
+
elementRect: rect ? {
|
|
1101
|
+
top: rect.top,
|
|
1102
|
+
left: rect.left,
|
|
1103
|
+
width: rect.width,
|
|
1104
|
+
height: rect.height
|
|
1105
|
+
} : void 0
|
|
1106
|
+
},
|
|
1107
|
+
"*"
|
|
1108
|
+
);
|
|
1109
|
+
}, [mode, fieldId, imageData, src, altText, objectFit, objectPosition]);
|
|
1110
|
+
useEffect5(() => {
|
|
1111
|
+
if (mode !== "inline-edit") return;
|
|
1112
|
+
const handleMessage2 = (event) => {
|
|
1113
|
+
if (event.data?.type === "YA_IMAGE_EDIT_COMPLETE" && event.data.fieldId === fieldId) {
|
|
1114
|
+
setIsSelected(false);
|
|
1115
|
+
}
|
|
1116
|
+
if (event.data?.type === "YA_IMAGE_EDIT_CANCEL" && event.data.fieldId === fieldId) {
|
|
1117
|
+
setIsSelected(false);
|
|
1118
|
+
}
|
|
1119
|
+
if (event.data?.type === "YA_IMAGE_UPDATE_PREVIEW" && event.data.fieldId === fieldId) {
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
window.addEventListener("message", handleMessage2);
|
|
1123
|
+
return () => window.removeEventListener("message", handleMessage2);
|
|
1124
|
+
}, [mode, fieldId]);
|
|
1125
|
+
useEffect5(() => {
|
|
1126
|
+
if (mode !== "inline-edit") return;
|
|
1127
|
+
const checkSize = () => {
|
|
1128
|
+
if (imgRef.current) {
|
|
1129
|
+
const { width, height } = imgRef.current.getBoundingClientRect();
|
|
1130
|
+
setIsSmallImage(width < SMALL_IMAGE_THRESHOLD || height < SMALL_IMAGE_THRESHOLD);
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
const img = imgRef.current;
|
|
1134
|
+
if (img) {
|
|
1135
|
+
if (img.complete) {
|
|
1136
|
+
checkSize();
|
|
1137
|
+
} else {
|
|
1138
|
+
img.addEventListener("load", checkSize);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
window.addEventListener("resize", checkSize);
|
|
1142
|
+
return () => {
|
|
1143
|
+
img?.removeEventListener("load", checkSize);
|
|
1144
|
+
window.removeEventListener("resize", checkSize);
|
|
1145
|
+
};
|
|
1146
|
+
}, [mode]);
|
|
1147
|
+
useEffect5(() => {
|
|
1148
|
+
if (!isSelected || mode !== "inline-edit") return;
|
|
1149
|
+
let lastRectKey = "";
|
|
1150
|
+
let lastTime = 0;
|
|
1151
|
+
let rafId;
|
|
1152
|
+
const loop = (time) => {
|
|
1153
|
+
if (time - lastTime >= 50) {
|
|
1154
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
1155
|
+
if (rect) {
|
|
1156
|
+
const rectKey = `${Math.round(rect.top)},${Math.round(rect.left)},${Math.round(rect.width)},${Math.round(rect.height)}`;
|
|
1157
|
+
if (rectKey !== lastRectKey) {
|
|
1158
|
+
lastRectKey = rectKey;
|
|
1159
|
+
window.parent.postMessage(
|
|
1160
|
+
{
|
|
1161
|
+
type: "YA_IMAGE_POSITION_UPDATE",
|
|
1162
|
+
fieldId,
|
|
1163
|
+
elementRect: {
|
|
1164
|
+
top: rect.top,
|
|
1165
|
+
left: rect.left,
|
|
1166
|
+
width: rect.width,
|
|
1167
|
+
height: rect.height
|
|
1168
|
+
}
|
|
1169
|
+
},
|
|
1170
|
+
"*"
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
lastTime = time;
|
|
1175
|
+
}
|
|
1176
|
+
rafId = requestAnimationFrame(loop);
|
|
1177
|
+
};
|
|
1178
|
+
rafId = requestAnimationFrame(loop);
|
|
1179
|
+
return () => cancelAnimationFrame(rafId);
|
|
1180
|
+
}, [isSelected, fieldId, mode]);
|
|
1181
|
+
if (mode === "read-only") {
|
|
1182
|
+
return /* @__PURE__ */ jsx6(
|
|
1183
|
+
"img",
|
|
1184
|
+
{
|
|
1185
|
+
src: resolveAssetUrl(src),
|
|
1186
|
+
alt: altText,
|
|
1187
|
+
className,
|
|
1188
|
+
style: {
|
|
1189
|
+
objectFit,
|
|
1190
|
+
objectPosition
|
|
1191
|
+
},
|
|
1192
|
+
loading,
|
|
1193
|
+
"data-ya-restricted": "true",
|
|
1194
|
+
"data-field-id": fieldId
|
|
1195
|
+
}
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
const editIcon = /* @__PURE__ */ jsxs3(
|
|
1199
|
+
"svg",
|
|
1200
|
+
{
|
|
1201
|
+
width: "24",
|
|
1202
|
+
height: "24",
|
|
1203
|
+
viewBox: "0 0 24 24",
|
|
1204
|
+
fill: "none",
|
|
1205
|
+
stroke: "currentColor",
|
|
1206
|
+
strokeWidth: "2",
|
|
1207
|
+
strokeLinecap: "round",
|
|
1208
|
+
strokeLinejoin: "round",
|
|
1209
|
+
children: [
|
|
1210
|
+
/* @__PURE__ */ jsx6("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
|
|
1211
|
+
/* @__PURE__ */ jsx6("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
|
|
1212
|
+
/* @__PURE__ */ jsx6("polyline", { points: "21 15 16 10 5 21" })
|
|
1213
|
+
]
|
|
1214
|
+
}
|
|
1215
|
+
);
|
|
1216
|
+
return /* @__PURE__ */ jsxs3(
|
|
1217
|
+
"div",
|
|
1218
|
+
{
|
|
1219
|
+
ref: containerRef,
|
|
1220
|
+
className: `ya-image-container ${isSelected ? "ya-image-selected" : "ya-image-editable"} ${isSmallImage ? "ya-image-small" : ""}`,
|
|
1221
|
+
onClick: handleClick,
|
|
1222
|
+
onMouseEnter: () => setIsHovered(true),
|
|
1223
|
+
onMouseLeave: () => setIsHovered(false),
|
|
1224
|
+
"data-ya-restricted": "true",
|
|
1225
|
+
"data-field-id": fieldId,
|
|
1226
|
+
"data-ya-image": "true",
|
|
1227
|
+
role: "button",
|
|
1228
|
+
tabIndex: 0,
|
|
1229
|
+
"aria-label": `Edit image: ${altText || fieldId}`,
|
|
1230
|
+
onKeyDown: (e) => {
|
|
1231
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1232
|
+
e.preventDefault();
|
|
1233
|
+
handleClick();
|
|
1234
|
+
}
|
|
1235
|
+
},
|
|
1236
|
+
children: [
|
|
1237
|
+
/* @__PURE__ */ jsx6(
|
|
1238
|
+
"img",
|
|
1239
|
+
{
|
|
1240
|
+
ref: imgRef,
|
|
1241
|
+
src: resolveAssetUrl(src),
|
|
1242
|
+
alt: altText,
|
|
1243
|
+
className,
|
|
1244
|
+
style: {
|
|
1245
|
+
objectFit,
|
|
1246
|
+
objectPosition
|
|
1247
|
+
},
|
|
1248
|
+
loading
|
|
1249
|
+
}
|
|
1250
|
+
),
|
|
1251
|
+
isSmallImage ? /* @__PURE__ */ jsxs3(YaTooltip, { anchorRef: containerRef, show: isHovered && !isSelected, children: [
|
|
1252
|
+
editIcon,
|
|
1253
|
+
/* @__PURE__ */ jsx6("span", { children: "Click to edit" })
|
|
1254
|
+
] }) : (
|
|
1255
|
+
/* For large images: show overlay inside the image */
|
|
1256
|
+
/* @__PURE__ */ jsxs3("div", { className: "ya-image-overlay", children: [
|
|
1257
|
+
/* @__PURE__ */ jsx6("div", { className: "ya-image-edit-icon", children: editIcon }),
|
|
1258
|
+
/* @__PURE__ */ jsx6("span", { className: "ya-image-edit-label", children: "Click to edit" })
|
|
1259
|
+
] })
|
|
1260
|
+
)
|
|
1261
|
+
]
|
|
1262
|
+
}
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// src/components/YaLink.tsx
|
|
1267
|
+
import { useEffect as useEffect6, useRef as useRef6, useState as useState6, useCallback as useCallback6 } from "react";
|
|
1268
|
+
import { createPortal as createPortal4 } from "react-dom";
|
|
1269
|
+
import { useEditor as useEditor2, EditorContent as EditorContent2 } from "@tiptap/react";
|
|
1270
|
+
import { BubbleMenu as BubbleMenu2 } from "@tiptap/react/menus";
|
|
1271
|
+
import StarterKit2 from "@tiptap/starter-kit";
|
|
1272
|
+
import { TextStyle as TextStyle2 } from "@tiptap/extension-text-style";
|
|
1273
|
+
import { Extension as Extension2 } from "@tiptap/core";
|
|
1274
|
+
import { Link as WouterLink, useLocation } from "wouter";
|
|
1275
|
+
import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1276
|
+
function isInternalPath(path) {
|
|
1277
|
+
if (!path) return false;
|
|
1278
|
+
if (path.startsWith("#")) return false;
|
|
1279
|
+
if (path.startsWith("//")) return false;
|
|
1280
|
+
if (path.includes("://")) return false;
|
|
1281
|
+
if (path.startsWith("mailto:") || path.startsWith("tel:")) return false;
|
|
1282
|
+
return path.startsWith("/");
|
|
1283
|
+
}
|
|
1284
|
+
var FontSize2 = Extension2.create({
|
|
1285
|
+
name: "fontSize",
|
|
1286
|
+
addOptions() {
|
|
1287
|
+
return { types: ["textStyle"] };
|
|
1288
|
+
},
|
|
1289
|
+
addGlobalAttributes() {
|
|
1290
|
+
return [
|
|
1291
|
+
{
|
|
1292
|
+
types: this.options.types,
|
|
1293
|
+
attributes: {
|
|
1294
|
+
fontSize: {
|
|
1295
|
+
default: null,
|
|
1296
|
+
parseHTML: (element) => element.style.fontSize || null,
|
|
1297
|
+
renderHTML: (attributes) => {
|
|
1298
|
+
if (!attributes.fontSize) return {};
|
|
1299
|
+
return { style: `font-size: ${attributes.fontSize}` };
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
];
|
|
1305
|
+
},
|
|
1306
|
+
addCommands() {
|
|
1307
|
+
return {
|
|
1308
|
+
setFontSize: (fontSize) => ({ chain }) => chain().setMark("textStyle", { fontSize }).run(),
|
|
1309
|
+
unsetFontSize: () => ({ chain }) => chain().setMark("textStyle", { fontSize: null }).removeEmptyTextStyle().run()
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
var FontWeight2 = Extension2.create({
|
|
1314
|
+
name: "fontWeight",
|
|
1315
|
+
addOptions() {
|
|
1316
|
+
return { types: ["textStyle"] };
|
|
1317
|
+
},
|
|
1318
|
+
addGlobalAttributes() {
|
|
1319
|
+
return [
|
|
1320
|
+
{
|
|
1321
|
+
types: this.options.types,
|
|
1322
|
+
attributes: {
|
|
1323
|
+
fontWeight: {
|
|
1324
|
+
default: null,
|
|
1325
|
+
parseHTML: (element) => element.style.fontWeight || null,
|
|
1326
|
+
renderHTML: (attributes) => {
|
|
1327
|
+
if (!attributes.fontWeight) return {};
|
|
1328
|
+
return { style: `font-weight: ${attributes.fontWeight}` };
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
];
|
|
1334
|
+
},
|
|
1335
|
+
addCommands() {
|
|
1336
|
+
return {
|
|
1337
|
+
setFontWeight: (fontWeight) => ({ chain }) => chain().setMark("textStyle", { fontWeight }).run(),
|
|
1338
|
+
unsetFontWeight: () => ({ chain }) => chain().setMark("textStyle", { fontWeight: null }).removeEmptyTextStyle().run()
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
var SIZE_PRESETS2 = {
|
|
1343
|
+
S: "14px",
|
|
1344
|
+
M: "16px",
|
|
1345
|
+
L: "20px",
|
|
1346
|
+
XL: "24px"
|
|
1347
|
+
};
|
|
1348
|
+
var WEIGHT_PRESETS2 = {
|
|
1349
|
+
Regular: "400",
|
|
1350
|
+
"Semi-bold": "600",
|
|
1351
|
+
Bold: "700",
|
|
1352
|
+
"Extra-bold": "800"
|
|
1353
|
+
};
|
|
1354
|
+
function discoverSectionsFromDOM() {
|
|
1355
|
+
const sections = [];
|
|
1356
|
+
const selectableElements = document.querySelectorAll("[data-mp-selectable]");
|
|
1357
|
+
selectableElements.forEach((el) => {
|
|
1358
|
+
const id = el.getAttribute("data-mp-selectable") || el.id;
|
|
1359
|
+
if (id) {
|
|
1360
|
+
const label = id.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1361
|
+
sections.push({ path: `#${id}`, label });
|
|
1362
|
+
}
|
|
1363
|
+
});
|
|
1364
|
+
return sections;
|
|
1365
|
+
}
|
|
1366
|
+
function YaLink({ fieldId, href: defaultHref = "#", className, as: Component = "a", children, availablePages, onClick }) {
|
|
1367
|
+
const { getValue, setValue, mode, saveToWorker, getPages } = useContentStore();
|
|
1368
|
+
const [, navigate] = useLocation();
|
|
1369
|
+
const pages = availablePages ?? getPages();
|
|
1370
|
+
const [sections, setSections] = useState6([]);
|
|
1371
|
+
const [sectionsExpanded, setSectionsExpanded] = useState6(false);
|
|
1372
|
+
const textFieldId = `${fieldId}.text`;
|
|
1373
|
+
const hrefFieldId = `${fieldId}.href`;
|
|
1374
|
+
const storeText = getValue(textFieldId);
|
|
1375
|
+
const storeHref = getValue(hrefFieldId);
|
|
1376
|
+
const isIconMode = children != null && typeof children !== "string";
|
|
1377
|
+
const text = storeText || (typeof children === "string" ? children : "");
|
|
1378
|
+
const href = storeHref || defaultHref;
|
|
1379
|
+
const [editingMode, setEditingMode] = useState6(null);
|
|
1380
|
+
const [showEditPopover, setShowEditPopover] = useState6(false);
|
|
1381
|
+
const [originalText, setOriginalText] = useState6(text);
|
|
1382
|
+
const [originalHref, setOriginalHref] = useState6(href);
|
|
1383
|
+
const [currentHref, setCurrentHref] = useState6(href);
|
|
1384
|
+
const [isExternalUrl, setIsExternalUrl] = useState6(false);
|
|
1385
|
+
const [externalUrl, setExternalUrl] = useState6("");
|
|
1386
|
+
const containerRef = useRef6(null);
|
|
1387
|
+
const hrefPopoverRef = useRef6(null);
|
|
1388
|
+
const hidePopoverTimeoutRef = useRef6(null);
|
|
1389
|
+
const editor = useEditor2({
|
|
1390
|
+
extensions: [
|
|
1391
|
+
StarterKit2.configure({
|
|
1392
|
+
heading: false,
|
|
1393
|
+
bulletList: false,
|
|
1394
|
+
orderedList: false,
|
|
1395
|
+
blockquote: false,
|
|
1396
|
+
codeBlock: false,
|
|
1397
|
+
horizontalRule: false
|
|
1398
|
+
}),
|
|
1399
|
+
TextStyle2,
|
|
1400
|
+
FontSize2,
|
|
1401
|
+
FontWeight2
|
|
1402
|
+
],
|
|
1403
|
+
content: text,
|
|
1404
|
+
editable: true,
|
|
1405
|
+
editorProps: {
|
|
1406
|
+
attributes: {
|
|
1407
|
+
class: "outline-none"
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
useEffect6(() => {
|
|
1412
|
+
if (editor && editingMode !== "text") {
|
|
1413
|
+
if (editor.getHTML() !== text) {
|
|
1414
|
+
editor.commands.setContent(text);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}, [text, editor, editingMode]);
|
|
1418
|
+
useEffect6(() => {
|
|
1419
|
+
if (editingMode !== "link") {
|
|
1420
|
+
setCurrentHref(href);
|
|
1421
|
+
}
|
|
1422
|
+
}, [href, editingMode]);
|
|
1423
|
+
useEffect6(() => {
|
|
1424
|
+
return () => {
|
|
1425
|
+
if (hidePopoverTimeoutRef.current) {
|
|
1426
|
+
clearTimeout(hidePopoverTimeoutRef.current);
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
}, []);
|
|
1430
|
+
useEffect6(() => {
|
|
1431
|
+
if (editingMode !== "link") return;
|
|
1432
|
+
const handleClickOutside = (event) => {
|
|
1433
|
+
const target = event.target;
|
|
1434
|
+
if (hrefPopoverRef.current?.contains(target)) return;
|
|
1435
|
+
if (containerRef.current?.contains(target)) return;
|
|
1436
|
+
setCurrentHref(originalHref);
|
|
1437
|
+
setEditingMode(null);
|
|
1438
|
+
setIsExternalUrl(false);
|
|
1439
|
+
setExternalUrl("");
|
|
1440
|
+
};
|
|
1441
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
1442
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
1443
|
+
}, [editingMode, originalHref]);
|
|
1444
|
+
const handleSaveText = useCallback6(() => {
|
|
1445
|
+
if (!editor) return;
|
|
1446
|
+
let html = editor.getHTML();
|
|
1447
|
+
html = html.replace(/<\/p><p>/g, "<br><br>").replace(/^<p>/, "").replace(/<\/p>$/, "");
|
|
1448
|
+
setValue(textFieldId, html);
|
|
1449
|
+
saveToWorker?.(textFieldId, html);
|
|
1450
|
+
setEditingMode(null);
|
|
1451
|
+
}, [editor, textFieldId, setValue, saveToWorker]);
|
|
1452
|
+
const handleSaveLink = useCallback6(() => {
|
|
1453
|
+
setValue(hrefFieldId, currentHref);
|
|
1454
|
+
saveToWorker?.(hrefFieldId, currentHref);
|
|
1455
|
+
setEditingMode(null);
|
|
1456
|
+
setIsExternalUrl(false);
|
|
1457
|
+
setExternalUrl("");
|
|
1458
|
+
}, [hrefFieldId, currentHref, setValue, saveToWorker]);
|
|
1459
|
+
const handleCancelText = useCallback6(() => {
|
|
1460
|
+
if (editor) {
|
|
1461
|
+
editor.commands.setContent(originalText);
|
|
1462
|
+
}
|
|
1463
|
+
setEditingMode(null);
|
|
1464
|
+
}, [editor, originalText]);
|
|
1465
|
+
const handleCancelLink = useCallback6(() => {
|
|
1466
|
+
setCurrentHref(originalHref);
|
|
1467
|
+
setEditingMode(null);
|
|
1468
|
+
setIsExternalUrl(false);
|
|
1469
|
+
setExternalUrl("");
|
|
1470
|
+
}, [originalHref]);
|
|
1471
|
+
const handleClick = useCallback6(
|
|
1472
|
+
(e) => {
|
|
1473
|
+
const selectModeEnabled = window.__builderSelectModeEnabled;
|
|
1474
|
+
if (selectModeEnabled) {
|
|
1475
|
+
e.preventDefault();
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
if (href.startsWith("#")) {
|
|
1479
|
+
e.preventDefault();
|
|
1480
|
+
const targetId = href.substring(1);
|
|
1481
|
+
const targetElement = document.getElementById(targetId);
|
|
1482
|
+
if (targetElement) {
|
|
1483
|
+
const navHeight = 118;
|
|
1484
|
+
const targetPosition = targetElement.offsetTop - navHeight;
|
|
1485
|
+
window.scrollTo({
|
|
1486
|
+
top: targetPosition,
|
|
1487
|
+
behavior: "smooth"
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
onClick?.();
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
if (isInternalPath(href)) {
|
|
1494
|
+
e.preventDefault();
|
|
1495
|
+
navigate(href);
|
|
1496
|
+
onClick?.();
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
onClick?.();
|
|
1500
|
+
},
|
|
1501
|
+
[href, navigate, onClick]
|
|
1502
|
+
);
|
|
1503
|
+
const handleMouseEnter = useCallback6(() => {
|
|
1504
|
+
if (hidePopoverTimeoutRef.current) {
|
|
1505
|
+
clearTimeout(hidePopoverTimeoutRef.current);
|
|
1506
|
+
hidePopoverTimeoutRef.current = null;
|
|
1507
|
+
}
|
|
1508
|
+
if (mode === "inline-edit" && !editingMode) {
|
|
1509
|
+
setShowEditPopover(true);
|
|
1510
|
+
}
|
|
1511
|
+
}, [mode, editingMode]);
|
|
1512
|
+
const handleMouseLeave = useCallback6(() => {
|
|
1513
|
+
hidePopoverTimeoutRef.current = window.setTimeout(() => {
|
|
1514
|
+
if (!editingMode) {
|
|
1515
|
+
setShowEditPopover(false);
|
|
1516
|
+
}
|
|
1517
|
+
}, 150);
|
|
1518
|
+
}, [editingMode]);
|
|
1519
|
+
const handleFocus = useCallback6(() => {
|
|
1520
|
+
if (mode === "inline-edit" && !editingMode) {
|
|
1521
|
+
setShowEditPopover(true);
|
|
1522
|
+
}
|
|
1523
|
+
}, [mode, editingMode]);
|
|
1524
|
+
const startEditText = useCallback6(() => {
|
|
1525
|
+
setShowEditPopover(false);
|
|
1526
|
+
setEditingMode("text");
|
|
1527
|
+
setOriginalText(text);
|
|
1528
|
+
setTimeout(() => {
|
|
1529
|
+
editor?.chain().focus().selectAll().run();
|
|
1530
|
+
}, 20);
|
|
1531
|
+
}, [text, editor]);
|
|
1532
|
+
const startEditLink = useCallback6(() => {
|
|
1533
|
+
setShowEditPopover(false);
|
|
1534
|
+
setEditingMode("link");
|
|
1535
|
+
setOriginalHref(href);
|
|
1536
|
+
setCurrentHref(href);
|
|
1537
|
+
setSections(discoverSectionsFromDOM());
|
|
1538
|
+
}, [href]);
|
|
1539
|
+
const handleKeyDown = useCallback6(
|
|
1540
|
+
(event) => {
|
|
1541
|
+
if (editingMode !== "text") return;
|
|
1542
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
1543
|
+
event.preventDefault();
|
|
1544
|
+
handleSaveText();
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
if (event.key === "Escape") {
|
|
1548
|
+
event.preventDefault();
|
|
1549
|
+
handleCancelText();
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "s") {
|
|
1553
|
+
event.preventDefault();
|
|
1554
|
+
handleSaveText();
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
},
|
|
1558
|
+
[editingMode, handleSaveText, handleCancelText]
|
|
1559
|
+
);
|
|
1560
|
+
const handleFontSizeChange = useCallback6(
|
|
1561
|
+
(e) => {
|
|
1562
|
+
if (!editor) return;
|
|
1563
|
+
const size = e.target.value;
|
|
1564
|
+
if (size === "") {
|
|
1565
|
+
editor.chain().focus().unsetFontSize().run();
|
|
1566
|
+
} else {
|
|
1567
|
+
editor.chain().focus().setFontSize(size).run();
|
|
1568
|
+
}
|
|
1569
|
+
},
|
|
1570
|
+
[editor]
|
|
1571
|
+
);
|
|
1572
|
+
const handleFontWeightChange = useCallback6(
|
|
1573
|
+
(e) => {
|
|
1574
|
+
if (!editor) return;
|
|
1575
|
+
const weight = e.target.value;
|
|
1576
|
+
if (weight === "") {
|
|
1577
|
+
editor.chain().focus().unsetFontWeight().run();
|
|
1578
|
+
} else {
|
|
1579
|
+
editor.chain().focus().setFontWeight(weight).run();
|
|
1580
|
+
}
|
|
1581
|
+
},
|
|
1582
|
+
[editor]
|
|
1583
|
+
);
|
|
1584
|
+
const handlePageSelect = useCallback6((path) => {
|
|
1585
|
+
setCurrentHref(path);
|
|
1586
|
+
setIsExternalUrl(false);
|
|
1587
|
+
}, []);
|
|
1588
|
+
const handleExternalUrlApply = useCallback6(() => {
|
|
1589
|
+
if (externalUrl) {
|
|
1590
|
+
setCurrentHref(externalUrl);
|
|
1591
|
+
}
|
|
1592
|
+
}, [externalUrl]);
|
|
1593
|
+
const getCurrentFontSize = () => {
|
|
1594
|
+
if (!editor) return "";
|
|
1595
|
+
const attrs = editor.getAttributes("textStyle");
|
|
1596
|
+
return attrs.fontSize || "";
|
|
1597
|
+
};
|
|
1598
|
+
const getCurrentFontWeight = () => {
|
|
1599
|
+
if (!editor) return "";
|
|
1600
|
+
const attrs = editor.getAttributes("textStyle");
|
|
1601
|
+
return attrs.fontWeight || "";
|
|
1602
|
+
};
|
|
1603
|
+
if (mode === "read-only") {
|
|
1604
|
+
const content = isIconMode ? children : /* @__PURE__ */ jsx7(SafeHtml, { content: text, mode });
|
|
1605
|
+
if (isInternalPath(href)) {
|
|
1606
|
+
return /* @__PURE__ */ jsx7(
|
|
1607
|
+
WouterLink,
|
|
1608
|
+
{
|
|
1609
|
+
href,
|
|
1610
|
+
className,
|
|
1611
|
+
"data-ya-restricted": "true",
|
|
1612
|
+
"data-field-id": fieldId,
|
|
1613
|
+
children: content
|
|
1614
|
+
}
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
return /* @__PURE__ */ jsx7(
|
|
1618
|
+
Component,
|
|
1619
|
+
{
|
|
1620
|
+
ref: containerRef,
|
|
1621
|
+
href: Component === "a" ? href : void 0,
|
|
1622
|
+
className,
|
|
1623
|
+
"data-ya-restricted": "true",
|
|
1624
|
+
"data-field-id": fieldId,
|
|
1625
|
+
children: content
|
|
1626
|
+
}
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
return /* @__PURE__ */ jsxs4("span", { className: "ya-link-wrapper", children: [
|
|
1630
|
+
/* @__PURE__ */ jsx7(
|
|
1631
|
+
Component,
|
|
1632
|
+
{
|
|
1633
|
+
ref: containerRef,
|
|
1634
|
+
href: Component === "a" ? href : void 0,
|
|
1635
|
+
className: `${className || ""} ${editingMode ? "ya-link-editing" : "ya-link-editable"}`,
|
|
1636
|
+
"data-ya-restricted": "true",
|
|
1637
|
+
"data-field-id": fieldId,
|
|
1638
|
+
onClick: handleClick,
|
|
1639
|
+
onMouseEnter: handleMouseEnter,
|
|
1640
|
+
onMouseLeave: handleMouseLeave,
|
|
1641
|
+
onFocus: handleFocus,
|
|
1642
|
+
onKeyDown: handleKeyDown,
|
|
1643
|
+
children: isIconMode ? (
|
|
1644
|
+
// Icon mode: render children directly, no text editing
|
|
1645
|
+
children
|
|
1646
|
+
) : editor ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
1647
|
+
createPortal4(
|
|
1648
|
+
/* @__PURE__ */ jsxs4(
|
|
1649
|
+
BubbleMenu2,
|
|
1650
|
+
{
|
|
1651
|
+
editor,
|
|
1652
|
+
shouldShow: () => editingMode === "text",
|
|
1653
|
+
options: { offset: 6, placement: "top" },
|
|
1654
|
+
className: "ya-bubble-menu",
|
|
1655
|
+
children: [
|
|
1656
|
+
/* @__PURE__ */ jsx7(
|
|
1657
|
+
"button",
|
|
1658
|
+
{
|
|
1659
|
+
type: "button",
|
|
1660
|
+
onClick: () => editor.chain().focus().toggleBold().run(),
|
|
1661
|
+
className: `ya-bubble-btn ${editor.isActive("bold") ? "is-active" : ""}`,
|
|
1662
|
+
title: "Bold",
|
|
1663
|
+
children: /* @__PURE__ */ jsx7("strong", { children: "B" })
|
|
1664
|
+
}
|
|
1665
|
+
),
|
|
1666
|
+
/* @__PURE__ */ jsx7(
|
|
1667
|
+
"button",
|
|
1668
|
+
{
|
|
1669
|
+
type: "button",
|
|
1670
|
+
onClick: () => editor.chain().focus().toggleItalic().run(),
|
|
1671
|
+
className: `ya-bubble-btn ${editor.isActive("italic") ? "is-active" : ""}`,
|
|
1672
|
+
title: "Italic",
|
|
1673
|
+
children: /* @__PURE__ */ jsx7("em", { children: "I" })
|
|
1674
|
+
}
|
|
1675
|
+
),
|
|
1676
|
+
/* @__PURE__ */ jsx7("span", { className: "ya-bubble-divider" }),
|
|
1677
|
+
/* @__PURE__ */ jsxs4(
|
|
1678
|
+
"select",
|
|
1679
|
+
{
|
|
1680
|
+
value: getCurrentFontSize(),
|
|
1681
|
+
onChange: handleFontSizeChange,
|
|
1682
|
+
className: "ya-bubble-select",
|
|
1683
|
+
title: "Font Size",
|
|
1684
|
+
children: [
|
|
1685
|
+
/* @__PURE__ */ jsx7("option", { value: "", children: "Size" }),
|
|
1686
|
+
Object.entries(SIZE_PRESETS2).map(([name, size]) => /* @__PURE__ */ jsx7("option", { value: size, children: name }, name))
|
|
1687
|
+
]
|
|
1688
|
+
}
|
|
1689
|
+
),
|
|
1690
|
+
/* @__PURE__ */ jsxs4(
|
|
1691
|
+
"select",
|
|
1692
|
+
{
|
|
1693
|
+
value: getCurrentFontWeight(),
|
|
1694
|
+
onChange: handleFontWeightChange,
|
|
1695
|
+
className: "ya-bubble-select",
|
|
1696
|
+
title: "Font Weight",
|
|
1697
|
+
children: [
|
|
1698
|
+
/* @__PURE__ */ jsx7("option", { value: "", children: "Weight" }),
|
|
1699
|
+
Object.entries(WEIGHT_PRESETS2).map(([name, weight]) => /* @__PURE__ */ jsx7("option", { value: weight, children: name }, name))
|
|
1700
|
+
]
|
|
1701
|
+
}
|
|
1702
|
+
)
|
|
1703
|
+
]
|
|
1704
|
+
}
|
|
1705
|
+
),
|
|
1706
|
+
document.body
|
|
1707
|
+
),
|
|
1708
|
+
editingMode === "text" ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
1709
|
+
/* @__PURE__ */ jsx7(EditorContent2, { editor }),
|
|
1710
|
+
/* @__PURE__ */ jsxs4("div", { className: "ya-link-actions", children: [
|
|
1711
|
+
/* @__PURE__ */ jsx7("button", { type: "button", onClick: handleCancelText, className: "ya-link-btn ya-link-btn-cancel", children: "Cancel" }),
|
|
1712
|
+
/* @__PURE__ */ jsx7("button", { type: "button", onClick: handleSaveText, className: "ya-link-btn ya-link-btn-save", children: "Save" })
|
|
1713
|
+
] })
|
|
1714
|
+
] }) : /* @__PURE__ */ jsx7(SafeHtml, { content: text, mode })
|
|
1715
|
+
] }) : /* @__PURE__ */ jsx7(SafeHtml, { content: text, mode })
|
|
1716
|
+
}
|
|
1717
|
+
),
|
|
1718
|
+
showEditPopover && !editingMode && mode === "inline-edit" && /* @__PURE__ */ jsxs4("div", { className: "ya-link-edit-popover", onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [
|
|
1719
|
+
!isIconMode && /* @__PURE__ */ jsx7("button", { type: "button", onClick: startEditText, children: "Edit text" }),
|
|
1720
|
+
/* @__PURE__ */ jsx7("button", { type: "button", onClick: startEditLink, children: "Edit link" })
|
|
1721
|
+
] }),
|
|
1722
|
+
editingMode === "link" && /* @__PURE__ */ jsxs4("div", { ref: hrefPopoverRef, className: "ya-href-popover", children: [
|
|
1723
|
+
/* @__PURE__ */ jsx7("div", { className: "ya-href-popover-header", children: "Link destination" }),
|
|
1724
|
+
!isExternalUrl ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
1725
|
+
sections.length > 0 && /* @__PURE__ */ jsxs4("div", { className: "ya-href-popover-section", children: [
|
|
1726
|
+
/* @__PURE__ */ jsxs4(
|
|
1727
|
+
"button",
|
|
1728
|
+
{
|
|
1729
|
+
type: "button",
|
|
1730
|
+
className: "ya-href-popover-label ya-href-collapsible-header",
|
|
1731
|
+
onClick: () => setSectionsExpanded(!sectionsExpanded),
|
|
1732
|
+
children: [
|
|
1733
|
+
/* @__PURE__ */ jsx7("span", { className: "ya-href-chevron", children: sectionsExpanded ? "\u25BC" : "\u25B6" }),
|
|
1734
|
+
"Scroll to section (",
|
|
1735
|
+
sections.length,
|
|
1736
|
+
")"
|
|
1737
|
+
]
|
|
1738
|
+
}
|
|
1739
|
+
),
|
|
1740
|
+
sectionsExpanded && /* @__PURE__ */ jsx7("div", { className: "ya-href-popover-pages", children: sections.map((section) => /* @__PURE__ */ jsxs4(
|
|
1741
|
+
"button",
|
|
1742
|
+
{
|
|
1743
|
+
type: "button",
|
|
1744
|
+
className: `ya-href-page-btn ${currentHref === section.path ? "is-selected" : ""}`,
|
|
1745
|
+
onClick: () => handlePageSelect(section.path),
|
|
1746
|
+
children: [
|
|
1747
|
+
section.label,
|
|
1748
|
+
/* @__PURE__ */ jsx7("span", { className: "ya-href-page-path", children: section.path })
|
|
1749
|
+
]
|
|
1750
|
+
},
|
|
1751
|
+
section.path
|
|
1752
|
+
)) })
|
|
1753
|
+
] }),
|
|
1754
|
+
pages.length > 0 && /* @__PURE__ */ jsxs4("div", { className: "ya-href-popover-section", children: [
|
|
1755
|
+
/* @__PURE__ */ jsx7("label", { className: "ya-href-popover-label", children: "Navigate to page" }),
|
|
1756
|
+
/* @__PURE__ */ jsx7("div", { className: "ya-href-popover-pages", children: pages.map((page) => /* @__PURE__ */ jsxs4(
|
|
1757
|
+
"button",
|
|
1758
|
+
{
|
|
1759
|
+
type: "button",
|
|
1760
|
+
className: `ya-href-page-btn ${currentHref === page.path ? "is-selected" : ""}`,
|
|
1761
|
+
onClick: () => handlePageSelect(page.path),
|
|
1762
|
+
children: [
|
|
1763
|
+
page.label,
|
|
1764
|
+
/* @__PURE__ */ jsx7("span", { className: "ya-href-page-path", children: page.path })
|
|
1765
|
+
]
|
|
1766
|
+
},
|
|
1767
|
+
page.path
|
|
1768
|
+
)) })
|
|
1769
|
+
] }),
|
|
1770
|
+
/* @__PURE__ */ jsx7(
|
|
1771
|
+
"button",
|
|
1772
|
+
{
|
|
1773
|
+
type: "button",
|
|
1774
|
+
className: "ya-href-external-toggle",
|
|
1775
|
+
onClick: () => {
|
|
1776
|
+
setIsExternalUrl(true);
|
|
1777
|
+
setExternalUrl(currentHref.startsWith("http") ? currentHref : "");
|
|
1778
|
+
},
|
|
1779
|
+
children: "Use external URL instead"
|
|
1780
|
+
}
|
|
1781
|
+
)
|
|
1782
|
+
] }) : /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
1783
|
+
/* @__PURE__ */ jsxs4("div", { className: "ya-href-popover-section", children: [
|
|
1784
|
+
/* @__PURE__ */ jsx7("label", { className: "ya-href-popover-label", children: "External URL" }),
|
|
1785
|
+
/* @__PURE__ */ jsx7(
|
|
1786
|
+
"input",
|
|
1787
|
+
{
|
|
1788
|
+
type: "url",
|
|
1789
|
+
className: "ya-href-url-input",
|
|
1790
|
+
placeholder: "https://example.com",
|
|
1791
|
+
value: externalUrl,
|
|
1792
|
+
onChange: (e) => setExternalUrl(e.target.value),
|
|
1793
|
+
autoFocus: true
|
|
1794
|
+
}
|
|
1795
|
+
)
|
|
1796
|
+
] }),
|
|
1797
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: "ya-href-external-toggle", onClick: () => setIsExternalUrl(false), children: "\u2190 Back to pages" })
|
|
1798
|
+
] }),
|
|
1799
|
+
/* @__PURE__ */ jsxs4("div", { className: "ya-href-popover-actions", children: [
|
|
1800
|
+
/* @__PURE__ */ jsx7("button", { type: "button", className: "ya-link-btn ya-link-btn-cancel", onClick: handleCancelLink, children: "Cancel" }),
|
|
1801
|
+
isExternalUrl ? /* @__PURE__ */ jsx7("button", { type: "button", className: "ya-link-btn ya-link-btn-save", onClick: handleExternalUrlApply, children: "Apply" }) : /* @__PURE__ */ jsx7("button", { type: "button", className: "ya-link-btn ya-link-btn-save", onClick: handleSaveLink, children: "Save" })
|
|
1802
|
+
] })
|
|
1803
|
+
] })
|
|
1804
|
+
] });
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// src/components/StaticText.tsx
|
|
1808
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
1809
|
+
function MpText({ fieldId, className, as: Component = "span", children }) {
|
|
1810
|
+
const content = getContent(fieldId) || (typeof children === "string" ? children : "");
|
|
1811
|
+
return /* @__PURE__ */ jsx8(
|
|
1812
|
+
Component,
|
|
1813
|
+
{
|
|
1814
|
+
className,
|
|
1815
|
+
"data-field-id": fieldId,
|
|
1816
|
+
dangerouslySetInnerHTML: { __html: content }
|
|
1817
|
+
}
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// src/components/StaticImage.tsx
|
|
1822
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
1823
|
+
function parseImageValue2(value) {
|
|
1824
|
+
if (!value) {
|
|
1825
|
+
return { src: "" };
|
|
1826
|
+
}
|
|
1827
|
+
try {
|
|
1828
|
+
const parsed = JSON.parse(value);
|
|
1829
|
+
if (typeof parsed === "object" && parsed.src) {
|
|
1830
|
+
return parsed;
|
|
1831
|
+
}
|
|
1832
|
+
} catch {
|
|
1833
|
+
}
|
|
1834
|
+
return { src: value };
|
|
1835
|
+
}
|
|
1836
|
+
function getObjectPosition2(imageData) {
|
|
1837
|
+
if (imageData.focalPoint) {
|
|
1838
|
+
return `${imageData.focalPoint.x}% ${imageData.focalPoint.y}%`;
|
|
1839
|
+
}
|
|
1840
|
+
return imageData.objectPosition || "50% 50%";
|
|
1841
|
+
}
|
|
1842
|
+
function MpImage({
|
|
1843
|
+
fieldId,
|
|
1844
|
+
className,
|
|
1845
|
+
alt,
|
|
1846
|
+
objectFit: propObjectFit,
|
|
1847
|
+
objectPosition: propObjectPosition,
|
|
1848
|
+
loading = "lazy",
|
|
1849
|
+
fallbackSrc,
|
|
1850
|
+
fallbackAlt
|
|
1851
|
+
}) {
|
|
1852
|
+
const rawValue = getContent(fieldId);
|
|
1853
|
+
const imageData = parseImageValue2(rawValue);
|
|
1854
|
+
const src = imageData.src || fallbackSrc || "";
|
|
1855
|
+
const altText = imageData.alt || alt || fallbackAlt || "";
|
|
1856
|
+
const objectFit = imageData.objectFit || propObjectFit || "cover";
|
|
1857
|
+
const objectPosition = getObjectPosition2(imageData) || propObjectPosition || "50% 50%";
|
|
1858
|
+
return /* @__PURE__ */ jsx9(
|
|
1859
|
+
"img",
|
|
1860
|
+
{
|
|
1861
|
+
src: resolveAssetUrl(src),
|
|
1862
|
+
alt: altText,
|
|
1863
|
+
className,
|
|
1864
|
+
style: {
|
|
1865
|
+
objectFit,
|
|
1866
|
+
objectPosition
|
|
1867
|
+
},
|
|
1868
|
+
loading,
|
|
1869
|
+
"data-field-id": fieldId
|
|
1870
|
+
}
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// src/components/MarkdownText.tsx
|
|
1875
|
+
import { Fragment as Fragment4 } from "react";
|
|
1876
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
1877
|
+
function tokenize(text) {
|
|
1878
|
+
const tokens = [];
|
|
1879
|
+
let remaining = text;
|
|
1880
|
+
const patterns = [
|
|
1881
|
+
// Bold: **text**
|
|
1882
|
+
{ regex: /^\*\*(.+?)\*\*/, type: "bold" },
|
|
1883
|
+
// Italic: *text* (but not **)
|
|
1884
|
+
{ regex: /^\*([^*]+?)\*/, type: "italic" },
|
|
1885
|
+
// Link: [text](url)
|
|
1886
|
+
{ regex: /^\[([^\]]+)\]\(([^)]+)\)/, type: "link" },
|
|
1887
|
+
// Newline
|
|
1888
|
+
{ regex: /^\n/, type: "newline" }
|
|
1889
|
+
];
|
|
1890
|
+
while (remaining.length > 0) {
|
|
1891
|
+
let matched = false;
|
|
1892
|
+
for (const pattern of patterns) {
|
|
1893
|
+
const match = remaining.match(pattern.regex);
|
|
1894
|
+
if (match) {
|
|
1895
|
+
if (pattern.type === "bold") {
|
|
1896
|
+
tokens.push({ type: "bold", content: match[1] });
|
|
1897
|
+
} else if (pattern.type === "italic") {
|
|
1898
|
+
tokens.push({ type: "italic", content: match[1] });
|
|
1899
|
+
} else if (pattern.type === "link") {
|
|
1900
|
+
tokens.push({ type: "link", text: match[1], url: match[2] });
|
|
1901
|
+
} else if (pattern.type === "newline") {
|
|
1902
|
+
tokens.push({ type: "newline" });
|
|
1903
|
+
}
|
|
1904
|
+
remaining = remaining.slice(match[0].length);
|
|
1905
|
+
matched = true;
|
|
1906
|
+
break;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
if (!matched) {
|
|
1910
|
+
const nextSpecial = remaining.search(/[*[\n]/);
|
|
1911
|
+
if (nextSpecial === -1) {
|
|
1912
|
+
tokens.push({ type: "text", content: remaining });
|
|
1913
|
+
remaining = "";
|
|
1914
|
+
} else if (nextSpecial === 0) {
|
|
1915
|
+
tokens.push({ type: "text", content: remaining[0] });
|
|
1916
|
+
remaining = remaining.slice(1);
|
|
1917
|
+
} else {
|
|
1918
|
+
tokens.push({ type: "text", content: remaining.slice(0, nextSpecial) });
|
|
1919
|
+
remaining = remaining.slice(nextSpecial);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
const merged = [];
|
|
1924
|
+
for (const token of tokens) {
|
|
1925
|
+
const last = merged[merged.length - 1];
|
|
1926
|
+
if (token.type === "text" && last?.type === "text") {
|
|
1927
|
+
last.content += token.content;
|
|
1928
|
+
} else {
|
|
1929
|
+
merged.push(token);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
return merged;
|
|
1933
|
+
}
|
|
1934
|
+
function tokensToElements(tokens) {
|
|
1935
|
+
return tokens.map((token, index) => {
|
|
1936
|
+
switch (token.type) {
|
|
1937
|
+
case "text":
|
|
1938
|
+
return /* @__PURE__ */ jsx10(Fragment4, { children: token.content }, index);
|
|
1939
|
+
case "bold":
|
|
1940
|
+
return /* @__PURE__ */ jsx10("strong", { children: token.content }, index);
|
|
1941
|
+
case "italic":
|
|
1942
|
+
return /* @__PURE__ */ jsx10("em", { children: token.content }, index);
|
|
1943
|
+
case "link":
|
|
1944
|
+
return /* @__PURE__ */ jsx10(
|
|
1945
|
+
"a",
|
|
1946
|
+
{
|
|
1947
|
+
href: token.url,
|
|
1948
|
+
target: "_blank",
|
|
1949
|
+
rel: "noopener noreferrer",
|
|
1950
|
+
className: "text-[var(--color-primary)] underline hover:text-[var(--color-secondary)]",
|
|
1951
|
+
children: token.text
|
|
1952
|
+
},
|
|
1953
|
+
index
|
|
1954
|
+
);
|
|
1955
|
+
case "newline":
|
|
1956
|
+
return /* @__PURE__ */ jsx10("br", {}, index);
|
|
1957
|
+
default:
|
|
1958
|
+
return null;
|
|
1959
|
+
}
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
function parseMarkdownToElements(content) {
|
|
1963
|
+
const tokens = tokenize(content);
|
|
1964
|
+
return tokensToElements(tokens);
|
|
1965
|
+
}
|
|
1966
|
+
function MarkdownText({ content, className }) {
|
|
1967
|
+
const elements = parseMarkdownToElements(content);
|
|
1968
|
+
return /* @__PURE__ */ jsx10("span", { className, children: elements });
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// src/router/Link.tsx
|
|
1972
|
+
import { Link as WouterLink2 } from "wouter";
|
|
1973
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
1974
|
+
function Link2({ to, href, children, className, onClick, replace, ...props }) {
|
|
1975
|
+
const target = href ?? to ?? "/";
|
|
1976
|
+
return /* @__PURE__ */ jsx11(WouterLink2, { href: target, className, onClick, replace, ...props, children });
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// src/router/useNavigate.ts
|
|
1980
|
+
import { useLocation as useLocation2 } from "wouter";
|
|
1981
|
+
function useNavigate() {
|
|
1982
|
+
const [, setLocation] = useLocation2();
|
|
1983
|
+
return (to, options) => {
|
|
1984
|
+
if (options?.replace) {
|
|
1985
|
+
window.history.replaceState(null, "", to);
|
|
1986
|
+
setLocation(to);
|
|
1987
|
+
} else {
|
|
1988
|
+
setLocation(to);
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// src/router/Router.tsx
|
|
1994
|
+
import { Router as WouterRouter } from "wouter";
|
|
1995
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
1996
|
+
function detectBasename() {
|
|
1997
|
+
if (typeof window === "undefined") return "";
|
|
1998
|
+
const sessionMatch = window.location.pathname.match(/^\/session\/[^/]+/);
|
|
1999
|
+
if (sessionMatch) return sessionMatch[0];
|
|
2000
|
+
const previewMatch = window.location.pathname.match(/^\/deploy-preview\/[^/]+/);
|
|
2001
|
+
if (previewMatch) return previewMatch[0];
|
|
2002
|
+
return "";
|
|
2003
|
+
}
|
|
2004
|
+
function Router({ children, base }) {
|
|
2005
|
+
const basename = base ?? detectBasename();
|
|
2006
|
+
return /* @__PURE__ */ jsx12(WouterRouter, { base: basename, children });
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// src/lib/builder-selection.ts
|
|
2010
|
+
var SELECTABLE_SELECTORS = [
|
|
2011
|
+
// Interactive elements
|
|
2012
|
+
"button",
|
|
2013
|
+
// Buttons
|
|
2014
|
+
"a[href]",
|
|
2015
|
+
// Links with href
|
|
2016
|
+
'[role="button"]',
|
|
2017
|
+
// ARIA buttons
|
|
2018
|
+
// Form elements
|
|
2019
|
+
"input",
|
|
2020
|
+
// Input fields (text, checkbox, etc.)
|
|
2021
|
+
"textarea",
|
|
2022
|
+
// Text areas
|
|
2023
|
+
"select",
|
|
2024
|
+
// Dropdown selects
|
|
2025
|
+
"label",
|
|
2026
|
+
// Form labels
|
|
2027
|
+
"form",
|
|
2028
|
+
// Forms as containers
|
|
2029
|
+
// Media elements
|
|
2030
|
+
"img",
|
|
2031
|
+
// Images
|
|
2032
|
+
"video",
|
|
2033
|
+
// Videos
|
|
2034
|
+
"audio",
|
|
2035
|
+
// Audio players
|
|
2036
|
+
"iframe",
|
|
2037
|
+
// Embedded content
|
|
2038
|
+
"svg",
|
|
2039
|
+
// SVG graphics
|
|
2040
|
+
"figure",
|
|
2041
|
+
// Figures with captions
|
|
2042
|
+
"figcaption",
|
|
2043
|
+
// Figure captions
|
|
2044
|
+
// Text/content elements
|
|
2045
|
+
"h1",
|
|
2046
|
+
"h2",
|
|
2047
|
+
"h3",
|
|
2048
|
+
"h4",
|
|
2049
|
+
"h5",
|
|
2050
|
+
"h6",
|
|
2051
|
+
// Headings
|
|
2052
|
+
"p",
|
|
2053
|
+
// Paragraphs
|
|
2054
|
+
"blockquote",
|
|
2055
|
+
// Block quotes
|
|
2056
|
+
"table",
|
|
2057
|
+
// Tables
|
|
2058
|
+
"tr",
|
|
2059
|
+
// Table rows
|
|
2060
|
+
"td",
|
|
2061
|
+
// Table cells
|
|
2062
|
+
"th",
|
|
2063
|
+
// Table headers
|
|
2064
|
+
// List elements
|
|
2065
|
+
"li",
|
|
2066
|
+
// List items (event cards, etc.)
|
|
2067
|
+
"ol",
|
|
2068
|
+
// Ordered lists
|
|
2069
|
+
"ul",
|
|
2070
|
+
// Unordered lists
|
|
2071
|
+
// Other
|
|
2072
|
+
"article",
|
|
2073
|
+
// Article cards
|
|
2074
|
+
"[data-mp-selectable]"
|
|
2075
|
+
// Explicit selectables (backward compatible)
|
|
2076
|
+
];
|
|
2077
|
+
var CONTAINER_SELECTORS = [
|
|
2078
|
+
"section",
|
|
2079
|
+
"[data-mp-editable]",
|
|
2080
|
+
"nav",
|
|
2081
|
+
"header",
|
|
2082
|
+
"footer"
|
|
2083
|
+
];
|
|
2084
|
+
var BuilderSelectionManager = class {
|
|
2085
|
+
enabled = false;
|
|
2086
|
+
selections = /* @__PURE__ */ new Map();
|
|
2087
|
+
hoverOverlay = null;
|
|
2088
|
+
currentHoveredElement = null;
|
|
2089
|
+
// Cache element references for lookup when syncing selections
|
|
2090
|
+
elementMap = /* @__PURE__ */ new Map();
|
|
2091
|
+
// Current selections from parent (for re-rendering on mode change)
|
|
2092
|
+
currentSelections = [];
|
|
2093
|
+
constructor() {
|
|
2094
|
+
if (window.parent === window) {
|
|
2095
|
+
console.log("[BuilderSelection] Not in iframe, skipping initialization");
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
console.log("[BuilderSelection] Initializing in iframe context");
|
|
2099
|
+
this.setupMessageListener();
|
|
2100
|
+
this.createHoverOverlay();
|
|
2101
|
+
this.setupScrollResizeListeners();
|
|
2102
|
+
this.setupKeyboardListener();
|
|
2103
|
+
this.setupWheelForwarding();
|
|
2104
|
+
this.notifyPageReady();
|
|
2105
|
+
window.addEventListener("popstate", () => this.notifyPageReady());
|
|
2106
|
+
}
|
|
2107
|
+
notifyPageReady() {
|
|
2108
|
+
this.sendToParent({
|
|
2109
|
+
type: "PAGE_READY",
|
|
2110
|
+
page: window.location.pathname
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
sendToParent(message) {
|
|
2114
|
+
try {
|
|
2115
|
+
window.parent.postMessage(message, "*");
|
|
2116
|
+
} catch (error) {
|
|
2117
|
+
console.error("[BuilderSelection] Failed to send message to parent:", error);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
setupMessageListener() {
|
|
2121
|
+
window.addEventListener("message", (event) => {
|
|
2122
|
+
const data = event.data;
|
|
2123
|
+
if (!data?.type) return;
|
|
2124
|
+
switch (data.type) {
|
|
2125
|
+
case "SELECTOR_MODE":
|
|
2126
|
+
this.setEnabled(data.enabled ?? false);
|
|
2127
|
+
break;
|
|
2128
|
+
case "SELECTION_SYNC":
|
|
2129
|
+
this.syncSelections(data.selections ?? [], data.currentPage ?? "");
|
|
2130
|
+
break;
|
|
2131
|
+
case "CLEAR_SELECTIONS":
|
|
2132
|
+
this.clearAllSelections();
|
|
2133
|
+
break;
|
|
2134
|
+
}
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
setEnabled(enabled) {
|
|
2138
|
+
console.log("[BuilderSelection] Selector mode:", enabled);
|
|
2139
|
+
this.enabled = enabled;
|
|
2140
|
+
window.__builderSelectModeEnabled = enabled;
|
|
2141
|
+
if (enabled) {
|
|
2142
|
+
document.body.style.cursor = "crosshair";
|
|
2143
|
+
document.body.classList.add("builder-selector-active");
|
|
2144
|
+
this.attachEventListeners();
|
|
2145
|
+
this.renderSelectionMasks();
|
|
2146
|
+
} else {
|
|
2147
|
+
document.body.style.cursor = "";
|
|
2148
|
+
document.body.classList.remove("builder-selector-active");
|
|
2149
|
+
this.detachEventListeners();
|
|
2150
|
+
this.hideHoverOverlay();
|
|
2151
|
+
this.clearAllSelections();
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
attachEventListeners() {
|
|
2155
|
+
document.addEventListener("mouseover", this.handleMouseOver);
|
|
2156
|
+
document.addEventListener("mouseout", this.handleMouseOut);
|
|
2157
|
+
document.addEventListener("click", this.handleClick, true);
|
|
2158
|
+
}
|
|
2159
|
+
detachEventListeners() {
|
|
2160
|
+
document.removeEventListener("mouseover", this.handleMouseOver);
|
|
2161
|
+
document.removeEventListener("mouseout", this.handleMouseOut);
|
|
2162
|
+
document.removeEventListener("click", this.handleClick, true);
|
|
2163
|
+
}
|
|
2164
|
+
handleMouseOver = (e) => {
|
|
2165
|
+
if (!this.enabled) return;
|
|
2166
|
+
const target = this.findDeepestSelectableAt(e.target);
|
|
2167
|
+
if (!target || target === this.currentHoveredElement) return;
|
|
2168
|
+
this.currentHoveredElement = target;
|
|
2169
|
+
this.showHoverOverlay(target);
|
|
2170
|
+
const selectorId = this.generateSelectorId(target);
|
|
2171
|
+
this.sendToParent({
|
|
2172
|
+
type: "ELEMENT_HOVER",
|
|
2173
|
+
selectorId
|
|
2174
|
+
});
|
|
2175
|
+
};
|
|
2176
|
+
handleMouseOut = (e) => {
|
|
2177
|
+
if (!this.enabled) return;
|
|
2178
|
+
const relatedTarget = e.relatedTarget;
|
|
2179
|
+
const stillInSelectable = relatedTarget ? this.findDeepestSelectableAt(relatedTarget) : null;
|
|
2180
|
+
if (!stillInSelectable || stillInSelectable !== this.currentHoveredElement) {
|
|
2181
|
+
this.currentHoveredElement = null;
|
|
2182
|
+
this.hideHoverOverlay();
|
|
2183
|
+
this.sendToParent({
|
|
2184
|
+
type: "ELEMENT_HOVER",
|
|
2185
|
+
selectorId: null
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
handleClick = (e) => {
|
|
2190
|
+
if (!this.enabled) return;
|
|
2191
|
+
const target = this.findDeepestSelectableAt(e.target);
|
|
2192
|
+
if (!target) return;
|
|
2193
|
+
e.preventDefault();
|
|
2194
|
+
e.stopPropagation();
|
|
2195
|
+
const selectorId = this.generateSelectorId(target);
|
|
2196
|
+
const label = this.deriveLabel(target);
|
|
2197
|
+
this.elementMap.set(selectorId, target);
|
|
2198
|
+
this.sendToParent({
|
|
2199
|
+
type: "ELEMENT_CLICKED",
|
|
2200
|
+
selectorId,
|
|
2201
|
+
label,
|
|
2202
|
+
page: window.location.pathname,
|
|
2203
|
+
modifier: e.shiftKey ? "shift" : e.metaKey || e.ctrlKey ? "cmd" : null
|
|
2204
|
+
});
|
|
2205
|
+
};
|
|
2206
|
+
handleKeyDown = (e) => {
|
|
2207
|
+
if (e.key === "Shift") {
|
|
2208
|
+
const activeElement = document.activeElement;
|
|
2209
|
+
const isEditing = activeElement?.closest(".ya-text-editing") || activeElement?.closest(".ya-link-editing");
|
|
2210
|
+
if (!isEditing) {
|
|
2211
|
+
this.sendToParent({ type: "SHIFT_KEY_PRESSED" });
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
};
|
|
2215
|
+
/**
|
|
2216
|
+
* Check if element matches any selectable selector
|
|
2217
|
+
*/
|
|
2218
|
+
isSelectableElement(el) {
|
|
2219
|
+
for (const selector of SELECTABLE_SELECTORS) {
|
|
2220
|
+
if (el.matches(selector)) return true;
|
|
2221
|
+
}
|
|
2222
|
+
for (const selector of CONTAINER_SELECTORS) {
|
|
2223
|
+
if (el.matches(selector)) return true;
|
|
2224
|
+
}
|
|
2225
|
+
return false;
|
|
2226
|
+
}
|
|
2227
|
+
/**
|
|
2228
|
+
* Find the deepest selectable element from the click/hover target
|
|
2229
|
+
* Walks up the DOM tree and returns the first (deepest) selectable found
|
|
2230
|
+
*/
|
|
2231
|
+
findDeepestSelectableAt(target) {
|
|
2232
|
+
if (target.closest(".mp-text-editing")) {
|
|
2233
|
+
return null;
|
|
2234
|
+
}
|
|
2235
|
+
let current = target;
|
|
2236
|
+
const selectables = [];
|
|
2237
|
+
while (current && current !== document.body) {
|
|
2238
|
+
if (this.isSelectableElement(current)) {
|
|
2239
|
+
selectables.push(current);
|
|
2240
|
+
}
|
|
2241
|
+
current = current.parentElement;
|
|
2242
|
+
}
|
|
2243
|
+
return selectables[0] || null;
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* Generate a unique selector ID for an element
|
|
2247
|
+
* Uses data-mp-selectable if available, otherwise generates from context
|
|
2248
|
+
*/
|
|
2249
|
+
generateSelectorId(el) {
|
|
2250
|
+
if (el.dataset.mpSelectable) return el.dataset.mpSelectable;
|
|
2251
|
+
if (el.id) return el.id;
|
|
2252
|
+
const tag = el.tagName.toLowerCase();
|
|
2253
|
+
const parent = el.closest("section, [data-mp-selectable], [data-mp-editable]");
|
|
2254
|
+
const parentId = parent?.dataset?.mpSelectable || parent?.dataset?.mpEditable || parent?.id || "page";
|
|
2255
|
+
const siblings = parent ? Array.from(parent.querySelectorAll(tag)) : [];
|
|
2256
|
+
const index = siblings.indexOf(el);
|
|
2257
|
+
return `${parentId}.${tag}${index > 0 ? `.${index}` : ""}`;
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Derive a human-readable label from the element
|
|
2261
|
+
*/
|
|
2262
|
+
deriveLabel(el) {
|
|
2263
|
+
if (el.dataset.mpSelectable) {
|
|
2264
|
+
return this.toTitleCase(el.dataset.mpSelectable);
|
|
2265
|
+
}
|
|
2266
|
+
const tag = el.tagName.toLowerCase();
|
|
2267
|
+
const text = el.textContent?.trim().slice(0, 30) || "";
|
|
2268
|
+
if (tag === "button" || el.getAttribute("role") === "button") {
|
|
2269
|
+
return text ? `"${text}" Button` : "Button";
|
|
2270
|
+
}
|
|
2271
|
+
if (tag === "a") {
|
|
2272
|
+
return text ? `"${text}" Link` : "Link";
|
|
2273
|
+
}
|
|
2274
|
+
if (tag === "img") {
|
|
2275
|
+
return el.alt || "Image";
|
|
2276
|
+
}
|
|
2277
|
+
if (tag === "video") {
|
|
2278
|
+
return "Video";
|
|
2279
|
+
}
|
|
2280
|
+
if (tag === "audio") {
|
|
2281
|
+
return "Audio Player";
|
|
2282
|
+
}
|
|
2283
|
+
if (tag === "iframe") {
|
|
2284
|
+
return "Embedded Content";
|
|
2285
|
+
}
|
|
2286
|
+
if (tag === "svg") {
|
|
2287
|
+
return "SVG Graphic";
|
|
2288
|
+
}
|
|
2289
|
+
if (tag === "figure") {
|
|
2290
|
+
const caption = el.querySelector("figcaption")?.textContent?.trim();
|
|
2291
|
+
return caption ? `Figure: ${caption.slice(0, 20)}` : "Figure";
|
|
2292
|
+
}
|
|
2293
|
+
if (tag === "figcaption") {
|
|
2294
|
+
return text ? `Caption: ${text.slice(0, 20)}` : "Caption";
|
|
2295
|
+
}
|
|
2296
|
+
if (tag === "input") {
|
|
2297
|
+
const type = el.type || "text";
|
|
2298
|
+
const placeholder = el.placeholder;
|
|
2299
|
+
if (type === "submit") return "Submit Button";
|
|
2300
|
+
if (type === "checkbox") return "Checkbox";
|
|
2301
|
+
if (type === "radio") return "Radio Button";
|
|
2302
|
+
return placeholder ? `"${placeholder}" Input` : `${type.charAt(0).toUpperCase() + type.slice(1)} Input`;
|
|
2303
|
+
}
|
|
2304
|
+
if (tag === "textarea") {
|
|
2305
|
+
const placeholder = el.placeholder;
|
|
2306
|
+
return placeholder ? `"${placeholder}" Text Area` : "Text Area";
|
|
2307
|
+
}
|
|
2308
|
+
if (tag === "select") {
|
|
2309
|
+
return "Dropdown";
|
|
2310
|
+
}
|
|
2311
|
+
if (tag === "label") {
|
|
2312
|
+
return text ? `Label: ${text.slice(0, 20)}` : "Label";
|
|
2313
|
+
}
|
|
2314
|
+
if (tag === "form") {
|
|
2315
|
+
return "Form";
|
|
2316
|
+
}
|
|
2317
|
+
if (["h1", "h2", "h3", "h4", "h5", "h6"].includes(tag)) {
|
|
2318
|
+
return text ? `Heading: ${text.slice(0, 25)}` : `${tag.toUpperCase()} Heading`;
|
|
2319
|
+
}
|
|
2320
|
+
if (tag === "p") {
|
|
2321
|
+
return text ? `Paragraph: ${text.slice(0, 20)}...` : "Paragraph";
|
|
2322
|
+
}
|
|
2323
|
+
if (tag === "blockquote") {
|
|
2324
|
+
return text ? `Quote: ${text.slice(0, 20)}...` : "Block Quote";
|
|
2325
|
+
}
|
|
2326
|
+
if (tag === "table") {
|
|
2327
|
+
return "Table";
|
|
2328
|
+
}
|
|
2329
|
+
if (tag === "tr") {
|
|
2330
|
+
return "Table Row";
|
|
2331
|
+
}
|
|
2332
|
+
if (tag === "td" || tag === "th") {
|
|
2333
|
+
return text ? `Cell: ${text.slice(0, 15)}` : "Table Cell";
|
|
2334
|
+
}
|
|
2335
|
+
if (tag === "ol") {
|
|
2336
|
+
return "Ordered List";
|
|
2337
|
+
}
|
|
2338
|
+
if (tag === "ul") {
|
|
2339
|
+
return "Unordered List";
|
|
2340
|
+
}
|
|
2341
|
+
if (tag === "li") {
|
|
2342
|
+
const preview = text.length > 20 ? text.slice(0, 20) + "..." : text;
|
|
2343
|
+
return preview ? `List Item: ${preview}` : "List Item";
|
|
2344
|
+
}
|
|
2345
|
+
if (tag === "section") {
|
|
2346
|
+
const heading = el.querySelector("h1, h2, h3")?.textContent?.trim();
|
|
2347
|
+
return heading ? `${heading} Section` : "Section";
|
|
2348
|
+
}
|
|
2349
|
+
if (tag === "article") {
|
|
2350
|
+
const heading = el.querySelector("h1, h2, h3, h4")?.textContent?.trim();
|
|
2351
|
+
return heading ? heading : "Article";
|
|
2352
|
+
}
|
|
2353
|
+
if (tag === "nav") {
|
|
2354
|
+
return "Navigation";
|
|
2355
|
+
}
|
|
2356
|
+
if (tag === "header") {
|
|
2357
|
+
return "Header";
|
|
2358
|
+
}
|
|
2359
|
+
if (tag === "footer") {
|
|
2360
|
+
return "Footer";
|
|
2361
|
+
}
|
|
2362
|
+
return text || this.toTitleCase(tag);
|
|
2363
|
+
}
|
|
2364
|
+
/**
|
|
2365
|
+
* Convert kebab-case or tag names to Title Case
|
|
2366
|
+
*/
|
|
2367
|
+
toTitleCase(str) {
|
|
2368
|
+
return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
2369
|
+
}
|
|
2370
|
+
createHoverOverlay() {
|
|
2371
|
+
this.hoverOverlay = document.createElement("div");
|
|
2372
|
+
this.hoverOverlay.id = "builder-hover-overlay";
|
|
2373
|
+
this.hoverOverlay.style.cssText = `
|
|
2374
|
+
position: fixed;
|
|
2375
|
+
pointer-events: none;
|
|
2376
|
+
border: 2px dashed #3B82F6;
|
|
2377
|
+
background: rgba(59, 130, 246, 0.05);
|
|
2378
|
+
z-index: 9999;
|
|
2379
|
+
display: none;
|
|
2380
|
+
transition: all 0.15s ease-out;
|
|
2381
|
+
border-radius: 4px;
|
|
2382
|
+
`;
|
|
2383
|
+
document.body.appendChild(this.hoverOverlay);
|
|
2384
|
+
}
|
|
2385
|
+
setupScrollResizeListeners() {
|
|
2386
|
+
window.addEventListener("scroll", this.updateSelectionPositions, { passive: true });
|
|
2387
|
+
window.addEventListener("resize", this.updateSelectionPositions, { passive: true });
|
|
2388
|
+
}
|
|
2389
|
+
setupKeyboardListener() {
|
|
2390
|
+
document.addEventListener("keydown", this.handleKeyDown);
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Forward wheel events with ctrlKey (pinch zoom) to parent
|
|
2394
|
+
* This allows pinch-to-zoom gestures inside the iframe to control
|
|
2395
|
+
* the parent's zoom state instead of zooming the iframe content
|
|
2396
|
+
*/
|
|
2397
|
+
setupWheelForwarding() {
|
|
2398
|
+
document.addEventListener("wheel", (e) => {
|
|
2399
|
+
if (e.ctrlKey) {
|
|
2400
|
+
e.preventDefault();
|
|
2401
|
+
this.sendToParent({
|
|
2402
|
+
type: "IFRAME_WHEEL",
|
|
2403
|
+
deltaY: e.deltaY,
|
|
2404
|
+
clientX: e.clientX,
|
|
2405
|
+
clientY: e.clientY,
|
|
2406
|
+
ctrlKey: true
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
}, { passive: false });
|
|
2410
|
+
}
|
|
2411
|
+
showHoverOverlay(element) {
|
|
2412
|
+
if (!this.hoverOverlay) return;
|
|
2413
|
+
const rect = element.getBoundingClientRect();
|
|
2414
|
+
this.hoverOverlay.style.top = `${rect.top}px`;
|
|
2415
|
+
this.hoverOverlay.style.left = `${rect.left}px`;
|
|
2416
|
+
this.hoverOverlay.style.width = `${rect.width}px`;
|
|
2417
|
+
this.hoverOverlay.style.height = `${rect.height}px`;
|
|
2418
|
+
this.hoverOverlay.style.display = "block";
|
|
2419
|
+
}
|
|
2420
|
+
hideHoverOverlay() {
|
|
2421
|
+
if (this.hoverOverlay) {
|
|
2422
|
+
this.hoverOverlay.style.display = "none";
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
syncSelections(selections, _currentPage) {
|
|
2426
|
+
console.log("[BuilderSelection] Syncing selections:", selections.length, "current page:", _currentPage);
|
|
2427
|
+
this.currentSelections = selections;
|
|
2428
|
+
if (!this.enabled) {
|
|
2429
|
+
this.clearAllSelections();
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
this.renderSelectionMasks();
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Render selection masks for current selections (only when enabled)
|
|
2436
|
+
*/
|
|
2437
|
+
renderSelectionMasks() {
|
|
2438
|
+
this.clearAllSelections();
|
|
2439
|
+
for (const sel of this.currentSelections) {
|
|
2440
|
+
if (sel.page !== window.location.pathname) continue;
|
|
2441
|
+
const element = this.findElementBySelectorId(sel.selectorId);
|
|
2442
|
+
if (!element) {
|
|
2443
|
+
console.warn(`[BuilderSelection] Element not found for selector: ${sel.selectorId}`);
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
this.renderSelectionIndicator(element, sel.id, sel.color);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
/**
|
|
2450
|
+
* Find element by selector ID (supports both explicit and auto-generated IDs)
|
|
2451
|
+
*/
|
|
2452
|
+
findElementBySelectorId(selectorId) {
|
|
2453
|
+
const explicit = document.querySelector(`[data-mp-selectable="${selectorId}"]`);
|
|
2454
|
+
if (explicit) return explicit;
|
|
2455
|
+
const byId = document.getElementById(selectorId);
|
|
2456
|
+
if (byId) return byId;
|
|
2457
|
+
const cached = this.elementMap.get(selectorId);
|
|
2458
|
+
if (cached && document.body.contains(cached)) return cached;
|
|
2459
|
+
const parts = selectorId.split(".");
|
|
2460
|
+
if (parts.length >= 2) {
|
|
2461
|
+
const parentId = parts[0];
|
|
2462
|
+
const tag = parts[1];
|
|
2463
|
+
const index = parts.length > 2 ? parseInt(parts[2], 10) : 0;
|
|
2464
|
+
let parent = null;
|
|
2465
|
+
if (parentId === "page") {
|
|
2466
|
+
parent = document.body;
|
|
2467
|
+
} else {
|
|
2468
|
+
parent = document.querySelector(`[data-mp-selectable="${parentId}"]`) || document.querySelector(`[data-mp-editable="${parentId}"]`) || document.getElementById(parentId) || document.querySelector(`section#${parentId}`);
|
|
2469
|
+
}
|
|
2470
|
+
if (parent) {
|
|
2471
|
+
const candidates = parent.querySelectorAll(tag);
|
|
2472
|
+
if (candidates[index]) {
|
|
2473
|
+
return candidates[index];
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
return null;
|
|
2478
|
+
}
|
|
2479
|
+
clearAllSelections() {
|
|
2480
|
+
this.selections.forEach(({ container }) => {
|
|
2481
|
+
container.remove();
|
|
2482
|
+
});
|
|
2483
|
+
this.selections.clear();
|
|
2484
|
+
}
|
|
2485
|
+
/**
|
|
2486
|
+
* Update positions of selection overlays when viewport changes (scroll/resize)
|
|
2487
|
+
*/
|
|
2488
|
+
updateSelectionPositions = () => {
|
|
2489
|
+
this.selections.forEach(({ element, container }) => {
|
|
2490
|
+
if (!document.body.contains(element)) {
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2493
|
+
const rect = element.getBoundingClientRect();
|
|
2494
|
+
container.style.top = `${rect.top}px`;
|
|
2495
|
+
container.style.left = `${rect.left}px`;
|
|
2496
|
+
container.style.width = `${rect.width}px`;
|
|
2497
|
+
container.style.height = `${rect.height}px`;
|
|
2498
|
+
});
|
|
2499
|
+
};
|
|
2500
|
+
renderSelectionIndicator(element, selectionId, color) {
|
|
2501
|
+
const rect = element.getBoundingClientRect();
|
|
2502
|
+
const container = document.createElement("div");
|
|
2503
|
+
container.className = "builder-selection-container";
|
|
2504
|
+
container.dataset.selectionId = selectionId;
|
|
2505
|
+
container.style.cssText = `
|
|
2506
|
+
position: fixed;
|
|
2507
|
+
top: ${rect.top}px;
|
|
2508
|
+
left: ${rect.left}px;
|
|
2509
|
+
width: ${rect.width}px;
|
|
2510
|
+
height: ${rect.height}px;
|
|
2511
|
+
pointer-events: none;
|
|
2512
|
+
z-index: 9999;
|
|
2513
|
+
`;
|
|
2514
|
+
const border = document.createElement("div");
|
|
2515
|
+
border.className = "builder-selection-border";
|
|
2516
|
+
border.style.cssText = `
|
|
2517
|
+
position: absolute;
|
|
2518
|
+
inset: 0;
|
|
2519
|
+
border: 2px solid ${color};
|
|
2520
|
+
border-radius: 4px;
|
|
2521
|
+
pointer-events: none;
|
|
2522
|
+
`;
|
|
2523
|
+
container.appendChild(border);
|
|
2524
|
+
const badge = document.createElement("div");
|
|
2525
|
+
badge.textContent = selectionId;
|
|
2526
|
+
badge.className = "builder-selection-badge";
|
|
2527
|
+
badge.style.cssText = `
|
|
2528
|
+
position: absolute;
|
|
2529
|
+
top: 8px;
|
|
2530
|
+
left: 8px;
|
|
2531
|
+
background: ${color};
|
|
2532
|
+
color: white;
|
|
2533
|
+
font-size: 11px;
|
|
2534
|
+
font-weight: 600;
|
|
2535
|
+
padding: 4px 10px;
|
|
2536
|
+
border-radius: 9999px;
|
|
2537
|
+
pointer-events: none;
|
|
2538
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
2539
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
2540
|
+
`;
|
|
2541
|
+
container.appendChild(badge);
|
|
2542
|
+
document.body.appendChild(container);
|
|
2543
|
+
this.selections.set(selectionId, { element, container, badge, border });
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
function initBuilderSelection() {
|
|
2547
|
+
if (typeof window !== "undefined" && window.parent !== window) {
|
|
2548
|
+
if (document.readyState === "loading") {
|
|
2549
|
+
document.addEventListener("DOMContentLoaded", () => new BuilderSelectionManager());
|
|
2550
|
+
} else {
|
|
2551
|
+
new BuilderSelectionManager();
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
export {
|
|
2556
|
+
ContentStoreProvider,
|
|
2557
|
+
ContentStoreProvider2 as ContentStoreProviderProd,
|
|
2558
|
+
Link2 as Link,
|
|
2559
|
+
MarkdownText,
|
|
2560
|
+
Router,
|
|
2561
|
+
SafeHtml,
|
|
2562
|
+
MpImage as StaticImage,
|
|
2563
|
+
MpText as StaticText,
|
|
2564
|
+
YaImage,
|
|
2565
|
+
YaLink,
|
|
2566
|
+
YaText,
|
|
2567
|
+
contentRegistry,
|
|
2568
|
+
getAllContent,
|
|
2569
|
+
getContent,
|
|
2570
|
+
hasContent,
|
|
2571
|
+
initBuilderSelection,
|
|
2572
|
+
registerContent,
|
|
2573
|
+
resolveAssetUrl,
|
|
2574
|
+
serializeImageValue,
|
|
2575
|
+
setAssetResolver,
|
|
2576
|
+
useContentStore,
|
|
2577
|
+
useContentStore2 as useContentStoreProd,
|
|
2578
|
+
useNavigate
|
|
2579
|
+
};
|