@mhmttosun/dnd 0.0.0-stage → 1.1.1

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.
@@ -0,0 +1,459 @@
1
+ /**
2
+ * SelectionManager - Tekli ve çoklu nesne seçimi, seçim çerçevesi,
3
+ * 8 yönlü boyutlandırma tutamaçları ve döndürme tutamacını yönetir.
4
+ */
5
+ class SelectionManager {
6
+ constructor(core, stageManager, objectManager, stageEl, options = {}) {
7
+ this.core = core;
8
+ this.stageManager = stageManager;
9
+ this.objectManager = objectManager;
10
+ this.stage = stageEl;
11
+ this.signal = options.signal;
12
+
13
+ this.selectedIds = new Set();
14
+ this.clipboard = [];
15
+
16
+ this.selectionOverlay = stageEl.querySelector('#selection-overlay');
17
+ this.init();
18
+ }
19
+
20
+ init() {
21
+ this.setupEventListeners();
22
+ this.core.on('projectLoaded', () => this.clearSelection());
23
+ this.core.on('objectDeleted', (e) => this.deselect(e.object.id));
24
+ this.core.on('objectUpdated', () => this.updateSelectionBox());
25
+ this.core.on('modeChanged', () => {
26
+ if (this.core.getMode() !== 'design') {
27
+ this.clearSelection();
28
+ }
29
+ });
30
+ }
31
+
32
+ setupEventListeners() {
33
+ // Klavye kısayolları
34
+ window.addEventListener('keydown', (e) => {
35
+ // Eğer kullanıcı bir input, textarea veya contenteditable içindeyse dinleme
36
+ if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement?.tagName) ||
37
+ document.activeElement?.isContentEditable) {
38
+ return;
39
+ }
40
+
41
+ if (this.core.getMode() !== 'design') return;
42
+
43
+ // Silme: Delete veya Backspace
44
+ if (e.key === 'Delete' || e.key === 'Backspace') {
45
+ if (this.selectedIds.size > 0) {
46
+ e.preventDefault();
47
+ this.deleteSelected();
48
+ }
49
+ }
50
+
51
+ // Kopyala: Ctrl+C / Cmd+C
52
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'c') {
53
+ e.preventDefault();
54
+ this.copy();
55
+ }
56
+
57
+ // Yapıştır: Ctrl+V / Cmd+V
58
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'v') {
59
+ e.preventDefault();
60
+ this.paste();
61
+ }
62
+
63
+ // Grupla: Ctrl+G / Cmd+G
64
+ if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key.toLowerCase() === 'g') {
65
+ e.preventDefault();
66
+ this.groupSelected();
67
+ }
68
+
69
+ // Grubu Çöz: Ctrl+Shift+G / Cmd+Shift+G
70
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'g') {
71
+ e.preventDefault();
72
+ this.ungroupSelected();
73
+ }
74
+
75
+ // Çoğalt: Ctrl+D / Cmd+D
76
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'd') {
77
+ e.preventDefault();
78
+ this.duplicateSelected();
79
+ }
80
+
81
+ // Tümünü Seç: Ctrl+A / Cmd+A
82
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'a') {
83
+ e.preventDefault();
84
+ this.selectAll();
85
+ }
86
+
87
+ // Seçimi Bırak: Escape
88
+ if (e.key === 'Escape') {
89
+ this.clearSelection();
90
+ }
91
+
92
+ // Enter: Seçili nesneyi yerinde düzenlemeye başla
93
+ if (e.key === 'Enter' && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
94
+ if (this.selectedIds.size === 1) {
95
+ const primary = this.getPrimarySelected();
96
+ if (primary && !primary.locked) {
97
+ e.preventDefault();
98
+ this.core.emit('requestInlineTextEdit', { objectId: primary.id });
99
+ }
100
+ }
101
+ }
102
+
103
+ // Ok Tuşlarıyla Kaydırma (Nudge)
104
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
105
+ if (this.selectedIds.size > 0) {
106
+ e.preventDefault();
107
+ const step = e.shiftKey ? 10 : 1;
108
+ let dx = 0;
109
+ let dy = 0;
110
+ if (e.key === 'ArrowUp') dy = -step;
111
+ if (e.key === 'ArrowDown') dy = step;
112
+ if (e.key === 'ArrowLeft') dx = -step;
113
+ if (e.key === 'ArrowRight') dx = step;
114
+
115
+ this.nudgeSelected(dx, dy);
116
+ }
117
+ }
118
+ }, this.signal ? { signal: this.signal } : undefined);
119
+ }
120
+
121
+ select(id, multi = false) {
122
+ if (this.core.getMode() !== 'design') return;
123
+ const obj = this.objectManager.getObject(id);
124
+ if (!obj) return;
125
+
126
+ if (!multi) {
127
+ this.selectedIds.clear();
128
+ }
129
+
130
+ // Eğer nesne bir gruba aitse, grubun tüm üyelerini seç
131
+ if (obj.groupId) {
132
+ const groupMembers = this.objectManager.getObjects().filter(o => o.groupId === obj.groupId);
133
+ groupMembers.forEach(m => this.selectedIds.add(m.id));
134
+ } else {
135
+ this.selectedIds.add(id);
136
+ }
137
+
138
+ this.updateSelectionBox();
139
+ this.core.emit('selectionChanged', {
140
+ selectedIds: Array.from(this.selectedIds),
141
+ primaryObject: obj
142
+ });
143
+ }
144
+
145
+ toggleSelect(id) {
146
+ const obj = this.objectManager.getObject(id);
147
+ if (!obj) return;
148
+
149
+ if (obj.groupId) {
150
+ const groupMembers = this.objectManager.getObjects().filter(o => o.groupId === obj.groupId);
151
+ const isAnySelected = groupMembers.some(m => this.selectedIds.has(m.id));
152
+ if (isAnySelected) {
153
+ groupMembers.forEach(m => this.selectedIds.delete(m.id));
154
+ } else {
155
+ groupMembers.forEach(m => this.selectedIds.add(m.id));
156
+ }
157
+ } else {
158
+ if (this.selectedIds.has(id)) {
159
+ this.deselect(id);
160
+ } else {
161
+ this.select(id, true);
162
+ }
163
+ }
164
+ this.updateSelectionBox();
165
+ this.core.emit('selectionChanged', {
166
+ selectedIds: Array.from(this.selectedIds),
167
+ primaryObject: this.getPrimarySelected()
168
+ });
169
+ }
170
+
171
+ deselect(id) {
172
+ this.selectedIds.delete(id);
173
+ this.updateSelectionBox();
174
+ this.core.emit('selectionChanged', {
175
+ selectedIds: Array.from(this.selectedIds),
176
+ primaryObject: this.getPrimarySelected()
177
+ });
178
+ }
179
+
180
+ clearSelection() {
181
+ if (this.selectedIds.size === 0) return;
182
+ this.selectedIds.clear();
183
+ this.updateSelectionBox();
184
+ this.core.emit('selectionChanged', { selectedIds: [], primaryObject: null });
185
+ }
186
+
187
+ selectAll() {
188
+ this.selectedIds.clear();
189
+ this.objectManager.getObjects().forEach(obj => {
190
+ if (!obj.locked && obj.visible !== false) {
191
+ this.selectedIds.add(obj.id);
192
+ }
193
+ });
194
+ this.updateSelectionBox();
195
+ this.core.emit('selectionChanged', {
196
+ selectedIds: Array.from(this.selectedIds),
197
+ primaryObject: this.getPrimarySelected()
198
+ });
199
+ }
200
+
201
+ getSelectedObjects() {
202
+ return Array.from(this.selectedIds)
203
+ .map(id => this.objectManager.getObject(id))
204
+ .filter(Boolean);
205
+ }
206
+
207
+ getPrimarySelected() {
208
+ if (this.selectedIds.size === 0) return null;
209
+ const firstId = this.selectedIds.values().next().value;
210
+ return this.objectManager.getObject(firstId) || null;
211
+ }
212
+
213
+ groupSelected() {
214
+ const selected = this.getSelectedObjects();
215
+ if (selected.length < 2) return null;
216
+
217
+ const newGroupId = 'grp_' + Date.now().toString(36) + '_' + Math.random().toString(36).substring(2, 6);
218
+ selected.forEach(o => {
219
+ this.objectManager.updateObject(o.id, { groupId: newGroupId }, false);
220
+ });
221
+ this.core.pushHistoryState('Nesneleri Grupla');
222
+ this.updateSelectionBox();
223
+ this.core.emit('selectionChanged', {
224
+ selectedIds: Array.from(this.selectedIds),
225
+ primaryObject: this.getPrimarySelected()
226
+ });
227
+ return newGroupId;
228
+ }
229
+
230
+ ungroupSelected() {
231
+ const selected = this.getSelectedObjects();
232
+ const groupIds = new Set(selected.map(o => o.groupId).filter(Boolean));
233
+ if (groupIds.size === 0) return false;
234
+
235
+ const allObjects = this.objectManager.getObjects();
236
+ allObjects.forEach(o => {
237
+ if (groupIds.has(o.groupId)) {
238
+ this.objectManager.updateObject(o.id, { groupId: null }, false);
239
+ }
240
+ });
241
+ this.core.pushHistoryState('Grubu Çöz');
242
+ this.updateSelectionBox();
243
+ this.core.emit('selectionChanged', {
244
+ selectedIds: Array.from(this.selectedIds),
245
+ primaryObject: this.getPrimarySelected()
246
+ });
247
+ return true;
248
+ }
249
+
250
+ deleteSelected() {
251
+ const ids = Array.from(this.selectedIds);
252
+ if (ids.length === 0) return;
253
+ this.clearSelection();
254
+ if (ids.length === 1) {
255
+ this.objectManager.deleteObject(ids[0], true);
256
+ } else {
257
+ ids.forEach(id => this.objectManager.deleteObject(id, false));
258
+ this.core.pushHistoryState('Seçili Nesneleri Sil');
259
+ }
260
+ }
261
+
262
+ duplicateSelected() {
263
+ const ids = Array.from(this.selectedIds);
264
+ if (ids.length === 0) return;
265
+ this.clearSelection();
266
+ const groupMap = new Map();
267
+ const newIds = [];
268
+ ids.forEach(id => {
269
+ const src = this.objectManager.getObject(id);
270
+ let targetGroupId = undefined;
271
+ if (src && src.groupId) {
272
+ if (!groupMap.has(src.groupId)) {
273
+ groupMap.set(src.groupId, 'grp_' + Date.now().toString(36) + '_' + Math.random().toString(36).substring(2, 6));
274
+ }
275
+ targetGroupId = groupMap.get(src.groupId);
276
+ }
277
+ const dup = this.objectManager.duplicateObject(id, false);
278
+ if (dup) {
279
+ if (targetGroupId !== undefined) {
280
+ this.objectManager.updateObject(dup.id, { groupId: targetGroupId }, false);
281
+ }
282
+ newIds.push(dup.id);
283
+ }
284
+ });
285
+ newIds.forEach(id => this.select(id, true));
286
+ this.core.pushHistoryState('Seçili Nesneleri Çoğalt');
287
+ }
288
+
289
+ copy() {
290
+ this.clipboard = this.getSelectedObjects().map(o => JSON.parse(JSON.stringify(o)));
291
+ }
292
+
293
+ paste() {
294
+ if (!this.clipboard || this.clipboard.length === 0) return;
295
+ this.clearSelection();
296
+ const groupMap = new Map();
297
+ const newIds = [];
298
+ this.clipboard.forEach(item => {
299
+ const copyProps = Object.assign({}, item);
300
+ delete copyProps.id;
301
+ copyProps.name = `${item.name} (Kopya)`;
302
+ copyProps.x = Math.min(this.core.data.stage.width - item.width, item.x + 25);
303
+ copyProps.y = Math.min(this.core.data.stage.height - item.height, item.y + 25);
304
+ if (item.groupId) {
305
+ if (!groupMap.has(item.groupId)) {
306
+ groupMap.set(item.groupId, 'grp_' + Date.now().toString(36) + '_' + Math.random().toString(36).substring(2, 6));
307
+ }
308
+ copyProps.groupId = groupMap.get(item.groupId);
309
+ }
310
+ const newObj = this.objectManager.createObject(item.type, item.role, copyProps, false);
311
+ if (newObj) newIds.push(newObj.id);
312
+ });
313
+ newIds.forEach(id => this.select(id, true));
314
+ this.core.pushHistoryState('Yapıştır');
315
+ }
316
+
317
+ nudgeSelected(dx, dy) {
318
+ const targets = this.getSelectedObjects().filter(obj => !obj.locked);
319
+ if (targets.length === 0) return;
320
+ targets.forEach(obj => {
321
+ this.objectManager.updateObject(obj.id, {
322
+ x: Math.max(0, Math.min(this.core.data.stage.width - obj.width, obj.x + dx)),
323
+ y: Math.max(0, Math.min(this.core.data.stage.height - obj.height, obj.y + dy))
324
+ }, false);
325
+ });
326
+ this.core.pushHistoryState('Nesneleri Kaydır');
327
+ this.updateSelectionBox();
328
+ }
329
+
330
+ // Seçim Kutusu DOM Güncellemesi
331
+ updateSelectionBox() {
332
+ if (!this.selectionOverlay) return;
333
+
334
+ // Önceki vurguları temizle
335
+ this.stage.querySelectorAll('.stage-object.is-selected').forEach(el => {
336
+ el.classList.remove('is-selected');
337
+ });
338
+
339
+ const selectedObjs = this.getSelectedObjects();
340
+
341
+ if (selectedObjs.length === 0 || this.core.getMode() !== 'design') {
342
+ this.selectionOverlay.style.display = 'none';
343
+ return;
344
+ }
345
+
346
+ selectedObjs.forEach(obj => {
347
+ const domEl = this.stage.querySelector(`[data-id="${obj.id}"]`);
348
+ if (domEl) domEl.classList.add('is-selected');
349
+ });
350
+
351
+ this.selectionOverlay.style.display = 'block';
352
+
353
+ if (selectedObjs.length === 1) {
354
+ // Tek nesne seçimi: Nesnenin kendi konumu ve dönüş açısı kullanılır
355
+ const obj = selectedObjs[0];
356
+ this.renderSingleSelectionBox(obj);
357
+ } else {
358
+ // Çoklu nesne seçimi: Tüm nesneleri kapsayan AABB kutusu
359
+ this.renderMultiSelectionBox(selectedObjs);
360
+ }
361
+ }
362
+
363
+ renderSingleSelectionBox(obj) {
364
+ this.selectionOverlay.style.width = `${obj.width}px`;
365
+ this.selectionOverlay.style.height = `${obj.height}px`;
366
+ this.selectionOverlay.style.transform = `translate(${obj.x}px, ${obj.y}px) rotate(${obj.rotation || 0}deg)`;
367
+ this.selectionOverlay.style.transformOrigin = 'center center';
368
+
369
+ if (obj.locked) {
370
+ this.selectionOverlay.className = 'selection-box single-select is-locked-selection';
371
+ const isNearTop = (obj.y || 0) < 45;
372
+ const pillPosStyle = isNearTop ? 'bottom: -38px; top: auto;' : 'top: -38px;';
373
+ this.selectionOverlay.innerHTML = `
374
+ <div class="selection-border locked-border"></div>
375
+ <div class="selection-lock-floating-pill" style="${pillPosStyle}">
376
+ <button type="button" class="lock-pill-unlock-btn" title="Kilidi Aç">🔓 Kilidi Aç</button>
377
+ </div>
378
+ `;
379
+ const unlockBtn = this.selectionOverlay.querySelector('.lock-pill-unlock-btn');
380
+ if (unlockBtn) {
381
+ unlockBtn.onclick = (e) => {
382
+ e.stopPropagation();
383
+ e.preventDefault();
384
+ this.objectManager.updateObject(obj.id, { locked: false });
385
+ this.updateSelectionBox();
386
+ this.core.emit('selectionChanged', {
387
+ selectedIds: Array.from(this.selectedIds),
388
+ primaryObject: this.objectManager.getObject(obj.id)
389
+ });
390
+ if (window.app && window.app.miniToolbar) {
391
+ window.app.miniToolbar.show();
392
+ }
393
+ };
394
+ }
395
+ return;
396
+ }
397
+
398
+ // Çizgi nesnesi için sadece iki uçta ölçeklendirme tutamaçları
399
+ if (obj.type === 'line') {
400
+ this.selectionOverlay.className = 'selection-box single-select is-line-selection';
401
+ this.selectionOverlay.innerHTML = `
402
+ <div class="resize-handle line-endpoint-handle handle-start" data-handle="w" title="Başlangıç ucunu uzat / kısalt"></div>
403
+ <div class="resize-handle line-endpoint-handle handle-end" data-handle="e" title="Bitiş ucunu uzat / kısalt"></div>
404
+ `;
405
+ return;
406
+ }
407
+
408
+ this.selectionOverlay.className = 'selection-box single-select';
409
+ // 8 tutamaç + döndürme tutamacı
410
+ this.selectionOverlay.innerHTML = `
411
+ <div class="selection-border"></div>
412
+ <div class="resize-handle handle-nw" data-handle="nw"></div>
413
+ <div class="resize-handle handle-n" data-handle="n"></div>
414
+ <div class="resize-handle handle-ne" data-handle="ne"></div>
415
+ <div class="resize-handle handle-e" data-handle="e"></div>
416
+ <div class="resize-handle handle-se" data-handle="se"></div>
417
+ <div class="resize-handle handle-s" data-handle="s"></div>
418
+ <div class="resize-handle handle-sw" data-handle="sw"></div>
419
+ <div class="resize-handle handle-w" data-handle="w"></div>
420
+ <div class="rotate-stem"></div>
421
+ <div class="rotate-handle" data-handle="rotate" title="Döndürmek için sürükleyin"></div>
422
+ `;
423
+ }
424
+
425
+ renderMultiSelectionBox(objects) {
426
+ let minX = Infinity;
427
+ let minY = Infinity;
428
+ let maxX = -Infinity;
429
+ let maxY = -Infinity;
430
+
431
+ objects.forEach(o => {
432
+ minX = Math.min(minX, o.x);
433
+ minY = Math.min(minY, o.y);
434
+ maxX = Math.max(maxX, o.x + o.width);
435
+ maxY = Math.max(maxY, o.y + o.height);
436
+ });
437
+
438
+ const w = maxX - minX;
439
+ const h = maxY - minY;
440
+
441
+ this.selectionOverlay.style.width = `${w}px`;
442
+ this.selectionOverlay.style.height = `${h}px`;
443
+ this.selectionOverlay.style.transform = `translate(${minX}px, ${minY}px) rotate(0deg)`;
444
+ this.selectionOverlay.style.transformOrigin = 'center center';
445
+ this.selectionOverlay.className = 'selection-box multi-select';
446
+
447
+ const isGroup = objects.length > 1 && objects.every(o => o.groupId && o.groupId === objects[0].groupId);
448
+
449
+ // Çoklu seçimde sadece taşıma ve sınır çerçevesi görünür
450
+ this.selectionOverlay.innerHTML = `
451
+ <div class="selection-border ${isGroup ? 'group-border' : ''}"></div>
452
+ <div class="multi-count-badge ${isGroup ? 'is-group-badge' : ''}">${isGroup ? `👥 Grup (${objects.length} nesne)` : `${objects.length} nesne seçildi`}</div>
453
+ `;
454
+ }
455
+ }
456
+
457
+ // Global namespace
458
+ window.DND = window.DND || {};
459
+ window.DND.SelectionManager = SelectionManager;