diff --git a/package.json b/package.json index 1e6e8e826..3a73dac47 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "license": "AGPL-3.0", "version": "0.1.0", "dependencies": { + "@hedgedoc/markdown-it-task-lists": "0.0.1", "@matejmazur/react-katex": "3.1.3", "@testing-library/jest-dom": "5.11.4", "@testing-library/react": "10.4.9", diff --git a/src/components/editor/app-bar/help-button/cheatsheet.tsx b/src/components/editor/app-bar/help-button/cheatsheet.tsx index 5a09da746..b78e9bde0 100644 --- a/src/components/editor/app-bar/help-button/cheatsheet.tsx +++ b/src/components/editor/app-bar/help-button/cheatsheet.tsx @@ -43,6 +43,7 @@ export const Cheatsheet: React.FC = () => { null} onTocChange={() => false} onMetaDataChange={() => false} onFirstHeadingChange={() => false} diff --git a/src/components/editor/document-renderer-pane/document-render-pane.tsx b/src/components/editor/document-renderer-pane/document-render-pane.tsx index 3e6d61dde..332ffb59c 100644 --- a/src/components/editor/document-renderer-pane/document-render-pane.tsx +++ b/src/components/editor/document-renderer-pane/document-render-pane.tsx @@ -12,12 +12,22 @@ import { YAMLMetaData } from '../yaml-metadata/yaml-metadata' interface DocumentRenderPaneProps { content: string - onMetadataChange: (metaData: YAMLMetaData | undefined) => void onFirstHeadingChange: (firstHeading: string | undefined) => void + onMetadataChange: (metaData: YAMLMetaData | undefined) => void + onTaskCheckedChange: (lineInMarkdown: number, checked: boolean) => void wide?: boolean } -export const DocumentRenderPane: React.FC = ({ content, onMetadataChange, onFirstHeadingChange, wide, scrollState, onScroll, onMakeScrollSource }) => { +export const DocumentRenderPane: React.FC = ({ + content, + onFirstHeadingChange, + onMakeScrollSource, + onMetadataChange, + onScroll, + onTaskCheckedChange, + scrollState, + wide +}) => { const [tocAst, setTocAst] = useState() const renderer = useRef(null) const { width } = useResizeObserver({ ref: renderer }) @@ -88,11 +98,12 @@ export const DocumentRenderPane: React.FC setTocAst(tocAst)} - onMetaDataChange={onMetadataChange} onFirstHeadingChange={onFirstHeadingChange} onLineMarkerPositionChanged={setLineMarks} + onMetaDataChange={onMetadataChange} + onTaskCheckedChange={onTaskCheckedChange} + onTocChange={(tocAst) => setTocAst(tocAst)} + wide={wide} />
diff --git a/src/components/editor/editor.tsx b/src/components/editor/editor.tsx index 36fcab04d..31f0429ab 100644 --- a/src/components/editor/editor.tsx +++ b/src/components/editor/editor.tsx @@ -26,6 +26,8 @@ export enum ScrollSource { RENDERER } +const TASK_REGEX = /(\s*[-*] )(\[[ xX]])( .*)/ + export const Editor: React.FC = () => { const { t } = useTranslation() const untitledNote = t('editor.untitledNote') @@ -55,15 +57,26 @@ export const Editor: React.FC = () => { } }, [untitledNote]) + const onFirstHeadingChange = useCallback((newFirstHeading: string | undefined) => { + firstHeading.current = newFirstHeading + updateDocumentTitle() + }, [updateDocumentTitle]) + const onMetadataChange = useCallback((metaData: YAMLMetaData | undefined) => { noteMetadata.current = metaData updateDocumentTitle() }, [updateDocumentTitle]) - const onFirstHeadingChange = useCallback((newFirstHeading: string | undefined) => { - firstHeading.current = newFirstHeading - updateDocumentTitle() - }, [updateDocumentTitle]) + const onTaskCheckedChange = useCallback((lineInMarkdown: number, checked: boolean) => { + const lines = markdownContent.split('\n') + const results = TASK_REGEX.exec(lines[lineInMarkdown]) + if (results) { + const before = results[1] + const after = results[3] + lines[lineInMarkdown] = `${before}[${checked ? 'x' : ' '}]${after}` + setMarkdownContent(lines.join('\n')) + } + }, [markdownContent, setMarkdownContent]) useEffect(() => { document.addEventListener('keydown', shortcutHandler, false) @@ -116,14 +129,15 @@ export const Editor: React.FC = () => { right={ { - scrollSource.current = ScrollSource.RENDERER - }}/>} + onMakeScrollSource={() => { scrollSource.current = ScrollSource.RENDERER }} + onMetadataChange={onMetadataChange} + onScroll={onMarkdownRendererScroll} + onTaskCheckedChange={onTaskCheckedChange} + scrollState={scrollState.rendererScrollState} + wide={editorMode === EditorMode.PREVIEW} + /> + } containerClassName={'overflow-hidden'}/>
diff --git a/src/components/editor/editorTestContent.ts b/src/components/editor/editorTestContent.ts index d06776585..8047293b0 100644 --- a/src/components/editor/editorTestContent.ts +++ b/src/components/editor/editorTestContent.ts @@ -115,4 +115,12 @@ end note @enduml \`\`\` +## ToDo List + +- [ ] ToDos + - [X] Buy some salad + - [ ] Brush teeth + - [x] Drink some water + - [ ] **Click my box** and see the source code, if you're allowed to edit! + ` diff --git a/src/components/markdown-renderer/markdown-renderer.tsx b/src/components/markdown-renderer/markdown-renderer.tsx index 2fadaf085..49592c29b 100644 --- a/src/components/markdown-renderer/markdown-renderer.tsx +++ b/src/components/markdown-renderer/markdown-renderer.tsx @@ -19,9 +19,9 @@ import plantuml from 'markdown-it-plantuml' import markdownItRegex from 'markdown-it-regex' import subscript from 'markdown-it-sub' import superscript from 'markdown-it-sup' -import taskList from 'markdown-it-task-lists' import toc from 'markdown-it-toc-done-right' import React, { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import markdownItTaskLists from '@hedgedoc/markdown-it-task-lists' import { Alert } from 'react-bootstrap' import ReactHtmlParser, { convertNodeToElement, Transform } from 'react-html-parser' import { Trans } from 'react-i18next' @@ -40,7 +40,6 @@ import { lineNumberMarker } from './markdown-it-plugins/line-number-marker' import { linkifyExtra } from './markdown-it-plugins/linkify-extra' import { MarkdownItParserDebugger } from './markdown-it-plugins/parser-debugger' import { plantumlError } from './markdown-it-plugins/plantuml-error' -import './markdown-renderer.scss' import { replaceAsciinemaLink } from './regex-plugins/replace-asciinema-link' import { replaceGistLink } from './regex-plugins/replace-gist-link' import { replaceLegacyGistShortCode } from './regex-plugins/replace-legacy-gist-short-code' @@ -65,8 +64,10 @@ import { KatexReplacer } from './replace-components/katex/katex-replacer' import { PdfReplacer } from './replace-components/pdf/pdf-replacer' import { PossibleWiderReplacer } from './replace-components/possible-wider/possible-wider-replacer' import { QuoteOptionsReplacer } from './replace-components/quote-options/quote-options-replacer' +import { TaskListReplacer } from './replace-components/task-list/task-list-replacer' import { VimeoReplacer } from './replace-components/vimeo/vimeo-replacer' import { YoutubeReplacer } from './replace-components/youtube/youtube-replacer' +import './markdown-renderer.scss' export interface LineMarkerPosition { line: number @@ -74,13 +75,14 @@ export interface LineMarkerPosition { } export interface MarkdownRendererProps { - content: string - wide?: boolean className?: string - onTocChange?: (ast: TocAst) => void - onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void + content: string onFirstHeadingChange?: (firstHeading: string | undefined) => void onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void + onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void + onTaskCheckedChange: (lineInMarkdown: number, checked: boolean) => void + onTocChange?: (ast: TocAst) => void + wide?: boolean } const markdownItTwitterEmojis = Object.keys((emojiData as unknown as Data).emojis) @@ -110,7 +112,16 @@ const forkAwesomeIconMap = Object.keys(ForkAwesomeIcons) return reduceObject }, {} as { [key: string]: string }) -export const MarkdownRenderer: React.FC = ({ content, onMetaDataChange, onFirstHeadingChange, onTocChange, className, wide, onLineMarkerPositionChanged }) => { +export const MarkdownRenderer: React.FC = ({ + className, + content, + onFirstHeadingChange, + onLineMarkerPositionChanged, + onMetaDataChange, + onTaskCheckedChange, + onTocChange, + wide +}) => { const [tocAst, setTocAst] = useState() const lastTocAst = useRef() const [yamlError, setYamlError] = useState(false) @@ -198,7 +209,7 @@ export const MarkdownRenderer: React.FC = ({ content, onM } }) } - md.use(taskList) + md.use(markdownItTaskLists, { lineNumber: true }) if (plantumlServer) { md.use(plantuml, { openMarker: '```plantuml', @@ -313,7 +324,8 @@ export const MarkdownRenderer: React.FC = ({ content, onM new FlowchartReplacer(), new HighlightedCodeReplacer(), new QuoteOptionsReplacer(), - new KatexReplacer() + new KatexReplacer(), + new TaskListReplacer(content, onTaskCheckedChange) ] if (onMetaDataChange) { // This is used if the front-matter callback is never called, because the user deleted everything regarding metadata from the document @@ -326,7 +338,7 @@ export const MarkdownRenderer: React.FC = ({ content, onM return tryToReplaceNode(node, index, allReplacers, subNodeConverter) || convertNodeToElement(node, index, transform) } return ReactHtmlParser(html, { transform: transform }) - }, [content, markdownIt, onMetaDataChange]) + }, [content, markdownIt, onMetaDataChange, onTaskCheckedChange]) return (
diff --git a/src/components/markdown-renderer/replace-components/task-list/task-list-replacer.tsx b/src/components/markdown-renderer/replace-components/task-list/task-list-replacer.tsx new file mode 100644 index 000000000..cbb90d99c --- /dev/null +++ b/src/components/markdown-renderer/replace-components/task-list/task-list-replacer.tsx @@ -0,0 +1,33 @@ +import React, { ReactElement } from 'react' +import { DomElement } from 'domhandler' +import { ComponentReplacer, SubNodeConverter } from '../ComponentReplacer' + +export class TaskListReplacer implements ComponentReplacer { + content: string + onTaskCheckedChange: (lineInMarkdown: number, checked: boolean) => void + + constructor (content: string, onTaskCheckedChange: (i: number, checked: boolean) => void) { + this.content = content + this.onTaskCheckedChange = onTaskCheckedChange + } + + handleCheckboxChange = (event: React.ChangeEvent): void => { + const lineNum = Number(event.currentTarget.dataset.line) + this.onTaskCheckedChange(lineNum, event.currentTarget.checked) + } + + getReplacement (node: DomElement, index:number, subNodeConverter: SubNodeConverter): (ReactElement|undefined) { + if (node.attribs?.class === 'task-list-item-checkbox') { + return ( + + ) + } + } +} diff --git a/src/external-types/markdown-it-task-lists/index.d.ts b/src/external-types/markdown-it-task-lists/index.d.ts deleted file mode 100644 index 05d74181b..000000000 --- a/src/external-types/markdown-it-task-lists/index.d.ts +++ /dev/null @@ -1,6 +0,0 @@ - -declare module 'markdown-it-task-lists' { - import MarkdownIt from 'markdown-it/lib' - const markdownItTaskLists: MarkdownIt.PluginSimple - export = markdownItTaskLists -} diff --git a/yarn.lock b/yarn.lock index 0cb8f563a..a142ff825 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1251,6 +1251,14 @@ dependencies: "@hapi/hoek" "^8.3.0" +"@hedgedoc/markdown-it-task-lists@0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@hedgedoc/markdown-it-task-lists/-/markdown-it-task-lists-0.0.1.tgz#73eff59c236ea474691377d66900daffb1ebaec2" + integrity sha512-wzht4gm2Pdbd7izjlZD/KV/Rsd2LkI5Le2I4x1pwtT1F58z5BFY9yl1SjneIlxT8Xa5Zm3IkEsH4MCPuBml9Ug== + dependencies: + "@types/markdown-it" "^10.0.2" + markdown-it "^11.0.0" + "@jest/console@^24.7.1", "@jest/console@^24.9.0": version "24.9.0" resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0" @@ -1824,7 +1832,7 @@ dependencies: "@types/markdown-it" "*" -"@types/markdown-it@*", "@types/markdown-it@10.0.2": +"@types/markdown-it@*", "@types/markdown-it@10.0.2", "@types/markdown-it@^10.0.2": version "10.0.2" resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-10.0.2.tgz#f93334b9c7821ddb19865dfd91ecf688094c2626" integrity sha512-FGKiVW1UgeIEAChYAuHcfCd0W4LsMEyrSyTVaZiuJhwR4BwSVUD8JKnzmWAMK2FHNLZSPGUaEkpa/dkZj2uq1w== @@ -1938,15 +1946,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.9.11", "@types/react@^16.9.35": - version "16.9.47" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.47.tgz#fb092936f0b56425f874d0ff1b08051fdf70c1ba" - integrity sha512-dAJO4VbrjYqTUwFiQqAKjLyHHl4RSTNnRyPdX3p16MPbDKvow51wxATUPxoe2QsiXNMEYrOjc2S6s92VjG+1VQ== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - -"@types/react@16.9.48": +"@types/react@*", "@types/react@^16.9.11", "@types/react@^16.9.35", "@types/react@16.9.48": version "16.9.48" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.48.tgz#d3387329f070d1b1bc0ff4a54a54ceefd5a8485c" integrity sha512-4ykBVswgYitPGMXFRxJCHkxJDU2rjfU3/zw67f8+dB7sNdVJXsrwqoYxz/stkAucymnEEbRPFmX7Ce5Mc/kJCw== @@ -8130,7 +8130,7 @@ markdown-it-toc-done-right@4.1.0: resolved "https://registry.yarnpkg.com/markdown-it-toc-done-right/-/markdown-it-toc-done-right-4.1.0.tgz#0f73ed531dda427c43c27716a2fc2208daa90605" integrity sha512-UhD2Oj6cZV3ycYPoelt4hTkwKIK3zbPP1wjjdpCq7UGtWQOFalDFDv1s2zBYV6aR2gMs/X8kpJcOYsQmUbiXDw== -markdown-it@11.0.0: +markdown-it@11.0.0, markdown-it@^11.0.0: version "11.0.0" resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-11.0.0.tgz#dbfc30363e43d756ebc52c38586b91b90046b876" integrity sha512-+CvOnmbSubmQFSA9dKz1BRiaSMV7rhexl3sngKqFyXSagoA3fBdJQ8oZWtRy2knXdpDXaBw44euz37DeJQ9asg==