1// JS that can be loaded on both single and multisku pages 2var isTestEnvironment = false; 3if (document.getElementById('jsTestEnvironment').value) { 4 isTestEnvironment = true; 5} 6 7$(function(){ 8 loadProductQuestions(); 9 overrideBreadcrumbToPreviousLocation(); 10 getWishlistStatus(); 11 getRequestAQuoteStatus(); 12 initMhProductGallery(); 13}); 14 15function overrideBreadcrumbToPreviousLocation() { 16 // replace the breadcrumb to the search results page or collection if the user came from there 17} 18 19// Shared price-rule scroll-snap helpers. Both the single-SKU and multi-SKU pages drive the same 20// #js-product-price-rule-nav strip, so these low-level DOM helpers live once here. 21function scrollPriceRuleIntoView(index) { 22 var tile = $('#js-product-price-rule-nav').children().eq(index)[0]; 23 if (tile && tile.scrollIntoView) { 24 tile.scrollIntoView({behavior: 'smooth', block: 'nearest', inline: 'nearest'}); 25 } 26} 27 28function scrollPriceRuleNavBy(nav, dir) { 29 if (!nav) { 30 return; 31 } 32 nav.scrollBy({left: dir * nav.clientWidth * 0.8, behavior: 'smooth'}); 33} 34 35function getPriceRuleArrowState(nav) { 36 if (!nav) { 37 return {hasOverflow: false, prevDisabled: true, nextDisabled: true}; 38 } 39 return { 40 hasOverflow: nav.scrollWidth > nav.clientWidth, 41 prevDisabled: nav.scrollLeft <= 0, 42 nextDisabled: nav.scrollLeft + nav.clientWidth >= nav.scrollWidth - 1 43 }; 44} 45 46function enablePriceRuleDragScroll(nav) { 47 // Mouse drag-to-scroll for the strip; touch scrolling is native. Idempotent per element: the nav 48 // is re-created when the multi-SKU v-if flips, so repeat calls from a refresh path are free. 49 if (!nav || nav.mhPriceRuleDragBound) { 50 return; 51 } 52 nav.mhPriceRuleDragBound = true; 53 54 var startX = 0; 55 var startScrollLeft = 0; 56 var dragged = false; 57 58 function onMouseMove(e) { 59 if (e.buttons === 0) { 60 // mouseup landed outside the window and never reached us; end the drag 61 onMouseUp(); 62 return; 63 } 64 var dx = e.pageX - startX; 65 if (!dragged && Math.abs(dx) < 5) { 66 return; // ignore jitter so plain tile clicks still register 67 } 68 if (!dragged) { 69 dragged = true; 70 // Kill snap + smooth scrolling while dragging so scrollLeft writes track the pointer 1:1. 71 nav.classList.add('product-price-rule-nav-dragging'); 72 } 73 e.preventDefault(); 74 nav.scrollLeft = startScrollLeft - dx; 75 } 76 77 function onMouseUp() { 78 document.removeEventListener('mousemove', onMouseMove); 79 document.removeEventListener('mouseup', onMouseUp); 80 nav.classList.remove('product-price-rule-nav-dragging'); 81 // leave `dragged` set: the click that follows mouseup is suppressed below 82 } 83 84 nav.addEventListener('mousedown', function(e) { 85 if (e.button !== 0) { 86 return; 87 } 88 startX = e.pageX; 89 startScrollLeft = nav.scrollLeft; 90 dragged = false; 91 document.addEventListener('mousemove', onMouseMove); 92 document.addEventListener('mouseup', onMouseUp); 93 }); 94 95 // Capture-phase so a drag never turns into a tile selection on either page. 96 nav.addEventListener('click', function(e) { 97 if (dragged) { 98 e.stopPropagation(); 99 e.preventDefault(); 100 dragged = false; 101 } 102 }, true); 103} 104 105// Pagination dot strips show a window of dots (CSS max-width: seven) rather than a row per seven 106// images. The strip is overflow: hidden with one dot per image inside, so this slides it by 107// scrollLeft to keep the active dot inside the window, one dot in from either edge, and marks a dot 108// on an edge that has more beyond it (.is-edge shrinks it) as the cue that the strip goes on. 109function syncGalleryDotStrip(strip, index) { 110 var dots = strip.children; 111 var count = dots.length; 112 var dotWidth = count ? dots[0].offsetWidth : 0; 113 if (!dotWidth) { 114 return; // display: none (the thumb rail is showing): nothing to measure or line up 115 } 116 var visible = Math.round(strip.clientWidth / dotWidth); 117 var maxStart = Math.max(0, count - visible); 118 var start = Math.round(strip.scrollLeft / dotWidth); 119 if (index < start + 1) { 120 start = index - 1; 121 } else if (index > start + visible - 2) { 122 start = index - visible + 2; 123 } 124 start = Math.max(0, Math.min(maxStart, start)); 125 strip.scrollLeft = start * dotWidth; 126 for (var i = 0; i < count; i++) { 127 var isEdge = (i === start && start > 0) || (i === start + visible - 1 && start < maxStart); 128 dots[i].classList.toggle('is-edge', isEdge); 129 } 130} 131 132// Product image gallery: a horizontal scroll-snap stage with a static thumb rail beside it. Both 133// product pages share the markup, so the controller lives here. Pages still served from cache have
134// the old markup and none of these ids, so the gate below makes this a clean no-op there. 135function initMhProductGallery() { 136 var stage = document.getElementById('jsMhGalleryStage'); 137 if (!stage) { 138 return; 139 } 140 141 var activeIndex = -1; 142 var slides = []; 143 var observer = null; 144 var ratios = {}; 145 146 function setActive(index) { 147 if (index === activeIndex) { 148 return; 149 } 150 activeIndex = index; 151 152 // The rail is capped at 6 tiles, so any slide at or past the cap lights the last ("+N") tile. 153 var thumbs = document.querySelectorAll('.mh--gallery-thumb'); 154 if (thumbs.length) { 155 var target = Math.min(index, thumbs.length - 1); 156 for (var i = 0; i < thumbs.length; i++) { 157 if (i === target) { 158 thumbs[i].classList.add('is-active'); 159 thumbs[i].setAttribute('aria-current', 'true'); 160 } else { 161 thumbs[i].classList.remove('is-active'); 162 thumbs[i].removeAttribute('aria-current'); 163 } 164 } 165 } 166 167 // The dots stand in for the rail on narrow viewports (CSS swaps them), so both track the slide. 168 var dots = document.querySelectorAll('.mh--gallery-dot'); 169 for (var d = 0; d < dots.length; d++) { 170 if (d === index) { 171 dots[d].classList.add('is-active'); 172 dots[d].setAttribute('aria-current', 'true'); 173 } else { 174 dots[d].classList.remove('is-active'); 175 dots[d].removeAttribute('aria-current'); 176 } 177 } 178 var strip = document.getElementById('jsMhGalleryDots'); 179 if (strip) { 180 syncGalleryDotStrip(strip, index); 181 } 182 183 // The sale badge sits over the stage, so it only belongs on the first image. 184 var $badge = $('#product-page-badge'); 185 if ($badge.length) { 186 if (index === 0) { 187 $badge.fadeIn('fast'); 188 } else { 189 $badge.fadeOut('fast'); 190 } 191 } 192 } 193 194 function goTo(index, behavior) { 195 if (isNaN(index) || index < 0) { 196 return; 197 }
198 // Instant by default, not smooth: thumb hover previews would otherwise lag the pointer. 199 // 'instant' rather than 'auto': the stage carries scroll-behavior: smooth in CSS. 200 stage.scrollTo({left: index * stage.clientWidth, behavior: behavior || 'instant'}); 201 } 202 203 // The templates render the dots, so this normally finds them in place and leaves them alone. Pages 204 // still served from cache predate them: there the dots are built here, so the narrow layout is 205 // never left with neither thumbs nor dots. Re-run on every SKU switch, since the image count 206 // changes with the SKU on those cached pages too. 207 function syncDots() { 208 var count = stage.querySelectorAll('.mh--gallery-slide').length; 209 var dots = document.getElementById('jsMhGalleryDots'); 210 if (!dots) { 211 if (count < 2) { 212 return; 213 } 214 dots = document.createElement('div'); 215 dots.className = 'mh--gallery-dots'; 216 dots.id = 'jsMhGalleryDots'; 217 // Directly after the stage: where the templates put it, and outside the v-for lists Vue patches. 218 stage.insertAdjacentElement('afterend', dots); 219 } 220 if (dots.querySelectorAll('.mh--gallery-dot').length === count) { 221 return; 222 } 223 dots.innerHTML = ''; 224 for (var i = 0; i < count; i++) { 225 var dot = document.createElement('button'); 226 dot.type = 'button'; 227 dot.className = 'mh--gallery-dot'; 228 dot.setAttribute('data-index', i); 229 dot.setAttribute('aria-label', 'View image ' + (i + 1) + ' of ' + count); 230 dots.appendChild(dot); 231 } 232 } 233 234 function observeSlides() { 235 // Re-read the slides every time: the multi-SKU page re-renders them whenever the SKU changes. 236 slides = []; 237 var slideNodes = stage.querySelectorAll('.mh--gallery-slide'); 238 for (var i = 0; i < slideNodes.length; i++) { 239 slides.push(slideNodes[i]); 240 } 241 if (typeof IntersectionObserver === 'undefined') { 242 return; 243 } 244 if (observer) { 245 observer.disconnect(); 246 } 247 ratios = {}; 248 observer = new IntersectionObserver(function (entries) { 249 entries.forEach(function (entry) { 250 var index = slides.indexOf(entry.target); 251 if (index === -1) { 252 return; 253 } 254 ratios[index] = entry.isIntersecting ? entry.intersectionRatio : 0; 255 }); 256 var best = -1; 257 var bestRatio = 0; 258 Object.keys(ratios).forEach(function (key) { 259 if (ratios[key] > bestRatio) { 260 bestRatio = ratios[key]; 261 best = parseInt(key, 10); 262 } 263 }); 264 // Once the stage is clamped at either end the centre band can no longer reach the first/last 265 // slide's middle, so pin those two positions. 266 if (stage.scrollLeft <= 1) { 267 best = 0; 268 } else if (stage.scrollLeft + stage.clientWidth >= stage.scrollWidth - 1) { 269 best = slides.length - 1; 270 } 271 if (best !== -1) { 272 setActive(best); 273 } 274 }, { 275 root: stage, 276 // Collapse the root to a thin band across its horizontal centre, so "active" means "the image 277 // crossing the middle", not "any visible". 278 rootMargin: '0px -45% 0px -45%', 279 threshold: 0 280 }); 281 slides.forEach(function (slide) { 282 observer.observe(slide); 283 }); 284 } 285 286 var startX = 0; 287 var startScrollLeft = 0; 288 var dragged = false; 289 290 function onMouseMove(e) { 291 if (e.buttons === 0) { 292 // mouseup landed outside the window and never reached us; end the drag 293 onMouseUp(); 294 return; 295 } 296 var dx = e.pageX - startX; 297 if (!dragged && Math.abs(dx) < 5) { 298 return; // ignore jitter so plain image clicks still register 299 } 300 if (!dragged) { 301 dragged = true; 302 // Kill snap + smooth scrolling while dragging so scrollLeft writes track the pointer 1:1. 303 stage.classList.add('mh--gallery-dragging'); 304 } 305 e.preventDefault(); 306 stage.scrollLeft = startScrollLeft - dx; 307 } 308 309 function onMouseUp() { 310 document.removeEventListener('mousemove', onMouseMove); 311 document.removeEventListener('mouseup', onMouseUp); 312 stage.classList.remove('mh--gallery-dragging'); 313 // leave `dragged` set: the click that follows mouseup is suppressed below 314 } 315 316 stage.addEventListener('mousedown', function(e) { 317 if (e.button !== 0) { 318 return; 319 } 320 startX = e.pageX; 321 startScrollLeft = stage.scrollLeft; 322 dragged = false; 323 document.addEventListener('mousemove', onMouseMove); 324 document.addEventListener('mouseup', onMouseUp); 325 }); 326 327 // Suppress native HTML5 image dragging so a mouse drag scrolls the stage. Unconditional: the 328 // browser starts the native drag at ~3px, before our own 5px slop threshold has flipped `dragged`. 329 stage.addEventListener('dragstart', function(e) { 330 e.preventDefault(); 331 }); 332 333 // Capture-phase so a drag never opens the lightbox. 334 stage.addEventListener('click', function(e) { 335 if (dragged) { 336 e.stopPropagation(); 337 e.preventDefault(); 338 dragged = false;
339 } 340 }, true); 341 342 // Delegated from the document because the multi-SKU page re-renders the rail. The "+N" tile is 343 // deliberately excluded: the lightbox owns that one. 344 $(document).on('mouseenter click', '.mh--gallery-thumb:not(.mh--gallery-thumb-more)', function() { 345 var index = parseInt(this.getAttribute('data-index'), 10); 346 goTo(index); 347 setActive(index); 348 }); 349 350 // Dots are tapped, never hovered, so the slide can glide over. 351 $(document).on('click', '.mh--gallery-dot', function() { 352 var index = parseInt(this.getAttribute('data-index'), 10); 353 goTo(index, 'smooth'); 354 setActive(index); 355 }); 356 357 // Keep the stage lined up with the active slide after a resize changes the slide width. The dot 358 // strip too: crossing 900px brings it out of display: none at scrollLeft 0. 359 var resizeTimer = null; 360 window.addEventListener('resize', function() { 361 clearTimeout(resizeTimer); 362 resizeTimer = setTimeout(function() { 363 if (activeIndex > 0) { 364 goTo(activeIndex); 365 } 366 var strip = document.getElementById('jsMhGalleryDots'); 367 if (strip && activeIndex >= 0) { 368 syncGalleryDotStrip(strip, activeIndex); 369 } 370 }, 150); 371 }); 372 373 if (typeof IntersectionObserver === 'undefined') { 374 // Fallback tracking: less precise, but the thumbs still follow the stage. 375 var scrollTimer = null; 376 stage.addEventListener('scroll', function() { 377 clearTimeout(scrollTimer); 378 scrollTimer = setTimeout(function() { 379 if (stage.clientWidth) { 380 setActive(Math.round(stage.scrollLeft / stage.clientWidth)); 381 } 382 }, 100); 383 }); 384 } 385 386 syncDots(); 387 observeSlides(); 388 setActive(0); 389 390 // The multi-SKU page swaps the whole image list when the SKU changes, so it calls this to put the 391 // gallery back on the first image. 392 window.mhProductGallery = { 393 reset: function() { 394 stage.scrollTo({left: 0, behavior: 'instant'}); 395 syncDots(); 396 observeSlides(); 397 activeIndex = -1; 398 setActive(0); 399 } 400 }; 401} 402 403function getRequestAQuoteStatus() { 404 $.ajax({ 405 type: 'POST', 406 dataType: 'json', 407 url: '/handlers/request-quote/status', 408 data: {}, 409 success: function (data) { 410 if(isTestEnvironment) { 411 console.log(data); 412 } 413 if(data.Status === 'success') { 414 if(data.IsOpen) { 415 $('.jsRequestQuoteCallUs').show(); 416 $('.jsRequestQuoteCallUsText').hide(); 417 } else { 418 $('.jsRequestQuoteCallUs').hide(); 419 } 420 421 if(data.EmailAddress) { 422 $('#jsRequestQuoteEmail').val(data.EmailAddress); 423 } 424 } 425 }, 426 error: function (xhr) { 427 console.log('Error setting up Request a Quote'); 428 } 429 }); 430} 431 432function getWishlistStatus() { 433 var skuKey = $('#sku-key').val(); 434 let payload = { 435 ProductSkuKey: skuKey}; 436 $.ajax({ 437 type: 'POST', 438 dataType: 'json', 439 url: '/handlers/product-sku/wishlist-status', 440 data: payload, 441 success: function (data) { 442 if(isTestEnvironment) { 443 console.log(data); 444 } 445 if(data.Status === 'success') { 446 if(data.HasAccount) { 447 if(data.HasBeenAdded) { 448 setWishlistDisplay('added'); 449 } else { 450 setWishlistDisplay('remove'); 451 } 452 } else { 453 setWishlistDisplay('not-signed-in'); 454 } 455 } 456 }, 457 error: function (xhr) { 458 console.log('Error loading product wishlist') 459 } 460 }); 461} 462 463function setWishlistDisplay(status = 'added') { 464 let $jsWishlistStatus = $('.jsWishlistStatus'); 465 $jsWishlistStatus.text(''); 466 467 if(status === 'not-signed-in') { 468 $jsWishlistStatus.html('<a href="/accounts/wishlists" class="css-link">Add to wishlist</a>').show(); 469 } else { 470 if (status === 'added') { 471 $jsWishlistStatus.html('<a href="/accounts/wishlists" class="css-link">View Wishlist</a>').show(); 472 $('.jsAddToWishlist').hide(); 473 $('.jsWishlistHeart').off().on('click', function () { 474 if(isTestEnvironment) { 475 console.log('remove from wishlist'); 476 } 477 let skuKey = $('#sku-key').val(); 478 removeFromWishlist(skuKey); 479 }); 480 $('.jsRemoveFromWishlist').show().text('Remove from wishlist'); 481 $('.product-page-wishlist-heart').addClass('product-page-wishlist-is-animating product-page-wishlist-is-active').on('animationend', function(){ 482 $(this).toggleClass('product-page-wishlist-is-animating'); 483 }); 484 } else { 485 $('.jsRemoveFromWishlist').hide(); 486 $('.jsWishlistHeart').off().on('click', function () { 487 if(isTestEnvironment) { 488 console.log('add to wishlist'); 489 } 490 let skuKey = $('#sku-key').val(); 491 addToWishlist(skuKey); 492 }); 493 $('.jsAddToWishlist').show().text('Add to wishlist'); 494 $('.product-page-wishlist-heart').removeClass('product-page-wishlist-is-animating product-page-wishlist-is-active'); 495 } 496 497 bindWishlistButtons(); 498 } 499} 500 501function addToWishlist(skuKey) { 502 let payload = { 503 ProductSkuKey: skuKey}; 504 $.ajax({ 505 type: 'POST', 506 dataType: 'json',
507 url: '/handlers/wishlist/add', 508 data: payload, 509 success: function (data) { 510 if(isTestEnvironment) { 511 console.log(data); 512 } 513 if(data.Status === 'success') { 514 setWishlistDisplay('added'); 515 snackbarNotify('Item added to wishlist', 'success', '/accounts/wishlists?ref=mh-toast-wishlist-add-success'); 516 517 } else { 518 } 519 }, 520 error: function (xhr) { 521 snackbarNotify('Oops! There was an error adding to your wishlist', 'error', '/accounts/wishlists?ref=mh-toast-wishlist-add-error'); 522 } 523 }); 524} 525 526function removeFromWishlist(skuKey){ 527 let payload = { 528 ProductSkuKey: skuKey}; 529 $.ajax({ 530 type: 'POST', 531 dataType: 'json', 532 url: '/handlers/wishlist/remove', 533 data: payload, 534 success: function (data) { 535 if(isTestEnvironment) { 536 console.log(data); 537 } 538 539 if(data.Status === 'success') { 540 setWishlistDisplay('remove'); 541 snackbarNotify('Item removed from wishlist', 'warning', '/accounts/wishlists?ref=mh-toast-wishlist-remove-success'); 542 } else { 543 } 544 }, 545 error: function (xhr) { 546 snackbarNotify('Oops! There was an error removing from your wishlist', 'error', '/accounts/wishlists?ref=mh-toast-wishlist-remove-error'); 547 } 548 }); 549} 550 551function bindWishlistButtons() { 552 $('.jsAddToWishlist').off().on('click', function () { 553 let skuKey = $('#sku-key').val(); 554 addToWishlist(skuKey); 555 }); 556 557 $('.jsRemoveFromWishlist').off().on('click', function () { 558 let skuKey = $('#sku-key').val(); 559 removeFromWishlist(skuKey); 560 }); 561} 562 563function loadProductQuestions() { 564 let $ajaxProductQuestions = $('#ajaxProductQuestions'); 565 let payload = { 566 ProductReference: $ajaxProductQuestions.attr('data-product-reference'), 567 ProductTitle: $ajaxProductQuestions.attr('data-product-title'), 568 ProductSkuKey: $ajaxProductQuestions.attr('data-sku-key')}; 569 $.ajax({ 570 type: 'POST', 571 dataType: 'json', 572 url: '/handlers/retrieve-product-questions', 573 data: payload, 574 success: function (data) { 575 if(data.Status === 'success') { 576 $('#ajaxProductQuestions').html(data.Content); 577 initComments(); 578 bindButtons(); 579 bindForms(); 580 scrollToQuestion(); 581 bindVoting($('#user-is-mod').val()); 582 bindLoadMoreQuestions(); 583 } else { 584 $('#ajaxProductQuestions').html(data.ErrorMessage); 585 } 586 }, 587 error: function (xhr) { 588 console.log('Error loading product questions') 589 } 590 }); 591} 592 593function bindLoadMoreQuestions() { 594 $('#jsProductQuestionsLoadMore').off().on('click', function () { 595 let $ajaxProductQuestions = $('#ajaxProductQuestions'); 596 let payload = { 597 StartingPage: $(this).attr('data-starting-page'), 598 ProductReference: $ajaxProductQuestions.attr('data-product-reference'), 599 ProductTitle: $ajaxProductQuestions.attr('data-product-title'), 600 ProductSkuKey: $ajaxProductQuestions.attr('data-sku-key')}; 601 $('#jsProductQuestionsLoadMore').html(loader_element_svg_blue); 602 $.ajax({ 603 type: 'POST', 604 dataType: 'json',
605 url: '/handlers/retrieve-product-questions', 606 data: payload, 607 success: function (data) { 608 if(data.Status === 'success') { 609 $('#jsProductQuestionsLoadMore').replaceWith(data.Content); 610 initComments(); 611 bindButtons(); 612 bindForms(); 613 scrollToQuestion(); 614 bindVoting($('#user-is-mod').val()); 615 bindLoadMoreQuestions(); 616 } 617 }, 618 error: function (xhr) { 619 console.log('Error loading product questions') 620 } 621 }); 622 }); 623} 624 625function bindUpdateShadowban() { 626 $('.js-comment-user-update').off().on('click', function(){ 627 var shadowBanFlag = $(this).attr('data-user-shadowban'); 628 629 var payload = { 630 userName : $(this).attr('data-user-name'), 631 shadowbanFlag : shadowBanFlag 632 }; 633 634 var confirmText = 'Adding a shadowban to this user will hide this comment, all previous comments, as well as any future comments, and it will appear as normal for the user. Are you sure you want to shadowban this user?'; 635 if(shadowBanFlag === "True") { 636 confirmText = 'Removing a shadowban from this user will unhide this comment, all previous comments, as well as any future comments. Are you sure you want to remove the shadowban on this user?'; 637 } 638 var verify_shadow_ban = confirm(confirmText); 639 640 if (verify_shadow_ban) { 641 $.ajax({ 642 type: 'POST', 643 dataType: 'json', 644 url: '/handlers/forum-update-shadowban', 645 data: payload, 646 success: function (data) { 647 if(!data.error_status) { 648 window.location.reload(); 649 } else { 650 var error_message = "Oops! unable to update the user's data"; 651 if(data.error_message) { 652 error_message = data.error_message; 653 } 654 snackbarNotify(error_message, 'error'); 655 } 656 }, 657 error: function (xhr) { 658 snackbarNotify("Oops! unable to update the user's data", 'error'); 659 } 660 }); 661 } 662 }) 663} 664 665function submitProductQuestion() { 666 //Serialize form data to submit 667 var payload = $('#formAddProductQuestion').serialize(); 668 $.ajax({ 669 type: 'POST', 670 dataType: 'json', 671 url: '/handlers/forum-add-topic', 672 data: payload, 673 success: function (data) { 674 if (data.error_status == 'True') { 675 $('#errorContainer').html(data.error_message); 676 bindSubmitProductQuestion(); 677 } else { 678 location.reload(); 679 } 680 }, 681 error: function (xhr) { 682 $('#errorContainer').html("Oops! We couldn't save your question, please try again later."); 683 bindSubmitProductQuestion(); 684 } 685 }); 686} 687 688function bindSubmitProductQuestion() { 689 $('#bSubmitProductQuestion').html('Submit').on('click', function () { 690 //Unbind click, so bindings don't accumulate 691 $(this).off('click'); 692 693 //Show loader element on button 694 $(this).html(loader_element); 695 696 //Clear error container 697 $('#errorContainer').html(''); 698 699 submitProductQuestion(); 700 }); 701} 702 703function scrollToQuestion() { 704 var hash = window.location.hash; 705 706 if (validateBlank(hash) && hash === "#questions") { // if hash is questions, scroll to that sucka 707 setTimeout(function () { 708 scrollToLocation('questions'); 709 }, 500); 710 } 711} 712 713function scrollToLocation(id) { 714 if($('#'+id).length) { 715 $('html,body').animate({scrollTop: $("#"+id).offset().top - 160}, 1000); 716 } 717} 718 719function bindForms() { // new topics and replies 720 721 // add new question 722 $('#formNewForumTopic').on('submit', function(e) { 723 e.preventDefault(); 724 725 var form = $(this).serialize(); 726 var key = $(this).find('ForumKey').val(); 727 728 $(this).find('.status').html(); 729 $(this).find('.button-submit').html('Processing...').addClass('fade-50'); 730 731 if (validateBlank($(this).find('#TopicTitle').val())) { 732 createForumTopic(form, key); 733 } else { 734 $(this).find('.button-submit').html('Ask Question').removeClass('fade-50'); 735 $(this).find('.status-question').html('Question cannot be blank.'); 736 $(this).closest('.form-wrapper').shake(); 737 } 738 }); 739 740 // add a new reply 741 $('.formReplyForumTopic').on('submit', function(e) { 742 e.preventDefault(); 743 var form = $(this).serialize(); 744 var key = $(this).find('.TopicKey').val(); 745 746 $(this).find('.status').html(); 747 $(this).find('.btn-add-response').html('Processing...').addClass('fade-50'); 748 749 if (validateBlank($(this).find('.TopicMessage').val())) { 750 replyForumTopic(form, key); 751 } else { 752 // shake it 753 $(this).find('.btn-add-response').html('Add Response').removeClass('fade-50'); 754 $(this).find('.status-question').html('Response cannot be blank.'); 755 $(this).closest('.form-wrapper').shake(); 756 } 757 }); 758} 759 760function bindButtons() { 761 // cta's 762 bindQuestionDelete(); 763 bindCommentDelete(); 764 bindViewAndReply(); 765 bindViewAndReplyClose(); 766 767 bindCommentsView(); 768 bindCommentsHide(); 769 770 bindCommentsViewAll(); 771 bindAddReply(); 772 bindCloseReply(); 773 bindCloseReplyNoComments(); 774 775 bindCommentFormattingGuide(); 776 777 bindUpdateShadowban(); 778 779 // close parent 780 $('.close-parent').off().on('click', function() { 781 $(this).parent().slideUp('medium'); 782 }); 783} 784 785function initComments() { 786 $('.comments').each(function(i, obj) { 787 var comment = $(this).find('.comment-single'); 788 var numComments = comment.length; 789 790 $(this).closest('.comments').attr('data-number-comments', numComments); 791 792 if(numComments > 0) { 793 794 var response = ' Response' 795 // using correct english 796 if (numComments > 1) { 797 response += 's'; 798 } 799 800 // hide the comment group if not curently set to open 801 if($(this).find('.comment-group').attr('data-view') == "open") { 802 803 // label the cta 804 $(this).find('.comment-cta-first').html('Hide ' + response).addClass('js-comments-hide'); 805 806 } else{ 807 $(this).find('.comment-group').hide().attr('data-view', 'closed'); 808 809 // label the cta 810 $(this).find('.comment-cta-first').html('View ' + numComments + response).addClass('js-comments-view'); 811 } 812 813 $(this).find('.button-cancel').addClass('js-comment-close-reply'); 814 815 } else { 816 // label the cta and add appropriate class handler if no comments 817 $(this).find('.comment-cta-first').html('Add Response').addClass('js-comment-view-and-reply'); 818 $(this).find('.comment-thread-cta').hide(); 819 $(this).find('.comment-group').show().attr('data-view', 'open'); 820 $(this).find('.button-cancel').addClass('js-comment-close-reply-no-comments'); 821 } 822 823 $(this).find('.comment-reply').hide().attr('data-view', 'closed'); 824 825 }); 826} 827 828function bindCommentsViewAll() { 829 // add up all question responses 830 var $questionResponses = 0; 831 $('.comments').each(function(questionResponses) { 832 var num = parseInt($(this).attr('data-number-comments')); 833 $questionResponses += num; 834 }); 835 836 // if there are responses, show button 837 if ($questionResponses > 0) { 838 $('#js-comments-view-all').off().on('click', function() { 839 $(this).fadeOut('fast', function(){ 840 841 if ($(this).hasClass('show-close')) { 842 $(this).html('Hide All Responses x').removeClass('show-close').addClass('button-mini-orange').fadeIn(); 843 844 $('.comment-group').each(function(i, obj) { 845 var numComments = $(this).closest('.comments').attr('data-number-comments'); 846 847 if(numComments > 0) { 848 $(this).slideDown('10').attr('data-view', 'open'); 849 $(this).closest('.comments').find('.comment-cta-first').hide(); 850 } 851 }); 852 853 } else { 854 $(this).html('View All Responses').addClass('show-close').removeClass('button-mini-orange').fadeIn(); 855 856 $('.comment-group').each(function(i, obj){ 857 var numComments = $(this).closest('.comments').attr('data-number-comments'); 858 859 if(numComments > 0) { 860 $(this).slideUp('10').attr('data-view', 'closed'); 861 862 if (numComments > 1) { 863 var response = ' Responses'; 864 } else { 865 var response = ' Response'; 866 } 867 868 $(this).closest('.comments').find('.comment-cta-first').html('View ' + numComments + response).addClass('js-comments-view').removeClass('js-comments-hide').show(); 869 bindCommentsView(); 870 } 871 }); 872 } 873 }); 874 }); 875 } else { 876 // otherwise hide button 877 $('#js-comments-view-all').hide(); 878 } 879} 880 881function bindCommentFormattingGuide() { 882 // text and js handlers for formatting guide 883 $('.comment-formatting-help').off().on('click', function(){ 884 $(this).fadeOut('fast', function(){ 885 if ($(this).hasClass('tag-close')) { 886 $(this).html('Show Formatting Guide').removeClass('tag-close').fadeIn('fast'); 887 $(this).closest('.comment-formatting-group').attr('data-view', 'closed'); 888 } else { 889 $(this).html('Hide Formatting Guide x').addClass('tag-close').fadeIn('fast'); 890 $(this).closest('.comment-formatting-group').attr('data-view', 'open') 891 } 892 }); 893 894 $(this).closest('.comment-formatting-group').find('.comment-formatting').slideToggle(350, function(){}); 895 }); 896} 897 898function bindCommentsView() { 899 $('.js-comments-view').off().on('click', function() { 900 $(this).html('Hide Responses').addClass('js-comments-hide').removeClass('js-comments-view').closest('.question-group').find('.comment-group').attr('data-view', 'open').slideDown('350'); 901 bindCommentsHide(); 902 }); 903} 904 905function bindCommentsHide() { 906 $('.js-comments-hide').off().on('click', function() { 907 var numComments = $(this).closest('.comments').attr('data-number-comments'); 908 909 if(numComments > 0) { 910 if (numComments > 1) { 911 var response = ' Responses' 912 } else { 913 var response = ' Response' 914 } 915 916 $(this).html('View ' + numComments + response).addClass('js-comments-view').removeClass('js-comments-hide').closest('.question-group').find('.comment-group').attr('data-view', 'closed').slide
916Up('350'); 917 } else { 918 919 } 920 921 bindCommentsView(); 922 }); 923} 924 925function bindAddReply() { 926 $('.js-comment-add-reply').off().on('click', function() { 927 $(this).closest('.question-group').find('.comment-thread-cta').slideUp('10').closest('.question-group').find('.comment-reply').slideDown('10'); 928 bindCloseReply(); 929 }); 930} 931 932function bindCloseReply() { 933 $('.js-comment-close-reply').off().on('click', function() { 934 $(this).closest('.question-group').find('.comment-thread-cta').slideDown('10').closest('.question-group').find('.comment-reply').slideUp('10'); 935 bindAddReply(); 936 }); 937} 938 939function bindCloseReplyNoComments() { 940 $('.js-comment-close-reply-no-comments').off().on('click', function() { 941 $(this).closest('.question-group').find('.comment-reply').slideUp('10'); 942 bindAddReply(); 943 944 $(this).closest('.question-group').find('.comment-cta-first').removeClass('js-comment-view-and-reply-close').addClass('js-comment-view-and-reply').html('Add Response'); 945 bindViewAndReply(); 946 }); 947} 948 949function bindViewAndReply() { 950 $('.js-comment-view-and-reply').off().on('click', function() { 951 $(this).removeClass('js-comment-view-and-reply').addClass('js-comment-view-and-reply-close').html('Hide Response').closest('.question-group').find('.comment-reply').slideDown('10'); 952 bindViewAndReplyClose(); 953 }); 954} 955 956function bindViewAndReplyClose() { 957 $('.js-comment-view-and-reply-close').off().on('click', function() { 958 $(this).removeClass('js-comment-view-and-reply-close').addClass('js-comment-view-and-reply').html('Add Response').closest('.question-group').find('.comment-reply').slideUp('10'); 959 bindViewAndReply(); 960 }); 961} 962 963function bindQuestionDelete() { 964 $('.js-question-delete').off().on('click', function () { 965 var key = $(this).attr('data-key'); 966 $(this).html('Processing...').addClass('fade-50'); 967 handlerDeleteQuestion(key); 968 $(this).closest('.question-group').fadeOut('slow'); 969 }); 970} 971 972function handlerDeleteQuestion(key) { 973 var payload = {topicKey: key} 974 $.ajax({ 975 type: 'POST', 976 dataType: 'json', 977 url: '/handlers/forum-delete-topic', 978 data: payload, 979 success: function (data) { 980 if (data.error_status === 'True') { 981 snackbarNotify(data.error_message, 'error'); 982 } 983 }, 984 error: function (xhr) { 985 snackbarNotify("Oops! Couldn't delete the question, please try again.", 'error'); 986 } 987 }); 988} 989 990function bindCommentDelete() { 991 $('.js-comment-delete').on('click', function () { 992 var key = $(this).attr('data-key'); 993 $(this).html('Processing...').addClass('fade-50'); 994 handlerDeleteReply(key); 995 $(this).closest('.comment-thread').fadeOut('slow'); 996 }); 997} 998 999function handlerDeleteReply(key) { 1000 var payload = {postKey: key} 1001 $.ajax({ 1002 type: 'POST', 1003 dataType: 'json', 1004 url: '/handlers/forum-delete-reply', 1005 data: payload, 1006 success: function (data) { 1007 if (data.error_status === 'True') { 1008 snackbarNotify(data.error_message, 'error'); 1009 } 1010 }, 1011 error: function (xhr) { 1012 snackbarNotify("Oops! Couldn't delete the question, please try again.", 'error'); 1013 } 1014 }); 1015} 1016 1017function createForumTopic(payload, key) { 1018 // payload is serialized form 1019 // key is id of the comment to show error message 1020 $.ajax({ 1021 type: 'POST', 1022 dataType: 'json', 1023 url: '/handlers/forum-add-topic', 1024 data: payload, 1025 success: function (data) { 1026 if (data.error_status == 'True') { 1027 $('#question-add .status').html(''); 1028 $('#question-add .js-ajax-response').html(data.error_message); 1029 $('#question-add').find('.button-submit').html('Ask Question').removeClass('fade-50'); 1030 $('#question-add').closest('.form-wrapper').shake(); 1031 } else { 1032 // success 1033 if (typeof(reload_callback) !== 'undefined') 1034 { 1035 setTimeout(function () { reload_callback(); }, 1000); 1036 } else { 1037 location.reload(); 1038 } 1039 } 1040 }, 1041 error: function (xhr) { 1042 $('.js-ajax-response', '#formNewForumTopic').html("Oops! We couldn't save your post, please try again."); 1043 } 1044 }); 1045} 1046 1047function replyForumTopic(payload, key) { 1048 // payload is serialized form 1049 // key is id of the comment to show error message 1050 $.ajax({ 1051 type: 'POST', 1052 dataType: 'json', 1053 url: '/handlers/forum-add-reply', 1054 data: payload, 1055 success: function (data) { 1056 if (data.error_status === 'True') { 1057 $('#'+ key + ' .status').html('').hide(); 1058 $('#'+ key + ' .js-ajax-response').show().html(data.error_message); 1059 $('#'+ key).find('.button-submit').html('Ask Question').removeClass('fade-50'); 1060 $('#' + key + '.form-wrapper').shake(); 1061 } else { 1062 // success 1063 if (typeof(reload_callback) !== 'undefined') 1064 { 1065 setTimeout(function () { reload_callback(); }, 500); 1066 } else { 1067 location.reload(); 1068 } 1069 } 1070 }, 1071 error: function (xhr) { 1072 snackbarNotify("Oops! Couldn't delete the question, please try again.", 'error'); 1073 } 1074 }); 1075} 1076 1077function validateBlank(str) { // blank field validation 1078 if(str && str.length > 0) { 1079 return true; 1080 } else { 1081 return false;
1082 } 1083} 1084 1085var loader_element_svg = '<span class="isProcessing"><img src="/static/ajax-spin-white.svg" title="processing..." width="14" height="14" /></span>'; 1086var loader_element_svg_blue = '<span class="isProcessing"><img src="/static/ajax-spin-blue.svg" title="processing..." width="14" height="14" /></span>';
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.