mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-18 00:54:43 -04:00
Refactor table size picker (#1662)
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
b68a55aa94
commit
9874d54404
9 changed files with 281 additions and 160 deletions
|
@ -175,25 +175,29 @@ describe('Toolbar Buttons', () => {
|
||||||
|
|
||||||
describe('for new tables', () => {
|
describe('for new tables', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.get('.table-picker-container').should('not.be.visible')
|
cy.getById('table-size-picker-popover').should('not.exist')
|
||||||
cy.getById('show-table-overlay').last().click()
|
cy.getById('table-size-picker-button').last().click()
|
||||||
cy.get('.table-picker-container').should('be.visible')
|
cy.getById('table-size-picker-popover').should('be.visible')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should open an overlay', () => {
|
it('should select table size', () => {
|
||||||
cy.get('.table-container > div:nth-of-type(25)').trigger('mouseover')
|
cy.getById('table-size-picker-popover')
|
||||||
cy.get('.table-cell.bg-primary').should('have.length', 15)
|
.find('.table-container > .table-cell:nth-of-type(25)')
|
||||||
cy.get('.table-picker-container > p').contains('5x3')
|
.trigger('mouseover')
|
||||||
cy.get('.table-container > div:nth-of-type(25)').click()
|
cy.getById('table-size-picker-popover')
|
||||||
|
.find('.table-container > .table-cell[data-cypress-selected="true"]')
|
||||||
|
.should('have.length', 15)
|
||||||
|
cy.getById('table-size-picker-popover').find('.popover-header').contains('5x3')
|
||||||
|
cy.getById('table-size-picker-popover').find('.table-container > .table-cell:nth-of-type(25)').click()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should open a modal for custom table sizes in the overlay', () => {
|
it('should open a custom table size in the modal', () => {
|
||||||
cy.get('.modal-dialog').should('not.exist')
|
cy.getById('custom-table-size-modal').should('not.exist')
|
||||||
cy.getById('show-custom-table-modal').first().click()
|
cy.getById('show-custom-table-modal').first().click()
|
||||||
cy.get('.modal-dialog').should('be.visible')
|
cy.getById('custom-table-size-modal').should('be.visible')
|
||||||
cy.get('.modal-content > .d-flex > input').first().type('5')
|
cy.getById('custom-table-size-modal').find('input').first().type('5')
|
||||||
cy.get('.modal-content > .d-flex > input').last().type('3')
|
cy.getById('custom-table-size-modal').find('input').last().type('3')
|
||||||
cy.get('.modal-footer > button').click()
|
cy.getById('custom-table-size-modal').find('.modal-footer > button').click()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|
|
@ -309,8 +309,8 @@
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
"uploadImage": "Upload Image",
|
"uploadImage": "Upload Image",
|
||||||
"table": {
|
"table": {
|
||||||
"title": "Table",
|
"titleWithoutSize": "Table",
|
||||||
"size": "{{cols}}x{{rows}} Table",
|
"titleWithSize": "{{cols}}x{{rows}} Table",
|
||||||
"customSize": "Custom Size",
|
"customSize": "Custom Size",
|
||||||
"cols": "Cols",
|
"cols": "Cols",
|
||||||
"rows": "Rows",
|
"rows": "Rows",
|
||||||
|
|
|
@ -4,58 +4,79 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { ChangeEvent } from 'react'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { Button, Form, ModalFooter } from 'react-bootstrap'
|
import { Button, Form, ModalFooter } from 'react-bootstrap'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
|
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
|
||||||
import { CommonModal } from '../../../../common/modals/common-modal'
|
import { CommonModal } from '../../../../common/modals/common-modal'
|
||||||
import type { TableSize } from './table-picker'
|
import type { TableSize } from './table-size-picker-popover'
|
||||||
|
import { cypressId } from '../../../../../utils/cypress-attribute'
|
||||||
|
|
||||||
export interface CustomTableSizeModalProps {
|
export interface CustomTableSizeModalProps {
|
||||||
showModal: boolean
|
showModal: boolean
|
||||||
onDismiss: () => void
|
onDismiss: () => void
|
||||||
onTablePicked: (row: number, cols: number) => void
|
onSizeSelect: (row: number, cols: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CustomTableSizeModal: React.FC<CustomTableSizeModalProps> = ({ showModal, onDismiss, onTablePicked }) => {
|
const initialTableSize: TableSize = {
|
||||||
|
rows: 0,
|
||||||
|
columns: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A modal that lets the user select a custom table size.
|
||||||
|
*
|
||||||
|
* @param showModal defines if the modal should be visible or not
|
||||||
|
* @param onDismiss is called if the modal should be hidden without a selection
|
||||||
|
* @param onSizeSelect is called if the user entered and confirmed a custom table size
|
||||||
|
*/
|
||||||
|
export const CustomTableSizeModal: React.FC<CustomTableSizeModalProps> = ({ showModal, onDismiss, onSizeSelect }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [tableSize, setTableSize] = useState<TableSize>({
|
const [tableSize, setTableSize] = useState<TableSize>(() => initialTableSize)
|
||||||
rows: 0,
|
|
||||||
columns: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTableSize({
|
if (showModal) {
|
||||||
rows: 0,
|
setTableSize(initialTableSize)
|
||||||
columns: 0
|
}
|
||||||
})
|
|
||||||
}, [showModal])
|
}, [showModal])
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
onTablePicked(tableSize.rows, tableSize.columns)
|
onSizeSelect(tableSize.rows, tableSize.columns)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}, [onDismiss, tableSize, onTablePicked])
|
}, [onDismiss, tableSize, onSizeSelect])
|
||||||
|
|
||||||
|
const onColChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = Number.parseInt(event.currentTarget.value)
|
||||||
|
setTableSize((old) => ({
|
||||||
|
rows: old.rows,
|
||||||
|
columns: isNaN(value) ? 0 : value
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onRowChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = Number.parseInt(event.currentTarget.value)
|
||||||
|
setTableSize((old) => ({
|
||||||
|
rows: isNaN(value) ? 0 : value,
|
||||||
|
columns: old.columns
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommonModal
|
<CommonModal
|
||||||
show={showModal}
|
show={showModal}
|
||||||
onHide={() => onDismiss()}
|
onHide={onDismiss}
|
||||||
title={'editor.editorToolbar.table.customSize'}
|
title={'editor.editorToolbar.table.customSize'}
|
||||||
showCloseButton={true}
|
showCloseButton={true}
|
||||||
titleIcon={'table'}>
|
titleIcon={'table'}
|
||||||
|
{...cypressId('custom-table-size-modal')}>
|
||||||
<div className={'col-lg-10 d-flex flex-row p-3 align-items-center'}>
|
<div className={'col-lg-10 d-flex flex-row p-3 align-items-center'}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type={'number'}
|
type={'number'}
|
||||||
min={1}
|
min={1}
|
||||||
placeholder={t('editor.editorToolbar.table.cols')}
|
placeholder={t('editor.editorToolbar.table.cols')}
|
||||||
isInvalid={tableSize.columns <= 0}
|
isInvalid={tableSize.columns <= 0}
|
||||||
onChange={(event) => {
|
onChange={onColChange}
|
||||||
const value = Number.parseInt(event.currentTarget.value)
|
|
||||||
setTableSize((old) => ({
|
|
||||||
rows: old.rows,
|
|
||||||
columns: isNaN(value) ? 0 : value
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<ForkAwesomeIcon icon='times' className='mx-2' fixedWidth={true} />
|
<ForkAwesomeIcon icon='times' className='mx-2' fixedWidth={true} />
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
@ -63,18 +84,12 @@ export const CustomTableSizeModal: React.FC<CustomTableSizeModalProps> = ({ show
|
||||||
min={1}
|
min={1}
|
||||||
placeholder={t('editor.editorToolbar.table.rows')}
|
placeholder={t('editor.editorToolbar.table.rows')}
|
||||||
isInvalid={tableSize.rows <= 0}
|
isInvalid={tableSize.rows <= 0}
|
||||||
onChange={(event) => {
|
onChange={onRowChange}
|
||||||
const value = Number.parseInt(event.currentTarget.value)
|
|
||||||
setTableSize((old) => ({
|
|
||||||
rows: isNaN(value) ? 0 : value,
|
|
||||||
columns: old.columns
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onClick={onClick} disabled={tableSize.rows <= 0 || tableSize.columns <= 0}>
|
<Button onClick={onClick} disabled={tableSize.rows <= 0 || tableSize.columns <= 0}>
|
||||||
{t('editor.editorToolbar.table.create')}
|
<Trans i18nKey={'editor.editorToolbar.table.create'} />
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</CommonModal>
|
</CommonModal>
|
||||||
|
|
|
@ -5,39 +5,106 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type CodeMirror from 'codemirror'
|
import type CodeMirror from 'codemirror'
|
||||||
import React, { Fragment, useState } from 'react'
|
import React, { Fragment, useCallback, useMemo, useRef, useState } from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button, Overlay } from 'react-bootstrap'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
|
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
|
||||||
import { addTable } from '../utils/toolbarButtonUtils'
|
import { addTable } from '../utils/toolbarButtonUtils'
|
||||||
import { TablePicker } from './table-picker'
|
|
||||||
import { cypressId } from '../../../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../../utils/cypress-attribute'
|
||||||
|
import { TableSizePickerPopover } from './table-size-picker-popover'
|
||||||
|
import { CustomTableSizeModal } from './custom-table-size-modal'
|
||||||
|
import type { OverlayInjectedProps } from 'react-bootstrap/Overlay'
|
||||||
|
import { ShowIf } from '../../../../common/show-if/show-if'
|
||||||
|
|
||||||
export interface TablePickerButtonProps {
|
export interface TablePickerButtonProps {
|
||||||
editor: CodeMirror.Editor
|
editor: CodeMirror.Editor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PickerMode {
|
||||||
|
INVISIBLE,
|
||||||
|
GRID,
|
||||||
|
CUSTOM
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the visibility of a {@link TableSizePicker table size picker overlay} and inserts the result into the editor.
|
||||||
|
*
|
||||||
|
* @param editor The editor in which the result should get inserted
|
||||||
|
*/
|
||||||
export const TablePickerButton: React.FC<TablePickerButtonProps> = ({ editor }) => {
|
export const TablePickerButton: React.FC<TablePickerButtonProps> = ({ editor }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [showTablePicker, setShowTablePicker] = useState(false)
|
const [pickerMode, setPickerMode] = useState<PickerMode>(PickerMode.INVISIBLE)
|
||||||
|
const onDismiss = useCallback(() => setPickerMode(PickerMode.INVISIBLE), [])
|
||||||
|
const onShowModal = useCallback(() => setPickerMode(PickerMode.CUSTOM), [])
|
||||||
|
|
||||||
|
const onSizeSelect = useCallback(
|
||||||
|
(rows: number, columns: number) => {
|
||||||
|
addTable(editor, rows, columns)
|
||||||
|
setPickerMode(PickerMode.INVISIBLE)
|
||||||
|
},
|
||||||
|
[editor]
|
||||||
|
)
|
||||||
|
|
||||||
|
const tableTitle = useMemo(() => t('editor.editorToolbar.table.title'), [t])
|
||||||
|
|
||||||
|
const button = useRef(null)
|
||||||
|
|
||||||
|
const toggleOverlayVisibility = useCallback(
|
||||||
|
() =>
|
||||||
|
setPickerMode((oldPickerMode) =>
|
||||||
|
oldPickerMode === PickerMode.INVISIBLE ? PickerMode.GRID : PickerMode.INVISIBLE
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onOverlayHide = useCallback(() => {
|
||||||
|
setPickerMode((oldMode) => {
|
||||||
|
if (oldMode === PickerMode.CUSTOM) {
|
||||||
|
return PickerMode.CUSTOM
|
||||||
|
} else {
|
||||||
|
return PickerMode.INVISIBLE
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const createPopoverElement = useCallback<(props: OverlayInjectedProps) => React.ReactElement>(
|
||||||
|
({ ref, ...popoverProps }) => (
|
||||||
|
<TableSizePickerPopover
|
||||||
|
onTableSizeSelected={onSizeSelect}
|
||||||
|
onShowCustomSizeModal={onShowModal}
|
||||||
|
onDismiss={onDismiss}
|
||||||
|
onRefUpdate={ref}
|
||||||
|
{...popoverProps}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[onDismiss, onShowModal, onSizeSelect]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<TablePicker
|
|
||||||
show={showTablePicker}
|
|
||||||
onDismiss={() => setShowTablePicker(false)}
|
|
||||||
onTablePicked={(rows, cols) => {
|
|
||||||
setShowTablePicker(false)
|
|
||||||
addTable(editor, rows, cols)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
{...cypressId('show-table-overlay')}
|
{...cypressId('table-size-picker-button')}
|
||||||
variant='light'
|
variant='light'
|
||||||
onClick={() => setShowTablePicker((old) => !old)}
|
onClick={toggleOverlayVisibility}
|
||||||
title={t('editor.editorToolbar.table.title')}>
|
title={tableTitle}
|
||||||
|
ref={button}>
|
||||||
<ForkAwesomeIcon icon='table' />
|
<ForkAwesomeIcon icon='table' />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Overlay
|
||||||
|
target={button.current}
|
||||||
|
onHide={onOverlayHide}
|
||||||
|
show={pickerMode === PickerMode.GRID}
|
||||||
|
placement={'bottom'}
|
||||||
|
rootClose={true}>
|
||||||
|
{createPopoverElement}
|
||||||
|
</Overlay>
|
||||||
|
<ShowIf condition={pickerMode === PickerMode.CUSTOM}>
|
||||||
|
<CustomTableSizeModal
|
||||||
|
showModal={pickerMode === PickerMode.CUSTOM}
|
||||||
|
onDismiss={onDismiss}
|
||||||
|
onSizeSelect={onSizeSelect}
|
||||||
|
/>
|
||||||
|
</ShowIf>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,10 @@
|
||||||
|
|
||||||
|
|
||||||
.table-picker-container {
|
.table-picker-container {
|
||||||
z-index: 1111;
|
|
||||||
|
|
||||||
@import "../../../../../style/variables.light";
|
@import "../../../../../style/variables.light";
|
||||||
|
|
||||||
|
z-index: 1111;
|
||||||
|
|
||||||
.table-cell {
|
.table-cell {
|
||||||
border: 1px solid $gray-700;
|
border: 1px solid $gray-700;
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
import { Button } from 'react-bootstrap'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useClickAway } from 'react-use'
|
|
||||||
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
|
|
||||||
import { createNumberRangeArray } from '../../../../common/number-range/number-range'
|
|
||||||
import { CustomTableSizeModal } from './custom-table-size-modal'
|
|
||||||
import './table-picker.scss'
|
|
||||||
import { cypressId } from '../../../../../utils/cypress-attribute'
|
|
||||||
|
|
||||||
export interface TablePickerProps {
|
|
||||||
show: boolean
|
|
||||||
onDismiss: () => void
|
|
||||||
onTablePicked: (row: number, cols: number) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TableSize = {
|
|
||||||
rows: number
|
|
||||||
columns: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TablePicker: React.FC<TablePickerProps> = ({ show, onDismiss, onTablePicked }) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
|
||||||
const [tableSize, setTableSize] = useState<TableSize>()
|
|
||||||
const [showDialog, setShowDialog] = useState(false)
|
|
||||||
|
|
||||||
useClickAway(containerRef, () => {
|
|
||||||
onDismiss()
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTableSize(undefined)
|
|
||||||
}, [show])
|
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
|
||||||
if (tableSize) {
|
|
||||||
onTablePicked(tableSize.rows, tableSize.columns)
|
|
||||||
}
|
|
||||||
}, [onTablePicked, tableSize])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`position-absolute table-picker-container p-2 ${!show || showDialog ? 'd-none' : ''} bg-light`}
|
|
||||||
ref={containerRef}
|
|
||||||
role='grid'>
|
|
||||||
<p className={'lead'}>
|
|
||||||
{tableSize
|
|
||||||
? t('editor.editorToolbar.table.size', { cols: tableSize?.columns, rows: tableSize.rows })
|
|
||||||
: t('editor.editorToolbar.table.title')}
|
|
||||||
</p>
|
|
||||||
<div className={'table-container'}>
|
|
||||||
{createNumberRangeArray(8).map((row: number) =>
|
|
||||||
createNumberRangeArray(10).map((col: number) => (
|
|
||||||
<div
|
|
||||||
key={`${row}_${col}`}
|
|
||||||
className={`table-cell ${
|
|
||||||
tableSize && row < tableSize.rows && col < tableSize.columns ? 'bg-primary border-primary' : ''
|
|
||||||
}`}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
setTableSize({
|
|
||||||
rows: row + 1,
|
|
||||||
columns: col + 1
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
title={t('editor.editorToolbar.table.size', { cols: col + 1, rows: row + 1 })}
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className='d-flex justify-content-center mt-2'>
|
|
||||||
<Button {...cypressId('show-custom-table-modal')} className={'text-center'} onClick={() => setShowDialog(true)}>
|
|
||||||
<ForkAwesomeIcon icon='table' />
|
|
||||||
{t('editor.editorToolbar.table.customSize')}
|
|
||||||
</Button>
|
|
||||||
<CustomTableSizeModal
|
|
||||||
showModal={showDialog}
|
|
||||||
onDismiss={() => setShowDialog(false)}
|
|
||||||
onTablePicked={onTablePicked}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||||
|
import { createNumberRangeArray } from '../../../../common/number-range/number-range'
|
||||||
|
import { Button, Popover } from 'react-bootstrap'
|
||||||
|
import { TableSizeText } from './table-size-text'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import { cypressAttribute, cypressId } from '../../../../../utils/cypress-attribute'
|
||||||
|
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
|
||||||
|
import type { PopoverProps } from 'react-bootstrap/Popover'
|
||||||
|
import { useOnRefChange } from '../../../../markdown-renderer/hooks/use-on-ref-change'
|
||||||
|
import './table-picker.scss'
|
||||||
|
|
||||||
|
export interface TableSizePickerPopoverProps extends Omit<PopoverProps, 'id'> {
|
||||||
|
onShowCustomSizeModal: () => void
|
||||||
|
onTableSizeSelected: (rows: number, cols: number) => void
|
||||||
|
onDismiss: () => void
|
||||||
|
onRefUpdate: (newRef: HTMLDivElement | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableSize {
|
||||||
|
rows: number
|
||||||
|
columns: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableSizePickerPopover: React.FC<TableSizePickerPopoverProps> = ({
|
||||||
|
onShowCustomSizeModal,
|
||||||
|
onTableSizeSelected,
|
||||||
|
onDismiss,
|
||||||
|
onRefUpdate,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [tableSize, setTableSize] = useState<TableSize>()
|
||||||
|
|
||||||
|
const onSizeHover = useCallback(
|
||||||
|
(selectedRows: number, selectedCols: number) => () => {
|
||||||
|
setTableSize({
|
||||||
|
rows: selectedRows,
|
||||||
|
columns: selectedCols
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const tableContainer = useMemo(
|
||||||
|
() =>
|
||||||
|
createNumberRangeArray(8).map((row: number) =>
|
||||||
|
createNumberRangeArray(10).map((col: number) => {
|
||||||
|
const selected = tableSize && row < tableSize.rows && col < tableSize.columns
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${row}_${col}`}
|
||||||
|
className={`table-cell ${selected ? 'bg-primary border-primary' : ''}`}
|
||||||
|
{...cypressAttribute('selected', selected ? 'true' : 'false')}
|
||||||
|
onMouseEnter={onSizeHover(row + 1, col + 1)}
|
||||||
|
title={t('editor.editorToolbar.table.size', { cols: col + 1, rows: row + 1 })}
|
||||||
|
onClick={() => onTableSizeSelected(row + 1, col + 1)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
[onTableSizeSelected, onSizeHover, t, tableSize]
|
||||||
|
)
|
||||||
|
|
||||||
|
const popoverRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
useOnRefChange(popoverRef, (newRef) => onRefUpdate(newRef))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
{...props}
|
||||||
|
ref={popoverRef}
|
||||||
|
id={'table-picker'}
|
||||||
|
{...cypressId('table-size-picker-popover')}
|
||||||
|
className={`table-picker-container bg-light`}>
|
||||||
|
<Popover.Title>
|
||||||
|
<TableSizeText tableSize={tableSize} />
|
||||||
|
</Popover.Title>
|
||||||
|
<Popover.Content>
|
||||||
|
<div className={'table-container'} role='grid'>
|
||||||
|
{tableContainer}
|
||||||
|
</div>
|
||||||
|
<div className='d-flex justify-content-center mt-2'>
|
||||||
|
<Button {...cypressId('show-custom-table-modal')} className={'text-center'} onClick={onShowCustomSizeModal}>
|
||||||
|
<ForkAwesomeIcon icon='table' />
|
||||||
|
|
||||||
|
<Trans i18nKey={'editor.editorToolbar.table.customSize'} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useMemo } from 'react'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import type { TableSize } from './table-size-picker-popover'
|
||||||
|
|
||||||
|
export interface TableSizeProps {
|
||||||
|
tableSize?: TableSize
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders an info text that contains the given {@link TableSize table size}.
|
||||||
|
*
|
||||||
|
* @param tableSize The table size that should be included
|
||||||
|
*/
|
||||||
|
export const TableSizeText: React.FC<TableSizeProps> = ({ tableSize }) => {
|
||||||
|
useTranslation()
|
||||||
|
|
||||||
|
const translationValues = useMemo(() => {
|
||||||
|
return tableSize ? { cols: tableSize.columns, rows: tableSize.rows } : undefined
|
||||||
|
}, [tableSize])
|
||||||
|
|
||||||
|
if (!translationValues) {
|
||||||
|
return <Trans i18nKey={'editor.editorToolbar.table.titleWithoutSize'} />
|
||||||
|
} else {
|
||||||
|
return <Trans i18nKey={'editor.editorToolbar.table.titleWithSize'} values={translationValues} />
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,11 +8,8 @@ import equal from 'fast-deep-equal'
|
||||||
import type { MutableRefObject } from 'react'
|
import type { MutableRefObject } from 'react'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
export const useOnRefChange = <T>(
|
export const useOnRefChange = <T>(reference: MutableRefObject<T>, onChange?: (newValue: T) => void): void => {
|
||||||
reference: MutableRefObject<T | undefined>,
|
const lastValue = useRef<T>()
|
||||||
onChange?: (newValue?: T) => void
|
|
||||||
): void => {
|
|
||||||
const lastValue = useRef<T | undefined>()
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onChange && !equal(reference, lastValue.current)) {
|
if (onChange && !equal(reference, lastValue.current)) {
|
||||||
lastValue.current = reference.current
|
lastValue.current = reference.current
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue