mirror of
https://github.com/mattermost/focalboard.git
synced 2025-02-07 19:30:18 +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}
|
showCard={this.showCard}
|
||||||
addCard={(show) => this.addCard('', show)}
|
addCard={(show) => this.addCard('', show)}
|
||||||
onCardClicked={this.cardClicked}
|
onCardClicked={this.cardClicked}
|
||||||
|
intl={this.props.intl}
|
||||||
/>}
|
/>}
|
||||||
{activeView.viewType === 'gallery' &&
|
{activeView.viewType === 'gallery' &&
|
||||||
<Gallery
|
<Gallery
|
||||||
|
@ -7,6 +7,7 @@ import './horizontalGrip.scss'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
templateId: string
|
templateId: string
|
||||||
|
onAutoSizeColumn: (columnID: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HorizontalGrip = React.memo((props: Props): JSX.Element => {
|
const HorizontalGrip = React.memo((props: Props): JSX.Element => {
|
||||||
@ -19,6 +20,7 @@ const HorizontalGrip = React.memo((props: Props): JSX.Element => {
|
|||||||
<div
|
<div
|
||||||
ref={drag}
|
ref={drag}
|
||||||
className='HorizontalGrip'
|
className='HorizontalGrip'
|
||||||
|
onDoubleClick={() => props.onAutoSizeColumn(props.templateId)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {FormattedMessage} from 'react-intl'
|
import {FormattedMessage, IntlShape} from 'react-intl'
|
||||||
import {useDrop, useDragLayer} from 'react-dnd'
|
import {useDrop, useDragLayer} from 'react-dnd'
|
||||||
|
|
||||||
import {IPropertyTemplate} from '../../blocks/board'
|
import {IPropertyTemplate} from '../../blocks/board'
|
||||||
@ -10,8 +10,11 @@ import {Card} from '../../blocks/card'
|
|||||||
import {Constants} from '../../constants'
|
import {Constants} from '../../constants'
|
||||||
import mutator from '../../mutator'
|
import mutator from '../../mutator'
|
||||||
import {Utils} from '../../utils'
|
import {Utils} from '../../utils'
|
||||||
|
|
||||||
import {BoardTree} from '../../viewModel/boardTree'
|
import {BoardTree} from '../../viewModel/boardTree'
|
||||||
|
|
||||||
|
import {OctoUtils} from './../../octoUtils'
|
||||||
|
|
||||||
import './table.scss'
|
import './table.scss'
|
||||||
import TableHeader from './tableHeader'
|
import TableHeader from './tableHeader'
|
||||||
import TableRow from './tableRow'
|
import TableRow from './tableRow'
|
||||||
@ -21,6 +24,7 @@ type Props = {
|
|||||||
selectedCardIds: string[]
|
selectedCardIds: string[]
|
||||||
readonly: boolean
|
readonly: boolean
|
||||||
cardIdToFocusOnRender: string
|
cardIdToFocusOnRender: string
|
||||||
|
intl: IntlShape
|
||||||
showCard: (cardId?: string) => void
|
showCard: (cardId?: string) => void
|
||||||
addCard: (show: boolean) => Promise<void>
|
addCard: (show: boolean) => Promise<void>
|
||||||
onCardClicked: (e: React.MouseEvent, card: Card) => 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(() => ({
|
const [, drop] = useDrop(() => ({
|
||||||
accept: 'horizontalGrip',
|
accept: 'horizontalGrip',
|
||||||
drop: (item: {id: string}, monitor) => {
|
drop: (item: {id: string}, monitor) => {
|
||||||
@ -59,6 +65,44 @@ const Table = (props: Props) => {
|
|||||||
},
|
},
|
||||||
}), [activeView])
|
}), [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) => {
|
const onDropToCard = (srcCard: Card, dstCard: Card) => {
|
||||||
Utils.log(`onDropToCard: ${dstCard.title}`)
|
Utils.log(`onDropToCard: ${dstCard.title}`)
|
||||||
const {selectedCardIds} = props
|
const {selectedCardIds} = props
|
||||||
@ -119,6 +163,7 @@ const Table = (props: Props) => {
|
|||||||
template={{id: Constants.titleColumnId, name: 'title', type: 'text', options: []}}
|
template={{id: Constants.titleColumnId, name: 'title', type: 'text', options: []}}
|
||||||
offset={resizingColumn === Constants.titleColumnId ? offset : 0}
|
offset={resizingColumn === Constants.titleColumnId ? offset : 0}
|
||||||
onDrop={onDropToColumn}
|
onDrop={onDropToColumn}
|
||||||
|
onAutoSizeColumn={onAutoSizeColumn}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Table header row */}
|
{/* Table header row */}
|
||||||
@ -142,6 +187,7 @@ const Table = (props: Props) => {
|
|||||||
key={template.id}
|
key={template.id}
|
||||||
offset={resizingColumn === template.id ? offset : 0}
|
offset={resizingColumn === template.id ? offset : 0}
|
||||||
onDrop={onDropToColumn}
|
onDrop={onDropToColumn}
|
||||||
|
onAutoSizeColumn={onAutoSizeColumn}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@ -170,6 +216,7 @@ const Table = (props: Props) => {
|
|||||||
onDrop={onDropToCard}
|
onDrop={onDropToCard}
|
||||||
offset={offset}
|
offset={offset}
|
||||||
resizingColumn={resizingColumn}
|
resizingColumn={resizingColumn}
|
||||||
|
columnRefs={columnRefs}
|
||||||
/>)
|
/>)
|
||||||
|
|
||||||
return tableRow
|
return tableRow
|
||||||
|
@ -10,6 +10,7 @@ import SortUpIcon from '../../widgets/icons/sortUp'
|
|||||||
import MenuWrapper from '../../widgets/menuWrapper'
|
import MenuWrapper from '../../widgets/menuWrapper'
|
||||||
import Label from '../../widgets/label'
|
import Label from '../../widgets/label'
|
||||||
import {useSortable} from '../../hooks/sortable'
|
import {useSortable} from '../../hooks/sortable'
|
||||||
|
import {Utils} from '../../utils'
|
||||||
|
|
||||||
import HorizontalGrip from './horizontalGrip'
|
import HorizontalGrip from './horizontalGrip'
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ type Props = {
|
|||||||
template: IPropertyTemplate
|
template: IPropertyTemplate
|
||||||
offset: number
|
offset: number
|
||||||
onDrop: (template: IPropertyTemplate, container: IPropertyTemplate) => void
|
onDrop: (template: IPropertyTemplate, container: IPropertyTemplate) => void
|
||||||
|
onAutoSizeColumn: (columnID: string, headerWidth: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const TableHeader = React.memo((props: Props): JSX.Element => {
|
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)
|
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'
|
let className = 'octo-table-cell header-cell'
|
||||||
if (isOver) {
|
if (isOver) {
|
||||||
className += ' dragover'
|
className += ' dragover'
|
||||||
@ -59,7 +71,10 @@ const TableHeader = React.memo((props: Props): JSX.Element => {
|
|||||||
<div className='octo-spacer'/>
|
<div className='octo-spacer'/>
|
||||||
|
|
||||||
{!props.readonly &&
|
{!props.readonly &&
|
||||||
<HorizontalGrip templateId={props.template.id}/>
|
<HorizontalGrip
|
||||||
|
templateId={props.template.id}
|
||||||
|
onAutoSizeColumn={onAutoSizeColumn}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -24,12 +24,13 @@ type Props = {
|
|||||||
readonly: boolean
|
readonly: boolean
|
||||||
offset: number
|
offset: number
|
||||||
resizingColumn: string
|
resizingColumn: string
|
||||||
|
columnRefs: Map<string, React.RefObject<HTMLDivElement>>
|
||||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||||
onDrop: (srcCard: Card, dstCard: Card) => void
|
onDrop: (srcCard: Card, dstCard: Card) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const TableRow = React.memo((props: Props) => {
|
const TableRow = React.memo((props: Props) => {
|
||||||
const {boardTree, onSaveWithEnter} = props
|
const {boardTree, onSaveWithEnter, columnRefs} = props
|
||||||
const {board, activeView} = boardTree
|
const {board, activeView} = boardTree
|
||||||
|
|
||||||
const titleRef = useRef<{focus(selectAll?: boolean): void}>(null)
|
const titleRef = useRef<{focus(selectAll?: boolean): void}>(null)
|
||||||
@ -56,6 +57,10 @@ const TableRow = React.memo((props: Props) => {
|
|||||||
className += ' dragover'
|
className += ' dragover'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!columnRefs.get(Constants.titleColumnId)) {
|
||||||
|
columnRefs.set(Constants.titleColumnId, React.createRef())
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={className}
|
className={className}
|
||||||
@ -70,6 +75,7 @@ const TableRow = React.memo((props: Props) => {
|
|||||||
className='octo-table-cell title-cell'
|
className='octo-table-cell title-cell'
|
||||||
id='mainBoardHeader'
|
id='mainBoardHeader'
|
||||||
style={{width: columnWidth(Constants.titleColumnId)}}
|
style={{width: columnWidth(Constants.titleColumnId)}}
|
||||||
|
ref={columnRefs.get(Constants.titleColumnId)}
|
||||||
>
|
>
|
||||||
<div className='octo-icontitle'>
|
<div className='octo-icontitle'>
|
||||||
<div className='octo-icon'>{card.icon}</div>
|
<div className='octo-icon'>{card.icon}</div>
|
||||||
@ -104,18 +110,22 @@ const TableRow = React.memo((props: Props) => {
|
|||||||
{board.cardProperties.
|
{board.cardProperties.
|
||||||
filter((template) => activeView.visiblePropertyIds.includes(template.id)).
|
filter((template) => activeView.visiblePropertyIds.includes(template.id)).
|
||||||
map((template) => {
|
map((template) => {
|
||||||
|
if (!columnRefs.get(template.id)) {
|
||||||
|
columnRefs.set(template.id, React.createRef())
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='octo-table-cell'
|
className='octo-table-cell'
|
||||||
key={template.id}
|
key={template.id}
|
||||||
style={{width: columnWidth(template.id)}}
|
style={{width: columnWidth(template.id)}}
|
||||||
|
ref={columnRefs.get(template.id)}
|
||||||
>
|
>
|
||||||
<PropertyValueElement
|
<PropertyValueElement
|
||||||
readOnly={props.readonly}
|
readOnly={props.readonly}
|
||||||
card={card}
|
card={card}
|
||||||
boardTree={boardTree}
|
boardTree={boardTree}
|
||||||
propertyTemplate={template}
|
propertyTemplate={template}
|
||||||
emptyDisplayValue='Empty'
|
emptyDisplayValue=''
|
||||||
/>
|
/>
|
||||||
</div>)
|
</div>)
|
||||||
})}
|
})}
|
||||||
|
@ -9,6 +9,11 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const IconClass = 'octo-icon'
|
||||||
|
const OpenButtonClass = 'open-button'
|
||||||
|
const SpacerClass = 'octo-spacer'
|
||||||
|
const HorizontalGripClass = 'HorizontalGrip'
|
||||||
|
|
||||||
class Utils {
|
class Utils {
|
||||||
static createGuid(): string {
|
static createGuid(): string {
|
||||||
const crypto = window.crypto || window.msCrypto
|
const crypto = window.crypto || window.msCrypto
|
||||||
@ -44,6 +49,61 @@ class Utils {
|
|||||||
return String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
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
|
// Markdown
|
||||||
|
|
||||||
static htmlFromMarkdown(text: string): string {
|
static htmlFromMarkdown(text: string): string {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user