1(() => { 2 'use strict'; 3 4 const qs = (selector, root = document) => root.querySelector(selector); 5 const qsa = (selector, root = document) => Array.from(root.querySelectorAll(selector)); 6 7 const codePointLength = (value) => Array.from(value || '').length; 8 9 const normalizeEmailField = (field) => { 10 if (!(field instanceof HTMLInputElement)) return; 11 const normalized = field.value.toLowerCase(); 12 if (field.value !== normalized) field.value = normalized; 13 }; 14 15 qsa('input[type="email"][name="email"]').forEach((field) => { 16 if (!(field instanceof HTMLInputElement)) return; 17 ['input', 'change', 'blur'].forEach((eventName) => { 18 field.addEventListener(eventName, () => normalizeEmailField(field)); 19 }); 20 normalizeEmailField(field); 21 }); 22 23 qsa('[data-auth-password-toggle]').forEach((button) => { 24 const selector = button.getAttribute('data-auth-password-toggle'); 25 const field = selector ? qs(selector) : null; 26 if (!(field instanceof HTMLInputElement)) return; 27 28 button.addEventListener('click', () => { 29 const showing = field.type === 'text'; 30 field.type = showing ? 'password' : 'text'; 31 button.setAttribute('aria-pressed', showing ? 'false' : 'true'); 32 button.setAttribute('aria-label', showing ? 'Show password' : 'Hide password'); 33 const icon = qs('i', button); 34 if (icon) { 35 icon.classList.toggle('fa-eye', showing); 36 icon.classList.toggle('fa-eye-slash', !showing); 37 } 38 field.focus({ preventScroll: true }); 39 }); 40 }); 41 42 qsa('[data-auth-counter]').forEach((field) => { 43 if (!(field instanceof HTMLInputElement)) return; 44 const output = qs(field.getAttribute('data-auth-counter') || ''); 45 if (!output) return; 46 const max = Number(field.getAttribute('data-auth-codepoint-max') || field.getAttribute('maxlength') || 0); 47 const update = () => { 48 output.textContent = `${codePointLength(field.value)}${max > 0 ? ` / ${max}` : ''}`; 49 }; 50 field.addEventListener('input', update); 51 update(); 52 }); 53 54 const registrationForm = qs('form[data-auth-availability-url]'); 55 const availabilityUrl = registrationForm instanceof HTMLFormElement 56 ? registrationForm.getAttribute('data-auth-availability-url') || '' 57 : ''; 58 59 if (registrationForm instanceof HTMLFormElement) { 60 const interactionField = qs('[data-auth-registration-interaction-field]', registrationForm); 61 const interactionToken = registrationForm.getAttribute('data-auth-registration-interaction') || ''; 62 if (interactionField instanceof HTMLInputElement && interactionToken !== '') { 63 const interactionEvents = ['pointerdown', 'keydown', 'input', 'change', 'click', 'touchstart']; 64 const markInteraction = (event) => { 65 if (event && event.isTrusted === false) return; 66 interactionField.value = interactionToken; 67 interactionEvents.forEach((type) => registrationForm.removeEventListener(type, markInteraction, true)); 68 }; 69 interactionEvents.forEach((type) => registrationForm.addEventListener(type, markInteraction, true)); 70 } 71 } 72 73 const nicknameValidationMessage = (value) => { 74 if (value === '') return ''; 75 const length = codePointLength(value); 76 const bytes = typeof TextEncoder !== 'undefined' ? new TextEncoder().encode(value).length : value.length; 77 if (length < 3 || length > 30) return 'Nickname must be between 3 and 30 characters.'; 78 if (!/^[\p{L}\p{N}_-]+$/u.test(value)) return 'Use letters, numbers, underscores, or hyphens only.'; 79 if (/^(?:guide|sysop|admin|dt|staff)_/iu.test(value)) return 'That nickname starts with a reserved staff prefix.'; 80 if (bytes > 50) return 'Use fewer decorative characters in this nickname.'; 81 return ''; 82 }; 83 84 const profileValidationMessage = (value) => { 85 if (value === '') return ''; 86 if (value.length < 3 || value.length > 30 || !/^[A-Za-z0-9_-]+$/.test(value)) { 87 return 'Profile name must be 3-30 characters and use letters, numbers, underscores, or hyphens only.'; 88 } 89 return ''; 90 }; 91 92 const setAvailabilityStatus = (status, state, message) => { 93 if (!status) return; 94 status.textContent = message; 95 status.className = 'buzzen-auth-inline-status' + (state ? ` is-${state}` : ''); 96 }; 97 98 const wireAvailabilityCheck = (field, type, status, validate) => { 99 if (!(field instanceof HTMLInputElement) || !availabilityUrl) return; 100 101 let timer = 0; 102 let requestId = 0; 103 let controller = null; 104 105 const run = () => { 106 window.clearTimeout(timer); 107 if (controller) controller.abort(); 108 109 const value = field.value.trim(); 110 const localMessage = validate(value); 111 field.setCustomValidity(localMessage); 112 113 if (value === '') { 114 setAvailabilityStatus(status, '', ''); 115 return; 116 } 117 if (localMessage !== '') { 118 setAvailabilityStatus(status, 'invalid', localMessage); 119 return; 120 } 121 122 const currentRequest = ++requestId; 123 setAvailabilityStatus(status, 'checking', `Checking ${type === 'nickname' ? 'nickname' : 'profile name'} availability...`); 124 125 timer = window.setTimeout(async () => { 126 controller = typeof AbortController !== 'undefined' ? new AbortController() : null; 127 const query = new URLSearchParams({ type, value }); 128 try { 129 const response = await fetch(`${availabilityUrl}?${query.toString()}`, { 130 method: 'GET', 131 headers: { 'Accept': 'application/json' }, 132 credentials: 'same-origin', 133 cache: 'no-store', 134 signal: controller ? controller.signal : undefined, 135 }); 136 const data = await response.json(); 137 if (currentRequest !== requestId || field.value.trim() !== value) return; 138 139 if (response.ok && data && data.success === true) { 140 const serverMessage = typeof data.message === 'string' ? data.message : ''; 141 if (data.valid === true && data.available === true) { 142 field.setCustomValidity(''); 143 setAvailabilityStatus(status, 'valid', serverMessage || 'Available.'); 144 } else { 145 field.setCustomValidity(serverMessage || 'That value is not available.'); 146 setAvailabilityStatus(status, 'invalid', serverMessage || 'That value is not available.'); 147 } 148 return; 149 } 150 151 field.setCustomValidity(''); 152 setAvailabilityStatus(status, 'neutral', 'Availability check is temporarily unavailable. We will verify it when you register.'); 153 } catch (error) {
154 if (error && error.name === 'AbortError') return; 155 if (currentRequest !== requestId) return; 156 field.setCustomValidity(''); 157 setAvailabilityStatus(status, 'neutral', 'Availability check is temporarily unavailable. We will verify it when you register.'); 158 } 159 }, 450); 160 }; 161 162 field.addEventListener('input', run); 163 field.addEventListener('blur', () => { 164 if (field.value.trim() !== '' && field.validationMessage === '') run(); 165 }); 166 run(); 167 }; 168 169 const nicknameField = qs('[data-auth-nickname]'); 170 const nicknameStatus = qs('[data-auth-nickname-status]'); 171 wireAvailabilityCheck(nicknameField, 'nickname', nicknameStatus, nicknameValidationMessage); 172 173 const profileField = qs('[data-auth-profile-name]'); 174 const profilePreview = qs('[data-auth-profile-preview]'); 175 const profileStatus = qs('[data-auth-profile-status]'); 176 if (profileField instanceof HTMLInputElement && profilePreview) { 177 const base = profilePreview.getAttribute('data-profile-base') || '/user/'; 178 const suffix = profilePreview.getAttribute('data-profile-suffix') || '/profile'; 179 const updatePreview = () => { 180 const value = profileField.value.trim() || 'your-name'; 181 profilePreview.textContent = `${base}${encodeURIComponent(value)}${suffix}`; 182 }; 183 profileField.addEventListener('input', updatePreview); 184 updatePreview(); 185 } 186 wireAvailabilityCheck(profileField, 'profile', profileStatus, profileValidationMessage); 187 188 const updatePasswordRules = (password, confirmation, root) => { 189 const value = password.value; 190 const minimum = Number(password.getAttribute('data-auth-password-min') || password.getAttribute('minlength') || 15); 191 const maximum = Number(password.getAttribute('data-auth-password-max') || password.getAttribute('maxlength') || 128); 192 const tests = { 193 length: value.length >= minimum && value.length <= maximum, 194 edges: value.length > 0 && !value.startsWith(' ') && !value.endsWith(' '), 195 }; 196 197 Object.entries(tests).forEach(([rule, valid]) => { 198 const item = qs(`[data-auth-rule="${rule}"]`, root); 199 if (!item) return; 200 item.classList.toggle('is-valid', valid); 201 item.classList.toggle('is-invalid', value.length > 0 && !valid); 202 }); 203 204 const match = qs('[data-auth-password-match]', root); 205 if (confirmation instanceof HTMLInputElement && match) { 206 if (confirmation.value === '') { 207 match.textContent = ''; 208 match.className = 'buzzen-auth-inline-status'; 209 } else if (confirmation.value === value) { 210 match.textContent = 'Passwords match.'; 211 match.className = 'buzzen-auth-inline-status is-valid'; 212 } else { 213 match.textContent = 'Passwords do not match yet.'; 214 match.className = 'buzzen-auth-inline-status is-invalid'; 215 } 216 } 217 }; 218 219 qsa('[data-auth-password-rules]').forEach((root) => { 220 const password = qs('[data-auth-password-primary]', root); 221 const confirmation = qs('[data-auth-password-confirm]', root); 222 if (!(password instanceof HTMLInputElement)) return; 223 const update = () => updatePasswordRules(password, confirmation, root); 224 password.addEventListener('input', update); 225 if (confirmation instanceof HTMLInputElement) confirmation.addEventListener('input', update); 226 update(); 227 }); 228 229 qsa('form[data-auth-submit]').forEach((form) => { 230 form.addEventListener('submit', () => { 231 qsa('input[type="email"][name="email"]', form).forEach(normalizeEmailField); 232 if (!form.checkValidity()) return; 233 const button = qs('button[type="submit"]', form); 234 if (!(button instanceof HTMLButtonElement)) return; 235 button.dataset.authOriginalHtml = button.innerHTML; 236 button.disabled = true; 237 button.classList.add('buzzen-auth-submit'); 238 button.setAttribute('aria-busy', 'true'); 239 const label = button.getAttribute('data-submitting-label'); 240 if (label) { 241 const icon = qs('i', button); 242 button.textContent = ''; 243 if (icon) button.append(icon); 244 button.append(document.createTextNode(` ${label}`)); 245 } 246 }); 247 }); 248 249 window.addEventListener('pageshow', () => { 250 qsa('form[data-auth-submit] button[type="submit"]').forEach((button) => { 251 if (!(button instanceof HTMLButtonElement)) return; 252 button.disabled = false;
253 button.removeAttribute('aria-busy'); 254 button.classList.remove('buzzen-auth-submit'); 255 if (button.dataset.authOriginalHtml) { 256 button.innerHTML = button.dataset.authOriginalHtml; 257 delete button.dataset.authOriginalHtml; 258 } 259 }); 260 }); 261 262})();
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.