From bbf35f848aca3593f0eb37bf1f8f99a0ad877f38 Mon Sep 17 00:00:00 2001 From: separatrix Date: Wed, 22 Apr 2026 13:16:18 +0300 Subject: [PATCH 1/8] feat: added page-constructor extension --- .release-please/config.json | 3 +- .release-please/manifest.json | 3 +- eslint.config.mjs | 17 ++ .../.prettierignore | 1 + packages/page-constructor-extension/README.md | 41 ++++ .../__mocks__/styleMock.cjs | 1 + .../page-constructor-extension/gulpfile.mjs | 20 ++ .../jest.config.cjs | 20 ++ .../page-constructor-extension/package.json | 127 +++++++++++ .../src/TextAreaFixed.tsx | 21 ++ .../src/YfmPageConstructor.test.ts | 128 +++++++++++ .../YfmPageConstructorNodeView/NodeView.tsx | 141 ++++++++++++ .../YfmPageConstructor.scss | 82 +++++++ .../YfmPageConstructorPreview.tsx | 76 +++++++ .../YfmPageConstructorView.tsx | 193 ++++++++++++++++ .../src/YfmPageConstructorNodeView/index.ts | 1 + .../src/YfmPageConstructorSpecs/const.ts | 21 ++ .../src/YfmPageConstructorSpecs/index.tsx | 78 +++++++ .../page-constructor-extension/src/actions.ts | 49 ++++ .../page-constructor-extension/src/const.ts | 1 + .../src/hocs/withYfmPageConstructor/index.tsx | 42 ++++ .../src/hocs/withYfmPageConstructor/types.ts | 1 + .../useYfmPageConstructorRuntime.ts | 6 + .../withYfmPageConstructor.scss | 5 + .../src/i18n/en.json | 8 + .../src/i18n/index.ts | 8 + .../src/i18n/ru.json | 8 + .../page-constructor-extension/src/index.ts | 46 ++++ .../page-constructor-extension/src/toolbar.ts | 14 ++ .../page-constructor-extension/src/types.ts | 3 + .../page-constructor-extension/tsconfig.json | 10 + pnpm-lock.yaml | 214 ++++++++++++++++++ 32 files changed, 1387 insertions(+), 2 deletions(-) create mode 100644 packages/page-constructor-extension/.prettierignore create mode 100644 packages/page-constructor-extension/README.md create mode 100644 packages/page-constructor-extension/__mocks__/styleMock.cjs create mode 100644 packages/page-constructor-extension/gulpfile.mjs create mode 100644 packages/page-constructor-extension/jest.config.cjs create mode 100644 packages/page-constructor-extension/package.json create mode 100644 packages/page-constructor-extension/src/TextAreaFixed.tsx create mode 100644 packages/page-constructor-extension/src/YfmPageConstructor.test.ts create mode 100644 packages/page-constructor-extension/src/YfmPageConstructorNodeView/NodeView.tsx create mode 100644 packages/page-constructor-extension/src/YfmPageConstructorNodeView/YfmPageConstructor.scss create mode 100644 packages/page-constructor-extension/src/YfmPageConstructorNodeView/YfmPageConstructorPreview.tsx create mode 100644 packages/page-constructor-extension/src/YfmPageConstructorNodeView/YfmPageConstructorView.tsx create mode 100644 packages/page-constructor-extension/src/YfmPageConstructorNodeView/index.ts create mode 100644 packages/page-constructor-extension/src/YfmPageConstructorSpecs/const.ts create mode 100644 packages/page-constructor-extension/src/YfmPageConstructorSpecs/index.tsx create mode 100644 packages/page-constructor-extension/src/actions.ts create mode 100644 packages/page-constructor-extension/src/const.ts create mode 100644 packages/page-constructor-extension/src/hocs/withYfmPageConstructor/index.tsx create mode 100644 packages/page-constructor-extension/src/hocs/withYfmPageConstructor/types.ts create mode 100644 packages/page-constructor-extension/src/hocs/withYfmPageConstructor/useYfmPageConstructorRuntime.ts create mode 100644 packages/page-constructor-extension/src/hocs/withYfmPageConstructor/withYfmPageConstructor.scss create mode 100644 packages/page-constructor-extension/src/i18n/en.json create mode 100644 packages/page-constructor-extension/src/i18n/index.ts create mode 100644 packages/page-constructor-extension/src/i18n/ru.json create mode 100644 packages/page-constructor-extension/src/index.ts create mode 100644 packages/page-constructor-extension/src/toolbar.ts create mode 100644 packages/page-constructor-extension/src/types.ts create mode 100644 packages/page-constructor-extension/tsconfig.json diff --git a/.release-please/config.json b/.release-please/config.json index 64e55f122..59cf015d5 100644 --- a/.release-please/config.json +++ b/.release-please/config.json @@ -12,6 +12,7 @@ ], "packages": { "packages/editor": {}, - "packages/latex-extension": {} + "packages/latex-extension": {}, + "packages/page-constructor-extension": {} } } \ No newline at end of file diff --git a/.release-please/manifest.json b/.release-please/manifest.json index d1139f32f..4612b5d71 100644 --- a/.release-please/manifest.json +++ b/.release-please/manifest.json @@ -1,4 +1,5 @@ { "packages/editor": "15.38.1", - "packages/latex-extension": "0.1.0" + "packages/latex-extension": "0.1.0", + "packages/page-constructor-extension": "0.0.0" } \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index b5d3a24f8..17f222057 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -60,4 +60,21 @@ export default defineConfig( }, }, }, + { + files: ['./packages/page-constructor-extension/**/*'], + languageOptions: { + parserOptions: { + project: './packages/page-constructor-extension/tsconfig.json', + tsconfigRootDir: import.meta.dirname, + }, + }, + settings: { + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + project: './packages/page-constructor-extension/tsconfig.json', + }, + }, + }, + }, ); diff --git a/packages/page-constructor-extension/.prettierignore b/packages/page-constructor-extension/.prettierignore new file mode 100644 index 000000000..378eac25d --- /dev/null +++ b/packages/page-constructor-extension/.prettierignore @@ -0,0 +1 @@ +build diff --git a/packages/page-constructor-extension/README.md b/packages/page-constructor-extension/README.md new file mode 100644 index 000000000..48f9fdc85 --- /dev/null +++ b/packages/page-constructor-extension/README.md @@ -0,0 +1,41 @@ +# @gravity-ui/markdown-editor-page-constructor-extension + +Page Constructor extension for [@gravity-ui/markdown-editor](https://github.com/gravity-ui/markdown-editor). + +Provides a WYSIWYG editing experience for [Page Constructor](https://github.com/gravity-ui/page-constructor) blocks inside the Markdown editor, as well as a preview HOC for split-mode rendering. + +## Installation + +```bash +npm install @gravity-ui/markdown-editor-page-constructor-extension +``` + +## Usage + +### WYSIWYG extension + +```typescript +import {YfmPageConstructor} from '@gravity-ui/markdown-editor-page-constructor-extension'; + +builder.use(YfmPageConstructor, { + autoSave: {enabled: true, delay: 1000}, +}); +``` + +### Toolbar button + +```typescript +import {wYfmPageConstructorItemData} from '@gravity-ui/markdown-editor-page-constructor-extension/toolbar'; +``` + +### Split-mode preview HOC + +```typescript +import {withYfmPageConstructor} from '@gravity-ui/markdown-editor-page-constructor-extension/hocs/withYfmPageConstructor'; + +const Preview = withYfmPageConstructor()(YfmStaticView); +``` + +## License + +MIT diff --git a/packages/page-constructor-extension/__mocks__/styleMock.cjs b/packages/page-constructor-extension/__mocks__/styleMock.cjs new file mode 100644 index 000000000..f053ebf79 --- /dev/null +++ b/packages/page-constructor-extension/__mocks__/styleMock.cjs @@ -0,0 +1 @@ +module.exports = {}; diff --git a/packages/page-constructor-extension/gulpfile.mjs b/packages/page-constructor-extension/gulpfile.mjs new file mode 100644 index 000000000..5ae14b32e --- /dev/null +++ b/packages/page-constructor-extension/gulpfile.mjs @@ -0,0 +1,20 @@ +import {dirname, resolve} from 'node:path'; +import {fileURLToPath} from 'node:url'; + +import {series, task} from '@markdown-editor/gulp-tasks'; +import {registerBuildTasks} from '@markdown-editor/gulp-tasks/build'; + +import pkg from './package.json' with {type: 'json'}; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const BUILD_DIR = resolve('build'); +const NODE_MODULES_DIR = resolve(__dirname, 'node_modules'); + +registerBuildTasks({ + version: pkg.version, + buildDir: BUILD_DIR, + nodeModulesDir: NODE_MODULES_DIR, +}); + +task('default', series('clean', 'build')); diff --git a/packages/page-constructor-extension/jest.config.cjs b/packages/page-constructor-extension/jest.config.cjs new file mode 100644 index 000000000..360d3f955 --- /dev/null +++ b/packages/page-constructor-extension/jest.config.cjs @@ -0,0 +1,20 @@ +/** @type {import('jest').Config} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + moduleNameMapper: { + '\\.(css|scss)$': '/__mocks__/styleMock.cjs', + }, + transformIgnorePatterns: ['node_modules/(?!(@gravity-ui|@diplodoc)/)'], + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + tsconfig: { + verbatimModuleSyntax: false, + }, + }, + ], + }, +}; diff --git a/packages/page-constructor-extension/package.json b/packages/page-constructor-extension/package.json new file mode 100644 index 000000000..4ffa1df0c --- /dev/null +++ b/packages/page-constructor-extension/package.json @@ -0,0 +1,127 @@ +{ + "name": "@gravity-ui/markdown-editor-page-constructor-extension", + "version": "0.0.0", + "description": "Page Constructor extension for @gravity-ui/markdown-editor", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/gravity-ui/markdown-editor" + }, + "keywords": [ + "md", + "yfm", + "wysiwyg", + "markdown", + "prosemirror", + "page-constructor" + ], + "scripts": { + "clean": "gulp clean", + "build": "gulp build", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "run-p -cs lint:*", + "lint:js": "eslint './**/*.{js,jsx,mjs,ts,tsx}'", + "lint:styles": "stylelint './**/*.{css,scss}' --allow-empty-input", + "lint:prettier": "prettier --check './**/*.{js,jsx,mjs,ts,tsx,css,scss}'", + "test": "jest --config jest.config.cjs", + "prepublishOnly": "nx lint && nx clean && nx build" + }, + "exports": { + ".": { + "import": { + "types": "./build/esm/index.d.ts", + "default": "./build/esm/index.js" + }, + "require": { + "types": "./build/cjs/index.d.ts", + "default": "./build/cjs/index.js" + } + }, + "./toolbar": { + "import": { + "types": "./build/esm/toolbar.d.ts", + "default": "./build/esm/toolbar.js" + }, + "require": { + "types": "./build/cjs/toolbar.d.ts", + "default": "./build/cjs/toolbar.js" + } + }, + "./hocs/withYfmPageConstructor": { + "import": { + "types": "./build/esm/hocs/withYfmPageConstructor/index.d.ts", + "default": "./build/esm/hocs/withYfmPageConstructor/index.js" + }, + "require": { + "types": "./build/cjs/hocs/withYfmPageConstructor/index.d.ts", + "default": "./build/cjs/hocs/withYfmPageConstructor/index.js" + } + } + }, + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/esm/index.d.ts", + "files": [ + "build", + "README.md" + ], + "dependencies": { + "tslib": "catalog:ts" + }, + "devDependencies": { + "@gravity-ui/markdown-editor": "workspace:*", + "@gravity-ui/uikit": "catalog:peer-gravity", + "@gravity-ui/page-constructor": "catalog:peer-gravity", + "@gravity-ui/icons": "^2.12.0", + "@diplodoc/page-constructor-extension": "catalog:peer-diplodoc", + "@markdown-editor/gulp-tasks": "workspace:*", + "@markdown-editor/linters": "workspace:*", + "@markdown-editor/tsconfig": "workspace:*", + "@types/jest": "^29.5.0", + "@types/react": "catalog:react", + "@types/react-dom": "catalog:react", + "gulp-cli": "catalog:", + "jest": "^29.7.0", + "npm-run-all": "^4.1.5", + "prosemirror-model": "^1.24.1", + "prosemirror-state": "^1.4.3", + "prosemirror-test-builder": "^1.1.1", + "prosemirror-view": "^1.38.0", + "react": "catalog:react", + "react-dom": "catalog:react", + "react-error-boundary": "^3.1.4", + "react-use": "catalog:", + "ts-dedent": "^2.2.0", + "ts-jest": "^29.2.0", + "typescript": "catalog:ts" + }, + "peerDependencies": { + "@diplodoc/page-constructor-extension": "^0.13.3", + "@gravity-ui/icons": "^2.12.0", + "@gravity-ui/markdown-editor": "workspace:^15.38.1", + "@gravity-ui/page-constructor": "^7.0.0", + "@gravity-ui/uikit": "^7.1.0", + "prosemirror-model": "^1.24.1", + "prosemirror-state": "^1.4.3", + "prosemirror-view": "^1.38.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "react-error-boundary": "^3.1.4", + "react-use": "^17.0.0" + }, + "peerDependenciesMeta": { + "@gravity-ui/page-constructor": { + "optional": true + }, + "react-error-boundary": { + "optional": true + }, + "react-use": { + "optional": true + } + }, + "sideEffects": [ + "*.css", + "*.scss" + ] +} diff --git a/packages/page-constructor-extension/src/TextAreaFixed.tsx b/packages/page-constructor-extension/src/TextAreaFixed.tsx new file mode 100644 index 000000000..e6ea5213e --- /dev/null +++ b/packages/page-constructor-extension/src/TextAreaFixed.tsx @@ -0,0 +1,21 @@ +import {forwardRef, useRef} from 'react'; + +import {type TextAreaProps, TextArea as TextAreaUIKit} from '@gravity-ui/uikit'; +import {useEffectOnce} from 'react-use'; + +export const TextAreaFixed = forwardRef((props, ref) => { + const inputRef = useRef(null); + const controlRef = (props.controlRef as React.RefObject) ?? inputRef; + + useEffectOnce(() => { + if (props.autoFocus) { + setTimeout(() => { + controlRef.current?.focus(); + }, 30); + } + }); + + return ; +}); + +TextAreaFixed.displayName = 'TextAreaFixed'; diff --git a/packages/page-constructor-extension/src/YfmPageConstructor.test.ts b/packages/page-constructor-extension/src/YfmPageConstructor.test.ts new file mode 100644 index 000000000..fd290e36f --- /dev/null +++ b/packages/page-constructor-extension/src/YfmPageConstructor.test.ts @@ -0,0 +1,128 @@ +import type {Parser, Serializer} from '@gravity-ui/markdown-editor'; +import { + BaseNode, + BaseSchemaSpecs, + BlockquoteSpecs, + ExtensionsManager, + blockquoteNodeName, +} from '@gravity-ui/markdown-editor'; +import type {Node} from 'prosemirror-model'; +import {builders} from 'prosemirror-test-builder'; +import dd from 'ts-dedent'; + +import {YfmPageConstructorSpecs} from './YfmPageConstructorSpecs'; +import {YfmPageConstructorAttrs, yfmPageConstructorNodeName} from './const'; + +jest.mock('@gravity-ui/markdown-editor', () => { + const actual = jest.requireActual('@gravity-ui/markdown-editor'); + return { + ...actual, + generateEntityId: (name = 'entity') => `${name}-eff-000-0ab`, + }; +}); + +function createMarkupChecker({parser, serializer}: {parser: Parser; serializer: Serializer}) { + function parse(text: string, doc: Node) { + expect(parser.parse(text).toJSON()).toEqual(doc.toJSON()); + } + + function serialize(doc: Node, text: string) { + expect(serializer.serialize(doc)).toBe(text); + } + + function same(text: string, doc: Node) { + parse(text, doc); + serialize(doc, text); + } + + return {same, parse, serialize}; +} + +const { + schema, + markupParser: parser, + serializer, +} = new ExtensionsManager({ + extensions: (builder) => + builder.use(BaseSchemaSpecs, {}).use(BlockquoteSpecs).use(YfmPageConstructorSpecs, {}), +}).buildDeps(); + +const {doc, yfmPageConstructor, quote} = builders<'doc' | 'yfmPageConstructor' | 'quote'>(schema, { + doc: {nodeType: BaseNode.Doc}, + yfmPageConstructor: {nodeType: yfmPageConstructorNodeName}, + quote: {nodeType: blockquoteNodeName}, +}); + +const {same} = createMarkupChecker({parser, serializer}); + +describe('YfmPageConstructor extension', () => { + it('should parse yfm page-constructor block', () => + same( + '::: page-constructor\nblocks:\n - type: "header-block"\n title: "Title"\n:::', + doc( + yfmPageConstructor({ + [YfmPageConstructorAttrs.content]: + 'blocks:\n - type: "header-block"\n title: "Title"\n', + [YfmPageConstructorAttrs.EntityId]: `${yfmPageConstructorNodeName}-eff-000-0ab`, + }), + ), + )); + + it('should parse yfm page-constructor block with multiline yaml', () => { + const yamlContent = dd` + blocks: + - type: "header-block" + title: "Hello World" + description: "Some description" + + `; + + const markup = dd` + ::: page-constructor + blocks: + - type: "header-block" + title: "Hello World" + description: "Some description" + ::: + `; + + same( + markup, + doc( + yfmPageConstructor({ + [YfmPageConstructorAttrs.content]: yamlContent, + [YfmPageConstructorAttrs.EntityId]: `${yfmPageConstructorNodeName}-eff-000-0ab`, + }), + ), + ); + }); + + it('should parse yfm page-constructor inside blockquote', () => { + const content = dd` + blocks: + - type: "header-block" + title: "Title" + + `; + + const markup = dd` + > ::: page-constructor + > blocks: + > - type: "header-block" + > title: "Title" + > ::: + `; + + same( + markup, + doc( + quote( + yfmPageConstructor({ + [YfmPageConstructorAttrs.content]: content, + [YfmPageConstructorAttrs.EntityId]: `${yfmPageConstructorNodeName}-eff-000-0ab`, + }), + ), + ), + ); + }); +}); diff --git a/packages/page-constructor-extension/src/YfmPageConstructorNodeView/NodeView.tsx b/packages/page-constructor-extension/src/YfmPageConstructorNodeView/NodeView.tsx new file mode 100644 index 000000000..f2d67afac --- /dev/null +++ b/packages/page-constructor-extension/src/YfmPageConstructorNodeView/NodeView.tsx @@ -0,0 +1,141 @@ +import { + generateEntityId, + getReactRendererFromState, + isInvalidEntityId, + removeNode, +} from '@gravity-ui/markdown-editor'; +import {Portal} from '@gravity-ui/uikit'; +import type {Node} from 'prosemirror-model'; +import type {EditorView, NodeView} from 'prosemirror-view'; + +import type {YfmPageConstructorOptions} from '..'; +import { + YfmPageConstructorConsts, + defaultYfmPageConstructorEntityId, +} from '../YfmPageConstructorSpecs/const'; + +import {STOP_EVENT_CLASSNAME, YfmPageConstructorView} from './YfmPageConstructorView'; + +export class WYfmPageConstructorNodeView implements NodeView { + readonly dom: HTMLElement; + private node: Node; + private readonly view: EditorView; + private readonly getPos: () => number | undefined; + private readonly renderItem; + private readonly options: YfmPageConstructorOptions; + + constructor( + node: Node, + view: EditorView, + getPos: () => number | undefined, + options: YfmPageConstructorOptions, + ) { + this.node = node; + this.view = view; + this.getPos = getPos; + this.options = options; + + this.dom = document.createElement('div'); + this.dom.classList.add('yfm-page-constructor-container'); + this.dom.contentEditable = 'false'; + + this.renderItem = getReactRendererFromState(view.state).createItem( + 'yfm-page-constructor-view', + this.renderPageConstructor.bind(this), + ); + + this.validateEntityId(); + } + + update(node: Node) { + if (node.type !== this.node.type) { + return false; + } + + this.node = node; + this.renderItem.rerender(); + + return true; + } + + destroy() { + this.renderItem.remove(); + } + + ignoreMutation() { + return true; + } + + stopEvent(e: Event) { + const {target} = e; + + if (target instanceof Element) { + return target.classList.contains(STOP_EVENT_CLASSNAME); + } + + return false; + } + + private validateEntityId() { + if ( + isInvalidEntityId({ + node: this.node, + doc: this.view.state.doc, + defaultId: defaultYfmPageConstructorEntityId, + }) + ) { + const newId = generateEntityId(YfmPageConstructorConsts.NodeName); + + this.view.dispatch( + this.view.state.tr.setNodeAttribute( + this.getPos()!, + YfmPageConstructorConsts.NodeAttrs.EntityId, + newId, + ), + ); + } + } + + private onChange = (content: string) => { + const pos = this.getPos(); + + if (pos === undefined) { + return; + } + + this.view.dispatch( + this.view.state.tr.setNodeAttribute( + pos, + YfmPageConstructorConsts.NodeAttrs.content, + content, + ), + ); + }; + + private onRemove = () => { + const pos = this.getPos(); + + if (pos === undefined) { + return; + } + + removeNode({node: this.node, pos, tr: this.view.state.tr, dispatch: this.view.dispatch}); + this.view.focus(); + }; + + private renderPageConstructor() { + return ( + + + + ); + } +} diff --git a/packages/page-constructor-extension/src/YfmPageConstructorNodeView/YfmPageConstructor.scss b/packages/page-constructor-extension/src/YfmPageConstructorNodeView/YfmPageConstructor.scss new file mode 100644 index 000000000..29ed1d55a --- /dev/null +++ b/packages/page-constructor-extension/src/YfmPageConstructorNodeView/YfmPageConstructor.scss @@ -0,0 +1,82 @@ +.g-md-YfmPageConstructor { + position: relative; + + display: flex; + justify-content: space-between; + gap: 12px; + + min-height: 30px; + padding: 4px; + + border: 1px solid var(--g-color-line-generic); + border-radius: var(--g-border-radius-m); + + &_edit { + display: flex; + } + + &__Constructor { + flex: 1 1 auto; + + min-width: 0; + height: fit-content; + + .pc-layout { + min-height: auto; + } + } + + &__Error { + flex: 1 1 auto; + + font-family: var(--g-font-family-monospace); + white-space: pre-wrap; + word-break: break-word; + + color: var(--g-color-text-danger); + } + + &__Preview { + flex: 1 1 auto; + + min-width: 0; + height: fit-content; + + .pc-layout { + min-height: auto; + } + } + + &__Editor { + flex: 1 1 auto; + + min-width: 320px; + + white-space: nowrap; + caret-color: auto; + } + + &__Controls { + display: flex; + justify-content: flex-end; + gap: 8px; + + margin-top: 8px; + } + + &__Menu { + position: absolute; + z-index: 10; + top: 0; + right: 0; + + padding: 2px; + + border-radius: var(--g-border-radius-s); + background: var(--g-color-base-background); + } +} + +.yfm-page-constructor-container { + padding-bottom: 20px; +} diff --git a/packages/page-constructor-extension/src/YfmPageConstructorNodeView/YfmPageConstructorPreview.tsx b/packages/page-constructor-extension/src/YfmPageConstructorNodeView/YfmPageConstructorPreview.tsx new file mode 100644 index 000000000..c9dd162a6 --- /dev/null +++ b/packages/page-constructor-extension/src/YfmPageConstructorNodeView/YfmPageConstructorPreview.tsx @@ -0,0 +1,76 @@ +import {useMemo} from 'react'; + +import {loadPageContent} from '@diplodoc/page-constructor-extension/plugin/csr'; +import { + PageConstructor as PageConstructorContent, + PageConstructorProvider, + type PageContent, + type Theme, +} from '@gravity-ui/page-constructor'; +import type {ContentTransformerProps} from '@gravity-ui/page-constructor/server'; +import {contentTransformer} from '@gravity-ui/page-constructor/server'; +import {Flex, Text, useThemeType} from '@gravity-ui/uikit'; +import {ErrorBoundary} from 'react-error-boundary'; + +import {cnYfmPageConstructor} from './YfmPageConstructorView'; + +const b = cnYfmPageConstructor; + +export type TransformerOptions = false | ContentTransformerProps['options']; + +export const YfmPageConstructorPreview: React.FC<{ + text: string; + transformerOptions: TransformerOptions; +}> = ({text = '', transformerOptions}) => { + const theme = useThemeType(); + const loadResult = useMemo(() => loadPageContent(text), [text]); + + const pageContent = useMemo(() => { + if (!loadResult.success) { + return undefined; + } + if (transformerOptions === false) { + return loadResult.content as PageContent; + } + return { + ...loadResult.content, + ...contentTransformer({ + content: loadResult.content as PageContent, + options: transformerOptions, + }), + }; + }, [loadResult, transformerOptions]); + + if (!loadResult.success) { + return ( +
+
{String(loadResult.error)}
+
+ ); + } + + return ( +
+ ( + + + {String(error?.message ?? error)} + + + )} + > + + + + +
+ ); +}; diff --git a/packages/page-constructor-extension/src/YfmPageConstructorNodeView/YfmPageConstructorView.tsx b/packages/page-constructor-extension/src/YfmPageConstructorNodeView/YfmPageConstructorView.tsx new file mode 100644 index 000000000..2098f31f6 --- /dev/null +++ b/packages/page-constructor-extension/src/YfmPageConstructorNodeView/YfmPageConstructorView.tsx @@ -0,0 +1,193 @@ +import {Suspense, lazy, useCallback, useMemo, useState} from 'react'; + +import {Ellipsis as DotsIcon} from '@gravity-ui/icons'; +import { + SharedStateKey, + cn, + useAutoSave, + useBooleanState, + useElementState, + useSharedEditingState, +} from '@gravity-ui/markdown-editor'; +import {Button, Flex, Icon, Loader, Menu, Popup, type PopupPlacement} from '@gravity-ui/uikit'; +import type {Node} from 'prosemirror-model'; +import type {EditorView} from 'prosemirror-view'; + +import {TextAreaFixed as TextArea} from '../TextAreaFixed'; +import {YfmPageConstructorConsts} from '../YfmPageConstructorSpecs/const'; +import {i18n} from '../i18n'; +import type {YfmPageConstructorOptions} from '../index'; +import type {YfmPageConstructorEntitySharedState} from '../types'; + +import type {TransformerOptions} from './YfmPageConstructorPreview'; + +import './YfmPageConstructor.scss'; + +export {type TransformerOptions}; +export const cnYfmPageConstructor: (...args: unknown[]) => string = cn('YfmPageConstructor'); +export const STOP_EVENT_CLASSNAME = 'prosemirror-stop-event'; + +const b = cnYfmPageConstructor; + +const popupPlacement: PopupPlacement = ['bottom-end']; + +const YfmPageConstructorPreviewLazy: React.LazyExoticComponent< + React.FC<{text: string; transformerOptions: TransformerOptions}> +> = lazy(() => + // @ts-ignore error TS2835: Relative import paths need explicit file extensions in ECMAScript (cjs build) + import('./YfmPageConstructorPreview').then((m) => ({default: m.YfmPageConstructorPreview})), +); + +const YfmPageConstructorPreview: React.FC<{ + text: string; + transformerOptions: TransformerOptions; + className?: string; +}> = (props) => ( + }> + + +); + +const PageConstructorEditMode: React.FC<{ + initialText: string; + onSave: (v: string) => void; + onCancel: () => void; + autoSave: YfmPageConstructorOptions['autoSave']; + transformerOptions: TransformerOptions; +}> = ({initialText, onSave, onCancel, autoSave, transformerOptions}) => { + const {value, handleChange, handleManualSave, isSaveDisabled, isAutoSaveEnabled} = useAutoSave({ + initialValue: initialText || '', + onSave, + onClose: onCancel, + autoSave, + }); + + return ( +
+ +
+