1import { Controller } from "@hotwired/stimulus" 2 3// chat--input: handles the composer textarea behavior â auto-grow, 4// Enter-submits, disabled-when-empty, in-flight spinner, and 5// optimistic clear-on-submit. 6export default class extends Controller { 7 static targets = ["input", "submit", "sendIcon", "spinner"] 8 9 connect() { 10 this.onSubmitStart = this.onSubmitStart.bind(this) 11 this.onSubmitEnd = this.onSubmitEnd.bind(this) 12 this.element.addEventListener("turbo:submit-start", this.onSubmitStart) 13 this.element.addEventListener("turbo:submit-end", this.onSubmitEnd) 14 this.updateDisabled() 15 } 16 17 disconnect() { 18 this.element.removeEventListener("turbo:submit-start", this.onSubmitStart) 19 this.element.removeEventListener("turbo:submit-end", this.onSubmitEnd) 20 } 21 22 isEmpty() { 23 return !this.hasInputTarget || this.inputTarget.value.trim() === "" 24 } 25 26 // Stays disabled while a message is in flight, even as the user types the next one. 27 updateDisabled() { 28 if (this.hasSubmitTarget) { 29 this.submitTarget.disabled = this.busy || this.isEmpty() 30 } 31 } 32 33 setBusy(busy) { 34 this.busy = busy 35 this.updateDisabled() 36 if (this.hasSubmitTarget) { 37 busy ? this.submitTarget.setAttribute("aria-busy", "true") : this.submitTarget.removeAttribute("aria-busy") 38 } 39 if (this.hasSendIconTarget) { 40 this.sendIconTarget.classList.toggle("hidden", busy) 41 } 42 if (this.hasSpinnerTarget) { 43 this.spinnerTarget.classList.toggle("hidden", !busy) 44 } 45 } 46 47 onSubmitStart() { 48 this.setBusy(true) 49 } 50 51 onSubmitEnd() { 52 this.setBusy(false) 53 } 54 55 autoGrow() { 56 if (!this.hasInputTarget) return 57 const el = this.inputTarget 58 el.style.height = "auto" 59 el.style.height = `${el.scrollHeight}px` 60 this.updateDisabled() 61 } 62 63 handleKey(event) { 64 if (event.key === "Enter" && !event.shiftKey && !event.isComposing) { 65 event.preventDefault() 66 if (!this.isEmpty()) { 67 this.element.requestSubmit() 68 } 69 } 70 } 71 72 submit(event) { 73 if (this.isEmpty()) { 74 event.preventDefault() 75 return 76 } 77 requestAnimationFrame(() => { 78 if (this.hasInputTarget) { 79 this.inputTarget.value = "" 80 this.inputTarget.style.height = "auto" 81 } 82 this.updateDisabled() 83 }) 84 } 85};
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.