1import { Controller } from "@hotwired/stimulus"; 2 3export default class extends Controller { 4 static targets = [ 5 "input", 6 "toggleIcon", 7 "strengthBar", 8 "strengthText", 9 "lengthCheck", 10 "lowercaseCheck", 11 "uppercaseCheck", 12 "numberCheck", 13 "confirm", 14 "confirmToggleIcon", 15 "matchIndicator", 16 "matchText", 17 ]; 18 19 static values = { 20 strength: { type: Boolean, default: false }, 21 requirements: { type: Boolean, default: false }, 22 confirm: { type: Boolean, default: false }, 23 confirmDelay: { type: Number, default: 300 }, 24 }; 25 26 connect() { 27 this.isVisible = false; 28 this.confirmVisible = false; 29 30 if (this.hasInputTarget) { 31 if (this.strengthValue) { 32 this.checkStrength(); 33 } 34 if (this.requirementsValue) { 35 this.checkRequirements(); 36 } 37 } 38 } 39 40 disconnect() { 41 // Clear any pending timeout when the controller is disconnected 42 if (this.confirmValue) { 43 clearTimeout(this.checkTimeout); 44 } 45 } 46 47 toggle() { 48 this.isVisible = !this.isVisible; 49 50 if (this.isVisible) { 51 this.inputTarget.type = "text"; 52 this.updateIcon(this.toggleIconTarget, true); 53 } else { 54 this.inputTarget.type = "password"; 55 this.updateIcon(this.toggleIconTarget, false); 56 } 57 } 58 59 toggleConfirm() { 60 if (!this.confirmValue) return; 61 62 this.confirmVisible = !this.confirmVisible; 63 64 if (this.confirmVisible) { 65 this.confirmTarget.type = "text"; 66 this.updateIcon(this.confirmToggleIconTarget, true); 67 } else { 68 this.confirmTarget.type = "password"; 69 this.updateIcon(this.confirmToggleIconTarget, false); 70 } 71 } 72 73 // Called on input event 74 handleInput() { 75 if (this.strengthValue) { 76 this.checkStrength(); 77 } 78 if (this.requirementsValue) { 79 this.checkRequirements(); 80 } 81 if (this.confirmValue && this.hasConfirmTarget && this.confirmTarget.value) { 82 this.checkMatch(); 83 } 84 } 85 86 // Strength checking functionality 87 checkStrength() { 88 const password = this.inputTarget.value; 89 let strength = 0; 90 const feedback = []; 91 92 // Length check 93 if (password.length >= 8) { 94 strength += 25; 95 } else if (password.length > 0) { 96 feedback.push("At least 8 characters"); 97 } 98 99 // Lowercase check 100 if (/[a-z]/.test(password)) { 101 strength += 25; 102 } else if (password.length > 0) { 103 feedback.push("Include lowercase letter"); 104 } 105 106 // Uppercase check 107 if (/[A-Z]/.test(password)) { 108 strength += 25; 109 } else if (password.length > 0) { 110 feedback.push("Include uppercase letter"); 111 } 112 113 // Number or special character check 114 if (/[0-9!@#$%^&*]/.test(password)) { 115 strength += 25; 116 } else if (password.length > 0) { 117 feedback.push("Include number or special character"); 118 } 119 120 this.updateStrengthIndicator(strength); 121 } 122 123 updateStrengthIndicator(strength) { 124 if (!this.hasStrengthBarTarget) return; 125 126 // Update bar width and color 127 this.strengthBarTarget.style.width = `${strength}%`; 128 129 // Update color classes 130 this.strengthBarTarget.classList.remove( 131 "bg-red-500", 132 "bg-yellow-500", 133 "bg-lime-500", 134 "bg-green-500", 135 "dark:bg-red-400", 136 "dark:bg-yellow-400", 137 "dark:bg-lime-400", 138 "dark:bg-green-400", 139 "bg-neutral-300", 140 "dark:bg-neutral-600", 141 ); 142 143 if (strength === 0) { 144 this.strengthBarTarget.classList.add("bg-neutral-300", "dark:bg-neutral-600"); 145 } else if (strength <= 25) { 146 this.strengthBarTarget.classList.add("bg-red-500", "dark:bg-red-400"); 147 } else if (strength <= 50) { 148 this.strengthBarTarget.classList.add("bg-yellow-500", "dark:bg-yellow-400"); 149 } else if (strength < 100) { 150 this.strengthBarTarget.classList.add("bg-lime-500", "dark:bg-lime-400"); 151 } else { 152 this.strengthBarTarget.classList.add("bg-green-500", "dark:bg-green-400"); 153 } 154 155 // Update text 156 if (this.hasStrengthTextTarget) { 157 if (strength === 0) { 158 this.strengthTextTarget.textContent = ""; 159 this.strengthTextTarget.classList.add("hidden"); 160 } else { 161 this.strengthTextTarget.classList.remove("hidden"); 162 163 // Remove all text color classes 164 this.strengthTextTarget.classList.remove( 165 "text-red-600", 166 "text-yellow-600", 167 "text-lime-600", 168 "text-green-600", 169 "dark:text-red-400", 170 "dark:text-yellow-400", 171 "dark:text-lime-400", 172 "dark:text-green-400", 173 ); 174 175 // Update text content and color classes 176 if (strength <= 25) { 177 this.strengthTextTarget.textContent = "Weak"; 178 this.strengthTextTarget.classList.add("text-red-600", "dark:text-red-400"); 179 } else if (strength <= 50) { 180 this.strengthTextTarget.textContent = "Fair"; 181 this.strengthTextTarget.classList.add("text-yellow-600", "dark:text-yellow-400"); 182 } else if (strength < 100) { 183 this.strengthTextTarget.textContent = "Good"; 184 this.strengthTextTarget.classList.add("text-lime-600", "dark:text-lime-400"); 185 } else { 186 this.strengthTextTarget.textContent = "Strong"; 187 this.strengthTextTarget.classList.add("text-green-600", "dark:text-green-400"); 188 } 189 } 190 } 191 } 192 193 // Requirements checking functionality 194 checkRequirements() { 195 const password = this.inputTarget.value; 196 197 // Check length
198 this.updateRequirement(this.lengthCheckTarget, password.length >= 8); 199 200 // Check lowercase 201 this.updateRequirement(this.lowercaseCheckTarget, /[a-z]/.test(password)); 202 203 // Check uppercase 204 this.updateRequirement(this.uppercaseCheckTarget, /[A-Z]/.test(password)); 205 206 // Check number or special character 207 this.updateRequirement(this.numberCheckTarget, /[0-9!@#$%^&*]/.test(password)); 208 } 209 210 updateRequirement(target, met) { 211 if (!target) return; 212 213 const icons = target.querySelectorAll("svg"); 214 const uncheckedIcon = icons[0]; // First SVG is the unchecked circle 215 const checkedIcon = icons[1]; // Second SVG is the checked circle 216 const text = target.querySelector("span"); 217 218 if (met) { 219 // Requirement is met - show checked icon 220 target.classList.remove("text-neutral-500", "dark:text-neutral-400"); 221 target.classList.add("text-green-600", "dark:text-green-500"); 222 223 // Toggle icons 224 if (uncheckedIcon) uncheckedIcon.classList.add("hidden"); 225 if (checkedIcon) checkedIcon.classList.remove("hidden"); 226 227 if (text) text.classList.add("line-through"); 228 } else { 229 // Requirement is not met - show unchecked icon 230 target.classList.remove("text-green-600", "dark:text-green-500"); 231 target.classList.add("text-neutral-500", "dark:text-neutral-400"); 232 233 // Toggle icons 234 if (uncheckedIcon) uncheckedIcon.classList.remove("hidden"); 235 if (checkedIcon) checkedIcon.classList.add("hidden"); 236 237 if (text) text.classList.remove("line-through"); 238 } 239 } 240 241 // Confirmation matching functionality 242 checkMatch() { 243 // Clear any existing timeout 244 clearTimeout(this.checkTimeout); 245 246 // Set a new timeout to check after the delay 247 this.checkTimeout = setTimeout(() => { 248 this.performCheck(); 249 }, this.confirmDelayValue); 250 } 251 252 performCheck() { 253 const password = this.inputTarget.value; 254 const confirm = this.confirmTarget.value; 255 256 if (!confirm) { 257 this.hideMatchIndicator(); 258 return; 259 } 260 261 if (password === confirm) { 262 this.showMatch(); 263 } else { 264 this.showMismatch(); 265 } 266 } 267 268 showMatch() { 269 if (this.hasMatchIndicatorTarget) { 270 this.matchIndicatorTarget.classList.remove("hidden", "text-red-600", "dark:text-red-400"); 271 this.matchIndicatorTarget.classList.add("text-green-600", "dark:text-green-400"); 272 273 // Update icon to checkmark 274 const iconElement = this.matchIndicatorTarget.querySelector("svg"); 275 if (iconElement) { 276 iconElement.innerHTML = ` 277 <g fill="currentColor"> 278 <path d="M5.25,11.75h-.002c-.177,0-.344-.081-.454-.219L1.794,7.531c-.202-.252-.161-.62,.092-.821,.253-.202,.621-.161,.821,.092l2.544,3.18L10.411,2.549c.204-.251,.571-.29,.822-.086,.251,.204,.29,.572,.086,.822l-5.611,8.246c-.111,.138-.278,.219-.458,.219Z" fill="currentColor"></path> 279 </g> 280 `; 281 } 282 } 283 284 if (this.hasMatchTextTarget) { 285 this.matchTextTarget.textContent = "Passwords match"; 286 } 287 288 // Update confirm input border 289 this.confirmTarget.classList.remove("input-error"); 290 this.confirmTarget.classList.add("input-success"); 291 } 292 293 showMismatch() { 294 if (this.hasMatchIndicatorTarget) { 295 this.matchIndicatorTarget.classList.remove("hidden", "text-green-600", "dark:text-green-400"); 296 this.matchIndicatorTarget.classList.add("text-red-600", "dark:text-red-400"); 297 298 // Update icon to X mark 299 const iconElement = this.matchIndicatorTarget.querySelector("svg"); 300 if (iconElement) { 301 iconElement.innerHTML = ` 302 <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> 303 <path d="M10 4L4 10M4 4l6 6"></path> 304 </g> 305 `; 306 } 307 } 308 309 if (this.hasMatchTextTarget) { 310 this.matchTextTarget.textContent = "Passwords do not match"; 311 } 312 313 // Update confirm input border 314 this.confirmTarget.classList.remove("input-success"); 315 this.confirmTarget.classList.add("input-error"); 316 } 317 318 hideMatchIndicator() { 319 if (this.hasMatchIndicatorTarget) { 320 this.matchIndicatorTarget.classList.add("hidden"); 321 } 322 323 // Reset confirm input border 324 if (this.hasConfirmTarget) { 325 this.confirmTarget.classList.remove("input-success", "input-error"); 326 } 327 } 328 329 // Shared icon update functionality 330 updateIcon(target, visible) { 331 if (!target) return; 332 333 if (visible) { 334 // Show eye-off icon when password is visible 335 target.innerHTML = ` 336 <svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M4.8077 13.1923C3.4687 12.267 2.56488 11.0325 2.04418 10.1133C1.65178 9.42061 1.65178 8.57951 2.04418 7.88681C2.99118
3366.21511 5.2055 3.50009 8.9999 3.50009C10.708 3.50009 12.0959 4.0503 13.1921 4.8078"></path> <path d="M15.327 6.9151C15.578 7.2579 15.7869 7.58889 15.9556 7.88669C16.348 8.57939 16.348 9.42049 15.9556 10.1132C15.0086 11.7849 12.7943 14.4999 8.99994 14.4999C8.59234 14.4999 8.20304 14.4686 7.83154 14.4106"></path> <path d="M7.05551 10.9446C6.55781 10.4469 6.25 9.7594 6.25 9C6.25 7.4812 7.4812 6.25 9 6.25C9.7594 6.25 10.4469 6.55779 10.9445 7.05539"></path> <path d="M2 16L16 2"></path></g></svg> 337 `; 338 } else { 339 // Show eye icon when password is hidden 340 target.innerHTML = ` 341 <svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="18" height="18" viewBox="0 0 18 18"><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><path d="M9 11.75C10.5188 11.75 11.75 10.5188 11.75 9C11.75 7.48122 10.5188 6.25 9 6.25C7.48122 6.25 6.25 7.48122 6.25 9C6.25 10.5188 7.48122 11.75 9 11.75Z"></path> <path d="M15.9557 7.88669C16.3481 8.57939 16.3481 9.42049 15.9557 10.1132C15.0087 11.7849 12.7944 14.4999 9 14.4999C5.2056 14.4999 2.9912 11.7849 2.0443 10.1132C1.6519 9.42049 1.6519 8.57939 2.0443 7.88669C2.9913 6.21499 5.2056 3.5 9 3.5C12.7944 3.5 15.0088 6.21499 15.9557 7.88669Z"></path></g></svg> 342 `; 343 } 344 } 345};
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.