senangwebs-tour 1.0.8 → 1.0.9

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,6 @@
1
1
  {
2
2
  "name": "senangwebs-tour",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "type": "module",
5
5
  "description": "VR 360° virtual tour system with visual editor and viewer library",
6
6
  "main": "dist/swt.js",
@@ -1,5 +1,6 @@
1
1
  // Main Editor Controller
2
2
  import { debounce, showModal, showToast } from './utils.js';
3
+ import EventEmitter, { EditorEvents } from './event-emitter.js';
3
4
 
4
5
  class TourEditor {
5
6
  constructor(options = {}) {
@@ -29,6 +30,47 @@ class TourEditor {
29
30
  this.hasUnsavedChanges = false;
30
31
  this.lastRenderedSceneIndex = -1;
31
32
  this.listenersSetup = false;
33
+
34
+ // Initialize event emitter
35
+ this.events = new EventEmitter();
36
+ }
37
+
38
+ /**
39
+ * Subscribe to editor events
40
+ * @param {string} event - Event name (use EditorEvents constants)
41
+ * @param {Function} callback - Callback function
42
+ * @returns {Function} Unsubscribe function
43
+ */
44
+ on(event, callback) {
45
+ return this.events.on(event, callback);
46
+ }
47
+
48
+ /**
49
+ * Subscribe to an event once
50
+ * @param {string} event - Event name
51
+ * @param {Function} callback - Callback function
52
+ * @returns {Function} Unsubscribe function
53
+ */
54
+ once(event, callback) {
55
+ return this.events.once(event, callback);
56
+ }
57
+
58
+ /**
59
+ * Unsubscribe from an event
60
+ * @param {string} event - Event name
61
+ * @param {Function} callback - Callback to remove
62
+ */
63
+ off(event, callback) {
64
+ this.events.off(event, callback);
65
+ }
66
+
67
+ /**
68
+ * Emit an event
69
+ * @param {string} event - Event name
70
+ * @param {Object} data - Event data
71
+ */
72
+ emit(event, data = {}) {
73
+ this.events.emit(event, data);
32
74
  }
33
75
 
34
76
  /**
@@ -89,6 +131,9 @@ class TourEditor {
89
131
 
90
132
  showToast('Editor ready', 'success');
91
133
 
134
+ // Emit ready event
135
+ this.emit(EditorEvents.READY, { config: this.options });
136
+
92
137
  return true;
93
138
  }
94
139
 
@@ -294,6 +339,9 @@ class TourEditor {
294
339
  }
295
340
 
296
341
  const scene = await this.sceneManager.addScene(file);
342
+ if (scene) {
343
+ this.emit(EditorEvents.SCENE_ADD, { scene, file });
344
+ }
297
345
  }
298
346
  this.uiController.setLoading(false);
299
347
  this.render();
@@ -327,6 +375,11 @@ class TourEditor {
327
375
  this.lastRenderedSceneIndex = -1;
328
376
  this.render();
329
377
  this.markUnsavedChanges();
378
+ this.emit(EditorEvents.HOTSPOT_ADD, {
379
+ hotspot,
380
+ position,
381
+ sceneId: this.sceneManager.getCurrentScene()?.id
382
+ });
330
383
  } else {
331
384
  console.error('Failed to add hotspot');
332
385
  }
@@ -372,6 +425,8 @@ class TourEditor {
372
425
  this.uiController.updateHotspotProperties(null);
373
426
  this.uiController.updateInitialSceneOptions();
374
427
  this.uiController.updateTargetSceneOptions();
428
+
429
+ this.emit(EditorEvents.SCENE_SELECT, { scene, index });
375
430
  }
376
431
  }
377
432
 
@@ -390,6 +445,8 @@ class TourEditor {
390
445
  if (hotspot) {
391
446
  this.previewController.pointCameraToHotspot(hotspot);
392
447
  }
448
+
449
+ this.emit(EditorEvents.HOTSPOT_SELECT, { hotspot, index });
393
450
  }
394
451
  }
395
452
 
@@ -397,9 +454,11 @@ class TourEditor {
397
454
  * Remove scene
398
455
  */
399
456
  removeScene(index) {
457
+ const scene = this.sceneManager.getScene(index);
400
458
  if (this.sceneManager.removeScene(index)) {
401
459
  this.render();
402
460
  this.markUnsavedChanges();
461
+ this.emit(EditorEvents.SCENE_REMOVE, { scene, index });
403
462
  }
404
463
  }
405
464
 
@@ -407,10 +466,12 @@ class TourEditor {
407
466
  * Remove hotspot
408
467
  */
409
468
  removeHotspot(index) {
469
+ const hotspot = this.hotspotEditor.getHotspot(index);
410
470
  if (this.hotspotEditor.removeHotspot(index)) {
411
471
  this.lastRenderedSceneIndex = -1;
412
472
  this.render();
413
473
  this.markUnsavedChanges();
474
+ this.emit(EditorEvents.HOTSPOT_REMOVE, { hotspot, index });
414
475
  }
415
476
  }
416
477
 
@@ -423,6 +484,7 @@ class TourEditor {
423
484
  this.lastRenderedSceneIndex = -1;
424
485
  this.render();
425
486
  this.markUnsavedChanges();
487
+ this.emit(EditorEvents.HOTSPOT_DUPLICATE, { hotspot, originalIndex: index });
426
488
  }
427
489
  }
428
490
 
@@ -433,6 +495,7 @@ class TourEditor {
433
495
  if (this.sceneManager.reorderScenes(fromIndex, toIndex)) {
434
496
  this.render();
435
497
  this.markUnsavedChanges();
498
+ this.emit(EditorEvents.SCENE_REORDER, { fromIndex, toIndex });
436
499
  }
437
500
  }
438
501
 
@@ -445,6 +508,12 @@ class TourEditor {
445
508
  await this.previewController.updateHotspotMarker(index);
446
509
  this.uiController.renderHotspotList();
447
510
  this.markUnsavedChanges();
511
+ this.emit(EditorEvents.HOTSPOT_UPDATE, {
512
+ hotspot: this.hotspotEditor.getHotspot(index),
513
+ index,
514
+ property,
515
+ value
516
+ });
448
517
  }
449
518
  }
450
519
 
@@ -481,6 +550,13 @@ class TourEditor {
481
550
  await this.previewController.updateHotspotMarker(index);
482
551
  this.uiController.renderHotspotList();
483
552
  this.markUnsavedChanges();
553
+ this.emit(EditorEvents.HOTSPOT_POSITION_CHANGE, {
554
+ hotspot,
555
+ index,
556
+ axis,
557
+ value,
558
+ position: hotspot.position
559
+ });
484
560
  }
485
561
  }
486
562
 
@@ -492,6 +568,12 @@ class TourEditor {
492
568
  if (this.sceneManager.updateScene(index, property, value)) {
493
569
  this.uiController.renderSceneList();
494
570
  this.markUnsavedChanges();
571
+ this.emit(EditorEvents.SCENE_UPDATE, {
572
+ scene: this.sceneManager.getScene(index),
573
+ index,
574
+ property,
575
+ value
576
+ });
495
577
  }
496
578
  }
497
579
 
@@ -518,6 +600,7 @@ class TourEditor {
518
600
  showToast('Scene image updated', 'success');
519
601
  }
520
602
  this.markUnsavedChanges();
603
+ this.emit(EditorEvents.SCENE_IMAGE_CHANGE, { scene, index, imageUrl });
521
604
  }
522
605
  }
523
606
 
@@ -545,6 +628,7 @@ class TourEditor {
545
628
  this.uiController.updateSceneProperties(scene);
546
629
  this.markUnsavedChanges();
547
630
  showToast('Starting position set', 'success');
631
+ this.emit(EditorEvents.SCENE_STARTING_POSITION_SET, { scene, startingPosition: scene.startingPosition });
548
632
  }
549
633
 
550
634
  /**
@@ -562,6 +646,7 @@ class TourEditor {
562
646
  this.uiController.updateSceneProperties(scene);
563
647
  this.markUnsavedChanges();
564
648
  showToast('Starting position cleared', 'success');
649
+ this.emit(EditorEvents.SCENE_STARTING_POSITION_CLEAR, { scene });
565
650
  }
566
651
 
567
652
  /**
@@ -603,6 +688,8 @@ class TourEditor {
603
688
  }
604
689
  this.lastRenderedSceneIndex = -1;
605
690
  }
691
+
692
+ this.emit(EditorEvents.UI_RENDER);
606
693
  }
607
694
 
608
695
  /**
@@ -617,6 +704,7 @@ class TourEditor {
617
704
  if (this.storageManager.saveProject(projectData)) {
618
705
  this.hasUnsavedChanges = false;
619
706
  showToast('Project saved', 'success');
707
+ this.emit(EditorEvents.PROJECT_SAVE, { projectData });
620
708
  return true;
621
709
  }
622
710
  return false;
@@ -633,6 +721,7 @@ class TourEditor {
633
721
  this.hasUnsavedChanges = false;
634
722
  this.render();
635
723
  showToast('Project loaded', 'success');
724
+ this.emit(EditorEvents.PROJECT_LOAD, { projectData });
636
725
  return true;
637
726
  }
638
727
  return false;
@@ -659,6 +748,7 @@ class TourEditor {
659
748
  this.render();
660
749
 
661
750
  showToast('New project created', 'success');
751
+ this.emit(EditorEvents.PROJECT_NEW, { config: this.config });
662
752
  return true;
663
753
  }
664
754
 
@@ -689,6 +779,7 @@ const importUpload = document.getElementById('importUpload');
689
779
  this.uiController.setLoading(false);
690
780
 
691
781
  showToast('Project imported successfully', 'success');
782
+ this.emit(EditorEvents.PROJECT_IMPORT, { projectData, file });
692
783
  } catch (error) {
693
784
  this.uiController.setLoading(false);
694
785
  console.error('Import failed:', error);
@@ -714,3 +805,4 @@ document.addEventListener('DOMContentLoaded', async () => {
714
805
  });
715
806
 
716
807
  export default TourEditor;
808
+ export { EditorEvents } from './event-emitter.js';
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Event Emitter for TourEditor
3
+ *
4
+ * Provides a comprehensive event system for the editor with:
5
+ * - Specific events for all editor operations
6
+ * - A unified 'change' event that fires for any modification
7
+ * - Support for wildcards and namespaced events
8
+ */
9
+
10
+ /**
11
+ * Event types for the Tour Editor
12
+ * These are the available events that can be listened to
13
+ */
14
+ export const EditorEvents = {
15
+ // Lifecycle
16
+ INIT: 'init',
17
+ READY: 'ready',
18
+ DESTROY: 'destroy',
19
+
20
+ // Scene events
21
+ SCENE_ADD: 'scene:add',
22
+ SCENE_REMOVE: 'scene:remove',
23
+ SCENE_SELECT: 'scene:select',
24
+ SCENE_UPDATE: 'scene:update',
25
+ SCENE_REORDER: 'scene:reorder',
26
+ SCENE_CLEAR: 'scene:clear',
27
+ SCENE_IMAGE_CHANGE: 'scene:imageChange',
28
+ SCENE_STARTING_POSITION_SET: 'scene:startingPositionSet',
29
+ SCENE_STARTING_POSITION_CLEAR: 'scene:startingPositionClear',
30
+
31
+ // Hotspot events
32
+ HOTSPOT_ADD: 'hotspot:add',
33
+ HOTSPOT_REMOVE: 'hotspot:remove',
34
+ HOTSPOT_SELECT: 'hotspot:select',
35
+ HOTSPOT_UPDATE: 'hotspot:update',
36
+ HOTSPOT_DUPLICATE: 'hotspot:duplicate',
37
+ HOTSPOT_POSITION_CHANGE: 'hotspot:positionChange',
38
+
39
+ // Project events
40
+ PROJECT_NEW: 'project:new',
41
+ PROJECT_SAVE: 'project:save',
42
+ PROJECT_LOAD: 'project:load',
43
+ PROJECT_IMPORT: 'project:import',
44
+ PROJECT_EXPORT: 'project:export',
45
+
46
+ // Config events
47
+ CONFIG_UPDATE: 'config:update',
48
+ INITIAL_SCENE_CHANGE: 'config:initialSceneChange',
49
+
50
+ // Preview events
51
+ PREVIEW_START: 'preview:start',
52
+ PREVIEW_STOP: 'preview:stop',
53
+ PREVIEW_SCENE_CHANGE: 'preview:sceneChange',
54
+
55
+ // UI events
56
+ UI_RENDER: 'ui:render',
57
+ UI_LOADING_START: 'ui:loadingStart',
58
+ UI_LOADING_END: 'ui:loadingEnd',
59
+ MODAL_OPEN: 'ui:modalOpen',
60
+ MODAL_CLOSE: 'ui:modalClose',
61
+
62
+ // Data events
63
+ DATA_CHANGE: 'data:change', // Fires when any data changes
64
+ UNSAVED_CHANGES: 'data:unsavedChanges',
65
+
66
+ // Unified change event - fires for ANY modification
67
+ CHANGE: 'change'
68
+ };
69
+
70
+ /**
71
+ * Event Emitter class
72
+ * Provides pub/sub functionality for editor events
73
+ */
74
+ class EventEmitter {
75
+ constructor() {
76
+ this.listeners = new Map();
77
+ this.onceListeners = new Map();
78
+ }
79
+
80
+ /**
81
+ * Register an event listener
82
+ * @param {string} event - Event name or 'change' for all changes
83
+ * @param {Function} callback - Function to call when event fires
84
+ * @returns {Function} Unsubscribe function
85
+ */
86
+ on(event, callback) {
87
+ if (!this.listeners.has(event)) {
88
+ this.listeners.set(event, new Set());
89
+ }
90
+ this.listeners.get(event).add(callback);
91
+
92
+ // Return unsubscribe function
93
+ return () => this.off(event, callback);
94
+ }
95
+
96
+ /**
97
+ * Register a one-time event listener
98
+ * @param {string} event - Event name
99
+ * @param {Function} callback - Function to call once when event fires
100
+ * @returns {Function} Unsubscribe function
101
+ */
102
+ once(event, callback) {
103
+ if (!this.onceListeners.has(event)) {
104
+ this.onceListeners.set(event, new Set());
105
+ }
106
+ this.onceListeners.get(event).add(callback);
107
+
108
+ return () => {
109
+ if (this.onceListeners.has(event)) {
110
+ this.onceListeners.get(event).delete(callback);
111
+ }
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Remove an event listener
117
+ * @param {string} event - Event name
118
+ * @param {Function} callback - Function to remove
119
+ */
120
+ off(event, callback) {
121
+ if (this.listeners.has(event)) {
122
+ this.listeners.get(event).delete(callback);
123
+ }
124
+ if (this.onceListeners.has(event)) {
125
+ this.onceListeners.get(event).delete(callback);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Remove all listeners for an event, or all listeners if no event specified
131
+ * @param {string} [event] - Optional event name
132
+ */
133
+ removeAllListeners(event) {
134
+ if (event) {
135
+ this.listeners.delete(event);
136
+ this.onceListeners.delete(event);
137
+ } else {
138
+ this.listeners.clear();
139
+ this.onceListeners.clear();
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Emit an event
145
+ * @param {string} event - Event name
146
+ * @param {Object} data - Event data
147
+ */
148
+ emit(event, data = {}) {
149
+ const eventData = {
150
+ type: event,
151
+ timestamp: Date.now(),
152
+ ...data
153
+ };
154
+
155
+ // Call specific event listeners
156
+ if (this.listeners.has(event)) {
157
+ this.listeners.get(event).forEach(callback => {
158
+ try {
159
+ callback(eventData);
160
+ } catch (error) {
161
+ console.error(`Error in event listener for "${event}":`, error);
162
+ }
163
+ });
164
+ }
165
+
166
+ // Call once listeners and remove them
167
+ if (this.onceListeners.has(event)) {
168
+ const onceCallbacks = this.onceListeners.get(event);
169
+ this.onceListeners.delete(event);
170
+ onceCallbacks.forEach(callback => {
171
+ try {
172
+ callback(eventData);
173
+ } catch (error) {
174
+ console.error(`Error in once listener for "${event}":`, error);
175
+ }
176
+ });
177
+ }
178
+
179
+ // Also emit to wildcard listeners (namespace:*)
180
+ const namespace = event.split(':')[0];
181
+ const wildcardEvent = `${namespace}:*`;
182
+ if (this.listeners.has(wildcardEvent)) {
183
+ this.listeners.get(wildcardEvent).forEach(callback => {
184
+ try {
185
+ callback(eventData);
186
+ } catch (error) {
187
+ console.error(`Error in wildcard listener for "${wildcardEvent}":`, error);
188
+ }
189
+ });
190
+ }
191
+
192
+ // Emit unified 'change' event for data-modifying events
193
+ if (this.isDataModifyingEvent(event) && event !== EditorEvents.CHANGE) {
194
+ this.emit(EditorEvents.CHANGE, {
195
+ originalEvent: event,
196
+ ...data
197
+ });
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Check if an event modifies data (should trigger 'change' event)
203
+ * @param {string} event - Event name
204
+ * @returns {boolean}
205
+ */
206
+ isDataModifyingEvent(event) {
207
+ const dataEvents = [
208
+ EditorEvents.SCENE_ADD,
209
+ EditorEvents.SCENE_REMOVE,
210
+ EditorEvents.SCENE_UPDATE,
211
+ EditorEvents.SCENE_REORDER,
212
+ EditorEvents.SCENE_CLEAR,
213
+ EditorEvents.SCENE_IMAGE_CHANGE,
214
+ EditorEvents.SCENE_STARTING_POSITION_SET,
215
+ EditorEvents.SCENE_STARTING_POSITION_CLEAR,
216
+ EditorEvents.HOTSPOT_ADD,
217
+ EditorEvents.HOTSPOT_REMOVE,
218
+ EditorEvents.HOTSPOT_UPDATE,
219
+ EditorEvents.HOTSPOT_DUPLICATE,
220
+ EditorEvents.HOTSPOT_POSITION_CHANGE,
221
+ EditorEvents.CONFIG_UPDATE,
222
+ EditorEvents.INITIAL_SCENE_CHANGE,
223
+ EditorEvents.PROJECT_LOAD,
224
+ EditorEvents.PROJECT_IMPORT,
225
+ EditorEvents.PROJECT_NEW,
226
+ EditorEvents.DATA_CHANGE
227
+ ];
228
+ return dataEvents.includes(event);
229
+ }
230
+
231
+ /**
232
+ * Get the number of listeners for an event
233
+ * @param {string} event - Event name
234
+ * @returns {number}
235
+ */
236
+ listenerCount(event) {
237
+ let count = 0;
238
+ if (this.listeners.has(event)) {
239
+ count += this.listeners.get(event).size;
240
+ }
241
+ if (this.onceListeners.has(event)) {
242
+ count += this.onceListeners.get(event).size;
243
+ }
244
+ return count;
245
+ }
246
+
247
+ /**
248
+ * Get all event names that have listeners
249
+ * @returns {string[]}
250
+ */
251
+ eventNames() {
252
+ const names = new Set([...this.listeners.keys(), ...this.onceListeners.keys()]);
253
+ return Array.from(names);
254
+ }
255
+ }
256
+
257
+ export default EventEmitter;
@@ -1,7 +1,8 @@
1
1
  // Export Manager - Handles JSON generation for SWT library
2
- import { downloadTextAsFile, showModal, copyToClipboard } from "./utils.js";
2
+ import { downloadTextAsFile, showModal, copyToClipboard, showToast } from "./utils.js";
3
3
  import { IconRenderer } from "../../IconRenderer.js";
4
4
  import { buildTourConfig } from "./data-transform.js";
5
+ import { EditorEvents } from "./event-emitter.js";
5
6
 
6
7
  class ExportManager {
7
8
  constructor(editor) {
@@ -21,6 +22,51 @@ class ExportManager {
21
22
  return buildTourConfig(config, scenes);
22
23
  }
23
24
 
25
+ /**
26
+ * Load tour data from JSON (inverse of generateJSON)
27
+ * This loads the entire tour configuration including initialScene and all scenes
28
+ * @param {Object} tourData - Tour configuration object with initialScene and scenes
29
+ * @param {string} tourData.initialScene - Initial scene ID
30
+ * @param {Array} tourData.scenes - Array of scene objects
31
+ * @returns {boolean} Success status
32
+ */
33
+ loadJSON(tourData) {
34
+ try {
35
+ if (!tourData || typeof tourData !== 'object') {
36
+ console.error('Invalid tour data: expected object');
37
+ return false;
38
+ }
39
+
40
+ // Load scenes into scene manager
41
+ const scenes = tourData.scenes || [];
42
+ this.editor.sceneManager.loadScenes(scenes);
43
+
44
+ // Set initial scene in config
45
+ if (tourData.initialScene) {
46
+ this.editor.config.initialSceneId = tourData.initialScene;
47
+ } else if (scenes.length > 0) {
48
+ this.editor.config.initialSceneId = scenes[0].id;
49
+ }
50
+
51
+ // Mark as having unsaved changes
52
+ this.editor.hasUnsavedChanges = true;
53
+
54
+ // Re-render the editor UI
55
+ this.editor.render();
56
+
57
+ showToast('Tour loaded successfully', 'success');
58
+
59
+ // Emit event
60
+ this.editor.emit(EditorEvents.PROJECT_LOAD, { tourData, source: 'loadJSON' });
61
+
62
+ return true;
63
+ } catch (error) {
64
+ console.error('Failed to load tour data:', error);
65
+ showToast('Failed to load tour', 'error');
66
+ return false;
67
+ }
68
+ }
69
+
24
70
  /**
25
71
  * Generate JSON with icons baked in as SVG data URLs
26
72
  * This ensures the exported HTML doesn't need the SenangStart icons library
@@ -325,7 +325,7 @@ class UIController {
325
325
  grid.appendChild(btn);
326
326
  });
327
327
 
328
- console.log(`Loaded ${iconsData.length} icons`);
328
+ // console.log(`Loaded ${iconsData.length} icons`);
329
329
  }
330
330
 
331
331
  /**