import { Injectable , OnDestroy } from '@angular/core' ; import { BehaviorSubject , combineLatest , Observable , Subject } from 'rxjs' ; import { distinctUntilChanged , filter , map , takeUntil } from 'rxjs/operators' ; import { tick } from '../../common/index' ; import { DragScrollEvent } from '../../directives/drag/index' ; import { DashboardOptions } from './dashboard.component' ; import { DashboardStackMode } from './widget/dashboard-stack-mode.enum' ; import { DashboardWidgetComponent } from './widget/dashboard-widget.component' ; @ Injectable ( ) export class DashboardService implements OnDestroy { private _widgetOrigin : { column ? : number ; row ? : number ; columnSpan ? : number ; rowSpan ? : number ; } ; private _actionWidget : DashboardAction ; private _rowHeight : number = 0 ; private _cache : DashboardCache [ ] ; private _event : MouseEvent ; initialized$ = new BehaviorSubject < boolean > ( false ) ; widgets$ = new BehaviorSubject < DashboardWidgetComponent [ ] > ( [ ] ) ; options$ = new BehaviorSubject < DashboardOptions > ( defaultOptions ) ; dimensions$ = new BehaviorSubject < DashboardDimensions > ( { } ) ; height$ : Observable < number > = this . dimensions$ . pipe ( tick ( ) , map ( dimensions => dimensions . height ) , distinctUntilChanged ( ) ) ; placeholder$ = new BehaviorSubject < DashboardPlaceholder > ( { visible : false , x : 0 , y : 0 , width : 0 , height : 0 } ) ; layout$ = new BehaviorSubject < DashboardLayoutData [ ] > ( [ ] ) ; stacked$ = new BehaviorSubject < boolean > ( false ) ; isDragging$ = new BehaviorSubject < DashboardWidgetComponent > ( null ) ; isGrabbing$ = new BehaviorSubject < DashboardWidgetComponent > ( null ) ; userLayoutChange$ = new Subject < DashboardLayoutData [ ] > ( ) ; get options ( ) : DashboardOptions { return this . options$ . getValue ( ) ; } get widgets ( ) : DashboardWidgetComponent [ ] { return this . widgets$ . getValue ( ) ; } get stacked ( ) : boolean { return this . stacked$ . getValue ( ) ; } get dimensions ( ) : DashboardDimensions { return this . dimensions$ . getValue ( ) ; } get columnWidth ( ) : number { return this . dimensions . width / this . options . columns ; } /** Unsubscribe from all observables on destroy */ private _onDestroy = new Subject < void > ( ) ; constructor ( ) { combineLatest ( this . layout$ , this . widgets$ , this . initialized$ ) . pipe ( tick ( ) , filter ( ( [ layout , widgets , initialized ] ) => layout && widgets . length > 0 && initialized ) , takeUntil ( this . _onDestroy ) ) . subscribe ( ( [ layout ] ) => { this . setLayoutData ( layout ) ; this . renderDashboard ( ) ; } ) ; this . stacked$ . pipe ( distinctUntilChanged ( ) , filter ( stacked => stacked === true ) , takeUntil ( this . _onDestroy ) ) . subscribe ( this . updateWhenStacked . bind ( this ) ) ; this . widgets$ . pipe ( tick ( ) , takeUntil ( this . _onDestroy ) ) . subscribe ( ( ) => this . renderDashboard ( ) ) ; this . dimensions$ . pipe ( tick ( ) , takeUntil ( this . _onDestroy ) ) . subscribe ( ( ) => this . renderDashboard ( ) ) ; } ngOnDestroy ( ) { this . _onDestroy . next ( ) ; this . _onDestroy . complete ( ) ; } param widget addWidget ( widget ) { this . widgets$ . next ( [ ... this . widgets$ . getValue ( ) , widget ] ) ; } param widget removeWidget ( widget ) { this . widgets$ . next ( this . widgets$ . getValue ( ) . filter ( _widget => _widget !== widget ) ) ; } param width param height setDimensions ( width = this . dimensions . width , height = this . dimensions . height ) { if ( this . dimensions . width !== width || this . dimensions . height !== height ) { this . dimensions$ . next ( { width , height } ) ; } } /**
     * Produce an object containing all the required layout data.
     * This can be useful for exporting/saving a layout
     */ getLayoutData ( ) { return this . widgets . map ( widget => { return { id : widget . id , col : widget . getColumn ( ) , row : widget . getRow ( ) , colSpan : widget . getColumnSpan ( ) , rowSpan : widget . getRowSpan ( ) , minColSpan : widget . minColSpan , minRowSpan : widget . minRowSpan } ; } ) ; } /**
     * Position widgets programatically
     */ setLayoutData ( widgets ) { widgets . forEach ( widget => { const target = this . widgets . find ( _widget => _widget . id === widget . id ) ; if ( target ) { target . setColumn ( widget . col ) ; target . setRow ( widget . row ) ; target . setColumnSpan ( widget . colSpan ) ; target . setRowSpan ( widget . rowSpan ) ; target . minColSpan = widget . minColSpan ?? 1 ; target . minRowSpan = widget . minRowSpan ?? 1 ; } } ) ; } /**
     * Update the positions and sizes of the widgets
     */ renderDashboard ( ) { this . _rowHeight = this . options . rowHeight || this . columnWidth ; this . stacked$ . next ( this . columnWidth < this . options . minWidth ) ; this . setDashboardLayout ( ) ; this . widgets . filter ( widget => ! this . _actionWidget || widget !== this . _actionWidget . widget ) . forEach ( widget => widget . render ( ) ) ; } /**
     * Determine where widgets should be positioned based on their positions, width and the size of the container
     */ setDashboardLayout ( ) { this . widgets . filter ( widget => widget . getColumn ( ) === undefined || widget . getRow ( ) === undefined ) . forEach ( widget => this . setWidgetPosition ( widget ) ) ; this . setDashboardHeight ( ) ; } updateWhenStacked ( ) { this . getWidgetsByOrder ( ) . forEach ( ( widget , idx , widgets ) => { const widgetsAbove = widgets . slice ( 0 , idx ) ; const row = widgetsAbove . reduce ( ( currentRow , _widget ) => currentRow + _widget . getRowSpan ( ) , 0 ) ; widget . setColumn ( 0 ) ; widget . setRow ( row ) ; } ) ; } /** Get widgets in the order they visually appear as the widgets array order does not reflect this */ getWidgetsByOrder ( ) { return [ ... this . widgets ] . sort ( ( w1 , w2 ) => { const w1Position = w1 . getColumn ( DashboardStackMode . Regular ) + ( w1 . getRow ( DashboardStackMode . Regular ) * this . options . columns ) ; const w2Position = w2 . getColumn ( DashboardStackMode . Regular ) + ( w2 . getRow ( DashboardStackMode . Regular ) * this . options . columns ) ; if ( w1Position < w2Position ) { return - 1 ; } if ( w1Position > w2Position ) { return 1 ; } return 0 ; } ) ; } param widget setWidgetPosition ( widget ) { let position = 0 ; let success = false ; while ( ! success ) { const column = position % this . options . columns ; const row = Math . floor ( position / this . options . columns ) ; if ( this . getPositionAvailable ( column , row , widget . getColumnSpan ( ) , widget . getRowSpan ( ) ) ) { success = true ; widget . setColumn ( column ) ; widget . setRow ( row ) ; return ; } if ( column === 0 && widget . colSpan > this . options . columns ) { throw new Error ( 'Dashboard widgets have a colSpan greater than the max number of dashboard columns!' ) ; } position ++ ; } } /**
     * Check if a position in the dashboard is vacant or not
     */ getPositionAvailable ( column , row , columnSpan , rowSpan , ignoreWidget ? ) { const spaces = this . getOccupiedSpaces ( ) ; if ( column + columnSpan > this . options . columns ) { return false ; } for ( let x = column ; x < column + columnSpan ; x ++ ) { for ( let y = row ; y < row + rowSpan ; y ++ ) { if ( spaces . find ( block => block . column === x && block . row === y && block . widget !== ignoreWidget ) ) { return false ; } } } return true ; } getOccupiedSpaces ( ) { return this . widgets . filter ( widget => widget . getColumn ( ) !== undefined && widget . getRow ( ) !== undefined ) . reduce ( ( value , widget ) => { this . forEachBlock ( widget , ( column , row ) => value . push ( { widget , column , row } ) ) ; return value ; } , [ ] ) ; } param action onResizeStart ( action ) { this . cacheWidgets ( ) ; this . _event = action . event ; this . _actionWidget = action ; this . bringToFront ( action . widget ) ; } onResizeDrag ( action ) { const mousePosX = this . _event . pageX - pageXOffset ; const mousePosY = this . _event . pageY - pageYOffset ; if ( action . event . x === mousePosX && action . event . y === mousePosY ) { return ; } this . _event = action . event ; const { handle } = action ; const bounds = handle . getBoundingClientRect ( ) ; const centerX = bounds . left + ( bounds . width / 2 ) ; const centerY = bounds . top + ( bounds . height / 2 ) ; const mouseX = mousePosX - centerX ; const mouseY = mousePosY - centerY ; const dimensions = { x : action . widget . x , y : action . widget . y , width : action . widget . width , height : action . widget . height } ; switch ( action . direction ) { case ActionDirection . Right : dimensions . width += mouseX ; break ; case ActionDirection . Left : dimensions . x += mouseX ; dimensions . width -= mouseX ; if ( dimensions . width < this . options . minWidth ) { const difference = this . options . minWidth - dimensions . width ; dimensions . x -= difference ; dimensions . width += difference ; } break ; case ActionDirection . Bottom : dimensions . height += mouseY ; break ; case ActionDirection . Top : dimensions . y += mouseY ; dimensions . height -= mouseY ; if ( dimensions . height < this . options . minHeight ) { const difference = this . options . minHeight - dimensions . height ; dimensions . y -= difference ; dimensions . height += difference ; } break ; case ActionDirection . TopLeft : dimensions . x += mouseX ; dimensions . width -= mouseX ; if ( dimensions . width < this . options . minWidth ) { const difference = this . options . minWidth - dimensions . width ; dimensions . x -= difference ; dimensions . width += difference ; } dimensions . y += mouseY ; dimensions . height -= mouseY ; if ( dimensions . height < this . options . minHeight ) { const difference = this . options . minHeight - dimensions . height ; dimensions . y -= difference ; dimensions . height += difference ; } break ; case ActionDirection . TopRight : dimensions . width += mouseX ; dimensions . y += mouseY ; dimensions . height -= mouseY ; if ( dimensions . height < this . options . minHeight ) { const difference = this . options . minHeight - dimensions . height ; dimensions . y -= difference ; dimensions . height += difference ; } break ; case ActionDirection . BottomLeft : dimensions . height += mouseY ; dimensions . x += mouseX ; dimensions . width -= mouseX ; if ( dimensions . width < this . options . minWidth ) { const difference = this . options . minWidth - dimensions . width ; dimensions . x -= difference ; dimensions . width += difference ; } break ; case ActionDirection . BottomRight : dimensions . height += mouseY ; dimensions . width += mouseX ; break ; } const currentWidth = action . widget . x + action . widget . width ; const currentHeight = action . widget . y + action . widget . height ; if ( dimensions . x < 0 ) { dimensions . x = 0 ; dimensions . width = currentWidth ; } if ( dimensions . y < 0 ) { dimensions . y = 0 ; dimensions . height = currentHeight ; } if ( ( dimensions . x + dimensions . width ) > this . dimensions . width ) { dimensions . width = this . dimensions . width - dimensions . x ; } if ( dimensions . width < this . options . minWidth ) { dimensions . x = action . widget . x ; dimensions . width = this . options . minWidth ; } if ( dimensions . height < this . options . minHeight ) { dimensions . y = action . widget . y ; dimensions . height = this . options . minHeight ; } const minWidth = ( ( this . getColumnWidth ( ) * action . widget . minColSpan ) - this . getColumnWidth ( ) / 2 ) + 1 ; const isBelowMinWidth = dimensions . width < minWidth ; if ( isBelowMinWidth ) { dimensions . x = action . widget . x ; dimensions . width = minWidth ; } const minHeight = ( ( this . options . rowHeight * action . widget . minRowSpan ) - this . options . rowHeight / 2 ) + 1 ; const isBelowMinHeight = dimensions . height < minHeight ; if ( isBelowMinHeight ) { dimensions . y = action . widget . y ; dimensions . height = minHeight ; } action . widget . setBounds ( dimensions . x , dimensions . y , dimensions . width , dimensions . height ) ; this . setPlaceholderBounds ( true , dimensions . x , dimensions . y , dimensions . width , dimensions . height ) ; this . updateWidgetPositions ( action . widget ) ; } onResizeEnd ( ) { const placeholder = this . placeholder$ . getValue ( ) ; this . commitWidgetChanges ( ) ; placeholder . visible = false ; this . placeholder$ . next ( placeholder ) ; this . _actionWidget = null ; this . _event = null ; this . shiftWidgetsUp ( ) ; this . setDashboardHeight ( ) ; this . layout$ . next ( this . getLayoutData ( ) ) ; this . userLayoutChange$ . next ( this . getLayoutData ( ) ) ; } onDragStart ( action ) { this . onResizeStart ( action ) ; this . setWidgetOrigin ( ) ; this . cacheWidgets ( ) ; this . isDragging$ . next ( action . widget ) ; } onDragEnd ( ) { this . onResizeEnd ( ) ; this . _widgetOrigin = { } ; this . isDragging$ . getValue ( ) . sendToBack ( ) ; this . isDragging$ . next ( null ) ; this . userLayoutChange$ . next ( this . getLayoutData ( ) ) ; } onDrag ( action ) { if ( action . event . clientX === this . _event . clientX && action . event . clientY === this . _event . clientY ) { return ; } const mouseX = action . event . clientX - this . _event . clientX ; const mouseY = action . event . clientY - this . _event . clientY ; this . _event = action . event ; this . moveWidget ( action . widget , mouseX , mouseY ) ; } onDragScroll ( widget , event ) { this . moveWidget ( widget , event . offsetX , event . offsetY ) ; } getRowHeight ( ) { return this . _rowHeight ; } cacheWidgets ( ) { this . _cache = this . widgets . map ( widget => ( { id : widget . id , column : widget . getColumn ( ) , row : widget . getRow ( ) , columnSpan : widget . getColumnSpan ( ) , rowSpan : widget . getRowSpan ( ) , } ) ) ; return [ ... this . _cache ] ; } restoreWidgets ( ignoreActionWidget = false , cache = this . _cache , restoreSize = false ) { cache . filter ( widget => ! ignoreActionWidget || widget . id !== this . _actionWidget . widget . id ) . forEach ( widget => { const match = this . widgets . find ( wgt => wgt . id === widget . id ) ; if ( match ) { match . setColumn ( widget . column ) ; match . setRow ( widget . row ) ; if ( restoreSize ) { match . setColumnSpan ( widget . columnSpan ) ; match . setRowSpan ( widget . rowSpan ) ; } } } ) ; } /**
     * Return the set of widgets which overlap the given dashboard region.
     */ getOverlappingWidgets ( region , actionWidget ) { const widgetsToMove = [ ] ; for ( let row = region . row ; row < region . row + region . rowSpan ; row ++ ) { for ( let column = region . column ; column < region . column + region . columnSpan ; column ++ ) { this . getOccupiedSpaces ( ) . filter ( space => space . column === column && space . row === row && space . widget !== actionWidget ) . forEach ( space => widgetsToMove . push ( space . widget ) ) ; } } return widgetsToMove . filter ( ( wgt , idx , array ) => array . indexOf ( wgt ) === idx ) ; } /**
     * Resolve any overlapping widgets after a widget changes rowSpan/colSpan.
     */ resizeWidget ( widget ) { this . _actionWidget = { widget , direction : 0 } ; this . _widgetOrigin = widget ; const widgetRegion = { row : widget . row , column : widget . col , rowSpan : widget . rowSpan , columnSpan : widget . colSpan } ; let done = false ; const ITERATION_LIMIT = 100 ; for ( let i = 0 ; i <= ITERATION_LIMIT ; i ++ ) { this . shiftWidgetsFromRegion ( widgetRegion , widget ) ; done = this . getOverlappingWidgets ( widgetRegion , widget ) . length === 0 ; if ( done ) { break ; } } if ( ! done ) { throw new Error ( 'Unable to resolve overlapping widgets!' ) ; } this . shiftWidgetsUp ( ) ; this . _actionWidget = undefined ; this . _widgetOrigin = undefined ; } /**
     * Move any widgets which intersect with the given dashboard region.
     */ shiftWidgetsFromRegion ( region , actionWidget , validatePosition ? ) { const widgetsToMove = this . getOverlappingWidgets ( region , actionWidget ) ; if ( widgetsToMove . length === 0 ) { return ; } const unmovedWidgets = widgetsToMove . slice ( ) ; widgetsToMove . forEach ( widget => { const grid = this . getOccupiedSpaces ( ) . filter ( space => ! unmovedWidgets . find ( wgt => wgt === space . widget ) ) ; for ( let row = this . _widgetOrigin . row ; row < this . _widgetOrigin . row + this . _widgetOrigin . rowSpan ; row ++ ) { for ( let column = this . _widgetOrigin . column ; column < this . _widgetOrigin . column + this . _widgetOrigin . columnSpan ; column ++ ) { const requiredSpaces = this . getRequiredSpacesFromPoint ( widget , column , row ) ; const available = requiredSpaces . every ( space => { return ! grid . find ( gridSpace => gridSpace . column === space . column && gridSpace . row === space . row ) && space . column < this . getColumnCount ( ) ; } ) ; if ( available ) { widget . setColumn ( column ) ; widget . setRow ( row ) ; unmovedWidgets . splice ( unmovedWidgets . findIndex ( wgt => wgt === widget ) , 1 ) ; return ; } } } if ( this . canWidgetMoveRight ( widget , true ) ) { validatePosition ?. ( ActionDirection . Right ) ; return ; } if ( this . canWidgetMoveLeft ( widget , true ) ) { validatePosition ?. ( ActionDirection . Left ) ; return ; } const distance = ( this . _actionWidget . widget . getRow ( ) - widget . getRow ( ) ) + this . _actionWidget . widget . getRowSpan ( ) ; this . moveWidgetDown ( widget , distance ) ; } ) ; } /**
     * When dragging any widgets that need to be moved should be moved to an appropriate position
     */ shiftWidgets ( ) { const placeholder = this . placeholder$ . getValue ( ) ; this . shiftWidgetsFromRegion ( placeholder , this . _actionWidget . widget , this . validatePlaceholderPosition . bind ( this ) ) ; } param shiftDirection validatePlaceholderPosition ( shiftDirection ) { const placeholder = this . placeholder$ . getValue ( ) ; if ( this . getWidgetsAtPosition ( placeholder . column , placeholder . row , true ) . length > 0 ) { switch ( shiftDirection ) { case ActionDirection . Left : this . setPlaceholderBounds ( placeholder . visible , placeholder . x + this . getColumnWidth ( ) , placeholder . y , placeholder . width , placeholder . height ) ; break ; case ActionDirection . Right : this . setPlaceholderBounds ( placeholder . visible , placeholder . x - this . getColumnWidth ( ) , placeholder . y , placeholder . width , placeholder . height ) ; break ; } this . validatePlaceholderPosition ( shiftDirection ) ; } } /**
     * Determine if a widget can be moved left - or if it can move the widgets to the right to make space for the widget
     */ canWidgetMoveLeft ( widget , performMove = false , shift = 0 ) { const actionWgt = this . _widgetOrigin ; let colShift = shift ; if ( widget === this . _actionWidget . widget || widget . getColumn ( ) === 0 ) { return false ; } if ( colShift === 0 ) { if ( actionWgt . row !== widget . getRow ( ) ) { if ( actionWgt . column === widget . getColumn ( ) ) { colShift = actionWgt . columnSpan ; } else { const widgetDifference = actionWgt . column - widget . getColumn ( ) ; colShift = widget . getColumnSpan ( ) - widgetDifference ; } } else { colShift = 1 ; } } if ( isNaN ( colShift ) || colShift === 0 ) { return ; } const targetSpaces = this . getOccupiedSpaces ( ) . filter ( space => space . widget === widget ) . map ( space => { return { column : space . column - colShift , row : space . row , widget : space . widget } ; } ) ; if ( targetSpaces . find ( space => space . column < 0 ) ) { return false ; } const moveable = targetSpaces . every ( space => this . getWidgetsAtPosition ( space . column , space . row ) . filter ( wgt => wgt !== space . widget ) . every ( wgt => this . canWidgetMoveLeft ( wgt , false , colShift ) ) ) ; if ( performMove && moveable ) { targetSpaces . forEach ( space => this . getWidgetsAtPosition ( space . column , space . row ) . filter ( wgt => wgt !== space . widget ) . forEach ( wgt => this . canWidgetMoveLeft ( wgt , true , colShift ) ) ) ; const column = targetSpaces . reduce ( ( target , space ) => Math . min ( target , space . column ) , Infinity ) ; if ( column !== Infinity ) { widget . setColumn ( column ) ; } } return moveable ; } /**
     * Determine if a widget can be moved right - or if it can move the widgets to the right to make space for the widget
     */ canWidgetMoveRight ( widget , performMove = false , shift = 0 ) { const actionWgt = this . _widgetOrigin ; let colShift = shift ; if ( widget === this . _actionWidget . widget || widget . getColumn ( ) + widget . getColumnSpan ( ) === this . options . columns ) { return false ; } if ( colShift === 0 ) { if ( actionWgt . row !== widget . getRow ( ) ) { if ( actionWgt . column === widget . getColumn ( ) ) { colShift = actionWgt . columnSpan ; } else { const widgetDifference = widget . getColumn ( ) - actionWgt . column ; colShift = actionWgt . columnSpan - widgetDifference ; } } else { colShift = 1 ; } } if ( isNaN ( colShift ) || colShift === 0 ) { return ; } const targetSpaces = this . getOccupiedSpaces ( ) . filter ( space => space . widget === widget ) . map ( space => { return { column : space . column + colShift , row : space . row , widget : space . widget } ; } ) ; if ( targetSpaces . find ( space => space . column >= this . getColumnCount ( ) ) ) { return false ; } const moveable = targetSpaces . every ( space => this . getWidgetsAtPosition ( space . column , space . row ) . filter ( wgt => wgt !== space . widget ) . every ( wgt => this . canWidgetMoveRight ( wgt , false , colShift ) ) ) ; if ( performMove && moveable ) { targetSpaces . forEach ( space => this . getWidgetsAtPosition ( space . column , space . row ) . filter ( wgt => wgt !== space . widget ) . forEach ( wgt => this . canWidgetMoveRight ( wgt , true , colShift ) ) ) ; widget . setColumn ( widget . getColumn ( ) + colShift ) ; } return moveable ; } /**
     * Store the initial position of the widget being dragged
     */ setWidgetOrigin ( ) { this . _widgetOrigin = { column : this . _actionWidget . widget . getColumn ( ) , row : this . _actionWidget . widget . getRow ( ) , columnSpan : this . _actionWidget . widget . getColumnSpan ( ) , rowSpan : this . _actionWidget . widget . getRowSpan ( ) } ; } /**
     * Calculate all the required positions is a widget was to be positioned at a particular point
     */ getRequiredSpacesFromPoint ( widget , column , row ) { const spaces = [ ] ; for ( let y = row ; y < row + widget . getRowSpan ( ) ; y ++ ) { for ( let x = column ; x < column + widget . getColumnSpan ( ) ; x ++ ) { spaces . push ( { column : x , row : y , widget } ) ; } } return spaces ; } /**
     * Position widgets based on the position of the placeholder - this is temporary until confirmed
     */ updateWidgetPositions ( widget ) { const placeholder = this . placeholder$ . getValue ( ) ; for ( let column = placeholder . column ; column < placeholder . column + placeholder . columnSpan ; column ++ ) { for ( let row = placeholder . row ; row < placeholder . row + placeholder . rowSpan ; row ++ ) { this . getWidgetsAtPosition ( column , row , true ) . filter ( wgt => wgt !== widget ) . forEach ( wgt => this . moveWidgetDown ( wgt ) ) ; } } this . setDashboardHeight ( ) ; if ( this . _actionWidget . direction !== ActionDirection . Top && this . _actionWidget . direction !== ActionDirection . TopLeft && this . _actionWidget . direction !== ActionDirection . TopRight ) { this . shiftWidgetsUp ( ) ; } } param column param row param ignoreResizing getWidgetsAtPosition ( column , row , ignoreResizing = false ) { return this . getOccupiedSpaces ( ) . filter ( space => space . column === column && space . row === row ) . filter ( space => this . _actionWidget && space . widget !== this . _actionWidget . widget || ! ignoreResizing ) . map ( space => space . widget ) ; } /**
     * Update the placeholder visibility, position and size
     */ setPlaceholderBounds ( visible , x , y , width , height ) { const placeholder = this . placeholder$ . getValue ( ) ; placeholder . visible = visible ; placeholder . column = this . getPlaceholderColumn ( x , width ) ; placeholder . row = this . getPlaceholderRow ( y , height ) ; placeholder . columnSpan = this . getPlaceholderColumnSpan ( width ) ; placeholder . rowSpan = this . getPlaceholderRowSpan ( height ) ; const rowCount = this . widgets . filter ( widget => widget !== this . _actionWidget . widget ) . reduce ( ( previous , widget ) => Math . max ( widget . getRow ( ) + widget . getRowSpan ( ) , previous ) , 0 ) ; placeholder . row = Math . min ( placeholder . row , rowCount ) ; placeholder . x = ( placeholder . column * this . getColumnWidth ( ) ) + this . options . padding ; placeholder . y = ( placeholder . row * this . _rowHeight ) + this . options . padding ; placeholder . width = ( placeholder . columnSpan * this . getColumnWidth ( ) ) - ( this . options . padding * 2 ) ; placeholder . height = ( placeholder . rowSpan * this . _rowHeight ) - ( this . options . padding * 2 ) ; this . _actionWidget . widget . setColumn ( placeholder . column , false ) ; this . _actionWidget . widget . setRow ( placeholder . row , false ) ; this . _actionWidget . widget . setColumnSpan ( placeholder . columnSpan , false ) ; this . _actionWidget . widget . setRowSpan ( placeholder . rowSpan , false ) ; this . placeholder$ . next ( placeholder ) ; } /**
     * Get the placeholder column position
     */ getPlaceholderColumn ( x , width ) { const column = this . getColumnFromPx ( x , this . _actionWidget . direction === ActionDirection . Move ? Rounding . RoundUpOverHalf : Rounding . RoundDown ) ; const columnSpan = Math . floor ( width / this . getColumnWidth ( ) ) ; const upperLimit = this . getColumnCount ( ) - columnSpan ; if ( this . _actionWidget . direction !== ActionDirection . Left && this . _actionWidget . direction !== ActionDirection . TopLeft && this . _actionWidget . direction !== ActionDirection . BottomLeft ) { return Math . max ( Math . min ( column , upperLimit ) , 0 ) ; } const overflow = width % this . getColumnWidth ( ) ; return ( x <= 0 || overflow === 0 || columnSpan === 0 || overflow > ( this . getColumnWidth ( ) / 2 ) ) ? Math . max ( Math . min ( column , upperLimit ) , 0 ) : Math . max ( Math . min ( column + 1 , upperLimit ) , 0 ) ; } /**
     * Get the column span of the placeholder
     */ getPlaceholderColumnSpan ( width ) { const columnSpan = this . getColumnFromPx ( width ) ; if ( this . _actionWidget . direction !== ActionDirection . Right && this . _actionWidget . direction !== ActionDirection . TopRight && this . _actionWidget . direction !== ActionDirection . BottomRight && this . _actionWidget . direction !== ActionDirection . Left && this . _actionWidget . direction !== ActionDirection . TopLeft && this . _actionWidget . direction !== ActionDirection . BottomLeft ) { return Math . max ( columnSpan , 1 ) ; } const overflow = width % this . getColumnWidth ( ) ; return ( columnSpan > 0 && overflow > ( this . getColumnWidth ( ) / 2 ) ) ? Math . max ( columnSpan + 1 , 1 ) : Math . max ( columnSpan , 1 ) ; } /**
     * Get the row position of the placeholder
     */ getPlaceholderRow ( y , height ) { const row = this . getRowFromPx ( y , this . _actionWidget . direction === ActionDirection . Move ? Rounding . RoundUpOverHalf : Rounding . RoundDown ) ; const rowSpan = Math . ceil ( height / this . _rowHeight ) ; if ( this . _actionWidget . direction !== ActionDirection . Top && this . _actionWidget . direction !== ActionDirection . TopLeft && this . _actionWidget . direction !== ActionDirection . TopRight ) { return Math . max ( row , 0 ) ; } const overflow = height < this . _rowHeight ? 0 : height % this . _rowHeight ; return ( y <= 0 || rowSpan === 0 || overflow === 0 || overflow > ( this . _rowHeight / 2 ) ) ? Math . max ( row , 0 ) : Math . max ( row + 1 , 0 ) ; } /**
     * Get the row span of the placeholder
     */ getPlaceholderRowSpan ( height ) { const rowSpan = this . getRowFromPx ( height ) ; if ( this . _actionWidget . direction !== ActionDirection . Top && this . _actionWidget . direction !== ActionDirection . TopLeft && this . _actionWidget . direction !== ActionDirection . TopRight && this . _actionWidget . direction !== ActionDirection . Bottom && this . _actionWidget . direction !== ActionDirection . BottomLeft && this . _actionWidget . direction !== ActionDirection . BottomRight ) { return Math . max ( rowSpan , 1 ) ; } const overflow = height % this . _rowHeight ; return ( overflow > ( this . _rowHeight / 2 ) ) ? Math . max ( rowSpan + 1 , 1 ) : Math . max ( rowSpan , 1 ) ; } getColumnFromPx ( x , rounding = Rounding . RoundDown ) { const column = Math . floor ( x / Math . floor ( this . getColumnWidth ( ) ) ) ; const overflow = ( x % Math . floor ( this . getColumnWidth ( ) ) ) ; const half = this . getColumnWidth ( ) / 2 ; switch ( rounding ) { case Rounding . RoundDown : return column ; case Rounding . RoundDownBelowHalf : return overflow < half ? column : column + 1 ; case Rounding . RoundUpOverHalf : return overflow > half ? column + 1 : column ; case Rounding . RoundUp : return overflow > 0 ? column + 1 : column ; } } getRowFromPx ( y , rounding = Rounding . RoundDown ) { const row = Math . floor ( y / Math . floor ( this . _rowHeight ) ) ; const overflow = ( y % Math . floor ( this . _rowHeight ) ) ; const half = this . _rowHeight / 2 ; switch ( rounding ) { case Rounding . RoundDown : return row ; case Rounding . RoundDownBelowHalf : return overflow < half ? row : row + 1 ; case Rounding . RoundUpOverHalf : return overflow > half ? row + 1 : row ; case Rounding . RoundUp : return overflow > 0 ? row + 1 : row ; } } commitWidgetChanges ( ) { const placeholder = this . placeholder$ . getValue ( ) ; if ( placeholder . column === undefined || placeholder . row === undefined || placeholder . columnSpan === undefined || placeholder . rowSpan === undefined ) { return ; } if ( this . _actionWidget ) { this . _actionWidget . widget . setColumn ( placeholder . column ) ; this . _actionWidget . widget . setRow ( placeholder . row ) ; this . _actionWidget . widget . setColumnSpan ( placeholder . columnSpan ) ; this . _actionWidget . widget . setRowSpan ( placeholder . rowSpan ) ; } placeholder . column = undefined ; placeholder . row = undefined ; placeholder . columnSpan = undefined ; placeholder . rowSpan = undefined ; this . placeholder$ . next ( placeholder ) ; } /**
     * Get the current column width
     */ getColumnWidth ( ) { return Math . floor ( this . columnWidth ) ; } /**
     * Calculate the number of rows populated with widgets
     */ getRowCount ( ) { return this . widgets . reduce ( ( previous , widget ) => Math . max ( widget . getRow ( ) + widget . getRowSpan ( ) , previous ) , 0 ) ; } /**
     * Set the height of the dashboard container element
     */ setDashboardHeight ( ) { let rowCount = this . getRowCount ( ) ; if ( this . options . emptyRow ) { rowCount ++ ; } this . setDimensions ( undefined , rowCount * this . _rowHeight ) ; } param widget bringToFront ( widget ) { this . widgets . forEach ( _widget => _widget === widget ? _widget . bringToFront ( ) : _widget . sendToBack ( ) ) ; } param widget moveWidgetDown ( widget , distance = 1 ) { if ( distance < 0 ) { return ; } widget . setRow ( ( widget . getRow ( DashboardStackMode . Auto ) ) + distance ) ; this . forEachBlock ( widget , ( column , row ) => this . getWidgetsAtPosition ( column , row , true ) . filter ( wgt => wgt !== widget ) . forEach ( wgt => this . moveWidgetDown ( wgt , distance ) ) ) ; } /**
     * Widgets should not be allowed to have a vacant space above them - if there is one they should move upwards to fill it
     */ shiftWidgetsUp ( ) { let stable = true ; this . widgets . forEach ( widget => { const widgetIsOnTopRow = widget . getRow ( ) === 0 ; const widgetIsBeingResized = this . _actionWidget ?. widget === widget ; const widgetShouldBeAutoPositioned = widget . autoPositioning || this . stacked ; const widgetIsBeingMoved = ! widgetShouldBeAutoPositioned && this . isDragging$ . value ?. id === widget . id ; if ( widgetIsOnTopRow || widgetIsBeingResized || widgetIsBeingMoved || ( ! widgetShouldBeAutoPositioned && ! this . _cache ) ) { return ; } if ( ! widgetShouldBeAutoPositioned ) { const cachedVersionOfWidget = this . _cache . find ( cachedWidget => cachedWidget . id === widget . id ) ; const isPreviousPositionAvailable = this . getPositionAvailable ( cachedVersionOfWidget . column , cachedVersionOfWidget . row , cachedVersionOfWidget . columnSpan , cachedVersionOfWidget . rowSpan , widget ) ; if ( isPreviousPositionAvailable && widget . row !== cachedVersionOfWidget . row ) { widget . setRow ( cachedVersionOfWidget . row ) ; stable = false ; } return ; } if ( this . getPositionAvailable ( widget . getColumn ( ) , widget . getRow ( ) - 1 , widget . getColumnSpan ( ) , 1 ) ) { widget . setRow ( widget . getRow ( ) - 1 ) ; stable = false ; } } ) ; if ( ! stable ) { this . shiftWidgetsUp ( ) ; return true ; } return false ; } param widget param callback forEachBlock ( widget , callback ) { for ( let row = widget . getRow ( ) ; row < widget . getRow ( ) + widget . getRowSpan ( ) ; row ++ ) { for ( let column = widget . getColumn ( ) ; column < widget . getColumn ( ) + widget . getColumnSpan ( ) ; column ++ ) { callback . call ( widget , column , row ) ; } } } getWidgetBelow ( widget ) { const target = this . getWidgetsAtPosition ( widget . getColumn ( ) , widget . getRow ( ) + widget . getRowSpan ( ) , true ) ; return target . length > 0 ? target [ 0 ] : null ; } /**
     * Returns the number of columns available
     */ getColumnCount ( ) { return this . stacked ? 1 : this . options . columns ; } onShiftStart ( widget ) { this . onDragStart ( { direction : ActionDirection . Move , widget } ) ; } /** Programmatically move a widget in a given direction */ onShift ( widget , direction ) { let deltaX = 0 , deltaY = 0 ; switch ( direction ) { case ActionDirection . Top : deltaY = - this . getRowHeight ( ) ; break ; case ActionDirection . Right : deltaX = this . getColumnWidth ( ) ; break ; case ActionDirection . Bottom : { deltaY = this . getRowHeight ( ) ; break ; } case ActionDirection . Left : deltaX = - this . getColumnWidth ( ) ; break ; } const dimensions = { x : widget . x + deltaX , y : widget . y + deltaY , width : widget . width , height : widget . height } ; this . setPlaceholderBounds ( false , dimensions . x , dimensions . y , dimensions . width , dimensions . height ) ; const { x , y } = this . placeholder$ . value ; widget . setBounds ( x - this . options . padding , y - this . options . padding , dimensions . width , dimensions . height ) ; this . setDashboardHeight ( ) ; } onShiftEnd ( ) { this . shiftWidgets ( ) ; this . setDashboardHeight ( ) ; this . onDragEnd ( ) ; } /** Programmatically resize a widget in a given direction */ onResize ( widget , direction ) { let deltaX = 0 , deltaY = 0 ; switch ( direction ) { case ActionDirection . Top : deltaY = - this . getRowHeight ( ) ; break ; case ActionDirection . Right : deltaX = this . getColumnWidth ( ) ; break ; case ActionDirection . Bottom : deltaY = this . getRowHeight ( ) ; break ; case ActionDirection . Left : deltaX = - this . getColumnWidth ( ) ; break ; } const dimensions = { x : widget . x , y : widget . y , width : widget . width + deltaX , height : widget . height + deltaY } ; const currentWidth = widget . x + widget . width ; const currentHeight = widget . y + widget . height ; if ( dimensions . x < 0 ) { dimensions . x = 0 ; dimensions . width = currentWidth ; } if ( dimensions . y < 0 ) { dimensions . y = 0 ; dimensions . height = currentHeight ; } if ( ( dimensions . x + dimensions . width ) > this . getColumnWidth ( ) * this . getColumnCount ( ) ) { dimensions . width = widget . width ; } if ( dimensions . width < this . getColumnWidth ( ) ) { dimensions . x = widget . x ; dimensions . width = this . getColumnWidth ( ) ; } if ( dimensions . height < this . getRowHeight ( ) ) { dimensions . y = widget . y ; dimensions . height = this . getRowHeight ( ) ; } const minWidth = this . getColumnWidth ( ) * widget . minColSpan ; const isBelowMinWidth = dimensions . width < minWidth ; if ( isBelowMinWidth ) { dimensions . x = widget . x ; dimensions . width = minWidth ; } const minHeight = this . options . rowHeight * widget . minRowSpan ; const isBelowMinHeight = dimensions . height < minHeight ; if ( isBelowMinHeight ) { dimensions . y = widget . y ; dimensions . height = minHeight ; } widget . setBounds ( dimensions . x , dimensions . y , dimensions . width , dimensions . height ) ; this . setPlaceholderBounds ( false , dimensions . x , dimensions . y , dimensions . width , dimensions . height ) ; this . setDashboardHeight ( ) ; } getSurroundingWidgets ( widget , direction ) { let widgets = [ ] ; for ( let column = widget . getColumn ( ) ; column < widget . getColumn ( ) + widget . getColumnSpan ( ) ; column ++ ) { switch ( direction ) { case ActionDirection . Top : widgets = [ ... widgets , ... this . getWidgetsAtPosition ( column , widget . getRow ( ) - 1 ) ] ; break ; case ActionDirection . Bottom : widgets = [ ... widgets , ... this . getWidgetsAtPosition ( column , widget . getRow ( ) + widget . getRowSpan ( ) ) ] ; break ; } } return widgets ; } private moveWidget ( widget , offsetX , offsetY ) { const dimensions = { x : widget . x + offsetX , y : widget . y + offsetY , width : widget . width , height : widget . height } ; this . restoreWidgets ( true ) ; widget . setBounds ( dimensions . x , dimensions . y , dimensions . width , dimensions . height ) ; this . setPlaceholderBounds ( true , dimensions . x , dimensions . y , dimensions . width , dimensions . height ) ; this . shiftWidgets ( ) ; this . setDashboardHeight ( ) ; } } export const defaultOptions = { columns : 5 , padding : 5 , minWidth : 100 , minHeight : 100 , emptyRow : true } ; export interface DashboardDimensions { width ? : number ; height ? : number ; } export interface DashboardWidgetDimensions { x : number ; y : number ; width : number ; height : number ; } export interface DashboardAction { widget : DashboardWidgetComponent ; direction : ActionDirection ; event ? : MouseEvent ; handle ? : HTMLElement ; } export interface DashboardSpace { widget : DashboardWidgetComponent ; column : number ; row : number ; } export interface DashboardRegion { column ? : number ; row ? : number ; columnSpan ? : number ; rowSpan ? : number ; } export interface DashboardPlaceholder extends DashboardRegion { visible : boolean ; x : number ; y : number ; width : number ; height : number ; } export interface DashboardCache { id : string ; column : number ; row : number ; columnSpan : number ; rowSpan : number ; } export interface DashboardLayoutData { id : string ; col : number ; row : number ; colSpan : number ; rowSpan : number ; minColSpan ? : number ; minRowSpan ? : number ; } export enum ActionDirection { Top = 0 , TopRight = 1 , Right = 2 , BottomRight = 3 , Bottom = 4 , BottomLeft = 5 , Left = 6 , TopLeft = 7 , Move = 8 } export enum Rounding { RoundDown , RoundDownBelowHalf , RoundUp , RoundUpOverHalf }