floatnote 1.0.0 → 1.0.6
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/bin/floatnote.js +16 -0
- package/package.json +13 -5
- package/.beads/config.json +0 -6
- package/.beads/issues/floatnote-1.md +0 -21
- package/.beads/issues/floatnote-10.md +0 -28
- package/.beads/issues/floatnote-11.md +0 -36
- package/.beads/issues/floatnote-12.md +0 -25
- package/.beads/issues/floatnote-13.md +0 -37
- package/.beads/issues/floatnote-14.md +0 -22
- package/.beads/issues/floatnote-15.md +0 -22
- package/.beads/issues/floatnote-16.md +0 -20
- package/.beads/issues/floatnote-17.md +0 -20
- package/.beads/issues/floatnote-18.md +0 -21
- package/.beads/issues/floatnote-19.md +0 -19
- package/.beads/issues/floatnote-2.md +0 -32
- package/.beads/issues/floatnote-20.md +0 -22
- package/.beads/issues/floatnote-3.md +0 -50
- package/.beads/issues/floatnote-4.md +0 -31
- package/.beads/issues/floatnote-5.md +0 -28
- package/.beads/issues/floatnote-6.md +0 -30
- package/.beads/issues/floatnote-7.md +0 -38
- package/.beads/issues/floatnote-8.md +0 -29
- package/.beads/issues/floatnote-9.md +0 -32
- package/CLAUDE.md +0 -61
- package/coverage/base.css +0 -224
- package/coverage/bin/floatnote.js.html +0 -739
- package/coverage/bin/index.html +0 -116
- package/coverage/block-navigation.js +0 -87
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -131
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/bin/floatnote.js.html +0 -739
- package/coverage/lcov-report/bin/index.html +0 -116
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -131
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov-report/src/index.html +0 -146
- package/coverage/lcov-report/src/main.js.html +0 -1483
- package/coverage/lcov-report/src/preload.js.html +0 -361
- package/coverage/lcov-report/src/renderer.js.html +0 -8767
- package/coverage/lcov.info +0 -3273
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/index.html +0 -146
- package/coverage/src/main.js.html +0 -1483
- package/coverage/src/preload.js.html +0 -361
- package/coverage/src/renderer.js.html +0 -8767
- package/jest.config.js +0 -48
- package/src/icon-template.png +0 -0
- package/src/index.html +0 -296
- package/src/main.js +0 -494
- package/src/preload.js +0 -96
- package/src/renderer.js +0 -3203
- package/src/styles.css +0 -1448
- package/tests/cli/floatnote.test.js +0 -167
- package/tests/main/main.test.js +0 -287
- package/tests/mocks/electron.js +0 -126
- package/tests/mocks/fs.js +0 -17
- package/tests/preload/preload.test.js +0 -218
- package/tests/renderer/history.test.js +0 -234
- package/tests/renderer/notes.test.js +0 -262
- package/tests/renderer/settings.test.js +0 -178
package/src/renderer.js
DELETED
|
@@ -1,3203 +0,0 @@
|
|
|
1
|
-
// Glassboard Renderer Process
|
|
2
|
-
// Handles drawing canvas, text overlays, and UI interactions
|
|
3
|
-
|
|
4
|
-
class Glassboard {
|
|
5
|
-
constructor() {
|
|
6
|
-
this.app = document.getElementById('app');
|
|
7
|
-
this.canvas = document.getElementById('draw-canvas');
|
|
8
|
-
this.ctx = this.canvas.getContext('2d');
|
|
9
|
-
this.textContainer = document.getElementById('text-container');
|
|
10
|
-
|
|
11
|
-
// Multi-note system
|
|
12
|
-
this.notes = []; // Array of notes
|
|
13
|
-
this.currentNoteIndex = 0;
|
|
14
|
-
|
|
15
|
-
this.isDrawing = false;
|
|
16
|
-
this.isTextMode = false;
|
|
17
|
-
this.isSelectMode = false;
|
|
18
|
-
this.currentColor = '#ffffff';
|
|
19
|
-
this.currentStrokeWidth = 4;
|
|
20
|
-
this.currentLine = null;
|
|
21
|
-
this.selectedTextId = null;
|
|
22
|
-
|
|
23
|
-
// Object grouping state
|
|
24
|
-
this.currentObjectId = null;
|
|
25
|
-
this.lastStrokeTime = 0;
|
|
26
|
-
this.objectGroupTimeout = 500; // ms - strokes within this time are grouped
|
|
27
|
-
|
|
28
|
-
// Selection state for drawing objects
|
|
29
|
-
this.selectedObjectId = null;
|
|
30
|
-
this.isDraggingObject = false;
|
|
31
|
-
this.dragStartPoint = null;
|
|
32
|
-
|
|
33
|
-
// Zoom state
|
|
34
|
-
this.zoomLevel = 1;
|
|
35
|
-
this.minZoom = 0.25;
|
|
36
|
-
this.maxZoom = 4;
|
|
37
|
-
|
|
38
|
-
// Pan state
|
|
39
|
-
this.panX = 0;
|
|
40
|
-
this.panY = 0;
|
|
41
|
-
|
|
42
|
-
// Rotation state
|
|
43
|
-
this.rotation = 0;
|
|
44
|
-
|
|
45
|
-
// Settings
|
|
46
|
-
this.settings = {
|
|
47
|
-
pinchZoom: true,
|
|
48
|
-
pan: true,
|
|
49
|
-
rotate: true,
|
|
50
|
-
showZoomControls: true,
|
|
51
|
-
openWithCleanSlate: false,
|
|
52
|
-
activeBgMode: 'transparent',
|
|
53
|
-
inactiveBgMode: 'transparent',
|
|
54
|
-
activeOpacity: 100,
|
|
55
|
-
inactiveOpacity: 50,
|
|
56
|
-
autoSaveToFolder: false
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// Selection rectangle state
|
|
60
|
-
this.isSelecting = false;
|
|
61
|
-
this.selectionStart = null;
|
|
62
|
-
this.selectionRect = document.getElementById('selection-rect');
|
|
63
|
-
|
|
64
|
-
// Multi-selection state
|
|
65
|
-
this.multiSelectedObjects = [];
|
|
66
|
-
|
|
67
|
-
// Clipboard state (internal)
|
|
68
|
-
this.clipboard = null; // { type: 'object' | 'text', data: ... }
|
|
69
|
-
|
|
70
|
-
// System clipboard state
|
|
71
|
-
this.systemClipboard = null; // Content from system clipboard
|
|
72
|
-
this.pasteOverlayVisible = false;
|
|
73
|
-
|
|
74
|
-
// Undo/redo history
|
|
75
|
-
this.history = [];
|
|
76
|
-
this.historyIndex = -1;
|
|
77
|
-
this.maxHistorySize = 50;
|
|
78
|
-
this.isUndoRedoAction = false;
|
|
79
|
-
|
|
80
|
-
// Auto-save debounce
|
|
81
|
-
this.saveTimeout = null;
|
|
82
|
-
|
|
83
|
-
// Select all state
|
|
84
|
-
this.allSelected = false;
|
|
85
|
-
this.selectedObjects = [];
|
|
86
|
-
|
|
87
|
-
// Double command key tracking
|
|
88
|
-
this.lastRightCommandTime = 0;
|
|
89
|
-
|
|
90
|
-
this.init();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
init() {
|
|
94
|
-
this.setupCanvas();
|
|
95
|
-
this.setupToolbar();
|
|
96
|
-
this.setupDrawing();
|
|
97
|
-
this.setupTextMode();
|
|
98
|
-
this.setupKeyboardShortcuts();
|
|
99
|
-
this.setupFocusHandling();
|
|
100
|
-
this.setupResize();
|
|
101
|
-
this.setupZoom();
|
|
102
|
-
this.setupContextMenu();
|
|
103
|
-
this.setupClipboardPaste();
|
|
104
|
-
this.setupSettings();
|
|
105
|
-
this.setupGestures();
|
|
106
|
-
this.setupFileDrop();
|
|
107
|
-
|
|
108
|
-
// Initialize with empty note if needed
|
|
109
|
-
if (this.notes.length === 0) {
|
|
110
|
-
this.notes.push(this.createEmptyNote());
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Load saved data
|
|
114
|
-
this.loadSavedData();
|
|
115
|
-
|
|
116
|
-
// Setup left edge resize
|
|
117
|
-
this.setupLeftResize();
|
|
118
|
-
|
|
119
|
-
// Setup pagination controls
|
|
120
|
-
this.setupPagination();
|
|
121
|
-
|
|
122
|
-
// Setup window toggle handler for clean slate
|
|
123
|
-
this.setupWindowToggleHandler();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Note management - getters for current note's data
|
|
127
|
-
get lines() {
|
|
128
|
-
return this.notes[this.currentNoteIndex]?.lines || [];
|
|
129
|
-
}
|
|
130
|
-
set lines(value) {
|
|
131
|
-
if (this.notes[this.currentNoteIndex]) {
|
|
132
|
-
this.notes[this.currentNoteIndex].lines = value;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
get textItems() {
|
|
137
|
-
return this.notes[this.currentNoteIndex]?.textItems || [];
|
|
138
|
-
}
|
|
139
|
-
set textItems(value) {
|
|
140
|
-
if (this.notes[this.currentNoteIndex]) {
|
|
141
|
-
this.notes[this.currentNoteIndex].textItems = value;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
get images() {
|
|
146
|
-
return this.notes[this.currentNoteIndex]?.images || [];
|
|
147
|
-
}
|
|
148
|
-
set images(value) {
|
|
149
|
-
if (this.notes[this.currentNoteIndex]) {
|
|
150
|
-
this.notes[this.currentNoteIndex].images = value;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
get attachments() {
|
|
155
|
-
return this.notes[this.currentNoteIndex]?.attachments || [];
|
|
156
|
-
}
|
|
157
|
-
set attachments(value) {
|
|
158
|
-
if (this.notes[this.currentNoteIndex]) {
|
|
159
|
-
this.notes[this.currentNoteIndex].attachments = value;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
createEmptyNote() {
|
|
164
|
-
// Get viewport dimensions for origin calculation
|
|
165
|
-
const rect = this.canvas?.getBoundingClientRect() || { width: 800, height: 600 };
|
|
166
|
-
const zoom = this.zoomLevel || 1;
|
|
167
|
-
return {
|
|
168
|
-
id: Date.now().toString(),
|
|
169
|
-
lines: [],
|
|
170
|
-
textItems: [],
|
|
171
|
-
images: [],
|
|
172
|
-
attachments: [],
|
|
173
|
-
// Store the origin point in content-space (center of viewport when note was created)
|
|
174
|
-
originX: (rect.width / 2) / zoom,
|
|
175
|
-
originY: (rect.height / 2) / zoom,
|
|
176
|
-
createdAt: Date.now(),
|
|
177
|
-
lastModified: Date.now()
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Navigate to previous note
|
|
182
|
-
previousNote() {
|
|
183
|
-
if (this.currentNoteIndex > 0) {
|
|
184
|
-
this.saveCurrentNoteState();
|
|
185
|
-
this.currentNoteIndex--;
|
|
186
|
-
this.loadCurrentNote();
|
|
187
|
-
this.updateNoteIndicator();
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Navigate to next note or create new one
|
|
192
|
-
nextNote() {
|
|
193
|
-
this.saveCurrentNoteState();
|
|
194
|
-
|
|
195
|
-
if (this.currentNoteIndex < this.notes.length - 1) {
|
|
196
|
-
// Go to existing next note
|
|
197
|
-
this.currentNoteIndex++;
|
|
198
|
-
} else {
|
|
199
|
-
// Create new note
|
|
200
|
-
this.notes.push(this.createEmptyNote());
|
|
201
|
-
this.currentNoteIndex = this.notes.length - 1;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
this.loadCurrentNote();
|
|
205
|
-
this.updateNoteIndicator();
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Save current note state before switching
|
|
209
|
-
saveCurrentNoteState() {
|
|
210
|
-
if (this.notes[this.currentNoteIndex]) {
|
|
211
|
-
this.notes[this.currentNoteIndex].lastModified = Date.now();
|
|
212
|
-
}
|
|
213
|
-
this.autoSave();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Load and display current note
|
|
217
|
-
loadCurrentNote() {
|
|
218
|
-
// Clear current display
|
|
219
|
-
this.clearDisplay();
|
|
220
|
-
|
|
221
|
-
const note = this.notes[this.currentNoteIndex];
|
|
222
|
-
if (!note) return;
|
|
223
|
-
|
|
224
|
-
// Restore text items
|
|
225
|
-
note.textItems.forEach(item => {
|
|
226
|
-
this.restoreTextItem(item);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// Restore images
|
|
230
|
-
note.images.forEach(img => {
|
|
231
|
-
this.restoreImage(img);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
// Redraw canvas with lines
|
|
235
|
-
this.redraw();
|
|
236
|
-
|
|
237
|
-
// Reset history for this note
|
|
238
|
-
this.history = [];
|
|
239
|
-
this.historyIndex = -1;
|
|
240
|
-
this.saveState();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Clear the display without affecting note data
|
|
244
|
-
clearDisplay() {
|
|
245
|
-
this.textContainer.querySelectorAll('.text-item').forEach(el => el.remove());
|
|
246
|
-
this.textContainer.querySelectorAll('.pasted-image').forEach(el => el.remove());
|
|
247
|
-
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
248
|
-
this.selectedTextId = null;
|
|
249
|
-
this.selectedObjectId = null;
|
|
250
|
-
this.allSelected = false;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Update note indicator in UI
|
|
254
|
-
updateNoteIndicator() {
|
|
255
|
-
// Update pagination counter
|
|
256
|
-
this.updatePaginationCounter();
|
|
257
|
-
|
|
258
|
-
// Show temporary indicator
|
|
259
|
-
let indicator = document.getElementById('note-indicator');
|
|
260
|
-
if (!indicator) {
|
|
261
|
-
indicator = document.createElement('div');
|
|
262
|
-
indicator.id = 'note-indicator';
|
|
263
|
-
this.app.appendChild(indicator);
|
|
264
|
-
}
|
|
265
|
-
indicator.textContent = `${this.currentNoteIndex + 1} / ${this.notes.length}`;
|
|
266
|
-
indicator.classList.add('visible');
|
|
267
|
-
|
|
268
|
-
// Hide after 1.5 seconds
|
|
269
|
-
clearTimeout(this.noteIndicatorTimeout);
|
|
270
|
-
this.noteIndicatorTimeout = setTimeout(() => {
|
|
271
|
-
indicator.classList.remove('visible');
|
|
272
|
-
}, 1500);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
setupCanvas() {
|
|
276
|
-
const resize = () => {
|
|
277
|
-
const rect = this.canvas.parentElement.getBoundingClientRect();
|
|
278
|
-
const dpr = window.devicePixelRatio || 1;
|
|
279
|
-
this.canvas.width = rect.width * dpr;
|
|
280
|
-
this.canvas.height = rect.height * dpr;
|
|
281
|
-
this.canvas.style.width = rect.width + 'px';
|
|
282
|
-
this.canvas.style.height = rect.height + 'px';
|
|
283
|
-
this.ctx.scale(dpr, dpr);
|
|
284
|
-
this.redraw();
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
resize();
|
|
288
|
-
window.addEventListener('resize', resize);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
setupToolbar() {
|
|
292
|
-
// Mode toggle
|
|
293
|
-
const selectModeBtn = document.getElementById('select-mode');
|
|
294
|
-
const drawModeBtn = document.getElementById('draw-mode');
|
|
295
|
-
const textModeBtn = document.getElementById('text-mode');
|
|
296
|
-
|
|
297
|
-
const setMode = (mode) => {
|
|
298
|
-
this.isSelectMode = mode === 'select';
|
|
299
|
-
this.isTextMode = mode === 'text';
|
|
300
|
-
|
|
301
|
-
// Update button states
|
|
302
|
-
selectModeBtn.classList.toggle('active', mode === 'select');
|
|
303
|
-
drawModeBtn.classList.toggle('active', mode === 'draw');
|
|
304
|
-
textModeBtn.classList.toggle('active', mode === 'text');
|
|
305
|
-
|
|
306
|
-
// Update canvas/text container interactions
|
|
307
|
-
if (mode === 'select') {
|
|
308
|
-
this.canvas.style.pointerEvents = 'auto';
|
|
309
|
-
this.canvas.style.cursor = 'default';
|
|
310
|
-
this.textContainer.style.pointerEvents = 'auto';
|
|
311
|
-
this.textContainer.style.cursor = 'default';
|
|
312
|
-
} else if (mode === 'draw') {
|
|
313
|
-
this.canvas.style.pointerEvents = 'auto';
|
|
314
|
-
this.canvas.style.cursor = 'crosshair';
|
|
315
|
-
this.textContainer.style.pointerEvents = 'none';
|
|
316
|
-
this.textContainer.style.cursor = 'default';
|
|
317
|
-
} else if (mode === 'text') {
|
|
318
|
-
this.canvas.style.pointerEvents = 'none';
|
|
319
|
-
this.textContainer.style.pointerEvents = 'auto';
|
|
320
|
-
this.textContainer.style.cursor = 'crosshair';
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
// Store setMode for keyboard shortcuts
|
|
325
|
-
this.setMode = setMode;
|
|
326
|
-
|
|
327
|
-
selectModeBtn.addEventListener('click', () => setMode('select'));
|
|
328
|
-
drawModeBtn.addEventListener('click', () => setMode('draw'));
|
|
329
|
-
textModeBtn.addEventListener('click', () => setMode('text'));
|
|
330
|
-
|
|
331
|
-
// Color picker dropdown
|
|
332
|
-
const colorBtns = document.querySelectorAll('.color-grid .color-btn');
|
|
333
|
-
const currentColorSwatch = document.querySelector('.current-color');
|
|
334
|
-
colorBtns.forEach(btn => {
|
|
335
|
-
btn.addEventListener('click', () => {
|
|
336
|
-
colorBtns.forEach(b => b.classList.remove('active'));
|
|
337
|
-
btn.classList.add('active');
|
|
338
|
-
this.currentColor = btn.dataset.color;
|
|
339
|
-
if (currentColorSwatch) {
|
|
340
|
-
currentColorSwatch.style.background = btn.dataset.color;
|
|
341
|
-
}
|
|
342
|
-
// Update selected object's color
|
|
343
|
-
if (this.selectedObjectId) {
|
|
344
|
-
this.changeObjectColor(this.selectedObjectId, btn.dataset.color);
|
|
345
|
-
}
|
|
346
|
-
// Update selected text item's color
|
|
347
|
-
if (this.selectedTextId) {
|
|
348
|
-
this.changeTextColor(this.selectedTextId, btn.dataset.color);
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
// Stroke width dropdown
|
|
354
|
-
const strokeOptions = document.querySelectorAll('.stroke-option');
|
|
355
|
-
const currentStroke = document.querySelector('.current-stroke');
|
|
356
|
-
strokeOptions.forEach(btn => {
|
|
357
|
-
btn.addEventListener('click', () => {
|
|
358
|
-
strokeOptions.forEach(b => b.classList.remove('active'));
|
|
359
|
-
btn.classList.add('active');
|
|
360
|
-
this.currentStrokeWidth = parseInt(btn.dataset.width);
|
|
361
|
-
if (currentStroke) {
|
|
362
|
-
const size = Math.min(16, 4 + parseInt(btn.dataset.width));
|
|
363
|
-
currentStroke.style.width = size + 'px';
|
|
364
|
-
currentStroke.style.height = size + 'px';
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
// Clear button
|
|
370
|
-
document.getElementById('clear-btn').addEventListener('click', () => {
|
|
371
|
-
this.clear();
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
// Background mode toggle
|
|
375
|
-
this.setupBackgroundToggle();
|
|
376
|
-
|
|
377
|
-
// Pin toggle
|
|
378
|
-
this.setupPinToggle();
|
|
379
|
-
|
|
380
|
-
// Size toggle
|
|
381
|
-
this.setupSizeToggle();
|
|
382
|
-
|
|
383
|
-
// Create zoom controls
|
|
384
|
-
this.createZoomControls();
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
setupPinToggle() {
|
|
388
|
-
const pinCheckbox = document.getElementById('pin-checkbox');
|
|
389
|
-
if (pinCheckbox) {
|
|
390
|
-
pinCheckbox.addEventListener('change', () => {
|
|
391
|
-
window.glassboard.setPinned(pinCheckbox.checked);
|
|
392
|
-
this.animatePinIcon();
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
togglePin() {
|
|
398
|
-
const pinCheckbox = document.getElementById('pin-checkbox');
|
|
399
|
-
if (pinCheckbox) {
|
|
400
|
-
pinCheckbox.checked = !pinCheckbox.checked;
|
|
401
|
-
window.glassboard.setPinned(pinCheckbox.checked);
|
|
402
|
-
this.animatePinIcon();
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
animatePinIcon() {
|
|
407
|
-
const pinToggle = document.querySelector('.pin-toggle');
|
|
408
|
-
if (pinToggle) {
|
|
409
|
-
pinToggle.classList.add('animate');
|
|
410
|
-
setTimeout(() => {
|
|
411
|
-
pinToggle.classList.remove('animate');
|
|
412
|
-
}, 400);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
setupSizeToggle() {
|
|
417
|
-
const sizeItems = document.querySelectorAll('.dropdown-item[data-size]');
|
|
418
|
-
sizeItems.forEach(item => {
|
|
419
|
-
item.addEventListener('click', () => {
|
|
420
|
-
sizeItems.forEach(i => i.classList.remove('active'));
|
|
421
|
-
item.classList.add('active');
|
|
422
|
-
window.glassboard.setWindowSize(item.dataset.size);
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
updateSizeDropdown(size) {
|
|
428
|
-
const sizeItems = document.querySelectorAll('.dropdown-item[data-size]');
|
|
429
|
-
sizeItems.forEach(item => {
|
|
430
|
-
item.classList.toggle('active', item.dataset.size === size);
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
createZoomControls() {
|
|
435
|
-
const zoomControls = document.createElement('div');
|
|
436
|
-
zoomControls.id = 'zoom-controls';
|
|
437
|
-
zoomControls.innerHTML = `
|
|
438
|
-
<button class="zoom-btn" id="zoom-out" title="Zoom out (Cmd+-)">−</button>
|
|
439
|
-
<span id="zoom-indicator">100%</span>
|
|
440
|
-
<button class="zoom-btn" id="zoom-in" title="Zoom in (Cmd++)">+</button>
|
|
441
|
-
<button class="zoom-btn" id="zoom-reset" title="Reset view (Cmd+0)">⟲</button>
|
|
442
|
-
`;
|
|
443
|
-
this.app.appendChild(zoomControls);
|
|
444
|
-
|
|
445
|
-
document.getElementById('zoom-out').addEventListener('click', () => this.zoomOut());
|
|
446
|
-
document.getElementById('zoom-in').addEventListener('click', () => this.zoomIn());
|
|
447
|
-
document.getElementById('zoom-reset').addEventListener('click', () => this.resetTransform());
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
setupBackgroundToggle() {
|
|
451
|
-
const dropdownItems = document.querySelectorAll('.dropdown-item[data-bg]');
|
|
452
|
-
|
|
453
|
-
// Set initial background mode
|
|
454
|
-
this.app.classList.add('bg-transparent');
|
|
455
|
-
this.currentBgMode = 'transparent';
|
|
456
|
-
|
|
457
|
-
dropdownItems.forEach(item => {
|
|
458
|
-
item.addEventListener('click', () => {
|
|
459
|
-
// Update active state
|
|
460
|
-
dropdownItems.forEach(i => i.classList.remove('active'));
|
|
461
|
-
item.classList.add('active');
|
|
462
|
-
|
|
463
|
-
// Update background class
|
|
464
|
-
const bgMode = item.dataset.bg;
|
|
465
|
-
this.currentBgMode = bgMode;
|
|
466
|
-
this.app.classList.remove('bg-transparent', 'bg-blur', 'bg-dark');
|
|
467
|
-
this.app.classList.add(`bg-${bgMode}`);
|
|
468
|
-
|
|
469
|
-
// Tell main process to update vibrancy (for blur effect)
|
|
470
|
-
window.glassboard.setBackgroundMode(bgMode);
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
setupDrawing() {
|
|
476
|
-
const getPoint = (e) => {
|
|
477
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
478
|
-
let x, y;
|
|
479
|
-
if (e.touches) {
|
|
480
|
-
x = e.touches[0].clientX - rect.left;
|
|
481
|
-
y = e.touches[0].clientY - rect.top;
|
|
482
|
-
} else {
|
|
483
|
-
x = e.clientX - rect.left;
|
|
484
|
-
y = e.clientY - rect.top;
|
|
485
|
-
}
|
|
486
|
-
// Clamp coordinates to canvas bounds and adjust for zoom
|
|
487
|
-
x = Math.max(0, Math.min(x, rect.width)) / this.zoomLevel;
|
|
488
|
-
y = Math.max(0, Math.min(y, rect.height)) / this.zoomLevel;
|
|
489
|
-
return { x, y };
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
const startDrawing = (e) => {
|
|
493
|
-
if (this.isTextMode) return;
|
|
494
|
-
|
|
495
|
-
const point = getPoint(e);
|
|
496
|
-
|
|
497
|
-
// Check if clicking on an existing stroke (for selection)
|
|
498
|
-
const clickedObjectId = this.findObjectAtPoint(point);
|
|
499
|
-
if (clickedObjectId) {
|
|
500
|
-
// Select the object
|
|
501
|
-
this.selectObject(clickedObjectId);
|
|
502
|
-
// Start dragging
|
|
503
|
-
this.isDraggingObject = true;
|
|
504
|
-
this.dragStartPoint = point;
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Deselect if clicking empty space
|
|
509
|
-
this.deselectObject();
|
|
510
|
-
this.clearMultiSelection();
|
|
511
|
-
|
|
512
|
-
// In select mode, start drag-box selection
|
|
513
|
-
if (this.isSelectMode) {
|
|
514
|
-
this.isSelecting = true;
|
|
515
|
-
this.selectionStart = point;
|
|
516
|
-
this.selectionRect.style.left = point.x + 'px';
|
|
517
|
-
this.selectionRect.style.top = point.y + 'px';
|
|
518
|
-
this.selectionRect.style.width = '0px';
|
|
519
|
-
this.selectionRect.style.height = '0px';
|
|
520
|
-
this.selectionRect.classList.add('active');
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// Start new drawing (only in draw mode)
|
|
525
|
-
this.isDrawing = true;
|
|
526
|
-
this.app.classList.add('drawing');
|
|
527
|
-
|
|
528
|
-
// Determine object ID based on timing
|
|
529
|
-
const now = Date.now();
|
|
530
|
-
if (now - this.lastStrokeTime > this.objectGroupTimeout) {
|
|
531
|
-
this.currentObjectId = now.toString();
|
|
532
|
-
}
|
|
533
|
-
this.lastStrokeTime = now;
|
|
534
|
-
|
|
535
|
-
this.currentLine = {
|
|
536
|
-
points: [point],
|
|
537
|
-
color: this.currentColor,
|
|
538
|
-
width: this.currentStrokeWidth,
|
|
539
|
-
objectId: this.currentObjectId
|
|
540
|
-
};
|
|
541
|
-
};
|
|
542
|
-
|
|
543
|
-
const draw = (e) => {
|
|
544
|
-
if (this.isTextMode) return;
|
|
545
|
-
e.preventDefault();
|
|
546
|
-
|
|
547
|
-
const point = getPoint(e);
|
|
548
|
-
|
|
549
|
-
// Handle drag-box selection
|
|
550
|
-
if (this.isSelecting && this.selectionStart) {
|
|
551
|
-
const x = Math.min(point.x, this.selectionStart.x);
|
|
552
|
-
const y = Math.min(point.y, this.selectionStart.y);
|
|
553
|
-
const width = Math.abs(point.x - this.selectionStart.x);
|
|
554
|
-
const height = Math.abs(point.y - this.selectionStart.y);
|
|
555
|
-
this.selectionRect.style.left = x + 'px';
|
|
556
|
-
this.selectionRect.style.top = y + 'px';
|
|
557
|
-
this.selectionRect.style.width = width + 'px';
|
|
558
|
-
this.selectionRect.style.height = height + 'px';
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Handle object dragging
|
|
563
|
-
if (this.isDraggingObject && this.selectedObjectId) {
|
|
564
|
-
const dx = point.x - this.dragStartPoint.x;
|
|
565
|
-
const dy = point.y - this.dragStartPoint.y;
|
|
566
|
-
this.moveObject(this.selectedObjectId, dx, dy);
|
|
567
|
-
this.dragStartPoint = point;
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
if (!this.isDrawing) return;
|
|
572
|
-
this.currentLine.points.push(point);
|
|
573
|
-
this.redraw();
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
const stopDrawing = (e) => {
|
|
577
|
-
// Handle drag-box selection completion
|
|
578
|
-
if (this.isSelecting) {
|
|
579
|
-
this.isSelecting = false;
|
|
580
|
-
this.selectionRect.classList.remove('active');
|
|
581
|
-
|
|
582
|
-
if (this.selectionStart) {
|
|
583
|
-
const point = e ? getPoint(e) : this.selectionStart;
|
|
584
|
-
const rect = {
|
|
585
|
-
x: Math.min(point.x, this.selectionStart.x),
|
|
586
|
-
y: Math.min(point.y, this.selectionStart.y),
|
|
587
|
-
width: Math.abs(point.x - this.selectionStart.x),
|
|
588
|
-
height: Math.abs(point.y - this.selectionStart.y)
|
|
589
|
-
};
|
|
590
|
-
this.selectObjectsInRect(rect);
|
|
591
|
-
}
|
|
592
|
-
this.selectionStart = null;
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
if (this.isDraggingObject) {
|
|
597
|
-
this.isDraggingObject = false;
|
|
598
|
-
this.dragStartPoint = null;
|
|
599
|
-
this.saveState(); // Save after dragging object
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
if (!this.isDrawing) return;
|
|
604
|
-
this.isDrawing = false;
|
|
605
|
-
this.app.classList.remove('drawing');
|
|
606
|
-
if (this.currentLine && this.currentLine.points.length > 1) {
|
|
607
|
-
this.lines.push(this.currentLine);
|
|
608
|
-
this.saveState(); // Save after drawing
|
|
609
|
-
}
|
|
610
|
-
this.currentLine = null;
|
|
611
|
-
};
|
|
612
|
-
|
|
613
|
-
// Mouse events - use document for move/up to continue drawing over other elements
|
|
614
|
-
this.canvas.addEventListener('mousedown', startDrawing);
|
|
615
|
-
document.addEventListener('mousemove', draw);
|
|
616
|
-
document.addEventListener('mouseup', stopDrawing);
|
|
617
|
-
|
|
618
|
-
// Touch events - use document for move/end to continue drawing over other elements
|
|
619
|
-
this.canvas.addEventListener('touchstart', startDrawing, { passive: false });
|
|
620
|
-
document.addEventListener('touchmove', draw, { passive: false });
|
|
621
|
-
document.addEventListener('touchend', stopDrawing);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Find which object (if any) is at the given point
|
|
625
|
-
findObjectAtPoint(point) {
|
|
626
|
-
const hitRadius = 10; // pixels tolerance for clicking
|
|
627
|
-
for (let i = this.lines.length - 1; i >= 0; i--) {
|
|
628
|
-
const line = this.lines[i];
|
|
629
|
-
for (const p of line.points) {
|
|
630
|
-
const dist = Math.sqrt((p.x - point.x) ** 2 + (p.y - point.y) ** 2);
|
|
631
|
-
if (dist < hitRadius + line.width / 2) {
|
|
632
|
-
return line.objectId;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
return null;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// Select all strokes belonging to an object
|
|
640
|
-
selectObject(objectId) {
|
|
641
|
-
this.selectedObjectId = objectId;
|
|
642
|
-
this.redraw();
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// Deselect current object
|
|
646
|
-
deselectObject() {
|
|
647
|
-
this.selectedObjectId = null;
|
|
648
|
-
this.redraw();
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Move all strokes of an object by dx, dy
|
|
652
|
-
moveObject(objectId, dx, dy) {
|
|
653
|
-
this.lines.forEach(line => {
|
|
654
|
-
if (line.objectId === objectId) {
|
|
655
|
-
line.points.forEach(p => {
|
|
656
|
-
p.x += dx;
|
|
657
|
-
p.y += dy;
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
});
|
|
661
|
-
this.redraw();
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Delete all strokes of an object
|
|
665
|
-
deleteObject(objectId) {
|
|
666
|
-
this.lines = this.lines.filter(line => line.objectId !== objectId);
|
|
667
|
-
if (this.selectedObjectId === objectId) {
|
|
668
|
-
this.selectedObjectId = null;
|
|
669
|
-
}
|
|
670
|
-
this.redraw();
|
|
671
|
-
this.saveState();
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// Select all objects within a rectangle
|
|
675
|
-
selectObjectsInRect(rect) {
|
|
676
|
-
const selectedIds = new Set();
|
|
677
|
-
|
|
678
|
-
// Find all objects with points inside the rectangle
|
|
679
|
-
this.lines.forEach(line => {
|
|
680
|
-
for (const p of line.points) {
|
|
681
|
-
if (p.x >= rect.x && p.x <= rect.x + rect.width &&
|
|
682
|
-
p.y >= rect.y && p.y <= rect.y + rect.height) {
|
|
683
|
-
selectedIds.add(line.objectId);
|
|
684
|
-
break;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
this.multiSelectedObjects = Array.from(selectedIds);
|
|
690
|
-
|
|
691
|
-
// If only one object, use single selection
|
|
692
|
-
if (this.multiSelectedObjects.length === 1) {
|
|
693
|
-
this.selectObject(this.multiSelectedObjects[0]);
|
|
694
|
-
this.multiSelectedObjects = [];
|
|
695
|
-
} else if (this.multiSelectedObjects.length > 1) {
|
|
696
|
-
this.redraw();
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// Clear multi-selection
|
|
701
|
-
clearMultiSelection() {
|
|
702
|
-
this.multiSelectedObjects = [];
|
|
703
|
-
this.redraw();
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// Check if object is in multi-selection
|
|
707
|
-
isObjectMultiSelected(objectId) {
|
|
708
|
-
return this.multiSelectedObjects.includes(objectId);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// Setup pagination controls
|
|
712
|
-
setupPagination() {
|
|
713
|
-
const prevBtn = document.getElementById('prev-note');
|
|
714
|
-
const nextBtn = document.getElementById('next-note');
|
|
715
|
-
const newNoteBtn = document.getElementById('new-note');
|
|
716
|
-
const counter = document.getElementById('note-counter');
|
|
717
|
-
|
|
718
|
-
if (prevBtn) {
|
|
719
|
-
prevBtn.addEventListener('click', () => this.previousNote());
|
|
720
|
-
}
|
|
721
|
-
if (nextBtn) {
|
|
722
|
-
nextBtn.addEventListener('click', () => this.nextNote());
|
|
723
|
-
}
|
|
724
|
-
if (newNoteBtn) {
|
|
725
|
-
newNoteBtn.addEventListener('click', () => this.createNewNoteAndSwitch());
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// Update counter display
|
|
729
|
-
this.updatePaginationCounter();
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// Create a new note and switch to it
|
|
733
|
-
createNewNoteAndSwitch() {
|
|
734
|
-
this.saveCurrentNoteState();
|
|
735
|
-
this.notes.push(this.createEmptyNote());
|
|
736
|
-
this.currentNoteIndex = this.notes.length - 1;
|
|
737
|
-
this.loadCurrentNote();
|
|
738
|
-
this.updateNoteIndicator();
|
|
739
|
-
this.updatePaginationCounter();
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
// Update pagination counter display
|
|
743
|
-
updatePaginationCounter() {
|
|
744
|
-
const counter = document.getElementById('note-counter');
|
|
745
|
-
if (counter) {
|
|
746
|
-
counter.textContent = `${this.currentNoteIndex + 1}/${this.notes.length}`;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// Update button states
|
|
750
|
-
const prevBtn = document.getElementById('prev-note');
|
|
751
|
-
const nextBtn = document.getElementById('next-note');
|
|
752
|
-
if (prevBtn) {
|
|
753
|
-
prevBtn.disabled = this.currentNoteIndex === 0;
|
|
754
|
-
}
|
|
755
|
-
if (nextBtn) {
|
|
756
|
-
nextBtn.disabled = false; // Always enabled (creates new note)
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// Handle window toggle for clean slate mode
|
|
761
|
-
setupWindowToggleHandler() {
|
|
762
|
-
if (window.glassboard?.onWindowToggledOpen) {
|
|
763
|
-
window.glassboard.onWindowToggledOpen(() => {
|
|
764
|
-
this.handleWindowToggledOpen();
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
handleWindowToggledOpen() {
|
|
770
|
-
// Only activate clean slate behavior if setting is enabled
|
|
771
|
-
if (!this.settings.openWithCleanSlate) {
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// Create a new blank note
|
|
776
|
-
this.notes.push(this.createEmptyNote());
|
|
777
|
-
this.currentNoteIndex = this.notes.length - 1;
|
|
778
|
-
this.updatePaginationCounter();
|
|
779
|
-
this.redraw();
|
|
780
|
-
|
|
781
|
-
// Switch to text mode
|
|
782
|
-
this.setMode('text');
|
|
783
|
-
|
|
784
|
-
// Create a text item at top-left (under window controls)
|
|
785
|
-
// Position: x=20 (left margin), y=50 (below toolbar)
|
|
786
|
-
const x = 20;
|
|
787
|
-
const y = 50;
|
|
788
|
-
this.createTextItem(x, y);
|
|
789
|
-
|
|
790
|
-
// Save state
|
|
791
|
-
this.saveState();
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// Change color of all strokes in an object
|
|
795
|
-
changeObjectColor(objectId, color) {
|
|
796
|
-
this.lines.forEach(line => {
|
|
797
|
-
if (line.objectId === objectId) {
|
|
798
|
-
line.color = color;
|
|
799
|
-
}
|
|
800
|
-
});
|
|
801
|
-
this.redraw();
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// Change color of a text item
|
|
805
|
-
changeTextColor(textId, color) {
|
|
806
|
-
const textItem = this.textItems.find(t => t.id === textId);
|
|
807
|
-
if (textItem) {
|
|
808
|
-
textItem.color = color;
|
|
809
|
-
}
|
|
810
|
-
const element = this.textContainer.querySelector(`[data-id="${textId}"]`);
|
|
811
|
-
if (element) {
|
|
812
|
-
element.style.color = color;
|
|
813
|
-
const editor = element.querySelector('.text-input');
|
|
814
|
-
if (editor) {
|
|
815
|
-
editor.style.color = color;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
// Copy selected object to clipboard
|
|
821
|
-
copyObject(objectId) {
|
|
822
|
-
const objectLines = this.lines.filter(line => line.objectId === objectId);
|
|
823
|
-
if (objectLines.length === 0) return;
|
|
824
|
-
|
|
825
|
-
// Deep copy the lines
|
|
826
|
-
const copiedLines = objectLines.map(line => ({
|
|
827
|
-
points: line.points.map(p => ({ x: p.x, y: p.y })),
|
|
828
|
-
color: line.color,
|
|
829
|
-
width: line.width
|
|
830
|
-
}));
|
|
831
|
-
|
|
832
|
-
this.clipboard = {
|
|
833
|
-
type: 'object',
|
|
834
|
-
data: copiedLines
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Copy selected text item to clipboard
|
|
839
|
-
copyTextItem(textId) {
|
|
840
|
-
const textItem = this.textItems.find(t => t.id === textId);
|
|
841
|
-
if (!textItem) return;
|
|
842
|
-
|
|
843
|
-
const element = this.textContainer.querySelector(`[data-id="${textId}"]`);
|
|
844
|
-
const editor = element?.querySelector('.text-input');
|
|
845
|
-
|
|
846
|
-
this.clipboard = {
|
|
847
|
-
type: 'text',
|
|
848
|
-
data: {
|
|
849
|
-
content: editor?.innerHTML || textItem.content || '',
|
|
850
|
-
color: textItem.color
|
|
851
|
-
}
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// Paste from clipboard
|
|
856
|
-
paste() {
|
|
857
|
-
if (!this.clipboard) return;
|
|
858
|
-
|
|
859
|
-
const pasteOffset = 20; // Offset for pasted items
|
|
860
|
-
|
|
861
|
-
if (this.clipboard.type === 'object') {
|
|
862
|
-
// Create new object ID for pasted object
|
|
863
|
-
const newObjectId = Date.now().toString();
|
|
864
|
-
|
|
865
|
-
// Calculate bounding box to find offset
|
|
866
|
-
let minX = Infinity, minY = Infinity;
|
|
867
|
-
this.clipboard.data.forEach(line => {
|
|
868
|
-
line.points.forEach(p => {
|
|
869
|
-
minX = Math.min(minX, p.x);
|
|
870
|
-
minY = Math.min(minY, p.y);
|
|
871
|
-
});
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
// Add lines with new objectId and offset
|
|
875
|
-
this.clipboard.data.forEach(line => {
|
|
876
|
-
this.lines.push({
|
|
877
|
-
points: line.points.map(p => ({
|
|
878
|
-
x: p.x + pasteOffset,
|
|
879
|
-
y: p.y + pasteOffset
|
|
880
|
-
})),
|
|
881
|
-
color: line.color,
|
|
882
|
-
width: line.width,
|
|
883
|
-
objectId: newObjectId
|
|
884
|
-
});
|
|
885
|
-
});
|
|
886
|
-
|
|
887
|
-
// Select the pasted object
|
|
888
|
-
this.selectedObjectId = newObjectId;
|
|
889
|
-
this.redraw();
|
|
890
|
-
|
|
891
|
-
} else if (this.clipboard.type === 'text') {
|
|
892
|
-
// Get center of viewport for paste location
|
|
893
|
-
const rect = this.textContainer.getBoundingClientRect();
|
|
894
|
-
const x = rect.width / 2 / this.zoomLevel;
|
|
895
|
-
const y = rect.height / 2 / this.zoomLevel;
|
|
896
|
-
|
|
897
|
-
// Create new text item
|
|
898
|
-
const id = Date.now().toString();
|
|
899
|
-
const item = document.createElement('div');
|
|
900
|
-
item.className = 'text-item selected';
|
|
901
|
-
item.dataset.id = id;
|
|
902
|
-
item.style.left = (x + pasteOffset) + 'px';
|
|
903
|
-
item.style.top = (y + pasteOffset) + 'px';
|
|
904
|
-
item.style.color = this.clipboard.data.color;
|
|
905
|
-
// Apply counter-scale so text doesn't zoom
|
|
906
|
-
item.style.transform = `scale(${1 / this.zoomLevel})`;
|
|
907
|
-
item.style.transformOrigin = 'top left';
|
|
908
|
-
|
|
909
|
-
// Add drag handle
|
|
910
|
-
const dragHandle = document.createElement('div');
|
|
911
|
-
dragHandle.className = 'text-drag-handle';
|
|
912
|
-
dragHandle.innerHTML = `
|
|
913
|
-
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
|
914
|
-
<path d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"/>
|
|
915
|
-
</svg>
|
|
916
|
-
`;
|
|
917
|
-
dragHandle.title = 'Drag to move';
|
|
918
|
-
|
|
919
|
-
// Add format bar
|
|
920
|
-
const formatBar = document.createElement('div');
|
|
921
|
-
formatBar.className = 'text-format-bar';
|
|
922
|
-
formatBar.innerHTML = `
|
|
923
|
-
<button class="format-btn" data-format="bold" title="Bold">B</button>
|
|
924
|
-
<button class="format-btn" data-format="italic" title="Italic">I</button>
|
|
925
|
-
<button class="format-btn" data-format="underline" title="Underline">U</button>
|
|
926
|
-
<button class="format-btn delete-btn" title="Delete">✕</button>
|
|
927
|
-
`;
|
|
928
|
-
|
|
929
|
-
// Create contentEditable div
|
|
930
|
-
const editor = document.createElement('div');
|
|
931
|
-
editor.className = 'text-input';
|
|
932
|
-
editor.contentEditable = 'true';
|
|
933
|
-
// Support both old 'text' and new 'content' formats
|
|
934
|
-
editor.innerHTML = this.clipboard.data.content || this.clipboard.data.text || '';
|
|
935
|
-
editor.style.color = this.clipboard.data.color;
|
|
936
|
-
|
|
937
|
-
// Add resize handle
|
|
938
|
-
const resizeHandle = document.createElement('div');
|
|
939
|
-
resizeHandle.className = 'text-resize-handle';
|
|
940
|
-
|
|
941
|
-
item.appendChild(dragHandle);
|
|
942
|
-
item.appendChild(formatBar);
|
|
943
|
-
item.appendChild(editor);
|
|
944
|
-
item.appendChild(resizeHandle);
|
|
945
|
-
this.textContainer.appendChild(item);
|
|
946
|
-
|
|
947
|
-
this.textItems.push({
|
|
948
|
-
id,
|
|
949
|
-
x: x + pasteOffset,
|
|
950
|
-
y: y + pasteOffset,
|
|
951
|
-
content: this.clipboard.data.content || this.clipboard.data.text || '',
|
|
952
|
-
color: this.clipboard.data.color
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
// Setup event handlers for the new text item
|
|
956
|
-
this.setupPastedTextItem(item, id, editor, formatBar);
|
|
957
|
-
|
|
958
|
-
// Select the pasted item
|
|
959
|
-
this.deselectAllText();
|
|
960
|
-
this.selectedTextId = id;
|
|
961
|
-
editor.focus();
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
// Setup event handlers for a pasted/restored text item
|
|
966
|
-
setupPastedTextItem(item, id, editor, formatBar) {
|
|
967
|
-
// Handle input changes
|
|
968
|
-
editor.addEventListener('input', () => {
|
|
969
|
-
const textItem = this.textItems.find(t => t.id === id);
|
|
970
|
-
if (textItem) {
|
|
971
|
-
textItem.content = editor.innerHTML;
|
|
972
|
-
}
|
|
973
|
-
});
|
|
974
|
-
|
|
975
|
-
// Handle delete on empty backspace and rich text shortcuts
|
|
976
|
-
editor.addEventListener('keydown', (e) => {
|
|
977
|
-
if (e.key === 'Backspace' && editor.textContent.trim() === '') {
|
|
978
|
-
e.preventDefault();
|
|
979
|
-
this.deleteTextItem(id);
|
|
980
|
-
}
|
|
981
|
-
// Rich text keyboard shortcuts
|
|
982
|
-
if (e.metaKey && !e.shiftKey) {
|
|
983
|
-
if (e.key === 'b') {
|
|
984
|
-
e.preventDefault();
|
|
985
|
-
document.execCommand('bold', false, null);
|
|
986
|
-
this.updateFormatButtonStates(formatBar);
|
|
987
|
-
} else if (e.key === 'i') {
|
|
988
|
-
e.preventDefault();
|
|
989
|
-
document.execCommand('italic', false, null);
|
|
990
|
-
this.updateFormatButtonStates(formatBar);
|
|
991
|
-
} else if (e.key === 'u') {
|
|
992
|
-
e.preventDefault();
|
|
993
|
-
document.execCommand('underline', false, null);
|
|
994
|
-
this.updateFormatButtonStates(formatBar);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
});
|
|
998
|
-
|
|
999
|
-
// Format button handlers - use execCommand for selection-based formatting
|
|
1000
|
-
formatBar.querySelectorAll('.format-btn[data-format]').forEach(btn => {
|
|
1001
|
-
btn.addEventListener('mousedown', (e) => {
|
|
1002
|
-
e.preventDefault();
|
|
1003
|
-
const format = btn.dataset.format;
|
|
1004
|
-
document.execCommand(format, false, null);
|
|
1005
|
-
this.updateFormatButtonStates(formatBar);
|
|
1006
|
-
});
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
// Delete button handler
|
|
1010
|
-
formatBar.querySelector('.delete-btn').addEventListener('mousedown', (e) => {
|
|
1011
|
-
e.preventDefault();
|
|
1012
|
-
e.stopPropagation();
|
|
1013
|
-
this.deleteTextItem(id);
|
|
1014
|
-
});
|
|
1015
|
-
|
|
1016
|
-
// Delete empty text item when clicking away
|
|
1017
|
-
editor.addEventListener('blur', () => {
|
|
1018
|
-
setTimeout(() => {
|
|
1019
|
-
if (editor.textContent.trim() === '') {
|
|
1020
|
-
this.deleteTextItem(id);
|
|
1021
|
-
}
|
|
1022
|
-
}, 100);
|
|
1023
|
-
});
|
|
1024
|
-
|
|
1025
|
-
// Update format button states when selection changes
|
|
1026
|
-
editor.addEventListener('mouseup', () => this.updateFormatButtonStates(formatBar));
|
|
1027
|
-
editor.addEventListener('keyup', () => this.updateFormatButtonStates(formatBar));
|
|
1028
|
-
|
|
1029
|
-
// Add resize handle if not already present
|
|
1030
|
-
let resizeHandle = item.querySelector('.text-resize-handle');
|
|
1031
|
-
if (!resizeHandle) {
|
|
1032
|
-
resizeHandle = document.createElement('div');
|
|
1033
|
-
resizeHandle.className = 'text-resize-handle';
|
|
1034
|
-
item.appendChild(resizeHandle);
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
// Setup drag and resize
|
|
1038
|
-
this.setupTextItemDrag(item, id);
|
|
1039
|
-
this.setupTextResize(item, id, resizeHandle);
|
|
1040
|
-
|
|
1041
|
-
// Select on click
|
|
1042
|
-
item.addEventListener('click', (e) => {
|
|
1043
|
-
e.stopPropagation();
|
|
1044
|
-
this.selectTextItem(id);
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
setupTextMode() {
|
|
1049
|
-
this.textContainer.addEventListener('click', (e) => {
|
|
1050
|
-
if (!this.isTextMode) return;
|
|
1051
|
-
if (e.target !== this.textContainer) return;
|
|
1052
|
-
|
|
1053
|
-
const rect = this.textContainer.getBoundingClientRect();
|
|
1054
|
-
const x = e.clientX - rect.left;
|
|
1055
|
-
const y = e.clientY - rect.top;
|
|
1056
|
-
|
|
1057
|
-
this.createTextItem(x, y);
|
|
1058
|
-
});
|
|
1059
|
-
|
|
1060
|
-
// Deselect on click outside (only in draw mode)
|
|
1061
|
-
document.addEventListener('click', (e) => {
|
|
1062
|
-
if (this.isTextMode) return;
|
|
1063
|
-
if (!e.target.closest('.text-item') && !e.target.closest('#toolbar')) {
|
|
1064
|
-
this.deselectAllText();
|
|
1065
|
-
}
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
createTextItem(x, y) {
|
|
1070
|
-
const id = Date.now().toString();
|
|
1071
|
-
const item = document.createElement('div');
|
|
1072
|
-
item.className = 'text-item selected';
|
|
1073
|
-
item.dataset.id = id;
|
|
1074
|
-
item.style.left = x + 'px';
|
|
1075
|
-
item.style.top = y + 'px';
|
|
1076
|
-
item.style.color = this.currentColor;
|
|
1077
|
-
// Apply counter-scale so text doesn't zoom
|
|
1078
|
-
item.style.transform = `scale(${1 / this.zoomLevel})`;
|
|
1079
|
-
item.style.transformOrigin = 'top left';
|
|
1080
|
-
|
|
1081
|
-
// Add drag handle with move icon
|
|
1082
|
-
const dragHandle = document.createElement('div');
|
|
1083
|
-
dragHandle.className = 'text-drag-handle';
|
|
1084
|
-
dragHandle.innerHTML = `
|
|
1085
|
-
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
|
1086
|
-
<path d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"/>
|
|
1087
|
-
</svg>
|
|
1088
|
-
`;
|
|
1089
|
-
dragHandle.title = 'Drag to move';
|
|
1090
|
-
|
|
1091
|
-
// Inline formatting toolbar
|
|
1092
|
-
const formatBar = document.createElement('div');
|
|
1093
|
-
formatBar.className = 'text-format-bar';
|
|
1094
|
-
formatBar.innerHTML = `
|
|
1095
|
-
<button class="format-btn" data-format="bold" title="Bold">B</button>
|
|
1096
|
-
<button class="format-btn" data-format="italic" title="Italic">I</button>
|
|
1097
|
-
<button class="format-btn" data-format="underline" title="Underline">U</button>
|
|
1098
|
-
<button class="format-btn delete-btn" title="Delete">✕</button>
|
|
1099
|
-
`;
|
|
1100
|
-
|
|
1101
|
-
// Use contentEditable div for rich text support
|
|
1102
|
-
const editor = document.createElement('div');
|
|
1103
|
-
editor.className = 'text-input';
|
|
1104
|
-
editor.contentEditable = 'true';
|
|
1105
|
-
editor.dataset.placeholder = 'Type here...';
|
|
1106
|
-
editor.style.color = this.currentColor;
|
|
1107
|
-
|
|
1108
|
-
// Add resize handle
|
|
1109
|
-
const resizeHandle = document.createElement('div');
|
|
1110
|
-
resizeHandle.className = 'text-resize-handle';
|
|
1111
|
-
|
|
1112
|
-
item.appendChild(dragHandle);
|
|
1113
|
-
item.appendChild(formatBar);
|
|
1114
|
-
item.appendChild(editor);
|
|
1115
|
-
item.appendChild(resizeHandle);
|
|
1116
|
-
this.textContainer.appendChild(item);
|
|
1117
|
-
|
|
1118
|
-
this.textItems.push({ id, x, y, content: '', color: this.currentColor });
|
|
1119
|
-
this.selectedTextId = id;
|
|
1120
|
-
|
|
1121
|
-
// Focus editor
|
|
1122
|
-
editor.focus();
|
|
1123
|
-
|
|
1124
|
-
// Handle input changes
|
|
1125
|
-
editor.addEventListener('input', () => {
|
|
1126
|
-
const textItem = this.textItems.find(t => t.id === id);
|
|
1127
|
-
if (textItem) {
|
|
1128
|
-
textItem.content = editor.innerHTML;
|
|
1129
|
-
}
|
|
1130
|
-
});
|
|
1131
|
-
|
|
1132
|
-
// Handle delete on empty backspace
|
|
1133
|
-
editor.addEventListener('keydown', (e) => {
|
|
1134
|
-
if (e.key === 'Backspace' && editor.textContent.trim() === '') {
|
|
1135
|
-
e.preventDefault();
|
|
1136
|
-
this.deleteTextItem(id);
|
|
1137
|
-
}
|
|
1138
|
-
// Rich text keyboard shortcuts
|
|
1139
|
-
if (e.metaKey && !e.shiftKey) {
|
|
1140
|
-
if (e.key === 'b') {
|
|
1141
|
-
e.preventDefault();
|
|
1142
|
-
document.execCommand('bold', false, null);
|
|
1143
|
-
this.updateFormatButtonStates(formatBar);
|
|
1144
|
-
} else if (e.key === 'i') {
|
|
1145
|
-
e.preventDefault();
|
|
1146
|
-
document.execCommand('italic', false, null);
|
|
1147
|
-
this.updateFormatButtonStates(formatBar);
|
|
1148
|
-
} else if (e.key === 'u') {
|
|
1149
|
-
e.preventDefault();
|
|
1150
|
-
document.execCommand('underline', false, null);
|
|
1151
|
-
this.updateFormatButtonStates(formatBar);
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
});
|
|
1155
|
-
|
|
1156
|
-
// Format button handlers - use execCommand for selection-based formatting
|
|
1157
|
-
formatBar.querySelectorAll('.format-btn[data-format]').forEach(btn => {
|
|
1158
|
-
btn.addEventListener('mousedown', (e) => {
|
|
1159
|
-
e.preventDefault(); // Prevent focus loss
|
|
1160
|
-
const format = btn.dataset.format;
|
|
1161
|
-
document.execCommand(format, false, null);
|
|
1162
|
-
this.updateFormatButtonStates(formatBar);
|
|
1163
|
-
});
|
|
1164
|
-
});
|
|
1165
|
-
|
|
1166
|
-
// Delete button handler
|
|
1167
|
-
formatBar.querySelector('.delete-btn').addEventListener('mousedown', (e) => {
|
|
1168
|
-
e.preventDefault();
|
|
1169
|
-
e.stopPropagation();
|
|
1170
|
-
this.deleteTextItem(id);
|
|
1171
|
-
});
|
|
1172
|
-
|
|
1173
|
-
// Delete empty text item when clicking away
|
|
1174
|
-
editor.addEventListener('blur', () => {
|
|
1175
|
-
setTimeout(() => {
|
|
1176
|
-
if (editor.textContent.trim() === '') {
|
|
1177
|
-
this.deleteTextItem(id);
|
|
1178
|
-
}
|
|
1179
|
-
}, 100); // Small delay to allow format button clicks
|
|
1180
|
-
});
|
|
1181
|
-
|
|
1182
|
-
// Update format button states when selection changes
|
|
1183
|
-
editor.addEventListener('mouseup', () => this.updateFormatButtonStates(formatBar));
|
|
1184
|
-
editor.addEventListener('keyup', () => this.updateFormatButtonStates(formatBar));
|
|
1185
|
-
|
|
1186
|
-
// Setup drag and resize
|
|
1187
|
-
this.setupTextItemDrag(item, id);
|
|
1188
|
-
this.setupTextResize(item, id, resizeHandle);
|
|
1189
|
-
|
|
1190
|
-
// Select on click
|
|
1191
|
-
item.addEventListener('click', (e) => {
|
|
1192
|
-
e.stopPropagation();
|
|
1193
|
-
this.selectTextItem(id);
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
updateFormatButtonStates(formatBar) {
|
|
1198
|
-
// Update format button active states based on current selection
|
|
1199
|
-
const boldBtn = formatBar.querySelector('[data-format="bold"]');
|
|
1200
|
-
const italicBtn = formatBar.querySelector('[data-format="italic"]');
|
|
1201
|
-
const underlineBtn = formatBar.querySelector('[data-format="underline"]');
|
|
1202
|
-
|
|
1203
|
-
if (boldBtn) {
|
|
1204
|
-
boldBtn.classList.toggle('active', document.queryCommandState('bold'));
|
|
1205
|
-
}
|
|
1206
|
-
if (italicBtn) {
|
|
1207
|
-
italicBtn.classList.toggle('active', document.queryCommandState('italic'));
|
|
1208
|
-
}
|
|
1209
|
-
if (underlineBtn) {
|
|
1210
|
-
underlineBtn.classList.toggle('active', document.queryCommandState('underline'));
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
setupTextItemDrag(element, id) {
|
|
1215
|
-
let isDragging = false;
|
|
1216
|
-
let startX, startY, origX, origY;
|
|
1217
|
-
|
|
1218
|
-
const startDrag = (e) => {
|
|
1219
|
-
// Don't drag from text editor, format buttons, or resize handle
|
|
1220
|
-
if (e.target.closest('.text-input') || e.target.closest('.format-btn') || e.target.closest('.text-resize-handle')) return;
|
|
1221
|
-
e.preventDefault();
|
|
1222
|
-
isDragging = true;
|
|
1223
|
-
startX = e.clientX;
|
|
1224
|
-
startY = e.clientY;
|
|
1225
|
-
origX = parseInt(element.style.left) || 0;
|
|
1226
|
-
origY = parseInt(element.style.top) || 0;
|
|
1227
|
-
element.style.cursor = 'grabbing';
|
|
1228
|
-
element.classList.add('dragging');
|
|
1229
|
-
};
|
|
1230
|
-
|
|
1231
|
-
const drag = (e) => {
|
|
1232
|
-
if (!isDragging) return;
|
|
1233
|
-
const dx = e.clientX - startX;
|
|
1234
|
-
const dy = e.clientY - startY;
|
|
1235
|
-
element.style.left = (origX + dx) + 'px';
|
|
1236
|
-
element.style.top = (origY + dy) + 'px';
|
|
1237
|
-
|
|
1238
|
-
const textItem = this.textItems.find(t => t.id === id);
|
|
1239
|
-
if (textItem) {
|
|
1240
|
-
textItem.x = origX + dx;
|
|
1241
|
-
textItem.y = origY + dy;
|
|
1242
|
-
}
|
|
1243
|
-
};
|
|
1244
|
-
|
|
1245
|
-
const stopDrag = () => {
|
|
1246
|
-
if (isDragging) {
|
|
1247
|
-
isDragging = false;
|
|
1248
|
-
element.style.cursor = 'grab';
|
|
1249
|
-
element.classList.remove('dragging');
|
|
1250
|
-
}
|
|
1251
|
-
};
|
|
1252
|
-
|
|
1253
|
-
element.addEventListener('mousedown', startDrag);
|
|
1254
|
-
document.addEventListener('mousemove', drag);
|
|
1255
|
-
document.addEventListener('mouseup', stopDrag);
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
setupTextResize(element, id, handle) {
|
|
1259
|
-
let isResizing = false;
|
|
1260
|
-
let startX, startY, initialWidth, initialHeight;
|
|
1261
|
-
|
|
1262
|
-
handle.addEventListener('mousedown', (e) => {
|
|
1263
|
-
isResizing = true;
|
|
1264
|
-
startX = e.clientX;
|
|
1265
|
-
startY = e.clientY;
|
|
1266
|
-
initialWidth = element.offsetWidth;
|
|
1267
|
-
initialHeight = element.offsetHeight;
|
|
1268
|
-
element.classList.add('resizing');
|
|
1269
|
-
e.preventDefault();
|
|
1270
|
-
e.stopPropagation();
|
|
1271
|
-
});
|
|
1272
|
-
|
|
1273
|
-
document.addEventListener('mousemove', (e) => {
|
|
1274
|
-
if (!isResizing) return;
|
|
1275
|
-
|
|
1276
|
-
const dx = (e.clientX - startX) / this.zoomLevel;
|
|
1277
|
-
const dy = (e.clientY - startY) / this.zoomLevel;
|
|
1278
|
-
|
|
1279
|
-
// Allow independent width/height resize
|
|
1280
|
-
const newWidth = Math.max(100, initialWidth + dx);
|
|
1281
|
-
const newHeight = Math.max(40, initialHeight + dy);
|
|
1282
|
-
|
|
1283
|
-
element.style.width = newWidth + 'px';
|
|
1284
|
-
element.style.minHeight = newHeight + 'px';
|
|
1285
|
-
|
|
1286
|
-
// Update editor width to match
|
|
1287
|
-
const editor = element.querySelector('.text-input');
|
|
1288
|
-
if (editor) {
|
|
1289
|
-
editor.style.width = '100%';
|
|
1290
|
-
}
|
|
1291
|
-
});
|
|
1292
|
-
|
|
1293
|
-
document.addEventListener('mouseup', () => {
|
|
1294
|
-
if (isResizing) {
|
|
1295
|
-
isResizing = false;
|
|
1296
|
-
element.classList.remove('resizing');
|
|
1297
|
-
|
|
1298
|
-
// Update stored dimensions
|
|
1299
|
-
const textItem = this.textItems.find(t => t.id === id);
|
|
1300
|
-
if (textItem) {
|
|
1301
|
-
textItem.width = element.offsetWidth;
|
|
1302
|
-
textItem.height = element.offsetHeight;
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
});
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
selectTextItem(id) {
|
|
1309
|
-
this.deselectAllText();
|
|
1310
|
-
this.selectedTextId = id;
|
|
1311
|
-
const element = this.textContainer.querySelector(`[data-id="${id}"]`);
|
|
1312
|
-
if (element) {
|
|
1313
|
-
element.classList.add('selected');
|
|
1314
|
-
const editor = element.querySelector('.text-input');
|
|
1315
|
-
if (editor) {
|
|
1316
|
-
editor.focus();
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
deselectAllText() {
|
|
1322
|
-
this.selectedTextId = null;
|
|
1323
|
-
this.textContainer.querySelectorAll('.text-item').forEach(item => {
|
|
1324
|
-
item.classList.remove('selected');
|
|
1325
|
-
});
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
deleteTextItem(id) {
|
|
1329
|
-
const element = this.textContainer.querySelector(`[data-id="${id}"]`);
|
|
1330
|
-
if (element) {
|
|
1331
|
-
element.remove();
|
|
1332
|
-
}
|
|
1333
|
-
this.textItems = this.textItems.filter(t => t.id !== id);
|
|
1334
|
-
if (this.selectedTextId === id) {
|
|
1335
|
-
this.selectedTextId = null;
|
|
1336
|
-
}
|
|
1337
|
-
this.saveState();
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
setupKeyboardShortcuts() {
|
|
1341
|
-
document.addEventListener('keydown', (e) => {
|
|
1342
|
-
// Check if we're in a text input
|
|
1343
|
-
const isInTextInput = e.target.closest('.text-input');
|
|
1344
|
-
|
|
1345
|
-
// Cmd+N - do nothing (prevent default new window behavior)
|
|
1346
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
|
|
1347
|
-
e.preventDefault();
|
|
1348
|
-
return;
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
// Cmd+W to close window
|
|
1352
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'w') {
|
|
1353
|
-
e.preventDefault();
|
|
1354
|
-
window.glassboard.closeWindow();
|
|
1355
|
-
return;
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
// Cmd+S to export as PNG
|
|
1359
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
1360
|
-
e.preventDefault();
|
|
1361
|
-
this.exportAsPNG();
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
// Cmd+, to open settings
|
|
1366
|
-
if ((e.metaKey || e.ctrlKey) && e.key === ',') {
|
|
1367
|
-
e.preventDefault();
|
|
1368
|
-
this.toggleSettings();
|
|
1369
|
-
return;
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
// Cmd+P to toggle pin
|
|
1373
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'p') {
|
|
1374
|
-
e.preventDefault();
|
|
1375
|
-
this.togglePin();
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
// Cmd+Z to undo
|
|
1380
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'z' && !e.shiftKey) {
|
|
1381
|
-
if (isInTextInput) return; // Let native undo handle it
|
|
1382
|
-
e.preventDefault();
|
|
1383
|
-
this.undo();
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
// Cmd+Shift+Z or Cmd+Y to redo
|
|
1388
|
-
if ((e.metaKey || e.ctrlKey) && (e.key === 'z' && e.shiftKey || e.key === 'y')) {
|
|
1389
|
-
if (isInTextInput) return; // Let native redo handle it
|
|
1390
|
-
e.preventDefault();
|
|
1391
|
-
this.redo();
|
|
1392
|
-
return;
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
// Cmd+C to copy
|
|
1396
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'c') {
|
|
1397
|
-
// Allow native copy in text inputs with selected text
|
|
1398
|
-
if (isInTextInput && window.getSelection().toString()) {
|
|
1399
|
-
return; // Let native copy handle it
|
|
1400
|
-
}
|
|
1401
|
-
// Copy selected object or text item
|
|
1402
|
-
if (this.selectedObjectId) {
|
|
1403
|
-
e.preventDefault();
|
|
1404
|
-
this.copyObject(this.selectedObjectId);
|
|
1405
|
-
return;
|
|
1406
|
-
}
|
|
1407
|
-
if (this.selectedTextId && !isInTextInput) {
|
|
1408
|
-
e.preventDefault();
|
|
1409
|
-
this.copyTextItem(this.selectedTextId);
|
|
1410
|
-
return;
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
// Cmd+V to smart paste
|
|
1415
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'v') {
|
|
1416
|
-
// Allow native paste in text inputs
|
|
1417
|
-
if (isInTextInput) {
|
|
1418
|
-
return; // Let native paste handle it
|
|
1419
|
-
}
|
|
1420
|
-
e.preventDefault();
|
|
1421
|
-
this.smartPaste();
|
|
1422
|
-
return;
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
// Cmd+A to select all
|
|
1426
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
|
|
1427
|
-
if (isInTextInput) return; // Let native select all handle it
|
|
1428
|
-
e.preventDefault();
|
|
1429
|
-
this.selectAll();
|
|
1430
|
-
return;
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
// 'd' or Delete/Backspace to delete selected object or all selected
|
|
1434
|
-
if ((e.key === 'd' || e.key === 'Delete' || e.key === 'Backspace') && !isInTextInput) {
|
|
1435
|
-
// If all selected (after Cmd+A), delete everything
|
|
1436
|
-
if (this.allSelected) {
|
|
1437
|
-
e.preventDefault();
|
|
1438
|
-
this.deleteAllSelected();
|
|
1439
|
-
return;
|
|
1440
|
-
}
|
|
1441
|
-
// Otherwise delete single selected object
|
|
1442
|
-
if (this.selectedObjectId) {
|
|
1443
|
-
e.preventDefault();
|
|
1444
|
-
this.deleteObject(this.selectedObjectId);
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
// Or delete selected text item
|
|
1448
|
-
if (this.selectedTextId) {
|
|
1449
|
-
e.preventDefault();
|
|
1450
|
-
this.deleteTextItem(this.selectedTextId);
|
|
1451
|
-
return;
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
// Cmd+Plus to zoom in
|
|
1456
|
-
if ((e.metaKey || e.ctrlKey) && (e.key === '=' || e.key === '+')) {
|
|
1457
|
-
e.preventDefault();
|
|
1458
|
-
this.zoomIn();
|
|
1459
|
-
return;
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
// Cmd+Minus to zoom out
|
|
1463
|
-
if ((e.metaKey || e.ctrlKey) && e.key === '-') {
|
|
1464
|
-
e.preventDefault();
|
|
1465
|
-
this.zoomOut();
|
|
1466
|
-
return;
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
// Cmd+0 to reset view (zoom, pan, rotation)
|
|
1470
|
-
if ((e.metaKey || e.ctrlKey) && e.key === '0') {
|
|
1471
|
-
e.preventDefault();
|
|
1472
|
-
this.resetTransform();
|
|
1473
|
-
return;
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
// Escape to deselect
|
|
1477
|
-
if (e.key === 'Escape') {
|
|
1478
|
-
this.deselectObject();
|
|
1479
|
-
this.deselectAllText();
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
// Note navigation (only when not in text input)
|
|
1483
|
-
if (!isInTextInput) {
|
|
1484
|
-
// [ or ArrowLeft to go to previous note
|
|
1485
|
-
if (e.key === '[' || e.key === 'ArrowLeft') {
|
|
1486
|
-
e.preventDefault();
|
|
1487
|
-
this.previousNote();
|
|
1488
|
-
return;
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
// ] or ArrowRight to go to next note or create new one
|
|
1492
|
-
if (e.key === ']' || e.key === 'ArrowRight') {
|
|
1493
|
-
e.preventDefault();
|
|
1494
|
-
this.nextNote();
|
|
1495
|
-
return;
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
// Mode shortcuts (only when not in text input)
|
|
1500
|
-
if (!isInTextInput) {
|
|
1501
|
-
if (e.key === 'v' || e.key === 'V') {
|
|
1502
|
-
this.setMode('select');
|
|
1503
|
-
return;
|
|
1504
|
-
}
|
|
1505
|
-
if (e.key === 'b' || e.key === 'B') {
|
|
1506
|
-
this.setMode('draw');
|
|
1507
|
-
return;
|
|
1508
|
-
}
|
|
1509
|
-
if (e.key === 't' || e.key === 'T') {
|
|
1510
|
-
this.setMode('text');
|
|
1511
|
-
return;
|
|
1512
|
-
}
|
|
1513
|
-
// Size shortcuts: Cmd+1=sm, Cmd+2=md, Cmd+3=lg
|
|
1514
|
-
if (e.metaKey && e.key === '1') {
|
|
1515
|
-
e.preventDefault();
|
|
1516
|
-
window.glassboard.setWindowSize('sm');
|
|
1517
|
-
this.updateSizeDropdown('sm');
|
|
1518
|
-
return;
|
|
1519
|
-
}
|
|
1520
|
-
if (e.metaKey && e.key === '2') {
|
|
1521
|
-
e.preventDefault();
|
|
1522
|
-
window.glassboard.setWindowSize('md');
|
|
1523
|
-
this.updateSizeDropdown('md');
|
|
1524
|
-
return;
|
|
1525
|
-
}
|
|
1526
|
-
if (e.metaKey && e.key === '3') {
|
|
1527
|
-
e.preventDefault();
|
|
1528
|
-
window.glassboard.setWindowSize('lg');
|
|
1529
|
-
this.updateSizeDropdown('lg');
|
|
1530
|
-
return;
|
|
1531
|
-
}
|
|
1532
|
-
// N for new note
|
|
1533
|
-
if (e.key === 'n' || e.key === 'N') {
|
|
1534
|
-
this.createNewNoteAndSwitch();
|
|
1535
|
-
return;
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
// Double right Command key to toggle (hide) Glassboard
|
|
1540
|
-
if (e.code === 'MetaRight' && !e.repeat) {
|
|
1541
|
-
const now = Date.now();
|
|
1542
|
-
if (now - this.lastRightCommandTime < 300) {
|
|
1543
|
-
// Double tap detected - hide the window
|
|
1544
|
-
window.glassboard.hideWindow();
|
|
1545
|
-
this.lastRightCommandTime = 0;
|
|
1546
|
-
} else {
|
|
1547
|
-
this.lastRightCommandTime = now;
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
});
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
setupFocusHandling() {
|
|
1554
|
-
window.glassboard.onFocusChange((focused) => {
|
|
1555
|
-
if (focused) {
|
|
1556
|
-
this.app.classList.add('focused');
|
|
1557
|
-
} else {
|
|
1558
|
-
this.app.classList.remove('focused');
|
|
1559
|
-
// Deselect all text items when window loses focus
|
|
1560
|
-
this.deselectAllText();
|
|
1561
|
-
}
|
|
1562
|
-
// Apply background modes via CSS classes
|
|
1563
|
-
this.applyBackgroundModes();
|
|
1564
|
-
// Apply appropriate opacity
|
|
1565
|
-
this.applyBackgroundOpacity();
|
|
1566
|
-
// Update native vibrancy based on current mode
|
|
1567
|
-
const currentMode = focused
|
|
1568
|
-
? this.settings.activeBgMode
|
|
1569
|
-
: this.settings.inactiveBgMode;
|
|
1570
|
-
window.glassboard.setBackgroundMode(currentMode || 'transparent');
|
|
1571
|
-
});
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
applyBackgroundModes() {
|
|
1575
|
-
// Remove all background classes first
|
|
1576
|
-
this.app.classList.remove('bg-transparent', 'bg-blur', 'bg-dark');
|
|
1577
|
-
this.app.classList.remove('inactive-bg-transparent', 'inactive-bg-blur', 'inactive-bg-dark');
|
|
1578
|
-
|
|
1579
|
-
// Apply active background class
|
|
1580
|
-
const activeMode = this.settings.activeBgMode || 'transparent';
|
|
1581
|
-
this.app.classList.add(`bg-${activeMode}`);
|
|
1582
|
-
|
|
1583
|
-
// Apply inactive background class
|
|
1584
|
-
const inactiveMode = this.settings.inactiveBgMode || 'transparent';
|
|
1585
|
-
this.app.classList.add(`inactive-bg-${inactiveMode}`);
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
setupResize() {
|
|
1589
|
-
// Add resize handles to window (corners and edges)
|
|
1590
|
-
const handles = ['se', 'sw', 'ne', 'nw', 'n', 's', 'e', 'w'];
|
|
1591
|
-
handles.forEach(pos => {
|
|
1592
|
-
const handle = document.createElement('div');
|
|
1593
|
-
handle.className = `resize-handle ${pos}`;
|
|
1594
|
-
this.app.appendChild(handle);
|
|
1595
|
-
|
|
1596
|
-
// Add resizing class when dragging resize handles
|
|
1597
|
-
handle.addEventListener('mousedown', () => {
|
|
1598
|
-
this.app.classList.add('resizing');
|
|
1599
|
-
});
|
|
1600
|
-
});
|
|
1601
|
-
|
|
1602
|
-
// Remove resizing class when mouse is released anywhere
|
|
1603
|
-
document.addEventListener('mouseup', () => {
|
|
1604
|
-
this.app.classList.remove('resizing');
|
|
1605
|
-
});
|
|
1606
|
-
|
|
1607
|
-
// Also detect native window resize events for yellow border
|
|
1608
|
-
let resizeTimeout;
|
|
1609
|
-
window.addEventListener('resize', () => {
|
|
1610
|
-
this.app.classList.add('resizing');
|
|
1611
|
-
clearTimeout(resizeTimeout);
|
|
1612
|
-
resizeTimeout = setTimeout(() => {
|
|
1613
|
-
this.app.classList.remove('resizing');
|
|
1614
|
-
}, 200);
|
|
1615
|
-
});
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
setupZoom() {
|
|
1619
|
-
// Zoom is now handled in setupGestures for trackpad support
|
|
1620
|
-
// This method is kept for future zoom-related setup
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
zoomIn() {
|
|
1624
|
-
this.zoomLevel = Math.min(this.maxZoom, this.zoomLevel + 0.25);
|
|
1625
|
-
this.applyZoom();
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
zoomOut() {
|
|
1629
|
-
this.zoomLevel = Math.max(this.minZoom, this.zoomLevel - 0.25);
|
|
1630
|
-
this.applyZoom();
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
resetZoom() {
|
|
1634
|
-
this.zoomLevel = 1;
|
|
1635
|
-
this.applyZoom();
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
applyZoom() {
|
|
1639
|
-
this.applyTransform();
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
applyTransform() {
|
|
1643
|
-
// Build transform string with translate, scale, and rotate
|
|
1644
|
-
const transform = `translate(${this.panX}px, ${this.panY}px) scale(${this.zoomLevel}) rotate(${this.rotation}deg)`;
|
|
1645
|
-
this.canvas.style.transform = transform;
|
|
1646
|
-
this.canvas.style.transformOrigin = 'center center';
|
|
1647
|
-
this.textContainer.style.transform = transform;
|
|
1648
|
-
this.textContainer.style.transformOrigin = 'center center';
|
|
1649
|
-
|
|
1650
|
-
// Counter-scale text items so they maintain visual size
|
|
1651
|
-
// Text items should move with pan/zoom but not change visual size
|
|
1652
|
-
const counterScale = 1 / this.zoomLevel;
|
|
1653
|
-
this.textContainer.querySelectorAll('.text-item').forEach(item => {
|
|
1654
|
-
item.style.transform = `scale(${counterScale})`;
|
|
1655
|
-
item.style.transformOrigin = 'top left';
|
|
1656
|
-
});
|
|
1657
|
-
|
|
1658
|
-
// Update zoom indicator if exists
|
|
1659
|
-
const zoomIndicator = document.getElementById('zoom-indicator');
|
|
1660
|
-
if (zoomIndicator) {
|
|
1661
|
-
zoomIndicator.textContent = `${Math.round(this.zoomLevel * 100)}%`;
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
resetTransform() {
|
|
1666
|
-
this.zoomLevel = 1;
|
|
1667
|
-
this.panX = 0;
|
|
1668
|
-
this.panY = 0;
|
|
1669
|
-
this.rotation = 0;
|
|
1670
|
-
this.applyTransform();
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
setupContextMenu() {
|
|
1674
|
-
this.canvas.addEventListener('contextmenu', (e) => {
|
|
1675
|
-
e.preventDefault();
|
|
1676
|
-
|
|
1677
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
1678
|
-
const x = (e.clientX - rect.left) / this.zoomLevel;
|
|
1679
|
-
const y = (e.clientY - rect.top) / this.zoomLevel;
|
|
1680
|
-
|
|
1681
|
-
const clickedObjectId = this.findObjectAtPoint({ x, y });
|
|
1682
|
-
if (clickedObjectId) {
|
|
1683
|
-
this.selectObject(clickedObjectId);
|
|
1684
|
-
this.showContextMenu(e.clientX, e.clientY, 'object');
|
|
1685
|
-
} else {
|
|
1686
|
-
// Show general context menu for empty area
|
|
1687
|
-
this.showContextMenu(e.clientX, e.clientY, 'general');
|
|
1688
|
-
}
|
|
1689
|
-
});
|
|
1690
|
-
|
|
1691
|
-
// Hide context menu on click elsewhere
|
|
1692
|
-
document.addEventListener('click', () => {
|
|
1693
|
-
this.hideContextMenu();
|
|
1694
|
-
});
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
showContextMenu(x, y, type = 'object') {
|
|
1698
|
-
let menu = document.getElementById('context-menu');
|
|
1699
|
-
if (menu) {
|
|
1700
|
-
menu.remove();
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
menu = document.createElement('div');
|
|
1704
|
-
menu.id = 'context-menu';
|
|
1705
|
-
|
|
1706
|
-
if (type === 'general') {
|
|
1707
|
-
// General context menu for empty area
|
|
1708
|
-
menu.innerHTML = `
|
|
1709
|
-
<button class="context-menu-item" data-action="zoom-in">
|
|
1710
|
-
<span>Zoom In</span>
|
|
1711
|
-
<span class="shortcut">⌘+</span>
|
|
1712
|
-
</button>
|
|
1713
|
-
<button class="context-menu-item" data-action="zoom-out">
|
|
1714
|
-
<span>Zoom Out</span>
|
|
1715
|
-
<span class="shortcut">⌘-</span>
|
|
1716
|
-
</button>
|
|
1717
|
-
<button class="context-menu-item" data-action="reset-zoom">
|
|
1718
|
-
<span>Reset View</span>
|
|
1719
|
-
<span class="shortcut">⌘0</span>
|
|
1720
|
-
</button>
|
|
1721
|
-
<div class="context-menu-divider"></div>
|
|
1722
|
-
<button class="context-menu-item" data-action="paste">
|
|
1723
|
-
<span>Paste</span>
|
|
1724
|
-
<span class="shortcut">⌘V</span>
|
|
1725
|
-
</button>
|
|
1726
|
-
<div class="context-menu-divider"></div>
|
|
1727
|
-
<button class="context-menu-item" data-action="settings">
|
|
1728
|
-
<span>Settings</span>
|
|
1729
|
-
<span class="shortcut">⌘,</span>
|
|
1730
|
-
</button>
|
|
1731
|
-
`;
|
|
1732
|
-
|
|
1733
|
-
menu.querySelector('[data-action="zoom-in"]').addEventListener('click', () => {
|
|
1734
|
-
this.zoomIn();
|
|
1735
|
-
this.hideContextMenu();
|
|
1736
|
-
});
|
|
1737
|
-
|
|
1738
|
-
menu.querySelector('[data-action="zoom-out"]').addEventListener('click', () => {
|
|
1739
|
-
this.zoomOut();
|
|
1740
|
-
this.hideContextMenu();
|
|
1741
|
-
});
|
|
1742
|
-
|
|
1743
|
-
menu.querySelector('[data-action="reset-zoom"]').addEventListener('click', () => {
|
|
1744
|
-
this.resetTransform();
|
|
1745
|
-
this.hideContextMenu();
|
|
1746
|
-
});
|
|
1747
|
-
|
|
1748
|
-
menu.querySelector('[data-action="paste"]').addEventListener('click', () => {
|
|
1749
|
-
this.paste();
|
|
1750
|
-
this.hideContextMenu();
|
|
1751
|
-
});
|
|
1752
|
-
|
|
1753
|
-
menu.querySelector('[data-action="settings"]').addEventListener('click', () => {
|
|
1754
|
-
this.toggleSettings();
|
|
1755
|
-
this.hideContextMenu();
|
|
1756
|
-
});
|
|
1757
|
-
} else {
|
|
1758
|
-
// Object context menu
|
|
1759
|
-
menu.innerHTML = `
|
|
1760
|
-
<button class="context-menu-item" data-action="copy">
|
|
1761
|
-
<span>Copy</span>
|
|
1762
|
-
<span class="shortcut">⌘C</span>
|
|
1763
|
-
</button>
|
|
1764
|
-
<button class="context-menu-item" data-action="paste">
|
|
1765
|
-
<span>Paste</span>
|
|
1766
|
-
<span class="shortcut">⌘V</span>
|
|
1767
|
-
</button>
|
|
1768
|
-
<div class="context-menu-divider"></div>
|
|
1769
|
-
<button class="context-menu-item" data-action="delete">
|
|
1770
|
-
<span>Delete</span>
|
|
1771
|
-
<span class="shortcut">D</span>
|
|
1772
|
-
</button>
|
|
1773
|
-
`;
|
|
1774
|
-
|
|
1775
|
-
menu.querySelector('[data-action="copy"]').addEventListener('click', () => {
|
|
1776
|
-
if (this.selectedObjectId) {
|
|
1777
|
-
this.copyObject(this.selectedObjectId);
|
|
1778
|
-
}
|
|
1779
|
-
this.hideContextMenu();
|
|
1780
|
-
});
|
|
1781
|
-
|
|
1782
|
-
menu.querySelector('[data-action="paste"]').addEventListener('click', () => {
|
|
1783
|
-
this.paste();
|
|
1784
|
-
this.hideContextMenu();
|
|
1785
|
-
});
|
|
1786
|
-
|
|
1787
|
-
menu.querySelector('[data-action="delete"]').addEventListener('click', () => {
|
|
1788
|
-
if (this.selectedObjectId) {
|
|
1789
|
-
this.deleteObject(this.selectedObjectId);
|
|
1790
|
-
}
|
|
1791
|
-
this.hideContextMenu();
|
|
1792
|
-
});
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
document.body.appendChild(menu);
|
|
1796
|
-
|
|
1797
|
-
// Update paste button state
|
|
1798
|
-
const pasteBtn = menu.querySelector('[data-action="paste"]');
|
|
1799
|
-
if (pasteBtn) {
|
|
1800
|
-
pasteBtn.disabled = !this.clipboard;
|
|
1801
|
-
pasteBtn.style.opacity = this.clipboard ? '1' : '0.5';
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
menu.style.left = x + 'px';
|
|
1805
|
-
menu.style.top = y + 'px';
|
|
1806
|
-
menu.classList.add('visible');
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
hideContextMenu() {
|
|
1810
|
-
const menu = document.getElementById('context-menu');
|
|
1811
|
-
if (menu) {
|
|
1812
|
-
menu.classList.remove('visible');
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
redraw() {
|
|
1817
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
1818
|
-
this.ctx.clearRect(0, 0, rect.width / this.zoomLevel, rect.height / this.zoomLevel);
|
|
1819
|
-
|
|
1820
|
-
// Draw center origin dot (2x2px, zoom-independent)
|
|
1821
|
-
this.drawCenterDot();
|
|
1822
|
-
|
|
1823
|
-
// Draw all saved lines
|
|
1824
|
-
this.lines.forEach(line => this.drawLine(line));
|
|
1825
|
-
|
|
1826
|
-
// Draw current line
|
|
1827
|
-
if (this.currentLine) {
|
|
1828
|
-
this.drawLine(this.currentLine);
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
// Draw selection highlight for single selection
|
|
1832
|
-
if (this.selectedObjectId) {
|
|
1833
|
-
this.drawSelectionHighlight();
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
// Draw selection highlights for multi-selected objects
|
|
1837
|
-
if (this.multiSelectedObjects.length > 0) {
|
|
1838
|
-
this.multiSelectedObjects.forEach(objectId => {
|
|
1839
|
-
this.drawSelectionHighlightForObject(objectId);
|
|
1840
|
-
});
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
// Draw selection highlights for all-selected objects
|
|
1844
|
-
if (this.allSelected && this.selectedObjects.length > 0) {
|
|
1845
|
-
this.selectedObjects.forEach(objectId => {
|
|
1846
|
-
this.drawSelectionHighlightForObject(objectId);
|
|
1847
|
-
});
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
drawLine(line) {
|
|
1852
|
-
if (line.points.length < 2) return;
|
|
1853
|
-
|
|
1854
|
-
this.ctx.beginPath();
|
|
1855
|
-
this.ctx.strokeStyle = line.color;
|
|
1856
|
-
this.ctx.lineWidth = line.width;
|
|
1857
|
-
this.ctx.lineCap = 'round';
|
|
1858
|
-
this.ctx.lineJoin = 'round';
|
|
1859
|
-
|
|
1860
|
-
this.ctx.moveTo(line.points[0].x, line.points[0].y);
|
|
1861
|
-
for (let i = 1; i < line.points.length; i++) {
|
|
1862
|
-
this.ctx.lineTo(line.points[i].x, line.points[i].y);
|
|
1863
|
-
}
|
|
1864
|
-
this.ctx.stroke();
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
drawCenterDot() {
|
|
1868
|
-
// Draw a 2x2px bright yellow dot at the note's stored origin
|
|
1869
|
-
// Size is compensated for zoom so it always appears 2x2px visually
|
|
1870
|
-
const dotSize = 2 / this.zoomLevel;
|
|
1871
|
-
|
|
1872
|
-
// Get origin from current note (already in content-space)
|
|
1873
|
-
const note = this.notes[this.currentNoteIndex];
|
|
1874
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
1875
|
-
// Fallback for notes without origin (legacy) or if note doesn't exist
|
|
1876
|
-
const originX = note?.originX ?? (rect.width / 2 / this.zoomLevel);
|
|
1877
|
-
const originY = note?.originY ?? (rect.height / 2 / this.zoomLevel);
|
|
1878
|
-
|
|
1879
|
-
this.ctx.fillStyle = '#facc15'; // Bright yellow
|
|
1880
|
-
this.ctx.fillRect(
|
|
1881
|
-
originX - dotSize / 2,
|
|
1882
|
-
originY - dotSize / 2,
|
|
1883
|
-
dotSize,
|
|
1884
|
-
dotSize
|
|
1885
|
-
);
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
drawSelectionHighlight() {
|
|
1889
|
-
// Get bounding box of selected object
|
|
1890
|
-
const selectedLines = this.lines.filter(l => l.objectId === this.selectedObjectId);
|
|
1891
|
-
if (selectedLines.length === 0) return;
|
|
1892
|
-
|
|
1893
|
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1894
|
-
selectedLines.forEach(line => {
|
|
1895
|
-
line.points.forEach(p => {
|
|
1896
|
-
minX = Math.min(minX, p.x);
|
|
1897
|
-
minY = Math.min(minY, p.y);
|
|
1898
|
-
maxX = Math.max(maxX, p.x);
|
|
1899
|
-
maxY = Math.max(maxY, p.y);
|
|
1900
|
-
});
|
|
1901
|
-
});
|
|
1902
|
-
|
|
1903
|
-
// Add padding
|
|
1904
|
-
const padding = 8;
|
|
1905
|
-
minX -= padding;
|
|
1906
|
-
minY -= padding;
|
|
1907
|
-
maxX += padding;
|
|
1908
|
-
maxY += padding;
|
|
1909
|
-
|
|
1910
|
-
// Draw selection box
|
|
1911
|
-
this.ctx.strokeStyle = '#3b82f6';
|
|
1912
|
-
this.ctx.lineWidth = 2;
|
|
1913
|
-
this.ctx.setLineDash([5, 5]);
|
|
1914
|
-
this.ctx.strokeRect(minX, minY, maxX - minX, maxY - minY);
|
|
1915
|
-
this.ctx.setLineDash([]);
|
|
1916
|
-
|
|
1917
|
-
// Draw corner handles
|
|
1918
|
-
const handleSize = 8;
|
|
1919
|
-
this.ctx.fillStyle = '#3b82f6';
|
|
1920
|
-
const corners = [
|
|
1921
|
-
[minX, minY], [maxX, minY],
|
|
1922
|
-
[minX, maxY], [maxX, maxY]
|
|
1923
|
-
];
|
|
1924
|
-
corners.forEach(([x, y]) => {
|
|
1925
|
-
this.ctx.fillRect(x - handleSize/2, y - handleSize/2, handleSize, handleSize);
|
|
1926
|
-
});
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
|
-
// Draw selection highlight for a specific object (used for multi-select)
|
|
1930
|
-
drawSelectionHighlightForObject(objectId) {
|
|
1931
|
-
const selectedLines = this.lines.filter(l => l.objectId === objectId);
|
|
1932
|
-
if (selectedLines.length === 0) return;
|
|
1933
|
-
|
|
1934
|
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1935
|
-
selectedLines.forEach(line => {
|
|
1936
|
-
line.points.forEach(p => {
|
|
1937
|
-
minX = Math.min(minX, p.x);
|
|
1938
|
-
minY = Math.min(minY, p.y);
|
|
1939
|
-
maxX = Math.max(maxX, p.x);
|
|
1940
|
-
maxY = Math.max(maxY, p.y);
|
|
1941
|
-
});
|
|
1942
|
-
});
|
|
1943
|
-
|
|
1944
|
-
// Add padding
|
|
1945
|
-
const padding = 8;
|
|
1946
|
-
minX -= padding;
|
|
1947
|
-
minY -= padding;
|
|
1948
|
-
maxX += padding;
|
|
1949
|
-
maxY += padding;
|
|
1950
|
-
|
|
1951
|
-
// Draw selection box (green for multi-select to distinguish)
|
|
1952
|
-
this.ctx.strokeStyle = '#22c55e';
|
|
1953
|
-
this.ctx.lineWidth = 2;
|
|
1954
|
-
this.ctx.setLineDash([5, 5]);
|
|
1955
|
-
this.ctx.strokeRect(minX, minY, maxX - minX, maxY - minY);
|
|
1956
|
-
this.ctx.setLineDash([]);
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
clear() {
|
|
1960
|
-
this.lines = [];
|
|
1961
|
-
this.currentLine = null;
|
|
1962
|
-
this.textItems = [];
|
|
1963
|
-
this.images = [];
|
|
1964
|
-
this.textContainer.innerHTML = '';
|
|
1965
|
-
this.redraw();
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
setupClipboardPaste() {
|
|
1969
|
-
// Check clipboard when window gains focus
|
|
1970
|
-
window.glassboard.onFocusChange((focused) => {
|
|
1971
|
-
if (focused) {
|
|
1972
|
-
this.checkSystemClipboard();
|
|
1973
|
-
} else {
|
|
1974
|
-
this.hidePasteOverlay();
|
|
1975
|
-
}
|
|
1976
|
-
});
|
|
1977
|
-
|
|
1978
|
-
// Also check on initial load
|
|
1979
|
-
setTimeout(() => this.checkSystemClipboard(), 500);
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
// File drag-and-drop support
|
|
1983
|
-
setupFileDrop() {
|
|
1984
|
-
// Prevent default drag behaviors on document
|
|
1985
|
-
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event => {
|
|
1986
|
-
document.addEventListener(event, (e) => {
|
|
1987
|
-
e.preventDefault();
|
|
1988
|
-
e.stopPropagation();
|
|
1989
|
-
});
|
|
1990
|
-
});
|
|
1991
|
-
|
|
1992
|
-
// Visual feedback on dragover
|
|
1993
|
-
this.app.addEventListener('dragenter', () => {
|
|
1994
|
-
this.app.classList.add('drag-over');
|
|
1995
|
-
});
|
|
1996
|
-
|
|
1997
|
-
this.app.addEventListener('dragleave', (e) => {
|
|
1998
|
-
if (!this.app.contains(e.relatedTarget)) {
|
|
1999
|
-
this.app.classList.remove('drag-over');
|
|
2000
|
-
}
|
|
2001
|
-
});
|
|
2002
|
-
|
|
2003
|
-
// Handle drop
|
|
2004
|
-
this.app.addEventListener('drop', (e) => {
|
|
2005
|
-
this.app.classList.remove('drag-over');
|
|
2006
|
-
this.handleFileDrop(e);
|
|
2007
|
-
});
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
handleFileDrop(e) {
|
|
2011
|
-
const files = e.dataTransfer.files;
|
|
2012
|
-
if (!files.length) return;
|
|
2013
|
-
|
|
2014
|
-
const dropX = e.clientX - this.app.getBoundingClientRect().left;
|
|
2015
|
-
const dropY = e.clientY - this.app.getBoundingClientRect().top;
|
|
2016
|
-
|
|
2017
|
-
// Process each file
|
|
2018
|
-
Array.from(files).forEach((file, index) => {
|
|
2019
|
-
const x = dropX + (index * 20); // Offset multiple files
|
|
2020
|
-
const y = dropY + (index * 20);
|
|
2021
|
-
|
|
2022
|
-
if (this.isImageFile(file)) {
|
|
2023
|
-
this.handleDroppedImage(file, x, y);
|
|
2024
|
-
} else {
|
|
2025
|
-
this.handleDroppedFile(file, x, y);
|
|
2026
|
-
}
|
|
2027
|
-
});
|
|
2028
|
-
|
|
2029
|
-
this.saveState();
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
isImageFile(file) {
|
|
2033
|
-
const imageTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml', 'image/bmp'];
|
|
2034
|
-
return imageTypes.includes(file.type) || /\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(file.name);
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
handleDroppedImage(file, x, y) {
|
|
2038
|
-
const reader = new FileReader();
|
|
2039
|
-
reader.onload = (e) => {
|
|
2040
|
-
const img = new Image();
|
|
2041
|
-
img.onload = () => {
|
|
2042
|
-
this.pasteImage(e.target.result, x, y, img.width, img.height);
|
|
2043
|
-
};
|
|
2044
|
-
img.src = e.target.result;
|
|
2045
|
-
};
|
|
2046
|
-
reader.readAsDataURL(file);
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
handleDroppedFile(file, x, y) {
|
|
2050
|
-
const id = Date.now().toString();
|
|
2051
|
-
|
|
2052
|
-
// Create attachment element
|
|
2053
|
-
const attachment = document.createElement('div');
|
|
2054
|
-
attachment.className = 'file-attachment';
|
|
2055
|
-
attachment.dataset.id = id;
|
|
2056
|
-
attachment.style.left = (x / this.zoomLevel) + 'px';
|
|
2057
|
-
attachment.style.top = (y / this.zoomLevel) + 'px';
|
|
2058
|
-
|
|
2059
|
-
// File icon based on extension
|
|
2060
|
-
const icon = this.getFileIcon(file.name);
|
|
2061
|
-
|
|
2062
|
-
attachment.innerHTML = `
|
|
2063
|
-
<div class="attachment-icon">${icon}</div>
|
|
2064
|
-
<div class="attachment-name" title="${file.name}">${file.name}</div>
|
|
2065
|
-
<button class="attachment-delete-btn">✕</button>
|
|
2066
|
-
`;
|
|
2067
|
-
|
|
2068
|
-
this.textContainer.appendChild(attachment);
|
|
2069
|
-
|
|
2070
|
-
// Store attachment data
|
|
2071
|
-
this.attachments.push({
|
|
2072
|
-
id,
|
|
2073
|
-
x: x / this.zoomLevel,
|
|
2074
|
-
y: y / this.zoomLevel,
|
|
2075
|
-
filePath: file.path, // Electron provides full path
|
|
2076
|
-
fileName: file.name,
|
|
2077
|
-
fileType: file.type || this.getExtension(file.name)
|
|
2078
|
-
});
|
|
2079
|
-
|
|
2080
|
-
// Setup interactions
|
|
2081
|
-
this.setupAttachmentDrag(attachment, id);
|
|
2082
|
-
this.setupAttachmentClick(attachment, id);
|
|
2083
|
-
|
|
2084
|
-
// Delete button
|
|
2085
|
-
attachment.querySelector('.attachment-delete-btn').addEventListener('click', (e) => {
|
|
2086
|
-
e.stopPropagation();
|
|
2087
|
-
this.deleteAttachment(id);
|
|
2088
|
-
});
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
getFileIcon(fileName) {
|
|
2092
|
-
const ext = this.getExtension(fileName).toLowerCase();
|
|
2093
|
-
const icons = {
|
|
2094
|
-
pdf: '📄', doc: '📝', docx: '📝', txt: '📃',
|
|
2095
|
-
xls: '📊', xlsx: '📊', csv: '📊',
|
|
2096
|
-
ppt: '📽️', pptx: '📽️',
|
|
2097
|
-
zip: '🗜️', rar: '🗜️', '7z': '🗜️',
|
|
2098
|
-
mp3: '🎵', wav: '🎵', m4a: '🎵',
|
|
2099
|
-
mp4: '🎬', mov: '🎬', avi: '🎬',
|
|
2100
|
-
js: '📜', py: '📜', html: '📜', css: '📜',
|
|
2101
|
-
default: '📎'
|
|
2102
|
-
};
|
|
2103
|
-
return icons[ext] || icons.default;
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
getExtension(fileName) {
|
|
2107
|
-
return fileName.split('.').pop() || '';
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
setupAttachmentDrag(element, id) {
|
|
2111
|
-
let isDragging = false;
|
|
2112
|
-
let startX, startY, initialX, initialY;
|
|
2113
|
-
|
|
2114
|
-
element.addEventListener('mousedown', (e) => {
|
|
2115
|
-
if (e.target.classList.contains('attachment-delete-btn')) return;
|
|
2116
|
-
isDragging = true;
|
|
2117
|
-
startX = e.clientX;
|
|
2118
|
-
startY = e.clientY;
|
|
2119
|
-
initialX = parseInt(element.style.left);
|
|
2120
|
-
initialY = parseInt(element.style.top);
|
|
2121
|
-
element.classList.add('dragging');
|
|
2122
|
-
});
|
|
2123
|
-
|
|
2124
|
-
document.addEventListener('mousemove', (e) => {
|
|
2125
|
-
if (!isDragging) return;
|
|
2126
|
-
const dx = (e.clientX - startX) / this.zoomLevel;
|
|
2127
|
-
const dy = (e.clientY - startY) / this.zoomLevel;
|
|
2128
|
-
element.style.left = (initialX + dx) + 'px';
|
|
2129
|
-
element.style.top = (initialY + dy) + 'px';
|
|
2130
|
-
});
|
|
2131
|
-
|
|
2132
|
-
document.addEventListener('mouseup', () => {
|
|
2133
|
-
if (isDragging) {
|
|
2134
|
-
isDragging = false;
|
|
2135
|
-
element.classList.remove('dragging');
|
|
2136
|
-
// Update stored position
|
|
2137
|
-
const att = this.attachments.find(a => a.id === id);
|
|
2138
|
-
if (att) {
|
|
2139
|
-
att.x = parseInt(element.style.left);
|
|
2140
|
-
att.y = parseInt(element.style.top);
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
});
|
|
2144
|
-
}
|
|
2145
|
-
|
|
2146
|
-
setupAttachmentClick(element, id) {
|
|
2147
|
-
element.addEventListener('dblclick', () => {
|
|
2148
|
-
const att = this.attachments.find(a => a.id === id);
|
|
2149
|
-
if (att?.filePath) {
|
|
2150
|
-
window.glassboard.openFile(att.filePath);
|
|
2151
|
-
}
|
|
2152
|
-
});
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
deleteAttachment(id) {
|
|
2156
|
-
this.attachments = this.attachments.filter(a => a.id !== id);
|
|
2157
|
-
const element = this.textContainer.querySelector(`.file-attachment[data-id="${id}"]`);
|
|
2158
|
-
if (element) element.remove();
|
|
2159
|
-
this.saveState();
|
|
2160
|
-
}
|
|
2161
|
-
|
|
2162
|
-
restoreAttachment(att) {
|
|
2163
|
-
const attachment = document.createElement('div');
|
|
2164
|
-
attachment.className = 'file-attachment';
|
|
2165
|
-
attachment.dataset.id = att.id;
|
|
2166
|
-
attachment.style.left = att.x + 'px';
|
|
2167
|
-
attachment.style.top = att.y + 'px';
|
|
2168
|
-
|
|
2169
|
-
const icon = this.getFileIcon(att.fileName);
|
|
2170
|
-
attachment.innerHTML = `
|
|
2171
|
-
<div class="attachment-icon">${icon}</div>
|
|
2172
|
-
<div class="attachment-name" title="${att.fileName}">${att.fileName}</div>
|
|
2173
|
-
<button class="attachment-delete-btn">✕</button>
|
|
2174
|
-
`;
|
|
2175
|
-
|
|
2176
|
-
this.textContainer.appendChild(attachment);
|
|
2177
|
-
this.setupAttachmentDrag(attachment, att.id);
|
|
2178
|
-
this.setupAttachmentClick(attachment, att.id);
|
|
2179
|
-
|
|
2180
|
-
attachment.querySelector('.attachment-delete-btn').addEventListener('click', (e) => {
|
|
2181
|
-
e.stopPropagation();
|
|
2182
|
-
this.deleteAttachment(att.id);
|
|
2183
|
-
});
|
|
2184
|
-
}
|
|
2185
|
-
|
|
2186
|
-
checkSystemClipboard() {
|
|
2187
|
-
const content = window.glassboard.getClipboardContent();
|
|
2188
|
-
if (content) {
|
|
2189
|
-
this.systemClipboard = content;
|
|
2190
|
-
} else {
|
|
2191
|
-
this.systemClipboard = null;
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
showPasteOverlay(x, y) {
|
|
2196
|
-
if (!this.systemClipboard) return;
|
|
2197
|
-
|
|
2198
|
-
// Remove existing overlay
|
|
2199
|
-
this.hidePasteOverlay();
|
|
2200
|
-
|
|
2201
|
-
const overlay = document.createElement('div');
|
|
2202
|
-
overlay.id = 'paste-overlay';
|
|
2203
|
-
overlay.className = 'paste-overlay';
|
|
2204
|
-
|
|
2205
|
-
if (this.systemClipboard.type === 'image') {
|
|
2206
|
-
// Image preview
|
|
2207
|
-
const img = document.createElement('img');
|
|
2208
|
-
img.src = this.systemClipboard.dataUrl;
|
|
2209
|
-
img.className = 'paste-preview-image';
|
|
2210
|
-
|
|
2211
|
-
const label = document.createElement('div');
|
|
2212
|
-
label.className = 'paste-label';
|
|
2213
|
-
label.innerHTML = `
|
|
2214
|
-
<span class="paste-icon">📋</span>
|
|
2215
|
-
<span>Paste Image</span>
|
|
2216
|
-
<span class="paste-size">${this.systemClipboard.width}×${this.systemClipboard.height}</span>
|
|
2217
|
-
`;
|
|
2218
|
-
|
|
2219
|
-
overlay.appendChild(img);
|
|
2220
|
-
overlay.appendChild(label);
|
|
2221
|
-
} else if (this.systemClipboard.type === 'text') {
|
|
2222
|
-
// Text preview
|
|
2223
|
-
const preview = document.createElement('div');
|
|
2224
|
-
preview.className = 'paste-preview-text';
|
|
2225
|
-
preview.textContent = this.systemClipboard.content.substring(0, 100) +
|
|
2226
|
-
(this.systemClipboard.content.length > 100 ? '...' : '');
|
|
2227
|
-
|
|
2228
|
-
const label = document.createElement('div');
|
|
2229
|
-
label.className = 'paste-label';
|
|
2230
|
-
label.innerHTML = `
|
|
2231
|
-
<span class="paste-icon">📋</span>
|
|
2232
|
-
<span>Paste Text</span>
|
|
2233
|
-
`;
|
|
2234
|
-
|
|
2235
|
-
overlay.appendChild(preview);
|
|
2236
|
-
overlay.appendChild(label);
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
// Position the overlay
|
|
2240
|
-
overlay.style.left = x + 'px';
|
|
2241
|
-
overlay.style.top = y + 'px';
|
|
2242
|
-
|
|
2243
|
-
// Click to paste
|
|
2244
|
-
overlay.addEventListener('click', (e) => {
|
|
2245
|
-
e.stopPropagation();
|
|
2246
|
-
this.pasteFromSystemClipboard(x, y);
|
|
2247
|
-
this.hidePasteOverlay();
|
|
2248
|
-
});
|
|
2249
|
-
|
|
2250
|
-
this.app.appendChild(overlay);
|
|
2251
|
-
this.pasteOverlayVisible = true;
|
|
2252
|
-
|
|
2253
|
-
// Auto-hide after a delay if not clicked
|
|
2254
|
-
setTimeout(() => {
|
|
2255
|
-
if (this.pasteOverlayVisible) {
|
|
2256
|
-
this.hidePasteOverlay();
|
|
2257
|
-
}
|
|
2258
|
-
}, 5000);
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
|
-
hidePasteOverlay() {
|
|
2262
|
-
const overlay = document.getElementById('paste-overlay');
|
|
2263
|
-
if (overlay) {
|
|
2264
|
-
overlay.remove();
|
|
2265
|
-
}
|
|
2266
|
-
this.pasteOverlayVisible = false;
|
|
2267
|
-
}
|
|
2268
|
-
|
|
2269
|
-
pasteFromSystemClipboard(x, y) {
|
|
2270
|
-
if (!this.systemClipboard) return;
|
|
2271
|
-
|
|
2272
|
-
if (this.systemClipboard.type === 'image') {
|
|
2273
|
-
this.pasteImage(this.systemClipboard.dataUrl, x, y,
|
|
2274
|
-
this.systemClipboard.width, this.systemClipboard.height);
|
|
2275
|
-
} else if (this.systemClipboard.type === 'text') {
|
|
2276
|
-
this.pasteText(this.systemClipboard.content, x, y);
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
pasteImage(dataUrl, x, y, width, height) {
|
|
2281
|
-
// Scale down if too large
|
|
2282
|
-
const maxSize = 400;
|
|
2283
|
-
let displayWidth = width;
|
|
2284
|
-
let displayHeight = height;
|
|
2285
|
-
|
|
2286
|
-
if (width > maxSize || height > maxSize) {
|
|
2287
|
-
const scale = Math.min(maxSize / width, maxSize / height);
|
|
2288
|
-
displayWidth = width * scale;
|
|
2289
|
-
displayHeight = height * scale;
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
// Create image element
|
|
2293
|
-
const id = Date.now().toString();
|
|
2294
|
-
const imageItem = document.createElement('div');
|
|
2295
|
-
imageItem.className = 'pasted-image';
|
|
2296
|
-
imageItem.dataset.id = id;
|
|
2297
|
-
imageItem.style.left = (x / this.zoomLevel) + 'px';
|
|
2298
|
-
imageItem.style.top = (y / this.zoomLevel) + 'px';
|
|
2299
|
-
imageItem.style.width = displayWidth + 'px';
|
|
2300
|
-
imageItem.style.height = displayHeight + 'px';
|
|
2301
|
-
|
|
2302
|
-
const img = document.createElement('img');
|
|
2303
|
-
img.src = dataUrl;
|
|
2304
|
-
img.draggable = false;
|
|
2305
|
-
|
|
2306
|
-
// Delete button
|
|
2307
|
-
const deleteBtn = document.createElement('button');
|
|
2308
|
-
deleteBtn.className = 'image-delete-btn';
|
|
2309
|
-
deleteBtn.innerHTML = '✕';
|
|
2310
|
-
deleteBtn.addEventListener('click', (e) => {
|
|
2311
|
-
e.stopPropagation();
|
|
2312
|
-
this.deleteImage(id);
|
|
2313
|
-
});
|
|
2314
|
-
|
|
2315
|
-
// Resize handle
|
|
2316
|
-
const resizeHandle = document.createElement('div');
|
|
2317
|
-
resizeHandle.className = 'image-resize-handle';
|
|
2318
|
-
|
|
2319
|
-
imageItem.appendChild(img);
|
|
2320
|
-
imageItem.appendChild(deleteBtn);
|
|
2321
|
-
imageItem.appendChild(resizeHandle);
|
|
2322
|
-
this.textContainer.appendChild(imageItem);
|
|
2323
|
-
|
|
2324
|
-
// Store reference
|
|
2325
|
-
this.images.push({
|
|
2326
|
-
id,
|
|
2327
|
-
x: x / this.zoomLevel,
|
|
2328
|
-
y: y / this.zoomLevel,
|
|
2329
|
-
width: displayWidth,
|
|
2330
|
-
height: displayHeight,
|
|
2331
|
-
dataUrl
|
|
2332
|
-
});
|
|
2333
|
-
|
|
2334
|
-
// Setup drag for image
|
|
2335
|
-
this.setupImageDrag(imageItem, id);
|
|
2336
|
-
this.setupImageResize(imageItem, id, resizeHandle);
|
|
2337
|
-
this.saveState();
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
setupImageDrag(element, id) {
|
|
2341
|
-
let isDragging = false;
|
|
2342
|
-
let startX, startY, initialX, initialY;
|
|
2343
|
-
|
|
2344
|
-
element.addEventListener('mousedown', (e) => {
|
|
2345
|
-
if (e.target.closest('.image-delete-btn') || e.target.closest('.image-resize-handle')) return;
|
|
2346
|
-
|
|
2347
|
-
isDragging = true;
|
|
2348
|
-
startX = e.clientX;
|
|
2349
|
-
startY = e.clientY;
|
|
2350
|
-
initialX = parseInt(element.style.left);
|
|
2351
|
-
initialY = parseInt(element.style.top);
|
|
2352
|
-
element.style.cursor = 'grabbing';
|
|
2353
|
-
e.preventDefault();
|
|
2354
|
-
});
|
|
2355
|
-
|
|
2356
|
-
document.addEventListener('mousemove', (e) => {
|
|
2357
|
-
if (!isDragging) return;
|
|
2358
|
-
|
|
2359
|
-
const dx = (e.clientX - startX) / this.zoomLevel;
|
|
2360
|
-
const dy = (e.clientY - startY) / this.zoomLevel;
|
|
2361
|
-
|
|
2362
|
-
element.style.left = (initialX + dx) + 'px';
|
|
2363
|
-
element.style.top = (initialY + dy) + 'px';
|
|
2364
|
-
});
|
|
2365
|
-
|
|
2366
|
-
document.addEventListener('mouseup', () => {
|
|
2367
|
-
if (isDragging) {
|
|
2368
|
-
isDragging = false;
|
|
2369
|
-
element.style.cursor = 'grab';
|
|
2370
|
-
|
|
2371
|
-
// Update stored position
|
|
2372
|
-
const imageData = this.images.find(i => i.id === id);
|
|
2373
|
-
if (imageData) {
|
|
2374
|
-
imageData.x = parseInt(element.style.left);
|
|
2375
|
-
imageData.y = parseInt(element.style.top);
|
|
2376
|
-
}
|
|
2377
|
-
}
|
|
2378
|
-
});
|
|
2379
|
-
}
|
|
2380
|
-
|
|
2381
|
-
setupImageResize(element, id, handle) {
|
|
2382
|
-
let isResizing = false;
|
|
2383
|
-
let startX, startY, initialWidth, initialHeight;
|
|
2384
|
-
|
|
2385
|
-
handle.addEventListener('mousedown', (e) => {
|
|
2386
|
-
isResizing = true;
|
|
2387
|
-
startX = e.clientX;
|
|
2388
|
-
startY = e.clientY;
|
|
2389
|
-
initialWidth = element.offsetWidth;
|
|
2390
|
-
initialHeight = element.offsetHeight;
|
|
2391
|
-
e.preventDefault();
|
|
2392
|
-
e.stopPropagation();
|
|
2393
|
-
});
|
|
2394
|
-
|
|
2395
|
-
document.addEventListener('mousemove', (e) => {
|
|
2396
|
-
if (!isResizing) return;
|
|
2397
|
-
|
|
2398
|
-
const dx = (e.clientX - startX) / this.zoomLevel;
|
|
2399
|
-
const dy = (e.clientY - startY) / this.zoomLevel;
|
|
2400
|
-
|
|
2401
|
-
// Maintain aspect ratio
|
|
2402
|
-
const scale = Math.max(dx / initialWidth, dy / initialHeight);
|
|
2403
|
-
const newWidth = Math.max(50, initialWidth * (1 + scale));
|
|
2404
|
-
const newHeight = Math.max(50, initialHeight * (1 + scale));
|
|
2405
|
-
|
|
2406
|
-
element.style.width = newWidth + 'px';
|
|
2407
|
-
element.style.height = newHeight + 'px';
|
|
2408
|
-
});
|
|
2409
|
-
|
|
2410
|
-
document.addEventListener('mouseup', () => {
|
|
2411
|
-
if (isResizing) {
|
|
2412
|
-
isResizing = false;
|
|
2413
|
-
|
|
2414
|
-
// Update stored size
|
|
2415
|
-
const imageData = this.images.find(i => i.id === id);
|
|
2416
|
-
if (imageData) {
|
|
2417
|
-
imageData.width = element.offsetWidth;
|
|
2418
|
-
imageData.height = element.offsetHeight;
|
|
2419
|
-
}
|
|
2420
|
-
}
|
|
2421
|
-
});
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
|
-
deleteImage(id) {
|
|
2425
|
-
this.images = this.images.filter(i => i.id !== id);
|
|
2426
|
-
const element = this.textContainer.querySelector(`.pasted-image[data-id="${id}"]`);
|
|
2427
|
-
if (element) {
|
|
2428
|
-
element.remove();
|
|
2429
|
-
}
|
|
2430
|
-
this.saveState();
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
pasteText(text, x, y) {
|
|
2434
|
-
// Create a new text item at the paste location
|
|
2435
|
-
const id = Date.now().toString();
|
|
2436
|
-
const item = document.createElement('div');
|
|
2437
|
-
item.className = 'text-item';
|
|
2438
|
-
item.dataset.id = id;
|
|
2439
|
-
item.style.left = (x / this.zoomLevel) + 'px';
|
|
2440
|
-
item.style.top = (y / this.zoomLevel) + 'px';
|
|
2441
|
-
item.style.color = this.currentColor;
|
|
2442
|
-
// Apply counter-scale so text doesn't zoom
|
|
2443
|
-
item.style.transform = `scale(${1 / this.zoomLevel})`;
|
|
2444
|
-
item.style.transformOrigin = 'top left';
|
|
2445
|
-
|
|
2446
|
-
// Add drag handle
|
|
2447
|
-
const dragHandle = document.createElement('div');
|
|
2448
|
-
dragHandle.className = 'text-drag-handle';
|
|
2449
|
-
dragHandle.innerHTML = `
|
|
2450
|
-
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
|
2451
|
-
<path d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"/>
|
|
2452
|
-
</svg>
|
|
2453
|
-
`;
|
|
2454
|
-
|
|
2455
|
-
// Add format bar
|
|
2456
|
-
const formatBar = document.createElement('div');
|
|
2457
|
-
formatBar.className = 'text-format-bar';
|
|
2458
|
-
formatBar.innerHTML = `
|
|
2459
|
-
<button class="format-btn" data-format="bold" title="Bold">B</button>
|
|
2460
|
-
<button class="format-btn" data-format="italic" title="Italic">I</button>
|
|
2461
|
-
<button class="format-btn" data-format="underline" title="Underline">U</button>
|
|
2462
|
-
<button class="format-btn delete-btn" title="Delete">✕</button>
|
|
2463
|
-
`;
|
|
2464
|
-
|
|
2465
|
-
// Create contentEditable div with pasted text
|
|
2466
|
-
const editor = document.createElement('div');
|
|
2467
|
-
editor.className = 'text-input';
|
|
2468
|
-
editor.contentEditable = 'true';
|
|
2469
|
-
// Escape HTML to prevent XSS when pasting plain text
|
|
2470
|
-
editor.textContent = text;
|
|
2471
|
-
editor.style.color = this.currentColor;
|
|
2472
|
-
|
|
2473
|
-
// Add resize handle
|
|
2474
|
-
const resizeHandle = document.createElement('div');
|
|
2475
|
-
resizeHandle.className = 'text-resize-handle';
|
|
2476
|
-
|
|
2477
|
-
item.appendChild(dragHandle);
|
|
2478
|
-
item.appendChild(formatBar);
|
|
2479
|
-
item.appendChild(editor);
|
|
2480
|
-
item.appendChild(resizeHandle);
|
|
2481
|
-
this.textContainer.appendChild(item);
|
|
2482
|
-
|
|
2483
|
-
this.textItems.push({
|
|
2484
|
-
id,
|
|
2485
|
-
x: x / this.zoomLevel,
|
|
2486
|
-
y: y / this.zoomLevel,
|
|
2487
|
-
content: editor.innerHTML,
|
|
2488
|
-
color: this.currentColor
|
|
2489
|
-
});
|
|
2490
|
-
|
|
2491
|
-
// Setup event handlers
|
|
2492
|
-
this.setupPastedTextItem(item, id, editor, formatBar);
|
|
2493
|
-
this.saveState();
|
|
2494
|
-
}
|
|
2495
|
-
|
|
2496
|
-
// Settings panel methods
|
|
2497
|
-
setupSettings() {
|
|
2498
|
-
this.settingsPanel = document.getElementById('settings-panel');
|
|
2499
|
-
const closeBtn = document.getElementById('settings-close');
|
|
2500
|
-
|
|
2501
|
-
// Close button
|
|
2502
|
-
closeBtn.addEventListener('click', () => {
|
|
2503
|
-
this.hideSettings();
|
|
2504
|
-
});
|
|
2505
|
-
|
|
2506
|
-
// Close on click outside
|
|
2507
|
-
this.settingsPanel.addEventListener('click', (e) => {
|
|
2508
|
-
if (e.target === this.settingsPanel) {
|
|
2509
|
-
this.hideSettings();
|
|
2510
|
-
}
|
|
2511
|
-
});
|
|
2512
|
-
|
|
2513
|
-
// Close on Escape
|
|
2514
|
-
document.addEventListener('keydown', (e) => {
|
|
2515
|
-
if (e.key === 'Escape' && this.settingsPanel.classList.contains('visible')) {
|
|
2516
|
-
e.preventDefault();
|
|
2517
|
-
this.hideSettings();
|
|
2518
|
-
}
|
|
2519
|
-
});
|
|
2520
|
-
|
|
2521
|
-
// Gesture toggles
|
|
2522
|
-
document.getElementById('setting-pinch-zoom').addEventListener('change', (e) => {
|
|
2523
|
-
this.settings.pinchZoom = e.target.checked;
|
|
2524
|
-
this.autoSave();
|
|
2525
|
-
});
|
|
2526
|
-
|
|
2527
|
-
document.getElementById('setting-pan').addEventListener('change', (e) => {
|
|
2528
|
-
this.settings.pan = e.target.checked;
|
|
2529
|
-
this.autoSave();
|
|
2530
|
-
});
|
|
2531
|
-
|
|
2532
|
-
document.getElementById('setting-rotate').addEventListener('change', (e) => {
|
|
2533
|
-
this.settings.rotate = e.target.checked;
|
|
2534
|
-
this.autoSave();
|
|
2535
|
-
});
|
|
2536
|
-
|
|
2537
|
-
// Zoom controls toggle
|
|
2538
|
-
document.getElementById('setting-zoom-controls').addEventListener('change', (e) => {
|
|
2539
|
-
this.settings.showZoomControls = e.target.checked;
|
|
2540
|
-
const zoomControls = document.getElementById('zoom-controls');
|
|
2541
|
-
if (zoomControls) {
|
|
2542
|
-
zoomControls.style.display = e.target.checked ? '' : 'none';
|
|
2543
|
-
}
|
|
2544
|
-
this.autoSave();
|
|
2545
|
-
});
|
|
2546
|
-
|
|
2547
|
-
// Active background mode options
|
|
2548
|
-
document.querySelectorAll('input[name="bg-mode"]').forEach(radio => {
|
|
2549
|
-
radio.addEventListener('change', (e) => {
|
|
2550
|
-
const mode = e.target.value;
|
|
2551
|
-
this.settings.activeBgMode = mode;
|
|
2552
|
-
this.app.dataset.activeBg = mode;
|
|
2553
|
-
if (this.app.classList.contains('focused')) {
|
|
2554
|
-
window.glassboard.setBackgroundMode(mode);
|
|
2555
|
-
}
|
|
2556
|
-
this.applyBackgroundModes();
|
|
2557
|
-
this.applyBackgroundOpacity();
|
|
2558
|
-
this.autoSave();
|
|
2559
|
-
});
|
|
2560
|
-
});
|
|
2561
|
-
|
|
2562
|
-
// Inactive background mode options
|
|
2563
|
-
document.querySelectorAll('input[name="inactive-bg-mode"]').forEach(radio => {
|
|
2564
|
-
radio.addEventListener('change', (e) => {
|
|
2565
|
-
const mode = e.target.value;
|
|
2566
|
-
this.settings.inactiveBgMode = mode;
|
|
2567
|
-
this.app.dataset.inactiveBg = mode;
|
|
2568
|
-
if (!this.app.classList.contains('focused')) {
|
|
2569
|
-
window.glassboard.setBackgroundMode(mode);
|
|
2570
|
-
}
|
|
2571
|
-
this.applyBackgroundModes();
|
|
2572
|
-
this.applyBackgroundOpacity();
|
|
2573
|
-
this.autoSave();
|
|
2574
|
-
});
|
|
2575
|
-
});
|
|
2576
|
-
|
|
2577
|
-
// Active opacity slider
|
|
2578
|
-
const activeOpacitySlider = document.getElementById('setting-active-opacity');
|
|
2579
|
-
const activeOpacityValue = document.getElementById('active-opacity-value');
|
|
2580
|
-
if (activeOpacitySlider) {
|
|
2581
|
-
activeOpacitySlider.addEventListener('input', (e) => {
|
|
2582
|
-
const value = parseInt(e.target.value);
|
|
2583
|
-
this.settings.activeOpacity = value;
|
|
2584
|
-
if (activeOpacityValue) {
|
|
2585
|
-
activeOpacityValue.textContent = `${value}%`;
|
|
2586
|
-
}
|
|
2587
|
-
this.applyBackgroundOpacity();
|
|
2588
|
-
this.autoSave();
|
|
2589
|
-
});
|
|
2590
|
-
}
|
|
2591
|
-
|
|
2592
|
-
// Inactive opacity slider
|
|
2593
|
-
const inactiveOpacitySlider = document.getElementById('setting-inactive-opacity');
|
|
2594
|
-
const inactiveOpacityValue = document.getElementById('inactive-opacity-value');
|
|
2595
|
-
if (inactiveOpacitySlider) {
|
|
2596
|
-
inactiveOpacitySlider.addEventListener('input', (e) => {
|
|
2597
|
-
const value = parseInt(e.target.value);
|
|
2598
|
-
this.settings.inactiveOpacity = value;
|
|
2599
|
-
if (inactiveOpacityValue) {
|
|
2600
|
-
inactiveOpacityValue.textContent = `${value}%`;
|
|
2601
|
-
}
|
|
2602
|
-
this.applyBackgroundOpacity();
|
|
2603
|
-
this.autoSave();
|
|
2604
|
-
});
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
// Clean slate setting
|
|
2608
|
-
const cleanSlateCheckbox = document.getElementById('setting-clean-slate');
|
|
2609
|
-
if (cleanSlateCheckbox) {
|
|
2610
|
-
cleanSlateCheckbox.addEventListener('change', (e) => {
|
|
2611
|
-
this.settings.openWithCleanSlate = e.target.checked;
|
|
2612
|
-
this.autoSave();
|
|
2613
|
-
});
|
|
2614
|
-
}
|
|
2615
|
-
|
|
2616
|
-
// Auto-save to folder setting
|
|
2617
|
-
const autoSaveFolderCheckbox = document.getElementById('setting-auto-save-folder');
|
|
2618
|
-
if (autoSaveFolderCheckbox) {
|
|
2619
|
-
autoSaveFolderCheckbox.addEventListener('change', (e) => {
|
|
2620
|
-
this.settings.autoSaveToFolder = e.target.checked;
|
|
2621
|
-
this.autoSave();
|
|
2622
|
-
});
|
|
2623
|
-
}
|
|
2624
|
-
|
|
2625
|
-
// Open folder button
|
|
2626
|
-
const openFolderBtn = document.getElementById('open-folder-btn');
|
|
2627
|
-
if (openFolderBtn) {
|
|
2628
|
-
openFolderBtn.addEventListener('click', () => {
|
|
2629
|
-
this.openFloatnoteFolder();
|
|
2630
|
-
});
|
|
2631
|
-
}
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
// Apply background opacity based on focus state
|
|
2635
|
-
applyBackgroundOpacity() {
|
|
2636
|
-
const isFocused = this.app.classList.contains('focused');
|
|
2637
|
-
const opacity = isFocused ? this.settings.activeOpacity : this.settings.inactiveOpacity;
|
|
2638
|
-
this.app.style.setProperty('--bg-opacity', opacity / 100);
|
|
2639
|
-
}
|
|
2640
|
-
|
|
2641
|
-
toggleSettings() {
|
|
2642
|
-
if (this.settingsPanel.classList.contains('visible')) {
|
|
2643
|
-
this.hideSettings();
|
|
2644
|
-
} else {
|
|
2645
|
-
this.showSettings();
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
showSettings() {
|
|
2650
|
-
this.settingsPanel.classList.add('visible');
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
|
-
hideSettings() {
|
|
2654
|
-
this.settingsPanel.classList.remove('visible');
|
|
2655
|
-
}
|
|
2656
|
-
|
|
2657
|
-
// Trackpad gesture support
|
|
2658
|
-
setupGestures() {
|
|
2659
|
-
// Pinch-to-zoom via wheel event (trackpad sends wheel events with ctrlKey)
|
|
2660
|
-
this.app.addEventListener('wheel', (e) => {
|
|
2661
|
-
// Pinch-to-zoom (trackpad sends ctrlKey with pinch)
|
|
2662
|
-
if (e.ctrlKey && this.settings.pinchZoom) {
|
|
2663
|
-
e.preventDefault();
|
|
2664
|
-
const delta = -e.deltaY * 0.01;
|
|
2665
|
-
const newZoom = Math.max(this.minZoom, Math.min(this.maxZoom, this.zoomLevel + delta));
|
|
2666
|
-
this.zoomLevel = newZoom;
|
|
2667
|
-
this.applyTransform();
|
|
2668
|
-
return;
|
|
2669
|
-
}
|
|
2670
|
-
|
|
2671
|
-
// Two-finger pan (no modifier key)
|
|
2672
|
-
if (this.settings.pan && !e.ctrlKey && !e.metaKey) {
|
|
2673
|
-
// Only pan if not drawing
|
|
2674
|
-
if (this.isDrawing) return;
|
|
2675
|
-
|
|
2676
|
-
e.preventDefault();
|
|
2677
|
-
this.panX -= e.deltaX;
|
|
2678
|
-
this.panY -= e.deltaY;
|
|
2679
|
-
this.applyTransform();
|
|
2680
|
-
}
|
|
2681
|
-
}, { passive: false });
|
|
2682
|
-
|
|
2683
|
-
// Safari/WebKit gesture events for pinch and rotate
|
|
2684
|
-
this.app.addEventListener('gesturestart', (e) => {
|
|
2685
|
-
e.preventDefault();
|
|
2686
|
-
this.gestureStartZoom = this.zoomLevel;
|
|
2687
|
-
this.gestureStartRotation = this.rotation;
|
|
2688
|
-
});
|
|
2689
|
-
|
|
2690
|
-
this.app.addEventListener('gesturechange', (e) => {
|
|
2691
|
-
e.preventDefault();
|
|
2692
|
-
|
|
2693
|
-
// Pinch-to-zoom
|
|
2694
|
-
if (this.settings.pinchZoom) {
|
|
2695
|
-
const newZoom = Math.max(this.minZoom, Math.min(this.maxZoom, this.gestureStartZoom * e.scale));
|
|
2696
|
-
this.zoomLevel = newZoom;
|
|
2697
|
-
}
|
|
2698
|
-
|
|
2699
|
-
// Rotation
|
|
2700
|
-
if (this.settings.rotate) {
|
|
2701
|
-
this.rotation = this.gestureStartRotation + e.rotation;
|
|
2702
|
-
}
|
|
2703
|
-
|
|
2704
|
-
this.applyTransform();
|
|
2705
|
-
});
|
|
2706
|
-
|
|
2707
|
-
this.app.addEventListener('gestureend', (e) => {
|
|
2708
|
-
e.preventDefault();
|
|
2709
|
-
});
|
|
2710
|
-
|
|
2711
|
-
// Double-tap to reset (via double-click)
|
|
2712
|
-
let lastTap = 0;
|
|
2713
|
-
this.app.addEventListener('click', (e) => {
|
|
2714
|
-
const now = Date.now();
|
|
2715
|
-
if (now - lastTap < 300) {
|
|
2716
|
-
// Double-tap detected - reset transform
|
|
2717
|
-
this.resetTransform();
|
|
2718
|
-
}
|
|
2719
|
-
lastTap = now;
|
|
2720
|
-
});
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
// Undo/Redo functionality
|
|
2724
|
-
saveState() {
|
|
2725
|
-
if (this.isUndoRedoAction) return;
|
|
2726
|
-
|
|
2727
|
-
// Create a snapshot of current state
|
|
2728
|
-
const state = {
|
|
2729
|
-
lines: JSON.parse(JSON.stringify(this.lines)),
|
|
2730
|
-
textItems: this.textItems.map(t => ({ ...t })),
|
|
2731
|
-
images: this.images.map(i => ({ ...i }))
|
|
2732
|
-
};
|
|
2733
|
-
|
|
2734
|
-
// Remove any states after current index (new branch)
|
|
2735
|
-
this.history = this.history.slice(0, this.historyIndex + 1);
|
|
2736
|
-
|
|
2737
|
-
// Add new state
|
|
2738
|
-
this.history.push(state);
|
|
2739
|
-
this.historyIndex++;
|
|
2740
|
-
|
|
2741
|
-
// Limit history size
|
|
2742
|
-
if (this.history.length > this.maxHistorySize) {
|
|
2743
|
-
this.history.shift();
|
|
2744
|
-
this.historyIndex--;
|
|
2745
|
-
}
|
|
2746
|
-
|
|
2747
|
-
// Trigger auto-save
|
|
2748
|
-
this.autoSave();
|
|
2749
|
-
}
|
|
2750
|
-
|
|
2751
|
-
undo() {
|
|
2752
|
-
if (this.historyIndex <= 0) return;
|
|
2753
|
-
|
|
2754
|
-
this.isUndoRedoAction = true;
|
|
2755
|
-
this.historyIndex--;
|
|
2756
|
-
this.restoreState(this.history[this.historyIndex]);
|
|
2757
|
-
this.isUndoRedoAction = false;
|
|
2758
|
-
this.autoSave();
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2761
|
-
redo() {
|
|
2762
|
-
if (this.historyIndex >= this.history.length - 1) return;
|
|
2763
|
-
|
|
2764
|
-
this.isUndoRedoAction = true;
|
|
2765
|
-
this.historyIndex++;
|
|
2766
|
-
this.restoreState(this.history[this.historyIndex]);
|
|
2767
|
-
this.isUndoRedoAction = false;
|
|
2768
|
-
this.autoSave();
|
|
2769
|
-
}
|
|
2770
|
-
|
|
2771
|
-
restoreState(state) {
|
|
2772
|
-
// Restore lines
|
|
2773
|
-
this.lines = JSON.parse(JSON.stringify(state.lines));
|
|
2774
|
-
|
|
2775
|
-
// Clear and restore text items
|
|
2776
|
-
this.textContainer.querySelectorAll('.text-item').forEach(el => el.remove());
|
|
2777
|
-
this.textItems = [];
|
|
2778
|
-
state.textItems.forEach(item => {
|
|
2779
|
-
this.restoreTextItem(item);
|
|
2780
|
-
});
|
|
2781
|
-
|
|
2782
|
-
// Clear and restore images
|
|
2783
|
-
this.textContainer.querySelectorAll('.pasted-image').forEach(el => el.remove());
|
|
2784
|
-
this.images = [];
|
|
2785
|
-
state.images.forEach(img => {
|
|
2786
|
-
this.restoreImage(img);
|
|
2787
|
-
});
|
|
2788
|
-
|
|
2789
|
-
this.redraw();
|
|
2790
|
-
}
|
|
2791
|
-
|
|
2792
|
-
restoreTextItem(item) {
|
|
2793
|
-
const element = document.createElement('div');
|
|
2794
|
-
element.className = 'text-item';
|
|
2795
|
-
element.dataset.id = item.id;
|
|
2796
|
-
element.style.left = item.x + 'px';
|
|
2797
|
-
element.style.top = item.y + 'px';
|
|
2798
|
-
element.style.color = item.color;
|
|
2799
|
-
if (item.width) element.style.width = item.width + 'px';
|
|
2800
|
-
// Apply counter-scale so text doesn't zoom
|
|
2801
|
-
element.style.transform = `scale(${1 / this.zoomLevel})`;
|
|
2802
|
-
element.style.transformOrigin = 'top left';
|
|
2803
|
-
|
|
2804
|
-
const dragHandle = document.createElement('div');
|
|
2805
|
-
dragHandle.className = 'text-drag-handle';
|
|
2806
|
-
dragHandle.innerHTML = `<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"/></svg>`;
|
|
2807
|
-
|
|
2808
|
-
const formatBar = document.createElement('div');
|
|
2809
|
-
formatBar.className = 'text-format-bar';
|
|
2810
|
-
formatBar.innerHTML = `
|
|
2811
|
-
<button class="format-btn" data-format="bold" title="Bold">B</button>
|
|
2812
|
-
<button class="format-btn" data-format="italic" title="Italic">I</button>
|
|
2813
|
-
<button class="format-btn" data-format="underline" title="Underline">U</button>
|
|
2814
|
-
<button class="format-btn delete-btn" title="Delete">✕</button>
|
|
2815
|
-
`;
|
|
2816
|
-
|
|
2817
|
-
const editor = document.createElement('div');
|
|
2818
|
-
editor.className = 'text-input';
|
|
2819
|
-
editor.contentEditable = 'true';
|
|
2820
|
-
// Support both old 'text' and new 'content' formats for backwards compatibility
|
|
2821
|
-
editor.innerHTML = item.content || item.text || '';
|
|
2822
|
-
editor.style.color = item.color;
|
|
2823
|
-
|
|
2824
|
-
const resizeHandle = document.createElement('div');
|
|
2825
|
-
resizeHandle.className = 'text-resize-handle';
|
|
2826
|
-
|
|
2827
|
-
element.appendChild(dragHandle);
|
|
2828
|
-
element.appendChild(formatBar);
|
|
2829
|
-
element.appendChild(editor);
|
|
2830
|
-
element.appendChild(resizeHandle);
|
|
2831
|
-
this.textContainer.appendChild(element);
|
|
2832
|
-
|
|
2833
|
-
// Normalize to new format
|
|
2834
|
-
const normalizedItem = { ...item, content: item.content || item.text || '' };
|
|
2835
|
-
delete normalizedItem.text;
|
|
2836
|
-
this.textItems.push(normalizedItem);
|
|
2837
|
-
this.setupPastedTextItem(element, item.id, editor, formatBar);
|
|
2838
|
-
}
|
|
2839
|
-
|
|
2840
|
-
restoreImage(img) {
|
|
2841
|
-
const imageItem = document.createElement('div');
|
|
2842
|
-
imageItem.className = 'pasted-image';
|
|
2843
|
-
imageItem.dataset.id = img.id;
|
|
2844
|
-
imageItem.style.left = img.x + 'px';
|
|
2845
|
-
imageItem.style.top = img.y + 'px';
|
|
2846
|
-
imageItem.style.width = img.width + 'px';
|
|
2847
|
-
imageItem.style.height = img.height + 'px';
|
|
2848
|
-
|
|
2849
|
-
const imgEl = document.createElement('img');
|
|
2850
|
-
imgEl.src = img.dataUrl;
|
|
2851
|
-
imgEl.draggable = false;
|
|
2852
|
-
|
|
2853
|
-
const deleteBtn = document.createElement('button');
|
|
2854
|
-
deleteBtn.className = 'image-delete-btn';
|
|
2855
|
-
deleteBtn.innerHTML = '✕';
|
|
2856
|
-
deleteBtn.addEventListener('click', (e) => {
|
|
2857
|
-
e.stopPropagation();
|
|
2858
|
-
this.deleteImage(img.id);
|
|
2859
|
-
});
|
|
2860
|
-
|
|
2861
|
-
const resizeHandle = document.createElement('div');
|
|
2862
|
-
resizeHandle.className = 'image-resize-handle';
|
|
2863
|
-
|
|
2864
|
-
imageItem.appendChild(imgEl);
|
|
2865
|
-
imageItem.appendChild(deleteBtn);
|
|
2866
|
-
imageItem.appendChild(resizeHandle);
|
|
2867
|
-
this.textContainer.appendChild(imageItem);
|
|
2868
|
-
|
|
2869
|
-
this.images.push({ ...img });
|
|
2870
|
-
this.setupImageDrag(imageItem, img.id);
|
|
2871
|
-
this.setupImageResize(imageItem, img.id, resizeHandle);
|
|
2872
|
-
}
|
|
2873
|
-
|
|
2874
|
-
// Smart paste - checks system clipboard first, then internal clipboard
|
|
2875
|
-
smartPaste() {
|
|
2876
|
-
// Check system clipboard first
|
|
2877
|
-
this.checkSystemClipboard();
|
|
2878
|
-
|
|
2879
|
-
if (this.systemClipboard) {
|
|
2880
|
-
// Paste from system clipboard at center of canvas
|
|
2881
|
-
const rect = this.app.getBoundingClientRect();
|
|
2882
|
-
const x = rect.width / 2;
|
|
2883
|
-
const y = rect.height / 2;
|
|
2884
|
-
this.pasteFromSystemClipboard(x, y);
|
|
2885
|
-
this.saveState();
|
|
2886
|
-
} else if (this.clipboard) {
|
|
2887
|
-
// Paste from internal clipboard
|
|
2888
|
-
this.paste();
|
|
2889
|
-
this.saveState();
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
|
-
|
|
2893
|
-
// Data persistence
|
|
2894
|
-
autoSave() {
|
|
2895
|
-
clearTimeout(this.saveTimeout);
|
|
2896
|
-
this.saveTimeout = setTimeout(() => {
|
|
2897
|
-
this.saveData();
|
|
2898
|
-
}, 1000); // Debounce saves to 1 second
|
|
2899
|
-
}
|
|
2900
|
-
|
|
2901
|
-
async saveData() {
|
|
2902
|
-
const data = {
|
|
2903
|
-
notes: this.notes,
|
|
2904
|
-
currentNoteIndex: this.currentNoteIndex,
|
|
2905
|
-
settings: this.settings,
|
|
2906
|
-
transform: {
|
|
2907
|
-
zoomLevel: this.zoomLevel,
|
|
2908
|
-
panX: this.panX,
|
|
2909
|
-
panY: this.panY,
|
|
2910
|
-
rotation: this.rotation
|
|
2911
|
-
}
|
|
2912
|
-
};
|
|
2913
|
-
|
|
2914
|
-
try {
|
|
2915
|
-
await window.glassboard.saveData(data);
|
|
2916
|
-
|
|
2917
|
-
// Also save to ~/.floatnote folder if enabled
|
|
2918
|
-
if (this.settings.autoSaveToFolder && this.notes[this.currentNoteIndex]) {
|
|
2919
|
-
await window.glassboard.exportToFloatnote(this.notes[this.currentNoteIndex]);
|
|
2920
|
-
}
|
|
2921
|
-
} catch (error) {
|
|
2922
|
-
console.error('Failed to save data:', error);
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
|
|
2926
|
-
// Export current note as PNG
|
|
2927
|
-
async exportAsPNG() {
|
|
2928
|
-
try {
|
|
2929
|
-
// Create a temporary canvas to render everything
|
|
2930
|
-
const exportCanvas = document.createElement('canvas');
|
|
2931
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
2932
|
-
exportCanvas.width = rect.width;
|
|
2933
|
-
exportCanvas.height = rect.height;
|
|
2934
|
-
const exportCtx = exportCanvas.getContext('2d');
|
|
2935
|
-
|
|
2936
|
-
// Fill with transparent background
|
|
2937
|
-
exportCtx.clearRect(0, 0, exportCanvas.width, exportCanvas.height);
|
|
2938
|
-
|
|
2939
|
-
// Draw all lines from current note
|
|
2940
|
-
this.lines.forEach(line => {
|
|
2941
|
-
if (line.points.length < 2) return;
|
|
2942
|
-
exportCtx.beginPath();
|
|
2943
|
-
exportCtx.strokeStyle = line.color;
|
|
2944
|
-
exportCtx.lineWidth = line.width;
|
|
2945
|
-
exportCtx.lineCap = 'round';
|
|
2946
|
-
exportCtx.lineJoin = 'round';
|
|
2947
|
-
exportCtx.moveTo(line.points[0].x, line.points[0].y);
|
|
2948
|
-
for (let i = 1; i < line.points.length; i++) {
|
|
2949
|
-
exportCtx.lineTo(line.points[i].x, line.points[i].y);
|
|
2950
|
-
}
|
|
2951
|
-
exportCtx.stroke();
|
|
2952
|
-
});
|
|
2953
|
-
|
|
2954
|
-
// Draw text items
|
|
2955
|
-
this.textItems.forEach(item => {
|
|
2956
|
-
const element = this.textContainer.querySelector(`[data-id="${item.id}"]`);
|
|
2957
|
-
if (element) {
|
|
2958
|
-
const editor = element.querySelector('.text-input');
|
|
2959
|
-
if (editor && editor.textContent) {
|
|
2960
|
-
exportCtx.font = '16px system-ui, -apple-system, sans-serif';
|
|
2961
|
-
exportCtx.fillStyle = item.color;
|
|
2962
|
-
const lines = editor.textContent.split('\n');
|
|
2963
|
-
lines.forEach((line, i) => {
|
|
2964
|
-
exportCtx.fillText(line, item.x, item.y + 20 + (i * 20));
|
|
2965
|
-
});
|
|
2966
|
-
}
|
|
2967
|
-
}
|
|
2968
|
-
});
|
|
2969
|
-
|
|
2970
|
-
// Draw images
|
|
2971
|
-
for (const img of this.images) {
|
|
2972
|
-
const imgElement = document.querySelector(`.pasted-image[data-id="${img.id}"] img`);
|
|
2973
|
-
if (imgElement) {
|
|
2974
|
-
exportCtx.drawImage(imgElement, img.x, img.y, img.width, img.height);
|
|
2975
|
-
}
|
|
2976
|
-
}
|
|
2977
|
-
|
|
2978
|
-
// Get data URL and export
|
|
2979
|
-
const dataUrl = exportCanvas.toDataURL('image/png');
|
|
2980
|
-
const result = await window.glassboard.exportPNG(dataUrl);
|
|
2981
|
-
|
|
2982
|
-
if (result.success) {
|
|
2983
|
-
console.log('Exported to:', result.path);
|
|
2984
|
-
}
|
|
2985
|
-
} catch (error) {
|
|
2986
|
-
console.error('Failed to export PNG:', error);
|
|
2987
|
-
}
|
|
2988
|
-
}
|
|
2989
|
-
|
|
2990
|
-
// Open ~/.floatnote folder in Finder
|
|
2991
|
-
async openFloatnoteFolder() {
|
|
2992
|
-
try {
|
|
2993
|
-
await window.glassboard.openFloatnoteFolder();
|
|
2994
|
-
} catch (error) {
|
|
2995
|
-
console.error('Failed to open folder:', error);
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
|
|
2999
|
-
async loadSavedData() {
|
|
3000
|
-
try {
|
|
3001
|
-
const result = await window.glassboard.loadData();
|
|
3002
|
-
if (result.success && result.data) {
|
|
3003
|
-
const data = result.data;
|
|
3004
|
-
|
|
3005
|
-
// Restore settings first
|
|
3006
|
-
if (data.settings) {
|
|
3007
|
-
this.settings = { ...this.settings, ...data.settings };
|
|
3008
|
-
// Update settings UI
|
|
3009
|
-
document.getElementById('setting-pinch-zoom').checked = this.settings.pinchZoom;
|
|
3010
|
-
document.getElementById('setting-pan').checked = this.settings.pan;
|
|
3011
|
-
document.getElementById('setting-rotate').checked = this.settings.rotate;
|
|
3012
|
-
document.getElementById('setting-zoom-controls').checked = this.settings.showZoomControls;
|
|
3013
|
-
const cleanSlateCheckbox = document.getElementById('setting-clean-slate');
|
|
3014
|
-
if (cleanSlateCheckbox) {
|
|
3015
|
-
cleanSlateCheckbox.checked = this.settings.openWithCleanSlate;
|
|
3016
|
-
}
|
|
3017
|
-
const autoSaveFolderCheckbox = document.getElementById('setting-auto-save-folder');
|
|
3018
|
-
if (autoSaveFolderCheckbox) {
|
|
3019
|
-
autoSaveFolderCheckbox.checked = this.settings.autoSaveToFolder || false;
|
|
3020
|
-
}
|
|
3021
|
-
const zoomControls = document.getElementById('zoom-controls');
|
|
3022
|
-
if (zoomControls) {
|
|
3023
|
-
zoomControls.style.display = this.settings.showZoomControls ? '' : 'none';
|
|
3024
|
-
}
|
|
3025
|
-
|
|
3026
|
-
// Restore background settings
|
|
3027
|
-
const activeBgRadio = document.querySelector(`input[name="bg-mode"][value="${this.settings.activeBgMode || 'transparent'}"]`);
|
|
3028
|
-
if (activeBgRadio) activeBgRadio.checked = true;
|
|
3029
|
-
this.app.dataset.activeBg = this.settings.activeBgMode || 'transparent';
|
|
3030
|
-
|
|
3031
|
-
const inactiveBgRadio = document.querySelector(`input[name="inactive-bg-mode"][value="${this.settings.inactiveBgMode || 'transparent'}"]`);
|
|
3032
|
-
if (inactiveBgRadio) inactiveBgRadio.checked = true;
|
|
3033
|
-
this.app.dataset.inactiveBg = this.settings.inactiveBgMode || 'transparent';
|
|
3034
|
-
|
|
3035
|
-
// Restore opacity settings
|
|
3036
|
-
const activeOpacitySlider = document.getElementById('setting-active-opacity');
|
|
3037
|
-
const activeOpacityValue = document.getElementById('active-opacity-value');
|
|
3038
|
-
if (activeOpacitySlider) {
|
|
3039
|
-
activeOpacitySlider.value = this.settings.activeOpacity || 100;
|
|
3040
|
-
if (activeOpacityValue) activeOpacityValue.textContent = `${this.settings.activeOpacity || 100}%`;
|
|
3041
|
-
}
|
|
3042
|
-
|
|
3043
|
-
const inactiveOpacitySlider = document.getElementById('setting-inactive-opacity');
|
|
3044
|
-
const inactiveOpacityValue = document.getElementById('inactive-opacity-value');
|
|
3045
|
-
if (inactiveOpacitySlider) {
|
|
3046
|
-
inactiveOpacitySlider.value = this.settings.inactiveOpacity || 50;
|
|
3047
|
-
if (inactiveOpacityValue) inactiveOpacityValue.textContent = `${this.settings.inactiveOpacity || 50}%`;
|
|
3048
|
-
}
|
|
3049
|
-
|
|
3050
|
-
this.applyBackgroundModes();
|
|
3051
|
-
this.applyBackgroundOpacity();
|
|
3052
|
-
|
|
3053
|
-
// Apply native vibrancy based on restored settings
|
|
3054
|
-
const currentBgMode = this.app.classList.contains('focused')
|
|
3055
|
-
? this.settings.activeBgMode
|
|
3056
|
-
: this.settings.inactiveBgMode;
|
|
3057
|
-
if (currentBgMode === 'blur') {
|
|
3058
|
-
window.glassboard.setBackgroundMode('blur');
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
|
|
3062
|
-
// Check if we should open with clean slate
|
|
3063
|
-
if (this.settings.openWithCleanSlate) {
|
|
3064
|
-
// Keep the notes but create/go to a new blank note
|
|
3065
|
-
if (data.notes && data.notes.length > 0) {
|
|
3066
|
-
this.notes = data.notes;
|
|
3067
|
-
this.notes.push(this.createEmptyNote());
|
|
3068
|
-
this.currentNoteIndex = this.notes.length - 1;
|
|
3069
|
-
}
|
|
3070
|
-
// Don't restore transform - start fresh
|
|
3071
|
-
this.redraw();
|
|
3072
|
-
this.saveState();
|
|
3073
|
-
return;
|
|
3074
|
-
}
|
|
3075
|
-
|
|
3076
|
-
// Restore notes (new multi-note format)
|
|
3077
|
-
if (data.notes && data.notes.length > 0) {
|
|
3078
|
-
this.notes = data.notes;
|
|
3079
|
-
this.currentNoteIndex = data.currentNoteIndex || 0;
|
|
3080
|
-
|
|
3081
|
-
// Make sure index is valid
|
|
3082
|
-
if (this.currentNoteIndex >= this.notes.length) {
|
|
3083
|
-
this.currentNoteIndex = this.notes.length - 1;
|
|
3084
|
-
}
|
|
3085
|
-
|
|
3086
|
-
// Load current note display
|
|
3087
|
-
this.loadCurrentNote();
|
|
3088
|
-
}
|
|
3089
|
-
// Legacy format support (single note)
|
|
3090
|
-
else if (data.lines || data.textItems || data.images) {
|
|
3091
|
-
// Migrate old format to new format
|
|
3092
|
-
this.notes = [{
|
|
3093
|
-
id: Date.now().toString(),
|
|
3094
|
-
lines: data.lines || [],
|
|
3095
|
-
textItems: data.textItems || [],
|
|
3096
|
-
images: data.images || [],
|
|
3097
|
-
createdAt: Date.now(),
|
|
3098
|
-
lastModified: Date.now()
|
|
3099
|
-
}];
|
|
3100
|
-
this.currentNoteIndex = 0;
|
|
3101
|
-
this.loadCurrentNote();
|
|
3102
|
-
}
|
|
3103
|
-
|
|
3104
|
-
// Restore transform
|
|
3105
|
-
if (data.transform) {
|
|
3106
|
-
this.zoomLevel = data.transform.zoomLevel || 1;
|
|
3107
|
-
this.panX = data.transform.panX || 0;
|
|
3108
|
-
this.panY = data.transform.panY || 0;
|
|
3109
|
-
this.rotation = data.transform.rotation || 0;
|
|
3110
|
-
this.applyTransform();
|
|
3111
|
-
}
|
|
3112
|
-
|
|
3113
|
-
// Initialize history with loaded state
|
|
3114
|
-
this.saveState();
|
|
3115
|
-
}
|
|
3116
|
-
} catch (error) {
|
|
3117
|
-
console.error('Failed to load data:', error);
|
|
3118
|
-
}
|
|
3119
|
-
}
|
|
3120
|
-
|
|
3121
|
-
setupLeftResize() {
|
|
3122
|
-
const handle = document.getElementById('left-resize-handle');
|
|
3123
|
-
if (!handle) return;
|
|
3124
|
-
|
|
3125
|
-
let isResizing = false;
|
|
3126
|
-
let startX = 0;
|
|
3127
|
-
|
|
3128
|
-
handle.addEventListener('mousedown', (e) => {
|
|
3129
|
-
isResizing = true;
|
|
3130
|
-
startX = e.screenX;
|
|
3131
|
-
e.preventDefault();
|
|
3132
|
-
});
|
|
3133
|
-
|
|
3134
|
-
document.addEventListener('mousemove', (e) => {
|
|
3135
|
-
if (!isResizing) return;
|
|
3136
|
-
|
|
3137
|
-
const deltaX = e.screenX - startX;
|
|
3138
|
-
if (deltaX !== 0) {
|
|
3139
|
-
window.glassboard.resizeWindowLeft(deltaX);
|
|
3140
|
-
startX = e.screenX;
|
|
3141
|
-
}
|
|
3142
|
-
});
|
|
3143
|
-
|
|
3144
|
-
document.addEventListener('mouseup', () => {
|
|
3145
|
-
isResizing = false;
|
|
3146
|
-
});
|
|
3147
|
-
}
|
|
3148
|
-
|
|
3149
|
-
// Select all items
|
|
3150
|
-
selectAll() {
|
|
3151
|
-
// Select all drawn objects (by selecting the most recent object group)
|
|
3152
|
-
const objectIds = [...new Set(this.lines.map(l => l.objectId))];
|
|
3153
|
-
this.selectedObjects = objectIds;
|
|
3154
|
-
|
|
3155
|
-
// Select all text items
|
|
3156
|
-
this.textItems.forEach(item => {
|
|
3157
|
-
const element = this.textContainer.querySelector(`[data-id="${item.id}"]`);
|
|
3158
|
-
if (element) {
|
|
3159
|
-
element.classList.add('selected');
|
|
3160
|
-
}
|
|
3161
|
-
});
|
|
3162
|
-
|
|
3163
|
-
// Select all images
|
|
3164
|
-
this.images.forEach(img => {
|
|
3165
|
-
const element = this.textContainer.querySelector(`.pasted-image[data-id="${img.id}"]`);
|
|
3166
|
-
if (element) {
|
|
3167
|
-
element.classList.add('selected');
|
|
3168
|
-
}
|
|
3169
|
-
});
|
|
3170
|
-
|
|
3171
|
-
this.allSelected = true;
|
|
3172
|
-
// Redraw to show selection highlights on drawn objects
|
|
3173
|
-
this.redraw();
|
|
3174
|
-
}
|
|
3175
|
-
|
|
3176
|
-
// Delete all selected items
|
|
3177
|
-
deleteAllSelected() {
|
|
3178
|
-
if (!this.allSelected) return;
|
|
3179
|
-
|
|
3180
|
-
// Delete all lines
|
|
3181
|
-
this.lines = [];
|
|
3182
|
-
|
|
3183
|
-
// Delete all text items
|
|
3184
|
-
this.textContainer.querySelectorAll('.text-item').forEach(el => el.remove());
|
|
3185
|
-
this.textItems = [];
|
|
3186
|
-
|
|
3187
|
-
// Delete all images
|
|
3188
|
-
this.textContainer.querySelectorAll('.pasted-image').forEach(el => el.remove());
|
|
3189
|
-
this.images = [];
|
|
3190
|
-
|
|
3191
|
-
this.allSelected = false;
|
|
3192
|
-
this.selectedObjectId = null;
|
|
3193
|
-
this.selectedTextId = null;
|
|
3194
|
-
|
|
3195
|
-
this.redraw();
|
|
3196
|
-
this.saveState();
|
|
3197
|
-
}
|
|
3198
|
-
}
|
|
3199
|
-
|
|
3200
|
-
// Initialize when DOM is ready
|
|
3201
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
3202
|
-
new Glassboard();
|
|
3203
|
-
});
|