module nativescript-community import { Animation , AnimationDefinition , CSSType , Color , CoreTypes , EventData , FlexboxLayout , GridLayout , ImageSource , isUserInteractionEnabledProperty } from '@nativescript/core' ; import { Button } from '@nativescript-community/ui-material-button' ; import { LinearGradient } from '@nativescript/core/ui/styling/gradient' ; import { NotifyData } from '@nativescript/core/data/observable' ; export class SpeedDialItemBase extends GridLayout { } const OPEN_DURATION = 200 ; const CLOSE_DURATION = 100 ; function transformAnimationValues ( values ) { return values . map ( ( value ) => { const data = { } ; if ( value . translate ) { data . translationX = value . translate . x ; data . translationY = value . translate . y ; } if ( value . scale ) { data . scaleX = value . scale . x ; data . scaleY = value . scale . y ; } const { translate , scale , duration , curve , ... others } = value ; Object . assign ( data , others ) ; return data ; } ) ; } const defaultSpeedDialItemTitleRippleColor = new Color ( '#797979' ) ; const defaultSpeedDialItemTitleBackgroundColor = new Color ( 'white' ) ; @ CSSType ( 'MDSpeedDialItemTitle' ) export class SpeedDialItemTitle extends Button { constructor ( ) { super ( ) ; this . verticalAlignment = 'middle' ; this . style [ 'css:text-transform' ] = 'none' ; this . style [ 'css:background-color' ] = defaultSpeedDialItemTitleBackgroundColor ; this . style [ 'css:color' ] = defaultSpeedDialItemTitleRippleColor ; this . style [ 'css:elevation' ] = 1 ; this . style [ 'css:font-size' ] = 10 ; this . style [ 'css:min-width' ] = 0 ; this . style [ 'css:min-height' ] = 0 ; this . style [ 'css:padding' ] = 4 ; } getDefaultElevation ( ) { return 1 ; } getDefaultDynamicElevationOffset ( ) { return 2 ; } } @ CSSType ( 'MDSpeedDialItemButton' ) export class SpeedDialItemButton extends Button { constructor ( ) { super ( ) ; this . verticalAlignment = 'middle' ; this . style [ 'css:font-size' ] = 24 ; this . style [ 'css:elevation' ] = 6 ; this . style [ 'css:padding' ] = 2 ; } getDefaultElevation ( ) { return 6 ; } getDefaultDynamicElevationOffset ( ) { return 6 ; } } @ CSSType ( 'MDSpeedDialButton' ) export class SpeedDialButton extends SpeedDialItemButton { } @ CSSType ( 'MDSpeedDialItem' ) export class SpeedDialItem extends SpeedDialItemBase { public actualActive = false ; mTitleView : WeakRef < SpeedDialItemTitle > ; mButton : WeakRef < SpeedDialItemButton > ; fabmenu : WeakRef < SpeedDial > ; get titleView ( ) { return this . mTitleView ?. get ( ) ; } get button ( ) { return this . mButton ?. get ( ) ; } constructor ( size = 'mini' , private isMain = false ) { super ( ) ; this . isPassThroughParentEnabled = true ; const titleView = new SpeedDialItemTitle ( ) ; titleView . notify = this . notifyChildEvent ( titleView , titleView . notify ) ; titleView . col = 1 ; titleView . text = this . title ; const button = isMain ? new SpeedDialButton ( ) : new SpeedDialItemButton ( ) ; button . notify = this . notifyChildEvent ( button , button . notify ) ; button . horizontalAlignment = 'center' ; button . col = this . fabButtonCol ; if ( size === 'mini' ) { button . style [ 'css:width' ] = 40 ; button . style [ 'css:height' ] = 40 ; button . style [ 'css:margin' ] = 16 ; } else { button . style [ 'css:width' ] = 56 ; button . style [ 'css:height' ] = 56 ; button . style [ 'css:margin' ] = 16 ; } ( this as any ) . columns = this . fabColumns ; this . addChild ( titleView ) ; this . addChild ( button ) ; this . mTitleView = new WeakRef ( titleView ) ; this . mButton = new WeakRef ( button ) ; } updateAlignment ( ) { ( this as any ) . columns = this . fabColumns ; this . button . col = this . fabButtonCol ; } initNativeView ( ) { super . initNativeView ( ) ; this . titleView . on ( 'tap' , this . onButtonTap , this ) ; this . button . on ( 'tap' , this . onButtonTap , this ) ; } disposeNativeView ( ) { super . disposeNativeView ( ) ; this . titleView . off ( 'tap' , this . onButtonTap , this ) ; this . button . off ( 'tap' , this . onButtonTap , this ) ; } notifyChildEvent ( child , superNotifyMethod ) { return < T extends NotifyData > ( data ) => { ( data as any ) . speeddialItem = this ; if ( data . eventName === 'tap' ) { if ( this . isMain ) { this . fabmenu . get ( ) . onButtonTap ( data ) ; } else { this . fabmenu . get ( ) . active = false ; } } superNotifyMethod . call ( child , data ) ; } ; } get fabButtonCol ( ) { return this . isRight ? 2 : 0 ; } onButtonTap ( args ) { if ( this . isMain ) { this . fabmenu . get ( ) . onButtonTap ( args ) ; } else { this . notify ( { object : this , ... args } ) ; this . fabmenu . get ( ) . active = false ; } } get isLeft ( ) { return this . fabmenu && this . fabmenu . get ( ) . isLeft ; } get isRight ( ) { return this . fabmenu && this . fabmenu . get ( ) . isRight ; } get fabColumns ( ) { return this . isRight ? '*,auto,60' : '60,auto,*' ; } get active ( ) { return this . fabmenu && this . fabmenu . get ( ) . active ; } set active ( value ) { if ( this . fabmenu ) { this . fabmenu . get ( ) . active = value ; } } [ isUserInteractionEnabledProperty . setNative ] ( value ) { super [ isUserInteractionEnabledProperty . setNative ] ( value ) ; this . button . isUserInteractionEnabled = value ; this . titleView . isUserInteractionEnabled = value ; } get title ( ) { return this . titleView . text ; } set title ( value ) { this . titleView . text = value ; } get text ( ) { return this . button . text ; } set text ( value ) { this . button . text = value ; } get icon ( ) { return this . button . src ; } set icon ( value ) { this . button . src = value ; } get buttonClass ( ) { return this . button . className ; } set buttonClass ( value ) { this . button . className = value ; } get titleClass ( ) { return this . titleView . className ; } set titleClass ( value ) { this . titleView . className = value ; } get backgroundColor ( ) { return this . button . backgroundColor ; } set backgroundColor ( value ) { this . button . backgroundColor = value ; } get backgroundImage ( ) { return this . button . backgroundImage ; } set backgroundImage ( value ) { this . button . backgroundImage = value ; } get color ( ) { return this . button && this . button . color ; } set color ( value ) { if ( this . button ) { this . button . color = value ; } } get buttonFontSize ( ) { return this . button . fontSize ; } set buttonFontSize ( value ) { this . button . fontSize = value ; } public addEventListener ( arg , callback , thisArg ? ) { if ( arg === 'tap' ) { this . button . addEventListener ( arg , callback , thisArg ) ; this . titleView . addEventListener ( arg , callback , thisArg ) ; } else { super . addEventListener ( arg , callback , thisArg ) ; } } } @ CSSType ( 'MDSpeedDial' ) export class SpeedDial extends SpeedDialItemBase { mFabs : WeakRef < SpeedDialItem > [ ] = [ ] ; private mFabsHolder : WeakRef < FlexboxLayout > ; rows : string ; columns : string ; orientation = 'vertical' ; isActive = false ; actualActive = false ; private mFabMainButton : WeakRef < SpeedDialItem > ; get fabsHolder ( ) { return this . mFabsHolder ?. get ( ) ; } get fabMainButton ( ) { return this . mFabMainButton ?. get ( ) ; } get fabs ( ) { return this . mFabs . map ( ( f ) => f ?. get ( ) ) . filter ( ( f ) => ! ! f ) ; } constructor ( ) { super ( ) ; this . actualActive = this . isActive ; this . width = { unit : '%' , value : 100 } ; this . height = { unit : '%' , value : 100 } ; this . rows = 'auto,*,auto,auto' ; this . style [ 'css:padding-left' ] = 8 ; this . style [ 'css:padding-right' ] = 8 ; const fabHolder = new FlexboxLayout ( ) ; fabHolder . row = 2 ; fabHolder . horizontalAlignment = this . horizontalAlignment ; this . isPassThroughParentEnabled = true ; if ( global . isIOS ) { fabHolder . isPassThroughParentEnabled = true ; } fabHolder . flexDirection = this . orientation === 'vertical' ? 'column-reverse' : 'row-reverse' ; fabHolder . visibility = 'hidden' ; this . backgroundColor = new Color ( '#00000000' ) ; const fabMainButton = new SpeedDialItem ( null , true ) ; this . prepareItem ( fabMainButton , true ) ; fabMainButton . row = 3 ; this . addChild ( fabMainButton ) ; this . addChild ( fabHolder ) ; this . mFabsHolder = new WeakRef ( fabHolder ) ; this . mFabMainButton = new WeakRef ( fabMainButton ) ; } get backDrop ( ) { return this ; } initNativeView ( ) { super . initNativeView ( ) ; this . backDrop . on ( 'tap' , this . onBackdropTap , this ) ; } disposeNativeView ( ) { super . disposeNativeView ( ) ; this . backDrop . off ( 'tap' , this . onBackdropTap , this ) ; } prepareItem ( item , isMain = false ) { item . fabmenu = new WeakRef ( this ) ; const animationData = this . computeAnimationData ( 'hide' , item , this . fabs . length , Math . max ( this . fabs . length , 1 ) , OPEN_DURATION , isMain ) ; transformAnimationValues ( animationData ) . forEach ( ( d ) => { const { target , ... others } = d ; if ( target === item . button && ! isMain ) { Object . assign ( item . button , others ) ; } else if ( target === item . titleView ) { Object . assign ( item . titleView , others ) ; } } ) ; } insertChild ( child , index ) { if ( child !== this . fabMainButton && child instanceof SpeedDialItem ) { this . prepareItem ( child ) ; this . mFabs . splice ( index , 0 , new WeakRef ( child ) ) ; this . fabsHolder . insertChild ( child , index ) ; } else { super . insertChild ( child , index ) ; } } addChild ( child ) { if ( child !== this . fabMainButton && child instanceof SpeedDialItem ) { this . prepareItem ( child ) ; this . mFabs . push ( new WeakRef ( child ) ) ; this . fabsHolder . addChild ( child ) ; } else { super . addChild ( child ) ; } } get isLeft ( ) { return this . horizontalAlignment === 'left' ; } get isRight ( ) { return this . horizontalAlignment === 'right' ; } onButtonTap ( args ) { this . active = ! this . active ; } computeAnimationData ( way , fab , index , count , duration , isMain = false ) { const delay = ( duration / count ) * index ; const curve = CoreTypes . AnimationCurve . easeOut ; if ( way === 'open' ) { const result = [ { target : fab . titleView , opacity : fab . titleView . text ? 1 : 0 , duration , curve , translate : { y : 0 , x : 0 } , delay } ] ; if ( ! isMain ) { result . push ( { target : fab . button , duration , curve , opacity : 1 , scale : { y : 1 , x : 1 } , delay } ) ; } return result ; } else { const result = [ { target : fab . titleView , opacity : 0 , curve , duration , translate : { y : 0 , x : - 20 } , delay } ] ; if ( ! isMain ) { result . push ( { target : fab . button , duration , curve , opacity : 0 , scale : { y : 0.5 , x : 0.5 } , delay } ) ; } return result ; } } async show ( duration = OPEN_DURATION ) { this . animating = true ; const fabs = this . fabs ; const length = fabs . length ; const params = fabs . reduce ( ( acc , fab , index ) => { acc . push ( ... this . computeAnimationData ( 'open' , fab , index , length , duration ) ) ; return acc ; } , [ ] as AnimationDefinition [ ] ) . filter ( ( a ) => ! ! a ) . concat ( this . computeAnimationData ( 'open' , this . fabMainButton , 0 , length , duration , true ) ) . concat ( [ { target : this . backDrop , backgroundColor : new Color ( '#00000099' ) , curve : CoreTypes . AnimationCurve . easeInOut , duration } ] ) ; try { this . fabsHolder . visibility = 'visible' ; this . isPassThroughParentEnabled = false ; await new Animation ( params ) . play ( ) ; fabs . forEach ( ( f ) => ( f . isUserInteractionEnabled = true ) ) ; } catch ( err ) { } finally { this . animating = false ; } } animating = false ; async hide ( duration = CLOSE_DURATION ) { this . animating = true ; const fabs = this . fabs ; const length = fabs . length ; const params = fabs . reduce ( ( acc , fab , index ) => { acc . push ( ... this . computeAnimationData ( 'hide' , fab , length - 1 - index , length , duration ) ) ; return acc ; } , [ ] as AnimationDefinition [ ] ) . filter ( ( a ) => ! ! a ) . concat ( this . computeAnimationData ( 'hide' , this . fabMainButton , 0 , length , duration , true ) ) . concat ( [ { target : this . backDrop , backgroundColor : new Color ( '#00000000' ) , curve : CoreTypes . AnimationCurve . easeInOut , duration } ] ) ; try { fabs . forEach ( ( f ) => ( f . isUserInteractionEnabled = false ) ) ; await new Animation ( params ) . play ( ) ; this . isPassThroughParentEnabled = true ; this . fabsHolder . visibility = 'hidden' ; } catch ( err ) { } finally { this . animating = false ; } } get active ( ) { return this . actualActive ; } set active ( value ) { if ( this . animating || value === this . actualActive ) { return ; } this . actualActive = value ; if ( value ) { this . show ( ) ; } else { this . hide ( ) ; } this . notify ( { eventName : 'activeChange' , object : this , propertyName : 'active' , value } ) ; } get icon ( ) { return this . fabMainButton . icon ; } set icon ( value ) { this . fabMainButton . icon = value ; this . fabMainButton . padding = 0 ; } get buttonClass ( ) { return this . fabMainButton . buttonClass ; } set buttonClass ( value ) { this . fabMainButton . buttonClass = value ; } get buttonFontSize ( ) { return this . fabMainButton . buttonFontSize ; } set buttonFontSize ( value ) { this . fabMainButton . buttonFontSize = value ; } get color ( ) { return this . fabMainButton . color ; } set color ( value ) { this . fabMainButton . color = value ; } get text ( ) { return this . fabMainButton . text ; } set text ( value ) { this . fabMainButton . text = value ; } get title ( ) { return this . fabMainButton . title ; } set title ( value ) { this . fabMainButton . title = value ; } get horizontalAlignment ( ) { return this . fabsHolder . horizontalAlignment ; } set horizontalAlignment ( value ) { this . fabsHolder . horizontalAlignment = value ; this . fabMainButton . updateAlignment ( ) ; this . fabsHolder . eachChild ( ( c ) => { if ( c instanceof SpeedDialItem ) { c . updateAlignment ( ) ; } return true ; } ) ; } get titleClass ( ) { return this . fabMainButton . titleClass ; } set titleClass ( value ) { this . fabMainButton . titleClass = value ; } onBackdropTap ( args ) { this . active = false ; } }