import * as React from "react" ; import { Utils as CoreUtils } from "@blueprintjs/core" ; import { IFocusedCellCoordinates } from "../common/cell" ; import * as FocusedCellUtils from "../common/internal/focusedCellUtils" ; import * as PlatformUtils from "../common/internal/platformUtils" ; import { Utils } from "../common/utils" ; import { Region , Regions } from "../regions" ; import { DragEvents } from "./dragEvents" ; import { Draggable , IDraggableProps } from "./draggable" ; import { ICoordinateData } from "./dragTypes" ; export type ISelectedRegionTransform = ( region : Region , event : MouseEvent | KeyboardEvent , coords ? : ICoordinateData ) => Region ; export type SelectedRegionTransform = ISelectedRegionTransform ; export interface ISelectableProps { default enableMultipleSelection ? : boolean ; /**
     * The currently focused cell.
     */ focusedCell ? : IFocusedCellCoordinates ; /**
     * When the user focuses something, this callback is called with new
     * focused cell coordinates. This should be considered the new focused cell
     * state for the entire table.
     */ onFocusedCell : ( focusedCell : IFocusedCellCoordinates ) => void ; /**
     * When the user selects something, this callback is called with a new
     * array of `Region`s. This array should be considered the new selection
     * state for the entire table.
     */ onSelection : ( regions : Region [ ] ) => void ; /**
     * An additional convenience callback invoked when the user releases the
     * mouse from either a click or a drag, indicating that the selection
     * interaction has ended.
     */ onSelectionEnd ? : ( regions : Region [ ] ) => void ; default selectedRegions ? : Region [ ] ; /**
     * An optional transform function that will be applied to the located
     * `Region`.
     *
     * This allows you to, for example, convert cell `Region`s into row
     * `Region`s while maintaining the existing multi-select and meta-click
     * functionality.
     */ selectedRegionTransform ? : ISelectedRegionTransform ; } export interface IDragSelectableProps extends ISelectableProps { /**
     * A list of CSS selectors that should _not_ trigger selection when a `mousedown` occurs inside of them.
     */ ignoredSelectors ? : string [ ] ; default disabled ? : boolean | ( ( event : MouseEvent ) => boolean ) ; /**
     * A callback that determines a `Region` for the single `MouseEvent`. If
     * no valid region can be found, `null` may be returned.
     */ locateClick : ( event : MouseEvent ) => Region ; /**
     * A callback that determines a `Region` for the `MouseEvent` and
     * coordinate data representing a drag. If no valid region can be found,
     * `null` may be returned.
     */ locateDrag : ( event : MouseEvent , coords : ICoordinateData , returnEndOnly ? : boolean ) => Region ; } export class DragSelectable extends React . PureComponent < IDragSelectableProps > { public static defaultProps : Partial < IDragSelectableProps > = { disabled : false , enableMultipleSelection : false , selectedRegions : [ ] , } ; private didExpandSelectionOnActivate = false ; private lastEmittedSelectedRegions : Region [ ] ; public render ( ) { const draggableProps = this . getDraggableProps ( ) ; return ( < Draggable { ... draggableProps } preventDefault = { false } > { this . props . children } < /Draggable>); } private getDraggableProps ( ) { return this . props . onSelection == null ? { } : { onActivate : this . handleActivate , onClick : this . handleClick , onDragEnd : this . handleDragEnd , onDragMove : this . handleDragMove , } ; } private handleActivate = ( event ) => { const { locateClick , selectedRegions , selectedRegionTransform } = this . props ; if ( this . shouldIgnoreMouseDown ( event ) ) { return false ; } let region = locateClick ( event ) ; if ( ! Regions . isValid ( region ) ) { return false ; } if ( selectedRegionTransform != null ) { region = selectedRegionTransform ( region , event ) ; } const foundIndex = Regions . findMatchingRegion ( selectedRegions , region ) ; const matchesExistingSelection = foundIndex !== - 1 ; if ( matchesExistingSelection && DragEvents . isAdditive ( event ) ) { this . handleClearSelectionAtIndex ( foundIndex ) ; return false ; } if ( matchesExistingSelection ) { this . handleClearAllSelectionsNotAtIndex ( foundIndex ) ; } else if ( this . shouldExpandSelection ( event ) ) { this . handleExpandSelection ( region ) ; } else if ( this . shouldAddDisjointSelection ( event ) ) { this . handleAddDisjointSelection ( region ) ; } else { this . handleReplaceSelection ( region ) ; } return true ; } ; private handleDragMove = ( event , coords ) => { const { enableMultipleSelection , focusedCell , locateClick , locateDrag , selectedRegions , selectedRegionTransform , } = this . props ; let region = enableMultipleSelection ? locateDrag ( event , coords , this . didExpandSelectionOnActivate ) : locateClick ( event ) ; if ( ! Regions . isValid ( region ) ) { return ; } else if ( selectedRegionTransform != null ) { region = selectedRegionTransform ( region , event , coords ) ; } const nextSelectedRegions = this . didExpandSelectionOnActivate ? this . expandSelectedRegions ( selectedRegions , region , focusedCell ) : Regions . update ( selectedRegions , region ) ; this . maybeInvokeSelectionCallback ( nextSelectedRegions ) ; if ( ! enableMultipleSelection ) { const lastIndex = nextSelectedRegions . length - 1 ; const mostRecentRegion = nextSelectedRegions [ lastIndex ] ; this . invokeOnFocusCallbackForRegion ( mostRecentRegion , lastIndex ) ; } } ; private handleDragEnd = ( ) => { this . finishInteraction ( ) ; } ; private handleClick = ( ) => { this . finishInteraction ( ) ; } ; private shouldExpandSelection = ( event ) => { const { enableMultipleSelection } = this . props ; return enableMultipleSelection && event . shiftKey ; } ; private shouldAddDisjointSelection = ( event ) => { const { enableMultipleSelection } = this . props ; return enableMultipleSelection && DragEvents . isAdditive ( event ) ; } ; private shouldIgnoreMouseDown ( event ) { const { disabled , ignoredSelectors = [ ] } = this . props ; const element = event . target as HTMLElement ; const isLeftClick = Utils . isLeftClick ( event ) ; const isContextMenuTrigger = isLeftClick && event . ctrlKey && PlatformUtils . isMac ( ) ; const isDisabled = typeof disabled === "function" ? disabled ( event ) : disabled ; return ( ! isLeftClick || isContextMenuTrigger || isDisabled || ignoredSelectors . some ( ( selector ) => element . closest ( selector ) != null ) ) ; } private handleClearSelectionAtIndex = ( selectedRegionIndex ) => { const { selectedRegions } = this . props ; const nextSelectedRegions = selectedRegions . slice ( ) ; nextSelectedRegions . splice ( selectedRegionIndex , 1 ) ; this . maybeInvokeSelectionCallback ( nextSelectedRegions ) ; if ( nextSelectedRegions . length > 0 ) { const lastIndex = nextSelectedRegions . length - 1 ; this . invokeOnFocusCallbackForRegion ( nextSelectedRegions [ lastIndex ] , lastIndex ) ; } } ; private handleClearAllSelectionsNotAtIndex = ( selectedRegionIndex ) => { const { selectedRegions } = this . props ; const nextSelectedRegion = selectedRegions [ selectedRegionIndex ] ; this . maybeInvokeSelectionCallback ( [ nextSelectedRegion ] ) ; this . invokeOnFocusCallbackForRegion ( nextSelectedRegion , 0 ) ; } ; private handleExpandSelection = ( region ) => { const { focusedCell , selectedRegions } = this . props ; this . didExpandSelectionOnActivate = true ; const nextSelectedRegions = this . expandSelectedRegions ( selectedRegions , region , focusedCell ) ; this . maybeInvokeSelectionCallback ( nextSelectedRegions ) ; if ( selectedRegions == null || selectedRegions . length === 0 ) { this . invokeOnFocusCallbackForRegion ( region ) ; } } ; private handleAddDisjointSelection = ( region ) => { const { selectedRegions } = this . props ; const nextSelectedRegions = Regions . add ( selectedRegions , region ) ; this . maybeInvokeSelectionCallback ( nextSelectedRegions ) ; this . invokeOnFocusCallbackForRegion ( region , nextSelectedRegions . length - 1 ) ; } ; private handleReplaceSelection = ( region ) => { const nextSelectedRegions = [ region ] ; this . maybeInvokeSelectionCallback ( nextSelectedRegions ) ; this . invokeOnFocusCallbackForRegion ( region ) ; } ; private maybeInvokeSelectionCallback ( nextSelectedRegions ) { const { onSelection } = this . props ; if ( this . lastEmittedSelectedRegions == null || ! CoreUtils . deepCompareKeys ( this . lastEmittedSelectedRegions , nextSelectedRegions ) ) { onSelection ( nextSelectedRegions ) ; this . lastEmittedSelectedRegions = nextSelectedRegions ; } } private invokeOnFocusCallbackForRegion = ( focusRegion , focusSelectionIndex = 0 ) => { const { onFocusedCell } = this . props ; const focusedCellCoords = Regions . getFocusCellCoordinatesFromRegion ( focusRegion ) ; onFocusedCell ( FocusedCellUtils . toFullCoordinates ( focusedCellCoords , focusSelectionIndex ) ) ; } ; private finishInteraction = ( ) => { this . props . onSelectionEnd ?. ( this . props . selectedRegions ) ; this . didExpandSelectionOnActivate = false ; this . lastEmittedSelectedRegions = null ; } ; /**
     * Expands the last-selected region to the new region, and replaces the
     * last-selected region with the expanded region. If a focused cell is provided,
     * the focused cell will serve as an anchor for the expansion.
     */ private expandSelectedRegions ( regions , region , focusedCell ? ) { if ( regions . length === 0 ) { return [ region ] ; } else if ( focusedCell != null ) { const expandedRegion = FocusedCellUtils . expandFocusedRegion ( focusedCell , region ) ; return Regions . update ( regions , expandedRegion ) ; } else { const expandedRegion = Regions . expandRegion ( regions [ regions . length - 1 ] , region ) ; return Regions . update ( regions , expandedRegion ) ; } } }