mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-21 02:35:23 -04: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
|
- 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
|
- 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
|
- 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:`)
|
- 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
|
- The code button now adds code fences even if the user selected nothing beforehand
|
||||||
- Code blocks with 'csv' as language render as tables.
|
- Code blocks with 'csv' as language render as tables.
|
||||||
|
|
|
@ -265,4 +265,34 @@ describe('Autocompletion', () => {
|
||||||
.should('exist')
|
.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', '----')
|
.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', () => {
|
it('comment', () => {
|
||||||
cy.get('.fa-comment')
|
cy.get('.fa-comment')
|
||||||
.click()
|
.click()
|
||||||
|
|
|
@ -256,6 +256,7 @@
|
||||||
"uploadImage": "Upload Image",
|
"uploadImage": "Upload Image",
|
||||||
"table": "Table",
|
"table": "Table",
|
||||||
"line": "Horizontal line",
|
"line": "Horizontal line",
|
||||||
|
"collapsableBlock": "Collapsable block",
|
||||||
"comment": "Comment",
|
"comment": "Comment",
|
||||||
"preferences": "Editor settings",
|
"preferences": "Editor settings",
|
||||||
"emoji": "Open emoji picker"
|
"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 { Editor, Hints } from 'codemirror'
|
||||||
import { CodeBlockHinter } from './code-block'
|
import { CodeBlockHinter } from './code-block'
|
||||||
|
import { CollapsableBlockHinter } from './collapsable-block'
|
||||||
import { ContainerHinter } from './container'
|
import { ContainerHinter } from './container'
|
||||||
import { EmojiHinter } from './emoji'
|
import { EmojiHinter } from './emoji'
|
||||||
import { HeaderHinter } from './header'
|
import { HeaderHinter } from './header'
|
||||||
|
@ -55,5 +56,6 @@ export const allHinters: Hinter[] = [
|
||||||
HeaderHinter,
|
HeaderHinter,
|
||||||
ImageHinter,
|
ImageHinter,
|
||||||
LinkAndExtraTagHinter,
|
LinkAndExtraTagHinter,
|
||||||
PDFHinter
|
PDFHinter,
|
||||||
|
CollapsableBlockHinter
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { EmojiPickerButton } from './emoji-picker/emoji-picker-button'
|
||||||
import './tool-bar.scss'
|
import './tool-bar.scss'
|
||||||
import {
|
import {
|
||||||
addCodeFences,
|
addCodeFences,
|
||||||
|
addCollapsableBlock,
|
||||||
addComment,
|
addComment,
|
||||||
addHeaderLevel,
|
addHeaderLevel,
|
||||||
addImage,
|
addImage,
|
||||||
|
@ -101,6 +102,9 @@ export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
|
||||||
<Button variant='light' onClick={() => addLine(editor)} title={t('editor.editorToolbar.line')}>
|
<Button variant='light' onClick={() => addLine(editor)} title={t('editor.editorToolbar.line')}>
|
||||||
<ForkAwesomeIcon icon="minus"/>
|
<ForkAwesomeIcon icon="minus"/>
|
||||||
</Button>
|
</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')}>
|
<Button variant='light' onClick={() => addComment(editor)} title={t('editor.editorToolbar.comment')}>
|
||||||
<ForkAwesomeIcon icon="comment"/>
|
<ForkAwesomeIcon icon="comment"/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { EmojiData } from 'emoji-mart'
|
||||||
import { Mock } from 'ts-mockery'
|
import { Mock } from 'ts-mockery'
|
||||||
import {
|
import {
|
||||||
addCodeFences,
|
addCodeFences,
|
||||||
|
addCollapsableBlock,
|
||||||
addComment,
|
addComment,
|
||||||
addEmoji,
|
addEmoji,
|
||||||
addHeaderLevel,
|
addHeaderLevel,
|
||||||
|
@ -1499,6 +1500,93 @@ describe('test addLine', () => {
|
||||||
addLine(editor)
|
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', () => {
|
describe('test addComment', () => {
|
||||||
const { cursor, firstLine, multiline, multilineOffset } = buildRanges()
|
const { cursor, firstLine, multiline, multilineOffset } = buildRanges()
|
||||||
const textFirstLine = testContent.split('\n')[0]
|
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 addImage = (editor: Editor): void => addLink(editor, '!')
|
||||||
|
|
||||||
export const addLine = (editor: Editor): void => changeLines(editor, line => `${line}\n----`)
|
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 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): void => changeLines(editor, line => `${line}\n| # 1 | # 2 | # 3 |\n| ---- | ---- | ---- |\n| Text | Text | Text |`)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue