mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 03:06:31 -05:00
Autocompletion and toolbar button for collapsable blocks (#615)
* Add autocompletion for <details construct * Add toolbar button for <details>-construct * Added CHANGELOG notice
This commit is contained in:
parent
2b6ba82b4b
commit
0f31c3b0b4
9 changed files with 171 additions and 2 deletions
|
@ -35,7 +35,8 @@
|
|||
- HedgeDoc instances can now be branded either with a '@ <custom string>' or '@ <custom logo>' after the HedgeDoc logo and text
|
||||
- Images will be loaded via proxy if an image proxy is configured in the backend
|
||||
- Asciinema videos may now be embedded by pasting the URL of one video into a single line
|
||||
- The Toolbar includes an EmojiPicker
|
||||
- The toolbar includes an EmojiPicker
|
||||
- Collapsable blocks can be added via a toolbar button or via autocompletion of "<details"
|
||||
- Added shortcodes for [fork-awesome icons](https://forkaweso.me/Fork-Awesome/icons/) (e.g. `:fa-picture-o:`)
|
||||
- The code button now adds code fences even if the user selected nothing beforehand
|
||||
- Code blocks with 'csv' as language render as tables.
|
||||
|
|
|
@ -265,4 +265,34 @@ describe('Autocompletion', () => {
|
|||
.should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('collapsable blocks', () => {
|
||||
it('via Enter', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('<d')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('{enter}')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '</details>') // after selecting the hint, the last line of the inserted suggestion is active
|
||||
cy.get('.markdown-body > details')
|
||||
.should('exist')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.get('.CodeMirror textarea')
|
||||
.type('<d')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('not.exist')
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', '</details>')
|
||||
cy.get('.markdown-body > details')
|
||||
.should('exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -261,6 +261,13 @@ describe('Toolbar', () => {
|
|||
.should('have.text', '----')
|
||||
})
|
||||
|
||||
it('collapsable block', () => {
|
||||
cy.get('.fa-caret-square-o-down')
|
||||
.click()
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(2) > .CodeMirror-line > span span')
|
||||
.should('have.text', '<details>')
|
||||
})
|
||||
|
||||
it('comment', () => {
|
||||
cy.get('.fa-comment')
|
||||
.click()
|
||||
|
|
|
@ -256,6 +256,7 @@
|
|||
"uploadImage": "Upload Image",
|
||||
"table": "Table",
|
||||
"line": "Horizontal line",
|
||||
"collapsableBlock": "Collapsable block",
|
||||
"comment": "Comment",
|
||||
"preferences": "Editor settings",
|
||||
"emoji": "Open emoji picker"
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import { Editor, Hint, Hints, Pos } from 'codemirror'
|
||||
import { findWordAtCursor, Hinter } from './index'
|
||||
|
||||
const allowedChars = /[<\w>]/
|
||||
const wordRegExp = /^(<d(?:e|et|eta|etai|etail|etails)?)$/
|
||||
|
||||
const collapsableBlockHint = (editor: Editor): Promise< Hints| null > => {
|
||||
return new Promise((resolve) => {
|
||||
const searchTerm = findWordAtCursor(editor, allowedChars)
|
||||
const searchResult = wordRegExp.exec(searchTerm.text)
|
||||
if (searchResult === null) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
const suggestions = ['<details>\n <summary>Toggle label</summary>\n Toggled content\n</details>']
|
||||
const cursor = editor.getCursor()
|
||||
if (!suggestions) {
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve({
|
||||
list: suggestions.map((suggestion: string): Hint => ({
|
||||
text: suggestion
|
||||
})),
|
||||
from: Pos(cursor.line, searchTerm.start),
|
||||
to: Pos(cursor.line, searchTerm.end + 1)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const CollapsableBlockHinter: Hinter = {
|
||||
allowedChars,
|
||||
wordRegExp,
|
||||
hint: collapsableBlockHint
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { Editor, Hints } from 'codemirror'
|
||||
import { CodeBlockHinter } from './code-block'
|
||||
import { CollapsableBlockHinter } from './collapsable-block'
|
||||
import { ContainerHinter } from './container'
|
||||
import { EmojiHinter } from './emoji'
|
||||
import { HeaderHinter } from './header'
|
||||
|
@ -55,5 +56,6 @@ export const allHinters: Hinter[] = [
|
|||
HeaderHinter,
|
||||
ImageHinter,
|
||||
LinkAndExtraTagHinter,
|
||||
PDFHinter
|
||||
PDFHinter,
|
||||
CollapsableBlockHinter
|
||||
]
|
||||
|
|
|
@ -8,6 +8,7 @@ import { EmojiPickerButton } from './emoji-picker/emoji-picker-button'
|
|||
import './tool-bar.scss'
|
||||
import {
|
||||
addCodeFences,
|
||||
addCollapsableBlock,
|
||||
addComment,
|
||||
addHeaderLevel,
|
||||
addImage,
|
||||
|
@ -101,6 +102,9 @@ export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
|
|||
<Button variant='light' onClick={() => addLine(editor)} title={t('editor.editorToolbar.line')}>
|
||||
<ForkAwesomeIcon icon="minus"/>
|
||||
</Button>
|
||||
<Button variant='light' onClick={() => addCollapsableBlock(editor)} title={t('editor.editorToolbar.collapsableBlock')}>
|
||||
<ForkAwesomeIcon icon="caret-square-o-down"/>
|
||||
</Button>
|
||||
<Button variant='light' onClick={() => addComment(editor)} title={t('editor.editorToolbar.comment')}>
|
||||
<ForkAwesomeIcon icon="comment"/>
|
||||
</Button>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { EmojiData } from 'emoji-mart'
|
|||
import { Mock } from 'ts-mockery'
|
||||
import {
|
||||
addCodeFences,
|
||||
addCollapsableBlock,
|
||||
addComment,
|
||||
addEmoji,
|
||||
addHeaderLevel,
|
||||
|
@ -1499,6 +1500,93 @@ describe('test addLine', () => {
|
|||
addLine(editor)
|
||||
})
|
||||
})
|
||||
|
||||
describe('test collapsable block', () => {
|
||||
const { cursor, firstLine, multiline, multilineOffset } = buildRanges()
|
||||
const textFirstLine = testContent.split('\n')[0]
|
||||
it('just cursor', done => {
|
||||
Mock.extend(editor).with({
|
||||
listSelections: () => (
|
||||
Mock.of<Range[]>([{
|
||||
anchor: cursor.from,
|
||||
head: cursor.to,
|
||||
from: () => cursor.from,
|
||||
to: () => cursor.to,
|
||||
empty: () => true
|
||||
}])
|
||||
),
|
||||
getLine: (): string => (textFirstLine),
|
||||
replaceRange: (replacement: string | string[]) => {
|
||||
expect(replacement).toEqual(`${textFirstLine}\n<details>\n <summary>Toggle label</summary>\n Toggled content\n</details>`)
|
||||
done()
|
||||
}
|
||||
})
|
||||
addCollapsableBlock(editor)
|
||||
})
|
||||
|
||||
it('1st line', done => {
|
||||
Mock.extend(editor).with({
|
||||
listSelections: () => (
|
||||
Mock.of<Range[]>([{
|
||||
anchor: firstLine.from,
|
||||
head: firstLine.to,
|
||||
from: () => firstLine.from,
|
||||
to: () => firstLine.to,
|
||||
empty: () => false
|
||||
}])
|
||||
),
|
||||
getLine: (): string => (textFirstLine),
|
||||
replaceRange: (replacement: string | string[], from: CodeMirror.Position, to?: CodeMirror.Position) => {
|
||||
expect(from).toEqual(firstLine.from)
|
||||
expect(to).toEqual(firstLine.to)
|
||||
expect(replacement).toEqual(`${textFirstLine}\n<details>\n <summary>Toggle label</summary>\n Toggled content\n</details>`)
|
||||
done()
|
||||
}
|
||||
})
|
||||
addCollapsableBlock(editor)
|
||||
})
|
||||
|
||||
it('multiple lines', done => {
|
||||
Mock.extend(editor).with({
|
||||
listSelections: () => (
|
||||
Mock.of<Range[]>([{
|
||||
anchor: multiline.from,
|
||||
head: multiline.to,
|
||||
from: () => multiline.from,
|
||||
to: () => multiline.to,
|
||||
empty: () => false
|
||||
}])
|
||||
),
|
||||
getLine: (): string => '2nd line',
|
||||
replaceRange: (replacement: string | string[]) => {
|
||||
expect(replacement).toEqual('2nd line\n<details>\n <summary>Toggle label</summary>\n Toggled content\n</details>')
|
||||
done()
|
||||
}
|
||||
})
|
||||
addCollapsableBlock(editor)
|
||||
})
|
||||
|
||||
it('multiple lines with offset', done => {
|
||||
Mock.extend(editor).with({
|
||||
listSelections: () => (
|
||||
Mock.of<Range[]>([{
|
||||
anchor: multilineOffset.from,
|
||||
head: multilineOffset.to,
|
||||
from: () => multilineOffset.from,
|
||||
to: () => multilineOffset.to,
|
||||
empty: () => false
|
||||
}])
|
||||
),
|
||||
getLine: (): string => '2nd line',
|
||||
replaceRange: (replacement: string | string[]) => {
|
||||
expect(replacement).toEqual('2nd line\n<details>\n <summary>Toggle label</summary>\n Toggled content\n</details>')
|
||||
done()
|
||||
}
|
||||
})
|
||||
addCollapsableBlock(editor)
|
||||
})
|
||||
})
|
||||
|
||||
describe('test addComment', () => {
|
||||
const { cursor, firstLine, multiline, multilineOffset } = buildRanges()
|
||||
const textFirstLine = testContent.split('\n')[0]
|
||||
|
|
|
@ -21,6 +21,7 @@ export const addTaskList = (editor: Editor): void => createList(editor, () => '-
|
|||
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 |`)
|
||||
|
||||
|
|
Loading…
Reference in a new issue