license import { MDCFoundation } from '@material/base/foundation' ; import { MDCRippleAdapter } from './adapter' ; import { cssClasses , numbers , strings } from './constants' ; import { MDCRipplePoint } from './types' ; import { getNormalizedEventCoords } from './util' ; interface ActivationStateType { isActivated ? : boolean ; hasDeactivationUXRun ? : boolean ; wasActivatedByPointer ? : boolean ; wasElementMadeActive ? : boolean ; activationEvent ? : Event ; isProgrammatic ? : boolean ; } interface FgTranslationCoordinates { startPoint : MDCRipplePoint ; endPoint : MDCRipplePoint ; } interface Coordinates { left : number ; top : number ; } interface EventHandlerNonNull { ( event ) : any ; } type ActivationEventType = 'touchstart' | 'pointerdown' | 'mousedown' | 'keydown' ; type DeactivationEventType = 'touchend' | 'pointerup' | 'mouseup' | 'contextmenu' ; const ACTIVATION_EVENT_TYPES = [ 'touchstart' , 'pointerdown' , 'mousedown' , 'keydown' , ] ; const POINTER_DEACTIVATION_EVENT_TYPES = [ 'touchend' , 'pointerup' , 'mouseup' , 'contextmenu' , ] ; let activatedTargets = [ ] ; export class MDCRippleFoundation extends MDCFoundation < MDCRippleAdapter > { static override get cssClasses ( ) { return cssClasses ; } static override get strings ( ) { return strings ; } static override get numbers ( ) { return numbers ; } static override get defaultAdapter ( ) : MDCRippleAdapter { return { addClass : ( ) => undefined , browserSupportsCssVars : ( ) => true , computeBoundingRect : ( ) => ( { top : 0 , right : 0 , bottom : 0 , left : 0 , width : 0 , height : 0 } as any ) , containsEventTarget : ( ) => true , deregisterDocumentInteractionHandler : ( ) => undefined , deregisterInteractionHandler : ( ) => undefined , deregisterResizeHandler : ( ) => undefined , getWindowPageOffset : ( ) => ( { x : 0 , y : 0 } ) , isSurfaceActive : ( ) => true , isSurfaceDisabled : ( ) => true , isUnbounded : ( ) => true , registerDocumentInteractionHandler : ( ) => undefined , registerInteractionHandler : ( ) => undefined , registerResizeHandler : ( ) => undefined , removeClass : ( ) => undefined , updateCssVariable : ( ) => undefined , } ; } private activationAnimationHasEnded = false ; private activationState : ActivationStateType ; private activationTimer = 0 ; private fgDeactivationRemovalTimer = 0 ; private fgScale = '0' ; private frame = { width : 0 , height : 0 } ; private initialSize = 0 ; private layoutFrame = 0 ; private maxRadius = 0 ; private unboundedCoords : Coordinates = { left : 0 , top : 0 } ; private readonly activationTimerCallback : ( ) => void ; private readonly activateHandler : EventHandlerNonNull ; private readonly deactivateHandler : EventHandlerNonNull ; private readonly focusHandler : EventHandlerNonNull ; private readonly blurHandler : EventHandlerNonNull ; private readonly resizeHandler : EventHandlerNonNull ; private previousActivationEvent ? : Event ; constructor ( adapter ? ) { super ( { ... MDCRippleFoundation . defaultAdapter , ... adapter } ) ; this . activationState = this . defaultActivationState ( ) ; this . activationTimerCallback = ( ) => { this . activationAnimationHasEnded = true ; this . runDeactivationUXLogicIfReady ( ) ; } ; this . activateHandler = ( e ) => { this . activateImpl ( e ) ; } ; this . deactivateHandler = ( ) => { this . deactivateImpl ( ) ; } ; this . focusHandler = ( ) => { this . handleFocus ( ) ; } ; this . blurHandler = ( ) => { this . handleBlur ( ) ; } ; this . resizeHandler = ( ) => { this . layout ( ) ; } ; } override init ( ) { const supportsPressRipple = this . supportsPressRipple ( ) ; this . registerRootHandlers ( supportsPressRipple ) ; if ( supportsPressRipple ) { const { ROOT , UNBOUNDED } = MDCRippleFoundation . cssClasses ; requestAnimationFrame ( ( ) => { this . adapter . addClass ( ROOT ) ; if ( this . adapter . isUnbounded ( ) ) { this . adapter . addClass ( UNBOUNDED ) ; this . layoutInternal ( ) ; } } ) ; } } override destroy ( ) { if ( this . supportsPressRipple ( ) ) { if ( this . activationTimer ) { clearTimeout ( this . activationTimer ) ; this . activationTimer = 0 ; this . adapter . removeClass ( MDCRippleFoundation . cssClasses . FG_ACTIVATION ) ; } if ( this . fgDeactivationRemovalTimer ) { clearTimeout ( this . fgDeactivationRemovalTimer ) ; this . fgDeactivationRemovalTimer = 0 ; this . adapter . removeClass ( MDCRippleFoundation . cssClasses . FG_DEACTIVATION ) ; } const { ROOT , UNBOUNDED } = MDCRippleFoundation . cssClasses ; requestAnimationFrame ( ( ) => { this . adapter . removeClass ( ROOT ) ; this . adapter . removeClass ( UNBOUNDED ) ; this . removeCssVars ( ) ; } ) ; } this . deregisterRootHandlers ( ) ; this . deregisterDeactivationHandlers ( ) ; } param evt activate ( evt ? ) { this . activateImpl ( evt ) ; } deactivate ( ) { this . deactivateImpl ( ) ; } layout ( ) { if ( this . layoutFrame ) { cancelAnimationFrame ( this . layoutFrame ) ; } this . layoutFrame = requestAnimationFrame ( ( ) => { this . layoutInternal ( ) ; this . layoutFrame = 0 ; } ) ; } setUnbounded ( unbounded ) { const { UNBOUNDED } = MDCRippleFoundation . cssClasses ; if ( unbounded ) { this . adapter . addClass ( UNBOUNDED ) ; } else { this . adapter . removeClass ( UNBOUNDED ) ; } } handleFocus ( ) { requestAnimationFrame ( ( ) => this . adapter . addClass ( MDCRippleFoundation . cssClasses . BG_FOCUSED ) ) ; } handleBlur ( ) { requestAnimationFrame ( ( ) => this . adapter . removeClass ( MDCRippleFoundation . cssClasses . BG_FOCUSED ) ) ; } /**
     * We compute this property so that we are not querying information about the client
     * until the point in time where the foundation requests it. This prevents scenarios where
     * client-side feature-detection may happen too early, such as when components are rendered on the server
     * and then initialized at mount time on the client.
     */ private supportsPressRipple ( ) { return this . adapter . browserSupportsCssVars ( ) ; } private defaultActivationState ( ) { return { activationEvent : undefined , hasDeactivationUXRun : false , isActivated : false , isProgrammatic : false , wasActivatedByPointer : false , wasElementMadeActive : false , } ; } /**
     * supportsPressRipple Passed from init to save a redundant function call
     */ private registerRootHandlers ( supportsPressRipple ) { if ( supportsPressRipple ) { for ( const evtType of ACTIVATION_EVENT_TYPES ) { this . adapter . registerInteractionHandler ( evtType , this . activateHandler ) ; } if ( this . adapter . isUnbounded ( ) ) { this . adapter . registerResizeHandler ( this . resizeHandler ) ; } } this . adapter . registerInteractionHandler ( 'focus' , this . focusHandler ) ; this . adapter . registerInteractionHandler ( 'blur' , this . blurHandler ) ; } private registerDeactivationHandlers ( evt ) { if ( evt . type === 'keydown' ) { this . adapter . registerInteractionHandler ( 'keyup' , this . deactivateHandler ) ; } else { for ( const evtType of POINTER_DEACTIVATION_EVENT_TYPES ) { this . adapter . registerDocumentInteractionHandler ( evtType , this . deactivateHandler ) ; } } } private deregisterRootHandlers ( ) { for ( const evtType of ACTIVATION_EVENT_TYPES ) { this . adapter . deregisterInteractionHandler ( evtType , this . activateHandler ) ; } this . adapter . deregisterInteractionHandler ( 'focus' , this . focusHandler ) ; this . adapter . deregisterInteractionHandler ( 'blur' , this . blurHandler ) ; if ( this . adapter . isUnbounded ( ) ) { this . adapter . deregisterResizeHandler ( this . resizeHandler ) ; } } private deregisterDeactivationHandlers ( ) { this . adapter . deregisterInteractionHandler ( 'keyup' , this . deactivateHandler ) ; for ( const evtType of POINTER_DEACTIVATION_EVENT_TYPES ) { this . adapter . deregisterDocumentInteractionHandler ( evtType , this . deactivateHandler ) ; } } private removeCssVars ( ) { const rippleStrings = MDCRippleFoundation . strings ; const keys = Object . keys ( rippleStrings ) as Array < keyof typeof rippleStrings > ; keys . forEach ( ( key ) => { if ( key . indexOf ( 'VAR_' ) === 0 ) { this . adapter . updateCssVariable ( rippleStrings [ key ] , null ) ; } } ) ; } private activateImpl ( evt ? ) { if ( this . adapter . isSurfaceDisabled ( ) ) { return ; } const activationState = this . activationState ; if ( activationState . isActivated ) { return ; } const previousActivationEvent = this . previousActivationEvent ; const isSameInteraction = previousActivationEvent && evt !== undefined && previousActivationEvent . type !== evt . type ; if ( isSameInteraction ) { return ; } activationState . isActivated = true ; activationState . isProgrammatic = evt === undefined ; activationState . activationEvent = evt ; activationState . wasActivatedByPointer = activationState . isProgrammatic ? false : evt !== undefined && ( evt . type === 'mousedown' || evt . type === 'touchstart' || evt . type === 'pointerdown' ) ; const hasActivatedChild = evt !== undefined && activatedTargets . length > 0 && activatedTargets . some ( ( target ) => this . adapter . containsEventTarget ( target ) ) ; if ( hasActivatedChild ) { this . resetActivationState ( ) ; return ; } if ( evt !== undefined ) { activatedTargets . push ( evt . target ) ; this . registerDeactivationHandlers ( evt ) ; } activationState . wasElementMadeActive = this . checkElementMadeActive ( evt ) ; if ( activationState . wasElementMadeActive ) { this . animateActivation ( ) ; } requestAnimationFrame ( ( ) => { activatedTargets = [ ] ; if ( ! activationState . wasElementMadeActive && evt !== undefined && ( ( evt as KeyboardEvent ) . key === ' ' || ( evt as KeyboardEvent ) . keyCode === 32 ) ) { activationState . wasElementMadeActive = this . checkElementMadeActive ( evt ) ; if ( activationState . wasElementMadeActive ) { this . animateActivation ( ) ; } } if ( ! activationState . wasElementMadeActive ) { this . activationState = this . defaultActivationState ( ) ; } } ) ; } private checkElementMadeActive ( evt ? ) { return ( evt !== undefined && evt . type === 'keydown' ) ? this . adapter . isSurfaceActive ( ) : true ; } private animateActivation ( ) { const { VAR_FG_TRANSLATE_START , VAR_FG_TRANSLATE_END } = MDCRippleFoundation . strings ; const { FG_DEACTIVATION , FG_ACTIVATION } = MDCRippleFoundation . cssClasses ; const { DEACTIVATION_TIMEOUT_MS } = MDCRippleFoundation . numbers ; this . layoutInternal ( ) ; let translateStart = '' ; let translateEnd = '' ; if ( ! this . adapter . isUnbounded ( ) ) { const { startPoint , endPoint } = this . getFgTranslationCoordinates ( ) ; translateStart = `${ startPoint . x }px, ${ startPoint . y }px` ; translateEnd = `${ endPoint . x }px, ${ endPoint . y }px` ; } this . adapter . updateCssVariable ( VAR_FG_TRANSLATE_START , translateStart ) ; this . adapter . updateCssVariable ( VAR_FG_TRANSLATE_END , translateEnd ) ; clearTimeout ( this . activationTimer ) ; clearTimeout ( this . fgDeactivationRemovalTimer ) ; this . rmBoundedActivationClasses ( ) ; this . adapter . removeClass ( FG_DEACTIVATION ) ; this . adapter . computeBoundingRect ( ) ; this . adapter . addClass ( FG_ACTIVATION ) ; this . activationTimer = setTimeout ( ( ) => { this . activationTimerCallback ( ) ; } , DEACTIVATION_TIMEOUT_MS ) ; } private getFgTranslationCoordinates ( ) { const { activationEvent , wasActivatedByPointer } = this . activationState ; let startPoint ; if ( wasActivatedByPointer ) { startPoint = getNormalizedEventCoords ( activationEvent , this . adapter . getWindowPageOffset ( ) , this . adapter . computeBoundingRect ( ) ) ; } else { startPoint = { x : this . frame . width / 2 , y : this . frame . height / 2 , } ; } startPoint = { x : startPoint . x - ( this . initialSize / 2 ) , y : startPoint . y - ( this . initialSize / 2 ) , } ; const endPoint = { x : ( this . frame . width / 2 ) - ( this . initialSize / 2 ) , y : ( this . frame . height / 2 ) - ( this . initialSize / 2 ) , } ; return { startPoint , endPoint } ; } private runDeactivationUXLogicIfReady ( ) { const { FG_DEACTIVATION } = MDCRippleFoundation . cssClasses ; const { hasDeactivationUXRun , isActivated } = this . activationState ; const activationHasEnded = hasDeactivationUXRun || ! isActivated ; if ( activationHasEnded && this . activationAnimationHasEnded ) { this . rmBoundedActivationClasses ( ) ; this . adapter . addClass ( FG_DEACTIVATION ) ; this . fgDeactivationRemovalTimer = setTimeout ( ( ) => { this . adapter . removeClass ( FG_DEACTIVATION ) ; } , numbers . FG_DEACTIVATION_MS ) ; } } private rmBoundedActivationClasses ( ) { const { FG_ACTIVATION } = MDCRippleFoundation . cssClasses ; this . adapter . removeClass ( FG_ACTIVATION ) ; this . activationAnimationHasEnded = false ; this . adapter . computeBoundingRect ( ) ; } private resetActivationState ( ) { this . previousActivationEvent = this . activationState . activationEvent ; this . activationState = this . defaultActivationState ( ) ; setTimeout ( ( ) => this . previousActivationEvent = undefined , MDCRippleFoundation . numbers . TAP_DELAY_MS ) ; } private deactivateImpl ( ) { const activationState = this . activationState ; if ( ! activationState . isActivated ) { return ; } const state = { ... activationState } ; if ( activationState . isProgrammatic ) { requestAnimationFrame ( ( ) => { this . animateDeactivation ( state ) ; } ) ; this . resetActivationState ( ) ; } else { this . deregisterDeactivationHandlers ( ) ; requestAnimationFrame ( ( ) => { this . activationState . hasDeactivationUXRun = true ; this . animateDeactivation ( state ) ; this . resetActivationState ( ) ; } ) ; } } private animateDeactivation ( { wasActivatedByPointer , wasElementMadeActive } ) { if ( wasActivatedByPointer || wasElementMadeActive ) { this . runDeactivationUXLogicIfReady ( ) ; } } private layoutInternal ( ) { this . frame = this . adapter . computeBoundingRect ( ) ; const maxDim = Math . max ( this . frame . height , this . frame . width ) ; const getBoundedRadius = ( ) => { const hypotenuse = Math . sqrt ( Math . pow ( this . frame . width , 2 ) + Math . pow ( this . frame . height , 2 ) ) ; return hypotenuse + MDCRippleFoundation . numbers . PADDING ; } ; this . maxRadius = this . adapter . isUnbounded ( ) ? maxDim : getBoundedRadius ( ) ; const initialSize = Math . floor ( maxDim * MDCRippleFoundation . numbers . INITIAL_ORIGIN_SCALE ) ; if ( this . adapter . isUnbounded ( ) && initialSize % 2 !== 0 ) { this . initialSize = initialSize - 1 ; } else { this . initialSize = initialSize ; } this . fgScale = `${ this . maxRadius / this . initialSize }` ; this . updateLayoutCssVars ( ) ; } private updateLayoutCssVars ( ) { const { VAR_FG_SIZE , VAR_LEFT , VAR_TOP , VAR_FG_SCALE , } = MDCRippleFoundation . strings ; this . adapter . updateCssVariable ( VAR_FG_SIZE , `${ this . initialSize }px` ) ; this . adapter . updateCssVariable ( VAR_FG_SCALE , this . fgScale ) ; if ( this . adapter . isUnbounded ( ) ) { this . unboundedCoords = { left : Math . round ( ( this . frame . width / 2 ) - ( this . initialSize / 2 ) ) , top : Math . round ( ( this . frame . height / 2 ) - ( this . initialSize / 2 ) ) , } ; this . adapter . updateCssVariable ( VAR_LEFT , `${ this . unboundedCoords . left }px` ) ; this . adapter . updateCssVariable ( VAR_TOP , `${ this . unboundedCoords . top }px` ) ; } } } export default MDCRippleFoundation ;