senangwebs-tour 1.0.7 → 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.7",
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
  /**
@@ -55,8 +97,8 @@ class TourEditor {
55
97
  // Setup event listeners
56
98
  this.setupEventListeners();
57
99
 
58
- // Populate icon grid
59
- this.uiController.populateIconGrid();
100
+ // Populate icon grid (async to wait for custom element registration)
101
+ await this.uiController.populateIconGrid();
60
102
 
61
103
  // Load saved project if exists (but only if it has valid data)
62
104
  if (this.storageManager.hasProject()) {
@@ -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
  /**
@@ -570,6 +655,7 @@ class TourEditor {
570
655
  render() {
571
656
  this.uiController.renderSceneList();
572
657
  this.uiController.renderHotspotList();
658
+ this.uiController.populateIconGrid(); // Re-render icon grid to ensure icons display
573
659
 
574
660
  const currentScene = this.sceneManager.getCurrentScene();
575
661
  const currentHotspot = this.hotspotEditor.getCurrentHotspot();
@@ -602,6 +688,8 @@ class TourEditor {
602
688
  }
603
689
  this.lastRenderedSceneIndex = -1;
604
690
  }
691
+
692
+ this.emit(EditorEvents.UI_RENDER);
605
693
  }
606
694
 
607
695
  /**
@@ -616,6 +704,7 @@ class TourEditor {
616
704
  if (this.storageManager.saveProject(projectData)) {
617
705
  this.hasUnsavedChanges = false;
618
706
  showToast('Project saved', 'success');
707
+ this.emit(EditorEvents.PROJECT_SAVE, { projectData });
619
708
  return true;
620
709
  }
621
710
  return false;
@@ -632,6 +721,7 @@ class TourEditor {
632
721
  this.hasUnsavedChanges = false;
633
722
  this.render();
634
723
  showToast('Project loaded', 'success');
724
+ this.emit(EditorEvents.PROJECT_LOAD, { projectData });
635
725
  return true;
636
726
  }
637
727
  return false;
@@ -658,6 +748,7 @@ class TourEditor {
658
748
  this.render();
659
749
 
660
750
  showToast('New project created', 'success');
751
+ this.emit(EditorEvents.PROJECT_NEW, { config: this.config });
661
752
  return true;
662
753
  }
663
754
 
@@ -688,6 +779,7 @@ const importUpload = document.getElementById('importUpload');
688
779
  this.uiController.setLoading(false);
689
780
 
690
781
  showToast('Project imported successfully', 'success');
782
+ this.emit(EditorEvents.PROJECT_IMPORT, { projectData, file });
691
783
  } catch (error) {
692
784
  this.uiController.setLoading(false);
693
785
  console.error('Import failed:', error);
@@ -713,3 +805,4 @@ document.addEventListener('DOMContentLoaded', async () => {
713
805
  });
714
806
 
715
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
@@ -281,11 +281,27 @@ class UIController {
281
281
 
282
282
  /**
283
283
  * Populate icon grid from SenangStart icons (baked in at build time)
284
+ * Waits for ss-icon custom element to be defined to avoid race conditions
284
285
  */
285
- populateIconGrid() {
286
+ async populateIconGrid() {
286
287
  const grid = document.getElementById("hotspotIconGrid");
287
288
  if (!grid) return;
288
289
 
290
+ // Wait for ss-icon custom element to be defined before populating
291
+ // This prevents race conditions where icons don't render if the
292
+ // custom element isn't registered yet when this method runs
293
+ try {
294
+ if (customElements.get('ss-icon') === undefined) {
295
+ // Give a reasonable timeout to avoid infinite waiting
296
+ await Promise.race([
297
+ customElements.whenDefined('ss-icon'),
298
+ new Promise((_, reject) => setTimeout(() => reject(new Error('ss-icon timeout')), 5000))
299
+ ]);
300
+ }
301
+ } catch (err) {
302
+ console.warn('ss-icon custom element not available, icon grid may not render properly:', err.message);
303
+ }
304
+
289
305
  // Clear existing content
290
306
  grid.innerHTML = "";
291
307
 
@@ -309,7 +325,7 @@ class UIController {
309
325
  grid.appendChild(btn);
310
326
  });
311
327
 
312
- console.log(`Loaded ${iconsData.length} icons`);
328
+ // console.log(`Loaded ${iconsData.length} icons`);
313
329
  }
314
330
 
315
331
  /**