From a24ef18dd45ea679bd433b9db992b4ba814cd282 Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Sun, 29 Nov 2020 13:17:40 +0100 Subject: [PATCH] added table overlay to table toolbar button. (#763) Co-authored by: Tilman Vatteroth Co-authored by: Erik Michelson Signed-off-by: Philip Molares --- CHANGELOG.md | 1 + cypress/integration/toolbar.spec.ts | 65 +++++++++++--- public/locales/en.json | 9 +- .../common/number-range/number-range.ts | 9 ++ .../table-picker/custom-table-size-modal.tsx | 81 ++++++++++++++++++ .../table-picker/table-picker-button.tsx | 38 +++++++++ .../tool-bar/table-picker/table-picker.scss | 44 ++++++++++ .../tool-bar/table-picker/table-picker.tsx | 85 +++++++++++++++++++ .../editor/editor-pane/tool-bar/tool-bar.tsx | 6 +- .../tool-bar/utils/toolbarButtonUtils.ts | 11 ++- 10 files changed, 332 insertions(+), 17 deletions(-) create mode 100644 src/components/common/number-range/number-range.ts create mode 100644 src/components/editor/editor-pane/tool-bar/table-picker/custom-table-size-modal.tsx create mode 100644 src/components/editor/editor-pane/tool-bar/table-picker/table-picker-button.tsx create mode 100644 src/components/editor/editor-pane/tool-bar/table-picker/table-picker.scss create mode 100644 src/components/editor/editor-pane/tool-bar/table-picker/table-picker.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index f9f1fb486..ffb5c2c53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0 - Code blocks have a 'Copy code to clipboard' button. - Code blocks with 'vega-lite' as language are rendered as [vega-lite diagrams](https://vega.github.io/vega-lite/examples/). - Markdown files can be imported into an existing note directly from the editor. +- The table button in the toolbar opens an overlay where the user can choose the number of columns and rows ### Changed diff --git a/cypress/integration/toolbar.spec.ts b/cypress/integration/toolbar.spec.ts index 918b55623..3d26579d5 100644 --- a/cypress/integration/toolbar.spec.ts +++ b/cypress/integration/toolbar.spec.ts @@ -249,15 +249,58 @@ describe('Toolbar', () => { }) }) - it('table', () => { - cy.get('.fa-table') - .click() - cy.get('.CodeMirror-code > div:nth-of-type(2) > .CodeMirror-line > span span') - .should('have.text', '| # 1 | # 2 | # 3 |') - cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span') - .should('have.text', '| ---- | ---- | ---- |') - cy.get('.CodeMirror-activeline > .CodeMirror-line > span ') - .should('have.text', '| Text | Text | Text |') + describe('table', () => { + beforeEach(() => { + cy.get('.table-picker-container') + .should('not.be.visible') + cy.get('.fa-table') + .last() + .click() + cy.get('.table-picker-container') + .should('be.visible') + }) + + it('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('custom', () => { + cy.get('.modal-dialog') + .should('not.exist') + cy.get('.fa-table') + .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() + }) + + afterEach(() => { + cy.get('.CodeMirror-code > div:nth-of-type(2) > .CodeMirror-line > span span') + .should('have.text', '| # 1 | # 2 | # 3 | # 4 | # 5 |') + cy.get('.CodeMirror-code > div:nth-of-type(3) > .CodeMirror-line > span span') + .should('have.text', '| ---- | ---- | ---- | ---- | ---- |') + cy.get('.CodeMirror-code > div:nth-of-type(4) > .CodeMirror-line > span span') + .should('have.text', '| Text | Text | Text | Text | Text |') + cy.get('.CodeMirror-code > div:nth-of-type(5) > .CodeMirror-line > span span') + .should('have.text', '| Text | Text | Text | Text | Text |') + cy.get('.CodeMirror-activeline > .CodeMirror-line > span ') + .should('have.text', '| Text | Text | Text | Text | Text |') + }) }) it('line', () => { @@ -269,9 +312,9 @@ describe('Toolbar', () => { it('collapsable block', () => { cy.get('.fa-caret-square-o-down') - .click() + .click() cy.get('.CodeMirror-code > div:nth-of-type(2) > .CodeMirror-line > span span') - .should('have.text', '
') + .should('have.text', '
') }) it('comment', () => { diff --git a/public/locales/en.json b/public/locales/en.json index 4ddce04da..4a4b54314 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -275,7 +275,14 @@ "link": "Link", "image": "Image", "uploadImage": "Upload Image", - "table": "Table", + "table": { + "title": "Table", + "size": "{{cols}}x{{rows}} Table", + "customSize": "Custom Size", + "cols": "Cols", + "rows": "Rows", + "create": "Create Custom Table" + }, "line": "Horizontal line", "collapsableBlock": "Collapsable block", "comment": "Comment", diff --git a/src/components/common/number-range/number-range.ts b/src/components/common/number-range/number-range.ts new file mode 100644 index 000000000..d4003ddcb --- /dev/null +++ b/src/components/common/number-range/number-range.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const createNumberRangeArray = (length: number) : number[] => { + return Array.from(Array(length).keys()) +} diff --git a/src/components/editor/editor-pane/tool-bar/table-picker/custom-table-size-modal.tsx b/src/components/editor/editor-pane/tool-bar/table-picker/custom-table-size-modal.tsx new file mode 100644 index 000000000..8b8227f09 --- /dev/null +++ b/src/components/editor/editor-pane/tool-bar/table-picker/custom-table-size-modal.tsx @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React, { useCallback, useEffect, useState } from 'react' +import { Button, Form, ModalFooter } from 'react-bootstrap' +import { useTranslation } from 'react-i18next' +import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon' +import { CommonModal } from '../../../../common/modals/common-modal' +import { TableSize } from './table-picker' + +export interface CustomTableSizeModalProps { + showModal: boolean + onDismiss: () => void + onTablePicked: (row: number, cols: number) => void +} + +export const CustomTableSizeModal: React.FC = ({ showModal, onDismiss, onTablePicked }) => { + const { t } = useTranslation() + const [tableSize, setTableSize] = useState({ + rows: 0, + columns: 0 + }) + + useEffect(() => { + setTableSize({ + rows: 0, + columns: 0 + }) + }, [showModal]) + + const onClick = useCallback(() => { + onTablePicked(tableSize.rows, tableSize.columns) + onDismiss() + }, [onDismiss, tableSize, onTablePicked]) + + return ( + onDismiss()} + titleI18nKey={'editor.editorToolbar.table.customSize'} + closeButton={true} + icon={'table'}> +
+ { + const value = Number.parseInt(event.currentTarget.value) + setTableSize(old => ({ + rows: old.rows, + columns: isNaN(value) ? 0 : value + })) + }} + /> + + { + const value = Number.parseInt(event.currentTarget.value) + setTableSize(old => ({ + rows: isNaN(value) ? 0 : value, + columns: old.columns + })) + }}/> +
+ + + +
+ ) +} diff --git a/src/components/editor/editor-pane/tool-bar/table-picker/table-picker-button.tsx b/src/components/editor/editor-pane/tool-bar/table-picker/table-picker-button.tsx new file mode 100644 index 000000000..df36a6790 --- /dev/null +++ b/src/components/editor/editor-pane/tool-bar/table-picker/table-picker-button.tsx @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import CodeMirror from 'codemirror' +import React, { Fragment, useState } from 'react' +import { Button } 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' + +export interface TablePickerButtonProps { + editor: CodeMirror.Editor +} + +export const TablePickerButton: React.FC = ({ editor }) => { + const { t } = useTranslation() + const [showTablePicker, setShowTablePicker] = useState(false) + + return ( + + setShowTablePicker(false)} + onTablePicked={(rows, cols) => { + setShowTablePicker(false) + addTable(editor, rows, cols) + }} + /> + + + ) +} diff --git a/src/components/editor/editor-pane/tool-bar/table-picker/table-picker.scss b/src/components/editor/editor-pane/tool-bar/table-picker/table-picker.scss new file mode 100644 index 000000000..cb2755993 --- /dev/null +++ b/src/components/editor/editor-pane/tool-bar/table-picker/table-picker.scss @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + + + +.table-picker-container { + z-index: 1111; + + @import "../../../../../style/variables.light"; + + .table-cell { + border-top: 1px solid $dark; + border-left: 1px solid $dark; + } + + .table-container { + border-bottom: 1px solid $dark; + border-right: 1px solid $dark; + display: grid; + grid-template-columns: repeat(10, 15px [col-start]); + grid-template-rows: repeat(8, 15px [row-start]); + } + + body.dark { + @import "../../../../../style/variables.dark"; + + .table-cell { + border-top: 1px solid $dark; + border-left: 1px solid $dark; + } + + .table-container { + border-bottom: 1px solid $dark; + border-right: 1px solid $dark; + display: grid; + grid-template-columns: repeat(10, 15px [col-start]); + grid-template-rows: repeat(8, 15px [row-start]); + } + } +} + diff --git a/src/components/editor/editor-pane/tool-bar/table-picker/table-picker.tsx b/src/components/editor/editor-pane/tool-bar/table-picker/table-picker.tsx new file mode 100644 index 000000000..7e9dc7191 --- /dev/null +++ b/src/components/editor/editor-pane/tool-bar/table-picker/table-picker.tsx @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: 2020 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' + +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/editor-pane/tool-bar/tool-bar.tsx b/src/components/editor/editor-pane/tool-bar/tool-bar.tsx index 521e9794e..f5435a30b 100644 --- a/src/components/editor/editor-pane/tool-bar/tool-bar.tsx +++ b/src/components/editor/editor-pane/tool-bar/tool-bar.tsx @@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next' import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon' import { EditorPreferences } from './editor-preferences/editor-preferences' import { EmojiPickerButton } from './emoji-picker/emoji-picker-button' +import { TablePickerButton } from './table-picker/table-picker-button' import './tool-bar.scss' import { addCodeFences, @@ -23,7 +24,6 @@ import { addList, addOrderedList, addQuotes, - addTable, addTaskList, makeSelectionBold, makeSelectionItalic, @@ -102,9 +102,7 @@ export const ToolBar: React.FC = ({ editor }) => { - + diff --git a/src/components/editor/editor-pane/tool-bar/utils/toolbarButtonUtils.ts b/src/components/editor/editor-pane/tool-bar/utils/toolbarButtonUtils.ts index e57d2d072..43e88fbdc 100644 --- a/src/components/editor/editor-pane/tool-bar/utils/toolbarButtonUtils.ts +++ b/src/components/editor/editor-pane/tool-bar/utils/toolbarButtonUtils.ts @@ -6,6 +6,7 @@ import { Editor } from 'codemirror' import { EmojiClickEventDetail } from 'emoji-picker-element/shared' +import { createNumberRangeArray } from '../../../../common/number-range/number-range' import { getEmojiShortCode } from './emojiUtils' export const makeSelectionBold = (editor: Editor): void => wrapTextWith(editor, '**') @@ -29,7 +30,15 @@ export const addImage = (editor: Editor): void => addLink(editor, '!') export const addLine = (editor: Editor): void => changeLines(editor, line => `${line}\n----`) export const addCollapsableBlock = (editor: Editor): void => changeLines(editor, line => `${line}\n
\n Toggle label\n Toggled content\n
`) export const addComment = (editor: Editor): void => changeLines(editor, line => `${line}\n> []`) -export const addTable = (editor: Editor): void => changeLines(editor, line => `${line}\n| # 1 | # 2 | # 3 |\n| ---- | ---- | ---- |\n| Text | Text | Text |`) +export const addTable = (editor: Editor, rows: number, columns: number): void => { + const rowArray = createNumberRangeArray(rows) + const colArray = createNumberRangeArray(columns).map(col => col + 1) + const head = '| # ' + colArray.join(' | # ') + ' |' + const divider = '| ' + colArray.map(() => '----').join(' | ') + ' |' + const body = rowArray.map(() => '| ' + colArray.map(() => 'Text').join(' | ') + ' |').join('\n') + const table = `${head}\n${divider}\n${body}` + changeLines(editor, line => `${line}\n${table}`) +} export const addEmoji = (emoji: EmojiClickEventDetail, editor: Editor): void => { const shortCode = getEmojiShortCode(emoji)