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
|
@@ -36,6 +36,9 @@ class ObjectHelper {
|
|
|
36
36
|
}
|
|
37
37
|
return Object?.keys(obj).length === 0 && obj?.constructor === Object;
|
|
38
38
|
}
|
|
39
|
+
static isClass(object, className) {
|
|
40
|
+
return !!object && object.__class__ === className;
|
|
41
|
+
}
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
class KritzelGeometryHelper {
|
|
@@ -224,7 +227,8 @@ class KritzelBaseObject {
|
|
|
224
227
|
isMounted = false;
|
|
225
228
|
isEditable = false;
|
|
226
229
|
isInteractive = false;
|
|
227
|
-
|
|
230
|
+
isResizable = true;
|
|
231
|
+
isRotatable = true;
|
|
228
232
|
/**
|
|
229
233
|
* Gets the total width of the object including padding.
|
|
230
234
|
* @returns The width plus double the padding value
|
|
@@ -531,21 +535,24 @@ class KritzelBaseObject {
|
|
|
531
535
|
Object.assign(copiedObject, this);
|
|
532
536
|
copiedObject.id = this.generateId();
|
|
533
537
|
copiedObject.isMounted = false;
|
|
538
|
+
copiedObject.isSelected = false;
|
|
539
|
+
copiedObject.isHovered = false;
|
|
540
|
+
copiedObject.isInteractive = false;
|
|
541
|
+
copiedObject.resizing = false;
|
|
542
|
+
copiedObject.markedForRemoval = false;
|
|
543
|
+
copiedObject.isVisible = true;
|
|
534
544
|
return copiedObject;
|
|
535
545
|
}
|
|
536
546
|
/**
|
|
537
547
|
* Serializes the object to a plain JavaScript object for persistence.
|
|
538
548
|
* Excludes internal references (_core, _elementRef) and computed properties.
|
|
539
|
-
*
|
|
549
|
+
* Subclasses should override to handle domain-specific serialization (e.g., HTML elements).
|
|
540
550
|
* @returns A serializable object containing all persistent properties
|
|
541
551
|
*/
|
|
542
552
|
serialize() {
|
|
543
|
-
const { _core, _elementRef, element, totalWidth, totalHeight, ...remainingProps } = this;
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
clonedProps.element = element.cloneNode(true);
|
|
547
|
-
}
|
|
548
|
-
return clonedProps;
|
|
553
|
+
const { _core, _elementRef, element, totalWidth, totalHeight, isSelected, isHovered, isMounted, isInteractive, resizing, markedForRemoval, isVisible, ...remainingProps } = this;
|
|
554
|
+
// No longer clone raw elements—let subclasses handle serializable payloads
|
|
555
|
+
return structuredClone(remainingProps);
|
|
549
556
|
}
|
|
550
557
|
/**
|
|
551
558
|
* Restores the object's state from a serialized plain object.
|
|
@@ -556,6 +563,14 @@ class KritzelBaseObject {
|
|
|
556
563
|
*/
|
|
557
564
|
deserialize(object) {
|
|
558
565
|
Object.assign(this, object);
|
|
566
|
+
// Selection/interactivity state is transient UI state and must not survive revival.
|
|
567
|
+
this.isSelected = false;
|
|
568
|
+
this.isHovered = false;
|
|
569
|
+
this.isMounted = false;
|
|
570
|
+
this.isInteractive = false;
|
|
571
|
+
this.resizing = false;
|
|
572
|
+
this.markedForRemoval = false;
|
|
573
|
+
this.isVisible = true;
|
|
559
574
|
return this;
|
|
560
575
|
}
|
|
561
576
|
/**
|
|
@@ -16455,7 +16470,6 @@ class IndexedDBAssetProvider {
|
|
|
16455
16470
|
type = 'local';
|
|
16456
16471
|
name = 'IndexedDBAssetProvider';
|
|
16457
16472
|
_dbName;
|
|
16458
|
-
_quiet;
|
|
16459
16473
|
_db = null;
|
|
16460
16474
|
/** In-memory cache of id -> object URL to avoid re-creating blob URLs. */
|
|
16461
16475
|
_urlCache = new Map();
|
|
@@ -16463,7 +16477,6 @@ class IndexedDBAssetProvider {
|
|
|
16463
16477
|
_knownIds = new Set();
|
|
16464
16478
|
constructor(options) {
|
|
16465
16479
|
this._dbName = options?.dbName ?? DEFAULT_DB_NAME;
|
|
16466
|
-
this._quiet = options?.quiet ?? false;
|
|
16467
16480
|
}
|
|
16468
16481
|
/**
|
|
16469
16482
|
* Factory helper enabling use as `IndexedDBAssetProvider.with({ dbName: '...' })`
|
|
@@ -16478,9 +16491,6 @@ class IndexedDBAssetProvider {
|
|
|
16478
16491
|
this._db = await this.openDatabase();
|
|
16479
16492
|
// Warm up the known-ids cache so canResolve can answer synchronously.
|
|
16480
16493
|
await this.loadKnownIds();
|
|
16481
|
-
if (!this._quiet) {
|
|
16482
|
-
console.info(`IndexedDBAssetProvider initialized: ${this._dbName}`);
|
|
16483
|
-
}
|
|
16484
16494
|
}
|
|
16485
16495
|
destroy() {
|
|
16486
16496
|
for (const url of this._urlCache.values()) {
|
|
@@ -16497,9 +16507,6 @@ class IndexedDBAssetProvider {
|
|
|
16497
16507
|
this._db.close();
|
|
16498
16508
|
this._db = null;
|
|
16499
16509
|
}
|
|
16500
|
-
if (!this._quiet) {
|
|
16501
|
-
console.info(`IndexedDBAssetProvider destroyed: ${this._dbName}`);
|
|
16502
|
-
}
|
|
16503
16510
|
}
|
|
16504
16511
|
canResolve(id) {
|
|
16505
16512
|
return this._knownIds.has(id);
|
|
@@ -16744,9 +16751,13 @@ class KritzelAssetResolver {
|
|
|
16744
16751
|
_pending = new Map();
|
|
16745
16752
|
_flushTimer = null;
|
|
16746
16753
|
_destroyed = false;
|
|
16747
|
-
_quiet =
|
|
16754
|
+
_quiet = true;
|
|
16748
16755
|
/** Dedupes concurrent resolves of the same id. */
|
|
16749
16756
|
_resolvePromises = new Map();
|
|
16757
|
+
/** Enables or disables resolver diagnostic logging. */
|
|
16758
|
+
setShowInfo(show) {
|
|
16759
|
+
this._quiet = !show;
|
|
16760
|
+
}
|
|
16750
16761
|
/**
|
|
16751
16762
|
* Whether any remote provider is configured. Used by the engine to
|
|
16752
16763
|
* decide whether to warn about a missing remote provider when Yjs is
|
|
@@ -16772,7 +16783,6 @@ class KritzelAssetResolver {
|
|
|
16772
16783
|
// Ensure the deferred ready promise exists before any async work so
|
|
16773
16784
|
// concurrent callers awaiting `ensureReady()` are unblocked together.
|
|
16774
16785
|
this.getReadyPromise();
|
|
16775
|
-
this._quiet = config?.quiet ?? false;
|
|
16776
16786
|
const providers = this.instantiateProviders(config?.providers ?? []);
|
|
16777
16787
|
// Ensure a local provider always exists so uploads can be optimistic
|
|
16778
16788
|
// and remote misses can be cached for later offline access.
|
|
@@ -16805,8 +16815,10 @@ class KritzelAssetResolver {
|
|
|
16805
16815
|
await provider.init();
|
|
16806
16816
|
}
|
|
16807
16817
|
catch (err) {
|
|
16808
|
-
|
|
16809
|
-
`
|
|
16818
|
+
if (this.canLogInfo()) {
|
|
16819
|
+
console.warn(`[Kritzel] Asset resolver remote provider "${provider.name}" failed to initialize. ` +
|
|
16820
|
+
`Assets will be served from the local provider until the remote becomes available.`, err);
|
|
16821
|
+
}
|
|
16810
16822
|
}
|
|
16811
16823
|
}
|
|
16812
16824
|
// Re-enqueue any uploads that were interrupted before completing.
|
|
@@ -16818,7 +16830,7 @@ class KritzelAssetResolver {
|
|
|
16818
16830
|
this._resolveReady = null;
|
|
16819
16831
|
this._rejectReady = null;
|
|
16820
16832
|
}
|
|
16821
|
-
if (
|
|
16833
|
+
if (this.canLogInfo()) {
|
|
16822
16834
|
this.logProviderConfiguration();
|
|
16823
16835
|
}
|
|
16824
16836
|
if (this._pending.size > 0) {
|
|
@@ -16827,8 +16839,8 @@ class KritzelAssetResolver {
|
|
|
16827
16839
|
}
|
|
16828
16840
|
/** Tears down the resolver and all providers. */
|
|
16829
16841
|
destroy() {
|
|
16830
|
-
if (
|
|
16831
|
-
console.info('Asset resolver destroyed');
|
|
16842
|
+
if (this.canLogInfo()) {
|
|
16843
|
+
console.info('[Kritzel] Asset resolver destroyed');
|
|
16832
16844
|
}
|
|
16833
16845
|
this._destroyed = true;
|
|
16834
16846
|
if (this._flushTimer) {
|
|
@@ -17024,7 +17036,7 @@ class KritzelAssetResolver {
|
|
|
17024
17036
|
// ------------------------------------------------------------------
|
|
17025
17037
|
logProviderConfiguration() {
|
|
17026
17038
|
const lines = [
|
|
17027
|
-
`Asset resolver initialized with ${this._providers.length} provider(s):`,
|
|
17039
|
+
`[Kritzel] Asset resolver initialized with ${this._providers.length} provider(s):`,
|
|
17028
17040
|
];
|
|
17029
17041
|
for (const provider of this._providers) {
|
|
17030
17042
|
const role = provider === this._local ? 'local (primary write target)' : provider.type;
|
|
@@ -17083,8 +17095,8 @@ class KritzelAssetResolver {
|
|
|
17083
17095
|
recoveredCount++;
|
|
17084
17096
|
}
|
|
17085
17097
|
}
|
|
17086
|
-
if (
|
|
17087
|
-
console.info(`Asset resolver
|
|
17098
|
+
if (this.canLogInfo() && recoveredCount > 0) {
|
|
17099
|
+
console.info(`[Kritzel] Asset resolver recovered ${recoveredCount} pending upload(s)`);
|
|
17088
17100
|
}
|
|
17089
17101
|
}
|
|
17090
17102
|
scheduleFlush(delayMs) {
|
|
@@ -17139,8 +17151,8 @@ class KritzelAssetResolver {
|
|
|
17139
17151
|
});
|
|
17140
17152
|
this._pending.delete(key);
|
|
17141
17153
|
await this.clearPendingRemote(job.id, job.providerName);
|
|
17142
|
-
if (
|
|
17143
|
-
console.info(`Asset resolver
|
|
17154
|
+
if (this.canLogInfo()) {
|
|
17155
|
+
console.info(`[Kritzel] Asset resolver uploaded ${job.id} to ${provider.name}`);
|
|
17144
17156
|
}
|
|
17145
17157
|
}
|
|
17146
17158
|
catch (err) {
|
|
@@ -17148,8 +17160,10 @@ class KritzelAssetResolver {
|
|
|
17148
17160
|
const delay = Math.min(MAX_RETRY_DELAY_MS, BASE_RETRY_DELAY_MS * Math.pow(2, job.attempts - 1));
|
|
17149
17161
|
job.nextAttemptAt = Date.now() + delay;
|
|
17150
17162
|
nextRetryIn = Math.min(nextRetryIn, delay);
|
|
17151
|
-
|
|
17152
|
-
`
|
|
17163
|
+
if (this.canLogInfo()) {
|
|
17164
|
+
console.warn(`[Kritzel] Asset resolver upload to ${provider.name} failed for ${job.id} (attempt ${job.attempts}). ` +
|
|
17165
|
+
`Retrying in ${Math.round(delay / 1000)}s.`, err);
|
|
17166
|
+
}
|
|
17153
17167
|
}
|
|
17154
17168
|
}
|
|
17155
17169
|
if (this._pending.size > 0 && Number.isFinite(nextRetryIn)) {
|
|
@@ -17207,6 +17221,9 @@ class KritzelAssetResolver {
|
|
|
17207
17221
|
const res = await fetch(dataUrl);
|
|
17208
17222
|
return res.blob();
|
|
17209
17223
|
}
|
|
17224
|
+
canLogInfo() {
|
|
17225
|
+
return !this._quiet;
|
|
17226
|
+
}
|
|
17210
17227
|
}
|
|
17211
17228
|
|
|
17212
17229
|
/**
|
|
@@ -17247,7 +17264,6 @@ class KritzelImage extends KritzelBaseObject {
|
|
|
17247
17264
|
maxWidth = 300;
|
|
17248
17265
|
maxHeight = 300;
|
|
17249
17266
|
maxCompressionSize = 1200;
|
|
17250
|
-
isDebugInfoVisible = true;
|
|
17251
17267
|
// --- Transient (not persisted to Yjs) ---
|
|
17252
17268
|
/**
|
|
17253
17269
|
* URL resolved from the asset storage layer (e.g. a `blob:` or https URL).
|
|
@@ -19660,6 +19676,610 @@ class KritzelShape extends KritzelBaseObject {
|
|
|
19660
19676
|
}
|
|
19661
19677
|
}
|
|
19662
19678
|
|
|
19679
|
+
const KRITZEL_RENDERER_REGISTRY_KEY = '__kritzelCustomElementRendererRegistry__';
|
|
19680
|
+
const getGlobalRegistryStore = () => {
|
|
19681
|
+
const globalScope = globalThis;
|
|
19682
|
+
const existing = globalScope[KRITZEL_RENDERER_REGISTRY_KEY];
|
|
19683
|
+
if (existing) {
|
|
19684
|
+
return existing;
|
|
19685
|
+
}
|
|
19686
|
+
const created = {
|
|
19687
|
+
renderers: new Map(),
|
|
19688
|
+
};
|
|
19689
|
+
globalScope[KRITZEL_RENDERER_REGISTRY_KEY] = created;
|
|
19690
|
+
return created;
|
|
19691
|
+
};
|
|
19692
|
+
/**
|
|
19693
|
+
* Global registry for custom element renderers.
|
|
19694
|
+
* Allows wrapper frameworks (Angular, React, Vue) to register renderer implementations
|
|
19695
|
+
* without coupling to core, and enables deterministic rehydration across reloads and
|
|
19696
|
+
* clipboard paste operations.
|
|
19697
|
+
*
|
|
19698
|
+
* Renderer keys are opaque strings that must be stable (e.g., 'angular-todo', 'react-editor').
|
|
19699
|
+
* When a custom element is deserialized with a rendererKey, the registry is consulted to
|
|
19700
|
+
* resolve the renderer and mount with the persisted rendererData.
|
|
19701
|
+
*/
|
|
19702
|
+
class KritzelCustomElementRendererRegistry {
|
|
19703
|
+
static get _renderers() {
|
|
19704
|
+
return getGlobalRegistryStore().renderers;
|
|
19705
|
+
}
|
|
19706
|
+
/**
|
|
19707
|
+
* Registers a renderer implementation for a given key.
|
|
19708
|
+
* @param key - Stable identifier for this renderer (e.g., 'angular-my-component')
|
|
19709
|
+
* @param renderer - Renderer implementation with mount/unmount/update handlers
|
|
19710
|
+
*/
|
|
19711
|
+
static register(key, renderer) {
|
|
19712
|
+
this._renderers.set(key, renderer);
|
|
19713
|
+
}
|
|
19714
|
+
/**
|
|
19715
|
+
* Retrieves a registered renderer by key.
|
|
19716
|
+
* @param key - Renderer identifier
|
|
19717
|
+
* @returns The registered renderer, or undefined if not found
|
|
19718
|
+
*/
|
|
19719
|
+
static get(key) {
|
|
19720
|
+
return this._renderers.get(key);
|
|
19721
|
+
}
|
|
19722
|
+
/**
|
|
19723
|
+
* Unregisters a renderer by key.
|
|
19724
|
+
* @param key - Renderer identifier to remove
|
|
19725
|
+
* @returns True if the renderer was found and removed; false otherwise
|
|
19726
|
+
*/
|
|
19727
|
+
static unregister(key) {
|
|
19728
|
+
return this._renderers.delete(key);
|
|
19729
|
+
}
|
|
19730
|
+
}
|
|
19731
|
+
|
|
19732
|
+
class KritzelHTMLHelper {
|
|
19733
|
+
static getNumericValueFromStyle(element, property) {
|
|
19734
|
+
const value = window.getComputedStyle(element).getPropertyValue(property);
|
|
19735
|
+
return parseFloat(value) || 0;
|
|
19736
|
+
}
|
|
19737
|
+
static isScrollableElement(element) {
|
|
19738
|
+
if (!(element instanceof HTMLElement)) {
|
|
19739
|
+
return false;
|
|
19740
|
+
}
|
|
19741
|
+
const style = window.getComputedStyle(element);
|
|
19742
|
+
const overflowX = style.overflowX;
|
|
19743
|
+
const overflowY = style.overflowY;
|
|
19744
|
+
const overflow = style.overflow;
|
|
19745
|
+
const canScrollX = (overflowX === 'auto' || overflowX === 'scroll' || overflowX === 'overlay') && element.scrollWidth > element.clientWidth;
|
|
19746
|
+
const canScrollY = (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') && element.scrollHeight > element.clientHeight;
|
|
19747
|
+
const canScroll = overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay';
|
|
19748
|
+
return canScrollX || canScrollY || (canScroll && (element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight));
|
|
19749
|
+
}
|
|
19750
|
+
static findScrollableElement(root) {
|
|
19751
|
+
if (!root) {
|
|
19752
|
+
return null;
|
|
19753
|
+
}
|
|
19754
|
+
const visited = new Set();
|
|
19755
|
+
const walk = (element) => {
|
|
19756
|
+
if (visited.has(element)) {
|
|
19757
|
+
return null;
|
|
19758
|
+
}
|
|
19759
|
+
visited.add(element);
|
|
19760
|
+
if (this.isScrollableElement(element)) {
|
|
19761
|
+
return element;
|
|
19762
|
+
}
|
|
19763
|
+
if (element.shadowRoot) {
|
|
19764
|
+
for (const child of Array.from(element.shadowRoot.children)) {
|
|
19765
|
+
const scrollable = walk(child);
|
|
19766
|
+
if (scrollable) {
|
|
19767
|
+
return scrollable;
|
|
19768
|
+
}
|
|
19769
|
+
}
|
|
19770
|
+
}
|
|
19771
|
+
if (element.tagName === 'SLOT') {
|
|
19772
|
+
for (const assignedElement of element.assignedElements({ flatten: true })) {
|
|
19773
|
+
const scrollable = walk(assignedElement);
|
|
19774
|
+
if (scrollable) {
|
|
19775
|
+
return scrollable;
|
|
19776
|
+
}
|
|
19777
|
+
}
|
|
19778
|
+
}
|
|
19779
|
+
for (const child of Array.from(element.children)) {
|
|
19780
|
+
const scrollable = walk(child);
|
|
19781
|
+
if (scrollable) {
|
|
19782
|
+
return scrollable;
|
|
19783
|
+
}
|
|
19784
|
+
}
|
|
19785
|
+
return null;
|
|
19786
|
+
};
|
|
19787
|
+
return walk(root);
|
|
19788
|
+
}
|
|
19789
|
+
static getScrollableParent(element) {
|
|
19790
|
+
if (!element) {
|
|
19791
|
+
return window;
|
|
19792
|
+
}
|
|
19793
|
+
const parent = element.parentNode?.host ?? element.parentElement;
|
|
19794
|
+
if (!parent || parent.tagName === 'BODY') {
|
|
19795
|
+
return window;
|
|
19796
|
+
}
|
|
19797
|
+
const style = window.getComputedStyle(parent);
|
|
19798
|
+
if (style.overflow === 'auto' || style.overflowY === 'auto' || style.overflow === 'scroll' || style.overflowY === 'scroll') {
|
|
19799
|
+
return parent;
|
|
19800
|
+
}
|
|
19801
|
+
return this.getScrollableParent(parent);
|
|
19802
|
+
}
|
|
19803
|
+
static isElementInViewport(element) {
|
|
19804
|
+
if (!element) {
|
|
19805
|
+
return false;
|
|
19806
|
+
}
|
|
19807
|
+
const scrollableParent = this.getScrollableParent(element);
|
|
19808
|
+
const rect = element.getBoundingClientRect();
|
|
19809
|
+
if (scrollableParent === window) {
|
|
19810
|
+
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
19811
|
+
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
19812
|
+
return rect.top >= 0 && rect.left >= 0 && rect.bottom <= viewportHeight && rect.right <= viewportWidth;
|
|
19813
|
+
}
|
|
19814
|
+
const parentRect = scrollableParent.getBoundingClientRect();
|
|
19815
|
+
return rect.top >= parentRect.top && rect.left >= parentRect.left && rect.bottom <= parentRect.bottom && rect.right <= parentRect.right;
|
|
19816
|
+
}
|
|
19817
|
+
static getFocusableElements(root) {
|
|
19818
|
+
if (!root)
|
|
19819
|
+
return [];
|
|
19820
|
+
const focusableElements = [];
|
|
19821
|
+
const focusableSelector = 'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details, [tabindex]:not([tabindex="-1"])';
|
|
19822
|
+
const isVisible = (el) => {
|
|
19823
|
+
const style = window.getComputedStyle(el);
|
|
19824
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
19825
|
+
};
|
|
19826
|
+
const find = (element) => {
|
|
19827
|
+
// Check if the element itself is focusable
|
|
19828
|
+
if (element instanceof HTMLElement && element.matches(focusableSelector) && !element.hasAttribute('disabled') && isVisible(element)) {
|
|
19829
|
+
focusableElements.push(element);
|
|
19830
|
+
}
|
|
19831
|
+
// Traverse shadow DOM
|
|
19832
|
+
if (element.shadowRoot) {
|
|
19833
|
+
Array.from(element.shadowRoot.children).forEach(find);
|
|
19834
|
+
}
|
|
19835
|
+
// Traverse slotted content
|
|
19836
|
+
if (element.tagName === 'SLOT') {
|
|
19837
|
+
element.assignedElements({ flatten: true }).forEach(find);
|
|
19838
|
+
}
|
|
19839
|
+
// Traverse light DOM children if there is no shadow root
|
|
19840
|
+
if (!element.shadowRoot) {
|
|
19841
|
+
Array.from(element.children).forEach(find);
|
|
19842
|
+
}
|
|
19843
|
+
};
|
|
19844
|
+
find(root);
|
|
19845
|
+
return focusableElements;
|
|
19846
|
+
}
|
|
19847
|
+
}
|
|
19848
|
+
|
|
19849
|
+
/**
|
|
19850
|
+
* Represents a custom HTML element that can be placed and manipulated on the Kritzel canvas.
|
|
19851
|
+
* Extends KritzelBaseObject to inherit common object functionality like positioning,
|
|
19852
|
+
* scaling, and lifecycle management.
|
|
19853
|
+
*/
|
|
19854
|
+
class KritzelCustomElement extends KritzelBaseObject {
|
|
19855
|
+
__class__ = 'KritzelCustomElement';
|
|
19856
|
+
/** The HTML element to be rendered on the canvas. */
|
|
19857
|
+
element;
|
|
19858
|
+
/**
|
|
19859
|
+
* Optional identifier for a registered renderer (e.g., 'angular-component', 'react-default').
|
|
19860
|
+
* When set, mount() resolves the renderer from a global registry and delegates lifecycle.
|
|
19861
|
+
* If null, falls back to DOM element mounting.
|
|
19862
|
+
*/
|
|
19863
|
+
rendererKey = null;
|
|
19864
|
+
/**
|
|
19865
|
+
* Custom data payload for the renderer (e.g., Angular component inputs, React props, Vue data).
|
|
19866
|
+
* Only meaningful when rendererKey is set. Captured on unmount and restored on rehydrate.
|
|
19867
|
+
*/
|
|
19868
|
+
rendererData;
|
|
19869
|
+
/**
|
|
19870
|
+
* Creates a new KritzelCustomElement instance.
|
|
19871
|
+
* @param config - Optional configuration object for the custom element.
|
|
19872
|
+
* @param config.element - The HTML element to wrap.
|
|
19873
|
+
* @param config.translateX - Initial X position offset. Defaults to 0.
|
|
19874
|
+
* @param config.translateY - Initial Y position offset. Defaults to 0.
|
|
19875
|
+
* @param config.scale - Initial scale factor. Defaults to 1.
|
|
19876
|
+
* @param config.height - Initial height in pixels. Defaults to 0.
|
|
19877
|
+
* @param config.width - Initial width in pixels. Defaults to 0.
|
|
19878
|
+
*/
|
|
19879
|
+
constructor(config) {
|
|
19880
|
+
super();
|
|
19881
|
+
if (config) {
|
|
19882
|
+
this.translateX = config.translateX || 0;
|
|
19883
|
+
this.translateY = config.translateY || 0;
|
|
19884
|
+
this.rotation = KritzelMathHelper.degreesToRadians(config.rotation ?? 0);
|
|
19885
|
+
this.scale = config.scale || 1;
|
|
19886
|
+
this.element = config.element ?? this.createPlaceholder();
|
|
19887
|
+
this.height = config.height || 0;
|
|
19888
|
+
this.width = config.width || 0;
|
|
19889
|
+
this.rendererKey = config.rendererKey ?? null;
|
|
19890
|
+
this.rendererData = config.rendererData;
|
|
19891
|
+
}
|
|
19892
|
+
}
|
|
19893
|
+
/**
|
|
19894
|
+
* Factory method to create a new KritzelCustomElement with proper core binding.
|
|
19895
|
+
* Generates a unique ID and assigns the element to the active workspace.
|
|
19896
|
+
* @param core - The KritzelCore instance for state management and ID generation.
|
|
19897
|
+
* @param config - Optional configuration object for the custom element.
|
|
19898
|
+
* @param config.element - The HTML element to wrap.
|
|
19899
|
+
* @param config.translateX - Initial X position offset.
|
|
19900
|
+
* @param config.translateY - Initial Y position offset.
|
|
19901
|
+
* @param config.scale - Initial scale factor.
|
|
19902
|
+
* @param config.height - Initial height in pixels.
|
|
19903
|
+
* @param config.width - Initial width in pixels.
|
|
19904
|
+
* @returns A new KritzelBaseObject instance bound to the core.
|
|
19905
|
+
*/
|
|
19906
|
+
static create(core, config) {
|
|
19907
|
+
const object = new KritzelCustomElement(config);
|
|
19908
|
+
object._core = core;
|
|
19909
|
+
object.id = object.generateId();
|
|
19910
|
+
object.workspaceId = core.store.state.activeWorkspace.id;
|
|
19911
|
+
object.userId = core.user?.id;
|
|
19912
|
+
object.zIndex = core.store.currentZIndex;
|
|
19913
|
+
return object;
|
|
19914
|
+
}
|
|
19915
|
+
/**
|
|
19916
|
+
* Mounts the custom element to a container element in the DOM.
|
|
19917
|
+
* If a renderer is registered for this.rendererKey, delegates to the renderer.
|
|
19918
|
+
* Otherwise, uses the standard DOM element mounting fallback.
|
|
19919
|
+
* Skips mounting if already mounted to the same container with content attached.
|
|
19920
|
+
* @param element - The container HTML element to mount this custom element into.
|
|
19921
|
+
*/
|
|
19922
|
+
mount(element) {
|
|
19923
|
+
if (element === null) {
|
|
19924
|
+
return;
|
|
19925
|
+
}
|
|
19926
|
+
this.applyInteractiveState();
|
|
19927
|
+
if (this.isMounted && this.elementRef === element) {
|
|
19928
|
+
// Renderer-based mounts should not be re-invoked on every engine rerender.
|
|
19929
|
+
if (this.rendererKey) {
|
|
19930
|
+
return;
|
|
19931
|
+
}
|
|
19932
|
+
// For direct DOM fallback, remount only if content was detached.
|
|
19933
|
+
if (this.element.parentElement === element) {
|
|
19934
|
+
return;
|
|
19935
|
+
}
|
|
19936
|
+
}
|
|
19937
|
+
this.elementRef = element;
|
|
19938
|
+
this.isMounted = true;
|
|
19939
|
+
// Try renderer-based approach if a key is set
|
|
19940
|
+
if (this.rendererKey) {
|
|
19941
|
+
const renderer = KritzelCustomElementRendererRegistry.get(this.rendererKey);
|
|
19942
|
+
if (renderer) {
|
|
19943
|
+
element.innerHTML = ''; // Clear container
|
|
19944
|
+
renderer.onMount({
|
|
19945
|
+
object: this,
|
|
19946
|
+
container: element,
|
|
19947
|
+
data: this.rendererData,
|
|
19948
|
+
});
|
|
19949
|
+
this.applyInteractiveState();
|
|
19950
|
+
return;
|
|
19951
|
+
}
|
|
19952
|
+
}
|
|
19953
|
+
// Fallback to standard DOM mounting
|
|
19954
|
+
this.elementRef.innerHTML = '';
|
|
19955
|
+
this.elementRef.appendChild(this.element);
|
|
19956
|
+
this.applyInteractiveState();
|
|
19957
|
+
}
|
|
19958
|
+
/**
|
|
19959
|
+
* Updates interactivity state for embedded HTML content.
|
|
19960
|
+
* When disabling interactivity, any focused descendant is blurred so
|
|
19961
|
+
* keyboard input is released immediately.
|
|
19962
|
+
*/
|
|
19963
|
+
setIsInteractive(isInteractive) {
|
|
19964
|
+
const hasChanged = this.isInteractive !== isInteractive;
|
|
19965
|
+
this.isInteractive = isInteractive;
|
|
19966
|
+
this.applyInteractiveState();
|
|
19967
|
+
if (hasChanged && !isInteractive) {
|
|
19968
|
+
this.blurFocusedDescendant();
|
|
19969
|
+
}
|
|
19970
|
+
}
|
|
19971
|
+
applyInteractiveState() {
|
|
19972
|
+
const pointerEvents = this.isInteractive ? 'auto' : 'none';
|
|
19973
|
+
const contentCursor = this.isInteractive ? 'auto' : '';
|
|
19974
|
+
if (this.element) {
|
|
19975
|
+
this.element.style.pointerEvents = pointerEvents;
|
|
19976
|
+
this.element.style.cursor = contentCursor;
|
|
19977
|
+
}
|
|
19978
|
+
if (this.elementRef instanceof HTMLElement) {
|
|
19979
|
+
// Keep the mount container clickable so unselected custom elements can still be selected.
|
|
19980
|
+
this.elementRef.style.pointerEvents = 'auto';
|
|
19981
|
+
this.elementRef.style.cursor = '';
|
|
19982
|
+
Array.from(this.elementRef.children).forEach(child => {
|
|
19983
|
+
if (child instanceof HTMLElement) {
|
|
19984
|
+
child.style.pointerEvents = pointerEvents;
|
|
19985
|
+
child.style.cursor = contentCursor;
|
|
19986
|
+
}
|
|
19987
|
+
});
|
|
19988
|
+
}
|
|
19989
|
+
}
|
|
19990
|
+
blurFocusedDescendant() {
|
|
19991
|
+
if (!this.element && !(this.elementRef instanceof HTMLElement)) {
|
|
19992
|
+
return;
|
|
19993
|
+
}
|
|
19994
|
+
const activeElement = this.getDeepActiveElement();
|
|
19995
|
+
const isInsideElement = !!this.element && this.isNodeInsideHost(activeElement, this.element);
|
|
19996
|
+
const isInsideMountedContainer = this.elementRef instanceof HTMLElement && this.isNodeInsideHost(activeElement, this.elementRef);
|
|
19997
|
+
if (activeElement && (isInsideElement || isInsideMountedContainer)) {
|
|
19998
|
+
activeElement.blur();
|
|
19999
|
+
}
|
|
20000
|
+
}
|
|
20001
|
+
/**
|
|
20002
|
+
* Returns true when a text-input-like element inside this custom element
|
|
20003
|
+
* currently owns focus.
|
|
20004
|
+
*/
|
|
20005
|
+
hasFocusedInputElement() {
|
|
20006
|
+
if (!this.isInteractive) {
|
|
20007
|
+
return false;
|
|
20008
|
+
}
|
|
20009
|
+
const focusHosts = this.getFocusHosts();
|
|
20010
|
+
// Primary path: locate the focused input-like element by walking from each host through
|
|
20011
|
+
// `:focus` (piercing open shadow roots). This works in real browsers even when the engine
|
|
20012
|
+
// and editor are nested web components, because it never relies on the document-level
|
|
20013
|
+
// active element resolving to a descendant of the host.
|
|
20014
|
+
if (focusHosts.some(host => this.hasFocusedInputDescendant(host))) {
|
|
20015
|
+
return true;
|
|
20016
|
+
}
|
|
20017
|
+
// Fallback path: resolve the deep active element and confirm it is input-like and inside a
|
|
20018
|
+
// host. Covers contenteditable content inside nested shadow roots (which `querySelector`
|
|
20019
|
+
// cannot reach) and test environments that mock the active element without real focus.
|
|
20020
|
+
const activeElement = this.getDeepActiveElement();
|
|
20021
|
+
if (activeElement && this.isInputLikeElement(activeElement) && focusHosts.some(host => this.isNodeInsideHost(activeElement, host))) {
|
|
20022
|
+
return true;
|
|
20023
|
+
}
|
|
20024
|
+
return false;
|
|
20025
|
+
}
|
|
20026
|
+
getFocusHosts() {
|
|
20027
|
+
const hosts = [];
|
|
20028
|
+
if (this.element) {
|
|
20029
|
+
hosts.push(this.element);
|
|
20030
|
+
}
|
|
20031
|
+
if (this.elementRef instanceof HTMLElement) {
|
|
20032
|
+
hosts.push(this.elementRef);
|
|
20033
|
+
const firstChild = this.elementRef.firstElementChild;
|
|
20034
|
+
if (firstChild instanceof HTMLElement) {
|
|
20035
|
+
hosts.push(firstChild);
|
|
20036
|
+
}
|
|
20037
|
+
}
|
|
20038
|
+
return hosts;
|
|
20039
|
+
}
|
|
20040
|
+
/**
|
|
20041
|
+
* Returns true when the given host (or, via the flat tree, one of its shadow descendants)
|
|
20042
|
+
* currently contains a focused input-like element.
|
|
20043
|
+
*/
|
|
20044
|
+
hasFocusedInputDescendant(host) {
|
|
20045
|
+
if (this.isInputLikeElement(host) && this.matchesFocus(host)) {
|
|
20046
|
+
return true;
|
|
20047
|
+
}
|
|
20048
|
+
// `:focus-within` crosses shadow boundaries for the host, so it is a cheap guard that tells
|
|
20049
|
+
// us whether focus lives anywhere inside this host before doing the deeper traversal.
|
|
20050
|
+
if (typeof host.matches === 'function') {
|
|
20051
|
+
try {
|
|
20052
|
+
if (!host.matches(':focus-within')) {
|
|
20053
|
+
return false;
|
|
20054
|
+
}
|
|
20055
|
+
}
|
|
20056
|
+
catch {
|
|
20057
|
+
// Selector unsupported in this environment; fall through to the traversal below.
|
|
20058
|
+
}
|
|
20059
|
+
}
|
|
20060
|
+
return this.findFocusedInputLikeElement(host) !== null;
|
|
20061
|
+
}
|
|
20062
|
+
/**
|
|
20063
|
+
* Walks the `:focus` chain from a tree scope, descending into open shadow roots, and returns
|
|
20064
|
+
* the focused element when it is input-like.
|
|
20065
|
+
*/
|
|
20066
|
+
findFocusedInputLikeElement(root) {
|
|
20067
|
+
let focused = null;
|
|
20068
|
+
try {
|
|
20069
|
+
focused = root.querySelector(':focus');
|
|
20070
|
+
}
|
|
20071
|
+
catch {
|
|
20072
|
+
focused = null;
|
|
20073
|
+
}
|
|
20074
|
+
if (!(focused instanceof HTMLElement)) {
|
|
20075
|
+
return null;
|
|
20076
|
+
}
|
|
20077
|
+
if (this.isInputLikeElement(focused)) {
|
|
20078
|
+
return focused;
|
|
20079
|
+
}
|
|
20080
|
+
if (focused.shadowRoot) {
|
|
20081
|
+
return this.findFocusedInputLikeElement(focused.shadowRoot);
|
|
20082
|
+
}
|
|
20083
|
+
return null;
|
|
20084
|
+
}
|
|
20085
|
+
matchesFocus(element) {
|
|
20086
|
+
if (typeof element.matches !== 'function') {
|
|
20087
|
+
return false;
|
|
20088
|
+
}
|
|
20089
|
+
try {
|
|
20090
|
+
return element.matches(':focus');
|
|
20091
|
+
}
|
|
20092
|
+
catch {
|
|
20093
|
+
return false;
|
|
20094
|
+
}
|
|
20095
|
+
}
|
|
20096
|
+
isInputLikeElement(element) {
|
|
20097
|
+
const tag = element.tagName;
|
|
20098
|
+
return (tag === 'INPUT' ||
|
|
20099
|
+
tag === 'TEXTAREA' ||
|
|
20100
|
+
tag === 'SELECT' ||
|
|
20101
|
+
element.isContentEditable);
|
|
20102
|
+
}
|
|
20103
|
+
isNodeInsideHost(node, host) {
|
|
20104
|
+
if (!node) {
|
|
20105
|
+
return false;
|
|
20106
|
+
}
|
|
20107
|
+
let current = node;
|
|
20108
|
+
while (current) {
|
|
20109
|
+
if (current === host) {
|
|
20110
|
+
return true;
|
|
20111
|
+
}
|
|
20112
|
+
if (current.parentNode) {
|
|
20113
|
+
current = current.parentNode;
|
|
20114
|
+
continue;
|
|
20115
|
+
}
|
|
20116
|
+
const root = current.getRootNode();
|
|
20117
|
+
if (root instanceof ShadowRoot) {
|
|
20118
|
+
current = root.host;
|
|
20119
|
+
continue;
|
|
20120
|
+
}
|
|
20121
|
+
break;
|
|
20122
|
+
}
|
|
20123
|
+
return false;
|
|
20124
|
+
}
|
|
20125
|
+
getDeepActiveElement(root = document) {
|
|
20126
|
+
const activeElement = root.activeElement;
|
|
20127
|
+
if (!(activeElement instanceof HTMLElement)) {
|
|
20128
|
+
return null;
|
|
20129
|
+
}
|
|
20130
|
+
if (activeElement.shadowRoot) {
|
|
20131
|
+
const nested = this.getDeepActiveElement(activeElement.shadowRoot);
|
|
20132
|
+
return nested ?? activeElement;
|
|
20133
|
+
}
|
|
20134
|
+
return activeElement;
|
|
20135
|
+
}
|
|
20136
|
+
/**
|
|
20137
|
+
* Unmounts the custom element and performs cleanup.
|
|
20138
|
+
* If a renderer is registered, delegates to renderer.onUnmount and captures returned state.
|
|
20139
|
+
* Otherwise, simply removes from DOM.
|
|
20140
|
+
* Called when object is deleted, viewport culled, or editor is unloading.
|
|
20141
|
+
*/
|
|
20142
|
+
unmount() {
|
|
20143
|
+
if (!this.isMounted || !this.elementRef) {
|
|
20144
|
+
return;
|
|
20145
|
+
}
|
|
20146
|
+
// Try renderer-based unmount if renderer is registered
|
|
20147
|
+
if (this.rendererKey) {
|
|
20148
|
+
const renderer = KritzelCustomElementRendererRegistry.get(this.rendererKey);
|
|
20149
|
+
if (renderer) {
|
|
20150
|
+
// Capture any updated data returned by renderer
|
|
20151
|
+
const updatedData = renderer.onUnmount({
|
|
20152
|
+
object: this,
|
|
20153
|
+
container: this.elementRef,
|
|
20154
|
+
data: this.rendererData,
|
|
20155
|
+
});
|
|
20156
|
+
if (updatedData !== undefined) {
|
|
20157
|
+
this.rendererData = updatedData;
|
|
20158
|
+
}
|
|
20159
|
+
}
|
|
20160
|
+
}
|
|
20161
|
+
this.isMounted = false;
|
|
20162
|
+
this.elementRef = null;
|
|
20163
|
+
}
|
|
20164
|
+
/**
|
|
20165
|
+
* Returns whether the rendered content contains a scrollable element.
|
|
20166
|
+
* Traverses light DOM, open shadow roots, and slotted content so nested
|
|
20167
|
+
* framework components are detected as well.
|
|
20168
|
+
*/
|
|
20169
|
+
isElementScrollable() {
|
|
20170
|
+
return !!KritzelHTMLHelper.findScrollableElement(this.element);
|
|
20171
|
+
}
|
|
20172
|
+
/**
|
|
20173
|
+
* Creates a placeholder element when HTML deserialization fails.
|
|
20174
|
+
* Used as fallback to ensure element is always defined.
|
|
20175
|
+
* @returns A div element with placeholder styling
|
|
20176
|
+
*/
|
|
20177
|
+
createPlaceholder() {
|
|
20178
|
+
const el = document.createElement('div');
|
|
20179
|
+
el.textContent = '[Custom Element]';
|
|
20180
|
+
el.style.border = '1px dashed #ccc';
|
|
20181
|
+
el.style.display = 'flex';
|
|
20182
|
+
el.style.alignItems = 'center';
|
|
20183
|
+
el.style.justifyContent = 'center';
|
|
20184
|
+
el.style.backgroundColor = '#f9f9f9';
|
|
20185
|
+
el.style.color = '#999';
|
|
20186
|
+
el.style.fontSize = '12px';
|
|
20187
|
+
return el;
|
|
20188
|
+
}
|
|
20189
|
+
/**
|
|
20190
|
+
* Resizes and repositions the custom element on the canvas.
|
|
20191
|
+
* Updates both the internal state and the DOM element's inline styles.
|
|
20192
|
+
* Syncs changes to Yjs for collaborative editing and cross-tab propagation.
|
|
20193
|
+
* @param x - The new X position (translateX) in canvas coordinates.
|
|
20194
|
+
* @param y - The new Y position (translateY) in canvas coordinates.
|
|
20195
|
+
* @param width - The new width in pixels. Must be greater than 1.
|
|
20196
|
+
* @param height - The new height in pixels. Must be greater than 1.
|
|
20197
|
+
*/
|
|
20198
|
+
resize(x, y, width, height) {
|
|
20199
|
+
if (width <= 1 || height <= 1) {
|
|
20200
|
+
return;
|
|
20201
|
+
}
|
|
20202
|
+
this.width = width;
|
|
20203
|
+
this.height = height;
|
|
20204
|
+
this.translateX = x;
|
|
20205
|
+
this.translateY = y;
|
|
20206
|
+
if (this.element) {
|
|
20207
|
+
this.element.style.width = `${width}px`;
|
|
20208
|
+
this.element.style.height = `${height}px`;
|
|
20209
|
+
}
|
|
20210
|
+
// Update to sync changes to y.js and propagate to other tabs
|
|
20211
|
+
this._core.store.objects.update(this);
|
|
20212
|
+
}
|
|
20213
|
+
/**
|
|
20214
|
+
* Creates a deep copy of this custom element.
|
|
20215
|
+
* The copy receives a new unique ID, is marked as unmounted,
|
|
20216
|
+
* and contains a cloned copy of the underlying HTML element.
|
|
20217
|
+
* @returns A new KritzelCustomElement instance with copied properties and a cloned DOM element.
|
|
20218
|
+
*/
|
|
20219
|
+
copy() {
|
|
20220
|
+
const copiedObject = Object.create(Object.getPrototypeOf(this));
|
|
20221
|
+
Object.assign(copiedObject, this);
|
|
20222
|
+
copiedObject.id = this.generateId();
|
|
20223
|
+
copiedObject.isMounted = false;
|
|
20224
|
+
copiedObject.isSelected = false;
|
|
20225
|
+
copiedObject.isHovered = false;
|
|
20226
|
+
copiedObject.isInteractive = false;
|
|
20227
|
+
copiedObject.resizing = false;
|
|
20228
|
+
copiedObject.markedForRemoval = false;
|
|
20229
|
+
copiedObject.isVisible = true;
|
|
20230
|
+
copiedObject.element = this.element.cloneNode(true);
|
|
20231
|
+
return copiedObject;
|
|
20232
|
+
}
|
|
20233
|
+
/**
|
|
20234
|
+
* Serializes the custom element to a plain JavaScript object for persistence.
|
|
20235
|
+
* Stores HTML as a string (safe for JSON/Yjs) and captures dimension metadata.
|
|
20236
|
+
* @returns A serializable object with htmlString and dimension snapshots
|
|
20237
|
+
*/
|
|
20238
|
+
serialize() {
|
|
20239
|
+
const { _core, _elementRef, element, totalWidth, totalHeight, isSelected, isHovered, isMounted, isInteractive, resizing, markedForRemoval, isVisible, ...props } = this;
|
|
20240
|
+
const cloned = structuredClone(props);
|
|
20241
|
+
// Include rendererKey and rendererData for lifecycle-driven rendering
|
|
20242
|
+
cloned.rendererKey = this.rendererKey;
|
|
20243
|
+
cloned.rendererData = this.rendererData;
|
|
20244
|
+
// Store HTML as string for fallback/compatibility when renderer is unavailable
|
|
20245
|
+
cloned.htmlString = element ? element.outerHTML : '';
|
|
20246
|
+
// Capture dimension snapshot at serialization time
|
|
20247
|
+
cloned.serializedWidth = this.width;
|
|
20248
|
+
cloned.serializedHeight = this.height;
|
|
20249
|
+
return cloned;
|
|
20250
|
+
}
|
|
20251
|
+
/**
|
|
20252
|
+
* Deserializes the custom element from a plain object.
|
|
20253
|
+
* Reconstructs the element from stored HTML string with fallback to placeholder.
|
|
20254
|
+
* @template T - The expected return type
|
|
20255
|
+
* @param object - The serialized object data to restore from
|
|
20256
|
+
* @returns This instance cast to type T
|
|
20257
|
+
*/
|
|
20258
|
+
deserialize(object) {
|
|
20259
|
+
super.deserialize(object);
|
|
20260
|
+
// Legacy compatibility: this deprecated field should not persist on revived objects.
|
|
20261
|
+
delete this.allowEmbeddedInteraction;
|
|
20262
|
+
// Restore renderer metadata
|
|
20263
|
+
this.rendererKey = object.rendererKey ?? null;
|
|
20264
|
+
this.rendererData = object.rendererData;
|
|
20265
|
+
// Reconstruct element from stored HTML, or create placeholder
|
|
20266
|
+
if (object.htmlString) {
|
|
20267
|
+
try {
|
|
20268
|
+
const temp = document.createElement('div');
|
|
20269
|
+
temp.innerHTML = object.htmlString;
|
|
20270
|
+
this.element = temp.firstElementChild || this.createPlaceholder();
|
|
20271
|
+
}
|
|
20272
|
+
catch {
|
|
20273
|
+
this.element = this.createPlaceholder();
|
|
20274
|
+
}
|
|
20275
|
+
}
|
|
20276
|
+
else {
|
|
20277
|
+
this.element = this.createPlaceholder();
|
|
20278
|
+
}
|
|
20279
|
+
return this;
|
|
20280
|
+
}
|
|
20281
|
+
}
|
|
20282
|
+
|
|
19663
20283
|
/**
|
|
19664
20284
|
* Abstract base class for all drawing tools in Kritzel.
|
|
19665
20285
|
* Provides common functionality and defines the interface that all tools must implement.
|
|
@@ -20128,6 +20748,12 @@ class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
20128
20748
|
this._cachedObjectIdsHash = currentHash;
|
|
20129
20749
|
return this._cachedObjects;
|
|
20130
20750
|
}
|
|
20751
|
+
syncChildSelectionState(children) {
|
|
20752
|
+
const shouldBeSelected = this.isSelected === true;
|
|
20753
|
+
for (const child of children) {
|
|
20754
|
+
child.isSelected = shouldBeSelected;
|
|
20755
|
+
}
|
|
20756
|
+
}
|
|
20131
20757
|
/**
|
|
20132
20758
|
* Gets the number of objects in the selection group.
|
|
20133
20759
|
* @returns The count of selected objects
|
|
@@ -20173,6 +20799,7 @@ class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
20173
20799
|
for (const object of objects) {
|
|
20174
20800
|
if (!this._objectIds.includes(object.id)) {
|
|
20175
20801
|
this._objectIds.push(object.id);
|
|
20802
|
+
object.isSelected = true;
|
|
20176
20803
|
changed = true;
|
|
20177
20804
|
}
|
|
20178
20805
|
}
|
|
@@ -20200,9 +20827,11 @@ class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
20200
20827
|
const index = this._objectIds.findIndex(id => id === object.id);
|
|
20201
20828
|
if (index === -1) {
|
|
20202
20829
|
this._objectIds.push(object.id);
|
|
20830
|
+
object.isSelected = true;
|
|
20203
20831
|
}
|
|
20204
20832
|
else {
|
|
20205
20833
|
this._objectIds.splice(index, 1);
|
|
20834
|
+
object.isSelected = false;
|
|
20206
20835
|
}
|
|
20207
20836
|
this._cachedObjects = null; // Invalidate cache
|
|
20208
20837
|
const children = this.objects; // Get cached objects once
|
|
@@ -20234,11 +20863,27 @@ class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
20234
20863
|
/**
|
|
20235
20864
|
* Sets the isSelected flag to false for all objects in the selection group.
|
|
20236
20865
|
* Used when clearing or changing the selection state.
|
|
20866
|
+
*
|
|
20867
|
+
* For KritzelGroup members, also clears the isSelected flag on all nested
|
|
20868
|
+
* descendants. A descendant can retain a lingering isSelected from the marquee
|
|
20869
|
+
* preview before it was resolved to its parent group during selection, which
|
|
20870
|
+
* would otherwise leave that object selected after the group is deselected and
|
|
20871
|
+
* cause the whole group to be re-selected on the next interaction.
|
|
20237
20872
|
*/
|
|
20238
20873
|
deselectAllChildren() {
|
|
20239
20874
|
const children = this.objects;
|
|
20875
|
+
const descendantIds = new Set();
|
|
20240
20876
|
for (const obj of children) {
|
|
20241
20877
|
obj.isSelected = false;
|
|
20878
|
+
if (KritzelClassHelper.isInstanceOf(obj, 'KritzelGroup')) {
|
|
20879
|
+
KritzelGroup.collectDescendantIds(obj, descendantIds);
|
|
20880
|
+
}
|
|
20881
|
+
}
|
|
20882
|
+
if (descendantIds.size > 0) {
|
|
20883
|
+
const descendants = this._core.store.objects.filter(o => descendantIds.has(o.id));
|
|
20884
|
+
for (const descendant of descendants) {
|
|
20885
|
+
descendant.isSelected = false;
|
|
20886
|
+
}
|
|
20242
20887
|
}
|
|
20243
20888
|
}
|
|
20244
20889
|
/**
|
|
@@ -20344,6 +20989,8 @@ class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
20344
20989
|
deserialize(object) {
|
|
20345
20990
|
// First, deserialize all base properties using parent's deserialize
|
|
20346
20991
|
super.deserialize(object);
|
|
20992
|
+
// Selection groups intentionally persist their selected state.
|
|
20993
|
+
this.isSelected = object.isSelected === true;
|
|
20347
20994
|
// Restore objectIds through setter
|
|
20348
20995
|
if (object.objectIds) {
|
|
20349
20996
|
this.objectIds = object.objectIds;
|
|
@@ -20352,6 +20999,7 @@ class KritzelSelectionGroup extends KritzelBaseObject {
|
|
|
20352
20999
|
if (object.unchangedObjectSnapshots) {
|
|
20353
21000
|
this.unchangedObjectSnapshots = new Map(Object.entries(object.unchangedObjectSnapshots));
|
|
20354
21001
|
}
|
|
21002
|
+
this.syncChildSelectionState(this.objects);
|
|
20355
21003
|
return this;
|
|
20356
21004
|
}
|
|
20357
21005
|
/**
|
|
@@ -22782,10 +23430,9 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
22782
23430
|
selectionBox.translateX = this.startX;
|
|
22783
23431
|
selectionBox.translateY = this.startY;
|
|
22784
23432
|
this._core.store.state.isSelecting = true;
|
|
22785
|
-
this._core.
|
|
22786
|
-
this._core.store.setSelectionBox(null);
|
|
23433
|
+
this._core.removeSelectionBox();
|
|
22787
23434
|
if (!isAdditive) {
|
|
22788
|
-
this._core.
|
|
23435
|
+
this._core.removeSelectionGroup();
|
|
22789
23436
|
}
|
|
22790
23437
|
this._core.store.objects.insert(selectionBox);
|
|
22791
23438
|
this._core.store.setSelectionBox(selectionBox);
|
|
@@ -22814,10 +23461,9 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
22814
23461
|
selectionBox.translateX = this.startX;
|
|
22815
23462
|
selectionBox.translateY = this.startY;
|
|
22816
23463
|
this._core.store.state.isSelecting = true;
|
|
22817
|
-
this._core.
|
|
22818
|
-
this._core.store.setSelectionBox(null);
|
|
23464
|
+
this._core.removeSelectionBox();
|
|
22819
23465
|
if (!isAdditive) {
|
|
22820
|
-
this._core.
|
|
23466
|
+
this._core.removeSelectionGroup();
|
|
22821
23467
|
}
|
|
22822
23468
|
this._core.store.objects.insert(selectionBox);
|
|
22823
23469
|
this._core.store.setSelectionBox(selectionBox);
|
|
@@ -22987,8 +23633,11 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
22987
23633
|
*/
|
|
22988
23634
|
clearSelectionPreview() {
|
|
22989
23635
|
// Only clear previously tracked selected objects - O(k) instead of O(n)
|
|
23636
|
+
const selectedGroupIds = new Set(this._core.store.selectionGroup?.objectIds ?? []);
|
|
22990
23637
|
for (const obj of this._previouslySelectedObjects) {
|
|
22991
|
-
obj.
|
|
23638
|
+
if (!selectedGroupIds.has(obj.id)) {
|
|
23639
|
+
obj.isSelected = false;
|
|
23640
|
+
}
|
|
22992
23641
|
}
|
|
22993
23642
|
this._previouslySelectedObjects.clear();
|
|
22994
23643
|
}
|
|
@@ -23007,7 +23656,11 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
23007
23656
|
// Resolve child objects to their parent groups
|
|
23008
23657
|
const parentGroup = KritzelGroup.findParentGroup(this._core, object.id);
|
|
23009
23658
|
const objectToSelect = parentGroup || object;
|
|
23010
|
-
|
|
23659
|
+
// When the click resolves to a parent group, clear the child's own flag so
|
|
23660
|
+
// it does not linger as selected and re-select the group on the next interaction.
|
|
23661
|
+
if (parentGroup) {
|
|
23662
|
+
object.isSelected = false;
|
|
23663
|
+
}
|
|
23011
23664
|
const existingGroup = this._core.store.selectionGroup;
|
|
23012
23665
|
if (shiftKey && existingGroup) {
|
|
23013
23666
|
existingGroup.addOrRemove(objectToSelect);
|
|
@@ -23052,9 +23705,16 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
23052
23705
|
const resolvedObjects = new Map();
|
|
23053
23706
|
for (const obj of selectedObjects) {
|
|
23054
23707
|
const parentGroup = childToParentMap.get(obj.id);
|
|
23055
|
-
|
|
23056
|
-
|
|
23057
|
-
|
|
23708
|
+
if (parentGroup) {
|
|
23709
|
+
// The child was selected (e.g. via marquee preview) but selection is
|
|
23710
|
+
// represented by its parent group. Clear the child's flag so it does
|
|
23711
|
+
// not linger as selected and re-select the group on the next interaction.
|
|
23712
|
+
obj.isSelected = false;
|
|
23713
|
+
resolvedObjects.set(parentGroup.id, parentGroup);
|
|
23714
|
+
}
|
|
23715
|
+
else {
|
|
23716
|
+
resolvedObjects.set(obj.id, obj);
|
|
23717
|
+
}
|
|
23058
23718
|
}
|
|
23059
23719
|
const unrolledObjects = Array.from(resolvedObjects.values());
|
|
23060
23720
|
const filteredObjects = this.filterDescendantsOfSelectedGroups(unrolledObjects);
|
|
@@ -23062,12 +23722,16 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
23062
23722
|
if (shiftKey && existingGroup) {
|
|
23063
23723
|
existingGroup.addObjects(filteredObjects);
|
|
23064
23724
|
this.removeDescendantsOfSelectedGroups(existingGroup);
|
|
23725
|
+
this._previouslySelectedObjects.clear();
|
|
23065
23726
|
this._core.rerender();
|
|
23066
23727
|
}
|
|
23067
23728
|
else {
|
|
23068
23729
|
// Create selection group and set all IDs at once (no per-item refresh)
|
|
23069
23730
|
const selectionGroup = KritzelSelectionGroup.create(this._core);
|
|
23070
23731
|
selectionGroup.objectIds = filteredObjects.map(o => o.id);
|
|
23732
|
+
filteredObjects.forEach(obj => {
|
|
23733
|
+
obj.isSelected = true;
|
|
23734
|
+
});
|
|
23071
23735
|
// Only refresh dimensions and capture snapshots ONCE at the end - O(n) instead of O(n²)
|
|
23072
23736
|
if (selectionGroup.length === 1) {
|
|
23073
23737
|
selectionGroup.rotation = selectionGroup.objects[0].rotation;
|
|
@@ -23076,6 +23740,7 @@ class KritzelSelectionHandler extends KritzelBaseHandler {
|
|
|
23076
23740
|
selectionGroup.refreshObjectDimensions();
|
|
23077
23741
|
selectionGroup.isSelected = true;
|
|
23078
23742
|
this._core.addSelectionGroup(selectionGroup);
|
|
23743
|
+
this._previouslySelectedObjects.clear();
|
|
23079
23744
|
this._core.rerender();
|
|
23080
23745
|
}
|
|
23081
23746
|
}
|
|
@@ -23963,8 +24628,18 @@ class KritzelSelectionTool extends KritzelBaseTool {
|
|
|
23963
24628
|
this._core.removeSelectionGroup();
|
|
23964
24629
|
this._core.rerender();
|
|
23965
24630
|
}
|
|
23966
|
-
if (selectedObject && selectedObject.isSelected === false && selectedObject?.objects.length === 1
|
|
23967
|
-
|
|
24631
|
+
if (selectedObject && selectedObject.isSelected === false && selectedObject?.objects.length === 1) {
|
|
24632
|
+
const firstObj = selectedObject.objects[0];
|
|
24633
|
+
if (KritzelClassHelper.isInstanceOf(firstObj, 'KritzelCustomElement')) {
|
|
24634
|
+
// Selected single custom elements hand pointer interaction to their
|
|
24635
|
+
// embedded DOM content. Unselected custom elements remain selectable.
|
|
24636
|
+
if (firstObj.isSelected && this._core.store.selectionGroup?.objects.length === 1)
|
|
24637
|
+
return;
|
|
24638
|
+
}
|
|
24639
|
+
else if (firstObj.isInteractive) {
|
|
24640
|
+
// Preserve existing behavior for text/shape edit mode
|
|
24641
|
+
return;
|
|
24642
|
+
}
|
|
23968
24643
|
}
|
|
23969
24644
|
}
|
|
23970
24645
|
this.lineHandleHandler.handlePointerDown(event);
|
|
@@ -23976,6 +24651,14 @@ class KritzelSelectionTool extends KritzelBaseTool {
|
|
|
23976
24651
|
}
|
|
23977
24652
|
if (event.pointerType === 'touch' || event.pointerType === 'pen') {
|
|
23978
24653
|
if (this._core.store.state.isScaling === true) {
|
|
24654
|
+
const activePointerCount = this._core.store.state.pointers.size;
|
|
24655
|
+
// During a real pinch (two active pointers), keep touch handling suspended.
|
|
24656
|
+
// If only one pointer is active, scaling is stale (debounce window after pinch),
|
|
24657
|
+
// so resume normal touch selection behavior immediately.
|
|
24658
|
+
if (activePointerCount === 1) {
|
|
24659
|
+
this._core.store.state.isScaling = false;
|
|
24660
|
+
this._savedSelectionGroupForPinch = null;
|
|
24661
|
+
}
|
|
23979
24662
|
// A second finger has arrived to start a pinch-zoom gesture. If we
|
|
23980
24663
|
// cleared a selection group on the first finger-down, restore it now
|
|
23981
24664
|
// so the user's selection is preserved while zooming.
|
|
@@ -23984,7 +24667,10 @@ class KritzelSelectionTool extends KritzelBaseTool {
|
|
|
23984
24667
|
this._savedSelectionGroupForPinch = null;
|
|
23985
24668
|
this._core.rerender();
|
|
23986
24669
|
}
|
|
23987
|
-
|
|
24670
|
+
if (this._core.store.state.isScaling === true) {
|
|
24671
|
+
return;
|
|
24672
|
+
}
|
|
24673
|
+
// Continue with normal touch handling when scaling was stale.
|
|
23988
24674
|
}
|
|
23989
24675
|
if (this._core.store.state.pointers.size === 1) {
|
|
23990
24676
|
this._core.store.state.isResizeHandleSelected = this.isHandleSelected(event);
|
|
@@ -24016,7 +24702,10 @@ class KritzelSelectionTool extends KritzelBaseTool {
|
|
|
24016
24702
|
this.rotationHandler.handlePointerDown(event);
|
|
24017
24703
|
this.resizeHandler.handlePointerDown(event);
|
|
24018
24704
|
this.moveHandler.handlePointerDown(event);
|
|
24019
|
-
|
|
24705
|
+
// Only process selection handler if we are not scaling (2 pointers)
|
|
24706
|
+
if (this._core.store.state.pointers.size === 1) {
|
|
24707
|
+
this.selectionHandler.handlePointerDown(event);
|
|
24708
|
+
}
|
|
24020
24709
|
this._core.rerender();
|
|
24021
24710
|
}
|
|
24022
24711
|
}
|
|
@@ -24090,7 +24779,7 @@ class KritzelSelectionTool extends KritzelBaseTool {
|
|
|
24090
24779
|
* @returns A KritzelSelectionGroup containing the object, or null if no object at that location
|
|
24091
24780
|
*/
|
|
24092
24781
|
getSelectedObject(event) {
|
|
24093
|
-
const path = event.composedPath()
|
|
24782
|
+
const path = event.composedPath();
|
|
24094
24783
|
const objectElement = path.find(element => element.classList && element.classList.contains('object'));
|
|
24095
24784
|
const object = objectElement?.id ? this._core.findObjectById(objectElement.id) : null;
|
|
24096
24785
|
if (!object) {
|
|
@@ -24100,6 +24789,10 @@ class KritzelSelectionTool extends KritzelBaseTool {
|
|
|
24100
24789
|
if (!object.hitTest(x, y)) {
|
|
24101
24790
|
return null;
|
|
24102
24791
|
}
|
|
24792
|
+
const currentSelectionGroup = this._core.store.selectionGroup;
|
|
24793
|
+
if (currentSelectionGroup?.objects.some(selectedObject => selectedObject.id === object.id)) {
|
|
24794
|
+
return currentSelectionGroup;
|
|
24795
|
+
}
|
|
24103
24796
|
if (object instanceof KritzelSelectionGroup) {
|
|
24104
24797
|
return object;
|
|
24105
24798
|
}
|
|
@@ -24107,7 +24800,7 @@ class KritzelSelectionTool extends KritzelBaseTool {
|
|
|
24107
24800
|
const group = KritzelSelectionGroup.create(this._core);
|
|
24108
24801
|
group.translateX = 0;
|
|
24109
24802
|
group.translateY = 0;
|
|
24110
|
-
group.
|
|
24803
|
+
group.objectIds = [object.id];
|
|
24111
24804
|
return group;
|
|
24112
24805
|
}
|
|
24113
24806
|
}
|
|
@@ -29557,10 +30250,13 @@ exports.KritzelBrushTool = KritzelBrushTool;
|
|
|
29557
30250
|
exports.KritzelClassHelper = KritzelClassHelper;
|
|
29558
30251
|
exports.KritzelColorHelper = KritzelColorHelper;
|
|
29559
30252
|
exports.KritzelCursorHelper = KritzelCursorHelper;
|
|
30253
|
+
exports.KritzelCustomElement = KritzelCustomElement;
|
|
30254
|
+
exports.KritzelCustomElementRendererRegistry = KritzelCustomElementRendererRegistry;
|
|
29560
30255
|
exports.KritzelDevicesHelper = KritzelDevicesHelper;
|
|
29561
30256
|
exports.KritzelEraserTool = KritzelEraserTool;
|
|
29562
30257
|
exports.KritzelEventHelper = KritzelEventHelper;
|
|
29563
30258
|
exports.KritzelGroup = KritzelGroup;
|
|
30259
|
+
exports.KritzelHTMLHelper = KritzelHTMLHelper;
|
|
29564
30260
|
exports.KritzelIconRegistry = KritzelIconRegistry;
|
|
29565
30261
|
exports.KritzelImage = KritzelImage;
|
|
29566
30262
|
exports.KritzelImageTool = KritzelImageTool;
|
|
@@ -29569,7 +30265,6 @@ exports.KritzelLicenseManager = KritzelLicenseManager;
|
|
|
29569
30265
|
exports.KritzelLine = KritzelLine;
|
|
29570
30266
|
exports.KritzelLineTool = KritzelLineTool;
|
|
29571
30267
|
exports.KritzelLocalizationManager = KritzelLocalizationManager;
|
|
29572
|
-
exports.KritzelMathHelper = KritzelMathHelper;
|
|
29573
30268
|
exports.KritzelPath = KritzelPath;
|
|
29574
30269
|
exports.KritzelSelectionBox = KritzelSelectionBox;
|
|
29575
30270
|
exports.KritzelSelectionGroup = KritzelSelectionGroup;
|