@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,468 @@
1
+ /**
2
+ * ObjectManager - Sahne nesnelerinin oluşturulması, güncellenmesi, çoğaltılması,
3
+ * silinmesi ve DOM üzerindeki temsillerini yönetir.
4
+ */
5
+ class ObjectManager {
6
+ constructor(core, stageEl) {
7
+ this.core = core;
8
+ this.stage = stageEl;
9
+ this.objectsContainer = stageEl.querySelector('#stage-objects');
10
+ this.idCounter = 1;
11
+
12
+ this.init();
13
+ }
14
+
15
+ init() {
16
+ this.core.on('projectLoaded', () => this.renderAll());
17
+ this.core.on('modeChanged', () => this.updateModeClass());
18
+ }
19
+
20
+ generateId(prefix = 'obj') {
21
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 6)}`;
22
+ }
23
+
24
+ // Yeni nesne oluşturma ve varsayılan özellikler
25
+ createObject(type, role = 'decorative', extra = {}, saveHistory = true) {
26
+ const id = this.generateId(role === 'draggable' ? 'drag' : (['target', 'category', 'sequence_slot'].includes(role) ? 'target' : 'obj'));
27
+ const maxZ = this.getMaxZIndex() + 1;
28
+
29
+ const baseObj = {
30
+ id,
31
+ name: extra.name || this.getDefaultName(type, role),
32
+ type, // 'text' | 'image' | 'rectangle' | 'circle' | 'zone'
33
+ role, // 'decorative' | 'draggable' | 'target' | 'category' | 'sequence_slot'
34
+ x: 100,
35
+ y: 100,
36
+ width: 160,
37
+ height: 60,
38
+ rotation: 0,
39
+ zIndex: maxZ,
40
+ locked: false,
41
+ visible: true,
42
+ opacity: 1,
43
+
44
+ // Stil özellikleri
45
+ backgroundColor: '#ffffff',
46
+ borderColor: '#94a3b8',
47
+ borderWidth: 2,
48
+ borderStyle: 'solid',
49
+ borderRadius: 8,
50
+ boxShadow: '0 2px 6px rgba(0,0,0,0.08)',
51
+
52
+ // Metin özellikleri
53
+ text: 'Metin',
54
+ fontFamily: 'Inter, system-ui, sans-serif',
55
+ fontSize: 18,
56
+ textColor: '#1e293b',
57
+ fontWeight: '600',
58
+ textAlign: 'center',
59
+ padding: 10,
60
+
61
+ // Görsel özellikleri
62
+ src: null,
63
+ alt: '',
64
+
65
+ // Hedef / Kategori / Sıralama özellikleri
66
+ targetType: 'single', // 'single' | 'category' | 'sequence'
67
+ acceptLimit: 0, // 0 = sınırsız
68
+ slotIndex: 0, // Sıralama için sıra no
69
+ visibleInGame: true // Oyun sırasında hedefin sınırları görünsün mü?
70
+ };
71
+
72
+ // Tür ve role göre özelleştirme
73
+ if (role === 'draggable') {
74
+ baseObj.backgroundColor = '#e0e7ff';
75
+ baseObj.borderColor = '#6366f1';
76
+ baseObj.borderWidth = 2;
77
+ baseObj.borderRadius = 10;
78
+ baseObj.text = 'Sürüklenebilir Öğe';
79
+ baseObj.boxShadow = '0 4px 10px rgba(99, 102, 241, 0.18)';
80
+ } else if (role === 'target') {
81
+ baseObj.backgroundColor = 'rgba(241, 245, 249, 0.7)';
82
+ baseObj.borderColor = '#3b82f6';
83
+ baseObj.borderStyle = 'dashed';
84
+ baseObj.borderWidth = 2.5;
85
+ baseObj.borderRadius = 12;
86
+ baseObj.text = 'Hedef Alanı';
87
+ baseObj.textColor = '#3b82f6';
88
+ } else if (role === 'category') {
89
+ baseObj.width = 280;
90
+ baseObj.height = 360;
91
+ baseObj.backgroundColor = '#f8fafc';
92
+ baseObj.borderColor = '#10b981';
93
+ baseObj.borderWidth = 3;
94
+ baseObj.borderStyle = 'dashed';
95
+ baseObj.borderRadius = 16;
96
+ baseObj.text = 'Kategori Başlığı';
97
+ baseObj.textColor = '#065f46';
98
+ baseObj.targetType = 'category';
99
+ baseObj.acceptLimit = 0; // Sınırsız
100
+ } else if (role === 'sequence_slot') {
101
+ baseObj.width = 140;
102
+ baseObj.height = 120;
103
+ baseObj.backgroundColor = '#f1f5f9';
104
+ baseObj.borderColor = '#8b5cf6';
105
+ baseObj.borderStyle = 'dashed';
106
+ baseObj.borderWidth = 2.5;
107
+ baseObj.borderRadius = 14;
108
+ baseObj.text = 'Adım 1';
109
+ baseObj.textColor = '#6d28d9';
110
+ baseObj.targetType = 'sequence';
111
+ baseObj.slotIndex = 0;
112
+ } else if (type === 'line') {
113
+ baseObj.width = 200;
114
+ baseObj.height = 24;
115
+ baseObj.backgroundColor = 'transparent';
116
+ baseObj.borderColor = '#1e293b';
117
+ baseObj.borderWidth = 3;
118
+ baseObj.borderStyle = 'solid';
119
+ baseObj.borderRadius = 0;
120
+ baseObj.boxShadow = 'none';
121
+ baseObj.text = '';
122
+ } else if (type === 'circle') {
123
+ baseObj.width = 100;
124
+ baseObj.height = 100;
125
+ baseObj.borderRadius = 9999;
126
+ baseObj.backgroundColor = '#fed7aa';
127
+ baseObj.borderColor = '#f97316';
128
+ baseObj.text = '';
129
+ } else if (type === 'rectangle') {
130
+ baseObj.width = 180;
131
+ baseObj.height = 120;
132
+ baseObj.backgroundColor = '#f1f5f9';
133
+ baseObj.borderColor = '#cbd5e1';
134
+ baseObj.text = '';
135
+ } else if (type === 'image') {
136
+ baseObj.width = 300;
137
+ baseObj.height = 200;
138
+ baseObj.backgroundColor = 'transparent';
139
+ baseObj.borderColor = 'transparent';
140
+ baseObj.borderWidth = 0;
141
+ baseObj.boxShadow = 'none';
142
+ baseObj.text = '';
143
+ } else if (type === 'text') {
144
+ baseObj.width = 220;
145
+ baseObj.height = 48;
146
+ baseObj.backgroundColor = 'transparent';
147
+ baseObj.borderColor = 'transparent';
148
+ baseObj.borderWidth = 0;
149
+ baseObj.boxShadow = 'none';
150
+ baseObj.borderRadius = 4;
151
+ baseObj.padding = 4;
152
+ baseObj.text = 'Metin yazın';
153
+ baseObj.fontSize = 18;
154
+ baseObj.fontWeight = '600';
155
+ baseObj.textColor = '#1e293b';
156
+ }
157
+
158
+ const finalObj = Object.assign({}, baseObj, extra);
159
+
160
+ this.core.data.objects.push(finalObj);
161
+ this.renderObjectDOM(finalObj);
162
+
163
+ this.core.emit('objectAdded', { object: finalObj, saveHistory });
164
+ this.core.emit('dataChanged', { source: 'createObject' });
165
+ this.core.autoSave();
166
+
167
+ return finalObj;
168
+ }
169
+
170
+ getDefaultName(type, role) {
171
+ if (role === 'draggable') return 'Sürüklenebilir ' + this.idCounter++;
172
+ if (role === 'target') return 'Hedef ' + this.idCounter++;
173
+ if (role === 'category') return 'Kategori ' + this.idCounter++;
174
+ if (role === 'sequence_slot') return 'Sıralama Adımı ' + this.idCounter++;
175
+ if (type === 'image') return 'Görsel ' + this.idCounter++;
176
+ if (type === 'circle') return 'Daire ' + this.idCounter++;
177
+ if (type === 'rectangle') return 'Kutu ' + this.idCounter++;
178
+ if (type === 'line') return 'Çizgi ' + this.idCounter++;
179
+ return 'Metin Kutusu ' + this.idCounter++;
180
+ }
181
+
182
+ getMaxZIndex() {
183
+ return this.core.data.objects.reduce((max, o) => Math.max(max, o.zIndex || 1), 1);
184
+ }
185
+
186
+ getObject(id) {
187
+ return this.core.data.objects.find(o => o.id === id);
188
+ }
189
+
190
+ getObjects() {
191
+ return this.core.data.objects;
192
+ }
193
+
194
+ updateObject(id, partialProps, saveHistory = true) {
195
+ const obj = this.getObject(id);
196
+ if (!obj) return null;
197
+
198
+ // Sahne dışına taşmayı engelle (nesne sahne içinde kalmalı)
199
+ if (this.core && this.core.data && this.core.data.stage) {
200
+ const stageW = this.core.data.stage.width;
201
+ const stageH = this.core.data.stage.height;
202
+ const w = partialProps.width !== undefined ? partialProps.width : (obj.width || 0);
203
+ const h = partialProps.height !== undefined ? partialProps.height : (obj.height || 0);
204
+
205
+ if (partialProps.x !== undefined) {
206
+ partialProps.x = Math.round(Math.max(0, Math.min(stageW - w, partialProps.x)));
207
+ }
208
+ if (partialProps.y !== undefined) {
209
+ partialProps.y = Math.round(Math.max(0, Math.min(stageH - h, partialProps.y)));
210
+ }
211
+ }
212
+
213
+ Object.assign(obj, partialProps);
214
+ this.updateObjectDOM(obj);
215
+
216
+ this.core.emit('objectUpdated', { object: obj, partialProps, saveHistory });
217
+ this.core.emit('dataChanged', { source: 'updateObject' });
218
+ this.core.autoSave();
219
+ return obj;
220
+ }
221
+
222
+ deleteObject(id, saveHistory = true) {
223
+ const idx = this.core.data.objects.findIndex(o => o.id === id);
224
+ if (idx === -1) return;
225
+
226
+ const removed = this.core.data.objects.splice(idx, 1)[0];
227
+
228
+ // DOM'dan kaldır
229
+ const el = this.objectsContainer.querySelector(`[data-id="${id}"]`);
230
+ if (el) el.remove();
231
+
232
+ // Cevap anahtarından temizle
233
+ if (this.core.data.answerKey) {
234
+ delete this.core.data.answerKey[id];
235
+ // Eğer silinen bir hedef id'si ise ona bağlı sürüklenebilirleri de temizle
236
+ for (const [k, v] of Object.entries(this.core.data.answerKey)) {
237
+ if (v === id) delete this.core.data.answerKey[k];
238
+ }
239
+ }
240
+
241
+ this.core.emit('objectDeleted', { object: removed, saveHistory });
242
+ this.core.emit('dataChanged', { source: 'deleteObject' });
243
+ this.core.autoSave();
244
+ }
245
+
246
+ duplicateObject(id, saveHistory = true) {
247
+ const src = this.getObject(id);
248
+ if (!src) return null;
249
+
250
+ const copyProps = JSON.parse(JSON.stringify(src));
251
+ delete copyProps.id;
252
+ copyProps.name = `${src.name} (Kopya)`;
253
+ copyProps.x = Math.min(this.core.data.stage.width - (src.width || 100), src.x + 25);
254
+ copyProps.y = Math.min(this.core.data.stage.height - (src.height || 60), src.y + 25);
255
+ copyProps.zIndex = this.getMaxZIndex() + 1;
256
+
257
+ const newObj = this.createObject(src.type, src.role, copyProps, saveHistory);
258
+ return newObj;
259
+ }
260
+
261
+ // DOM Çizim Metotları
262
+ renderAll() {
263
+ this.objectsContainer.innerHTML = '';
264
+ const sorted = [...this.core.data.objects].sort((a, b) => (a.zIndex || 1) - (b.zIndex || 1));
265
+ sorted.forEach(obj => this.renderObjectDOM(obj));
266
+ }
267
+
268
+ renderObjectDOM(obj) {
269
+ let el = this.objectsContainer.querySelector(`[data-id="${obj.id}"]`);
270
+ if (!el) {
271
+ el = document.createElement('div');
272
+ el.className = 'stage-object';
273
+ el.setAttribute('data-id', obj.id);
274
+ this.objectsContainer.appendChild(el);
275
+ }
276
+
277
+ this.updateObjectDOM(obj, el);
278
+ }
279
+
280
+ updateObjectDOM(obj, el = null) {
281
+ if (!el) {
282
+ el = this.objectsContainer.querySelector(`[data-id="${obj.id}"]`);
283
+ }
284
+ if (!el) return;
285
+
286
+ el.setAttribute('data-role', obj.role || 'decorative');
287
+ el.setAttribute('data-type', obj.type || 'text');
288
+ el.classList.toggle('is-locked', !!obj.locked);
289
+ el.classList.toggle('is-hidden', obj.visible === false);
290
+
291
+ // Konum, boyut ve dönüşüm (CSS transform)
292
+ el.style.width = `${obj.width}px`;
293
+ el.style.height = `${obj.height}px`;
294
+ el.style.transform = `translate(${obj.x}px, ${obj.y}px) rotate(${obj.rotation || 0}deg)`;
295
+ el.style.transformOrigin = 'center center';
296
+ el.style.zIndex = obj.zIndex || 1;
297
+ el.style.opacity = obj.opacity !== undefined ? obj.opacity : 1;
298
+
299
+ // Görsel stiller
300
+ el.style.backgroundColor = obj.backgroundColor || 'transparent';
301
+ el.style.borderColor = obj.borderColor || 'transparent';
302
+ el.style.borderWidth = `${obj.borderWidth !== undefined ? obj.borderWidth : 0}px`;
303
+ el.style.borderStyle = obj.borderStyle || 'solid';
304
+ el.style.borderRadius = `${obj.borderRadius !== undefined ? obj.borderRadius : 0}px`;
305
+ el.style.boxShadow = obj.boxShadow || 'none';
306
+
307
+ // İçerik Oluşturma
308
+ el.innerHTML = '';
309
+
310
+ // Kilit İkonu (Tıklanabilir buton)
311
+ if (obj.locked) {
312
+ const lockIcon = document.createElement('button');
313
+ lockIcon.type = 'button';
314
+ lockIcon.className = 'lock-indicator lock-badge-btn';
315
+ lockIcon.innerHTML = '🔒';
316
+ lockIcon.title = 'Nesne kilitli. Kilidi açmak için tıklayın.';
317
+ lockIcon.onclick = (e) => {
318
+ e.stopPropagation();
319
+ e.preventDefault();
320
+ this.updateObject(obj.id, { locked: false });
321
+ if (window.app && window.app.selectionManager) {
322
+ window.app.selectionManager.select(obj.id);
323
+ }
324
+ };
325
+ el.appendChild(lockIcon);
326
+ }
327
+
328
+ // Gövde İçeriği
329
+ const content = document.createElement('div');
330
+ content.className = 'object-inner-content';
331
+ content.style.fontFamily = obj.fontFamily || 'sans-serif';
332
+ content.style.fontSize = `${obj.fontSize || 16}px`;
333
+ content.style.fontWeight = obj.fontWeight || 'normal';
334
+ content.style.fontStyle = obj.fontStyle || 'normal';
335
+ content.style.textDecoration = obj.textDecoration || 'none';
336
+ content.style.color = obj.textColor || '#1e293b';
337
+ content.style.padding = `${obj.padding !== undefined ? obj.padding : 8}px`;
338
+
339
+ // Dikey hizalama (Top / Middle / Bottom)
340
+ if (obj.verticalAlign === 'top') {
341
+ content.style.justifyContent = 'flex-start';
342
+ } else if (obj.verticalAlign === 'bottom') {
343
+ content.style.justifyContent = 'flex-end';
344
+ } else {
345
+ content.style.justifyContent = 'center';
346
+ }
347
+
348
+ // Yatay hizalama (Left / Center / Right)
349
+ const textAlign = obj.textAlign || 'center';
350
+ content.style.textAlign = textAlign;
351
+ if (textAlign === 'left') {
352
+ content.style.alignItems = 'flex-start';
353
+ } else if (textAlign === 'right') {
354
+ content.style.alignItems = 'flex-end';
355
+ } else {
356
+ content.style.alignItems = 'center';
357
+ }
358
+
359
+ if (obj.type === 'line') {
360
+ el.style.backgroundColor = 'transparent';
361
+ el.style.borderColor = 'transparent';
362
+ el.style.borderWidth = '0px';
363
+ el.style.boxShadow = 'none';
364
+ content.style.padding = '0px';
365
+ content.innerHTML = this.renderLineSVG(obj);
366
+ } else if (obj.type === 'image' && obj.src) {
367
+ const img = document.createElement('img');
368
+ img.src = obj.src;
369
+ img.alt = obj.alt || obj.name || 'Görsel';
370
+ img.draggable = false;
371
+ content.appendChild(img);
372
+ if (obj.text) {
373
+ const caption = document.createElement('div');
374
+ caption.className = 'image-caption';
375
+ caption.style.width = '100%';
376
+ caption.style.textAlign = textAlign;
377
+ caption.textContent = obj.text;
378
+ content.appendChild(caption);
379
+ }
380
+ } else if (obj.role === 'category') {
381
+ const catHeader = document.createElement('div');
382
+ catHeader.className = 'category-header';
383
+ catHeader.style.width = '100%';
384
+ catHeader.style.textAlign = textAlign;
385
+ catHeader.textContent = obj.text || obj.name;
386
+ content.appendChild(catHeader);
387
+
388
+ const catBody = document.createElement('div');
389
+ catBody.className = 'category-drop-hint';
390
+ catBody.textContent = 'Öğeleri buraya bırakın';
391
+ content.appendChild(catBody);
392
+ } else if (obj.role === 'sequence_slot') {
393
+ const slotNum = document.createElement('div');
394
+ slotNum.className = 'sequence-slot-number';
395
+ slotNum.textContent = (obj.slotIndex !== undefined ? obj.slotIndex + 1 : 1);
396
+ content.appendChild(slotNum);
397
+
398
+ const slotLbl = document.createElement('div');
399
+ slotLbl.className = 'sequence-slot-label';
400
+ slotLbl.style.width = '100%';
401
+ slotLbl.style.textAlign = textAlign;
402
+ slotLbl.textContent = obj.text || '';
403
+ content.appendChild(slotLbl);
404
+ } else {
405
+ // Standart metin veya şekil içi metin
406
+ const shouldRenderText = obj.type === 'text' || (obj.text && obj.text.trim() !== '');
407
+ if (shouldRenderText) {
408
+ const textNode = document.createElement('span');
409
+ textNode.className = 'object-text-span';
410
+ textNode.style.width = '100%';
411
+ textNode.style.display = 'block';
412
+ textNode.style.textAlign = textAlign;
413
+ textNode.textContent = obj.text || '';
414
+ content.appendChild(textNode);
415
+ }
416
+ }
417
+
418
+ el.appendChild(content);
419
+ }
420
+
421
+ renderLineSVG(obj) {
422
+ const w = Math.max(10, obj.width || 200);
423
+ const h = Math.max(10, obj.height || 24);
424
+ const cy = h / 2;
425
+ const strokeColor = (obj.borderColor && obj.borderColor !== 'transparent') ? obj.borderColor : '#1e293b';
426
+ const strokeWidth = (obj.borderWidth !== undefined && obj.borderWidth > 0) ? obj.borderWidth : 3;
427
+ const strokeStyle = obj.borderStyle || 'solid';
428
+
429
+ let dashArray = 'none';
430
+ if (strokeStyle === 'dashed') dashArray = '8 6';
431
+ else if (strokeStyle === 'dotted') dashArray = '2 6';
432
+
433
+ const hasStartArrow = !!obj.arrowStart;
434
+ const hasEndArrow = !!obj.arrowEnd;
435
+ const arrowSize = Math.min(Math.max(8, strokeWidth * 3), w / 2.5);
436
+
437
+ const x1 = hasStartArrow ? arrowSize - 1 : 0;
438
+ const x2 = hasEndArrow ? w - (arrowSize - 1) : w;
439
+
440
+ let svgHtml = `<svg class="object-line-bar" width="100%" height="100%" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none" style="overflow: visible; display: block; pointer-events: none;">`;
441
+
442
+ // Ana Çizgi
443
+ svgHtml += `<line x1="${x1}" y1="${cy}" x2="${x2}" y2="${cy}" stroke="${strokeColor}" stroke-width="${strokeWidth}" stroke-dasharray="${dashArray}" stroke-linecap="round" />`;
444
+
445
+ // Başlangıç Oku
446
+ if (hasStartArrow) {
447
+ svgHtml += `<polygon points="${arrowSize},${cy - arrowSize * 0.6} 0,${cy} ${arrowSize},${cy + arrowSize * 0.6}" fill="${strokeColor}" />`;
448
+ }
449
+
450
+ // Bitiş Oku
451
+ if (hasEndArrow) {
452
+ svgHtml += `<polygon points="${w - arrowSize},${cy - arrowSize * 0.6} ${w},${cy} ${w - arrowSize},${cy + arrowSize * 0.6}" fill="${strokeColor}" />`;
453
+ }
454
+
455
+ svgHtml += `</svg>`;
456
+ return svgHtml;
457
+ }
458
+
459
+ updateModeClass() {
460
+ const mode = this.core.getMode();
461
+ this.stage.classList.remove('mode-design', 'mode-answer-key', 'mode-preview');
462
+ this.stage.classList.add(`mode-${mode}`);
463
+ }
464
+ }
465
+
466
+ // Global namespace
467
+ window.DND = window.DND || {};
468
+ window.DND.ObjectManager = ObjectManager;