From 15767d7d3b0334126e34149d811ce6b6d62909d2 Mon Sep 17 00:00:00 2001 From: Alexey Okhrimenko Date: Thu, 19 Dec 2024 16:51:03 +0300 Subject: [PATCH 1/4] feat(markup): smart re-indent on paste (#530) --- src/markup/codemirror/create.ts | 41 ++++-- .../smart-reindent/__tests__/index.test.ts | 122 ++++++++++++++++++ .../smart-reindent/__tests__/utils.test.ts | 27 ++++ src/markup/codemirror/smart-reindent/index.ts | 46 +++++++ src/markup/codemirror/smart-reindent/utils.ts | 63 +++++++++ 5 files changed, 287 insertions(+), 12 deletions(-) create mode 100644 src/markup/codemirror/smart-reindent/__tests__/index.test.ts create mode 100644 src/markup/codemirror/smart-reindent/__tests__/utils.test.ts create mode 100644 src/markup/codemirror/smart-reindent/index.ts create mode 100644 src/markup/codemirror/smart-reindent/utils.ts diff --git a/src/markup/codemirror/create.ts b/src/markup/codemirror/create.ts index 8396cf9b..0272afb8 100644 --- a/src/markup/codemirror/create.ts +++ b/src/markup/codemirror/create.ts @@ -46,6 +46,7 @@ import {MarkdownConverter} from './html-to-markdown/converters'; import {PairingCharactersExtension} from './pairing-chars'; import {ReactRendererFacet} from './react-facet'; import {SearchPanelPlugin} from './search-plugin/plugin'; +import {smartReindent} from './smart-reindent'; import {type YfmLangOptions, yfmLang} from './yfm'; export type {YfmLangOptions}; @@ -162,12 +163,17 @@ export function createCodemirror(params: CreateCodemirrorParams) { paste(event, editor) { if (!event.clipboardData) return; + const {from} = editor.state.selection.main; + const line = editor.state.doc.lineAt(from); + const currentLine = line.text; + // if clipboard contains YFM content - avoid any meddling with pasted content // since text/yfm will contain valid markdown const yfmContent = event.clipboardData.getData(DataTransferType.Yfm); if (yfmContent) { event.preventDefault(); - editor.dispatch(editor.state.replaceSelection(yfmContent)); + const reindentedYfmContent = smartReindent(yfmContent, currentLine); + editor.dispatch(editor.state.replaceSelection(reindentedYfmContent)); return; } @@ -195,7 +201,11 @@ export function createCodemirror(params: CreateCodemirrorParams) { if (parsedMarkdownMarkup !== undefined) { event.preventDefault(); - editor.dispatch(editor.state.replaceSelection(parsedMarkdownMarkup)); + const reindentedHtmlContent = smartReindent( + parsedMarkdownMarkup, + currentLine, + ); + editor.dispatch(editor.state.replaceSelection(reindentedHtmlContent)); return; } } @@ -206,19 +216,26 @@ export function createCodemirror(params: CreateCodemirrorParams) { event.clipboardData.getData(DataTransferType.Text) ?? '', ) || {}; - if (!imageUrl) { - return; + if (imageUrl) { + event.preventDefault(); + + insertImages([ + { + url: imageUrl, + alt: title, + title, + }, + ])(editor); } + } + // Reindenting pasted plain text + const pastedText = event.clipboardData.getData(DataTransferType.Text); + const reindentedText = smartReindent(pastedText, currentLine); + // but only if there is a need for reindentation + if (pastedText !== reindentedText) { + editor.dispatch(editor.state.replaceSelection(reindentedText)); event.preventDefault(); - - insertImages([ - { - url: imageUrl, - alt: title, - title, - }, - ])(editor); } }, }), diff --git a/src/markup/codemirror/smart-reindent/__tests__/index.test.ts b/src/markup/codemirror/smart-reindent/__tests__/index.test.ts new file mode 100644 index 00000000..3c1fdb02 --- /dev/null +++ b/src/markup/codemirror/smart-reindent/__tests__/index.test.ts @@ -0,0 +1,122 @@ +import {smartReindent} from '../index'; + +describe('smartReindent', () => { + // Basic functionality + it('should preserve pasted text when current line is empty', () => { + const pastedText = 'First line\nSecond line'; + const currentLine = ''; + + expect(smartReindent(pastedText, currentLine)).toBe(pastedText); + }); + + it('should preserve pasted text when current line has no markers', () => { + const pastedText = 'First line\nSecond line'; + const currentLine = 'Just plain text'; + + expect(smartReindent(pastedText, currentLine)).toBe(pastedText); + }); + + // List markers + it('should reindent with numeric list markers', () => { + const pastedText = 'First item\nSecond item\nThird item'; + const currentLine = '1. List item'; + + expect(smartReindent(pastedText, currentLine)).toBe( + 'First item\n Second item\n Third item', + ); + }); + + it('should reindent with dash list markers', () => { + const pastedText = 'First item\nSecond item'; + const currentLine = '- List item'; + + expect(smartReindent(pastedText, currentLine)).toBe('First item\n Second item'); + }); + + it('should reindent with asterisk list markers', () => { + const pastedText = 'First item\nSecond item'; + const currentLine = '* List item'; + + expect(smartReindent(pastedText, currentLine)).toBe('First item\n Second item'); + }); + + it('should reindent with plus list markers', () => { + const pastedText = 'First item\nSecond item'; + const currentLine = '+ List item'; + + expect(smartReindent(pastedText, currentLine)).toBe('First item\n Second item'); + }); + + // Edge cases + it('should handle multi-digit numeric markers correctly', () => { + const pastedText = 'First item\nSecond item'; + const currentLine = '123. List item'; + + expect(smartReindent(pastedText, currentLine)).toBe('First item\n Second item'); + }); + + it('should preserve empty lines with indentation', () => { + const pastedText = 'First item\n\nThird item'; + const currentLine = '- List item'; + + expect(smartReindent(pastedText, currentLine)).toBe('First item\n \n Third item'); + }); + + it('should handle multiple markers correctly', () => { + const pastedText = 'First item\nSecond item'; + const currentLine = ' - Nested list item'; + + expect(smartReindent(pastedText, currentLine)).toBe('First item\n Second item'); + }); + + it('should handle single-line paste correctly', () => { + const pastedText = 'Single line'; + const currentLine = '- List item'; + + expect(smartReindent(pastedText, currentLine)).toBe('Single line'); + }); + + it('should handle windows-style line endings', () => { + const pastedText = 'First item\r\nSecond item'; + const currentLine = '- List item'; + + expect(smartReindent(pastedText, currentLine)).toBe('First item\r\n Second item'); + }); + + // Block quotes + it('should reindent with blockquote markers', () => { + const pastedText = 'First quote\nSecond quote'; + const currentLine = '> Quoted text'; + + expect(smartReindent(pastedText, currentLine)).toBe('First quote\n> Second quote'); + }); + + it('should handle nested blockquotes', () => { + const pastedText = 'First quote\nSecond quote'; + const currentLine = '> > Nested quote'; + + expect(smartReindent(pastedText, currentLine)).toBe('First quote\n> > Second quote'); + }); + + // Spaces and indentation + it('should handle double space indentation', () => { + const pastedText = 'First line\nSecond line'; + const currentLine = ' Indented text'; + + expect(smartReindent(pastedText, currentLine)).toBe('First line\n Second line'); + }); + + it('should handle code block indentation (4 spaces)', () => { + const pastedText = 'var x = 1;\nvar y = 2;'; + const currentLine = ' Code block'; + + expect(smartReindent(pastedText, currentLine)).toBe('var x = 1;\n var y = 2;'); + }); + + it('should handle mixed markers correctly', () => { + const pastedText = 'First line\nSecond line'; + const currentLine = ' > - Nested quote with list'; + + expect(smartReindent(pastedText, currentLine)).toBe('First line\n > Second line'); + }); +}); diff --git a/src/markup/codemirror/smart-reindent/__tests__/utils.test.ts b/src/markup/codemirror/smart-reindent/__tests__/utils.test.ts new file mode 100644 index 00000000..38eb102c --- /dev/null +++ b/src/markup/codemirror/smart-reindent/__tests__/utils.test.ts @@ -0,0 +1,27 @@ +import {parseMarkers} from '../utils'; + +describe('parseMarkers', () => { + it('should parse list markers correctly', () => { + expect(parseMarkers('* list')).toEqual(['* ']); + expect(parseMarkers('- list')).toEqual(['- ']); + expect(parseMarkers('+ list')).toEqual(['+ ']); + expect(parseMarkers(' * list')).toEqual([' ', ' ', '* ']); + expect(parseMarkers(' * list')).toEqual([' ', '* ']); + }); + + it('should parse blockquote markers correctly', () => { + expect(parseMarkers('> quote')).toEqual(['> ']); + expect(parseMarkers(' > quote')).toEqual([' ', ' ', '> ']); + }); + + it('should parse indentation correctly', () => { + expect(parseMarkers(' text')).toEqual([' ', ' ']); + expect(parseMarkers(' text')).toEqual([' ']); + }); + + it('should handle empty or invalid input', () => { + expect(parseMarkers('')).toEqual([]); + expect(parseMarkers('text')).toEqual([]); + expect(parseMarkers(' text')).toEqual([' ']); + }); +}); diff --git a/src/markup/codemirror/smart-reindent/index.ts b/src/markup/codemirror/smart-reindent/index.ts new file mode 100644 index 00000000..af02f4f5 --- /dev/null +++ b/src/markup/codemirror/smart-reindent/index.ts @@ -0,0 +1,46 @@ +import {parseMarkers} from './utils'; + +/** + * Reindents pasted text based on the current line's markers + */ +export function smartReindent(pastedText: string, currentLineText: string): string { + // If current line is empty, return pasted text as is + if (currentLineText.length === 0) { + return pastedText; + } + + // Get markers from current line + const markers = parseMarkers(currentLineText); + + // If no markers found, return pasted text as is + if (markers.length === 0) { + return pastedText; + } + + // Create indentation for subsequent lines by replacing list markers with spaces + const subsequentIndent = markers + .map((marker) => { + if (marker.match(/^\d{1,6}\. |-|\*|\+/)) { + return ' '.repeat(marker.length); + } + return marker; + }) + .join(''); + + // Split and process the pasted text + const lines = pastedText.split('\n'); + + const reindentedText = lines + .map((line, index) => { + // First line doesn't need indentation + if (index === 0) { + return line; + } + + // Add indentation to all subsequent lines, including empty ones + return subsequentIndent + line; + }) + .join('\n'); + + return reindentedText; +} diff --git a/src/markup/codemirror/smart-reindent/utils.ts b/src/markup/codemirror/smart-reindent/utils.ts new file mode 100644 index 00000000..3353e156 --- /dev/null +++ b/src/markup/codemirror/smart-reindent/utils.ts @@ -0,0 +1,63 @@ +/** + * Parses markdown-style markers from the start of a line + * Returns an array of markers found: + * - ' ' for indentation + * - '> ' for blockquotes + * - '* ' or '- ' for list items + * - '1. ' for numbered lists + * + * Example inputs: + * " * list" -> [' ', '* '] + * "> quoted" -> ['> '] + * " nested" -> [' ', ' '] + * "1. list" -> ['1. '] + */ +export function parseMarkers(text: string): string[] { + const markers: string[] = []; + let pos = 0; + + while (pos < text.length) { + // Handle code block (4 spaces) + if ( + pos + 3 < text.length && + text[pos] === ' ' && + text[pos + 1] === ' ' && + text[pos + 2] === ' ' && + text[pos + 3] === ' ' + ) { + markers.push(' '); + pos += 4; + continue; + } + + // Handle numbered lists (1-6 digits followed by dot and space) + if (/^\d{1,6}\. /.test(text.slice(pos))) { + const match = text.slice(pos).match(/^(\d{1,6}\. )/); + if (match) { + markers.push(match[1]); + pos += match[1].length; + continue; + } + } + + // Handle block quotes and list markers + if (text[pos] === '>' || text[pos] === '-' || text[pos] === '*' || text[pos] === '+') { + if (pos + 1 < text.length && text[pos + 1] === ' ') { + markers.push(text[pos] + ' '); + pos += 2; + continue; + } + } + + // Handle single space (last priority) + if (text[pos] === ' ') { + markers.push(' '); + pos += 1; + continue; + } + + break; + } + + return markers; +} From 3ebf14fd580ce29dc0133715cd2cb6bb6ea4ca8a Mon Sep 17 00:00:00 2001 From: Sergey Makhnatkin Date: Thu, 19 Dec 2024 16:16:21 +0100 Subject: [PATCH 2/4] feat(toolbars): restructured toolbar configuration and presets (#509) --- demo/components/Playground.tsx | 104 ++- demo/stories/presets/Preset.tsx | 4 + demo/stories/presets/Presets.stories.tsx | 62 ++ src/bundle/MarkdownEditorView.tsx | 62 +- src/bundle/config/action-names.ts | 77 ++- src/bundle/config/index.ts | 3 + src/bundle/config/markup.tsx | 3 + src/bundle/config/wysiwyg.ts | 3 + src/bundle/toolbar/utils.ts | 136 ++++ src/i18n/menubar/en.json | 1 + src/i18n/menubar/ru.json | 1 + src/modules/toolbars/constants.ts | 14 + src/modules/toolbars/items.tsx | 813 +++++++++++++++++++++++ src/modules/toolbars/presets.ts | 614 +++++++++++++++++ src/modules/toolbars/types.ts | 86 +++ src/toolbar/types.ts | 8 + 16 files changed, 1893 insertions(+), 98 deletions(-) create mode 100644 src/bundle/toolbar/utils.ts create mode 100644 src/modules/toolbars/constants.ts create mode 100644 src/modules/toolbars/items.tsx create mode 100644 src/modules/toolbars/presets.ts create mode 100644 src/modules/toolbars/types.ts diff --git a/demo/components/Playground.tsx b/demo/components/Playground.tsx index b6c5708f..19815475 100644 --- a/demo/components/Playground.tsx +++ b/demo/components/Playground.tsx @@ -18,7 +18,6 @@ import { type UseMarkdownEditorProps, WysiwygPlaceholderOptions, logger, - markupToolbarConfigs, useMarkdownEditor, wysiwygToolbarConfigs, } from '../../src'; @@ -29,8 +28,8 @@ import {Math} from '../../src/extensions/additional/Math'; import {Mermaid} from '../../src/extensions/additional/Mermaid'; import {YfmHtmlBlock} from '../../src/extensions/additional/YfmHtmlBlock'; import {getSanitizeYfmHtmlBlock} from '../../src/extensions/additional/YfmHtmlBlock/utils'; -import {cloneDeep} from '../../src/lodash'; import type {CodeEditor} from '../../src/markup'; +import {ToolbarsPreset} from '../../src/modules/toolbars/types'; import {VERSION} from '../../src/version'; import {getPlugins} from '../defaults/md-plugins'; import useYfmHtmlBlockStyles from '../hooks/useYfmHtmlBlockStyles'; @@ -52,19 +51,6 @@ const fileUploadHandler: FileUploadHandler = async (file) => { return {url: URL.createObjectURL(file)}; }; -const mToolbarConfig = [ - ...markupToolbarConfigs.mToolbarConfig, - [markupToolbarConfigs.mMermaidButton, markupToolbarConfigs.mYfmHtmlBlockButton], -]; -mToolbarConfig[2].push(markupToolbarConfigs.mMathListItem); - -const wToolbarConfig = cloneDeep(wysiwygToolbarConfigs.wToolbarConfig); -wToolbarConfig[2].push(wysiwygToolbarConfigs.wMathListItem); -wToolbarConfig.push([ - wysiwygToolbarConfigs.wMermaidItemData, - wysiwygToolbarConfigs.wYfmHtmlBlockItemData, -]); - const wCommandMenuConfig = wysiwygToolbarConfigs.wCommandMenuConfig.concat( wysiwygToolbarConfigs.wMathInlineItemData, wysiwygToolbarConfigs.wMathBlockItemData, @@ -92,6 +78,7 @@ export type PlaygroundProps = { escapeConfig?: EscapeConfig; wysiwygCommandMenuConfig?: wysiwygToolbarConfigs.WToolbarItemData[]; markupToolbarConfig?: ToolbarGroupData[]; + toolbarsPreset?: ToolbarsPreset; onChangeEditorType?: (mode: MarkdownEditorMode) => void; onChangeSplitModeEnabled?: (splitModeEnabled: boolean) => void; directiveSyntax?: DirectiveSyntaxValue; @@ -137,6 +124,7 @@ export const Playground = React.memo((props) => { height, extraExtensions, extensionOptions, + toolbarsPreset, wysiwygToolbarConfig, wysiwygCommandMenuConfig, markupConfigExtensions, @@ -175,6 +163,47 @@ export const Playground = React.memo((props) => { const mdEditor = useMarkdownEditor( { + preset: 'full', + wysiwygConfig: { + escapeConfig, + placeholderOptions: placeholderOptions, + extensions: (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' + ); + }, + }) + .use(FoldingHeading) + .use(YfmHtmlBlock, { + useConfig: useYfmHtmlBlockStyles, + sanitize: getSanitizeYfmHtmlBlock({options: defaultOptions}), + head: ` + + ((props) => { initialSplitModeEnabled: initialSplitModeEnabled, initialToolbarVisible: true, splitMode: splitModeOrientation, - escapeConfig: escapeConfig, needToSetDimensionsForUploadedImages, renderPreview: renderPreviewDefined ? renderPreview : undefined, fileUploadHandler, - wysiwygConfig: { - placeholderOptions: placeholderOptions, - }, experimental: { ...experimental, directiveSyntax, @@ -209,42 +234,6 @@ export const Playground = React.memo((props) => { extensions: markupConfigExtensions, parseInsertedUrlAsImage, }, - extraExtensions: (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' - ); - }, - }) - .use(FoldingHeading) - .use(YfmHtmlBlock, { - useConfig: useYfmHtmlBlockStyles, - sanitize: getSanitizeYfmHtmlBlock({options: defaultOptions}), - head: ` - - ((props) => { toaster={toaster} className={b('editor-view')} stickyToolbar={Boolean(stickyToolbar)} - wysiwygToolbarConfig={wysiwygToolbarConfig ?? wToolbarConfig} - markupToolbarConfig={markupToolbarConfig ?? mToolbarConfig} + toolbarsPreset={toolbarsPreset} + wysiwygToolbarConfig={wysiwygToolbarConfig} + markupToolbarConfig={markupToolbarConfig} settingsVisible={settingsVisible} editor={mdEditor} enableSubmitInPreview={enableSubmitInPreview} diff --git a/demo/stories/presets/Preset.tsx b/demo/stories/presets/Preset.tsx index 58fc5c14..57477476 100644 --- a/demo/stories/presets/Preset.tsx +++ b/demo/stories/presets/Preset.tsx @@ -11,6 +11,7 @@ import { logger, useMarkdownEditor, } from '../../../src'; +import {ToolbarsPreset} from '../../../src/modules/toolbars/types'; import type {FileUploadHandler} from '../../../src/utils/upload'; import {VERSION} from '../../../src/version'; // --- @@ -41,6 +42,7 @@ export type PresetDemoProps = { splitModeOrientation?: 'horizontal' | 'vertical' | false; stickyToolbar?: boolean; height?: CSSProperties['height']; + toolbarsPreset?: ToolbarsPreset; }; logger.setLogger({ @@ -60,6 +62,7 @@ export const Preset = React.memo((props) => { splitModeOrientation, stickyToolbar, height, + toolbarsPreset, } = props; const [editorMode, setEditorMode] = React.useState('wysiwyg'); const [mdRaw, setMdRaw] = React.useState(''); @@ -130,6 +133,7 @@ export const Preset = React.memo((props) => {
= { @@ -22,6 +40,50 @@ export const Full: StoryObj = { args: {preset: 'full'}, }; +export const Custom: StoryObj = { + args: { + toolbarsPreset: { + items: { + [Action.undo]: { + view: undoItemView, + wysiwyg: undoItemWysiwyg, + markup: undoItemMarkup, + }, + [Action.redo]: { + view: redoItemView, + wysiwyg: redoItemWysiwyg, + markup: redoItemMarkup, + }, + [Action.bold]: { + view: boldItemView, + wysiwyg: boldItemWysiwyg, + }, + [Action.italic]: { + view: italicItemView, + markup: italicItemMarkup, + }, + [Action.colorify]: { + view: colorifyItemView, + wysiwyg: colorifyItemWysiwyg, + markup: colorifyItemMarkup, + }, + }, + orders: { + [Toolbar.wysiwygMain]: [ + [Action.colorify], + [Action.bold], + [Action.undo, Action.redo], + ], + [Toolbar.markupMain]: [ + [Action.colorify], + [Action.italic], + [Action.undo, Action.redo], + ], + }, + }, + }, +}; + export default { component, title: 'Extensions / Presets', diff --git a/src/bundle/MarkdownEditorView.tsx b/src/bundle/MarkdownEditorView.tsx index 437cd2de..d0fe9d6f 100644 --- a/src/bundle/MarkdownEditorView.tsx +++ b/src/bundle/MarkdownEditorView.tsx @@ -7,6 +7,7 @@ import {useEnsuredForwardedRef, useKey, useUpdate} from 'react-use'; import {ClassNameProps, cn} from '../classname'; import {i18n} from '../i18n/bundle'; import {logger} from '../logger'; +import type {ToolbarsPreset} from '../modules/toolbars/types'; import {ToasterContext, useBooleanState, useSticky} from '../react-utils'; import {isMac} from '../utils'; @@ -15,19 +16,11 @@ import {HorizontalDrag} from './HorizontalDrag'; import {MarkupEditorView} from './MarkupEditorView'; import {SplitModeView} from './SplitModeView'; import {WysiwygEditorView} from './WysiwygEditorView'; -import { - MToolbarData, - MToolbarItemData, - WToolbarData, - WToolbarItemData, - mHiddenDataByPreset, - mToolbarConfigByPreset, - wHiddenDataByPreset, - wToolbarConfigByPreset, -} from './config'; +import {MToolbarData, MToolbarItemData, WToolbarData, WToolbarItemData} from './config'; import {useMarkdownEditorContext} from './context'; import {EditorSettings, EditorSettingsProps} from './settings'; import {stickyCn} from './sticky'; +import {getToolbarsConfigs} from './toolbar/utils'; import type {MarkdownEditorMode} from './types'; import '../styles/styles.scss'; @@ -39,9 +32,22 @@ const b = cnEditorComponent; export type MarkdownEditorViewProps = ClassNameProps & { editor?: Editor; autofocus?: boolean; + toolbarsPreset?: ToolbarsPreset; + /** + * @deprecated use `toolbarsPreset` instead + */ markupToolbarConfig?: MToolbarData; + /** + * @deprecated use `toolbarsPreset` instead + */ wysiwygToolbarConfig?: WToolbarData; + /** + * @deprecated use `toolbarsPreset` instead + */ markupHiddenActionsConfig?: MToolbarItemData[]; + /** + * @deprecated use `toolbarsPreset` instead + */ wysiwygHiddenActionsConfig?: WToolbarItemData[]; /** @default true */ settingsVisible?: boolean; @@ -73,16 +79,44 @@ export const MarkdownEditorView = React.forwardRef + getToolbarsConfigs({ + toolbarsPreset, + props: { + wysiwygToolbarConfig: initialWysiwygToolbarConfig, + markupToolbarConfig: initialMarkupToolbarConfig, + wysiwygHiddenActionsConfig: initialWysiwygHiddenActionsConfig, + markupHiddenActionsConfig: initialMarkupHiddenActionsConfig, + }, + preset: editor.preset, + }), + [ + toolbarsPreset, + initialWysiwygToolbarConfig, + initialMarkupToolbarConfig, + initialWysiwygHiddenActionsConfig, + initialMarkupHiddenActionsConfig, + editor.preset, + ], + ); + const rerender = useUpdate(); React.useLayoutEffect(() => { editor.on('rerender', rerender); diff --git a/src/bundle/config/action-names.ts b/src/bundle/config/action-names.ts index 29e127ae..a4a5d2e1 100644 --- a/src/bundle/config/action-names.ts +++ b/src/bundle/config/action-names.ts @@ -1,44 +1,67 @@ const names = [ - 'undo', - 'redo', + 'anchor', 'bold', - 'italic', - 'underline', - 'strike', - 'mono', - 'mark', - 'paragraph', + 'bulletList', + 'checkbox', + /** @deprecated use codeBlock */ + 'code_block', + 'codeBlock', + /** @deprecated use codeInline */ + 'code_inline', + 'codeInline', + 'colorify', + 'emoji', + 'file', + 'filePopup', + 'gpt', 'heading1', 'heading2', 'heading3', 'heading4', 'heading5', 'heading6', - 'bulletList', - 'orderedList', + /** @deprecated use horizontalRule */ + 'horizontalrule', + 'horizontalRule', + 'image', + 'imagePopup', + 'italic', 'liftListItem', - 'sinkListItem', - 'checkbox', 'link', + 'mark', + /** @deprecated use mathBlock */ + 'math_block', + 'mathBlock', + /** @deprecated use mathInline */ + 'math_inline', + 'mathInline', + 'mermaid', + 'mono', + 'orderedList', + 'paragraph', 'quote', - 'yfm_cut', - 'yfm_note', + 'redo', + 'sinkListItem', + 'strike', + 'table', + 'tabs', + 'underline', + 'undo', + /** @deprecated use block */ 'yfm_block', + 'block', + /** @deprecated use cut */ + 'yfm_cut', + 'cut', + /** @deprecated use htmlBlock */ 'yfm_html_block', + 'htmlBlock', + /** @deprecated use layout */ 'yfm_layout', - 'table', - 'code_inline', - 'code_block', - 'image', - 'horizontalrule', - 'emoji', - 'file', - 'anchor', - 'math_inline', - 'math_block', - 'tabs', - 'mermaid', - 'gpt', + 'layout', + /** @deprecated use note */ + 'yfm_note', + 'note', ] as const; type ItemsType = L extends readonly (infer T)[] ? T : never; diff --git a/src/bundle/config/index.ts b/src/bundle/config/index.ts index aa50e1d1..5b7fbe26 100644 --- a/src/bundle/config/index.ts +++ b/src/bundle/config/index.ts @@ -1,2 +1,5 @@ +/** + * @deprecated This file is deprecated. Use ToolbarsPreset instead. + */ export * from './wysiwyg'; export * from './markup'; diff --git a/src/bundle/config/markup.tsx b/src/bundle/config/markup.tsx index 38dcbb17..b6835be9 100644 --- a/src/bundle/config/markup.tsx +++ b/src/bundle/config/markup.tsx @@ -1,3 +1,6 @@ +/** + * @deprecated This file is deprecated. Use ToolbarsPreset instead. + */ import React from 'react'; import {i18n} from '../../i18n/menubar'; diff --git a/src/bundle/config/wysiwyg.ts b/src/bundle/config/wysiwyg.ts index bc761b6a..db9d1a38 100644 --- a/src/bundle/config/wysiwyg.ts +++ b/src/bundle/config/wysiwyg.ts @@ -1,3 +1,6 @@ +/** + * @deprecated This file is deprecated. Use ToolbarsPreset instead. + */ import {ActionStorage} from 'src/core'; import {headingType, pType} from '../../extensions'; diff --git a/src/bundle/toolbar/utils.ts b/src/bundle/toolbar/utils.ts new file mode 100644 index 00000000..94447bc1 --- /dev/null +++ b/src/bundle/toolbar/utils.ts @@ -0,0 +1,136 @@ +import {ToolbarName} from '../../modules/toolbars/constants'; +import {commonmark, defaultPreset, full, yfm, zero} from '../../modules/toolbars/presets'; +import type { + ToolbarItem, + ToolbarItemMarkup, + ToolbarItemWysiwyg, + ToolbarsPreset, +} from '../../modules/toolbars/types'; +import type {MToolbarData, MToolbarItemData, WToolbarData, WToolbarItemData} from '../../toolbar'; +import {ToolbarDataType, ToolbarIconData} from '../../toolbar'; +import type {MarkdownEditorViewProps} from '../MarkdownEditorView'; +import {MarkdownEditorPreset} from '../types'; + +const defaultPresets: Record = { + zero, + commonmark, + default: defaultPreset, + yfm, + full, +}; + +interface TransformedItem { + type: ToolbarDataType; + id: string; + title?: string | (() => string); + hint?: string | (() => string); + icon?: ToolbarIconData; + hotkey?: string; + withArrow?: boolean; + wysiwyg?: ToolbarItemWysiwyg; + markup?: ToolbarItemMarkup; +} + +const transformItem = ( + type: 'wysiwyg' | 'markup', + item?: ToolbarItem, + id = 'unknown', +): TransformedItem => { + if (!item) { + console.warn( + `Toolbar item "${id}" not found, it might not have been added to the items dictionary.`, + ); + return {} as TransformedItem; + } + + const isListButton = item.view.type === ToolbarDataType.ListButton; + + return { + type: item.view.type ?? ToolbarDataType.SingleButton, + id, + title: item.view.title, + hint: item.view.hint, + icon: item.view.icon, + hotkey: item.view.hotkey, + ...(isListButton && {withArrow: (item.view as any).withArrow}), + ...(type === 'wysiwyg' && item.wysiwyg && {...item.wysiwyg}), + ...(type === 'markup' && item.markup && {...item.markup}), + }; +}; + +export const createConfig = ( + editorType: 'wysiwyg' | 'markup', + toolbarPreset: ToolbarsPreset | MarkdownEditorPreset, + toolbarName: string, +): T => { + const preset = + typeof toolbarPreset === 'string' + ? defaultPresets[toolbarPreset] || defaultPresets.default + : toolbarPreset; + const orders = preset.orders[toolbarName] ?? [[]]; + const {items} = preset; + + const toolbarData = orders.map((group) => + group.map((action) => { + return typeof action === 'string' + ? transformItem(editorType, items[action], action) + : { + ...transformItem(editorType, items[action.id], action.id), + data: action.items.map((id) => transformItem(editorType, items[id], id)), + }; + }), + ); + + return toolbarData as T; +}; + +const flattenPreset = (config: T) => { + // TODO: @makhnatkin add logic for flatten + return (config[0] ?? []) as unknown as T extends WToolbarData + ? WToolbarItemData[] + : MToolbarItemData[]; +}; + +interface GetToolbarsConfigsArgs { + toolbarsPreset?: ToolbarsPreset; + props: Pick< + MarkdownEditorViewProps, + | 'markupToolbarConfig' + | 'wysiwygToolbarConfig' + | 'wysiwygHiddenActionsConfig' + | 'markupHiddenActionsConfig' + >; + preset: MarkdownEditorPreset; +} +export const getToolbarsConfigs = ({toolbarsPreset, props, preset}: GetToolbarsConfigsArgs) => { + const wysiwygToolbarConfig = toolbarsPreset + ? createConfig('wysiwyg', toolbarsPreset, ToolbarName.wysiwygMain) + : props.wysiwygToolbarConfig ?? + createConfig('wysiwyg', preset, ToolbarName.wysiwygMain); + + const markupToolbarConfig = toolbarsPreset + ? createConfig('markup', toolbarsPreset, ToolbarName.markupMain) + : props.markupToolbarConfig ?? + createConfig('markup', preset, ToolbarName.markupMain); + + const wysiwygHiddenActionsConfig = toolbarsPreset + ? flattenPreset( + createConfig('wysiwyg', toolbarsPreset, ToolbarName.wysiwygHidden), + ) + : props.wysiwygHiddenActionsConfig ?? + flattenPreset(createConfig('wysiwyg', preset, ToolbarName.wysiwygHidden)); + + const markupHiddenActionsConfig = toolbarsPreset + ? flattenPreset( + createConfig('markup', toolbarsPreset, ToolbarName.markupHidden), + ) + : props.markupHiddenActionsConfig ?? + flattenPreset(createConfig('markup', preset, ToolbarName.markupHidden)); + + return { + wysiwygToolbarConfig, + markupToolbarConfig, + wysiwygHiddenActionsConfig, + markupHiddenActionsConfig, + }; +}; diff --git a/src/i18n/menubar/en.json b/src/i18n/menubar/en.json index d007825c..7f15a7f5 100644 --- a/src/i18n/menubar/en.json +++ b/src/i18n/menubar/en.json @@ -44,6 +44,7 @@ "mermaid": "Mermaid", "mono": "Monospace", "more_action": "More action", + "move_list": "Move list item", "note": "Note", "olist": "Ordered list", "quote": "Quote", diff --git a/src/i18n/menubar/ru.json b/src/i18n/menubar/ru.json index 10d123d9..378bfac0 100644 --- a/src/i18n/menubar/ru.json +++ b/src/i18n/menubar/ru.json @@ -44,6 +44,7 @@ "mermaid": "Mermaid", "mono": "Моноширинный", "more_action": "Другие действия", + "move_list": "Переместить элемент списка", "note": "Примечание", "olist": "Нумерованный список", "quote": "Цитата", diff --git a/src/modules/toolbars/constants.ts b/src/modules/toolbars/constants.ts new file mode 100644 index 00000000..1b7cd315 --- /dev/null +++ b/src/modules/toolbars/constants.ts @@ -0,0 +1,14 @@ +export enum ListName { + heading = 'heading', + lists = 'lists', + code = 'code', +} + +export enum ToolbarName { + markupHidden = 'markupHidden', + markupMain = 'markupMain', + wysiwygHidden = 'wysiwygHidden', + wysiwygMain = 'wysiwygMain', + wysiwygSelection = 'wysiwygSelection', + wysiwygSlash = 'wysiwygSlash', +} diff --git a/src/modules/toolbars/items.tsx b/src/modules/toolbars/items.tsx new file mode 100644 index 00000000..80331f1d --- /dev/null +++ b/src/modules/toolbars/items.tsx @@ -0,0 +1,813 @@ +import React from 'react'; + +import {icons} from '../../bundle/config/icons'; +import {MToolbarColors} from '../../bundle/toolbar/markup/MToolbarColors'; +import {MToolbarFilePopup} from '../../bundle/toolbar/markup/MToolbarFilePopup'; +import {MToolbarImagePopup} from '../../bundle/toolbar/markup/MToolbarImagePopup'; +import {WToolbarColors} from '../../bundle/toolbar/wysiwyg/WToolbarColors'; +import {WToolbarTextSelect} from '../../bundle/toolbar/wysiwyg/WToolbarTextSelect'; +import {headingType, pType} from '../../extensions'; +import {gptHotKeys} from '../../extensions/additional/GPT/constants'; +import {i18n as i18nHint} from '../../i18n/hints'; +import {i18n} from '../../i18n/menubar'; +import { + insertHRule, + insertLink, + insertMermaidDiagram, + insertYfmHtmlBlock, + insertYfmTable, + insertYfmTabs, + liftListItem as liftListItemCommand, + redo, + redoDepth, + sinkListItem as sinkListItemCommand, + toBulletList, + toH1, + toH2, + toH3, + toH4, + toH5, + toH6, + toOrderedList, + toggleBold, + toggleItalic, + toggleMarked, + toggleMonospace, + toggleStrikethrough, + toggleUnderline, + undo, + undoDepth, + wrapToBlockquote, + wrapToCheckbox, + wrapToCodeBlock, + wrapToInlineCode, + wrapToMathBlock, + wrapToMathInline, + wrapToYfmCut, + wrapToYfmNote, +} from '../../markup/commands'; +import {Action as A, formatter as f} from '../../shortcuts'; +import {ToolbarDataType} from '../../toolbar'; + +import {ToolbarItemMarkup, ToolbarItemView, ToolbarItemWysiwyg} from './types'; + +const noop = () => {}; +const inactive = () => false; +const enable = () => true; +const disable = () => false; + +// ---- Undo ---- +export const undoItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'undo'), + icon: icons.undo, + hotkey: f.toView(A.Undo), +}; +export const undoItemWysiwyg: ToolbarItemWysiwyg = { + hintWhenDisabled: false, + exec: (e) => e.actions.undo.run(), + isActive: (e) => e.actions.undo.isActive(), + isEnable: (e) => e.actions.undo.isEnable(), +}; +export const undoItemMarkup: ToolbarItemMarkup = { + hintWhenDisabled: false, + exec: (e) => undo(e.cm), + isActive: inactive, + isEnable: (e) => undoDepth(e.cm.state) > 0, +}; + +// ---- Redo ---- +export const redoItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'redo'), + icon: icons.redo, + hotkey: f.toView(A.Redo), +}; +export const redoItemWysiwyg: ToolbarItemWysiwyg = { + hintWhenDisabled: false, + exec: (e) => e.actions.redo.run(), + isActive: (e) => e.actions.redo.isActive(), + isEnable: (e) => e.actions.redo.isEnable(), +}; +export const redoItemMarkup: ToolbarItemMarkup = { + hintWhenDisabled: false, + exec: (e) => redo(e.cm), + isActive: inactive, + isEnable: (e) => redoDepth(e.cm.state) > 0, +}; + +// ---- Bold ---- +export const boldItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'bold'), + icon: icons.bold, + hotkey: f.toView(A.Bold), +}; +export const boldItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.bold.run(), + isActive: (e) => e.actions.bold.isActive(), + isEnable: (e) => e.actions.bold.isEnable(), +}; +export const boldItemMarkup: ToolbarItemMarkup = { + exec: (e) => toggleBold(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Italic ---- +export const italicItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'italic'), + icon: icons.italic, + hotkey: f.toView(A.Italic), +}; +export const italicItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.italic.run(), + isActive: (e) => e.actions.italic.isActive(), + isEnable: (e) => e.actions.italic.isEnable(), +}; +export const italicItemMarkup: ToolbarItemMarkup = { + exec: (e) => toggleItalic(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Underline ---- +export const underlineItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'underline'), + icon: icons.underline, + hotkey: f.toView(A.Underline), +}; +export const underlineItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.underline.run(), + isActive: (e) => e.actions.underline.isActive(), + isEnable: (e) => e.actions.underline.isEnable(), +}; +export const underlineItemMarkup: ToolbarItemMarkup = { + exec: (e) => toggleUnderline(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Strikethrough ---- +export const strikethroughItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'strike'), + icon: icons.strikethrough, + hotkey: f.toView(A.Strike), +}; +export const strikethroughItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.strike.run(), + isActive: (e) => e.actions.strike.isActive(), + isEnable: (e) => e.actions.strike.isEnable(), +}; +export const strikethroughItemMarkup: ToolbarItemMarkup = { + exec: (e) => toggleStrikethrough(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Monospace ---- +export const monospaceItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'mono'), + icon: icons.mono, +}; +export const monospaceItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.mono.run(), + isActive: (e) => e.actions.mono.isActive(), + isEnable: (e) => e.actions.mono.isEnable(), +}; +export const monospaceItemMarkup: ToolbarItemMarkup = { + exec: (e) => toggleMonospace(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Marked ---- +export const markedItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'mark'), + icon: icons.mark, +}; +export const markedItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.mark.run(), + isActive: (e) => e.actions.mark.isActive(), + isEnable: (e) => e.actions.mark.isEnable(), +}; +export const markedItemMarkup: ToolbarItemMarkup = { + exec: (e) => toggleMarked(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Checkbox ---- +export const checkboxItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'checkbox'), + icon: icons.checklist, +}; +export const checkboxItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.addCheckbox.run(), + isActive: (e) => e.actions.addCheckbox.isActive(), + isEnable: (e) => e.actions.addCheckbox.isEnable(), +}; +export const checkboxItemMarkup: ToolbarItemMarkup = { + exec: (e) => wrapToCheckbox(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Link ---- +export const linkItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'link'), + icon: icons.link, + hotkey: f.toView(A.Link), +}; +export const linkItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.addLink.run(), + isActive: (e) => e.actions.addLink.isActive(), + isEnable: (e) => e.actions.addLink.isEnable(), +}; +export const linkItemMarkup: ToolbarItemMarkup = { + exec: (e) => insertLink(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Quote ---- +export const quoteItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'quote'), + icon: icons.quote, + hotkey: f.toView(A.Quote), +}; +export const quoteItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.quote.run(), + isActive: (e) => e.actions.quote.isActive(), + isEnable: (e) => e.actions.quote.isEnable(), +}; +export const quoteItemMarkup: ToolbarItemMarkup = { + exec: (e) => wrapToBlockquote(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Cut ---- +export const cutItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'cut'), + icon: icons.cut, + hotkey: f.toView(A.Cut), +}; +export const cutItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.toYfmCut.run(), + isActive: (e) => e.actions.toYfmCut.isActive(), + isEnable: (e) => e.actions.toYfmCut.isEnable(), +}; +export const cutItemMarkup: ToolbarItemMarkup = { + exec: (e) => wrapToYfmCut(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Note ---- +export const noteItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'note'), + icon: icons.note, + hotkey: f.toView(A.Note), +}; +export const noteItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.toYfmNote.run(), + isActive: (e) => e.actions.toYfmNote.isActive(), + isEnable: (e) => e.actions.toYfmNote.isEnable(), +}; +export const noteItemMarkup: ToolbarItemMarkup = { + exec: (e) => wrapToYfmNote(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Table ---- +export const tableItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'table'), + icon: icons.table, +}; +export const tableItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.createYfmTable.run(), + isActive: (e) => e.actions.createYfmTable.isActive(), + isEnable: (e) => e.actions.createYfmTable.isEnable(), +}; +export const tableItemMarkup: ToolbarItemMarkup = { + exec: (e) => insertYfmTable(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Code ---- +export const codeItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'code_inline'), + icon: icons.code, + hotkey: f.toView(A.Code), +}; +export const codeItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.code.run(), + isActive: (e) => e.actions.code.isActive(), + isEnable: (e) => e.actions.code.isEnable(), +}; +export const codeItemMarkup: ToolbarItemMarkup = { + exec: (e) => wrapToInlineCode(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Image ---- +export const imageItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'image'), + icon: icons.image, +}; +export const imagePopupItemView: ToolbarItemView = { + type: ToolbarDataType.ButtonPopup, + title: i18n.bind(null, 'image'), + icon: icons.image, +}; +export const imageItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.addImageWidget.run(), + isActive: (e) => e.actions.addImageWidget.isActive(), + isEnable: (e) => e.actions.addImageWidget.isEnable(), +}; +export const imageItemMarkup: ToolbarItemMarkup = { + exec: noop, + isActive: inactive, + isEnable: enable, + renderPopup: (props) => , +}; + +// ---- File ---- +export const fileItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'file'), + icon: icons.file, +}; +export const filePopupItemView: ToolbarItemView = { + type: ToolbarDataType.ButtonPopup, + title: i18n.bind(null, 'file'), + icon: icons.file, +}; +export const fileItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.addFile.run(), + isActive: (e) => e.actions.addFile.isActive(), + isEnable: (e) => e.actions.addFile.isEnable(), +}; +export const fileItemMarkup: ToolbarItemMarkup = { + exec: noop, + isActive: inactive, + isEnable: enable, + renderPopup: (props) => , +}; + +// ---- Tabs ---- +export const tabsItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'tabs'), + icon: icons.tabs, +}; +export const tabsItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.toYfmTabs.run(), + isActive: (e) => e.actions.toYfmTabs.isActive(), + isEnable: (e) => e.actions.toYfmTabs.isEnable(), +}; +export const tabsItemMarkup: ToolbarItemMarkup = { + exec: (e) => insertYfmTabs(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Math Inline ---- +export const mathInlineItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'math_inline'), + icon: icons.functionInline, + hint: () => `${i18nHint.bind(null, 'math_hint')()} ${i18nHint.bind(null, 'math_hint_katex')()}`, +}; +export const mathInlineItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.addMathInline.run(), + isActive: (e) => e.actions.addMathInline.isActive(), + isEnable: (e) => e.actions.addMathInline.isEnable(), +}; +export const mathInlineItemMarkup: ToolbarItemMarkup = { + exec: (e) => wrapToMathInline(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Math Block ---- +export const mathBlockItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'math_block'), + icon: icons.functionBlock, + hint: () => `${i18nHint.bind(null, 'math_hint')()} ${i18nHint.bind(null, 'math_hint_katex')()}`, +}; +export const mathBlockItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.toMathBlock.run(), + isActive: (e) => e.actions.toMathBlock.isActive(), + isEnable: (e) => e.actions.toMathBlock.isEnable(), +}; +export const mathBlockItemMarkup: ToolbarItemMarkup = { + exec: (e) => wrapToMathBlock(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Yfm Html Block ---- +export const yfmHtmlBlockItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'html'), + icon: icons.html, +}; +export const yfmHtmlBlockItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.createYfmHtmlBlock.run(), + isActive: (e) => e.actions.createYfmHtmlBlock.isActive(), + isEnable: (e) => e.actions.createYfmHtmlBlock.isEnable(), +}; +export const yfmHtmlBlockItemMarkup: ToolbarItemMarkup = { + exec: (e) => insertYfmHtmlBlock(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Mermaid ---- +export const mermaidItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'mermaid'), + icon: icons.mermaid, +}; +export const mermaidItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.createMermaid.run(), + isActive: (e) => e.actions.createMermaid.isActive(), + isEnable: (e) => e.actions.createMermaid.isEnable(), +}; +export const mermaidItemMarkup: ToolbarItemMarkup = { + exec: (e) => insertMermaidDiagram(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Code Block ---- +export const codeBlockItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'codeblock'), + icon: icons.codeBlock, + hotkey: f.toView(A.CodeBlock), +}; +export const codeBlockItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.toCodeBlock.run(), + isActive: (e) => e.actions.toCodeBlock.isActive(), + isEnable: (e) => e.actions.toCodeBlock.isEnable(), +}; +export const codeBlockItemMarkup: ToolbarItemMarkup = { + exec: (e) => wrapToCodeBlock(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Horizontal Rule ---- +export const hruleItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'hrule'), + icon: icons.horizontalRule, +}; +export const hruleItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.hRule.run(), + isActive: (e) => e.actions.hRule.isActive(), + isEnable: (e) => e.actions.hRule.isEnable(), +}; +export const hruleItemMarkup: ToolbarItemMarkup = { + exec: (e) => insertHRule(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Emoji ---- +export const emojiItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'emoji'), + icon: icons.emoji, +}; +export const emojiItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.openEmojiSuggest.run({}), + isActive: (e) => e.actions.openEmojiSuggest.isActive(), + isEnable: (e) => e.actions.openEmojiSuggest.isEnable(), +}; +export const emojiItemMarkup: ToolbarItemMarkup = { + exec: noop, + hintWhenDisabled: i18n.bind(null, 'emoji__hint'), + isActive: inactive, + isEnable: disable, +}; + +// ---- Heading 1 ---- +export const heading1ItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'heading1'), + icon: icons.h1, + hotkey: f.toView(A.Heading1), +}; +export const heading1ItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.toH1.run(), + isActive: (e) => e.actions.toH1.isActive(), + isEnable: (e) => e.actions.toH1.isEnable(), +}; +export const heading1ItemMarkup: ToolbarItemMarkup = { + exec: (e) => toH1(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Heading 2 ---- +export const heading2ItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'heading2'), + icon: icons.h2, + hotkey: f.toView(A.Heading2), +}; +export const heading2ItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.toH2.run(), + isActive: (e) => e.actions.toH2.isActive(), + isEnable: (e) => e.actions.toH2.isEnable(), +}; +export const heading2ItemMarkup: ToolbarItemMarkup = { + exec: (e) => toH2(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Heading 3 ---- +export const heading3ItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'heading3'), + icon: icons.h3, + hotkey: f.toView(A.Heading3), +}; +export const heading3ItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.toH3.run(), + isActive: (e) => e.actions.toH3.isActive(), + isEnable: (e) => e.actions.toH3.isEnable(), +}; +export const heading3ItemMarkup: ToolbarItemMarkup = { + exec: (e) => toH3(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Heading 4 ---- +export const heading4ItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'heading4'), + icon: icons.h4, + hotkey: f.toView(A.Heading4), +}; +export const heading4ItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.toH4.run(), + isActive: (e) => e.actions.toH4.isActive(), + isEnable: (e) => e.actions.toH4.isEnable(), +}; +export const heading4ItemMarkup: ToolbarItemMarkup = { + exec: (e) => toH4(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Heading 5 ---- +export const heading5ItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'heading5'), + icon: icons.h5, + hotkey: f.toView(A.Heading5), +}; +export const heading5ItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.toH5.run(), + isActive: (e) => e.actions.toH5.isActive(), + isEnable: (e) => e.actions.toH5.isEnable(), +}; +export const heading5ItemMarkup: ToolbarItemMarkup = { + exec: (e) => toH5(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Heading 6 ---- +export const heading6ItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'heading6'), + icon: icons.h6, + hotkey: f.toView(A.Heading6), +}; +export const heading6ItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.toH6.run(), + isActive: (e) => e.actions.toH6.isActive(), + isEnable: (e) => e.actions.toH6.isEnable(), +}; +export const heading6ItemMarkup: ToolbarItemMarkup = { + exec: (e) => toH6(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Bullet List ---- +export const bulletListItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'ulist'), + icon: icons.bulletList, + hotkey: f.toView(A.BulletList), +}; +export const bulletListItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.toBulletList.run(), + isActive: (e) => e.actions.toBulletList.isActive(), + isEnable: (e) => e.actions.toBulletList.isEnable(), +}; +export const bulletListItemMarkup: ToolbarItemMarkup = { + exec: (e) => toBulletList(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Ordered List ---- +export const orderedListItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'olist'), + icon: icons.orderedList, + hotkey: f.toView(A.OrderedList), +}; +export const orderedListItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.toOrderedList.run(), + isActive: (e) => e.actions.toOrderedList.isActive(), + isEnable: (e) => e.actions.toOrderedList.isEnable(), +}; +export const orderedListItemMarkup: ToolbarItemMarkup = { + exec: (e) => toOrderedList(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Sink List ---- +export const sinkListItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'list__action_sink'), + icon: icons.sink, + hotkey: f.toView(A.SinkListItem), +}; +export const sinkListItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.sinkListItem.run(), + hintWhenDisabled: () => i18n('list_action_disabled'), + isActive: (e) => e.actions.sinkListItem.isActive(), + isEnable: (e) => e.actions.sinkListItem.isEnable(), +}; +export const sinkListItemMarkup: ToolbarItemMarkup = { + exec: (e) => sinkListItemCommand(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Lift List ---- +export const liftListItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'list__action_lift'), + icon: icons.lift, + hotkey: f.toView(A.LiftListItem), +}; +export const liftListItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.liftListItem.run(), + hintWhenDisabled: () => i18n('list_action_disabled'), + isActive: (e) => e.actions.liftListItem.isActive(), + isEnable: (e) => e.actions.liftListItem.isEnable(), +}; +export const liftListItemMarkup: ToolbarItemMarkup = { + exec: (e) => liftListItemCommand(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Toggle Heading Folding ---- +export const toggleHeadingFoldingItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + icon: icons.foldingHeading, + title: () => i18n('folding-heading'), + hint: () => i18n('folding-heading__hint'), +}; +export const toggleHeadingFoldingItemWysiwyg: ToolbarItemWysiwyg = { + isActive: (editor) => editor.actions.toggleHeadingFolding?.isActive() ?? false, + isEnable: (editor) => editor.actions.toggleHeadingFolding?.isEnable() ?? false, + exec: (editor) => editor.actions.toggleHeadingFolding.run(), + condition: 'enabled', +}; + +// ---- Text Context ---- +export const textContextItemView: ToolbarItemView = { + type: ToolbarDataType.ReactComponent, +}; +export const textContextItemWisywig: ToolbarItemWysiwyg = { + component: WToolbarTextSelect, + width: 0, + condition: ({selection: {$from, $to}, schema}) => { + if (!$from.sameParent($to)) return false; + const {parent} = $from; + return parent.type === pType(schema) || parent.type === headingType(schema); + }, +}; + +// ---- Paragraph ---- +export const paragraphItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'text'), + icon: icons.text, + hotkey: f.toView(A.Text), + doNotActivateList: true, +}; +export const paragraphItemWisywig: ToolbarItemWysiwyg = { + exec: (e) => e.actions.toParagraph.run(), + isActive: (e) => e.actions.toParagraph.isActive(), + isEnable: (e) => e.actions.toParagraph.isEnable(), +}; +export const paragraphItemMarkup: ToolbarItemMarkup = { + exec: noop, + isActive: inactive, + isEnable: enable, +}; + +// --- Colorify ---- +export const colorifyItemView: ToolbarItemView = { + type: ToolbarDataType.ReactComponent, +}; +export const colorifyItemWysiwyg: ToolbarItemWysiwyg = { + component: WToolbarColors, + width: 42, +}; +export const colorifyItemMarkup: ToolbarItemMarkup = { + component: MToolbarColors, + width: 42, +}; + +// ---- GPT ---- +export const gptItemView: ToolbarItemView = { + type: ToolbarDataType.SingleButton, + title: i18n.bind(null, 'gpt'), + hotkey: gptHotKeys.openGptKeyTooltip, + icon: icons.gpt, +}; +export const gptItemWysiwyg: ToolbarItemWysiwyg = { + exec: (e) => e.actions.addGptWidget.run({}), + isActive: (e) => e.actions.addGptWidget.isActive(), + isEnable: (e) => e.actions.addGptWidget.isEnable(), +}; +export const gptItemMarkup: ToolbarItemMarkup = { + exec: (e) => insertMermaidDiagram(e.cm), + isActive: inactive, + isEnable: enable, +}; + +// ---- Heading list ---- +export const headingListItemView: ToolbarItemView = { + type: ToolbarDataType.ListButton, + icon: icons.headline, + title: i18n.bind(null, 'heading'), + withArrow: true, +}; + +// ---- Lists list ---- +export const listsListItemView: ToolbarItemView = { + type: ToolbarDataType.ListButton, + icon: icons.bulletList, + withArrow: true, + title: i18n.bind(null, 'list'), +}; + +// ---- Move list ---- +export const moveListItemView: ToolbarItemView = { + type: ToolbarDataType.ListButton, + icon: icons.lift, + withArrow: true, + title: i18n.bind(null, 'move_list'), +}; + +// ---- Code list ---- +export const codeBlocksListItemView: ToolbarItemView = { + type: ToolbarDataType.ListButton, + icon: icons.code, + title: i18n.bind(null, 'code'), + withArrow: true, +}; + +// ---- Math list ---- +export const mathListItemView: ToolbarItemView = { + type: ToolbarDataType.ListButton, + icon: icons.functionInline, + withArrow: true, + title: i18n.bind(null, 'math'), +}; diff --git a/src/modules/toolbars/presets.ts b/src/modules/toolbars/presets.ts new file mode 100644 index 00000000..531f8324 --- /dev/null +++ b/src/modules/toolbars/presets.ts @@ -0,0 +1,614 @@ +import {ActionName as Action} from '../../bundle/config/action-names'; + +import {ListName as List, ToolbarName as Toolbar} from './constants'; +import { + boldItemMarkup, + boldItemView, + boldItemWysiwyg, + bulletListItemMarkup, + bulletListItemView, + bulletListItemWysiwyg, + checkboxItemMarkup, + checkboxItemView, + checkboxItemWysiwyg, + codeBlockItemMarkup, + codeBlockItemView, + codeBlockItemWysiwyg, + codeBlocksListItemView, + codeItemMarkup, + codeItemView, + codeItemWysiwyg, + colorifyItemMarkup, + colorifyItemView, + colorifyItemWysiwyg, + cutItemMarkup, + cutItemView, + cutItemWysiwyg, + emojiItemMarkup, + emojiItemView, + emojiItemWysiwyg, + fileItemMarkup, + fileItemView, + fileItemWysiwyg, + filePopupItemView, + heading1ItemMarkup, + heading1ItemView, + heading1ItemWysiwyg, + heading2ItemMarkup, + heading2ItemView, + heading2ItemWysiwyg, + heading3ItemMarkup, + heading3ItemView, + heading3ItemWysiwyg, + heading4ItemMarkup, + heading4ItemView, + heading4ItemWysiwyg, + heading5ItemMarkup, + heading5ItemView, + heading5ItemWysiwyg, + heading6ItemMarkup, + heading6ItemView, + heading6ItemWysiwyg, + headingListItemView, + hruleItemMarkup, + hruleItemView, + hruleItemWysiwyg, + imageItemMarkup, + imageItemView, + imageItemWysiwyg, + imagePopupItemView, + italicItemMarkup, + italicItemView, + italicItemWysiwyg, + liftListItemMarkup, + liftListItemView, + liftListItemWysiwyg, + linkItemMarkup, + linkItemView, + linkItemWysiwyg, + listsListItemView, + markedItemMarkup, + markedItemView, + markedItemWysiwyg, + monospaceItemMarkup, + monospaceItemView, + monospaceItemWysiwyg, + noteItemMarkup, + noteItemView, + noteItemWysiwyg, + orderedListItemMarkup, + orderedListItemView, + orderedListItemWysiwyg, + paragraphItemMarkup, + paragraphItemView, + paragraphItemWisywig, + quoteItemMarkup, + quoteItemView, + quoteItemWysiwyg, + redoItemMarkup, + redoItemView, + redoItemWysiwyg, + sinkListItemMarkup, + sinkListItemView, + sinkListItemWysiwyg, + strikethroughItemMarkup, + strikethroughItemView, + tableItemMarkup, + tableItemView, + tableItemWysiwyg, + underlineItemMarkup, + underlineItemView, + underlineItemWysiwyg, + undoItemMarkup, + undoItemView, + undoItemWysiwyg, +} from './items'; +import {ToolbarsPreset} from './types'; + +// presets +export const zero: ToolbarsPreset = { + items: { + [Action.undo]: { + view: undoItemView, + wysiwyg: undoItemWysiwyg, + markup: undoItemMarkup, + }, + [Action.redo]: { + view: redoItemView, + wysiwyg: redoItemWysiwyg, + markup: redoItemMarkup, + }, + }, + orders: { + [Toolbar.wysiwygMain]: [[Action.undo, Action.redo]], + [Toolbar.markupMain]: [[Action.undo, Action.redo]], + }, +}; + +export const commonmark: ToolbarsPreset = { + items: { + ...zero.items, + [Action.bold]: { + view: boldItemView, + wysiwyg: boldItemWysiwyg, + markup: boldItemMarkup, + }, + [Action.italic]: { + view: italicItemView, + wysiwyg: italicItemWysiwyg, + markup: italicItemMarkup, + }, + [List.heading]: { + view: headingListItemView, + }, + [Action.paragraph]: { + view: paragraphItemView, + wysiwyg: paragraphItemWisywig, + markup: paragraphItemMarkup, + }, + [Action.heading1]: { + view: heading1ItemView, + wysiwyg: heading1ItemWysiwyg, + markup: heading1ItemMarkup, + }, + [Action.heading2]: { + view: heading2ItemView, + wysiwyg: heading2ItemWysiwyg, + markup: heading2ItemMarkup, + }, + [Action.heading3]: { + view: heading3ItemView, + wysiwyg: heading3ItemWysiwyg, + markup: heading3ItemMarkup, + }, + [Action.heading4]: { + view: heading4ItemView, + wysiwyg: heading4ItemWysiwyg, + markup: heading4ItemMarkup, + }, + [Action.heading5]: { + view: heading5ItemView, + wysiwyg: heading5ItemWysiwyg, + markup: heading5ItemMarkup, + }, + [Action.heading6]: { + view: heading6ItemView, + wysiwyg: heading6ItemWysiwyg, + markup: heading6ItemMarkup, + }, + [List.lists]: { + view: listsListItemView, + }, + [Action.bulletList]: { + view: bulletListItemView, + wysiwyg: bulletListItemWysiwyg, + markup: bulletListItemMarkup, + }, + [Action.orderedList]: { + view: orderedListItemView, + wysiwyg: orderedListItemWysiwyg, + markup: orderedListItemMarkup, + }, + [Action.sinkListItem]: { + view: sinkListItemView, + wysiwyg: sinkListItemWysiwyg, + markup: sinkListItemMarkup, + }, + [Action.liftListItem]: { + view: liftListItemView, + wysiwyg: liftListItemWysiwyg, + markup: liftListItemMarkup, + }, + [Action.link]: { + view: linkItemView, + wysiwyg: linkItemWysiwyg, + markup: linkItemMarkup, + }, + [Action.quote]: { + view: quoteItemView, + wysiwyg: quoteItemWysiwyg, + markup: quoteItemMarkup, + }, + [List.code]: { + view: codeBlocksListItemView, + }, + [Action.codeInline]: { + view: codeItemView, + wysiwyg: codeItemWysiwyg, + markup: codeItemMarkup, + }, + [Action.codeBlock]: { + view: codeBlockItemView, + wysiwyg: codeBlockItemWysiwyg, + markup: codeBlockItemMarkup, + }, + [Action.horizontalRule]: { + view: hruleItemView, + wysiwyg: hruleItemWysiwyg, + markup: hruleItemMarkup, + }, + }, + orders: { + [Toolbar.wysiwygMain]: [ + [Action.undo, Action.redo], + [Action.bold, Action.italic], + [ + { + id: List.heading, + items: [ + Action.paragraph, + Action.heading1, + Action.heading2, + Action.heading3, + Action.heading4, + Action.heading5, + Action.heading6, + ], + }, + { + id: List.lists, + items: [ + Action.bulletList, + Action.orderedList, + Action.sinkListItem, + Action.liftListItem, + ], + }, + Action.link, + Action.quote, + { + id: List.code, + items: [Action.codeInline, Action.codeBlock], + }, + ], + ], + [Toolbar.markupMain]: [ + [Action.undo, Action.redo], + [Action.bold, Action.italic], + [ + { + id: List.heading, + items: [ + Action.paragraph, + Action.heading1, + Action.heading2, + Action.heading3, + Action.heading4, + Action.heading5, + Action.heading6, + ], + }, + { + id: List.lists, + items: [ + Action.bulletList, + Action.orderedList, + Action.sinkListItem, + Action.liftListItem, + ], + }, + Action.link, + Action.quote, + { + id: List.code, + items: [Action.codeInline, Action.codeBlock], + }, + ], + ], + [Toolbar.wysiwygHidden]: [[Action.horizontalRule]], + [Toolbar.markupHidden]: [[Action.horizontalRule]], + }, +}; + +export const defaultPreset: ToolbarsPreset = { + items: { + ...commonmark.items, + [Action.strike]: { + view: strikethroughItemView, + wysiwyg: sinkListItemWysiwyg, + markup: strikethroughItemMarkup, + }, + }, + orders: { + [Toolbar.wysiwygMain]: [ + [Action.undo, Action.redo], + [Action.bold, Action.italic, Action.strike], + [ + { + id: List.heading, + items: [ + Action.paragraph, + Action.heading1, + Action.heading2, + Action.heading3, + Action.heading4, + Action.heading5, + Action.heading6, + ], + }, + { + id: List.lists, + items: [ + Action.bulletList, + Action.orderedList, + Action.sinkListItem, + Action.liftListItem, + ], + }, + Action.link, + Action.quote, + { + id: List.code, + items: [Action.codeInline, Action.codeBlock], + }, + ], + ], + [Toolbar.markupMain]: [ + [Action.undo, Action.redo], + [Action.bold, Action.italic, Action.strike], + [ + { + id: List.heading, + items: [ + Action.paragraph, + Action.heading1, + Action.heading2, + Action.heading3, + Action.heading4, + Action.heading5, + Action.heading6, + ], + }, + { + id: List.lists, + items: [ + Action.bulletList, + Action.orderedList, + Action.sinkListItem, + Action.liftListItem, + ], + }, + Action.link, + Action.quote, + { + id: List.code, + items: [Action.codeInline, Action.codeBlock], + }, + ], + ], + [Toolbar.wysiwygHidden]: [[Action.horizontalRule]], + [Toolbar.markupHidden]: [[Action.horizontalRule]], + }, +}; + +export const yfm: ToolbarsPreset = { + items: { + ...defaultPreset.items, + [Action.underline]: { + view: underlineItemView, + wysiwyg: underlineItemWysiwyg, + markup: underlineItemMarkup, + }, + [Action.mono]: { + view: monospaceItemView, + wysiwyg: monospaceItemWysiwyg, + markup: monospaceItemMarkup, + }, + [Action.note]: { + view: noteItemView, + wysiwyg: noteItemWysiwyg, + markup: noteItemMarkup, + }, + [Action.cut]: { + view: cutItemView, + wysiwyg: cutItemWysiwyg, + markup: cutItemMarkup, + }, + [Action.image]: { + view: imageItemView, + wysiwyg: imageItemWysiwyg, + }, + [Action.imagePopup]: { + view: imagePopupItemView, + markup: imageItemMarkup, + }, + [Action.file]: { + view: fileItemView, + wysiwyg: fileItemWysiwyg, + }, + [Action.filePopup]: { + view: filePopupItemView, + markup: fileItemMarkup, + }, + [Action.table]: { + view: tableItemView, + wysiwyg: tableItemWysiwyg, + markup: tableItemMarkup, + }, + [Action.checkbox]: { + view: checkboxItemView, + wysiwyg: checkboxItemWysiwyg, + markup: checkboxItemMarkup, + }, + [Action.tabs]: { + view: tableItemView, + wysiwyg: tableItemWysiwyg, + markup: tableItemMarkup, + }, + }, + orders: { + [Toolbar.wysiwygMain]: [ + [Action.undo, Action.redo], + [Action.bold, Action.italic, Action.underline, Action.strike, Action.mono], + [ + { + id: List.heading, + items: [ + Action.paragraph, + Action.heading1, + Action.heading2, + Action.heading3, + Action.heading4, + Action.heading5, + Action.heading6, + ], + }, + { + id: List.lists, + items: [ + Action.bulletList, + Action.orderedList, + Action.sinkListItem, + Action.liftListItem, + ], + }, + Action.link, + Action.note, + Action.cut, + Action.quote, + { + id: List.code, + items: [Action.codeInline, Action.codeBlock], + }, + ], + [Action.image, Action.file, Action.table, Action.checkbox], + ], + [Toolbar.markupMain]: [ + [Action.undo, Action.redo], + [Action.bold, Action.italic, Action.underline, Action.strike, Action.mono], + [ + { + id: List.heading, + items: [ + Action.paragraph, + Action.heading1, + Action.heading2, + Action.heading3, + Action.heading4, + Action.heading5, + Action.heading6, + ], + }, + { + id: List.lists, + items: [ + Action.bulletList, + Action.orderedList, + Action.sinkListItem, + Action.liftListItem, + ], + }, + Action.link, + Action.note, + Action.cut, + Action.quote, + { + id: List.code, + items: [Action.codeInline, Action.codeBlock], + }, + ], + [Action.imagePopup, Action.filePopup, Action.table, Action.checkbox], + ], + [Toolbar.wysiwygHidden]: [[Action.horizontalRule, Action.tabs]], + [Toolbar.markupHidden]: [[Action.horizontalRule, Action.tabs]], + }, +}; + +export const full: ToolbarsPreset = { + items: { + ...yfm.items, + [Action.mark]: { + view: markedItemView, + wysiwyg: markedItemWysiwyg, + markup: markedItemMarkup, + }, + [Action.colorify]: { + view: colorifyItemView, + wysiwyg: colorifyItemWysiwyg, + markup: colorifyItemMarkup, + }, + [Action.emoji]: { + view: emojiItemView, + wysiwyg: emojiItemWysiwyg, + markup: emojiItemMarkup, + }, + }, + orders: { + [Toolbar.wysiwygMain]: [ + [Action.undo, Action.redo], + [Action.bold, Action.italic, Action.underline, Action.strike, Action.mono, Action.mark], + [ + { + id: List.heading, + items: [ + Action.paragraph, + Action.heading1, + Action.heading2, + Action.heading3, + Action.heading4, + Action.heading5, + Action.heading6, + ], + }, + { + id: List.lists, + items: [ + Action.bulletList, + Action.orderedList, + Action.sinkListItem, + Action.liftListItem, + ], + }, + Action.colorify, + Action.link, + Action.note, + Action.cut, + Action.quote, + { + id: List.code, + items: [Action.codeInline, Action.codeBlock], + }, + ], + [Action.image, Action.file, Action.table, Action.checkbox], + ], + [Toolbar.markupMain]: [ + [Action.undo, Action.redo], + [Action.bold, Action.italic, Action.underline, Action.strike, Action.mono, Action.mark], + [ + { + id: List.heading, + items: [ + Action.paragraph, + Action.heading1, + Action.heading2, + Action.heading3, + Action.heading4, + Action.heading5, + Action.heading6, + ], + }, + { + id: List.lists, + items: [ + Action.bulletList, + Action.orderedList, + Action.sinkListItem, + Action.liftListItem, + ], + }, + Action.colorify, + Action.link, + Action.note, + Action.cut, + Action.quote, + { + id: List.code, + items: [Action.codeInline, Action.codeBlock], + }, + ], + [Action.imagePopup, Action.filePopup, Action.table, Action.checkbox], + ], + [Toolbar.wysiwygHidden]: [[Action.horizontalRule, Action.emoji, Action.tabs]], + [Toolbar.markupHidden]: [[Action.horizontalRule, Action.emoji, Action.tabs]], + }, +}; diff --git a/src/modules/toolbars/types.ts b/src/modules/toolbars/types.ts new file mode 100644 index 00000000..ec3e2de6 --- /dev/null +++ b/src/modules/toolbars/types.ts @@ -0,0 +1,86 @@ +import type {RefObject} from 'react'; + +import type {HotkeyProps} from '@gravity-ui/uikit'; +import type {EditorState} from 'prosemirror-state'; + +import type {ActionStorage} from '../../core'; +import type {CodeEditor} from '../../markup'; +import type {ToolbarBaseProps, ToolbarDataType, ToolbarIconData} from '../../toolbar'; + +// Items +export type ToolbarItemId = string & {}; +export type ToolbarListId = string & {}; + +export interface ToolbarList { + id: ToolbarListId; + items: ToolbarItemId[]; +} + +/** + * The default value for the `type` property is `ToolbarDataType.SingleButton`. + */ +export type ToolbarItemView = { + className?: string; + hint?: string | (() => string); + hotkey?: HotkeyProps['value']; + type?: ToolbarDataType; + doNotActivateList?: boolean; +} & (T extends ToolbarDataType.SingleButton + ? { + icon: ToolbarIconData; + title: string | (() => string); + } + : T extends ToolbarDataType.ListButton + ? { + withArrow?: boolean; + icon: ToolbarIconData; + title: string | (() => string); + } + : {}); + +export interface EditorActions { + exec(editor: E): void; + isActive(editor: E): boolean; + isEnable(editor: E): boolean; +} + +type ToolbarItemEditor = Partial> & { + hintWhenDisabled?: boolean | string | (() => string); + condition?: ((state: EditorState) => void) | 'enabled'; +} & (T extends ToolbarDataType.ButtonPopup + ? { + renderPopup: ( + props: ToolbarBaseProps & { + hide: () => void; + anchorRef: RefObject; + }, + ) => React.ReactNode; + } + : T extends ToolbarDataType.ReactComponent + ? { + width: number; + component: React.ComponentType>; + } + : {}); + +export type ToolbarItemWysiwyg = + ToolbarItemEditor; +export type ToolbarItemMarkup = + ToolbarItemEditor; + +export type ToolbarItem = { + view: ToolbarItemView; + wysiwyg?: ToolbarItemWysiwyg; + markup?: ToolbarItemMarkup; +}; +export type ToolbarsItems = Record>; + +// Orders +export type ToolbarId = string; +export type ToolbarOrders = (ToolbarList | ToolbarItemId)[][]; +export type ToolbarsOrders = Record; + +export interface ToolbarsPreset { + items: ToolbarsItems; + orders: ToolbarsOrders; +} diff --git a/src/toolbar/types.ts b/src/toolbar/types.ts index 50da041c..4061c797 100644 --- a/src/toolbar/types.ts +++ b/src/toolbar/types.ts @@ -43,7 +43,9 @@ export enum ToolbarDataType { SingleButton = 's-button', ListButton = 'list-b', ButtonPopup = 'b-popup', + /** @deprecated Use ReactComponent type instead */ ReactNode = 'r-node', + /** @deprecated Use ReactComponent type instead */ ReactNodeFn = 'r-node-fn', ReactComponent = 'r-component', } @@ -90,6 +92,9 @@ export type ToolbarListButtonItemData = ToolbarItemData & { doNotActivateList?: boolean; }; +/** + * @deprecated Use ReactComponent type instead + * */ export type ToolbarReactNodeData = { id: string; type: ToolbarDataType.ReactNode; @@ -97,6 +102,9 @@ export type ToolbarReactNodeData = { content: React.ReactNode; }; +/** + * @deprecated Use ReactComponent type instead + * */ export type ToolbarReactNodeFnData = { id: string; type: ToolbarDataType.ReactNodeFn; From db1d1cabd85c317bcd1bd63ff8c56d3af0e5dbb2 Mon Sep 17 00:00:00 2001 From: Sergey Makhnatkin Date: Fri, 20 Dec 2024 10:03:03 +0100 Subject: [PATCH 3/4] chore(docs, demo): updated stories names, updated README (#532) --- .storybook/preview.ts | 2 +- README-ru.md | 14 +++++++++----- README.md | 14 ++++++++------ .../stories/css-variables/CSSVariables.stories.tsx | 2 +- docs/how-to-add-editor-with-create-react-app.md | 2 +- docs/how-to-add-editor-with-nextjs.md | 2 +- docs/how-to-add-preview.md | 2 +- docs/how-to-connect-gpt-extensions.md | 2 +- docs/how-to-connect-html-extension.md | 2 +- docs/how-to-connect-latex-extension.md | 2 +- docs/how-to-connect-mermaid-extension.md | 2 +- docs/how-to-customize-the-editor.md | 2 +- 12 files changed, 27 insertions(+), 21 deletions(-) diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 875a0042..163d052f 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -12,7 +12,7 @@ const preview: Preview = { }, options: { storySort: { - order: ['Playground', 'Docs', 'Extensions', ['Presets', '*'], '*'], + order: ['Playground', 'Docs', 'Extensions', 'Settings', ['Presets', '*'], '*'], }, }, controls: { diff --git a/README-ru.md b/README-ru.md index 6c8269e5..4047d071 100644 --- a/README-ru.md +++ b/README-ru.md @@ -51,6 +51,7 @@ function Editor({onSubmit}) { return ; } ``` + Полезные ссылки: - [Как подключить редактор в Create React App](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-install-create-react-app--docs) - [Как добавить предварительный просмотр для режима разметки](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-develop-preview--docs) @@ -61,7 +62,13 @@ function Editor({onSubmit}) { - [Как добавить расширение GPT](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-connect-gpt--docs) - [Как добавить расширение привязки текста в Markdown](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-develop-extension-with-popup--docs) +### Разработка + +Для запуска Storybook в режиме разработки выполните следующую команду: +```shell +npm start +``` ### i18n @@ -77,10 +84,7 @@ configure({ Обязательно сделайте вызов `configure()` из [UIKit](https://github.com/gravity-ui/uikit?tab=readme-ov-file#i18n) и других UI-библиотек. -## Разработка -Для запуска Storybook в режиме разработки выполните следующую команду: +### Участие в разработке -```shell -npm start -``` +- [Информация для контрибьюетров](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-contributing--docs) diff --git a/README.md b/README.md index bfbea225..6f435e18 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,12 @@ Read more: - [How to add GPT extension](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-connect-gpt--docs) - [How to add text binding extension in markdown](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-develop-extension-with-popup--docs) +### Development +To start the dev storybook + +```shell +npm start +``` ### i18n @@ -77,10 +83,6 @@ configure({ Don't forget to call `configure()` from [UIKit](https://github.com/gravity-ui/uikit?tab=readme-ov-file#i18n) and other UI libraries. -## Development +### Contributing -To start the dev storybook - -```shell -npm start -``` +- [Contributor Guidelines](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-contributing--docs) diff --git a/demo/stories/css-variables/CSSVariables.stories.tsx b/demo/stories/css-variables/CSSVariables.stories.tsx index 89a7faf1..17de4ed0 100644 --- a/demo/stories/css-variables/CSSVariables.stories.tsx +++ b/demo/stories/css-variables/CSSVariables.stories.tsx @@ -33,6 +33,6 @@ export const Story: StoryObj = { Story.storyName = 'Custom CSS Variables'; export default { - title: 'Experiments / Custom CSS Variables', + title: 'Settings / Custom CSS Variables', component, }; diff --git a/docs/how-to-add-editor-with-create-react-app.md b/docs/how-to-add-editor-with-create-react-app.md index 104bfa90..97f3fb6e 100644 --- a/docs/how-to-add-editor-with-create-react-app.md +++ b/docs/how-to-add-editor-with-create-react-app.md @@ -1,4 +1,4 @@ -##### Install / Create react app +##### Getting started / Create react app ## Installation Guide diff --git a/docs/how-to-add-editor-with-nextjs.md b/docs/how-to-add-editor-with-nextjs.md index 31741db4..c677a9c7 100644 --- a/docs/how-to-add-editor-with-nextjs.md +++ b/docs/how-to-add-editor-with-nextjs.md @@ -1,4 +1,4 @@ -##### Install / NextJS +##### Getting started / NextJS ## Connection and Configuration This document provides instructions for configuring Webpack and Turbopack to avoid issues related to the 'fs' module and for connecting the editor on the nextjs client side. diff --git a/docs/how-to-add-preview.md b/docs/how-to-add-preview.md index 42c94788..e7bdb5ff 100644 --- a/docs/how-to-add-preview.md +++ b/docs/how-to-add-preview.md @@ -1,4 +1,4 @@ -##### Develop / Preview +##### Getting started / Preview ## How to Add Preview for Markup Mode diff --git a/docs/how-to-connect-gpt-extensions.md b/docs/how-to-connect-gpt-extensions.md index 97bd75b3..d09a2721 100644 --- a/docs/how-to-connect-gpt-extensions.md +++ b/docs/how-to-connect-gpt-extensions.md @@ -1,4 +1,4 @@ -##### Connect / GPT +##### Extensions / GPT ## How to connect GPT extensions to editor diff --git a/docs/how-to-connect-html-extension.md b/docs/how-to-connect-html-extension.md index fde3961d..b0dca349 100644 --- a/docs/how-to-connect-html-extension.md +++ b/docs/how-to-connect-html-extension.md @@ -1,4 +1,4 @@ -##### Connect / Html block +##### Extensions / Html block ## How to Connect the HTML Extension in the Editor diff --git a/docs/how-to-connect-latex-extension.md b/docs/how-to-connect-latex-extension.md index 39558aa0..9a4047bc 100644 --- a/docs/how-to-connect-latex-extension.md +++ b/docs/how-to-connect-latex-extension.md @@ -1,4 +1,4 @@ -##### Connect / Latex extension +##### Extensions / Latex extension ## How to Connect the Latex Extension in the Editor diff --git a/docs/how-to-connect-mermaid-extension.md b/docs/how-to-connect-mermaid-extension.md index c3190054..aae109a8 100644 --- a/docs/how-to-connect-mermaid-extension.md +++ b/docs/how-to-connect-mermaid-extension.md @@ -1,4 +1,4 @@ -##### Connect / Mermaid Extension +##### Extensions / Mermaid Extension ## How to Connect the Mermaid Extension in the Editor diff --git a/docs/how-to-customize-the-editor.md b/docs/how-to-customize-the-editor.md index 3ae64cc7..30abd9ab 100644 --- a/docs/how-to-customize-the-editor.md +++ b/docs/how-to-customize-the-editor.md @@ -1,4 +1,4 @@ -##### Develop / Editor customization +##### Getting started / Editor customization ## How to customize the editor You can use CSS variables to make editor contents fit your own needs From 300bfdf6d383a6e0452dc098579561f0144fc459 Mon Sep 17 00:00:00 2001 From: Gravity UI Bot <111915794+gravity-ui-bot@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:56:20 +0300 Subject: [PATCH 4/4] chore(main): release 14.9.0 (#529) --- CHANGELOG.md | 13 +++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4c24db0..a25b387f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [14.9.0](https://github.com/gravity-ui/markdown-editor/compare/v14.8.0...v14.9.0) (2024-12-20) + + +### Features + +* **markup:** smart re-indent on paste ([#530](https://github.com/gravity-ui/markdown-editor/issues/530)) ([15767d7](https://github.com/gravity-ui/markdown-editor/commit/15767d7d3b0334126e34149d811ce6b6d62909d2)) +* **toolbars:** restructured toolbar configuration and presets ([#509](https://github.com/gravity-ui/markdown-editor/issues/509)) ([3ebf14f](https://github.com/gravity-ui/markdown-editor/commit/3ebf14fd580ce29dc0133715cd2cb6bb6ea4ca8a)) + + +### Bug Fixes + +* **Link:** fixed pasting link to empty selection ([#528](https://github.com/gravity-ui/markdown-editor/issues/528)) ([bd52bee](https://github.com/gravity-ui/markdown-editor/commit/bd52bee93aceaf0af5bd9b8da284e93338b89a32)) + ## [14.8.0](https://github.com/gravity-ui/markdown-editor/compare/v14.7.0...v14.8.0) (2024-12-17) diff --git a/package-lock.json b/package-lock.json index cffa164d..64278275 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gravity-ui/markdown-editor", - "version": "14.8.0", + "version": "14.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gravity-ui/markdown-editor", - "version": "14.8.0", + "version": "14.9.0", "license": "MIT", "dependencies": { "@bem-react/classname": "^1.6.0", diff --git a/package.json b/package.json index 1c280ed6..cb78b7a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gravity-ui/markdown-editor", - "version": "14.8.0", + "version": "14.9.0", "description": "Markdown wysiwyg and markup editor", "license": "MIT", "repository": {