1/* 2 * html5 speech recognition api for audio input 3 */ 4function startDictation(searchform, searchbox, voiceicon, speakText, searchText) { 5 document.getElementById(searchbox).focus(); 6 if (window.hasOwnProperty('webkitSpeechRecognition')) { 7 var recognition = new webkitSpeechRecognition(); 8 recognition.continuous = false; 9 recognition.interimResults = false; 10 recognition.start(); 11 document.getElementById(searchbox).placeholder = speakText; 12 document.getElementById(searchbox).classList.add('voiceactive'); 13 document.getElementById(voiceicon).style['color'] = '#a01030'; 14 recognition.onresult = function (e) { 15 document.getElementById(searchbox).value = e.results[0][0].transcript; 16 recognition.stop(); 17 document.getElementById(searchform).submit(); 18 }; 19 recognition.onerror = function (e) { 20 recognition.stop(); 21 }; 22 recognition.onend = function (e) { 23 document.getElementById(searchbox).placeholder = searchText; 24 document.getElementById(searchbox).classList.remove('voiceactive'); 25 document.getElementById(voiceicon).style['color'] = '#888'; 26 }; 27 } 28} 29 30/* 31 * Add events to button clicks tied to a corresponding dialog. 32 */ 33function linkDialogAndButtons(dialog_id, button_open_id, button_close_id) 34{ 35 const dialog = document.getElementById(dialog_id); 36 document.getElementById(button_open_id).addEventListener('click', () => { dialog.showModal() }); 37 document.getElementById(button_close_id).addEventListener('click', () => { dialog.close() }); 38} 39 40/* 41 * URL templates for the social media sharing dialogue, keyed to match the id suffix of each provider's 42 * anchor in the dialogue (eg 'baidu' -> #button-share-baidu). `url` and `title` are already 43 * encodeURIComponent()-escaped by the caller. 44 */ 45const shareLinkBuilders = { 46 baidu: (url, title) => 'http://tieba.baidu.com/i/app/open_share_api?link=' + url + '&title=' + title, 47 blogger: (url, title) => 'https://www.blogger.com/blog-this.g?u=' + url + '&n=' + title, 48 // Reference: https://docs.bsky.app/docs/advanced-guides/intent-links 49 bluesky: (url, title) => 'https://bsky.app/intent/compose?text=' + title + '%20' + url, 50 // Reference: https://developers.facebook.com/docs/plugins/share-button/ 51 facebook: (url, title) => 'https://www.facebook.com/sharer/sharer.php?u=' + url, 52 // Reference: https://developers.line.biz/en/docs/line-social-plugins/install-guide/using-line-share-buttons/ 53 line: (url, title) => 'https://social-plugins.line.me/lineit/share?url=' + url + '&text=' + title, 54 // Reference: https://www.linkedin.com/pulse/how-make-custom-linkedin-share-button-your-website-ahmad-hayat-qm61f 55 linkedin: (url, title) => 'https://www.linkedin.com/shareArticle?url=' + url + '&title=' + title, 56 okru: (url, title) => 'https://connect.ok.ru/dk?st.shareUrl=' + url + '&st.title=' + title + '&st.cmd=WidgetSharePreview', 57 pinterest: (url, title) => 'https://www.pinterest.com/pin/create/button/?url=' + url + '&description=' + title, 58 qzone: (url, title) => 'http://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url=' + url + '&title=' + title, 59 reddit: (url, title) => 'https://www.reddit.com/submit?url=' + url + '&title=' + title, 60 // Reference: https://core.telegram.org/widgets/share 61 telegram: (url, title) => 'https://t.me/share?url=' + url + '&text=' + title, 62 threads: (url, title) => 'https://threads.net/intent/post?text=' + title + '%20' + url, 63 // Reference: https://help.tumblr.com/knowledge-base/share-button-documentation/ 64 tumblr: (url, title) => 'https://www.tumblr.com/widgets/share/tool?canonicalUrl=' + url + '&title=' + title, 65 // Reference: https://developers.viber.com/docs/tools/share-button/ 66 viber: (url, title) => 'viber://forward?text=' + title + '%20' + url, 67 // Reference: https://dev.vk.com/en/widgets/share 68 vk: (url, title) => 'https://vk.com/share.php?url=' + url + '&title=' + title, 69 weibo: (url, title) => 'http://service.weibo.com/share/share.php?url=' + url + '&title=' + title, 70 whatsapp: (url, title) => 'https://wa.me/?text=' + title + '%20' + url, 71 x: (url, title) => 'https://twitter.com/intent/tweet?url=' + url + '&text=' + title, 72 // E-mail at end to separate from social media platforms. 73 email: (url, title) => 'mailto:?body=' + url + '&subject=' + title, 74}; 75 76// Tracks the currently open share target so the analytics event below can report it. 77let currentShareUrl = ''; 78 79/* 80 * Populate the single, page-wide share dialogue with a specific item's link and title, without 81 * opening it. Called both to seed the page-level default share target on page load, and by 82 * openShareDialog() below before it shows the dialogue - so any number of items on a page can share 83 * the one dialogue instead of each carrying its own copy. 84 */ 85function populateShareDialog(url, title, qrcodeUrl) { 86 currentShareUrl = url; 87 const encodedUrl = encodeURIComponent(url); 88 const encodedTitle = encodeURIComponent(title); 89 90 for (const [id, buildLink] of Object.entries(shareLinkBuilders)) { 91 document.getElementById('button-share-' + id).href = buildLink(encodedUrl, encodedTitle); 92 } 93 94 document.getElementById('link-share-text').value = url; 95 96 const qrcodeImage = document.getElementById('link-share-qrcode');
97 qrcodeImage.src = qrcodeUrl; 98 qrcodeImage.alt = 'QR code for ' + url; 99} 100 101/* 102 * Populate the share dialogue for a specific item and open it. Used by share-trigger clicks. 103 */ 104function openShareDialog(url, title, qrcodeUrl) { 105 populateShareDialog(url, title, qrcodeUrl); 106 document.getElementById('dialog-share').showModal(); 107} 108 109/* 110 * Registered globally so picturebook-player can invoke it by name via an action's 'callback' 111 * field (see @glorecnet/gap-list's invokeActionCallback()), which calls it with a single params 112 * object rather than openShareDialog()'s positional arguments. 113 */ 114function shareTrack(params) { 115 openShareDialog(params.url, params.title, params.qrCodeUrl); 116} 117 118/* 119 * Initialise the page-wide share dialogue: the close button, the copy-link button, per-provider click 120 * analytics, and delegated clicks from any number of share-trigger buttons/icons on the page (each 121 * carrying data-share-url/title/qrcode attributes, eg from grn_share_button() in common.php). 122 * Delegating on document means trigger elements can appear anywhere in the page, in any order 123 * relative to the dialogue itself. 124 */ 125function initShareDialog() { 126 document.getElementById('button-share-close').addEventListener('click', () => { 127 document.getElementById('dialog-share').close(); 128 }); 129 130 document.getElementById('link-share-copy').addEventListener('click', () => { 131 navigator.clipboard.writeText(document.getElementById('link-share-text').value); 132 133 const copyNotification = document.getElementById('link-share-text-copied'); 134 copyNotification.style.visibility = 'visible'; 135 setTimeout(() => { copyNotification.style.visibility = 'hidden'; }, 2000); 136 }); 137 138 // Google Tags for Analytics - gtag() is defined in html_scripttags() of htmltags.php, called from grn_startdoc(). 139 // Reference: https://developers.google.com/analytics/devguides/collection/ga4/reference/events#share 140 const shareLinks = document.getElementById('dialog-share-container').getElementsByTagName('a'); 141 for (const shareLink of shareLinks) { 142 shareLink.addEventListener('click', () => { 143 // Expected ID format: button-share-XXX 144 gtag('event', 'share', { 'method': shareLink.id.split('-')[2], 'content_type': document.documentElement.lang, 'item_id': currentShareUrl }); 145 }); 146 } 147 148 document.addEventListener('click', (event) => { 149 const trigger = event.target.closest('.button-share-trigger'); 150 if (trigger) { 151 openShareDialog(trigger.dataset.shareUrl, trigger.dataset.shareTitle, trigger.dataset.shareQrcode); 152 } 153 }); 154} 155 156/* 157 * Convert <time> fields to localised variants, including both relative and absolute dates. 158 */ 159function localiseDates(language_tag) { 160 document.querySelectorAll('time[datetime]').forEach(timeElement => { 161 const elementDate = new Date(timeElement.dateTime); 162 const now = new Date(); 163 const elapsedSeconds = (elementDate - now) / 1000; 164 165 const timeUnits = [ 166 ['year', 31536000], 167 ['month', 2592000], 168 ['week', 604800], 169 ['day', 86400] 170 ]; 171 172 const relativeFormatter = new Intl.RelativeTimeFormat(language_tag, { numeric: 'auto' }); 173 const absoluteFormatter = new Intl.DateTimeFormat(language_tag, { dateStyle: 'long' }); 174 175 for (const [timeUnit, seconds] of timeUnits) { 176 const elapsedTime = elapsedSeconds / seconds; 177 if (Math.abs(elapsedTime) >= 1 || timeUnit === 'day') { 178 timeElement.textContent = relativeFormatter.format(Math.round(elapsedTime), timeUnit); 179 break; 180 } 181 } 182 183 timeElement.title = absoluteFormatter.format(elementDate); 184 }); 185}
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.