import { hiddenInputMap , formsMap , formElementsMap , internalsMap , onSubmitMap } from './maps.js' ; import { ICustomElement , IElementInternals , LabelsList } from './types.js' ; const observerConfig = { attributes : true , attributeFilter : [ 'disabled' ] } ; const observer = new MutationObserver ( ( mutationsList ) => { for ( const mutation of mutationsList ) { const target = mutation . target as ICustomElement ; if ( target . constructor [ 'formAssociated' ] ) { const isDisabled = target . hasAttribute ( 'disabled' ) ; target . toggleAttribute ( 'internals-disabled' , isDisabled ) ; if ( target . formDisabledCallback ) { target . formDisabledCallback . apply ( target , [ target . hasAttribute ( 'disabled' ) ] ) ; } } } } ) ; param { IElementInternals } internals return { void } export const removeHiddenInputs = ( internals ) : void => { const hiddenInputs = hiddenInputMap . get ( internals ) ; hiddenInputs . forEach ( hiddenInput => { hiddenInput . remove ( ) ; } ) ; hiddenInputMap . set ( internals , [ ] ) ; } ; param { ICustomElement } ref param { IElementInternals } internals return { HTMLInputElement } export const createHiddenInput = ( ref , internals ) : HTMLInputElement | null => { const input = document . createElement ( 'input' ) ; input . type = 'hidden' ; input . name = ref . getAttribute ( 'name' ) ; ref . after ( input ) ; hiddenInputMap . get ( internals ) . push ( input ) ; return input ; } ; param { ICustomElement } ref param { IElementInternals } internals return { void } export const initRef = ( ref , internals ) : void => { hiddenInputMap . set ( internals , [ ] ) ; const isDisabled = ref . hasAttribute ( 'disabled' ) ; ref . toggleAttribute ( 'internals-disabled' , isDisabled ) ; observer . observe ( ref , observerConfig ) ; } ; param { ICustomElement } ref param { LabelsList } labels return { void } export const initLabels = ( ref , labels ) : void => { if ( labels . length ) { Array . from ( labels ) . forEach ( label => label . addEventListener ( 'click' , ref . focus . bind ( ref ) ) ) ; let firstLabelId = labels [ 0 ] . id ; if ( ! labels [ 0 ] . id ) { firstLabelId = `${ labels [ 0 ] . htmlFor }_Label` ; labels [ 0 ] . id = firstLabelId ; } ref . setAttribute ( 'aria-labelledby' , firstLabelId ) ; } } ; param { HTMLFormElement } return { void } export const setFormValidity = ( form ) => { const nativeControlValidity = Array . from ( form . elements ) . map ( ( element ) => element . validity . valid ) ; const polyfilledVaidity = Array . from ( formElementsMap . get ( form ) ) . filter ( control => control . isConnected ) . map ( ( control ) => internalsMap . get ( control ) . validity . valid ) ; const hasInvalid = [ ... nativeControlValidity , ... polyfilledVaidity ] . includes ( false ) ; form . toggleAttribute ( 'internals-invalid' , hasInvalid ) ; form . toggleAttribute ( 'internals-valid' , ! hasInvalid ) ; } ; param { Event } return { void } export const formInputCallback = ( event ) => { setFormValidity ( findParentForm ( event . target ) ) ; } ; param { Event } return { void } export const formChangeCallback = ( event ) => { setFormValidity ( findParentForm ( event . target ) ) ; } ; param { Event } return { void } export const formSubmitCallback = ( event ) => { /** Get the Set of elements attached to this form */ const form = event . target as HTMLFormElement ; const elements = formElementsMap . get ( form ) ; /**
     * If this form does not validate then we're done
     */ if ( form . noValidate ) { return ; } /** If the Set has items, continue */ if ( elements . size ) { const nodes = Array . from ( elements ) ; /** Check the internals.checkValidity() of all nodes */ const validityList = nodes . reverse ( ) . map ( node => { const internals = internalsMap . get ( node ) ; return internals . reportValidity ( ) ; } ) ; /** If any node is false, stop the event */ if ( validityList . includes ( false ) ) { event . stopImmediatePropagation ( ) ; event . stopPropagation ( ) ; event . preventDefault ( ) ; } else if ( onSubmitMap . get ( form ) ) { const callback = onSubmitMap . get ( form ) ; const canceled = callback . call ( form , event ) ; if ( canceled === false ) { event . preventDefault ( ) ; } } } } ; return { void } export const formResetCallback = ( event ) => { /** Get the Set of elements attached to this form */ const elements = formElementsMap . get ( event . target as HTMLFormElement ) ; /** Some forms won't contain form associated custom elements */ if ( elements && elements . size ) { /** Loop over the elements and call formResetCallback if applicable */ elements . forEach ( element => { if ( ( element . constructor as any ) . formAssociated && element . formResetCallback ) { element . formResetCallback . apply ( element ) ; } } ) ; } } ; param { HTMLElement } ref param { HTMLFormElement } form param { ElementInternals } internals return { void } export const initForm = ( ref , form , internals ) => { if ( form ) { /** If the form has an onsubmit function, save it and remove it */ if ( form . onsubmit ) { /** TODO: Find a way to parse arguments better */ onSubmitMap . set ( form , form . onsubmit . bind ( form ) ) ; form . onsubmit = null ; } /** This will be a WeakMap<HTMLFormElement, Set<HTMLElement> */ const formElements = formElementsMap . get ( form ) ; if ( formElements ) { /** If formElements exists, add to it */ formElements . add ( ref ) ; } else { /** If formElements doesn't exist, create it and add to it */ const initSet = new Set < ICustomElement > ( ) ; initSet . add ( ref ) ; formElementsMap . set ( form , initSet ) ; /** Add listeners to emulate validation and reset behavior */ form . addEventListener ( 'submit' , formSubmitCallback ) ; form . addEventListener ( 'reset' , formResetCallback ) ; form . addEventListener ( 'input' , formInputCallback ) ; form . addEventListener ( 'change' , formChangeCallback ) ; } formsMap . set ( form , { ref , internals } ) ; /** Call formAssociatedCallback if applicable */ if ( ref . constructor [ 'formAssociated' ] && ref . formAssociatedCallback ) { setTimeout ( ( ) => { ref . formAssociatedCallback . apply ( ref , [ form ] ) ; } , 0 ) ; } setFormValidity ( form ) ; } } ; param { Element } elem return { HTMLFormElement | null } export const findParentForm = ( elem ) => { let parent = elem . parentNode ; if ( parent && parent . tagName !== 'FORM' ) { parent = findParentForm ( parent ) ; } else if ( ! parent && elem . toString ( ) === '[object ShadowRoot]' ) { parent = findParentForm ( elem . host ) ; } return parent ; } ; param ref { ICustomElement } param message { string } param ErrorType { any } export const throwIfNotFormAssociated = ( ref , message , ErrorType = DOMException ) : void => { if ( ! ref . constructor [ 'formAssociated' ] ) { throw new ErrorType ( message ) ; } } ; param form { HTMLFormElement } param returnValue { boolean } param method { 'checkValidity' | 'reportValidity' } returns { boolean } export const overrideFormMethod = ( form , returnValue , method ) : boolean => { const elements = formElementsMap . get ( form ) ; /** Some forms won't contain form associated custom elements */ if ( elements && elements . size ) { elements . forEach ( element => { const internals = internalsMap . get ( element ) ; const valid = internals [ method ] ( ) ; if ( ! valid ) { returnValue = false ; } } ) ; } return returnValue ; } ; param ref { ICustomElement } export const upgradeInternals = ( ref ) => { if ( ref . constructor [ 'formAssociated' ] ) { const internals = internalsMap . get ( ref ) ; const { labels , form } = internals ; initLabels ( ref , labels ) ; initForm ( ref , form , internals ) ; } } ;