1import { Controller } from "@hotwired/stimulus" 2import { isSpeechRecognitionSupported, startSpeechRecognition } from "lib/speech" 3import { toHalfWidthNumberText } from "lib/speech_number" 4 5const OVERLAY_MARGIN = 4 6// æ¢åã®æåã¯ãªã¢ãã¿ã³ (right: 0.6rem) ã¨æããå³ç«¯ããã®è·é¢ 7const BUTTON_RIGHT_INSET = 8 8const ERROR_STATUS_DURATION = 5000 9// ãã¤ã¯ãã¿ã³ã¸å°ãã§ãéãªãè¦ç´ ãæ¾ãããã«ããã¿ã³ã®å¹ ãçåãã¦æ¢ãåæ° 10const OVERLAP_PROBE_COUNT = 5 11// æ°éãå価ãªã©ã®æ°åæ¬ãèªã¿ä¸ãã®è¡¨è¨ãããåè§æ°åã¸ç´ãã¦ããå ¥ãã 12const NUMBER_INPUT_TYPES = ["number", "tel"] 13// æã§ã¹ã¯ãã¼ã«ã§ããé åã® overflow æå® 14const SCROLLABLE_OVERFLOWS = ["auto", "scroll"] 15// æ¤ç´¢ãã¼ããã¤ã¢ãã°ã®ããã«ããã¼ã¸ã®ä¸èº«ã¨ã¯å¥ã«ç»é¢ã¸è²¼ãä»ãã¦åã position æå® 16const PINNED_POSITIONS = ["fixed", "sticky"] 17// æ°åã 1 æåãã¤è¶³ãã¦ããé»åãã¿ã³ (components/number_input_buttons) 18const NUMBER_INPUT_BUTTONS_SELECTOR = ".components_easy_number_input_buttons" 19 20// ã¢ããªã® WebView ã«ã ãæ³¨å ¥ãããé³å£°å ¥åããªãã¸ãããã¨ãã 21// ç»é¢ä¸ã®ãã¹ã¦ã®ããã¹ãå ¥åæ¬ã¸ãã¤ã¯ãã¿ã³ãéãã¦è¡¨ç¤ºããã 22// body ã«æ¥ç¶ã㦠DOM ã®å¤åãç£è¦ããããã 23// Turbo ãªã©ã§åçã«æ¿å ¥ãããå ¥åæ¬ã«ããã©ã¼ã«ã¹æä½ãªãã§ãã¿ã³ãä»ãã 24export default class extends Controller { 25 static targets = ["overlay", "buttonTemplate", "status"] 26 27 connect() { 28 this.session = null 29 this.statusInput = null 30 // ã¹ãã¼ã¿ã¹ã¯ãã¤ã¯ãã¿ã³ã¨åãåãç¿ã¸ç§»ãã¦ä½¿ãåãã 31 // åãç¿ãã¨æ¶ãã¦ãä»ãç´ãããããtarget ã§ã¯ãªãè¦ç´ ã®åç §ãæã¡ç¶ãã 32 this.statusElement = this.statusTarget 33 this.hideStatusTimeout = null 34 this.buttonsByInput = new Map() 35 this.shiftedOverlaysByInput = new Map() 36 this.overlaysByHost = new Map() 37 this.syncScheduled = false 38 this.handleSync = this.scheduleSync.bind(this) 39 window.addEventListener("agrihubAppSpeechReady", this.handleSync) 40 // ã¹ã¯ãã¼ã«ã§å ¥åæ¬ãè¦ãé ãããã®ã§ã表示ã®åãæ¿ãã®ããã« capture ã§æ¾ã 41 window.addEventListener("scroll", this.handleSync, { capture: true, passive: true }) 42 window.addEventListener("resize", this.handleSync) 43 this.mutationObserver = new MutationObserver((records) => { 44 // èªåã管çãããã¿ã³ã®è¿½å ã»ç§»åã§ååæãç¶ããªãããã«ãã 45 if (records.every((record) => this.ownOverlayElement(record.target))) return 46 47 this.scheduleSync() 48 }) 49 this.mutationObserver.observe(this.element, { 50 childList: true, 51 subtree: true, 52 attributes: true, 53 attributeFilter: ["class", "style", "disabled", "readonly", "type", "hidden"] 54 }) 55 this.scheduleSync() 56 } 57 58 disconnect() { 59 this.mutationObserver.disconnect() 60 window.removeEventListener("agrihubAppSpeechReady", this.handleSync) 61 window.removeEventListener("scroll", this.handleSync, { capture: true }) 62 window.removeEventListener("resize", this.handleSync) 63 if (this.session != null) this.cancelSession() 64 } 65 66 scheduleSync() { 67 if (this.syncScheduled) return 68 69 this.syncScheduled = true 70 requestAnimationFrame(() => { 71 this.syncScheduled = false 72 this.sync() 73 }) 74 } 75 76 sync() { 77 if (!isSpeechRecognitionSupported()) return 78 79 const eligibleInputs = new Set( 80 [...this.element.querySelectorAll("textarea, input[type='text'], input[type='search'], input[type='number'], input[type='tel']")] 81 .filter((input) => this.eligibleInput(input)) 82 ) 83 84 for (const [input, button] of this.buttonsByInput) { 85 if (!eligibleInputs.has(input)) { 86 this.restoreShiftedOverlays(input) 87 button.remove() 88 this.buttonsByInput.delete(input) 89 this.shiftedOverlaysByInput.delete(input) 90 } 91 } 92 for (const host of this.overlaysByHost.keys()) { 93 if (!host.isConnected) this.overlaysByHost.delete(host) 94 } 95 for (const input of eligibleInputs) this.placeButton(input) 96 97 if (this.statusInput != null && !this.statusInput.isConnected) this.hideStatus() 98 99 // èªèä¸ã®å ¥åæ¬ãç»é¢ãã¨æ¶ããã¨ãã¯èªèãç ´æ£ãã 100 if (this.session != null && !this.session.cancelled && !this.session.input.isConnected) this.cancelSession() 101 } 102 103 eligibleInput(input) { 104 if (input.readOnly || input.disabled || "speechInputSkip" in input.dataset) return false 105 106 // Android ã®é³å£°èªèã¯é·æã«åããªããããã¡ã¢ãªã©ã®è¤æ°è¡æ¬ã«ã¯ãã¤ã¯ãåºããªã 107 return !(this.androidPlatform() && input.tagName === "TEXTAREA") 108 } 109 110 androidPlatform() { 111 return this.element.dataset.platform === "android" 112 } 113 114 placeButton(input) { 115 let button = this.buttonsByInput.get(input) 116 const inputRect = this.visibleRect(input) 117 118 if (inputRect == null) { 119 if (button != null) { 120 button.classList.add("d-none") 121 this.restoreShiftedOverlays(input) 122 } 123 return 124 } 125 126 if (button == null) { 127 button = this.buttonTemplateTarget.content.firstElementChild.cloneNode(true) 128 button.dataset.speechInputFor = input.id || input.name 129 this.buttonsByInput.set(input, button) 130 this.shiftedOverlaysByInput.set(input, new Map()) 131 }
132 const overlay = this.overlayFor(input) 133 if (button.parentElement !== overlay) overlay.append(button) 134 button.classList.remove("d-none") 135 136 const buttonSize = button.offsetHeight 137 // è¤æ°è¡ã®æ¬ã¯æ¸ãåºãã®éªéã«ãªããªãå³ä¸ã¸ã1 è¡ã®æ¬ã¯å³ç«¯ã®ä¸å¤®ã¸éãã 138 const buttonTop = input.tagName === "TEXTAREA" 139 ? inputRect.bottom - buttonSize - OVERLAY_MARGIN 140 : inputRect.top + (inputRect.height - buttonSize) / 2 141 // åãç¿ã¯å ¥åæ¬ã¨ä¸ç·ã«åãã®ã§ãåãç¿ããè¦ãä½ç½®ã§ç½®ãã°ã¹ã¯ãã¼ã«ä¸ããããªã 142 const overlayRect = overlay.getBoundingClientRect() 143 button.style.top = `${buttonTop - overlayRect.top}px` 144 button.style.left = `${inputRect.right - BUTTON_RIGHT_INSET - button.offsetWidth - overlayRect.left}px` 145 this.shiftOverlappingElementsLeft(input, inputRect, button, buttonTop, buttonSize) 146 } 147 148 // å ¥åæ¬ã¨åãåãæ¹ãããè¦ç´ ãã¨ã«åãç¿ãç¨æãã 149 overlayFor(input) { 150 const host = this.overlayHostFor(input) 151 if (host == null) return this.overlayTarget 152 153 const overlay = this.overlaysByHost.get(host) 154 if (overlay != null && overlay.isConnected) return overlay 155 156 const newOverlay = document.createElement("div") 157 // ã¹ã¯ãã¼ã«ããé åã§ã¯ä¸èº«ã¨ä¸ç·ã«åãå¿ è¦ãããã®ã§ãåãç¿ãä¸èº«ã¨ãã¦æµãè¾¼ã 158 const scrollerModifier = this.scrollableElement(host) ? " speech-input-overlay--scroller" : "" 159 newOverlay.className = `speech-input-overlay${scrollerModifier} d-print-none` 160 host.append(newOverlay) 161 this.overlaysByHost.set(host, newOverlay) 162 return newOverlay 163 } 164 165 // å ¥åæ¬ã¨ä¸ç·ã«åãä¸çªå å´ã®è¦ç´ ããã¼ã¸ã®ä¸èº«ã¨ä¸ç·ã«åãã ãã®ã¨ã㯠null ãè¿ã 166 overlayHostFor(input) { 167 for (let element = input.parentElement; element !== this.element; element = element.parentElement) { 168 if (this.scrollableElement(element) || this.pinnedElement(element)) return element 169 } 170 return null 171 } 172 173 scrollableElement(element) { 174 const { overflowX, overflowY } = getComputedStyle(element) 175 return SCROLLABLE_OVERFLOWS.includes(overflowX) || SCROLLABLE_OVERFLOWS.includes(overflowY) 176 } 177 178 // ãã¼ã¸ã®ä¸èº«ãæµãã¦ãç»é¢ã¸è²¼ãä»ããã¾ã¾åãè¦ç´ ãã©ãã 179 pinnedElement(element) { 180 return PINNED_POSITIONS.includes(getComputedStyle(element).position) 181 } 182 183 ownOverlayElement(element) { 184 return element.closest(".speech-input-overlay") 185 } 186 187 // ãã¤ã¯ãã¿ã³ã¯å¸¸ã«å ¥åæ¬ã®å³ç«¯ã«ç½®ããåä½è¡¨ç¤ºãæåã¯ãªã¢ãã¿ã³ãªã©å³ç«¯ã«éãªã£ã¦ããå¥ã®è¦ç´ ã¯ãã¤ã¯ã®å·¦é£ã¸ããã 188 shiftOverlappingElementsLeft(input, inputRect, button, buttonTop, buttonSize) { 189 const buttonLeft = inputRect.right - BUTTON_RIGHT_INSET - button.offsetWidth 190 const buttonRight = inputRect.right - BUTTON_RIGHT_INSET 191 const probeY = buttonTop + buttonSize / 2 192 const shiftedOverlays = this.shiftedOverlaysByInput.get(input) 193 194 // ãã¤ã¯ãã¿ã³èªèº«ã«å½ãããªããããå¤å®ä¸ã ãéãæããããã«ãã 195 button.style.pointerEvents = "none" 196 try { 197 for (const overlayElement of this.elementsOverButton(input, buttonLeft, buttonRight, probeY)) { 198 // position: absolute + right æå®ã®è¦ç´ ã ãã対象ããã以å¤ã¯åãããªãã®ã§è«¦ãã 199 const overlayStyle = getComputedStyle(overlayElement) 200 if (overlayStyle.position !== "absolute") continue 201 202 const overlayShift = overlayElement.getBoundingClientRect().right - (buttonLeft - OVERLAY_MARGIN) 203 if (overlayShift <= 0) continue 204 205 // ãã¤ã¯ãã¿ã³ãæ¶ããã¨ãã«å ã¸æ»ãããããåãã¦ãããè¦ç´ ã®æå®ãæ§ãã¦ãã 206 if (!shiftedOverlays.has(overlayElement)) shiftedOverlays.set(overlayElement, overlayElement.style.right) 207 208 overlayElement.style.right = `${parseFloat(overlayStyle.right) + overlayShift}px` 209 } 210 } finally { 211 button.style.pointerEvents = "" 212 } 213 } 214 215 // ãã¤ã¯ãã¿ã³ã®ä¸ã«æããã¦ããè¦ç´ ãããã¿ã³ã®å¹ ãçåããä½ç½®ã§æ¢ãã¦éããã 216 // åä½è¡¨ç¤º (.unit-over-input) ã®ããã«ãã¿ã³ã®å·¦ç«¯ã ãã¸é£ãè¾¼ãç´°ãè¦ç´ ãåããã¼ããªã 217 elementsOverButton(input, buttonLeft, buttonRight, probeY) { 218 const overlayElements = new Set() 219 // 左端ã¸é£ãè¾¼ãã ã ãã®è¦ç´ ãæ¾ããããããã¿ã³ã®å·¦ç«¯ããå³ç«¯ã¾ã§ãçééã«æ¢ã 220 const probeStep = (buttonRight - buttonLeft - 1) / (OVERLAP_PROBE_COUNT - 1) 221 222 for (let probeIndex = 0; probeIndex < OVERLAP_PROBE_COUNT; probeIndex += 1) { 223 for (const hitElement of document.elementsFromPoint(buttonLeft + 0.5 + probeStep * probeIndex, probeY)) { 224 // å ¥åæ¬ããå¾ãã«æããã¦ããè¦ç´ ã¯éãªã£ã¦ããªãã®ã§ãããã§æã¡åã 225 if (hitElement === input || hitElement.contains(input)) break 226 if (this.ownOverlayElement(hitElement)) continue 227 228 // ã¢ã¤ã³ã³ãªã©ã®ä¸èº«ã«å½ãã£ãã¨ãã¯ããã®è¦ªã®ãã¿ã³å ¨ä½ãããã 229 overlayElements.add(hitElement.closest("button, a, [role='button']") ?? hitElement) 230 } 231 } 232 return overlayElements 233 } 234 235 // ããããè¦ç´ ãå ã®æå®ã¸æ»ããæ»ã対象ãç¡ãã¨ãã¯æ¸ãè¾¼ã¾ãªãã®ã§ã 236 // style ã®å¤åãæ¾ã MutationObserver ã¨ã®éã§åæãç¹°ãè¿ããªã 237 restoreShiftedOverlays(input) { 238 const shiftedOverlays = this.shiftedOverlaysByInput.get(input) 239 for (const [overlayElement, originalRight] of shiftedOverlays) overlayElement.style.right = originalRight 240 shiftedOverlays.clear() 241 } 242 243 // ã¹ãã¼ã¿ã¹ããã¤ã¯ãã¿ã³ã¨åãåãç¿ã¸å ¥ãã¦ãå ¥åæ¬ã®å³ä¸ã¸ç½®ãã 244 // åãç¿ãå ¥åæ¬ã¨ä¸ç·ã«åãã®ã§ãã¹ã¯ãã¼ã«ä¸ã«ä½ç½®ãè¨ç®ãç´ããªãã¦ããããªã 245 placeStatus(input) { 246 // èªèä¸ã«æ¬ãç·¨éã§ããªããªãã¨ãã¤ã¯ãã¿ã³ã¯å¤ããããããåãç¿ã¯ãã¿ã³ããã§ã¯ãªãå ¥åæ¬ããæ±ºãã
247 const overlay = this.overlayFor(input) 248 if (this.statusElement.parentElement !== overlay) overlay.append(this.statusElement) 249 250 const inputRect = input.getBoundingClientRect() 251 const overlayRect = overlay.getBoundingClientRect() 252 this.statusElement.style.top = `${inputRect.bottom + OVERLAY_MARGIN - overlayRect.top}px` 253 this.statusElement.style.right = `${overlayRect.right - inputRect.right}px` 254 } 255 256 // ç»é¢å ã«è¦ãã¦ããå ¥åæ¬ã®ä½ç½®ãè¿ãããã¤ã¢ãã°ãªã©ã®ä¸ã«é ãã¦ããæ¬ã«ã¯ null ãè¿ã 257 visibleRect(input) { 258 const rect = input.getBoundingClientRect() 259 if (rect.width === 0 || rect.height === 0) return null 260 if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth) return null 261 262 const centerY = rect.top + rect.height / 2 263 const probePoints = [ 264 [rect.left + 2, centerY], 265 [rect.left + rect.width / 2, centerY] 266 ] 267 const uncovered = probePoints.some(([x, y]) => { 268 const hitElement = document.elementFromPoint(x, y) 269 return hitElement === input || input.contains(hitElement) 270 }) 271 return uncovered ? rect : null 272 } 273 274 // ãã¿ã³ã®ã¿ããã§å ¥åæ¬ã®ãã©ã¼ã«ã¹ã¨ã«ã¼ã½ã«ä½ç½®ã失ããªãããã«ãã 275 keepFocus(event) { 276 event.preventDefault() 277 } 278 279 toggle(event) { 280 const button = event.currentTarget 281 if (this.session?.button === button) { 282 this.stop() 283 return 284 } 285 286 this.start(this.inputFor(button), button) 287 } 288 289 inputFor(button) { 290 for (const [input, candidate] of this.buttonsByInput) { 291 if (candidate === button) return input 292 } 293 } 294 295 start(input, button) { 296 // å¥ã®æ¬ã§èªèä¸ãªãããã®å 容ã確å®ããã¦ããæ°ããæ¬ã§éå§ããã 297 // 確å®ããå ¨æã¯èªèã®çµããã«å±ããããåã®æ¬ã¸ã®åæ ã¯ãã®ã»ãã·ã§ã³ã® onEnd ã«ä»»ãã 298 if (this.session != null) { 299 this.stopRecognition(this.session) 300 this.setIdleState(this.session.button) 301 this.session = null 302 } 303 304 // èªèä¸ã«å¥ã®æ¬ã¸ç§»ã£ã¦ããéä¸çµéãéå§ããæ¬ã¸åæ ãç¶ããããã«ã»ãã·ã§ã³ã¸æãã 305 const numberInput = this.numberOnlyInput(input) 306 const session = { 307 input, 308 button, 309 numberInput, 310 originalValue: input.value, 311 // æ°åæ¬ã¯è¨ãç´ãã®ãã³ã«æ¬ãã¨å ¥ãæ¿ãããããæ¿å ¥ä½ç½®ãæããªã 312 insertionRange: numberInput ? null : this.insertionRangeFor(input) 313 } 314 this.session = session 315 const isCurrentSession = () => this.session === session 316 317 this.setListeningState(button) 318 this.showStatus(input, "é³å£°å ¥åãéå§ãã¦ãã¾ãâ¦") 319 320 try { 321 session.recognition = startSpeechRecognition({ 322 locale: "ja-JP", 323 onStart: () => { if (isCurrentSession()) this.showStatus(input, "é³å£°ãå ¥åãã¦ãã¾ãâ¦") }, 324 onTranscript: (transcript) => this.insertTranscript(session, transcript), 325 onError: (message) => { if (isCurrentSession()) this.showStatus(input, message, true) }, 326 // å¥ã®æ¬ã¸ç§»ã£ããã¨ã«çµãã£ãã¨ãããå ¥ãçµãã£ãæ¬ã®è¨ç®ã¯åãã 327 onEnd: () => { 328 this.commitValue(session) 329 if (isCurrentSession()) this.finishListening() 330 } 331 }) 332 } catch { 333 this.finishListening() 334 this.showStatus(input, "é³å£°å ¥åãéå§ã§ãã¾ããã§ãã", true) 335 } 336 } 337 338 // 忢ãä¼ãã¦ãã表示ãå¤ããã表示ããå ã«æ¢ãã¦ããã°ãèªèã ããåãç¶ãã¦æ¬¡ã®éå§ãæã¾ãããã¨ããªã 339 stop() { 340 this.stopRecognition(this.session) 341 this.session.button.disabled = true 342 this.showStatus(this.session.input, "é³å£°å ¥åãçµäºãã¦ãã¾ãâ¦") 343 } 344 345 // é³å£°å ¥åä¸ã«ç»é¢ã®ä»ã®å ´æãã¿ããããããããã§å ¥åã確å®ãã¦çµããã 346 // 忢ãã¿ã³ãæ¼ãå¿ãã¦ããæåãªã©ã®å ¥åãçµãã¦ããåãè¨ç®ãç½®ãã¦ããããªãããã«ãã 347 stopOnOutsideTap(event) { 348 if (this.session == null) return 349 // ãã¤ã¯ãã¿ã³ã®ã¿ãã㯠toggle ãæ±ã 350 if (this.ownOverlayElement(event.target)) return 351 // èªèä¸ã®æ¬ãã®ãã®ã®ã¿ããã¯ãã«ã¼ã½ã«ãåãããã ããªã®ã§ç¶ãã 352 if (event.target === this.session.input) return 353 354 this.stop() 355 } 356 357 // ã¢ããªã¯åæ¢ãäºåº¦ä¼ããããã¨æ¬¡ã®éå§ãæãããã忢ã¯ä¸åº¦ã ãä¼ãã 358 stopRecognition(session) { 359 if (session.stopping) return 360 361 session.stopping = true 362 session.recognition.stop() 363 } 364 365 cancelSession() { 366 this.session.cancelled = true 367 this.session.recognition.cancel() 368 } 369 370 finishListening() { 371 const session = this.session 372 this.setIdleState(session.button) 373 this.session = null 374 375 // éä¸çµéã§ã¯æ°åã«ãªãã®ãå¾ ã¤ãããæå¾ã¾ã§æ°åã«ãªããªãã£ãã¨ãã«ã ãç¥ãããã 376 // ç»é¢ãã¨æ¶ããæ¬ãç ´æ£ããã¨ãã¯ãç¥ãããå ´æãç¡ãã®ã§
376åºããªã 377 if (session.numberNotRecognized && !session.cancelled) this.showStatus(session.input, "æ°åãèãåãã¾ããã§ãã", true) 378 } 379 380 // å¤ãä»£å ¥ãã¦ããã©ã¦ã¶ã¯ change ãåºããªããããèªèã®çµããã«èªåã§éãã 381 // åè¨ä½æ¥æéã®åè¨ç®ãåç©«æã®å ¥åæ¬ã®åãæ¿ããªã©ãæ¬ã«ç»é²æ¸ã¿ã® change å¦çã¸å¼ãç¶ãã 382 // éä¸çµéã®ãã³ã«éãã¨åç©«ãã§ãã¯ã®éä¿¡ãç¹°ãè¿ãã®ã§ãèªèãã¨ã«ä¸åº¦ã ãéãã 383 // å¥ã®æ¬ã¸ç§»ã£ããã¨ã§åã®æ¬ã®ç¢ºå®çµæãå±ããã¨ããé£åãåãæ®ãããªããããã»ãã·ã§ã³åä½ã§è¡ã 384 commitValue(session) { 385 if (session.cancelled || session.input.value === session.originalValue) return 386 387 session.input.dispatchEvent(new Event("change", { bubbles: true })) 388 } 389 390 insertTranscript(session, transcript) { 391 if (session.numberInput) { 392 this.insertNumber(session, transcript) 393 return 394 } 395 396 const prefix = session.originalValue.slice(0, session.insertionRange.start) 397 const suffix = session.originalValue.slice(session.insertionRange.end) 398 session.input.value = `${prefix}${transcript}${suffix}` 399 400 const caretPosition = prefix.length + transcript.length 401 session.input.setSelectionRange(caretPosition, caretPosition) 402 this.dispatchInput(session.input) 403 } 404 405 // æ¤ç´¢ãåè¨ç®ãåããã¦ããåç»é¢ã¯ãæå ¥åã keyup ã§æ¾ã£ã¦ããã 406 // é³å£°ã§å ¥ããæåãæã§æã£ãã¨ãã¨åãããã«æ±ããããããinput ã¨åãã㦠keyup ãç¥ããã 407 dispatchInput(input) { 408 input.dispatchEvent(new Event("input", { bubbles: true })) 409 input.dispatchEvent(new Event("keyup", { bubbles: true })) 410 } 411 412 // æ°åã ããå ¥ããæ¬ãã©ããã 413 // å°æ°ãæã¡çµããã¾ã§å¤ãä¿ã¦ããã type='text' ã«ãã¦ããæ¬ãã 414 // é»åãã¿ã³ãå¤ãå ¥ããå ã«ãªã£ã¦ãããªãæ°åã ãã®æ¬ã¨ãã¦æ±ã 415 numberOnlyInput(input) { 416 if (NUMBER_INPUT_TYPES.includes(input.type)) return true 417 418 return [...this.element.querySelectorAll(NUMBER_INPUT_BUTTONS_SELECTOR)].some((numberInputButtons) => ( 419 input.matches(numberInputButtons.getAttribute("target_name")) && 420 numberInputButtons.closest(numberInputButtons.getAttribute("parents_name")).contains(input) 421 )) 422 } 423 424 // æ°åæ¬ã¯è¨ãç´ãã§å¤ã䏿¸ããã¦ããããã追è¨ã§ã¯ãªãæ¬å ¨ä½ãèªèããæ°å¤ã¸ç½®ãæãã 425 insertNumber(session, transcript) { 426 const numberText = toHalfWidthNumberText(transcript) 427 session.numberNotRecognized = numberText == null 428 if (numberText == null) return 429 430 session.input.value = numberText 431 this.dispatchInput(session.input) 432 } 433 434 insertionRangeFor(input) { 435 // ãã©ã¼ã«ã¹ããã¯ã³ã¿ããã§éå§ããã¨ãã¯æ«å°¾ã¸è¿½è¨ãã 436 if (document.activeElement !== input) return { start: input.value.length, end: input.value.length } 437 438 return { start: input.selectionStart, end: input.selectionEnd } 439 } 440 441 setListeningState(button) { 442 button.disabled = false 443 button.classList.add("speech-input-button--listening") 444 button.setAttribute("aria-label", "é³å£°å ¥åã忢") 445 button.setAttribute("aria-pressed", "true") 446 const icon = button.querySelector("i") 447 icon.classList.remove("fa-microphone") 448 icon.classList.add("fa-stop") 449 } 450 451 setIdleState(button) { 452 button.disabled = false 453 button.classList.remove("speech-input-button--listening") 454 button.setAttribute("aria-label", "é³å£°å ¥åãéå§") 455 button.setAttribute("aria-pressed", "false") 456 const icon = button.querySelector("i") 457 icon.classList.remove("fa-stop") 458 icon.classList.add("fa-microphone") 459 460 if (!this.statusElement.classList.contains("text-danger")) this.hideStatus() 461 } 462 463 showStatus(input, message, error = false) { 464 // æ¬ãç»é¢ããæ¶ãããã¨ã«èªèã®çµäºã忢ãå±ããã¨ãããã 465 // ç½®ãå ´æã«ãªãåãç¿ãæ¬ãã辿ããªãã®ã§ãç¥ããã¯åºããã«çä»ãã 466 if (!input.isConnected) { 467 this.hideStatus() 468 return 469 } 470 471 clearTimeout(this.hideStatusTimeout) 472 this.statusInput = input 473 this.statusElement.textContent = message 474 this.statusElement.classList.toggle("text-danger", error) 475 this.statusElement.classList.toggle("text-secondary", !error) 476 this.statusElement.classList.remove("d-none") 477 this.placeStatus(input) 478 479 // ã¨ã©ã¼ã¯èªããæéã ãåºãã¦èªåã§æ¶ãï¼å¸¸æè¡¨ç¤ºã®ãã¿ã³ã®å¨ãã«æ®ãç¶ããªãããã«ãã) 480 if (error) this.hideStatusTimeout = setTimeout(() => this.hideStatus(), ERROR_STATUS_DURATION) 481 } 482 483 hideStatus() { 484 clearTimeout(this.hideStatusTimeout) 485 this.statusInput = null 486 this.statusElement.classList.add("d-none") 487 } 488};
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.