mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-29 01:54:21 -05:00
Add table formatting on paste of detected table (#957)
This commit is contained in:
parent
107f0f6fa3
commit
0b4a0afa16
14 changed files with 375 additions and 27 deletions
|
@ -68,8 +68,9 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
- Easier possibility to share notes via native share-buttons on supported devices.
|
- Easier possibility to share notes via native share-buttons on supported devices.
|
||||||
- Surround selected text with a link via shortcut (ctrl+k or cmd+k).
|
- Surround selected text with a link via shortcut (ctrl+k or cmd+k).
|
||||||
- A sidebar for menu options
|
- A sidebar for menu options
|
||||||
- Improved security by wrapping the markdown rendering into an iframe
|
- Improved security by wrapping the markdown rendering into an iframe.
|
||||||
- The intro page content can be changed by editing `public/intro.md`
|
- The intro page content can be changed by editing `public/intro.md`.
|
||||||
|
- When pasting tables (e.g. from LibreOffice Calc or MS Excel) they get reformatted to markdown tables.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,8 @@ describe('File upload', () => {
|
||||||
.then((image: string) => {
|
.then((image: string) => {
|
||||||
const pasteEvent = {
|
const pasteEvent = {
|
||||||
clipboardData: {
|
clipboardData: {
|
||||||
files: [Cypress.Blob.base64StringToBlob(image, 'image/png')]
|
files: [Cypress.Blob.base64StringToBlob(image, 'image/png')],
|
||||||
|
getData: (_: string) => ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cy.get('.CodeMirror-scroll')
|
cy.get('.CodeMirror-scroll')
|
||||||
|
|
|
@ -421,6 +421,9 @@
|
||||||
},
|
},
|
||||||
"ligatures": {
|
"ligatures": {
|
||||||
"label": "Show ligatures"
|
"label": "Show ligatures"
|
||||||
|
},
|
||||||
|
"smartPaste": {
|
||||||
|
"label": "Auto format pasted content"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -41,6 +41,7 @@ import { defaultKeyMap } from './key-map'
|
||||||
import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './status-bar/status-bar'
|
import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './status-bar/status-bar'
|
||||||
import { ToolBar } from './tool-bar/tool-bar'
|
import { ToolBar } from './tool-bar/tool-bar'
|
||||||
import { handleUpload } from './upload-handler'
|
import { handleUpload } from './upload-handler'
|
||||||
|
import { handleFilePaste, handleTablePaste, PasteEvent } from './tool-bar/utils/pasteHandlers'
|
||||||
|
|
||||||
export interface EditorPaneProps {
|
export interface EditorPaneProps {
|
||||||
onContentChange: (content: string) => void
|
onContentChange: (content: string) => void
|
||||||
|
@ -62,23 +63,6 @@ const onChange = (editor: Editor) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PasteEvent {
|
|
||||||
clipboardData: {
|
|
||||||
files: FileList
|
|
||||||
},
|
|
||||||
preventDefault: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPaste = (pasteEditor: Editor, event: PasteEvent) => {
|
|
||||||
if (event && event.clipboardData && event.clipboardData.files && event.clipboardData.files.length > 0) {
|
|
||||||
event.preventDefault()
|
|
||||||
const files: FileList = event.clipboardData.files
|
|
||||||
if (files && files.length >= 1) {
|
|
||||||
handleUpload(files[0], pasteEditor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DropEvent {
|
interface DropEvent {
|
||||||
pageX: number,
|
pageX: number,
|
||||||
pageY: number,
|
pageY: number,
|
||||||
|
@ -92,6 +76,7 @@ interface DropEvent {
|
||||||
export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentChange, content, scrollState, onScroll, onMakeScrollSource }) => {
|
export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentChange, content, scrollState, onScroll, onMakeScrollSource }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
||||||
|
const smartPasteEnabled = useSelector((state: ApplicationState) => state.editorConfig.smartPaste)
|
||||||
const [showMaxLengthWarning, setShowMaxLengthWarning] = useState(false)
|
const [showMaxLengthWarning, setShowMaxLengthWarning] = useState(false)
|
||||||
const maxLengthWarningAlreadyShown = useRef(false)
|
const maxLengthWarningAlreadyShown = useRef(false)
|
||||||
const [editor, setEditor] = useState<Editor>()
|
const [editor, setEditor] = useState<Editor>()
|
||||||
|
@ -103,6 +88,19 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
|
||||||
const [editorScroll, setEditorScroll] = useState<ScrollInfo>()
|
const [editorScroll, setEditorScroll] = useState<ScrollInfo>()
|
||||||
const onEditorScroll = useCallback((editor: Editor, data: ScrollInfo) => setEditorScroll(data), [])
|
const onEditorScroll = useCallback((editor: Editor, data: ScrollInfo) => setEditorScroll(data), [])
|
||||||
|
|
||||||
|
const onPaste = useCallback((pasteEditor: Editor, event: PasteEvent) => {
|
||||||
|
if (!event || !event.clipboardData) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (smartPasteEnabled) {
|
||||||
|
const tableInserted = handleTablePaste(event, pasteEditor)
|
||||||
|
if (tableInserted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleFilePaste(event, pasteEditor)
|
||||||
|
}, [smartPasteEnabled])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor || !onScroll || !editorScroll) {
|
if (!editor || !onScroll || !editorScroll) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { convertClipboardTableToMarkdown, isTable } from './table-extractor'
|
||||||
|
|
||||||
|
describe('isTable detection: ', () => {
|
||||||
|
|
||||||
|
it('empty string is no table', () => {
|
||||||
|
expect(isTable(''))
|
||||||
|
.toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('single line is no table', () => {
|
||||||
|
const input = 'some none table'
|
||||||
|
expect(isTable(input))
|
||||||
|
.toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines without tabs are no table', () => {
|
||||||
|
const input = 'some none table\nanother line'
|
||||||
|
expect(isTable(input))
|
||||||
|
.toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('code blocks are no table', () => {
|
||||||
|
const input = '```python\ndef a:\n\tprint("a")\n\tprint("b")```'
|
||||||
|
expect(isTable(input))
|
||||||
|
.toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tab-indented text is no table', () => {
|
||||||
|
const input = '\tsome tab indented text\n\tabc\n\tdef'
|
||||||
|
expect(isTable(input))
|
||||||
|
.toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('not equal number of tabs is no table', () => {
|
||||||
|
const input = '1 ...\n2\tabc\n3\td\te\tf\n4\t16'
|
||||||
|
expect(isTable(input))
|
||||||
|
.toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('table without newline at end is valid', () => {
|
||||||
|
const input = '1\t1\n2\t4\n3\t9\n4\t16\n5\t25'
|
||||||
|
expect(isTable(input))
|
||||||
|
.toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('table with newline at end is valid', () => {
|
||||||
|
const input = '1\t1\n2\t4\n3\t9\n4\t16\n5\t25\n'
|
||||||
|
expect(isTable(input))
|
||||||
|
.toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('table with some first cells missing is valid', () => {
|
||||||
|
const input = '1\t1\n\t0\n\t0\n4\t16\n5\t25\n'
|
||||||
|
expect(isTable(input))
|
||||||
|
.toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('table with some last cells missing is valid', () => {
|
||||||
|
const input = '1\t1\n2\t\n3\t\n4\t16\n'
|
||||||
|
expect(isTable(input))
|
||||||
|
.toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Conversion from clipboard table to markdown format', () => {
|
||||||
|
it('normal table without newline at end converts right', () => {
|
||||||
|
const input = '1\t1\ta\n2\t4\tb\n3\t9\tc\n4\t16\td'
|
||||||
|
expect(convertClipboardTableToMarkdown(input))
|
||||||
|
.toEqual('| #1 | #2 | #3 |\n| -- | -- | -- |\n| 1 | 1 | a |\n| 2 | 4 | b |\n| 3 | 9 | c |\n| 4 | 16 | d |')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('normal table with newline at end converts right', () => {
|
||||||
|
const input = '1\t1\n2\t4\n3\t9\n4\t16\n'
|
||||||
|
expect(convertClipboardTableToMarkdown(input))
|
||||||
|
.toEqual('| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| 2 | 4 |\n| 3 | 9 |\n| 4 | 16 |')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('table with some first cells missing converts right', () => {
|
||||||
|
const input = '1\t1\n\t0\n\t0\n4\t16\n'
|
||||||
|
expect(convertClipboardTableToMarkdown(input))
|
||||||
|
.toEqual('| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| | 0 |\n| | 0 |\n| 4 | 16 |')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('table with some last cells missing converts right', () => {
|
||||||
|
const input = '1\t1\n2\t\n3\t\n4\t16\n'
|
||||||
|
expect(convertClipboardTableToMarkdown(input))
|
||||||
|
.toEqual('| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| 2 | |\n| 3 | |\n| 4 | 16 |')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('empty input results in empty output', () => {
|
||||||
|
expect(convertClipboardTableToMarkdown('')).toEqual('')
|
||||||
|
})
|
||||||
|
})
|
58
src/components/editor-page/editor-pane/table-extractor.ts
Normal file
58
src/components/editor-page/editor-pane/table-extractor.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createNumberRangeArray } from '../../common/number-range/number-range'
|
||||||
|
|
||||||
|
export const isTable = (text: string): boolean => {
|
||||||
|
// Tables must consist of multiple rows and columns
|
||||||
|
if (!text.includes('\n') || !text.includes('\t')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Code within code blocks should not be parsed as a table
|
||||||
|
if (text.startsWith('```')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = text.split(/\r?\n/)
|
||||||
|
.filter(line => line.trim() !== '')
|
||||||
|
|
||||||
|
// Tab-indented text should not be matched as a table
|
||||||
|
if (lines.every(line => line.startsWith('\t'))) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Every line should have the same amount of tabs (table columns)
|
||||||
|
const tabsPerLines = lines.map(line => line.match(/\t/g)?.length ?? 0)
|
||||||
|
return tabsPerLines.every(line => line === tabsPerLines[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertClipboardTableToMarkdown = (pasteData: string): string => {
|
||||||
|
if (pasteData.trim() === '') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const tableRows = pasteData.split(/\r?\n/)
|
||||||
|
.filter(row => row.trim() !== '')
|
||||||
|
const tableCells = tableRows.reduce((cellsInRow, row, index) => {
|
||||||
|
cellsInRow[index] = row.split('\t')
|
||||||
|
return cellsInRow
|
||||||
|
}, [] as string[][])
|
||||||
|
const arrayMaxRows = createNumberRangeArray(tableCells.length)
|
||||||
|
const arrayMaxColumns = createNumberRangeArray(Math.max(...tableCells.map(row => row.length)))
|
||||||
|
|
||||||
|
const headRow1 = arrayMaxColumns
|
||||||
|
.map(col => `| #${ col + 1 } `)
|
||||||
|
.join('') + '|'
|
||||||
|
const headRow2 = arrayMaxColumns
|
||||||
|
.map(col => `| -${ '-'.repeat((col + 1).toString().length) } `)
|
||||||
|
.join('') + '|'
|
||||||
|
const body = arrayMaxRows
|
||||||
|
.map(row => {
|
||||||
|
return arrayMaxColumns
|
||||||
|
.map(col => '| ' + tableCells[row][col] + ' ')
|
||||||
|
.join('') + '|'
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
return `${ headRow1 }\n${ headRow2 }\n${ body }`
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import React, { ChangeEvent, useCallback } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { ApplicationState } from '../../../../../redux'
|
||||||
|
import { setEditorSmartPaste } from '../../../../../redux/editor/methods'
|
||||||
|
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
|
||||||
|
|
||||||
|
export const EditorPreferenceSmartPasteSelect: React.FC = () => {
|
||||||
|
const smartPasteEnabled = useSelector((state: ApplicationState) => Boolean(state.editorConfig.smartPaste)
|
||||||
|
.toString())
|
||||||
|
const saveSmartPaste = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const smartPasteActivated: boolean = event.target.value === 'true'
|
||||||
|
setEditorSmartPaste(smartPasteActivated)
|
||||||
|
}, [])
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditorPreferenceInput
|
||||||
|
onChange={ saveSmartPaste }
|
||||||
|
value={ smartPasteEnabled }
|
||||||
|
property={ 'smartPaste' }
|
||||||
|
type={ EditorPreferenceInputType.BOOLEAN }
|
||||||
|
>
|
||||||
|
<option value='true'>{ t(`common.yes`) }</option>
|
||||||
|
<option value='false'>{ t(`common.no`) }</option>
|
||||||
|
</EditorPreferenceInput>
|
||||||
|
)
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import { EditorPreferenceLigaturesSelect } from './editor-preference-ligatures-s
|
||||||
import { EditorPreferenceNumberProperty } from './editor-preference-number-property'
|
import { EditorPreferenceNumberProperty } from './editor-preference-number-property'
|
||||||
import { EditorPreferenceProperty } from './editor-preference-property'
|
import { EditorPreferenceProperty } from './editor-preference-property'
|
||||||
import { EditorPreferenceSelectProperty } from './editor-preference-select-property'
|
import { EditorPreferenceSelectProperty } from './editor-preference-select-property'
|
||||||
|
import { EditorPreferenceSmartPasteSelect } from './editor-preference-smart-paste-select'
|
||||||
|
|
||||||
export const EditorPreferences: React.FC = () => {
|
export const EditorPreferences: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
@ -57,6 +58,9 @@ export const EditorPreferences: React.FC = () => {
|
||||||
<ListGroup.Item>
|
<ListGroup.Item>
|
||||||
<EditorPreferenceLigaturesSelect/>
|
<EditorPreferenceLigaturesSelect/>
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
|
<ListGroup.Item>
|
||||||
|
<EditorPreferenceSmartPasteSelect/>
|
||||||
|
</ListGroup.Item>
|
||||||
<ListGroup.Item>
|
<ListGroup.Item>
|
||||||
<EditorPreferenceInput onChange={ () => alert('This feature is not yet implemented.') }
|
<EditorPreferenceInput onChange={ () => alert('This feature is not yet implemented.') }
|
||||||
property={ EditorPreferenceProperty.SPELL_CHECK }
|
property={ EditorPreferenceProperty.SPELL_CHECK }
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Mock } from 'ts-mockery'
|
||||||
|
import { Editor } from 'codemirror'
|
||||||
|
import { isCursorInCodefence } from './codefenceDetection'
|
||||||
|
|
||||||
|
Mock.configure('jest')
|
||||||
|
|
||||||
|
const mockEditor = (content: string, line: number) => {
|
||||||
|
const contentLines = content.split('\n')
|
||||||
|
return Mock.of<Editor>({
|
||||||
|
getCursor() {
|
||||||
|
return {
|
||||||
|
line: line,
|
||||||
|
ch: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDoc() {
|
||||||
|
return {
|
||||||
|
getLine(ln: number) {
|
||||||
|
return contentLines[ln] ?? ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Check whether cursor is in codefence', () => {
|
||||||
|
|
||||||
|
it('returns false for empty document', () => {
|
||||||
|
const editor = mockEditor('', 0)
|
||||||
|
expect(isCursorInCodefence(editor))
|
||||||
|
.toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns true with one open codefence directly above', () => {
|
||||||
|
const editor = mockEditor('```\n', 1)
|
||||||
|
expect(isCursorInCodefence(editor))
|
||||||
|
.toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns true with one open codefence and empty lines above', () => {
|
||||||
|
const editor = mockEditor('```\n\n\n', 3)
|
||||||
|
expect(isCursorInCodefence(editor))
|
||||||
|
.toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false with one completed codefence above', () => {
|
||||||
|
const editor = mockEditor('```\n\n```\n', 3)
|
||||||
|
expect(isCursorInCodefence(editor))
|
||||||
|
.toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns true with one completed and one open codefence above', () => {
|
||||||
|
const editor = mockEditor('```\n\n```\n\n```\n\n', 6)
|
||||||
|
expect(isCursorInCodefence(editor))
|
||||||
|
.toBe(true)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Editor } from 'codemirror'
|
||||||
|
|
||||||
|
export const isCursorInCodefence = (editor: Editor): boolean => {
|
||||||
|
const currentLine = editor.getCursor().line
|
||||||
|
let codefenceCount = 0
|
||||||
|
for (let line = currentLine; line >= 0; --line) {
|
||||||
|
const markdownContentLine = editor.getDoc().getLine(line)
|
||||||
|
if (markdownContentLine.startsWith('```')) {
|
||||||
|
codefenceCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return codefenceCount % 2 === 1
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Editor } from 'codemirror'
|
||||||
|
import { convertClipboardTableToMarkdown, isTable } from '../../table-extractor'
|
||||||
|
import { handleUpload } from '../../upload-handler'
|
||||||
|
import { insertAtCursor } from './toolbarButtonUtils'
|
||||||
|
import { isCursorInCodefence } from './codefenceDetection'
|
||||||
|
|
||||||
|
type ClipboardDataFormats = 'text' | 'url' | 'text/plain' | 'text/uri-list' | 'text/html'
|
||||||
|
|
||||||
|
export interface PasteEvent {
|
||||||
|
clipboardData: {
|
||||||
|
files: FileList,
|
||||||
|
getData: (format: ClipboardDataFormats) => string
|
||||||
|
},
|
||||||
|
preventDefault: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleTablePaste = (event: PasteEvent, editor: Editor): boolean => {
|
||||||
|
const pasteText = event.clipboardData.getData('text')
|
||||||
|
if (!pasteText || isCursorInCodefence(editor) || !isTable(pasteText)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
const markdownTable = convertClipboardTableToMarkdown(pasteText)
|
||||||
|
insertAtCursor(editor, markdownTable)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleFilePaste = (event: PasteEvent, editor: Editor): boolean => {
|
||||||
|
if (!event.clipboardData.files || event.clipboardData.files.length < 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
const files: FileList = event.clipboardData.files
|
||||||
|
if (files && files.length >= 1) {
|
||||||
|
handleUpload(files[0], editor)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import {
|
||||||
SetEditorConfigAction,
|
SetEditorConfigAction,
|
||||||
SetEditorLigaturesAction,
|
SetEditorLigaturesAction,
|
||||||
SetEditorPreferencesAction,
|
SetEditorPreferencesAction,
|
||||||
|
SetEditorSmartPasteAction,
|
||||||
SetEditorSyncScrollAction
|
SetEditorSyncScrollAction
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
|
@ -48,7 +49,7 @@ export const setEditorMode = (editorMode: EditorMode): void => {
|
||||||
export const setEditorSyncScroll = (syncScroll: boolean): void => {
|
export const setEditorSyncScroll = (syncScroll: boolean): void => {
|
||||||
const action: SetEditorSyncScrollAction = {
|
const action: SetEditorSyncScrollAction = {
|
||||||
type: EditorConfigActionType.SET_SYNC_SCROLL,
|
type: EditorConfigActionType.SET_SYNC_SCROLL,
|
||||||
syncScroll: syncScroll
|
syncScroll
|
||||||
}
|
}
|
||||||
store.dispatch(action)
|
store.dispatch(action)
|
||||||
}
|
}
|
||||||
|
@ -56,7 +57,15 @@ export const setEditorSyncScroll = (syncScroll: boolean): void => {
|
||||||
export const setEditorLigatures = (ligatures: boolean): void => {
|
export const setEditorLigatures = (ligatures: boolean): void => {
|
||||||
const action: SetEditorLigaturesAction = {
|
const action: SetEditorLigaturesAction = {
|
||||||
type: EditorConfigActionType.SET_LIGATURES,
|
type: EditorConfigActionType.SET_LIGATURES,
|
||||||
ligatures: ligatures
|
ligatures
|
||||||
|
}
|
||||||
|
store.dispatch(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setEditorSmartPaste = (smartPaste: boolean): void => {
|
||||||
|
const action: SetEditorSmartPasteAction = {
|
||||||
|
type: EditorConfigActionType.SET_SMART_PASTE,
|
||||||
|
smartPaste
|
||||||
}
|
}
|
||||||
store.dispatch(action)
|
store.dispatch(action)
|
||||||
}
|
}
|
||||||
|
@ -64,7 +73,7 @@ export const setEditorLigatures = (ligatures: boolean): void => {
|
||||||
export const mergeEditorPreferences = (preferences: EditorConfiguration): void => {
|
export const mergeEditorPreferences = (preferences: EditorConfiguration): void => {
|
||||||
const action: SetEditorPreferencesAction = {
|
const action: SetEditorPreferencesAction = {
|
||||||
type: EditorConfigActionType.MERGE_EDITOR_PREFERENCES,
|
type: EditorConfigActionType.MERGE_EDITOR_PREFERENCES,
|
||||||
preferences: preferences
|
preferences
|
||||||
}
|
}
|
||||||
store.dispatch(action)
|
store.dispatch(action)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
SetEditorConfigAction,
|
SetEditorConfigAction,
|
||||||
SetEditorLigaturesAction,
|
SetEditorLigaturesAction,
|
||||||
SetEditorPreferencesAction,
|
SetEditorPreferencesAction,
|
||||||
|
SetEditorSmartPasteAction,
|
||||||
SetEditorSyncScrollAction
|
SetEditorSyncScrollAction
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ const initialState: EditorConfig = {
|
||||||
editorMode: EditorMode.BOTH,
|
editorMode: EditorMode.BOTH,
|
||||||
ligatures: true,
|
ligatures: true,
|
||||||
syncScroll: true,
|
syncScroll: true,
|
||||||
|
smartPaste: true,
|
||||||
preferences: {
|
preferences: {
|
||||||
theme: 'one-dark',
|
theme: 'one-dark',
|
||||||
keyMap: 'sublime',
|
keyMap: 'sublime',
|
||||||
|
@ -57,6 +59,13 @@ export const EditorConfigReducer: Reducer<EditorConfig, EditorConfigActions> = (
|
||||||
}
|
}
|
||||||
saveToLocalStorage(newState)
|
saveToLocalStorage(newState)
|
||||||
return newState
|
return newState
|
||||||
|
case EditorConfigActionType.SET_SMART_PASTE:
|
||||||
|
newState = {
|
||||||
|
...state,
|
||||||
|
smartPaste: (action as SetEditorSmartPasteAction).smartPaste
|
||||||
|
}
|
||||||
|
saveToLocalStorage(newState)
|
||||||
|
return newState
|
||||||
case EditorConfigActionType.MERGE_EDITOR_PREFERENCES:
|
case EditorConfigActionType.MERGE_EDITOR_PREFERENCES:
|
||||||
newState = {
|
newState = {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -12,18 +12,20 @@ export enum EditorConfigActionType {
|
||||||
SET_EDITOR_VIEW_MODE = 'editor/mode/set',
|
SET_EDITOR_VIEW_MODE = 'editor/mode/set',
|
||||||
SET_SYNC_SCROLL = 'editor/syncScroll/set',
|
SET_SYNC_SCROLL = 'editor/syncScroll/set',
|
||||||
MERGE_EDITOR_PREFERENCES = 'editor/preferences/merge',
|
MERGE_EDITOR_PREFERENCES = 'editor/preferences/merge',
|
||||||
SET_LIGATURES = 'editor/preferences/setLigatures'
|
SET_LIGATURES = 'editor/preferences/setLigatures',
|
||||||
|
SET_SMART_PASTE = 'editor/preferences/setSmartPaste'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorConfig {
|
export interface EditorConfig {
|
||||||
editorMode: EditorMode;
|
editorMode: EditorMode
|
||||||
syncScroll: boolean;
|
syncScroll: boolean
|
||||||
ligatures: boolean
|
ligatures: boolean
|
||||||
|
smartPaste: boolean
|
||||||
preferences: EditorConfiguration
|
preferences: EditorConfiguration
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorConfigActions extends Action<EditorConfigActionType> {
|
export interface EditorConfigActions extends Action<EditorConfigActionType> {
|
||||||
type: EditorConfigActionType;
|
type: EditorConfigActionType
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetEditorSyncScrollAction extends EditorConfigActions {
|
export interface SetEditorSyncScrollAction extends EditorConfigActions {
|
||||||
|
@ -34,6 +36,10 @@ export interface SetEditorLigaturesAction extends EditorConfigActions {
|
||||||
ligatures: boolean
|
ligatures: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SetEditorSmartPasteAction extends EditorConfigActions {
|
||||||
|
smartPaste: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface SetEditorConfigAction extends EditorConfigActions {
|
export interface SetEditorConfigAction extends EditorConfigActions {
|
||||||
mode: EditorMode
|
mode: EditorMode
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue