1import { Controller } from "@hotwired/stimulus"; 2 3export default class extends Controller { 4 static targets = ["input", "confirmButton", "checkbox"]; 5 static values = { 6 confirmText: { type: String, default: "" }, 7 caseSensitive: { type: Boolean, default: false }, 8 }; 9 10 connect() { 11 this.confirmed = false; 12 this.confirmTextArray = this.parseConfirmText(); 13 this.updateConfirmButton(); 14 15 // Add form submission prevention 16 const form = this.element.closest("form"); 17 if (form) { 18 this.boundHandleSubmit = this.handleSubmit.bind(this); 19 form.addEventListener("submit", this.boundHandleSubmit); 20 } 21 22 // Add input listeners 23 if (this.hasInputTarget) { 24 this.inputTarget.addEventListener("input", () => this.checkConfirmation()); 25 this.inputTarget.addEventListener("keydown", (event) => { 26 if (event.key === "Enter") { 27 if (this.confirmed) { 28 event.preventDefault(); 29 this.confirm(); 30 } else { 31 // Prevent form submission when confirmation is not valid 32 event.preventDefault(); 33 } 34 } 35 }); 36 } 37 38 // Add checkbox listeners 39 if (this.hasCheckboxTarget) { 40 this.checkboxTargets.forEach((checkbox) => { 41 checkbox.addEventListener("change", () => this.checkConfirmation()); 42 }); 43 } 44 } 45 46 disconnect() { 47 // Remove form submission listener 48 const form = this.element.closest("form"); 49 if (form && this.boundHandleSubmit) { 50 form.removeEventListener("submit", this.boundHandleSubmit); 51 } 52 53 if (this.hasInputTarget) { 54 this.inputTarget.removeEventListener("input", () => this.checkConfirmation()); 55 } 56 57 if (this.hasCheckboxTarget) { 58 this.checkboxTargets.forEach((checkbox) => { 59 checkbox.removeEventListener("change", () => this.checkConfirmation()); 60 }); 61 } 62 } 63 64 parseConfirmText() { 65 if (!this.confirmTextValue) return []; 66 67 // Simple comma-separated approach for arrays 68 if (this.confirmTextValue.includes(",")) { 69 return this.confirmTextValue 70 .split(",") 71 .map((item) => item.trim()) 72 .filter((item) => item.length > 0); 73 } 74 75 return [this.confirmTextValue]; 76 } 77 78 checkConfirmation() { 79 // Check text confirmation 80 let textConfirmed = false; 81 if (this.hasInputTarget && this.confirmTextArray.length > 0) { 82 const inputValue = this.caseSensitiveValue ? this.inputTarget.value : this.inputTarget.value.toLowerCase(); 83 84 if (this.confirmTextArray.length === 1) { 85 const confirmValue = this.caseSensitiveValue 86 ? this.confirmTextArray[0] 87 : this.confirmTextArray[0].toLowerCase(); 88 textConfirmed = inputValue === confirmValue; 89 } else { 90 textConfirmed = this.confirmTextArray.some((text) => { 91 const confirmValue = this.caseSensitiveValue ? text : text.toLowerCase(); 92 return inputValue === confirmValue; 93 }); 94 } 95 } else { 96 textConfirmed = this.hasInputTarget ? this.inputTarget.value.trim().length > 0 : true; 97 } 98 99 // Check checkbox confirmation 100 let checkboxConfirmed = true; 101 if (this.hasCheckboxTarget && this.checkboxTargets.length > 0) { 102 checkboxConfirmed = this.checkboxTargets.every((checkbox) => checkbox.checked); 103 } 104 105 this.confirmed = textConfirmed && checkboxConfirmed; 106 this.updateConfirmButton(); 107 } 108 109 updateConfirmButton() { 110 if (this.hasConfirmButtonTarget) { 111 this.confirmButtonTarget.disabled = !this.confirmed; 112 113 if (this.confirmed) { 114 this.confirmButtonTarget.classList.remove("opacity-50", "cursor-not-allowed"); 115 } else { 116 this.confirmButtonTarget.classList.add("opacity-50", "cursor-not-allowed"); 117 } 118 } 119 } 120 121 confirm() { 122 if (!this.confirmed) return false; 123 124 const beforeConfirmEvent = new CustomEvent("confirmation:before", { 125 detail: { 126 value: this.getConfirmationValue(), 127 controller: this, 128 }, 129 cancelable: true, 130 }); 131 132 if (!this.element.dispatchEvent(beforeConfirmEvent)) { 133 return false; 134 } 135 136 this.performConfirmation(); 137 138 this.element.dispatchEvent( 139 new CustomEvent("confirmation:confirmed", { 140 detail: { 141 value: this.getConfirmationValue(), 142 controller: this, 143 }, 144 }), 145 ); 146 147 return true; 148 } 149 150 handleSubmit(event) { 151 // Only allow form submission if confirmation is valid 152 if (!this.confirmed) { 153 event.preventDefault(); 154 event.stopPropagation(); 155 return false;
156 } 157 } 158 159 cancel() { 160 this.reset(); 161 this.element.dispatchEvent( 162 new CustomEvent("confirmation:cancelled", { 163 detail: { controller: this }, 164 }), 165 ); 166 } 167 168 reset() { 169 this.confirmed = false; 170 171 if (this.hasInputTarget) { 172 this.inputTarget.value = ""; 173 } 174 175 if (this.hasCheckboxTarget) { 176 this.checkboxTargets.forEach((checkbox) => { 177 checkbox.checked = false; 178 }); 179 } 180 181 this.updateConfirmButton(); 182 } 183 184 performConfirmation() { 185 const form = this.element.closest("form"); 186 if (form) { 187 form.submit(); 188 } 189 } 190 191 getConfirmationValue() { 192 const textValue = this.hasInputTarget ? this.inputTarget.value : ""; 193 const checkboxValues = this.hasCheckboxTarget 194 ? this.checkboxTargets.map((checkbox) => ({ 195 id: checkbox.id, 196 checked: checkbox.checked, 197 })) 198 : []; 199 200 return { 201 text: textValue, 202 checkboxes: checkboxValues, 203 }; 204 } 205};
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.