Feature/lazy load components (#590)

This commit is contained in:
mrdrogdrog 2020-09-26 09:54:17 +02:00 committed by GitHub
parent 9c38655a92
commit 101292da92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 261 additions and 248 deletions

View file

@ -1,5 +1,5 @@
import { RegisterError } from '../../components/register-page/register-page'
import { expectResponseCode, getApiUrl, defaultFetchConfig } from '../utils'
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
export const doInternalLogin = async (username: string, password: string): Promise<void> => {
const response = await fetch(getApiUrl() + '/auth/internal', {

View file

@ -1,4 +1,4 @@
import { expectResponseCode, getApiUrl, defaultFetchConfig } from '../utils'
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
import { Config } from './types'
export const getConfig = async (): Promise<Config> => {

View file

@ -1,6 +1,5 @@
import { expectResponseCode, getApiUrl, defaultFetchConfig } from '../utils'
import { HistoryEntry } from '../../components/history-page/history-page'
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
export const getHistory = async (): Promise<HistoryEntry[]> => {
const response = await fetch(getApiUrl() + '/history')

View file

@ -1,5 +1,5 @@
import { UserResponse } from '../users/types'
import { expectResponseCode, getApiUrl, defaultFetchConfig } from '../utils'
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
export const getMe = async (): Promise<UserResponse> => {
const response = await fetch(getApiUrl() + '/me', {

View file

@ -1,5 +1,5 @@
import { ImageProxyResponse } from '../../components/markdown-renderer/replace-components/image/types'
import { expectResponseCode, getApiUrl, defaultFetchConfig } from '../utils'
import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils'
export const getProxiedUrl = async (imageUrl: string): Promise<ImageProxyResponse> => {
const response = await fetch(getApiUrl() + '/media/proxy', {

View file

@ -1,8 +1,7 @@
import React, { Fragment, useCallback, useEffect, useState } from 'react'
import React, { Suspense, useCallback, useEffect, useState } from 'react'
import { useLocation } from 'react-router'
import './application-loader.scss'
import { createSetUpTaskList, InitTask } from './initializers'
import { LoadingScreen } from './loading-screen'
export const ApplicationLoader: React.FC = ({ children }) => {
@ -33,9 +32,13 @@ export const ApplicationLoader: React.FC = ({ children }) => {
}
}, [initTasks, runTask])
return (
doneTasks < initTasks.length || initTasks.length === 0
? <LoadingScreen failedTitle={failedTitle}/>
: <Fragment>{children}</Fragment>
)
const tasksAreRunning = doneTasks < initTasks.length || initTasks.length === 0
if (tasksAreRunning) {
return <LoadingScreen failedTitle={failedTitle}/>
} else {
return <Suspense fallback={(<LoadingScreen/>)}>
{children}
</Suspense>
}
}

View file

@ -4,7 +4,7 @@ import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
import { ShowIf } from '../common/show-if/show-if'
export interface LoadingScreenProps {
failedTitle: string
failedTitle?: string
}
export const LoadingScreen: React.FC<LoadingScreenProps> = ({ failedTitle }) => {
@ -14,7 +14,7 @@ export const LoadingScreen: React.FC<LoadingScreenProps> = ({ failedTitle }) =>
<ForkAwesomeIcon icon="file-text" size="5x"
className={failedTitle ? 'animation-shake' : 'animation-pulse'}/>
</div>
<ShowIf condition={ failedTitle !== ''}>
<ShowIf condition={ !!failedTitle}>
<Alert variant={'danger'}>
The task '{failedTitle}' failed.<br/>
For further information look into the browser console.

View file

@ -1,4 +1,3 @@
import equal from 'fast-deep-equal'
import React from 'react'
import { useSelector } from 'react-redux'

View file

@ -1,4 +1,3 @@
import 'fork-awesome/css/fork-awesome.min.css'
import React from 'react'
import { IconName, IconSize } from './types'

View file

@ -1,8 +1,8 @@
import React from 'react'
import { Button, ButtonProps } from 'react-bootstrap'
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
import './icon-button.scss'
import { IconName } from '../fork-awesome/types'
import './icon-button.scss'
export interface IconButtonProps extends ButtonProps {
icon: IconName

View file

@ -1,6 +1,5 @@
import React from 'react'
import { Trans } from 'react-i18next'
import './icon-button.scss'
import { IconButton, IconButtonProps } from './icon-button'
export interface TranslatedIconButton extends IconButtonProps {

View file

@ -1,5 +1,5 @@
import React from 'react'
import { Modal, Button } from 'react-bootstrap'
import { Button, Modal } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { CommonModal, CommonModalProps } from './common-modal'

View file

@ -1,9 +1,9 @@
import equal from 'fast-deep-equal'
import React from 'react'
import { Alert, Button } from 'react-bootstrap'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { ApplicationState } from '../../../redux'
import { Alert, Button } from 'react-bootstrap'
import { setBanner } from '../../../redux/banner/methods'
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
import { ShowIf } from '../show-if/show-if'

View file

@ -9,12 +9,12 @@ import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { ShowIf } from '../../common/show-if/show-if'
import { SignInButton } from '../../landing-layout/navigation/sign-in-button'
import { UserDropdown } from '../../landing-layout/navigation/user-dropdown'
import { SyncScrollButtons } from './sync-scroll-buttons/sync-scroll-buttons'
import { EditorPathParams } from '../editor'
import { DarkModeButton } from './dark-mode-button'
import { EditorViewMode } from './editor-view-mode'
import { HelpButton } from './help-button/help-button'
import { NavbarBranding } from './navbar-branding'
import { SyncScrollButtons } from './sync-scroll-buttons/sync-scroll-buttons'
export const AppBar: React.FC = () => {
const { t } = useTranslation()

View file

@ -2,10 +2,10 @@ import React from 'react'
import { Col, Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import links from '../../../../links.json'
import { ApplicationState } from '../../../../redux'
import { TranslatedExternalLink } from '../../../common/links/translated-external-link'
import { TranslatedInternalLink } from '../../../common/links/translated-internal-link'
import links from '../../../../links.json'
export const Links: React.FC = () => {
useTranslation()

View file

@ -1,19 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="512"
height="512"
viewBox="0 0 135.46666 135.46666"
version="1.1"
id="svg8"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="buttonIcon.svg">
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="512"
height="512"
viewBox="0 0 135.46666 135.46666"
version="1.1"
id="svg8"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="buttonIcon.svg">
<defs
id="defs2" />
<sodipodi:namedview

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View file

@ -1,8 +1,8 @@
import React from 'react'
import { Dropdown } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import links from '../../../../links.json'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
const ExportMenu: React.FC = () => {
useTranslation()

View file

@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from 'react'
import { Alert, Col, ListGroup, Modal, Row, Button } from 'react-bootstrap'
import { Alert, Button, Col, ListGroup, Modal, Row } from 'react-bootstrap'
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'

View file

@ -1,35 +1,38 @@
import { Editor, Hint, Hints, Pos } from 'codemirror'
import hljs from 'highlight.js'
import { findWordAtCursor, Hinter, search } from './index'
const allowedChars = /[`\w-_+]/
const wordRegExp = /^```((\w|-|_|\+)*)$/
const allSupportedLanguages = hljs.listLanguages().concat('csv', 'flow', 'html')
let allSupportedLanguages: string[] = []
const codeBlockHint = (editor: Editor): Promise< Hints| null > => {
return new Promise((resolve) => {
const searchTerm = findWordAtCursor(editor, allowedChars)
const searchResult = wordRegExp.exec(searchTerm.text)
if (searchResult === null) {
resolve(null)
return
}
const term = searchResult[1]
const suggestions = search(term, allSupportedLanguages)
const cursor = editor.getCursor()
if (!suggestions) {
resolve(null)
} else {
resolve({
list: suggestions.map((suggestion: string): Hint => ({
text: '```' + suggestion + '\n\n```\n',
displayText: suggestion
})),
from: Pos(cursor.line, searchTerm.start),
to: Pos(cursor.line, searchTerm.end)
})
}
})
return import(/* webpackChunkName: "highlight.js" */ 'highlight.js').then(hljs =>
new Promise((resolve) => {
const searchTerm = findWordAtCursor(editor, allowedChars)
const searchResult = wordRegExp.exec(searchTerm.text)
if (searchResult === null) {
resolve(null)
return
}
const term = searchResult[1]
if (allSupportedLanguages.length === 0) {
allSupportedLanguages = hljs.listLanguages().concat('csv', 'flow', 'html')
}
const suggestions = search(term, allSupportedLanguages)
const cursor = editor.getCursor()
if (!suggestions) {
resolve(null)
} else {
resolve({
list: suggestions.map((suggestion: string): Hint => ({
text: '```' + suggestion + '\n\n```\n',
displayText: suggestion
})),
from: Pos(cursor.line, searchTerm.start),
to: Pos(cursor.line, searchTerm.end)
})
}
}))
}
export const CodeBlockHinter: Hinter = {

View file

@ -1,8 +1,8 @@
@import '../../../../node_modules/codemirror/lib/codemirror.css';
@import '../../../../node_modules/codemirror/addon/display/fullscreen.css';
@import '../../../../node_modules/codemirror/addon/dialog/dialog.css';
@import '../../../../node_modules/codemirror/theme/neat.css';
@import './one-dark.css';
@import '../../../../node_modules/codemirror/lib/codemirror';
@import '../../../../node_modules/codemirror/addon/display/fullscreen';
@import '../../../../node_modules/codemirror/addon/dialog/dialog';
@import '../../../../node_modules/codemirror/theme/neat';
@import './one-dark';
@import 'hints';
.CodeMirror {

View file

@ -3,8 +3,8 @@ import React, { Fragment, useState } from 'react'
import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
import { EmojiPicker } from './emoji-picker'
import { addEmoji } from '../utils/toolbarButtonUtils'
import { EmojiPicker } from './emoji-picker'
export interface EmojiPickerButtonProps {
editor: CodeMirror.Editor

View file

@ -1,3 +1,5 @@
@import '../../../../../../node_modules/emoji-mart/css/emoji-mart';
.emoji-mart {
position: absolute;
z-index: 10000;

View file

@ -1,12 +1,11 @@
import { CustomEmoji, Data, EmojiData, NimblePicker } from 'emoji-mart'
import 'emoji-mart/css/emoji-mart.css'
import emojiData from 'emoji-mart/data/twitter.json'
import React, { useRef } from 'react'
import { useClickAway } from 'react-use'
import { ShowIf } from '../../../../common/show-if/show-if'
import './emoji-picker.scss'
import { ForkAwesomeIcons } from './icon-names'
import forkawesomeIcon from './forkawesome.png'
import { ForkAwesomeIcons } from './icon-names'
export interface EmojiPickerProps {
show: boolean

View file

@ -145,3 +145,5 @@ export const Editor: React.FC = () => {
</Fragment>
)
}
export default Editor

View file

@ -1,20 +1,5 @@
.history-menu {
.fa, &::after {
opacity: 0.5;
}
&:hover .fa, &:hover::after {
opacity: 1;
}
&.btn {
padding: 0.6rem 0.65rem;
}
}
.dropup .dropdown-toggle, .dropdown-toggle {
&.no-arrow::after {
content: initial;
}
}

View file

@ -18,7 +18,7 @@ export interface EntryMenuProps {
className?: string
}
const EntryMenu: React.FC<EntryMenuProps> = ({ id, title, location, isDark, onRemove, onDelete, className }) => {
export const EntryMenu: React.FC<EntryMenuProps> = ({ id, title, location, isDark, onRemove, onDelete, className }) => {
useTranslation()
return (
@ -54,5 +54,3 @@ const EntryMenu: React.FC<EntryMenuProps> = ({ id, title, location, isDark, onRe
</Dropdown>
)
}
export { EntryMenu }

View file

@ -189,33 +189,33 @@ export const HistoryPage: React.FC = () => {
sortAndFilterEntries(allEntries, toolbarState),
[allEntries, toolbarState])
return (
<Fragment>
<ErrorModal show={error !== ''} onHide={resetError}
titleI18nKey={error !== '' ? `landing.history.error.${error}.title` : ''}>
<h5>
<Trans i18nKey={error !== '' ? `landing.history.error.${error}.text` : ''}/>
</h5>
</ErrorModal>
<h1 className="mb-4"><Trans i18nKey="landing.navigation.history"/></h1>
<Row className={'justify-content-center mt-5 mb-3'}>
<HistoryToolbar
onSettingsChange={setToolbarState}
tags={tags}
onClearHistory={clearHistory}
onRefreshHistory={refreshHistory}
onExportHistory={exportHistory}
onImportHistory={importHistory}
onUploadAll={uploadAll}
/>
</Row>
<HistoryContent
viewState={toolbarState.viewState}
entries={entriesToShow}
onPinClick={pinClick}
onRemoveClick={removeFromHistoryClick}
onDeleteClick={deleteNoteClick}
return <Fragment>
<ErrorModal show={error !== ''} onHide={resetError}
titleI18nKey={error !== '' ? `landing.history.error.${error}.title` : ''}>
<h5>
<Trans i18nKey={error !== '' ? `landing.history.error.${error}.text` : ''}/>
</h5>
</ErrorModal>
<h1 className="mb-4"><Trans i18nKey="landing.navigation.history"/></h1>
<Row className={'justify-content-center mt-5 mb-3'}>
<HistoryToolbar
onSettingsChange={setToolbarState}
tags={tags}
onClearHistory={clearHistory}
onRefreshHistory={refreshHistory}
onExportHistory={exportHistory}
onImportHistory={importHistory}
onUploadAll={uploadAll}
/>
</Fragment>
)
</Row>
<HistoryContent
viewState={toolbarState.viewState}
entries={entriesToShow}
onPinClick={pinClick}
onRemoveClick={removeFromHistoryClick}
onDeleteClick={deleteNoteClick}
/>
</Fragment>
}
export default HistoryPage

View file

@ -6,26 +6,22 @@ import { CoverButtons } from './cover-buttons/cover-buttons'
import { FeatureLinks } from './feature-links'
import screenshot from './img/screenshot.png'
const IntroPage: React.FC = () => {
export const IntroPage: React.FC = () => {
const { t } = useTranslation()
return (
<Fragment>
<h1 dir='auto' className={'align-items-center d-flex justify-content-center'}>
<ForkAwesomeIcon icon="file-text" className={'mr-2'}/>
<span>HedgeDoc</span>
<Branding/>
</h1>
<p className="lead mb-5">
<Trans i18nKey="app.slogan"/>
</p>
return <Fragment>
<h1 dir='auto' className={'align-items-center d-flex justify-content-center'}>
<ForkAwesomeIcon icon="file-text" className={'mr-2'}/>
<span>HedgeDoc</span>
<Branding/>
</h1>
<p className="lead mb-5">
<Trans i18nKey="app.slogan"/>
</p>
<CoverButtons/>
<CoverButtons/>
<img alt={t('landing.intro.screenShotAltText')} src={screenshot} className="img-fluid mb-5"/>
<FeatureLinks/>
</Fragment>
)
<img alt={t('landing.intro.screenShotAltText')} src={screenshot} className="img-fluid mb-5"/>
<FeatureLinks/>
</Fragment>
}
export { IntroPage }

View file

@ -100,7 +100,7 @@ export interface ViaOneClickProps {
optionalName?: string;
}
const ViaOneClick: React.FC<ViaOneClickProps> = ({ oneClickType, optionalName }) => {
export const ViaOneClick: React.FC<ViaOneClickProps> = ({ oneClickType, optionalName }) => {
const backendUrl = useSelector((state: ApplicationState) => state.apiUrl.apiUrl)
const { name, icon, className, url } = getMetadata(backendUrl, oneClickType)
const text = optionalName || name
@ -116,5 +116,3 @@ const ViaOneClick: React.FC<ViaOneClickProps> = ({ oneClickType, optionalName })
</SocialLinkButton>
)
}
export { ViaOneClick }

View file

@ -1,4 +1,4 @@
import React from 'react'
import React, { Fragment } from 'react'
import { Card, Col, Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
@ -10,6 +10,7 @@ import { ViaLdap } from './auth/via-ldap'
import { OneClickType, ViaOneClick } from './auth/via-one-click'
import { ViaOpenId } from './auth/via-openid'
import equal from 'fast-deep-equal'
export const LoginPage: React.FC = () => {
useTranslation()
const authProviders = useSelector((state: ApplicationState) => state.config.authProviders, equal)
@ -38,7 +39,7 @@ export const LoginPage: React.FC = () => {
)
}
return (
return <Fragment>
<div className="my-3">
<Row className="h-100 flex justify-content-center">
<ShowIf condition={authProviders.internal || authProviders.ldap || authProviders.openid}>
@ -76,5 +77,5 @@ export const LoginPage: React.FC = () => {
</ShowIf>
</Row>
</div>
)
</Fragment>
}

View file

@ -1,19 +1,21 @@
import React, { useEffect, useRef } from 'react'
import { renderAbc } from 'abcjs'
export interface AbcFrameProps {
code: string
}
export const AbcFrame: React.FC<AbcFrameProps> = ({ code }) => {
const container = useRef<HTMLDivElement>(null)
useEffect(() => {
if (container.current) {
renderAbc(container.current, code)
if (!container.current) {
return
}
const actualContainer = container.current
import(/* webpackChunkName: "abc.js" */ 'abcjs').then((imp) => {
imp.renderAbc(actualContainer, code)
}).catch(() => { console.error('error while loading abcjs') })
}, [code])
return (
<div ref={container} className={'bg-white text-center'}/>
)
return <div ref={container} className={'bg-white text-center'}/>
}

View file

@ -1,4 +1,3 @@
import { parse } from 'flowchart.js'
import React, { useEffect, useRef, useState } from 'react'
import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
@ -17,20 +16,21 @@ export const FlowChart: React.FC<FlowChartProps> = ({ code }) => {
if (diagramRef.current === null) {
return
}
const parserOutput = parse(code)
try {
parserOutput.drawSVG(diagramRef.current, {
'line-width': 2,
fill: 'none',
'font-size': '16px',
'font-family': 'Source Code Pro, twemoji, monospace'
})
setError(false)
} catch (error) {
setError(true)
}
const currentDiagramRef = diagramRef.current
import(/* webpackChunkName: "flowchart.js" */ 'flowchart.js').then((imp) => {
const parserOutput = imp.parse(code)
try {
parserOutput.drawSVG(currentDiagramRef, {
'line-width': 2,
fill: 'none',
'font-size': '16px',
'font-family': 'Source Code Pro, twemoji, monospace'
})
setError(false)
} catch (error) {
setError(true)
}
}).catch(() => { console.error('error while loading flowchart.js') })
return () => {
Array.from(currentDiagramRef.children).forEach(value => value.remove())
@ -41,8 +41,8 @@ export const FlowChart: React.FC<FlowChartProps> = ({ code }) => {
return (
<Alert variant={'danger'}>
<Trans i18nKey={'renderer.flowchart.invalidSyntax'}/>
</Alert>
)
</Alert>)
} else {
return <div ref={diagramRef} className={'text-center'}/>
}
return <div ref={diagramRef} className={'text-center'}/>
}

View file

@ -1,7 +1,5 @@
import { graphviz } from 'd3-graphviz'
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { Alert } from 'react-bootstrap'
import '@hpcc-js/wasm'
import { ShowIf } from '../../../common/show-if/show-if'
export interface GraphvizFrameProps {
@ -25,14 +23,18 @@ export const GraphvizFrame: React.FC<GraphvizFrameProps> = ({ code }) => {
if (!container.current) {
return
}
try {
setError(undefined)
graphviz(container.current, { useWorker: false, zoom: false })
.onerror(showError)
.renderDot(code)
} catch (error) {
showError(error)
}
const actualContainer = container.current
Promise.all([import(/* webpackChunkName: "d3-graphviz" */ 'd3-graphviz'), import('@hpcc-js/wasm')]).then(([imp]) => {
try {
setError(undefined)
imp.graphviz(actualContainer, { useWorker: false, zoom: false })
.onerror(showError)
.renderDot(code)
} catch (error) {
showError(error)
}
}).catch(() => { console.error('error while loading graphviz') })
}, [code, error, showError])
return <Fragment>
@ -42,3 +44,5 @@ export const GraphvizFrame: React.FC<GraphvizFrameProps> = ({ code }) => {
<div className={'text-center'} ref={container} />
</Fragment>
}
export default GraphvizFrame

View file

@ -1,5 +1,4 @@
import hljs from 'highlight.js'
import React, { Fragment, useMemo } from 'react'
import React, { Fragment, ReactElement, useEffect, useState } from 'react'
import ReactHtmlParser from 'react-html-parser'
import { CopyToClipboardButton } from '../../../../common/copyable/copy-to-clipboard-button/copy-to-clipboard-button'
import '../../../utils/button-inside.scss'
@ -21,10 +20,6 @@ export const escapeHtml = (unsafe: string): string => {
.replace(/'/g, '&#039;')
}
const checkIfLanguageIsSupported = (language: string): boolean => {
return hljs.listLanguages().includes(language)
}
const correctLanguage = (language: string | undefined): string | undefined => {
switch (language) {
case 'html':
@ -34,29 +29,36 @@ const correctLanguage = (language: string | undefined): string | undefined => {
}
}
const replaceCode = (code: string): ReactElement[][] => {
return code.split('\n')
.filter(line => !!line)
.map(line => ReactHtmlParser(line))
}
export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language, startLineNumber, wrapLines }) => {
const highlightedCode = useMemo(() => {
const replacedLanguage = correctLanguage(language)
return ((!!replacedLanguage && checkIfLanguageIsSupported(replacedLanguage)) ? hljs.highlight(replacedLanguage, code).value : escapeHtml(code))
.split('\n')
.filter(line => !!line)
.map(line => ReactHtmlParser(line))
}, [code, language])
const [dom, setDom] = useState<ReactElement[]>()
useEffect(() => {
import(/* webpackChunkName: "highlight.js" */ 'highlight.js').then((hljs) => {
const correctedLanguage = correctLanguage(language)
const languageSupported = (lang: string) => hljs.listLanguages().includes(lang)
const unreplacedCode = !!correctedLanguage && languageSupported(correctedLanguage) ? hljs.highlight(correctedLanguage, code).value : escapeHtml(code)
const replacedDom = replaceCode(unreplacedCode).map((line, index) => (
<Fragment key={index}>
<span className={'linenumber'} data-line-number={(startLineNumber || 1) + index}/>
<div className={'codeline'}>
{line}
</div>
</Fragment>
))
setDom(replacedDom)
}).catch(() => { console.error('error while loading highlight.js') })
}, [code, language, startLineNumber])
return (
<Fragment>
<code className={`hljs ${startLineNumber !== undefined ? 'showGutter' : ''} ${wrapLines ? 'wrapLines' : ''}`}>
{
highlightedCode
.map((line, index) => (
<Fragment key={index}>
<span className={'linenumber'} data-line-number={(startLineNumber || 1) + index}/>
<div className={'codeline'}>
{line}
</div>
</Fragment>
))
}
{ dom }
</code>
<div className={'text-right button-inside'}>
<CopyToClipboardButton content={code}/>

View file

@ -1,8 +1,7 @@
import { DomElement } from 'domhandler'
import React from 'react'
import 'katex/dist/katex.min.css'
import TeX from '@matejmazur/react-katex'
import { ComponentReplacer } from '../ComponentReplacer'
import './katex.scss'
const getNodeIfKatexBlock = (node: DomElement): (DomElement|undefined) => {
if (node.name !== 'p' || !node.children || node.children.length === 0) {
@ -17,13 +16,15 @@ const getNodeIfInlineKatex = (node: DomElement): (DomElement|undefined) => {
return (node.name === 'app-katex' && node.attribs?.inline !== undefined) ? node : undefined
}
const KaTeX = React.lazy(() => import(/* webpackChunkName: "katex" */ '@matejmazur/react-katex'))
export class KatexReplacer extends ComponentReplacer {
public getReplacement (node: DomElement): React.ReactElement | undefined {
const katex = getNodeIfKatexBlock(node) || getNodeIfInlineKatex(node)
if (katex?.children && katex.children[0]) {
const mathJaxContent = katex.children[0]?.data as string
const isInline = (katex.attribs?.inline) !== undefined
return <TeX block={!isInline} math={mathJaxContent} errorColor={'#cc0000'}/>
return <KaTeX block={!isInline} math={mathJaxContent} errorColor={'#cc0000'}/>
}
}
}

View file

@ -0,0 +1 @@
@import '../../../../../node_modules/katex/dist/katex.min';

View file

@ -1,5 +1,3 @@
import { transform } from 'markmap-lib/dist/transform'
import { Markmap } from 'markmap-lib/dist/view'
import React, { useEffect, useRef } from 'react'
export interface MarkmapFrameProps {
@ -13,12 +11,16 @@ export const MarkmapFrame: React.FC<MarkmapFrameProps> = ({ code }) => {
if (!diagramContainer.current) {
return
}
const svg: SVGSVGElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svg.setAttribute('width', '100%')
diagramContainer.current.querySelectorAll('svg').forEach(child => child.remove())
diagramContainer.current.appendChild(svg)
const data = transform(code)
Markmap.create(svg, {}, data)
const actualContainer = diagramContainer.current
Promise.all([import(/* webpackChunkName: "markmap" */ 'markmap-lib/dist/transform'), import(/* webpackChunkName: "markmap" */ 'markmap-lib/dist/view')])
.then(([transform, view]) => {
const svg: SVGSVGElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svg.setAttribute('width', '100%')
actualContainer.querySelectorAll('svg').forEach(child => child.remove())
actualContainer.appendChild(svg)
const data = transform.transform(code)
view.Markmap.create(svg, {}, data)
}).catch(() => { console.error('error while loading markmap') })
}, [code])
return <div className={'text-center'} ref={diagramContainer}/>

View file

@ -1,7 +1,7 @@
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { Alert } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import embed, { VisualizationSpec } from 'vega-embed'
import { VisualizationSpec } from 'vega-embed'
import { ShowIf } from '../../../common/show-if/show-if'
export interface VegaChartProps {
@ -25,27 +25,31 @@ export const VegaChart: React.FC<VegaChartProps> = ({ code }) => {
if (!diagramContainer.current) {
return
}
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const spec: VisualizationSpec = JSON.parse(code)
showError('')
embed(diagramContainer.current, spec, {
actions: {
export: true,
source: false,
compiled: false,
editor: false
},
i18n: {
PNG_ACTION: t('renderer.vega-lite.png'),
SVG_ACTION: t('renderer.vega-lite.svg')
import(/* webpackChunkName: "vega" */ 'vega-embed').then((embed) => {
try {
if (!diagramContainer.current) {
return
}
})
.then(result => console.log(result))
.catch(err => showError(err))
} catch (err) {
showError(t('renderer.vega-lite.errorJson'))
}
const spec = JSON.parse(code) as VisualizationSpec
embed.default(diagramContainer.current, spec, {
actions: {
export: true,
source: false,
compiled: false,
editor: false
},
i18n: {
PNG_ACTION: t('renderer.vega-lite.png'),
SVG_ACTION: t('renderer.vega-lite.svg')
}
})
.then(() => showError(''))
.catch(err => showError(err))
} catch (err) {
showError(t('renderer.vega-lite.errorJson'))
}
}).catch(() => { console.error('error while loading vega-light') })
}, [code, showError, t])
return <Fragment>

View file

@ -1,5 +1,5 @@
import { DomElement } from 'domhandler'
import React, { Fragment, ReactElement } from 'react'
import React, { ReactElement, Suspense } from 'react'
import { convertNodeToElement, Transform } from 'react-html-parser'
import {
ComponentReplacer,
@ -69,7 +69,9 @@ export const buildTransformer = (lineKeys: (LineKeys[] | undefined), allReplacer
} else if (tryReplacement === undefined) {
return nativeRenderer(node, key)
} else {
return <Fragment key={key}>{tryReplacement}</Fragment>
return <Suspense key={key} fallback={<span>Loading...</span>}>
{ tryReplacement }
</Suspense>
}
}
return transform

View file

@ -1,4 +1,4 @@
import React from 'react'
import React, { Fragment } from 'react'
import { Col, Row } from 'react-bootstrap'
import { useSelector } from 'react-redux'
import { Redirect } from 'react-router'
@ -18,7 +18,7 @@ export const ProfilePage: React.FC = () => {
)
}
return (
return <Fragment>
<div className="my-3">
<Row className="h-100 flex justify-content-center">
<Col lg={6}>
@ -30,5 +30,5 @@ export const ProfilePage: React.FC = () => {
</Col>
</Row>
</div>
)
</Fragment>
}

View file

@ -1,4 +1,4 @@
import React, { FormEvent, useCallback, useEffect, useState } from 'react'
import React, { FormEvent, Fragment, useCallback, useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { Redirect } from 'react-router'
import { doInternalRegister } from '../../api/auth'
@ -53,7 +53,7 @@ export const RegisterPage: React.FC = () => {
)
}
return (
return <Fragment>
<div className='my-3'>
<h1 className='mb-4'><Trans i18nKey='login.register.title'/></h1>
<Row className='h-100 d-flex justify-content-center'>
@ -138,5 +138,5 @@ export const RegisterPage: React.FC = () => {
</Col>
</Row>
</div>
)
</Fragment>
}

View file

@ -5,7 +5,6 @@ import { BrowserRouter as Router, Redirect, Route, Switch } from 'react-router-d
import { ApplicationLoader } from './components/application-loader/application-loader'
import { NotFoundErrorScreen } from './components/common/routing/not-found-error-screen'
import { Redirector } from './components/common/routing/redirector'
import { Editor } from './components/editor/editor'
import { ErrorBoundary } from './components/error-boundary/error-boundary'
import { HistoryPage } from './components/history-page/history-page'
import { IntroPage } from './components/intro-page/intro-page'
@ -15,8 +14,10 @@ import { ProfilePage } from './components/profile-page/profile-page'
import { RegisterPage } from './components/register-page/register-page'
import { store } from './redux'
import * as serviceWorker from './service-worker'
import './style/index.scss'
import './style/dark.scss'
import './style/index.scss'
const Editor = React.lazy(() => import(/* webpackPrefetch: true */ './components/editor/editor'))
ReactDOM.render(
<Provider store={store}>

View file

@ -1,10 +1,5 @@
import { Reducer } from 'redux'
import {
BannerActions,
BannerActionType,
BannerState,
SetBannerAction
} from './types'
import { BannerActions, BannerActionType, BannerState, SetBannerAction } from './types'
export const initialState: BannerState = {
show: true,

View file

@ -1,5 +1,5 @@
import { Config } from '../../api/config/types'
import { store } from '..'
import { Config } from '../../api/config/types'
import { ConfigActionType, SetConfigAction } from './types'
export const setConfig = (state: Config): void => {

View file

@ -3,6 +3,7 @@
@import '../../node_modules/react-bootstrap-typeahead/css/Typeahead';
@import "fonts/source-code-pro/source-code-pro";
@import "fonts/twemoji/twemoji";
@import '../../node_modules/fork-awesome/css/fork-awesome.min';
.text-black, body.dark .text-black {
color: $black;
@ -56,3 +57,19 @@ body {
.cursor-zoom-out {
cursor: zoom-out;
}
.faded-fa {
.fa, &::after {
opacity: 0.5;
}
&:hover .fa, &:hover::after {
opacity: 1;
}
}
.dropup .dropdown-toggle, .dropdown-toggle {
&.no-arrow::after {
content: initial;
}
}