mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-28 05:54:43 -04:00
Add clickable todos (#283)
This commit is contained in:
parent
0f30803529
commit
528e7e5904
9 changed files with 117 additions and 43 deletions
|
@ -43,6 +43,7 @@ export const Cheatsheet: React.FC = () => {
|
|||
<MarkdownRenderer
|
||||
content={code}
|
||||
wide={false}
|
||||
onTaskCheckedChange={(_) => null}
|
||||
onTocChange={() => false}
|
||||
onMetaDataChange={() => false}
|
||||
onFirstHeadingChange={() => false}
|
||||
|
|
|
@ -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'}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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!
|
||||
|
||||
`
|
||||
|
|
|
@ -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'}>
|
||||
|
|
|
@ -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']}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
declare module 'markdown-it-task-lists' {
|
||||
import MarkdownIt from 'markdown-it/lib'
|
||||
const markdownItTaskLists: MarkdownIt.PluginSimple
|
||||
export = markdownItTaskLists
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue