diff --git a/CHANGELOG.md b/CHANGELOG.md index 41bbfa52e..a7653652e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,8 @@ - HedgeDoc instances can now be branded either with a '@ ' or '@ ' 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 " { .should('exist') }) }) + + describe('collapsable blocks', () => { + it('via Enter', () => { + cy.get('.CodeMirror textarea') + .type(' .CodeMirror-line > span') + .should('have.text', '') // 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(' li') + .first() + .dblclick() + cy.get('.CodeMirror-hints') + .should('not.exist') + cy.get('.CodeMirror-activeline > .CodeMirror-line > span') + .should('have.text', '') + cy.get('.markdown-body > details') + .should('exist') + }) + }) }) diff --git a/cypress/integration/toolbar.spec.ts b/cypress/integration/toolbar.spec.ts index 4ce5553cb..4cc62e835 100644 --- a/cypress/integration/toolbar.spec.ts +++ b/cypress/integration/toolbar.spec.ts @@ -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', '
') + }) + it('comment', () => { cy.get('.fa-comment') .click() diff --git a/public/locales/en.json b/public/locales/en.json index 9d2ee8363..46cb74edb 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -256,6 +256,7 @@ "uploadImage": "Upload Image", "table": "Table", "line": "Horizontal line", + "collapsableBlock": "Collapsable block", "comment": "Comment", "preferences": "Editor settings", "emoji": "Open emoji picker" diff --git a/src/components/editor/editor-pane/autocompletion/collapsable-block.ts b/src/components/editor/editor-pane/autocompletion/collapsable-block.ts new file mode 100644 index 000000000..a2c45b4d1 --- /dev/null +++ b/src/components/editor/editor-pane/autocompletion/collapsable-block.ts @@ -0,0 +1,35 @@ +import { Editor, Hint, Hints, Pos } from 'codemirror' +import { findWordAtCursor, Hinter } from './index' + +const allowedChars = /[<\w>]/ +const wordRegExp = /^( => { + return new Promise((resolve) => { + const searchTerm = findWordAtCursor(editor, allowedChars) + const searchResult = wordRegExp.exec(searchTerm.text) + if (searchResult === null) { + resolve(null) + return + } + const suggestions = ['
\n Toggle label\n Toggled content\n
'] + 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 +} diff --git a/src/components/editor/editor-pane/autocompletion/index.ts b/src/components/editor/editor-pane/autocompletion/index.ts index f290a5255..609d0e340 100644 --- a/src/components/editor/editor-pane/autocompletion/index.ts +++ b/src/components/editor/editor-pane/autocompletion/index.ts @@ -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 ] diff --git a/src/components/editor/editor-pane/tool-bar/tool-bar.tsx b/src/components/editor/editor-pane/tool-bar/tool-bar.tsx index 880b6a4f0..87fcf88dd 100644 --- a/src/components/editor/editor-pane/tool-bar/tool-bar.tsx +++ b/src/components/editor/editor-pane/tool-bar/tool-bar.tsx @@ -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 = ({ editor }) => { + diff --git a/src/components/editor/editor-pane/tool-bar/utils/toolbarButtonUtils.test.ts b/src/components/editor/editor-pane/tool-bar/utils/toolbarButtonUtils.test.ts index 245d7eacf..53c296b15 100644 --- a/src/components/editor/editor-pane/tool-bar/utils/toolbarButtonUtils.test.ts +++ b/src/components/editor/editor-pane/tool-bar/utils/toolbarButtonUtils.test.ts @@ -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([{ + 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
\n Toggle label\n Toggled content\n
`) + done() + } + }) + addCollapsableBlock(editor) + }) + + it('1st line', done => { + Mock.extend(editor).with({ + listSelections: () => ( + Mock.of([{ + 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
\n Toggle label\n Toggled content\n
`) + done() + } + }) + addCollapsableBlock(editor) + }) + + it('multiple lines', done => { + Mock.extend(editor).with({ + listSelections: () => ( + Mock.of([{ + 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
\n Toggle label\n Toggled content\n
') + done() + } + }) + addCollapsableBlock(editor) + }) + + it('multiple lines with offset', done => { + Mock.extend(editor).with({ + listSelections: () => ( + Mock.of([{ + 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
\n Toggle label\n Toggled content\n
') + done() + } + }) + addCollapsableBlock(editor) + }) +}) + describe('test addComment', () => { const { cursor, firstLine, multiline, multilineOffset } = buildRanges() const textFirstLine = testContent.split('\n')[0] diff --git a/src/components/editor/editor-pane/tool-bar/utils/toolbarButtonUtils.ts b/src/components/editor/editor-pane/tool-bar/utils/toolbarButtonUtils.ts index ed700aa8e..cfffe1f77 100644 --- a/src/components/editor/editor-pane/tool-bar/utils/toolbarButtonUtils.ts +++ b/src/components/editor/editor-pane/tool-bar/utils/toolbarButtonUtils.ts @@ -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
\n Toggle label\n Toggled content\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 |`)