@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/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@mhmttosun/dnd",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.1",
4
+ "description": "HTML Tabanlı Görsel Sürükle-Bırak Etkinlik Editörü ve Oynatıcısı (100% DOM/SVG, Zero Canvas, Bağımsız Çevrimdışı Tek Parça HTML İhracı, React/Vue/Web-Component Entegrasyonu)",
5
+ "main": "./dist/player.umd.cjs",
6
+ "module": "./dist/player.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/player.js",
12
+ "require": "./dist/player.umd.cjs"
13
+ },
14
+ "./player": {
15
+ "types": "./dist/player.d.ts",
16
+ "import": "./dist/player.js",
17
+ "require": "./dist/player.umd.cjs"
18
+ },
19
+ "./editor": {
20
+ "types": "./dist/editor.d.ts",
21
+ "import": "./dist/editor.js"
22
+ },
23
+ "./web-component": {
24
+ "import": "./dist/dnd-player-element.js"
25
+ },
26
+ "./player.css": "./dist/player.css",
27
+ "./editor.css": "./dist/editor.css"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "src",
32
+ "css",
33
+ "js",
34
+ "index.html",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "scripts": {
39
+ "start": "python3 -m http.server 8000",
40
+ "build": "python3 scripts/build.py",
41
+ "test": "python3 tests/run_tests.py",
42
+ "prepublishOnly": "python3 scripts/build.py && python3 tests/run_tests.py"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "keywords": [
48
+ "drag-and-drop",
49
+ "activity-editor",
50
+ "activity-player",
51
+ "web-component",
52
+ "education",
53
+ "edtech",
54
+ "interactive-learning",
55
+ "vanilla-js",
56
+ "react-dnd",
57
+ "no-canvas",
58
+ "svg",
59
+ "offline-first"
60
+ ],
61
+ "author": "mhmttosun <mhmttosun@gmail.com>",
62
+ "license": "MIT",
63
+ "repository": {
64
+ "type": "git",
65
+ "url": "git+https://github.com/mhmttosun/dnd.git"
66
+ },
67
+ "bugs": {
68
+ "url": "https://github.com/mhmttosun/dnd/issues"
69
+ },
70
+ "homepage": "https://github.com/mhmttosun/dnd#readme"
71
+ }
@@ -0,0 +1,309 @@
1
+ /**
2
+ * DND ActivityPlayer - Modern JavaScript / TypeScript Kütüphane Sarmalayıcısı.
3
+ *
4
+ * Herhangi bir JS projesine (React, Vue, Angular, Svelte, Vanilla JS)
5
+ * tek satırla entegre olabilen yaşam döngüsü (lifecycle) yönetimli etkinlik oynatıcısı.
6
+ */
7
+
8
+ // Global veya import edilmiş ActivityEngine referansı
9
+ const getActivityEngine = () => {
10
+ if (typeof window !== 'undefined' && window.DND && window.DND.ActivityEngine) {
11
+ return window.DND.ActivityEngine;
12
+ }
13
+ return null;
14
+ };
15
+
16
+ export class ActivityPlayer {
17
+ /**
18
+ * @param {Object|string} options Veya doğrudan hedef DOM elementi / seçicisi
19
+ * @param {HTMLElement|string} [options.target] Hedef container elementi veya CSS seçicisi ('#quiz')
20
+ * @param {Object} [options.data] Etkinlik JSON veri modeli
21
+ * @param {string} [options.src] Etkinlik JSON dosyasının URL adresi
22
+ * @param {boolean} [options.sound=true] Ses efektleri açık mı
23
+ * @param {number|null} [options.stagePadding=null] Sahnenin kapsayıcı içindeki toplam yatay/dikey boşluğu
24
+ * @param {Function} [options.onComplete] Etkinlik tamamlandığında çağrılacak callback
25
+ * @param {Function} [options.onStep] Her adımda (drop) çağrılacak callback
26
+ * @param {Function} [options.onDrop] Her bırakma işleminde çağrılacak callback
27
+ * @param {Function} [options.onReset] Yeniden başlatıldığında çağrılacak callback
28
+ * @param {Function} [options.onReady] Oynatıcı hazır olduğunda çağrılacak callback
29
+ */
30
+ constructor(options = {}) {
31
+ this.options = Object.assign({
32
+ sound: true,
33
+ playMode: 'learning',
34
+ stagePadding: null,
35
+ onComplete: null,
36
+ onStep: null,
37
+ onDrop: null,
38
+ onReset: null,
39
+ onReady: null,
40
+ initialState: null
41
+ }, options);
42
+
43
+ this.container = null;
44
+ this.engine = null;
45
+ this.isDestroyed = false;
46
+
47
+ // Hedef elementi çözümle
48
+ if (this.options.target) {
49
+ this.resolveContainer(this.options.target);
50
+ }
51
+
52
+ // Veri başlatma (Doğrudan veri, URL, File veya JSON metni)
53
+ if (this.options.data) {
54
+ this.mount(this.options.data);
55
+ } else if (this.options.src) {
56
+ this.loadFromUrl(this.options.src);
57
+ } else if (this.options.file) {
58
+ this.loadFromFile(this.options.file);
59
+ } else if (this.options.jsonString) {
60
+ this.loadFromJsonString(this.options.jsonString);
61
+ }
62
+ }
63
+
64
+ resolveContainer(target) {
65
+ if (typeof target === 'string') {
66
+ this.container = document.querySelector(target);
67
+ } else if (target instanceof HTMLElement) {
68
+ this.container = target;
69
+ }
70
+ if (!this.container) {
71
+ throw new Error(`[ActivityPlayer] Hedef container elementi bulunamadı: ${target}`);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * JSON dosya URL'sinden veya yolundan veriyi asenkron çekip oynatıcıyı başlatır
77
+ * @param {string} url
78
+ */
79
+ async loadFromUrl(url) {
80
+ try {
81
+ const response = await fetch(url);
82
+ if (!response.ok) {
83
+ throw new Error(`HTTP Hata: ${response.status} (${response.statusText})`);
84
+ }
85
+ const data = await response.json();
86
+ this.mount(data);
87
+ } catch (err) {
88
+ console.error(`[ActivityPlayer] Veri yüklenemedi:`, err);
89
+ if (this.container) {
90
+ this.container.innerHTML = `
91
+ <div style="padding: 24px; text-align: center; color: #ef4444; font-family: sans-serif;">
92
+ ⚠️ Etkinlik verisi yüklenemedi: ${err.message}
93
+ </div>
94
+ `;
95
+ }
96
+ throw err;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Bir File veya Blob nesnesini (örneğin <input type="file"> ile seçilen yerel dosyayı)
102
+ * doğrudan okuyup etkinliği başlatır
103
+ * @param {File|Blob} file
104
+ */
105
+ async loadFromFile(file) {
106
+ if (!file) throw new Error('[ActivityPlayer] Dosya nesnesi boş olamaz.');
107
+ try {
108
+ let text = '';
109
+ if (typeof file.text === 'function') {
110
+ text = await file.text();
111
+ } else {
112
+ text = await new Promise((resolve, reject) => {
113
+ const reader = new FileReader();
114
+ reader.onload = () => resolve(reader.result);
115
+ reader.onerror = () => reject(reader.error);
116
+ reader.readAsText(file, 'utf-8');
117
+ });
118
+ }
119
+ this.loadFromJsonString(text);
120
+ } catch (err) {
121
+ console.error('[ActivityPlayer] Dosya okunamadı:', err);
122
+ if (this.container) {
123
+ this.container.innerHTML = `
124
+ <div style="padding: 24px; text-align: center; color: #ef4444; font-family: sans-serif;">
125
+ ⚠️ Dosya okunamadı: ${err.message}
126
+ </div>
127
+ `;
128
+ }
129
+ throw err;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Ham JSON metnini (JSON String) ayrıştırıp etkinliği başlatır
135
+ * @param {string} jsonString
136
+ */
137
+ loadFromJsonString(jsonString) {
138
+ try {
139
+ const data = typeof jsonString === 'string' ? JSON.parse(jsonString) : jsonString;
140
+ this.mount(data);
141
+ } catch (err) {
142
+ console.error('[ActivityPlayer] JSON ayrıştırma hatası:', err);
143
+ if (this.container) {
144
+ this.container.innerHTML = `
145
+ <div style="padding: 24px; text-align: center; color: #ef4444; font-family: sans-serif;">
146
+ ⚠️ Geçersiz JSON formatı: ${err.message}
147
+ </div>
148
+ `;
149
+ }
150
+ throw err;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Oynatıcıyı container içine monte eder
156
+ * @param {Object} projectData
157
+ */
158
+ mount(projectData) {
159
+ if (this.isDestroyed || !this.container) return;
160
+
161
+ // Önceki motor varsa temizle
162
+ if (this.engine) {
163
+ this.engine.destroy();
164
+ this.engine = null;
165
+ }
166
+
167
+ const EngineClass = getActivityEngine();
168
+ if (!EngineClass) {
169
+ throw new Error('[ActivityPlayer] ActivityEngine motoru bulunamadı! DND kütüphanesinin yüklendiğinden emin olun.');
170
+ }
171
+
172
+ // Seçenekleri hazırla
173
+ const engineOptions = {
174
+ isEmbedded: false,
175
+ playMode: this.options.playMode,
176
+ stagePadding: this.options.stagePadding,
177
+ onComplete: (res) => {
178
+ if (typeof this.options.onComplete === 'function') this.options.onComplete(res);
179
+ },
180
+ onStep: (state) => {
181
+ if (typeof this.options.onStep === 'function') this.options.onStep(state);
182
+ },
183
+ onDrop: (dropInfo) => {
184
+ if (typeof this.options.onDrop === 'function') this.options.onDrop(dropInfo);
185
+ },
186
+ onReset: (state) => {
187
+ if (typeof this.options.onReset === 'function') this.options.onReset(state);
188
+ }
189
+ };
190
+
191
+ // Ses ayarını uygula
192
+ const mountedData = JSON.parse(JSON.stringify(projectData));
193
+ mountedData.settings = Object.assign({}, mountedData.settings, {
194
+ soundEnabled: this.options.sound !== false
195
+ });
196
+
197
+ this.engine = new EngineClass(this.container, mountedData, engineOptions);
198
+ if (this.options.initialState) this.engine.setState(this.options.initialState);
199
+
200
+ if (typeof this.options.onReady === 'function') {
201
+ this.options.onReady(this);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Olay dinleyici kaydeder ('complete', 'step', 'drop', 'reset', 'destroy')
207
+ */
208
+ on(event, callback) {
209
+ if (this.engine) {
210
+ return this.engine.on(event, callback);
211
+ }
212
+ return () => {};
213
+ }
214
+
215
+ /**
216
+ * Olay dinleyiciyi kaldırır
217
+ */
218
+ off(event, callback) {
219
+ if (this.engine) {
220
+ this.engine.off(event, callback);
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Anlık oyun durumu ve başarı puanını döner
226
+ */
227
+ getState() {
228
+ return this.engine ? this.engine.getState() : null;
229
+ }
230
+
231
+ setState(state) {
232
+ if (!this.engine) throw new Error('[ActivityPlayer] Oynatıcı henüz başlatılmadı.');
233
+ return this.engine.setState(state);
234
+ }
235
+
236
+ /**
237
+ * Ses efektlerini açar veya kapatır
238
+ */
239
+ setSound(enabled) {
240
+ if (this.engine && this.engine.audio) {
241
+ this.engine.audio.setEnabled(!!enabled);
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Kontrol alanındaki geri bildirim mesajını günceller
247
+ * @param {string} message
248
+ * @param {'info'|'success'|'error'|'warning'} [type='info']
249
+ * @param {number} [duration=0]
250
+ */
251
+ setFeedback(message, type = 'info', duration = 0) {
252
+ if (this.engine && typeof this.engine.setFeedback === 'function') {
253
+ this.engine.setFeedback(message, type, duration);
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Anlık geri bildirim durumunu döner
259
+ */
260
+ getFeedback() {
261
+ return (this.engine && typeof this.engine.getFeedback === 'function')
262
+ ? this.engine.getFeedback()
263
+ : null;
264
+ }
265
+
266
+ /**
267
+ * Aktif oyun modunu döner ('learning' veya 'assessment')
268
+ */
269
+ getPlayMode() {
270
+ return (this.engine && typeof this.engine.getPlayMode === 'function')
271
+ ? this.engine.getPlayMode()
272
+ : this.options.playMode || 'learning';
273
+ }
274
+
275
+ /**
276
+ * Oyun modunu değiştirir ('learning' veya 'assessment')
277
+ * @param {'learning'|'assessment'} mode
278
+ */
279
+ setPlayMode(mode) {
280
+ if (this.engine && typeof this.engine.setPlayMode === 'function') {
281
+ this.engine.setPlayMode(mode);
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Etkinliği sıfırlayıp baştan başlatır
287
+ */
288
+ reset() {
289
+ if (this.engine) {
290
+ this.engine.resetGame();
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Yaşam döngüsü sonlandırıcı (React useEffect cleanup, Vue onUnmounted)
296
+ * Tüm event listener'ları ve ses kanallarını kapatır, bellek sızıntısını önler.
297
+ */
298
+ destroy() {
299
+ if (this.isDestroyed) return;
300
+ if (this.engine) {
301
+ this.engine.destroy();
302
+ this.engine = null;
303
+ }
304
+ if (this.container) {
305
+ this.container.innerHTML = '';
306
+ }
307
+ this.isDestroyed = true;
308
+ }
309
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Direct-mount editor API for SPA integrations. The build bundles the legacy
3
+ * editor classes before this module and injects the editor CSS into EDITOR_CSS.
4
+ */
5
+ const EDITOR_CSS = globalThis.__DND_EDITOR_CSS__ || '';
6
+
7
+ const shell = () => `
8
+ <div class="dnd-editor-root">
9
+ <nav class="app-sub-toolbar">
10
+ <div class="sub-toolbar-cluster cluster-left-group">
11
+ <div class="mode-pill-group">
12
+ <button class="mode-toggle-btn active" data-mode="design" type="button" title="Tasarım Modu">
13
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
14
+ </button>
15
+ <button class="mode-toggle-btn" data-mode="answer-key" type="button" title="Cevap Anahtarı Modu">
16
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M9 16l2 2 4-4"/></svg>
17
+ </button>
18
+ <button class="mode-toggle-btn" data-mode="preview" type="button" title="Önizleme Modu">
19
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
20
+ </button>
21
+ </div>
22
+ <div class="template-dropdown-wrapper">
23
+ <button id="btn-toggle-templates" class="sub-tool-btn" type="button" title="Örnek Etkinlik Şablonları">
24
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
25
+ </button>
26
+ <div id="templates-menu" class="templates-menu" style="display:none">
27
+ <div class="menu-title">Örnek Şablonlar</div>
28
+ <button class="template-menu-item" data-sample="matching" type="button">1. Eşleştirme (Kavram ve Tanım)</button>
29
+ <button class="template-menu-item" data-sample="image-labeling" type="button">2. Görsel Etiketleme (İnsan Anatomisi)</button>
30
+ <button class="template-menu-item" data-sample="categorization" type="button">3. Kategorilere Ayırma (Meyve/Sebze)</button>
31
+ <button class="template-menu-item" data-sample="sequencing" type="button">4. Sıralama (Kelebek Yaşam Döngüsü)</button>
32
+ </div>
33
+ </div>
34
+ <div class="answer-key-toolbar-guide" id="answer-key-guide-bar" style="display: none;">
35
+ <div class="ak-toolbar-divider"></div>
36
+ <div class="ak-toolbar-content">
37
+ <div class="ak-toolbar-title-row">
38
+ <span class="ak-toolbar-status-icon" id="ak-status-icon">
39
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
40
+ <circle cx="12" cy="12" r="10"/>
41
+ <polyline points="9 12 12 15 16 10"/>
42
+ </svg>
43
+ </span>
44
+ <span class="ak-guide-title" id="ak-guide-title">Eşleştirmeler tamamlandı</span>
45
+ </div>
46
+ <div class="ak-guide-instruction" id="ak-guide-instruction">4/4 eşleşme · Cevap anahtarını kontrol et</div>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ <div class="sub-toolbar-cluster cluster-add-objects" id="cluster-add-objects">
51
+ <button id="btn-add-text" class="sub-tool-btn" type="button" title="Metin Kutusu Ekle">
52
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="4 7 4 4 20 4 20 7"/><line x1="12" y1="4" x2="12" y2="20"/><line x1="8" y1="20" x2="16" y2="20"/></svg>
53
+ </button>
54
+ <button id="btn-add-rect" class="sub-tool-btn" type="button" title="Kutu / Kart Ekle">
55
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="3"/></svg>
56
+ </button>
57
+ <button id="btn-add-circle" class="sub-tool-btn" type="button" title="Daire Şekli Ekle">
58
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/></svg>
59
+ </button>
60
+ <button id="btn-add-image" class="sub-tool-btn" type="button" title="Bilgisayardan Görsel Yükle">
61
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
62
+ </button>
63
+ <input id="file-input-image" type="file" accept="image/*" hidden>
64
+ <button id="btn-add-target" class="sub-tool-btn" type="button" title="Çizgi Ekle">
65
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="4" y1="20" x2="20" y2="4"/></svg>
66
+ </button>
67
+ </div>
68
+ <div class="sub-toolbar-cluster cluster-history-zoom" id="cluster-history-zoom">
69
+ <button id="btn-undo" class="sub-tool-btn" type="button" disabled title="Geri Al (Ctrl+Z)">
70
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>
71
+ </button>
72
+ <button id="btn-redo" class="sub-tool-btn" type="button" disabled title="İleri Al (Ctrl+Y)">
73
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"/></svg>
74
+ </button>
75
+ <span class="sub-divider"></span>
76
+ <button id="btn-zoom-out" class="sub-tool-btn" type="button" title="Uzaklaştır">
77
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
78
+ </button>
79
+ <button id="btn-zoom-reset" class="sub-tool-btn" type="button" title="Ekrana Sığdır">
80
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>
81
+ </button>
82
+ <button id="btn-zoom-in" class="sub-tool-btn" type="button" title="Yakınlaştır">
83
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
84
+ </button>
85
+ </div>
86
+ </nav>
87
+ <main id="editor-layout" class="editor-workspace">
88
+ <div id="stage-viewport" class="stage-viewport">
89
+ <div id="stage-container" class="stage-container">
90
+ <div id="stage-scaler" class="stage-scaler">
91
+ <div id="stage" class="stage grid-visible mode-design">
92
+ <svg class="stage-svg-overlay" id="svg-overlay"><g id="svg-connections"></g></svg>
93
+ <div class="stage-objects" id="stage-objects"></div>
94
+ <div class="selection-box" id="selection-overlay"></div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </main>
100
+ <div id="preview-container"></div>
101
+ </div>`;
102
+
103
+ function resolveContainer(target) {
104
+ const container = typeof target === 'string' ? document.querySelector(target) : target;
105
+ if (!(container instanceof HTMLElement)) throw new TypeError('DND editörü için geçerli bir container gerekli.');
106
+ return container;
107
+ }
108
+
109
+ function xml(value) {
110
+ return String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;' })[char]);
111
+ }
112
+
113
+ export function createDefaultActivity(activityType = 'matching', name = 'Eşleştirme') {
114
+ const Core = window.DND?.EditorCore;
115
+ if (!Core) throw new Error('DND editör çekirdeği yüklenmedi.');
116
+ return new Core().createDefaultProject(activityType, name);
117
+ }
118
+
119
+ export function renderActivityPreview(projectData) {
120
+ const width = projectData.stage?.width || 1200;
121
+ const height = projectData.stage?.height || 675;
122
+ const background = projectData.stage?.backgroundColor || '#ffffff';
123
+ const objects = [...(projectData.objects || [])].filter(item => item.visible !== false).sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
124
+ const body = [];
125
+ if (projectData.stage?.backgroundImage) {
126
+ body.push(`<image href="${xml(projectData.stage.backgroundImage)}" x="0" y="0" width="${width}" height="${height}" preserveAspectRatio="xMidYMid meet"/>`);
127
+ }
128
+ for (const item of objects) {
129
+ const x = Number(item.x) || 0, y = Number(item.y) || 0;
130
+ const w = Math.max(1, Number(item.width) || 1), h = Math.max(1, Number(item.height) || 1);
131
+ const transform = item.rotation ? ` transform="rotate(${Number(item.rotation) || 0} ${x + w / 2} ${y + h / 2})"` : '';
132
+ if (item.type === 'line') {
133
+ const dash = item.borderStyle === 'dashed' ? '8 6' : item.borderStyle === 'dotted' ? '2 6' : '';
134
+ body.push(`<line x1="${x}" y1="${y + h / 2}" x2="${x + w}" y2="${y + h / 2}" stroke="${xml(item.borderColor || '#1e293b')}" stroke-width="${Number(item.borderWidth) || 2}"${dash ? ` stroke-dasharray="${dash}"` : ''}${transform}/>`);
135
+ continue;
136
+ }
137
+ const radius = item.type === 'circle' ? Math.min(w, h) / 2 : Number(item.borderRadius) || 0;
138
+ body.push(`<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${radius}" fill="${xml(item.backgroundColor || 'transparent')}" stroke="${xml(item.borderColor || 'transparent')}" stroke-width="${Number(item.borderWidth) || 0}"${transform}/>`);
139
+ if (item.type === 'image' && item.src) {
140
+ body.push(`<image href="${xml(item.src)}" x="${x}" y="${y}" width="${w}" height="${h}" preserveAspectRatio="xMidYMid meet"${transform}/>`);
141
+ }
142
+ if (item.text) {
143
+ body.push(`<text x="${x + w / 2}" y="${y + h / 2}" dominant-baseline="middle" text-anchor="middle" font-family="${xml(item.fontFamily || 'system-ui, sans-serif')}" font-size="${Number(item.fontSize) || 16}" font-weight="${xml(item.fontWeight || 'normal')}" fill="${xml(item.textColor || '#1e293b')}"${transform}>${xml(item.text)}</text>`);
144
+ }
145
+ }
146
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="${xml(background)}"/>${body.join('')}</svg>`;
147
+ }
148
+
149
+ export function initializeDndEditor(options = {}) {
150
+ const container = resolveContainer(options.container || options.target);
151
+ if (options.signal?.aborted) throw new DOMException('DND editörü açma işlemi iptal edildi.', 'AbortError');
152
+ const lifecycle = new AbortController();
153
+ const shadow = container.shadowRoot || container.attachShadow({ mode: 'open' });
154
+ shadow.innerHTML = `<style>${EDITOR_CSS}</style>${shell()}`;
155
+ const root = shadow;
156
+ const host = root.querySelector('.dnd-editor-root');
157
+ if (options.ui?.fileActions === false) host.classList.add('hide-file-actions');
158
+ if (options.ui?.embedded === true) host.classList.add('is-embedded');
159
+
160
+ const stageViewport = root.getElementById('stage-viewport');
161
+ const stageEl = root.getElementById('stage');
162
+ const core = new window.DND.EditorCore();
163
+ const stageManager = new window.DND.StageManager(core, stageViewport, stageEl, { signal: lifecycle.signal });
164
+ const objectManager = new window.DND.ObjectManager(core, stageEl);
165
+ const selectionManager = new window.DND.SelectionManager(core, stageManager, objectManager, stageEl, { signal: lifecycle.signal });
166
+ const historyManager = new window.DND.HistoryManager(core, 40, { signal: lifecycle.signal });
167
+ const layerManager = new window.DND.LayerManager(core, objectManager);
168
+ const answerKeyManager = new window.DND.AnswerKeyManager(core, stageManager, objectManager, stageEl);
169
+ const transformEngine = new window.DND.TransformEngine(core, stageManager, objectManager, selectionManager, historyManager, stageEl, { signal: lifecycle.signal });
170
+ const exportManager = new window.DND.ExportManager(core);
171
+ const pending = new Set();
172
+ let taskError = null;
173
+ let destroyed = false;
174
+ const trackTask = task => {
175
+ pending.add(task);
176
+ task.catch(error => { taskError = error; }).finally(() => pending.delete(task));
177
+ };
178
+ const miniToolbar = new window.DND.MiniToolbar(core, objectManager, selectionManager, layerManager, stageManager, root, { signal: lifecycle.signal });
179
+ const uiManager = new window.DND.UIManager(core, stageManager, objectManager, selectionManager, historyManager, exportManager, {
180
+ root,
181
+ uploadImage: options.uploadImage,
182
+ signal: lifecycle.signal,
183
+ trackTask,
184
+ });
185
+ const initial = options.activity || createDefaultActivity('matching', 'Eşleştirme');
186
+ core.loadProject(initial);
187
+ uiManager.updateSaveStatus(true);
188
+
189
+ if (options.signal) options.signal.addEventListener('abort', () => lifecycle.abort(), { once: true });
190
+
191
+ const ensureAlive = () => { if (destroyed) throw new Error('DND editörü kapatılmış.'); };
192
+ return {
193
+ async flush() {
194
+ ensureAlive();
195
+ uiManager.finishInlineEditing?.();
196
+ await Promise.all([...pending]);
197
+ if (taskError) { const error = taskError; taskError = null; throw error; }
198
+ },
199
+ getActivity() { ensureAlive(); return core.getProjectData(); },
200
+ setActivity(activity) { ensureAlive(); core.loadProject(activity); },
201
+ getMode() { ensureAlive(); return core.getMode(); },
202
+ setMode(mode) { ensureAlive(); core.setMode(mode); },
203
+ async exportPreview() {
204
+ await this.flush();
205
+ return new Blob([renderActivityPreview(core.getProjectData())], { type: 'image/svg+xml' });
206
+ },
207
+ destroy() {
208
+ if (destroyed) return;
209
+ destroyed = true;
210
+ lifecycle.abort();
211
+ uiManager.activePlayerInstance?.destroy();
212
+ stageManager.resizeObserver?.disconnect();
213
+ miniToolbar.toolbarEl?.remove();
214
+ shadow.replaceChildren();
215
+ },
216
+ };
217
+ }