1(function () { 2 var config = window.haData || {}; 3 var errorIdCounter = 0; 4 5 function getFormAlert(form) { 6 var alerts = Array.prototype.slice.call(form.querySelectorAll('.ha-v2-form-alert')); 7 8 return alerts.find(function (alert) { 9 return !alert.closest('[hidden]'); 10 }) || alerts[0] || null; 11 } 12 13 function ensureFormAlert(form) { 14 var alert = getFormAlert(form); 15 16 if (alert && !alert.closest('[hidden]')) { 17 return alert; 18 } 19 20 alert = form.querySelector('.ha-v2-form-alert[data-ha-fallback-alert]'); 21 22 if (!alert) { 23 alert = document.createElement('div'); 24 alert.className = 'ha-v2-form-alert'; 25 alert.setAttribute('role', 'status'); 26 alert.setAttribute('aria-live', 'polite'); 27 alert.dataset.haFallbackAlert = 'true'; 28 29 var progress = form.querySelector('[data-ha-form-progress]'); 30 if (progress) { 31 progress.insertAdjacentElement('afterend', alert); 32 } else { 33 form.insertBefore(alert, form.firstChild); 34 } 35 } 36 37 return alert; 38 } 39 40 function clearAlerts(form) { 41 form.querySelectorAll('.ha-v2-form-alert').forEach(function (alert) { 42 alert.textContent = ''; 43 alert.classList.remove('is-success', 'is-error', 'is-visible'); 44 }); 45 } 46 47 function setAlert(form, type, message) { 48 clearAlerts(form); 49 50 if (!message) { 51 return; 52 } 53 54 var alert = ensureFormAlert(form); 55 56 if (!alert) { 57 return; 58 } 59 60 alert.textContent = message; 61 alert.classList.add('is-visible', type === 'success' ? 'is-success' : 'is-error'); 62 63 if (typeof alert.scrollIntoView === 'function') { 64 alert.scrollIntoView({ block: 'center', inline: 'nearest' }); 65 } 66 } 67 68 function getPayloadData(payload) { 69 if (!payload || typeof payload.data === 'undefined') { 70 return {}; 71 } 72 73 if (payload.data && typeof payload.data === 'object') { 74 return payload.data; 75 } 76 77 return { 78 message: String(payload.data || ''), 79 }; 80 } 81 82 function getPayloadMessage(payload, data, fallback) { 83 return data.message || (payload && payload.message) || fallback; 84 } 85 86 function setSubmitting(form, isSubmitting) { 87 var submit = form.querySelector('[type="submit"]'); 88 89 if (!submit) { 90 return; 91 } 92 93 if (!submit.dataset.defaultText) { 94 submit.dataset.defaultText = submit.textContent; 95 } 96 97 submit.disabled = isSubmitting; 98 submit.textContent = isSubmitting ? 'Sending...' : submit.dataset.defaultText; 99 } 100 101 function getAction(form) { 102 var actionField = form.querySelector('[name="action"]'); 103 return form.dataset.action || (actionField ? actionField.value : ''); 104 } 105 106 function getCustomSelectForNative(nativeSelect) { 107 var wrap = nativeSelect ? nativeSelect.closest('.ha-v2-select-wrap') : null; 108 return wrap ? wrap.querySelector('.ha-v2-custom-select') : null; 109 } 110 111 function getNativeSelectForCustom(customSelect) { 112 var id = customSelect ? customSelect.getAttribute('data-ha-v2-select-target') : ''; 113 var wrap = customSelect ? customSelect.closest('.ha-v2-select-wrap') : null; 114 115 if (id) { 116 return document.getElementById(id); 117 } 118 119 return wrap ? wrap.querySelector('select') : null; 120 } 121 122 function getDropdownForCustomSelect(customSelect) { 123 var wrap = customSelect ? customSelect.closest('.ha-v2-select-wrap') : null; 124 return wrap ? wrap.querySelector('.ha-v2-dropdown') : null; 125 } 126 127 function getCustomSelectForDropdown(dropdown) { 128 var wrap = dropdown ? dropdown.closest('.ha-v2-select-wrap') : null; 129 return wrap ? wrap.querySelector('.ha-v2-custom-select') : null; 130 } 131 132 function getCustomSelectForTarget(target) { 133 if (!target || !target.matches) { 134 return null; 135 } 136 137 if (target.matches('.ha-v2-custom-select')) { 138 return target; 139 } 140 141 if (target.matches('select.ha-v2-native-select')) { 142 return getCustomSelectForNative(target); 143 } 144 145 if (target.matches('.ha-v2-select-wrap')) { 146 return target.querySelector('.ha-v2-custom-select'); 147 } 148 149 return null; 150 } 151 152 function getCustomDropdownItems(dropdown) { 153 return dropdown ? Array.prototype.slice.call(dropdown.querySelectorAll('.ha-spo-dropdown-item')) : []; 154 } 155 156 function setCustomSelectInvalid(target, isInvalid, errorId) { 157 var customSelect = getCustomSelectForTarget(target); 158 159 if (!customSelect) { 160 return; 161 } 162 163 if (isInvalid) { 164 customSelect.setAttribute('aria-invalid', 'true'); 165 if (errorId) { 166 addDescribedBy(customSelect, errorId); 167 } 168 } else { 169 customSelect.removeAttribute('aria-invalid'); 170 if (errorId) { 171 removeDescribedBy(customSelect, errorId); 172 } 173 } 174 } 175 176 function syncCustomDropdownItemSemantics(dropdown) {
177 getCustomDropdownItems(dropdown).forEach(function (option) { 178 option.setAttribute('role', 'option'); 179 option.setAttribute('tabindex', '-1'); 180 option.setAttribute('aria-selected', option.classList.contains('selected') ? 'true' : 'false'); 181 }); 182 } 183 184 function setCustomSelectOpen(customSelect, isOpen) { 185 var dropdown = getDropdownForCustomSelect(customSelect); 186 187 if (!dropdown) { 188 return; 189 } 190 191 dropdown.classList.toggle('open', isOpen); 192 customSelect.classList.toggle('open', isOpen); 193 customSelect.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 194 } 195 196 function closeCustomSelects(exceptSelect) { 197 document.querySelectorAll('.ha-v2-custom-select.open').forEach(function (customSelect) { 198 if (exceptSelect && customSelect === exceptSelect) { 199 return; 200 } 201 202 setCustomSelectOpen(customSelect, false); 203 }); 204 } 205 206 function focusCustomDropdownItem(dropdown, target) { 207 var items = getCustomDropdownItems(dropdown); 208 var item = null; 209 210 if (!items.length) { 211 return; 212 } 213 214 if (target === 'last') { 215 item = items[items.length - 1]; 216 } else if (target === 'selected') { 217 item = dropdown.querySelector('.ha-spo-dropdown-item.selected') || items[0]; 218 } else { 219 item = items[0]; 220 } 221 222 item.focus(); 223 } 224 225 function openCustomSelect(customSelect, focusTarget) { 226 closeCustomSelects(customSelect); 227 setCustomSelectOpen(customSelect, true); 228 229 if (focusTarget) { 230 focusCustomDropdownItem(getDropdownForCustomSelect(customSelect), focusTarget); 231 } 232 } 233 234 function moveCustomDropdownFocus(item, direction) { 235 var dropdown = item.closest('.ha-v2-dropdown'); 236 var items = getCustomDropdownItems(dropdown); 237 var index = items.indexOf(item); 238 239 if (!items.length || index === -1) { 240 return; 241 } 242 243 items[(index + direction + items.length) % items.length].focus(); 244 } 245 246 function syncCustomSelectFromNative(nativeSelect) { 247 var customSelect = getCustomSelectForNative(nativeSelect); 248 var dropdown = getDropdownForCustomSelect(customSelect); 249 var text = customSelect ? customSelect.querySelector('.ha-spo-select-text') : null; 250 var selectedOption = nativeSelect ? nativeSelect.options[nativeSelect.selectedIndex] : null; 251 var value = nativeSelect ? nativeSelect.value : ''; 252 var label = selectedOption ? selectedOption.textContent.trim() : ''; 253 254 if (!nativeSelect || !customSelect || !dropdown) { 255 return; 256 } 257 258 if (text) { 259 text.textContent = label; 260 } 261 262 getCustomDropdownItems(dropdown).forEach(function (option) { 263 var isSelected = option.getAttribute('data-value') === value; 264 option.classList.toggle('selected', isSelected); 265 option.setAttribute('aria-selected', isSelected ? 'true' : 'false'); 266 }); 267 } 268 269 function selectCustomDropdownItem(item, returnFocus) { 270 var dropdown = item.closest('.ha-v2-dropdown'); 271 var customSelect = getCustomSelectForDropdown(dropdown); 272 var nativeSelect = getNativeSelectForCustom(customSelect); 273 var value = item.getAttribute('data-value'); 274 275 if (!dropdown || !customSelect || !nativeSelect) { 276 return; 277 } 278 279 nativeSelect.value = value; 280 syncCustomSelectFromNative(nativeSelect); 281 setCustomSelectOpen(customSelect, false); 282 283 nativeSelect.dispatchEvent(new Event('input', { bubbles: true })); 284 nativeSelect.dispatchEvent(new Event('change', { bubbles: true })); 285 286 if (returnFocus !== false) { 287 customSelect.focus(); 288 } 289 } 290 291 function initCustomSelects(form) { 292 form.querySelectorAll('.ha-v2-select-wrap').forEach(function (wrap) { 293 var nativeSelect = wrap.querySelector('select.ha-v2-native-select'); 294 var customSelect = wrap.querySelector('.ha-v2-custom-select'); 295 var dropdown = wrap.querySelector('.ha-v2-dropdown'); 296 297 if (!nativeSelect || !customSelect || !dropdown || wrap.dataset.haV2SelectReady === 'true') { 298 return; 299 } 300 301 wrap.dataset.haV2SelectReady = 'true'; 302 customSelect.setAttribute('aria-expanded', 'false'); 303 syncCustomDropdownItemSemantics(dropdown); 304 syncCustomSelectFromNative(nativeSelect); 305 306 customSelect.addEventListener('click', function (event) { 307 event.stopPropagation(); 308 309 if (customSelect.classList.contains('open')) {
310 setCustomSelectOpen(customSelect, false); 311 } else { 312 openCustomSelect(customSelect); 313 } 314 }); 315 316 customSelect.addEventListener('keydown', function (event) { 317 if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') { 318 event.preventDefault(); 319 if (customSelect.classList.contains('open')) { 320 setCustomSelectOpen(customSelect, false); 321 } else { 322 openCustomSelect(customSelect, 'selected'); 323 } 324 } else if (event.key === 'ArrowDown') { 325 event.preventDefault(); 326 openCustomSelect(customSelect, 'selected'); 327 } else if (event.key === 'ArrowUp') { 328 event.preventDefault(); 329 openCustomSelect(customSelect, 'last'); 330 } else if (event.key === 'Escape') { 331 setCustomSelectOpen(customSelect, false); 332 } 333 }); 334 335 getCustomDropdownItems(dropdown).forEach(function (item) { 336 item.addEventListener('click', function (event) { 337 event.stopPropagation(); 338 selectCustomDropdownItem(item); 339 }); 340 341 item.addEventListener('keydown', function (event) { 342 if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') { 343 event.preventDefault(); 344 selectCustomDropdownItem(item); 345 } else if (event.key === 'ArrowDown') { 346 event.preventDefault(); 347 moveCustomDropdownFocus(item, 1); 348 } else if (event.key === 'ArrowUp') { 349 event.preventDefault(); 350 moveCustomDropdownFocus(item, -1); 351 } else if (event.key === 'Home') { 352 event.preventDefault(); 353 focusCustomDropdownItem(dropdown, 'first'); 354 } else if (event.key === 'End') { 355 event.preventDefault(); 356 focusCustomDropdownItem(dropdown, 'last'); 357 } else if (event.key === 'Escape') { 358 event.preventDefault(); 359 setCustomSelectOpen(customSelect, false); 360 customSelect.focus(); 361 } 362 }); 363 }); 364 365 nativeSelect.addEventListener('change', function () { 366 syncCustomSelectFromNative(nativeSelect); 367 }); 368 }); 369 370 form.addEventListener('reset', function () { 371 window.setTimeout(function () { 372 form.querySelectorAll('select.ha-v2-native-select').forEach(syncCustomSelectFromNative); 373 closeCustomSelects(); 374 }, 0); 375 }); 376 377 if (!initCustomSelects.hasDocumentHandler) { 378 document.addEventListener('click', function () { 379 closeCustomSelects(); 380 }); 381 initCustomSelects.hasDocumentHandler = true; 382 } 383 } 384 385 function initChoiceControls(form) { 386 form.querySelectorAll('.ha-v2-choice').forEach(function (choice) { 387 var input = choice.querySelector('input[type="checkbox"], input[type="radio"]'); 388 389 if (!input || choice.dataset.haV2ChoiceReady === 'true') { 390 return; 391 } 392 393 choice.dataset.haV2ChoiceReady = 'true'; 394 395 choice.addEventListener('click', function (event) { 396 if (event.target === input || input.disabled) { 397 return; 398 } 399 400 event.preventDefault(); 401 402 if (input.type === 'radio') { 403 input.checked = true; 404 } else { 405 input.checked = !input.checked; 406 } 407 408 input.dispatchEvent(new Event('input', { bubbles: true })); 409 input.dispatchEvent(new Event('change', { bubbles: true })); 410 }); 411 }); 412 } 413 414 function clearInvalid(form) { 415 clearInlineErrors(form); 416 417 form.querySelectorAll('[aria-invalid="true"]').forEach(function (field) { 418 field.removeAttribute('aria-invalid'); 419 }); 420 } 421 422 function addDescribedBy(field, id) { 423 var ids = (field.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean); 424 425 if (ids.indexOf(id) === -1) { 426 ids.push(id); 427 } 428 429 field.setAttribute('aria-describedby', ids.join(' ')); 430 } 431 432 function removeDescribedBy(field, id) { 433 var ids = (field.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean); 434 var nextIds = ids.filter(function (currentId) { 435 return currentId !== id && currentId.indexOf('ha-v2-error-') !== 0; 436 }); 437 438 if (nextIds.length) { 439 field.setAttribute('aria-describedby', nextIds.join(' ')); 440 } else { 441 field.removeAttribute('aria-describedby'); 442 } 443 } 444 445 function getDescribedFields(target) { 446 if (!target) { 447 return []; 448 } 449 450 if (target.matches('input, select, textarea, .ha-v2-custom-select')) { 451 return [target]; 452 } 453 454 return Array.prototype.slice.call(target.querySelectorAll('input, select, textarea, .ha-v2-custom-select')); 455 } 456 457 function clearInlineError(target) { 458 if (!target || !target.getAttribute) { 459 return; 460 } 461 462 var id = target.getAttribute('data-ha-error-id'); 463 464 if (!id) { 465 return; 466 } 467 468 getDescribedFields(target).forEach(function (field) { 469 removeDescribedBy(field, id); 470 }); 471
472 setCustomSelectInvalid(target, false, id); 473 474 var error = document.getElementById(id); 475 if (error) { 476 error.remove(); 477 } 478 479 target.removeAttribute('data-ha-error-id'); 480 } 481 482 function clearInlineErrors(scope) { 483 scope.querySelectorAll('[data-ha-error-id]').forEach(clearInlineError); 484 scope.querySelectorAll('[data-ha-field-error]').forEach(function (error) { 485 error.remove(); 486 }); 487 } 488 489 function getErrorAnchor(target) { 490 if (target.matches('select.ha-v2-native-select')) { 491 return target.closest('.ha-v2-select-wrap') || target; 492 } 493 494 if (target.matches('.ha-v2-select-wrap')) { 495 return target; 496 } 497 498 if (target.matches('input, select, textarea, [data-ha-required-group], .ha-v2-choice')) { 499 return target; 500 } 501 502 if (target.matches('.ha-v2-choice-field')) { 503 return target.querySelector('[data-ha-required-group], .ha-v2-choice-grid') || target; 504 } 505 506 return target; 507 } 508 509 function normalizeErrorMessage(message) { 510 if (Array.isArray(message)) { 511 return message.filter(Boolean).join(' '); 512 } 513 514 if (message && typeof message === 'object' && message.message) { 515 return String(message.message); 516 } 517 518 return String(message || 'Please check this field.'); 519 } 520 521 function ensureInlineError(target) { 522 var id = target.getAttribute('data-ha-error-id'); 523 var error = id ? document.getElementById(id) : null; 524 525 if (!id) { 526 errorIdCounter += 1; 527 id = 'ha-v2-error-' + errorIdCounter; 528 target.setAttribute('data-ha-error-id', id); 529 } 530 531 if (!error) { 532 error = document.createElement('p'); 533 error.id = id; 534 error.className = 'ha-v2-field-error'; 535 error.setAttribute('aria-live', 'polite'); 536 error.dataset.haFieldError = 'true'; 537 538 if (target.matches('.ha-v2-choice-field')) { 539 error.classList.add('ha-v2-field-error-choice'); 540 } 541 542 getErrorAnchor(target).insertAdjacentElement('afterend', error); 543 } 544 545 return error; 546 } 547 548 function setInlineError(target, message) { 549 if (!target) { 550 return; 551 } 552 553 var error = ensureInlineError(target); 554 555 target.setAttribute('aria-invalid', 'true'); 556 error.textContent = normalizeErrorMessage(message); 557 setCustomSelectInvalid(target, true, error.id); 558 559 getDescribedFields(target).forEach(function (field) { 560 addDescribedBy(field, error.id); 561 }); 562 } 563 564 function clearRelatedInvalid(form, field) { 565 var name = field.getAttribute('name'); 566 var names = []; 567 var choice = field.closest('.ha-v2-choice'); 568 var choiceField = field.closest('.ha-v2-choice-field'); 569 var requiredGroup = field.closest('[data-ha-required-group]'); 570 571 field.removeAttribute('aria-invalid'); 572 clearInlineError(field); 573 setCustomSelectInvalid(field, false); 574 575 if (choice) { 576 choice.removeAttribute('aria-invalid'); 577 clearInlineError(choice); 578 } 579 580 if (name) { 581 names.push(name); 582 583 if (name.slice(-2) === '[]') { 584 names.push(name.slice(0, -2)); 585 } else { 586 names.push(name + '[]'); 587 } 588 } 589 590 names.forEach(function (fieldName) { 591 form.querySelectorAll('[name="' + CSS.escape(fieldName) + '"]').forEach(function (relatedField) { 592 relatedField.removeAttribute('aria-invalid'); 593 clearInlineError(relatedField); 594 595 var relatedChoice = relatedField.closest('.ha-v2-choice'); 596 if (relatedChoice) { 597 relatedChoice.removeAttribute('aria-invalid'); 598 clearInlineError(relatedChoice); 599 } 600 }); 601 602 form.querySelectorAll('[data-ha-required-group="' + CSS.escape(fieldName) + '"]').forEach(function (group) { 603 group.removeAttribute('aria-invalid'); 604 clearInlineError(group); 605 }); 606 }); 607 608 if (requiredGroup) { 609 requiredGroup.removeAttribute('aria-invalid'); 610 clearInlineError(requiredGroup); 611 } 612 613 if (choiceField) { 614 choiceField.removeAttribute('aria-invalid'); 615 clearInlineError(choiceField); 616 choiceField.querySelectorAll('[aria-invalid="true"]').forEach(function (invalidField) { 617 invalidField.removeAttribute('aria-invalid'); 618 clearInlineError(invalidField); 619 }); 620 } 621 } 622 623 function initInvalidStateClearing(form) { 624 function handleFieldChange(event) { 625 var field = event.target; 626 627 if (!field || !field.matches('input, select, textarea')) { 628 return; 629 } 630 631 clearRelatedInvalid(form, field); 632 633 if (!form.querySelector('[aria-invalid="true"]')) { 634 clearAlerts(form); 635 } 636 } 637 638 form.addEventListener('input', handleFieldChange); 639 form.addEventListener('change', handleFieldChange); 640 } 641 642 function findErrorTarget(form, name) { 643 var escapedName = CSS.escape(name); 644 var group = form.querySelector('[data-ha-required-group="' + escapedName + '"]'); 645 var field = form.querySelector('[name="' + escapedName + '"]'); 646 647 if (!field && name.slice(-2) === '[]') { 648 field = form.querySelector('[name="' + CSS.escape(name.slice(0, -2)) + '"]'); 649 } 650 651 if (!field && name.slice(-2) !== '[]') { 652 field = form.querySelector('[name="' + CSS.escape(name + '[]') + '"]'); 653 } 654 655 return group || field || null; 656 } 657 658 function markInvalid(form, errors) { 659 var firstInvalid = null; 660 661 Object.keys(errors || {}
661).forEach(function (name) { 662 var target = findErrorTarget(form, name); 663 664 if (!firstInvalid && target) { 665 firstInvalid = target; 666 } 667 668 if (target) { 669 setInlineError(target, errors[name]); 670 } 671 }); 672 673 return firstInvalid; 674 } 675 676 function getValidationMessage(field) { 677 var validity = field.validity || {}; 678 679 if (validity.valueMissing) { 680 if (field.type === 'file') { 681 return 'Upload a file.'; 682 } 683 684 if (field.type === 'checkbox') { 685 return 'This checkbox is required.'; 686 } 687 688 if (field.tagName === 'SELECT') { 689 return 'Select an option.'; 690 } 691 692 return 'This field is required.'; 693 } 694 695 if (validity.typeMismatch && field.type === 'email') { 696 return 'Enter a valid email address.'; 697 } 698 699 if (validity.rangeUnderflow && field.min) { 700 return 'Minimum ' + field.min + '.'; 701 } 702 703 if (validity.rangeOverflow && field.max) { 704 return 'Maximum ' + field.max + '.'; 705 } 706 707 if (validity.stepMismatch) { 708 return field.step === '1' ? 'Use a whole number.' : 'Enter a valid value.'; 709 } 710 711 if (validity.tooShort && field.minLength > 0) { 712 return 'Use at least ' + field.minLength + ' characters.'; 713 } 714 715 if (validity.tooLong && field.maxLength > 0) { 716 return 'Use no more than ' + field.maxLength + ' characters.'; 717 } 718 719 if (validity.patternMismatch) { 720 return field.title || 'Enter a valid value.'; 721 } 722 723 return field.validationMessage || 'Please check this field.'; 724 } 725 726 function formatFileSize(size) { 727 if (!size) { 728 return '0 KB'; 729 } 730 731 if (size < 1024 * 1024) { 732 return Math.max(1, Math.round(size / 1024)) + ' KB'; 733 } 734 735 return (size / (1024 * 1024)).toFixed(1).replace(/\.0$/, '') + ' MB'; 736 } 737 738 function getFileTypeLabel(file) { 739 var extension = (file.name.split('.').pop() || '').toUpperCase(); 740 741 if (extension) { 742 return extension + ' selected'; 743 } 744 745 return 'File selected'; 746 } 747 748 function setPreviewLinkHref(link, url) { 749 if (!link) { 750 return; 751 } 752 if (url) { 753 link.setAttribute('href', url); 754 } else { 755 link.removeAttribute('href'); 756 } 757 } 758 759 function clearFilePreview(preview) { 760 var image = preview.querySelector('[data-ha-file-preview-image]'); 761 var icon = preview.querySelector('.ha-v2-upload-preview-icon'); 762 var link = preview.querySelector('[data-ha-file-preview-link]'); 763 var name = preview.querySelector('[data-ha-file-preview-name]'); 764 var detail = preview.querySelector('[data-ha-file-preview-detail]'); 765 var url = preview.dataset.haPreviewUrl; 766 767 if (url && window.URL && typeof window.URL.revokeObjectURL === 'function') { 768 window.URL.revokeObjectURL(url); 769 } 770 771 delete preview.dataset.haPreviewUrl; 772 preview.hidden = true; 773 774 if (image) { 775 image.onload = null; 776 image.onerror = null; 777 image.hidden = true; 778 image.removeAttribute('src'); 779 } 780 781 if (icon) { 782 setHidden(icon, true); 783 } 784 785 if (link) { 786 setPreviewLinkHref(link, ''); 787 link.removeAttribute('aria-label'); 788 } 789 790 if (name) { 791 name.textContent = ''; 792 } 793 794 if (detail) { 795 detail.textContent = ''; 796 } 797 } 798 799 function setHidden(el, hidden) { 800 if (!el) { 801 return; 802 } 803 if (hidden) { 804 el.setAttribute('hidden', ''); 805 } else { 806 el.removeAttribute('hidden'); 807 } 808 } 809 810 function canPreviewFile(file) { 811 return /^image\//.test(file.type) || /\.(jpe?g|png|gif|webp)$/i.test(file.name); 812 } 813 814 function updateFilePreview(input, preview) { 815 var file = input.files && input.files[0]; 816 var image = preview.querySelector('[data-ha-file-preview-image]'); 817 var icon = preview.querySelector('.ha-v2-upload-preview-icon'); 818 var link = preview.querySelector('[data-ha-file-preview-link]'); 819 var name = preview.querySelector('[data-ha-file-preview-name]'); 820 var detail = preview.querySelector('[data-ha-file-preview-detail]'); 821 822 clearFilePreview(preview); 823 824 if (!file) { 825 return; 826 } 827 828 preview.hidden = false; 829 830 if (name) { 831 name.textContent = file.name; 832 } 833 834 if (detail) { 835 detail.textContent = getFileTypeLabel(file) + ' - ' + formatFileSize(file.size); 836 } 837 838 if (link) { 839 link.setAttribute('aria-label', 'Preview ' + file.name + ' in a new tab'); 840 } 841 842 if (!window.URL || typeof window.URL.createObjectURL !== 'function') { 843 if (icon) { 844 setHidden(icon, false); 845 } 846 return; 847 } 848 849 var url = window.URL.createObjectURL(file); 850 preview.dataset.haPreviewUrl = url; 851 setPreviewLinkHref(link, url); 852 853 if (!canPreviewFile(file)) { 854 if (icon) { 855 setHidden(icon, false); 856 } 857 return; 858 } 859 860 if (!image) { 861 return; 862 } 863 864 image.hidden = true; 865 866 image.onload = function () { 867 image.hidden = false;
868 if (icon) { 869 setHidden(icon, true); 870 } 871 }; 872 873 image.onerror = function () { 874 image.hidden = true; 875 image.removeAttribute('src'); 876 if (icon) { 877 setHidden(icon, false); 878 } 879 detail.textContent = getFileTypeLabel(file) + ' - ' + formatFileSize(file.size); 880 }; 881 882 image.src = url; 883 } 884 885 function initFilePreviews(form) { 886 var previews = Array.prototype.slice.call(form.querySelectorAll('[data-ha-file-preview]')); 887 888 if (!previews.length) { 889 return; 890 } 891 892 previews.forEach(function (preview) { 893 var field = preview.closest('.ha-v2-field'); 894 var input = field ? field.querySelector('input[type="file"]') : null; 895 896 if (!input) { 897 return; 898 } 899 900 input.addEventListener('change', function () { 901 updateFilePreview(input, preview); 902 }); 903 }); 904 905 function clearAllPreviews() { 906 previews.forEach(clearFilePreview); 907 } 908 909 form.addEventListener('reset', function () { 910 window.setTimeout(clearAllPreviews, 0); 911 }); 912 913 form.addEventListener('ha:v2-form-success', clearAllPreviews); 914 } 915 916 function submitFormWithToken(form, token) { 917 var formData = new FormData(form); 918 formData.set('action', getAction(form)); 919 formData.set('nonce', config.nonce || formData.get('nonce') || ''); 920 if (token) { 921 formData.set('g-recaptcha-response', token); 922 } 923 924 fetch(config.ajaxUrl || form.action, { 925 method: 'POST', 926 body: formData, 927 credentials: 'same-origin', 928 }) 929 .then(function (response) { 930 return response.json().catch(function () { 931 return { 932 success: false, 933 data: { 934 message: 'The form could not be submitted. Please try again.', 935 }, 936 }; 937 }); 938 }) 939 .then(function (payload) { 940 var data = getPayloadData(payload); 941 942 if (!payload || !payload.success) { 943 var firstInvalid = markInvalid(form, data.errors || {}); 944 form.dispatchEvent(new CustomEvent('ha:v2-form-errors', { 945 detail: { 946 errors: data.errors || {}, 947 firstInvalid: firstInvalid, 948 }, 949 })); 950 setAlert(form, 'error', getPayloadMessage(payload, data, 'Please check the highlighted fields.')); 951 return; 952 } 953 954 form.reset(); 955 form.dispatchEvent(new CustomEvent('ha:v2-form-success')); 956 setAlert(form, 'success', getPayloadMessage(payload, data, 'Your submission was successful.')); 957 }) 958 .catch(function () { 959 setAlert(form, 'error', 'The form could not be submitted. Please try again.'); 960 }) 961 .finally(function () { 962 setSubmitting(form, false); 963 }); 964 } 965 966 function initAjaxForm(form) { 967 form.addEventListener('submit', function (event) { 968 event.preventDefault(); 969 clearInvalid(form); 970 setAlert(form, '', ''); 971 972 if (config.recaptchaSiteKey && typeof grecaptcha === 'undefined') { 973 setAlert(form, 'error', 'reCAPTCHA could not be loaded. Please try again.'); 974 return; 975 } 976 977 if (typeof grecaptcha !== 'undefined' && config.recaptchaSiteKey) { 978 setSubmitting(form, true); 979 var onRecaptchaError = function () { 980 setAlert(form, 'error', 'reCAPTCHA verification could not be executed. Please try again.'); 981 setSubmitting(form, false); 982 }; 983 grecaptcha.ready(function() { 984 // execute() throws synchronously for an invalid site key. 985 try { 986 grecaptcha.execute(config.recaptchaSiteKey, {action: 'submit'}).then(function(token) { 987 submitFormWithToken(form, token); 988 }).catch(onRecaptchaError); 989 } catch (error) { 990 onRecaptchaError(); 991 } 992 }); 993 } else { 994 setSubmitting(form, true); 995 submitFormWithToken(form, ''); 996 } 997 }); 998 } 999 1000 function isVisibleField(field) { 1001 if (field.matches('select.ha-v2-native-select')) { 1002 return !field.disabled; 1003 } 1004 1005 return !field.disabled && field.type !== 'hidden' && field.offsetParent !== null; 1006 } 1007 1008 function validateStep(step) {
1009 var valid = true; 1010 var radioNames = {}; 1011 var fields = step.querySelectorAll('input, select, textarea'); 1012 var firstInvalid = null; 1013 1014 clearInvalid(step); 1015 1016 function invalidate(target, message) { 1017 valid = false; 1018 1019 if (!firstInvalid) { 1020 firstInvalid = target; 1021 } 1022 1023 setInlineError(target, message); 1024 } 1025 1026 step.querySelectorAll('[data-ha-required-group]').forEach(function (group) { 1027 group.removeAttribute('aria-invalid'); 1028 1029 if (!group.querySelector('input:checked')) { 1030 invalidate(group, 'Choose at least one option.'); 1031 } 1032 }); 1033 1034 fields.forEach(function (field) { 1035 if (!isVisibleField(field)) { 1036 return; 1037 } 1038 1039 field.removeAttribute('aria-invalid'); 1040 1041 if (!field.required) { 1042 return; 1043 } 1044 1045 if (field.type === 'radio') { 1046 if (radioNames[field.name]) { 1047 return; 1048 } 1049 radioNames[field.name] = true; 1050 if (!step.querySelector('input[name="' + CSS.escape(field.name) + '"]:checked')) { 1051 invalidate(field.closest('.ha-v2-choice-field') || field, 'Select an option.'); 1052 } 1053 return; 1054 } 1055 1056 if (field.type === 'checkbox' && !field.checked) { 1057 invalidate(field.closest('.ha-v2-choice') || field, getValidationMessage(field)); 1058 return; 1059 } 1060 1061 if (field.type === 'file' && !field.files.length) { 1062 invalidate(field, getValidationMessage(field)); 1063 return; 1064 } 1065 1066 if (typeof field.checkValidity === 'function') { 1067 if (!field.checkValidity()) { 1068 invalidate(field, getValidationMessage(field)); 1069 } 1070 return; 1071 } 1072 1073 if (!field.value) { 1074 invalidate(field, 'This field is required.'); 1075 } 1076 }); 1077 1078 if (firstInvalid) { 1079 step.dispatchEvent(new CustomEvent('ha:v2-step-invalid', { 1080 bubbles: true, 1081 detail: { 1082 firstInvalid: firstInvalid, 1083 }, 1084 })); 1085 } 1086 1087 return valid; 1088 } 1089 1090 function initSteps(form) { 1091 var steps = Array.prototype.slice.call(form.querySelectorAll('[data-ha-form-step]')); 1092 var progress = form.querySelector('[data-ha-form-progress]'); 1093 var progressItems = []; 1094 var current = 0; 1095 1096 if (!steps.length) { 1097 return; 1098 } 1099 1100 function getStepLabel(step, index) { 1101 var legend = step.querySelector('.ha-v2-step-head legend, legend'); 1102 var label = legend ? legend.textContent.trim() : ''; 1103 1104 return label || 'Step ' + (index + 1); 1105 } 1106 1107 if (progress) { 1108 progress.textContent = ''; 1109 1110 if (progress.tagName.toLowerCase() !== 'ol' && progress.tagName.toLowerCase() !== 'ul') { 1111 progress.setAttribute('role', 'list'); 1112 } 1113 1114 steps.forEach(function (step, index) { 1115 var item = document.createElement('li'); 1116 var number = document.createElement('span'); 1117 var label = document.createElement('span'); 1118 var labelText = getStepLabel(step, index); 1119 1120 item.className = 'ha-v2-progress-step'; 1121 number.className = 'ha-v2-progress-number'; 1122 label.className = 'ha-v2-progress-label'; 1123 item.setAttribute('aria-label', 'Step ' + (index + 1) + ': ' + labelText); 1124 number.setAttribute('aria-hidden', 'true'); 1125 number.textContent = index + 1; 1126 label.textContent = labelText; 1127 1128 item.appendChild(number); 1129 item.appendChild(label); 1130 progress.appendChild(item); 1131 progressItems.push(item); 1132 }); 1133 } 1134 1135 function render() { 1136 steps.forEach(function (step, index) { 1137 step.hidden = index !== current; 1138 }); 1139 1140 progressItems.forEach(function (item, index) { 1141 item.classList.toggle('is-active', index === current); 1142 item.classList.toggle('is-complete', index < current); 1143 1144 if (index === current) { 1145 item.setAttribute('aria-current', 'step'); 1146 } else { 1147 item.removeAttribute('aria-current'); 1148 } 1149 }); 1150 } 1151 1152 function getStickyTopOffset() { 1153 var selectors = ['#wpadminbar', '#ha-header', '.ha-header', '#site-header', '.site-header']; 1154 var offset = 0; 1155
1156 selectors.forEach(function (selector) { 1157 var element = document.querySelector(selector); 1158 1159 if (!element || typeof element.getBoundingClientRect !== 'function') { 1160 return; 1161 } 1162 1163 var style = window.getComputedStyle ? window.getComputedStyle(element) : null; 1164 var position = style ? style.position : ''; 1165 1166 if (position !== 'fixed' && position !== 'sticky') { 1167 return; 1168 } 1169 1170 var rect = element.getBoundingClientRect(); 1171 1172 if (rect.height > 0 && rect.bottom > 0 && rect.top <= 0) { 1173 offset = Math.max(offset, rect.bottom); 1174 } 1175 }); 1176 1177 return offset; 1178 } 1179 1180 function scrollToFormTop() { 1181 if (typeof window.scrollTo !== 'function' || typeof form.getBoundingClientRect !== 'function') { 1182 return; 1183 } 1184 1185 var formStyle = window.getComputedStyle ? window.getComputedStyle(form) : null; 1186 var scrollGap = formStyle ? parseFloat(formStyle.getPropertyValue('--space-4')) : 0; 1187 var pageOffset = window.pageYOffset || document.documentElement.scrollTop || 0; 1188 var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 1189 var target = Math.max(0, form.getBoundingClientRect().top + pageOffset - getStickyTopOffset() - (scrollGap || 16)); 1190 var schedule = window.requestAnimationFrame || function (callback) { 1191 return window.setTimeout(callback, 0); 1192 }; 1193 1194 schedule(function () { 1195 try { 1196 window.scrollTo({ 1197 top: target, 1198 behavior: reducedMotion ? 'auto' : 'smooth', 1199 }); 1200 } catch (error) { 1201 window.scrollTo(0, target); 1202 } 1203 }); 1204 } 1205 1206 function getFocusableInvalid(target) { 1207 if (!target) { 1208 return null; 1209 } 1210 1211 if (target.matches('select.ha-v2-native-select')) { 1212 return getCustomSelectForNative(target) || target; 1213 } 1214 1215 if (target.matches('input, select, textarea, .ha-v2-custom-select')) { 1216 return target; 1217 } 1218 1219 return target.querySelector('.ha-v2-custom-select, input, select, textarea'); 1220 } 1221 1222 function focusInvalid(target) { 1223 var field = getFocusableInvalid(target); 1224 1225 if (field && !field.disabled && typeof field.focus === 'function') { 1226 try { 1227 field.focus({ preventScroll: true }); 1228 } catch (error) { 1229 field.focus(); 1230 } 1231 } 1232 1233 if (target && typeof target.scrollIntoView === 'function') { 1234 target.scrollIntoView({ block: 'center', inline: 'nearest' }); 1235 } 1236 } 1237 1238 function goToInvalid(target) { 1239 var step = target && target.closest('[data-ha-form-step]'); 1240 var stepIndex = step ? steps.indexOf(step) : -1; 1241 1242 if (stepIndex >= 0) { 1243 current = stepIndex; 1244 render(); 1245 } 1246 1247 window.setTimeout(function () { 1248 focusInvalid(target); 1249 }, 0); 1250 } 1251 1252 form.querySelectorAll('[data-ha-next-step]').forEach(function (button) { 1253 button.addEventListener('click', function () { 1254 if (!validateStep(steps[current])) { 1255 setAlert(form, 'error', 'Please complete the required fields before continuing.'); 1256 return; 1257 } 1258 setAlert(form, '', ''); 1259 current = Math.min(current + 1, steps.length - 1); 1260 render(); 1261 scrollToFormTop(); 1262 }); 1263 }); 1264 1265 form.querySelectorAll('[data-ha-prev-step]').forEach(function (button) { 1266 button.addEventListener('click', function () { 1267 setAlert(form, '', ''); 1268 current = Math.max(current - 1, 0); 1269 render(); 1270 scrollToFormTop(); 1271 }); 1272 }); 1273 1274 form.addEventListener('submit', function (event) { 1275 if (!validateStep(steps[current])) { 1276 event.preventDefault(); 1277 setAlert(form, 'error', 'Please complete the required fields before submitting.'); 1278 } 1279 }, true); 1280 1281 form.addEventListener('ha:v2-step-invalid', function (event) { 1282 if (event.detail && event.detail.firstInvalid) { 1283 window.setTimeout(function () { 1284 focusInvalid(event.detail.firstInvalid); 1285 }, 0); 1286 } 1287 }); 1288 1289 form.addEventListener('ha:v2-form-errors', function (event) { 1290 if (event.detail && event.detail.firstInvalid) { 1291 goToInvalid(event.detail.firstInvalid); 1292 } 1293 }); 1294 1295 form.addEventListener('ha:v2-form-success', function () { 1296 current = 0; 1297 render(); 1298 }); 1299 1300 render(); 1301 } 1302 1303 document.addEventListener('DOMContentLoaded', function () { 1304 document.querySelectorAll('[data-ha-native-form]').forEach(function (form) { 1305 initCustomSelects(form); 1306 initChoiceControls(form); 1307 initFilePreviews(form); 1308 initInvalidStateClearing(form); 1309 initSteps(form); 1310 initAjaxForm(form); 1311 }); 1312 }); 1313})();
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.