kritzel-stencil 0.3.18 → 0.3.20
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-Xav9JFHg.js +2 -2
- package/dist/cjs/index.cjs.js +1 -1
- package/dist/cjs/{kritzel-active-users_44.cjs.entry.js → kritzel-active-users_45.cjs.entry.js} +739 -310
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/{schema.constants-DrHO_CYF.js → schema.constants-IBG5VW_J.js} +116 -59
- package/dist/cjs/stencil.cjs.js +1 -1
- package/dist/collection/classes/core/core.class.js +22 -210
- package/dist/collection/classes/handlers/key.handler.js +1 -22
- package/dist/collection/classes/managers/browser-permission.manager.js +64 -0
- package/dist/collection/classes/managers/clipboard.manager.js +401 -0
- package/dist/collection/classes/objects/image.class.js +42 -0
- package/dist/collection/classes/tools/image-tool.class.js +1 -59
- package/dist/collection/collection-manifest.json +1 -0
- package/dist/collection/components/core/kritzel-editor/kritzel-editor.css +53 -0
- package/dist/collection/components/core/kritzel-editor/kritzel-editor.js +133 -53
- package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +101 -43
- package/dist/collection/components/core/kritzel-watermark/kritzel-watermark.js +1 -1
- package/dist/collection/components/shared/kritzel-avatar/kritzel-avatar.js +3 -3
- package/dist/collection/components/shared/kritzel-button/kritzel-button.js +2 -2
- package/dist/collection/components/shared/kritzel-color/kritzel-color.js +2 -2
- package/dist/collection/components/shared/kritzel-color-palette/kritzel-color-palette.js +1 -1
- package/dist/collection/components/shared/kritzel-font/kritzel-font.js +1 -1
- package/dist/collection/components/shared/kritzel-font-size/kritzel-font-size.js +1 -1
- package/dist/collection/components/shared/kritzel-input/kritzel-input.js +1 -1
- package/dist/collection/components/shared/kritzel-master-detail/kritzel-master-detail.js +3 -3
- package/dist/collection/components/shared/kritzel-menu/kritzel-menu.js +1 -1
- package/dist/collection/components/shared/kritzel-menu-item/kritzel-menu-item.js +2 -2
- package/dist/collection/components/shared/kritzel-numeric-input/kritzel-numeric-input.js +1 -1
- package/dist/collection/components/shared/kritzel-opacity-slider/kritzel-opacity-slider.js +1 -1
- package/dist/collection/components/shared/kritzel-portal/kritzel-portal.js +1 -1
- package/dist/collection/components/shared/kritzel-slide-toggle/kritzel-slide-toggle.js +1 -1
- package/dist/collection/components/shared/kritzel-split-button/kritzel-split-button.js +1 -1
- package/dist/collection/components/shared/kritzel-stroke-size/kritzel-stroke-size.js +1 -1
- package/dist/collection/components/shared/kritzel-tooltip/kritzel-tooltip.js +2 -2
- package/dist/collection/components/ui/kritzel-back-to-content/kritzel-back-to-content.js +1 -1
- package/dist/collection/components/ui/kritzel-controls/kritzel-controls.js +6 -6
- package/dist/collection/components/ui/kritzel-current-user/kritzel-current-user.js +1 -1
- package/dist/collection/components/ui/kritzel-current-user-dialog/kritzel-current-user-dialog.js +1 -1
- package/dist/collection/components/ui/kritzel-export/kritzel-export.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 +1 -1
- package/dist/collection/components/ui/kritzel-notification-card/kritzel-notification-card.css +101 -0
- package/dist/collection/components/ui/kritzel-notification-card/kritzel-notification-card.js +173 -0
- 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/configs/default-engine-config.js +0 -1
- package/dist/collection/constants/version.js +1 -1
- package/dist/collection/interfaces/notification.interface.js +1 -0
- package/dist/collection/themes/dark-theme.js +36 -0
- package/dist/collection/themes/light-theme.js +36 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +1 -1
- package/dist/components/kritzel-active-users.js +1 -1
- package/dist/components/kritzel-avatar.js +1 -1
- package/dist/components/kritzel-awareness-cursors.js +1 -1
- package/dist/components/kritzel-back-to-content.js +1 -1
- package/dist/components/kritzel-button.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-current-user-dialog.js +1 -1
- package/dist/components/kritzel-current-user.js +1 -1
- package/dist/components/kritzel-editor.js +1 -1
- package/dist/components/kritzel-engine.js +1 -1
- package/dist/components/kritzel-export.js +1 -1
- package/dist/components/kritzel-font-size.js +1 -1
- package/dist/components/kritzel-font.js +1 -1
- package/dist/components/kritzel-input.js +1 -1
- package/dist/components/kritzel-login-dialog.js +1 -1
- package/dist/components/kritzel-master-detail.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-notification-card.d.ts +11 -0
- package/dist/components/kritzel-notification-card.js +1 -0
- package/dist/components/kritzel-numeric-input.js +1 -1
- package/dist/components/kritzel-opacity-slider.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-slide-toggle.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-tooltip.js +1 -1
- package/dist/components/kritzel-utility-panel.js +1 -1
- package/dist/components/kritzel-watermark.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-BvRrA4hN.js → p-6Ca7Eolf.js} +1 -1
- package/dist/components/{p-BpnIvNvq.js → p-BO1CnEOT.js} +1 -1
- package/dist/components/{p-CyqRcqsO.js → p-BPoDE87Q.js} +1 -1
- package/dist/components/p-BX-nQAyH.js +1 -0
- package/dist/components/{p-DNdXJp8F.js → p-Bg0xIZiR.js} +1 -1
- package/dist/components/{p-DmWSRsjK.js → p-BhODnvMO.js} +1 -1
- package/dist/components/{p-mz3pUWW4.js → p-Bm0aKWPw.js} +1 -1
- package/dist/components/{p-3HxnBrCM.js → p-BrAeYciD.js} +1 -1
- package/dist/components/{p-f8aW1ye7.js → p-BwuemYET.js} +1 -1
- package/dist/components/{p-U4oawa1x.js → p-C-RiEw9X.js} +1 -1
- package/dist/components/{p-DdH1cKED.js → p-C1jg5IQi.js} +1 -1
- package/dist/components/{p-SDZNC8GF.js → p-C4-Som48.js} +1 -1
- package/dist/components/{p-C9HGoDHE.js → p-C5TZjIE1.js} +1 -1
- package/dist/components/{p-7NsK0uHu.js → p-CC0J_kEP.js} +1 -1
- package/dist/components/{p-CIcLzcfA.js → p-CJEIUs0x.js} +1 -1
- package/dist/components/{p-Bzv9Px8v.js → p-CO0eEzos.js} +1 -1
- package/dist/components/{p-DDkmsPpV.js → p-COdwK_mi.js} +1 -1
- package/dist/components/{p-CPtDfadX.js → p-COj8EVzL.js} +1 -1
- package/dist/components/{p-CEnEDaix.js → p-CVV-HzFR.js} +1 -1
- package/dist/components/{p-6RjeGuvH.js → p-Cb5YwXbC.js} +1 -1
- package/dist/components/{p-DX5K8xnh.js → p-CbMLbDou.js} +1 -1
- package/dist/components/{p-DZdgXCAx.js → p-CdkNxxut.js} +1 -1
- package/dist/components/{p-BKJSh8qQ.js → p-CpR9lc-o.js} +1 -1
- package/dist/components/p-CpzicliK.js +1 -0
- package/dist/components/{p-IpoC5EEY.js → p-D0RYktBV.js} +1 -1
- package/dist/components/{p-DI4vQRE3.js → p-D0v9WP5w.js} +1 -1
- package/dist/components/{p-DgmtCdnL.js → p-D28ZylHn.js} +1 -1
- package/dist/components/{p-F5_X4dZG.js → p-DEpnJBA3.js} +1 -1
- package/dist/components/{p-BCNyR5Sw.js → p-DRsD38Nl.js} +1 -1
- package/dist/components/p-DWhWFDcC.js +9 -0
- package/dist/components/{p-Dz-Ti24X.js → p-DeqALrzV.js} +1 -1
- package/dist/components/{p-BKvHg9cv.js → p-Ndib1Jb3.js} +1 -1
- package/dist/components/{p-vAeiXe6c.js → p-NwN6r0N0.js} +1 -1
- package/dist/components/{p-NuLP1xHe.js → p-W1ZwImdd.js} +1 -1
- package/dist/components/{p-Jn6TNdfe.js → p-YFR180v-.js} +1 -1
- package/dist/components/p-pagW8YF7.js +1 -0
- package/dist/components/{p-v7dxxrL5.js → p-vf28uSsc.js} +1 -1
- package/dist/esm/index-Dhio9uis.js +2 -2
- package/dist/esm/index.js +2 -2
- package/dist/esm/{kritzel-active-users_44.entry.js → kritzel-active-users_45.entry.js} +739 -311
- package/dist/esm/loader.js +1 -1
- package/dist/esm/{schema.constants-DchTXG3V.js → schema.constants-D35cwCYd.js} +116 -60
- package/dist/esm/stencil.js +1 -1
- package/dist/stencil/index.esm.js +1 -1
- package/dist/stencil/p-D35cwCYd.js +1 -0
- package/dist/stencil/p-e8e69a16.entry.js +9 -0
- package/dist/stencil/stencil.esm.js +1 -1
- package/dist/types/classes/core/core.class.d.ts +16 -19
- package/dist/types/classes/handlers/key.handler.d.ts +1 -1
- package/dist/types/classes/managers/browser-permission.manager.d.ts +41 -0
- package/dist/types/classes/managers/clipboard.manager.d.ts +86 -0
- package/dist/types/classes/objects/image.class.d.ts +10 -0
- package/dist/types/classes/tools/image-tool.class.d.ts +0 -14
- package/dist/types/components/core/kritzel-editor/kritzel-editor.d.ts +20 -1
- package/dist/types/components/core/kritzel-engine/kritzel-engine.d.ts +8 -4
- package/dist/types/components/ui/kritzel-notification-card/kritzel-notification-card.d.ts +22 -0
- package/dist/types/components.d.ts +83 -9
- package/dist/types/constants/version.d.ts +1 -1
- package/dist/types/interfaces/engine-state.interface.d.ts +0 -3
- package/dist/types/interfaces/notification.interface.d.ts +10 -0
- package/dist/types/interfaces/theme.interface.d.ts +43 -0
- package/package.json +8 -2
- package/readme.md +7 -111
- package/dist/components/p-Bc55X65h.js +0 -1
- package/dist/components/p-C_fKgKHu.js +0 -9
- package/dist/components/p-Cu9KYyoq.js +0 -1
- package/dist/stencil/p-86348986.entry.js +0 -9
- package/dist/stencil/p-DchTXG3V.js +0 -1
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { KritzelImage } from "../objects/image.class";
|
|
2
|
+
import { KritzelText } from "../objects/text.class";
|
|
3
|
+
import { KritzelSelectionGroup } from "../objects/selection-group.class";
|
|
4
|
+
import { KritzelClassHelper } from "../../helpers/class.helper";
|
|
5
|
+
import { KritzelReviver } from "../core/reviver.class";
|
|
6
|
+
import imageCompression from "browser-image-compression";
|
|
7
|
+
const CLIPBOARD_PAYLOAD_PREFIX = 'kritzel-clipboard/v1:';
|
|
8
|
+
/**
|
|
9
|
+
* Manages clipboard operations (copy/cut/paste) for a core instance.
|
|
10
|
+
*
|
|
11
|
+
* The system clipboard is the single source of truth: copy serializes the current
|
|
12
|
+
* selection into a Kritzel payload, paste reads + revives it. No copied objects are
|
|
13
|
+
* cached on the engine state.
|
|
14
|
+
*/
|
|
15
|
+
export class KritzelClipboardManager {
|
|
16
|
+
_core;
|
|
17
|
+
/**
|
|
18
|
+
* In-memory cascade offset (world units) applied to successive pastes of the same
|
|
19
|
+
* clipboard content so they don't stack exactly on top of each other. Reset on copy/cut.
|
|
20
|
+
*/
|
|
21
|
+
_pasteOffset = 0;
|
|
22
|
+
constructor(core) {
|
|
23
|
+
this._core = core;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Serializes an object for the clipboard payload. Groups additionally embed their
|
|
27
|
+
* (recursively serialized) children under `__children__` so the payload is self-contained.
|
|
28
|
+
*/
|
|
29
|
+
serializeForClipboard(object) {
|
|
30
|
+
const serialized = (typeof object.serialize === 'function' ? object.serialize() : object);
|
|
31
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelGroup')) {
|
|
32
|
+
serialized.__children__ = object.children.map(child => this.serializeForClipboard(child));
|
|
33
|
+
}
|
|
34
|
+
return serialized;
|
|
35
|
+
}
|
|
36
|
+
serializeClipboardPayload(payload) {
|
|
37
|
+
return `${CLIPBOARD_PAYLOAD_PREFIX}${JSON.stringify(payload)}`;
|
|
38
|
+
}
|
|
39
|
+
parseClipboardPayload(text) {
|
|
40
|
+
if (!text || !text.startsWith(CLIPBOARD_PAYLOAD_PREFIX)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(text.slice(CLIPBOARD_PAYLOAD_PREFIX.length));
|
|
45
|
+
if (parsed?.version !== 1 || !Array.isArray(parsed.objects)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
determineTextPasteIntent(text) {
|
|
55
|
+
const payload = this.parseClipboardPayload(text);
|
|
56
|
+
if (payload) {
|
|
57
|
+
return { type: 'kritzel_objects', payload };
|
|
58
|
+
}
|
|
59
|
+
if (text && text.trim().length > 0) {
|
|
60
|
+
return { type: 'plain_text', text };
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
async writeBrowserClipboardPayload(payload, event) {
|
|
65
|
+
const serialized = this.serializeClipboardPayload(payload);
|
|
66
|
+
if (event?.clipboardData) {
|
|
67
|
+
event.clipboardData.setData('text/plain', serialized);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
await navigator.clipboard.writeText(serialized);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Ignore clipboard permission failures and keep the local clipboard working.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Revives a single serialized clipboard object (and its embedded group children).
|
|
82
|
+
* IDs are NOT regenerated here; see regenerateIdsAndRemapAnchors.
|
|
83
|
+
*/
|
|
84
|
+
reviveClipboardObject(serialized) {
|
|
85
|
+
const reviver = new KritzelReviver(this._core);
|
|
86
|
+
const { __children__, ...objectData } = serialized;
|
|
87
|
+
const revived = reviver.revive(objectData);
|
|
88
|
+
if (!revived) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (__children__ && KritzelClassHelper.isInstanceOf(revived, 'KritzelGroup')) {
|
|
92
|
+
const children = __children__.map(child => this.reviveClipboardObject(child)).filter((child) => child != null);
|
|
93
|
+
revived._pendingChildren = children;
|
|
94
|
+
revived.childIds = children.map(child => child.id);
|
|
95
|
+
}
|
|
96
|
+
return revived;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Assigns fresh unique IDs to every revived object (including nested group children),
|
|
100
|
+
* keeping group.childIds in sync and remapping line anchors that reference copied
|
|
101
|
+
* siblings to their new IDs. Anchors pointing at objects outside the pasted set are
|
|
102
|
+
* left untouched so they stay connected to the existing object.
|
|
103
|
+
*/
|
|
104
|
+
regenerateIdsAndRemapAnchors(topLevel) {
|
|
105
|
+
const all = [];
|
|
106
|
+
const collect = (objects) => {
|
|
107
|
+
for (const object of objects) {
|
|
108
|
+
all.push(object);
|
|
109
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelGroup')) {
|
|
110
|
+
collect(object._pendingChildren);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
collect(topLevel);
|
|
115
|
+
const idMap = new Map();
|
|
116
|
+
for (const object of all) {
|
|
117
|
+
const newId = object.generateId();
|
|
118
|
+
idMap.set(object.id, newId);
|
|
119
|
+
object.id = newId;
|
|
120
|
+
object.isMounted = false;
|
|
121
|
+
}
|
|
122
|
+
for (const object of all) {
|
|
123
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelGroup')) {
|
|
124
|
+
object.childIds = object.childIds.map(id => idMap.get(id) ?? id);
|
|
125
|
+
}
|
|
126
|
+
if (KritzelClassHelper.isInstanceOf(object, 'KritzelLine')) {
|
|
127
|
+
if (object.startAnchor && idMap.has(object.startAnchor.objectId)) {
|
|
128
|
+
object.startAnchor = { objectId: idMap.get(object.startAnchor.objectId) };
|
|
129
|
+
}
|
|
130
|
+
if (object.endAnchor && idMap.has(object.endAnchor.objectId)) {
|
|
131
|
+
object.endAnchor = { objectId: idMap.get(object.endAnchor.objectId) };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Evaluates the clipboard event to determine exactly what the user is trying to paste.
|
|
138
|
+
* Returns a PasteIntent covering the 3 main cases: Kritzel objects, Images, or Plain text.
|
|
139
|
+
*/
|
|
140
|
+
async determinePasteIntent(event) {
|
|
141
|
+
if (event?.clipboardData) {
|
|
142
|
+
const data = event.clipboardData;
|
|
143
|
+
const imageFiles = Array.from(data.files ?? []).filter(file => file.type.startsWith('image/'));
|
|
144
|
+
if (imageFiles.length > 0) {
|
|
145
|
+
return { type: 'image_files', files: imageFiles };
|
|
146
|
+
}
|
|
147
|
+
if (data.types.includes('text/plain')) {
|
|
148
|
+
const textIntent = this.determineTextPasteIntent(data.getData('text/plain'));
|
|
149
|
+
if (textIntent)
|
|
150
|
+
return textIntent;
|
|
151
|
+
}
|
|
152
|
+
return { type: 'unknown' };
|
|
153
|
+
}
|
|
154
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
|
155
|
+
return { type: 'unknown' };
|
|
156
|
+
}
|
|
157
|
+
const { clipboard } = navigator;
|
|
158
|
+
const permissionState = await this._core.permissionManager.checkPermission('clipboard-read');
|
|
159
|
+
if (permissionState !== 'granted') {
|
|
160
|
+
const requestedState = await this._core.permissionManager.requestPermission('clipboard-read');
|
|
161
|
+
if (requestedState !== 'granted') {
|
|
162
|
+
return { type: 'unknown' };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (clipboard.read) {
|
|
166
|
+
try {
|
|
167
|
+
const items = await clipboard.read();
|
|
168
|
+
const { imageFiles, text } = await this.processClipboardItems(items);
|
|
169
|
+
if (imageFiles.length > 0) {
|
|
170
|
+
return { type: 'image_files', files: imageFiles };
|
|
171
|
+
}
|
|
172
|
+
if (text !== null) {
|
|
173
|
+
const textIntent = this.determineTextPasteIntent(text);
|
|
174
|
+
if (textIntent)
|
|
175
|
+
return textIntent;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Fall back to readText below when read() is denied or unavailable at runtime.
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (clipboard.readText) {
|
|
183
|
+
const textIntent = this.determineTextPasteIntent(await clipboard.readText());
|
|
184
|
+
if (textIntent)
|
|
185
|
+
return textIntent;
|
|
186
|
+
}
|
|
187
|
+
return { type: 'unknown' };
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Helper to parse clipboard items in parallel
|
|
191
|
+
*/
|
|
192
|
+
async processClipboardItems(items) {
|
|
193
|
+
const imageFiles = [];
|
|
194
|
+
let text = null;
|
|
195
|
+
// Process items concurrently
|
|
196
|
+
await Promise.all(items.map(async (item) => {
|
|
197
|
+
const imageType = item.types.find(type => type.startsWith('image/'));
|
|
198
|
+
if (imageType) {
|
|
199
|
+
const blob = await item.getType(imageType);
|
|
200
|
+
imageFiles.push(new File([blob], 'pasted-image', { type: imageType }));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (text === null && item.types.includes('text/plain')) {
|
|
204
|
+
const blob = await item.getType('text/plain');
|
|
205
|
+
text = await blob.text();
|
|
206
|
+
}
|
|
207
|
+
}));
|
|
208
|
+
return { imageFiles, text };
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Copies currently selected objects into internal clipboard state.
|
|
212
|
+
*/
|
|
213
|
+
copy(event) {
|
|
214
|
+
const selectionGroup = this._core.store.selectionGroup;
|
|
215
|
+
if (!selectionGroup) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const payload = {
|
|
219
|
+
version: 1,
|
|
220
|
+
sourceWorkspaceId: this._core.store.state.activeWorkspace?.id,
|
|
221
|
+
objects: selectionGroup.objects.sort((a, b) => a.zIndex - b.zIndex).map(obj => this.serializeForClipboard(obj)),
|
|
222
|
+
};
|
|
223
|
+
// A fresh copy resets the cascade so the first paste lands at the default offset.
|
|
224
|
+
this._pasteOffset = 0;
|
|
225
|
+
void this.writeBrowserClipboardPayload(payload, event);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Cuts currently selected objects (copy then delete).
|
|
229
|
+
*/
|
|
230
|
+
cut(event) {
|
|
231
|
+
const selectionGroup = this._core.store.selectionGroup;
|
|
232
|
+
if (!selectionGroup) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
this.copy(event);
|
|
236
|
+
this._core.delete();
|
|
237
|
+
}
|
|
238
|
+
materializeKritzelObjects(intent) {
|
|
239
|
+
const objects = intent.payload.objects.map(object => this.reviveClipboardObject(object)).filter((object) => object != null);
|
|
240
|
+
this.regenerateIdsAndRemapAnchors(objects);
|
|
241
|
+
return objects;
|
|
242
|
+
}
|
|
243
|
+
async materializeImageFiles(intent) {
|
|
244
|
+
try {
|
|
245
|
+
const scale = this._core.store.state.scale;
|
|
246
|
+
const offset = 25 / scale;
|
|
247
|
+
const preparePromises = intent.files.map(async (file, index) => {
|
|
248
|
+
// Use the default image compression size of 1200
|
|
249
|
+
const compressed = await imageCompression(file, { maxWidthOrHeight: 1200 });
|
|
250
|
+
return KritzelImage.fromBlob(this._core, compressed, {
|
|
251
|
+
translateX: index * offset,
|
|
252
|
+
translateY: index * offset,
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
return await Promise.all(preparePromises);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
console.error('Failed to import pasted image files:', err);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async materializePlainText(intent) {
|
|
263
|
+
const textObject = KritzelText.create(this._core);
|
|
264
|
+
const paragraphs = intent.text.split('\n').map(line => {
|
|
265
|
+
if (line.length === 0) {
|
|
266
|
+
return { type: 'paragraph' }; // Empty paragraph
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
type: 'paragraph',
|
|
270
|
+
content: [{ type: 'text', text: line }],
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
textObject.setContent({
|
|
274
|
+
type: 'doc',
|
|
275
|
+
content: paragraphs,
|
|
276
|
+
});
|
|
277
|
+
return [textObject];
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Pastes objects from clipboard with optional explicit world coordinates.
|
|
281
|
+
*/
|
|
282
|
+
async paste(x, y, event) {
|
|
283
|
+
const pasteIntent = await this.determinePasteIntent(event);
|
|
284
|
+
let objects = null;
|
|
285
|
+
if (pasteIntent.type === 'kritzel_objects') {
|
|
286
|
+
objects = this.materializeKritzelObjects(pasteIntent);
|
|
287
|
+
}
|
|
288
|
+
else if (pasteIntent.type === 'image_files') {
|
|
289
|
+
objects = await this.materializeImageFiles(pasteIntent);
|
|
290
|
+
}
|
|
291
|
+
else if (pasteIntent.type === 'plain_text') {
|
|
292
|
+
objects = await this.materializePlainText(pasteIntent);
|
|
293
|
+
}
|
|
294
|
+
if (!objects || objects.length === 0) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
await this.insertMaterializedObjects(objects, pasteIntent, x, y);
|
|
298
|
+
}
|
|
299
|
+
async insertMaterializedObjects(copiedObjects, pasteIntent, x, y) {
|
|
300
|
+
const activeWorkspace = this._core.store.state.activeWorkspace;
|
|
301
|
+
if (!activeWorkspace) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// Determine if we should center the objects in the viewport.
|
|
305
|
+
// This happens if the user specifically pasted from a different workspace,
|
|
306
|
+
// OR if we're inserting brand new external items (like images or text).
|
|
307
|
+
const isDifferentWorkspace = copiedObjects.some(obj => obj.workspaceId !== activeWorkspace.id);
|
|
308
|
+
const isNewExternalContent = pasteIntent.type !== 'kritzel_objects';
|
|
309
|
+
const shouldCenterInViewport = isDifferentWorkspace || isNewExternalContent;
|
|
310
|
+
const hasExplicitPastePosition = x !== undefined && y !== undefined;
|
|
311
|
+
const minX = Math.min(...copiedObjects.map(obj => obj.translateX));
|
|
312
|
+
const minY = Math.min(...copiedObjects.map(obj => obj.translateY));
|
|
313
|
+
let pasteX;
|
|
314
|
+
let pasteY;
|
|
315
|
+
const step = 25 / this._core.store.state.scale;
|
|
316
|
+
if (x !== undefined && y !== undefined) {
|
|
317
|
+
pasteX = x;
|
|
318
|
+
pasteY = y;
|
|
319
|
+
}
|
|
320
|
+
else if (shouldCenterInViewport) {
|
|
321
|
+
pasteX = minX;
|
|
322
|
+
pasteY = minY;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
pasteX = minX + step + this._pasteOffset;
|
|
326
|
+
pasteY = minY + step + this._pasteOffset;
|
|
327
|
+
// Advance the cascade so the next same-content paste is offset further.
|
|
328
|
+
this._pasteOffset += step;
|
|
329
|
+
}
|
|
330
|
+
const offsetX = pasteX - minX;
|
|
331
|
+
const offsetY = pasteY - minY;
|
|
332
|
+
this._core.removeSelectionGroup();
|
|
333
|
+
this._core.removeSelectionBox();
|
|
334
|
+
const selectionGroup = KritzelSelectionGroup.create(this._core);
|
|
335
|
+
const baseZIndex = this._core.store.currentZIndex;
|
|
336
|
+
this._core.store.objects.transaction(() => {
|
|
337
|
+
const flushPendingChildren = (group, parentZIndex) => {
|
|
338
|
+
if (group._pendingChildren.length === 0) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const pendingChildren = group._pendingChildren;
|
|
342
|
+
group._pendingChildren = [];
|
|
343
|
+
pendingChildren.forEach((child, childIndex) => {
|
|
344
|
+
if (child.workspaceId !== activeWorkspace.id) {
|
|
345
|
+
child.workspaceId = activeWorkspace.id;
|
|
346
|
+
}
|
|
347
|
+
child.updatePosition(child.translateX + offsetX, child.translateY + offsetY);
|
|
348
|
+
child.zIndex = parentZIndex + childIndex + 1;
|
|
349
|
+
if (KritzelClassHelper.isInstanceOf(child, 'KritzelGroup')) {
|
|
350
|
+
flushPendingChildren(child, child.zIndex);
|
|
351
|
+
}
|
|
352
|
+
this._core.addObject(child);
|
|
353
|
+
});
|
|
354
|
+
group.finalize();
|
|
355
|
+
};
|
|
356
|
+
const newlyAddedObjects = [];
|
|
357
|
+
copiedObjects.forEach((obj, i) => {
|
|
358
|
+
if (obj.workspaceId !== activeWorkspace.id) {
|
|
359
|
+
obj.workspaceId = activeWorkspace.id;
|
|
360
|
+
}
|
|
361
|
+
obj.updatePosition(obj.translateX + offsetX, obj.translateY + offsetY);
|
|
362
|
+
obj.zIndex = baseZIndex + i;
|
|
363
|
+
if (KritzelClassHelper.isInstanceOf(obj, 'KritzelGroup')) {
|
|
364
|
+
flushPendingChildren(obj, obj.zIndex);
|
|
365
|
+
}
|
|
366
|
+
this._core.addObject(obj);
|
|
367
|
+
newlyAddedObjects.push(obj);
|
|
368
|
+
selectionGroup.addOrRemove(obj);
|
|
369
|
+
});
|
|
370
|
+
this._core.engine.emitObjectsAdded(newlyAddedObjects);
|
|
371
|
+
});
|
|
372
|
+
this._core.anchorManager.rebuildIndex();
|
|
373
|
+
selectionGroup.isSelected = true;
|
|
374
|
+
if (copiedObjects.length === 1) {
|
|
375
|
+
selectionGroup.rotation = copiedObjects[0].rotation;
|
|
376
|
+
}
|
|
377
|
+
if (shouldCenterInViewport && !hasExplicitPastePosition) {
|
|
378
|
+
selectionGroup.centerInViewport();
|
|
379
|
+
}
|
|
380
|
+
this._core.addSelectionGroup(selectionGroup);
|
|
381
|
+
this._core.engine.viewport?.bringIntoViewIfNeeded(selectionGroup);
|
|
382
|
+
this._core.store.state.isSelecting = false;
|
|
383
|
+
// Use a double requestAnimationFrame to ensure the browser has fully rendered the DOM,
|
|
384
|
+
// and objects like KritzelText have executed their own internal size-adjustment rAF calls.
|
|
385
|
+
await new Promise(resolve => {
|
|
386
|
+
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
|
387
|
+
});
|
|
388
|
+
// Re-evaluate bounding box dimensions now that any DOM nodes (like ProseMirror text editors)
|
|
389
|
+
// have mounted and calculated their actual bounds.
|
|
390
|
+
selectionGroup.refreshObjectDimensions();
|
|
391
|
+
// We should update it if the centerInViewport or width adjustments changed it significantly
|
|
392
|
+
if (shouldCenterInViewport && !hasExplicitPastePosition) {
|
|
393
|
+
selectionGroup.centerInViewport();
|
|
394
|
+
}
|
|
395
|
+
this._core.engine.emitObjectsChange();
|
|
396
|
+
this._core.rerender();
|
|
397
|
+
this._core.store.setState('activeTool', this._core.toolRegistry.getTool('selection'));
|
|
398
|
+
this._core.engine.emitObjectsChange();
|
|
399
|
+
this._core.rerender();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
@@ -429,6 +429,48 @@ export class KritzelImage extends KritzelBaseObject {
|
|
|
429
429
|
img.src = src;
|
|
430
430
|
});
|
|
431
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* Creates a fully initialized KritzelImage from a Blob.
|
|
434
|
+
* Uploads the blob via the asset resolver and sets up dimensions/asset references.
|
|
435
|
+
*
|
|
436
|
+
* @param core - The KritzelCore instance providing the asset resolver and state.
|
|
437
|
+
* @param blob - The image Blob (e.g. from clipboard or file picker).
|
|
438
|
+
* @param config - Optional configuration to merge into the new image.
|
|
439
|
+
* @returns A promise resolving to a fully loaded KritzelImage object.
|
|
440
|
+
*/
|
|
441
|
+
static async fromBlob(core, blob, config) {
|
|
442
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
443
|
+
try {
|
|
444
|
+
const dimensions = await new Promise((resolve, reject) => {
|
|
445
|
+
const img = new Image();
|
|
446
|
+
img.onload = () => resolve({ naturalWidth: img.naturalWidth, naturalHeight: img.naturalHeight });
|
|
447
|
+
img.onerror = err => reject(err);
|
|
448
|
+
img.src = objectUrl;
|
|
449
|
+
});
|
|
450
|
+
const mimeType = blob.type || 'image/png';
|
|
451
|
+
const asset = await core.assetResolver.put(blob, {
|
|
452
|
+
mimeType,
|
|
453
|
+
kind: 'image',
|
|
454
|
+
width: dimensions.naturalWidth,
|
|
455
|
+
height: dimensions.naturalHeight,
|
|
456
|
+
});
|
|
457
|
+
const image = KritzelImage.create(core, config);
|
|
458
|
+
image.assetId = asset.id;
|
|
459
|
+
image.mimeType = asset.mimeType;
|
|
460
|
+
const { scaledWidth, scaledHeight } = image.calculateScaledDimensions({
|
|
461
|
+
width: dimensions.naturalWidth,
|
|
462
|
+
height: dimensions.naturalHeight,
|
|
463
|
+
});
|
|
464
|
+
image.width = scaledWidth;
|
|
465
|
+
image.height = scaledHeight;
|
|
466
|
+
image.resolvedSrc = await core.assetResolver.resolve(asset.id);
|
|
467
|
+
image.loadState = 'ready';
|
|
468
|
+
return image;
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
URL.revokeObjectURL(objectUrl);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
432
474
|
/**
|
|
433
475
|
* Calculates scaled dimensions for an image while respecting maxWidth and maxHeight constraints.
|
|
434
476
|
* Maintains the original aspect ratio by using the smaller of the two scale ratios.
|
|
@@ -79,7 +79,7 @@ export class KritzelImageTool extends KritzelBaseTool {
|
|
|
79
79
|
}
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
|
-
const prepared = files.map(file => imageCompression(file, { maxWidthOrHeight: this.maxCompressionSize }).then(compressedFile => this.
|
|
82
|
+
const prepared = files.map(file => imageCompression(file, { maxWidthOrHeight: this.maxCompressionSize }).then(compressedFile => KritzelImage.fromBlob(this._core, compressedFile)));
|
|
83
83
|
Promise.allSettled(prepared)
|
|
84
84
|
.then(results => {
|
|
85
85
|
const images = [];
|
|
@@ -101,64 +101,6 @@ export class KritzelImageTool extends KritzelBaseTool {
|
|
|
101
101
|
input.value = '';
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
-
/**
|
|
105
|
-
* Compresses-then-measures an already compressed blob, uploads it
|
|
106
|
-
* through the asset resolver, and returns a fully configured
|
|
107
|
-
* `KritzelImage` that has not yet been added to the canvas.
|
|
108
|
-
*
|
|
109
|
-
* @param blob - Compressed image bytes to store and render
|
|
110
|
-
*/
|
|
111
|
-
async prepareImage(blob) {
|
|
112
|
-
let tempUrl = null;
|
|
113
|
-
try {
|
|
114
|
-
const mimeType = blob.type || 'image/png';
|
|
115
|
-
tempUrl = URL.createObjectURL(blob);
|
|
116
|
-
const dimensions = await this.measureImage(tempUrl);
|
|
117
|
-
const asset = await this._core.assetResolver.put(blob, {
|
|
118
|
-
mimeType,
|
|
119
|
-
kind: 'image',
|
|
120
|
-
width: dimensions.naturalWidth,
|
|
121
|
-
height: dimensions.naturalHeight,
|
|
122
|
-
});
|
|
123
|
-
const image = KritzelImage.create(this._core);
|
|
124
|
-
image.assetId = asset.id;
|
|
125
|
-
image.mimeType = asset.mimeType;
|
|
126
|
-
const { scaledWidth, scaledHeight } = image.calculateScaledDimensions({
|
|
127
|
-
width: dimensions.naturalWidth,
|
|
128
|
-
height: dimensions.naturalHeight,
|
|
129
|
-
});
|
|
130
|
-
image.width = scaledWidth;
|
|
131
|
-
image.height = scaledHeight;
|
|
132
|
-
// Seed the resolved URL so the renderer can display the image
|
|
133
|
-
// without waiting for a resolver round-trip.
|
|
134
|
-
image.resolvedSrc = await this._core.assetResolver.resolve(asset.id);
|
|
135
|
-
image.loadState = 'ready';
|
|
136
|
-
return image;
|
|
137
|
-
}
|
|
138
|
-
finally {
|
|
139
|
-
if (tempUrl) {
|
|
140
|
-
try {
|
|
141
|
-
URL.revokeObjectURL(tempUrl);
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
/* ignore */
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Measures the natural dimensions of an image from a URL. Used to
|
|
151
|
-
* provide width/height hints when uploading and to compute the
|
|
152
|
-
* scaled canvas dimensions.
|
|
153
|
-
*/
|
|
154
|
-
measureImage(url) {
|
|
155
|
-
return new Promise((resolve, reject) => {
|
|
156
|
-
const img = new Image();
|
|
157
|
-
img.onload = () => resolve({ naturalWidth: img.naturalWidth, naturalHeight: img.naturalHeight });
|
|
158
|
-
img.onerror = err => reject(err);
|
|
159
|
-
img.src = url;
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
104
|
/**
|
|
163
105
|
* Adds a batch of prepared images to the canvas store and switches
|
|
164
106
|
* to the selection tool. The first image is centered in the viewport;
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"components/ui/kritzel-export/kritzel-export.js",
|
|
40
40
|
"components/ui/kritzel-login-dialog/kritzel-login-dialog.js",
|
|
41
41
|
"components/ui/kritzel-more-menu/kritzel-more-menu.js",
|
|
42
|
+
"components/ui/kritzel-notification-card/kritzel-notification-card.js",
|
|
42
43
|
"components/ui/kritzel-settings/kritzel-settings.js",
|
|
43
44
|
"components/ui/kritzel-share-dialog/kritzel-share-dialog.js",
|
|
44
45
|
"components/ui/kritzel-tool-config/kritzel-tool-config.js",
|
|
@@ -44,6 +44,59 @@ kritzel-controls.keyboard-open {
|
|
|
44
44
|
gap: 8px;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
.top-center-notification-layer {
|
|
48
|
+
position: absolute;
|
|
49
|
+
top: var(--kritzel-editor-top-center-notification-top, 14px);
|
|
50
|
+
left: 0;
|
|
51
|
+
right: 0;
|
|
52
|
+
z-index: var(--kritzel-editor-top-center-notification-z-index, 10001);
|
|
53
|
+
display: flex;
|
|
54
|
+
justify-content: center;
|
|
55
|
+
pointer-events: none;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.top-center-notification-container {
|
|
59
|
+
width: min(90vw, 420px);
|
|
60
|
+
pointer-events: auto;
|
|
61
|
+
animation: kritzel-notification-slide-in var(--kritzel-editor-notification-animation-duration, 220ms)
|
|
62
|
+
var(--kritzel-editor-notification-animation-easing, cubic-bezier(0.22, 1, 0.36, 1));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.top-center-notification-container.is-dismissing {
|
|
66
|
+
animation: kritzel-notification-fade-out var(--kritzel-editor-notification-dismiss-animation-duration, 180ms)
|
|
67
|
+
var(--kritzel-editor-notification-dismiss-animation-easing, ease-in) forwards;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@keyframes kritzel-notification-slide-in {
|
|
71
|
+
from {
|
|
72
|
+
opacity: 0;
|
|
73
|
+
transform: translateY(-10px);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
to {
|
|
77
|
+
opacity: 1;
|
|
78
|
+
transform: translateY(0);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@keyframes kritzel-notification-fade-out {
|
|
83
|
+
from {
|
|
84
|
+
opacity: 1;
|
|
85
|
+
transform: translateY(0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
to {
|
|
89
|
+
opacity: 0;
|
|
90
|
+
transform: translateY(-6px);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@media (prefers-reduced-motion: reduce) {
|
|
95
|
+
.top-center-notification-container {
|
|
96
|
+
animation: none;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
47
100
|
.top-right-buttons {
|
|
48
101
|
position: absolute;
|
|
49
102
|
top: var(--kritzel-editor-top-right-buttons-top, 14px);
|