Add clickable todos (#283)

This commit is contained in:
Jakob Klepp 2020-09-02 13:44:13 +02:00 committed by GitHub
parent 0f30803529
commit 528e7e5904
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 117 additions and 43 deletions

View file

@ -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",

View file

@ -43,6 +43,7 @@ export const Cheatsheet: React.FC = () => {
<MarkdownRenderer
content={code}
wide={false}
onTaskCheckedChange={(_) => null}
onTocChange={() => false}
onMetaDataChange={() => false}
onFirstHeadingChange={() => false}

View file

@ -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<DocumentRenderPaneProps & ScrollProps> = ({ content, onMetadataChange, onFirstHeadingChange, wide, scrollState, onScroll, onMakeScrollSource }) => {
export const DocumentRenderPane: React.FC<DocumentRenderPaneProps & ScrollProps> = ({
content,
onFirstHeadingChange,
onMakeScrollSource,
onMetadataChange,
onScroll,
onTaskCheckedChange,
scrollState,
wide
}) => {
const [tocAst, setTocAst] = useState<TocAst>()
const renderer = useRef<HTMLDivElement>(null)
const { width } = useResizeObserver({ ref: renderer })
@ -88,11 +98,12 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps & ScrollProps>
<MarkdownRenderer
className={'flex-fill mb-3'}
content={content}
wide={wide}
onTocChange={(tocAst) => setTocAst(tocAst)}
onMetaDataChange={onMetadataChange}
onFirstHeadingChange={onFirstHeadingChange}
onLineMarkerPositionChanged={setLineMarks}
onMetaDataChange={onMetadataChange}
onTaskCheckedChange={onTaskCheckedChange}
onTocChange={(tocAst) => setTocAst(tocAst)}
wide={wide}
/>
<div className={'col-md'}>

View file

@ -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={
<DocumentRenderPane
content={markdownContent}
wide={editorMode === EditorMode.PREVIEW}
scrollState={scrollState.rendererScrollState}
onScroll={onMarkdownRendererScroll}
onMetadataChange={onMetadataChange}
onFirstHeadingChange={onFirstHeadingChange}
onMakeScrollSource={() => {
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'}/>
</div>
</Fragment>

View file

@ -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!
`

View file

@ -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<MarkdownRendererProps> = ({ content, onMetaDataChange, onFirstHeadingChange, onTocChange, className, wide, onLineMarkerPositionChanged }) => {
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
className,
content,
onFirstHeadingChange,
onLineMarkerPositionChanged,
onMetaDataChange,
onTaskCheckedChange,
onTocChange,
wide
}) => {
const [tocAst, setTocAst] = useState<TocAst>()
const lastTocAst = useRef<TocAst>()
const [yamlError, setYamlError] = useState(false)
@ -198,7 +209,7 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ 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<MarkdownRendererProps> = ({ 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<MarkdownRendererProps> = ({ 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 (
<div className={'bg-light flex-fill'}>

View file

@ -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<HTMLInputElement>): 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 (
<input
className="task-list-item-checkbox"
type="checkbox"
checked={node.attribs.checked !== undefined}
onChange={this.handleCheckboxChange}
data-line={node.attribs['data-line']}
key={`task-list-item-checkbox${node.attribs['data-line']}`}
/>
)
}
}
}

View file

@ -1,6 +0,0 @@
declare module 'markdown-it-task-lists' {
import MarkdownIt from 'markdown-it/lib'
const markdownItTaskLists: MarkdownIt.PluginSimple
export = markdownItTaskLists
}

View file

@ -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==