1/** 2 * Single course page interactions. 3 * 4 * Accordion, sticky buy bar, intro/lesson video playback, 5 * and native lesson progress tracking. 6 * 7 * @package ZhaketAcademy 8 * @since 1.0.22 9 */ 10(function () { 11 'use strict'; 12 13 var cfg = window.zhaketAcademy || {}; 14 var course = window.zhaketCourse || {}; 15 16 /** 17 * Convert Latin digits to Persian for progress UI updates. 18 * 19 * @param {string|number} value Raw value. 20 * @return {string} 21 */ 22 function toFa(value) { 23 return String(value).replace(/\d/g, function (d) { 24 return 'Û°Û±Û²Û³Û´ÛµÛ¶Û·Û¸Û¹'[d]; 25 }); 26 } 27 28 /** 29 * Accordion open/close for curriculum and FAQ. 30 * 31 * @return {void} 32 */ 33 function initAccordion() { 34 document.addEventListener('click', function (e) { 35 var top = e.target.closest('.acc-top'); 36 if (!top) { 37 return; 38 } 39 var acc = top.parentElement; 40 if (acc && acc.classList.contains('acc')) { 41 acc.classList.toggle('open'); 42 } 43 }); 44 } 45 46 /** 47 * Wire clickable 5-star rating widgets -- writes the picked value into 48 * the accompanying hidden `name="rating"` input so the existing review 49 * submit handler (below) needs no changes. 50 * 51 * @return {void} 52 */ 53 function initStarRating() { 54 var widgets = document.querySelectorAll('[data-star-rate]'); 55 56 widgets.forEach(function (widget) { 57 var input = widget.parentElement.querySelector('[data-star-rate-input]'); 58 var stars = Array.prototype.slice.call( 59 widget.querySelectorAll('[data-value]') 60 ); 61 62 function render(value) { 63 stars.forEach(function (s) { 64 var on = Number(s.getAttribute('data-value')) <= value; 65 s.classList.toggle('on', on); 66 s.setAttribute('aria-pressed', on ? 'true' : 'false'); 67 }); 68 } 69 70 stars.forEach(function (s) { 71 s.addEventListener('click', function () { 72 var v = s.getAttribute('data-value'); 73 if (input) { 74 input.value = v; 75 input.dispatchEvent(new Event('change', { bubbles: true })); 76 } 77 render(Number(v)); 78 }); 79 s.addEventListener('mouseenter', function () { 80 render(Number(s.getAttribute('data-value'))); 81 }); 82 }); 83 84 widget.addEventListener('mouseleave', function () { 85 render(Number((input && input.value) || 0)); 86 }); 87 }); 88 } 89 90 /** 91 * Sticky mobile buy bar -- always on (matches the reference site), except 92 * right above the footer where it would overlap it. 93 * 94 * Used to only reveal itself once `.buy-card` scrolled out of view, back 95 * when the buy-box sat at the very top of the page on mobile. Since the 96 * buy-box now sits further down in source order (see single-product.php, 97 * matching the reference site's rhythm), that anchor could take a full 98 * page of scrolling to pass, leaving mobile visitors with no purchase CTA 99 * at all until then. 100 * 101 * @return {void} 102 */ 103 function initBuyBar() { 104 var bar = document.querySelector('.buy-bar'); 105 var foot = document.querySelector('footer.site'); 106 107 if (!bar) { 108 return; 109 } 110 111 // Reveal the bar element (hidden until JS boots). 112 bar.hidden = false; 113 114 /** 115 * Toggle visibility based on scroll position. 116 * 117 * @return {void} 118 */ 119 function onScroll() { 120 var near = foot 121 ? foot.getBoundingClientRect().top < window.innerHeight - 40 122 : false; 123 var show = !near; 124 bar.classList.toggle('on', show); 125 document.body.classList.toggle('has-buybar', show); 126 } 127 128 window.addEventListener('scroll', onScroll, { passive: true }); 129 window.addEventListener('resize', onScroll); 130 onScroll(); 131 } 132 133 /** 134 * Ensure a shared lesson video modal exists in the DOM. 135 * 136 * @return {{modal: HTMLElement, video: HTMLVideoElement}} 137 */ 138 function ensureModal() { 139 var modal = document.getElementById('zhaketLessonModal'); 140 if (modal) { 141 return { 142 modal: modal, 143 video: modal.querySelector('video') 144 }; 145 } 146 147 modal = document.createElement('div'); 148 modal.id = 'zhaketLessonModal'; 149 modal.className = 'zhaket-lesson-modal'; 150 modal.innerHTML = 151 '<div class="zhaket-lesson-modal__box">' + 152 '<button type="button" class="zhaket-lesson-modal__close" aria-label="Close">×</button>' + 153 '<video controls playsinline></video>' + 154 '</div>'; 155 document.body.appendChild(modal); 156 157 var video = modal.querySelector('video'); 158 var closeBtn = modal.querySelector('.zhaket-lesson-modal__close'); 159 160 /** 161 * Close modal and pause playback. 162 * 163 * @return {void} 164 */ 165 function close() { 166 modal.classList.remove('on'); 167 video.pause(); 168 video.removeAttribute('src'); 169 video.load(); 170 } 171 172 closeBtn.addEventListener('click', close); 173 modal.addEventListener('click', function (e) { 174 if (e.target === modal) { 175 close(); 176 } 177 }); 178 179 return { modal: modal, video: video }; 180 } 181 182 /** 183 * Mark a lesson watched via AJAX when native progress tracking is enabled. 184 * 185 * @param {HTMLElement} lesson Lesson row element. 186 * @return {void} 187 */ 188 function trackProgress(lesson) { 189 if (!lesson || lesson.getAttribute('data-track-progress') !== '1') { 190 return; 191 } 192 if (!cfg.ajaxUrl || !course.progressNonce) { 193 return; 194 } 195 196 var body = new FormData(); 197 body.append('action', 'zhaket_mark_lesson_watched'); 198 body.append('nonce', course.progressNonce); 199 body.append('product_id', lesson.getAttribute('data-product-id') || ''); 200 body.append('section_index', lesson.getAttribute('data-section-index') || ''); 201 body.append('lesson_index', lesson.getAttribute('data-lesson-index') || ''); 202 203 fetch(cfg.ajaxUrl, { 204 method: 'POST', 205 credentials: 'same-origin', 206 body: body 207 }) 208 .then(function (res) { 209 return res.json(); 210 }) 211 .then(function (json) { 212 if (!json || !json.success || !json.data || !json.data.progress) { 213 return; 214 } 215 updateProgressUi(json.data.progress); 216 }) 217 .catch(function () { 218 /* Silent: progress is best-effort. */ 219 }); 220 } 221 222 /** 223 * Update sidebar / buy-bar progress numbers after a watched event. 224 * 225 * @param {{watched:number,total:number,percent:number}} progress Progress payload. 226 * @return {void} 227 */ 228 function updateProgressUi(progress) { 229 var percentEl = document.querySelector('[data-progress-percent]'); 230 var barEl = document.querySelector('[data-progress-bar]'); 231 var metaEl = document.querySelector('[data-progress-meta]'); 232 var buybar = document.querySelector('[data-buybar-progress]'); 233 234 if (percentEl) { 235 percentEl.textContent = toFa(progress.percent) + 'Ùª'; 236 } 237 if (barEl) { 238 barEl.style.width = String(progress.percent) + '%'; 239 } 240 if (metaEl) { 241 metaEl.textContent = 242 'Ø¬ÙØ³Ù ' + toFa(progress.watched) + ' از ' + toFa(progress.total); 243 } 244 if (buybar) { 245 buybar.innerHTML = 246 toFa(progress.percent) + 247 'Ùª <small>' + 248 ((course.i18n && course.i18n.completed) || '') + 249 '</small>'; 250 } 251 } 252
253 /** 254 * Resolve a lesson's video into a playable URL. 255 * 256 * Migrated `video_url` values are bucket-relative storage keys, not real 257 * URLs, even when they carry a stray scheme -- deciding "is this already 258 * public" from the string alone is unreliable (see class-sign-lesson-video.php). 259 * That decision now lives entirely server-side (host-based, re-derived 260 * from the lesson data, not from this attribute), so every lookup always 261 * goes through the AJAX endpoint instead of duplicating the logic here. 262 * 263 * @param {HTMLElement} lesson Lesson element with data-video-url. 264 * @return {Promise<string>} Resolves to a playable URL, rejects on failure. 265 */ 266 function resolveLessonUrl(lesson) { 267 if (!cfg.ajaxUrl && !course.ajaxUrl) { 268 return Promise.reject(new Error('no ajax url')); 269 } 270 if (!course.signNonce) { 271 return Promise.reject(new Error('no sign nonce')); 272 } 273 274 var body = new FormData(); 275 body.append('action', 'zhaket_sign_lesson_video'); 276 body.append('nonce', course.signNonce); 277 body.append('product_id', lesson.getAttribute('data-product-id') || ''); 278 body.append('section_index', lesson.getAttribute('data-section-index') || ''); 279 body.append('lesson_index', lesson.getAttribute('data-lesson-index') || ''); 280 281 return fetch(course.ajaxUrl || cfg.ajaxUrl, { 282 method: 'POST', 283 credentials: 'same-origin', 284 body: body 285 }) 286 .then(function (res) { 287 return res.json(); 288 }) 289 .then(function (json) { 290 if (!json || !json.success || !json.data || !json.data.url) { 291 throw new Error((json && json.data && json.data.message) || 'sign failed'); 292 } 293 return json.data.url; 294 }); 295 } 296 297 /** 298 * Open a lesson/preview video in the modal player. 299 * 300 * @param {HTMLElement} lesson Lesson element with data-video-url. 301 * @return {void} 302 */ 303 function playLesson(lesson) { 304 if (!lesson.getAttribute('data-video-url')) { 305 return; 306 } 307 var parts = ensureModal(); 308 parts.modal.classList.add('on', 'is-loading'); 309 310 resolveLessonUrl(lesson) 311 .then(function (url) { 312 parts.modal.classList.remove('is-loading'); 313 parts.video.src = url; 314 parts.video.play(); 315 316 parts.video.onended = function () { 317 trackProgress(lesson); 318 }; 319 }) 320 .catch(function () { 321 parts.modal.classList.remove('on', 'is-loading'); 322 }); 323 } 324 325 /** 326 * Wire playable lesson rows and intro player. 327 * 328 * @return {void} 329 */ 330 function initVideo() { 331 document.addEventListener('click', function (e) { 332 var lesson = e.target.closest('.lesson--playable'); 333 if (lesson) { 334 e.preventDefault(); 335 playLesson(lesson); 336 return; 337 } 338 339 var play = e.target.closest('.cp-player--has-video .play'); 340 if (!play) { 341 return; 342 } 343 344 var player = play.closest('.cp-player'); 345 var url = player.getAttribute('data-video-url'); 346 var video = player.querySelector('.cp-player__video'); 347 if (!url || !video) { 348 return; 349 } 350 351 video.hidden = false; 352 video.muted = false; 353 video.src = url; 354 player.classList.add('is-playing'); 355 video.play(); 356 }); 357 358 document.addEventListener('keydown', function (e) { 359 if (e.key !== 'Enter' && e.key !== ' ') { 360 return; 361 } 362 var lesson = e.target.closest('.lesson--playable'); 363 if (!lesson) { 364 return; 365 } 366 e.preventDefault(); 367 playLesson(lesson); 368 }); 369 } 370 371 /** 372 * When no poster/gallery cover exists, park the intro video on its first frame. 373 * 374 * @return {void} 375 */ 376 function initFrameFallback() { 377 var players = document.querySelectorAll('.cp-player[data-frame-fallback="1"]'); 378 players.forEach(function (player) { 379 var url = player.getAttribute('data-video-url'); 380 var video = player.querySelector('.cp-player__video'); 381 if (!url || !video) { 382 return; 383 } 384 385 // Show the video under the play control so the first frame acts as cover. 386 video.muted = true; 387 video.playsInline = true; 388 video.preload = 'metadata'; 389 video.hidden = false; 390 video.setAttribute('src', url); 391 392 var onSeeked = function () { 393 try { 394 video.pause(); 395 } catch (err) { 396 // Ignore pause errors on unfinished loads. 397 } 398 video.removeEventListener('seeked', onSeeked); 399 }; 400 401 video.addEventListener('loadeddata', function () { 402 try { 403 video.currentTime = 0.05; 404 } catch (err) { 405 // Some hosts block seeking until more data arrives. 406 } 407 }); 408 video.addEventListener('seeked', onSeeked); 409 }); 410 } 411 412 /**
413 * AJAX pagination for product reviews (uses Discussion â comments_per_page). 414 * 415 * @return {void} 416 */ 417 function initReviewsPager() { 418 var box = document.getElementById('reviews'); 419 if (!box) { 420 return; 421 } 422 423 var list = box.querySelector('[data-reviews-list]'); 424 var pagerWrap = box.querySelector('[data-reviews-pager]'); 425 var productId = box.getAttribute('data-product-id'); 426 if (!list || !pagerWrap || !productId) { 427 return; 428 } 429 430 var loading = false; 431 432 /** 433 * Fetch one reviews page and swap list + pager markup. 434 * 435 * @param {number} page Target page number. 436 * @return {void} 437 */ 438 function loadPage(page) { 439 if (loading || page < 1) { 440 return; 441 } 442 loading = true; 443 box.classList.add('is-loading-reviews'); 444 445 var body = new URLSearchParams(); 446 body.set('action', 'zhaket_load_reviews'); 447 body.set('product_id', productId); 448 body.set('page', String(page)); 449 450 fetch(course.ajaxUrl || cfg.ajaxUrl || '/wp-admin/admin-ajax.php', { 451 method: 'POST', 452 credentials: 'same-origin', 453 headers: { 454 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 455 }, 456 body: body.toString(), 457 }) 458 .then(function (res) { 459 return res.json(); 460 }) 461 .then(function (json) { 462 if (!json || !json.success || !json.data) { 463 throw new Error('reviews'); 464 } 465 list.innerHTML = json.data.html || ''; 466 pagerWrap.innerHTML = json.data.pager || ''; 467 list.scrollIntoView({ behavior: 'smooth', block: 'start' }); 468 }) 469 .catch(function () { 470 window.alert( 471 (course.i18n && course.i18n.reviewsError) || 472 'Reviews load failed' 473 ); 474 }) 475 .finally(function () { 476 loading = false; 477 box.classList.remove('is-loading-reviews'); 478 }); 479 } 480 481 pagerWrap.addEventListener('click', function (e) { 482 var btn = e.target.closest('.cmt-pager__btn[data-page]'); 483 if (!btn || btn.getAttribute('aria-current') === 'page') { 484 return; 485 } 486 e.preventDefault(); 487 var page = parseInt(btn.getAttribute('data-page'), 10); 488 if (!isNaN(page)) { 489 loadPage(page); 490 } 491 }); 492 } 493 494 /** 495 * Convert Latin digits to Persian for rating summary refresh. 496 * 497 * @param {string|number} value Raw value. 498 * @return {string} 499 */ 500 function toFaDigits(value) { 501 return toFa(value); 502 } 503 504 /** 505 * Build a 5-star glyph string from an average rating. 506 * 507 * @param {number} rating Average rating. 508 * @return {string} 509 */ 510 function starsGlyph(rating) { 511 var full = Math.round(Number(rating) || 0); 512 full = Math.max(0, Math.min(5, full)); 513 return 'â â â â â '.slice(0, full) + 'âââââ'.slice(0, 5 - full); 514 } 515 516 /** 517 * Refresh the rating summary block from AJAX breakdown data. 518 * 519 * @param {HTMLElement} box Reviews root. 520 * @param {Object} breakdown Rating payload from the server. 521 * @return {void} 522 */ 523 function updateRateSum(box, breakdown) { 524 if (!box || !breakdown) { 525 return; 526 } 527 var avgEl = box.querySelector('.rate-big b'); 528 var stEl = box.querySelector('.rate-big .st'); 529 var countEl = box.querySelector('.rate-big > span'); 530 if (avgEl) { 531 avgEl.textContent = toFaDigits( 532 Number(breakdown.average || 0).toFixed(1) 533 ); 534 } 535 if (stEl) { 536 stEl.textContent = starsGlyph(breakdown.average); 537 } 538 if (countEl) { 539 countEl.textContent = 540 'از ' + toFaDigits(String(breakdown.count || 0)) + ' Ø¯ÛØ¯Ú¯Ø§Ù'; 541 } 542 var bars = Array.isArray(breakdown.bars) ? breakdown.bars : []; 543 var rows = box.querySelectorAll('.rate-bars .rb'); 544 rows.forEach(function (row, i) { 545 var bar = bars[i]; 546 if (!bar) { 547 return; 548 } 549 var fill = row.querySelector('.bar i'); 550 var n = row.querySelector('.n'); 551 if (fill) { 552 fill.style.width = String(bar.percent || 0) + '%'; 553 } 554 if (n) { 555 n.textContent = toFaDigits(String(bar.count || 0)); 556 } 557 }); 558 } 559 560 /** 561 * AJAX submit for product review forms. 562 * 563 * @return {void} 564 */ 565 function initReviewSubmit() { 566 var box = document.getElementById('reviews'); 567 if (!box) { 568 return; 569 } 570 571 var forms = box.querySelectorAll('[data-review-form]'); 572 if (!forms.length) { 573 return; 574 } 575 576 /** 577 * Point the form at a review (or back at a fresh review when id is 0). 578 * While replying, the rating row is hidden: a reply carries no stars. 579 * 580 * @param {HTMLFormElement} form Review form. 581 * @param {number|string} id Top-level review id, 0 for none. 582 * @param {string} author Name shown in the "replying to" bar. 583 * @return {void} 584 */ 585 function setReplyTarget(form, id, author) { 586 var parent = form.querySelector('[name="comment_parent"]'); 587 var bar = form.querySelector('[data-reply-bar]'); 588 var text = form.querySelector('[data-reply-text]'); 589 var ratingRow = form.querySelector('.review-rating-row'); 590 if (parent) {
591 parent.value = String(id || 0); 592 } 593 if (bar) { 594 bar.hidden = !id; 595 } 596 if (text) { 597 text.textContent = id ? (bar.getAttribute('data-reply-label') || '') + ' ' + author : ''; 598 } 599 if (ratingRow) { 600 ratingRow.hidden = !!id; 601 } 602 } 603 604 // The list is re-rendered by AJAX, so listen on the box rather than on each button. 605 box.addEventListener('click', function (e) { 606 var form = box.querySelector('[data-review-form]:not([data-requires-login="1"])'); 607 if (!form) { 608 return; 609 } 610 var replyBtn = e.target.closest('[data-reply-to]'); 611 if (replyBtn) { 612 setReplyTarget(form, replyBtn.getAttribute('data-reply-to'), replyBtn.getAttribute('data-reply-author') || ''); 613 form.scrollIntoView({ behavior: 'smooth', block: 'center' }); 614 var field = form.querySelector('[name="comment"]'); 615 if (field) { 616 field.focus({ preventScroll: true }); 617 } 618 return; 619 } 620 if (e.target.closest('[data-reply-cancel]')) { 621 setReplyTarget(form, 0, ''); 622 } 623 }); 624 625 forms.forEach(function (form) { 626 form.addEventListener('submit', function (e) { 627 e.preventDefault(); 628 629 var btn = form.querySelector('[data-review-submit]'); 630 var msg = form.querySelector('[data-review-msg]'); 631 var list = box.querySelector('[data-reviews-list]'); 632 var pagerWrap = box.querySelector('[data-reviews-pager]'); 633 var sending = false; 634 635 if (form.getAttribute('data-sending') === '1') { 636 return; 637 } 638 639 var comment = form.querySelector('[name="comment"]'); 640 var rating = form.querySelector('[name="rating"]'); 641 var parentField = form.querySelector('[name="comment_parent"]'); 642 var replyTo = parentField && parentField.value !== '0' ? parentField.value : ''; 643 if (comment && !String(comment.value || '').trim()) { 644 comment.focus(); 645 return; 646 } 647 // A reply has no rating. 648 if (rating && !replyTo && !rating.value) { 649 rating.focus(); 650 return; 651 } 652 653 // Guest form: a review is tied to a real account. Stash what 654 // was typed so it survives the round trip to login and back, 655 // then send them there instead of calling the AJAX endpoint 656 // (which would just 403 with no author identity anyway). 657 if (form.getAttribute('data-requires-login') === '1') { 658 var productIdForDraft = 659 (form.querySelector('[name="product_id"]') || {}).value || 660 box.getAttribute('data-product-id') || 661 ''; 662 try { 663 localStorage.setItem( 664 'zhaket_review_draft_' + productIdForDraft, 665 JSON.stringify({ 666 comment: comment ? comment.value : '', 667 rating: rating ? rating.value : '', 668 }) 669 ); 670 } catch (err) { 671 // Private mode / storage disabled -- draft just won't survive the redirect. 672 } 673 window.location.href = form.getAttribute('data-login-url') || '#'; 674 return; 675 } 676 677 form.setAttribute('data-sending', '1'); 678 sending = true; 679 if (btn) { 680 btn.disabled = true; 681 btn.textContent = 682 (course.i18n && course.i18n.submitSending) || 683 'Sendingâ¦'; 684 } 685 if (msg) { 686 msg.hidden = true; 687 msg.className = 'cmt-form-msg'; 688 msg.textContent = ''; 689 } 690 691 var body = new URLSearchParams(); 692 body.set('action', 'zhaket_submit_review'); 693 body.set('nonce', course.submitNonce || ''); 694 body.set( 695 'product_id', 696 form.querySelector('[name="product_id"]').value || 697 box.getAttribute('data-product-id') || 698 '' 699 ); 700 body.set('comment', comment ? comment.value : ''); 701 body.set('rating', rating && !replyTo ? rating.value : ''); 702 if (replyTo) { 703 body.set('comment_parent', replyTo); 704 } 705 706 var author = form.querySelector('[name="author"]'); 707 var email = form.querySelector('[name="email"]'); 708 if (author) { 709 body.set('author', author.value || ''); 710 } 711 if (email) { 712 body.set('email', email.value || ''); 713 } 714 715 fetch(course.ajaxUrl || cfg.ajaxUrl || '/wp-admin/admin-ajax.php', { 716 method: 'POST', 717 credentials: 'same-origin', 718 headers: { 719 'Content-Type': 720 'application/x-www-form-urlencoded; charset=UTF-8', 721 }, 722 body: body.toString(), 723 }) 724 .then(function (res) { 725 return res.json(); 726 }) 727 .then(function (json) { 728 if (!json || !json.success || !json.data) { 729 var errMsg = 730 json && json.data && json.data.message 731 ? json.data.message 732 : (course.i18n && course.i18n.reviewsError) || 733 'Error'; 734 throw new Error(errMsg); 735 } 736 737 if (msg) { 738 msg.hidden = false;
739 msg.className = 'cmt-form-msg is-ok'; 740 msg.textContent = json.data.message || ''; 741 } 742 743 setReplyTarget(form, 0, ''); 744 if (comment) { 745 comment.value = ''; 746 } 747 if (rating) { 748 rating.value = ''; 749 var stars = form.querySelectorAll('[data-star-rate] [data-value]'); 750 stars.forEach(function (s) { 751 s.classList.remove('on'); 752 s.setAttribute('aria-pressed', 'false'); 753 }); 754 } 755 756 if (json.data.approved) { 757 if (list) { 758 list.innerHTML = json.data.html || ''; 759 } 760 if (pagerWrap) { 761 pagerWrap.innerHTML = json.data.pager || ''; 762 } 763 updateRateSum(box, json.data.breakdown); 764 } 765 }) 766 .catch(function (err) { 767 if (msg) { 768 msg.hidden = false; 769 msg.className = 'cmt-form-msg is-err'; 770 msg.textContent = 771 (err && err.message) || 772 (course.i18n && course.i18n.reviewsError) || 773 'Error'; 774 } 775 }) 776 .finally(function () { 777 form.removeAttribute('data-sending'); 778 sending = false; 779 if (btn) { 780 btn.disabled = false; 781 btn.textContent = 782 (course.i18n && course.i18n.submitLabel) || 783 'Submit'; 784 } 785 }); 786 }); 787 }); 788 } 789 790 /** 791 * After returning from the login redirect above, refill the (now 792 * logged-in) review form from the stashed draft and submit it right 793 * away, so the visitor never has to retype their review. 794 * 795 * @return {void} 796 */ 797 function restoreReviewDraft() { 798 var box = document.getElementById('reviews'); 799 if (!box) { 800 return; 801 } 802 803 var productId = box.getAttribute('data-product-id') || ''; 804 var key = 'zhaket_review_draft_' + productId; 805 var raw = null; 806 try { 807 raw = localStorage.getItem(key); 808 } catch (err) { 809 return; 810 } 811 if (!raw) { 812 return; 813 } 814 try { 815 localStorage.removeItem(key); 816 } catch (err) { 817 // Nothing to clean up if storage isn't writable. 818 } 819 820 var draft = null; 821 try { 822 draft = JSON.parse(raw); 823 } catch (err) { 824 return; 825 } 826 if (!draft || !draft.comment) { 827 return; 828 } 829 830 var form = box.querySelector( 831 '[data-review-form]:not([data-requires-login="1"])' 832 ); 833 if (!form) { 834 // Still logged out (or comments closed) -- leave the draft consumed; 835 // nothing sensible to restore it into. 836 return; 837 } 838 839 var comment = form.querySelector('[name="comment"]'); 840 var rating = form.querySelector('[name="rating"]'); 841 if (comment) { 842 comment.value = draft.comment; 843 } 844 if (rating && draft.rating) { 845 rating.value = draft.rating; 846 var stars = form.querySelectorAll('[data-star-rate] [data-value]'); 847 stars.forEach(function (s) { 848 var on = Number(s.getAttribute('data-value')) <= Number(draft.rating); 849 s.classList.toggle('on', on); 850 s.setAttribute('aria-pressed', on ? 'true' : 'false'); 851 }); 852 } 853 854 form.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true })); 855 } 856 857 initAccordion(); 858 initBuyBar(); 859 initVideo(); 860 initFrameFallback(); 861 initReviewsPager(); 862 initReviewSubmit(); 863 initStarRating(); 864 restoreReviewDraft(); 865})();
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.