-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdetectAutocomplete.min.js
More file actions
1 lines (1 loc) · 23 KB
/
Copy pathdetectAutocomplete.min.js
File metadata and controls
1 lines (1 loc) · 23 KB
1
javascript:(function(){function cleanup(){document.querySelectorAll(".ac-indicator, .ac-panel").forEach(el=>el.remove());const existingStyle=document.querySelector("#ac-styles");if(existingStyle)existingStyle.remove()}cleanup();const fragment=document.createDocumentFragment();const elements=document.querySelectorAll("input, select, textarea");const results={valid:[],invalid:[],missing:[]};const messages={validation:{empty:"Empty value",controlChars:"Invalid: autocomplete value cannot contain control characters",htmlChars:"Invalid: autocomplete value cannot contain HTML special characters",lineBreaks:"Invalid: autocomplete value cannot contain line breaks",leadingSpace:"Invalid: leading or trailing spaces are not allowed",startHyphen:"Invalid: autocomplete value cannot start with a hyphen"},section:{spaceInPrefix:'Invalid: no space allowed in "section-" prefix (format must be "section-" immediately followed by value)',doubleHyphen:'Invalid: section prefix must have exactly one hyphen (format must be "section-")',emptyValue:"Invalid: section prefix must be followed by a value (section-billing, section-shipping, etc.)",badPrefix:'Invalid: section prefix must be exactly "section-"',missingHyphen:"Invalid: section prefix must include a hyphen (section-)",quotes:"Invalid: section name cannot contain quotes",nonAscii:"Invalid: section name must contain only ASCII characters",multipleSections:"Invalid: multiple section-* prefixes are not allowed",wrongPlace:"Invalid: section-* prefix must be at the start of the autocomplete attribute",noSpaceAfterHyphen:'Invalid: "section-" must be immediately followed by a value (no spaces after the hyphen)'},status:{missing:"Autocomplete attribute recommended",notValidToken:token=>`Invalid: "${token}" is not a valid autocomplete token`,onOffAlone:"'on' and 'off' must be used alone",validValue:"Valid value",validCanonical:value=>`Valid value (canonical form: ${value.toLowerCase()})`,validSection:"Valid section",validSectionCanonical:value=>`Valid section (canonical form: ${value.toLowerCase()})`,validCombined:"Valid combined value",validCombinedCanonical:value=>`Valid combined value (canonical form: ${value.toLowerCase()})`,extraWhitespace:canonicalForm=>`Valid but contains extra whitespace (canonical form: ${canonicalForm})`,invalidFieldName:token=>`Invalid: "${token}" is not a valid field name`,mustFollowField:token=>`Invalid: "${token}" must be followed by a valid field name`,sectionNeedsField:token=>`Invalid: "${token}" section must be followed by a valid field (name, address, etc.)`,addressOnly:token=>`Invalid: ${token} can only be combined with address or contact information`},ui:{panelTitle:"detectAutocomplete",total:count=>`Total: ${count}`,valid:count=>`Valid: ${count}`,invalid:count=>`Invalid: ${count}`,missing:count=>`Missing: ${count}`,escapeHint:"Use <kbd>Esc</kbd> to close panel",exportInfo:"Export a CSV file containing the results of the accessibility analysis: a list of tested items, error types found, and suggested fixes."},buttons:{hideDisplay:"Hide/Display",close:"Close",darkLight:"Dark/Light",export:"Export to CSV",screenReaderLabels:{indicators:" autocomplete indicators",panel:" the panel and remove all indicators",mode:" mode toggle",results:" the autocomplete audit results"}},indicators:{valid:"Valid",invalid:"Invalid",missing:"autocomplete missing",combined:parts=>`(Combined: ${parts.join(" + ")})`,autocompletePrefix:value=>`autocomplete="${value}"`},csv:{header:"Tag,Name,Type,Autocomplete,Status,Validation Message,Data Details,Value",noName:"no-name",noType:"no-type",empty:"empty",noDetails:"No details",filename:date=>`autocomplete-audit-${date.toISOString().slice(0,10)}.csv`,missingAttribute:"Missing autocomplete attribute"}};const style=document.createElement("style");style.id="ac-styles";style.textContent=`\n .ac-panel {\n position: fixed;\n top: 10px;\n left: 10px;\n background: #fff;\n color: #000;\n padding: 15px;\n border-radius: 5px;\n box-shadow: 0 0 10px rgba(0, 0, 0, .2);\n z-index: 99999;\n max-width: 250px;\n font-family: -apple-system, system-ui, sans-serif;\n }\n #ac-panel-title {\n font-size: 1.5em;\n }\n .ac-panel p {\n font-size: 1em;\n }\n .ac-stats {\n margin: 10px 0;\n padding: 10px;\n background: #f5f5f5;\n border-radius: 3px;\n }\n .ac-stats p {\n margin: 5px 0;\n font-size: 1em;\n }\n .ac-indicator {\n background-color: #ff0;\n color: #000;\n display: block;\n max-inline-size: fit-content;\n padding: 2px 5px;\n margin-top: 2px;\n border-radius: 3px;\n font-size: 15px;\n font-family: monospace;\n font-weight: bold;\n transition: .2s;\n }\n .ac-valid {\n background-color: #cfc;\n border-left: 4px solid #4caf50;\n }\n .ac-invalid {\n background-color: #fcc;\n border-left: 4px solid #f44336;\n }\n .ac-missing {\n background-color: #ffc;\n border-left: 4px solid #ff9800;\n }\n .ac-hidden {\n display: none;\n }\n .ac-footer {\n display: flex;\n flex-direction: column;\n }\n .ac-btn {\n margin: 5px;\n padding: 5px 10px;\n border: none;\n border-radius: 3px;\n background: #0d6efd;\n color: #fff;\n font-size: .8em;\n cursor: pointer;\n }\n .ac-btn:focus {\n outline: 2px solid #0d6efd;\n outline-offset: 2px;\n box-shadow: 0 0 10px rgba(13, 110, 253, 0.25);\n }\n @media (forced-colors: active) {\n .ac-btn:focus {\n outline: 3px solid #000;\n outline-offset: 3px;\n }\n }\n @keyframes fadeIn {\n from {\n opacity: 0;\n transform: translateY(-8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n @media (prefers-reduced-motion: no-preference) {\n .ac-panel {\n animation: fadeIn 0.5s ease-out;\n }\n .ac-indicator {\n animation: fadeIn 0.3s ease-out;\n }\n }\n @media (prefers-color-scheme: dark) {\n .ac-panel {\n background: #1e1e1e;\n color: #f0f0f0;\n }\n .ac-stats {\n background: #2c2c2c;\n }\n .ac-valid {\n background-color: #4caf50;\n border-left-color: #cfc;\n }\n .ac-invalid {\n background-color: #f44336;\n border-left-color: #fcc;\n }\n .ac-missing {\n background-color: #ff9800;\n border-left-color: #ffc;\n }\n }\n body.ac-dark .ac-panel {\n background: #1e1e1e;\n color: #eee;\n }\n body.ac-dark .ac-stats {\n background: #2c2c2c;\n }\n body.ac-dark .ac-valid {\n background-color: #4caf50;\n border-left-color: #cfc;\n }\n body.ac-dark .ac-invalid {\n background-color: #f44336;\n border-left-color: #fcc;\n }\n body.ac-dark .ac-missing {\n background-color: #ff9800;\n border-left-color: #ffc;\n }\n .visually-hidden {\n border: 0 !important;\n clip: rect(1px, 1px, 1px, 1px) !important;\n -webkit-clip-path: inset(50%) !important;\n clip-path: inset(50%) !important;\n height: 1px !important;\n margin: -1px !important;\n overflow: hidden !important;\n padding: 0 !important;\n position: absolute !important;\n width: 1px !important;\n white-space: nowrap !important;\n }\n `;fragment.appendChild(style);const allowedValuesMap=new Map([["on",{category:"general",description:"Enable autocomplete",validation:"standalone"}],["off",{category:"general",description:"Disable autocomplete",validation:"standalone"}],["name",{category:"names",description:"Full name",validation:"standard"}],["given-name",{category:"names",description:"First name",validation:"standard"}],["additional-name",{category:"names",description:"Middle name",validation:"standard"}],["family-name",{category:"names",description:"Last name",validation:"standard"}],["honorific-prefix",{category:"names",description:"Title prefix (Mr., Dr., etc.)",validation:"standard"}],["honorific-suffix",{category:"names",description:"Title suffix (Jr., PhD, etc.)",validation:"standard"}],["nickname",{category:"names",description:"Nickname",validation:"standard"}],["email",{category:"contact",description:"Email address",validation:"standard"}],["tel",{category:"contact",description:"Phone number",validation:"standard"}],["url",{category:"contact",description:"Website URL",validation:"standard"}],["impp",{category:"contact",description:"Instant messaging URL",validation:"standard"}],["street-address",{category:"address",description:"Street address",validation:"standard"}],["address-line1",{category:"address",description:"Address line 1",validation:"standard"}],["address-line2",{category:"address",description:"Address line 2",validation:"standard"}],["address-line3",{category:"address",description:"Address line 3",validation:"standard"}],["address-level1",{category:"address",description:"State/Province",validation:"standard"}],["address-level2",{category:"address",description:"City",validation:"standard"}],["address-level3",{category:"address",description:"District",validation:"standard"}],["address-level4",{category:"address",description:"Neighborhood",validation:"standard"}],["country",{category:"address",description:"Country code",validation:"standard"}],["country-name",{category:"address",description:"Country name",validation:"standard"}],["postal-code",{category:"address",description:"Postal/ZIP code",validation:"standard"}],["cc-name",{category:"payment",description:"Full name on card",validation:"sensitive"}],["cc-given-name",{category:"payment",description:"First name on card",validation:"sensitive"}],["cc-additional-name",{category:"payment",description:"Middle name on card",validation:"sensitive"}],["cc-family-name",{category:"payment",description:"Last name on card",validation:"sensitive"}],["cc-number",{category:"payment",description:"Card number",validation:"sensitive"}],["cc-exp",{category:"payment",description:"Expiration date",validation:"sensitive"}],["cc-exp-month",{category:"payment",description:"Expiration month",validation:"sensitive"}],["cc-exp-year",{category:"payment",description:"Expiration year",validation:"sensitive"}],["cc-csc",{category:"payment",description:"Security code",validation:"sensitive"}],["cc-type",{category:"payment",description:"Card type",validation:"sensitive"}],["transaction-currency",{category:"payment",description:"Transaction currency",validation:"sensitive"}],["transaction-amount",{category:"payment",description:"Transaction amount",validation:"sensitive"}],["bday",{category:"personal",description:"Full birthday",validation:"standard"}],["bday-day",{category:"personal",description:"Birthday day",validation:"standard"}],["bday-month",{category:"personal",description:"Birthday month",validation:"standard"}],["bday-year",{category:"personal",description:"Birthday year",validation:"standard"}],["sex",{category:"personal",description:"Biological sex",validation:"standard"}],["organization",{category:"personal",description:"Company name",validation:"standard"}],["organization-title",{category:"personal",description:"Job title",validation:"standard"}],["language",{category:"personal",description:"Preferred language",validation:"standard"}],["photo",{category:"personal",description:"Photo URL",validation:"standard"}],["username",{category:"credentials",description:"Username",validation:"sensitive"}],["new-password",{category:"credentials",description:"New password",validation:"sensitive"}],["current-password",{category:"credentials",description:"Current password",validation:"sensitive"}],["one-time-code",{category:"credentials",description:"One-time code",validation:"sensitive"}],["tel-country-code",{category:"phone",description:"Country code",validation:"standard"}],["tel-national",{category:"phone",description:"National number",validation:"standard"}],["tel-area-code",{category:"phone",description:"Area code",validation:"standard"}],["tel-local",{category:"phone",description:"Local number",validation:"standard"}],["tel-local-prefix",{category:"phone",description:"First part of local number",validation:"standard"}],["tel-local-suffix",{category:"phone",description:"Last part of local number",validation:"standard"}],["tel-extension",{category:"phone",description:"Extension number",validation:"standard"}]]);const allowedSectionsMap=new Map([["shipping",{type:"address-contact-only",description:"Shipping address or contact information only",validFields:new Set(["name","given-name","additional-name","family-name","honorific-prefix","honorific-suffix","street-address","address-line1","address-line2","address-line3","address-level1","address-level2","address-level3","address-level4","country","country-name","postal-code","tel","tel-country-code","tel-national","tel-area-code","tel-local","tel-local-prefix","tel-local-suffix","tel-extension","email"])}],["billing",{type:"address-contact-only",description:"Billing address or contact information only",validFields:new Set(["name","given-name","additional-name","family-name","honorific-prefix","honorific-suffix","street-address","address-line1","address-line2","address-line3","address-level1","address-level2","address-level3","address-level4","country","country-name","postal-code","tel","tel-country-code","tel-national","tel-area-code","tel-local","tel-local-prefix","tel-local-suffix","tel-extension","email"])}],["home",{validFields:new Set(["tel","tel-country-code","tel-national","tel-area-code","tel-local","tel-local-prefix","tel-local-suffix","tel-extension","email","impp"])}],["work",{validFields:new Set(["tel","tel-country-code","tel-national","tel-area-code","tel-local","tel-local-prefix","tel-local-suffix","tel-extension","email","impp"])}],["mobile",{validFields:new Set(["tel","tel-country-code","tel-national","tel-area-code","tel-local","tel-local-prefix","tel-local-suffix","tel-extension"])}],["fax",{validFields:new Set(["tel","tel-country-code","tel-national","tel-area-code","tel-local","tel-local-prefix","tel-local-suffix","tel-extension"])}],["pager",{validFields:new Set(["tel","tel-country-code","tel-national","tel-area-code","tel-local","tel-local-prefix","tel-local-suffix","tel-extension"])}]]);function containsNonASCII(str){return/[^\x00-\x7F]/.test(str)}function hasExtraWhitespace(str){return/[ ]{2,}|\t/.test(str)}function validateSectionPrefix(value){const rawValue=value.toLowerCase();const parts=rawValue.split(/\s+/);const sectionRules=[{pattern:/section [- ]/,message:messages.section.spaceInPrefix},{pattern:/section--/,message:messages.section.doubleHyphen},{pattern:/section-(\s|$)/,message:messages.section.emptyValue}];for(const rule of sectionRules){if(rule.pattern.test(rawValue)){return{isValid:false,message:rule.message}}}if(rawValue.includes("section-")&&!rawValue.startsWith("section-")){return{isValid:false,message:messages.section.wrongPlace,suggestion:`section-${value.replace(/section-/i,"").trim()}`}}const sectionPrefixes=parts.filter(part=>part.startsWith("section-"));if(sectionPrefixes.length>1){return{isValid:false,message:messages.section.multipleSections}}const invalidVariant=parts.find(part=>part.startsWith("section")&&part.includes("-")&&!part.startsWith("section-")||part.match(/section[a-z]+-/));if(invalidVariant){return{isValid:false,message:messages.section.badPrefix,suggestion:value.replace(/section[a-z]*-?/,"section-")}}if(parts[0].includes('"')||parts[0].includes("'")){return{isValid:false,message:messages.section.quotes}}if(containsNonASCII(parts[0])){return{isValid:false,message:messages.section.nonAscii}}return{isValid:true,originalValue:value,hasExtraWhitespace:hasExtraWhitespace(value)}}function validateStandardToken(token,hasNonCanonicalCase){const isValid=allowedValuesMap.has(token);return{isValid:isValid,message:isValid?hasNonCanonicalCase?messages.status.validCanonical(token):messages.status.validValue:messages.status.notValidToken(token)}}function validateBasicFormat(value){const formatErrors=[{test:!value,message:messages.validation.empty},{test:/[\x00-\x1F\x7F]/.test(value),message:messages.validation.controlChars},{test:/[<>&]|<|>|&/.test(value),message:messages.validation.htmlChars},{test:/[\n\r]/.test(value),message:messages.validation.lineBreaks},{test:value.trim()!==value,message:messages.validation.leadingSpace},{test:value.startsWith("-"),message:messages.validation.startHyphen}];const error=formatErrors.find(error=>error.test);return error?{isValid:false,message:error.message}:{isValid:true}}function validateAutocomplete(value){const formatValidation=validateBasicFormat(value);if(!formatValidation.isValid){return formatValidation}const hasNonCanonicalCase=value!==value.toLowerCase();const parts=value.toLowerCase().split(/[ \t]+/).filter(Boolean);const firstToken=parts[0];if(["on","off"].includes(firstToken)){return{isValid:parts.length===1,message:parts.length>1?messages.status.onOffAlone:hasNonCanonicalCase?messages.status.validCanonical(value):messages.status.validValue}}if(value.includes("section")){const sectionValidation=validateSectionPrefix(value);if(!sectionValidation.isValid){return sectionValidation}if(parts[0].startsWith("section-")){const lastToken=parts[parts.length-1];const lastTokenValidation=validateStandardToken(lastToken,hasNonCanonicalCase);return{isValid:parts.length>=2&&lastTokenValidation.isValid,message:parts.length<2?messages.status.mustFollowField(parts[0]):!lastTokenValidation.isValid?messages.status.invalidFieldName(lastToken):sectionValidation.hasExtraWhitespace?messages.status.extraWhitespace(parts.join(" ")):hasNonCanonicalCase?messages.status.validSectionCanonical(value):messages.status.validSection}}}if(allowedSectionsMap.has(firstToken)){const section=allowedSectionsMap.get(firstToken);const lastToken=parts[parts.length-1];const isValidSection=parts.length>=2&§ion.validFields.has(lastToken);return{isValid:isValidSection,message:parts.length<2?messages.status.sectionNeedsField(firstToken):isValidSection?hasNonCanonicalCase?messages.status.validCombinedCanonical(value):messages.status.validCombined:messages.status.addressOnly(firstToken)}}return validateStandardToken(firstToken,hasNonCanonicalCase)}elements.forEach(function(element){if(element.hasAttribute("autocomplete")){const value=element.getAttribute("autocomplete");const validation=validateAutocomplete(value);validation.isValid?results.valid.push(element):results.invalid.push(element)}else{results.missing.push(element)}});const panel=document.createElement("div");panel.className="ac-panel";panel.setAttribute("role","dialog");panel.setAttribute("aria-modal","true");panel.setAttribute("aria-labelledby","ac-panel-title");panel.setAttribute("aria-live","polite");panel.setAttribute("aria-keyshortcuts","Escape");panel.innerHTML=`\n <p id="ac-panel-title">${messages.ui.panelTitle}</p>\n <div class="ac-stats">\n <p>${messages.ui.total(elements.length)}</p>\n <p>${messages.ui.valid(results.valid.length)}</p>\n <p>${messages.ui.invalid(results.invalid.length)}</p>\n <p>${messages.ui.missing(results.missing.length)}</p>\n </div>\n <div class="ac-footer">\n <button class="ac-btn" id="ac-toggle">\n ${messages.buttons.hideDisplay}\n <span class="visually-hidden">${messages.buttons.screenReaderLabels.indicators}</span>\n </button>\n <button class="ac-btn" id="ac-cleanup">\n ${messages.buttons.close}\n <span class="visually-hidden">${messages.buttons.screenReaderLabels.panel}</span>\n </button>\n <button class="ac-btn" id="ac-theme">\n ${messages.buttons.darkLight}\n <span class="visually-hidden">${messages.buttons.screenReaderLabels.mode}</span>\n </button>\n <button class="ac-btn" id="ac-export" aria-describedby="exportInfo">\n ${messages.buttons.export}\n <span class="visually-hidden">${messages.buttons.screenReaderLabels.results}</span>\n </button>\n </div>\n <p id="exportInfo" class="visually-hidden">\n ${messages.ui.exportInfo}\n </p>\n <p>\n <small>${messages.ui.escapeHint}</small>\n </p>\n `;fragment.appendChild(panel);requestAnimationFrame(()=>{elements.forEach(function(element){const label=document.createElement("p");label.className="ac-indicator";if(element.hasAttribute("autocomplete")){const value=element.getAttribute("autocomplete");const validation=validateAutocomplete(value);const parts=value.toLowerCase().split(" ").filter(Boolean);const valueSpan=document.createElement("span");valueSpan.textContent=messages.indicators.autocompletePrefix(value);const statusSpan=document.createElement("span");statusSpan.setAttribute("role",validation.isValid?"status":"alert");statusSpan.textContent=validation.isValid?` ${messages.indicators.valid}`:` ${messages.indicators.invalid}`;if(parts.length>1){statusSpan.textContent+=` ${messages.indicators.combined(parts)}`}label.appendChild(valueSpan);label.appendChild(statusSpan);label.setAttribute("data-details",validation.message);label.className+=validation.isValid?" ac-valid":" ac-invalid"}else{label.textContent=messages.indicators.missing;label.className+=" ac-missing";label.setAttribute("data-details",messages.status.missing)}element.parentNode.insertBefore(label,element.nextSibling)})});function exportToCSV(){const header=messages.csv.header+"\n";const bom="\ufeff";const csv=header+Array.from(elements).map(el=>{const autocomplete=el.getAttribute("autocomplete")||"missing";const indicator=el.nextElementSibling;if(!indicator?.classList.contains("ac-indicator")){return null}return[el.tagName.toLowerCase(),el.name||messages.csv.noName,el.type||messages.csv.noType,autocomplete,indicator.getAttribute("data-status"),indicator.getAttribute("data-details")?.replace(/,/g,";")||messages.csv.noDetails,el.value||messages.csv.empty].join(",")}).filter(Boolean).join("\n");try{const blob=new Blob([bom+csv],{type:"text/csv;charset=utf-8"});const url=URL.createObjectURL(blob);const a=document.createElement("a");a.href=url;a.download=messages.csv.filename(new Date);a.click();URL.revokeObjectURL(url)}catch(error){console.error("Error exporting CSV:",error)}}requestAnimationFrame(()=>{document.body.insertBefore(fragment,document.body.firstChild);const panel=document.querySelector(".ac-panel");if(panel){const firstButton=panel.querySelector("button");if(firstButton){firstButton.focus()}panel.addEventListener("keydown",e=>{if(e.key==="Escape"){cleanup();document.activeElement?.blur()}})}let isIndicatorsVisible=true;let isDarkMode=false;const toggleButton=document.getElementById("ac-toggle");if(toggleButton){toggleButton.setAttribute("aria-pressed","true");toggleButton.addEventListener("click",()=>{isIndicatorsVisible=!isIndicatorsVisible;toggleButton.setAttribute("aria-pressed",isIndicatorsVisible?"true":"false");requestAnimationFrame(()=>{document.querySelectorAll(".ac-indicator").forEach(el=>el.classList.toggle("ac-hidden"))})})}const themeButton=document.getElementById("ac-theme");if(themeButton){themeButton.setAttribute("aria-pressed","false");themeButton.addEventListener("click",()=>{isDarkMode=!isDarkMode;themeButton.setAttribute("aria-pressed",isDarkMode?"true":"false");requestAnimationFrame(()=>{document.body.classList.toggle("ac-dark")})})}const exportButton=document.getElementById("ac-export");if(exportButton){exportButton.addEventListener("click",exportToCSV)}const cleanupButton=document.getElementById("ac-cleanup");if(cleanupButton){cleanupButton.addEventListener("click",cleanup)}})})();