mirror of
https://github.com/mattermost/focalboard.git
synced 2025-02-01 19:14:35 +02:00
[GH-353] initial implmentation of autosize columns (#355)
* initial implmentation of autosize columns * fix lint * move to utils * table header provide its own length * implement padding and font from DOM * cleanup, lint fixes * more cleanup
This commit is contained in:
parent
d8a2c489bb
commit
019dd3da8f
@ -165,6 +165,7 @@ class CenterPanel extends React.Component<Props, State> {
|
||||
showCard={this.showCard}
|
||||
addCard={(show) => this.addCard('', show)}
|
||||
onCardClicked={this.cardClicked}
|
||||
intl={this.props.intl}
|
||||
/>}
|
||||
{activeView.viewType === 'gallery' &&
|
||||
<Gallery
|
||||
|
@ -7,6 +7,7 @@ import './horizontalGrip.scss'
|
||||
|
||||
type Props = {
|
||||
templateId: string
|
||||
onAutoSizeColumn: (columnID: string) => void;
|
||||
}
|
||||
|
||||
const HorizontalGrip = React.memo((props: Props): JSX.Element => {
|
||||
@ -19,6 +20,7 @@ const HorizontalGrip = React.memo((props: Props): JSX.Element => {
|
||||
<div
|
||||
ref={drag}
|
||||
className='HorizontalGrip'
|
||||
onDoubleClick={() => props.onAutoSizeColumn(props.templateId)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
import {FormattedMessage, IntlShape} from 'react-intl'
|
||||
import {useDrop, useDragLayer} from 'react-dnd'
|
||||
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
@ -10,8 +10,11 @@ import {Card} from '../../blocks/card'
|
||||
import {Constants} from '../../constants'
|
||||
import mutator from '../../mutator'
|
||||
import {Utils} from '../../utils'
|
||||
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
|
||||
import {OctoUtils} from './../../octoUtils'
|
||||
|
||||
import './table.scss'
|
||||
import TableHeader from './tableHeader'
|
||||
import TableRow from './tableRow'
|
||||
@ -21,6 +24,7 @@ type Props = {
|
||||
selectedCardIds: string[]
|
||||
readonly: boolean
|
||||
cardIdToFocusOnRender: string
|
||||
intl: IntlShape
|
||||
showCard: (cardId?: string) => void
|
||||
addCard: (show: boolean) => Promise<void>
|
||||
onCardClicked: (e: React.MouseEvent, card: Card) => void
|
||||
@ -43,6 +47,8 @@ const Table = (props: Props) => {
|
||||
}
|
||||
})
|
||||
|
||||
const columnRefs: Map<string, React.RefObject<HTMLDivElement>> = new Map()
|
||||
|
||||
const [, drop] = useDrop(() => ({
|
||||
accept: 'horizontalGrip',
|
||||
drop: (item: {id: string}, monitor) => {
|
||||
@ -59,6 +65,44 @@ const Table = (props: Props) => {
|
||||
},
|
||||
}), [activeView])
|
||||
|
||||
const onAutoSizeColumn = ((columnID: string, headerWidth: number) => {
|
||||
let longestSize = headerWidth
|
||||
const visibleProperties = board.cardProperties.filter(() => activeView.visiblePropertyIds.includes(columnID))
|
||||
const columnRef = columnRefs.get(columnID)
|
||||
if (!columnRef?.current) {
|
||||
return
|
||||
}
|
||||
const {fontDescriptor, padding} = Utils.getFontAndPaddingFromCell(columnRef.current)
|
||||
|
||||
cards.forEach((card) => {
|
||||
let displayValue = card.title
|
||||
if (columnID !== Constants.titleColumnId) {
|
||||
const template = visibleProperties.find((t) => t.id === columnID)
|
||||
if (!template) {
|
||||
return
|
||||
}
|
||||
|
||||
displayValue = OctoUtils.propertyDisplayValue(card, card.properties[columnID], template!, props.intl) || ''
|
||||
if (template.type === 'select') {
|
||||
displayValue = displayValue.toUpperCase()
|
||||
}
|
||||
}
|
||||
const thisLen = Utils.getTextWidth(displayValue, fontDescriptor) + padding
|
||||
if (thisLen > longestSize) {
|
||||
longestSize = thisLen
|
||||
}
|
||||
})
|
||||
|
||||
if (longestSize === 0) {
|
||||
return
|
||||
}
|
||||
const columnWidths = {...activeView.columnWidths}
|
||||
columnWidths[columnID] = longestSize
|
||||
const newView = new MutableBoardView(activeView)
|
||||
newView.columnWidths = columnWidths
|
||||
mutator.updateBlock(newView, activeView, 'autosize column')
|
||||
})
|
||||
|
||||
const onDropToCard = (srcCard: Card, dstCard: Card) => {
|
||||
Utils.log(`onDropToCard: ${dstCard.title}`)
|
||||
const {selectedCardIds} = props
|
||||
@ -119,6 +163,7 @@ const Table = (props: Props) => {
|
||||
template={{id: Constants.titleColumnId, name: 'title', type: 'text', options: []}}
|
||||
offset={resizingColumn === Constants.titleColumnId ? offset : 0}
|
||||
onDrop={onDropToColumn}
|
||||
onAutoSizeColumn={onAutoSizeColumn}
|
||||
/>
|
||||
|
||||
{/* Table header row */}
|
||||
@ -142,6 +187,7 @@ const Table = (props: Props) => {
|
||||
key={template.id}
|
||||
offset={resizingColumn === template.id ? offset : 0}
|
||||
onDrop={onDropToColumn}
|
||||
onAutoSizeColumn={onAutoSizeColumn}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@ -170,6 +216,7 @@ const Table = (props: Props) => {
|
||||
onDrop={onDropToCard}
|
||||
offset={offset}
|
||||
resizingColumn={resizingColumn}
|
||||
columnRefs={columnRefs}
|
||||
/>)
|
||||
|
||||
return tableRow
|
||||
|
@ -10,6 +10,7 @@ import SortUpIcon from '../../widgets/icons/sortUp'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import Label from '../../widgets/label'
|
||||
import {useSortable} from '../../hooks/sortable'
|
||||
import {Utils} from '../../utils'
|
||||
|
||||
import HorizontalGrip from './horizontalGrip'
|
||||
|
||||
@ -24,6 +25,7 @@ type Props = {
|
||||
template: IPropertyTemplate
|
||||
offset: number
|
||||
onDrop: (template: IPropertyTemplate, container: IPropertyTemplate) => void
|
||||
onAutoSizeColumn: (columnID: string, headerWidth: number) => void
|
||||
}
|
||||
|
||||
const TableHeader = React.memo((props: Props): JSX.Element => {
|
||||
@ -33,6 +35,16 @@ const TableHeader = React.memo((props: Props): JSX.Element => {
|
||||
return Math.max(Constants.minColumnWidth, (props.boardTree.activeView.columnWidths[templateId] || 0) + props.offset)
|
||||
}
|
||||
|
||||
const onAutoSizeColumn = (templateId: string) => {
|
||||
let width = Constants.minColumnWidth
|
||||
if (columnRef.current) {
|
||||
const {fontDescriptor, padding} = Utils.getFontAndPaddingFromCell(columnRef.current)
|
||||
const textWidth = Utils.getTextWidth(columnRef.current.innerText.toUpperCase(), fontDescriptor)
|
||||
width = textWidth + padding
|
||||
}
|
||||
props.onAutoSizeColumn(templateId, width)
|
||||
}
|
||||
|
||||
let className = 'octo-table-cell header-cell'
|
||||
if (isOver) {
|
||||
className += ' dragover'
|
||||
@ -59,7 +71,10 @@ const TableHeader = React.memo((props: Props): JSX.Element => {
|
||||
<div className='octo-spacer'/>
|
||||
|
||||
{!props.readonly &&
|
||||
<HorizontalGrip templateId={props.template.id}/>
|
||||
<HorizontalGrip
|
||||
templateId={props.template.id}
|
||||
onAutoSizeColumn={onAutoSizeColumn}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
@ -24,12 +24,13 @@ type Props = {
|
||||
readonly: boolean
|
||||
offset: number
|
||||
resizingColumn: string
|
||||
columnRefs: Map<string, React.RefObject<HTMLDivElement>>
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||
onDrop: (srcCard: Card, dstCard: Card) => void
|
||||
}
|
||||
|
||||
const TableRow = React.memo((props: Props) => {
|
||||
const {boardTree, onSaveWithEnter} = props
|
||||
const {boardTree, onSaveWithEnter, columnRefs} = props
|
||||
const {board, activeView} = boardTree
|
||||
|
||||
const titleRef = useRef<{focus(selectAll?: boolean): void}>(null)
|
||||
@ -56,6 +57,10 @@ const TableRow = React.memo((props: Props) => {
|
||||
className += ' dragover'
|
||||
}
|
||||
|
||||
if (!columnRefs.get(Constants.titleColumnId)) {
|
||||
columnRefs.set(Constants.titleColumnId, React.createRef())
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
@ -70,6 +75,7 @@ const TableRow = React.memo((props: Props) => {
|
||||
className='octo-table-cell title-cell'
|
||||
id='mainBoardHeader'
|
||||
style={{width: columnWidth(Constants.titleColumnId)}}
|
||||
ref={columnRefs.get(Constants.titleColumnId)}
|
||||
>
|
||||
<div className='octo-icontitle'>
|
||||
<div className='octo-icon'>{card.icon}</div>
|
||||
@ -104,18 +110,22 @@ const TableRow = React.memo((props: Props) => {
|
||||
{board.cardProperties.
|
||||
filter((template) => activeView.visiblePropertyIds.includes(template.id)).
|
||||
map((template) => {
|
||||
if (!columnRefs.get(template.id)) {
|
||||
columnRefs.set(template.id, React.createRef())
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className='octo-table-cell'
|
||||
key={template.id}
|
||||
style={{width: columnWidth(template.id)}}
|
||||
ref={columnRefs.get(template.id)}
|
||||
>
|
||||
<PropertyValueElement
|
||||
readOnly={props.readonly}
|
||||
card={card}
|
||||
boardTree={boardTree}
|
||||
propertyTemplate={template}
|
||||
emptyDisplayValue='Empty'
|
||||
emptyDisplayValue=''
|
||||
/>
|
||||
</div>)
|
||||
})}
|
||||
|
@ -9,6 +9,11 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const IconClass = 'octo-icon'
|
||||
const OpenButtonClass = 'open-button'
|
||||
const SpacerClass = 'octo-spacer'
|
||||
const HorizontalGripClass = 'HorizontalGrip'
|
||||
|
||||
class Utils {
|
||||
static createGuid(): string {
|
||||
const crypto = window.crypto || window.msCrypto
|
||||
@ -44,6 +49,61 @@ class Utils {
|
||||
return String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
}
|
||||
|
||||
// re-use canvas object for better performance
|
||||
static canvas = document.createElement('canvas') as HTMLCanvasElement
|
||||
static getTextWidth(displayText: string, fontDescriptor: string) {
|
||||
if (displayText !== '') {
|
||||
const context = this.canvas.getContext('2d')
|
||||
if (context) {
|
||||
context.font = fontDescriptor
|
||||
const metrics = context.measureText(displayText)
|
||||
return Math.ceil(metrics.width)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
static getFontAndPaddingFromCell = (cell: Element) : {fontDescriptor: string, padding: number} => {
|
||||
const style = getComputedStyle(cell)
|
||||
const padding = Utils.getHorizontalPadding(style)
|
||||
return Utils.getFontAndPaddingFromChildren(cell.children, padding)
|
||||
}
|
||||
|
||||
// recursive routine to determine the padding and font from its children
|
||||
// specifically for the table view
|
||||
static getFontAndPaddingFromChildren = (children: HTMLCollection, pad: number) : {fontDescriptor: string, padding: number} => {
|
||||
const myResults = {
|
||||
fontDescriptor: '',
|
||||
padding: pad,
|
||||
}
|
||||
Array.from(children).forEach((element) => {
|
||||
switch (element.className) {
|
||||
case IconClass:
|
||||
case SpacerClass:
|
||||
case HorizontalGripClass:
|
||||
myResults.padding += element.clientWidth
|
||||
break
|
||||
case OpenButtonClass:
|
||||
break
|
||||
default: {
|
||||
const style = getComputedStyle(element)
|
||||
myResults.fontDescriptor = style.font
|
||||
myResults.padding += Utils.getHorizontalPadding(style)
|
||||
const childResults = Utils.getFontAndPaddingFromChildren(element.children, myResults.padding)
|
||||
if (childResults.fontDescriptor !== '') {
|
||||
myResults.fontDescriptor = childResults.fontDescriptor
|
||||
myResults.padding = childResults.padding
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return myResults
|
||||
}
|
||||
|
||||
static getHorizontalPadding = (style: CSSStyleDeclaration): number => {
|
||||
return parseInt(style.paddingLeft, 10) + parseInt(style.paddingRight, 10) + parseInt(style.marginLeft, 10) + parseInt(style.marginRight, 10) + parseInt(style.borderLeft, 10) + parseInt(style.borderRight, 10)
|
||||
}
|
||||
|
||||
// Markdown
|
||||
|
||||
static htmlFromMarkdown(text: string): string {
|
||||
|
Loading…
x
Reference in New Issue
Block a user