kritzel-stencil 0.4.7 → 0.4.9
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 +9 -1
- package/dist/cjs/kritzel-active-users_47.cjs.entry.js +1396 -1243
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/{schema.constants-C8hqt--r.js → schema.constants-ByNZ_qAj.js} +136 -7
- package/dist/cjs/stencil.cjs.js +1 -1
- package/dist/collection/classes/handlers/line-handle.handler.js +1 -1
- package/dist/collection/classes/managers/license.manager.js +56 -0
- package/dist/collection/classes/objects/image.class.js +9 -0
- package/dist/collection/classes/objects/line.class.js +1 -1
- package/dist/collection/classes/objects/path.class.js +9 -0
- package/dist/collection/classes/objects/shape.class.js +12 -3
- package/dist/collection/classes/objects/text.class.js +1 -1
- package/dist/collection/classes/providers/sync/broadcast-sync-provider.class.js +1 -0
- package/dist/collection/classes/providers/sync/hocuspocus-sync-provider.class.js +2 -0
- package/dist/collection/classes/providers/sync/in-memory-sync-provider.class.js +1 -0
- package/dist/collection/classes/providers/sync/indexeddb-sync-provider.class.js +1 -0
- package/dist/collection/classes/providers/sync/websocket-sync-provider.class.js +2 -0
- package/dist/collection/components/core/kritzel-editor/kritzel-editor.js +197 -67
- package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +146 -172
- package/dist/collection/components/ui/kritzel-export/kritzel-export.js +1 -1
- package/dist/collection/components/ui/kritzel-loading-overlay/kritzel-loading-overlay.js +1 -1
- package/dist/collection/components/ui/kritzel-login-dialog/kritzel-login-dialog.js +1 -1
- package/dist/collection/components/ui/kritzel-more-menu/kritzel-more-menu.js +90 -4
- package/dist/collection/components/ui/kritzel-settings/kritzel-settings.js +1 -1
- package/dist/collection/components/ui/kritzel-share-dialog/kritzel-share-dialog.js +2 -2
- package/dist/collection/components/ui/kritzel-utility-panel/kritzel-utility-panel.js +1 -1
- package/dist/collection/components/ui/kritzel-zoom-panel/kritzel-zoom-panel.js +1 -1
- package/dist/collection/constants/license.constants.js +1 -1
- package/dist/collection/constants/version.js +1 -1
- package/dist/collection/helpers/object.helper.js +11 -0
- package/dist/collection/helpers/svg-export.helper.js +19 -9
- package/dist/collection/helpers/theme.helper.js +8 -0
- package/dist/collection/index.js +1 -0
- package/dist/collection/interfaces/object-export.interface.js +1 -0
- package/dist/collection/interfaces/sync-config.interface.js +21 -1
- package/dist/collection/themes/dark-theme.js +2 -0
- package/dist/collection/themes/light-theme.js +2 -0
- package/dist/components/index.js +11 -4
- package/dist/components/kritzel-awareness-cursors.js +1 -1
- package/dist/components/kritzel-color-palette.js +1 -1
- package/dist/components/kritzel-color.js +1 -1
- package/dist/components/kritzel-controls.js +1 -1
- package/dist/components/kritzel-editor.js +180 -83
- package/dist/components/kritzel-engine.js +1 -1
- package/dist/components/kritzel-export.js +1 -1
- package/dist/components/kritzel-loading-overlay.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-stroke-size.js +1 -1
- package/dist/components/kritzel-tool-config.js +1 -1
- package/dist/components/kritzel-utility-panel.js +1 -1
- package/dist/components/kritzel-workspace-manager.js +1 -1
- package/dist/components/kritzel-zoom-panel.js +1 -1
- package/dist/components/{p-DwY5pghH.js → p-6rQICkGr.js} +1 -1
- package/dist/components/{p-DkH3hoLz.js → p-8ScektIm.js} +6 -6
- package/dist/components/{p-1nG2hxfJ.js → p-9m1cAVwG.js} +2 -2
- package/dist/components/{p-DkY7pIg5.js → p-BRGufCki.js} +95 -6
- package/dist/components/{p-BKOsmXB5.js → p-BqWiEmYI.js} +1 -1
- package/dist/components/{p-BLOPBDUF.js → p-C2CeSrTm.js} +1 -1
- package/dist/components/{p-zd7zXld8.js → p-CMZ09rB1.js} +1 -1
- package/dist/components/{p-BmyTdQGy.js → p-CURiiQfq.js} +12 -0
- package/dist/components/{p-DDbO5q-e.js → p-CXJZxJvp.js} +4 -4
- package/dist/components/{p-7Rt7ml3v.js → p-Cj0NElII.js} +1 -1
- package/dist/components/{p-Bm_BWQa4.js → p-Ckm1Aq0V.js} +1 -1
- package/dist/components/{p-BVEGQLBD.js → p-CoY7emAj.js} +1 -1
- package/dist/components/{p-BZxOukfY.js → p-Cr2z2UBL.js} +4 -4
- package/dist/components/{p-D0oplzU-.js → p-D7H8oxZb.js} +2 -2
- package/dist/components/{p-Dd-pMvlr.js → p-DPONTran.js} +11 -0
- package/dist/components/{p-BMRln59F.js → p-DW7WW4EN.js} +2 -2
- package/dist/components/{p-D8TOEhPY.js → p-DuVkk58V.js} +2 -2
- package/dist/components/{p-OLfoATNk.js → p-U9FjWrp-.js} +1 -1
- package/dist/components/{p-C2yi85nc.js → p-ZPPtEla5.js} +1213 -1131
- package/dist/components/{p-COjsYPir.js → p-dVfh-Sy9.js} +1 -1
- package/dist/components/{p-Ka7OWbnk.js → p-lQ7nfhOy.js} +12 -3
- package/dist/esm/index.js +9 -2
- package/dist/esm/kritzel-active-users_47.entry.js +1396 -1243
- package/dist/esm/loader.js +1 -1
- package/dist/esm/{schema.constants-uC7_X6yr.js → schema.constants-1t45c2_U.js} +136 -8
- package/dist/esm/stencil.js +1 -1
- package/dist/stencil/index.esm.js +9 -2
- package/dist/stencil/{p-uC7_X6yr.js → p-1t45c2_U.js} +136 -8
- package/dist/stencil/{p-472f2e90.entry.js → p-f5f088ea.entry.js} +1396 -1243
- package/dist/stencil/stencil.esm.js +1 -1
- package/dist/types/classes/managers/license.manager.d.ts +6 -0
- package/dist/types/classes/objects/image.class.d.ts +3 -0
- package/dist/types/classes/objects/path.class.d.ts +3 -0
- package/dist/types/classes/objects/shape.class.d.ts +3 -0
- package/dist/types/classes/providers/sync/broadcast-sync-provider.class.d.ts +1 -0
- package/dist/types/classes/providers/sync/hocuspocus-sync-provider.class.d.ts +1 -0
- package/dist/types/classes/providers/sync/in-memory-sync-provider.class.d.ts +1 -0
- package/dist/types/classes/providers/sync/indexeddb-sync-provider.class.d.ts +1 -0
- package/dist/types/classes/providers/sync/websocket-sync-provider.class.d.ts +1 -0
- package/dist/types/components/core/kritzel-editor/kritzel-editor.d.ts +15 -3
- package/dist/types/components/core/kritzel-engine/kritzel-engine.d.ts +16 -34
- package/dist/types/components/ui/kritzel-more-menu/kritzel-more-menu.d.ts +10 -1
- package/dist/types/components.d.ts +27 -26
- package/dist/types/constants/license.constants.d.ts +1 -1
- package/dist/types/constants/version.d.ts +1 -1
- package/dist/types/helpers/object.helper.d.ts +4 -0
- package/dist/types/helpers/theme.helper.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/interfaces/menu-item.interface.d.ts +1 -1
- package/dist/types/interfaces/object-export.interface.d.ts +20 -0
- package/dist/types/interfaces/sync-config.interface.d.ts +5 -0
- package/dist/types/interfaces/sync-provider.interface.d.ts +5 -0
- package/dist/types/interfaces/theme.interface.d.ts +2 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as registerInstance, h, H as Host, c as createEvent, g as getElement } from './p-Dhio9uis.js';
|
|
2
|
-
import { c as KritzelPath, e as KritzelLine,
|
|
2
|
+
import { c as KritzelPath, e as KritzelLine, U as KritzelColorHelper, V as KritzelDevicesHelper, X as KritzelMouseButton, Y as KritzelHTMLHelper, Z as DEFAULT_STROKE_SIZES, _ as DEFAULT_COLOR_PALETTE, S as ShapeType, q as KritzelSelectionTool, D as DEFAULT_BRUSH_CONFIG, j as KritzelBrushTool, l as KritzelEraserTool, C as DEFAULT_LINE_TOOL_CONFIG, k as KritzelLineTool, o as KritzelShapeTool, B as DEFAULT_TEXT_CONFIG, n as KritzelTextTool, m as KritzelImageTool, N as KritzelAlignment, z as hasRemoteSyncProvider, E as DEFAULT_ASSET_STORAGE_CONFIG, $ as ObjectHelper, a0 as resolveTextToolAvailableFonts, a1 as ThemeHelper, G as darkTheme, F as lightTheme, a2 as KritzelSelectionGroup, a3 as KritzelSelectionBox, a4 as KritzelIconRegistry, y as KritzelFontRegistry, a5 as KritzelKeyboardHelper, a6 as KritzelBaseHandler, s as KritzelWorkspace, f as KritzelGroup, h as KritzelCustomElement, d as KritzelImage, g as KritzelShape, b as KritzelText, O as runMigrations, T as CURRENT_WORKSPACE_SCHEMA_VERSION, Q as WORKSPACE_MIGRATIONS, a7 as KritzelClassHelper, R as CURRENT_APP_STATE_SCHEMA_VERSION, P as APP_STATE_MIGRATIONS, p as KritzelCursorHelper, a8 as KritzelImageFileHelper, a9 as imageCompression, t as KritzelAnchorManager, u as KritzelThemeManager, v as KritzelLocalizationManager, w as KritzelLicenseManager, r as KritzelAssetResolver, aa as KritzelEventHelper, W as WORKSPACE_EXPORT_VERSION, i as KritzelBaseTool } from './p-1t45c2_U.js';
|
|
3
3
|
import * as Y from 'yjs';
|
|
4
4
|
import 'y-indexeddb';
|
|
5
5
|
import 'y-websocket';
|
|
@@ -2243,6 +2243,19 @@ const KritzelEditor = class {
|
|
|
2243
2243
|
group: 'clipboard',
|
|
2244
2244
|
action: (menu, _) => this.engineRef.paste(menu.x, menu.y),
|
|
2245
2245
|
},
|
|
2246
|
+
{
|
|
2247
|
+
label: 'menu.export',
|
|
2248
|
+
icon: 'upload',
|
|
2249
|
+
group: 'clipboard',
|
|
2250
|
+
visible: (_, objects) => this.canExportObjects(objects),
|
|
2251
|
+
action: (_, objects) => {
|
|
2252
|
+
const format = this.getPreferredExportFormat(objects);
|
|
2253
|
+
if (!format) {
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
void this.engineRef.exportSelectedObject(format);
|
|
2257
|
+
},
|
|
2258
|
+
},
|
|
2246
2259
|
{
|
|
2247
2260
|
label: 'menu.order',
|
|
2248
2261
|
icon: 'ordering',
|
|
@@ -2284,16 +2297,43 @@ const KritzelEditor = class {
|
|
|
2284
2297
|
},
|
|
2285
2298
|
],
|
|
2286
2299
|
},
|
|
2300
|
+
{ label: 'menu.delete', icon: 'delete', group: 'edit', action: () => this.engineRef.delete() },
|
|
2301
|
+
];
|
|
2302
|
+
moreMenuItems = [
|
|
2303
|
+
{
|
|
2304
|
+
id: 'share',
|
|
2305
|
+
label: 'menu.share',
|
|
2306
|
+
icon: 'share',
|
|
2307
|
+
isVisible: async () => hasRemoteSyncProvider(this.syncConfig),
|
|
2308
|
+
action: () => {
|
|
2309
|
+
if (!this.isLoggedIn && this.loginConfig) {
|
|
2310
|
+
this.openLoginDialog();
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
this.shareDialogRef.open();
|
|
2314
|
+
},
|
|
2315
|
+
},
|
|
2287
2316
|
{
|
|
2317
|
+
id: 'export',
|
|
2288
2318
|
label: 'menu.export',
|
|
2319
|
+
icon: 'upload',
|
|
2320
|
+
action: async () => {
|
|
2321
|
+
const preview = await this.engineRef.getScreenshot('png');
|
|
2322
|
+
this.exportRef.open(preview ?? undefined);
|
|
2323
|
+
},
|
|
2324
|
+
},
|
|
2325
|
+
{
|
|
2326
|
+
id: 'import',
|
|
2327
|
+
label: 'menu.import',
|
|
2289
2328
|
icon: 'download',
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2329
|
+
action: () => this.engineRef.importFromFile(),
|
|
2330
|
+
},
|
|
2331
|
+
{
|
|
2332
|
+
id: 'settings',
|
|
2333
|
+
label: 'menu.settings',
|
|
2334
|
+
icon: 'settings',
|
|
2335
|
+
action: () => this.settingsRef.open(),
|
|
2295
2336
|
},
|
|
2296
|
-
{ label: 'menu.delete', icon: 'delete', group: 'edit', action: () => this.engineRef.delete() },
|
|
2297
2337
|
];
|
|
2298
2338
|
themes;
|
|
2299
2339
|
theme = 'light';
|
|
@@ -2365,6 +2405,7 @@ const KritzelEditor = class {
|
|
|
2365
2405
|
isNotificationDismissing = false;
|
|
2366
2406
|
contextMenuState;
|
|
2367
2407
|
normalizedControls = [];
|
|
2408
|
+
resolvedMoreMenuItems = [];
|
|
2368
2409
|
notificationDismissTimeout;
|
|
2369
2410
|
notificationDismissAnimationTimeout;
|
|
2370
2411
|
notificationDisplayDurationMs = 5000;
|
|
@@ -2407,6 +2448,18 @@ const KritzelEditor = class {
|
|
|
2407
2448
|
onControlsChange(newValue) {
|
|
2408
2449
|
this.normalizedControls = this.normalizeControls(newValue);
|
|
2409
2450
|
}
|
|
2451
|
+
onMoreMenuItemsChange() {
|
|
2452
|
+
this.recomputeMoreMenuItems();
|
|
2453
|
+
}
|
|
2454
|
+
onSyncConfigChange() {
|
|
2455
|
+
this.recomputeMoreMenuItems();
|
|
2456
|
+
}
|
|
2457
|
+
onLoginConfigChange() {
|
|
2458
|
+
this.recomputeMoreMenuItems();
|
|
2459
|
+
}
|
|
2460
|
+
onUserChange() {
|
|
2461
|
+
this.recomputeMoreMenuItems();
|
|
2462
|
+
}
|
|
2410
2463
|
onActiveWorkspaceIdChange(newId) {
|
|
2411
2464
|
// If no ID is provided, do nothing (allows other logic to control activeWorkspace)
|
|
2412
2465
|
if (!newId) {
|
|
@@ -2456,6 +2509,7 @@ const KritzelEditor = class {
|
|
|
2456
2509
|
}
|
|
2457
2510
|
this.resolvedTerms = await this.engineRef.getResolvedTerms();
|
|
2458
2511
|
this.availableLocaleOptions = await this.engineRef.getAvailableLocaleOptions();
|
|
2512
|
+
this.recomputeMoreMenuItems();
|
|
2459
2513
|
}
|
|
2460
2514
|
onTouchStart(event) {
|
|
2461
2515
|
if (event.cancelable) {
|
|
@@ -2616,11 +2670,14 @@ const KritzelEditor = class {
|
|
|
2616
2670
|
async exportViewportAsSvg(options) {
|
|
2617
2671
|
return this.engineRef.exportViewportAsSvg(options);
|
|
2618
2672
|
}
|
|
2619
|
-
async
|
|
2620
|
-
return this.engineRef.
|
|
2673
|
+
async getSelectedObjectSupportedExportFormats() {
|
|
2674
|
+
return this.engineRef.getSelectedObjectSupportedExportFormats();
|
|
2675
|
+
}
|
|
2676
|
+
async canExportSelectedObjectAs(format) {
|
|
2677
|
+
return this.engineRef.canExportSelectedObjectAs(format);
|
|
2621
2678
|
}
|
|
2622
|
-
async
|
|
2623
|
-
return this.engineRef.
|
|
2679
|
+
async exportSelectedObject(format) {
|
|
2680
|
+
return this.engineRef.exportSelectedObject(format);
|
|
2624
2681
|
}
|
|
2625
2682
|
async downloadAsJson(filename) {
|
|
2626
2683
|
return this.engineRef.downloadAsJson(filename);
|
|
@@ -2687,6 +2744,7 @@ const KritzelEditor = class {
|
|
|
2687
2744
|
currentUserDialogRef;
|
|
2688
2745
|
componentWillLoad() {
|
|
2689
2746
|
this.normalizedControls = this.normalizeControls(this.controls);
|
|
2747
|
+
this.recomputeMoreMenuItems();
|
|
2690
2748
|
this.loadSettingsFromStorage();
|
|
2691
2749
|
this.applyTheme();
|
|
2692
2750
|
}
|
|
@@ -2707,13 +2765,18 @@ const KritzelEditor = class {
|
|
|
2707
2765
|
};
|
|
2708
2766
|
});
|
|
2709
2767
|
}
|
|
2768
|
+
recomputeMoreMenuItems() {
|
|
2769
|
+
if (this.moreMenuItems) {
|
|
2770
|
+
this.resolvedMoreMenuItems = this.moreMenuItems;
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2710
2774
|
applyTheme() {
|
|
2711
2775
|
const themeObj = this.resolveThemeObject();
|
|
2712
2776
|
ThemeHelper.applyThemeToElement(this.host, themeObj);
|
|
2713
2777
|
}
|
|
2714
2778
|
resolveThemeObject() {
|
|
2715
|
-
return this.themes?.find(t => t.name === this.theme)
|
|
2716
|
-
?? (this.theme === 'dark' ? darkTheme : lightTheme);
|
|
2779
|
+
return this.themes?.find(t => t.name === this.theme) ?? (this.theme === 'dark' ? darkTheme : lightTheme);
|
|
2717
2780
|
}
|
|
2718
2781
|
componentDidLoad() {
|
|
2719
2782
|
this.registerCustomSvgIcons();
|
|
@@ -2748,7 +2811,7 @@ const KritzelEditor = class {
|
|
|
2748
2811
|
syncConfig: this.syncConfig,
|
|
2749
2812
|
assetStorageConfig: this.assetStorageConfig,
|
|
2750
2813
|
loginConfig: this.loginConfig,
|
|
2751
|
-
theme: this.theme
|
|
2814
|
+
theme: this.theme,
|
|
2752
2815
|
});
|
|
2753
2816
|
}
|
|
2754
2817
|
async onEngineReady(event) {
|
|
@@ -2911,43 +2974,6 @@ const KritzelEditor = class {
|
|
|
2911
2974
|
}
|
|
2912
2975
|
this.themeChange.emit(event.detail.theme);
|
|
2913
2976
|
}
|
|
2914
|
-
get moreMenuItems() {
|
|
2915
|
-
return [
|
|
2916
|
-
{
|
|
2917
|
-
id: 'share',
|
|
2918
|
-
label: this.resolvedTerms['menu.share'] ?? 'Share',
|
|
2919
|
-
icon: 'share',
|
|
2920
|
-
action: () => {
|
|
2921
|
-
if (!this.isLoggedIn && this.loginConfig) {
|
|
2922
|
-
this.openLoginDialog();
|
|
2923
|
-
return;
|
|
2924
|
-
}
|
|
2925
|
-
this.shareDialogRef.open();
|
|
2926
|
-
},
|
|
2927
|
-
},
|
|
2928
|
-
{
|
|
2929
|
-
id: 'export',
|
|
2930
|
-
label: this.resolvedTerms['menu.export'] ?? 'Export',
|
|
2931
|
-
icon: 'upload',
|
|
2932
|
-
action: async () => {
|
|
2933
|
-
const preview = await this.engineRef.getScreenshot('png');
|
|
2934
|
-
this.exportRef.open(preview ?? undefined);
|
|
2935
|
-
},
|
|
2936
|
-
},
|
|
2937
|
-
{
|
|
2938
|
-
id: 'import',
|
|
2939
|
-
label: this.resolvedTerms['menu.import'] ?? 'Import',
|
|
2940
|
-
icon: 'download',
|
|
2941
|
-
action: () => this.engineRef.importFromFile(),
|
|
2942
|
-
},
|
|
2943
|
-
{
|
|
2944
|
-
id: 'settings',
|
|
2945
|
-
label: this.resolvedTerms['menu.settings'] ?? 'Settings',
|
|
2946
|
-
icon: 'settings',
|
|
2947
|
-
action: () => this.settingsRef.open(),
|
|
2948
|
-
}
|
|
2949
|
-
];
|
|
2950
|
-
}
|
|
2951
2977
|
handleToggleIsPublic = async (event) => {
|
|
2952
2978
|
const isPublic = event.detail;
|
|
2953
2979
|
const activeWorkspace = await this.engineRef.getActiveWorkspace();
|
|
@@ -2977,6 +3003,28 @@ const KritzelEditor = class {
|
|
|
2977
3003
|
get isLoggedIn() {
|
|
2978
3004
|
return !!this.user && !this.user.isGuest;
|
|
2979
3005
|
}
|
|
3006
|
+
canExportObjectsAs(objects, format) {
|
|
3007
|
+
if (!objects || objects.length !== 1) {
|
|
3008
|
+
return false;
|
|
3009
|
+
}
|
|
3010
|
+
const [selected] = objects;
|
|
3011
|
+
if (!ObjectHelper.isKritzelExportable(selected)) {
|
|
3012
|
+
return false;
|
|
3013
|
+
}
|
|
3014
|
+
return selected.getSupportedExportFormats().includes(format);
|
|
3015
|
+
}
|
|
3016
|
+
canExportObjects(objects) {
|
|
3017
|
+
return this.canExportObjectsAs(objects, 'png') || this.canExportObjectsAs(objects, 'svg');
|
|
3018
|
+
}
|
|
3019
|
+
getPreferredExportFormat(objects) {
|
|
3020
|
+
if (this.canExportObjectsAs(objects, 'png')) {
|
|
3021
|
+
return 'png';
|
|
3022
|
+
}
|
|
3023
|
+
if (this.canExportObjectsAs(objects, 'svg')) {
|
|
3024
|
+
return 'svg';
|
|
3025
|
+
}
|
|
3026
|
+
return null;
|
|
3027
|
+
}
|
|
2980
3028
|
async openLoginDialog() {
|
|
2981
3029
|
this.loginDialogRef?.open();
|
|
2982
3030
|
}
|
|
@@ -3153,41 +3201,41 @@ const KritzelEditor = class {
|
|
|
3153
3201
|
const isLoggedIn = this.isLoggedIn;
|
|
3154
3202
|
const shouldShowCurrentUser = isLoggedIn;
|
|
3155
3203
|
const shouldShowLoginButton = this.isReady && !!this.loginConfig && !isLoggedIn;
|
|
3156
|
-
return (h(Host, { key: '
|
|
3204
|
+
return (h(Host, { key: '3119d6909544f0d15bd805f65ccf3f18469dc11d' }, h("div", { key: '7528eec8df968cd7814a2343fa3aa792802b9856', class: "editor-content", style: {
|
|
3157
3205
|
opacity: this.isEditorVisible ? '1' : '0',
|
|
3158
3206
|
visibility: this.isEditorVisible ? 'visible' : 'hidden',
|
|
3159
3207
|
transition: 'opacity 0.2s ease-in-out, visibility 0.2s ease-in-out',
|
|
3160
|
-
} }, h("div", { key: '
|
|
3208
|
+
} }, h("div", { key: '71f0dba52cddc6e28ea7f7b3b6ff78c32aa19acf', class: "top-left-buttons" }, h("kritzel-workspace-manager", { key: '579826701b13fb4b12dbdbf0b31b05031f01c3d1', visible: this.isWorkspaceManagerVisible, workspaces: this.workspaces, activeWorkspace: this.activeWorkspace, terms: this.resolvedTerms, onWorkspaceChange: event => (this.activeWorkspace = event.detail), onIsWorkspaceManagerReady: () => (this.isWorkspaceManagerReady = true) }), h("kritzel-back-to-content", { key: '84fb52fba5f898cf1034780f87f7ce49d370cdfe', visible: this.isBackToContentButtonVisible, text: this.resolvedTerms['backToContent.label'] ?? 'Back to content', onBackToContent: () => this.backToContent() })), this.activeNotification && (h("div", { key: 'ecca20769db06f56bd52199b04f9e44c74bf525d', class: "top-center-notification-layer", role: "presentation" }, h("div", { key: 'aa2232646d7d77d70af04c8b413e2eb8033a00eb', class: { 'top-center-notification-container': true, 'is-dismissing': this.isNotificationDismissing }, role: "presentation" }, h("kritzel-notification-card", { key: 'fa111382e6fdebf8e5944c6148cad9583bc8b68d', notification: this.activeNotification, locale: this.locale, onDismiss: this.dismissNotification, onHoverChange: event => this.handleNotificationHoverChange(event) })))), this.contextMenuState && this.contextMenuState.items.length > 0 && (h("kritzel-context-menu", { key: '420caf2b6bfb79f3ebda3537cc379f8735eb8c09', class: "context-menu", items: this.contextMenuState.items, objects: this.contextMenuState.objects, style: {
|
|
3161
3209
|
position: 'absolute',
|
|
3162
3210
|
left: `${this.contextMenuState.position.x}px`,
|
|
3163
3211
|
top: `${this.contextMenuState.position.y}px`,
|
|
3164
3212
|
zIndex: '10002',
|
|
3165
|
-
}, onActionSelected: event => this.handleContextMenuActionSelected(event), onClose: () => this.hideContextMenu() })), h("kritzel-engine", { key: '
|
|
3213
|
+
}, onActionSelected: event => this.handleContextMenuActionSelected(event), onClose: () => this.hideContextMenu() })), h("kritzel-engine", { key: 'd10589d02c626712e10f55b07f716907d83b28c1', ref: el => {
|
|
3166
3214
|
if (el) {
|
|
3167
3215
|
this.engineRef = el;
|
|
3168
3216
|
}
|
|
3169
|
-
}, workspace: this.activeWorkspace, activeWorkspaceId: this.activeWorkspaceId, editorId: this.editorId, syncConfig: this.syncConfig, assetStorageConfig: this.assetStorageConfig, user: this.user, scaleMax: this.scaleMax, lockDrawingScale: this.lockDrawingScale, isObjectDistanceFadingActive: this.isObjectDistanceFadingActive, scaleMin: this.scaleMin, cursorTarget: this.cursorTarget, isLoading: this.isLoading, viewportBoundaryLeft: this.viewportBoundaryLeft, viewportBoundaryRight: this.viewportBoundaryRight, viewportBoundaryTop: this.viewportBoundaryTop, viewportBoundaryBottom: this.viewportBoundaryBottom, isPanningEnabled: this.isPanningEnabled, isZoomingEnabled: this.isZoomingEnabled, theme: this.theme, themes: this.themes, licenseKey: this.licenseKey, locale: this.locale, locales: this.locales, fallbackLocale: this.fallbackLocale, debugInfo: this.debugInfo, globalContextMenuItems: this.globalContextMenuItems, objectContextMenuItems: this.objectContextMenuItems, onIsEngineReady: event => this.onEngineReady(event), onWorkspacesChange: event => this.handleWorkspacesChange(event), onActiveWorkspaceChange: event => this.handleActiveWorkspaceChange(event), onObjectsChange: event => this.handleObjectsChange(event), onObjectsAdded: event => this.handleObjectsAdded(event), onObjectsRemoved: event => this.handleObjectsRemoved(event), onObjectsUpdated: event => this.handleObjectsUpdated(event), onUndoStateChange: event => this.handleUndoStateChange(event), onObjectsInViewportChange: event => this.handleObjectsInViewportChange(event), onViewportChange: event => this.handleViewportChange(event), onAwarenessChange: event => this.handleAwarenessChange(event), onNotificationsChange: event => this.handleNotificationsChange(event), onContextMenuStateChange: event => this.handleContextMenuStateChange(event) }), h("kritzel-controls", { key: '
|
|
3217
|
+
}, workspace: this.activeWorkspace, activeWorkspaceId: this.activeWorkspaceId, editorId: this.editorId, syncConfig: this.syncConfig, assetStorageConfig: this.assetStorageConfig, user: this.user, scaleMax: this.scaleMax, lockDrawingScale: this.lockDrawingScale, isObjectDistanceFadingActive: this.isObjectDistanceFadingActive, scaleMin: this.scaleMin, cursorTarget: this.cursorTarget, isLoading: this.isLoading, viewportBoundaryLeft: this.viewportBoundaryLeft, viewportBoundaryRight: this.viewportBoundaryRight, viewportBoundaryTop: this.viewportBoundaryTop, viewportBoundaryBottom: this.viewportBoundaryBottom, isPanningEnabled: this.isPanningEnabled, isZoomingEnabled: this.isZoomingEnabled, theme: this.theme, themes: this.themes, licenseKey: this.licenseKey, locale: this.locale, locales: this.locales, fallbackLocale: this.fallbackLocale, debugInfo: this.debugInfo, globalContextMenuItems: this.globalContextMenuItems, objectContextMenuItems: this.objectContextMenuItems, onIsEngineReady: event => this.onEngineReady(event), onWorkspacesChange: event => this.handleWorkspacesChange(event), onActiveWorkspaceChange: event => this.handleActiveWorkspaceChange(event), onObjectsChange: event => this.handleObjectsChange(event), onObjectsAdded: event => this.handleObjectsAdded(event), onObjectsRemoved: event => this.handleObjectsRemoved(event), onObjectsUpdated: event => this.handleObjectsUpdated(event), onUndoStateChange: event => this.handleUndoStateChange(event), onObjectsInViewportChange: event => this.handleObjectsInViewportChange(event), onViewportChange: event => this.handleViewportChange(event), onAwarenessChange: event => this.handleAwarenessChange(event), onNotificationsChange: event => this.handleNotificationsChange(event), onContextMenuStateChange: event => this.handleContextMenuStateChange(event) }), h("kritzel-controls", { key: '121480e948808fe6a44a94511becb834f9249897', visible: this.isControlsVisible, class: { 'keyboard-open': this.isVirtualKeyboardOpen }, ref: el => {
|
|
3170
3218
|
if (el) {
|
|
3171
3219
|
this.controlsRef = el;
|
|
3172
3220
|
}
|
|
3173
|
-
}, controls: this.normalizedControls, isUtilityPanelVisible: this.isUtilityPanelVisible, undoState: this.undoState ?? undefined, theme: this.theme, terms: this.resolvedTerms, onIsControlsReady: () => (this.isControlsReady = true) }), h("div", { key: '
|
|
3221
|
+
}, controls: this.normalizedControls, isUtilityPanelVisible: this.isUtilityPanelVisible, undoState: this.undoState ?? undefined, theme: this.theme, terms: this.resolvedTerms, onIsControlsReady: () => (this.isControlsReady = true) }), h("div", { key: 'e84f31858a6a45fb9f7bcc3686122b349551618b', class: "bottom-left-buttons" }, h("kritzel-zoom-panel", { key: 'a25d93767a124e1a2fdcdd4b77706ec7d032234c', visible: this.isZoomPanelVisible, disabled: !this.isZoomingEnabled, zoomPercent: this.currentZoomPercent, terms: this.resolvedTerms, onZoomIn: () => this.zoomIn(), onZoomOut: () => this.zoomOut() })), h("div", { key: '49810743d464d4e019670859f6ed31d9c12cd560', class: "top-right-buttons" }, h("kritzel-settings", { key: '57daff63b4ec1de36f94be99948d0d0aa091c522', ref: el => {
|
|
3174
3222
|
if (el) {
|
|
3175
3223
|
this.settingsRef = el;
|
|
3176
3224
|
}
|
|
3177
|
-
}, shortcuts: this.shortcuts, availableThemes: this.themes && this.themes.length > 0 ? this.themes.map(t => t.name) : ['light', 'dark'], availableLocales: this.availableLocaleOptions, settings: this.currentSettingsConfig, terms: this.resolvedTerms, onSettingsChange: event => this.handleSettingsChange(event) }), h("kritzel-export", { key: '
|
|
3225
|
+
}, shortcuts: this.shortcuts, availableThemes: this.themes && this.themes.length > 0 ? this.themes.map(t => t.name) : ['light', 'dark'], availableLocales: this.availableLocaleOptions, settings: this.currentSettingsConfig, terms: this.resolvedTerms, onSettingsChange: event => this.handleSettingsChange(event) }), h("kritzel-export", { key: 'cad9614f0e439713e96108476ed3c400c14546cc', ref: el => {
|
|
3178
3226
|
if (el) {
|
|
3179
3227
|
this.exportRef = el;
|
|
3180
3228
|
}
|
|
3181
|
-
}, workspaceName: this.activeWorkspace?.name || 'workspace', terms: this.resolvedTerms, onExportPng: () => this.engineRef.exportViewportAsPng(), onExportSvg: () => this.engineRef.exportViewportAsSvg(), onExportJson: event => this.engineRef.downloadAsJson(event.detail) }), h("kritzel-active-users", { key: '
|
|
3229
|
+
}, workspaceName: this.activeWorkspace?.name || 'workspace', terms: this.resolvedTerms, onExportPng: () => this.engineRef.exportViewportAsPng(), onExportSvg: () => this.engineRef.exportViewportAsSvg(), onExportJson: event => this.engineRef.downloadAsJson(event.detail) }), h("kritzel-active-users", { key: '191775e6c3f62391e4c62cb8039ace51b4853555', users: this.activeUsers }), shouldShowCurrentUser && h("kritzel-current-user", { key: '385264c0ebb74eb1c3e3793ad889b8bd84ec1920', user: this.user, terms: this.resolvedTerms, onClick: () => this.currentUserDialogRef?.open() }), shouldShowCurrentUser && (h("kritzel-current-user-dialog", { key: '065b583dd31019fe88739171089414123af7d640', ref: el => {
|
|
3182
3230
|
if (el) {
|
|
3183
3231
|
this.currentUserDialogRef = el;
|
|
3184
3232
|
this.currentUserDialogRef.addEventListener('logoutRequest', this.handleCurrentUserLogout);
|
|
3185
3233
|
}
|
|
3186
|
-
}, user: this.user, terms: this.resolvedTerms })), shouldShowLoginButton &&
|
|
3234
|
+
}, user: this.user, terms: this.resolvedTerms })), shouldShowLoginButton && h("kritzel-button", { key: 'b655001273147d58f299983c9512d10ee16847a1', onButtonClick: () => this.loginDialogRef?.open() }, this.resolvedTerms['login.dialogTitle'] ?? 'Sign in'), h("kritzel-more-menu", { key: '265cd05a2e62fb3a16beae85a82a81e69410f284', items: this.resolvedMoreMenuItems, visible: this.isMoreMenuVisible, terms: this.resolvedTerms }), h("kritzel-share-dialog", { key: '0c441b88aab039e4f9eb86d226670732f0635446', ref: el => {
|
|
3187
3235
|
if (el) {
|
|
3188
3236
|
this.shareDialogRef = el;
|
|
3189
3237
|
}
|
|
3190
|
-
}, isPublic: this.currentIsPublic, workspaceId: this.activeWorkspace?.id, terms: this.resolvedTerms, onToggleIsPublic: this.handleToggleIsPublic }), this.loginConfig && (h("kritzel-login-dialog", { key: '
|
|
3238
|
+
}, isPublic: this.currentIsPublic, workspaceId: this.activeWorkspace?.id, terms: this.resolvedTerms, onToggleIsPublic: this.handleToggleIsPublic }), this.loginConfig && (h("kritzel-login-dialog", { key: '3e06e1b33c3dc98cfa8f76641acdb5a3ea51c1d5', ref: el => {
|
|
3191
3239
|
if (el) {
|
|
3192
3240
|
this.loginDialogRef = el;
|
|
3193
3241
|
}
|
|
@@ -3212,6 +3260,18 @@ const KritzelEditor = class {
|
|
|
3212
3260
|
"controls": [{
|
|
3213
3261
|
"onControlsChange": 0
|
|
3214
3262
|
}],
|
|
3263
|
+
"moreMenuItems": [{
|
|
3264
|
+
"onMoreMenuItemsChange": 0
|
|
3265
|
+
}],
|
|
3266
|
+
"syncConfig": [{
|
|
3267
|
+
"onSyncConfigChange": 0
|
|
3268
|
+
}],
|
|
3269
|
+
"loginConfig": [{
|
|
3270
|
+
"onLoginConfigChange": 0
|
|
3271
|
+
}],
|
|
3272
|
+
"user": [{
|
|
3273
|
+
"onUserChange": 0
|
|
3274
|
+
}],
|
|
3215
3275
|
"activeWorkspaceId": [{
|
|
3216
3276
|
"onActiveWorkspaceIdChange": 0
|
|
3217
3277
|
}],
|
|
@@ -25918,952 +25978,310 @@ class KritzelCore {
|
|
|
25918
25978
|
}
|
|
25919
25979
|
}
|
|
25920
25980
|
|
|
25921
|
-
|
|
25922
|
-
|
|
25923
|
-
|
|
25924
|
-
|
|
25925
|
-
class KritzelSvgExportHelper {
|
|
25926
|
-
/**
|
|
25927
|
-
* Calculates the combined bounding box that encompasses all objects.
|
|
25928
|
-
* Uses rotatedBoundingBox to account for object rotation.
|
|
25929
|
-
* @param objects - Array of objects to calculate bounds for
|
|
25930
|
-
* @returns Combined bounding box, or null if no objects provided
|
|
25931
|
-
*/
|
|
25932
|
-
static calculateCombinedBounds(objects) {
|
|
25933
|
-
if (!objects || objects.length === 0) {
|
|
25934
|
-
return null;
|
|
25935
|
-
}
|
|
25936
|
-
let minX = Infinity;
|
|
25937
|
-
let minY = Infinity;
|
|
25938
|
-
let maxX = -Infinity;
|
|
25939
|
-
let maxY = -Infinity;
|
|
25940
|
-
for (const obj of objects) {
|
|
25941
|
-
const bbox = obj.rotatedBoundingBox;
|
|
25942
|
-
minX = Math.min(minX, bbox.x);
|
|
25943
|
-
minY = Math.min(minY, bbox.y);
|
|
25944
|
-
maxX = Math.max(maxX, bbox.x + bbox.width);
|
|
25945
|
-
maxY = Math.max(maxY, bbox.y + bbox.height);
|
|
25946
|
-
}
|
|
25947
|
-
return {
|
|
25948
|
-
x: minX,
|
|
25949
|
-
y: minY,
|
|
25950
|
-
z: 1,
|
|
25951
|
-
width: maxX - minX,
|
|
25952
|
-
height: maxY - minY,
|
|
25953
|
-
};
|
|
25981
|
+
function resolveUrl(url, baseUrl) {
|
|
25982
|
+
// url is absolute already
|
|
25983
|
+
if (url.match(/^[a-z]+:\/\//i)) {
|
|
25984
|
+
return url;
|
|
25954
25985
|
}
|
|
25955
|
-
|
|
25956
|
-
|
|
25957
|
-
|
|
25958
|
-
|
|
25959
|
-
|
|
25960
|
-
|
|
25961
|
-
|
|
25962
|
-
|
|
25963
|
-
|
|
25964
|
-
|
|
25965
|
-
|
|
25966
|
-
|
|
25967
|
-
|
|
25968
|
-
|
|
25969
|
-
|
|
25970
|
-
|
|
25986
|
+
// url is absolute already, without protocol
|
|
25987
|
+
if (url.match(/^\/\//)) {
|
|
25988
|
+
return window.location.protocol + url;
|
|
25989
|
+
}
|
|
25990
|
+
// dataURI, mailto:, tel:, etc.
|
|
25991
|
+
if (url.match(/^[a-z]+:/i)) {
|
|
25992
|
+
return url;
|
|
25993
|
+
}
|
|
25994
|
+
const doc = document.implementation.createHTMLDocument();
|
|
25995
|
+
const base = doc.createElement('base');
|
|
25996
|
+
const a = doc.createElement('a');
|
|
25997
|
+
doc.head.appendChild(base);
|
|
25998
|
+
doc.body.appendChild(a);
|
|
25999
|
+
if (baseUrl) {
|
|
26000
|
+
base.href = baseUrl;
|
|
26001
|
+
}
|
|
26002
|
+
a.href = url;
|
|
26003
|
+
return a.href;
|
|
26004
|
+
}
|
|
26005
|
+
const uuid = (() => {
|
|
26006
|
+
// generate uuid for className of pseudo elements.
|
|
26007
|
+
// We should not use GUIDs, otherwise pseudo elements sometimes cannot be captured.
|
|
26008
|
+
let counter = 0;
|
|
26009
|
+
// ref: http://stackoverflow.com/a/6248722/2519373
|
|
26010
|
+
const random = () =>
|
|
26011
|
+
// eslint-disable-next-line no-bitwise
|
|
26012
|
+
`0000${((Math.random() * 36 ** 4) << 0).toString(36)}`.slice(-4);
|
|
26013
|
+
return () => {
|
|
26014
|
+
counter += 1;
|
|
26015
|
+
return `u${random()}${counter}`;
|
|
26016
|
+
};
|
|
26017
|
+
})();
|
|
26018
|
+
function toArray(arrayLike) {
|
|
26019
|
+
const arr = [];
|
|
26020
|
+
for (let i = 0, l = arrayLike.length; i < l; i++) {
|
|
26021
|
+
arr.push(arrayLike[i]);
|
|
26022
|
+
}
|
|
26023
|
+
return arr;
|
|
26024
|
+
}
|
|
26025
|
+
let styleProps = null;
|
|
26026
|
+
function getStyleProperties(options = {}) {
|
|
26027
|
+
if (styleProps) {
|
|
26028
|
+
return styleProps;
|
|
26029
|
+
}
|
|
26030
|
+
if (options.includeStyleProperties) {
|
|
26031
|
+
styleProps = options.includeStyleProperties;
|
|
26032
|
+
return styleProps;
|
|
26033
|
+
}
|
|
26034
|
+
styleProps = toArray(window.getComputedStyle(document.documentElement));
|
|
26035
|
+
return styleProps;
|
|
26036
|
+
}
|
|
26037
|
+
function px(node, styleProperty) {
|
|
26038
|
+
const win = node.ownerDocument.defaultView || window;
|
|
26039
|
+
const val = win.getComputedStyle(node).getPropertyValue(styleProperty);
|
|
26040
|
+
return val ? parseFloat(val.replace('px', '')) : 0;
|
|
26041
|
+
}
|
|
26042
|
+
function getNodeWidth(node) {
|
|
26043
|
+
const leftBorder = px(node, 'border-left-width');
|
|
26044
|
+
const rightBorder = px(node, 'border-right-width');
|
|
26045
|
+
return node.clientWidth + leftBorder + rightBorder;
|
|
26046
|
+
}
|
|
26047
|
+
function getNodeHeight(node) {
|
|
26048
|
+
const topBorder = px(node, 'border-top-width');
|
|
26049
|
+
const bottomBorder = px(node, 'border-bottom-width');
|
|
26050
|
+
return node.clientHeight + topBorder + bottomBorder;
|
|
26051
|
+
}
|
|
26052
|
+
function getImageSize(targetNode, options = {}) {
|
|
26053
|
+
const width = options.width || getNodeWidth(targetNode);
|
|
26054
|
+
const height = options.height || getNodeHeight(targetNode);
|
|
26055
|
+
return { width, height };
|
|
26056
|
+
}
|
|
26057
|
+
function getPixelRatio() {
|
|
26058
|
+
let ratio;
|
|
26059
|
+
let FINAL_PROCESS;
|
|
26060
|
+
try {
|
|
26061
|
+
FINAL_PROCESS = process;
|
|
26062
|
+
}
|
|
26063
|
+
catch (e) {
|
|
26064
|
+
// pass
|
|
26065
|
+
}
|
|
26066
|
+
const val = FINAL_PROCESS && FINAL_PROCESS.env
|
|
26067
|
+
? FINAL_PROCESS.env.devicePixelRatio
|
|
26068
|
+
: null;
|
|
26069
|
+
if (val) {
|
|
26070
|
+
ratio = parseInt(val, 10);
|
|
26071
|
+
if (Number.isNaN(ratio)) {
|
|
26072
|
+
ratio = 1;
|
|
25971
26073
|
}
|
|
25972
|
-
// Add padding to bounds
|
|
25973
|
-
const viewBoxX = bounds.x - padding;
|
|
25974
|
-
const viewBoxY = bounds.y - padding;
|
|
25975
|
-
const viewBoxWidth = bounds.width + padding * 2;
|
|
25976
|
-
const viewBoxHeight = bounds.height + padding * 2;
|
|
25977
|
-
// Collect all defs (markers, patterns, etc.)
|
|
25978
|
-
const defs = this.collectDefs(allObjects, theme);
|
|
25979
|
-
// Generate SVG elements for each object
|
|
25980
|
-
const elements = objects
|
|
25981
|
-
.sort((a, b) => a.zIndex - b.zIndex)
|
|
25982
|
-
.map(obj => this.objectToSvgElement(obj, theme, imageDataUrls, rasterSafeForCanvas))
|
|
25983
|
-
.filter(Boolean)
|
|
25984
|
-
.join('\n ');
|
|
25985
|
-
const xmlDecl = includeXmlDeclaration ? '<?xml version="1.0" encoding="UTF-8"?>\n' : '';
|
|
25986
|
-
const defsContent = defs.length > 0 ? `\n <defs>\n ${defs.join('\n ')}\n </defs>` : '';
|
|
25987
|
-
return `${xmlDecl}<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}" width="${viewBoxWidth}" height="${viewBoxHeight}">${defsContent}
|
|
25988
|
-
${elements}
|
|
25989
|
-
</svg>`;
|
|
25990
26074
|
}
|
|
25991
|
-
|
|
25992
|
-
|
|
25993
|
-
|
|
25994
|
-
|
|
25995
|
-
|
|
25996
|
-
|
|
25997
|
-
|
|
25998
|
-
|
|
25999
|
-
|
|
26000
|
-
|
|
26075
|
+
return ratio || window.devicePixelRatio || 1;
|
|
26076
|
+
}
|
|
26077
|
+
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
|
|
26078
|
+
const canvasDimensionLimit = 16384;
|
|
26079
|
+
function checkCanvasDimensions(canvas) {
|
|
26080
|
+
if (canvas.width > canvasDimensionLimit ||
|
|
26081
|
+
canvas.height > canvasDimensionLimit) {
|
|
26082
|
+
if (canvas.width > canvasDimensionLimit &&
|
|
26083
|
+
canvas.height > canvasDimensionLimit) {
|
|
26084
|
+
if (canvas.width > canvas.height) {
|
|
26085
|
+
canvas.height *= canvasDimensionLimit / canvas.width;
|
|
26086
|
+
canvas.width = canvasDimensionLimit;
|
|
26001
26087
|
}
|
|
26002
26088
|
else {
|
|
26003
|
-
|
|
26089
|
+
canvas.width *= canvasDimensionLimit / canvas.height;
|
|
26090
|
+
canvas.height = canvasDimensionLimit;
|
|
26004
26091
|
}
|
|
26005
26092
|
}
|
|
26006
|
-
|
|
26007
|
-
|
|
26008
|
-
|
|
26009
|
-
* Resolves every image referenced by the given objects (including those
|
|
26010
|
-
* nested inside groups) to an inlined `data:` URL, keyed by object id.
|
|
26011
|
-
*
|
|
26012
|
-
* This is required for export flows that rasterize the SVG (e.g. PNG
|
|
26013
|
-
* export): an SVG loaded into an `<img>` element will NOT load external
|
|
26014
|
-
* or `blob:` resources, so a `<image href="blob:...">` renders as a
|
|
26015
|
-
* broken-image placeholder. Embedding the bytes as a base64 `data:` URL
|
|
26016
|
-
* makes the image self-contained and survive rasterization.
|
|
26017
|
-
*
|
|
26018
|
-
* Resolution is best-effort: images that fail to resolve are simply
|
|
26019
|
-
* omitted from the map, and the SVG falls back to whatever `href` the
|
|
26020
|
-
* synchronous path would have produced.
|
|
26021
|
-
* @param objects - Objects to inline images for
|
|
26022
|
-
* @param assetResolver - Resolver used to fetch asset bytes; optional
|
|
26023
|
-
* @returns Map of image object id -> inlined `data:` URL
|
|
26024
|
-
*/
|
|
26025
|
-
static async resolveImageDataUrls(objects, assetResolver) {
|
|
26026
|
-
const dataUrls = new Map();
|
|
26027
|
-
if (!objects || objects.length === 0) {
|
|
26028
|
-
return dataUrls;
|
|
26093
|
+
else if (canvas.width > canvasDimensionLimit) {
|
|
26094
|
+
canvas.height *= canvasDimensionLimit / canvas.width;
|
|
26095
|
+
canvas.width = canvasDimensionLimit;
|
|
26029
26096
|
}
|
|
26030
|
-
|
|
26031
|
-
|
|
26032
|
-
|
|
26033
|
-
|
|
26034
|
-
|
|
26035
|
-
|
|
26036
|
-
|
|
26037
|
-
|
|
26038
|
-
|
|
26039
|
-
|
|
26040
|
-
|
|
26041
|
-
|
|
26042
|
-
|
|
26043
|
-
|
|
26044
|
-
|
|
26097
|
+
else {
|
|
26098
|
+
canvas.width *= canvasDimensionLimit / canvas.height;
|
|
26099
|
+
canvas.height = canvasDimensionLimit;
|
|
26100
|
+
}
|
|
26101
|
+
}
|
|
26102
|
+
}
|
|
26103
|
+
function createImage(url) {
|
|
26104
|
+
return new Promise((resolve, reject) => {
|
|
26105
|
+
const img = new Image();
|
|
26106
|
+
img.onload = () => {
|
|
26107
|
+
img.decode().then(() => {
|
|
26108
|
+
requestAnimationFrame(() => resolve(img));
|
|
26109
|
+
});
|
|
26110
|
+
};
|
|
26111
|
+
img.onerror = reject;
|
|
26112
|
+
img.crossOrigin = 'anonymous';
|
|
26113
|
+
img.decoding = 'async';
|
|
26114
|
+
img.src = url;
|
|
26115
|
+
});
|
|
26116
|
+
}
|
|
26117
|
+
async function svgToDataURL(svg) {
|
|
26118
|
+
return Promise.resolve()
|
|
26119
|
+
.then(() => new XMLSerializer().serializeToString(svg))
|
|
26120
|
+
.then(encodeURIComponent)
|
|
26121
|
+
.then((html) => `data:image/svg+xml;charset=utf-8,${html}`);
|
|
26122
|
+
}
|
|
26123
|
+
async function nodeToDataURL(node, width, height) {
|
|
26124
|
+
const xmlns = 'http://www.w3.org/2000/svg';
|
|
26125
|
+
const svg = document.createElementNS(xmlns, 'svg');
|
|
26126
|
+
const foreignObject = document.createElementNS(xmlns, 'foreignObject');
|
|
26127
|
+
svg.setAttribute('width', `${width}`);
|
|
26128
|
+
svg.setAttribute('height', `${height}`);
|
|
26129
|
+
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
26130
|
+
foreignObject.setAttribute('width', '100%');
|
|
26131
|
+
foreignObject.setAttribute('height', '100%');
|
|
26132
|
+
foreignObject.setAttribute('x', '0');
|
|
26133
|
+
foreignObject.setAttribute('y', '0');
|
|
26134
|
+
foreignObject.setAttribute('externalResourcesRequired', 'true');
|
|
26135
|
+
svg.appendChild(foreignObject);
|
|
26136
|
+
foreignObject.appendChild(node);
|
|
26137
|
+
return svgToDataURL(svg);
|
|
26138
|
+
}
|
|
26139
|
+
const isInstanceOfElement = (node, instance) => {
|
|
26140
|
+
if (node instanceof instance)
|
|
26141
|
+
return true;
|
|
26142
|
+
const nodePrototype = Object.getPrototypeOf(node);
|
|
26143
|
+
if (nodePrototype === null)
|
|
26144
|
+
return false;
|
|
26145
|
+
return (nodePrototype.constructor.name === instance.name ||
|
|
26146
|
+
isInstanceOfElement(nodePrototype, instance));
|
|
26147
|
+
};
|
|
26148
|
+
|
|
26149
|
+
function formatCSSText(style) {
|
|
26150
|
+
const content = style.getPropertyValue('content');
|
|
26151
|
+
return `${style.cssText} content: '${content.replace(/'|"/g, '')}';`;
|
|
26152
|
+
}
|
|
26153
|
+
function formatCSSProperties(style, options) {
|
|
26154
|
+
return getStyleProperties(options)
|
|
26155
|
+
.map((name) => {
|
|
26156
|
+
const value = style.getPropertyValue(name);
|
|
26157
|
+
const priority = style.getPropertyPriority(name);
|
|
26158
|
+
return `${name}: ${value}${priority ? ' !important' : ''};`;
|
|
26159
|
+
})
|
|
26160
|
+
.join(' ');
|
|
26161
|
+
}
|
|
26162
|
+
function getPseudoElementStyle(className, pseudo, style, options) {
|
|
26163
|
+
const selector = `.${className}:${pseudo}`;
|
|
26164
|
+
const cssText = style.cssText
|
|
26165
|
+
? formatCSSText(style)
|
|
26166
|
+
: formatCSSProperties(style, options);
|
|
26167
|
+
return document.createTextNode(`${selector}{${cssText}}`);
|
|
26168
|
+
}
|
|
26169
|
+
function clonePseudoElement(nativeNode, clonedNode, pseudo, options) {
|
|
26170
|
+
const style = window.getComputedStyle(nativeNode, pseudo);
|
|
26171
|
+
const content = style.getPropertyValue('content');
|
|
26172
|
+
if (content === '' || content === 'none') {
|
|
26173
|
+
return;
|
|
26174
|
+
}
|
|
26175
|
+
const className = uuid();
|
|
26176
|
+
try {
|
|
26177
|
+
clonedNode.className = `${clonedNode.className} ${className}`;
|
|
26178
|
+
}
|
|
26179
|
+
catch (err) {
|
|
26180
|
+
return;
|
|
26181
|
+
}
|
|
26182
|
+
const styleElement = document.createElement('style');
|
|
26183
|
+
styleElement.appendChild(getPseudoElementStyle(className, pseudo, style, options));
|
|
26184
|
+
clonedNode.appendChild(styleElement);
|
|
26185
|
+
}
|
|
26186
|
+
function clonePseudoElements(nativeNode, clonedNode, options) {
|
|
26187
|
+
clonePseudoElement(nativeNode, clonedNode, ':before', options);
|
|
26188
|
+
clonePseudoElement(nativeNode, clonedNode, ':after', options);
|
|
26189
|
+
}
|
|
26190
|
+
|
|
26191
|
+
const WOFF = 'application/font-woff';
|
|
26192
|
+
const JPEG = 'image/jpeg';
|
|
26193
|
+
const mimes = {
|
|
26194
|
+
woff: WOFF,
|
|
26195
|
+
woff2: WOFF,
|
|
26196
|
+
ttf: 'application/font-truetype',
|
|
26197
|
+
eot: 'application/vnd.ms-fontobject',
|
|
26198
|
+
png: 'image/png',
|
|
26199
|
+
jpg: JPEG,
|
|
26200
|
+
jpeg: JPEG,
|
|
26201
|
+
gif: 'image/gif',
|
|
26202
|
+
tiff: 'image/tiff',
|
|
26203
|
+
svg: 'image/svg+xml',
|
|
26204
|
+
webp: 'image/webp',
|
|
26205
|
+
};
|
|
26206
|
+
function getExtension(url) {
|
|
26207
|
+
const match = /\.([^./]*?)$/g.exec(url);
|
|
26208
|
+
return match ? match[1] : '';
|
|
26209
|
+
}
|
|
26210
|
+
function getMimeType(url) {
|
|
26211
|
+
const extension = getExtension(url).toLowerCase();
|
|
26212
|
+
return mimes[extension] || '';
|
|
26213
|
+
}
|
|
26214
|
+
|
|
26215
|
+
function getContentFromDataUrl(dataURL) {
|
|
26216
|
+
return dataURL.split(/,/)[1];
|
|
26217
|
+
}
|
|
26218
|
+
function isDataUrl(url) {
|
|
26219
|
+
return url.search(/^(data:)/) !== -1;
|
|
26220
|
+
}
|
|
26221
|
+
function makeDataUrl(content, mimeType) {
|
|
26222
|
+
return `data:${mimeType};base64,${content}`;
|
|
26223
|
+
}
|
|
26224
|
+
async function fetchAsDataURL(url, init, process) {
|
|
26225
|
+
const res = await fetch(url, init);
|
|
26226
|
+
if (res.status === 404) {
|
|
26227
|
+
throw new Error(`Resource "${res.url}" not found`);
|
|
26228
|
+
}
|
|
26229
|
+
const blob = await res.blob();
|
|
26230
|
+
return new Promise((resolve, reject) => {
|
|
26231
|
+
const reader = new FileReader();
|
|
26232
|
+
reader.onerror = reject;
|
|
26233
|
+
reader.onloadend = () => {
|
|
26045
26234
|
try {
|
|
26046
|
-
|
|
26047
|
-
dataUrls.set(image.id, await this.blobToDataUrl(blob));
|
|
26235
|
+
resolve(process({ res, result: reader.result }));
|
|
26048
26236
|
}
|
|
26049
|
-
catch (
|
|
26050
|
-
|
|
26237
|
+
catch (error) {
|
|
26238
|
+
reject(error);
|
|
26051
26239
|
}
|
|
26052
|
-
}
|
|
26053
|
-
|
|
26240
|
+
};
|
|
26241
|
+
reader.readAsDataURL(blob);
|
|
26242
|
+
});
|
|
26243
|
+
}
|
|
26244
|
+
const cache = {};
|
|
26245
|
+
function getCacheKey(url, contentType, includeQueryParams) {
|
|
26246
|
+
let key = url.replace(/\?.*/, '');
|
|
26247
|
+
if (includeQueryParams) {
|
|
26248
|
+
key = url;
|
|
26054
26249
|
}
|
|
26055
|
-
|
|
26056
|
-
|
|
26057
|
-
|
|
26058
|
-
|
|
26059
|
-
|
|
26060
|
-
|
|
26061
|
-
|
|
26062
|
-
|
|
26063
|
-
|
|
26064
|
-
|
|
26065
|
-
|
|
26066
|
-
|
|
26067
|
-
|
|
26068
|
-
|
|
26069
|
-
|
|
26070
|
-
|
|
26071
|
-
|
|
26072
|
-
|
|
26073
|
-
|
|
26074
|
-
|
|
26075
|
-
|
|
26076
|
-
|
|
26077
|
-
}
|
|
26078
|
-
catch {
|
|
26079
|
-
// Fall through to the ArrayBuffer-based encoder.
|
|
26250
|
+
// font resource
|
|
26251
|
+
if (/ttf|otf|eot|woff2?/i.test(key)) {
|
|
26252
|
+
key = key.replace(/.*\//, '');
|
|
26253
|
+
}
|
|
26254
|
+
return contentType ? `[${contentType}]${key}` : key;
|
|
26255
|
+
}
|
|
26256
|
+
async function resourceToDataURL(resourceUrl, contentType, options) {
|
|
26257
|
+
const cacheKey = getCacheKey(resourceUrl, contentType, options.includeQueryParams);
|
|
26258
|
+
if (cache[cacheKey] != null) {
|
|
26259
|
+
return cache[cacheKey];
|
|
26260
|
+
}
|
|
26261
|
+
// ref: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
|
|
26262
|
+
if (options.cacheBust) {
|
|
26263
|
+
// eslint-disable-next-line no-param-reassign
|
|
26264
|
+
resourceUrl += (/\?/.test(resourceUrl) ? '&' : '?') + new Date().getTime();
|
|
26265
|
+
}
|
|
26266
|
+
let dataURL;
|
|
26267
|
+
try {
|
|
26268
|
+
const content = await fetchAsDataURL(resourceUrl, options.fetchRequestInit, ({ res, result }) => {
|
|
26269
|
+
if (!contentType) {
|
|
26270
|
+
// eslint-disable-next-line no-param-reassign
|
|
26271
|
+
contentType = res.headers.get('Content-Type') || '';
|
|
26080
26272
|
}
|
|
26081
|
-
|
|
26082
|
-
|
|
26083
|
-
|
|
26084
|
-
return `data:${mimeType};base64,${this.uint8ToBase64(bytes)}`;
|
|
26273
|
+
return getContentFromDataUrl(result);
|
|
26274
|
+
});
|
|
26275
|
+
dataURL = makeDataUrl(content, contentType);
|
|
26085
26276
|
}
|
|
26086
|
-
|
|
26087
|
-
|
|
26088
|
-
|
|
26089
|
-
|
|
26090
|
-
|
|
26091
|
-
return Buffer.from(bytes).toString('base64');
|
|
26277
|
+
catch (error) {
|
|
26278
|
+
dataURL = options.imagePlaceholder || '';
|
|
26279
|
+
let msg = `Failed to fetch resource: ${resourceUrl}`;
|
|
26280
|
+
if (error) {
|
|
26281
|
+
msg = typeof error === 'string' ? error : error.message;
|
|
26092
26282
|
}
|
|
26093
|
-
|
|
26094
|
-
|
|
26095
|
-
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
26096
|
-
const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length));
|
|
26097
|
-
binary += String.fromCharCode(...chunk);
|
|
26098
|
-
}
|
|
26099
|
-
if (typeof btoa === 'function') {
|
|
26100
|
-
return btoa(binary);
|
|
26101
|
-
}
|
|
26102
|
-
throw new Error('No base64 encoder available in this runtime');
|
|
26103
|
-
}
|
|
26104
|
-
/**
|
|
26105
|
-
* Collects SVG defs (markers, patterns, etc.) needed by the objects.
|
|
26106
|
-
* @param objects - Objects to collect defs for
|
|
26107
|
-
* @param theme - Theme for color resolution
|
|
26108
|
-
* @returns Array of def element strings
|
|
26109
|
-
*/
|
|
26110
|
-
static collectDefs(objects, theme) {
|
|
26111
|
-
const defs = [];
|
|
26112
|
-
for (const obj of objects) {
|
|
26113
|
-
if (KritzelClassHelper.isInstanceOf(obj, 'KritzelLine')) {
|
|
26114
|
-
if (obj.hasStartArrow) {
|
|
26115
|
-
defs.push(this.generateArrowMarker(obj, 'start', theme));
|
|
26116
|
-
}
|
|
26117
|
-
if (obj.hasEndArrow) {
|
|
26118
|
-
defs.push(this.generateArrowMarker(obj, 'end', theme));
|
|
26119
|
-
}
|
|
26120
|
-
}
|
|
26121
|
-
}
|
|
26122
|
-
return defs;
|
|
26123
|
-
}
|
|
26124
|
-
/**
|
|
26125
|
-
* Generates an SVG marker element for line arrows.
|
|
26126
|
-
* @param line - The line object
|
|
26127
|
-
* @param end - Which end of the line ('start' or 'end')
|
|
26128
|
-
* @param theme - Theme for color resolution
|
|
26129
|
-
* @returns SVG marker element string
|
|
26130
|
-
*/
|
|
26131
|
-
static generateArrowMarker(line, end, _theme) {
|
|
26132
|
-
const markerId = end === 'start' ? line.startMarkerId : line.endMarkerId;
|
|
26133
|
-
const size = line.getArrowSize(end);
|
|
26134
|
-
const fill = line.getArrowFill(end);
|
|
26135
|
-
const style = end === 'start' ? line.arrows?.start?.style : line.arrows?.end?.style;
|
|
26136
|
-
const path = line.getArrowPath(style);
|
|
26137
|
-
const orient = end === 'start' ? 'auto-start-reverse' : 'auto';
|
|
26138
|
-
const refX = 0;
|
|
26139
|
-
const refY = size / 2;
|
|
26140
|
-
return `<marker id="${markerId}" markerWidth="${size}" markerHeight="${size}" refX="${refX}" refY="${refY}" orient="${orient}" markerUnits="userSpaceOnUse">
|
|
26141
|
-
<path d="${path}" fill="${fill}" transform="scale(${size / 10})"/>
|
|
26142
|
-
</marker>`;
|
|
26143
|
-
}
|
|
26144
|
-
/**
|
|
26145
|
-
* Converts a Kritzel object to its SVG element representation.
|
|
26146
|
-
* @param object - The object to convert
|
|
26147
|
-
* @param theme - Theme for color resolution
|
|
26148
|
-
* @param imageDataUrls - Optional map of image id -> inlined data URL
|
|
26149
|
-
* @returns SVG element string, or empty string if object type is not supported
|
|
26150
|
-
*/
|
|
26151
|
-
static objectToSvgElement(object, theme, imageDataUrls, rasterSafeForCanvas = false) {
|
|
26152
|
-
if (KritzelClassHelper.isInstanceOf(object, 'KritzelPath')) {
|
|
26153
|
-
return this.pathToSvg(object, theme);
|
|
26154
|
-
}
|
|
26155
|
-
if (KritzelClassHelper.isInstanceOf(object, 'KritzelLine')) {
|
|
26156
|
-
return this.lineToSvg(object, theme);
|
|
26157
|
-
}
|
|
26158
|
-
if (KritzelClassHelper.isInstanceOf(object, 'KritzelShape')) {
|
|
26159
|
-
return this.shapeToSvg(object, theme);
|
|
26160
|
-
}
|
|
26161
|
-
if (KritzelClassHelper.isInstanceOf(object, 'KritzelText')) {
|
|
26162
|
-
return this.textToSvg(object, theme, rasterSafeForCanvas);
|
|
26163
|
-
}
|
|
26164
|
-
if (KritzelClassHelper.isInstanceOf(object, 'KritzelImage')) {
|
|
26165
|
-
return this.imageToSvg(object, imageDataUrls);
|
|
26166
|
-
}
|
|
26167
|
-
if (KritzelClassHelper.isInstanceOf(object, 'KritzelCustomElement')) {
|
|
26168
|
-
return this.customElementToSvg(object, rasterSafeForCanvas);
|
|
26169
|
-
}
|
|
26170
|
-
if (KritzelClassHelper.isInstanceOf(object, 'KritzelGroup')) {
|
|
26171
|
-
return this.groupToSvg(object, theme, imageDataUrls, rasterSafeForCanvas);
|
|
26172
|
-
}
|
|
26173
|
-
return '';
|
|
26174
|
-
}
|
|
26175
|
-
/**
|
|
26176
|
-
* Converts a KritzelPath to SVG.
|
|
26177
|
-
* @param path - The path object
|
|
26178
|
-
* @param theme - Theme for color resolution
|
|
26179
|
-
* @returns SVG element string
|
|
26180
|
-
*/
|
|
26181
|
-
static pathToSvg(path, theme) {
|
|
26182
|
-
const transform = this.buildTransform(path);
|
|
26183
|
-
const fill = KritzelColorHelper.resolveThemeColor(path.fill, theme);
|
|
26184
|
-
const stroke = KritzelColorHelper.resolveThemeColor(path.stroke, theme);
|
|
26185
|
-
const opacity = path.opacity !== 1 ? ` opacity="${path.opacity}"` : '';
|
|
26186
|
-
const scale = this.getObjectScale(path);
|
|
26187
|
-
// Path needs to be wrapped in a g with translation since path.d uses local coordinates
|
|
26188
|
-
return `<g transform="${transform}"${opacity}>
|
|
26189
|
-
<svg viewBox="${path.viewBox}" width="${path.totalWidth / scale}" height="${path.totalHeight / scale}" overflow="visible">
|
|
26190
|
-
<path d="${path.d}" fill="${fill || 'none'}" stroke="${stroke || 'none'}"${path.strokeWidth ? ` stroke-width="${path.strokeWidth}"` : ''}/>
|
|
26191
|
-
</svg>
|
|
26192
|
-
</g>`;
|
|
26193
|
-
}
|
|
26194
|
-
/**
|
|
26195
|
-
* Converts a KritzelLine to SVG.
|
|
26196
|
-
* @param line - The line object
|
|
26197
|
-
* @param theme - Theme for color resolution
|
|
26198
|
-
* @returns SVG element string
|
|
26199
|
-
*/
|
|
26200
|
-
static lineToSvg(line, theme) {
|
|
26201
|
-
const transform = this.buildTransform(line);
|
|
26202
|
-
const stroke = KritzelColorHelper.resolveThemeColor(line.stroke, theme);
|
|
26203
|
-
const opacity = line.opacity !== 1 ? ` opacity="${line.opacity}"` : '';
|
|
26204
|
-
const scale = this.getObjectScale(line);
|
|
26205
|
-
const markerStart = line.hasStartArrow ? ` marker-start="url(#${line.startMarkerId})"` : '';
|
|
26206
|
-
const markerEnd = line.hasEndArrow ? ` marker-end="url(#${line.endMarkerId})"` : '';
|
|
26207
|
-
return `<g transform="${transform}"${opacity}>
|
|
26208
|
-
<svg viewBox="${line.viewBox}" width="${line.totalWidth / scale}" height="${line.totalHeight / scale}" overflow="visible">
|
|
26209
|
-
<path d="${line.d}" fill="none" stroke="${stroke}" stroke-width="${line.strokeWidth}" stroke-linecap="round"${markerStart}${markerEnd}/>
|
|
26210
|
-
</svg>
|
|
26211
|
-
</g>`;
|
|
26212
|
-
}
|
|
26213
|
-
/**
|
|
26214
|
-
* Converts a KritzelShape to SVG.
|
|
26215
|
-
* @param shape - The shape object
|
|
26216
|
-
* @param theme - Theme for color resolution
|
|
26217
|
-
* @returns SVG element string
|
|
26218
|
-
*/
|
|
26219
|
-
static shapeToSvg(shape, theme) {
|
|
26220
|
-
const transform = this.buildTransform(shape);
|
|
26221
|
-
const fill = KritzelColorHelper.resolveThemeColor(shape.fillColor, theme);
|
|
26222
|
-
const stroke = KritzelColorHelper.resolveThemeColor(shape.strokeColor, theme);
|
|
26223
|
-
const opacity = shape.opacity !== 1 ? ` opacity="${shape.opacity}"` : '';
|
|
26224
|
-
const scale = this.getObjectScale(shape);
|
|
26225
|
-
// Get the SVG path for the shape
|
|
26226
|
-
const pathD = shape.getSvgPath();
|
|
26227
|
-
// Handle text content if present
|
|
26228
|
-
let textContent = '';
|
|
26229
|
-
if (shape.editor && !shape.editor.state.doc.textContent.trim()) ;
|
|
26230
|
-
else if (shape.content) {
|
|
26231
|
-
const textHtml = this.prosemirrorToHtml(shape.content, shape, theme);
|
|
26232
|
-
if (textHtml) {
|
|
26233
|
-
const fontColor = KritzelColorHelper.resolveThemeColor(shape.fontColor, theme);
|
|
26234
|
-
textContent = `
|
|
26235
|
-
<foreignObject x="${shape.x}" y="${shape.y}" width="${shape.width}" height="${shape.height}">
|
|
26236
|
-
<div xmlns="http://www.w3.org/1999/xhtml" style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; text-align: center; font-family: ${shape.fontFamily}; font-size: ${shape.fontSize}px; color: ${fontColor}; overflow: hidden;">
|
|
26237
|
-
${textHtml}
|
|
26238
|
-
</div>
|
|
26239
|
-
</foreignObject>`;
|
|
26240
|
-
}
|
|
26241
|
-
}
|
|
26242
|
-
return `<g transform="${transform}"${opacity}>
|
|
26243
|
-
<svg viewBox="${shape.viewBox}" width="${shape.totalWidth / scale}" height="${shape.totalHeight / scale}" overflow="visible" preserveAspectRatio="none">
|
|
26244
|
-
<path d="${pathD}" fill="${fill || 'transparent'}" stroke="${stroke}" stroke-width="${shape.strokeWidth}"/>${textContent}
|
|
26245
|
-
</svg>
|
|
26246
|
-
</g>`;
|
|
26247
|
-
}
|
|
26248
|
-
/**
|
|
26249
|
-
* Converts a KritzelText to SVG using foreignObject for rich text support.
|
|
26250
|
-
* @param text - The text object
|
|
26251
|
-
* @param theme - Theme for color resolution
|
|
26252
|
-
* @returns SVG element string
|
|
26253
|
-
*/
|
|
26254
|
-
static textToSvg(text, theme, rasterSafeForCanvas = false) {
|
|
26255
|
-
if (rasterSafeForCanvas) {
|
|
26256
|
-
return this.textToSvgRasterSafe(text, theme);
|
|
26257
|
-
}
|
|
26258
|
-
const transform = this.buildTransform(text);
|
|
26259
|
-
const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
|
|
26260
|
-
const fontColor = KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
|
|
26261
|
-
const bgColor = KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
|
|
26262
|
-
const scale = this.getObjectScale(text);
|
|
26263
|
-
// Convert ProseMirror content to HTML
|
|
26264
|
-
const htmlContent = this.prosemirrorToHtml(text.content, text, theme);
|
|
26265
|
-
// Calculate dimensions accounting for scale
|
|
26266
|
-
const width = text.totalWidth / scale;
|
|
26267
|
-
const height = text.totalHeight / scale;
|
|
26268
|
-
return `<g transform="${transform}"${opacity}>
|
|
26269
|
-
<foreignObject x="0" y="0" width="${width}" height="${height}">
|
|
26270
|
-
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: ${text.fontFamily}; font-size: ${text.fontSize}pt; color: ${fontColor}; background-color: ${bgColor || 'transparent'}; transform: scale(${text.scaleFactor}); transform-origin: top left; white-space: pre-wrap; word-wrap: break-word;">
|
|
26271
|
-
${htmlContent}
|
|
26272
|
-
</div>
|
|
26273
|
-
</foreignObject>
|
|
26274
|
-
</g>`;
|
|
26275
|
-
}
|
|
26276
|
-
/**
|
|
26277
|
-
* Converts a KritzelText to SVG using pure SVG text primitives.
|
|
26278
|
-
* This path avoids `<foreignObject>`, which taints canvases when the
|
|
26279
|
-
* generated SVG is rasterized through `<img>` and `drawImage`.
|
|
26280
|
-
*/
|
|
26281
|
-
static textToSvgRasterSafe(text, theme) {
|
|
26282
|
-
const transform = this.buildTransform(text);
|
|
26283
|
-
const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
|
|
26284
|
-
const fontColor = KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
|
|
26285
|
-
const bgColor = KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
|
|
26286
|
-
const scale = this.getObjectScale(text);
|
|
26287
|
-
const width = text.totalWidth / scale;
|
|
26288
|
-
const height = text.totalHeight / scale;
|
|
26289
|
-
const lines = this.prosemirrorToPlainTextLines(text.content);
|
|
26290
|
-
const resolvedLines = lines.length > 0 ? lines : [''];
|
|
26291
|
-
const resolvedFontSize = Math.max(1, text.fontSize * (text.scaleFactor || 1));
|
|
26292
|
-
const lineHeight = resolvedFontSize * 1.2;
|
|
26293
|
-
const startY = resolvedFontSize;
|
|
26294
|
-
const bgRect = bgColor && bgColor !== 'transparent'
|
|
26295
|
-
? `<rect x="0" y="0" width="${width}" height="${height}" fill="${bgColor}"/>`
|
|
26296
|
-
: '';
|
|
26297
|
-
const tspans = resolvedLines
|
|
26298
|
-
.map((line, index) => {
|
|
26299
|
-
const y = startY + index * lineHeight;
|
|
26300
|
-
return `<tspan x="0" y="${y}" xml:space="preserve">${this.escapeHtml(line)}</tspan>`;
|
|
26301
|
-
})
|
|
26302
|
-
.join('');
|
|
26303
|
-
return `<g transform="${transform}"${opacity}>
|
|
26304
|
-
${bgRect}
|
|
26305
|
-
<text font-family="${this.escapeHtml(text.fontFamily)}" font-size="${resolvedFontSize}" fill="${fontColor}">${tspans}</text>
|
|
26306
|
-
</g>`;
|
|
26307
|
-
}
|
|
26308
|
-
/**
|
|
26309
|
-
* Converts a KritzelImage to SVG.
|
|
26310
|
-
*
|
|
26311
|
-
* Prefers an inlined `data:` URL from `imageDataUrls` (keyed by object
|
|
26312
|
-
* id) when provided, so the embedded bytes survive rasterization to
|
|
26313
|
-
* PNG. Falls back to `resolvedSrc` (populated by the renderer via the
|
|
26314
|
-
* asset resolver) and finally the legacy inline `src` field for
|
|
26315
|
-
* documents persisted before the asset layer existed.
|
|
26316
|
-
*
|
|
26317
|
-
* Note: a `blob:` `resolvedSrc` renders correctly on-canvas but becomes
|
|
26318
|
-
* a broken-image placeholder once the SVG is loaded into an `<img>` and
|
|
26319
|
-
* drawn to a canvas. Callers that rasterize the SVG must pass
|
|
26320
|
-
* `imageDataUrls` (see {@link KritzelSvgExportHelper.resolveImageDataUrls}).
|
|
26321
|
-
* @param image - The image object
|
|
26322
|
-
* @param imageDataUrls - Optional map of image id -> inlined data URL
|
|
26323
|
-
* @returns SVG element string
|
|
26324
|
-
*/
|
|
26325
|
-
static imageToSvg(image, imageDataUrls) {
|
|
26326
|
-
const transform = this.buildTransform(image);
|
|
26327
|
-
const opacity = image.opacity !== 1 ? ` opacity="${image.opacity}"` : '';
|
|
26328
|
-
const scale = this.getObjectScale(image);
|
|
26329
|
-
// Calculate dimensions
|
|
26330
|
-
const width = image.totalWidth / scale;
|
|
26331
|
-
const height = image.totalHeight / scale;
|
|
26332
|
-
const href = imageDataUrls?.get(image.id) || image.resolvedSrc || image.src || '';
|
|
26333
|
-
return `<g transform="${transform}"${opacity}>
|
|
26334
|
-
<image href="${href}" x="0" y="0" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet"/>
|
|
26335
|
-
</g>`;
|
|
26336
|
-
}
|
|
26337
|
-
/**
|
|
26338
|
-
* Converts a KritzelCustomElement to SVG.
|
|
26339
|
-
* SVG mode: embeds HTML in foreignObject (best-effort).
|
|
26340
|
-
* PNG/raster mode: renders placeholder rect with warning.
|
|
26341
|
-
* @param obj - The custom element object
|
|
26342
|
-
* @param rasterSafeForCanvas - If true, render placeholder (for PNG export)
|
|
26343
|
-
* @returns SVG element string
|
|
26344
|
-
*/
|
|
26345
|
-
static customElementToSvg(obj, rasterSafeForCanvas = false) {
|
|
26346
|
-
const transform = this.buildTransform(obj);
|
|
26347
|
-
const opacity = obj.opacity !== 1 ? ` opacity="${obj.opacity}"` : '';
|
|
26348
|
-
const scale = this.getObjectScale(obj);
|
|
26349
|
-
const width = obj.totalWidth / scale;
|
|
26350
|
-
const height = obj.totalHeight / scale;
|
|
26351
|
-
if (rasterSafeForCanvas) {
|
|
26352
|
-
// PNG export: render placeholder rect (foreignObject doesn't rasterize to PNG well)
|
|
26353
|
-
console.warn('[KritzelCustomElement SVG Export] Custom elements do not serialize well to PNG. Use SVG export or screenshot instead.');
|
|
26354
|
-
return `<g transform="${transform}"${opacity}>
|
|
26355
|
-
<rect x="0" y="0" width="${width}" height="${height}" fill="#f0f0f0" stroke="#999" stroke-width="1" />
|
|
26356
|
-
<text x="${width / 2}" y="${height / 2}" text-anchor="middle" dominant-baseline="middle" font-size="12" fill="#999">[Custom Element]</text>
|
|
26357
|
-
</g>`;
|
|
26358
|
-
}
|
|
26359
|
-
// SVG export: use foreignObject to embed HTML
|
|
26360
|
-
const html = obj.element?.outerHTML || '<div style="border:1px dashed #ccc">Empty</div>';
|
|
26361
|
-
return `<g transform="${transform}"${opacity}>
|
|
26362
|
-
<foreignObject x="0" y="0" width="${width}" height="${height}">
|
|
26363
|
-
<div xmlns="http://www.w3.org/1999/xhtml" style="width:100%;height:100%;overflow:hidden;">
|
|
26364
|
-
${html}
|
|
26365
|
-
</div>
|
|
26366
|
-
</foreignObject>
|
|
26367
|
-
</g>`;
|
|
26368
|
-
}
|
|
26369
|
-
/**
|
|
26370
|
-
* Converts a KritzelGroup to SVG by recursively converting children.
|
|
26371
|
-
* @param group - The group object
|
|
26372
|
-
* @param theme - Theme for color resolution
|
|
26373
|
-
* @param imageDataUrls - Optional map of image id -> inlined data URL
|
|
26374
|
-
* @returns SVG element string
|
|
26375
|
-
*/
|
|
26376
|
-
static groupToSvg(group, theme, imageDataUrls, rasterSafeForCanvas = false) {
|
|
26377
|
-
const children = group.children
|
|
26378
|
-
.sort((a, b) => a.zIndex - b.zIndex)
|
|
26379
|
-
.map(child => this.objectToSvgElement(child, theme, imageDataUrls, rasterSafeForCanvas))
|
|
26380
|
-
.filter(Boolean)
|
|
26381
|
-
.join('\n ');
|
|
26382
|
-
if (!children) {
|
|
26383
|
-
return '';
|
|
26384
|
-
}
|
|
26385
|
-
const opacity = group.opacity !== 1 ? ` opacity="${group.opacity}"` : '';
|
|
26386
|
-
return `<g${opacity}>
|
|
26387
|
-
${children}
|
|
26388
|
-
</g>`;
|
|
26389
|
-
}
|
|
26390
|
-
/**
|
|
26391
|
-
* Builds a transform attribute string for an object.
|
|
26392
|
-
* Combines translation and rotation.
|
|
26393
|
-
* @param object - The object to build transform for
|
|
26394
|
-
* @returns Transform attribute value string
|
|
26395
|
-
*/
|
|
26396
|
-
static buildTransform(object) {
|
|
26397
|
-
const transforms = [];
|
|
26398
|
-
const scale = this.getObjectScale(object);
|
|
26399
|
-
// Translation
|
|
26400
|
-
transforms.push(`translate(${object.translateX}, ${object.translateY})`);
|
|
26401
|
-
// Rotation around center
|
|
26402
|
-
if (object.rotation !== 0) {
|
|
26403
|
-
const centerX = object.totalWidth / 2 / scale;
|
|
26404
|
-
const centerY = object.totalHeight / 2 / scale;
|
|
26405
|
-
const degrees = object.rotation * (180 / Math.PI);
|
|
26406
|
-
transforms.push(`rotate(${degrees}, ${centerX}, ${centerY})`);
|
|
26407
|
-
}
|
|
26408
|
-
return transforms.join(' ');
|
|
26409
|
-
}
|
|
26410
|
-
/**
|
|
26411
|
-
* Returns a safe non-zero scale for export geometry calculations.
|
|
26412
|
-
*/
|
|
26413
|
-
static getObjectScale(object) {
|
|
26414
|
-
const scale = object.scale ?? 1;
|
|
26415
|
-
return scale === 0 ? 1 : scale;
|
|
26416
|
-
}
|
|
26417
|
-
/**
|
|
26418
|
-
* Converts ProseMirror JSON content to HTML string.
|
|
26419
|
-
* @param content - ProseMirror document JSON
|
|
26420
|
-
* @param context - The text or shape object for styling context
|
|
26421
|
-
* @param theme - Theme for color resolution
|
|
26422
|
-
* @returns HTML string
|
|
26423
|
-
*/
|
|
26424
|
-
static prosemirrorToHtml(content, _context, theme) {
|
|
26425
|
-
if (!content || !content.content) {
|
|
26426
|
-
return '';
|
|
26427
|
-
}
|
|
26428
|
-
return content.content
|
|
26429
|
-
.map((node) => this.nodeToHtml(node, theme))
|
|
26430
|
-
.join('');
|
|
26431
|
-
}
|
|
26432
|
-
/**
|
|
26433
|
-
* Converts a ProseMirror node to HTML.
|
|
26434
|
-
* @param node - ProseMirror node JSON
|
|
26435
|
-
* @param theme - Theme for color resolution
|
|
26436
|
-
* @returns HTML string
|
|
26437
|
-
*/
|
|
26438
|
-
static nodeToHtml(node, theme) {
|
|
26439
|
-
if (!node)
|
|
26440
|
-
return '';
|
|
26441
|
-
switch (node.type) {
|
|
26442
|
-
case 'paragraph':
|
|
26443
|
-
const pContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
26444
|
-
return `<p style="margin: 0;">${pContent || '<br/>'}</p>`;
|
|
26445
|
-
case 'text':
|
|
26446
|
-
let text = this.escapeHtml(node.text || '');
|
|
26447
|
-
if (node.marks) {
|
|
26448
|
-
for (const mark of node.marks) {
|
|
26449
|
-
text = this.applyMark(text, mark, theme);
|
|
26450
|
-
}
|
|
26451
|
-
}
|
|
26452
|
-
return text;
|
|
26453
|
-
case 'hard_break':
|
|
26454
|
-
return '<br/>';
|
|
26455
|
-
case 'bullet_list':
|
|
26456
|
-
const blContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
26457
|
-
return `<ul style="margin: 0; padding-left: 1.5em;">${blContent}</ul>`;
|
|
26458
|
-
case 'ordered_list':
|
|
26459
|
-
const olContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
26460
|
-
return `<ol style="margin: 0; padding-left: 1.5em;">${olContent}</ol>`;
|
|
26461
|
-
case 'list_item':
|
|
26462
|
-
const liContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
26463
|
-
return `<li>${liContent}</li>`;
|
|
26464
|
-
default:
|
|
26465
|
-
// For unknown node types, try to render content
|
|
26466
|
-
if (node.content) {
|
|
26467
|
-
return node.content.map((n) => this.nodeToHtml(n, theme)).join('');
|
|
26468
|
-
}
|
|
26469
|
-
return '';
|
|
26470
|
-
}
|
|
26471
|
-
}
|
|
26472
|
-
/**
|
|
26473
|
-
* Converts ProseMirror JSON content to plain-text lines.
|
|
26474
|
-
* Used by raster-safe SVG export, which cannot depend on HTML/foreignObject.
|
|
26475
|
-
*/
|
|
26476
|
-
static prosemirrorToPlainTextLines(content) {
|
|
26477
|
-
if (!content || !Array.isArray(content.content)) {
|
|
26478
|
-
return [];
|
|
26479
|
-
}
|
|
26480
|
-
const plainText = content.content
|
|
26481
|
-
.map((node) => this.nodeToPlainText(node))
|
|
26482
|
-
.join('')
|
|
26483
|
-
.replace(/\r\n/g, '\n')
|
|
26484
|
-
.replace(/\n+$/, '');
|
|
26485
|
-
if (!plainText) {
|
|
26486
|
-
return [];
|
|
26487
|
-
}
|
|
26488
|
-
return plainText.split('\n');
|
|
26489
|
-
}
|
|
26490
|
-
/**
|
|
26491
|
-
* Converts a ProseMirror node tree into plain text.
|
|
26492
|
-
*/
|
|
26493
|
-
static nodeToPlainText(node) {
|
|
26494
|
-
if (!node) {
|
|
26495
|
-
return '';
|
|
26496
|
-
}
|
|
26497
|
-
switch (node.type) {
|
|
26498
|
-
case 'text':
|
|
26499
|
-
return node.text || '';
|
|
26500
|
-
case 'hard_break':
|
|
26501
|
-
return '\n';
|
|
26502
|
-
case 'paragraph': {
|
|
26503
|
-
const paragraph = Array.isArray(node.content)
|
|
26504
|
-
? node.content.map((child) => this.nodeToPlainText(child)).join('')
|
|
26505
|
-
: '';
|
|
26506
|
-
return `${paragraph}\n`;
|
|
26507
|
-
}
|
|
26508
|
-
case 'list_item': {
|
|
26509
|
-
const item = Array.isArray(node.content)
|
|
26510
|
-
? node.content.map((child) => this.nodeToPlainText(child)).join('')
|
|
26511
|
-
: '';
|
|
26512
|
-
return `- ${item.trim()}\n`;
|
|
26513
|
-
}
|
|
26514
|
-
default:
|
|
26515
|
-
return Array.isArray(node.content)
|
|
26516
|
-
? node.content.map((child) => this.nodeToPlainText(child)).join('')
|
|
26517
|
-
: '';
|
|
26518
|
-
}
|
|
26519
|
-
}
|
|
26520
|
-
/**
|
|
26521
|
-
* Applies a ProseMirror mark to text.
|
|
26522
|
-
* @param text - The text to wrap
|
|
26523
|
-
* @param mark - The mark to apply
|
|
26524
|
-
* @param theme - Theme for color resolution
|
|
26525
|
-
* @returns Wrapped HTML string
|
|
26526
|
-
*/
|
|
26527
|
-
static applyMark(text, mark, _theme) {
|
|
26528
|
-
switch (mark.type) {
|
|
26529
|
-
case 'bold':
|
|
26530
|
-
case 'strong':
|
|
26531
|
-
return `<strong>${text}</strong>`;
|
|
26532
|
-
case 'italic':
|
|
26533
|
-
case 'em':
|
|
26534
|
-
return `<em>${text}</em>`;
|
|
26535
|
-
case 'underline':
|
|
26536
|
-
return `<u>${text}</u>`;
|
|
26537
|
-
case 'strike':
|
|
26538
|
-
return `<s>${text}</s>`;
|
|
26539
|
-
case 'code':
|
|
26540
|
-
return `<code>${text}</code>`;
|
|
26541
|
-
case 'link':
|
|
26542
|
-
const href = mark.attrs?.href || '#';
|
|
26543
|
-
return `<a href="${this.escapeHtml(href)}">${text}</a>`;
|
|
26544
|
-
default:
|
|
26545
|
-
return text;
|
|
26546
|
-
}
|
|
26547
|
-
}
|
|
26548
|
-
/**
|
|
26549
|
-
* Escapes HTML special characters.
|
|
26550
|
-
* @param str - String to escape
|
|
26551
|
-
* @returns Escaped string
|
|
26552
|
-
*/
|
|
26553
|
-
static escapeHtml(str) {
|
|
26554
|
-
return str
|
|
26555
|
-
.replace(/&/g, '&')
|
|
26556
|
-
.replace(/</g, '<')
|
|
26557
|
-
.replace(/>/g, '>')
|
|
26558
|
-
.replace(/"/g, '"')
|
|
26559
|
-
.replace(/'/g, ''');
|
|
26560
|
-
}
|
|
26561
|
-
}
|
|
26562
|
-
|
|
26563
|
-
function resolveUrl(url, baseUrl) {
|
|
26564
|
-
// url is absolute already
|
|
26565
|
-
if (url.match(/^[a-z]+:\/\//i)) {
|
|
26566
|
-
return url;
|
|
26567
|
-
}
|
|
26568
|
-
// url is absolute already, without protocol
|
|
26569
|
-
if (url.match(/^\/\//)) {
|
|
26570
|
-
return window.location.protocol + url;
|
|
26571
|
-
}
|
|
26572
|
-
// dataURI, mailto:, tel:, etc.
|
|
26573
|
-
if (url.match(/^[a-z]+:/i)) {
|
|
26574
|
-
return url;
|
|
26575
|
-
}
|
|
26576
|
-
const doc = document.implementation.createHTMLDocument();
|
|
26577
|
-
const base = doc.createElement('base');
|
|
26578
|
-
const a = doc.createElement('a');
|
|
26579
|
-
doc.head.appendChild(base);
|
|
26580
|
-
doc.body.appendChild(a);
|
|
26581
|
-
if (baseUrl) {
|
|
26582
|
-
base.href = baseUrl;
|
|
26583
|
-
}
|
|
26584
|
-
a.href = url;
|
|
26585
|
-
return a.href;
|
|
26586
|
-
}
|
|
26587
|
-
const uuid = (() => {
|
|
26588
|
-
// generate uuid for className of pseudo elements.
|
|
26589
|
-
// We should not use GUIDs, otherwise pseudo elements sometimes cannot be captured.
|
|
26590
|
-
let counter = 0;
|
|
26591
|
-
// ref: http://stackoverflow.com/a/6248722/2519373
|
|
26592
|
-
const random = () =>
|
|
26593
|
-
// eslint-disable-next-line no-bitwise
|
|
26594
|
-
`0000${((Math.random() * 36 ** 4) << 0).toString(36)}`.slice(-4);
|
|
26595
|
-
return () => {
|
|
26596
|
-
counter += 1;
|
|
26597
|
-
return `u${random()}${counter}`;
|
|
26598
|
-
};
|
|
26599
|
-
})();
|
|
26600
|
-
function toArray(arrayLike) {
|
|
26601
|
-
const arr = [];
|
|
26602
|
-
for (let i = 0, l = arrayLike.length; i < l; i++) {
|
|
26603
|
-
arr.push(arrayLike[i]);
|
|
26604
|
-
}
|
|
26605
|
-
return arr;
|
|
26606
|
-
}
|
|
26607
|
-
let styleProps = null;
|
|
26608
|
-
function getStyleProperties(options = {}) {
|
|
26609
|
-
if (styleProps) {
|
|
26610
|
-
return styleProps;
|
|
26611
|
-
}
|
|
26612
|
-
if (options.includeStyleProperties) {
|
|
26613
|
-
styleProps = options.includeStyleProperties;
|
|
26614
|
-
return styleProps;
|
|
26615
|
-
}
|
|
26616
|
-
styleProps = toArray(window.getComputedStyle(document.documentElement));
|
|
26617
|
-
return styleProps;
|
|
26618
|
-
}
|
|
26619
|
-
function px(node, styleProperty) {
|
|
26620
|
-
const win = node.ownerDocument.defaultView || window;
|
|
26621
|
-
const val = win.getComputedStyle(node).getPropertyValue(styleProperty);
|
|
26622
|
-
return val ? parseFloat(val.replace('px', '')) : 0;
|
|
26623
|
-
}
|
|
26624
|
-
function getNodeWidth(node) {
|
|
26625
|
-
const leftBorder = px(node, 'border-left-width');
|
|
26626
|
-
const rightBorder = px(node, 'border-right-width');
|
|
26627
|
-
return node.clientWidth + leftBorder + rightBorder;
|
|
26628
|
-
}
|
|
26629
|
-
function getNodeHeight(node) {
|
|
26630
|
-
const topBorder = px(node, 'border-top-width');
|
|
26631
|
-
const bottomBorder = px(node, 'border-bottom-width');
|
|
26632
|
-
return node.clientHeight + topBorder + bottomBorder;
|
|
26633
|
-
}
|
|
26634
|
-
function getImageSize(targetNode, options = {}) {
|
|
26635
|
-
const width = options.width || getNodeWidth(targetNode);
|
|
26636
|
-
const height = options.height || getNodeHeight(targetNode);
|
|
26637
|
-
return { width, height };
|
|
26638
|
-
}
|
|
26639
|
-
function getPixelRatio() {
|
|
26640
|
-
let ratio;
|
|
26641
|
-
let FINAL_PROCESS;
|
|
26642
|
-
try {
|
|
26643
|
-
FINAL_PROCESS = process;
|
|
26644
|
-
}
|
|
26645
|
-
catch (e) {
|
|
26646
|
-
// pass
|
|
26647
|
-
}
|
|
26648
|
-
const val = FINAL_PROCESS && FINAL_PROCESS.env
|
|
26649
|
-
? FINAL_PROCESS.env.devicePixelRatio
|
|
26650
|
-
: null;
|
|
26651
|
-
if (val) {
|
|
26652
|
-
ratio = parseInt(val, 10);
|
|
26653
|
-
if (Number.isNaN(ratio)) {
|
|
26654
|
-
ratio = 1;
|
|
26655
|
-
}
|
|
26656
|
-
}
|
|
26657
|
-
return ratio || window.devicePixelRatio || 1;
|
|
26658
|
-
}
|
|
26659
|
-
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
|
|
26660
|
-
const canvasDimensionLimit = 16384;
|
|
26661
|
-
function checkCanvasDimensions(canvas) {
|
|
26662
|
-
if (canvas.width > canvasDimensionLimit ||
|
|
26663
|
-
canvas.height > canvasDimensionLimit) {
|
|
26664
|
-
if (canvas.width > canvasDimensionLimit &&
|
|
26665
|
-
canvas.height > canvasDimensionLimit) {
|
|
26666
|
-
if (canvas.width > canvas.height) {
|
|
26667
|
-
canvas.height *= canvasDimensionLimit / canvas.width;
|
|
26668
|
-
canvas.width = canvasDimensionLimit;
|
|
26669
|
-
}
|
|
26670
|
-
else {
|
|
26671
|
-
canvas.width *= canvasDimensionLimit / canvas.height;
|
|
26672
|
-
canvas.height = canvasDimensionLimit;
|
|
26673
|
-
}
|
|
26674
|
-
}
|
|
26675
|
-
else if (canvas.width > canvasDimensionLimit) {
|
|
26676
|
-
canvas.height *= canvasDimensionLimit / canvas.width;
|
|
26677
|
-
canvas.width = canvasDimensionLimit;
|
|
26678
|
-
}
|
|
26679
|
-
else {
|
|
26680
|
-
canvas.width *= canvasDimensionLimit / canvas.height;
|
|
26681
|
-
canvas.height = canvasDimensionLimit;
|
|
26682
|
-
}
|
|
26683
|
-
}
|
|
26684
|
-
}
|
|
26685
|
-
function createImage(url) {
|
|
26686
|
-
return new Promise((resolve, reject) => {
|
|
26687
|
-
const img = new Image();
|
|
26688
|
-
img.onload = () => {
|
|
26689
|
-
img.decode().then(() => {
|
|
26690
|
-
requestAnimationFrame(() => resolve(img));
|
|
26691
|
-
});
|
|
26692
|
-
};
|
|
26693
|
-
img.onerror = reject;
|
|
26694
|
-
img.crossOrigin = 'anonymous';
|
|
26695
|
-
img.decoding = 'async';
|
|
26696
|
-
img.src = url;
|
|
26697
|
-
});
|
|
26698
|
-
}
|
|
26699
|
-
async function svgToDataURL(svg) {
|
|
26700
|
-
return Promise.resolve()
|
|
26701
|
-
.then(() => new XMLSerializer().serializeToString(svg))
|
|
26702
|
-
.then(encodeURIComponent)
|
|
26703
|
-
.then((html) => `data:image/svg+xml;charset=utf-8,${html}`);
|
|
26704
|
-
}
|
|
26705
|
-
async function nodeToDataURL(node, width, height) {
|
|
26706
|
-
const xmlns = 'http://www.w3.org/2000/svg';
|
|
26707
|
-
const svg = document.createElementNS(xmlns, 'svg');
|
|
26708
|
-
const foreignObject = document.createElementNS(xmlns, 'foreignObject');
|
|
26709
|
-
svg.setAttribute('width', `${width}`);
|
|
26710
|
-
svg.setAttribute('height', `${height}`);
|
|
26711
|
-
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
26712
|
-
foreignObject.setAttribute('width', '100%');
|
|
26713
|
-
foreignObject.setAttribute('height', '100%');
|
|
26714
|
-
foreignObject.setAttribute('x', '0');
|
|
26715
|
-
foreignObject.setAttribute('y', '0');
|
|
26716
|
-
foreignObject.setAttribute('externalResourcesRequired', 'true');
|
|
26717
|
-
svg.appendChild(foreignObject);
|
|
26718
|
-
foreignObject.appendChild(node);
|
|
26719
|
-
return svgToDataURL(svg);
|
|
26720
|
-
}
|
|
26721
|
-
const isInstanceOfElement = (node, instance) => {
|
|
26722
|
-
if (node instanceof instance)
|
|
26723
|
-
return true;
|
|
26724
|
-
const nodePrototype = Object.getPrototypeOf(node);
|
|
26725
|
-
if (nodePrototype === null)
|
|
26726
|
-
return false;
|
|
26727
|
-
return (nodePrototype.constructor.name === instance.name ||
|
|
26728
|
-
isInstanceOfElement(nodePrototype, instance));
|
|
26729
|
-
};
|
|
26730
|
-
|
|
26731
|
-
function formatCSSText(style) {
|
|
26732
|
-
const content = style.getPropertyValue('content');
|
|
26733
|
-
return `${style.cssText} content: '${content.replace(/'|"/g, '')}';`;
|
|
26734
|
-
}
|
|
26735
|
-
function formatCSSProperties(style, options) {
|
|
26736
|
-
return getStyleProperties(options)
|
|
26737
|
-
.map((name) => {
|
|
26738
|
-
const value = style.getPropertyValue(name);
|
|
26739
|
-
const priority = style.getPropertyPriority(name);
|
|
26740
|
-
return `${name}: ${value}${priority ? ' !important' : ''};`;
|
|
26741
|
-
})
|
|
26742
|
-
.join(' ');
|
|
26743
|
-
}
|
|
26744
|
-
function getPseudoElementStyle(className, pseudo, style, options) {
|
|
26745
|
-
const selector = `.${className}:${pseudo}`;
|
|
26746
|
-
const cssText = style.cssText
|
|
26747
|
-
? formatCSSText(style)
|
|
26748
|
-
: formatCSSProperties(style, options);
|
|
26749
|
-
return document.createTextNode(`${selector}{${cssText}}`);
|
|
26750
|
-
}
|
|
26751
|
-
function clonePseudoElement(nativeNode, clonedNode, pseudo, options) {
|
|
26752
|
-
const style = window.getComputedStyle(nativeNode, pseudo);
|
|
26753
|
-
const content = style.getPropertyValue('content');
|
|
26754
|
-
if (content === '' || content === 'none') {
|
|
26755
|
-
return;
|
|
26756
|
-
}
|
|
26757
|
-
const className = uuid();
|
|
26758
|
-
try {
|
|
26759
|
-
clonedNode.className = `${clonedNode.className} ${className}`;
|
|
26760
|
-
}
|
|
26761
|
-
catch (err) {
|
|
26762
|
-
return;
|
|
26763
|
-
}
|
|
26764
|
-
const styleElement = document.createElement('style');
|
|
26765
|
-
styleElement.appendChild(getPseudoElementStyle(className, pseudo, style, options));
|
|
26766
|
-
clonedNode.appendChild(styleElement);
|
|
26767
|
-
}
|
|
26768
|
-
function clonePseudoElements(nativeNode, clonedNode, options) {
|
|
26769
|
-
clonePseudoElement(nativeNode, clonedNode, ':before', options);
|
|
26770
|
-
clonePseudoElement(nativeNode, clonedNode, ':after', options);
|
|
26771
|
-
}
|
|
26772
|
-
|
|
26773
|
-
const WOFF = 'application/font-woff';
|
|
26774
|
-
const JPEG = 'image/jpeg';
|
|
26775
|
-
const mimes = {
|
|
26776
|
-
woff: WOFF,
|
|
26777
|
-
woff2: WOFF,
|
|
26778
|
-
ttf: 'application/font-truetype',
|
|
26779
|
-
eot: 'application/vnd.ms-fontobject',
|
|
26780
|
-
png: 'image/png',
|
|
26781
|
-
jpg: JPEG,
|
|
26782
|
-
jpeg: JPEG,
|
|
26783
|
-
gif: 'image/gif',
|
|
26784
|
-
tiff: 'image/tiff',
|
|
26785
|
-
svg: 'image/svg+xml',
|
|
26786
|
-
webp: 'image/webp',
|
|
26787
|
-
};
|
|
26788
|
-
function getExtension(url) {
|
|
26789
|
-
const match = /\.([^./]*?)$/g.exec(url);
|
|
26790
|
-
return match ? match[1] : '';
|
|
26791
|
-
}
|
|
26792
|
-
function getMimeType(url) {
|
|
26793
|
-
const extension = getExtension(url).toLowerCase();
|
|
26794
|
-
return mimes[extension] || '';
|
|
26795
|
-
}
|
|
26796
|
-
|
|
26797
|
-
function getContentFromDataUrl(dataURL) {
|
|
26798
|
-
return dataURL.split(/,/)[1];
|
|
26799
|
-
}
|
|
26800
|
-
function isDataUrl(url) {
|
|
26801
|
-
return url.search(/^(data:)/) !== -1;
|
|
26802
|
-
}
|
|
26803
|
-
function makeDataUrl(content, mimeType) {
|
|
26804
|
-
return `data:${mimeType};base64,${content}`;
|
|
26805
|
-
}
|
|
26806
|
-
async function fetchAsDataURL(url, init, process) {
|
|
26807
|
-
const res = await fetch(url, init);
|
|
26808
|
-
if (res.status === 404) {
|
|
26809
|
-
throw new Error(`Resource "${res.url}" not found`);
|
|
26810
|
-
}
|
|
26811
|
-
const blob = await res.blob();
|
|
26812
|
-
return new Promise((resolve, reject) => {
|
|
26813
|
-
const reader = new FileReader();
|
|
26814
|
-
reader.onerror = reject;
|
|
26815
|
-
reader.onloadend = () => {
|
|
26816
|
-
try {
|
|
26817
|
-
resolve(process({ res, result: reader.result }));
|
|
26818
|
-
}
|
|
26819
|
-
catch (error) {
|
|
26820
|
-
reject(error);
|
|
26821
|
-
}
|
|
26822
|
-
};
|
|
26823
|
-
reader.readAsDataURL(blob);
|
|
26824
|
-
});
|
|
26825
|
-
}
|
|
26826
|
-
const cache = {};
|
|
26827
|
-
function getCacheKey(url, contentType, includeQueryParams) {
|
|
26828
|
-
let key = url.replace(/\?.*/, '');
|
|
26829
|
-
if (includeQueryParams) {
|
|
26830
|
-
key = url;
|
|
26831
|
-
}
|
|
26832
|
-
// font resource
|
|
26833
|
-
if (/ttf|otf|eot|woff2?/i.test(key)) {
|
|
26834
|
-
key = key.replace(/.*\//, '');
|
|
26835
|
-
}
|
|
26836
|
-
return contentType ? `[${contentType}]${key}` : key;
|
|
26837
|
-
}
|
|
26838
|
-
async function resourceToDataURL(resourceUrl, contentType, options) {
|
|
26839
|
-
const cacheKey = getCacheKey(resourceUrl, contentType, options.includeQueryParams);
|
|
26840
|
-
if (cache[cacheKey] != null) {
|
|
26841
|
-
return cache[cacheKey];
|
|
26842
|
-
}
|
|
26843
|
-
// ref: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
|
|
26844
|
-
if (options.cacheBust) {
|
|
26845
|
-
// eslint-disable-next-line no-param-reassign
|
|
26846
|
-
resourceUrl += (/\?/.test(resourceUrl) ? '&' : '?') + new Date().getTime();
|
|
26847
|
-
}
|
|
26848
|
-
let dataURL;
|
|
26849
|
-
try {
|
|
26850
|
-
const content = await fetchAsDataURL(resourceUrl, options.fetchRequestInit, ({ res, result }) => {
|
|
26851
|
-
if (!contentType) {
|
|
26852
|
-
// eslint-disable-next-line no-param-reassign
|
|
26853
|
-
contentType = res.headers.get('Content-Type') || '';
|
|
26854
|
-
}
|
|
26855
|
-
return getContentFromDataUrl(result);
|
|
26856
|
-
});
|
|
26857
|
-
dataURL = makeDataUrl(content, contentType);
|
|
26858
|
-
}
|
|
26859
|
-
catch (error) {
|
|
26860
|
-
dataURL = options.imagePlaceholder || '';
|
|
26861
|
-
let msg = `Failed to fetch resource: ${resourceUrl}`;
|
|
26862
|
-
if (error) {
|
|
26863
|
-
msg = typeof error === 'string' ? error : error.message;
|
|
26864
|
-
}
|
|
26865
|
-
if (msg) {
|
|
26866
|
-
console.warn(msg);
|
|
26283
|
+
if (msg) {
|
|
26284
|
+
console.warn(msg);
|
|
26867
26285
|
}
|
|
26868
26286
|
}
|
|
26869
26287
|
cache[cacheKey] = dataURL;
|
|
@@ -27397,156 +26815,808 @@ async function embedWebFonts(clonedNode, options) {
|
|
|
27397
26815
|
clonedNode.appendChild(styleNode);
|
|
27398
26816
|
}
|
|
27399
26817
|
}
|
|
27400
|
-
}
|
|
27401
|
-
|
|
27402
|
-
async function toSvg(node, options = {}) {
|
|
27403
|
-
const { width, height } = getImageSize(node, options);
|
|
27404
|
-
const clonedNode = (await cloneNode(node, options, true));
|
|
27405
|
-
await embedWebFonts(clonedNode, options);
|
|
27406
|
-
await embedImages(clonedNode, options);
|
|
27407
|
-
applyStyle(clonedNode, options);
|
|
27408
|
-
const datauri = await nodeToDataURL(clonedNode, width, height);
|
|
27409
|
-
return datauri;
|
|
27410
|
-
}
|
|
27411
|
-
async function toCanvas(node, options = {}) {
|
|
27412
|
-
const { width, height } = getImageSize(node, options);
|
|
27413
|
-
const svg = await toSvg(node, options);
|
|
27414
|
-
const img = await createImage(svg);
|
|
27415
|
-
const canvas = document.createElement('canvas');
|
|
27416
|
-
const context = canvas.getContext('2d');
|
|
27417
|
-
const ratio = options.pixelRatio || getPixelRatio();
|
|
27418
|
-
const canvasWidth = options.canvasWidth || width;
|
|
27419
|
-
const canvasHeight = options.canvasHeight || height;
|
|
27420
|
-
canvas.width = canvasWidth * ratio;
|
|
27421
|
-
canvas.height = canvasHeight * ratio;
|
|
27422
|
-
if (!options.skipAutoScale) {
|
|
27423
|
-
checkCanvasDimensions(canvas);
|
|
26818
|
+
}
|
|
26819
|
+
|
|
26820
|
+
async function toSvg(node, options = {}) {
|
|
26821
|
+
const { width, height } = getImageSize(node, options);
|
|
26822
|
+
const clonedNode = (await cloneNode(node, options, true));
|
|
26823
|
+
await embedWebFonts(clonedNode, options);
|
|
26824
|
+
await embedImages(clonedNode, options);
|
|
26825
|
+
applyStyle(clonedNode, options);
|
|
26826
|
+
const datauri = await nodeToDataURL(clonedNode, width, height);
|
|
26827
|
+
return datauri;
|
|
26828
|
+
}
|
|
26829
|
+
async function toCanvas(node, options = {}) {
|
|
26830
|
+
const { width, height } = getImageSize(node, options);
|
|
26831
|
+
const svg = await toSvg(node, options);
|
|
26832
|
+
const img = await createImage(svg);
|
|
26833
|
+
const canvas = document.createElement('canvas');
|
|
26834
|
+
const context = canvas.getContext('2d');
|
|
26835
|
+
const ratio = options.pixelRatio || getPixelRatio();
|
|
26836
|
+
const canvasWidth = options.canvasWidth || width;
|
|
26837
|
+
const canvasHeight = options.canvasHeight || height;
|
|
26838
|
+
canvas.width = canvasWidth * ratio;
|
|
26839
|
+
canvas.height = canvasHeight * ratio;
|
|
26840
|
+
if (!options.skipAutoScale) {
|
|
26841
|
+
checkCanvasDimensions(canvas);
|
|
26842
|
+
}
|
|
26843
|
+
canvas.style.width = `${canvasWidth}`;
|
|
26844
|
+
canvas.style.height = `${canvasHeight}`;
|
|
26845
|
+
if (options.backgroundColor) {
|
|
26846
|
+
context.fillStyle = options.backgroundColor;
|
|
26847
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
26848
|
+
}
|
|
26849
|
+
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
26850
|
+
return canvas;
|
|
26851
|
+
}
|
|
26852
|
+
async function toPng(node, options = {}) {
|
|
26853
|
+
const canvas = await toCanvas(node, options);
|
|
26854
|
+
return canvas.toDataURL();
|
|
26855
|
+
}
|
|
26856
|
+
|
|
26857
|
+
class KritzelWheelHelper {
|
|
26858
|
+
static shouldKeepWheelInCustomElement(ev, customElement) {
|
|
26859
|
+
// Keep Ctrl+wheel available for viewport zoom gestures.
|
|
26860
|
+
if (ev.ctrlKey || customElement.isSelected !== true) {
|
|
26861
|
+
return false;
|
|
26862
|
+
}
|
|
26863
|
+
const path = typeof ev.composedPath === 'function' ? ev.composedPath() : [];
|
|
26864
|
+
const scrollContainer = this.getScrollableContainerFromEventPath(path, customElement);
|
|
26865
|
+
// No scroll container found inside the custom element: let the wheel
|
|
26866
|
+
// event pass through so the viewport can continue handling it.
|
|
26867
|
+
if (!scrollContainer) {
|
|
26868
|
+
return false;
|
|
26869
|
+
}
|
|
26870
|
+
return this.canElementConsumeWheel(scrollContainer, ev.deltaX, ev.deltaY);
|
|
26871
|
+
}
|
|
26872
|
+
static getScrollableContainerFromEventPath(path, customElement) {
|
|
26873
|
+
const potentialRoots = [];
|
|
26874
|
+
if (customElement.element instanceof HTMLElement) {
|
|
26875
|
+
potentialRoots.push(customElement.element);
|
|
26876
|
+
}
|
|
26877
|
+
if (customElement.elementRef instanceof HTMLElement) {
|
|
26878
|
+
potentialRoots.push(customElement.elementRef);
|
|
26879
|
+
}
|
|
26880
|
+
const legacyElementRef = customElement.elmeentRef;
|
|
26881
|
+
if (legacyElementRef instanceof HTMLElement) {
|
|
26882
|
+
potentialRoots.push(legacyElementRef);
|
|
26883
|
+
}
|
|
26884
|
+
for (const root of potentialRoots) {
|
|
26885
|
+
const rootIndex = path.findIndex(entry => entry === root);
|
|
26886
|
+
if (rootIndex === -1) {
|
|
26887
|
+
continue;
|
|
26888
|
+
}
|
|
26889
|
+
for (const entry of path.slice(0, rootIndex + 1)) {
|
|
26890
|
+
if (!(entry instanceof HTMLElement)) {
|
|
26891
|
+
continue;
|
|
26892
|
+
}
|
|
26893
|
+
if (this.isScrollableElement(entry)) {
|
|
26894
|
+
return entry;
|
|
26895
|
+
}
|
|
26896
|
+
}
|
|
26897
|
+
}
|
|
26898
|
+
// Fallback for synthetic/custom wheel events where composedPath is absent
|
|
26899
|
+
// or does not include the custom-element subtree.
|
|
26900
|
+
for (const root of potentialRoots) {
|
|
26901
|
+
if (this.isScrollableElement(root)) {
|
|
26902
|
+
return root;
|
|
26903
|
+
}
|
|
26904
|
+
const firstChild = root.firstElementChild;
|
|
26905
|
+
if (firstChild instanceof HTMLElement && this.isScrollableElement(firstChild)) {
|
|
26906
|
+
return firstChild;
|
|
26907
|
+
}
|
|
26908
|
+
}
|
|
26909
|
+
return null;
|
|
26910
|
+
}
|
|
26911
|
+
static isScrollableElement(element) {
|
|
26912
|
+
const canScrollVertically = element.scrollHeight - element.clientHeight > 0;
|
|
26913
|
+
const canScrollHorizontally = element.scrollWidth - element.clientWidth > 0;
|
|
26914
|
+
if (!canScrollVertically && !canScrollHorizontally) {
|
|
26915
|
+
return false;
|
|
26916
|
+
}
|
|
26917
|
+
const computedStyle = window.getComputedStyle(element);
|
|
26918
|
+
const overflowX = computedStyle.overflowX;
|
|
26919
|
+
const overflowY = computedStyle.overflowY;
|
|
26920
|
+
const allowsVerticalScroll = overflowY !== 'visible' && overflowY !== 'clip';
|
|
26921
|
+
const allowsHorizontalScroll = overflowX !== 'visible' && overflowX !== 'clip';
|
|
26922
|
+
return (canScrollVertically && allowsVerticalScroll) || (canScrollHorizontally && allowsHorizontalScroll);
|
|
26923
|
+
}
|
|
26924
|
+
static canElementConsumeWheel(element, deltaX, deltaY) {
|
|
26925
|
+
const canConsumeVertical = this.canElementConsumeVerticalWheel(element, deltaY);
|
|
26926
|
+
const canConsumeHorizontal = this.canElementConsumeHorizontalWheel(element, deltaX);
|
|
26927
|
+
// Keep wheel events inside the custom element whenever at least one
|
|
26928
|
+
// requested axis can still be consumed by its scroll container.
|
|
26929
|
+
return canConsumeVertical || canConsumeHorizontal;
|
|
26930
|
+
}
|
|
26931
|
+
static canElementConsumeVerticalWheel(element, deltaY) {
|
|
26932
|
+
if (deltaY === 0) {
|
|
26933
|
+
return false;
|
|
26934
|
+
}
|
|
26935
|
+
const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
|
|
26936
|
+
if (maxScrollTop === 0) {
|
|
26937
|
+
return false;
|
|
26938
|
+
}
|
|
26939
|
+
const epsilon = 1;
|
|
26940
|
+
const scrollingDown = deltaY > 0;
|
|
26941
|
+
const scrollingUp = deltaY < 0;
|
|
26942
|
+
if (scrollingDown) {
|
|
26943
|
+
return element.scrollTop < maxScrollTop - epsilon;
|
|
26944
|
+
}
|
|
26945
|
+
if (scrollingUp) {
|
|
26946
|
+
return element.scrollTop > epsilon;
|
|
26947
|
+
}
|
|
26948
|
+
return false;
|
|
26949
|
+
}
|
|
26950
|
+
static canElementConsumeHorizontalWheel(element, deltaX) {
|
|
26951
|
+
if (deltaX === 0) {
|
|
26952
|
+
return false;
|
|
26953
|
+
}
|
|
26954
|
+
const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth);
|
|
26955
|
+
if (maxScrollLeft === 0) {
|
|
26956
|
+
return false;
|
|
26957
|
+
}
|
|
26958
|
+
const epsilon = 1;
|
|
26959
|
+
const scrollingRight = deltaX > 0;
|
|
26960
|
+
const scrollingLeft = deltaX < 0;
|
|
26961
|
+
if (scrollingRight) {
|
|
26962
|
+
return element.scrollLeft < maxScrollLeft - epsilon;
|
|
26963
|
+
}
|
|
26964
|
+
if (scrollingLeft) {
|
|
26965
|
+
return element.scrollLeft > epsilon;
|
|
26966
|
+
}
|
|
26967
|
+
return false;
|
|
26968
|
+
}
|
|
26969
|
+
}
|
|
26970
|
+
|
|
26971
|
+
/**
|
|
26972
|
+
* Helper class for exporting Kritzel objects to SVG format.
|
|
26973
|
+
* Generates clean, standards-compliant SVG markup from canvas objects.
|
|
26974
|
+
*/
|
|
26975
|
+
class KritzelSvgExportHelper {
|
|
26976
|
+
/**
|
|
26977
|
+
* Calculates the combined bounding box that encompasses all objects.
|
|
26978
|
+
* Uses rotatedBoundingBox to account for object rotation.
|
|
26979
|
+
* @param objects - Array of objects to calculate bounds for
|
|
26980
|
+
* @returns Combined bounding box, or null if no objects provided
|
|
26981
|
+
*/
|
|
26982
|
+
static calculateCombinedBounds(objects) {
|
|
26983
|
+
if (!objects || objects.length === 0) {
|
|
26984
|
+
return null;
|
|
26985
|
+
}
|
|
26986
|
+
let minX = Infinity;
|
|
26987
|
+
let minY = Infinity;
|
|
26988
|
+
let maxX = -Infinity;
|
|
26989
|
+
let maxY = -Infinity;
|
|
26990
|
+
for (const obj of objects) {
|
|
26991
|
+
const bbox = obj.rotatedBoundingBox;
|
|
26992
|
+
minX = Math.min(minX, bbox.x);
|
|
26993
|
+
minY = Math.min(minY, bbox.y);
|
|
26994
|
+
maxX = Math.max(maxX, bbox.x + bbox.width);
|
|
26995
|
+
maxY = Math.max(maxY, bbox.y + bbox.height);
|
|
26996
|
+
}
|
|
26997
|
+
return {
|
|
26998
|
+
x: minX,
|
|
26999
|
+
y: minY,
|
|
27000
|
+
z: 1,
|
|
27001
|
+
width: maxX - minX,
|
|
27002
|
+
height: maxY - minY,
|
|
27003
|
+
};
|
|
27004
|
+
}
|
|
27005
|
+
/**
|
|
27006
|
+
* Generates a complete SVG document string from the given objects.
|
|
27007
|
+
* @param objects - Objects to export
|
|
27008
|
+
* @param options - Export options
|
|
27009
|
+
* @returns SVG document string
|
|
27010
|
+
*/
|
|
27011
|
+
static generateSvg(objects, options = {}) {
|
|
27012
|
+
const { theme, padding = 0, includeXmlDeclaration = true, rasterSafeForCanvas = false, imageDataUrls } = options;
|
|
27013
|
+
if (!objects || objects.length === 0) {
|
|
27014
|
+
return '';
|
|
27015
|
+
}
|
|
27016
|
+
// Flatten groups to get all objects including nested children
|
|
27017
|
+
const allObjects = this.flattenObjects(objects);
|
|
27018
|
+
const bounds = this.calculateCombinedBounds(allObjects);
|
|
27019
|
+
if (!bounds) {
|
|
27020
|
+
return '';
|
|
27021
|
+
}
|
|
27022
|
+
// Add padding to bounds
|
|
27023
|
+
const viewBoxX = bounds.x - padding;
|
|
27024
|
+
const viewBoxY = bounds.y - padding;
|
|
27025
|
+
const viewBoxWidth = bounds.width + padding * 2;
|
|
27026
|
+
const viewBoxHeight = bounds.height + padding * 2;
|
|
27027
|
+
// Collect all defs (markers, patterns, etc.)
|
|
27028
|
+
const defs = this.collectDefs(allObjects, theme);
|
|
27029
|
+
// Generate SVG elements for each object
|
|
27030
|
+
const elements = objects
|
|
27031
|
+
.sort((a, b) => a.zIndex - b.zIndex)
|
|
27032
|
+
.map(obj => this.objectToSvgElement(obj, theme, imageDataUrls, rasterSafeForCanvas))
|
|
27033
|
+
.filter(Boolean)
|
|
27034
|
+
.join('\n ');
|
|
27035
|
+
const xmlDecl = includeXmlDeclaration ? '<?xml version="1.0" encoding="UTF-8"?>\n' : '';
|
|
27036
|
+
const defsContent = defs.length > 0 ? `\n <defs>\n ${defs.join('\n ')}\n </defs>` : '';
|
|
27037
|
+
return `${xmlDecl}<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}" width="${viewBoxWidth}" height="${viewBoxHeight}">${defsContent}
|
|
27038
|
+
${elements}
|
|
27039
|
+
</svg>`;
|
|
27040
|
+
}
|
|
27041
|
+
/**
|
|
27042
|
+
* Flattens a list of objects, expanding groups into their children recursively.
|
|
27043
|
+
* @param objects - Objects to flatten
|
|
27044
|
+
* @returns Flat array of all objects including group children
|
|
27045
|
+
*/
|
|
27046
|
+
static flattenObjects(objects) {
|
|
27047
|
+
const result = [];
|
|
27048
|
+
for (const obj of objects) {
|
|
27049
|
+
if (KritzelClassHelper.isInstanceOf(obj, 'KritzelGroup')) {
|
|
27050
|
+
result.push(...this.flattenObjects(obj.children));
|
|
27051
|
+
}
|
|
27052
|
+
else {
|
|
27053
|
+
result.push(obj);
|
|
27054
|
+
}
|
|
27055
|
+
}
|
|
27056
|
+
return result;
|
|
27057
|
+
}
|
|
27058
|
+
/**
|
|
27059
|
+
* Resolves every image referenced by the given objects (including those
|
|
27060
|
+
* nested inside groups) to an inlined `data:` URL, keyed by object id.
|
|
27061
|
+
*
|
|
27062
|
+
* This is required for export flows that rasterize the SVG (e.g. PNG
|
|
27063
|
+
* export): an SVG loaded into an `<img>` element will NOT load external
|
|
27064
|
+
* or `blob:` resources, so a `<image href="blob:...">` renders as a
|
|
27065
|
+
* broken-image placeholder. Embedding the bytes as a base64 `data:` URL
|
|
27066
|
+
* makes the image self-contained and survive rasterization.
|
|
27067
|
+
*
|
|
27068
|
+
* Resolution is best-effort: images that fail to resolve are simply
|
|
27069
|
+
* omitted from the map, and the SVG falls back to whatever `href` the
|
|
27070
|
+
* synchronous path would have produced.
|
|
27071
|
+
* @param objects - Objects to inline images for
|
|
27072
|
+
* @param assetResolver - Resolver used to fetch asset bytes; optional
|
|
27073
|
+
* @returns Map of image object id -> inlined `data:` URL
|
|
27074
|
+
*/
|
|
27075
|
+
static async resolveImageDataUrls(objects, assetResolver) {
|
|
27076
|
+
const dataUrls = new Map();
|
|
27077
|
+
if (!objects || objects.length === 0) {
|
|
27078
|
+
return dataUrls;
|
|
27079
|
+
}
|
|
27080
|
+
const images = this.flattenObjects(objects).filter((obj) => KritzelClassHelper.isInstanceOf(obj, 'KritzelImage'));
|
|
27081
|
+
await Promise.all(images.map(async (image) => {
|
|
27082
|
+
// Legacy inline data URL is already embeddable as-is.
|
|
27083
|
+
if (image.src?.startsWith('data:')) {
|
|
27084
|
+
dataUrls.set(image.id, image.src);
|
|
27085
|
+
return;
|
|
27086
|
+
}
|
|
27087
|
+
// A resolvedSrc that is itself a data URL can be embedded directly.
|
|
27088
|
+
if (image.resolvedSrc?.startsWith('data:')) {
|
|
27089
|
+
dataUrls.set(image.id, image.resolvedSrc);
|
|
27090
|
+
return;
|
|
27091
|
+
}
|
|
27092
|
+
if (!image.assetId || !assetResolver) {
|
|
27093
|
+
return;
|
|
27094
|
+
}
|
|
27095
|
+
try {
|
|
27096
|
+
const blob = await assetResolver.fetchBlob(image.assetId);
|
|
27097
|
+
dataUrls.set(image.id, await this.blobToDataUrl(blob));
|
|
27098
|
+
}
|
|
27099
|
+
catch (err) {
|
|
27100
|
+
console.warn(`[KritzelSvgExportHelper] Failed to inline image asset ${image.assetId} for export:`, err);
|
|
27101
|
+
}
|
|
27102
|
+
}));
|
|
27103
|
+
return dataUrls;
|
|
27104
|
+
}
|
|
27105
|
+
/**
|
|
27106
|
+
* Reads a Blob into a base64 `data:` URL.
|
|
27107
|
+
* @param blob - The blob to encode
|
|
27108
|
+
* @returns Promise resolving to the data URL string
|
|
27109
|
+
*/
|
|
27110
|
+
static async blobToDataUrl(blob) {
|
|
27111
|
+
// Prefer FileReader in browser-like runtimes for efficiency.
|
|
27112
|
+
const hasFileReader = typeof FileReader !== 'undefined';
|
|
27113
|
+
if (hasFileReader) {
|
|
27114
|
+
try {
|
|
27115
|
+
return await new Promise((resolve, reject) => {
|
|
27116
|
+
const reader = new FileReader();
|
|
27117
|
+
reader.onload = () => {
|
|
27118
|
+
if (typeof reader.result === 'string') {
|
|
27119
|
+
resolve(reader.result);
|
|
27120
|
+
return;
|
|
27121
|
+
}
|
|
27122
|
+
reject(new Error('FileReader result is not a string'));
|
|
27123
|
+
};
|
|
27124
|
+
reader.onerror = () => reject(reader.error ?? new Error('Failed to read blob as data URL'));
|
|
27125
|
+
reader.readAsDataURL(blob);
|
|
27126
|
+
});
|
|
27127
|
+
}
|
|
27128
|
+
catch {
|
|
27129
|
+
// Fall through to the ArrayBuffer-based encoder.
|
|
27130
|
+
}
|
|
27131
|
+
}
|
|
27132
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
27133
|
+
const mimeType = blob.type || 'application/octet-stream';
|
|
27134
|
+
return `data:${mimeType};base64,${this.uint8ToBase64(bytes)}`;
|
|
27135
|
+
}
|
|
27136
|
+
/**
|
|
27137
|
+
* Encodes a byte array to base64 across browser and Node-like runtimes.
|
|
27138
|
+
*/
|
|
27139
|
+
static uint8ToBase64(bytes) {
|
|
27140
|
+
if (typeof Buffer !== 'undefined') {
|
|
27141
|
+
return Buffer.from(bytes).toString('base64');
|
|
27142
|
+
}
|
|
27143
|
+
let binary = '';
|
|
27144
|
+
const chunkSize = 0x8000;
|
|
27145
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
27146
|
+
const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length));
|
|
27147
|
+
binary += String.fromCharCode(...chunk);
|
|
27148
|
+
}
|
|
27149
|
+
if (typeof btoa === 'function') {
|
|
27150
|
+
return btoa(binary);
|
|
27151
|
+
}
|
|
27152
|
+
throw new Error('No base64 encoder available in this runtime');
|
|
27153
|
+
}
|
|
27154
|
+
/**
|
|
27155
|
+
* Collects SVG defs (markers, patterns, etc.) needed by the objects.
|
|
27156
|
+
* @param objects - Objects to collect defs for
|
|
27157
|
+
* @param theme - Theme for color resolution
|
|
27158
|
+
* @returns Array of def element strings
|
|
27159
|
+
*/
|
|
27160
|
+
static collectDefs(objects, theme) {
|
|
27161
|
+
const defs = [];
|
|
27162
|
+
for (const obj of objects) {
|
|
27163
|
+
if (KritzelClassHelper.isInstanceOf(obj, 'KritzelLine')) {
|
|
27164
|
+
if (obj.hasStartArrow) {
|
|
27165
|
+
defs.push(this.generateArrowMarker(obj, 'start', theme));
|
|
27166
|
+
}
|
|
27167
|
+
if (obj.hasEndArrow) {
|
|
27168
|
+
defs.push(this.generateArrowMarker(obj, 'end', theme));
|
|
27169
|
+
}
|
|
27170
|
+
}
|
|
27171
|
+
}
|
|
27172
|
+
return defs;
|
|
27173
|
+
}
|
|
27174
|
+
/**
|
|
27175
|
+
* Generates an SVG marker element for line arrows.
|
|
27176
|
+
* @param line - The line object
|
|
27177
|
+
* @param end - Which end of the line ('start' or 'end')
|
|
27178
|
+
* @param theme - Theme for color resolution
|
|
27179
|
+
* @returns SVG marker element string
|
|
27180
|
+
*/
|
|
27181
|
+
static generateArrowMarker(line, end, _theme) {
|
|
27182
|
+
const markerId = end === 'start' ? line.startMarkerId : line.endMarkerId;
|
|
27183
|
+
const size = line.getArrowSize(end);
|
|
27184
|
+
const fill = line.getArrowFill(end);
|
|
27185
|
+
const style = end === 'start' ? line.arrows?.start?.style : line.arrows?.end?.style;
|
|
27186
|
+
const path = line.getArrowPath(style);
|
|
27187
|
+
const orient = end === 'start' ? 'auto-start-reverse' : 'auto';
|
|
27188
|
+
const refX = 0;
|
|
27189
|
+
const refY = size / 2;
|
|
27190
|
+
return `<marker id="${markerId}" markerWidth="${size}" markerHeight="${size}" refX="${refX}" refY="${refY}" orient="${orient}" markerUnits="userSpaceOnUse">
|
|
27191
|
+
<path d="${path}" fill="${fill}" transform="scale(${size / 10})"/>
|
|
27192
|
+
</marker>`;
|
|
27193
|
+
}
|
|
27194
|
+
/**
|
|
27195
|
+
* Converts a Kritzel object to its SVG element representation.
|
|
27196
|
+
* @param object - The object to convert
|
|
27197
|
+
* @param theme - Theme for color resolution
|
|
27198
|
+
* @param imageDataUrls - Optional map of image id -> inlined data URL
|
|
27199
|
+
* @returns SVG element string, or empty string if object type is not supported
|
|
27200
|
+
*/
|
|
27201
|
+
static objectToSvgElement(object, theme, imageDataUrls, rasterSafeForCanvas = false) {
|
|
27202
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelPath')) {
|
|
27203
|
+
return this.pathToSvg(object, theme);
|
|
27204
|
+
}
|
|
27205
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelLine')) {
|
|
27206
|
+
return this.lineToSvg(object, theme);
|
|
27207
|
+
}
|
|
27208
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelShape')) {
|
|
27209
|
+
return this.shapeToSvg(object, theme);
|
|
27210
|
+
}
|
|
27211
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelText')) {
|
|
27212
|
+
return this.textToSvg(object, theme, rasterSafeForCanvas);
|
|
27213
|
+
}
|
|
27214
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelImage')) {
|
|
27215
|
+
return this.imageToSvg(object, imageDataUrls);
|
|
27216
|
+
}
|
|
27217
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelCustomElement')) {
|
|
27218
|
+
return this.customElementToSvg(object, rasterSafeForCanvas);
|
|
27219
|
+
}
|
|
27220
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelGroup')) {
|
|
27221
|
+
return this.groupToSvg(object, theme, imageDataUrls, rasterSafeForCanvas);
|
|
27222
|
+
}
|
|
27223
|
+
return '';
|
|
27224
|
+
}
|
|
27225
|
+
/**
|
|
27226
|
+
* Converts a KritzelPath to SVG.
|
|
27227
|
+
* @param path - The path object
|
|
27228
|
+
* @param theme - Theme for color resolution
|
|
27229
|
+
* @returns SVG element string
|
|
27230
|
+
*/
|
|
27231
|
+
static pathToSvg(path, theme) {
|
|
27232
|
+
const transform = this.buildTransform(path);
|
|
27233
|
+
const fill = KritzelColorHelper.resolveThemeColor(path.fill, theme);
|
|
27234
|
+
const stroke = KritzelColorHelper.resolveThemeColor(path.stroke, theme);
|
|
27235
|
+
const opacity = path.opacity !== 1 ? ` opacity="${path.opacity}"` : '';
|
|
27236
|
+
const scale = this.getObjectScale(path);
|
|
27237
|
+
// Path needs to be wrapped in a g with translation since path.d uses local coordinates
|
|
27238
|
+
return `<g transform="${transform}"${opacity}>
|
|
27239
|
+
<svg viewBox="${path.viewBox}" width="${path.totalWidth / scale}" height="${path.totalHeight / scale}" overflow="visible">
|
|
27240
|
+
<path d="${path.d}" fill="${fill || 'none'}" stroke="${stroke || 'none'}"${path.strokeWidth ? ` stroke-width="${path.strokeWidth}"` : ''}/>
|
|
27241
|
+
</svg>
|
|
27242
|
+
</g>`;
|
|
27243
|
+
}
|
|
27244
|
+
/**
|
|
27245
|
+
* Converts a KritzelLine to SVG.
|
|
27246
|
+
* @param line - The line object
|
|
27247
|
+
* @param theme - Theme for color resolution
|
|
27248
|
+
* @returns SVG element string
|
|
27249
|
+
*/
|
|
27250
|
+
static lineToSvg(line, theme) {
|
|
27251
|
+
const transform = this.buildTransform(line);
|
|
27252
|
+
const stroke = KritzelColorHelper.resolveThemeColor(line.stroke, theme);
|
|
27253
|
+
const opacity = line.opacity !== 1 ? ` opacity="${line.opacity}"` : '';
|
|
27254
|
+
const scale = this.getObjectScale(line);
|
|
27255
|
+
const markerStart = line.hasStartArrow ? ` marker-start="url(#${line.startMarkerId})"` : '';
|
|
27256
|
+
const markerEnd = line.hasEndArrow ? ` marker-end="url(#${line.endMarkerId})"` : '';
|
|
27257
|
+
return `<g transform="${transform}"${opacity}>
|
|
27258
|
+
<svg viewBox="${line.viewBox}" width="${line.totalWidth / scale}" height="${line.totalHeight / scale}" overflow="visible">
|
|
27259
|
+
<path d="${line.d}" fill="none" stroke="${stroke}" stroke-width="${line.strokeWidth}" stroke-linecap="round"${markerStart}${markerEnd}/>
|
|
27260
|
+
</svg>
|
|
27261
|
+
</g>`;
|
|
27262
|
+
}
|
|
27263
|
+
/**
|
|
27264
|
+
* Converts a KritzelShape to SVG.
|
|
27265
|
+
* @param shape - The shape object
|
|
27266
|
+
* @param theme - Theme for color resolution
|
|
27267
|
+
* @returns SVG element string
|
|
27268
|
+
*/
|
|
27269
|
+
static shapeToSvg(shape, theme) {
|
|
27270
|
+
const transform = this.buildTransform(shape);
|
|
27271
|
+
const fill = KritzelColorHelper.resolveThemeColor(shape.fillColor, theme);
|
|
27272
|
+
const stroke = KritzelColorHelper.resolveThemeColor(shape.strokeColor, theme);
|
|
27273
|
+
const opacity = shape.opacity !== 1 ? ` opacity="${shape.opacity}"` : '';
|
|
27274
|
+
const scale = this.getObjectScale(shape);
|
|
27275
|
+
// Get the SVG path for the shape
|
|
27276
|
+
const pathD = shape.getSvgPath();
|
|
27277
|
+
// Handle text content if present
|
|
27278
|
+
let textContent = '';
|
|
27279
|
+
if (shape.editor && !shape.editor.state.doc.textContent.trim()) ;
|
|
27280
|
+
else if (shape.content) {
|
|
27281
|
+
const textHtml = this.prosemirrorToHtml(shape.content, shape, theme);
|
|
27282
|
+
if (textHtml) {
|
|
27283
|
+
const fontColor = KritzelColorHelper.resolveThemeColor(shape.fontColor, theme);
|
|
27284
|
+
textContent = `
|
|
27285
|
+
<foreignObject x="${shape.x}" y="${shape.y}" width="${shape.width}" height="${shape.height}">
|
|
27286
|
+
<div xmlns="http://www.w3.org/1999/xhtml" style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; text-align: center; font-family: ${shape.fontFamily}; font-size: ${shape.fontSize}px; color: ${fontColor}; overflow: hidden;">
|
|
27287
|
+
${textHtml}
|
|
27288
|
+
</div>
|
|
27289
|
+
</foreignObject>`;
|
|
27290
|
+
}
|
|
27291
|
+
}
|
|
27292
|
+
return `<g transform="${transform}"${opacity}>
|
|
27293
|
+
<svg viewBox="${shape.viewBox}" width="${shape.totalWidth / scale}" height="${shape.totalHeight / scale}" overflow="visible" preserveAspectRatio="none">
|
|
27294
|
+
<path d="${pathD}" fill="${fill || 'transparent'}" stroke="${stroke}" stroke-width="${shape.strokeWidth}"/>${textContent}
|
|
27295
|
+
</svg>
|
|
27296
|
+
</g>`;
|
|
27297
|
+
}
|
|
27298
|
+
/**
|
|
27299
|
+
* Converts a KritzelText to SVG using foreignObject for rich text support.
|
|
27300
|
+
* @param text - The text object
|
|
27301
|
+
* @param theme - Theme for color resolution
|
|
27302
|
+
* @returns SVG element string
|
|
27303
|
+
*/
|
|
27304
|
+
static textToSvg(text, theme, rasterSafeForCanvas = false) {
|
|
27305
|
+
if (rasterSafeForCanvas) {
|
|
27306
|
+
return this.textToSvgRasterSafe(text, theme);
|
|
27307
|
+
}
|
|
27308
|
+
const transform = this.buildTransform(text);
|
|
27309
|
+
const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
|
|
27310
|
+
const fontColor = KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
|
|
27311
|
+
const bgColor = KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
|
|
27312
|
+
const scale = this.getObjectScale(text);
|
|
27313
|
+
// Convert ProseMirror content to HTML
|
|
27314
|
+
const htmlContent = this.prosemirrorToHtml(text.content, text, theme);
|
|
27315
|
+
// Calculate dimensions in SVG world units (world units = CSS px in a 1:1 SVG).
|
|
27316
|
+
const width = text.totalWidth / scale;
|
|
27317
|
+
const height = text.totalHeight / scale;
|
|
27318
|
+
// Convert the font size from pt to SVG world units (px equivalent).
|
|
27319
|
+
// text.fontSize is stored in CSS pt; 1 pt = 4/3 px at 96 dpi.
|
|
27320
|
+
// We also bake in scaleFactor (user resize) and divide by the object scale so
|
|
27321
|
+
// that the rendered font aligns with the world-unit dimensions above.
|
|
27322
|
+
// The CSS transform: scale(scaleFactor) is removed from the div because the
|
|
27323
|
+
// adjusted font-size already accounts for it – keeping both would double-scale
|
|
27324
|
+
// the content and overflow the foreignObject.
|
|
27325
|
+
const svgFontSizePx = (text.fontSize * (4 / 3) * (text.scaleFactor || 1) / scale).toFixed(4);
|
|
27326
|
+
return `<g transform="${transform}"${opacity}>
|
|
27327
|
+
<foreignObject x="0" y="0" width="${width}" height="${height}" overflow="visible">
|
|
27328
|
+
<div xmlns="http://www.w3.org/1999/xhtml" style="font-family: ${text.fontFamily}; font-size: ${svgFontSizePx}px; color: ${fontColor}; background-color: ${bgColor || 'transparent'}; white-space: pre-wrap; word-wrap: break-word; width: 100%; margin: 0; padding: 0; box-sizing: border-box;">${htmlContent.trim()}</div>
|
|
27329
|
+
</foreignObject>
|
|
27330
|
+
</g>`;
|
|
27331
|
+
}
|
|
27332
|
+
/**
|
|
27333
|
+
* Converts a KritzelText to SVG using pure SVG text primitives.
|
|
27334
|
+
* This path avoids `<foreignObject>`, which taints canvases when the
|
|
27335
|
+
* generated SVG is rasterized through `<img>` and `drawImage`.
|
|
27336
|
+
*/
|
|
27337
|
+
static textToSvgRasterSafe(text, theme) {
|
|
27338
|
+
const transform = this.buildTransform(text);
|
|
27339
|
+
const opacity = text.opacity !== 1 ? ` opacity="${text.opacity}"` : '';
|
|
27340
|
+
const fontColor = KritzelColorHelper.resolveThemeColor(text.fontColor, theme);
|
|
27341
|
+
const bgColor = KritzelColorHelper.resolveThemeColor(text.backgroundColor, theme);
|
|
27342
|
+
const scale = this.getObjectScale(text);
|
|
27343
|
+
const width = text.totalWidth / scale;
|
|
27344
|
+
const height = text.totalHeight / scale;
|
|
27345
|
+
const lines = this.prosemirrorToPlainTextLines(text.content);
|
|
27346
|
+
const resolvedLines = lines.length > 0 ? lines : [''];
|
|
27347
|
+
// Convert the font size from pt to SVG world units (px equivalent).
|
|
27348
|
+
// text.fontSize is stored in CSS pt; 1 pt = 4/3 px at 96 dpi.
|
|
27349
|
+
// We also bake in scaleFactor (user resize) and divide by the object scale so
|
|
27350
|
+
// that the rendered lines align with the world-unit bounding box dimensions above.
|
|
27351
|
+
const resolvedFontSizePx = Math.max(1, text.fontSize * (4 / 3) * (text.scaleFactor || 1) / scale);
|
|
27352
|
+
const lineHeight = resolvedFontSizePx * 1.2;
|
|
27353
|
+
const startY = resolvedFontSizePx;
|
|
27354
|
+
const bgRect = bgColor && bgColor !== 'transparent'
|
|
27355
|
+
? `<rect x="0" y="0" width="${width}" height="${height}" fill="${bgColor}"/>`
|
|
27356
|
+
: '';
|
|
27357
|
+
const tspans = resolvedLines
|
|
27358
|
+
.map((line, index) => {
|
|
27359
|
+
const y = startY + index * lineHeight;
|
|
27360
|
+
return `<tspan x="0" y="${y}" xml:space="preserve">${this.escapeHtml(line)}</tspan>`;
|
|
27361
|
+
})
|
|
27362
|
+
.join('');
|
|
27363
|
+
return `<g transform="${transform}"${opacity}>
|
|
27364
|
+
${bgRect}
|
|
27365
|
+
<text font-family="${this.escapeHtml(text.fontFamily)}" font-size="${resolvedFontSizePx}" fill="${fontColor}">${tspans}</text>
|
|
27366
|
+
</g>`;
|
|
27424
27367
|
}
|
|
27425
|
-
|
|
27426
|
-
|
|
27427
|
-
|
|
27428
|
-
|
|
27429
|
-
|
|
27368
|
+
/**
|
|
27369
|
+
* Converts a KritzelImage to SVG.
|
|
27370
|
+
*
|
|
27371
|
+
* Prefers an inlined `data:` URL from `imageDataUrls` (keyed by object
|
|
27372
|
+
* id) when provided, so the embedded bytes survive rasterization to
|
|
27373
|
+
* PNG. Falls back to `resolvedSrc` (populated by the renderer via the
|
|
27374
|
+
* asset resolver) and finally the legacy inline `src` field for
|
|
27375
|
+
* documents persisted before the asset layer existed.
|
|
27376
|
+
*
|
|
27377
|
+
* Note: a `blob:` `resolvedSrc` renders correctly on-canvas but becomes
|
|
27378
|
+
* a broken-image placeholder once the SVG is loaded into an `<img>` and
|
|
27379
|
+
* drawn to a canvas. Callers that rasterize the SVG must pass
|
|
27380
|
+
* `imageDataUrls` (see {@link KritzelSvgExportHelper.resolveImageDataUrls}).
|
|
27381
|
+
* @param image - The image object
|
|
27382
|
+
* @param imageDataUrls - Optional map of image id -> inlined data URL
|
|
27383
|
+
* @returns SVG element string
|
|
27384
|
+
*/
|
|
27385
|
+
static imageToSvg(image, imageDataUrls) {
|
|
27386
|
+
const transform = this.buildTransform(image);
|
|
27387
|
+
const opacity = image.opacity !== 1 ? ` opacity="${image.opacity}"` : '';
|
|
27388
|
+
const scale = this.getObjectScale(image);
|
|
27389
|
+
// Calculate dimensions
|
|
27390
|
+
const width = image.totalWidth / scale;
|
|
27391
|
+
const height = image.totalHeight / scale;
|
|
27392
|
+
const href = imageDataUrls?.get(image.id) || image.resolvedSrc || image.src || '';
|
|
27393
|
+
return `<g transform="${transform}"${opacity}>
|
|
27394
|
+
<image href="${href}" x="0" y="0" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet"/>
|
|
27395
|
+
</g>`;
|
|
27430
27396
|
}
|
|
27431
|
-
|
|
27432
|
-
|
|
27433
|
-
|
|
27434
|
-
|
|
27435
|
-
|
|
27436
|
-
|
|
27437
|
-
|
|
27438
|
-
|
|
27439
|
-
|
|
27440
|
-
|
|
27441
|
-
|
|
27442
|
-
|
|
27443
|
-
|
|
27444
|
-
|
|
27445
|
-
|
|
27446
|
-
|
|
27447
|
-
|
|
27448
|
-
|
|
27449
|
-
|
|
27450
|
-
|
|
27397
|
+
/**
|
|
27398
|
+
* Converts a KritzelCustomElement to SVG.
|
|
27399
|
+
* SVG mode: embeds HTML in foreignObject (best-effort).
|
|
27400
|
+
* PNG/raster mode: renders placeholder rect with warning.
|
|
27401
|
+
* @param obj - The custom element object
|
|
27402
|
+
* @param rasterSafeForCanvas - If true, render placeholder (for PNG export)
|
|
27403
|
+
* @returns SVG element string
|
|
27404
|
+
*/
|
|
27405
|
+
static customElementToSvg(obj, rasterSafeForCanvas = false) {
|
|
27406
|
+
const transform = this.buildTransform(obj);
|
|
27407
|
+
const opacity = obj.opacity !== 1 ? ` opacity="${obj.opacity}"` : '';
|
|
27408
|
+
const scale = this.getObjectScale(obj);
|
|
27409
|
+
const width = obj.totalWidth / scale;
|
|
27410
|
+
const height = obj.totalHeight / scale;
|
|
27411
|
+
if (rasterSafeForCanvas) {
|
|
27412
|
+
// PNG export: render placeholder rect (foreignObject doesn't rasterize to PNG well)
|
|
27413
|
+
console.warn('[KritzelCustomElement SVG Export] Custom elements do not serialize well to PNG. Use SVG export or screenshot instead.');
|
|
27414
|
+
return `<g transform="${transform}"${opacity}>
|
|
27415
|
+
<rect x="0" y="0" width="${width}" height="${height}" fill="#f0f0f0" stroke="#999" stroke-width="1" />
|
|
27416
|
+
<text x="${width / 2}" y="${height / 2}" text-anchor="middle" dominant-baseline="middle" font-size="12" fill="#999">[Custom Element]</text>
|
|
27417
|
+
</g>`;
|
|
27451
27418
|
}
|
|
27452
|
-
|
|
27419
|
+
// SVG export: use foreignObject to embed HTML
|
|
27420
|
+
const html = obj.element?.outerHTML || '<div style="border:1px dashed #ccc">Empty</div>';
|
|
27421
|
+
return `<g transform="${transform}"${opacity}>
|
|
27422
|
+
<foreignObject x="0" y="0" width="${width}" height="${height}">
|
|
27423
|
+
<div xmlns="http://www.w3.org/1999/xhtml" style="width:100%;height:100%;overflow:hidden;">
|
|
27424
|
+
${html}
|
|
27425
|
+
</div>
|
|
27426
|
+
</foreignObject>
|
|
27427
|
+
</g>`;
|
|
27453
27428
|
}
|
|
27454
|
-
|
|
27455
|
-
|
|
27456
|
-
|
|
27457
|
-
|
|
27458
|
-
|
|
27459
|
-
|
|
27460
|
-
|
|
27461
|
-
|
|
27462
|
-
const
|
|
27463
|
-
|
|
27464
|
-
|
|
27465
|
-
|
|
27466
|
-
|
|
27467
|
-
|
|
27468
|
-
|
|
27469
|
-
continue;
|
|
27470
|
-
}
|
|
27471
|
-
for (const entry of path.slice(0, rootIndex + 1)) {
|
|
27472
|
-
if (!(entry instanceof HTMLElement)) {
|
|
27473
|
-
continue;
|
|
27474
|
-
}
|
|
27475
|
-
if (this.isScrollableElement(entry)) {
|
|
27476
|
-
return entry;
|
|
27477
|
-
}
|
|
27478
|
-
}
|
|
27479
|
-
}
|
|
27480
|
-
// Fallback for synthetic/custom wheel events where composedPath is absent
|
|
27481
|
-
// or does not include the custom-element subtree.
|
|
27482
|
-
for (const root of potentialRoots) {
|
|
27483
|
-
if (this.isScrollableElement(root)) {
|
|
27484
|
-
return root;
|
|
27485
|
-
}
|
|
27486
|
-
const firstChild = root.firstElementChild;
|
|
27487
|
-
if (firstChild instanceof HTMLElement && this.isScrollableElement(firstChild)) {
|
|
27488
|
-
return firstChild;
|
|
27489
|
-
}
|
|
27429
|
+
/**
|
|
27430
|
+
* Converts a KritzelGroup to SVG by recursively converting children.
|
|
27431
|
+
* @param group - The group object
|
|
27432
|
+
* @param theme - Theme for color resolution
|
|
27433
|
+
* @param imageDataUrls - Optional map of image id -> inlined data URL
|
|
27434
|
+
* @returns SVG element string
|
|
27435
|
+
*/
|
|
27436
|
+
static groupToSvg(group, theme, imageDataUrls, rasterSafeForCanvas = false) {
|
|
27437
|
+
const children = group.children
|
|
27438
|
+
.sort((a, b) => a.zIndex - b.zIndex)
|
|
27439
|
+
.map(child => this.objectToSvgElement(child, theme, imageDataUrls, rasterSafeForCanvas))
|
|
27440
|
+
.filter(Boolean)
|
|
27441
|
+
.join('\n ');
|
|
27442
|
+
if (!children) {
|
|
27443
|
+
return '';
|
|
27490
27444
|
}
|
|
27491
|
-
|
|
27445
|
+
const opacity = group.opacity !== 1 ? ` opacity="${group.opacity}"` : '';
|
|
27446
|
+
return `<g${opacity}>
|
|
27447
|
+
${children}
|
|
27448
|
+
</g>`;
|
|
27492
27449
|
}
|
|
27493
|
-
|
|
27494
|
-
|
|
27495
|
-
|
|
27496
|
-
|
|
27497
|
-
|
|
27450
|
+
/**
|
|
27451
|
+
* Builds a transform attribute string for an object.
|
|
27452
|
+
* Combines translation and rotation.
|
|
27453
|
+
* @param object - The object to build transform for
|
|
27454
|
+
* @returns Transform attribute value string
|
|
27455
|
+
*/
|
|
27456
|
+
static buildTransform(object) {
|
|
27457
|
+
const transforms = [];
|
|
27458
|
+
const scale = this.getObjectScale(object);
|
|
27459
|
+
// Translation
|
|
27460
|
+
transforms.push(`translate(${object.translateX}, ${object.translateY})`);
|
|
27461
|
+
// Rotation around center
|
|
27462
|
+
if (object.rotation !== 0) {
|
|
27463
|
+
const centerX = object.totalWidth / 2 / scale;
|
|
27464
|
+
const centerY = object.totalHeight / 2 / scale;
|
|
27465
|
+
const degrees = object.rotation * (180 / Math.PI);
|
|
27466
|
+
transforms.push(`rotate(${degrees}, ${centerX}, ${centerY})`);
|
|
27498
27467
|
}
|
|
27499
|
-
|
|
27500
|
-
const overflowX = computedStyle.overflowX;
|
|
27501
|
-
const overflowY = computedStyle.overflowY;
|
|
27502
|
-
const allowsVerticalScroll = overflowY !== 'visible' && overflowY !== 'clip';
|
|
27503
|
-
const allowsHorizontalScroll = overflowX !== 'visible' && overflowX !== 'clip';
|
|
27504
|
-
return (canScrollVertically && allowsVerticalScroll) || (canScrollHorizontally && allowsHorizontalScroll);
|
|
27468
|
+
return transforms.join(' ');
|
|
27505
27469
|
}
|
|
27506
|
-
|
|
27507
|
-
|
|
27508
|
-
|
|
27509
|
-
|
|
27510
|
-
|
|
27511
|
-
return
|
|
27470
|
+
/**
|
|
27471
|
+
* Returns a safe non-zero scale for export geometry calculations.
|
|
27472
|
+
*/
|
|
27473
|
+
static getObjectScale(object) {
|
|
27474
|
+
const scale = object.scale ?? 1;
|
|
27475
|
+
return scale === 0 ? 1 : scale;
|
|
27512
27476
|
}
|
|
27513
|
-
|
|
27514
|
-
|
|
27515
|
-
|
|
27477
|
+
/**
|
|
27478
|
+
* Converts ProseMirror JSON content to HTML string.
|
|
27479
|
+
* @param content - ProseMirror document JSON
|
|
27480
|
+
* @param context - The text or shape object for styling context
|
|
27481
|
+
* @param theme - Theme for color resolution
|
|
27482
|
+
* @returns HTML string
|
|
27483
|
+
*/
|
|
27484
|
+
static prosemirrorToHtml(content, _context, theme) {
|
|
27485
|
+
if (!content || !content.content) {
|
|
27486
|
+
return '';
|
|
27516
27487
|
}
|
|
27517
|
-
|
|
27518
|
-
|
|
27519
|
-
|
|
27488
|
+
return content.content
|
|
27489
|
+
.map((node) => this.nodeToHtml(node, theme))
|
|
27490
|
+
.join('');
|
|
27491
|
+
}
|
|
27492
|
+
/**
|
|
27493
|
+
* Converts a ProseMirror node to HTML.
|
|
27494
|
+
* @param node - ProseMirror node JSON
|
|
27495
|
+
* @param theme - Theme for color resolution
|
|
27496
|
+
* @returns HTML string
|
|
27497
|
+
*/
|
|
27498
|
+
static nodeToHtml(node, theme) {
|
|
27499
|
+
if (!node)
|
|
27500
|
+
return '';
|
|
27501
|
+
switch (node.type) {
|
|
27502
|
+
case 'paragraph':
|
|
27503
|
+
const pContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
27504
|
+
return `<p style="margin: 0;">${pContent || '<br/>'}</p>`;
|
|
27505
|
+
case 'text':
|
|
27506
|
+
let text = this.escapeHtml(node.text || '');
|
|
27507
|
+
if (node.marks) {
|
|
27508
|
+
for (const mark of node.marks) {
|
|
27509
|
+
text = this.applyMark(text, mark, theme);
|
|
27510
|
+
}
|
|
27511
|
+
}
|
|
27512
|
+
return text;
|
|
27513
|
+
case 'hard_break':
|
|
27514
|
+
return '<br/>';
|
|
27515
|
+
case 'bullet_list':
|
|
27516
|
+
const blContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
27517
|
+
return `<ul style="margin: 0; padding-left: 1.5em;">${blContent}</ul>`;
|
|
27518
|
+
case 'ordered_list':
|
|
27519
|
+
const olContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
27520
|
+
return `<ol style="margin: 0; padding-left: 1.5em;">${olContent}</ol>`;
|
|
27521
|
+
case 'list_item':
|
|
27522
|
+
const liContent = node.content ? node.content.map((n) => this.nodeToHtml(n, theme)).join('') : '';
|
|
27523
|
+
return `<li>${liContent}</li>`;
|
|
27524
|
+
default:
|
|
27525
|
+
// For unknown node types, try to render content
|
|
27526
|
+
if (node.content) {
|
|
27527
|
+
return node.content.map((n) => this.nodeToHtml(n, theme)).join('');
|
|
27528
|
+
}
|
|
27529
|
+
return '';
|
|
27520
27530
|
}
|
|
27521
|
-
|
|
27522
|
-
|
|
27523
|
-
|
|
27524
|
-
|
|
27525
|
-
|
|
27531
|
+
}
|
|
27532
|
+
/**
|
|
27533
|
+
* Converts ProseMirror JSON content to plain-text lines.
|
|
27534
|
+
* Used by raster-safe SVG export, which cannot depend on HTML/foreignObject.
|
|
27535
|
+
*/
|
|
27536
|
+
static prosemirrorToPlainTextLines(content) {
|
|
27537
|
+
if (!content || !Array.isArray(content.content)) {
|
|
27538
|
+
return [];
|
|
27526
27539
|
}
|
|
27527
|
-
|
|
27528
|
-
|
|
27540
|
+
const plainText = content.content
|
|
27541
|
+
.map((node) => this.nodeToPlainText(node))
|
|
27542
|
+
.join('')
|
|
27543
|
+
.replace(/\r\n/g, '\n')
|
|
27544
|
+
.replace(/\n+$/, '');
|
|
27545
|
+
if (!plainText) {
|
|
27546
|
+
return [];
|
|
27529
27547
|
}
|
|
27530
|
-
return
|
|
27548
|
+
return plainText.split('\n');
|
|
27531
27549
|
}
|
|
27532
|
-
|
|
27533
|
-
|
|
27534
|
-
|
|
27535
|
-
|
|
27536
|
-
|
|
27537
|
-
|
|
27538
|
-
return false;
|
|
27550
|
+
/**
|
|
27551
|
+
* Converts a ProseMirror node tree into plain text.
|
|
27552
|
+
*/
|
|
27553
|
+
static nodeToPlainText(node) {
|
|
27554
|
+
if (!node) {
|
|
27555
|
+
return '';
|
|
27539
27556
|
}
|
|
27540
|
-
|
|
27541
|
-
|
|
27542
|
-
|
|
27543
|
-
|
|
27544
|
-
|
|
27557
|
+
switch (node.type) {
|
|
27558
|
+
case 'text':
|
|
27559
|
+
return node.text || '';
|
|
27560
|
+
case 'hard_break':
|
|
27561
|
+
return '\n';
|
|
27562
|
+
case 'paragraph': {
|
|
27563
|
+
const paragraph = Array.isArray(node.content)
|
|
27564
|
+
? node.content.map((child) => this.nodeToPlainText(child)).join('')
|
|
27565
|
+
: '';
|
|
27566
|
+
return `${paragraph}\n`;
|
|
27567
|
+
}
|
|
27568
|
+
case 'list_item': {
|
|
27569
|
+
const item = Array.isArray(node.content)
|
|
27570
|
+
? node.content.map((child) => this.nodeToPlainText(child)).join('')
|
|
27571
|
+
: '';
|
|
27572
|
+
return `- ${item.trim()}\n`;
|
|
27573
|
+
}
|
|
27574
|
+
default:
|
|
27575
|
+
return Array.isArray(node.content)
|
|
27576
|
+
? node.content.map((child) => this.nodeToPlainText(child)).join('')
|
|
27577
|
+
: '';
|
|
27545
27578
|
}
|
|
27546
|
-
|
|
27547
|
-
|
|
27579
|
+
}
|
|
27580
|
+
/**
|
|
27581
|
+
* Applies a ProseMirror mark to text.
|
|
27582
|
+
* @param text - The text to wrap
|
|
27583
|
+
* @param mark - The mark to apply
|
|
27584
|
+
* @param theme - Theme for color resolution
|
|
27585
|
+
* @returns Wrapped HTML string
|
|
27586
|
+
*/
|
|
27587
|
+
static applyMark(text, mark, _theme) {
|
|
27588
|
+
switch (mark.type) {
|
|
27589
|
+
case 'bold':
|
|
27590
|
+
case 'strong':
|
|
27591
|
+
return `<strong>${text}</strong>`;
|
|
27592
|
+
case 'italic':
|
|
27593
|
+
case 'em':
|
|
27594
|
+
return `<em>${text}</em>`;
|
|
27595
|
+
case 'underline':
|
|
27596
|
+
return `<u>${text}</u>`;
|
|
27597
|
+
case 'strike':
|
|
27598
|
+
return `<s>${text}</s>`;
|
|
27599
|
+
case 'code':
|
|
27600
|
+
return `<code>${text}</code>`;
|
|
27601
|
+
case 'link':
|
|
27602
|
+
const href = mark.attrs?.href || '#';
|
|
27603
|
+
return `<a href="${this.escapeHtml(href)}">${text}</a>`;
|
|
27604
|
+
default:
|
|
27605
|
+
return text;
|
|
27548
27606
|
}
|
|
27549
|
-
|
|
27607
|
+
}
|
|
27608
|
+
/**
|
|
27609
|
+
* Escapes HTML special characters.
|
|
27610
|
+
* @param str - String to escape
|
|
27611
|
+
* @returns Escaped string
|
|
27612
|
+
*/
|
|
27613
|
+
static escapeHtml(str) {
|
|
27614
|
+
return str
|
|
27615
|
+
.replace(/&/g, '&')
|
|
27616
|
+
.replace(/</g, '<')
|
|
27617
|
+
.replace(/>/g, '>')
|
|
27618
|
+
.replace(/"/g, '"')
|
|
27619
|
+
.replace(/'/g, ''');
|
|
27550
27620
|
}
|
|
27551
27621
|
}
|
|
27552
27622
|
|
|
@@ -28743,10 +28813,7 @@ const KritzelEngine = class {
|
|
|
28743
28813
|
console.error('Failed to export viewport as PNG: screenshot could not be generated');
|
|
28744
28814
|
return;
|
|
28745
28815
|
}
|
|
28746
|
-
|
|
28747
|
-
link.download = filename;
|
|
28748
|
-
link.href = dataUrl;
|
|
28749
|
-
link.click();
|
|
28816
|
+
this.triggerDataUrlDownload(dataUrl, filename);
|
|
28750
28817
|
}
|
|
28751
28818
|
catch (error) {
|
|
28752
28819
|
console.error('Failed to export viewport as PNG:', error);
|
|
@@ -28768,129 +28835,57 @@ const KritzelEngine = class {
|
|
|
28768
28835
|
console.error('Failed to export viewport as SVG: screenshot could not be generated');
|
|
28769
28836
|
return;
|
|
28770
28837
|
}
|
|
28771
|
-
|
|
28772
|
-
link.download = filename;
|
|
28773
|
-
link.href = dataUrl;
|
|
28774
|
-
link.click();
|
|
28838
|
+
this.triggerDataUrlDownload(dataUrl, filename);
|
|
28775
28839
|
}
|
|
28776
28840
|
catch (error) {
|
|
28777
28841
|
console.error('Failed to export viewport as SVG:', error);
|
|
28778
28842
|
}
|
|
28779
28843
|
}
|
|
28780
28844
|
/**
|
|
28781
|
-
*
|
|
28782
|
-
*
|
|
28783
|
-
* @param options - Optional export settings (theme, padding, raster-safe mode for canvas export).
|
|
28784
|
-
* @returns SVG markup string, or null if no objects are selected.
|
|
28845
|
+
* Returns export formats supported by the currently selected object.
|
|
28846
|
+
* Only returns formats when exactly one object is selected.
|
|
28785
28847
|
*/
|
|
28786
|
-
async
|
|
28787
|
-
const
|
|
28788
|
-
if (!
|
|
28789
|
-
return
|
|
28848
|
+
async getSelectedObjectSupportedExportFormats() {
|
|
28849
|
+
const selected = this.getSingleSelectedExportableObject();
|
|
28850
|
+
if (!selected) {
|
|
28851
|
+
return [];
|
|
28790
28852
|
}
|
|
28791
|
-
|
|
28792
|
-
// Inline image bytes as data URLs so they survive both standalone SVG
|
|
28793
|
-
// files and rasterization to PNG (a `blob:` href is not loaded when an
|
|
28794
|
-
// SVG is drawn into a canvas, which otherwise yields a broken image).
|
|
28795
|
-
const imageDataUrls = await KritzelSvgExportHelper.resolveImageDataUrls(selectedObjects, this.core.assetResolver);
|
|
28796
|
-
return KritzelSvgExportHelper.generateSvg(selectedObjects, {
|
|
28797
|
-
theme,
|
|
28798
|
-
padding: options?.padding ?? 0,
|
|
28799
|
-
includeXmlDeclaration: true,
|
|
28800
|
-
rasterSafeForCanvas: options?.rasterSafeForCanvas ?? false,
|
|
28801
|
-
imageDataUrls,
|
|
28802
|
-
});
|
|
28853
|
+
return selected.getSupportedExportFormats();
|
|
28803
28854
|
}
|
|
28804
28855
|
/**
|
|
28805
|
-
*
|
|
28806
|
-
*
|
|
28807
|
-
* @param options - Optional export settings (theme, padding).
|
|
28856
|
+
* Checks whether the currently selected object supports a specific export format.
|
|
28857
|
+
* Returns false when none or multiple objects are selected.
|
|
28808
28858
|
*/
|
|
28809
|
-
async
|
|
28810
|
-
|
|
28811
|
-
|
|
28812
|
-
|
|
28813
|
-
console.warn('No objects selected for SVG export');
|
|
28814
|
-
return;
|
|
28815
|
-
}
|
|
28816
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
28817
|
-
const filename = `selection-${timestamp}.svg`;
|
|
28818
|
-
const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|
28819
|
-
const url = URL.createObjectURL(blob);
|
|
28820
|
-
const link = document.createElement('a');
|
|
28821
|
-
link.download = filename;
|
|
28822
|
-
link.href = url;
|
|
28823
|
-
link.click();
|
|
28824
|
-
URL.revokeObjectURL(url);
|
|
28825
|
-
}
|
|
28826
|
-
catch (error) {
|
|
28827
|
-
console.error('Failed to export selected objects as SVG:', error);
|
|
28859
|
+
async canExportSelectedObjectAs(format) {
|
|
28860
|
+
const selected = this.getSingleSelectedExportableObject();
|
|
28861
|
+
if (!selected) {
|
|
28862
|
+
return false;
|
|
28828
28863
|
}
|
|
28864
|
+
return selected.getSupportedExportFormats().includes(format);
|
|
28829
28865
|
}
|
|
28830
28866
|
/**
|
|
28831
|
-
*
|
|
28832
|
-
*
|
|
28833
|
-
* @param options - Optional export settings (theme, padding, scale for resolution).
|
|
28834
|
-
* @returns PNG data URL string, or null if no objects are selected.
|
|
28867
|
+
* Exports the currently selected object in the requested format and triggers a file download.
|
|
28868
|
+
* Only supports single selection.
|
|
28835
28869
|
*/
|
|
28836
|
-
async
|
|
28837
|
-
const
|
|
28838
|
-
|
|
28839
|
-
|
|
28840
|
-
// Avoid foreignObject-based text serialization, which taints the
|
|
28841
|
-
// canvas in SVG->PNG conversion paths.
|
|
28842
|
-
rasterSafeForCanvas: true,
|
|
28843
|
-
});
|
|
28844
|
-
if (!svgString) {
|
|
28845
|
-
return null;
|
|
28870
|
+
async exportSelectedObject(format) {
|
|
28871
|
+
const selected = this.getSingleSelectedExportableObject();
|
|
28872
|
+
if (!selected) {
|
|
28873
|
+
return;
|
|
28846
28874
|
}
|
|
28847
|
-
const scale = options?.scale ?? 2; // Default 2x for better quality
|
|
28848
|
-
return new Promise((resolve, reject) => {
|
|
28849
|
-
const img = new Image();
|
|
28850
|
-
const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|
28851
|
-
const url = URL.createObjectURL(svgBlob);
|
|
28852
|
-
img.onload = () => {
|
|
28853
|
-
const canvas = document.createElement('canvas');
|
|
28854
|
-
canvas.width = img.width * scale;
|
|
28855
|
-
canvas.height = img.height * scale;
|
|
28856
|
-
const ctx = canvas.getContext('2d');
|
|
28857
|
-
if (!ctx) {
|
|
28858
|
-
URL.revokeObjectURL(url);
|
|
28859
|
-
reject(new Error('Failed to get canvas context'));
|
|
28860
|
-
return;
|
|
28861
|
-
}
|
|
28862
|
-
ctx.scale(scale, scale);
|
|
28863
|
-
ctx.drawImage(img, 0, 0);
|
|
28864
|
-
URL.revokeObjectURL(url);
|
|
28865
|
-
resolve(canvas.toDataURL('image/png'));
|
|
28866
|
-
};
|
|
28867
|
-
img.onerror = () => {
|
|
28868
|
-
URL.revokeObjectURL(url);
|
|
28869
|
-
reject(new Error('Failed to load SVG for PNG conversion'));
|
|
28870
|
-
};
|
|
28871
|
-
img.src = url;
|
|
28872
|
-
});
|
|
28873
|
-
}
|
|
28874
|
-
/**
|
|
28875
|
-
* Exports the currently selected objects as a PNG file and triggers a browser download.
|
|
28876
|
-
* @param options - Optional export settings (theme, padding, scale for resolution).
|
|
28877
|
-
*/
|
|
28878
|
-
async exportSelectedObjectsAsPng(options) {
|
|
28879
28875
|
try {
|
|
28880
|
-
const dataUrl = await this.
|
|
28876
|
+
const dataUrl = await selected.exportAs(format, this.getObjectExportContext());
|
|
28881
28877
|
if (!dataUrl) {
|
|
28882
|
-
console.
|
|
28878
|
+
console.error(`Failed to export selected object as ${format}: export returned no data URL`);
|
|
28883
28879
|
return;
|
|
28884
28880
|
}
|
|
28881
|
+
const activeWorkspaceName = this.core.store.state?.activeWorkspace?.name || 'workspace';
|
|
28882
|
+
const objectName = selected.__class__.replace(/^Kritzel/, '').toLowerCase() || 'object';
|
|
28885
28883
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
28886
|
-
const filename =
|
|
28887
|
-
|
|
28888
|
-
link.download = filename;
|
|
28889
|
-
link.href = dataUrl;
|
|
28890
|
-
link.click();
|
|
28884
|
+
const filename = `${activeWorkspaceName}-${objectName}-${timestamp}.${format}`;
|
|
28885
|
+
this.triggerDataUrlDownload(dataUrl, filename);
|
|
28891
28886
|
}
|
|
28892
28887
|
catch (error) {
|
|
28893
|
-
console.error(
|
|
28888
|
+
console.error(`Failed to export selected object as ${format}:`, error);
|
|
28894
28889
|
}
|
|
28895
28890
|
}
|
|
28896
28891
|
/**
|
|
@@ -29085,6 +29080,73 @@ const KritzelEngine = class {
|
|
|
29085
29080
|
const response = await fetch(dataUrl);
|
|
29086
29081
|
return response.blob();
|
|
29087
29082
|
}
|
|
29083
|
+
triggerDataUrlDownload(dataUrl, filename) {
|
|
29084
|
+
const link = document.createElement('a');
|
|
29085
|
+
link.download = filename;
|
|
29086
|
+
link.href = dataUrl;
|
|
29087
|
+
link.click();
|
|
29088
|
+
}
|
|
29089
|
+
getSingleSelectedExportableObject() {
|
|
29090
|
+
const selectedObjects = this.core.store.selectionGroup?.objects ?? [];
|
|
29091
|
+
if (selectedObjects.length !== 1) {
|
|
29092
|
+
return null;
|
|
29093
|
+
}
|
|
29094
|
+
const [selected] = selectedObjects;
|
|
29095
|
+
if (!ObjectHelper.isKritzelExportable(selected)) {
|
|
29096
|
+
return null;
|
|
29097
|
+
}
|
|
29098
|
+
return selected;
|
|
29099
|
+
}
|
|
29100
|
+
getObjectExportContext() {
|
|
29101
|
+
return {
|
|
29102
|
+
getObjectAsSvgDataUrl: async (object, options) => {
|
|
29103
|
+
const svg = await this.getObjectAsSvgMarkup(object, options?.rasterSafeForCanvas ?? false);
|
|
29104
|
+
return svg ? this.svgMarkupToDataUrl(svg) : null;
|
|
29105
|
+
},
|
|
29106
|
+
getObjectAsPngDataUrl: async (object) => {
|
|
29107
|
+
const svg = await this.getObjectAsSvgMarkup(object, true);
|
|
29108
|
+
return svg ? this.svgMarkupToPngDataUrl(svg) : null;
|
|
29109
|
+
},
|
|
29110
|
+
};
|
|
29111
|
+
}
|
|
29112
|
+
async getObjectAsSvgMarkup(object, rasterSafeForCanvas) {
|
|
29113
|
+
const imageDataUrls = await KritzelSvgExportHelper.resolveImageDataUrls([object], this.core.assetResolver);
|
|
29114
|
+
const svg = KritzelSvgExportHelper.generateSvg([object], {
|
|
29115
|
+
theme: this.theme,
|
|
29116
|
+
rasterSafeForCanvas,
|
|
29117
|
+
imageDataUrls,
|
|
29118
|
+
});
|
|
29119
|
+
return svg || null;
|
|
29120
|
+
}
|
|
29121
|
+
svgMarkupToDataUrl(svgMarkup) {
|
|
29122
|
+
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgMarkup)}`;
|
|
29123
|
+
}
|
|
29124
|
+
async svgMarkupToPngDataUrl(svgMarkup) {
|
|
29125
|
+
const svgBlob = new Blob([svgMarkup], { type: 'image/svg+xml;charset=utf-8' });
|
|
29126
|
+
const svgBlobUrl = URL.createObjectURL(svgBlob);
|
|
29127
|
+
try {
|
|
29128
|
+
const img = await new Promise((resolve, reject) => {
|
|
29129
|
+
const element = new Image();
|
|
29130
|
+
element.onload = () => resolve(element);
|
|
29131
|
+
element.onerror = err => reject(err);
|
|
29132
|
+
element.src = svgBlobUrl;
|
|
29133
|
+
});
|
|
29134
|
+
const width = Math.max(1, Math.ceil(img.naturalWidth || img.width));
|
|
29135
|
+
const height = Math.max(1, Math.ceil(img.naturalHeight || img.height));
|
|
29136
|
+
const canvas = document.createElement('canvas');
|
|
29137
|
+
canvas.width = width;
|
|
29138
|
+
canvas.height = height;
|
|
29139
|
+
const context = canvas.getContext('2d');
|
|
29140
|
+
if (!context) {
|
|
29141
|
+
throw new Error('Could not create 2D canvas context for PNG export');
|
|
29142
|
+
}
|
|
29143
|
+
context.drawImage(img, 0, 0, width, height);
|
|
29144
|
+
return canvas.toDataURL('image/png');
|
|
29145
|
+
}
|
|
29146
|
+
finally {
|
|
29147
|
+
URL.revokeObjectURL(svgBlobUrl);
|
|
29148
|
+
}
|
|
29149
|
+
}
|
|
29088
29150
|
getImportedWorkspaceName(rawName) {
|
|
29089
29151
|
if (typeof rawName === 'string' && rawName.trim().length > 0) {
|
|
29090
29152
|
return rawName;
|
|
@@ -29961,7 +30023,9 @@ const KritzelEngine = class {
|
|
|
29961
30023
|
this.emitObjectsInViewportChange();
|
|
29962
30024
|
}
|
|
29963
30025
|
}
|
|
29964
|
-
return (h(Host, null, h("kritzel-loading-overlay", { visible: this.core.store.state.isLoading, text: this.core.localizationManager.translate('engine.loading') }), h("kritzel-sync-indicator", { visible: this.isSyncing, text: "Syncing..." }), this.core.store.state.debugInfo.showViewportInfo && (h("div", { class: "debug-panel"
|
|
30026
|
+
return (h(Host, null, h("kritzel-loading-overlay", { visible: this.core.store.state.isLoading, text: this.core.localizationManager.translate('engine.loading') }), h("kritzel-sync-indicator", { visible: this.isSyncing, text: "Syncing..." }), this.core.store.state.debugInfo.showViewportInfo && (h("div", { class: "debug-panel", style: {
|
|
30027
|
+
color: 'var(--kritzel-global-text-primary)',
|
|
30028
|
+
} }, h("div", null, "ActiveWorkspaceId: ", this.core.store.state?.activeWorkspace?.id), h("div", null, "ActiveWorkspaceName: ", this.core.store.state?.activeWorkspace?.name), h("div", null, "TranslateX: ", this.core.store.state?.translateX), h("div", null, "TranslateY: ", this.core.store.state?.translateY), h("div", null, "ViewportWidth: ", this.core.store.state?.viewportWidth), h("div", null, "ViewportHeight: ", this.core.store.state?.viewportHeight), h("div", null, "PointerCount: ", this.core.store.state.pointers.size), h("div", null, "Scale: ", this.core.store.state?.scale), h("div", null, "ActiveTool: ", this.core.store.state?.activeTool?.name), h("div", null, "HasViewportChanged: ", this.core.store.state?.hasViewportChanged ? 'true' : 'false'), h("div", null, "IsEnabled: ", this.core.store.state?.isEnabled ? 'true' : 'false'), h("div", null, "IsScaling: ", this.core.store.state?.isScaling ? 'true' : 'false'), h("div", null, "IsPanning: ", this.core.store.state?.isPanning ? 'true' : 'false'), h("div", null, "IsSelecting: ", this.isSelecting ? 'true' : 'false'), h("div", null, "IsSelectionActive: ", this.isSelectionActive ? 'true' : 'false'), h("div", null, "IsResizeHandleSelected: ", this.core.store.state.isResizeHandleSelected ? 'true' : 'false'), h("div", null, "IsRotationHandleSelected: ", this.core.store.state.isRotationHandleSelected ? 'true' : 'false'), h("div", null, "IsRotationHandleHovered: ", this.core.store.state.isRotationHandleHovered ? 'true' : 'false'), h("div", null, "IsDrawing: ", this.core.store.state.isDrawing ? 'true' : 'false'), h("div", null, "IsWriting: ", this.core.store.state.isWriting ? 'true' : 'false'), h("div", null, "IsPointerDown: ", this.core.store.isPointerDown ? 'true' : 'false'), h("div", null, "PointerX: ", this.core.store.state?.pointerX), h("div", null, "PointerY: ", this.core.store.state?.pointerY), h("div", null, "TotalObjects: ", this.core.store.totalObjectCount), h("div", null, "ObjectsInViewport: ", this.core.store.objectsInViewport.length), h("div", null, "SelectedObjects: ", this.core.store.selectionGroup?.objects.length || 0), h("div", null, "ViewportCenter: (", viewportCenterX.toFixed(2), ", ", viewportCenterY.toFixed(2), ")"))), h("div", { id: "origin", class: "origin", style: {
|
|
29965
30029
|
transform: `matrix(${this.core.store.state?.scale}, 0, 0, ${this.core.store.state?.scale}, ${this.core.store.state?.translateX}, ${this.core.store.state?.translateY})`,
|
|
29966
30030
|
opacity: this._sceneBootstrapOpacity.toString(),
|
|
29967
30031
|
transition: this._sceneBootstrapTransitionEnabled ? 'opacity 220ms ease-out' : 'none',
|
|
@@ -30144,6 +30208,7 @@ const KritzelEngine = class {
|
|
|
30144
30208
|
left: `${object.totalWidth}px`,
|
|
30145
30209
|
top: '0',
|
|
30146
30210
|
zIndex: (object.zIndex + 2).toString(),
|
|
30211
|
+
color: 'var(--kritzel-global-text-primary)',
|
|
30147
30212
|
} }, h("div", { style: { whiteSpace: 'nowrap', fontSize: '10px' } }, "Id: ", object.id), h("div", { style: { whiteSpace: 'nowrap', fontSize: '10px' } }, "userId: ", object.userId), h("div", { style: { whiteSpace: 'nowrap', fontSize: '10px' } }, "width: ", object.width), h("div", { style: { whiteSpace: 'nowrap', fontSize: '10px' } }, "height: ", object.height), h("div", { style: { whiteSpace: 'nowrap', fontSize: '10px' } }, "translateX: ", object.translateX), h("div", { style: { whiteSpace: 'nowrap', fontSize: '10px' } }, "translateY: ", object.translateY), h("div", { style: { whiteSpace: 'nowrap', fontSize: '10px' } }, "scale: ", object.scale), h("div", { style: { whiteSpace: 'nowrap', fontSize: '10px' } }, "rotationDegrees: ", object.rotationDegrees), h("div", { style: { whiteSpace: 'nowrap', fontSize: '10px' } }, "zIndex: ", object.zIndex), h("div", { style: { whiteSpace: 'nowrap', fontSize: '10px' } }, "isSelected: ", object.isSelected ? 'true' : 'false'), KritzelClassHelper.isInstanceOf(object, 'KritzelCustomElement') && (h("div", null, h("div", { style: { whiteSpace: 'nowrap', fontSize: '10px' } }, "isInteractive: ", object.isInteractive ? 'true' : 'false'))), KritzelClassHelper.isInstanceOf(object, 'KritzelImage') && (h("div", null, h("div", { style: { whiteSpace: 'nowrap', fontSize: '10px' } }, "assetId: ", object.assetId), h("div", { style: { whiteSpace: 'nowrap', fontSize: '10px' } }, "mimeType: ", object.mimeType))))), (this.core.displaySelectionGroupUI(object) || this.core.displaySelectionLineUI(object)) &&
|
|
30148
30213
|
(() => {
|
|
30149
30214
|
const isSelectionGroup = KritzelClassHelper.isInstanceOf(object, 'KritzelSelectionGroup');
|
|
@@ -30344,8 +30409,12 @@ const KritzelEngine = class {
|
|
|
30344
30409
|
strokeDasharray: data.dashArray,
|
|
30345
30410
|
strokeLinecap: 'round',
|
|
30346
30411
|
} }))), h("circle", { cx: data.centerX, cy: data.centerY, r: data.indicatorRadius, style: {
|
|
30347
|
-
fill: isActiveSnap
|
|
30348
|
-
|
|
30412
|
+
fill: isActiveSnap
|
|
30413
|
+
? 'var(--kritzel-snap-indicator-fill, rgba(59, 130, 246, 0.3))'
|
|
30414
|
+
: 'var(--kritzel-snap-indicator-fill-inactive, var(--kritzel-snap-indicator-fill, rgba(59, 130, 246, 0.18)))',
|
|
30415
|
+
stroke: isActiveSnap
|
|
30416
|
+
? 'var(--kritzel-snap-indicator-stroke, #007bff)'
|
|
30417
|
+
: 'var(--kritzel-snap-indicator-stroke-inactive, var(--kritzel-snap-indicator-stroke, rgba(59, 130, 246, 0.5)))',
|
|
30349
30418
|
strokeWidth: data.indicatorStrokeWidth,
|
|
30350
30419
|
} }))));
|
|
30351
30420
|
})(), (() => {
|
|
@@ -30549,7 +30618,7 @@ const KritzelExport = class {
|
|
|
30549
30618
|
return (h("div", { class: "export-tab-content" }, h("kritzel-input", { label: this.terms['export.filename.label'] ?? 'Filename', value: this.exportFilename, placeholder: this.terms['export.filename.placeholder'] ?? 'Enter filename', suffix: ".json", onValueChange: this.handleFilenameChange })));
|
|
30550
30619
|
}
|
|
30551
30620
|
render() {
|
|
30552
|
-
return (h(Host, { key: '0bc45e80716be4a7b19ae12a6464a641b5bd4550' }, h("kritzel-dialog", { key: 'f336f8b1e4ae10954d48ca67da88504faf06b295', isOpen: this.isDialogOpen, dialogTitle: this.terms['export.dialogTitle'] ?? 'Export', closable: true, contained: true, onDialogClose: this.closeDialog }, h("div", { key: 'e2d1209652bb54fb7f623e50bc4bbbb703083b76', class: "export-content" }, h("kritzel-pill-tabs", { key: 'dd78430b78d1e8c4ac0a76e8a6a5d48bebf5e889', tabs: this.tabs, value: this.activeTab, onValueChange: this.handleTabChange }), this.activeTab === 'viewport' && this.renderViewportExport(), this.activeTab === 'workspace' && this.renderWorkspaceExport(), h("button", { key: '
|
|
30621
|
+
return (h(Host, { key: '0bc45e80716be4a7b19ae12a6464a641b5bd4550' }, h("kritzel-dialog", { key: 'f336f8b1e4ae10954d48ca67da88504faf06b295', isOpen: this.isDialogOpen, dialogTitle: this.terms['export.dialogTitle'] ?? 'Export', closable: true, contained: true, onDialogClose: this.closeDialog }, h("div", { key: 'e2d1209652bb54fb7f623e50bc4bbbb703083b76', class: "export-content" }, h("kritzel-pill-tabs", { key: 'dd78430b78d1e8c4ac0a76e8a6a5d48bebf5e889', tabs: this.tabs, value: this.activeTab, onValueChange: this.handleTabChange }), this.activeTab === 'viewport' && this.renderViewportExport(), this.activeTab === 'workspace' && this.renderWorkspaceExport()), h("div", { key: 'd04221d9f98a066d538f98f8c9409c558fdfa29f', slot: "footer" }, h("button", { key: '5cbb062756f29a897088d6647c6b8eae9f5781e2', class: "export-primary-button", onClick: this.handleExport }, this.terms['export.exportButton'] ?? 'Export')))));
|
|
30553
30622
|
}
|
|
30554
30623
|
};
|
|
30555
30624
|
KritzelExport.style = kritzelExportCss();
|
|
@@ -30823,7 +30892,7 @@ const KritzelLoadingOverlay = class {
|
|
|
30823
30892
|
*/
|
|
30824
30893
|
text = 'Loading...';
|
|
30825
30894
|
render() {
|
|
30826
|
-
return (h(Host, { key: '
|
|
30895
|
+
return (h(Host, { key: 'ce2edf698db335518979f597ad32fa259c3055b9' }, h("div", { key: '7cf019929b72d18fdd1cea55157dc5b5fc2ea151', class: { 'loading-overlay': true, visible: this.visible }, "aria-hidden": this.visible ? 'false' : 'true' }, h("span", { key: '86b7cb040bcfb1abb2161a38f9cff153d5aa6c18', class: "loading-spinner" }), this.text)));
|
|
30827
30896
|
}
|
|
30828
30897
|
};
|
|
30829
30898
|
KritzelLoadingOverlay.style = kritzelLoadingOverlayCss();
|
|
@@ -30875,7 +30944,7 @@ const KritzelLoginDialog = class {
|
|
|
30875
30944
|
this.dialogClosed.emit();
|
|
30876
30945
|
};
|
|
30877
30946
|
render() {
|
|
30878
|
-
return (h(Host, { key: '
|
|
30947
|
+
return (h(Host, { key: '561b4e76017e71d99f05cd33ed9f6e3769bb9aec' }, h("kritzel-dialog", { key: '74b904f67496debebf7a1854e3e3dd193f14a445', dialogTitle: this.dialogTitle, isOpen: this.isDialogOpen, onDialogClose: this.closeDialog, size: "small", contained: true }, h("div", { key: 'dcc2a024349fcdfcad7c7c6d5b7754e5ae990ae3', class: "login-content" }, this.subtitle && (h("p", { key: 'fdc927c84294a2ac92aec90001c3da373cc5f082', class: "login-subtitle" }, this.subtitle)), h("div", { key: 'a6e74ff9b3211df195b05edd2be0ce69d1b128df', class: "login-providers" }, this.providers.map(provider => (h("button", { key: provider.name, class: {
|
|
30879
30948
|
'provider-button': true,
|
|
30880
30949
|
'is-loading': this.loadingProvider === provider.name,
|
|
30881
30950
|
'is-disabled': this.loadingProvider !== null && this.loadingProvider !== provider.name,
|
|
@@ -31220,6 +31289,14 @@ const KritzelMoreMenu = class {
|
|
|
31220
31289
|
itemSelect;
|
|
31221
31290
|
menuAnchor = null;
|
|
31222
31291
|
isTouchDevice = KritzelDevicesHelper.isTouchDevice();
|
|
31292
|
+
visibleItems = [];
|
|
31293
|
+
visibilityResolveVersion = 0;
|
|
31294
|
+
onItemsChange() {
|
|
31295
|
+
void this.resolveVisibleItems();
|
|
31296
|
+
}
|
|
31297
|
+
componentWillLoad() {
|
|
31298
|
+
void this.resolveVisibleItems();
|
|
31299
|
+
}
|
|
31223
31300
|
toggleMenu = (event) => {
|
|
31224
31301
|
event.stopPropagation();
|
|
31225
31302
|
if (this.menuAnchor) {
|
|
@@ -31228,13 +31305,84 @@ const KritzelMoreMenu = class {
|
|
|
31228
31305
|
else {
|
|
31229
31306
|
window.dispatchEvent(new CustomEvent('kritzel-dismiss-menus'));
|
|
31230
31307
|
this.menuAnchor = this.host;
|
|
31308
|
+
this.visibleItems = this.filterVisibleItemsSync(this.items ?? []);
|
|
31309
|
+
void this.resolveVisibleItems();
|
|
31231
31310
|
}
|
|
31232
31311
|
};
|
|
31233
31312
|
closeMenu = () => {
|
|
31234
31313
|
this.menuAnchor = null;
|
|
31235
31314
|
};
|
|
31236
|
-
|
|
31237
|
-
|
|
31315
|
+
async evaluateVisibility(visibility) {
|
|
31316
|
+
if (typeof visibility === 'function') {
|
|
31317
|
+
try {
|
|
31318
|
+
return (await visibility()) !== false;
|
|
31319
|
+
}
|
|
31320
|
+
catch {
|
|
31321
|
+
return false;
|
|
31322
|
+
}
|
|
31323
|
+
}
|
|
31324
|
+
return visibility !== false;
|
|
31325
|
+
}
|
|
31326
|
+
evaluateVisibilitySync(visibility) {
|
|
31327
|
+
if (typeof visibility === 'function') {
|
|
31328
|
+
try {
|
|
31329
|
+
const result = visibility();
|
|
31330
|
+
return result instanceof Promise ? null : result !== false;
|
|
31331
|
+
}
|
|
31332
|
+
catch {
|
|
31333
|
+
return false;
|
|
31334
|
+
}
|
|
31335
|
+
}
|
|
31336
|
+
return visibility !== false;
|
|
31337
|
+
}
|
|
31338
|
+
filterVisibleItemsSync(items) {
|
|
31339
|
+
return items.reduce((visibleItems, item) => {
|
|
31340
|
+
const isVisible = this.evaluateVisibilitySync(item.isVisible);
|
|
31341
|
+
if (isVisible === null || !isVisible) {
|
|
31342
|
+
return visibleItems;
|
|
31343
|
+
}
|
|
31344
|
+
if (!item.children?.length) {
|
|
31345
|
+
visibleItems.push(item);
|
|
31346
|
+
return visibleItems;
|
|
31347
|
+
}
|
|
31348
|
+
visibleItems.push({
|
|
31349
|
+
...item,
|
|
31350
|
+
children: this.filterVisibleItemsSync(item.children),
|
|
31351
|
+
});
|
|
31352
|
+
return visibleItems;
|
|
31353
|
+
}, []);
|
|
31354
|
+
}
|
|
31355
|
+
async filterVisibleItems(items) {
|
|
31356
|
+
const resolved = await Promise.all(items.map(async (item) => {
|
|
31357
|
+
const isVisible = await this.evaluateVisibility(item.isVisible);
|
|
31358
|
+
if (!isVisible) {
|
|
31359
|
+
return null;
|
|
31360
|
+
}
|
|
31361
|
+
if (!item.children?.length) {
|
|
31362
|
+
return item;
|
|
31363
|
+
}
|
|
31364
|
+
return {
|
|
31365
|
+
...item,
|
|
31366
|
+
children: await this.filterVisibleItems(item.children),
|
|
31367
|
+
};
|
|
31368
|
+
}));
|
|
31369
|
+
return resolved.filter((item) => !!item);
|
|
31370
|
+
}
|
|
31371
|
+
async resolveVisibleItems() {
|
|
31372
|
+
const version = ++this.visibilityResolveVersion;
|
|
31373
|
+
const items = this.items ?? [];
|
|
31374
|
+
const resolvedItems = await this.filterVisibleItems(items);
|
|
31375
|
+
if (version !== this.visibilityResolveVersion) {
|
|
31376
|
+
return;
|
|
31377
|
+
}
|
|
31378
|
+
this.visibleItems = resolvedItems;
|
|
31379
|
+
}
|
|
31380
|
+
resolveMenuLabels(items) {
|
|
31381
|
+
return items.map(item => ({
|
|
31382
|
+
...item,
|
|
31383
|
+
label: this.terms[item.label] ?? item.label,
|
|
31384
|
+
children: item.children ? this.resolveMenuLabels(item.children) : undefined,
|
|
31385
|
+
}));
|
|
31238
31386
|
}
|
|
31239
31387
|
handleMenuItemSelect = (event) => {
|
|
31240
31388
|
const { item, parent } = event.detail;
|
|
@@ -31245,8 +31393,13 @@ const KritzelMoreMenu = class {
|
|
|
31245
31393
|
this.closeMenu();
|
|
31246
31394
|
};
|
|
31247
31395
|
render() {
|
|
31248
|
-
return (h(Host, { key: '
|
|
31396
|
+
return (h(Host, { key: 'dbfe312eccd24901ffc68179ef93ba0a94a765c5', class: { mobile: this.isTouchDevice }, style: { display: this.visible ? '' : 'none' } }, h("div", { key: '33bd535bb58ae32a32d44133a109fdd3667b4947', class: { 'more-menu-wrapper': true, visible: this.visible } }, h("button", { key: '23dbae51d73b92b181721bdef4a44049ec6484c5', class: "more-menu-button", "data-testid": "more-menu-button", onClick: this.toggleMenu, "aria-label": this.terms['moreMenu.ariaLabel'] ?? 'More options' }, h("kritzel-icon", { key: 'bda7c092b57af366b5d6b882cbab991cd9bb77ba', name: this.icon, size: this.iconSize })), h("kritzel-portal", { key: '2959ac3e6e0a5598c735ce7a01619bda8753039f', anchor: this.menuAnchor, offsetY: this.offsetY, onClose: this.closeMenu }, h("kritzel-menu", { key: 'a5481a1bc02dc059dc0ad188b93b9eaf9b56e98f', items: this.resolveMenuLabels(this.visibleItems), onItemSelect: this.handleMenuItemSelect })))));
|
|
31249
31397
|
}
|
|
31398
|
+
static get watchers() { return {
|
|
31399
|
+
"items": [{
|
|
31400
|
+
"onItemsChange": 0
|
|
31401
|
+
}]
|
|
31402
|
+
}; }
|
|
31250
31403
|
};
|
|
31251
31404
|
KritzelMoreMenu.style = kritzelMoreMenuCss();
|
|
31252
31405
|
|
|
@@ -31818,7 +31971,7 @@ const KritzelPortal = class {
|
|
|
31818
31971
|
* This file is auto-generated by the version bump scripts.
|
|
31819
31972
|
* Do not modify manually.
|
|
31820
31973
|
*/
|
|
31821
|
-
const KRITZEL_VERSION = '0.4.
|
|
31974
|
+
const KRITZEL_VERSION = '0.4.9';
|
|
31822
31975
|
|
|
31823
31976
|
const kritzelSettingsCss = () => `:host{display:contents}kritzel-dialog{--kritzel-dialog-body-padding:0;--kritzel-dialog-width-large:800px;--kritzel-dialog-height-large:500px}.footer-button{padding:8px 16px;border-radius:6px;cursor:pointer;font-size:14px}.cancel-button{border:1px solid #ebebeb;background:#fff;color:inherit}.cancel-button:hover{background:#f5f5f5}.settings-tab-panel{display:none}.settings-tab-panel.is-active{display:block}.settings-content{padding:0}.settings-content h3{margin:0 0 16px 0;font-size:18px;font-weight:600;color:var(--kritzel-settings-content-heading-color, #333333)}.settings-content p{margin:0;font-size:14px;color:var(--kritzel-settings-content-text-color, #666666);line-height:1.5}.settings-group{display:flex;flex-direction:column;gap:24px}.settings-item{display:flex;flex-direction:column;gap:8px}.settings-row{display:flex;align-items:center;justify-content:space-between;gap:16px}.settings-label{font-size:14px;font-weight:600;color:var(--kritzel-settings-label-color, #333333);margin:0 0 4px 0}.settings-description{font-size:12px;color:var(--kritzel-settings-description-color, #888888);margin:0;line-height:1.4}.shortcuts-list{display:flex;flex-direction:column;gap:24px}.shortcuts-category{display:flex;flex-direction:column;gap:8px}.shortcuts-category-title{font-size:14px;font-weight:600;color:var(--kritzel-settings-label-color, #333333);margin:0 0 4px 0}.shortcuts-group{display:flex;flex-direction:column;gap:4px}.shortcut-item{display:flex;justify-content:space-between;align-items:center;padding:6px 8px;border-radius:4px;background:var(--kritzel-settings-shortcut-item-bg, rgba(0, 0, 0, 0.02))}.shortcut-label{font-size:14px;color:var(--kritzel-settings-content-text-color, #666666)}.shortcut-key{font-family:monospace;font-size:12px;padding:2px 8px;border-radius:4px;background:var(--kritzel-settings-shortcut-key-bg, #f0f0f0);color:var(--kritzel-settings-shortcut-key-color, #333333);border:1px solid var(--kritzel-settings-shortcut-key-border, #ddd)}`;
|
|
31824
31977
|
|
|
@@ -32030,7 +32183,7 @@ const KritzelSettings = class {
|
|
|
32030
32183
|
return (h("div", { class: "settings-categories-wrapper" }, h("div", { class: { 'settings-tab-panel': true, 'is-active': this.selectedCategoryId === 'general' } }, h("div", { class: "settings-content" }, h("h3", null, this.t('settings.general.title', 'General Settings')), h("div", { class: "settings-group" }, h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.general.theme.label', 'Theme')), h("p", { class: "settings-description" }, this.t('settings.general.theme.description', 'Select a registered color theme for the editor interface.')), h("kritzel-dropdown", { options: this.availableThemes.map(t => ({ value: t, label: t })), value: this.theme, onValueChanged: this.handleThemeChange })), this.availableLocales.length > 0 && (h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.general.language.label', 'Language')), h("p", { class: "settings-description" }, this.t('settings.general.language.description', 'Select the display language for the editor interface.')), h("kritzel-dropdown", { options: this.availableLocales.map(l => ({ value: l.code, label: l.label })), value: this.locale, onValueChanged: this.handleLocaleChange }))), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.general.lockDrawingScale.label', 'Lock Drawing Scale')), h("p", { class: "settings-description" }, this.t('settings.general.lockDrawingScale.description', 'When enabled, drawn objects maintain a fixed visual size regardless of the current zoom level.')), h("kritzel-slide-toggle", { checked: this.lockDrawingScale, label: this.t('settings.general.lockDrawingScale.label', 'Lock Drawing Scale'), onCheckedChange: this.handleLockDrawingScaleChange }))))), h("div", { class: { 'settings-tab-panel': true, 'is-active': this.selectedCategoryId === 'viewport' } }, h("div", { class: "settings-content" }, h("h3", null, this.t('settings.viewport.title', 'Viewport Settings')), h("div", { class: "settings-group" }, h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.viewport.minZoom.label', 'Minimum Zoom Level')), h("p", { class: "settings-description" }, this.t('settings.viewport.minZoom.description', 'Sets the minimum zoom level. Lower values allow zooming out further to see more of the canvas.')), h("kritzel-numeric-input", { value: this.scaleMin, min: 0.0001, max: 1, step: 0.0001, onValueChange: this.handleScaleMinChange })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.viewport.maxZoom.label', 'Maximum Zoom Level')), h("p", { class: "settings-description" }, this.t('settings.viewport.maxZoom.description', 'Sets the maximum zoom level. Higher values allow zooming in closer for detailed work.')), h("kritzel-numeric-input", { value: this.scaleMax, min: 1, max: 1000, step: 1, onValueChange: this.handleScaleMaxChange })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.viewport.boundaryLeft.label', 'Viewport Boundary Left')), h("p", { class: "settings-description" }, this.t('settings.viewport.boundaryLeft.description', 'Left boundary in world coordinates. Set to limit how far left the viewport can pan.')), h("kritzel-numeric-input", { value: this.viewportBoundaryLeft, step: 100, placeholder: this.t('settings.viewport.boundaryPlaceholder', 'Infinite'), onValueChange: this.handleViewportBoundaryLeftChange })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.viewport.boundaryRight.label', 'Viewport Boundary Right')), h("p", { class: "settings-description" }, this.t('settings.viewport.boundaryRight.description', 'Right boundary in world coordinates. Set to limit how far right the viewport can pan.')), h("kritzel-numeric-input", { value: this.viewportBoundaryRight, step: 100, placeholder: this.t('settings.viewport.boundaryPlaceholder', 'Infinite'), onValueChange: this.handleViewportBoundaryRightChange })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.viewport.boundaryTop.label', 'Viewport Boundary Top')), h("p", { class: "settings-description" }, this.t('settings.viewport.boundaryTop.description', 'Top boundary in world coordinates. Set to limit how far up the viewport can pan.')), h("kritzel-numeric-input", { value: this.viewportBoundaryTop, step: 100, placeholder: this.t('settings.viewport.boundaryPlaceholder', 'Infinite'), onValueChange: this.handleViewportBoundaryTopChange })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.viewport.boundaryBottom.label', 'Viewport Boundary Bottom')), h("p", { class: "settings-description" }, this.t('settings.viewport.boundaryBottom.description', 'Bottom boundary in world coordinates. Set to limit how far down the viewport can pan.')), h("kritzel-numeric-input", { value: this.viewportBoundaryBottom, step: 100, placeholder: this.t('settings.viewport.boundaryPlaceholder', 'Infinite'), onValueChange: this.handleViewportBoundaryBottomChange }))))), h("div", { class: { 'settings-tab-panel': true, 'is-active': this.selectedCategoryId === 'shortcuts' } }, h("div", { class: "settings-content" }, h("h3", null, this.t('settings.shortcuts.title', 'Keyboard Shortcuts')), h("div", { class: "shortcuts-list" }, Array.from(this.groupShortcutsByCategory()).map(([category, shortcuts]) => (h("div", { class: "shortcuts-category", key: category }, h("h4", { class: "shortcuts-category-title" }, category), h("div", { class: "shortcuts-group" }, shortcuts.map(shortcut => (h("div", { class: "shortcut-item", key: shortcut.key + shortcut.label }, h("span", { class: "shortcut-label" }, shortcut.label), h("kbd", { class: "shortcut-key" }, this.formatKeyCombo(shortcut)))))))))))), h("div", { class: { 'settings-tab-panel': true, 'is-active': this.selectedCategoryId === 'developer' } }, h("div", { class: "settings-content" }, h("h3", null, this.t('settings.developer.title', 'Developer Options')), h("div", { class: "settings-group" }, h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.developer.showViewportInfo.label', 'Show Viewport Info')), h("p", { class: "settings-description" }, this.t('settings.developer.showViewportInfo.description', 'Display viewport debug information such as position, zoom level, and boundaries.')), h("kritzel-slide-toggle", { checked: this.debugInfo.showViewportInfo, label: this.t('settings.developer.showViewportInfo.label', 'Show Viewport Info'), onCheckedChange: this.handleDebugInfoChange('showViewportInfo') })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.developer.showObjectInfo.label', 'Show Object Info')), h("p", { class: "settings-description" }, this.t('settings.developer.showObjectInfo.description', 'Display debug information about objects on the canvas.')), h("kritzel-slide-toggle", { checked: this.debugInfo.showObjectInfo, label: this.t('settings.developer.showObjectInfo.label', 'Show Object Info'), onCheckedChange: this.handleDebugInfoChange('showObjectInfo') })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.developer.showSyncProviderInfo.label', 'Show Sync Provider Info')), h("p", { class: "settings-description" }, this.t('settings.developer.showSyncProviderInfo.description', 'Display debug information about the sync provider connection status.')), h("kritzel-slide-toggle", { checked: this.debugInfo.showSyncProviderInfo, label: this.t('settings.developer.showSyncProviderInfo.label', 'Show Sync Provider Info'), onCheckedChange: this.handleDebugInfoChange('showSyncProviderInfo') })), h("div", { class: "settings-item" }, h("label", { class: "settings-label" }, this.t('settings.developer.showMigrationInfo.label', 'Show Migration Info')), h("p", { class: "settings-description" }, this.t('settings.developer.showMigrationInfo.description', 'Display debug information about data migrations.')), h("kritzel-slide-toggle", { checked: this.debugInfo.showMigrationInfo, label: this.t('settings.developer.showMigrationInfo.label', 'Show Migration Info'), onCheckedChange: this.handleDebugInfoChange('showMigrationInfo') }))))), h("div", { class: { 'settings-tab-panel': true, 'is-active': this.selectedCategoryId === 'about' } }, h("div", { class: "settings-content" }, h("h3", null, this.t('settings.about.title', 'About')), h("p", null, this.t('settings.about.description', 'Kritzel - A drawing application')), h("p", { class: "version-info" }, "Version ", KRITZEL_VERSION), h("p", { class: "version-info" }, "App-State Schema v", CURRENT_APP_STATE_SCHEMA_VERSION), h("p", { class: "version-info" }, "Workspace Schema v", CURRENT_WORKSPACE_SCHEMA_VERSION)))));
|
|
32031
32184
|
}
|
|
32032
32185
|
render() {
|
|
32033
|
-
return (h(Host, { key: '
|
|
32186
|
+
return (h(Host, { key: 'b8a899abd3dd55bc3493b7d124ac5505e2284036' }, h("kritzel-dialog", { key: '8d47f3548271e24a8a280a30139f04b2e4b4ec2b', isOpen: this.isDialogOpen, dialogTitle: this.t('settings.dialogTitle', 'Settings'), size: "large", contained: true, onDialogClose: this.closeDialog }, h("kritzel-master-detail", { key: 'a5213307761b84b8f5f67cb35409ab1c0c1fa57b', items: this.categories, selectedItemId: this.selectedCategoryId, onItemSelect: this.handleCategorySelect }, this.renderCategoryContent()))));
|
|
32034
32187
|
}
|
|
32035
32188
|
static get watchers() { return {
|
|
32036
32189
|
"settings": [{
|
|
@@ -35270,9 +35423,9 @@ const KritzelShareDialog = class {
|
|
|
35270
35423
|
this.dialogClosed.emit();
|
|
35271
35424
|
};
|
|
35272
35425
|
render() {
|
|
35273
|
-
return (h(Host, { key: '
|
|
35426
|
+
return (h(Host, { key: '75deaa66e6a6f640506985a8b3a2746892b5da07' }, h("kritzel-dialog", { key: '237413c9302e3756c5f7653e69326fac80d9e181', dialogTitle: this.terms['share.dialogTitle'] ?? 'Share Workspace', size: "small", isOpen: this.isDialogOpen, onDialogClose: this.closeDialog, contained: true }, h("div", { key: '295394b851cc2210fece6f3e297dfbc0c516b0f7', class: "share-content" }, h("div", { key: 'df64d71f9c7a7064d36db6d0a196071f1e8835a9', class: "share-section" }, h("div", { key: '9db4902a5edd917efb5ad6c183f0862b12313635', class: "share-row" }, h("div", { key: '8ead1d363d3430a4e65fa3b5835401b0e20006db', class: "share-label-group" }, h("label", { key: '2f3c28a3909eab86a123ba6cf7386b79894717c5', class: "share-label" }, this.terms['share.linkSharing.label'] ?? 'Link sharing'), h("p", { key: '3167ae1ed6ff52abc531eafdd905f1397807f551', class: "share-description" }, this.internalIsPublic
|
|
35274
35427
|
? (this.terms['share.linkSharing.enabledDescription'] ?? 'Anyone with the link can access this workspace.')
|
|
35275
|
-
: (this.terms['share.linkSharing.disabledDescription'] ?? 'Link sharing is disabled. Only you can access this workspace.'))), h("kritzel-slide-toggle", { key: '
|
|
35428
|
+
: (this.terms['share.linkSharing.disabledDescription'] ?? 'Link sharing is disabled. Only you can access this workspace.'))), h("kritzel-slide-toggle", { key: '3ff608b262e2ff3fa779f7d1e9a4ee3c0b9a16c0', checked: this.internalIsPublic, onCheckedChange: this.handleToggleChange, label: this.terms['share.linkSharing.toggleLabel'] ?? 'Enable link sharing' }))), this.internalIsPublic && (h("div", { key: '355261384b0ed386f3b48c041bcf0039db0534c1', class: "share-section" }, h("div", { key: 'ad3d975c650757d0b6dbd5317c9bac4f98029e0c', class: "share-url-container" }, h("input", { key: '0da2cc9cb30f1da8bcab3aa82c679b8d685641bd', type: "text", class: "share-url-input", value: this.getShareUrl(), readOnly: true, onClick: (e) => e.target.select() }), h("button", { key: 'aee232b7a70b524d52a4b4cf9074c099d29a5300', class: { 'copy-button': true, 'copy-success': this.copySuccess }, onClick: this.handleCopyUrl, title: this.copySuccess ? (this.terms['share.copyLink.copied'] ?? 'Copied!') : (this.terms['share.copyLink.title'] ?? 'Copy link') }, h("kritzel-icon", { key: 'c151baf7b890dbf51c4f4831d60b0ebb1a5d05fa', name: this.copySuccess ? 'check' : 'copy', size: 18 }))), this.qrCodeDataUrl && (h("div", { key: '63b43efe3dc5929054af9e5ee7f64d093864b3b0', class: "share-qr-container" }, h("img", { key: '9d3b0418eb80ff8c7e9106ad86d7cc211d017504', class: "share-qr-image", src: this.qrCodeDataUrl, alt: "QR code for share link" })))))))));
|
|
35276
35429
|
}
|
|
35277
35430
|
static get watchers() { return {
|
|
35278
35431
|
"isPublic": [{
|
|
@@ -35914,7 +36067,7 @@ const KritzelUtilityPanel = class {
|
|
|
35914
36067
|
this.redo.emit();
|
|
35915
36068
|
}
|
|
35916
36069
|
render() {
|
|
35917
|
-
return (h(Host, { key: '
|
|
36070
|
+
return (h(Host, { key: '001ee010d714513a4b5027f84cac4586c64e2260' }, h("button", { key: '6ba6cbf100e2ad9f81de6416b85f981cd1467f1c', class: "utility-button", "data-testid": "utility-undo", disabled: !this.undoState?.canUndo, onClick: event => this.handleUndo(event), "aria-label": this.terms['utility.undo'] ?? 'Undo' }, h("kritzel-icon", { key: 'b246d3964623e57e9458e5f0266a2f9e0565c646', name: "undo" })), h("button", { key: '1f5015fbd2fd84508bc8b8f295d35960bf31af5b', class: "utility-button", "data-testid": "utility-redo", disabled: !this.undoState?.canRedo, onClick: event => this.handleRedo(event), "aria-label": this.terms['utility.redo'] ?? 'Redo' }, h("kritzel-icon", { key: 'b3694d884707c6844a0036610990cdcfa74c6931', name: "redo" })), h("div", { key: '4f8f4725213d1453af11dc3b4c9ff271f1933df5', class: "utility-separator" }), h("button", { key: '5eba2c56dd96698538a48a8ea9f32fcf8b4d8402', class: "utility-button", "data-testid": "utility-delete", onClick: () => this.delete.emit(), "aria-label": this.terms['utility.delete'] ?? 'Delete selected items' }, h("kritzel-icon", { key: '268a186e4e7eaa1ea874ef23e72df511f86b0343', name: "delete" }))));
|
|
35918
36071
|
}
|
|
35919
36072
|
};
|
|
35920
36073
|
KritzelUtilityPanel.style = kritzelUtilityPanelCss();
|
|
@@ -36123,7 +36276,7 @@ const KritzelZoomPanel = class {
|
|
|
36123
36276
|
return Number.isFinite(this.zoomPercent) ? Math.max(1, Math.round(this.zoomPercent)) : 100;
|
|
36124
36277
|
}
|
|
36125
36278
|
render() {
|
|
36126
|
-
return (h(Host, { key: '
|
|
36279
|
+
return (h(Host, { key: 'aa4a0194b7ba89a32e369ae5ce9a8c148c05574f' }, h("div", { key: 'c88d4b299210d2aff28e07815bcbd4864ff13afd', class: { panel: true, visible: this.visible } }, h("button", { key: 'bf2c78d32ba31c25dcfed0b1f87fbfaee776acc1', class: "zoom-button", type: "button", "aria-label": this.terms['zoom.zoomOut'] ?? 'Zoom out', disabled: this.disabled, onClick: this.handleZoomOut }, h("kritzel-icon", { key: '3d350d003ed9d0b4548929d0765aae5da82c41b0', name: "minus" })), h("span", { key: 'da6454d4c28b5bdde41300c21b6593d91ce08085', class: "zoom-level", "aria-live": "polite" }, this.normalizedZoomPercent, "%"), h("button", { key: 'cf314fb20dcff5b3e728eae2129978cb12942947', class: "zoom-button", type: "button", "aria-label": this.terms['zoom.zoomIn'] ?? 'Zoom in', disabled: this.disabled, onClick: this.handleZoomIn }, h("kritzel-icon", { key: '71ca263815fd1774d5511118d8217863adcac64b', name: "plus" })))));
|
|
36127
36280
|
}
|
|
36128
36281
|
};
|
|
36129
36282
|
KritzelZoomPanel.style = kritzelZoomPanelCss();
|