1
0
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:
Scott Bishel 2021-05-10 06:52:00 -06:00 committed by GitHub
parent d8a2c489bb
commit 019dd3da8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 139 additions and 4 deletions

View File

@ -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

View File

@ -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)}
/>
)
})

View File

@ -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

View File

@ -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>
)

View File

@ -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>)
})}

View File

@ -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(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/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 {