@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,220 @@
1
+ /**
2
+ * EditorCore - Merkezi veri modeli, durum yönetimi ve olay yayınlama sistemi.
3
+ * DOM'u veri kaynağı olarak kullanmaz; tek doğru kaynak (Single Source of Truth) bu sınıftır.
4
+ */
5
+ class EditorCore {
6
+ constructor() {
7
+ this.events = {};
8
+ this.mode = 'design'; // 'design' | 'answer-key' | 'preview'
9
+ this.data = this.createDefaultProject();
10
+ this.activeSubMode = null;
11
+ this.historyManager = null;
12
+ }
13
+
14
+ // Geçmiş Yönetimi Entegrasyonu (Merkezi API)
15
+ setHistoryManager(historyManager) {
16
+ this.historyManager = historyManager;
17
+ }
18
+
19
+ pushHistoryState(description = '') {
20
+ if (this.historyManager && typeof this.historyManager.pushState === 'function') {
21
+ this.historyManager.pushState(description);
22
+ } else {
23
+ this.emit('requestHistoryPush', { description });
24
+ }
25
+ }
26
+
27
+ undo() {
28
+ if (this.historyManager && typeof this.historyManager.undo === 'function') {
29
+ return this.historyManager.undo();
30
+ }
31
+ }
32
+
33
+ redo() {
34
+ if (this.historyManager && typeof this.historyManager.redo === 'function') {
35
+ return this.historyManager.redo();
36
+ }
37
+ }
38
+
39
+ canUndo() {
40
+ return this.historyManager ? this.historyManager.canUndo() : false;
41
+ }
42
+
43
+ canRedo() {
44
+ return this.historyManager ? this.historyManager.canRedo() : false;
45
+ }
46
+
47
+ createDefaultProject(activityType = 'image-labeling', name = 'Yeni Etkinlik') {
48
+ return {
49
+ schemaVersion: 1,
50
+ version: '1.0',
51
+ project: {
52
+ id: 'proj_' + Date.now().toString(36),
53
+ name: name,
54
+ activityType: activityType,
55
+ description: 'Öğretmen tarafından hazırlanan etkileşimli sürükle-bırak etkinliği'
56
+ },
57
+ stage: {
58
+ width: 1200,
59
+ height: 675,
60
+ backgroundColor: '#f8fafc',
61
+ backgroundImage: null
62
+ },
63
+ objects: [],
64
+ answerKey: {},
65
+ settings: {
66
+ evaluationMode: 'instant', // 'instant' | 'submit'
67
+ wrongAnswerBehavior: 'return', // 'return' | 'stay'
68
+ correctAnswerBehavior: 'lock', // 'lock'
69
+ showFeedback: true,
70
+ showScore: true,
71
+ allowRetry: true,
72
+ shuffleDraggables: true,
73
+ soundEnabled: true,
74
+ showTargetsInGame: true,
75
+ completionMessage: 'Harika İş! Etkinliği başarıyla tamamladınız.'
76
+ }
77
+ };
78
+ }
79
+
80
+ // Olay Dinleme ve Tetikleme
81
+ on(event, handler) {
82
+ if (!this.events[event]) this.events[event] = [];
83
+ this.events[event].push(handler);
84
+ return () => this.off(event, handler);
85
+ }
86
+
87
+ off(event, handler) {
88
+ if (!this.events[event]) return;
89
+ this.events[event] = this.events[event].filter(h => h !== handler);
90
+ }
91
+
92
+ emit(event, payload) {
93
+ if (this.events[event]) {
94
+ this.events[event].forEach(handler => {
95
+ try {
96
+ handler(payload);
97
+ } catch (err) {
98
+ console.error(`Olay işleyici hatası [${event}]:`, err);
99
+ }
100
+ });
101
+ }
102
+ }
103
+
104
+ // Mod Değişimi: 'design', 'answer-key', 'preview'
105
+ setMode(newMode) {
106
+ if (this.mode === newMode) return;
107
+ const oldMode = this.mode;
108
+ this.mode = newMode;
109
+ this.emit('modeChanged', { oldMode, newMode });
110
+ }
111
+
112
+ getMode() {
113
+ return this.mode;
114
+ }
115
+
116
+ // Proje Veri Yönetimi
117
+ loadProject(projectData, saveToHistory = true) {
118
+ const valResult = window.DND.ValidationEngine.validateProject(projectData);
119
+ if (!valResult.valid) {
120
+ throw new Error('Proje yüklenemedi: ' + valResult.errors.join(', '));
121
+ }
122
+
123
+ // Güvenli kopyalama ve varsayılan alan tamamlama
124
+ const defaults = this.createDefaultProject(
125
+ projectData.project?.activityType || 'image-labeling',
126
+ projectData.project?.name || 'Etkinlik'
127
+ );
128
+
129
+ this.data = {
130
+ schemaVersion: projectData.schemaVersion || 1,
131
+ version: projectData.version || '1.0',
132
+ project: Object.assign({}, defaults.project, projectData.project),
133
+ stage: Object.assign({}, defaults.stage, projectData.stage),
134
+ objects: (projectData.objects || []).map(obj => Object.assign({}, obj)),
135
+ answerKey: Object.assign({}, projectData.answerKey || {}),
136
+ settings: Object.assign({}, defaults.settings, projectData.settings)
137
+ };
138
+
139
+ const isHistoryRestore = (saveToHistory === false) || (typeof saveToHistory === 'object' && saveToHistory.isHistoryRestore);
140
+ const shouldSaveHistory = typeof saveToHistory === 'object' ? (saveToHistory.saveToHistory ?? true) : !!saveToHistory;
141
+
142
+ this.emit('projectLoaded', { project: this.data, saveToHistory: shouldSaveHistory, isHistoryRestore });
143
+ this.emit('dataChanged', { source: 'loadProject' });
144
+ this.autoSave();
145
+ }
146
+
147
+ getProjectData() {
148
+ // Derin kopya döndür
149
+ return JSON.parse(JSON.stringify(this.data));
150
+ }
151
+
152
+ setActivityType(newType) {
153
+ if (!window.DND.ValidationEngine.VALID_ACTIVITY_TYPES.includes(newType)) return;
154
+ this.data.project.activityType = newType;
155
+ this.emit('activityTypeChanged', { activityType: newType });
156
+ this.emit('dataChanged', { source: 'setActivityType' });
157
+ this.autoSave();
158
+ }
159
+
160
+ setProjectName(name) {
161
+ this.data.project.name = String(name).trim() || 'Adsız Etkinlik';
162
+ this.emit('projectNameChanged', { name: this.data.project.name });
163
+ this.emit('dataChanged', { source: 'setProjectName' });
164
+ this.autoSave();
165
+ }
166
+
167
+ updateSettings(partialSettings) {
168
+ this.data.settings = Object.assign({}, this.data.settings, partialSettings);
169
+ this.emit('settingsChanged', { settings: this.data.settings });
170
+ this.emit('dataChanged', { source: 'updateSettings' });
171
+ this.autoSave();
172
+ }
173
+
174
+ updateStage(partialStage) {
175
+ this.data.stage = Object.assign({}, this.data.stage, partialStage);
176
+ this.emit('stageChanged', { stage: this.data.stage });
177
+ this.emit('dataChanged', { source: 'updateStage' });
178
+ this.autoSave();
179
+ }
180
+
181
+ // Yerel depolama (Autosave)
182
+ autoSave() {
183
+ try {
184
+ const serialized = JSON.stringify(this.data);
185
+ // localStorage boyut sınırı (~5MB) kontrolü
186
+ if (serialized.length < 4 * 1024 * 1024) {
187
+ localStorage.setItem('dnd_autosave_project', serialized);
188
+ localStorage.setItem('dnd_autosave_time', Date.now().toString());
189
+ }
190
+ } catch (e) {
191
+ console.warn('Otomatik kayıt localStorage kapasitesi aşıldı veya depolama devre dışı.');
192
+ }
193
+ }
194
+
195
+ hasAutoSavedProject() {
196
+ try {
197
+ return !!localStorage.getItem('dnd_autosave_project');
198
+ } catch (e) {
199
+ return false;
200
+ }
201
+ }
202
+
203
+ loadAutoSavedProject() {
204
+ try {
205
+ const raw = localStorage.getItem('dnd_autosave_project');
206
+ if (raw) {
207
+ const parsed = JSON.parse(raw);
208
+ this.loadProject(parsed, false);
209
+ return true;
210
+ }
211
+ } catch (e) {
212
+ console.error('Otomatik kayıt yükleme hatası:', e);
213
+ }
214
+ return false;
215
+ }
216
+ }
217
+
218
+ // Global namespace
219
+ window.DND = window.DND || {};
220
+ window.DND.EditorCore = EditorCore;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * HistoryManager - Geri Al (Undo) ve İleri Al (Redo) geçmiş yönetim sistemi.
3
+ * Nesne taşıma, boyutlandırma, silme, ekleme ve eşleştirme durumlarını kaydeder.
4
+ */
5
+ class HistoryManager {
6
+ constructor(core, maxHistory = 40, options = {}) {
7
+ this.core = core;
8
+ if (this.core) {
9
+ this.core.historyManager = this;
10
+ if (typeof this.core.setHistoryManager === 'function') {
11
+ this.core.setHistoryManager(this);
12
+ }
13
+ }
14
+ this.maxHistory = maxHistory;
15
+ this.undoStack = [];
16
+ this.redoStack = [];
17
+ this.isApplying = false;
18
+ this.signal = options.signal;
19
+
20
+ this.init();
21
+
22
+ // Çekirdekte mevcut bir proje verisi varsa başlangıç taban durumunu kaydet
23
+ if (this.core && this.core.data) {
24
+ this.pushState('Başlangıç');
25
+ }
26
+ }
27
+
28
+ init() {
29
+ // Dışarıdan yeni bir proje yüklendiğinde geçmiş sıfırlanır ve yeni başlangıç durumu kaydedilir;
30
+ // ancak iç geri yüklemelerde (undo/redo sırasında isApplying veya saveToHistory === false) geçmiş korunur!
31
+ this.core.on('projectLoaded', (payload) => {
32
+ if (this.isApplying) return;
33
+ if (payload && (payload.saveToHistory === false || payload.isHistoryRestore)) return;
34
+ this.clear();
35
+ this.pushState('Proje Yüklendi');
36
+ });
37
+
38
+ // Çekirdekten veya bileşenlerden gelen açık geçmiş kaydetme isteği
39
+ this.core.on('requestHistoryPush', (payload) => {
40
+ if (this.isApplying) return;
41
+ this.pushState(payload?.description || '');
42
+ });
43
+
44
+ // Nesne ekleme işlemi
45
+ this.core.on('objectAdded', (payload) => {
46
+ if (this.isApplying) return;
47
+ if (payload && payload.saveHistory === false) return;
48
+ this.pushState('Nesne Eklendi');
49
+ });
50
+
51
+ // Nesne silme işlemi
52
+ this.core.on('objectDeleted', (payload) => {
53
+ if (this.isApplying) return;
54
+ if (payload && payload.saveHistory === false) return;
55
+ this.pushState('Nesne Silindi');
56
+ });
57
+
58
+ // Nesne özelliği güncelleme (renk, rol, metin, boyut vb.)
59
+ this.core.on('objectUpdated', (payload) => {
60
+ if (this.isApplying) return;
61
+ if (payload && payload.saveHistory === false) return;
62
+ this.pushState('Nesne Güncellendi');
63
+ });
64
+
65
+ // Katman sırası değişimi
66
+ this.core.on('layerChanged', (payload) => {
67
+ if (this.isApplying) return;
68
+ if (payload && payload.saveHistory === false) return;
69
+ this.pushState('Katman Değiştirildi');
70
+ });
71
+
72
+ // Cevap anahtarı eşleşme değişimi
73
+ this.core.on('answerKeyChanged', (payload) => {
74
+ if (this.isApplying) return;
75
+ if (payload && payload.saveHistory === false) return;
76
+ this.pushState('Cevap Anahtarı Güncellendi');
77
+ });
78
+
79
+ // Klavye kısayolları: Ctrl+Z / Ctrl+Y
80
+ window.addEventListener('keydown', (e) => {
81
+ if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement?.tagName) ||
82
+ document.activeElement?.isContentEditable) {
83
+ return;
84
+ }
85
+
86
+ if (this.core.getMode() !== 'design') return;
87
+
88
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
89
+ const cmdOrCtrl = isMac ? e.metaKey : e.ctrlKey;
90
+
91
+ if (cmdOrCtrl && e.key.toLowerCase() === 'z') {
92
+ if (e.shiftKey) {
93
+ // Redo
94
+ e.preventDefault();
95
+ this.redo();
96
+ } else {
97
+ // Undo
98
+ e.preventDefault();
99
+ this.undo();
100
+ }
101
+ } else if (cmdOrCtrl && e.key.toLowerCase() === 'y') {
102
+ // Redo
103
+ e.preventDefault();
104
+ this.redo();
105
+ }
106
+ }, this.signal ? { signal: this.signal } : undefined);
107
+ }
108
+
109
+ push(description = '') {
110
+ return this.pushState(description);
111
+ }
112
+
113
+ pushState(description = '') {
114
+ if (this.isApplying) return;
115
+
116
+ const snapshot = JSON.stringify(this.core.data);
117
+
118
+ // En son kaydedilen durumla aynıysa mükerrer kayıt yapma
119
+ if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length - 1] === snapshot) {
120
+ return;
121
+ }
122
+
123
+ this.undoStack.push(snapshot);
124
+ if (this.undoStack.length > this.maxHistory) {
125
+ this.undoStack.shift();
126
+ }
127
+
128
+ // Yeni işlem yapıldığında Redo yığını sıfırlanır
129
+ this.redoStack = [];
130
+
131
+ this.notifyState(description);
132
+ }
133
+
134
+ undo() {
135
+ if (this.undoStack.length <= 1) return; // En az 1 başlangıç durumu kalmalı
136
+
137
+ this.isApplying = true;
138
+ try {
139
+ const currentState = this.undoStack.pop();
140
+ this.redoStack.push(currentState);
141
+
142
+ const prevState = this.undoStack[this.undoStack.length - 1];
143
+ const parsed = JSON.parse(prevState);
144
+ this.core.loadProject(parsed, { saveToHistory: false, isHistoryRestore: true });
145
+ } finally {
146
+ this.isApplying = false;
147
+ this.notifyState('Geri Alındı');
148
+ }
149
+ }
150
+
151
+ redo() {
152
+ if (this.redoStack.length === 0) return;
153
+
154
+ this.isApplying = true;
155
+ try {
156
+ const nextState = this.redoStack.pop();
157
+ this.undoStack.push(nextState);
158
+
159
+ const parsed = JSON.parse(nextState);
160
+ this.core.loadProject(parsed, { saveToHistory: false, isHistoryRestore: true });
161
+ } finally {
162
+ this.isApplying = false;
163
+ this.notifyState('İleri Alındı');
164
+ }
165
+ }
166
+
167
+ canUndo() {
168
+ return this.undoStack.length > 1;
169
+ }
170
+
171
+ canRedo() {
172
+ return this.redoStack.length > 0;
173
+ }
174
+
175
+ clear() {
176
+ this.undoStack = [];
177
+ this.redoStack = [];
178
+ this.notifyState('Geçmiş Sıfırlandı');
179
+ }
180
+
181
+ notifyState(action = '') {
182
+ this.core.emit('historyChanged', {
183
+ canUndo: this.canUndo(),
184
+ canRedo: this.canRedo(),
185
+ undoCount: Math.max(0, this.undoStack.length - 1),
186
+ redoCount: this.redoStack.length,
187
+ action
188
+ });
189
+ }
190
+ }
191
+
192
+ // Global namespace
193
+ window.DND = window.DND || {};
194
+ window.DND.HistoryManager = HistoryManager;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * LayerManager - Sahnedeki nesnelerin katman sırasını (z-index) ve
3
+ * öne/arkaya getirme işlemlerini yönetir.
4
+ */
5
+ class LayerManager {
6
+ constructor(core, objectManager) {
7
+ this.core = core;
8
+ this.objectManager = objectManager;
9
+ }
10
+
11
+ getSortedObjects() {
12
+ return [...this.core.data.objects].sort((a, b) => (a.zIndex || 1) - (b.zIndex || 1));
13
+ }
14
+
15
+ normalizeLayers() {
16
+ const sorted = this.getSortedObjects();
17
+ sorted.forEach((obj, idx) => {
18
+ obj.zIndex = idx + 1;
19
+ this.objectManager.updateObjectDOM(obj);
20
+ });
21
+ }
22
+
23
+ bringToFront(id, saveHistory = true) {
24
+ const obj = this.objectManager.getObject(id);
25
+ if (!obj) return;
26
+
27
+ this.normalizeLayers();
28
+ const sorted = this.getSortedObjects();
29
+ const maxZ = sorted[sorted.length - 1]?.zIndex || 1;
30
+ obj.zIndex = maxZ + 1;
31
+
32
+ this.normalizeLayers();
33
+ this.core.emit('layerChanged', { object: obj, saveHistory });
34
+ this.core.emit('dataChanged', { source: 'layer' });
35
+ }
36
+
37
+ sendToBack(id, saveHistory = true) {
38
+ const obj = this.objectManager.getObject(id);
39
+ if (!obj) return;
40
+
41
+ this.normalizeLayers();
42
+ obj.zIndex = 0;
43
+
44
+ this.normalizeLayers();
45
+ this.core.emit('layerChanged', { object: obj, saveHistory });
46
+ this.core.emit('dataChanged', { source: 'layer' });
47
+ }
48
+
49
+ bringForward(id, saveHistory = true) {
50
+ this.normalizeLayers();
51
+ const sorted = this.getSortedObjects();
52
+ const idx = sorted.findIndex(o => o.id === id);
53
+ if (idx === -1 || idx === sorted.length - 1) return;
54
+
55
+ // Bir üsttekiyle yer değiştir
56
+ const nextObj = sorted[idx + 1];
57
+ const tempZ = sorted[idx].zIndex;
58
+ sorted[idx].zIndex = nextObj.zIndex;
59
+ nextObj.zIndex = tempZ;
60
+
61
+ this.normalizeLayers();
62
+ this.core.emit('layerChanged', { object: sorted[idx], saveHistory });
63
+ this.core.emit('dataChanged', { source: 'layer' });
64
+ }
65
+
66
+ sendBackward(id, saveHistory = true) {
67
+ this.normalizeLayers();
68
+ const sorted = this.getSortedObjects();
69
+ const idx = sorted.findIndex(o => o.id === id);
70
+ if (idx <= 0) return;
71
+
72
+ // Bir alttakiyle yer değiştir
73
+ const prevObj = sorted[idx - 1];
74
+ const tempZ = sorted[idx].zIndex;
75
+ sorted[idx].zIndex = prevObj.zIndex;
76
+ prevObj.zIndex = tempZ;
77
+
78
+ this.normalizeLayers();
79
+ this.core.emit('layerChanged', { object: sorted[idx], saveHistory });
80
+ this.core.emit('dataChanged', { source: 'layer' });
81
+ }
82
+ }
83
+
84
+ // Global namespace
85
+ window.DND = window.DND || {};
86
+ window.DND.LayerManager = LayerManager;