kritzel-stencil 0.3.22 → 0.3.24
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-D8JoUfNc.js} +744 -49
- 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 +28 -11
- 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 +29 -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-BO1CnEOT.js → p-B5XGjTLd.js} +1 -1
- package/dist/components/p-BNu3Akp5.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-Bz_zj6dQ.js +1 -0
- 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-DNDt6O6A.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-CHrSFOSI.js → p-DX2nriaq.js} +1 -1
- package/dist/components/{p-BAN5dnHX.js → p-Dd-pMvlr.js} +1 -1
- package/dist/components/{p-CbMLbDou.js → p-Dw-t1qQc.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-on11Z8cZ.js +9 -0
- 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-B3IqnD8l.js} +742 -49
- package/dist/stencil/index.esm.js +1 -1
- package/dist/stencil/p-63bc234f.entry.js +9 -0
- package/dist/stencil/p-B3IqnD8l.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 +7 -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 +1 -1
- package/dist/components/p-BML28BJR.js +0 -1
- package/dist/components/p-CVV-HzFR.js +0 -1
- package/dist/components/p-DWhWFDcC.js +0 -9
- package/dist/components/p-Dd_EODkk.js +0 -1
- package/dist/stencil/p-D35cwCYd.js +0 -1
- package/dist/stencil/p-fcbf7965.entry.js +0 -9
|
@@ -14,6 +14,9 @@ class ObjectHelper {
|
|
|
14
14
|
}
|
|
15
15
|
return Object?.keys(obj).length === 0 && obj?.constructor === Object;
|
|
16
16
|
}
|
|
17
|
+
static isClass(object, className) {
|
|
18
|
+
return !!object && object.__class__ === className;
|
|
19
|
+
}
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
class KritzelGeometryHelper {
|
|
@@ -202,7 +205,8 @@ class KritzelBaseObject {
|
|
|
202
205
|
isMounted = false;
|
|
203
206
|
isEditable = false;
|
|
204
207
|
isInteractive = false;
|
|
205
|
-
|
|
208
|
+
isResizable = true;
|
|
209
|
+
isRotatable = true;
|
|
206
210
|
/**
|
|
207
211
|
* Gets the total width of the object including padding.
|
|
208
212
|
* @returns The width plus double the padding value
|
|
@@ -509,21 +513,24 @@ class KritzelBaseObject {
|
|
|
509
513
|
Object.assign(copiedObject, this);
|
|
510
514
|
copiedObject.id = this.generateId();
|
|
511
515
|
copiedObject.isMounted = false;
|
|
516
|
+
copiedObject.isSelected = false;
|
|
517
|
+
copiedObject.isHovered = false;
|
|
518
|
+
copiedObject.isInteractive = false;
|
|
519
|
+
copiedObject.resizing = false;
|
|
520
|
+
copiedObject.markedForRemoval = false;
|
|
521
|
+
copiedObject.isVisible = true;
|
|
512
522
|
return copiedObject;
|
|
513
523
|
}
|
|
514
524
|
/**
|
|
515
525
|
* Serializes the object to a plain JavaScript object for persistence.
|
|
516
526
|
* Excludes internal references (_core, _elementRef) and computed properties.
|
|
517
|
-
*
|
|
527
|
+
* Subclasses should override to handle domain-specific serialization (e.g., HTML elements).
|
|
518
528
|
* @returns A serializable object containing all persistent properties
|
|
519
529
|
*/
|
|
520
530
|
serialize() {
|
|
521
|
-
const { _core, _elementRef, element, totalWidth, totalHeight, ...remainingProps } = this;
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
clonedProps.element = element.cloneNode(true);
|
|
525
|
-
}
|
|
526
|
-
return clonedProps;
|
|
531
|
+
const { _core, _elementRef, element, totalWidth, totalHeight, isSelected, isHovered, isMounted, isInteractive, resizing, markedForRemoval, isVisible, ...remainingProps } = this;
|
|
532
|
+
// No longer clone raw elements—let subclasses handle serializable payloads
|
|
533
|
+
return structuredClone(remainingProps);
|
|
527
534
|
}
|
|
528
535
|
/**
|
|
529
536
|
* Restores the object's state from a serialized plain object.
|
|
@@ -534,6 +541,14 @@ class KritzelBaseObject {
|
|
|
534
541
|
*/
|
|
535
542
|
deserialize(object) {
|
|
536
543
|
Object.assign(this, object);
|
|
544
|
+
// Selection/interactivity state is transient UI state and must not survive revival.
|
|
545
|
+
this.isSelected = false;
|
|
546
|
+
this.isHovered = false;
|
|
547
|
+
this.isMounted = false;
|
|
548
|
+
this.isInteractive = false;
|
|
549
|
+
this.resizing = false;
|
|
550
|
+
this.markedForRemoval = false;
|
|
551
|
+
this.isVisible = true;
|
|
537
552
|
return this;
|
|
538
553
|
}
|
|
539
554
|
/**
|
|
@@ -16433,7 +16448,6 @@ class IndexedDBAssetProvider {
|
|
|
16433
16448
|
type = 'local';
|
|
16434
16449
|
name = 'IndexedDBAssetProvider';
|
|
16435
16450
|
_dbName;
|
|
16436
|
-
_quiet;
|
|
16437
16451
|
_db = null;
|
|
16438
16452
|
/** In-memory cache of id -> object URL to avoid re-creating blob URLs. */
|
|
16439
16453
|
_urlCache = new Map();
|
|
@@ -16441,7 +16455,6 @@ class IndexedDBAssetProvider {
|
|
|
16441
16455
|
_knownIds = new Set();
|
|
16442
16456
|
constructor(options) {
|
|
16443
16457
|
this._dbName = options?.dbName ?? DEFAULT_DB_NAME;
|
|
16444
|
-
this._quiet = options?.quiet ?? false;
|
|
16445
16458
|
}
|
|
16446
16459
|
/**
|
|
16447
16460
|
* Factory helper enabling use as `IndexedDBAssetProvider.with({ dbName: '...' })`
|
|
@@ -16456,9 +16469,6 @@ class IndexedDBAssetProvider {
|
|
|
16456
16469
|
this._db = await this.openDatabase();
|
|
16457
16470
|
// Warm up the known-ids cache so canResolve can answer synchronously.
|
|
16458
16471
|
await this.loadKnownIds();
|
|
16459
|
-
if (!this._quiet) {
|
|
16460
|
-
console.info(`IndexedDBAssetProvider initialized: ${this._dbName}`);
|
|
16461
|
-
}
|
|
16462
16472
|
}
|
|
16463
16473
|
destroy() {
|
|
16464
16474
|
for (const url of this._urlCache.values()) {
|
|
@@ -16475,9 +16485,6 @@ class IndexedDBAssetProvider {
|
|
|
16475
16485
|
this._db.close();
|
|
16476
16486
|
this._db = null;
|
|
16477
16487
|
}
|
|
16478
|
-
if (!this._quiet) {
|
|
16479
|
-
console.info(`IndexedDBAssetProvider destroyed: ${this._dbName}`);
|
|
16480
|
-
}
|
|
16481
16488
|
}
|
|
16482
16489
|
canResolve(id) {
|
|
16483
16490
|
return this._knownIds.has(id);
|
|
@@ -16722,9 +16729,13 @@ class KritzelAssetResolver {
|
|
|
16722
16729
|
_pending = new Map();
|
|
16723
16730
|
_flushTimer = null;
|
|
16724
16731
|
_destroyed = false;
|
|
16725
|
-
_quiet =
|
|
16732
|
+
_quiet = true;
|
|
16726
16733
|
/** Dedupes concurrent resolves of the same id. */
|
|
16727
16734
|
_resolvePromises = new Map();
|
|
16735
|
+
/** Enables or disables resolver diagnostic logging. */
|
|
16736
|
+
setShowInfo(show) {
|
|
16737
|
+
this._quiet = !show;
|
|
16738
|
+
}
|
|
16728
16739
|
/**
|
|
16729
16740
|
* Whether any remote provider is configured. Used by the engine to
|
|
16730
16741
|
* decide whether to warn about a missing remote provider when Yjs is
|
|
@@ -16750,7 +16761,6 @@ class KritzelAssetResolver {
|
|
|
16750
16761
|
// Ensure the deferred ready promise exists before any async work so
|
|
16751
16762
|
// concurrent callers awaiting `ensureReady()` are unblocked together.
|
|
16752
16763
|
this.getReadyPromise();
|
|
16753
|
-
this._quiet = config?.quiet ?? false;
|
|
16754
16764
|
const providers = this.instantiateProviders(config?.providers ?? []);
|
|
16755
16765
|
// Ensure a local provider always exists so uploads can be optimistic
|
|
16756
16766
|
// and remote misses can be cached for later offline access.
|
|
@@ -16783,8 +16793,10 @@ class KritzelAssetResolver {
|
|
|
16783
16793
|
await provider.init();
|
|
16784
16794
|
}
|
|
16785
16795
|
catch (err) {
|
|
16786
|
-
|
|
16787
|
-
`
|
|
16796
|
+
if (this.canLogInfo()) {
|
|
16797
|
+
console.warn(`[Kritzel] Asset resolver remote provider "${provider.name}" failed to initialize. ` +
|
|
16798
|
+
`Assets will be served from the local provider until the remote becomes available.`, err);
|
|
16799
|
+
}
|
|
16788
16800
|
}
|
|
16789
16801
|
}
|
|
16790
16802
|
// Re-enqueue any uploads that were interrupted before completing.
|
|
@@ -16796,7 +16808,7 @@ class KritzelAssetResolver {
|
|
|
16796
16808
|
this._resolveReady = null;
|
|
16797
16809
|
this._rejectReady = null;
|
|
16798
16810
|
}
|
|
16799
|
-
if (
|
|
16811
|
+
if (this.canLogInfo()) {
|
|
16800
16812
|
this.logProviderConfiguration();
|
|
16801
16813
|
}
|
|
16802
16814
|
if (this._pending.size > 0) {
|
|
@@ -16805,8 +16817,8 @@ class KritzelAssetResolver {
|
|
|
16805
16817
|
}
|
|
16806
16818
|
/** Tears down the resolver and all providers. */
|
|
16807
16819
|
destroy() {
|
|
16808
|
-
if (
|
|
16809
|
-
console.info('Asset resolver destroyed');
|
|
16820
|
+
if (this.canLogInfo()) {
|
|
16821
|
+
console.info('[Kritzel] Asset resolver destroyed');
|
|
16810
16822
|
}
|
|
16811
16823
|
this._destroyed = true;
|
|
16812
16824
|
if (this._flushTimer) {
|
|
@@ -17002,7 +17014,7 @@ class KritzelAssetResolver {
|
|
|
17002
17014
|
// ------------------------------------------------------------------
|
|
17003
17015
|
logProviderConfiguration() {
|
|
17004
17016
|
const lines = [
|
|
17005
|
-
`Asset resolver initialized with ${this._providers.length} provider(s):`,
|
|
17017
|
+
`[Kritzel] Asset resolver initialized with ${this._providers.length} provider(s):`,
|
|
17006
17018
|
];
|
|
17007
17019
|
for (const provider of this._providers) {
|
|
17008
17020
|
const role = provider === this._local ? 'local (primary write target)' : provider.type;
|
|
@@ -17061,8 +17073,8 @@ class KritzelAssetResolver {
|
|
|
17061
17073
|
recoveredCount++;
|
|
17062
17074
|
}
|
|
17063
17075
|
}
|
|
17064
|
-
if (
|
|
17065
|
-
console.info(`Asset resolver
|
|
17076
|
+
if (this.canLogInfo() && recoveredCount > 0) {
|
|
17077
|
+
console.info(`[Kritzel] Asset resolver recovered ${recoveredCount} pending upload(s)`);
|
|
17066
17078
|
}
|
|
17067
17079
|
}
|
|
17068
17080
|
scheduleFlush(delayMs) {
|
|
@@ -17117,8 +17129,8 @@ class KritzelAssetResolver {
|
|
|
17117
17129
|
});
|
|
17118
17130
|
this._pending.delete(key);
|
|
17119
17131
|
await this.clearPendingRemote(job.id, job.providerName);
|
|
17120
|
-
if (
|
|
17121
|
-
console.info(`Asset resolver
|
|
17132
|
+
if (this.canLogInfo()) {
|
|
17133
|
+
console.info(`[Kritzel] Asset resolver uploaded ${job.id} to ${provider.name}`);
|
|
17122
17134
|
}
|
|
17123
17135
|
}
|
|
17124
17136
|
catch (err) {
|
|
@@ -17126,8 +17138,10 @@ class KritzelAssetResolver {
|
|
|
17126
17138
|
const delay = Math.min(MAX_RETRY_DELAY_MS, BASE_RETRY_DELAY_MS * Math.pow(2, job.attempts - 1));
|
|
17127
17139
|
job.nextAttemptAt = Date.now() + delay;
|
|
17128
17140
|
nextRetryIn = Math.min(nextRetryIn, delay);
|
|
17129
|
-
|
|
17130
|
-
`
|
|
17141
|
+
if (this.canLogInfo()) {
|
|
17142
|
+
console.warn(`[Kritzel] Asset resolver upload to ${provider.name} failed for ${job.id} (attempt ${job.attempts}). ` +
|
|
17143
|
+
`Retrying in ${Math.round(delay / 1000)}s.`, err);
|
|
17144
|
+
}
|
|
17131
17145
|
}
|
|
17132
17146
|
}
|
|
17133
17147
|
if (this._pending.size > 0 && Number.isFinite(nextRetryIn)) {
|
|
@@ -17185,6 +17199,9 @@ class KritzelAssetResolver {
|
|
|
17185
17199
|
const res = await fetch(dataUrl);
|
|
17186
17200
|
return res.blob();
|
|
17187
17201
|
}
|
|
17202
|
+
canLogInfo() {
|
|
17203
|
+
return !this._quiet;
|
|
17204
|
+
}
|
|
17188
17205
|
}
|
|
17189
17206
|
|
|
17190
17207
|
/**
|
|
@@ -17225,7 +17242,6 @@ class KritzelImage extends KritzelBaseObject {
|
|
|
17225
17242
|
maxWidth = 300;
|
|
17226
17243
|
maxHeight = 300;
|
|
17227
17244
|
maxCompressionSize = 1200;
|
|
17228
|
-
isDebugInfoVisible = true;
|
|
17229
17245
|
// --- Transient (not persisted to Yjs) ---
|
|
17230
17246
|
/**
|
|
17231
17247
|
* URL resolved from the asset storage layer (e.g. a `blob:` or https URL).
|
|
@@ -19638,6 +19654,610 @@ class KritzelShape extends KritzelBaseObject {
|
|
|
19638
19654
|
}
|
|
19639
19655
|
}
|
|
19640
19656
|
|
|
19657
|
+
const KRITZEL_RENDERER_REGISTRY_KEY = '__kritzelCustomElementRendererRegistry__';
|
|
19658
|
+
const getGlobalRegistryStore = () => {
|
|
19659
|
+
const globalScope = globalThis;
|
|
19660
|
+
const existing = globalScope[KRITZEL_RENDERER_REGISTRY_KEY];
|
|
19661
|
+
if (existing) {
|
|
19662
|
+
return existing;
|
|
19663
|
+
}
|
|
19664
|
+
const created = {
|
|
19665
|
+
renderers: new Map(),
|
|
19666
|
+
};
|
|
19667
|
+
globalScope[KRITZEL_RENDERER_REGISTRY_KEY] = created;
|
|
19668
|
+
return created;
|
|
19669
|
+
};
|
|
19670
|
+
/**
|
|
19671
|
+
* Global registry for custom element renderers.
|
|
19672
|
+
* Allows wrapper frameworks (Angular, React, Vue) to register renderer implementations
|
|
19673
|
+
* without coupling to core, and enables deterministic rehydration across reloads and
|
|
19674
|
+
* clipboard paste operations.
|
|
19675
|
+
*
|
|
19676
|
+
* Renderer keys are opaque strings that must be stable (e.g., 'angular-todo', 'react-editor').
|
|
19677
|
+
* When a custom element is deserialized with a rendererKey, the registry is consulted to
|
|
19678
|
+
* resolve the renderer and mount with the persisted rendererData.
|
|
19679
|
+
*/
|
|
19680
|
+
class KritzelCustomElementRendererRegistry {
|
|
19681
|
+
static get _renderers() {
|
|
19682
|
+
return getGlobalRegistryStore().renderers;
|
|
19683
|
+
}
|
|
19684
|
+
/**
|
|
19685
|
+
* Registers a renderer implementation for a given key.
|
|
19686
|
+
* @param key - Stable identifier for this renderer (e.g., 'angular-my-component')
|
|
19687
|
+
* @param renderer - Renderer implementation with mount/unmount/update handlers
|
|
19688
|
+
*/
|
|
19689
|
+
static register(key, renderer) {
|
|
19690
|
+
this._renderers.set(key, renderer);
|
|
19691
|
+
}
|
|
19692
|
+
/**
|
|
19693
|
+
* Retrieves a registered renderer by key.
|
|
19694
|
+
* @param key - Renderer identifier
|
|
19695
|
+
* @returns The registered renderer, or undefined if not found
|
|
19696
|
+
*/
|
|
19697
|
+
static get(key) {
|
|
19698
|
+
return this._renderers.get(key);
|
|
19699
|
+
}
|
|
19700
|
+
/**
|
|
19701
|
+
* Unregisters a renderer by key.
|
|
19702
|
+
* @param key - Renderer identifier to remove
|
|
19703
|
+
* @returns True if the renderer was found and removed; false otherwise
|
|
19704
|
+
*/
|
|
19705
|
+
static unregister(key) {
|
|
19706
|
+
return this._renderers.delete(key);
|
|
19707
|
+
}
|
|
19708
|
+
}
|
|
19709
|
+
|
|
19710
|
+
class KritzelHTMLHelper {
|
|
19711
|
+
static getNumericValueFromStyle(element, property) {
|
|
19712
|
+
const value = window.getComputedStyle(element).getPropertyValue(property);
|
|
19713
|
+
return parseFloat(value) || 0;
|
|
19714
|
+
}
|
|
19715
|
+
static isScrollableElement(element) {
|
|
19716
|
+
if (!(element instanceof HTMLElement)) {
|
|
19717
|
+
return false;
|
|
19718
|
+
}
|
|
19719
|
+
const style = window.getComputedStyle(element);
|
|
19720
|
+
const overflowX = style.overflowX;
|
|
19721
|
+
const overflowY = style.overflowY;
|
|
19722
|
+
const overflow = style.overflow;
|
|
19723
|
+
const canScrollX = (overflowX === 'auto' || overflowX === 'scroll' || overflowX === 'overlay') && element.scrollWidth > element.clientWidth;
|
|
19724
|
+
const canScrollY = (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') && element.scrollHeight > element.clientHeight;
|
|
19725
|
+
const canScroll = overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay';
|
|
19726
|
+
return canScrollX || canScrollY || (canScroll && (element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight));
|
|
19727
|
+
}
|
|
19728
|
+
static findScrollableElement(root) {
|
|
19729
|
+
if (!root) {
|
|
19730
|
+
return null;
|
|
19731
|
+
}
|
|
19732
|
+
const visited = new Set();
|
|
19733
|
+
const walk = (element) => {
|
|
19734
|
+
if (visited.has(element)) {
|
|
19735
|
+
return null;
|
|
19736
|
+
}
|
|
19737
|
+
visited.add(element);
|
|
19738
|
+
if (this.isScrollableElement(element)) {
|
|
19739
|
+
return element;
|
|
19740
|
+
}
|
|
19741
|
+
if (element.shadowRoot) {
|
|
19742
|
+
for (const child of Array.from(element.shadowRoot.children)) {
|
|
19743
|
+
const scrollable = walk(child);
|
|
19744
|
+
if (scrollable) {
|
|
19745
|
+
return scrollable;
|
|
19746
|
+
}
|
|
19747
|
+
}
|
|
19748
|
+
}
|
|
19749
|
+
if (element.tagName === 'SLOT') {
|
|
19750
|
+
for (const assignedElement of element.assignedElements({ flatten: true })) {
|
|
19751
|
+
const scrollable = walk(assignedElement);
|
|
19752
|
+
if (scrollable) {
|
|
19753
|
+
return scrollable;
|
|
19754
|
+
}
|
|
19755
|
+
}
|
|
19756
|
+
}
|
|
19757
|
+
for (const child of Array.from(element.children)) {
|
|
19758
|
+
const scrollable = walk(child);
|
|
19759
|
+
if (scrollable) {
|
|
19760
|
+
return scrollable;
|
|
19761
|
+
}
|
|
19762
|
+
}
|
|
19763
|
+
return null;
|
|
19764
|
+
};
|
|
19765
|
+
return walk(root);
|
|
19766
|
+
}
|
|
19767
|
+
static getScrollableParent(element) {
|
|
19768
|
+
if (!element) {
|
|
19769
|
+
return window;
|
|
19770
|
+
}
|
|
19771
|
+
const parent = element.parentNode?.host ?? element.parentElement;
|
|
19772
|
+
if (!parent || parent.tagName === 'BODY') {
|
|
19773
|
+
return window;
|
|
19774
|
+
}
|
|
19775
|
+
const style = window.getComputedStyle(parent);
|
|
19776
|
+
if (style.overflow === 'auto' || style.overflowY === 'auto' || style.overflow === 'scroll' || style.overflowY === 'scroll') {
|
|
19777
|
+
return parent;
|
|
19778
|
+
}
|
|
19779
|
+
return this.getScrollableParent(parent);
|
|
19780
|
+
}
|
|
19781
|
+
static isElementInViewport(element) {
|
|
19782
|
+
if (!element) {
|
|
19783
|
+
return false;
|
|
19784
|
+
}
|
|
19785
|
+
const scrollableParent = this.getScrollableParent(element);
|
|
19786
|
+
const rect = element.getBoundingClientRect();
|
|
19787
|
+
if (scrollableParent === window) {
|
|
19788
|
+
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
19789
|
+
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
19790
|
+
return rect.top >= 0 && rect.left >= 0 && rect.bottom <= viewportHeight && rect.right <= viewportWidth;
|
|
19791
|
+
}
|
|
19792
|
+
const parentRect = scrollableParent.getBoundingClientRect();
|
|
19793
|
+
return rect.top >= parentRect.top && rect.left >= parentRect.left && rect.bottom <= parentRect.bottom && rect.right <= parentRect.right;
|
|
19794
|
+
}
|
|
19795
|
+
static getFocusableElements(root) {
|
|
19796
|
+
if (!root)
|
|
19797
|
+
return [];
|
|
19798
|
+
const focusableElements = [];
|
|
19799
|
+
const focusableSelector = 'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details, [tabindex]:not([tabindex="-1"])';
|
|
19800
|
+
const isVisible = (el) => {
|
|
19801
|
+
const style = window.getComputedStyle(el);
|
|
19802
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
19803
|
+
};
|
|
19804
|
+
const find = (element) => {
|
|
19805
|
+
// Check if the element itself is focusable
|
|
19806
|
+
if (element instanceof HTMLElement && element.matches(focusableSelector) && !element.hasAttribute('disabled') && isVisible(element)) {
|
|
19807
|
+
focusableElements.push(element);
|
|
19808
|
+
}
|
|
19809
|
+
// Traverse shadow DOM
|
|
19810
|
+
if (element.shadowRoot) {
|
|
19811
|
+
Array.from(element.shadowRoot.children).forEach(find);
|
|
19812
|
+
}
|
|
19813
|
+
// Traverse slotted content
|
|
19814
|
+
if (element.tagName === 'SLOT') {
|
|
19815
|
+
element.assignedElements({ flatten: true }).forEach(find);
|
|
19816
|
+
}
|
|
19817
|
+
// Traverse light DOM children if there is no shadow root
|
|
19818
|
+
if (!element.shadowRoot) {
|
|
19819
|
+
Array.from(element.children).forEach(find);
|
|
19820
|
+
}
|
|
19821
|
+
};
|
|
19822
|
+
find(root);
|
|
19823
|
+
return focusableElements;
|
|
19824
|
+
}
|
|
19825
|
+
}
|
|
19826
|
+
|
|
19827
|
+
/**
|
|
19828
|
+
* Represents a custom HTML element that can be placed and manipulated on the Kritzel canvas.
|
|
19829
|
+
* Extends KritzelBaseObject to inherit common object functionality like positioning,
|
|
19830
|
+
* scaling, and lifecycle management.
|
|
19831
|
+
*/
|
|
19832
|
+
class KritzelCustomElement extends KritzelBaseObject {
|
|
19833
|
+
__class__ = 'KritzelCustomElement';
|
|
19834
|
+
/** The HTML element to be rendered on the canvas. */
|
|
19835
|
+
element;
|
|
19836
|
+
/**
|
|
19837
|
+
* Optional identifier for a registered renderer (e.g., 'angular-component', 'react-default').
|
|
19838
|
+
* When set, mount() resolves the renderer from a global registry and delegates lifecycle.
|
|
19839
|
+
* If null, falls back to DOM element mounting.
|
|
19840
|
+
*/
|
|
19841
|
+
rendererKey = null;
|
|
19842
|
+
/**
|
|
19843
|
+
* Custom data payload for the renderer (e.g., Angular component inputs, React props, Vue data).
|
|
19844
|
+
* Only meaningful when rendererKey is set. Captured on unmount and restored on rehydrate.
|
|
19845
|
+
*/
|
|
19846
|
+
rendererData;
|
|
19847
|
+
/**
|
|
19848
|
+
* Creates a new KritzelCustomElement instance.
|
|
19849
|
+
* @param config - Optional configuration object for the custom element.
|
|
19850
|
+
* @param config.element - The HTML element to wrap.
|
|
19851
|
+
* @param config.translateX - Initial X position offset. Defaults to 0.
|
|
19852
|
+
* @param config.translateY - Initial Y position offset. Defaults to 0.
|
|
19853
|
+
* @param config.scale - Initial scale factor. Defaults to 1.
|
|
19854
|
+
* @param config.height - Initial height in pixels. Defaults to 0.
|
|
19855
|
+
* @param config.width - Initial width in pixels. Defaults to 0.
|
|
19856
|
+
*/
|
|
19857
|
+
constructor(config) {
|
|
19858
|
+
super();
|
|
19859
|
+
if (config) {
|
|
19860
|
+
this.translateX = config.translateX || 0;
|
|
19861
|
+
this.translateY = config.translateY || 0;
|
|
19862
|
+
this.rotation = KritzelMathHelper.degreesToRadians(config.rotation ?? 0);
|
|
19863
|
+
this.scale = config.scale || 1;
|
|
19864
|
+
this.element = config.element ?? this.createPlaceholder();
|
|
19865
|
+
this.height = config.height || 0;
|
|
19866
|
+
this.width = config.width || 0;
|
|
19867
|
+
this.rendererKey = config.rendererKey ?? null;
|
|
19868
|
+
this.rendererData = config.rendererData;
|
|
19869
|
+
}
|
|
19870
|
+
}
|
|
19871
|
+
/**
|
|
19872
|
+
* Factory method to create a new KritzelCustomElement with proper core binding.
|
|
19873
|
+
* Generates a unique ID and assigns the element to the active workspace.
|
|
19874
|
+
* @param core - The KritzelCore instance for state management and ID generation.
|
|
19875
|
+
* @param config - Optional configuration object for the custom element.
|
|
19876
|
+
* @param config.element - The HTML element to wrap.
|
|
19877
|
+
* @param config.translateX - Initial X position offset.
|
|
19878
|
+
* @param config.translateY - Initial Y position offset.
|
|
19879
|
+
* @param config.scale - Initial scale factor.
|
|
19880
|
+
* @param config.height - Initial height in pixels.
|
|
19881
|
+
* @param config.width - Initial width in pixels.
|
|
19882
|
+
* @returns A new KritzelBaseObject instance bound to the core.
|
|
19883
|
+
*/
|
|
19884
|
+
static create(core, config) {
|
|
19885
|
+
const object = new KritzelCustomElement(config);
|
|
19886
|
+
object._core = core;
|
|
19887
|
+
object.id = object.generateId();
|
|
19888
|
+
object.workspaceId = core.store.state.activeWorkspace.id;
|
|
19889
|
+
object.userId = core.user?.id;
|
|
19890
|
+
object.zIndex = core.store.currentZIndex;
|
|
19891
|
+
return object;
|
|
19892
|
+
}
|
|
19893
|
+
/**
|
|
19894
|
+
* Mounts the custom element to a container element in the DOM.
|
|
19895
|
+
* If a renderer is registered for this.rendererKey, delegates to the renderer.
|
|
19896
|
+
* Otherwise, uses the standard DOM element mounting fallback.
|
|
19897
|
+
* Skips mounting if already mounted to the same container with content attached.
|
|
19898
|
+
* @param element - The container HTML element to mount this custom element into.
|
|
19899
|
+
*/
|
|
19900
|
+
mount(element) {
|
|
19901
|
+
if (element === null) {
|
|
19902
|
+
return;
|
|
19903
|
+
}
|
|
19904
|
+
this.applyInteractiveState();
|
|
19905
|
+
if (this.isMounted && this.elementRef === element) {
|
|
19906
|
+
// Renderer-based mounts should not be re-invoked on every engine rerender.
|
|
19907
|
+
if (this.rendererKey) {
|
|
19908
|
+
return;
|
|
19909
|
+
}
|
|
19910
|
+
// For direct DOM fallback, remount only if content was detached.
|
|
19911
|
+
if (this.element.parentElement === element) {
|
|
19912
|
+
return;
|
|
19913
|
+
}
|
|
19914
|
+
}
|
|
19915
|
+
this.elementRef = element;
|
|
19916
|
+
this.isMounted = true;
|
|
19917
|
+
// Try renderer-based approach if a key is set
|
|
19918
|
+
if (this.rendererKey) {
|
|
19919
|
+
const renderer = KritzelCustomElementRendererRegistry.get(this.rendererKey);
|
|
19920
|
+
if (renderer) {
|
|
19921
|
+
element.innerHTML = ''; // Clear container
|
|
19922
|
+
renderer.onMount({
|
|
19923
|
+
object: this,
|
|
19924
|
+
container: element,
|
|
19925
|
+
data: this.rendererData,
|
|
19926
|
+
});
|
|
19927
|
+
this.applyInteractiveState();
|
|
19928
|
+
return;
|
|
19929
|
+
}
|
|
19930
|
+
}
|
|
19931
|
+
// Fallback to standard DOM mounting
|
|
19932
|
+
this.elementRef.innerHTML = '';
|
|
19933
|
+
this.elementRef.appendChild(this.element);
|
|
19934
|
+
this.applyInteractiveState();
|
|
19935
|
+
}
|
|
19936
|
+
/**
|
|
19937
|
+
* Updates interactivity state for embedded HTML content.
|
|
19938
|
+
* When disabling interactivity, any focused descendant is blurred so
|
|
19939
|
+
* keyboard input is released immediately.
|
|
19940
|
+
*/
|
|
19941
|
+
setIsInteractive(isInteractive) {
|
|
19942
|
+
const hasChanged = this.isInteractive !== isInteractive;
|
|
19943
|
+
this.isInteractive = isInteractive;
|
|
19944
|
+
this.applyInteractiveState();
|
|
19945
|
+
if (hasChanged && !isInteractive) {
|
|
19946
|
+
this.blurFocusedDescendant();
|
|
19947
|
+
}
|
|
19948
|
+
}
|
|
19949
|
+
applyInteractiveState() {
|
|
19950
|
+
const pointerEvents = this.isInteractive ? 'auto' : 'none';
|
|
19951
|
+
const contentCursor = this.isInteractive ? 'auto' : '';
|
|
19952
|
+
if (this.element) {
|
|
19953
|
+
this.element.style.pointerEvents = pointerEvents;
|
|
19954
|
+
this.element.style.cursor = contentCursor;
|
|
19955
|
+
}
|
|
19956
|
+
if (this.elementRef instanceof HTMLElement) {
|
|
19957
|
+
// Keep the mount container clickable so unselected custom elements can still be selected.
|
|
19958
|
+
this.elementRef.style.pointerEvents = 'auto';
|
|
19959
|
+
this.elementRef.style.cursor = '';
|
|
19960
|
+
Array.from(this.elementRef.children).forEach(child => {
|
|
19961
|
+
if (child instanceof HTMLElement) {
|
|
19962
|
+
child.style.pointerEvents = pointerEvents;
|
|
19963
|
+
child.style.cursor = contentCursor;
|
|
19964
|
+
}
|
|
19965
|
+
});
|
|
19966
|
+
}
|
|
19967
|
+
}
|
|
19968
|
+
blurFocusedDescendant() {
|
|
19969
|
+
if (!this.element && !(this.elementRef instanceof HTMLElement)) {
|
|
19970
|
+
return;
|
|
19971
|
+
}
|
|
19972
|
+
const activeElement = this.getDeepActiveElement();
|
|
19973
|
+
const isInsideElement = !!this.element && this.isNodeInsideHost(activeElement, this.element);
|
|
19974
|
+
const isInsideMountedContainer = this.elementRef instanceof HTMLElement && this.isNodeInsideHost(activeElement, this.elementRef);
|
|
19975
|
+
if (activeElement && (isInsideElement || isInsideMountedContainer)) {
|
|
19976
|
+
activeElement.blur();
|
|
19977
|
+
}
|
|
19978
|
+
}
|
|
19979
|
+
/**
|
|
19980
|
+
* Returns true when a text-input-like element inside this custom element
|
|
19981
|
+
* currently owns focus.
|
|
19982
|
+
*/
|
|
19983
|
+
hasFocusedInputElement() {
|
|
19984
|
+
if (!this.isInteractive) {
|
|
19985
|
+
return false;
|
|
19986
|
+
}
|
|
19987
|
+
const focusHosts = this.getFocusHosts();
|
|
19988
|
+
// Primary path: locate the focused input-like element by walking from each host through
|
|
19989
|
+
// `:focus` (piercing open shadow roots). This works in real browsers even when the engine
|
|
19990
|
+
// and editor are nested web components, because it never relies on the document-level
|
|
19991
|
+
// active element resolving to a descendant of the host.
|
|
19992
|
+
if (focusHosts.some(host => this.hasFocusedInputDescendant(host))) {
|
|
19993
|
+
return true;
|
|
19994
|
+
}
|
|
19995
|
+
// Fallback path: resolve the deep active element and confirm it is input-like and inside a
|
|
19996
|
+
// host. Covers contenteditable content inside nested shadow roots (which `querySelector`
|
|
19997
|
+
// cannot reach) and test environments that mock the active element without real focus.
|
|
19998
|
+
const activeElement = this.getDeepActiveElement();
|
|
19999
|
+
if (activeElement && this.isInputLikeElement(activeElement) && focusHosts.some(host => this.isNodeInsideHost(activeElement, host))) {
|
|
20000
|
+
return true;
|
|
20001
|
+
}
|
|
20002
|
+
return false;
|
|
20003
|
+
}
|
|
20004
|
+
getFocusHosts() {
|
|
20005
|
+
const hosts = [];
|
|
20006
|
+
if (this.element) {
|
|
20007
|
+
hosts.push(this.element);
|
|
20008
|
+
}
|
|
20009
|
+
if (this.elementRef instanceof HTMLElement) {
|
|
20010
|
+
hosts.push(this.elementRef);
|
|
20011
|
+
const firstChild = this.elementRef.firstElementChild;
|
|
20012
|
+
if (firstChild instanceof HTMLElement) {
|
|
20013
|
+
hosts.push(firstChild);
|
|
20014
|
+
}
|
|
20015
|
+
}
|
|
20016
|
+
return hosts;
|
|
20017
|
+
}
|
|
20018
|
+
/**
|
|
20019
|
+
* Returns true when the given host (or, via the flat tree, one of its shadow descendants)
|
|
20020
|
+
* currently contains a focused input-like element.
|
|
20021
|
+
*/
|
|
20022
|
+
hasFocusedInputDescendant(host) {
|
|
20023
|
+
if (this.isInputLikeElement(host) && this.matchesFocus(host)) {
|
|
20024
|
+
return true;
|
|
20025
|
+
}
|
|
20026
|
+
// `:focus-within` crosses shadow boundaries for the host, so it is a cheap guard that tells
|
|
20027
|
+
// us whether focus lives anywhere inside this host before doing the deeper traversal.
|
|
20028
|
+
if (typeof host.matches === 'function') {
|
|
20029
|
+
try {
|
|
20030
|
+
if (!host.matches(':focus-within')) {
|
|
20031
|
+
return false;
|
|
20032
|
+
}
|
|
20033
|
+
}
|
|
20034
|
+
catch {
|
|
20035
|
+
// Selector unsupported in this environment; fall through to the traversal below.
|
|
20036
|
+
}
|
|
20037
|
+
}
|
|
20038
|
+
return this.findFocusedInputLikeElement(host) !== null;
|
|
20039
|
+
}
|
|
20040
|
+
/**
|
|
20041
|
+
* Walks the `:focus` chain from a tree scope, descending into open shadow roots, and returns
|
|
20042
|
+
* the focused element when it is input-like.
|
|
20043
|
+
*/
|
|
20044
|
+
findFocusedInputLikeElement(root) {
|
|
20045
|
+
let focused = null;
|
|
20046
|
+
try {
|
|
20047
|
+
focused = root.querySelector(':focus');
|
|
20048
|
+
}
|
|
20049
|
+
catch {
|
|
20050
|
+
focused = null;
|
|
20051
|
+
}
|
|
20052
|
+
if (!(focused instanceof HTMLElement)) {
|
|
20053
|
+
return null;
|
|
20054
|
+
}
|
|
20055
|
+
if (this.isInputLikeElement(focused)) {
|
|
20056
|
+
return focused;
|
|
20057
|
+
}
|
|
20058
|
+
if (focused.shadowRoot) {
|
|
20059
|
+
return this.findFocusedInputLikeElement(focused.shadowRoot);
|
|
20060
|
+
}
|
|
20061
|
+
return null;
|
|
20062
|
+
}
|
|
20063
|
+
matchesFocus(element) {
|
|
20064
|
+
if (typeof element.matches !== 'function') {
|
|
20065
|
+
return false;
|
|
20066
|
+
}
|
|
20067
|
+
try {
|
|
20068
|
+
return element.matches(':focus');
|
|
20069
|
+
}
|
|
20070
|
+
catch {
|
|
20071
|
+
return false;
|
|
20072
|
+
}
|
|
20073
|
+
}
|
|
20074
|
+
isInputLikeElement(element) {
|
|
20075
|
+
const tag = element.tagName;
|
|
20076
|
+
return (tag === 'INPUT' ||
|
|
20077
|
+
tag === 'TEXTAREA' ||
|
|
20078
|
+
tag === 'SELECT' ||
|
|
20079
|
+
element.isContentEditable);
|
|
20080
|
+
}
|
|
20081
|
+
isNodeInsideHost(node, host) {
|
|
20082
|
+
if (!node) {
|
|
20083
|
+
return false;
|
|
20084
|
+
}
|
|
20085
|
+
let current = node;
|
|
20086
|
+
while (current) {
|
|
20087
|
+
if (current === host) {
|
|
20088
|
+
return true;
|
|
20089
|
+
}
|
|
20090
|
+
if (current.parentNode) {
|
|
20091
|
+
current = current.parentNode;
|
|
20092
|
+
continue;
|
|
20093
|
+
}
|
|
20094
|
+
const root = current.getRootNode();
|
|
20095
|
+
if (root instanceof ShadowRoot) {
|
|
20096
|
+
current = root.host;
|
|
20097
|
+
continue;
|
|
20098
|
+
}
|
|
20099
|
+
break;
|
|
20100
|
+
}
|
|
20101
|
+
return false;
|
|
20102
|
+
}
|
|
20103
|
+
getDeepActiveElement(root = document) {
|
|
20104
|
+
const activeElement = root.activeElement;
|
|
20105
|
+
if (!(activeElement instanceof HTMLElement)) {
|
|
20106
|
+
return null;
|
|
20107
|
+
}
|
|
20108
|
+
if (activeElement.shadowRoot) {
|
|
20109
|
+
const nested = this.getDeepActiveElement(activeElement.shadowRoot);
|
|
20110
|
+
return nested ?? activeElement;
|
|
20111
|
+
}
|
|
20112
|
+
return activeElement;
|
|
20113
|
+
}
|
|
20114
|
+
/**
|
|
20115
|
+
* Unmounts the custom element and performs cleanup.
|
|
20116
|
+
* If a renderer is registered, delegates to renderer.onUnmount and captures returned state.
|
|
20117
|
+
* Otherwise, simply removes from DOM.
|
|
20118
|
+
* Called when object is deleted, viewport culled, or editor is unloading.
|
|
20119
|
+
*/
|
|
20120
|
+
unmount() {
|
|
20121
|
+
if (!this.isMounted || !this.elementRef) {
|
|
20122
|
+
return;
|
|
20123
|
+
}
|
|
20124
|
+
// Try renderer-based unmount if renderer is registered
|
|
20125
|
+
if (this.rendererKey) {
|
|
20126
|
+
const renderer = KritzelCustomElementRendererRegistry.get(this.rendererKey);
|
|
20127
|
+
if (renderer) {
|
|
20128
|
+
// Capture any updated data returned by renderer
|
|
20129
|
+
const updatedData = renderer.onUnmount({
|
|
20130
|
+
object: this,
|
|
20131
|
+
container: this.elementRef,
|
|
20132
|
+
data: this.rendererData,
|
|
20133
|
+
});
|
|
20134
|
+
if (updatedData !== undefined) {
|
|
20135
|
+
this.rendererData = updatedData;
|
|
20136
|
+
}
|
|
20137
|
+
}
|
|
20138
|
+
}
|
|
20139
|
+
this.isMounted = false;
|
|
20140
|
+
this.elementRef = null;
|
|
20141
|
+
}
|
|
20142
|
+
/**
|
|
20143
|
+
* Returns whether the rendered content contains a scrollable element.
|
|
20144
|
+
* Traverses light DOM, open shadow roots, and slotted content so nested
|
|
20145
|
+
* framework components are detected as well.
|
|
20146
|
+
*/
|
|
20147
|
+
isElementScrollable() {
|
|
20148
|
+
return !!KritzelHTMLHelper.findScrollableElement(this.element);
|
|
20149
|
+
}
|
|
20150
|
+
/**
|
|
20151
|
+
* Creates a placeholder element when HTML deserialization fails.
|
|
20152
|
+
* Used as fallback to ensure element is always defined.
|
|
20153
|
+
* @returns A div element with placeholder styling
|
|
20154
|
+
*/
|
|
20155
|
+
createPlaceholder() {
|
|
20156
|
+
const el = document.createElement('div');
|
|
20157
|
+
el.textContent = '[Custom Element]';
|
|
20158
|
+
el.style.border = '1px dashed #ccc';
|
|
20159
|
+
el.style.display = 'flex';
|
|
20160
|
+
el.style.alignItems = 'center';
|
|
20161
|
+
el.style.justifyContent = 'center';
|
|
20162
|
+
el.style.backgroundColor = '#f9f9f9';
|
|
20163
|
+
el.style.color = '#999';
|
|
20164
|
+
el.style.fontSize = '12px';
|
|
20165
|
+
return el;
|
|
20166
|
+
}
|
|
20167
|
+
/**
|
|
20168
|
+
* Resizes and repositions the custom element on the canvas.
|
|
20169
|
+
* Updates both the internal state and the DOM element's inline styles.
|
|
20170
|
+
* Syncs changes to Yjs for collaborative editing and cross-tab propagation.
|
|
20171
|
+
* @param x - The new X position (translateX) in canvas coordinates.
|
|
20172
|
+
* @param y - The new Y position (translateY) in canvas coordinates.
|
|
20173
|
+
* @param width - The new width in pixels. Must be greater than 1.
|
|
20174
|
+
* @param height - The new height in pixels. Must be greater than 1.
|
|
20175
|
+
*/
|
|
20176
|
+
resize(x, y, width, height) {
|
|
20177
|
+
if (width <= 1 || height <= 1) {
|
|
20178
|
+
return;
|
|
20179
|
+
}
|
|
20180
|
+
this.width = width;
|
|
20181
|
+
this.height = height;
|
|
20182
|
+
this.translateX = x;
|
|
20183
|
+
this.translateY = y;
|
|
20184
|
+
if (this.element) {
|
|
20185
|
+
this.element.style.width = `${width}px`;
|
|
20186
|
+
this.element.style.height = `${height}px`;
|
|
20187
|
+
}
|
|
20188
|
+
// Update to sync changes to y.js and propagate to other tabs
|
|
20189
|
+
this._core.store.objects.update(this);
|
|
20190
|
+
}
|
|
20191
|
+
/**
|
|
20192
|
+
* Creates a deep copy of this custom element.
|
|
20193
|
+
* The copy receives a new unique ID, is marked as unmounted,
|
|
20194
|
+
* and contains a cloned copy of the underlying HTML element.
|
|
20195
|
+
* @returns A new KritzelCustomElement instance with copied properties and a cloned DOM element.
|
|
20196
|
+
*/
|
|
20197
|
+
copy() {
|
|
20198
|
+
const copiedObject = Object.create(Object.getPrototypeOf(this));
|
|
20199
|
+
Object.assign(copiedObject, this);
|
|
20200
|
+
copiedObject.id = this.generateId();
|
|
20201
|
+
copiedObject.isMounted = false;
|
|
20202
|
+
copiedObject.isSelected = false;
|
|
20203
|
+
copiedObject.isHovered = false;
|
|
20204
|
+
copiedObject.isInteractive = false;
|
|
20205
|
+
copiedObject.resizing = false;
|
|
20206
|
+
copiedObject.markedForRemoval = false;
|
|
20207
|
+
copiedObject.isVisible = true;
|
|
20208
|
+
copiedObject.element = this.element.cloneNode(true);
|
|
20209
|
+
return copiedObject;
|
|
20210
|
+
}
|
|
20211
|
+
/**
|
|
20212
|
+
* Serializes the custom element to a plain JavaScript object for persistence.
|
|
20213
|
+
* Stores HTML as a string (safe for JSON/Yjs) and captures dimension metadata.
|
|
20214
|
+
* @returns A serializable object with htmlString and dimension snapshots
|
|
20215
|
+
*/
|
|
20216
|
+
serialize() {
|
|
20217
|
+
const { _core, _elementRef, element, totalWidth, totalHeight, isSelected, isHovered, isMounted, isInteractive, resizing, markedForRemoval, isVisible, ...props } = this;
|
|
20218
|
+
const cloned = structuredClone(props);
|
|
20219
|
+
// Include rendererKey and rendererData for lifecycle-driven rendering
|
|
20220
|
+
cloned.rendererKey = this.rendererKey;
|
|
20221
|
+
cloned.rendererData = this.rendererData;
|
|
20222
|
+
// Store HTML as string for fallback/compatibility when renderer is unavailable
|
|
20223
|
+
cloned.htmlString = element ? element.outerHTML : '';
|
|
20224
|
+
// Capture dimension snapshot at serialization time
|
|
20225
|
+
cloned.serializedWidth = this.width;
|
|
20226
|
+
cloned.serializedHeight = this.height;
|
|
20227
|
+
return cloned;
|
|
20228
|
+
}
|
|
20229
|
+
/**
|
|
20230
|
+
* Deserializes the custom element from a plain object.
|
|
20231
|
+
* Reconstructs the element from stored HTML string with fallback to placeholder.
|
|
20232
|
+
* @template T - The expected return type
|
|
20233
|
+
* @param object - The serialized object data to restore from
|
|
20234
|
+
* @returns This instance cast to type T
|
|
20235
|
+
*/
|
|
20236
|
+
deserialize(object) {
|
|
20237
|
+
super.deserialize(object);
|
|
20238
|
+
// Legacy compatibility: this deprecated field should not persist on revived objects.
|
|
20239
|
+
delete this.allowEmbeddedInteraction;
|
|
20240
|
+
// Restore renderer metadata
|
|
20241
|
+
this.rendererKey = object.rendererKey ?? null;
|
|
20242
|
+
this.rendererData = object.rendererData;
|
|
20243
|
+
// Reconstruct element from stored HTML, or create placeholder
|
|
20244
|
+
if (object.htmlString) {
|
|
20245
|
+
try {
|
|
20246
|
+
const temp = document.createElement('div');
|
|
20247
|
+
temp.innerHTML = object.htmlString;
|
|
20248
|
+
this.element = temp.firstElementChild || this.createPlaceholder();
|
|
20249
|
+
}
|
|
20250
|
+
catch {
|
|
20251
|
+
this.element = this.createPlaceholder();
|
|
20252
|
+
}
|
|
20253
|
+
}
|
|
20254
|
+
else {
|
|
20255
|
+
this.element = this.createPlaceholder();
|
|
20256
|
+
}
|
|
20257
|
+
return this;
|
|
20258
|
+
}
|
|
20259
|
+
}
|
|
20260
|
+
|
|
19641
20261
|
/**
|
|
19642
20262
|
* Abstract base class for all drawing tools in Kritzel.
|
|
19643
20263
|
* Provides common functionality and defines the interface that all tools must implement.
|
|
@@ -20106,6 +20726,12 @@ class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
20106
20726
|
this._cachedObjectIdsHash = currentHash;
|
|
20107
20727
|
return this._cachedObjects;
|
|
20108
20728
|
}
|
|
20729
|
+
syncChildSelectionState(children) {
|
|
20730
|
+
const shouldBeSelected = this.isSelected === true;
|
|
20731
|
+
for (const child of children) {
|
|
20732
|
+
child.isSelected = shouldBeSelected;
|
|
20733
|
+
}
|
|
20734
|
+
}
|
|
20109
20735
|
/**
|
|
20110
20736
|
* Gets the number of objects in the selection group.
|
|
20111
20737
|
* @returns The count of selected objects
|
|
@@ -20151,6 +20777,7 @@ class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
20151
20777
|
for (const object of objects) {
|
|
20152
20778
|
if (!this._objectIds.includes(object.id)) {
|
|
20153
20779
|
this._objectIds.push(object.id);
|
|
20780
|
+
object.isSelected = true;
|
|
20154
20781
|
changed = true;
|
|
20155
20782
|
}
|
|
20156
20783
|
}
|
|
@@ -20178,9 +20805,11 @@ class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
20178
20805
|
const index = this._objectIds.findIndex(id => id === object.id);
|
|
20179
20806
|
if (index === -1) {
|
|
20180
20807
|
this._objectIds.push(object.id);
|
|
20808
|
+
object.isSelected = true;
|
|
20181
20809
|
}
|
|
20182
20810
|
else {
|
|
20183
20811
|
this._objectIds.splice(index, 1);
|
|
20812
|
+
object.isSelected = false;
|
|
20184
20813
|
}
|
|
20185
20814
|
this._cachedObjects = null; // Invalidate cache
|
|
20186
20815
|
const children = this.objects; // Get cached objects once
|
|
@@ -20212,11 +20841,27 @@ class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
20212
20841
|
/**
|
|
20213
20842
|
* Sets the isSelected flag to false for all objects in the selection group.
|
|
20214
20843
|
* Used when clearing or changing the selection state.
|
|
20844
|
+
*
|
|
20845
|
+
* For KritzelGroup members, also clears the isSelected flag on all nested
|
|
20846
|
+
* descendants. A descendant can retain a lingering isSelected from the marquee
|
|
20847
|
+
* preview before it was resolved to its parent group during selection, which
|
|
20848
|
+
* would otherwise leave that object selected after the group is deselected and
|
|
20849
|
+
* cause the whole group to be re-selected on the next interaction.
|
|
20215
20850
|
*/
|
|
20216
20851
|
deselectAllChildren() {
|
|
20217
20852
|
const children = this.objects;
|
|
20853
|
+
const descendantIds = new Set();
|
|
20218
20854
|
for (const obj of children) {
|
|
20219
20855
|
obj.isSelected = false;
|
|
20856
|
+
if (KritzelClassHelper.isInstanceOf(obj, 'KritzelGroup')) {
|
|
20857
|
+
KritzelGroup.collectDescendantIds(obj, descendantIds);
|
|
20858
|
+
}
|
|
20859
|
+
}
|
|
20860
|
+
if (descendantIds.size > 0) {
|
|
20861
|
+
const descendants = this._core.store.objects.filter(o => descendantIds.has(o.id));
|
|
20862
|
+
for (const descendant of descendants) {
|
|
20863
|
+
descendant.isSelected = false;
|
|
20864
|
+
}
|
|
20220
20865
|
}
|
|
20221
20866
|
}
|
|
20222
20867
|
/**
|
|
@@ -20322,6 +20967,8 @@ class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
20322
20967
|
deserialize(object) {
|
|
20323
20968
|
// First, deserialize all base properties using parent's deserialize
|
|
20324
20969
|
super.deserialize(object);
|
|
20970
|
+
// Selection groups intentionally persist their selected state.
|
|
20971
|
+
this.isSelected = object.isSelected === true;
|
|
20325
20972
|
// Restore objectIds through setter
|
|
20326
20973
|
if (object.objectIds) {
|
|
20327
20974
|
this.objectIds = object.objectIds;
|
|
@@ -20330,6 +20977,7 @@ class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
20330
20977
|
if (object.unchangedObjectSnapshots) {
|
|
20331
20978
|
this.unchangedObjectSnapshots = new Map(Object.entries(object.unchangedObjectSnapshots));
|
|
20332
20979
|
}
|
|
20980
|
+
this.syncChildSelectionState(this.objects);
|
|
20333
20981
|
return this;
|
|
20334
20982
|
}
|
|
20335
20983
|
/**
|
|
@@ -22760,10 +23408,9 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
22760
23408
|
selectionBox.translateX = this.startX;
|
|
22761
23409
|
selectionBox.translateY = this.startY;
|
|
22762
23410
|
this._core.store.state.isSelecting = true;
|
|
22763
|
-
this._core.
|
|
22764
|
-
this._core.store.setSelectionBox(null);
|
|
23411
|
+
this._core.removeSelectionBox();
|
|
22765
23412
|
if (!isAdditive) {
|
|
22766
|
-
this._core.
|
|
23413
|
+
this._core.removeSelectionGroup();
|
|
22767
23414
|
}
|
|
22768
23415
|
this._core.store.objects.insert(selectionBox);
|
|
22769
23416
|
this._core.store.setSelectionBox(selectionBox);
|
|
@@ -22792,10 +23439,9 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
22792
23439
|
selectionBox.translateX = this.startX;
|
|
22793
23440
|
selectionBox.translateY = this.startY;
|
|
22794
23441
|
this._core.store.state.isSelecting = true;
|
|
22795
|
-
this._core.
|
|
22796
|
-
this._core.store.setSelectionBox(null);
|
|
23442
|
+
this._core.removeSelectionBox();
|
|
22797
23443
|
if (!isAdditive) {
|
|
22798
|
-
this._core.
|
|
23444
|
+
this._core.removeSelectionGroup();
|
|
22799
23445
|
}
|
|
22800
23446
|
this._core.store.objects.insert(selectionBox);
|
|
22801
23447
|
this._core.store.setSelectionBox(selectionBox);
|
|
@@ -22965,8 +23611,11 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
22965
23611
|
*/
|
|
22966
23612
|
clearSelectionPreview() {
|
|
22967
23613
|
// Only clear previously tracked selected objects - O(k) instead of O(n)
|
|
23614
|
+
const selectedGroupIds = new Set(this._core.store.selectionGroup?.objectIds ?? []);
|
|
22968
23615
|
for (const obj of this._previouslySelectedObjects) {
|
|
22969
|
-
obj.
|
|
23616
|
+
if (!selectedGroupIds.has(obj.id)) {
|
|
23617
|
+
obj.isSelected = false;
|
|
23618
|
+
}
|
|
22970
23619
|
}
|
|
22971
23620
|
this._previouslySelectedObjects.clear();
|
|
22972
23621
|
}
|
|
@@ -22985,7 +23634,11 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
22985
23634
|
// Resolve child objects to their parent groups
|
|
22986
23635
|
const parentGroup = KritzelGroup.findParentGroup(this._core, object.id);
|
|
22987
23636
|
const objectToSelect = parentGroup || object;
|
|
22988
|
-
|
|
23637
|
+
// When the click resolves to a parent group, clear the child's own flag so
|
|
23638
|
+
// it does not linger as selected and re-select the group on the next interaction.
|
|
23639
|
+
if (parentGroup) {
|
|
23640
|
+
object.isSelected = false;
|
|
23641
|
+
}
|
|
22989
23642
|
const existingGroup = this._core.store.selectionGroup;
|
|
22990
23643
|
if (shiftKey && existingGroup) {
|
|
22991
23644
|
existingGroup.addOrRemove(objectToSelect);
|
|
@@ -23030,9 +23683,16 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
23030
23683
|
const resolvedObjects = new Map();
|
|
23031
23684
|
for (const obj of selectedObjects) {
|
|
23032
23685
|
const parentGroup = childToParentMap.get(obj.id);
|
|
23033
|
-
|
|
23034
|
-
|
|
23035
|
-
|
|
23686
|
+
if (parentGroup) {
|
|
23687
|
+
// The child was selected (e.g. via marquee preview) but selection is
|
|
23688
|
+
// represented by its parent group. Clear the child's flag so it does
|
|
23689
|
+
// not linger as selected and re-select the group on the next interaction.
|
|
23690
|
+
obj.isSelected = false;
|
|
23691
|
+
resolvedObjects.set(parentGroup.id, parentGroup);
|
|
23692
|
+
}
|
|
23693
|
+
else {
|
|
23694
|
+
resolvedObjects.set(obj.id, obj);
|
|
23695
|
+
}
|
|
23036
23696
|
}
|
|
23037
23697
|
const unrolledObjects = Array.from(resolvedObjects.values());
|
|
23038
23698
|
const filteredObjects = this.filterDescendantsOfSelectedGroups(unrolledObjects);
|
|
@@ -23040,12 +23700,16 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
23040
23700
|
if (shiftKey && existingGroup) {
|
|
23041
23701
|
existingGroup.addObjects(filteredObjects);
|
|
23042
23702
|
this.removeDescendantsOfSelectedGroups(existingGroup);
|
|
23703
|
+
this._previouslySelectedObjects.clear();
|
|
23043
23704
|
this._core.rerender();
|
|
23044
23705
|
}
|
|
23045
23706
|
else {
|
|
23046
23707
|
// Create selection group and set all IDs at once (no per-item refresh)
|
|
23047
23708
|
const selectionGroup = KritzelSelectionGroup.create(this._core);
|
|
23048
23709
|
selectionGroup.objectIds = filteredObjects.map(o => o.id);
|
|
23710
|
+
filteredObjects.forEach(obj => {
|
|
23711
|
+
obj.isSelected = true;
|
|
23712
|
+
});
|
|
23049
23713
|
// Only refresh dimensions and capture snapshots ONCE at the end - O(n) instead of O(n²)
|
|
23050
23714
|
if (selectionGroup.length === 1) {
|
|
23051
23715
|
selectionGroup.rotation = selectionGroup.objects[0].rotation;
|
|
@@ -23054,6 +23718,7 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
23054
23718
|
selectionGroup.refreshObjectDimensions();
|
|
23055
23719
|
selectionGroup.isSelected = true;
|
|
23056
23720
|
this._core.addSelectionGroup(selectionGroup);
|
|
23721
|
+
this._previouslySelectedObjects.clear();
|
|
23057
23722
|
this._core.rerender();
|
|
23058
23723
|
}
|
|
23059
23724
|
}
|
|
@@ -23941,8 +24606,18 @@ class KritzelSelectionTool extends KritzelBaseTool {
|
|
|
23941
24606
|
this._core.removeSelectionGroup();
|
|
23942
24607
|
this._core.rerender();
|
|
23943
24608
|
}
|
|
23944
|
-
if (selectedObject && selectedObject.isSelected === false && selectedObject?.objects.length === 1
|
|
23945
|
-
|
|
24609
|
+
if (selectedObject && selectedObject.isSelected === false && selectedObject?.objects.length === 1) {
|
|
24610
|
+
const firstObj = selectedObject.objects[0];
|
|
24611
|
+
if (KritzelClassHelper.isInstanceOf(firstObj, 'KritzelCustomElement')) {
|
|
24612
|
+
// Selected single custom elements hand pointer interaction to their
|
|
24613
|
+
// embedded DOM content. Unselected custom elements remain selectable.
|
|
24614
|
+
if (firstObj.isSelected && this._core.store.selectionGroup?.objects.length === 1)
|
|
24615
|
+
return;
|
|
24616
|
+
}
|
|
24617
|
+
else if (firstObj.isInteractive) {
|
|
24618
|
+
// Preserve existing behavior for text/shape edit mode
|
|
24619
|
+
return;
|
|
24620
|
+
}
|
|
23946
24621
|
}
|
|
23947
24622
|
}
|
|
23948
24623
|
this.lineHandleHandler.handlePointerDown(event);
|
|
@@ -23954,6 +24629,14 @@ class KritzelSelectionTool extends KritzelBaseTool {
|
|
|
23954
24629
|
}
|
|
23955
24630
|
if (event.pointerType === 'touch' || event.pointerType === 'pen') {
|
|
23956
24631
|
if (this._core.store.state.isScaling === true) {
|
|
24632
|
+
const activePointerCount = this._core.store.state.pointers.size;
|
|
24633
|
+
// During a real pinch (two active pointers), keep touch handling suspended.
|
|
24634
|
+
// If only one pointer is active, scaling is stale (debounce window after pinch),
|
|
24635
|
+
// so resume normal touch selection behavior immediately.
|
|
24636
|
+
if (activePointerCount === 1) {
|
|
24637
|
+
this._core.store.state.isScaling = false;
|
|
24638
|
+
this._savedSelectionGroupForPinch = null;
|
|
24639
|
+
}
|
|
23957
24640
|
// A second finger has arrived to start a pinch-zoom gesture. If we
|
|
23958
24641
|
// cleared a selection group on the first finger-down, restore it now
|
|
23959
24642
|
// so the user's selection is preserved while zooming.
|
|
@@ -23962,7 +24645,10 @@ class KritzelSelectionTool extends KritzelBaseTool {
|
|
|
23962
24645
|
this._savedSelectionGroupForPinch = null;
|
|
23963
24646
|
this._core.rerender();
|
|
23964
24647
|
}
|
|
23965
|
-
|
|
24648
|
+
if (this._core.store.state.isScaling === true) {
|
|
24649
|
+
return;
|
|
24650
|
+
}
|
|
24651
|
+
// Continue with normal touch handling when scaling was stale.
|
|
23966
24652
|
}
|
|
23967
24653
|
if (this._core.store.state.pointers.size === 1) {
|
|
23968
24654
|
this._core.store.state.isResizeHandleSelected = this.isHandleSelected(event);
|
|
@@ -23994,7 +24680,10 @@ class KritzelSelectionTool extends KritzelBaseTool {
|
|
|
23994
24680
|
this.rotationHandler.handlePointerDown(event);
|
|
23995
24681
|
this.resizeHandler.handlePointerDown(event);
|
|
23996
24682
|
this.moveHandler.handlePointerDown(event);
|
|
23997
|
-
|
|
24683
|
+
// Only process selection handler if we are not scaling (2 pointers)
|
|
24684
|
+
if (this._core.store.state.pointers.size === 1) {
|
|
24685
|
+
this.selectionHandler.handlePointerDown(event);
|
|
24686
|
+
}
|
|
23998
24687
|
this._core.rerender();
|
|
23999
24688
|
}
|
|
24000
24689
|
}
|
|
@@ -24068,7 +24757,7 @@ class KritzelSelectionTool extends KritzelBaseTool {
|
|
|
24068
24757
|
* @returns A KritzelSelectionGroup containing the object, or null if no object at that location
|
|
24069
24758
|
*/
|
|
24070
24759
|
getSelectedObject(event) {
|
|
24071
|
-
const path = event.composedPath()
|
|
24760
|
+
const path = event.composedPath();
|
|
24072
24761
|
const objectElement = path.find(element => element.classList && element.classList.contains('object'));
|
|
24073
24762
|
const object = objectElement?.id ? this._core.findObjectById(objectElement.id) : null;
|
|
24074
24763
|
if (!object) {
|
|
@@ -24078,6 +24767,10 @@ class KritzelSelectionTool extends KritzelBaseTool {
|
|
|
24078
24767
|
if (!object.hitTest(x, y)) {
|
|
24079
24768
|
return null;
|
|
24080
24769
|
}
|
|
24770
|
+
const currentSelectionGroup = this._core.store.selectionGroup;
|
|
24771
|
+
if (currentSelectionGroup?.objects.some(selectedObject => selectedObject.id === object.id)) {
|
|
24772
|
+
return currentSelectionGroup;
|
|
24773
|
+
}
|
|
24081
24774
|
if (object instanceof KritzelSelectionGroup) {
|
|
24082
24775
|
return object;
|
|
24083
24776
|
}
|
|
@@ -24085,7 +24778,7 @@ class KritzelSelectionTool extends KritzelBaseTool {
|
|
|
24085
24778
|
const group = KritzelSelectionGroup.create(this._core);
|
|
24086
24779
|
group.translateX = 0;
|
|
24087
24780
|
group.translateY = 0;
|
|
24088
|
-
group.
|
|
24781
|
+
group.objectIds = [object.id];
|
|
24089
24782
|
return group;
|
|
24090
24783
|
}
|
|
24091
24784
|
}
|
|
@@ -29510,4 +30203,4 @@ const CURRENT_APP_STATE_SCHEMA_VERSION = 2;
|
|
|
29510
30203
|
/** Current schema version for workspace Y.Docs (drawable objects). */
|
|
29511
30204
|
const CURRENT_WORKSPACE_SCHEMA_VERSION = 2;
|
|
29512
30205
|
|
|
29513
|
-
export {
|
|
30206
|
+
export { KritzelSelectionGroup as $, AssetNotFoundError as A, DEFAULT_ASSET_STORAGE_CONFIG as B, lightTheme as C, DEFAULT_BRUSH_CONFIG as D, darkTheme as E, EN_LOCALE as F, DE_LOCALE as G, HocuspocusProvider as H, IndexedDBAssetProvider as I, FR_LOCALE as J, KritzelBaseObject as K, KritzelAlignment as L, runMigrations as M, APP_STATE_MIGRATIONS as N, WORKSPACE_MIGRATIONS as O, CURRENT_APP_STATE_SCHEMA_VERSION as P, CURRENT_WORKSPACE_SCHEMA_VERSION as Q, KritzelColorHelper as R, ShapeType as S, KritzelDevicesHelper as T, KritzelMouseButton as U, KritzelHTMLHelper as V, WORKSPACE_EXPORT_VERSION as W, DEFAULT_STROKE_SIZES as X, DEFAULT_COLOR_PALETTE as Y, ObjectHelper as Z, ThemeHelper as _, HocuspocusProviderWebsocket as a, KritzelSelectionBox as a0, KritzelIconRegistry as a1, KritzelKeyboardHelper as a2, KritzelBaseHandler as a3, KritzelClassHelper as a4, imageCompression as a5, KritzelEventHelper as a6, KritzelText as b, KritzelPath as c, KritzelImage as d, KritzelLine as e, KritzelGroup as f, KritzelShape as g, KritzelCustomElement as h, KritzelBaseTool as i, KritzelBrushTool as j, KritzelLineTool as k, KritzelEraserTool as l, KritzelImageTool as m, KritzelTextTool as n, KritzelShapeTool as o, KritzelCursorHelper as p, KritzelSelectionTool as q, KritzelAssetResolver as r, KritzelWorkspace as s, KritzelAnchorManager as t, KritzelThemeManager as u, KritzelLocalizationManager as v, KritzelLicenseManager as w, KritzelCustomElementRendererRegistry as x, DEFAULT_TEXT_CONFIG as y, DEFAULT_LINE_TOOL_CONFIG as z };
|