From 9126756fe5e241c6ab2badec4689b1df8f0009c3 Mon Sep 17 00:00:00 2001 From: Yuriy Demidov Date: Thu, 5 Dec 2024 12:05:59 +0300 Subject: [PATCH 01/15] fix(Cursor): input-rules does not work when cursor in virtual selection (GapCursorSelection) (#515) --- src/extensions/behavior/Cursor/gapcursor.ts | 19 +++++++++++++++++-- src/extensions/behavior/Cursor/index.ts | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/extensions/behavior/Cursor/gapcursor.ts b/src/extensions/behavior/Cursor/gapcursor.ts index 57bc969f..96724555 100644 --- a/src/extensions/behavior/Cursor/gapcursor.ts +++ b/src/extensions/behavior/Cursor/gapcursor.ts @@ -1,6 +1,6 @@ import {DOMSerializer} from 'prosemirror-model'; -import {EditorState, Plugin, PluginKey} from 'prosemirror-state'; -import {Decoration, DecorationSet, EditorView} from 'prosemirror-view'; +import {type EditorState, Plugin, PluginKey, TextSelection} from 'prosemirror-state'; +import {Decoration, DecorationSet, type EditorView} from 'prosemirror-view'; import {isNodeSelection} from '../../../utils/selection'; import {pType} from '../../base/BaseSchema'; @@ -32,6 +32,21 @@ export const gapCursor = () => }; }, props: { + handleKeyPress(view) { + const { + state, + state: {selection: sel}, + } = view; + if (isGapCursorSelection(sel)) { + // Replace GapCursorSelection with empty textblock before run all other handlers. + // This should be done before all inputRules and other handlers, that handle text input. + // Thus, entering text into a native textblock and into a "virtual" one – GapCursor – will be the same. + const tr = state.tr.replaceSelectionWith(pType(state.schema).create()); + tr.setSelection(TextSelection.create(tr.doc, sel.pos + 1)); + view.dispatch(tr.scrollIntoView()); + } + return false; + }, decorations: ({doc, selection}: EditorState) => { if (isGapCursorSelection(selection)) { const position = selection.head; diff --git a/src/extensions/behavior/Cursor/index.ts b/src/extensions/behavior/Cursor/index.ts index 08503a59..8b583279 100644 --- a/src/extensions/behavior/Cursor/index.ts +++ b/src/extensions/behavior/Cursor/index.ts @@ -11,6 +11,6 @@ export type CursorOptions = { }; export const Cursor: ExtensionAuto = (builder, opts) => { - builder.addPlugin(() => gapCursor()); + builder.addPlugin(() => gapCursor(), builder.Priority.Highest); builder.addPlugin(() => dropCursor(opts.dropOptions)); }; From 89c9881331df2b0fae5968258a29b9c9eed179ef Mon Sep 17 00:00:00 2001 From: ReFFaT <102167552+ReFFaT@users.noreply.github.com> Date: Sun, 8 Dec 2024 22:39:06 +0300 Subject: [PATCH 02/15] fix: Gpt extension render (#519) --- demo/stories/gpt/GPT.tsx | 15 ++++++--------- demo/stories/gpt/gptWidgetOptions.tsx | 7 ++----- docs/how-to-connect-gpt-extensions.md | 15 +++++++-------- src/bundle/config/wysiwyg.ts | 12 +----------- src/extensions/additional/GPT/index.ts | 2 +- .../GPT/{toolbar.ts => wGptItemData.ts} | 2 +- 6 files changed, 18 insertions(+), 35 deletions(-) rename src/extensions/additional/GPT/{toolbar.ts => wGptItemData.ts} (93%) diff --git a/demo/stories/gpt/GPT.tsx b/demo/stories/gpt/GPT.tsx index 01635830..680d9a95 100644 --- a/demo/stories/gpt/GPT.tsx +++ b/demo/stories/gpt/GPT.tsx @@ -3,13 +3,12 @@ import React, {useState} from 'react'; import cloneDeep from 'lodash/cloneDeep'; import { - type MarkupString, gptExtension, logger, mGptExtension, mGptToolbarItem, markupToolbarConfigs, - wGptToolbarItem, + wGptItemData, wysiwygToolbarConfigs, } from '../../../src'; import {Playground} from '../../components/Playground'; @@ -18,7 +17,7 @@ import {initialMdContent} from './content'; import {gptWidgetProps} from './gptWidgetOptions'; const wToolbarConfig = cloneDeep(wysiwygToolbarConfigs.wToolbarConfig); -wToolbarConfig.unshift([wGptToolbarItem]); +wToolbarConfig.unshift([wGptItemData]); wToolbarConfig.push([ wysiwygToolbarConfigs.wMermaidItemData, wysiwygToolbarConfigs.wYfmHtmlBlockItemData, @@ -37,7 +36,7 @@ const wCommandMenuConfig = wysiwygToolbarConfigs.wCommandMenuConfig.concat( wysiwygToolbarConfigs.wYfmHtmlBlockItemData, ); -wCommandMenuConfig.unshift(wysiwygToolbarConfigs.wGptItemData); +wCommandMenuConfig.unshift(wGptItemData); const mToolbarConfig = cloneDeep(markupToolbarConfigs.mToolbarConfig); @@ -49,11 +48,9 @@ mToolbarConfig.push([ mToolbarConfig.unshift([mGptToolbarItem]); export const GPT = React.memo(() => { - const [yfmRaw, setYfmRaw] = React.useState(initialMdContent); - const [showedAlertGpt, setShowedAlertGpt] = useState(true); - const gptExtensionProps = gptWidgetProps(setYfmRaw, { + const gptExtensionProps = gptWidgetProps({ showedGptAlert: Boolean(showedAlertGpt), onCloseGptAlert: () => { setShowedAlertGpt(false); @@ -61,12 +58,12 @@ export const GPT = React.memo(() => { }); const markupExtension = mGptExtension(gptExtensionProps); - const wSelectionMenuConfig = [[wGptToolbarItem], ...wysiwygToolbarConfigs.wSelectionMenuConfig]; + const wSelectionMenuConfig = [[wGptItemData], ...wysiwygToolbarConfigs.wSelectionMenuConfig]; return ( builder.use(gptExtension, gptExtensionProps)} wysiwygCommandMenuConfig={wCommandMenuConfig} extensionOptions={{selectionContext: {config: wSelectionMenuConfig}}} diff --git a/demo/stories/gpt/gptWidgetOptions.tsx b/demo/stories/gpt/gptWidgetOptions.tsx index ce1fdcdf..bc21ad7b 100644 --- a/demo/stories/gpt/gptWidgetOptions.tsx +++ b/demo/stories/gpt/gptWidgetOptions.tsx @@ -30,7 +30,6 @@ const gptRequestHandler = async ({ }; export const gptWidgetProps = ( - setYfmRaw: (yfmRaw: string) => void, gptAlertProps?: GptWidgetOptions['gptAlertProps'], ): GptWidgetOptions => { return { @@ -64,12 +63,10 @@ export const gptWidgetProps = ( onLike: async () => {}, onDislike: async () => {}, onApplyResult: (markup) => { - setYfmRaw(markup); + console.log('onApplyResult:', markup); }, onUpdate: (event) => { - if (event?.rawText) { - setYfmRaw(event.rawText); - } + console.log('update:', event); }, }; }; diff --git a/docs/how-to-connect-gpt-extensions.md b/docs/how-to-connect-gpt-extensions.md index 15d3889d..97bd75b3 100644 --- a/docs/how-to-connect-gpt-extensions.md +++ b/docs/how-to-connect-gpt-extensions.md @@ -151,7 +151,7 @@ Add in tool bar ```ts import { ... - wGptToolbarItem, + wGptItemData, wysiwygToolbarConfigs, } from '@gravity-ui/markdown-editor'; @@ -160,7 +160,7 @@ import {cloneDeep} from '@gravity-ui/markdown-editor/_/lodash'; export const Editor: React.FC = (props) => { ... const wToolbarConfig = cloneDeep(wysiwygToolbarConfigs.wToolbarConfig); - wToolbarConfig.unshift([wGptToolbarItem]); + wToolbarConfig.unshift([wGptItemData]); ... @@ -176,7 +176,7 @@ Add in menu bar ```ts export const Editor: React.FC = (props) => { ... - const wSelectionMenuConfig = [[wGptToolbarItem], ...wysiwygToolbarConfigs.wSelectionMenuConfig]; + const wSelectionMenuConfig = [[wGptItemData], ...wysiwygToolbarConfigs.wSelectionMenuConfig]; const mdEditor = useMarkdownEditor({ ... @@ -195,7 +195,7 @@ Add in command menu config (/) export const Editor: React.FC = (props) => { ... const wCommandMenuConfig = wysiwygToolbarConfigs.wCommandMenuConfig // main commands - wCommandMenuConfig.unshift(wysiwygToolbarConfigs.wGptItemData); // add GPT command + wCommandMenuConfig.unshift(wGptItemData); // add GPT command const mdEditor = useMarkdownEditor({ ... @@ -217,7 +217,6 @@ import React from 'react'; import { gptExtension, MarkdownEditorView, - wGptToolbarItem, wysiwygToolbarConfigs, useMarkdownEditor, } from '@gravity-ui/markdown-editor'; @@ -227,12 +226,12 @@ import {gptWidgetProps} from './gptWidgetProps'; export const Editor: React.FC = (props) => { const wToolbarConfig = cloneDeep(wysiwygToolbarConfigs.wToolbarConfig); - wToolbarConfig.unshift([wGptToolbarItem]); + wToolbarConfig.unshift([wGptItemData]); - const wSelectionMenuConfig = [[wGptToolbarItem], ...wysiwygToolbarConfigs.wSelectionMenuConfig]; + const wSelectionMenuConfig = [[wGptItemData], ...wysiwygToolbarConfigs.wSelectionMenuConfig]; const wCommandMenuConfig = wysiwygToolbarConfigs.wCommandMenuConfig // main commands - wCommandMenuConfig.unshift(wysiwygToolbarConfigs.wGptItemData); // add GPT command + wCommandMenuConfig.unshift(wGptItemData); // add GPT command const mdEditor = useMarkdownEditor({ // ... diff --git a/src/bundle/config/wysiwyg.ts b/src/bundle/config/wysiwyg.ts index 41e0abd8..bc761b6a 100644 --- a/src/bundle/config/wysiwyg.ts +++ b/src/bundle/config/wysiwyg.ts @@ -1,7 +1,6 @@ import {ActionStorage} from 'src/core'; import {headingType, pType} from '../../extensions'; -import {gptHotKeys} from '../../extensions/additional/GPT/constants'; // for typings from Math import type {} from '../../extensions/additional/Math'; import type { @@ -244,16 +243,7 @@ export const wYfmHtmlBlockItemData: WToolbarSingleItemData = { isActive: (e) => e.actions.createYfmHtmlBlock.isActive(), isEnable: (e) => e.actions.createYfmHtmlBlock.isEnable(), }; -export const wGptItemData: WToolbarSingleItemData = { - id: ActionName.gpt, - type: ToolbarDataType.SingleButton, - title: i18n.bind(null, 'gpt'), - hotkey: gptHotKeys.openGptKeyTooltip, - icon: icons.gpt, - exec: (e) => e.actions.addGptWidget.run({}), - isActive: (e) => e.actions.addGptWidget.isActive(), - isEnable: (e) => e.actions.addGptWidget.isEnable(), -}; + export const wMermaidItemData: WToolbarSingleItemData = { id: ActionName.mermaid, type: ToolbarDataType.SingleButton, diff --git a/src/extensions/additional/GPT/index.ts b/src/extensions/additional/GPT/index.ts index 314f5100..40252beb 100644 --- a/src/extensions/additional/GPT/index.ts +++ b/src/extensions/additional/GPT/index.ts @@ -1,3 +1,3 @@ -export * from './toolbar'; +export * from './wGptItemData'; export * from './gptExtension/gptExtension'; export * from './MarkupGpt'; diff --git a/src/extensions/additional/GPT/toolbar.ts b/src/extensions/additional/GPT/wGptItemData.ts similarity index 93% rename from src/extensions/additional/GPT/toolbar.ts rename to src/extensions/additional/GPT/wGptItemData.ts index c5708595..64a1d67f 100644 --- a/src/extensions/additional/GPT/toolbar.ts +++ b/src/extensions/additional/GPT/wGptItemData.ts @@ -8,7 +8,7 @@ import {gptHotKeys} from './constants'; export const cnGptButton = cn('gpt-button'); -export const wGptToolbarItem: WToolbarSingleItemData = { +export const wGptItemData: WToolbarSingleItemData = { type: ToolbarDataType.SingleButton, id: 'gpt', title: () => `${i18n('help-with-text')}`, From 54ac0d36499e572844e42cfff8f7781387731b00 Mon Sep 17 00:00:00 2001 From: Yuriy Demidov Date: Tue, 10 Dec 2024 15:58:23 +0300 Subject: [PATCH 03/15] feat(bundle): update view of text color action item in toolbar (#514) --- .../toolbar/ToolbarButtonWithPopupMenu.tsx | 25 +++++++++++++++---- src/bundle/toolbar/custom/ToolbarColors.scss | 9 +++++++ src/bundle/toolbar/custom/ToolbarColors.tsx | 3 +++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/bundle/toolbar/ToolbarButtonWithPopupMenu.tsx b/src/bundle/toolbar/ToolbarButtonWithPopupMenu.tsx index f225d53e..03d0afdd 100644 --- a/src/bundle/toolbar/ToolbarButtonWithPopupMenu.tsx +++ b/src/bundle/toolbar/ToolbarButtonWithPopupMenu.tsx @@ -26,8 +26,12 @@ export type MenuItem = { export type ToolbarButtonWithPopupMenuProps = Omit< ToolbarBaseProps & { icon: ToolbarIconData; + iconClassName?: string; + chevronIconClassName?: string; title: string | (() => string); menuItems: MenuItem[]; + /** @default 'classic' */ + _selectionType?: 'classic' | 'light'; }, 'editor' >; @@ -37,8 +41,11 @@ export const ToolbarButtonWithPopupMenu: React.FC { const buttonRef = React.useRef(null); const [open, , hide, toggleOpen] = useBooleanState(false); @@ -48,7 +55,7 @@ export const ToolbarButtonWithPopupMenu: React.FC ({...i, group: i.group || ''})), 'group', ), - [menuItems, groupBy], + [menuItems], ); const someActive = menuItems.some( @@ -64,6 +71,14 @@ export const ToolbarButtonWithPopupMenu: React.FC - + {''} - + diff --git a/src/bundle/toolbar/custom/ToolbarColors.scss b/src/bundle/toolbar/custom/ToolbarColors.scss index 1b573159..e5e8bcd4 100644 --- a/src/bundle/toolbar/custom/ToolbarColors.scss +++ b/src/bundle/toolbar/custom/ToolbarColors.scss @@ -2,10 +2,19 @@ $colors: ('gray', 'yellow', 'orange', 'red', 'green', 'blue', 'violet'); .g-md-toolbar-colors { @each $name in $colors { + &__menu-icon_color_#{$name} { + color: var(--yfm-colorify-#{$name}); + } + + &__chevron-icon_color_#{$name} { + color: var(--yfm-colorify-#{$name}); + } + &__item-icon_color_#{$name} { color: var(--yfm-colorify-#{$name}); } } + &__item-icon_color_default { color: var(--g-color-text-primary); } diff --git a/src/bundle/toolbar/custom/ToolbarColors.tsx b/src/bundle/toolbar/custom/ToolbarColors.tsx index 68b087a5..1633e6c9 100644 --- a/src/bundle/toolbar/custom/ToolbarColors.tsx +++ b/src/bundle/toolbar/custom/ToolbarColors.tsx @@ -77,6 +77,9 @@ export const ToolbarColors: React.FC = (props) => { title={i18n('colorify')} menuItems={items} icon={textColorIcon} + _selectionType="light" + iconClassName={b('menu-icon', {color: currentColor})} + chevronIconClassName={b('chevron-icon', {color: currentColor})} /> ); }; From dac5314615c3808a086ba33ebe19b0063fdb6ffd Mon Sep 17 00:00:00 2001 From: Gravity UI Bot <111915794+gravity-ui-bot@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:10:58 +0300 Subject: [PATCH 04/15] chore(main): release 14.6.0 (#513) --- CHANGELOG.md | 14 ++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5bfbbe5..d7adad77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [14.6.0](https://github.com/gravity-ui/markdown-editor/compare/v14.5.1...v14.6.0) (2024-12-10) + + +### Features + +* **bundle:** update view of text color action item in toolbar ([#514](https://github.com/gravity-ui/markdown-editor/issues/514)) ([54ac0d3](https://github.com/gravity-ui/markdown-editor/commit/54ac0d36499e572844e42cfff8f7781387731b00)) + + +### Bug Fixes + +* **Cursor:** input-rules does not work when cursor in virtual selection (GapCursorSelection) ([#515](https://github.com/gravity-ui/markdown-editor/issues/515)) ([9126756](https://github.com/gravity-ui/markdown-editor/commit/9126756fe5e241c6ab2badec4689b1df8f0009c3)) +* **deps:** bumped @lezer/markdown to fix large text hang ([#512](https://github.com/gravity-ui/markdown-editor/issues/512)) ([8a8fce8](https://github.com/gravity-ui/markdown-editor/commit/8a8fce8ff5f9603f6e755264fc474c03a36d6bb7)) +* Gpt extension render ([#519](https://github.com/gravity-ui/markdown-editor/issues/519)) ([89c9881](https://github.com/gravity-ui/markdown-editor/commit/89c9881331df2b0fae5968258a29b9c9eed179ef)) + ## [14.5.1](https://github.com/gravity-ui/markdown-editor/compare/v14.5.0...v14.5.1) (2024-12-02) diff --git a/package-lock.json b/package-lock.json index bbd9767a..8c6fdb39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gravity-ui/markdown-editor", - "version": "14.5.1", + "version": "14.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gravity-ui/markdown-editor", - "version": "14.5.1", + "version": "14.6.0", "license": "MIT", "dependencies": { "@bem-react/classname": "^1.6.0", diff --git a/package.json b/package.json index 3fe94296..e46574e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gravity-ui/markdown-editor", - "version": "14.5.1", + "version": "14.6.0", "description": "Markdown wysiwyg and markup editor", "license": "MIT", "repository": { From 4355519ef4310e37f322b0313e0281880a4b5315 Mon Sep 17 00:00:00 2001 From: Rich Voronov Date: Tue, 17 Dec 2024 09:43:10 +0300 Subject: [PATCH 05/15] build: added react 19 support for peerDependencies (#524) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e46574e3..754bae7a 100644 --- a/package.json +++ b/package.json @@ -300,8 +300,8 @@ "lodash": "^4.17.20", "lowlight": "^3.0.0", "markdown-it": "^13.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "lint-staged": { "*.{css,scss}": [ From dc049af1c5d3a1016406afec3237b85bad2211c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D0=B0=D0=B2=D0=BB=D0=BE=D0=B2=D0=B8=D1=87=20=D0=9C?= =?UTF-8?q?=D0=B8=D1=85=D0=B0=D0=B8=D0=BB=20=D0=90=D0=BD=D1=82=D0=BE=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=B8=D1=87?= <87013925+PMAWorks@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:05:45 +0300 Subject: [PATCH 06/15] feat(bundle): added empty row placeholder (#506) --- demo/components/Playground.tsx | 6 ++++++ src/bundle/types.ts | 12 ++++++++++++ src/bundle/useMarkdownEditor.ts | 1 + src/bundle/wysiwyg-preset.ts | 22 ++++++++++++++++++---- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/demo/components/Playground.tsx b/demo/components/Playground.tsx index 3492b81f..b6c5708f 100644 --- a/demo/components/Playground.tsx +++ b/demo/components/Playground.tsx @@ -16,6 +16,7 @@ import { type RenderPreview, type ToolbarGroupData, type UseMarkdownEditorProps, + WysiwygPlaceholderOptions, logger, markupToolbarConfigs, useMarkdownEditor, @@ -79,6 +80,7 @@ export type PlaygroundProps = { breaks?: boolean; linkify?: boolean; linkifyTlds?: string | string[]; + placeholderOptions?: WysiwygPlaceholderOptions; sanitizeHtml?: boolean; prepareRawMarkup?: boolean; splitModeOrientation?: 'horizontal' | 'vertical' | false; @@ -139,6 +141,7 @@ export const Playground = React.memo((props) => { wysiwygCommandMenuConfig, markupConfigExtensions, markupToolbarConfig, + placeholderOptions, escapeConfig, enableSubmitInPreview, hidePreviewAfterSubmit, @@ -185,6 +188,9 @@ export const Playground = React.memo((props) => { needToSetDimensionsForUploadedImages, renderPreview: renderPreviewDefined ? renderPreview : undefined, fileUploadHandler, + wysiwygConfig: { + placeholderOptions: placeholderOptions, + }, experimental: { ...experimental, directiveSyntax, diff --git a/src/bundle/types.ts b/src/bundle/types.ts index 34d17136..b25ad335 100644 --- a/src/bundle/types.ts +++ b/src/bundle/types.ts @@ -26,6 +26,17 @@ export type RenderPreview = (params: RenderPreviewParams) => ReactNode; export type ParseInsertedUrlAsImage = (text: string) => {imageUrl: string; title?: string} | null; +export type WysiwygPlaceholderOptions = { + value?: string | (() => string); + /** Default – empty-doc + Values: + - 'empty-doc' – The placeholder will only be shown when the document is completely empty; + - 'empty-row-top-level' – The placeholder will be displayed in an empty line that is at the top level of the document structure; + - 'empty-row' – The placeholder will be shown in any empty line within the document, regardless of its nesting level. + */ + behavior?: 'empty-doc' | 'empty-row-top-level' | 'empty-row'; +}; + export type MarkdownEditorMdOptions = { html?: boolean; breaks?: boolean; @@ -148,6 +159,7 @@ export type MarkdownEditorWysiwygConfig = { extensions?: Extension; extensionOptions?: ExtensionsOptions; escapeConfig?: EscapeConfig; + placeholderOptions?: WysiwygPlaceholderOptions; }; // [major] TODO: remove generic type diff --git a/src/bundle/useMarkdownEditor.ts b/src/bundle/useMarkdownEditor.ts index b44cb463..6cdb1a7c 100644 --- a/src/bundle/useMarkdownEditor.ts +++ b/src/bundle/useMarkdownEditor.ts @@ -59,6 +59,7 @@ export function useMarkdownEditor( editor.emit('submit', null); return true; }, + placeholderOptions: wysiwygConfig.placeholderOptions, mdBreaks: breaks, fileUploadHandler: uploadFile, needToSetDimensionsForUploadedImages, diff --git a/src/bundle/wysiwyg-preset.ts b/src/bundle/wysiwyg-preset.ts index 42874148..af43bc4a 100644 --- a/src/bundle/wysiwyg-preset.ts +++ b/src/bundle/wysiwyg-preset.ts @@ -16,7 +16,7 @@ import type {FileUploadHandler} from '../utils/upload'; import {wCommandMenuConfigByPreset, wSelectionMenuConfigByPreset} from './config/wysiwyg'; import {emojiDefs} from './emoji'; -import type {MarkdownEditorPreset} from './types'; +import type {MarkdownEditorPreset, WysiwygPlaceholderOptions} from './types'; const DEFAULT_IGNORED_KEYS = ['Tab', 'Shift-Tab'] as const; @@ -27,6 +27,7 @@ export type BundlePresetOptions = ExtensionsOptions & preset: MarkdownEditorPreset; mdBreaks?: boolean; fileUploadHandler?: FileUploadHandler; + placeholderOptions?: WysiwygPlaceholderOptions; /** * If we need to set dimensions for uploaded images * @@ -63,9 +64,22 @@ export const BundlePreset: ExtensionAuto = (builder, opts) baseSchema: { paragraphKey: f.toPM(A.Text), paragraphPlaceholder: (node: Node, parent?: Node | null) => { - const isDocEmpty = - !node.text && parent?.type.name === BaseNode.Doc && parent.childCount === 1; - return isDocEmpty ? i18nPlaceholder('doc_empty') : null; + const {value, behavior} = opts.placeholderOptions || {}; + + const emptyEntries = { + 'empty-row': !node.text, + 'empty-row-top-level': !node.text && parent?.type.name === BaseNode.Doc, + 'empty-doc': + !node.text && parent?.type.name === BaseNode.Doc && parent.childCount === 1, + }; + + const showPlaceholder = emptyEntries[behavior || 'empty-doc']; + + if (!showPlaceholder) return null; + + return typeof value === 'function' + ? value() + : value ?? i18nPlaceholder('doc_empty'); }, ...opts.baseSchema, }, From a7c23b59af7f2d7a8fd52e3cdb927468854f6c09 Mon Sep 17 00:00:00 2001 From: Yuriy Demidov Date: Tue, 17 Dec 2024 19:29:59 +0300 Subject: [PATCH 07/15] fix(Checkbox): added parse dom rules and fixed pasting of checkboxes (#523) --- src/extensions/yfm/Checkbox/Checkbox.test.ts | 31 +++++++++- .../yfm/Checkbox/CheckboxSpecs/const.ts | 5 ++ .../yfm/Checkbox/CheckboxSpecs/index.ts | 12 ++-- .../yfm/Checkbox/CheckboxSpecs/schema.ts | 58 ++++++++++++++++--- src/extensions/yfm/Checkbox/index.ts | 2 + .../yfm/Checkbox/plugins/fix-paste.ts | 22 +++++++ src/utils/schema.ts | 2 +- tests/parse-dom.ts | 7 ++- 8 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 src/extensions/yfm/Checkbox/plugins/fix-paste.ts diff --git a/src/extensions/yfm/Checkbox/Checkbox.test.ts b/src/extensions/yfm/Checkbox/Checkbox.test.ts index c23b69d9..cfbae01e 100644 --- a/src/extensions/yfm/Checkbox/Checkbox.test.ts +++ b/src/extensions/yfm/Checkbox/Checkbox.test.ts @@ -1,11 +1,13 @@ import {builders} from 'prosemirror-test-builder'; +import {parseDOM} from '../../../../tests/parse-dom'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {ExtensionsManager} from '../../../core'; import {BaseNode, BaseSchemaSpecs} from '../../base/specs'; import {BoldSpecs, boldMarkName} from '../../markdown/specs'; -import {CheckboxNode, CheckboxSpecs} from './CheckboxSpecs'; +import {CheckboxAttr, CheckboxNode, CheckboxSpecs} from './CheckboxSpecs'; +import {fixPastePlugin} from './plugins/fix-paste'; const { schema, @@ -96,4 +98,31 @@ describe('Checkbox extension', () => { '[ ] checkbox-placeholder', ); }); + + it('should parse dom with checkbox', () => { + parseDOM( + schema, + ` + +
+ + +
`, + doc(checkbox(cbInput({[CheckboxAttr.Checked]: 'true'}), cbLabel('два'))), + [fixPastePlugin()], + ); + }); + + it('should parse dom with input[type=checkbox]', () => { + parseDOM( + schema, + ` + + + +`, + doc(checkbox(cbInput(), cbLabel('todo2'))), + [fixPastePlugin()], + ); + }); }); diff --git a/src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts index cdf2aceb..72d88c00 100644 --- a/src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts +++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts @@ -1,4 +1,5 @@ import {cn} from '../../../../classname'; +import {nodeTypeFactory} from '../../../../utils/schema'; export enum CheckboxNode { Checkbox = 'checkbox', @@ -17,3 +18,7 @@ export const CheckboxAttr = { export const idPrefix = 'yfm-editor-checkbox'; export const b = cn('checkbox'); + +export const checkboxType = nodeTypeFactory(CheckboxNode.Checkbox); +export const checkboxLabelType = nodeTypeFactory(CheckboxNode.Label); +export const checkboxInputType = nodeTypeFactory(CheckboxNode.Input); diff --git a/src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts index 72b01332..edfb2dc1 100644 --- a/src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts +++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts @@ -2,17 +2,19 @@ import checkboxPlugin from '@diplodoc/transform/lib/plugins/checkbox'; import type {NodeSpec} from 'prosemirror-model'; import type {ExtensionAuto, ExtensionNodeSpec} from '../../../../core'; -import {nodeTypeFactory} from '../../../../utils/schema'; import {CheckboxNode, b, idPrefix} from './const'; import {parserTokens} from './parser'; import {getSchemaSpecs} from './schema'; import {serializerTokens} from './serializer'; -export {CheckboxAttr, CheckboxNode} from './const'; -export const checkboxType = nodeTypeFactory(CheckboxNode.Checkbox); -export const checkboxLabelType = nodeTypeFactory(CheckboxNode.Label); -export const checkboxInputType = nodeTypeFactory(CheckboxNode.Input); +export { + CheckboxAttr, + CheckboxNode, + checkboxType, + checkboxLabelType, + checkboxInputType, +} from './const'; export type CheckboxSpecsOptions = { /** diff --git a/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts index 3210ec87..d8af22f2 100644 --- a/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts +++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/schema.ts @@ -1,8 +1,8 @@ -import type {NodeSpec} from 'prosemirror-model'; +import {Fragment, type NodeSpec} from 'prosemirror-model'; -import {PlaceholderOptions} from '../../../../utils/placeholder'; +import type {PlaceholderOptions} from '../../../../utils/placeholder'; -import {CheckboxAttr, CheckboxNode, b} from './const'; +import {CheckboxAttr, CheckboxNode, b, checkboxInputType, checkboxLabelType} from './const'; import type {CheckboxSpecsOptions} from './index'; @@ -13,14 +13,51 @@ export const getSchemaSpecs = ( placeholder?: PlaceholderOptions, ): Record => ({ [CheckboxNode.Checkbox]: { - group: 'block', + group: 'block checkbox', content: `${CheckboxNode.Input} ${CheckboxNode.Label}`, selectable: true, allowSelection: false, - parseDOM: [], attrs: { [CheckboxAttr.Class]: {default: b()}, }, + parseDOM: [ + { + tag: 'div.checkbox', + priority: 100, + getContent(node, schema) { + const input = (node as HTMLElement).querySelector( + 'input[type=checkbox]', + ); + const label = (node as HTMLElement).querySelector( + 'label[for]', + ); + + const checked = input?.checked ? 'true' : null; + const text = label?.textContent; + + return Fragment.from([ + checkboxInputType(schema).create({[CheckboxAttr.Checked]: checked}), + checkboxLabelType(schema).create(null, text ? schema.text(text) : null), + ]); + }, + }, + { + tag: 'input[type=checkbox]', + priority: 50, + getContent(node, schema) { + const id = (node as HTMLElement).id; + const checked = (node as HTMLInputElement).checked ? 'true' : null; + const text = node.parentNode?.querySelector( + `label[for=${id}]`, + )?.textContent; + + return Fragment.from([ + checkboxInputType(schema).create({[CheckboxAttr.Checked]: checked}), + checkboxLabelType(schema).create(null, text ? schema.text(text) : null), + ]); + }, + }, + ], toDOM(node) { return ['div', node.attrs, 0]; }, @@ -28,7 +65,7 @@ export const getSchemaSpecs = ( }, [CheckboxNode.Input]: { - group: 'block', + group: 'block checkbox', parseDOM: [], attrs: { [CheckboxAttr.Type]: {default: 'checkbox'}, @@ -45,7 +82,7 @@ export const getSchemaSpecs = ( [CheckboxNode.Label]: { content: 'inline*', - group: 'block', + group: 'block checkbox', parseDOM: [ { tag: `span[class="${b('label')}"]`, @@ -53,6 +90,13 @@ export const getSchemaSpecs = ( [CheckboxAttr.For]: (node as Element).getAttribute(CheckboxAttr.For) || '', }), }, + { + // input handled by checkbox node parse rule + // ignore label + tag: 'input[type=checkbox] ~ label[for]', + ignore: true, + consuming: true, + }, ], attrs: { [CheckboxAttr.For]: {default: null}, diff --git a/src/extensions/yfm/Checkbox/index.ts b/src/extensions/yfm/Checkbox/index.ts index 4ca8c867..715d441f 100644 --- a/src/extensions/yfm/Checkbox/index.ts +++ b/src/extensions/yfm/Checkbox/index.ts @@ -5,6 +5,7 @@ import {CheckboxSpecs, type CheckboxSpecsOptions} from './CheckboxSpecs'; import {addCheckbox} from './actions'; import {CheckboxInputView} from './nodeviews'; import {keymapPlugin} from './plugin'; +import {fixPastePlugin} from './plugins/fix-paste'; import {checkboxInputType, checkboxType} from './utils'; import './index.scss'; @@ -29,6 +30,7 @@ export const Checkbox: ExtensionAuto = (builder, opts) => { builder .addPlugin(keymapPlugin, builder.Priority.High) + .addPlugin(fixPastePlugin) .addAction(checkboxAction, () => addCheckbox()) .addInputRules(({schema}) => ({ rules: [ diff --git a/src/extensions/yfm/Checkbox/plugins/fix-paste.ts b/src/extensions/yfm/Checkbox/plugins/fix-paste.ts new file mode 100644 index 00000000..eccf83a7 --- /dev/null +++ b/src/extensions/yfm/Checkbox/plugins/fix-paste.ts @@ -0,0 +1,22 @@ +import {Slice} from 'prosemirror-model'; +import {Plugin} from 'prosemirror-state'; + +import {checkboxType} from '../CheckboxSpecs'; + +export const fixPastePlugin = () => + new Plugin({ + props: { + transformPasted(slice) { + const {firstChild} = slice.content; + if (firstChild && firstChild.type === checkboxType(firstChild.type.schema)) { + // When paste html with checkboxes and checkbox is first node, + // pm creates slice with broken openStart and openEnd. + // And content is inserted without a container block for checkboxes. + // It is fixed by create new slice with zeroed openStart and openEnd. + return new Slice(slice.content, 0, 0); + } + + return slice; + }, + }, + }); diff --git a/src/utils/schema.ts b/src/utils/schema.ts index cd28b2ca..0d7addf0 100644 --- a/src/utils/schema.ts +++ b/src/utils/schema.ts @@ -1,4 +1,4 @@ -import {Node, NodeType, Schema} from 'prosemirror-model'; +import {Node, type NodeType, type Schema} from 'prosemirror-model'; export const nodeTypeFactory = (nodeName: string) => (schema: Schema) => schema.nodes[nodeName]; export const markTypeFactory = (markName: string) => (schema: Schema) => schema.marks[markName]; diff --git a/tests/parse-dom.ts b/tests/parse-dom.ts index a6c4763d..e1df6b7f 100644 --- a/tests/parse-dom.ts +++ b/tests/parse-dom.ts @@ -1,11 +1,12 @@ /* eslint-disable no-implicit-globals */ import type {Node, Schema} from 'prosemirror-model'; -import {EditorState} from 'prosemirror-state'; +import {EditorState, type Plugin} from 'prosemirror-state'; import {EditorView} from 'prosemirror-view'; + import {dispatchPasteEvent} from './dispatch-event'; -export function parseDOM(schema: Schema, html: string, doc: Node): void { - const view = new EditorView(null, {state: EditorState.create({schema})}); +export function parseDOM(schema: Schema, html: string, doc: Node, plugins?: Plugin[]): void { + const view = new EditorView(null, {state: EditorState.create({schema}), plugins}); dispatchPasteEvent(view, {'text/html': html}); expect(view.state.doc).toMatchNode(doc); } From 9e54ad2bbfd680d445acf41e2c44bec97f67a17d Mon Sep 17 00:00:00 2001 From: Gravity UI Bot <111915794+gravity-ui-bot@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:15:16 +0300 Subject: [PATCH 08/15] chore(main): release 14.7.0 (#525) --- CHANGELOG.md | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7adad77..398adbf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [14.7.0](https://github.com/gravity-ui/markdown-editor/compare/v14.6.0...v14.7.0) (2024-12-17) + + +### Features + +* **bundle:** added empty row placeholder ([#506](https://github.com/gravity-ui/markdown-editor/issues/506)) ([dc049af](https://github.com/gravity-ui/markdown-editor/commit/dc049af1c5d3a1016406afec3237b85bad2211c0)) + + +### Bug Fixes + +* **Checkbox:** added parse dom rules and fixed pasting of checkboxes ([#523](https://github.com/gravity-ui/markdown-editor/issues/523)) ([a7c23b5](https://github.com/gravity-ui/markdown-editor/commit/a7c23b59af7f2d7a8fd52e3cdb927468854f6c09)) + ## [14.6.0](https://github.com/gravity-ui/markdown-editor/compare/v14.5.1...v14.6.0) (2024-12-10) diff --git a/package-lock.json b/package-lock.json index 8c6fdb39..34202627 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gravity-ui/markdown-editor", - "version": "14.6.0", + "version": "14.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gravity-ui/markdown-editor", - "version": "14.6.0", + "version": "14.7.0", "license": "MIT", "dependencies": { "@bem-react/classname": "^1.6.0", diff --git a/package.json b/package.json index 754bae7a..b29f2da3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gravity-ui/markdown-editor", - "version": "14.6.0", + "version": "14.7.0", "description": "Markdown wysiwyg and markup editor", "license": "MIT", "repository": { From 03b39624c32adde84adae74c4e320ce389d0eddb Mon Sep 17 00:00:00 2001 From: Kirill Kharitonov Date: Tue, 17 Dec 2024 18:15:48 +0100 Subject: [PATCH 09/15] feat(build): added a sideEffects property for tree shaking package (#522) --- package.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b29f2da3..2a4ab3ee 100644 --- a/package.json +++ b/package.json @@ -313,5 +313,13 @@ "prettier --write" ], "*.{md,json,yaml,yml}": "prettier --write" - } + }, + "sideEffects": [ + "*.css", + "*.scss", + "src/shortcuts/index.ts", + "src/shortcuts/default.ts", + "build/**/shortcuts/index.js", + "build/**/shortcuts/default.js" + ] } From 0d74a7c8c6d5cc45c7d7f7296b374e7b966a78be Mon Sep 17 00:00:00 2001 From: Gravity UI Bot <111915794+gravity-ui-bot@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:20:54 +0300 Subject: [PATCH 10/15] chore(main): release 14.8.0 (#526) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 398adbf8..a4c24db0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [14.8.0](https://github.com/gravity-ui/markdown-editor/compare/v14.7.0...v14.8.0) (2024-12-17) + + +### Features + +* **build:** added a sideEffects property for tree shaking package ([#522](https://github.com/gravity-ui/markdown-editor/issues/522)) ([03b3962](https://github.com/gravity-ui/markdown-editor/commit/03b39624c32adde84adae74c4e320ce389d0eddb)) + ## [14.7.0](https://github.com/gravity-ui/markdown-editor/compare/v14.6.0...v14.7.0) (2024-12-17) diff --git a/package-lock.json b/package-lock.json index 34202627..cffa164d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gravity-ui/markdown-editor", - "version": "14.7.0", + "version": "14.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gravity-ui/markdown-editor", - "version": "14.7.0", + "version": "14.8.0", "license": "MIT", "dependencies": { "@bem-react/classname": "^1.6.0", diff --git a/package.json b/package.json index 2a4ab3ee..1c280ed6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gravity-ui/markdown-editor", - "version": "14.7.0", + "version": "14.8.0", "description": "Markdown wysiwyg and markup editor", "license": "MIT", "repository": { From 5a82a6cdd7236bf3abf779a6a04df96daae48bb5 Mon Sep 17 00:00:00 2001 From: vvtimofeev <108340247+vvtimofeev@users.noreply.github.com> Date: Wed, 18 Dec 2024 18:36:50 +0300 Subject: [PATCH 11/15] docs: add ru readme (#527) --- README-ru.md | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 16 +++++----- 2 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 README-ru.md diff --git a/README-ru.md b/README-ru.md new file mode 100644 index 00000000..6c8269e5 --- /dev/null +++ b/README-ru.md @@ -0,0 +1,86 @@ +![Markdown Editor](https://github.com/user-attachments/assets/0b4e5f65-54cf-475f-9c68-557a4e9edb46) + +# @gravity-ui/markdown-editor · [![npm package](https://img.shields.io/npm/v/@gravity-ui/markdown-editor)](https://www.npmjs.com/package/@gravity-ui/markdown-editor) [![CI](https://img.shields.io/github/actions/workflow/status/gravity-ui/markdown-editor/ci.yml?branch=main&label=CI)](https://github.com/gravity-ui/markdown-editor/actions/workflows/ci.yml?query=branch:main) [![Release](https://img.shields.io/github/actions/workflow/status/gravity-ui/markdown-editor/release.yml?branch=main&label=Release)](https://github.com/gravity-ui/markdown-editor/actions/workflows/release.yml?query=branch:main) [![storybook](https://img.shields.io/badge/Storybook-deployed-ff4685)](https://preview.gravity-ui.com/md-editor/) + +## Редактор Markdown с поддержкой режимов WYSIWYG и Markup + +`MarkdownEditor` — эффективный инструмент для работы с Markdown, сочетающий режимы WYSIWYG и Markup. Он позволяет создавать и редактировать контент в удобном визуальном режиме с полным контролем над разметкой. + +### 🔧 Основные характеристики + +- Поддержка базового синтаксиса Markdown и [YFM](https://ydocs.tech). +- Расширяемость за счет использования движков ProseMirror и CodeMirror. +- Возможность работы в режимах WYSIWYG и Markup для максимальной гибкости. + +## Установка + +```shell +npm install @gravity-ui/markdown-editor +``` + +### Необходимые зависимости + +Для начала работы с пакетом в проекте необходимо предварительно установить следующие зависимости: `@diplodoc/transform`, `react`, `react-dom` и др. Подробную информацию можно найти в разделе `peerDependencies` файла `package.json`. + +## Начало работы + +`MarkdownEditor` поставляется в виде React-хука для создания экземпляра редактора и компонента для рендеринга представления. +Для настройки стиля и темы см. [документацию UIKit](https://github.com/gravity-ui/uikit?tab=readme-ov-file#styles). + +```tsx +import React from 'react'; +import {useMarkdownEditor, MarkdownEditorView} from '@gravity-ui/markdown-editor'; +import {toaster} from '@gravity-ui/uikit/toaster-singleton-react-18'; + +function Editor({onSubmit}) { + const editor = useMarkdownEditor({allowHTML: false}); + + React.useEffect(() => { + function submitHandler() { + // Serialize current content to markdown markup + const value = editor.getValue(); + onSubmit(value); + } + + editor.on('submit', submitHandler); + return () => { + editor.off('submit', submitHandler); + }; + }, [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) +- [Как добавить расширение HTML](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-connect-html-block--docs) +- [Как добавить расширение Latex](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-connect-latex-extension--docs) +- [Как добавить расширение Mermaid](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-connect-mermaid-extension--docs) +- [Как создать собственное расширение](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-develop-extension-creation--docs) +- [Как добавить расширение 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) + + + +### i18n + +Для настройки интернационализации используйте `configure`: + +```typescript +import {configure} from '@gravity-ui/markdown-editor'; + +configure({ + lang: 'ru', +}); +``` + +Обязательно сделайте вызов `configure()` из [UIKit](https://github.com/gravity-ui/uikit?tab=readme-ov-file#i18n) и других UI-библиотек. + +## Разработка + +Для запуска Storybook в режиме разработки выполните следующую команду: + +```shell +npm start +``` diff --git a/README.md b/README.md index 003baf43..bfbea225 100644 --- a/README.md +++ b/README.md @@ -52,14 +52,14 @@ function Editor({onSubmit}) { } ``` Read more: -- [How to connect the editor in the Create React App](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-add-editor-with-create-react-app.md) -- [How to add preview for markup mode](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-add-preview.md) -- [How to add HTML extension](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-connect-html-extension.md) -- [How to add Latex extension](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-connect-latex-extension.md) -- [How to add Mermaid extension](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-connect-mermaid-extension.md) -- [How to write extension](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-create-extension.md) -- [How to add GPT extension](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-connect-gpt-extensions.md) -- [How to add text binding extension in markdown](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-add-text-binding-extension-in-markdown.md) +- [How to connect the editor in the Create React App](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-install-create-react-app--docs) +- [How to add preview for markup mode](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-develop-preview--docs) +- [How to add HTML extension](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-connect-html-block--docs) +- [How to add Latex extension](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-connect-latex-extension--docs) +- [How to add Mermaid extension](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-connect-mermaid-extension--docs) +- [How to write extension](https://preview.gravity-ui.com/md-editor/?path=/docs/docs-develop-extension-creation--docs) +- [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) From bd52bee93aceaf0af5bd9b8da284e93338b89a32 Mon Sep 17 00:00:00 2001 From: Yuriy Demidov Date: Thu, 19 Dec 2024 12:05:17 +0300 Subject: [PATCH 12/15] fix(Link): fixed pasting link to empty selection (#528) --- src/extensions/markdown/Link/paste-plugin.ts | 32 ++++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/extensions/markdown/Link/paste-plugin.ts b/src/extensions/markdown/Link/paste-plugin.ts index 9c72f370..1e283707 100644 --- a/src/extensions/markdown/Link/paste-plugin.ts +++ b/src/extensions/markdown/Link/paste-plugin.ts @@ -1,4 +1,4 @@ -import {Plugin, TextSelection} from 'prosemirror-state'; +import {Plugin, TextSelection, type Transaction} from 'prosemirror-state'; import type {ExtensionDeps, Parser} from '../../../core'; import {isNodeSelection, isTextSelection} from '../../../utils/selection'; @@ -14,28 +14,48 @@ export function linkPasteEnhance({markupParser: parser}: ExtensionDeps) { paste(view, e): boolean { const {state, dispatch} = view; const sel = state.selection; + let tr: Transaction | null = null; + if ( isTextSelection(sel) || (isNodeSelection(sel) && sel.node.type === imageType(state.schema)) ) { const {$from, $to} = sel; - if ($from.pos !== $to.pos && $from.sameParent($to)) { + if ($from.pos === $to.pos) { const url = getUrl(e.clipboardData, parser); if (url) { - const tr = state.tr.addMark( + const linkMarkType = linkType(state.schema); + tr = state.tr.replaceSelectionWith( + state.schema.text(url, [ + ...$from + .marks() + .filter((mark) => mark.type !== linkMarkType), + linkMarkType.create({[LinkAttr.Href]: url}), + ]), + false, + ); + } + } else if ($from.sameParent($to)) { + const url = getUrl(e.clipboardData, parser); + if (url) { + tr = state.tr.addMark( $from.pos, $to.pos, linkType(state.schema).create({ [LinkAttr.Href]: url, }), ); - dispatch(tr.setSelection(TextSelection.create(tr.doc, $to.pos))); - e.preventDefault(); - return true; + tr.setSelection(TextSelection.create(tr.doc, $to.pos)); } } } + if (tr) { + dispatch(tr.scrollIntoView()); + e.preventDefault(); + return true; + } + return false; }, }, From 15767d7d3b0334126e34149d811ce6b6d62909d2 Mon Sep 17 00:00:00 2001 From: Alexey Okhrimenko Date: Thu, 19 Dec 2024 16:51:03 +0300 Subject: [PATCH 13/15] 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 14/15] 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 15/15] 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