1/** 2 * @namespace WPGMZA 3 * @module Tour 4 * @requires WPGMZA 5 */ 6 7jQuery(function($) { 8 /** 9 * Constructor 10 * 11 * @param Element element The wrapper element 12 */ 13 WPGMZA.Tour = function(element){ 14 this.findElements(element); 15 this.bindEvents(); 16 17 this.prepare(); 18 setTimeout(() => { 19 this.prompt(); 20 }, 3000); 21 } 22 23 WPGMZA.Tour.INPUT_CHANGE_INTERVAL = 1500; 24 25 /** 26 * Create a single tour instance 27 * 28 * @param Element element The wrapper element 29 * @return WPGMZA.Tour 30 */ 31 WPGMZA.Tour.createInstance = function(element){ 32 return new WPGMZA.Tour(element); 33 } 34 35 /** 36 * Static auto init of all tours 37 * 38 * @return void 39 */ 40 WPGMZA.Tour.AutoInit = function(){ 41 WPGMZA.adminTours = {}; 42 $(document.body).find('.wpgmza-tour').each(function(index, element){ 43 const type = $(element).data('type'); 44 WPGMZA.adminTours[type] = WPGMZA.Tour.createInstance(element); 45 }); 46 47 if($(document.body).find('[data-wpgmza-ftu]').length > 0){ 48 const ftuTag = $(document.body).find('[data-wpgmza-ftu]').data('wpgmza-ftu'); 49 if(ftuTag){ 50 /* We have a first time usage trigger */ 51 switch(ftuTag){ 52 case 'ftu.trigger.markercreator': 53 /* Trigger a click of the marker creator */ 54 $(document.body).on('markersplaced.wpgmza', () => { 55 $('.wpgmza-editor .item[data-group="map-markers-editor"]').click(); 56 }); 57 break; 58 } 59 } 60 } 61 } 62 63 /** 64 * Prepare the tour based on the elements we have in this instance 65 * 66 * This finds the max steps, and then also sets the start, while initializing a state tracker 67 * 68 * @return void 69 */ 70 WPGMZA.Tour.prototype.prepare = function(){ 71 this.slug = this.elements.wrapper.data('type'); 72 73 this.state = { 74 running : false, 75 step : 0, 76 steps : this.elements.steps.length || 0 77 } 78 } 79 80 /** 81 * Find all relelvant tour elements 82 * 83 * @param Element wrapper The tour wrapper element 84 * 85 * @return void 86 */ 87 WPGMZA.Tour.prototype.findElements = function(wrapper){ 88 this.elements = {}; 89 this.elements.wrapper = $(wrapper); 90 91 this.elements.prompt = this.elements.wrapper.find('.wpgmza-tour-prompt'); 92 this.elements.promptAction = this.elements.prompt.find('.wpgmza-tour-prompt-actions .wpgmza-button'); 93 94 this.elements.steps = this.elements.wrapper.find('.wpgmza-tour-step'); 95 } 96 97 /** 98 * Bind events for the tour within the system 99 * 100 * This is more so for the main tour handlers, and less on the anchors 101 * 102 * @return void 103 */ 104 WPGMZA.Tour.prototype.bindEvents = function(){ 105 /* Prebound events */ 106 this.elements.promptAction.on('click', (event) => { 107 if(event && event.currentTarget){ 108 this.onPromptAction(event.currentTarget); 109 } 110 }); 111 112 /* Nested events */ 113 this.elements.steps.each((i, elem) => { 114 const anchor = $(elem).data('anchor'); 115 if(anchor){ 116 $(anchor).addClass('wpgmza-tour-anchor-link'); 117 } 118 }); 119 120 /* Global events, dynamically bounds to body, filers to class */ 121 $(document.body).on('click', '.wpgmza-tour-next-step-delegate', (event) => { 122 if(event.currentTarget instanceof HTMLInputElement){ 123 /* This is an input, let the input delegate handler sort it */ 124 return; 125 } 126 127 if($(event.currentTarget).data('auto-step')){ 128 /* This step is automatically handled by the system, no user interaction required */ 129 event.preventDefault(); 130 return; 131 } 132 133 this.next(); 134 }); 135 136 $(document.body).on('keyup', '.wpgmza-tour-next-step-delegate', (event) => { 137 if(event.currentTarget instanceof HTMLInputElement){ 138 if(event.currentTarget._wpgmzaChangeTimer){ 139 clearTimeout(event.currentTarget._wpgmzaChangeTimer); 140 } 141 142 /* Timeouts should allow for a few characters to be typed before continuing to the next step */ 143 event.currentTarget._wpgmzaChangeTimer = setTimeout(() => { 144 this.next(); 145 }, WPGMZA.Tour.INPUT_CHANGE_INTERVAL); 146 } 147 }); 148 149 $(document.body).on('click', '.wpgmza-tour-anchor-link',
149(event) => { 150 if(!this.state.running){ 151 /* The user clicked on a tour item before starting the tour, let's dismiss it early */ 152 this.stop(); 153 154 /* We dismiss it as a short term dismissal, this allows it to reload in a few days ideally */ 155 this.dismiss(true); 156 } 157 }); 158 159 $(document.body).on('click', (event) => { 160 this.onFramedClick(event); 161 }); 162 } 163 164 /** 165 * On prompt action taen 166 * 167 * @param Element context The button that triggered this event 168 */ 169 WPGMZA.Tour.prototype.onPromptAction = function(context){ 170 if(context instanceof Element){ 171 const action = $(context).data('action'); 172 switch(action){ 173 case 'start': 174 this.state.running = true; 175 this.step(0); 176 break; 177 default: 178 this.stop(); 179 this.dismiss(); 180 break; 181 } 182 183 } 184 } 185 186 /** 187 * On click while framed, we assume framed, but this method will actually check if the frame is active, and do the things 188 * 189 * @param Event event The event that triggered this element 190 * 191 * @return void 192 */ 193 WPGMZA.Tour.prototype.onFramedClick = function(event) { 194 if(this.elements.frame && this.elements.frame.hasClass('active')){ 195 if(!jQuery.contains(this.elements.wrapper.get(0), event.target)){ 196 /* The element is not a part of the tour, that's for sure, but lets make sure it's not a delegate */ 197 if(!$(event.target).hasClass('wpgmza-tour-next-step-delegate') && !$(event.target).hasClass('wpgmza-tour-anchor-link')){ 198 199 /* Finally, because we don't actually fully bind to elements on an individual level, we must run a boundary comparison on the frame */ 200 const boundary = Object.assign({}, this._lastFramePlacement); 201 const pointerEvent = event.originalEvent || false; 202 if(boundary.top && boundary.left && pointerEvent && pointerEvent instanceof PointerEvent){ 203 boundary.right = boundary.left + boundary.width; 204 boundary.bottom = boundary.top + boundary.height; 205 206 const mouse = { 207 x : pointerEvent.clientX, 208 y : pointerEvent.clientY 209 }; 210 211 let shouldDismiss = false; 212 if(mouse.x < boundary.left || mouse.x > boundary.right){ 213 /* User is either too far left or too far right */ 214 shouldDismiss = true; 215 } 216 217 if(mouse.y < boundary.top || mouse.y > boundary.bottom){ 218 /* User is either too high or too low */ 219 shouldDismiss = true; 220 } 221 222 if(shouldDismiss){ 223 this.stop(); 224 this.dismiss(true); 225 } 226 } else { 227 /* Assume but ya - Short term dismissal*/ 228 this.stop(); 229 this.dismiss(true); 230 } 231 } 232 } 233 } 234 } 235 236 /** 237 * Load the prompt popup to either start or dismiss the tour 238 * 239 * This also sets the default step to the start so that everything flows correctly 240 * 241 * @return void 242 */ 243 WPGMZA.Tour.prototype.prompt = function(){ 244 this.state.running = false; 245 this.state.step = 0; 246 247 this.elements.steps.removeClass('active'); 248 this.elements.prompt.addClass('active'); 249 } 250 251 /** 252 * Stop the tour 253 * 254 * @return void 255 */ 256 WPGMZA.Tour.prototype.stop = function(){ 257 this.clearViewport(); 258 259 this.elements.prompt.removeClass('active'); 260 this.elements.steps.removeClass('active'); 261 } 262 263 /** 264 * Load a specific step in the tour 265 * 266 * This will hide prompt, and any open steps 267 * 268 * @param int index The step index you want to load up 269 * 270 * @return void 271 */ 272 WPGMZA.Tour.prototype.step = function(index){ 273 if(this.state.running){
274 this.state.step = index; 275 276 this.elements.prompt.removeClass('active'); 277 this.elements.steps.removeClass('active'); 278 279 /* Unbind delegates */ 280 $('.wpgmza-tour-next-step-delegate').removeClass('wpgmza-tour-next-step-delegate'); 281 282 if(this.elements.steps[this.state.step]){ 283 const stepElement = $(this.elements.steps[this.state.step]); 284 const anchor = stepElement.data('anchor'); 285 286 this.frame(anchor); 287 288 if(this._lastFramePlacement){ 289 290 stepElement.addClass('active'); 291 292 stepElement.css({ 293 left : (this._lastFramePlacement.left + this._lastFramePlacement.width) + 'px', 294 top : ((this._lastFramePlacement.top + (this._lastFramePlacement.height / 2)) - (stepElement.outerHeight() / 2)) + 'px' 295 }); 296 297 } 298 299 /* Bind delegates */ 300 $(anchor).addClass('wpgmza-tour-next-step-delegate'); 301 302 if(this._lastAutoStepTimer){ 303 clearTimeout(this._lastAutoStepTimer); 304 } 305 if(stepElement.data('auto-step')){ 306 this._lastAutoStepTimer = setTimeout(() => { 307 this.next(); 308 }, parseInt(stepElement.data('auto-step'))); 309 } 310 } 311 } 312 } 313 314 /** 315 * Move to the next step 316 * 317 * This would be driven by the anchor being clicked/interacted with on screen, perhaps also by a next button, but we will see 318 * 319 * @return void 320 */ 321 WPGMZA.Tour.prototype.next = function(){ 322 if(this.state.running){ 323 this.clearViewport(); 324 325 let nextStep = this.state.step + 1; 326 if(nextStep < this.state.steps){ 327 /* We can step */ 328 let delay = this.getStepDelay(nextStep); 329 if(delay){ 330 /* This has a delay */ 331 setTimeout(() => { 332 this.step(nextStep); 333 }, delay); 334 } else { 335 /* Run it */ 336 this.step(nextStep); 337 } 338 } else { 339 this.complete(); 340 } 341 } 342 } 343 344 /** 345 * Frame the anchor with a shadow box 346 * 347 * @param string anchor The anchor selector 348 * 349 * @return void 350 */ 351 WPGMZA.Tour.prototype.frame = function(anchor){ 352 if(!this.elements.frame){ 353 this.elements.frame = $("<div class='wpgmza-tour-frame'></div>"); 354 this.elements.frame.appendTo(this.elements.wrapper); 355 } 356 357 this._lastFramePlacement = false; 358 this.elements.frame.removeClass('active'); 359 360 const anchorElement = document.querySelector(anchor); 361 if(anchorElement){ 362 const anchorRect = anchorElement.getBoundingClientRect(); 363 const computedStyles = window.getComputedStyle(anchorElement, null); 364 365 const frameStyle = { 366 top : parseInt(anchorRect.top), 367 left : parseInt(anchorRect.left), 368 width : parseInt(anchorRect.width), 369 height : parseInt(anchorRect.height), 370 borderRadius : 0 371 }; 372 373 this.elements.frame.css('--wpgmza-tour-frame-border-radius', '0px'); 374 if(parseInt(computedStyles['border-radius'])){ 375 frameStyle.borderRadius = parseInt(computedStyles['border-radius']); 376 this.elements.frame.css('--wpgmza-tour-frame-border-radius', parseInt(computedStyles['border-radius']) + 'px'); 377 } 378 379 this._lastFramePlacement = Object.assign({}, frameStyle); 380 381 for(let i in frameStyle){ 382 frameStyle[i] += "px"; 383 } 384 385 this.elements.frame.css(frameStyle); 386 this.elements.frame.addClass('active'); 387 } 388 } 389 390 /** 391 * Clears the active viewport in full, of frames and popups 392 * 393 * @return void 394 */ 395 WPGMZA.Tour.prototype.clearViewport = function(){ 396 if(this.elements.frame && this.elements.frame.hasClass('active')){ 397 this.elements.frame.removeClass('active'); 398 } 399 400 this.elements.steps.removeClass('active'); 401 } 402 403 /** 404 * Get and check if a step has a delay associated with it 405 * 406 * @param int index The step you want to get the delay for 407 * 408 * @return int 409 */ 410 WPGMZA.Tour.prototype.getStepDelay = function(index){ 411 if($(this.elements.steps[index]).data('step-delay')){ 412 return parseInt($(this.elements.steps[index]).data('step-delay')); 413 } 414 return 0; 415 } 416 417 /** 418 * Dismiss the tour 419 * 420 * This sends a request to server-side to actually flag this tour as dismissed, you could allow a tour to be dismissed for a few days, or permanently 421 * 422 * @param bool short Should this be a short term dismissal, or is it permanent 423 * 424 * @return void 425 */ 426 WPGMZA.Tour.prototype.dismiss = function(short){ 427 if(this.state.complete){ 428 /* It's already been completed */ 429 return true; 430 } 431 432 short = short ? true : false;
433 const data = { 434 action : 'wpgmza_tour_progress_update', 435 wpgmza_security : WPGMZA.ajaxnonce, 436 tour : this.slug, 437 type : short ? 'sleep' : 'dismiss' 438 }; 439 440 this.request(data, () => { 441 /* We don't need to do anything */ 442 }); 443 } 444 445 /** 446 * Complete a tour, this could store metadata about the tour, if needed, and trigger the next tour, if you wanted it to 447 * 448 * @return void 449 */ 450 WPGMZA.Tour.prototype.complete = function(){ 451 this.state.running = false; 452 this.state.complete = true; 453 454 const data = { 455 action : 'wpgmza_tour_progress_update', 456 wpgmza_security : WPGMZA.ajaxnonce, 457 tour : this.slug, 458 type : 'complete' 459 }; 460 461 this.request(data, () => { 462 /* We don't need to do anything */ 463 }); 464 } 465 466 /** 467 * Server side request, always sent via POST, but with any data you might need 468 * 469 * @param object data 470 * @param function complete 471 * 472 * @return void 473 */ 474 WPGMZA.Tour.prototype.request = function(data, complete){ 475 if(typeof complete !== 'function'){ 476 complete = () => {}; 477 } 478 479 $.ajax(WPGMZA.ajaxurl, { 480 method: "POST", 481 data: data, 482 success: function(response, status, xhr) { 483 complete(response); 484 }, 485 error : function(){ 486 complete(); 487 } 488 }); 489 } 490 491 /* Init */ 492 $(document).ready(function(event) { 493 if(WPGMZA.getCurrentPage()){ 494 WPGMZA.Tour.AutoInit(); 495 } 496 }); 497})
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.