import classNames from "classnames" ; import * as React from "react" ; import { DISPLAYNAME_PREFIX , Icon , Props , Popover , Position } from "@blueprintjs/core" ; import * as Classes from "../../common/classes" ; import { Utils } from "../../common/utils" ; import { Locator } from "../../locator" ; const CONTENT_DIV_WIDTH_DELTA = 25 ; export enum TruncatedPopoverMode { ALWAYS = "always" , NEVER = "never" , WHEN_TRUNCATED = "when-truncated" , WHEN_TRUNCATED_APPROX = "when-truncated-approx" } export interface ITrucatedFormateMeasureByApproximateOptions { default approximateCharWidth : number ; default approximateLineHeight : number ; default cellHorizontalPadding : number ; default numBufferLines : number ; } export type TruncatedFormatProps = ITruncatedFormatProps ; export interface ITruncatedFormatProps extends Props { children ? : string ; default detectTruncation ? : boolean ; /**
     * Values to use for character width, line height, cell padding, and buffer lines desired, when using approximate truncation.
     * These values are used to guess at the size of the text and determine if the popover should be drawn. They should work well
     * enough for default table styles, but may need to be overridden for more accuracy if the default styles or font size, etc
     * are changed.
     */ measureByApproxOptions ? : ITrucatedFormateMeasureByApproximateOptions ; /**
     * Height of the parent cell. Used by shouldComponentUpdate only
     */ parentCellHeight ? : number ; /**
     * Width of the parent cell. Used by shouldComponentUpdate only
     */ parentCellWidth ? : number ; default preformatted ? : boolean ; default showPopover ? : TruncatedPopoverMode ; default truncateLength ? : number ; default truncationSuffix ? : string ; } export interface ITruncatedFormatState { isTruncated ? : boolean ; isPopoverOpen ? : boolean ; } export class TruncatedFormat extends React . PureComponent < ITruncatedFormatProps , ITruncatedFormatState > { public static displayName = `${ DISPLAYNAME_PREFIX }.TruncatedFormat` ; public static defaultProps : ITruncatedFormatProps = { detectTruncation : false , measureByApproxOptions : { approximateCharWidth : 8 , approximateLineHeight : 18 , cellHorizontalPadding : 2 * Locator . CELL_HORIZONTAL_PADDING , numBufferLines : 0 , } , preformatted : false , showPopover : TruncatedPopoverMode . WHEN_TRUNCATED , truncateLength : 2000 , truncationSuffix : "..." , } ; public state : ITruncatedFormatState = { isPopoverOpen : false , isTruncated : false , } ; private contentDiv : HTMLDivElement ; public componentDidMount ( ) { this . setTruncationState ( ) ; } public componentDidUpdate ( ) { this . setTruncationState ( ) ; } public render ( ) { const { children , detectTruncation , truncateLength , truncationSuffix } = this . props ; const content = "" + children ; let cellContent = content ; if ( ! detectTruncation && truncateLength > 0 && cellContent . length > truncateLength ) { cellContent = cellContent . substring ( 0 , truncateLength ) + truncationSuffix ; } if ( this . shouldShowPopover ( content ) ) { const className = classNames ( this . props . className , Classes . TABLE_TRUNCATED_FORMAT ) ; return ( < div className = { className } > < div className = { Classes . TABLE_TRUNCATED_VALUE } ref = { this . handleContentDivRef } > { cellContent } < /div> { this . renderPopover ( ) } < /div>); } else { const className = classNames ( this . props . className , Classes . TABLE_TRUNCATED_FORMAT_TEXT ) ; return ( < div className = { className } ref = { this . handleContentDivRef } > { cellContent } < /div>); } } private renderPopover ( ) { const { children , preformatted } = this . props ; if ( this . state . isPopoverOpen ) { const popoverClasses = classNames ( Classes . TABLE_TRUNCATED_POPOVER , preformatted ? Classes . TABLE_POPOVER_WHITESPACE_PRE : Classes . TABLE_POPOVER_WHITESPACE_NORMAL ) ; const popoverContent = < div className = { popoverClasses } > { children } < /div>; return ( < Popover className = { Classes . TABLE_TRUNCATED_POPOVER_TARGET } modifiers = { { preventOverflow : { boundariesElement : "window" } } } content = { popoverContent } position = { Position . BOTTOM } isOpen = { true } onClose = { this . handlePopoverClose } > < Icon icon = "more" / > { } < /Popover>); } else { return ( < span className = { Classes . TABLE_TRUNCATED_POPOVER_TARGET } onClick = { this . handlePopoverOpen } > < Icon icon = "more" / > < /span>); } } private handleContentDivRef = ( ref ) => ( this . contentDiv = ref ) ; private handlePopoverOpen = ( ) => { this . setState ( { isPopoverOpen : true } ) ; } ; private handlePopoverClose = ( ) => { this . setState ( { isPopoverOpen : false } ) ; } ; private shouldShowPopover ( content ) { const { detectTruncation , measureByApproxOptions , showPopover , truncateLength } = this . props ; switch ( showPopover ) { case TruncatedPopoverMode . ALWAYS : return true ; case TruncatedPopoverMode . NEVER : return false ; case TruncatedPopoverMode . WHEN_TRUNCATED : return detectTruncation ? this . state . isTruncated : truncateLength > 0 && content . length > truncateLength ; case TruncatedPopoverMode . WHEN_TRUNCATED_APPROX : if ( ! detectTruncation ) { return truncateLength > 0 && content . length > truncateLength ; } if ( this . props . parentCellHeight == null || this . props . parentCellWidth == null ) { return false ; } const { approximateCharWidth , approximateLineHeight , cellHorizontalPadding , numBufferLines , } = measureByApproxOptions ; const cellWidth = this . props . parentCellWidth ; const approxCellHeight = Utils . getApproxCellHeight ( content , cellWidth , approximateCharWidth , approximateLineHeight , cellHorizontalPadding , numBufferLines ) ; const shouldTruncate = approxCellHeight > this . props . parentCellHeight ; return shouldTruncate ; default : return false ; } } private setTruncationState ( ) { if ( ! this . props . detectTruncation || this . props . showPopover !== TruncatedPopoverMode . WHEN_TRUNCATED ) { return ; } if ( this . contentDiv === undefined ) { this . setState ( { isTruncated : false } ) ; return ; } const { isTruncated } = this . state ; const { clientHeight : containerHeight , clientWidth : containerWidth , scrollHeight : actualContentHeight , scrollWidth : contentWidth , } = this . contentDiv ; const actualContentWidth = isTruncated ? contentWidth - CONTENT_DIV_WIDTH_DELTA : contentWidth ; const shouldTruncate = ( isTruncated && actualContentWidth === containerWidth ) || actualContentWidth > containerWidth || actualContentHeight > containerHeight ; this . setState ( { isTruncated : shouldTruncate } ) ; } }