mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-21 10:45:20 -04:00
Add image placeholder and upload indicating frame (#1666)
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de> Co-authored-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
parent
58fecc0b3a
commit
d4251519e2
37 changed files with 908 additions and 72 deletions
|
@ -46,7 +46,7 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
|
|||
baseUrl,
|
||||
currentLineMarkers,
|
||||
useMemo(() => [new HeadlineAnchorsMarkdownExtension()], []),
|
||||
lineOffset,
|
||||
lineOffset ?? 0,
|
||||
onTaskCheckedChange,
|
||||
onImageClick,
|
||||
onTocChange
|
||||
|
|
|
@ -78,6 +78,8 @@ export const useConvertMarkdownToReactDom = (
|
|||
return useMemo(() => {
|
||||
const html = markdownIt.render(markdownCode)
|
||||
|
||||
htmlToReactTransformer.resetReplacers()
|
||||
|
||||
return convertHtmlToReact(html, {
|
||||
transform: (node, index) => htmlToReactTransformer.translateNodeToReactElement(node, index),
|
||||
preprocessNodes: (document) => nodePreProcessor(document)
|
||||
|
|
|
@ -40,6 +40,8 @@ import type { ImageClickHandler } from '../markdown-extension/image/proxy-image-
|
|||
import type { TocAst } from 'markdown-it-toc-done-right'
|
||||
import type { MarkdownExtension } from '../markdown-extension/markdown-extension'
|
||||
import { IframeCapsuleMarkdownExtension } from '../markdown-extension/iframe-capsule/iframe-capsule-markdown-extension'
|
||||
import { ImagePlaceholderMarkdownExtension } from '../markdown-extension/image-placeholder/image-placeholder-markdown-extension'
|
||||
import { UploadIndicatingImageFrameMarkdownExtension } from '../markdown-extension/upload-indicating-image-frame/upload-indicating-image-frame-markdown-extension'
|
||||
|
||||
/**
|
||||
* Provides a list of {@link MarkdownExtension markdown extensions} that is a combination of the common extensions and the given additional.
|
||||
|
@ -57,7 +59,7 @@ export const useMarkdownExtensions = (
|
|||
baseUrl: string,
|
||||
currentLineMarkers: MutableRefObject<LineMarkers[] | undefined> | undefined,
|
||||
additionalExtensions: MarkdownExtension[],
|
||||
lineOffset?: number,
|
||||
lineOffset: number,
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void,
|
||||
onImageClick?: ImageClickHandler,
|
||||
onTocChange?: (ast?: TocAst) => void
|
||||
|
@ -71,10 +73,12 @@ export const useMarkdownExtensions = (
|
|||
new VegaLiteMarkdownExtension(),
|
||||
new MarkmapMarkdownExtension(),
|
||||
new LinemarkerMarkdownExtension(
|
||||
currentLineMarkers ? (lineMarkers) => (currentLineMarkers.current = lineMarkers) : undefined,
|
||||
lineOffset
|
||||
lineOffset,
|
||||
currentLineMarkers ? (lineMarkers) => (currentLineMarkers.current = lineMarkers) : undefined
|
||||
),
|
||||
new IframeCapsuleMarkdownExtension(),
|
||||
new ImagePlaceholderMarkdownExtension(lineOffset),
|
||||
new UploadIndicatingImageFrameMarkdownExtension(),
|
||||
new GistMarkdownExtension(),
|
||||
new YoutubeMarkdownExtension(),
|
||||
new VimeoMarkdownExtension(),
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type MarkdownIt from 'markdown-it/lib'
|
||||
import { ImagePlaceholderMarkdownExtension } from './image-placeholder-markdown-extension'
|
||||
|
||||
/**
|
||||
* A {@link MarkdownIt.PluginSimple markdown it plugin} that adds the line number of the markdown code to every placeholder image.
|
||||
*
|
||||
* @param markdownIt The markdown it instance to which the plugin should be added
|
||||
*/
|
||||
export const addLineToPlaceholderImageTags: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt) => {
|
||||
markdownIt.core.ruler.push('image-placeholder', (state) => {
|
||||
state.tokens.forEach((token) => {
|
||||
if (token.type !== 'inline') {
|
||||
return
|
||||
}
|
||||
token.children?.forEach((childToken) => {
|
||||
if (
|
||||
childToken.type === 'image' &&
|
||||
childToken.attrGet('src') === ImagePlaceholderMarkdownExtension.PLACEHOLDER_URL
|
||||
) {
|
||||
const line = token.map?.[0]
|
||||
if (line !== undefined) {
|
||||
childToken.attrSet('data-line', String(line))
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
return true
|
||||
})
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useRendererToEditorCommunicator } from '../../../../editor-page/render-context/renderer-to-editor-communicator-context-provider'
|
||||
import { useCallback } from 'react'
|
||||
import { CommunicationMessageType } from '../../../../render-page/window-post-message-communicator/rendering-message'
|
||||
import { Logger } from '../../../../../utils/logger'
|
||||
|
||||
const log = new Logger('useOnImageUpload')
|
||||
|
||||
/**
|
||||
* Converts a {@link File} to a data url.
|
||||
*
|
||||
* @param file The file to convert
|
||||
* @return The file content represented as data url
|
||||
*/
|
||||
const readFileAsDataUrl = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => resolve(reader.result as string)
|
||||
reader.onerror = (error) => reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a callback that sends a {@link File file} to the editor via iframe communication.
|
||||
*
|
||||
* @param lineIndex The index of the line in the markdown content where the placeholder is defined
|
||||
* @param placeholderIndexInLine The index of the placeholder in the markdown content line
|
||||
*/
|
||||
export const useOnImageUpload = (
|
||||
lineIndex: number | undefined,
|
||||
placeholderIndexInLine: number | undefined
|
||||
): ((file: File) => void) => {
|
||||
const communicator = useRendererToEditorCommunicator()
|
||||
|
||||
return useCallback(
|
||||
(file: File) => {
|
||||
readFileAsDataUrl(file)
|
||||
.then((dataUri) => {
|
||||
communicator.sendMessageToOtherSide({
|
||||
type: CommunicationMessageType.IMAGE_UPLOAD,
|
||||
dataUri,
|
||||
fileName: file.name,
|
||||
lineIndex,
|
||||
placeholderIndexInLine
|
||||
})
|
||||
})
|
||||
.catch((error: ProgressEvent) => log.error('Error while uploading image', error))
|
||||
},
|
||||
[communicator, placeholderIndexInLine, lineIndex]
|
||||
)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { CSSProperties } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { calculatePlaceholderContainerSize } from '../utils/build-placeholder-size-css'
|
||||
|
||||
/**
|
||||
* Creates the style attribute for a placeholder container with width and height.
|
||||
*
|
||||
* @param width The wanted width
|
||||
* @param height The wanted height
|
||||
* @return The created style attributes
|
||||
*/
|
||||
export const usePlaceholderSizeStyle = (width?: string | number, height?: string | number): CSSProperties => {
|
||||
return useMemo(() => {
|
||||
const [convertedWidth, convertedHeight] = calculatePlaceholderContainerSize(width, height)
|
||||
|
||||
return {
|
||||
width: `${convertedWidth}px`,
|
||||
height: `${convertedHeight}px`
|
||||
}
|
||||
}, [height, width])
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { MarkdownExtension } from '../markdown-extension'
|
||||
import { addLineToPlaceholderImageTags } from './add-line-to-placeholder-image-tags'
|
||||
import type MarkdownIt from 'markdown-it/lib'
|
||||
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||
import { ImagePlaceholderReplacer } from './image-placeholder-replacer'
|
||||
|
||||
/**
|
||||
* A markdown extension that
|
||||
*/
|
||||
export class ImagePlaceholderMarkdownExtension extends MarkdownExtension {
|
||||
public static readonly PLACEHOLDER_URL = 'https://'
|
||||
|
||||
constructor(private lineOffset: number) {
|
||||
super()
|
||||
}
|
||||
|
||||
configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||
addLineToPlaceholderImageTags(markdownIt)
|
||||
}
|
||||
|
||||
buildReplacers(): ComponentReplacer[] {
|
||||
return [new ImagePlaceholderReplacer(this.lineOffset)]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { NativeRenderer, NodeReplacement, SubNodeTransform } from '../../replace-components/component-replacer'
|
||||
import { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||
import type { Element } from 'domhandler'
|
||||
import { ImagePlaceholder } from './image-placeholder'
|
||||
import { ImagePlaceholderMarkdownExtension } from './image-placeholder-markdown-extension'
|
||||
|
||||
/**
|
||||
* Replaces every image tag that has the {@link ImagePlaceholderMarkdownExtension.PLACEHOLDER_URL placeholder url} with the {@link ImagePlaceholder image placeholder element}.
|
||||
*/
|
||||
export class ImagePlaceholderReplacer extends ComponentReplacer {
|
||||
private countPerSourceLine = new Map<number, number>()
|
||||
|
||||
constructor(private lineOffset: number) {
|
||||
super()
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.countPerSourceLine = new Map<number, number>()
|
||||
}
|
||||
|
||||
replace(node: Element, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): NodeReplacement {
|
||||
if (node.name === 'img' && node.attribs && node.attribs.src === ImagePlaceholderMarkdownExtension.PLACEHOLDER_URL) {
|
||||
const lineIndex = Number(node.attribs['data-line'])
|
||||
const indexInLine = this.countPerSourceLine.get(lineIndex) ?? 0
|
||||
this.countPerSourceLine.set(lineIndex, indexInLine + 1)
|
||||
return (
|
||||
<ImagePlaceholder
|
||||
alt={node.attribs.alt}
|
||||
title={node.attribs.title}
|
||||
width={node.attribs.width}
|
||||
height={node.attribs.height}
|
||||
lineIndex={isNaN(lineIndex) ? undefined : lineIndex + this.lineOffset}
|
||||
placeholderIndexInLine={indexInLine}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.image-drop {
|
||||
@import "../../../../style/variables.light.scss";
|
||||
border: 3px dashed $dark;
|
||||
|
||||
body.dark & {
|
||||
@import "../../../../style/variables.dark.scss";
|
||||
border-color: $dark;
|
||||
}
|
||||
|
||||
border-radius: 3px;
|
||||
transition: background-color 50ms, color 50ms;
|
||||
|
||||
.altText {
|
||||
text-overflow: ellipsis;
|
||||
flex: 1 1;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||
import './image-placeholder.scss'
|
||||
import { acceptedMimeTypes } from '../../../common/upload-image-mimetypes'
|
||||
import { useOnImageUpload } from './hooks/use-on-image-upload'
|
||||
import { usePlaceholderSizeStyle } from './hooks/use-placeholder-size-style'
|
||||
|
||||
export interface PlaceholderImageFrameProps {
|
||||
alt?: string
|
||||
title?: string
|
||||
width?: string | number
|
||||
height?: string | number
|
||||
lineIndex?: number
|
||||
placeholderIndexInLine?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a placeholder for an actual image with the possibility to upload images via button or drag'n'drop.
|
||||
*
|
||||
* @param alt The alt text of the image. Will be shown in the placeholder
|
||||
* @param title The title text of the image. Will be shown in the placeholder
|
||||
* @param width The width of the placeholder
|
||||
* @param height The height of the placeholder
|
||||
* @param lineIndex The index of the line in the markdown content where the placeholder is defined
|
||||
* @param placeholderIndexInLine The index of the placeholder in the markdown line
|
||||
*/
|
||||
export const ImagePlaceholder: React.FC<PlaceholderImageFrameProps> = ({
|
||||
alt,
|
||||
title,
|
||||
width,
|
||||
height,
|
||||
lineIndex,
|
||||
placeholderIndexInLine
|
||||
}) => {
|
||||
useTranslation()
|
||||
const fileInputReference = useRef<HTMLInputElement>(null)
|
||||
const onImageUpload = useOnImageUpload(lineIndex, placeholderIndexInLine)
|
||||
|
||||
const [showDragStatus, setShowDragStatus] = useState(false)
|
||||
|
||||
const onDropHandler = useCallback(
|
||||
(event: React.DragEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault()
|
||||
if (event?.dataTransfer?.files?.length > 0) {
|
||||
onImageUpload(event.dataTransfer.files[0])
|
||||
}
|
||||
},
|
||||
[onImageUpload]
|
||||
)
|
||||
|
||||
const onDragOverHandler = useCallback((event: React.DragEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault()
|
||||
setShowDragStatus(true)
|
||||
}, [])
|
||||
|
||||
const onDragLeave = useCallback(() => {
|
||||
setShowDragStatus(false)
|
||||
}, [])
|
||||
|
||||
const onChangeHandler = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const fileList = event.target.files
|
||||
if (!fileList || fileList.length < 1) {
|
||||
return
|
||||
}
|
||||
onImageUpload(fileList[0])
|
||||
},
|
||||
[onImageUpload]
|
||||
)
|
||||
|
||||
const uploadButtonClicked = useCallback(() => fileInputReference.current?.click(), [])
|
||||
const containerStyle = usePlaceholderSizeStyle(width, height)
|
||||
|
||||
const containerDragClasses = useMemo(() => (showDragStatus ? 'bg-primary text-white' : 'text-dark'), [showDragStatus])
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`image-drop d-inline-flex flex-column align-items-center ${containerDragClasses} p-1`}
|
||||
style={containerStyle}
|
||||
onDrop={onDropHandler}
|
||||
onDragOver={onDragOverHandler}
|
||||
onDragLeave={onDragLeave}>
|
||||
<input
|
||||
type='file'
|
||||
className='d-none'
|
||||
accept={acceptedMimeTypes}
|
||||
onChange={onChangeHandler}
|
||||
ref={fileInputReference}
|
||||
/>
|
||||
<div className={'align-items-center flex-column justify-content-center flex-fill d-flex'}>
|
||||
<div className={'d-flex flex-column'}>
|
||||
<span className='my-2'>
|
||||
<Trans i18nKey={'editor.embeddings.placeholderImage.placeholderText'} />
|
||||
</span>
|
||||
<span className={'altText'}>{alt ?? title ?? ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button size={'sm'} variant={'primary'} onClick={uploadButtonClicked}>
|
||||
<ForkAwesomeIcon icon={'upload'} fixedWidth={true} className='my-2' />
|
||||
<Trans i18nKey={'editor.embeddings.placeholderImage.upload'} className='my-2' />
|
||||
</Button>
|
||||
</span>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { calculatePlaceholderContainerSize, parseSizeNumber } from './build-placeholder-size-css'
|
||||
|
||||
describe('parseSizeNumber', () => {
|
||||
it('undefined', () => {
|
||||
expect(parseSizeNumber(undefined)).toBe(undefined)
|
||||
})
|
||||
it('zero as number', () => {
|
||||
expect(parseSizeNumber(0)).toBe(0)
|
||||
})
|
||||
it('positive number', () => {
|
||||
expect(parseSizeNumber(234)).toBe(234)
|
||||
})
|
||||
it('negative number', () => {
|
||||
expect(parseSizeNumber(-123)).toBe(-123)
|
||||
})
|
||||
it('zero as string', () => {
|
||||
expect(parseSizeNumber('0')).toBe(0)
|
||||
})
|
||||
it('negative number as string', () => {
|
||||
expect(parseSizeNumber('-123')).toBe(-123)
|
||||
})
|
||||
it('positive number as string', () => {
|
||||
expect(parseSizeNumber('345')).toBe(345)
|
||||
})
|
||||
it('positive number with px as string', () => {
|
||||
expect(parseSizeNumber('456px')).toBe(456)
|
||||
})
|
||||
it('negative number with px as string', () => {
|
||||
expect(parseSizeNumber('-456px')).toBe(-456)
|
||||
})
|
||||
})
|
||||
|
||||
describe('calculatePlaceholderContainerSize', () => {
|
||||
it('width undefined | height undefined', () => {
|
||||
expect(calculatePlaceholderContainerSize(undefined, undefined)).toStrictEqual([500, 200])
|
||||
})
|
||||
it('width 200 | height undefined', () => {
|
||||
expect(calculatePlaceholderContainerSize(200, undefined)).toStrictEqual([200, 80])
|
||||
})
|
||||
it('width undefined | height 100', () => {
|
||||
expect(calculatePlaceholderContainerSize(undefined, 100)).toStrictEqual([250, 100])
|
||||
})
|
||||
it('width "0" | height 0', () => {
|
||||
expect(calculatePlaceholderContainerSize('0', 0)).toStrictEqual([0, 0])
|
||||
})
|
||||
it('width 0 | height "0"', () => {
|
||||
expect(calculatePlaceholderContainerSize(0, '0')).toStrictEqual([0, 0])
|
||||
})
|
||||
it('width -345 | height 234', () => {
|
||||
expect(calculatePlaceholderContainerSize(-345, 234)).toStrictEqual([-345, 234])
|
||||
})
|
||||
it('width 345 | height -234', () => {
|
||||
expect(calculatePlaceholderContainerSize(345, -234)).toStrictEqual([345, -234])
|
||||
})
|
||||
it('width "-345" | height -234', () => {
|
||||
expect(calculatePlaceholderContainerSize('-345', -234)).toStrictEqual([-345, -234])
|
||||
})
|
||||
it('width -345 | height "-234"', () => {
|
||||
expect(calculatePlaceholderContainerSize(-345, '-234')).toStrictEqual([-345, -234])
|
||||
})
|
||||
})
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const regex = /^(-?[0-9]+)px$/
|
||||
|
||||
/**
|
||||
* Inspects the given value and checks if it is a number or a pixel size string.
|
||||
*
|
||||
* @param value The value to check
|
||||
* @return the number representation of the string or undefined if it couldn't be parsed
|
||||
*/
|
||||
export const parseSizeNumber = (value: string | number | undefined): number | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
}
|
||||
|
||||
const regexMatches = regex.exec(value)
|
||||
if (regexMatches !== null) {
|
||||
if (regexMatches && regexMatches.length > 1) {
|
||||
return parseInt(regexMatches[1])
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (!Number.isNaN(value)) {
|
||||
return parseInt(value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the final width and height for a placeholder container.
|
||||
* Every parameter that is empty will be defaulted using a 500:200 ratio.
|
||||
*
|
||||
* @param width The wanted width
|
||||
* @param height The wanted height
|
||||
* @return the calculated size
|
||||
*/
|
||||
export const calculatePlaceholderContainerSize = (
|
||||
width: string | number | undefined,
|
||||
height: string | number | undefined
|
||||
): [width: number, height: number] => {
|
||||
const defaultWidth = 500
|
||||
const defaultHeight = 200
|
||||
const ratio = defaultWidth / defaultHeight
|
||||
|
||||
const convertedWidth = parseSizeNumber(width)
|
||||
const convertedHeight = parseSizeNumber(height)
|
||||
|
||||
if (convertedWidth === undefined && convertedHeight !== undefined) {
|
||||
return [convertedHeight * ratio, convertedHeight]
|
||||
} else if (convertedWidth !== undefined && convertedHeight === undefined) {
|
||||
return [convertedWidth, convertedWidth * (1 / ratio)]
|
||||
} else if (convertedWidth !== undefined && convertedHeight !== undefined) {
|
||||
return [convertedWidth, convertedHeight]
|
||||
} else {
|
||||
return [defaultWidth, defaultHeight]
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ import type MarkdownIt from 'markdown-it'
|
|||
export class LinemarkerMarkdownExtension extends MarkdownExtension {
|
||||
public static readonly tagName = 'app-linemarker'
|
||||
|
||||
constructor(private onLineMarkers?: (lineMarkers: LineMarkers[]) => void, private lineOffset?: number) {
|
||||
constructor(private lineOffset: number, private onLineMarkers?: (lineMarkers: LineMarkers[]) => void) {
|
||||
super()
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import markdownItTaskLists from '@hedgedoc/markdown-it-task-lists'
|
|||
* Adds support for interactive checkbox lists to the markdown rendering using the github checklist syntax.
|
||||
*/
|
||||
export class TaskListMarkdownExtension extends MarkdownExtension {
|
||||
constructor(private frontmatterLinesToSkip?: number, private onTaskCheckedChange?: TaskCheckedChangeHandler) {
|
||||
constructor(private frontmatterLinesToSkip: number, private onTaskCheckedChange?: TaskCheckedChangeHandler) {
|
||||
super()
|
||||
}
|
||||
|
||||
|
|
|
@ -18,10 +18,10 @@ export type TaskCheckedChangeHandler = (lineInMarkdown: number, checked: boolean
|
|||
export class TaskListReplacer extends ComponentReplacer {
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
|
||||
constructor(frontmatterLinesToSkip?: number, onTaskCheckedChange?: TaskCheckedChangeHandler) {
|
||||
constructor(frontmatterLinesToSkip: number, onTaskCheckedChange?: TaskCheckedChangeHandler) {
|
||||
super()
|
||||
this.onTaskCheckedChange = (lineInMarkdown, checked) => {
|
||||
if (onTaskCheckedChange === undefined || frontmatterLinesToSkip === undefined) {
|
||||
if (onTaskCheckedChange === undefined) {
|
||||
return
|
||||
}
|
||||
onTaskCheckedChange(frontmatterLinesToSkip + lineInMarkdown, checked)
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { usePlaceholderSizeStyle } from '../image-placeholder/hooks/use-placeholder-size-style'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
export interface UploadIndicatingFrameProps {
|
||||
width?: string | number
|
||||
height?: string | number
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a placeholder frame for images that are currently uploaded.
|
||||
*
|
||||
* @param width The frame width
|
||||
* @param height The frame height
|
||||
*/
|
||||
export const UploadIndicatingFrame: React.FC<UploadIndicatingFrameProps> = ({ width, height }) => {
|
||||
const containerStyle = usePlaceholderSizeStyle(width, height)
|
||||
useTranslation()
|
||||
|
||||
return (
|
||||
<span
|
||||
className='image-drop d-inline-flex flex-column align-items-center justify-content-center bg-primary text-white p-4'
|
||||
style={containerStyle}>
|
||||
<span className={'h1 border-bottom-0 my-2'}>
|
||||
<Trans i18nKey={'renderer.uploadIndicator.uploadMessage'} />
|
||||
</span>
|
||||
<ForkAwesomeIcon icon={'cog'} size={'5x'} fixedWidth={true} className='my-2 fa-spin' />
|
||||
</span>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { MarkdownExtension } from '../markdown-extension'
|
||||
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||
import { UploadIndicatingImageFrameReplacer } from './upload-indicating-image-frame-replacer'
|
||||
|
||||
/**
|
||||
* A markdown extension that shows {@link UploadIndicatingFrame} for images that are getting uploaded.
|
||||
*/
|
||||
export class UploadIndicatingImageFrameMarkdownExtension extends MarkdownExtension {
|
||||
buildReplacers(): ComponentReplacer[] {
|
||||
return [new UploadIndicatingImageFrameReplacer()]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { NativeRenderer, NodeReplacement, SubNodeTransform } from '../../replace-components/component-replacer'
|
||||
import { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||
import type { Element } from 'domhandler'
|
||||
import { UploadIndicatingFrame } from './upload-indicating-frame'
|
||||
|
||||
const uploadIdRegex = /^upload-(.+)$/
|
||||
|
||||
/**
|
||||
* Replaces an image tag whose url is an upload-id with the {@link UploadIndicatingFrame upload indicating frame}.
|
||||
*/
|
||||
export class UploadIndicatingImageFrameReplacer extends ComponentReplacer {
|
||||
replace(node: Element, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): NodeReplacement {
|
||||
if (node.name === 'img' && uploadIdRegex.test(node.attribs.src)) {
|
||||
return <UploadIndicatingFrame width={node.attribs.width} height={node.attribs.height} />
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,6 +49,13 @@ export abstract class ComponentReplacer {
|
|||
return node.children.map((value, index) => subNodeTransform(value, index))
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be used to reset the replacers internal state before rendering.
|
||||
*/
|
||||
public reset(): void {
|
||||
// left blank for overrides
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current node should be altered or replaced and does if needed.
|
||||
*
|
||||
|
|
|
@ -45,7 +45,7 @@ export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps
|
|||
baseUrl,
|
||||
undefined,
|
||||
useMemo(() => [new RevealMarkdownExtension()], []),
|
||||
lineOffset,
|
||||
lineOffset ?? 0,
|
||||
onTaskCheckedChange,
|
||||
onImageClick,
|
||||
onTocChange
|
||||
|
|
|
@ -31,6 +31,13 @@ export class NodeToReactTransformer {
|
|||
this.replacers = replacers
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all replacers before rendering.
|
||||
*/
|
||||
public resetReplacers(): void {
|
||||
this.replacers.forEach((replacer) => replacer.reset())
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given {@link Node} to a react element.
|
||||
*
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue