kritzel-stencil 0.3.21 → 0.3.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.cjs.js +3 -1
- package/dist/cjs/kritzel-active-users_45.cjs.entry.js +507 -288
- package/dist/cjs/{schema.constants-IBG5VW_J.js → schema.constants-Lr_zNkox.js} +713 -47
- package/dist/collection/classes/core/core.class.js +83 -5
- package/dist/collection/classes/core/store.class.js +9 -0
- package/dist/collection/classes/handlers/key.handler.js +1 -1
- package/dist/collection/classes/handlers/selection.handler.js +13 -9
- package/dist/collection/classes/objects/base-object.class.js +20 -8
- package/dist/collection/classes/objects/custom-element.class.js +330 -8
- package/dist/collection/classes/objects/image.class.js +0 -1
- package/dist/collection/classes/objects/selection-group.class.js +12 -0
- package/dist/collection/classes/providers/assets/asset-resolver.class.js +24 -14
- package/dist/collection/classes/providers/assets/indexeddb-asset-provider.class.js +0 -8
- package/dist/collection/classes/registries/custom-element-renderer.registry.js +52 -0
- package/dist/collection/classes/structures/app-state-map.structure.js +17 -3
- package/dist/collection/classes/structures/object-map.structure.js +17 -3
- package/dist/collection/classes/tools/selection-tool.class.js +34 -6
- package/dist/collection/components/core/kritzel-editor/kritzel-editor.js +9 -8
- package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +211 -78
- package/dist/collection/components/ui/kritzel-settings/kritzel-settings.js +2 -1
- package/dist/collection/configs/default-engine-config.js +3 -2
- package/dist/collection/constants/version.js +1 -1
- package/dist/collection/helpers/html.helper.js +52 -0
- package/dist/collection/helpers/object.helper.js +3 -0
- package/dist/collection/helpers/svg-export.helper.js +35 -0
- package/dist/collection/helpers/wheel.helper.js +113 -0
- package/dist/collection/index.js +3 -0
- package/dist/collection/interfaces/custom-element-renderer.interface.js +1 -0
- package/dist/components/index.js +1 -1
- package/dist/components/kritzel-awareness-cursors.js +1 -1
- package/dist/components/kritzel-controls.js +1 -1
- package/dist/components/kritzel-current-user-dialog.js +1 -1
- package/dist/components/kritzel-current-user.js +1 -1
- package/dist/components/kritzel-dialog.js +1 -1
- package/dist/components/kritzel-editor.js +1 -1
- package/dist/components/kritzel-engine.js +1 -1
- package/dist/components/kritzel-export.js +1 -1
- package/dist/components/kritzel-login-dialog.js +1 -1
- package/dist/components/kritzel-menu-item.js +1 -1
- package/dist/components/kritzel-menu.js +1 -1
- package/dist/components/kritzel-more-menu.js +1 -1
- package/dist/components/kritzel-portal.js +1 -1
- package/dist/components/kritzel-settings.js +1 -1
- package/dist/components/kritzel-share-dialog.js +1 -1
- package/dist/components/kritzel-split-button.js +1 -1
- package/dist/components/kritzel-tool-config.js +1 -1
- package/dist/components/kritzel-tooltip.js +1 -1
- package/dist/components/kritzel-workspace-manager.js +1 -1
- package/dist/components/{p-BwuemYET.js → p-B4WfPDj0.js} +1 -1
- package/dist/components/p-BNu3Akp5.js +1 -0
- package/dist/components/p-BYveczcb.js +1 -0
- package/dist/components/{p-D28ZylHn.js → p-BixEMVtW.js} +1 -1
- package/dist/components/{p-Ndib1Jb3.js → p-BntMEYss.js} +1 -1
- package/dist/components/p-C48bv_0f.js +9 -0
- package/dist/components/{p-BO1CnEOT.js → p-CPMpf9ku.js} +1 -1
- package/dist/components/{p-BhODnvMO.js → p-CZQz_mul.js} +1 -1
- package/dist/components/{p-COj8EVzL.js → p-Ckpa5tHF.js} +1 -1
- package/dist/components/{p-BPoDE87Q.js → p-DJ3KI725.js} +1 -1
- package/dist/components/{p-DRsD38Nl.js → p-DPlEzPKO.js} +1 -1
- package/dist/components/{p-C-RiEw9X.js → p-DSd5xTAH.js} +1 -1
- package/dist/components/{p-CbMLbDou.js → p-DTqMF94b.js} +1 -1
- package/dist/components/{p-CHrSFOSI.js → p-DX2nriaq.js} +1 -1
- package/dist/components/{p-BAN5dnHX.js → p-Dd-pMvlr.js} +1 -1
- package/dist/components/{p-C5TZjIE1.js → p-M9m9-OEY.js} +1 -1
- package/dist/components/p-WUdQqYnu.js +1 -0
- package/dist/components/{p-YFR180v-.js → p-XDNjFYRB.js} +1 -1
- package/dist/components/{p-CJEIUs0x.js → p-X_pyPCe8.js} +1 -1
- package/dist/components/{p-CpR9lc-o.js → p-meY80XnF.js} +1 -1
- package/dist/components/{p-6Ca7Eolf.js → p-y5T0_Wvi.js} +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/kritzel-active-users_45.entry.js +500 -281
- package/dist/esm/{schema.constants-D35cwCYd.js → schema.constants-DMPBDrZh.js} +711 -47
- package/dist/stencil/index.esm.js +1 -1
- package/dist/stencil/p-27d41f6f.entry.js +9 -0
- package/dist/stencil/p-DMPBDrZh.js +1 -0
- package/dist/stencil/stencil.esm.js +1 -1
- package/dist/types/classes/core/core.class.d.ts +15 -0
- package/dist/types/classes/core/store.class.d.ts +6 -0
- package/dist/types/classes/objects/base-object.class.d.ts +3 -2
- package/dist/types/classes/objects/custom-element.class.d.ts +87 -21
- package/dist/types/classes/objects/image.class.d.ts +0 -1
- package/dist/types/classes/objects/selection-group.class.d.ts +1 -0
- package/dist/types/classes/providers/assets/asset-resolver.class.d.ts +3 -0
- package/dist/types/classes/providers/assets/indexeddb-asset-provider.class.d.ts +0 -1
- package/dist/types/classes/registries/custom-element-renderer.registry.d.ts +32 -0
- package/dist/types/components/core/kritzel-engine/kritzel-engine.d.ts +7 -1
- package/dist/types/components.d.ts +2 -2
- package/dist/types/constants/version.d.ts +1 -1
- package/dist/types/helpers/html.helper.d.ts +2 -0
- package/dist/types/helpers/object.helper.d.ts +1 -0
- package/dist/types/helpers/svg-export.helper.d.ts +9 -0
- package/dist/types/helpers/wheel.helper.d.ts +9 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/interfaces/asset-storage-config.interface.d.ts +0 -4
- package/dist/types/interfaces/custom-element-renderer.interface.d.ts +40 -0
- package/dist/types/interfaces/debug-info.interface.d.ts +1 -0
- package/dist/types/interfaces/object.interface.d.ts +0 -1
- package/package.json +2 -2
- package/dist/components/p-BML28BJR.js +0 -1
- package/dist/components/p-BYzaazty.js +0 -1
- package/dist/components/p-CVV-HzFR.js +0 -1
- package/dist/components/p-DWhWFDcC.js +0 -9
- package/dist/stencil/p-60842099.entry.js +0 -9
- package/dist/stencil/p-D35cwCYd.js +0 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { KritzelBaseObject } from "./base-object.class";
|
|
2
2
|
import { KritzelMathHelper } from "../../helpers/math.helper";
|
|
3
|
+
import { KritzelCustomElementRendererRegistry } from "../registries/custom-element-renderer.registry";
|
|
4
|
+
import { KritzelHTMLHelper } from "../../helpers/html.helper";
|
|
3
5
|
/**
|
|
4
6
|
* Represents a custom HTML element that can be placed and manipulated on the Kritzel canvas.
|
|
5
7
|
* Extends KritzelBaseObject to inherit common object functionality like positioning,
|
|
@@ -9,8 +11,17 @@ export class KritzelCustomElement extends KritzelBaseObject {
|
|
|
9
11
|
__class__ = 'KritzelCustomElement';
|
|
10
12
|
/** The HTML element to be rendered on the canvas. */
|
|
11
13
|
element;
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Optional identifier for a registered renderer (e.g., 'angular-component', 'react-default').
|
|
16
|
+
* When set, mount() resolves the renderer from a global registry and delegates lifecycle.
|
|
17
|
+
* If null, falls back to DOM element mounting.
|
|
18
|
+
*/
|
|
19
|
+
rendererKey = null;
|
|
20
|
+
/**
|
|
21
|
+
* Custom data payload for the renderer (e.g., Angular component inputs, React props, Vue data).
|
|
22
|
+
* Only meaningful when rendererKey is set. Captured on unmount and restored on rehydrate.
|
|
23
|
+
*/
|
|
24
|
+
rendererData;
|
|
14
25
|
/**
|
|
15
26
|
* Creates a new KritzelCustomElement instance.
|
|
16
27
|
* @param config - Optional configuration object for the custom element.
|
|
@@ -28,9 +39,11 @@ export class KritzelCustomElement extends KritzelBaseObject {
|
|
|
28
39
|
this.translateY = config.translateY || 0;
|
|
29
40
|
this.rotation = KritzelMathHelper.degreesToRadians(config.rotation ?? 0);
|
|
30
41
|
this.scale = config.scale || 1;
|
|
31
|
-
this.element = config.element;
|
|
42
|
+
this.element = config.element ?? this.createPlaceholder();
|
|
32
43
|
this.height = config.height || 0;
|
|
33
44
|
this.width = config.width || 0;
|
|
45
|
+
this.rendererKey = config.rendererKey ?? null;
|
|
46
|
+
this.rendererData = config.rendererData;
|
|
34
47
|
}
|
|
35
48
|
}
|
|
36
49
|
/**
|
|
@@ -52,11 +65,13 @@ export class KritzelCustomElement extends KritzelBaseObject {
|
|
|
52
65
|
object.id = object.generateId();
|
|
53
66
|
object.workspaceId = core.store.state.activeWorkspace.id;
|
|
54
67
|
object.userId = core.user?.id;
|
|
68
|
+
object.zIndex = core.store.currentZIndex;
|
|
55
69
|
return object;
|
|
56
70
|
}
|
|
57
71
|
/**
|
|
58
72
|
* Mounts the custom element to a container element in the DOM.
|
|
59
|
-
*
|
|
73
|
+
* If a renderer is registered for this.rendererKey, delegates to the renderer.
|
|
74
|
+
* Otherwise, uses the standard DOM element mounting fallback.
|
|
60
75
|
* Skips mounting if already mounted to the same container with content attached.
|
|
61
76
|
* @param element - The container HTML element to mount this custom element into.
|
|
62
77
|
*/
|
|
@@ -64,15 +79,268 @@ export class KritzelCustomElement extends KritzelBaseObject {
|
|
|
64
79
|
if (element === null) {
|
|
65
80
|
return;
|
|
66
81
|
}
|
|
67
|
-
|
|
68
|
-
if (this.isMounted && this.elementRef === element
|
|
69
|
-
|
|
82
|
+
this.applyInteractiveState();
|
|
83
|
+
if (this.isMounted && this.elementRef === element) {
|
|
84
|
+
// Renderer-based mounts should not be re-invoked on every engine rerender.
|
|
85
|
+
if (this.rendererKey) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// For direct DOM fallback, remount only if content was detached.
|
|
89
|
+
if (this.element.parentElement === element) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
70
92
|
}
|
|
71
93
|
this.elementRef = element;
|
|
72
94
|
this.isMounted = true;
|
|
73
|
-
//
|
|
95
|
+
// Try renderer-based approach if a key is set
|
|
96
|
+
if (this.rendererKey) {
|
|
97
|
+
const renderer = KritzelCustomElementRendererRegistry.get(this.rendererKey);
|
|
98
|
+
if (renderer) {
|
|
99
|
+
element.innerHTML = ''; // Clear container
|
|
100
|
+
renderer.onMount({
|
|
101
|
+
object: this,
|
|
102
|
+
container: element,
|
|
103
|
+
data: this.rendererData,
|
|
104
|
+
});
|
|
105
|
+
this.applyInteractiveState();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Fallback to standard DOM mounting
|
|
74
110
|
this.elementRef.innerHTML = '';
|
|
75
111
|
this.elementRef.appendChild(this.element);
|
|
112
|
+
this.applyInteractiveState();
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Updates interactivity state for embedded HTML content.
|
|
116
|
+
* When disabling interactivity, any focused descendant is blurred so
|
|
117
|
+
* keyboard input is released immediately.
|
|
118
|
+
*/
|
|
119
|
+
setIsInteractive(isInteractive) {
|
|
120
|
+
const hasChanged = this.isInteractive !== isInteractive;
|
|
121
|
+
this.isInteractive = isInteractive;
|
|
122
|
+
this.applyInteractiveState();
|
|
123
|
+
if (hasChanged && !isInteractive) {
|
|
124
|
+
this.blurFocusedDescendant();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
applyInteractiveState() {
|
|
128
|
+
const pointerEvents = this.isInteractive ? 'auto' : 'none';
|
|
129
|
+
const contentCursor = this.isInteractive ? 'auto' : '';
|
|
130
|
+
if (this.element) {
|
|
131
|
+
this.element.style.pointerEvents = pointerEvents;
|
|
132
|
+
this.element.style.cursor = contentCursor;
|
|
133
|
+
}
|
|
134
|
+
if (this.elementRef instanceof HTMLElement) {
|
|
135
|
+
// Keep the mount container clickable so unselected custom elements can still be selected.
|
|
136
|
+
this.elementRef.style.pointerEvents = 'auto';
|
|
137
|
+
this.elementRef.style.cursor = '';
|
|
138
|
+
Array.from(this.elementRef.children).forEach(child => {
|
|
139
|
+
if (child instanceof HTMLElement) {
|
|
140
|
+
child.style.pointerEvents = pointerEvents;
|
|
141
|
+
child.style.cursor = contentCursor;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
blurFocusedDescendant() {
|
|
147
|
+
if (!this.element && !(this.elementRef instanceof HTMLElement)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const activeElement = this.getDeepActiveElement();
|
|
151
|
+
const isInsideElement = !!this.element && this.isNodeInsideHost(activeElement, this.element);
|
|
152
|
+
const isInsideMountedContainer = this.elementRef instanceof HTMLElement && this.isNodeInsideHost(activeElement, this.elementRef);
|
|
153
|
+
if (activeElement && (isInsideElement || isInsideMountedContainer)) {
|
|
154
|
+
activeElement.blur();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Returns true when a text-input-like element inside this custom element
|
|
159
|
+
* currently owns focus.
|
|
160
|
+
*/
|
|
161
|
+
hasFocusedInputElement() {
|
|
162
|
+
if (!this.isInteractive) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
const focusHosts = this.getFocusHosts();
|
|
166
|
+
// Primary path: locate the focused input-like element by walking from each host through
|
|
167
|
+
// `:focus` (piercing open shadow roots). This works in real browsers even when the engine
|
|
168
|
+
// and editor are nested web components, because it never relies on the document-level
|
|
169
|
+
// active element resolving to a descendant of the host.
|
|
170
|
+
if (focusHosts.some(host => this.hasFocusedInputDescendant(host))) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
// Fallback path: resolve the deep active element and confirm it is input-like and inside a
|
|
174
|
+
// host. Covers contenteditable content inside nested shadow roots (which `querySelector`
|
|
175
|
+
// cannot reach) and test environments that mock the active element without real focus.
|
|
176
|
+
const activeElement = this.getDeepActiveElement();
|
|
177
|
+
if (activeElement && this.isInputLikeElement(activeElement) && focusHosts.some(host => this.isNodeInsideHost(activeElement, host))) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
getFocusHosts() {
|
|
183
|
+
const hosts = [];
|
|
184
|
+
if (this.element) {
|
|
185
|
+
hosts.push(this.element);
|
|
186
|
+
}
|
|
187
|
+
if (this.elementRef instanceof HTMLElement) {
|
|
188
|
+
hosts.push(this.elementRef);
|
|
189
|
+
const firstChild = this.elementRef.firstElementChild;
|
|
190
|
+
if (firstChild instanceof HTMLElement) {
|
|
191
|
+
hosts.push(firstChild);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return hosts;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Returns true when the given host (or, via the flat tree, one of its shadow descendants)
|
|
198
|
+
* currently contains a focused input-like element.
|
|
199
|
+
*/
|
|
200
|
+
hasFocusedInputDescendant(host) {
|
|
201
|
+
if (this.isInputLikeElement(host) && this.matchesFocus(host)) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
// `:focus-within` crosses shadow boundaries for the host, so it is a cheap guard that tells
|
|
205
|
+
// us whether focus lives anywhere inside this host before doing the deeper traversal.
|
|
206
|
+
if (typeof host.matches === 'function') {
|
|
207
|
+
try {
|
|
208
|
+
if (!host.matches(':focus-within')) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// Selector unsupported in this environment; fall through to the traversal below.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return this.findFocusedInputLikeElement(host) !== null;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Walks the `:focus` chain from a tree scope, descending into open shadow roots, and returns
|
|
220
|
+
* the focused element when it is input-like.
|
|
221
|
+
*/
|
|
222
|
+
findFocusedInputLikeElement(root) {
|
|
223
|
+
let focused = null;
|
|
224
|
+
try {
|
|
225
|
+
focused = root.querySelector(':focus');
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
focused = null;
|
|
229
|
+
}
|
|
230
|
+
if (!(focused instanceof HTMLElement)) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
if (this.isInputLikeElement(focused)) {
|
|
234
|
+
return focused;
|
|
235
|
+
}
|
|
236
|
+
if (focused.shadowRoot) {
|
|
237
|
+
return this.findFocusedInputLikeElement(focused.shadowRoot);
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
matchesFocus(element) {
|
|
242
|
+
if (typeof element.matches !== 'function') {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
return element.matches(':focus');
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
isInputLikeElement(element) {
|
|
253
|
+
const tag = element.tagName;
|
|
254
|
+
return (tag === 'INPUT' ||
|
|
255
|
+
tag === 'TEXTAREA' ||
|
|
256
|
+
tag === 'SELECT' ||
|
|
257
|
+
element.isContentEditable);
|
|
258
|
+
}
|
|
259
|
+
isNodeInsideHost(node, host) {
|
|
260
|
+
if (!node) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
let current = node;
|
|
264
|
+
while (current) {
|
|
265
|
+
if (current === host) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
if (current.parentNode) {
|
|
269
|
+
current = current.parentNode;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const root = current.getRootNode();
|
|
273
|
+
if (root instanceof ShadowRoot) {
|
|
274
|
+
current = root.host;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
getDeepActiveElement(root = document) {
|
|
282
|
+
const activeElement = root.activeElement;
|
|
283
|
+
if (!(activeElement instanceof HTMLElement)) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
if (activeElement.shadowRoot) {
|
|
287
|
+
const nested = this.getDeepActiveElement(activeElement.shadowRoot);
|
|
288
|
+
return nested ?? activeElement;
|
|
289
|
+
}
|
|
290
|
+
return activeElement;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Unmounts the custom element and performs cleanup.
|
|
294
|
+
* If a renderer is registered, delegates to renderer.onUnmount and captures returned state.
|
|
295
|
+
* Otherwise, simply removes from DOM.
|
|
296
|
+
* Called when object is deleted, viewport culled, or editor is unloading.
|
|
297
|
+
*/
|
|
298
|
+
unmount() {
|
|
299
|
+
if (!this.isMounted || !this.elementRef) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
// Try renderer-based unmount if renderer is registered
|
|
303
|
+
if (this.rendererKey) {
|
|
304
|
+
const renderer = KritzelCustomElementRendererRegistry.get(this.rendererKey);
|
|
305
|
+
if (renderer) {
|
|
306
|
+
// Capture any updated data returned by renderer
|
|
307
|
+
const updatedData = renderer.onUnmount({
|
|
308
|
+
object: this,
|
|
309
|
+
container: this.elementRef,
|
|
310
|
+
data: this.rendererData,
|
|
311
|
+
});
|
|
312
|
+
if (updatedData !== undefined) {
|
|
313
|
+
this.rendererData = updatedData;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
this.isMounted = false;
|
|
318
|
+
this.elementRef = null;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Returns whether the rendered content contains a scrollable element.
|
|
322
|
+
* Traverses light DOM, open shadow roots, and slotted content so nested
|
|
323
|
+
* framework components are detected as well.
|
|
324
|
+
*/
|
|
325
|
+
isElementScrollable() {
|
|
326
|
+
return !!KritzelHTMLHelper.findScrollableElement(this.element);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Creates a placeholder element when HTML deserialization fails.
|
|
330
|
+
* Used as fallback to ensure element is always defined.
|
|
331
|
+
* @returns A div element with placeholder styling
|
|
332
|
+
*/
|
|
333
|
+
createPlaceholder() {
|
|
334
|
+
const el = document.createElement('div');
|
|
335
|
+
el.textContent = '[Custom Element]';
|
|
336
|
+
el.style.border = '1px dashed #ccc';
|
|
337
|
+
el.style.display = 'flex';
|
|
338
|
+
el.style.alignItems = 'center';
|
|
339
|
+
el.style.justifyContent = 'center';
|
|
340
|
+
el.style.backgroundColor = '#f9f9f9';
|
|
341
|
+
el.style.color = '#999';
|
|
342
|
+
el.style.fontSize = '12px';
|
|
343
|
+
return el;
|
|
76
344
|
}
|
|
77
345
|
/**
|
|
78
346
|
* Resizes and repositions the custom element on the canvas.
|
|
@@ -109,7 +377,61 @@ export class KritzelCustomElement extends KritzelBaseObject {
|
|
|
109
377
|
Object.assign(copiedObject, this);
|
|
110
378
|
copiedObject.id = this.generateId();
|
|
111
379
|
copiedObject.isMounted = false;
|
|
380
|
+
copiedObject.isSelected = false;
|
|
381
|
+
copiedObject.isHovered = false;
|
|
382
|
+
copiedObject.isInteractive = false;
|
|
383
|
+
copiedObject.resizing = false;
|
|
384
|
+
copiedObject.markedForRemoval = false;
|
|
385
|
+
copiedObject.isVisible = true;
|
|
112
386
|
copiedObject.element = this.element.cloneNode(true);
|
|
113
387
|
return copiedObject;
|
|
114
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* Serializes the custom element to a plain JavaScript object for persistence.
|
|
391
|
+
* Stores HTML as a string (safe for JSON/Yjs) and captures dimension metadata.
|
|
392
|
+
* @returns A serializable object with htmlString and dimension snapshots
|
|
393
|
+
*/
|
|
394
|
+
serialize() {
|
|
395
|
+
const { _core, _elementRef, element, totalWidth, totalHeight, isSelected, isHovered, isMounted, isInteractive, resizing, markedForRemoval, isVisible, ...props } = this;
|
|
396
|
+
const cloned = structuredClone(props);
|
|
397
|
+
// Include rendererKey and rendererData for lifecycle-driven rendering
|
|
398
|
+
cloned.rendererKey = this.rendererKey;
|
|
399
|
+
cloned.rendererData = this.rendererData;
|
|
400
|
+
// Store HTML as string for fallback/compatibility when renderer is unavailable
|
|
401
|
+
cloned.htmlString = element ? element.outerHTML : '';
|
|
402
|
+
// Capture dimension snapshot at serialization time
|
|
403
|
+
cloned.serializedWidth = this.width;
|
|
404
|
+
cloned.serializedHeight = this.height;
|
|
405
|
+
return cloned;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Deserializes the custom element from a plain object.
|
|
409
|
+
* Reconstructs the element from stored HTML string with fallback to placeholder.
|
|
410
|
+
* @template T - The expected return type
|
|
411
|
+
* @param object - The serialized object data to restore from
|
|
412
|
+
* @returns This instance cast to type T
|
|
413
|
+
*/
|
|
414
|
+
deserialize(object) {
|
|
415
|
+
super.deserialize(object);
|
|
416
|
+
// Legacy compatibility: this deprecated field should not persist on revived objects.
|
|
417
|
+
delete this.allowEmbeddedInteraction;
|
|
418
|
+
// Restore renderer metadata
|
|
419
|
+
this.rendererKey = object.rendererKey ?? null;
|
|
420
|
+
this.rendererData = object.rendererData;
|
|
421
|
+
// Reconstruct element from stored HTML, or create placeholder
|
|
422
|
+
if (object.htmlString) {
|
|
423
|
+
try {
|
|
424
|
+
const temp = document.createElement('div');
|
|
425
|
+
temp.innerHTML = object.htmlString;
|
|
426
|
+
this.element = temp.firstElementChild || this.createPlaceholder();
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
this.element = this.createPlaceholder();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
this.element = this.createPlaceholder();
|
|
434
|
+
}
|
|
435
|
+
return this;
|
|
436
|
+
}
|
|
115
437
|
}
|
|
@@ -39,7 +39,6 @@ export class KritzelImage extends KritzelBaseObject {
|
|
|
39
39
|
maxWidth = 300;
|
|
40
40
|
maxHeight = 300;
|
|
41
41
|
maxCompressionSize = 1200;
|
|
42
|
-
isDebugInfoVisible = true;
|
|
43
42
|
// --- Transient (not persisted to Yjs) ---
|
|
44
43
|
/**
|
|
45
44
|
* URL resolved from the asset storage layer (e.g. a `blob:` or https URL).
|
|
@@ -82,6 +82,12 @@ export class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
82
82
|
this._cachedObjectIdsHash = currentHash;
|
|
83
83
|
return this._cachedObjects;
|
|
84
84
|
}
|
|
85
|
+
syncChildSelectionState(children) {
|
|
86
|
+
const shouldBeSelected = this.isSelected === true;
|
|
87
|
+
for (const child of children) {
|
|
88
|
+
child.isSelected = shouldBeSelected;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
85
91
|
/**
|
|
86
92
|
* Gets the number of objects in the selection group.
|
|
87
93
|
* @returns The count of selected objects
|
|
@@ -127,6 +133,7 @@ export class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
127
133
|
for (const object of objects) {
|
|
128
134
|
if (!this._objectIds.includes(object.id)) {
|
|
129
135
|
this._objectIds.push(object.id);
|
|
136
|
+
object.isSelected = true;
|
|
130
137
|
changed = true;
|
|
131
138
|
}
|
|
132
139
|
}
|
|
@@ -154,9 +161,11 @@ export class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
154
161
|
const index = this._objectIds.findIndex(id => id === object.id);
|
|
155
162
|
if (index === -1) {
|
|
156
163
|
this._objectIds.push(object.id);
|
|
164
|
+
object.isSelected = true;
|
|
157
165
|
}
|
|
158
166
|
else {
|
|
159
167
|
this._objectIds.splice(index, 1);
|
|
168
|
+
object.isSelected = false;
|
|
160
169
|
}
|
|
161
170
|
this._cachedObjects = null; // Invalidate cache
|
|
162
171
|
const children = this.objects; // Get cached objects once
|
|
@@ -298,6 +307,8 @@ export class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
298
307
|
deserialize(object) {
|
|
299
308
|
// First, deserialize all base properties using parent's deserialize
|
|
300
309
|
super.deserialize(object);
|
|
310
|
+
// Selection groups intentionally persist their selected state.
|
|
311
|
+
this.isSelected = object.isSelected === true;
|
|
301
312
|
// Restore objectIds through setter
|
|
302
313
|
if (object.objectIds) {
|
|
303
314
|
this.objectIds = object.objectIds;
|
|
@@ -306,6 +317,7 @@ export class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
306
317
|
if (object.unchangedObjectSnapshots) {
|
|
307
318
|
this.unchangedObjectSnapshots = new Map(Object.entries(object.unchangedObjectSnapshots));
|
|
308
319
|
}
|
|
320
|
+
this.syncChildSelectionState(this.objects);
|
|
309
321
|
return this;
|
|
310
322
|
}
|
|
311
323
|
/**
|
|
@@ -60,9 +60,13 @@ export class KritzelAssetResolver {
|
|
|
60
60
|
_pending = new Map();
|
|
61
61
|
_flushTimer = null;
|
|
62
62
|
_destroyed = false;
|
|
63
|
-
_quiet =
|
|
63
|
+
_quiet = true;
|
|
64
64
|
/** Dedupes concurrent resolves of the same id. */
|
|
65
65
|
_resolvePromises = new Map();
|
|
66
|
+
/** Enables or disables resolver diagnostic logging. */
|
|
67
|
+
setShowInfo(show) {
|
|
68
|
+
this._quiet = !show;
|
|
69
|
+
}
|
|
66
70
|
/**
|
|
67
71
|
* Whether any remote provider is configured. Used by the engine to
|
|
68
72
|
* decide whether to warn about a missing remote provider when Yjs is
|
|
@@ -88,7 +92,6 @@ export class KritzelAssetResolver {
|
|
|
88
92
|
// Ensure the deferred ready promise exists before any async work so
|
|
89
93
|
// concurrent callers awaiting `ensureReady()` are unblocked together.
|
|
90
94
|
this.getReadyPromise();
|
|
91
|
-
this._quiet = config?.quiet ?? false;
|
|
92
95
|
const providers = this.instantiateProviders(config?.providers ?? []);
|
|
93
96
|
// Ensure a local provider always exists so uploads can be optimistic
|
|
94
97
|
// and remote misses can be cached for later offline access.
|
|
@@ -121,8 +124,10 @@ export class KritzelAssetResolver {
|
|
|
121
124
|
await provider.init();
|
|
122
125
|
}
|
|
123
126
|
catch (err) {
|
|
124
|
-
|
|
125
|
-
`
|
|
127
|
+
if (this.canLogInfo()) {
|
|
128
|
+
console.warn(`[Kritzel] Asset resolver remote provider "${provider.name}" failed to initialize. ` +
|
|
129
|
+
`Assets will be served from the local provider until the remote becomes available.`, err);
|
|
130
|
+
}
|
|
126
131
|
}
|
|
127
132
|
}
|
|
128
133
|
// Re-enqueue any uploads that were interrupted before completing.
|
|
@@ -134,7 +139,7 @@ export class KritzelAssetResolver {
|
|
|
134
139
|
this._resolveReady = null;
|
|
135
140
|
this._rejectReady = null;
|
|
136
141
|
}
|
|
137
|
-
if (
|
|
142
|
+
if (this.canLogInfo()) {
|
|
138
143
|
this.logProviderConfiguration();
|
|
139
144
|
}
|
|
140
145
|
if (this._pending.size > 0) {
|
|
@@ -143,8 +148,8 @@ export class KritzelAssetResolver {
|
|
|
143
148
|
}
|
|
144
149
|
/** Tears down the resolver and all providers. */
|
|
145
150
|
destroy() {
|
|
146
|
-
if (
|
|
147
|
-
console.info('Asset resolver destroyed');
|
|
151
|
+
if (this.canLogInfo()) {
|
|
152
|
+
console.info('[Kritzel] Asset resolver destroyed');
|
|
148
153
|
}
|
|
149
154
|
this._destroyed = true;
|
|
150
155
|
if (this._flushTimer) {
|
|
@@ -340,7 +345,7 @@ export class KritzelAssetResolver {
|
|
|
340
345
|
// ------------------------------------------------------------------
|
|
341
346
|
logProviderConfiguration() {
|
|
342
347
|
const lines = [
|
|
343
|
-
`Asset resolver initialized with ${this._providers.length} provider(s):`,
|
|
348
|
+
`[Kritzel] Asset resolver initialized with ${this._providers.length} provider(s):`,
|
|
344
349
|
];
|
|
345
350
|
for (const provider of this._providers) {
|
|
346
351
|
const role = provider === this._local ? 'local (primary write target)' : provider.type;
|
|
@@ -399,8 +404,8 @@ export class KritzelAssetResolver {
|
|
|
399
404
|
recoveredCount++;
|
|
400
405
|
}
|
|
401
406
|
}
|
|
402
|
-
if (
|
|
403
|
-
console.info(`Asset resolver
|
|
407
|
+
if (this.canLogInfo() && recoveredCount > 0) {
|
|
408
|
+
console.info(`[Kritzel] Asset resolver recovered ${recoveredCount} pending upload(s)`);
|
|
404
409
|
}
|
|
405
410
|
}
|
|
406
411
|
scheduleFlush(delayMs) {
|
|
@@ -455,8 +460,8 @@ export class KritzelAssetResolver {
|
|
|
455
460
|
});
|
|
456
461
|
this._pending.delete(key);
|
|
457
462
|
await this.clearPendingRemote(job.id, job.providerName);
|
|
458
|
-
if (
|
|
459
|
-
console.info(`Asset resolver
|
|
463
|
+
if (this.canLogInfo()) {
|
|
464
|
+
console.info(`[Kritzel] Asset resolver uploaded ${job.id} to ${provider.name}`);
|
|
460
465
|
}
|
|
461
466
|
}
|
|
462
467
|
catch (err) {
|
|
@@ -464,8 +469,10 @@ export class KritzelAssetResolver {
|
|
|
464
469
|
const delay = Math.min(MAX_RETRY_DELAY_MS, BASE_RETRY_DELAY_MS * Math.pow(2, job.attempts - 1));
|
|
465
470
|
job.nextAttemptAt = Date.now() + delay;
|
|
466
471
|
nextRetryIn = Math.min(nextRetryIn, delay);
|
|
467
|
-
|
|
468
|
-
`
|
|
472
|
+
if (this.canLogInfo()) {
|
|
473
|
+
console.warn(`[Kritzel] Asset resolver upload to ${provider.name} failed for ${job.id} (attempt ${job.attempts}). ` +
|
|
474
|
+
`Retrying in ${Math.round(delay / 1000)}s.`, err);
|
|
475
|
+
}
|
|
469
476
|
}
|
|
470
477
|
}
|
|
471
478
|
if (this._pending.size > 0 && Number.isFinite(nextRetryIn)) {
|
|
@@ -523,4 +530,7 @@ export class KritzelAssetResolver {
|
|
|
523
530
|
const res = await fetch(dataUrl);
|
|
524
531
|
return res.blob();
|
|
525
532
|
}
|
|
533
|
+
canLogInfo() {
|
|
534
|
+
return !this._quiet;
|
|
535
|
+
}
|
|
526
536
|
}
|
|
@@ -20,7 +20,6 @@ export class IndexedDBAssetProvider {
|
|
|
20
20
|
type = 'local';
|
|
21
21
|
name = 'IndexedDBAssetProvider';
|
|
22
22
|
_dbName;
|
|
23
|
-
_quiet;
|
|
24
23
|
_db = null;
|
|
25
24
|
/** In-memory cache of id -> object URL to avoid re-creating blob URLs. */
|
|
26
25
|
_urlCache = new Map();
|
|
@@ -28,7 +27,6 @@ export class IndexedDBAssetProvider {
|
|
|
28
27
|
_knownIds = new Set();
|
|
29
28
|
constructor(options) {
|
|
30
29
|
this._dbName = options?.dbName ?? DEFAULT_DB_NAME;
|
|
31
|
-
this._quiet = options?.quiet ?? false;
|
|
32
30
|
}
|
|
33
31
|
/**
|
|
34
32
|
* Factory helper enabling use as `IndexedDBAssetProvider.with({ dbName: '...' })`
|
|
@@ -43,9 +41,6 @@ export class IndexedDBAssetProvider {
|
|
|
43
41
|
this._db = await this.openDatabase();
|
|
44
42
|
// Warm up the known-ids cache so canResolve can answer synchronously.
|
|
45
43
|
await this.loadKnownIds();
|
|
46
|
-
if (!this._quiet) {
|
|
47
|
-
console.info(`IndexedDBAssetProvider initialized: ${this._dbName}`);
|
|
48
|
-
}
|
|
49
44
|
}
|
|
50
45
|
destroy() {
|
|
51
46
|
for (const url of this._urlCache.values()) {
|
|
@@ -62,9 +57,6 @@ export class IndexedDBAssetProvider {
|
|
|
62
57
|
this._db.close();
|
|
63
58
|
this._db = null;
|
|
64
59
|
}
|
|
65
|
-
if (!this._quiet) {
|
|
66
|
-
console.info(`IndexedDBAssetProvider destroyed: ${this._dbName}`);
|
|
67
|
-
}
|
|
68
60
|
}
|
|
69
61
|
canResolve(id) {
|
|
70
62
|
return this._knownIds.has(id);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const KRITZEL_RENDERER_REGISTRY_KEY = '__kritzelCustomElementRendererRegistry__';
|
|
2
|
+
const getGlobalRegistryStore = () => {
|
|
3
|
+
const globalScope = globalThis;
|
|
4
|
+
const existing = globalScope[KRITZEL_RENDERER_REGISTRY_KEY];
|
|
5
|
+
if (existing) {
|
|
6
|
+
return existing;
|
|
7
|
+
}
|
|
8
|
+
const created = {
|
|
9
|
+
renderers: new Map(),
|
|
10
|
+
};
|
|
11
|
+
globalScope[KRITZEL_RENDERER_REGISTRY_KEY] = created;
|
|
12
|
+
return created;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Global registry for custom element renderers.
|
|
16
|
+
* Allows wrapper frameworks (Angular, React, Vue) to register renderer implementations
|
|
17
|
+
* without coupling to core, and enables deterministic rehydration across reloads and
|
|
18
|
+
* clipboard paste operations.
|
|
19
|
+
*
|
|
20
|
+
* Renderer keys are opaque strings that must be stable (e.g., 'angular-todo', 'react-editor').
|
|
21
|
+
* When a custom element is deserialized with a rendererKey, the registry is consulted to
|
|
22
|
+
* resolve the renderer and mount with the persisted rendererData.
|
|
23
|
+
*/
|
|
24
|
+
export class KritzelCustomElementRendererRegistry {
|
|
25
|
+
static get _renderers() {
|
|
26
|
+
return getGlobalRegistryStore().renderers;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Registers a renderer implementation for a given key.
|
|
30
|
+
* @param key - Stable identifier for this renderer (e.g., 'angular-my-component')
|
|
31
|
+
* @param renderer - Renderer implementation with mount/unmount/update handlers
|
|
32
|
+
*/
|
|
33
|
+
static register(key, renderer) {
|
|
34
|
+
this._renderers.set(key, renderer);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Retrieves a registered renderer by key.
|
|
38
|
+
* @param key - Renderer identifier
|
|
39
|
+
* @returns The registered renderer, or undefined if not found
|
|
40
|
+
*/
|
|
41
|
+
static get(key) {
|
|
42
|
+
return this._renderers.get(key);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Unregisters a renderer by key.
|
|
46
|
+
* @param key - Renderer identifier to remove
|
|
47
|
+
* @returns True if the renderer was found and removed; false otherwise
|
|
48
|
+
*/
|
|
49
|
+
static unregister(key) {
|
|
50
|
+
return this._renderers.delete(key);
|
|
51
|
+
}
|
|
52
|
+
}
|