mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-15 07:34:42 -04:00
added table overlay to table toolbar button. (#763)
Co-authored by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Co-authored by: Erik Michelson <github@erik.michelson.eu> Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
parent
5077c95cba
commit
a24ef18dd4
10 changed files with 332 additions and 17 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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', '<details>')
|
||||
.should('have.text', '<details>')
|
||||
})
|
||||
|
||||
it('comment', () => {
|
||||
|
|
|
@ -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",
|
||||
|
|
9
src/components/common/number-range/number-range.ts
Normal file
9
src/components/common/number-range/number-range.ts
Normal file
|
@ -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())
|
||||
}
|
|
@ -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<CustomTableSizeModalProps> = ({ showModal, onDismiss, onTablePicked }) => {
|
||||
const { t } = useTranslation()
|
||||
const [tableSize, setTableSize] = useState<TableSize>({
|
||||
rows: 0,
|
||||
columns: 0
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setTableSize({
|
||||
rows: 0,
|
||||
columns: 0
|
||||
})
|
||||
}, [showModal])
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
onTablePicked(tableSize.rows, tableSize.columns)
|
||||
onDismiss()
|
||||
}, [onDismiss, tableSize, onTablePicked])
|
||||
|
||||
return (
|
||||
<CommonModal
|
||||
show={showModal}
|
||||
onHide={() => onDismiss()}
|
||||
titleI18nKey={'editor.editorToolbar.table.customSize'}
|
||||
closeButton={true}
|
||||
icon={'table'}>
|
||||
<div className={'col-lg-10 d-flex flex-row p-3 align-items-center'}>
|
||||
<Form.Control
|
||||
type={'number'}
|
||||
min={1}
|
||||
placeholder={t('editor.editorToolbar.table.cols')}
|
||||
isInvalid={tableSize.columns <= 0}
|
||||
onChange={(event) => {
|
||||
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}/>
|
||||
<Form.Control
|
||||
type={'number'}
|
||||
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
|
||||
}))
|
||||
}}/>
|
||||
</div>
|
||||
<ModalFooter>
|
||||
<Button onClick={onClick} disabled={tableSize.rows <= 0 || tableSize.columns <= 0}>
|
||||
{t('editor.editorToolbar.table.create')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</CommonModal>
|
||||
)
|
||||
}
|
|
@ -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<TablePickerButtonProps> = ({ editor }) => {
|
||||
const { t } = useTranslation()
|
||||
const [showTablePicker, setShowTablePicker] = useState(false)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<TablePicker
|
||||
show={showTablePicker}
|
||||
onDismiss={() => setShowTablePicker(false)}
|
||||
onTablePicked={(rows, cols) => {
|
||||
setShowTablePicker(false)
|
||||
addTable(editor, rows, cols)
|
||||
}}
|
||||
/>
|
||||
<Button variant='light' onClick={() => setShowTablePicker(old => !old)} title={t('editor.editorToolbar.table.title')}>
|
||||
<ForkAwesomeIcon icon="table"/>
|
||||
</Button>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<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 ? '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
|
||||
className={`table-cell ${tableSize && row < tableSize.rows && col < tableSize.columns ? 'bg-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 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>
|
||||
)
|
||||
}
|
|
@ -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<ToolBarProps> = ({ editor }) => {
|
|||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className={'mx-1 flex-wrap'}>
|
||||
<Button variant='light' onClick={() => addTable(editor)} title={t('editor.editorToolbar.table')}>
|
||||
<ForkAwesomeIcon icon="table"/>
|
||||
</Button>
|
||||
<TablePickerButton editor={editor}/>
|
||||
<Button variant='light' onClick={() => addLine(editor)} title={t('editor.editorToolbar.line')}>
|
||||
<ForkAwesomeIcon icon="minus"/>
|
||||
</Button>
|
||||
|
|
|
@ -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<details>\n <summary>Toggle label</summary>\n Toggled content\n</details>`)
|
||||
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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue