PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 8.5.69
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v8.5.69
9.1.2 9.1.1 9.1.0 9.0.3 9.0.2 9.0.1 9.0.0 8.5.79 8.5.78 8.5.77 8.5.76 8.5.75 8.5.74 8.5.73 8.5.72 8.5.71 8.5.70 8.5.69 8.5.68 8.5.35 8.5.36 8.5.37 8.5.38 8.5.39 8.5.4 All 221 releases
wpvr / admin / js / setup-wizard.js

setup-wizard.js in WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress 8.5.69, at admin/js/setup-wizard.js

1,385 lines 61.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 jQuery(document).ready(function ($) {
2 if (typeof window.LinnoOnboarding === 'undefined') {
3 console.error('Linno Onboarding library not loaded');
4 return;
5 }
6
7 const { registerOnboarding, engine, tracker } = window.LinnoOnboarding;
8
9 const content = {
10 'real-estate': {
11 title: "Show your property without another site visit",
12 sub: "Most agents publish their first property tour in minutes.",
13 cta: "USE PROPERTY TEMPLATE",
14 publishText: "Publish property tour",
15 prev: "This is how buyers will experience your listing",
16 succ: "Your tour is published!",
17 succSub: "Your virtual tour has been created and is ready to be displayed on your website.",
18 icon: "🏠"
19 },
20 'hotel': {
21 title: "Let guests experience your hotel before they book",
22 sub: "Virtual tours help increase trust and direct bookings.",
23 cta: "USE HOTEL TEMPLATE",
24 publishText: "Publish hotel tour",
25 prev: "This is how guests will see your room",
26 succ: "Your tour is published!",
27 succSub: "Your virtual tour has been created and is ready to be displayed on your website.",
28 icon: "🏨"
29 },
30 'school': {
31 title: "Showcase your campus to prospective students",
32 sub: "Virtual campus tours help increase enrollment and engagement.",
33 cta: "USE CAMPUS TEMPLATE",
34 publishText: "Publish campus tour",
35 prev: "This is how students will explore your campus",
36 succ: "Your tour is published!",
37 succSub: "Your virtual tour has been created and is ready to be displayed on your website.",
38 icon: "🏫"
39 },
40 'automotive': {
41 title: "Showcase your vehicles in stunning detail",
42 sub: "Virtual tours help customers explore every angle before purchase.",
43 cta: "USE AUTOMOTIVE TEMPLATE",
44 publishText: "Publish automotive tour",
45 prev: "This is how customers will explore your vehicles",
46 succ: "Your tour is published!",
47 succSub: "Your virtual tour has been created and is ready to be displayed on your website.",
48 icon: "🚗"
49 },
50 'ecommerce': {
51 title: "Create immersive shopping experiences",
52 sub: "Virtual tours help customers visualize products in real spaces.",
53 cta: "USE E-COMMERCE TEMPLATE",
54 publishText: "Publish ecommerce tour",
55 prev: "This is how customers will shop your products",
56 succ: "Your tour is published!",
57 succSub: "Your virtual tour has been created and is ready to be displayed on your website.",
58 icon: "🛍"
59 },
60 'exhibitions': {
61 title: "Bring your art and exhibitions to life",
62 sub: "Virtual tours help visitors explore galleries from anywhere.",
63 cta: "USE EXHIBITION TEMPLATE",
64 publishText: "Publish exhibition tour",
65 prev: "This is how visitors will experience your exhibition",
66 succ: "Your tour is published!",
67 succSub: "Your virtual tour has been created and is ready to be displayed on your website.",
68 icon: "🎨"
69 },
70 'offices': {
71 title: "Showcase your workspace to potential clients",
72 sub: "Virtual office tours help build trust and attract talent.",
73 cta: "USE OFFICE TEMPLATE",
74 publishText: "Publish office tour",
75 prev: "This is how visitors will explore your office",
76 succ: "Your tour is published!",
77 succSub: "Your virtual tour has been created and is ready to be displayed on your website.",
78 icon: "🏢"
79 },
80 'training': {
81 title: "Create engaging onboarding experiences",
82 sub: "Virtual training tours help new employees learn faster.",
83 cta: "USE TRAINING TEMPLATE",
84 publishText: "Publish training tour",
85 prev: "This is how trainees will experience your program",
86 succ: "Your tour is published!",
87 succSub: "Your virtual tour has been created and is ready to be displayed on your website.",
88 icon: "📘"
89 },
90 'showrooms': {
91 title: "Transform your showroom into a virtual experience",
92 sub: "Virtual showrooms help customers explore products anytime.",
93 cta: "USE SHOWROOM TEMPLATE",
94 publishText: "Publish showroom tour",
95 prev: "This is how customers will browse your showroom",
96 succ: "Your tour is published!",
97 succSub: "Your virtual tour has been created and is ready to be displayed on your website.",
98 icon: "🏬"
99 },
100 'tourism': {
101 title: "Let travelers explore destinations before they arrive",
102 sub: "Virtual tours help increase bookings and visitor engagement.",
103 cta: "USE TOURISM TEMPLATE",
104 publishText: "Publish tourism tour",
105 prev: "This is how travelers will discover your destination",
106 succ: "🎉 Your tour is published!",
107 succSub: "Your virtual tour has been created and is ready to be displayed on your website.",
108 icon: "🏖"
109 },
110 'fitness': {
111 title: "Showcase your fitness facilities",
112 sub: "Virtual tours help attract new members and showcase amenities.",
113 cta: "USE FITNESS TEMPLATE",
114 publishText: "Publish fitness tour",
115 prev: "This is how members will explore your facility",
116 succ: "Your tour is published!",
117 succSub: "Your virtual tour has been created and is ready to be displayed on your website.",
118 icon: "🏋️"
119 },
120 'museums': {
121 title: "Make cultural sites accessible to everyone",
122 sub: "Virtual museum tours help reach global audiences.",
123 cta: "USE MUSEUM TEMPLATE",
124 publishText: "Publish museum tour",
125 prev: "This is how visitors will explore your museum",
126 succ: "Your tour is published!",
127 succSub: "Your virtual tour has been created and is ready to be displayed on your website.",
128 icon: "🏛"
129 }
130 };
131
132 // Pro / hotspot-limit config (populated via wp_localize_script)
133 const wpvrWizardCfg = (typeof wpvrSetupWizardData !== 'undefined') ? wpvrSetupWizardData : {};
134 const isPro = !!wpvrWizardCfg.is_pro;
135 const HOTSPOT_LIMIT = isPro ? Infinity : (parseInt(wpvrWizardCfg.hotspot_limit, 10) || 5);
136
137 const wizardStrings = wpvrWizardCfg.wizard_strings || {};
138
139 let industrySkipped = false; // true when user chose to upload own image from step 1
140 let bypassTemplateOnBack = false; // true when returning backward through the bypassed template step
141 let selectedIndustry = null;
142 let uploadedImageUrl = null;
143 let uploadedImageFile = null;
144 let uploadedImageId = null; // WordPress attachment ID
145 let panoramaViewer = null;
146 let hotspots = [];
147 let pendingHotspotCoords = null;
148 let panoramaMouseDown = null;
149 let panoramaIsDragging = false;
150 let templateData = null; // Store template tour object
151 let mediaUploader = null; // WordPress media uploader instance
152
153 // Skip wizard and redirect to tour listing
154 function skipWizard() {
155 const listingUrl = ajaxurl.replace('admin-ajax.php', 'edit.php?post_type=wpvr_item');
156 window.location.href = listingUrl;
157 }
158
159 // Bind "Remind me later" button (exits the wizard, redirects to listing)
160 $('#wizard-exit').on('click', function (e) {
161 e.preventDefault();
162 skipWizard();
163 });
164
165 // Generate random string for scene IDs (matching WPVR format)
166 function generateRandomString(length) {
167 var result = '';
168 var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
169 var charactersLength = characters.length;
170 for (var i = 0; i < length; i++) {
171 var randomIndex = Math.floor(Math.random() * charactersLength);
172 result += characters.charAt(randomIndex);
173 }
174 return result;
175 }
176
177 // Universal open WP Media method for our workflow
178 function openWPMediaForDirectUpload(callback) {
179 // Create WordPress media uploader with standard frame
180 const uploader = wp.media({
181 title: 'Select or Upload 360° Image',
182 button: {
183 text: 'Select Image'
184 },
185 multiple: false,
186 library: {
187 type: 'image'
188 },
189 state: 'library'
190 });
191
192 // When image is selected
193 uploader.on('select', function () {
194 const attachment = uploader.state().get('selection').first().toJSON();
195 callback(attachment.url, attachment.id);
196 });
197
198 uploader.open();
199 }
200
201 // ── Skip-mode UI helper ─────────────────────────────────────────
202 // Toggles the sidebar Template nav item and the progress bars in
203 // step-preview and step-success between the 4-step default and the
204 // 3-step "no template" variant.
205 function updateSkipModeUI(isSkipped) {
206 const filled = '<div class="step-seg filled"></div>';
207 const empty = '<div class="step-seg"></div>';
208
209 if (isSkipped) {
210 // Hide template nav, renumber remaining steps as 1→2→3
211 $('.nav-item[data-step="step-template"]').hide();
212 $('.nav-item[data-step="step-preview"]').attr('data-number', '2');
213 $('.nav-item[data-step="step-success"]').attr('data-number', '3');
214
215 // Progress bars — 3-step mode
216 $('#step-vertical .step-progress').html(
217 filled + empty + empty +
218 '<span class="step-count">1/3</span>'
219 );
220 $('#step-preview .step-progress').html(
221 filled + filled + empty +
222 '<span class="step-count">2/3</span>'
223 );
224 $('#step-success .step-progress').html(
225 filled + filled + filled +
226 '<span class="step-count">3/3</span>'
227 );
228 } else {
229 // Show template nav, restore original numbers 1→2→3→4
230 $('.nav-item[data-step="step-template"]').show();
231 $('.nav-item[data-step="step-preview"]').attr('data-number', '3');
232 $('.nav-item[data-step="step-success"]').attr('data-number', '4');
233
234 // Progress bars — 4-step mode
235 $('#step-vertical .step-progress').html(
236 filled + empty + empty + empty +
237 '<span class="step-count">1/4</span>'
238 );
239 $('#step-preview .step-progress').html(
240 filled + filled + filled + empty +
241 '<span class="step-count">3/4</span>'
242 );
243 $('#step-success .step-progress').html(
244 filled + filled + filled + filled +
245 '<span class="step-count">4/4</span>'
246 );
247 }
248 }
249
250 // Helper function to update dynamic content based on selected industry
251 function updateDynamicContent() {
252 const data = content[selectedIndustry];
253 if (!data) return;
254
255 $('.dynamic-title').text(data.title);
256 $('.dynamic-sub').text(data.sub);
257 $('#use-template-btn').text(data.cta);
258 $('.dynamic-prev-title').text(data.prev);
259 $('.dynamic-success-title').text(data.succ);
260 $('.dynamic-success-sub').text(data.succSub);
261 $('#publish-btn').text(data.publishText);
262 }
263
264 function updateIconPlaceholder() {
265 const data = content[selectedIndustry];
266 if (!data) return;
267
268 $('#template-icon-placeholder').text(data.icon);
269 }
270
271 // Navigation helper function
272 function navigateTo(stepId) {
273 // Show/Hide Steps Container (contains sidebar + content)
274 if (stepId === 'step-welcome') {
275 $('#steps-container').hide();
276 $('#welcome-content').show();
277 $('#onboarding-app').removeClass('sidebar-visible');
278 } else {
279 $('#steps-container').css('display', 'flex');
280 $('#welcome-content').hide();
281 $('#onboarding-app').addClass('sidebar-visible');
282 }
283
284 // Toggle Active Step
285 $('.step').removeClass('active');
286 $('#' + stepId).addClass('active');
287
288 // Update Sidebar Navigation UI
289 $('.nav-item').removeClass('active');
290 $(`.nav-item[data-step="${stepId}"]`).addClass('active');
291
292 // Mark previous steps as completed
293 updateNavProgress(stepId);
294
295 // Trigger celebration effect on success step (only once)
296 if (stepId === 'step-success' && !celebrationTriggered) {
297 // Small delay to ensure step is fully rendered
298 setTimeout(() => {
299 triggerCelebration();
300 }, 300);
301 } else if (stepId !== 'step-success') {
302 // Remove confetti if navigating away from success
303 $('.confetti-container').remove();
304 // Reset celebration trigger if going back (optional - comment out if you want it to only play once ever)
305 // celebrationTriggered = false;
306 }
307 }
308
309 function updateNavProgress(currentId) {
310 // Dynamically build steps array depending on 'industrySkipped' or direct upload path
311 let steps = ['step-vertical', 'step-template', 'step-preview', 'step-success'];
312
313 if (industrySkipped || (uploadedImageUrl && !templateData)) {
314 // Hide the template nav item in the DOM
315 $('.nav-item[data-step="step-template"]').hide();
316 steps = ['step-vertical', 'step-preview', 'step-success'];
317 } else {
318 $('.nav-item[data-step="step-template"]').show();
319 }
320
321 const currentIndex = steps.indexOf(currentId);
322
323 $('.nav-item').each(function () {
324 const stepId = $(this).data('step');
325
326 // Skip checking hidden steps
327 if (steps.indexOf(stepId) === -1) {
328 $(this).removeClass('completed active');
329 return;
330 }
331
332 const stepIndex = steps.indexOf(stepId);
333
334 if (stepId === currentId) {
335 $(this).removeClass('completed').addClass('active');
336 } else {
337 $(this).removeClass('active');
338 if (stepIndex < currentIndex && stepIndex !== -1) {
339 $(this).addClass('completed');
340 } else {
341 $(this).removeClass('completed');
342 }
343 }
344 });
345 }
346
347 // Track if celebration has been triggered to prevent replay
348 let celebrationTriggered = false;
349
350 function triggerCelebration() {
351 // Check if already triggered
352 if (celebrationTriggered) {
353 return;
354 }
355
356 // Check for prefers-reduced-motion
357 const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
358 if (prefersReducedMotion) {
359 return;
360 }
361
362 // Mark as triggered
363 celebrationTriggered = true;
364
365 // Remove any existing confetti
366 $('.confetti-container').remove();
367
368 // Create confetti container
369 const confettiContainer = $('<div class="confetti-container"></div>');
370 $('body').append(confettiContainer);
371
372 // Create lightweight confetti particles (fewer particles for performance)
373 const colors = ['#3F04FE', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'];
374 const particleCount = 30; // Reduced from 50 for better performance
375
376 // Get check circle position for burst origin
377 const checkCircle = $('.check-circle');
378 let centerX = window.innerWidth / 2;
379 let centerY = window.innerHeight / 2;
380
381 if (checkCircle.length) {
382 const offset = checkCircle.offset();
383 if (offset) {
384 centerX = offset.left + checkCircle.outerWidth() / 2;
385 centerY = offset.top + checkCircle.outerHeight() / 2;
386 }
387 }
388
389 for (let i = 0; i < particleCount; i++) {
390 const confetti = $('<div class="confetti"></div>');
391 const color = colors[Math.floor(Math.random() * colors.length)];
392
393 // Calculate random direction and distance
394 const angle = (Math.PI * 2 * i) / particleCount + (Math.random() * 0.5);
395 const distance = 100 + Math.random() * 150;
396 const tx = Math.cos(angle) * distance;
397 const ty = Math.sin(angle) * distance;
398 const rotation = 360 + Math.random() * 360;
399
400 confetti.css({
401 'background': color,
402 'left': centerX + 'px',
403 'top': centerY + 'px',
404 '--tx': tx + 'px',
405 '--ty': ty + 'px',
406 '--rot': rotation + 'deg',
407 'width': (6 + Math.random() * 6) + 'px',
408 'height': (6 + Math.random() * 6) + 'px',
409 'border-radius': Math.random() > 0.5 ? '50%' : '0'
410 });
411
412 confettiContainer.append(confetti);
413 }
414
415 // Remove confetti after animation completes (1.2s duration)
416 setTimeout(function () {
417 confettiContainer.remove();
418 }, 1500);
419 }
420
421 // Register onboarding with Linno Onboarding library
422 registerOnboarding({
423 plugin: 'wpvr-onboarding',
424 version: '8.5.54',
425 theme: {
426 color: '#3F04FE'
427 },
428 // WordPress Telemetry Integration
429 telemetry: {
430 onSetupStarted: (data) => {
431 console.log('📊 Telemetry: setup_started', data);
432 },
433 onSetupCompleted: (data) => {
434 console.log('Setup completed event emitted by wizard flow');
435 },
436 onFirstStrikeCompleted: (data) => {
437 console.log('First strike is tracked on first tour publish');
438 }
439 },
440 steps: [
441 {
442 id: 'vertical',
443 title: 'Industry',
444 description: 'What are you creating a virtual tour for?',
445 canGoBack: false,
446 canSkip: false,
447 mount: (container, context) => {
448 navigateTo('step-vertical');
449
450 // Reset global state each time we visit the first step from scratch
451 industrySkipped = false;
452 bypassTemplateOnBack = false;
453 updateSkipModeUI(false);
454
455 setTimeout(() => {
456 // Initially disable next button until industry is selected
457 const nextBtn = $('.next-btn[data-next="step-template"]');
458 if (!selectedIndustry) {
459 nextBtn.prop('disabled', true).addClass('disabled');
460 }
461
462 // Select Industry Flow
463 $('.v-card').off('click').on('click', function () {
464 const newIndustry = $(this).data('vertical');
465
466 // Reset all template/upload/hotspot state when the industry changes
467 if (selectedIndustry && selectedIndustry !== newIndustry) {
468 uploadedImageUrl = null;
469 uploadedImageFile = null;
470 uploadedImageId = null;
471 templateData = null;
472 hotspots = [];
473 mediaUploader = null;
474 if (panoramaViewer) {
475 try { panoramaViewer.destroy(); } catch (e) { }
476 panoramaViewer = null;
477 }
478 }
479
480 $('.v-card').removeClass('active');
481 $(this).addClass('active');
482 selectedIndustry = newIndustry;
483 // Pre-fetch template in the background here (optional) to make next step faster
484
485 // Auto-proceed to step 2 (Template)
486 context.goNext();
487 });
488
489 // Skip / Upload My Own — triggers wp.media immediately
490 $('#skip-industry-link').off('click').on('click', function (e) {
491 e.preventDefault();
492
493 openWPMediaForDirectUpload(function (url, id) {
494 if (url) {
495 industrySkipped = true;
496 selectedIndustry = null;
497 uploadedImageUrl = url;
498 uploadedImageId = id;
499 uploadedImageFile = null;
500 templateData = null;
501 hotspots = [];
502 $('.v-card').removeClass('active');
503
504 // Apply 3-step UI before navigating
505 updateSkipModeUI(true);
506
507 // goNext() lands on template mount which will auto-skip forward
508 context.goNext();
509 }
510 });
511 });
512
513 // Fallback continue btn
514 $('.next-btn[data-next="step-template"]').off('click').on('click', () => {
515 context.goNext();
516 });
517
518 }, 0);
519 }
520 },
521 {
522 id: 'template',
523 title: 'Template',
524 description: 'Choose your template',
525 canGoBack: true,
526 canSkip: false,
527 onNext: (context) => {
528 if (!uploadedImageUrl) {
529 alert('Please upload a 360° image or use a template before proceeding.');
530 return false;
531 }
532 return true;
533 },
534 mount: (container, context) => {
535 // ── Bypass logic ────────────────────────────────────────────
536 // If industry was skipped (forward pass) → jump straight to preview
537 if (industrySkipped) {
538 updateSkipModeUI(true);
539 context.goNext();
540 return;
541 }
542 // If returning backward through a previously-skipped template → go to industry
543 if (bypassTemplateOnBack) {
544 bypassTemplateOnBack = false;
545 updateSkipModeUI(false);
546 context.goBack();
547 return;
548 }
549 // ── Normal template step ─────────────────────────────────
550 navigateTo('step-template');
551 updateDynamicContent();
552 updateIconPlaceholder();
553
554 setTimeout(() => {
555 const $useTemplateBtn = $('#use-template-btn');
556 $useTemplateBtn.removeData('default-text');
557 const defaultUseTemplateText = $useTemplateBtn.text();
558 $useTemplateBtn.data('default-text', defaultUseTemplateText);
559 $useTemplateBtn.prop('disabled', false).removeClass('is-loading');
560
561 // Always show template box side-by-side with upload button
562 $('#template-preview-box').show();
563 $('#upload-section').show();
564
565 // Bind navigation buttons
566 $('.btn-back').off('click').on('click', () => {
567 context.goBack();
568 });
569
570 $('.next-btn[data-next="step-preview"]').not('#use-template-btn').off('click').on('click', () => {
571 context.goNext();
572 });
573
574 // Bind WP media direct trigger — both the zone and the browse link
575 $('#template-upload-zone, #btn-upload-own-image').off('click.tmplUpload').on('click.tmplUpload', function (e) {
576 e.preventDefault();
577 openWPMediaForDirectUpload(function (url, id) {
578 if (url) {
579 uploadedImageUrl = url;
580 uploadedImageId = id;
581 uploadedImageFile = null;
582 templateData = null;
583 hotspots = [];
584
585 // Jump immediately to step-preview
586 context.goNext();
587 }
588 });
589 });
590
591 // Use template button handler - call remote API
592 $('#use-template-btn').off('click.template').on('click.template', function (e) {
593 e.preventDefault();
594
595 // Show loading state
596 const $btn = $(this);
597 const originalText = $btn.text();
598 $btn.prop('disabled', true).addClass('is-loading').text('Importing template & media...');
599
600 // Call AJAX to fetch template
601 $.ajax({
602 url: ajaxurl,
603 method: 'POST',
604 data: {
605 action: 'wpvr_fetch_template',
606 industry: selectedIndustry,
607 security: wpvrNonce
608 },
609 success: function (response) {
610 if (response.success && response.data.template) {
611 templateData = response.data.template;
612
613 if (templateData.post_id) {
614 window.wpvrCreatedTourId = templateData.post_id;
615 window.wpvrCreatedTourUrl = templateData.view_url || '';
616 window.wpvrCreatedTourEditUrl = templateData.edit_url || '';
617 }
618
619 // If template has an image URL, use it
620 hotspots = [];
621 if (templateData.image_url) {
622 uploadedImageUrl = templateData.image_url;
623 } else {
624 // Fallback placeholder
625 uploadedImageUrl = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAwIiBoZWlnaHQ9IjQwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iODAwIiBoZWlnaHQ9IjQwMCIgZmlsbD0iI2YzZjRmNiIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMjQiIGZpbGw9IiM2YjcyODAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIj5UZW1wbGF0ZSBQcmV2aWV3PC90ZXh0Pjwvc3ZnPg==';
626 }
627
628 // Auto proceed to next step
629 setTimeout(() => {
630 context.goNext();
631 }, 500);
632 } else {
633 alert('Failed to load template. Please try again.');
634 $btn.prop('disabled', false).removeClass('is-loading').text(originalText);
635 }
636 },
637 error: function (xhr, status, error) {
638 console.error('Template fetch error:', error);
639 alert('Failed to load template. Please try again.');
640 $btn.prop('disabled', false).removeClass('is-loading').text(originalText);
641 }
642 });
643 });
644 }, 0);
645 }
646 },
647 {
648 id: 'preview',
649 title: 'Preview',
650 description: 'This is how buyers will experience your listing',
651 canGoBack: true,
652 canSkip: false,
653 onNext: (context) => {
654 // Validate that user has either uploaded an image or used a template
655 if (!uploadedImageUrl) {
656 alert('Please upload a 360° image or use a template before proceeding.');
657 return false;
658 }
659 return true;
660 },
661 mount: (container, context) => {
662 navigateTo('step-preview');
663 updateDynamicContent();
664 // Defensive: re-apply skip mode UI in case of any timing issues
665 updateSkipModeUI(industrySkipped);
666
667 setTimeout(() => {
668 // Back button: if user came through skip-mode, go back to industry rather than template
669 $('.btn-back').off('click').on('click', () => {
670 if (industrySkipped) {
671 // Signal template mount to pass straight through backward
672 bypassTemplateOnBack = true;
673 industrySkipped = false;
674 context.goBack(); // lands on template → template auto-calls goBack() to industry
675 } else {
676 context.goBack();
677 }
678 });
679
680 $('#publish-btn').off('click').on('click', function () {
681 const $btn = $(this);
682 const originalText = $btn.text();
683 $btn.prop('disabled', true).text('Publishing...');
684
685 // Build panodata object matching WPVR format
686 // Generate scene ID (8 character random string)
687 const sceneId = generateRandomString(8);
688
689 // Create scene object matching WPVR structure
690 const scene = {
691 'scene-type': 'equirectangular',
692 'scene-id': sceneId,
693 'hotspot-list': [],
694 'dscene': 'off',
695 'scene-attachment-url': uploadedImageUrl
696 };
697
698 // Add hotspots if any (matching WPVR format)
699 if (hotspots.length > 0) {
700 hotspots.forEach(function (hotspot, index) {
701 const hotspotId = generateRandomString(8);
702 scene['hotspot-list'].push({
703 'hotspot-title': hotspotId,
704 'hotspot-pitch': hotspot.pitch,
705 'hotspot-yaw': hotspot.yaw,
706 'hotspot-customclass': '',
707 'hotspot-scene': '',
708 'hotspot-url': '',
709 'hotspot-content': '',
710 'hotspot-hover': '<p>' + (hotspot.text || '') + '</p>',
711 'hotspot-type': hotspot.type || 'info',
712 'hotspot-scene-list': 'none',
713 'wpvr_url_open': {}
714 });
715 });
716 }
717
718 // Build panodata structure matching WPVR format
719 let panodata = {
720 'autoLoad': true,
721 'showControls': true,
722 'customcontrol': '',
723 'genericform': 'off',
724 'genericformshortcode': '',
725 'preview': '',
726 'defaultscene': sceneId, // Set default scene to generated scene ID
727 'scenefadeduration': '',
728 'panodata': {
729 'scene-list': {
730 '1': scene // WPVR uses numeric keys starting from 1
731 }
732 },
733 'previewtext': 'Click To Load Panorama'
734 };
735
736 // If template data exists, merge it with panodata
737 if (templateData && templateData.panodata) {
738 // Merge template settings
739 if (templateData.autoLoad !== undefined) panodata.autoLoad = templateData.autoLoad;
740 if (templateData.showControls !== undefined) panodata.showControls = templateData.showControls;
741 if (templateData.defaultscene) panodata.defaultscene = templateData.defaultscene;
742
743 // Merge template panodata scene-list if exists
744 if (templateData.panodata && templateData.panodata['scene-list']) {
745 // Merge template scenes with our scene
746 Object.assign(panodata.panodata['scene-list'], templateData.panodata['scene-list']);
747 // Ensure our scene is the first one (key 1)
748 panodata.panodata['scene-list']['1'] = scene;
749 panodata.defaultscene = sceneId;
750 }
751 }
752
753 // Create tour via AJAX
754 $.ajax({
755 url: ajaxurl,
756 type: 'POST',
757 data: {
758 action: 'wpvr_create_tour_from_wizard',
759 panodata: JSON.stringify(panodata),
760 templateMeta: templateData && templateData.meta ? JSON.stringify(templateData.meta) : '',
761 existing_post_id: templateData && templateData.post_id ? templateData.post_id : '',
762 title: 'My ' + (
763 selectedIndustry === 'real-estate' ? 'Property' :
764 selectedIndustry === 'hotel' ? 'Hotel' :
765 selectedIndustry === 'school' ? 'Campus' :
766 selectedIndustry === 'automotive' ? 'Automotive' :
767 selectedIndustry === 'ecommerce' ? 'E-commerce' :
768 selectedIndustry === 'exhibitions' ? 'Exhibition' :
769 selectedIndustry === 'offices' ? 'Office' :
770 selectedIndustry === 'training' ? 'Training' :
771 selectedIndustry === 'showrooms' ? 'Showroom' :
772 selectedIndustry === 'tourism' ? 'Tourism' :
773 selectedIndustry === 'fitness' ? 'Fitness' :
774 selectedIndustry === 'museums' ? 'Museum' : 'Virtual'
775 ) + ' Tour',
776 industry: selectedIndustry,
777 security: wpvrNonce
778 },
779 success: function (response) {
780 if (response.success) {
781 // Store tour ID and URLs for later use
782 window.wpvrCreatedTourId = response.data.post_id;
783 window.wpvrCreatedTourUrl = response.data.view_url;
784 window.wpvrCreatedTourEditUrl = response.data.edit_url;
785
786 // Proceed to next step
787 context.goNext();
788 } else {
789 alert('Failed to create tour: ' + (response.data.message || 'Unknown error'));
790 $btn.prop('disabled', false).text(originalText);
791 }
792 },
793 error: function (xhr, status, error) {
794 console.error('Tour creation error:', error);
795 alert('Failed to create tour. Please try again.');
796 $btn.prop('disabled', false).text(originalText);
797 }
798 });
799 });
800
801 // Bind skip button
802 $('#skip-preview').off('click').on('click', function (e) {
803 e.preventDefault();
804 skipWizard();
805 });
806
807 // Initialize panorama viewer if image is uploaded
808 if (uploadedImageUrl) {
809 initPanoramaViewer();
810 } else {
811 $('#panorama-container').hide();
812 $('#preview-image-placeholder').show();
813 }
814 renderHotspotList();
815 }, 0);
816 }
817 },
818 {
819 id: 'success',
820 title: 'Publish',
821 description: 'Your property tour is live!',
822 canGoBack: true,
823 canSkip: false,
824 mount: (container, context) => {
825 navigateTo('step-success');
826 updateDynamicContent();
827
828 // Change pro badge color to #01B5FF on last step
829 setTimeout(() => {
830 $('.pro-badge').css({
831 'background': '#01B5FF',
832 'background-color': '#01B5FF'
833 });
834 }, 0);
835
836 // Automatically complete onboarding when user reaches the last step
837 setTimeout(() => {
838 // Complete the final step to trigger onboarding completion and firstStrike
839 context.completeStep();
840 }, 100);
841
842 setTimeout(() => {
843 // Bind Consent Checkbox
844 $('#consent-checkbox').off('change').on('change', function () {
845 const isChecked = $(this).is(':checked');
846 $.ajax({
847 url: ajaxurl,
848 type: 'POST',
849 data: {
850 action: 'wpvr_save_opt_in_toggle',
851 opt_in: isChecked ? 1 : 0,
852 security: wpvrNonce
853 },
854 success: function (response) {
855 console.log('Opt-in preference updated:', response);
856 }
857 });
858 });
859 // Fire it initially if checked (as it's default checked in template)
860 if ($('#consent-checkbox').is(':checked')) {
861 $('#consent-checkbox').trigger('change');
862 }
863
864 // Clear localStorage
865 if (typeof Storage !== 'undefined') {
866 localStorage.clear();
867 }
868
869 // Bind back button (restart)
870 $('.btn-back').off('click').on('click', () => {
871 location.reload();
872 });
873
874 // Update button text and show shortcode if tour is created
875 if (window.wpvrCreatedTourId) {
876 // Show shortcode instructions - insert before primary CTA
877 const shortcode = '[wpvr id="' + window.wpvrCreatedTourId + '"]';
878 const shortcodeHtml = '<div class="shortcode-instructions">' +
879 '<h4 class="shortcode-title">Use This Shortcode on Your Site</h4>' +
880 '<p class="shortcode-description">Copy this shortcode and paste it into any page, post, or widget area to display your virtual tour.</p>' +
881 '<div class="shortcode-box">' +
882 '<code id="tour-shortcode" class="shortcode-code">' + shortcode + '</code>' +
883 '<button id="copy-shortcode-btn" class="btn-copy-shortcode">Copy Shortcode</button>' +
884 '</div>' +
885 '</div>';
886
887 // Insert shortcode instructions before primary CTA container
888 if ($('.shortcode-instructions').length === 0) {
889 $('#step-success .primary-cta-container').before(shortcodeHtml);
890 }
891
892 // Copy shortcode functionality
893 $('#copy-shortcode-btn').off('click').on('click', function () {
894 const shortcodeText = $('#tour-shortcode').text();
895
896 // Modern clipboard API
897 if (navigator.clipboard && navigator.clipboard.writeText) {
898 navigator.clipboard.writeText(shortcodeText).then(function () {
899 const $btn = $(this);
900 const originalText = $btn.text();
901 $btn.text('Copied!').css('background', '#10B981');
902 setTimeout(function () {
903 $btn.text(originalText).css('background', '#E8E1FF');
904 }, 2000);
905 }.bind(this));
906 } else {
907 // Fallback for older browsers
908 const tempInput = $('<input>');
909 $('body').append(tempInput);
910 tempInput.val(shortcodeText).select();
911 document.execCommand('copy');
912 tempInput.remove();
913
914 const $btn = $(this);
915 const originalText = $btn.text();
916 $btn.text('Copied!').css('background', '#3F04FE');
917 setTimeout(function () {
918 $btn.text(originalText).css('background', '#E8E1FF');
919 }, 2000);
920 }
921 });
922 }
923
924 // Bind "Edit Your Tour" button - opens WP admin edit page
925 $('#edit-tour-btn').off('click.editTour').on('click.editTour', function (e) {
926 e.preventDefault();
927
928 // Complete onboarding first
929 if (context && typeof context.completeStep === 'function') {
930 context.completeStep();
931 }
932
933 // Navigate to edit tour page
934 if (window.wpvrCreatedTourId) {
935 const editUrl = ajaxurl.replace('admin-ajax.php', 'post.php?post=' + window.wpvrCreatedTourId + '&action=edit');
936 window.location.href = editUrl;
937 } else if (window.wpvrCreatedTourEditUrl) {
938 window.location.href = window.wpvrCreatedTourEditUrl;
939 }
940 });
941
942 // Bind skip button
943 $('#skip-success').off('click').on('click', function (e) {
944 e.preventDefault();
945 skipWizard();
946 });
947
948 // Bind footer "FINISH" button - go to tour listing
949 $('#step-success .btn-primary-outline').off('click.finish').on('click.finish', function (e) {
950 e.preventDefault();
951
952 // Complete onboarding
953 if (context && typeof context.completeStep === 'function') {
954 context.completeStep();
955 }
956
957 // Navigate to tour listing page
958 const listingUrl = ajaxurl.replace('admin-ajax.php', 'edit.php?post_type=wpvr_item');
959 window.location.href = listingUrl;
960 });
961 }, 0);
962 }
963 }
964 ],
965 firstStrike: {
966 label: 'WP VR Onboarding Completed',
967 verify: () => {
968 console.log('First Strike verified! Onboarding completed.');
969 return true;
970 }
971 }
972 });
973
974 // Start onboarding
975 engine.start();
976
977 // Listen to step changes and update UI
978 tracker.on('step_changed', (data) => {
979 const currentStep = engine.getCurrentStep();
980 if (currentStep && currentStep.mount) {
981 const container = document.getElementById('onboarding-app');
982 if (container) {
983 currentStep.mount(container, engine.getStepContext());
984 }
985 }
986 });
987
988 tracker.on('step_completed', (data) => {
989 console.log('Step completed:', data);
990 });
991
992 tracker.on('onboarding_completed', (data) => {
993 console.log('Onboarding completed!', data);
994 // Celebration is already handled in the success step mount
995 // Note: Setup completed is now handled in telemetry.onSetupCompleted callback
996 });
997
998 tracker.on('first_strike_verified', (data) => {
999 console.log('First Strike verified!', data);
1000 // firstStrike verification is complete
1001 });
1002
1003 // Initial render - show welcome step
1004 const currentStep = engine.getCurrentStep();
1005 if (currentStep && currentStep.mount) {
1006 const container = document.getElementById('onboarding-app');
1007 if (container) {
1008 currentStep.mount(container, engine.getStepContext());
1009 }
1010 }
1011
1012 // Initialize Panorama Viewer
1013 function initPanoramaViewer() {
1014 // Hide placeholder, show panorama container
1015 $('#preview-image-placeholder').hide();
1016 $('#panorama-container').show();
1017
1018 // Destroy existing viewer if any
1019 if (panoramaViewer) {
1020 try {
1021 panoramaViewer.destroy();
1022 } catch (e) {
1023 console.log('Viewer already destroyed');
1024 }
1025 }
1026
1027 // Initialize pannellum viewer
1028 const config = {
1029 type: "equirectangular",
1030 panorama: uploadedImageUrl,
1031 autoLoad: true,
1032 showControls: true,
1033 hotSpotDebug: false,
1034 hotSpots: hotspots
1035 };
1036
1037 panoramaViewer = pannellum.viewer('panorama', config);
1038
1039 // Remove existing 'load' listeners to avoid duplicates
1040 panoramaViewer.off('load');
1041
1042 // Use Pannellum's load event instead of setTimeout for reliable handler attachment
1043 panoramaViewer.on('load', function () {
1044 // Get both the container and canvas elements
1045 const panoramaElement = document.getElementById('panorama');
1046 const canvas = document.querySelector('#panorama canvas');
1047
1048 if (!panoramaElement) {
1049 return;
1050 }
1051
1052 // Remove existing listeners from both elements
1053 if (canvas) {
1054 canvas.removeEventListener('mousedown', handlePanoramaMouseDown, true);
1055 canvas.removeEventListener('mousemove', handlePanoramaMouseMove, true);
1056 canvas.removeEventListener('mouseup', handlePanoramaMouseUp, true);
1057 }
1058
1059 panoramaElement.removeEventListener('mousedown', handlePanoramaMouseDown, true);
1060 panoramaElement.removeEventListener('mousemove', handlePanoramaMouseMove, true);
1061 panoramaElement.removeEventListener('mouseup', handlePanoramaMouseUp, true);
1062
1063 // Attach to the panorama container with capture phase
1064 // This will catch events before Pannellum's handlers
1065 panoramaElement.addEventListener('mousedown', handlePanoramaMouseDown, true);
1066 panoramaElement.addEventListener('mousemove', handlePanoramaMouseMove, true);
1067 panoramaElement.addEventListener('mouseup', handlePanoramaMouseUp, true);
1068 });
1069 }
1070
1071 // Handle panorama mousedown - track if it's a click or drag
1072 function handlePanoramaMouseDown(e) {
1073 // Only handle left clicks
1074 if (e.button !== 0) {
1075 return;
1076 }
1077
1078 // Ignore clicks on Pannellum UI control buttons (zoom, fullscreen, etc.)
1079 // But NOT pnlm-dragfix which is the main dragging overlay
1080 if (e.target.closest('.pnlm-zoom-in, .pnlm-zoom-out, .pnlm-fullscreen, .pnlm-fullscreen-toggle-button, .pnlm-load-button, .pnlm-about-msg, .pnlm-compass, .pnlm-orientation')) {
1081 return;
1082 }
1083
1084 // Check if clicking on an existing hotspot
1085 const hotspotElement = e.target.closest('.pnlm-hotspot');
1086 if (hotspotElement) {
1087 return;
1088 }
1089
1090 // Verify panoramaViewer exists and has required methods
1091 if (!panoramaViewer) {
1092 return;
1093 }
1094
1095 // Use Pannellum's built-in projection math to get accurate pitch/yaw
1096 // This handles all edge cases, HFOV variations, and aspect ratios correctly
1097 let clickPitch, clickYaw;
1098
1099 if (typeof panoramaViewer.mouseEventToCoords === 'function') {
1100 const coords = panoramaViewer.mouseEventToCoords(e);
1101 if (coords && coords.length === 2) {
1102 clickPitch = coords[0];
1103 clickYaw = coords[1];
1104 } else {
1105 return;
1106 }
1107 } else {
1108 // Fallback to manual calculation if mouseEventToCoords is not available
1109 if (typeof panoramaViewer.getPitch !== 'function') {
1110 return;
1111 }
1112
1113 const currentPitch = panoramaViewer.getPitch();
1114 const currentYaw = panoramaViewer.getYaw();
1115 const currentHfov = panoramaViewer.getHfov();
1116
1117 const canvas = document.querySelector('#panorama canvas');
1118 if (!canvas) {
1119 return;
1120 }
1121
1122 const rect = canvas.getBoundingClientRect();
1123 const canvasWidth = rect.width;
1124 const canvasHeight = rect.height;
1125
1126 const mouseX = e.clientX - rect.left;
1127 const mouseY = e.clientY - rect.top;
1128 const normalizedX = (mouseX / canvasWidth - 0.5) * 2;
1129 const normalizedY = (mouseY / canvasHeight - 0.5) * 2;
1130
1131 const vfov = currentHfov * (canvasHeight / canvasWidth);
1132 const yawOffset = normalizedX * (currentHfov / 2);
1133 const pitchOffset = -normalizedY * (vfov / 2);
1134
1135 clickPitch = currentPitch + pitchOffset;
1136 clickYaw = currentYaw + yawOffset;
1137 }
1138
1139 // Store mouse down position, time, and calculated coordinates
1140 panoramaMouseDown = {
1141 x: e.clientX,
1142 y: e.clientY,
1143 time: Date.now(),
1144 pitch: clickPitch,
1145 yaw: clickYaw
1146 };
1147 panoramaIsDragging = false;
1148
1149 // Don't prevent default or stop propagation - let Pannellum handle dragging
1150 }
1151
1152 // Handle panorama mousemove - detect if dragging
1153 function handlePanoramaMouseMove(e) {
1154 if (panoramaMouseDown) {
1155 const deltaX = Math.abs(e.clientX - panoramaMouseDown.x);
1156 const deltaY = Math.abs(e.clientY - panoramaMouseDown.y);
1157 const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
1158
1159 // If mouse moved more than 5 pixels, it's a drag
1160 if (distance > 5 && !panoramaIsDragging) {
1161 panoramaIsDragging = true;
1162 }
1163 }
1164 }
1165
1166 // Handle panorama mouseup - only show hotspot if it was a click, not a drag
1167 function handlePanoramaMouseUp(e) {
1168 if (!panoramaMouseDown) return;
1169
1170 // Only handle left clicks
1171 if (e.button !== 0) {
1172 panoramaMouseDown = null;
1173 return;
1174 }
1175
1176 // Ignore clicks on Pannellum UI control buttons (zoom, fullscreen, etc.)
1177 // But NOT pnlm-dragfix which is the main dragging overlay
1178 if (e.target.closest('.pnlm-zoom-in, .pnlm-zoom-out, .pnlm-fullscreen, .pnlm-fullscreen-toggle-button, .pnlm-load-button, .pnlm-about-msg, .pnlm-compass, .pnlm-orientation')) {
1179 panoramaMouseDown = null;
1180 panoramaIsDragging = false;
1181 return;
1182 }
1183
1184 // Check if clicking on an existing hotspot
1185 if (e.target.closest('.pnlm-hotspot')) {
1186 panoramaMouseDown = null;
1187 panoramaIsDragging = false;
1188 return;
1189 }
1190
1191 // Only show hotspot modal if it was a click (not a drag)
1192 if (!panoramaIsDragging) {
1193 const timeDiff = Date.now() - panoramaMouseDown.time;
1194
1195 // Also check time - if mouse was held for less than 300ms, it's likely a click
1196 if (timeDiff < 300) {
1197 // Use the pitch and yaw captured at mousedown (before any potential drag)
1198 const pitch = panoramaMouseDown.pitch;
1199 const yaw = panoramaMouseDown.yaw;
1200
1201 // Store coordinates for hotspot creation
1202 pendingHotspotCoords = { pitch, yaw };
1203
1204 // Enforce free-version hotspot limit before opening modal
1205 if (!isPro && hotspots.length >= HOTSPOT_LIMIT) {
1206 showHotspotLimitNotice();
1207 pendingHotspotCoords = null;
1208 return;
1209 }
1210
1211 // Show hotspot modal
1212 showHotspotModal();
1213 }
1214 }
1215
1216 // Reset tracking
1217 panoramaMouseDown = null;
1218 panoramaIsDragging = false;
1219 }
1220
1221 // Show hotspot modal
1222 function showHotspotModal() {
1223 $('#hotspot-modal').fadeIn(200);
1224 $('#hotspot-text').val('').focus();
1225 }
1226
1227 // Hide hotspot modal
1228 function hideHotspotModal() {
1229 $('#hotspot-modal').fadeOut(200);
1230 $('#hotspot-text').val('');
1231 pendingHotspotCoords = null;
1232 }
1233
1234 // Show the premium upgrade popup when hotspot limit is reached
1235 function showHotspotLimitNotice() {
1236 var $popup = $('#wpvr_premium_feature_popup');
1237 if (!$popup.length) {
1238 return;
1239 }
1240
1241 // Swap in hotspot-limit-specific text
1242 var $heading = $popup.find('.wpvr-premium-feature__heading');
1243 var $subheading = $popup.find('.wpvr-premium-feature__subheading');
1244 $heading.data('original-text', $heading.text());
1245 $subheading.data('original-html', $subheading.html());
1246 $heading.text(wizardStrings.limit_heading || 'Limit Reached');
1247 $subheading.html(
1248 '<p>' + (wizardStrings.limit_line1 || 'You can add up to ' + HOTSPOT_LIMIT + ' hotspots on each scene in the Free version.') + '</p>' +
1249 '<p>' + (wizardStrings.limit_line2 || 'Upgrade to Pro to add an unlimited number of hotspots.') + '</p>'
1250 );
1251
1252 // Apply the discount label CSS custom property
1253 var $label = $popup.find('.wpvr-premium-feature__discount-price-label');
1254 if ($label.length) {
1255 $label[0].style.setProperty('--discount-content-value', '"' + ($label.data('discount') || '') + '"');
1256 }
1257 $popup.show();
1258 }
1259
1260 // Hide the premium upgrade popup and restore original heading text
1261 function hideHotspotLimitNotice() {
1262 var $popup = $('#wpvr_premium_feature_popup');
1263 var $heading = $popup.find('.wpvr-premium-feature__heading');
1264 var $subheading = $popup.find('.wpvr-premium-feature__subheading');
1265 if ($heading.data('original-text')) {
1266 $heading.text($heading.data('original-text'));
1267 }
1268 if ($subheading.data('original-html')) {
1269 $subheading.html($subheading.data('original-html'));
1270 }
1271 $popup.hide();
1272 }
1273
1274 // Save hotspot
1275 function saveHotspot() {
1276 const text = $('#hotspot-text').val().trim();
1277
1278 if (!isPro && hotspots.length >= HOTSPOT_LIMIT) {
1279 hideHotspotModal();
1280 showHotspotLimitNotice();
1281 return;
1282 }
1283
1284 if (!text) {
1285 alert('Please enter hotspot text');
1286 return;
1287 }
1288
1289 if (!pendingHotspotCoords) {
1290 alert('Error: No coordinates available');
1291 return;
1292 }
1293
1294 // Create hotspot object
1295 const hotspot = {
1296 pitch: pendingHotspotCoords.pitch,
1297 yaw: pendingHotspotCoords.yaw,
1298 type: "info",
1299 text: text
1300 };
1301
1302 // Add to hotspots array
1303 hotspots.push(hotspot);
1304
1305 // Reinitialize viewer with updated hotspots
1306 initPanoramaViewer();
1307
1308 // Update hotspot list
1309 renderHotspotList();
1310
1311 // Hide modal
1312 hideHotspotModal();
1313 }
1314
1315 // Render hotspot list below panorama with remove buttons
1316 function renderHotspotList() {
1317 const $list = $('#hotspot-list');
1318 if (!$list.length) return;
1319
1320 $list.empty();
1321
1322 if (hotspots.length === 0) {
1323 return;
1324 }
1325
1326 const $label = $('<p class="hotspot-list-label">Hotspot</p>');
1327 const $items = $('<ul class="hotspot-items"></ul>');
1328 hotspots.forEach(function (hotspot, index) {
1329 const escapedText = $('<span>').text(hotspot.text).html();
1330 const $item = $('<li class="hotspot-item">' +
1331 '<span class="hotspot-item-text">' + escapedText + '</span>' +
1332 '<button class="hotspot-item-remove" data-index="' + index + '" title="Remove hotspot" aria-label="Remove hotspot">&times;</button>' +
1333 '</li>');
1334 $items.append($item);
1335 });
1336 $list.append($label);
1337 $list.append($items);
1338
1339 $list.find('.hotspot-item-remove').off('click').on('click', function () {
1340 const index = parseInt($(this).data('index'), 10);
1341 hotspots.splice(index, 1);
1342 initPanoramaViewer();
1343 renderHotspotList();
1344 });
1345 }
1346
1347 // Bind hotspot modal handlers
1348 $(document).ready(function () {
1349 $('#hotspot-save').off('click').on('click', saveHotspot);
1350 $('#hotspot-cancel, #hotspot-modal-close').off('click').on('click', hideHotspotModal);
1351
1352 // Close the premium popup via the built-in close button
1353 $(document).off('click.limitDismiss').on('click.limitDismiss', '#wpvr_premium_feature_close', function () {
1354 hideHotspotLimitNotice();
1355 });
1356
1357 // Close on clicking outside modal
1358 $('#hotspot-modal').off('click').on('click', function (e) {
1359 if ($(e.target).is('#hotspot-modal')) {
1360 hideHotspotModal();
1361 }
1362 });
1363
1364 // Prevent modal content clicks from closing modal
1365 $('.hotspot-modal-content').off('click').on('click', function (e) {
1366 e.stopPropagation();
1367 });
1368
1369 // Close on Escape key
1370 $(document).off('keydown.hotspot').on('keydown.hotspot', function (e) {
1371 if (e.key === 'Escape' && $('#hotspot-modal').is(':visible')) {
1372 hideHotspotModal();
1373 }
1374 });
1375
1376 // Submit on Enter key
1377 $('#hotspot-text').off('keypress').on('keypress', function (e) {
1378 if (e.which === 13) {
1379 e.preventDefault();
1380 saveHotspot();
1381 }
1382 });
1383 });
1384 });
1385