mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-23 11:37:02 -04:00
readd toolbar (#302)
* added all functionality to the toolbar buttons * added unit tests for the toolbar functions * added unit tests to CI * Added translated titles to buttons of toolbar Co-authored-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Co-authored-by: mrdrogdrog <mr.drogdrog@gmail.com> Co-authored-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
f0fe7f5ac2
commit
1b52bac838
10 changed files with 644 additions and 46 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
@ -27,5 +27,7 @@ jobs:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn install
|
run: yarn install
|
||||||
|
- name: run unit tests
|
||||||
|
run: yarn test
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: yarn build
|
run: yarn build
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
- The gist and pdf embeddings now use a one-click aproach similar to vimeo and youtube
|
- The gist and pdf embeddings now use a one-click aproach similar to vimeo and youtube
|
||||||
- Use [Twemoji](https://twemoji.twitter.com/) as icon font
|
- Use [Twemoji](https://twemoji.twitter.com/) as icon font
|
||||||
- The `[name=...]`, `[time=...]` and `[color=...]` tags may now be used anywhere in the document and not just inside of blockquotes and lists.
|
- The `[name=...]`, `[time=...]` and `[color=...]` tags may now be used anywhere in the document and not just inside of blockquotes and lists.
|
||||||
|
- The <i class="fa fa-picture-o"/> (add image) and <i class="fa fa-link"/> (add link) toolbar buttons, put selected links directly in the `()` instead of the `[]` part of the generated markdown
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,12 @@ You will also see any lint errors in the console.
|
||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
|
|
||||||
|
#### Unit
|
||||||
|
|
||||||
|
Unit testing is done via jest.
|
||||||
|
|
||||||
|
1. `yarn test`
|
||||||
|
|
||||||
#### End2End
|
#### End2End
|
||||||
|
|
||||||
We use [cypress](https://cypress.io) for e2e tests.
|
We use [cypress](https://cypress.io) for e2e tests.
|
||||||
|
|
|
@ -203,7 +203,7 @@
|
||||||
"bold": "Bold",
|
"bold": "Bold",
|
||||||
"italic": "Italic",
|
"italic": "Italic",
|
||||||
"strikethrough": "Strikethrough",
|
"strikethrough": "Strikethrough",
|
||||||
"header": "Header",
|
"header": "Heading",
|
||||||
"code": "Code",
|
"code": "Code",
|
||||||
"blockquote": "Blockquote",
|
"blockquote": "Blockquote",
|
||||||
"unorderedList": "Unordered List",
|
"unorderedList": "Unordered List",
|
||||||
|
@ -213,7 +213,7 @@
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
"uploadImage": "Upload Image",
|
"uploadImage": "Upload Image",
|
||||||
"table": "Table",
|
"table": "Table",
|
||||||
"line": "Line",
|
"line": "Horizontal line",
|
||||||
"comment": "Comment"
|
"comment": "Comment"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
|
|
|
@ -11,61 +11,94 @@ import 'codemirror/addon/search/match-highlighter'
|
||||||
import 'codemirror/addon/selection/active-line'
|
import 'codemirror/addon/selection/active-line'
|
||||||
import 'codemirror/keymap/sublime.js'
|
import 'codemirror/keymap/sublime.js'
|
||||||
import 'codemirror/mode/gfm/gfm.js'
|
import 'codemirror/mode/gfm/gfm.js'
|
||||||
import React from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
|
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import './editor-window.scss'
|
import './editor-window.scss'
|
||||||
|
import { Positions, SelectionData } from './interfaces'
|
||||||
|
import { ToolBar } from './tool-bar/tool-bar'
|
||||||
|
|
||||||
export interface EditorWindowProps {
|
export interface EditorWindowProps {
|
||||||
onContentChange: (content: string) => void
|
onContentChange: (content: string) => void
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, content }) => {
|
export const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, content }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const [positions, setPositions] = useState<Positions>({
|
||||||
|
startPosition: {
|
||||||
|
ch: 0,
|
||||||
|
line: 0
|
||||||
|
},
|
||||||
|
endPosition: {
|
||||||
|
ch: 0,
|
||||||
|
line: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSelection = useCallback((editor, data: SelectionData) => {
|
||||||
|
const { anchor, head } = data.ranges[0]
|
||||||
|
const headFirst = head.line < anchor.line || (head.line === anchor.line && head.ch < anchor.ch)
|
||||||
|
|
||||||
|
setPositions({
|
||||||
|
startPosition: {
|
||||||
|
line: headFirst ? head.line : anchor.line,
|
||||||
|
ch: headFirst ? head.ch : anchor.ch
|
||||||
|
},
|
||||||
|
endPosition: {
|
||||||
|
line: headFirst ? anchor.line : head.line,
|
||||||
|
ch: headFirst ? anchor.ch : head.ch
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ControlledCodeMirror
|
<div className={'d-flex flex-column h-100'}>
|
||||||
className="h-100 w-100 flex-fill"
|
<ToolBar
|
||||||
value={content}
|
content={content}
|
||||||
options={{
|
onContentChange={onContentChange}
|
||||||
mode: 'gfm',
|
positions={positions}
|
||||||
theme: 'one-dark',
|
/>
|
||||||
keyMap: 'sublime',
|
<ControlledCodeMirror
|
||||||
viewportMargin: 20,
|
className="overflow-hidden w-100 flex-fill"
|
||||||
styleActiveLine: true,
|
value={content}
|
||||||
lineNumbers: true,
|
options={{
|
||||||
lineWrapping: true,
|
mode: 'gfm',
|
||||||
showCursorWhenSelecting: true,
|
theme: 'one-dark',
|
||||||
highlightSelectionMatches: true,
|
keyMap: 'sublime',
|
||||||
indentUnit: 4,
|
viewportMargin: 20,
|
||||||
// continueComments: 'Enter',
|
styleActiveLine: true,
|
||||||
inputStyle: 'textarea',
|
lineNumbers: true,
|
||||||
matchBrackets: true,
|
lineWrapping: true,
|
||||||
autoCloseBrackets: true,
|
showCursorWhenSelecting: true,
|
||||||
matchTags: {
|
highlightSelectionMatches: true,
|
||||||
bothTags: true
|
indentUnit: 4,
|
||||||
},
|
// continueComments: 'Enter',
|
||||||
autoCloseTags: true,
|
inputStyle: 'textarea',
|
||||||
foldGutter: true,
|
matchBrackets: true,
|
||||||
gutters: [
|
autoCloseBrackets: true,
|
||||||
'CodeMirror-linenumbers',
|
matchTags: {
|
||||||
'authorship-gutters',
|
bothTags: true
|
||||||
'CodeMirror-foldgutter'
|
},
|
||||||
],
|
autoCloseTags: true,
|
||||||
// extraKeys: this.defaultExtraKeys,
|
foldGutter: true,
|
||||||
flattenSpans: true,
|
gutters: [
|
||||||
addModeClass: true,
|
'CodeMirror-linenumbers',
|
||||||
// autoRefresh: true,
|
'authorship-gutters',
|
||||||
// otherCursors: true
|
'CodeMirror-foldgutter'
|
||||||
placeholder: t('editor.placeholder')
|
],
|
||||||
}
|
// extraKeys: this.defaultExtraKeys,
|
||||||
}
|
flattenSpans: true,
|
||||||
onBeforeChange={(editor, data, value) => {
|
addModeClass: true,
|
||||||
onContentChange(value)
|
// autoRefresh: true,
|
||||||
}}
|
// otherCursors: true
|
||||||
/>
|
placeholder: t('editor.placeholder')
|
||||||
|
}}
|
||||||
|
onBeforeChange={(editor, data, value) => {
|
||||||
|
onContentChange(value)
|
||||||
|
}}
|
||||||
|
onSelection={onSelection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { EditorWindow }
|
|
||||||
|
|
15
src/components/editor/editor-window/interfaces.ts
Normal file
15
src/components/editor/editor-window/interfaces.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import CodeMirror from 'codemirror'
|
||||||
|
|
||||||
|
export interface SelectionData {
|
||||||
|
ranges: AnchorAndHead[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AnchorAndHead {
|
||||||
|
anchor: CodeMirror.Position
|
||||||
|
head: CodeMirror.Position
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Positions {
|
||||||
|
startPosition: CodeMirror.Position
|
||||||
|
endPosition: CodeMirror.Position
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
.btn-toolbar {
|
||||||
|
border: 1px solid #ededed;
|
||||||
|
}
|
82
src/components/editor/editor-window/tool-bar/tool-bar.tsx
Normal file
82
src/components/editor/editor-window/tool-bar/tool-bar.tsx
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { Button, ButtonToolbar } from 'react-bootstrap'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||||
|
import { Positions } from '../interfaces'
|
||||||
|
import './tool-bar.scss'
|
||||||
|
import { addCodeFences, addHeaderLevel, addLink, addMarkup, addQuotes, createList, replaceSelection } from './utils'
|
||||||
|
|
||||||
|
export interface ToolBarProps {
|
||||||
|
content: string
|
||||||
|
onContentChange: (content: string) => void
|
||||||
|
positions: Positions
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToolBar: React.FC<ToolBarProps> = ({ content, positions, onContentChange }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const notImplemented = () => {
|
||||||
|
alert('This feature is not yet implemented')
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeSelectionBold = () => addMarkup(content, positions.startPosition, positions.endPosition, onContentChange, '**')
|
||||||
|
const makeSelectionItalic = () => addMarkup(content, positions.startPosition, positions.endPosition, onContentChange, '*')
|
||||||
|
const strikeThroughSelection = () => addMarkup(content, positions.startPosition, positions.endPosition, onContentChange, '~~')
|
||||||
|
|
||||||
|
const addList = () => createList(content, positions.startPosition, positions.endPosition, onContentChange, () => '-')
|
||||||
|
const addOrderedList = () => createList(content, positions.startPosition, positions.endPosition, onContentChange, j => `${j}.`)
|
||||||
|
const addTaskList = () => createList(content, positions.startPosition, positions.endPosition, onContentChange, () => '- [ ]')
|
||||||
|
|
||||||
|
const addLine = () => replaceSelection(content, positions.startPosition, positions.endPosition, onContentChange, '----')
|
||||||
|
const addComment = () => replaceSelection(content, positions.startPosition, positions.endPosition, onContentChange, '> []')
|
||||||
|
const addTable = () => replaceSelection(content, positions.startPosition, positions.endPosition, onContentChange, '| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonToolbar className='flex-nowrap bg-light'>
|
||||||
|
<Button variant='light' onClick={makeSelectionBold} title={t('editor.editorToolbar.bold')}>
|
||||||
|
<ForkAwesomeIcon icon="bold"/>
|
||||||
|
</Button>
|
||||||
|
<Button variant='light' onClick={makeSelectionItalic} title={t('editor.editorToolbar.italic')}>
|
||||||
|
<ForkAwesomeIcon icon="italic"/>
|
||||||
|
</Button>
|
||||||
|
<Button variant='light' onClick={strikeThroughSelection} title={t('editor.editorToolbar.strikethrough')}>
|
||||||
|
<ForkAwesomeIcon icon="strikethrough"/>
|
||||||
|
</Button>
|
||||||
|
<Button variant='light' onClick={() => addHeaderLevel(content, positions.startPosition, onContentChange)} title={t('editor.editorToolbar.header')}>
|
||||||
|
<ForkAwesomeIcon icon="header"/>
|
||||||
|
</Button>
|
||||||
|
<Button variant='light' onClick={() => addCodeFences(content, positions.startPosition, positions.endPosition, onContentChange)} title={t('editor.editorToolbar.code')}>
|
||||||
|
<ForkAwesomeIcon icon="code"/>
|
||||||
|
</Button>
|
||||||
|
<Button variant='light' onClick={() => addQuotes(content, positions.startPosition, positions.endPosition, onContentChange)} title={t('editor.editorToolbar.blockquote')}>
|
||||||
|
<ForkAwesomeIcon icon="quote-right"/>
|
||||||
|
</Button>
|
||||||
|
<Button variant='light' onClick={addList} title={t('editor.editorToolbar.unorderedList')}>
|
||||||
|
<ForkAwesomeIcon icon="list"/>
|
||||||
|
</Button>
|
||||||
|
<Button variant='light' onClick={addOrderedList} title={t('editor.editorToolbar.orderedList')}>
|
||||||
|
<ForkAwesomeIcon icon="list-ol"/>
|
||||||
|
</Button>
|
||||||
|
<Button variant='light' onClick={addTaskList} title={t('editor.editorToolbar.checkList')}>
|
||||||
|
<ForkAwesomeIcon icon="check-square"/>
|
||||||
|
</Button>
|
||||||
|
<Button variant='light' onClick={() => addLink(content, positions.startPosition, positions.endPosition, onContentChange)} title={t('editor.editorToolbar.link')}>
|
||||||
|
<ForkAwesomeIcon icon="link"/>
|
||||||
|
</Button>
|
||||||
|
<Button variant='light' onClick={() => addLink(content, positions.startPosition, positions.endPosition, onContentChange, '!')} title={t('editor.editorToolbar.image')}>
|
||||||
|
<ForkAwesomeIcon icon="picture-o"/>
|
||||||
|
</Button>
|
||||||
|
<Button variant='light' onClick={notImplemented} title={t('editor.editorToolbar.uploadImage')}>
|
||||||
|
<ForkAwesomeIcon icon="upload"/>
|
||||||
|
</Button>
|
||||||
|
<Button variant='light' onClick={addTable} title={t('editor.editorToolbar.table')}>
|
||||||
|
<ForkAwesomeIcon icon="table"/>
|
||||||
|
</Button>
|
||||||
|
<Button variant='light' onClick={addLine} title={t('editor.editorToolbar.line')}>
|
||||||
|
<ForkAwesomeIcon icon="minus"/>
|
||||||
|
</Button>
|
||||||
|
<Button variant='light' onClick={addComment} title={t('editor.editorToolbar.comment')}>
|
||||||
|
<ForkAwesomeIcon icon="comment"/>
|
||||||
|
</Button>
|
||||||
|
</ButtonToolbar>
|
||||||
|
)
|
||||||
|
}
|
352
src/components/editor/editor-window/tool-bar/utils.test.ts
Normal file
352
src/components/editor/editor-window/tool-bar/utils.test.ts
Normal file
|
@ -0,0 +1,352 @@
|
||||||
|
import {
|
||||||
|
addCodeFences,
|
||||||
|
addHeaderLevel,
|
||||||
|
addLink,
|
||||||
|
addMarkup,
|
||||||
|
addQuotes,
|
||||||
|
createList,
|
||||||
|
removeLastNewLine,
|
||||||
|
replaceSelection
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
|
const testContent = '1st line\n2nd line\n3rd line'
|
||||||
|
const cursor = {
|
||||||
|
startPosition: { line: 0, ch: 0 },
|
||||||
|
endPosition: { line: 0, ch: 0 }
|
||||||
|
}
|
||||||
|
const firstLine = {
|
||||||
|
startPosition: { line: 0, ch: 0 },
|
||||||
|
endPosition: { line: 0, ch: 9 }
|
||||||
|
}
|
||||||
|
const multiline = {
|
||||||
|
startPosition: { line: 1, ch: 0 },
|
||||||
|
endPosition: { line: 2, ch: 9 }
|
||||||
|
}
|
||||||
|
const multilineOffset = {
|
||||||
|
startPosition: { line: 1, ch: 4 },
|
||||||
|
endPosition: { line: 2, ch: 4 }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('test removeLastNewLine', () => {
|
||||||
|
const testSentence = 'This is a test sentence'
|
||||||
|
const testMultiLine = 'This is a\ntest sentence over two lines'
|
||||||
|
it('single line without \\n at the end', () => {
|
||||||
|
expect(removeLastNewLine(testSentence)).toEqual(testSentence)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('single line with \\n at the end', () => {
|
||||||
|
expect(removeLastNewLine(testSentence + '\n')).toEqual(testSentence)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multi line without \\n at the end', () => {
|
||||||
|
expect(removeLastNewLine(testMultiLine)).toEqual(testMultiLine)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multi line with \\n at the end', () => {
|
||||||
|
expect(removeLastNewLine(testMultiLine + '\n')).toEqual(testMultiLine)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test addMarkUp', () => {
|
||||||
|
it('just cursor', done => {
|
||||||
|
let error = false
|
||||||
|
addMarkup(testContent, cursor.startPosition, cursor.endPosition, () => {
|
||||||
|
// This should never be called
|
||||||
|
error = true
|
||||||
|
}, '**')
|
||||||
|
expect(error).toBeFalsy()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('1st line', done => {
|
||||||
|
const newContent = testContent
|
||||||
|
.split('\n')
|
||||||
|
.map((line, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
line = `**${line}**`
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
addMarkup(testContent, firstLine.startPosition, firstLine.endPosition, content => {
|
||||||
|
expect(content).toEqual(newContent)
|
||||||
|
done()
|
||||||
|
}, '**')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines', done => {
|
||||||
|
const newContent = '1st line\n**2nd line\n3rd line**'
|
||||||
|
addMarkup(testContent, multiline.startPosition, multiline.endPosition, content => {
|
||||||
|
expect(content).toEqual(newContent)
|
||||||
|
done()
|
||||||
|
}, '**')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines with offset', done => {
|
||||||
|
const newContent = '1st line\n2nd **line\n3rd **line'
|
||||||
|
addMarkup(testContent, multilineOffset.startPosition, multilineOffset.endPosition, content => {
|
||||||
|
expect(content).toEqual(newContent)
|
||||||
|
done()
|
||||||
|
}, '**')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test addHeaderLevel', () => {
|
||||||
|
const firstHeading = '# 1st line\n2nd line\n3rd line'
|
||||||
|
it('no heading before', done => {
|
||||||
|
addHeaderLevel(testContent, cursor.startPosition, content => {
|
||||||
|
expect(content).toEqual(firstHeading)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('level one heading before', done => {
|
||||||
|
const secondHeading = '## 1st line\n2nd line\n3rd line'
|
||||||
|
addHeaderLevel(firstHeading, cursor.startPosition, content => {
|
||||||
|
expect(content).toEqual(secondHeading)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('1st line', done => {
|
||||||
|
addHeaderLevel(testContent, firstLine.startPosition, content => {
|
||||||
|
expect(content).toEqual(firstHeading)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const newMultilineContent = '1st line\n# 2nd line\n3rd line'
|
||||||
|
it('multiple lines', done => {
|
||||||
|
addHeaderLevel(testContent, multiline.startPosition, content => {
|
||||||
|
expect(content).toEqual(newMultilineContent)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines with offset', done => {
|
||||||
|
addHeaderLevel(testContent, multilineOffset.startPosition, content => {
|
||||||
|
expect(content).toEqual(newMultilineContent)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test addCodeFences', () => {
|
||||||
|
it('just cursor', done => {
|
||||||
|
addCodeFences(testContent, cursor.startPosition, cursor.endPosition, content => {
|
||||||
|
expect(content).toEqual('```\n\n```' + testContent)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('1st line', done => {
|
||||||
|
addCodeFences(testContent, firstLine.startPosition, firstLine.endPosition, content => {
|
||||||
|
expect(content).toEqual('```\n1st line\n```\n2nd line\n3rd line')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines', done => {
|
||||||
|
addCodeFences(testContent, multiline.startPosition, multiline.endPosition, content => {
|
||||||
|
expect(content).toEqual('1st line\n```\n2nd line\n3rd line\n```')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines with offset', done => {
|
||||||
|
addCodeFences(testContent, multilineOffset.startPosition, multilineOffset.endPosition, content => {
|
||||||
|
expect(content).toEqual('1st line\n2nd ```\nline\n3rd \n```line')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test addQuotes', () => {
|
||||||
|
it('just cursor', done => {
|
||||||
|
addQuotes(testContent, cursor.startPosition, cursor.endPosition, content => {
|
||||||
|
expect(content).toEqual('> ' + testContent)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('1st line', done => {
|
||||||
|
addQuotes(testContent, firstLine.startPosition, firstLine.endPosition, content => {
|
||||||
|
expect(content).toEqual('> ' + testContent)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines', done => {
|
||||||
|
addQuotes(testContent, multiline.startPosition, multiline.endPosition, content => {
|
||||||
|
expect(content).toEqual('1st line\n> 2nd line\n> 3rd line')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines with offset', done => {
|
||||||
|
addQuotes(testContent, multilineOffset.startPosition, multilineOffset.endPosition, content => {
|
||||||
|
expect(content).toEqual('1st line\n> 2nd line\n> 3rd line')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test createList', () => {
|
||||||
|
describe('unordered list', () => {
|
||||||
|
it('just cursor', done => {
|
||||||
|
createList(testContent, cursor.startPosition, cursor.endPosition, content => {
|
||||||
|
expect(content).toEqual('- ' + testContent)
|
||||||
|
done()
|
||||||
|
}, () => '-')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('1st line', done => {
|
||||||
|
createList(testContent, firstLine.startPosition, firstLine.endPosition, content => {
|
||||||
|
expect(content).toEqual('- ' + testContent)
|
||||||
|
done()
|
||||||
|
}, () => '-')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines', done => {
|
||||||
|
createList(testContent, multiline.startPosition, multiline.endPosition, content => {
|
||||||
|
expect(content).toEqual('1st line\n- 2nd line\n- 3rd line')
|
||||||
|
done()
|
||||||
|
}, () => '-')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines with offset', done => {
|
||||||
|
createList(testContent, multilineOffset.startPosition, multilineOffset.endPosition, content => {
|
||||||
|
expect(content).toEqual('1st line\n- 2nd line\n- 3rd line')
|
||||||
|
done()
|
||||||
|
}, () => '-')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ordered list', () => {
|
||||||
|
it('just cursor', done => {
|
||||||
|
createList(testContent, cursor.startPosition, cursor.endPosition, content => {
|
||||||
|
expect(content).toEqual('1. ' + testContent)
|
||||||
|
done()
|
||||||
|
}, (j) => `${j}.`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('1st line', done => {
|
||||||
|
createList(testContent, firstLine.startPosition, firstLine.endPosition, content => {
|
||||||
|
expect(content).toEqual('1. ' + testContent)
|
||||||
|
done()
|
||||||
|
}, (j) => `${j}.`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines', done => {
|
||||||
|
createList(testContent, multiline.startPosition, multiline.endPosition, content => {
|
||||||
|
expect(content).toEqual('1st line\n1. 2nd line\n2. 3rd line')
|
||||||
|
done()
|
||||||
|
}, (j) => `${j}.`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines with offset', done => {
|
||||||
|
createList(testContent, multilineOffset.startPosition, multilineOffset.endPosition, content => {
|
||||||
|
expect(content).toEqual('1st line\n1. 2nd line\n2. 3rd line')
|
||||||
|
done()
|
||||||
|
}, (j) => `${j}.`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test addLink', () => {
|
||||||
|
it('just cursor', done => {
|
||||||
|
addLink(testContent, cursor.startPosition, cursor.endPosition, content => {
|
||||||
|
expect(content).toEqual('[](https://)' + testContent)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('1st line', done => {
|
||||||
|
addLink(testContent, firstLine.startPosition, firstLine.endPosition, content => {
|
||||||
|
expect(content).toEqual('[1st line](https://)\n2nd line\n3rd line')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines', done => {
|
||||||
|
addLink(testContent, multiline.startPosition, multiline.endPosition, content => {
|
||||||
|
expect(content).toEqual('1st line\n[2nd line\n3rd line](https://)')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines with offset', done => {
|
||||||
|
addLink(testContent, multilineOffset.startPosition, multilineOffset.endPosition, content => {
|
||||||
|
expect(content).toEqual('1st line\n2nd [line\n3rd ](https://)line')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('line with link', done => {
|
||||||
|
const link = 'http://example.com'
|
||||||
|
addLink(link, firstLine.startPosition, { line: 0, ch: link.length }, content => {
|
||||||
|
expect(content).toEqual(`[](${link})`)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test addImage', () => {
|
||||||
|
it('just cursor', done => {
|
||||||
|
addLink(testContent, cursor.startPosition, cursor.endPosition, content => {
|
||||||
|
expect(content).toEqual('' + testContent)
|
||||||
|
done()
|
||||||
|
}, '!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('1st line', done => {
|
||||||
|
addLink(testContent, firstLine.startPosition, firstLine.endPosition, content => {
|
||||||
|
expect(content).toEqual('\n2nd line\n3rd line')
|
||||||
|
done()
|
||||||
|
}, '!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines', done => {
|
||||||
|
addLink(testContent, multiline.startPosition, multiline.endPosition, content => {
|
||||||
|
expect(content).toEqual('1st line\n')
|
||||||
|
done()
|
||||||
|
}, '!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines with offset', done => {
|
||||||
|
addLink(testContent, multilineOffset.startPosition, multilineOffset.endPosition, content => {
|
||||||
|
expect(content).toEqual('1st line\n2nd line')
|
||||||
|
done()
|
||||||
|
}, '!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test changeSelection', () => {
|
||||||
|
it('just cursor', done => {
|
||||||
|
replaceSelection(testContent, cursor.startPosition, cursor.endPosition, content => {
|
||||||
|
expect(content).toEqual('----' + testContent)
|
||||||
|
done()
|
||||||
|
}, '----')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('1st line', done => {
|
||||||
|
replaceSelection(testContent, firstLine.startPosition, firstLine.endPosition, content => {
|
||||||
|
expect(content).toEqual('----\n2nd line\n3rd line')
|
||||||
|
done()
|
||||||
|
}, '----')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines', done => {
|
||||||
|
replaceSelection(testContent, multiline.startPosition, multiline.endPosition, content => {
|
||||||
|
expect(content).toEqual('1st line\n----\n3rd line')
|
||||||
|
done()
|
||||||
|
}, '----')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('multiple lines with offset', done => {
|
||||||
|
replaceSelection(testContent, multilineOffset.startPosition, multilineOffset.endPosition, content => {
|
||||||
|
expect(content).toEqual('1st line\n2nd ----line\n3rd line')
|
||||||
|
done()
|
||||||
|
}, '----')
|
||||||
|
})
|
||||||
|
})
|
104
src/components/editor/editor-window/tool-bar/utils.ts
Normal file
104
src/components/editor/editor-window/tool-bar/utils.ts
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import CodeMirror from 'codemirror'
|
||||||
|
|
||||||
|
export const replaceSelection = (content: string, startPosition: CodeMirror.Position, endPosition: CodeMirror.Position, onContentChange: (content: string) => void, replaceText: string): void => {
|
||||||
|
const contentLines = content.split('\n')
|
||||||
|
const replaceTextLines = replaceText.split('\n')
|
||||||
|
const numberOfExtraLines = replaceTextLines.length - 1 - (endPosition.line - startPosition.line)
|
||||||
|
const replaceTextIncludeNewline = replaceText.includes('\n')
|
||||||
|
if (!replaceTextIncludeNewline) {
|
||||||
|
contentLines[startPosition.line] = contentLines[startPosition.line].slice(0, startPosition.ch) + replaceText + contentLines[startPosition.line].slice(endPosition.ch)
|
||||||
|
} else {
|
||||||
|
const lastPart = contentLines[endPosition.line].slice(endPosition.ch)
|
||||||
|
contentLines.push(...contentLines.slice(endPosition.line + 1))
|
||||||
|
contentLines[startPosition.line] = contentLines[startPosition.line].slice(0, startPosition.ch) + replaceTextLines[0]
|
||||||
|
contentLines.splice(startPosition.line + 1, replaceTextLines.length - 1, ...replaceTextLines.slice(1))
|
||||||
|
contentLines[numberOfExtraLines + endPosition.line] += lastPart
|
||||||
|
}
|
||||||
|
onContentChange(contentLines.join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractSelection = (content: string, startPosition: CodeMirror.Position, endPosition: CodeMirror.Position): string => {
|
||||||
|
if (startPosition.line === endPosition.line && startPosition.ch === endPosition.ch) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = content.split('\n')
|
||||||
|
|
||||||
|
if (startPosition.line === endPosition.line) {
|
||||||
|
return removeLastNewLine(lines[startPosition.line].slice(startPosition.ch, endPosition.ch))
|
||||||
|
}
|
||||||
|
|
||||||
|
let multiLineSelection = lines[startPosition.line].slice(startPosition.ch) + '\n'
|
||||||
|
for (let i = startPosition.line + 1; i <= endPosition.line; i++) {
|
||||||
|
if (i === endPosition.line) {
|
||||||
|
multiLineSelection += lines[i].slice(0, endPosition.ch)
|
||||||
|
} else {
|
||||||
|
multiLineSelection += lines[i] + '\n'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return multiLineSelection
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeLastNewLine = (selection: string): string => {
|
||||||
|
if (selection.endsWith('\n')) {
|
||||||
|
selection = selection.slice(0, -1)
|
||||||
|
}
|
||||||
|
return selection
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addMarkup = (content: string, startPosition: CodeMirror.Position, endPosition: CodeMirror.Position, onContentChange: (content: string) => void, markUp: string): void => {
|
||||||
|
const selection = extractSelection(content, startPosition, endPosition)
|
||||||
|
if (selection === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
replaceSelection(content, startPosition, endPosition, onContentChange, `${markUp}${selection}${markUp}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createList = (content: string, startPosition: CodeMirror.Position, endPosition: CodeMirror.Position, onContentChange: (content: string) => void, listMark: (j: number) => string): void => {
|
||||||
|
const lines = content.split('\n')
|
||||||
|
let j = 1
|
||||||
|
for (let i = startPosition.line; i <= endPosition.line; i++) {
|
||||||
|
lines[i] = `${listMark(j)} ${lines[i]}`
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
onContentChange(lines.join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addHeaderLevel = (content: string, startPosition: CodeMirror.Position, onContentChange: (content: string) => void): void => {
|
||||||
|
const lines = content.split('\n')
|
||||||
|
const startLine = lines[startPosition.line]
|
||||||
|
const isHeadingAlready = startLine.startsWith('#')
|
||||||
|
lines[startPosition.line] = `#${!isHeadingAlready ? ' ' : ''}${startLine}`
|
||||||
|
onContentChange(lines.join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addLink = (content: string, startPosition: CodeMirror.Position, endPosition: CodeMirror.Position, onContentChange: (content: string) => void, prefix?: string): void => {
|
||||||
|
const selection = extractSelection(content, startPosition, endPosition)
|
||||||
|
const linkRegex = /^(?:https?|ftp|mailto):/
|
||||||
|
if (linkRegex.exec(selection)) {
|
||||||
|
replaceSelection(content, startPosition, endPosition, onContentChange, `${prefix || ''}[](${selection})`)
|
||||||
|
} else {
|
||||||
|
replaceSelection(content, startPosition, endPosition, onContentChange, `${prefix || ''}[${selection}](https://)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addQuotes = (content: string, startPosition: CodeMirror.Position, endPosition: CodeMirror.Position, onContentChange: (content: string) => void): void => {
|
||||||
|
const selection = extractSelection(content, startPosition, endPosition)
|
||||||
|
if (selection === '') {
|
||||||
|
replaceSelection(content, startPosition, endPosition, onContentChange, '> ')
|
||||||
|
} else if (!selection.includes('\n')) {
|
||||||
|
const lines = content.split('\n')
|
||||||
|
replaceSelection(content, startPosition, endPosition, onContentChange, '> ' + lines[startPosition.line])
|
||||||
|
} else {
|
||||||
|
const lines = content.split('\n')
|
||||||
|
for (let i = startPosition.line; i <= endPosition.line; i++) {
|
||||||
|
lines[i] = `> ${lines[i]}`
|
||||||
|
}
|
||||||
|
onContentChange(lines.join('\n'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addCodeFences = (content: string, startPosition: CodeMirror.Position, endPosition: CodeMirror.Position, onContentChange: (content: string) => void): void => {
|
||||||
|
const selection = extractSelection(content, startPosition, endPosition)
|
||||||
|
replaceSelection(content, startPosition, endPosition, onContentChange, `\`\`\`\n${selection}\n\`\`\``)
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue