diff --git a/cypress/integration/toolbar.spec.ts b/cypress/integration/toolbar.spec.ts index 86da0ec12..992b29207 100644 --- a/cypress/integration/toolbar.spec.ts +++ b/cypress/integration/toolbar.spec.ts @@ -175,25 +175,29 @@ describe('Toolbar Buttons', () => { describe('for new tables', () => { beforeEach(() => { - cy.get('.table-picker-container').should('not.be.visible') - cy.getById('show-table-overlay').last().click() - cy.get('.table-picker-container').should('be.visible') + cy.getById('table-size-picker-popover').should('not.exist') + cy.getById('table-size-picker-button').last().click() + cy.getById('table-size-picker-popover').should('be.visible') }) - it('should open an overlay', () => { - cy.get('.table-container > div:nth-of-type(25)').trigger('mouseover') - cy.get('.table-cell.bg-primary').should('have.length', 15) - cy.get('.table-picker-container > p').contains('5x3') - cy.get('.table-container > div:nth-of-type(25)').click() + it('should select table size', () => { + cy.getById('table-size-picker-popover') + .find('.table-container > .table-cell:nth-of-type(25)') + .trigger('mouseover') + 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', () => { - cy.get('.modal-dialog').should('not.exist') + it('should open a custom table size in the modal', () => { + cy.getById('custom-table-size-modal').should('not.exist') cy.getById('show-custom-table-modal').first().click() - cy.get('.modal-dialog').should('be.visible') - cy.get('.modal-content > .d-flex > input').first().type('5') - cy.get('.modal-content > .d-flex > input').last().type('3') - cy.get('.modal-footer > button').click() + cy.getById('custom-table-size-modal').should('be.visible') + cy.getById('custom-table-size-modal').find('input').first().type('5') + cy.getById('custom-table-size-modal').find('input').last().type('3') + cy.getById('custom-table-size-modal').find('.modal-footer > button').click() }) afterEach(() => { diff --git a/locales/en.json b/locales/en.json index b17e954de..b0147ce88 100644 --- a/locales/en.json +++ b/locales/en.json @@ -309,8 +309,8 @@ "image": "Image", "uploadImage": "Upload Image", "table": { - "title": "Table", - "size": "{{cols}}x{{rows}} Table", + "titleWithoutSize": "Table", + "titleWithSize": "{{cols}}x{{rows}} Table", "customSize": "Custom Size", "cols": "Cols", "rows": "Rows", diff --git a/src/components/editor-page/editor-pane/tool-bar/table-picker/custom-table-size-modal.tsx b/src/components/editor-page/editor-pane/tool-bar/table-picker/custom-table-size-modal.tsx index ec4682ac6..999772b92 100644 --- a/src/components/editor-page/editor-pane/tool-bar/table-picker/custom-table-size-modal.tsx +++ b/src/components/editor-page/editor-pane/tool-bar/table-picker/custom-table-size-modal.tsx @@ -4,58 +4,79 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import type { ChangeEvent } from 'react' import React, { useCallback, useEffect, useState } from 'react' 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 { 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 { showModal: boolean onDismiss: () => void - onTablePicked: (row: number, cols: number) => void + onSizeSelect: (row: number, cols: number) => void } -export const CustomTableSizeModal: React.FC = ({ 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 = ({ showModal, onDismiss, onSizeSelect }) => { const { t } = useTranslation() - const [tableSize, setTableSize] = useState({ - rows: 0, - columns: 0 - }) + const [tableSize, setTableSize] = useState(() => initialTableSize) useEffect(() => { - setTableSize({ - rows: 0, - columns: 0 - }) + if (showModal) { + setTableSize(initialTableSize) + } }, [showModal]) const onClick = useCallback(() => { - onTablePicked(tableSize.rows, tableSize.columns) + onSizeSelect(tableSize.rows, tableSize.columns) onDismiss() - }, [onDismiss, tableSize, onTablePicked]) + }, [onDismiss, tableSize, onSizeSelect]) + + const onColChange = useCallback((event: ChangeEvent) => { + const value = Number.parseInt(event.currentTarget.value) + setTableSize((old) => ({ + rows: old.rows, + columns: isNaN(value) ? 0 : value + })) + }, []) + + const onRowChange = useCallback((event: ChangeEvent) => { + const value = Number.parseInt(event.currentTarget.value) + setTableSize((old) => ({ + rows: isNaN(value) ? 0 : value, + columns: old.columns + })) + }, []) return ( onDismiss()} + onHide={onDismiss} title={'editor.editorToolbar.table.customSize'} showCloseButton={true} - titleIcon={'table'}> + titleIcon={'table'} + {...cypressId('custom-table-size-modal')}>
{ - const value = Number.parseInt(event.currentTarget.value) - setTableSize((old) => ({ - rows: old.rows, - columns: isNaN(value) ? 0 : value - })) - }} + onChange={onColChange} /> = ({ show min={1} placeholder={t('editor.editorToolbar.table.rows')} isInvalid={tableSize.rows <= 0} - onChange={(event) => { - const value = Number.parseInt(event.currentTarget.value) - setTableSize((old) => ({ - rows: isNaN(value) ? 0 : value, - columns: old.columns - })) - }} + onChange={onRowChange} />
diff --git a/src/components/editor-page/editor-pane/tool-bar/table-picker/table-picker-button.tsx b/src/components/editor-page/editor-pane/tool-bar/table-picker/table-picker-button.tsx index 6ef7827fc..09bf2d19d 100644 --- a/src/components/editor-page/editor-pane/tool-bar/table-picker/table-picker-button.tsx +++ b/src/components/editor-page/editor-pane/tool-bar/table-picker/table-picker-button.tsx @@ -5,39 +5,106 @@ */ import type CodeMirror from 'codemirror' -import React, { Fragment, useState } from 'react' -import { Button } from 'react-bootstrap' +import React, { Fragment, useCallback, useMemo, useRef, useState } from 'react' +import { Button, Overlay } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon' import { addTable } from '../utils/toolbarButtonUtils' -import { TablePicker } from './table-picker' 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 { 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 = ({ editor }) => { const { t } = useTranslation() - const [showTablePicker, setShowTablePicker] = useState(false) + const [pickerMode, setPickerMode] = useState(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 }) => ( + + ), + [onDismiss, onShowModal, onSizeSelect] + ) return ( - setShowTablePicker(false)} - onTablePicked={(rows, cols) => { - setShowTablePicker(false) - addTable(editor, rows, cols) - }} - /> + + {createPopoverElement} + + + + ) } diff --git a/src/components/editor-page/editor-pane/tool-bar/table-picker/table-picker.scss b/src/components/editor-page/editor-pane/tool-bar/table-picker/table-picker.scss index 16811cf3c..c86469259 100644 --- a/src/components/editor-page/editor-pane/tool-bar/table-picker/table-picker.scss +++ b/src/components/editor-page/editor-pane/tool-bar/table-picker/table-picker.scss @@ -6,10 +6,10 @@ .table-picker-container { - z-index: 1111; - @import "../../../../../style/variables.light"; + z-index: 1111; + .table-cell { border: 1px solid $gray-700; margin: 1px; diff --git a/src/components/editor-page/editor-pane/tool-bar/table-picker/table-picker.tsx b/src/components/editor-page/editor-pane/tool-bar/table-picker/table-picker.tsx deleted file mode 100644 index 0fe8e4e4c..000000000 --- a/src/components/editor-page/editor-pane/tool-bar/table-picker/table-picker.tsx +++ /dev/null @@ -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 = ({ show, onDismiss, onTablePicked }) => { - const { t } = useTranslation() - const containerRef = useRef(null) - const [tableSize, setTableSize] = useState() - 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 ( -
-

- {tableSize - ? t('editor.editorToolbar.table.size', { cols: tableSize?.columns, rows: tableSize.rows }) - : t('editor.editorToolbar.table.title')} -

-
- {createNumberRangeArray(8).map((row: number) => - createNumberRangeArray(10).map((col: number) => ( -
{ - setTableSize({ - rows: row + 1, - columns: col + 1 - }) - }} - title={t('editor.editorToolbar.table.size', { cols: col + 1, rows: row + 1 })} - onClick={onClick} - /> - )) - )} -
-
- - setShowDialog(false)} - onTablePicked={onTablePicked} - /> -
-
- ) -} diff --git a/src/components/editor-page/editor-pane/tool-bar/table-picker/table-size-picker-popover.tsx b/src/components/editor-page/editor-pane/tool-bar/table-picker/table-size-picker-popover.tsx new file mode 100644 index 000000000..063e8495f --- /dev/null +++ b/src/components/editor-page/editor-pane/tool-bar/table-picker/table-size-picker-popover.tsx @@ -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 { + 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 = ({ + onShowCustomSizeModal, + onTableSizeSelected, + onDismiss, + onRefUpdate, + ...props +}) => { + const { t } = useTranslation() + const [tableSize, setTableSize] = useState() + + 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 ( +
onTableSizeSelected(row + 1, col + 1)} + /> + ) + }) + ), + [onTableSizeSelected, onSizeHover, t, tableSize] + ) + + const popoverRef = useRef(null) + useOnRefChange(popoverRef, (newRef) => onRefUpdate(newRef)) + + return ( + + + + + +
+ {tableContainer} +
+
+ +
+
+
+ ) +} diff --git a/src/components/editor-page/editor-pane/tool-bar/table-picker/table-size-text.tsx b/src/components/editor-page/editor-pane/tool-bar/table-picker/table-size-text.tsx new file mode 100644 index 000000000..be572876a --- /dev/null +++ b/src/components/editor-page/editor-pane/tool-bar/table-picker/table-size-text.tsx @@ -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 = ({ tableSize }) => { + useTranslation() + + const translationValues = useMemo(() => { + return tableSize ? { cols: tableSize.columns, rows: tableSize.rows } : undefined + }, [tableSize]) + + if (!translationValues) { + return + } else { + return + } +} diff --git a/src/components/markdown-renderer/hooks/use-on-ref-change.ts b/src/components/markdown-renderer/hooks/use-on-ref-change.ts index ef8f0975d..f6495bf35 100644 --- a/src/components/markdown-renderer/hooks/use-on-ref-change.ts +++ b/src/components/markdown-renderer/hooks/use-on-ref-change.ts @@ -8,11 +8,8 @@ import equal from 'fast-deep-equal' import type { MutableRefObject } from 'react' import { useEffect, useRef } from 'react' -export const useOnRefChange = ( - reference: MutableRefObject, - onChange?: (newValue?: T) => void -): void => { - const lastValue = useRef() +export const useOnRefChange = (reference: MutableRefObject, onChange?: (newValue: T) => void): void => { + const lastValue = useRef() useEffect(() => { if (onChange && !equal(reference, lastValue.current)) { lastValue.current = reference.current