diff --git a/.eslintrc b/.eslintrc index 9b117219..ca27ed0f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,6 +8,7 @@ ], "plugins": ["lodash"], "rules": { - "lodash/import-scope": [2, "method"] + "lodash/import-scope": [2, "method"], + "jsx-a11y/no-autofocus": "warn" } } diff --git a/demo/HtmlPreview.tsx b/demo/HtmlPreview.tsx deleted file mode 100644 index 385938ac..00000000 --- a/demo/HtmlPreview.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; - -import {useLatex} from '@diplodoc/latex-extension/react'; -import transform from '@diplodoc/transform'; - -import {MarkupString, colorClassName} from '../src'; -import type {ClassNameProps} from '../src/classname'; - -import {LATEX_RUNTIME, plugins} from './md-plugins'; - -type PlaygroundHtmlPreviewProps = ClassNameProps & { - value: MarkupString; - allowHTML?: boolean; - breaks?: boolean; - linkify?: boolean; - linkifyTlds?: string | string[]; -}; - -type Meta = {script?: string[]; style?: string[]}; - -export const PlaygroundHtmlPreview: React.FC = - function PlaygroundHtmlPreview({value, allowHTML, breaks, linkify, linkifyTlds, className}) { - const divRef = React.useRef(null); - const renderLatex = useLatex(); - - const result = React.useMemo(() => { - return transform(value, { - allowHTML, - breaks, - plugins, - linkify, - linkifyTlds, - defaultClassName: colorClassName, // markdown-it-color - }).result; - }, [allowHTML, breaks, linkify, linkifyTlds, value]); - - // Load katex only if one or more formulas should be rendered - if (((result.meta ?? {}) as Meta).script?.includes(LATEX_RUNTIME)) { - import('@diplodoc/latex-extension/runtime'); - } - if (((result.meta ?? {}) as Meta).style?.includes(LATEX_RUNTIME)) { - // @ts-expect-error - import('@diplodoc/latex-extension/runtime/styles'); - } - - React.useEffect(() => { - renderLatex({throwOnError: false}); - }, [result.html, renderLatex]); - - return ( -
- ); - }; diff --git a/demo/PMSelection.tsx b/demo/PMSelection.tsx index ea3abc96..90699939 100644 --- a/demo/PMSelection.tsx +++ b/demo/PMSelection.tsx @@ -1,14 +1,52 @@ import React from 'react'; -import type {EditorView} from 'prosemirror-view'; +import {EditorView} from 'prosemirror-view'; +import {useEffectOnce, useUpdate} from 'react-use'; -import {ClassNameProps, isNodeSelection, isTextSelection, isWholeSelection} from '../src'; +import {type ClassNameProps, isNodeSelection, isTextSelection, isWholeSelection} from '../src'; +import type {Editor} from '../src/bundle'; -export type PMSelectionProps = ClassNameProps & { +export type WysiwygSelectionProps = ClassNameProps & { + editorRef: React.RefObject; +}; + +export function WysiwygSelection({editorRef, className}: WysiwygSelectionProps) { + const rerender = useUpdate(); + useEffectOnce(() => { + rerender(); + }); + + const editor = editorRef.current; + const view = editor?.currentType === 'wysiwyg' && editor._wysiwygView; + + React.useLayoutEffect(() => { + if (!editor) return undefined; + editor.on( + // @ts-expect-error TODO: add public event for selection change + 'rerender-toolbar', + rerender, + ); + editor.on('change-editor-type', rerender); + return () => { + editor.off( + // @ts-expect-error TODO: add public event for selection change + 'rerender-toolbar', + rerender, + ); + editor.off('change-editor-type', rerender); + }; + }, [editor, rerender]); + + if (!view) return null; + + return ; +} + +type PMSelectionProps = ClassNameProps & { view: EditorView; }; -export function PMSelection({view, className}: PMSelectionProps) { +function PMSelection({view, className}: PMSelectionProps) { const sel = view.state.selection; const renderFromTo = () => ( <> diff --git a/demo/Playground.scss b/demo/Playground.scss index e4474c1e..e24193f3 100644 --- a/demo/Playground.scss +++ b/demo/Playground.scss @@ -15,30 +15,26 @@ @include text-header-2(); } - &__preview-type { + &__version { position: absolute; right: 0; bottom: 0; - } - - &__controls { - display: flex; - align-items: center; - column-gap: 8px; - & > p { - margin: 0; - } + @include text-code-inline-1(); } &__markup { + overflow-y: auto; + + margin: 0; + padding: 5px 10px; + background-color: var(--g-color-base-generic); } &__editor-view { - min-height: 100px; + min-height: 150px; margin: 20px 0; - padding-left: 4px; } &__pm-selection { diff --git a/demo/Playground.stories.tsx b/demo/Playground.stories.tsx index 3392a462..becc665e 100644 --- a/demo/Playground.stories.tsx +++ b/demo/Playground.stories.tsx @@ -1,25 +1,49 @@ import React from 'react'; -import type {ComponentMeta, Story} from '@storybook/react'; // eslint-disable-line import/no-extraneous-dependencies +// eslint-disable-next-line import/no-extraneous-dependencies +import type {ComponentMeta, Story} from '@storybook/react'; import {Playground as PlaygroundComponent, PlaygroundProps} from './Playground'; +import {parseLocation} from './location'; import {initialMdContent} from './md-content'; export default { title: 'YFM Editor', + component: PlaygroundComponent, } as ComponentMeta; type PlaygroundStoryProps = Pick< PlaygroundProps, - 'breaks' | 'allowHTML' | 'linkify' | 'linkifyTlds' + | 'initialEditor' + | 'settingsVisible' + | 'breaks' + | 'allowHTML' + | 'linkify' + | 'linkifyTlds' + | 'sanitizeHtml' + | 'prepareRawMarkup' + | 'splitModeOrientation' + | 'stickyToolbar' + | 'initialSplitModeEnabled' + | 'renderPreviewDefined' + | 'height' >; export const Playground: Story = (props) => ( - + ); Playground.args = { + initialEditor: 'wysiwyg', + settingsVisible: true, allowHTML: true, breaks: true, linkify: true, linkifyTlds: [], + sanitizeHtml: false, + prepareRawMarkup: false, + splitModeOrientation: 'horizontal', + stickyToolbar: true, + initialSplitModeEnabled: false, + renderPreviewDefined: true, + height: 'initial', }; diff --git a/demo/Playground.tsx b/demo/Playground.tsx index 0e1f077e..9a471351 100644 --- a/demo/Playground.tsx +++ b/demo/Playground.tsx @@ -1,247 +1,272 @@ -import React from 'react'; +import React, {CSSProperties, useCallback} from 'react'; -import {Button, RadioButton, TextArea} from '@gravity-ui/uikit'; -import block from 'bem-cn-lite'; // eslint-disable-line import/no-extraneous-dependencies -import {useUpdate} from 'react-use'; +import {Button, DropdownMenu} from '@gravity-ui/uikit'; +import {toaster} from '@gravity-ui/uikit/toaster-singleton-react-18'; +import {MarkupString, logger} from '../src'; import { - BaseNode, - BasePreset, - BehaviorPreset, - Extension, - FlexToolbar, - MarkdownBlocksPreset, - MarkdownMarksPreset, - MarkupString, - ReactRenderStorage, - ReactRendererComponent, - YfmEditorComponent, - YfmPreset, - logger, - useYfmEditor, -} from '../src'; + YfmEditor, + YfmEditorProps, + YfmEditorRef, + YfmEditorType, + markupToolbarConfigs, + wysiwygToolbarConfigs, +} from '../src/bundle'; +import {RenderPreview} from '../src/bundle/Editor'; import {Math} from '../src/extensions/yfm/Math'; -import {wHiddenData, wToolbarConfig} from '../src/toolbar/config/wysiwyg'; +import {Mermaid} from '../src/extensions/yfm/Mermaid'; +import {cloneDeep} from '../src/lodash'; +import type {FileUploadHandler} from '../src/utils/upload'; +import {VERSION} from '../src/version'; -import {PlaygroundHtmlPreview} from './HtmlPreview'; -import {PMSelection} from './PMSelection'; -import {ProseMirrorDevTools} from './ProseMirrorDevTools'; -import {keys} from './keys'; +import {WysiwygSelection} from './PMSelection'; +import {WysiwygDevTools} from './ProseMirrorDevTools'; +import {SplitModePreview} from './SplitModePreview'; +import {block} from './cn'; +import {randomDelay} from './delay'; +import {debouncedUpdateLocation as updateLocation} from './location'; +import {plugins} from './md-plugins'; import './Playground.scss'; const b = block('playground'); +const onAction: YfmEditorProps['onMenuBarAction'] = ({action, editorType}) => { + console.info(`The '${action}' action is performed in the ${editorType}-editor.`); +}; + +const mToolbarConfig = [ + ...markupToolbarConfigs.mToolbarConfig, + [markupToolbarConfigs.mMermaidButton], +]; +mToolbarConfig[2].push(markupToolbarConfigs.mMathListItem); + +const wToolbarConfig = cloneDeep(wysiwygToolbarConfigs.wToolbarConfig); +wToolbarConfig[2].push(wysiwygToolbarConfigs.wMathListItem); + +const wCommandMenuConfig = wysiwygToolbarConfigs.wCommandMenuConfig.concat( + wysiwygToolbarConfigs.wMathInlineItemData, + wysiwygToolbarConfigs.wMathBlockItemData, + wysiwygToolbarConfigs.wMermaidItemData, +); export type PlaygroundProps = { initial?: MarkupString; allowHTML?: boolean; + settingsVisible?: boolean; + initialEditor?: YfmEditorType; breaks?: boolean; linkify?: boolean; linkifyTlds?: string | string[]; + sanitizeHtml?: boolean; + prepareRawMarkup?: boolean; + splitModeOrientation?: 'horizontal' | 'vertical' | false; + stickyToolbar?: boolean; + initialSplitModeEnabled?: boolean; + renderPreviewDefined?: boolean; + height?: CSSProperties['height']; }; -const enum PreviewType { - Markup = 'markup', - Html = 'html', -} - logger.setLogger({ metrics: console.info, - action: console.info, + action: (data) => console.info(`Action: ${data.action}`, data), ...console, }); -const Playground = React.memo((props) => { - const {initial, allowHTML, breaks, linkify, linkifyTlds} = props; - const [previewType, setPreviewType] = React.useState(PreviewType.Markup); - const [yfmRaw, setYfmRaw] = React.useState(initial || ''); - const rerender = useUpdate(); - - const renderStorage = React.useMemo(() => new ReactRenderStorage(), []); - const extensions = React.useMemo( - () => (builder) => - builder - .use(BasePreset, { - baseSchema: { - paragraphPlaceholder(_node, parent) { - return parent?.type.name === BaseNode.Doc && parent.childCount === 1 - ? 'Now... start typing' - : null; - }, - }, - }) - .use(BehaviorPreset, { - history: { - undoKey: keys.undo, - redoKey: keys.redo, - }, - reactRenderer: renderStorage, - }) - .use(MarkdownBlocksPreset, { - image: false, - heading: false, - lists: { - ulKey: keys.ulist, - olKey: keys.olist, - }, - breaks: {preferredBreak: breaks ? 'soft' : 'hard'}, - }) - .use(MarkdownMarksPreset, { - bold: {boldKey: keys.bold}, - italic: {italicKey: keys.italic}, - strike: {strikeKey: keys.strike}, - underline: {underlineKey: keys.underline}, - code: {codeKey: keys.code}, - }) - .use(YfmPreset, {}) - .use(Math, { - loadRuntimeScript: async () => { - await Promise.all([ - import('@diplodoc/latex-extension/runtime'), // @ts-expect-error - import('@diplodoc/latex-extension/runtime/styles'), - ]); - }, - }), - [breaks, renderStorage], - ); - - const editor = useYfmEditor({ +export const Playground = React.memo((props) => { + const { + initial, + initialEditor, + initialSplitModeEnabled, + settingsVisible, + allowHTML, + breaks, linkify, linkifyTlds, - allowHTML, - extensions, - initialContent: yfmRaw, - onChange: () => rerender(), - onDocChange: (e) => setYfmRaw(e.getValue()), - }); + sanitizeHtml, + prepareRawMarkup, + splitModeOrientation, + stickyToolbar, + renderPreviewDefined, + height, + } = props; + const [editorType, setEditorType] = React.useState(initialEditor ?? 'wysiwyg'); + const [yfmRaw, setYfmRaw] = React.useState(initial || ''); + const editorRef = React.useRef(null); + + React.useEffect(() => { + updateLocation(yfmRaw); + }, [yfmRaw]); + + React.useEffect(() => { + console.log('[Playground] YfmEditor domElem:', editorRef.current?.domElem()); + }, []); + + const renderPreview = useCallback( + ({getValue}) => ( + + ), + [allowHTML, breaks, linkify, linkifyTlds, sanitizeHtml], + ); return (
YFM Editor Playground - - - Markup - HTML - - + {VERSION}
-
-

isEmpty: {String(editor.isEmpty())}

- + } + > + { + editorRef.current?.clear(); + editorRef.current?.focus(); }} - > - Clear - - - - - - -
-
-
- editor.focus()} - data={wToolbarConfig} - hiddenActions={wHiddenData} /> - - - - - -
+ { + editorRef.current?.moveCursor({line: 115}); + editorRef.current?.focus(); + }} + /> + +
+ +
+ setYfmRaw(e.getValue())} + onEditorTypeChange={setEditorType} + onMenuBarAction={onAction} + onFileUpload={fileUploadHandler} + settingsVisible={settingsVisible} + onSplitModeEnabledChange={(splitModeEnabled) => { + console.log(`Split mode enabled: ${splitModeEnabled}`); + }} + onMenuVisibleChange={(isMenuVisible) => { + console.log('Menubar visible: ' + isMenuVisible); + }} + prepareRawMarkup={ + prepareRawMarkup + ? (value) => '**prepare raw markup**\n\n' + value + : undefined + } + onCancel={() => { + alert('Editor: cancel'); + return true; + }} + onSubmit={() => { + alert('Editor: submit'); + return true; + }} + extensionOptions={{ + commandMenu: {actions: wCommandMenuConfig}, + }} + wysiwygExtraExtensions={(builder) => + builder + .use(Math, { + loadRuntimeScript: () => { + import( + /* webpackChunkName: "latex-runtime" */ '@diplodoc/latex-extension/runtime' + ); + import( + // @ts-expect-error // no types for styles + /* webpackChunkName: "latex-styles" */ '@diplodoc/latex-extension/runtime/styles' + ); + }, + }) + .use(Mermaid, { + loadRuntimeScript: () => { + import( + /* webpackChunkName: "mermaid-runtime" */ '@diplodoc/mermaid-extension/runtime' + ); + }, + }) + } + /> + + +
+

- {previewType === PreviewType.Markup && ( -