1(function () { 2 'use strict'; 3 const phone = window.matchMedia('(max-width: 767px)'); 4 let cleanup = () => {}; 5 6 function update() { 7 cleanup(); 8 cleanup = () => {}; 9 if (!phone.matches) return; 10 const undo = []; 11 const chinese = document.body.dataset.mobileLocale === 'zh'; 12 const listen = (node, type, fn) => { 13 node.addEventListener(type, fn); 14 undo.push(() => node.removeEventListener(type, fn)); 15 }; 16 const attribute = (node, name, value) => { 17 const previous = node.getAttribute(name); 18 if (value === null) node.removeAttribute(name); else node.setAttribute(name, value); 19 undo.push(() => previous === null ? node.removeAttribute(name) : node.setAttribute(name, previous)); 20 }; 21 22 // Internal navigation stays in the same tab on a phone. 23 document.querySelectorAll('a[target="_blank"][href^="/blog/"], a[target="_blank"][href^="/product/"]').forEach(link => attribute(link, 'target', null)); 24 document.querySelectorAll('.support p').forEach(paragraph => { 25 Array.from(paragraph.childNodes).filter(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim() === '[email protected]').forEach(node => { 26 const link = document.createElement('a'); 27 link.href = 'mailto:[email protected]'; 28 link.textContent = node.textContent.trim(); 29 node.replaceWith(link); 30 undo.push(() => link.replaceWith(node)); 31 }); 32 }); 33 34 let dialog; 35 let qrTrigger; 36 const closeQR = () => { if (dialog?.open) dialog.close(); }; 37 document.querySelectorAll('.support .qrcode-icon, .partners .qrcode-icon').forEach(trigger => { 38 const source = trigger.querySelector('img'); 39 if (!source) return; 40 const title = trigger.parentElement.querySelector('h5, p')?.textContent.trim() || (chinese ? 'äºç»´ç ' : 'QR code'); 41 attribute(trigger, 'role', 'button'); 42 attribute(trigger, 'tabindex', '0'); 43 attribute(trigger, 'aria-label', chinese ? `æ¥ç${title}äºç»´ç ` : `Show ${title} QR code`); 44 attribute(trigger, 'aria-haspopup', 'dialog'); 45 attribute(trigger, 'aria-expanded', 'false'); 46 const openQR = () => { 47 if (!dialog) { 48 dialog = document.createElement('dialog'); 49 dialog.className = 'mobile-qr-dialog'; 50 dialog.setAttribute('aria-labelledby', 'mobile-qr-title'); 51 const close = document.createElement('button'); 52 close.type = 'button'; 53 close.textContent = 'Ã'; 54 close.setAttribute('aria-label', chinese ? 'å ³éäºç»´ç ' : 'Close QR code'); 55 close.addEventListener('click', closeQR); 56 const heading = document.createElement('h2'); 57 heading.id = 'mobile-qr-title'; 58 const image = document.createElement('img'); 59 const note = document.createElement('p'); 60 note.textContent = chinese ? 'å¯é¿æä¿åäºç»´ç ï¼æä½¿ç¨å¦ä¸å°è®¾å¤æ«ç ã' : 'Touch and hold to save this QR code, or scan it with another device.'; 61 dialog.append(close, heading, image, note); 62 dialog.addEventListener('click', event => { 63 const bounds = dialog.getBoundingClientRect(); 64 if (event.target === dialog && (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom)) closeQR(); 65 }); 66 dialog.addEventListener('close', () => { 67 if (phone.matches) { 68 qrTrigger?.setAttribute('aria-expanded', 'false'); 69 qrTrigger?.focus({ preventScroll: true }); 70 } 71 }); 72 document.body.append(dialog); 73 } 74 qrTrigger = trigger; 75 dialog.querySelector('h2').textContent = title; 76 dialog.querySelector('img').src = source.currentSrc || source.src; 77 dialog.querySelector('img').alt = chinese ? `${title}äºç»´ç ` : `${title} QR code`; 78 trigger.setAttribute('aria-expanded', 'true'); 79 dialog.showModal(); 80 }; 81 listen(trigger, 'click', event => { event.preventDefault(); openQR(); }); 82 listen(trigger, 'keydown', event => { 83 if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openQR(); } 84 }); 85 }); 86 undo.push(() => { closeQR(); dialog?.remove(); }); 87
88 // Do not advance a slide while someone is reading it on a small screen. 89 const carousel = document.querySelector('#carouselExampleCaptions'); 90 if (carousel && window.jQuery?.fn.carousel) { 91 window.jQuery(carousel).carousel('pause'); 92 undo.push(() => window.jQuery(carousel).carousel('cycle')); 93 const status = document.createElement('span'); 94 status.className = 'mobile-carousel-status'; 95 status.setAttribute('aria-live', 'polite'); 96 const updateStatus = () => { 97 const slides = [...carousel.querySelectorAll('.carousel-item')]; 98 status.textContent = `${slides.findIndex(slide => slide.classList.contains('active')) + 1} / ${slides.length}`; 99 }; 100 carousel.append(status); 101 updateStatus(); 102 window.jQuery(carousel).on('slid.bs.carousel', updateStatus); 103 undo.push(() => { window.jQuery(carousel).off('slid.bs.carousel', updateStatus); status.remove(); }); 104 } 105 106 const reviews = document.querySelector('.comments-div .comment-list'); 107 if (reviews) { 108 attribute(reviews, 'tabindex', '0'); 109 attribute(reviews, 'aria-label', chinese ? 'ç¨æ·è¯ä»·ï¼å¯å·¦å³æ»å¨æ¥ç' : 'User reviews, scroll horizontally to read more'); 110 // Size the horizontal viewport to the visible review, not the longest one. 111 const originalHeight = reviews.style.height; 112 let heightFrame = 0; 113 const fitReview = () => { 114 heightFrame = 0; 115 const left = reviews.getBoundingClientRect().left; 116 const items = [...reviews.querySelectorAll('.comment-item')].filter(item => item.getClientRects().length); 117 const current = items.reduce((closest, item) => !closest || Math.abs(item.getBoundingClientRect().left - left) < Math.abs(closest.getBoundingClientRect().left - left) ? item : closest, null); 118 if (!current) return; 119 const style = getComputedStyle(reviews); 120 const scrollbar = reviews.offsetHeight - reviews.clientHeight; 121 const height = `${Math.ceil(current.getBoundingClientRect().height + parseFloat(style.paddingTop) + parseFloat(style.paddingBottom) + scrollbar)}px`; 122 if (reviews.style.height !== height) reviews.style.height = height; 123 }; 124 const scheduleHeight = () => { 125 if (!heightFrame) heightFrame = requestAnimationFrame(fitReview); 126 }; 127 listen(reviews, 'scroll', scheduleHeight); 128 listen(window, 'resize', scheduleHeight); 129 const sizeObserver = new ResizeObserver(scheduleHeight); 130 reviews.querySelectorAll('.comment-item').forEach(item => sizeObserver.observe(item)); 131 scheduleHeight(); 132 undo.push(() => { 133 sizeObserver.disconnect(); 134 cancelAnimationFrame(heightFrame); 135 reviews.style.height = originalHeight; 136 }); 137 } 138 139 document.querySelectorAll('#android_store_download_link, #android_tv_download_link').forEach(link => listen(link, 'click', event => event.preventDefault())); 140 document.querySelectorAll('.download-overlay').forEach(overlay => { 141 const panel = overlay.querySelector('.download-popup'); 142 const close = overlay.querySelector('.download-close-btn'); 143 if (!panel || !close) return; 144 attribute(panel, 'role', 'dialog'); 145 attribute(panel, 'aria-modal', 'true'); 146 attribute(panel, 'aria-label', overlay.querySelector('.download-title')?.textContent.trim() || (chinese ? 'ä¸è½½åºç¨' : 'Download app')); 147 attribute(close, 'aria-label', chinese ? 'å ³éä¸è½½çªå£' : 'Close download dialog'); 148 let returnFocus; 149 let wasOpen = false; 150 const focusOnChange = () => { 151 const open = overlay.style.display === 'flex'; 152 if (open && !wasOpen) { returnFocus = document.activeElement; close.focus({ preventScroll: true }); } 153 if (!open && wasOpen) returnFocus?.focus({ preventScroll: true }); 154 wasOpen = open; 155 }; 156 const observer = new MutationObserver(focusOnChange); 157 observer.observe(overlay, { attributes: true, attributeFilter: ['style'] }); 158 listen(panel, 'keydown', event => { 159 if (event.key === 'Escape') { event.stopPropagation(); close.click(); } 160 if (event.key !== 'Tab') return; 161 const controls = [...panel.querySelectorAll('button, a[href], input, select, textarea, [tabindex="0"]')].filter(node =>
161 node.getClientRects().length); 162 const first = controls[0], last = controls[controls.length - 1]; 163 if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } 164 else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } 165 }); 166 undo.push(() => observer.disconnect()); 167 }); 168 cleanup = () => undo.reverse().forEach(fn => fn()); 169 } 170 document.addEventListener('DOMContentLoaded', update, { once: true }); 171 phone.addEventListener('change', update); 172})();
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.