@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.
- package/LICENSE +21 -0
- package/README.md +327 -2
- package/css/answer-key.css +200 -0
- package/css/editor.css +1797 -0
- package/css/player.css +782 -0
- package/css/stage.css +490 -0
- package/dist/dnd-player-element.js +1758 -0
- package/dist/dnd.js +2041 -0
- package/dist/editor.css +3278 -0
- package/dist/editor.d.ts +23 -0
- package/dist/editor.js +8506 -0
- package/dist/index.d.ts +102 -0
- package/dist/player.css +782 -0
- package/dist/player.d.ts +1 -0
- package/dist/player.js +2041 -0
- package/dist/player.umd.cjs +1758 -0
- package/index.html +203 -0
- package/js/app.js +188 -0
- package/js/core/AnswerKeyManager.js +494 -0
- package/js/core/EditorCore.js +220 -0
- package/js/core/HistoryManager.js +194 -0
- package/js/core/LayerManager.js +86 -0
- package/js/core/ObjectManager.js +468 -0
- package/js/core/SelectionManager.js +459 -0
- package/js/core/StageManager.js +295 -0
- package/js/data/SampleProjects.js +883 -0
- package/js/engine/TransformEngine.js +632 -0
- package/js/engine/ValidationEngine.js +147 -0
- package/js/player/ActivityEngine.js +1272 -0
- package/js/player/ExportManager.js +820 -0
- package/js/ui/MiniToolbar.js +1546 -0
- package/js/ui/PropertyPanel.js +776 -0
- package/js/ui/UIManager.js +596 -0
- package/js/utils/AudioSynthesizer.js +111 -0
- package/package.json +69 -4
- package/src/ActivityPlayer.js +309 -0
- package/src/DNDEditor.js +217 -0
- package/src/components/DNDPlayerElement.js +191 -0
- package/src/index.js +12 -0
- package/src/player.js +13 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UIManager - DND arayüzü, üst başlık, ikincil araç çubuğu,
|
|
3
|
+
* şablonlar menüsü, mod geçişleri ve bildirimleri yönetir.
|
|
4
|
+
* Yan paneller kaldırılmış, tam ekran tuval düzeni uygulanmıştır.
|
|
5
|
+
*/
|
|
6
|
+
class UIManager {
|
|
7
|
+
constructor(core, stageManager, objectManager, selectionManager, historyManager, exportManager, options = {}) {
|
|
8
|
+
this.core = core;
|
|
9
|
+
this.stageManager = stageManager;
|
|
10
|
+
this.objectManager = objectManager;
|
|
11
|
+
this.selectionManager = selectionManager;
|
|
12
|
+
this.historyManager = historyManager;
|
|
13
|
+
this.exportManager = exportManager;
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.root = options.root || document;
|
|
16
|
+
|
|
17
|
+
this.previewContainer = this.root.getElementById('preview-container');
|
|
18
|
+
this.editorContainer = this.root.getElementById('editor-layout');
|
|
19
|
+
this.activePlayerInstance = null;
|
|
20
|
+
|
|
21
|
+
this.init();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
init() {
|
|
25
|
+
this.bindHeaderActions();
|
|
26
|
+
this.bindSubToolbar();
|
|
27
|
+
this.bindModeSwitcher();
|
|
28
|
+
this.bindProjectTitle();
|
|
29
|
+
this.bindInlineTextEditing();
|
|
30
|
+
|
|
31
|
+
this.core.on('historyChanged', ({ canUndo, canRedo }) => {
|
|
32
|
+
const undoBtn = this.root.getElementById('btn-undo');
|
|
33
|
+
const redoBtn = this.root.getElementById('btn-redo');
|
|
34
|
+
if (undoBtn) undoBtn.disabled = !canUndo;
|
|
35
|
+
if (redoBtn) redoBtn.disabled = !canRedo;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
this.core.on('projectNameChanged', ({ name }) => {
|
|
39
|
+
const titleInput = this.root.getElementById('project-title-input');
|
|
40
|
+
if (titleInput && titleInput.value !== name) {
|
|
41
|
+
titleInput.value = name;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
this.core.on('dataChanged', () => {
|
|
46
|
+
this.updateSaveStatus(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this.core.on('modeChanged', ({ newMode }) => {
|
|
50
|
+
this.handleModeChanged(newMode);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
updateSaveStatus(isSaved) {
|
|
55
|
+
const statusBox = this.root.querySelector('.project-save-status');
|
|
56
|
+
const statusText = this.root.getElementById('status-text');
|
|
57
|
+
if (statusBox && statusText) {
|
|
58
|
+
if (isSaved) {
|
|
59
|
+
statusBox.classList.add('is-saved');
|
|
60
|
+
statusText.textContent = 'Kaydedildi';
|
|
61
|
+
} else {
|
|
62
|
+
statusBox.classList.remove('is-saved');
|
|
63
|
+
statusText.textContent = 'Kaydedilmedi';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
bindHeaderActions() {
|
|
69
|
+
// 1. Proje Aç (JSON)
|
|
70
|
+
const btnOpen = this.root.getElementById('btn-open-project');
|
|
71
|
+
const fileInput = this.root.getElementById('file-input-project');
|
|
72
|
+
if (btnOpen && fileInput) {
|
|
73
|
+
btnOpen.onclick = () => fileInput.click();
|
|
74
|
+
fileInput.onchange = (e) => {
|
|
75
|
+
const file = e.target.files[0];
|
|
76
|
+
if (file) {
|
|
77
|
+
const reader = new FileReader();
|
|
78
|
+
reader.onload = (ev) => {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(ev.target.result);
|
|
81
|
+
this.core.loadProject(parsed);
|
|
82
|
+
this.updateSaveStatus(true);
|
|
83
|
+
this.showToast('Proje başarıyla yüklendi.', 'success');
|
|
84
|
+
} catch (err) {
|
|
85
|
+
alert('Proje dosyası geçersiz: ' + err.message);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
reader.readAsText(file);
|
|
89
|
+
}
|
|
90
|
+
fileInput.value = '';
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2. Projeyi Kaydet (JSON İndir)
|
|
95
|
+
const btnSave = this.root.getElementById('btn-save-project');
|
|
96
|
+
if (btnSave) {
|
|
97
|
+
btnSave.onclick = () => {
|
|
98
|
+
const projectData = this.core.getProjectData();
|
|
99
|
+
const jsonStr = JSON.stringify(projectData, null, 2);
|
|
100
|
+
const blob = new Blob([jsonStr], { type: 'application/json;charset=utf-8' });
|
|
101
|
+
const url = URL.createObjectURL(blob);
|
|
102
|
+
|
|
103
|
+
const safeTitle = ((projectData.project && projectData.project.name) || 'etkinlik')
|
|
104
|
+
.toLowerCase()
|
|
105
|
+
.replace(/[^a-z0-9ğüşıöç]+/g, '_');
|
|
106
|
+
|
|
107
|
+
const a = document.createElement('a');
|
|
108
|
+
a.href = url;
|
|
109
|
+
a.download = `${safeTitle}.json`;
|
|
110
|
+
document.body.appendChild(a);
|
|
111
|
+
a.click();
|
|
112
|
+
document.body.removeChild(a);
|
|
113
|
+
URL.revokeObjectURL(url);
|
|
114
|
+
|
|
115
|
+
this.updateSaveStatus(true);
|
|
116
|
+
this.showToast('Proje JSON olarak kaydedildi.', 'success');
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 3. HTML İndir
|
|
121
|
+
const btnExport = this.root.getElementById('btn-export-html');
|
|
122
|
+
if (btnExport) {
|
|
123
|
+
btnExport.onclick = () => {
|
|
124
|
+
const ok = this.exportManager.exportStandaloneHTML();
|
|
125
|
+
if (ok) {
|
|
126
|
+
this.showToast('Bağımsız HTML etkinliği indirildi!', 'success');
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
bindSubToolbar() {
|
|
133
|
+
// Şablonlar Menüsü Aç / Kapa
|
|
134
|
+
const btnTemplates = this.root.getElementById('btn-toggle-templates');
|
|
135
|
+
const menuTemplates = this.root.getElementById('templates-menu');
|
|
136
|
+
const btnMore = this.root.getElementById('btn-more-menu');
|
|
137
|
+
const menuMore = this.root.getElementById('more-dropdown-menu');
|
|
138
|
+
|
|
139
|
+
if (btnTemplates && menuTemplates) {
|
|
140
|
+
btnTemplates.onclick = (e) => {
|
|
141
|
+
e.stopPropagation();
|
|
142
|
+
if (menuMore) menuMore.style.display = 'none';
|
|
143
|
+
const isHidden = menuTemplates.style.display === 'none';
|
|
144
|
+
menuTemplates.style.display = isHidden ? 'flex' : 'none';
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
window.addEventListener('click', () => {
|
|
148
|
+
if (menuTemplates) menuTemplates.style.display = 'none';
|
|
149
|
+
}, this.options.signal ? { signal: this.options.signal } : undefined);
|
|
150
|
+
|
|
151
|
+
menuTemplates.querySelectorAll('[data-sample]').forEach(item => {
|
|
152
|
+
item.onclick = (e) => {
|
|
153
|
+
e.stopPropagation();
|
|
154
|
+
const sampleKey = item.getAttribute('data-sample');
|
|
155
|
+
const sample = window.DND.SAMPLE_PROJECTS[sampleKey];
|
|
156
|
+
if (sample) {
|
|
157
|
+
this.core.loadProject(sample);
|
|
158
|
+
this.updateSaveStatus(true);
|
|
159
|
+
this.showToast(`"${sample.project.name}" şablonu yüklendi.`, 'success');
|
|
160
|
+
}
|
|
161
|
+
menuTemplates.style.display = 'none';
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Daha Fazla Seçenek Menüsü Aç / Kapa (Proje Aç, Çevrimdışı HTML İndir, Kaydet)
|
|
167
|
+
if (btnMore && menuMore) {
|
|
168
|
+
btnMore.onclick = (e) => {
|
|
169
|
+
e.stopPropagation();
|
|
170
|
+
if (menuTemplates) menuTemplates.style.display = 'none';
|
|
171
|
+
const isHidden = menuMore.style.display === 'none';
|
|
172
|
+
menuMore.style.display = isHidden ? 'flex' : 'none';
|
|
173
|
+
btnMore.setAttribute('aria-expanded', String(isHidden));
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
window.addEventListener('click', () => {
|
|
177
|
+
if (menuMore) {
|
|
178
|
+
menuMore.style.display = 'none';
|
|
179
|
+
btnMore.setAttribute('aria-expanded', 'false');
|
|
180
|
+
}
|
|
181
|
+
}, this.options.signal ? { signal: this.options.signal } : undefined);
|
|
182
|
+
|
|
183
|
+
menuMore.querySelectorAll('.more-menu-item').forEach(item => {
|
|
184
|
+
item.addEventListener('click', () => {
|
|
185
|
+
menuMore.style.display = 'none';
|
|
186
|
+
btnMore.setAttribute('aria-expanded', 'false');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Nesne Ekleme: Metin Kutusu
|
|
192
|
+
const btnAddText = this.root.getElementById('btn-add-text');
|
|
193
|
+
if (btnAddText) {
|
|
194
|
+
btnAddText.onclick = () => {
|
|
195
|
+
const obj = this.objectManager.createObject('text', 'decorative', {
|
|
196
|
+
text: 'Metin yazın',
|
|
197
|
+
x: 450,
|
|
198
|
+
y: 280,
|
|
199
|
+
width: 220,
|
|
200
|
+
height: 48,
|
|
201
|
+
fontSize: 18,
|
|
202
|
+
fontWeight: '600',
|
|
203
|
+
textColor: '#1e293b',
|
|
204
|
+
backgroundColor: 'transparent',
|
|
205
|
+
borderColor: 'transparent',
|
|
206
|
+
borderWidth: 0,
|
|
207
|
+
borderRadius: 4,
|
|
208
|
+
boxShadow: 'none',
|
|
209
|
+
padding: 4
|
|
210
|
+
});
|
|
211
|
+
this.selectionManager.select(obj.id);
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Nesne Ekleme: Dikdörtgen / Kart
|
|
216
|
+
const btnAddRect = this.root.getElementById('btn-add-rect');
|
|
217
|
+
if (btnAddRect) {
|
|
218
|
+
btnAddRect.onclick = () => {
|
|
219
|
+
const obj = this.objectManager.createObject('rectangle', 'decorative', {
|
|
220
|
+
text: '',
|
|
221
|
+
x: 420,
|
|
222
|
+
y: 240,
|
|
223
|
+
width: 240,
|
|
224
|
+
height: 60,
|
|
225
|
+
fontSize: 15,
|
|
226
|
+
fontWeight: '600',
|
|
227
|
+
backgroundColor: '#ffffff',
|
|
228
|
+
borderColor: '#f97316',
|
|
229
|
+
borderWidth: 2,
|
|
230
|
+
borderRadius: 12
|
|
231
|
+
});
|
|
232
|
+
this.selectionManager.select(obj.id);
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Nesne Ekleme: Daire
|
|
237
|
+
const btnAddCircle = this.root.getElementById('btn-add-circle');
|
|
238
|
+
if (btnAddCircle) {
|
|
239
|
+
btnAddCircle.onclick = () => {
|
|
240
|
+
const obj = this.objectManager.createObject('circle', 'decorative', {
|
|
241
|
+
text: '',
|
|
242
|
+
x: 480,
|
|
243
|
+
y: 240,
|
|
244
|
+
width: 100,
|
|
245
|
+
height: 100,
|
|
246
|
+
borderRadius: 9999,
|
|
247
|
+
backgroundColor: '#eff6ff',
|
|
248
|
+
borderColor: '#2563eb',
|
|
249
|
+
borderWidth: 2
|
|
250
|
+
});
|
|
251
|
+
this.selectionManager.select(obj.id);
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Nesne Ekleme: Görsel Yükle
|
|
256
|
+
const btnAddImage = this.root.getElementById('btn-add-image');
|
|
257
|
+
const imageInput = this.root.getElementById('file-input-image');
|
|
258
|
+
if (btnAddImage && imageInput) {
|
|
259
|
+
btnAddImage.onclick = () => imageInput.click();
|
|
260
|
+
imageInput.onchange = (e) => {
|
|
261
|
+
const file = e.target.files[0];
|
|
262
|
+
if (file) {
|
|
263
|
+
if (!file.type.startsWith('image/')) {
|
|
264
|
+
alert('Lütfen geçerli bir görsel dosyası seçin.');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const task = (async () => {
|
|
268
|
+
const uploaded = typeof this.options.uploadImage === 'function'
|
|
269
|
+
? await this.options.uploadImage(file, { signal: this.options.signal })
|
|
270
|
+
: null;
|
|
271
|
+
const dataUrl = uploaded?.url || await new Promise((resolve, reject) => {
|
|
272
|
+
const reader = new FileReader();
|
|
273
|
+
reader.onload = (ev) => resolve(ev.target.result);
|
|
274
|
+
reader.onerror = () => reject(reader.error || new Error('Görsel okunamadı.'));
|
|
275
|
+
reader.readAsDataURL(file);
|
|
276
|
+
});
|
|
277
|
+
const img = new Image();
|
|
278
|
+
await new Promise((resolve, reject) => {
|
|
279
|
+
img.onload = resolve;
|
|
280
|
+
img.onerror = () => reject(new Error('Görsel yüklenemedi.'));
|
|
281
|
+
img.src = dataUrl;
|
|
282
|
+
});
|
|
283
|
+
let w = img.width, h = img.height;
|
|
284
|
+
const maxDim = 320;
|
|
285
|
+
if (w > maxDim || h > maxDim) {
|
|
286
|
+
if (w > h) { h = Math.round((h * maxDim) / w); w = maxDim; }
|
|
287
|
+
else { w = Math.round((w * maxDim) / h); h = maxDim; }
|
|
288
|
+
}
|
|
289
|
+
const obj = this.objectManager.createObject('image', 'decorative', {
|
|
290
|
+
name: file.name.split('.')[0] || 'Görsel',
|
|
291
|
+
src: dataUrl,
|
|
292
|
+
x: 400,
|
|
293
|
+
y: 200,
|
|
294
|
+
width: w,
|
|
295
|
+
height: h,
|
|
296
|
+
text: '',
|
|
297
|
+
backgroundColor: 'transparent',
|
|
298
|
+
borderColor: 'transparent',
|
|
299
|
+
borderWidth: 0
|
|
300
|
+
});
|
|
301
|
+
this.selectionManager.select(obj.id);
|
|
302
|
+
})().catch((error) => {
|
|
303
|
+
this.showToast(error.message || 'Görsel yüklenemedi.', 'error');
|
|
304
|
+
throw error;
|
|
305
|
+
});
|
|
306
|
+
if (typeof this.options.trackTask === 'function') this.options.trackTask(task);
|
|
307
|
+
}
|
|
308
|
+
imageInput.value = '';
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Çizgi Ekleme (btn-add-target)
|
|
313
|
+
const btnAddTarget = this.root.getElementById('btn-add-target');
|
|
314
|
+
if (btnAddTarget) {
|
|
315
|
+
btnAddTarget.onclick = () => {
|
|
316
|
+
const obj = this.objectManager.createObject('line', 'decorative', {
|
|
317
|
+
x: 400,
|
|
318
|
+
y: 250,
|
|
319
|
+
width: 200,
|
|
320
|
+
height: 24,
|
|
321
|
+
borderColor: '#1e293b',
|
|
322
|
+
borderWidth: 3,
|
|
323
|
+
borderStyle: 'solid'
|
|
324
|
+
});
|
|
325
|
+
this.selectionManager.select(obj.id);
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Geri Al / İleri Al
|
|
330
|
+
const btnUndo = this.root.getElementById('btn-undo');
|
|
331
|
+
if (btnUndo) btnUndo.onclick = () => this.historyManager.undo();
|
|
332
|
+
|
|
333
|
+
const btnRedo = this.root.getElementById('btn-redo');
|
|
334
|
+
if (btnRedo) btnRedo.onclick = () => this.historyManager.redo();
|
|
335
|
+
|
|
336
|
+
// Yakınlaştır / Uzaklaştır / Sıfırla
|
|
337
|
+
const btnZoomIn = this.root.getElementById('btn-zoom-in');
|
|
338
|
+
if (btnZoomIn) btnZoomIn.onclick = () => this.stageManager.zoomIn();
|
|
339
|
+
|
|
340
|
+
const btnZoomOut = this.root.getElementById('btn-zoom-out');
|
|
341
|
+
if (btnZoomOut) btnZoomOut.onclick = () => this.stageManager.zoomOut();
|
|
342
|
+
|
|
343
|
+
const btnZoomReset = this.root.getElementById('btn-zoom-reset');
|
|
344
|
+
if (btnZoomReset) btnZoomReset.onclick = () => this.stageManager.fitToScreen();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
bindModeSwitcher() {
|
|
348
|
+
const modeBtns = this.root.querySelectorAll('.mode-toggle-btn');
|
|
349
|
+
modeBtns.forEach(btn => {
|
|
350
|
+
btn.onclick = () => {
|
|
351
|
+
const mode = btn.getAttribute('data-mode');
|
|
352
|
+
this.core.setMode(mode);
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
handleModeChanged(newMode) {
|
|
358
|
+
this.root.querySelectorAll('.mode-toggle-btn').forEach(btn => {
|
|
359
|
+
btn.classList.toggle('active', btn.getAttribute('data-mode') === newMode);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const subToolbar = this.root.querySelector('.app-sub-toolbar');
|
|
363
|
+
if (subToolbar) {
|
|
364
|
+
subToolbar.classList.toggle('mode-answer-key', newMode === 'answer-key');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const clusterAdd = this.root.querySelector('.cluster-add-objects') || this.root.getElementById('cluster-add-objects');
|
|
368
|
+
const clusterHistoryZoom = this.root.querySelector('.cluster-history-zoom') || this.root.getElementById('cluster-history-zoom');
|
|
369
|
+
const templateDropdown = this.root.querySelector('.template-dropdown-wrapper');
|
|
370
|
+
if (clusterAdd) clusterAdd.style.display = newMode === 'answer-key' ? 'none' : 'flex';
|
|
371
|
+
if (clusterHistoryZoom) clusterHistoryZoom.style.display = newMode === 'answer-key' ? 'none' : 'flex';
|
|
372
|
+
if (templateDropdown) templateDropdown.style.display = newMode === 'answer-key' ? 'none' : '';
|
|
373
|
+
const moreMenuDivider = this.root.getElementById('more-menu-divider');
|
|
374
|
+
if (moreMenuDivider) moreMenuDivider.style.display = newMode === 'answer-key' ? 'none' : 'block';
|
|
375
|
+
|
|
376
|
+
if (newMode === 'preview') {
|
|
377
|
+
this.editorContainer.style.display = 'none';
|
|
378
|
+
this.previewContainer.style.display = 'block';
|
|
379
|
+
|
|
380
|
+
if (this.activePlayerInstance) {
|
|
381
|
+
this.activePlayerInstance.destroy();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
this.activePlayerInstance = new window.DND.ActivityEngine(
|
|
385
|
+
this.previewContainer,
|
|
386
|
+
this.core.getProjectData(),
|
|
387
|
+
{
|
|
388
|
+
isEmbedded: true,
|
|
389
|
+
onExit: () => {
|
|
390
|
+
this.core.setMode('design');
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
);
|
|
394
|
+
} else {
|
|
395
|
+
if (this.activePlayerInstance) {
|
|
396
|
+
this.activePlayerInstance.destroy();
|
|
397
|
+
this.activePlayerInstance = null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
this.previewContainer.style.display = 'none';
|
|
401
|
+
this.editorContainer.style.display = 'flex';
|
|
402
|
+
this.stageManager.updateScale();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
bindProjectTitle() {
|
|
407
|
+
const titleInput = this.root.getElementById('project-title-input');
|
|
408
|
+
if (titleInput) {
|
|
409
|
+
titleInput.oninput = (e) => {
|
|
410
|
+
this.core.setProjectName(e.target.value);
|
|
411
|
+
this.updateSaveStatus(false);
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Çift Tıklayarak veya Enter ile Yerinde Metin Düzenleme (Canva / Figma benzeri)
|
|
417
|
+
bindInlineTextEditing() {
|
|
418
|
+
this.isEditingText = false;
|
|
419
|
+
this.activeEditingObjId = null;
|
|
420
|
+
|
|
421
|
+
const stage = this.root.getElementById('stage');
|
|
422
|
+
if (!stage) return;
|
|
423
|
+
|
|
424
|
+
// 1. Çift Tıklama Olayı (Doğrudan nesneye veya seçim çerçevesine çift tıklandığında)
|
|
425
|
+
stage.addEventListener('dblclick', (e) => {
|
|
426
|
+
if (this.core.getMode() !== 'design') return;
|
|
427
|
+
|
|
428
|
+
let objId = null;
|
|
429
|
+
const objEl = e.target.closest('.stage-object');
|
|
430
|
+
if (objEl) {
|
|
431
|
+
objId = objEl.getAttribute('data-id');
|
|
432
|
+
} else if (e.target.closest('.selection-box, .selection-border')) {
|
|
433
|
+
const primary = this.selectionManager.getPrimarySelected();
|
|
434
|
+
if (primary) objId = primary.id;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!objId) return;
|
|
438
|
+
this.startInlineEditing(objId);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// 2. Çekirdek Olayı Dinleme (Enter tuşu veya MiniToolbar butonundan gelen tetikleme)
|
|
442
|
+
this.core.on('requestInlineTextEdit', (data) => {
|
|
443
|
+
if (data && data.objectId) {
|
|
444
|
+
this.startInlineEditing(data.objectId);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// 3. Dışarı Tıklamada Düzenlemeyi Tamamlama
|
|
449
|
+
window.addEventListener('pointerdown', (e) => {
|
|
450
|
+
if (!this.isEditingText) return;
|
|
451
|
+
// Eğer tıklama düzenlenen metin kutusu içindeyse veya mini araç çubuğundaysa kapatma
|
|
452
|
+
if (e.target.closest('.is-editing-text') || e.target.closest('#contextual-mini-toolbar')) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
this.finishInlineEditing();
|
|
456
|
+
}, this.options.signal ? { capture: true, signal: this.options.signal } : true);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
startInlineEditing(objId) {
|
|
460
|
+
const obj = this.objectManager.getObject(objId);
|
|
461
|
+
if (!obj || obj.locked || obj.type === 'line') return;
|
|
462
|
+
|
|
463
|
+
// Zaten bu nesne düzenleniyorsa tekrar başlatma
|
|
464
|
+
if (this.isEditingText && this.activeEditingObjId === objId) return;
|
|
465
|
+
if (this.isEditingText) {
|
|
466
|
+
this.finishInlineEditing();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const objEl = this.root.querySelector(`.stage-object[data-id="${objId}"]`);
|
|
470
|
+
if (!objEl) return;
|
|
471
|
+
|
|
472
|
+
const contentEl = objEl.querySelector('.object-inner-content');
|
|
473
|
+
if (!contentEl) return;
|
|
474
|
+
|
|
475
|
+
// Hedef metin öğesini bul veya oluştur
|
|
476
|
+
let targetEl = objEl.querySelector('.object-text-span, .image-caption, .category-header, .sequence-slot-label');
|
|
477
|
+
if (!targetEl) {
|
|
478
|
+
if (obj.type === 'image') {
|
|
479
|
+
targetEl = document.createElement('div');
|
|
480
|
+
targetEl.className = 'image-caption';
|
|
481
|
+
targetEl.style.width = '100%';
|
|
482
|
+
targetEl.style.textAlign = obj.textAlign || 'center';
|
|
483
|
+
targetEl.textContent = obj.text || '';
|
|
484
|
+
contentEl.appendChild(targetEl);
|
|
485
|
+
} else {
|
|
486
|
+
targetEl = document.createElement('span');
|
|
487
|
+
targetEl.className = 'object-text-span';
|
|
488
|
+
targetEl.style.width = '100%';
|
|
489
|
+
targetEl.style.display = 'block';
|
|
490
|
+
targetEl.style.textAlign = obj.textAlign || 'center';
|
|
491
|
+
targetEl.textContent = obj.text || '';
|
|
492
|
+
contentEl.appendChild(targetEl);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
this.isEditingText = true;
|
|
497
|
+
this.activeEditingObjId = objId;
|
|
498
|
+
|
|
499
|
+
objEl.classList.add('is-editing-text');
|
|
500
|
+
targetEl.classList.add('is-editing-text-target');
|
|
501
|
+
targetEl.contentEditable = 'true';
|
|
502
|
+
targetEl.spellcheck = false;
|
|
503
|
+
|
|
504
|
+
// Seçim kutusunu düzenleme moduna al (tıklamaları metne geçir)
|
|
505
|
+
if (this.selectionManager && this.selectionManager.selectionOverlay) {
|
|
506
|
+
this.selectionManager.selectionOverlay.classList.add('is-editing-text');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Odaklan ve tüm metni seç
|
|
510
|
+
targetEl.focus();
|
|
511
|
+
try {
|
|
512
|
+
const range = document.createRange();
|
|
513
|
+
range.selectNodeContents(targetEl);
|
|
514
|
+
const sel = window.getSelection();
|
|
515
|
+
sel.removeAllRanges();
|
|
516
|
+
sel.addRange(range);
|
|
517
|
+
} catch (err) {
|
|
518
|
+
// Güvenli devam
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Klavye kısayolları
|
|
522
|
+
targetEl.onkeydown = (ke) => {
|
|
523
|
+
if (ke.key === 'Escape') {
|
|
524
|
+
ke.preventDefault();
|
|
525
|
+
this.finishInlineEditing();
|
|
526
|
+
} else if (ke.key === 'Enter' && (ke.ctrlKey || ke.metaKey)) {
|
|
527
|
+
ke.preventDefault();
|
|
528
|
+
this.finishInlineEditing();
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
finishInlineEditing() {
|
|
534
|
+
if (!this.isEditingText || !this.activeEditingObjId) return;
|
|
535
|
+
|
|
536
|
+
const objId = this.activeEditingObjId;
|
|
537
|
+
const obj = this.objectManager.getObject(objId);
|
|
538
|
+
this.isEditingText = false;
|
|
539
|
+
this.activeEditingObjId = null;
|
|
540
|
+
|
|
541
|
+
const objEl = this.root.querySelector(`.stage-object[data-id="${objId}"]`);
|
|
542
|
+
if (objEl) {
|
|
543
|
+
objEl.classList.remove('is-editing-text');
|
|
544
|
+
const targetEl = objEl.querySelector('.is-editing-text-target') ||
|
|
545
|
+
objEl.querySelector('.object-text-span, .image-caption, .category-header, .sequence-slot-label');
|
|
546
|
+
|
|
547
|
+
if (targetEl) {
|
|
548
|
+
targetEl.contentEditable = 'false';
|
|
549
|
+
targetEl.classList.remove('is-editing-text-target');
|
|
550
|
+
targetEl.onkeydown = null;
|
|
551
|
+
|
|
552
|
+
// Metin içeriğini temizleyip güncelle
|
|
553
|
+
let rawText = targetEl.innerText !== undefined ? targetEl.innerText : targetEl.textContent;
|
|
554
|
+
// Sondaki gereksiz fazladan boşluk veya satırları normalize et
|
|
555
|
+
let newText = rawText.replace(/\r\n/g, '\n').replace(/\n+$/, '');
|
|
556
|
+
if (obj && ['rectangle', 'circle', 'image'].includes(obj.type) && !newText.trim()) {
|
|
557
|
+
newText = '';
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (obj && obj.text !== newText) {
|
|
561
|
+
this.objectManager.updateObject(objId, { text: newText });
|
|
562
|
+
} else if (obj && ['rectangle', 'circle', 'image'].includes(obj.type) && newText === '') {
|
|
563
|
+
targetEl.remove();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Seçim kutusunu normale döndür
|
|
569
|
+
if (this.selectionManager && this.selectionManager.selectionOverlay) {
|
|
570
|
+
this.selectionManager.selectionOverlay.classList.remove('is-editing-text');
|
|
571
|
+
this.selectionManager.updateSelectionBox();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
showToast(message, type = 'info', duration = 3000) {
|
|
576
|
+
let toast = this.root.getElementById('app-toast');
|
|
577
|
+
if (!toast) {
|
|
578
|
+
toast = document.createElement('div');
|
|
579
|
+
toast.id = 'app-toast';
|
|
580
|
+
toast.className = 'app-toast';
|
|
581
|
+
(this.root.body || this.root).appendChild(toast);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
toast.className = `app-toast toast-${type} is-visible`;
|
|
585
|
+
toast.textContent = message;
|
|
586
|
+
|
|
587
|
+
clearTimeout(this.toastTimeout);
|
|
588
|
+
this.toastTimeout = setTimeout(() => {
|
|
589
|
+
toast.classList.remove('is-visible');
|
|
590
|
+
}, duration);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Global namespace
|
|
595
|
+
window.DND = window.DND || {};
|
|
596
|
+
window.DND.UIManager = UIManager;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AudioSynthesizer - Tarayıcı Web Audio API kullanarak harici ses dosyalarına
|
|
3
|
+
* ihtiyaç duymadan temiz, profesyonel ses efektleri üretir.
|
|
4
|
+
* Tamamen çevrimdışı çalışır.
|
|
5
|
+
*/
|
|
6
|
+
class AudioSynthesizer {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.ctx = null;
|
|
9
|
+
this.enabled = true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
init() {
|
|
13
|
+
if (!this.ctx && (window.AudioContext || window.webkitAudioContext)) {
|
|
14
|
+
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
15
|
+
this.ctx = new AudioCtx();
|
|
16
|
+
}
|
|
17
|
+
if (this.ctx && this.ctx.state === 'suspended') {
|
|
18
|
+
this.ctx.resume();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setEnabled(val) {
|
|
23
|
+
this.enabled = !!val;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
playTone(freq, type, duration, delay = 0, gainLevel = 0.15) {
|
|
27
|
+
if (!this.enabled) return;
|
|
28
|
+
try {
|
|
29
|
+
this.init();
|
|
30
|
+
if (!this.ctx) return;
|
|
31
|
+
|
|
32
|
+
const startTime = this.ctx.currentTime + delay;
|
|
33
|
+
const osc = this.ctx.createOscillator();
|
|
34
|
+
const gain = this.ctx.createGain();
|
|
35
|
+
|
|
36
|
+
osc.type = type;
|
|
37
|
+
osc.frequency.setValueAtTime(freq, startTime);
|
|
38
|
+
|
|
39
|
+
gain.gain.setValueAtTime(gainLevel, startTime);
|
|
40
|
+
gain.gain.exponentialRampToValueAtTime(0.0001, startTime + duration);
|
|
41
|
+
|
|
42
|
+
osc.connect(gain);
|
|
43
|
+
gain.connect(this.ctx.destination);
|
|
44
|
+
|
|
45
|
+
osc.start(startTime);
|
|
46
|
+
osc.stop(startTime + duration);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// Ses hatası sessizce geçilir
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Doğru cevap sesi: Neşeli, yükselen majör akor (C5, E5, G5)
|
|
53
|
+
playCorrect() {
|
|
54
|
+
if (!this.enabled) return;
|
|
55
|
+
this.playTone(523.25, 'triangle', 0.12, 0, 0.18); // C5
|
|
56
|
+
this.playTone(659.25, 'triangle', 0.14, 0.08, 0.18); // E5
|
|
57
|
+
this.playTone(783.99, 'triangle', 0.25, 0.16, 0.22); // G5
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Yanlış cevap sesi: Yumuşak alçalan uyarı tonu
|
|
61
|
+
playWrong() {
|
|
62
|
+
if (!this.enabled) return;
|
|
63
|
+
this.playTone(320, 'sine', 0.12, 0, 0.15);
|
|
64
|
+
this.playTone(260, 'sine', 0.22, 0.09, 0.15);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Nesneyi eline alma / tıklama çıt sesi
|
|
68
|
+
playPickup() {
|
|
69
|
+
if (!this.enabled) return;
|
|
70
|
+
this.playTone(480, 'sine', 0.05, 0, 0.08);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Nesneyi bırakma sesi
|
|
74
|
+
playDrop() {
|
|
75
|
+
if (!this.enabled) return;
|
|
76
|
+
this.playTone(360, 'sine', 0.07, 0, 0.1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Etkinlik tamamlandığında kutlama fanfarı
|
|
80
|
+
playComplete() {
|
|
81
|
+
if (!this.enabled) return;
|
|
82
|
+
const notes = [
|
|
83
|
+
{ f: 523.25, d: 0.12, t: 0 }, // C5
|
|
84
|
+
{ f: 659.25, d: 0.12, t: 0.1 }, // E5
|
|
85
|
+
{ f: 783.99, d: 0.14, t: 0.2 }, // G5
|
|
86
|
+
{ f: 1046.50, d: 0.45, t: 0.32 } // C6
|
|
87
|
+
];
|
|
88
|
+
notes.forEach(n => {
|
|
89
|
+
this.playTone(n.f, 'triangle', n.d, n.t, 0.2);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Web Audio Context kapatma ve bellek temizliği (Lifecycle Destroy)
|
|
94
|
+
close() {
|
|
95
|
+
if (this.ctx && typeof this.ctx.close === 'function') {
|
|
96
|
+
try {
|
|
97
|
+
this.ctx.close();
|
|
98
|
+
} catch (e) {}
|
|
99
|
+
this.ctx = null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Universal namespace export
|
|
105
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
106
|
+
module.exports = { AudioSynthesizer };
|
|
107
|
+
}
|
|
108
|
+
if (typeof window !== 'undefined') {
|
|
109
|
+
window.DND = window.DND || {};
|
|
110
|
+
window.DND.AudioSynthesizer = AudioSynthesizer;
|
|
111
|
+
}
|