├── .changelogrc.js ├── .commitlintrc.js ├── .dumirc.ts ├── .editorconfig ├── .eslintrc.js ├── .fatherrc.ts ├── .github ├── ISSUE_TEMPLATE │ ├── 1_bug_report.yml │ ├── 2_feature_request.yml │ ├── 3_question.yml │ └── 4_other.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── deploy.yml │ ├── preview.yml │ └── test.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .i18nrc.js ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .releaserc.js ├── .stylelintrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README.zh-CN.md ├── __mocks__ └── zustand │ └── traditional.ts ├── docs ├── guide │ ├── data-management.en-US.md │ ├── data-management.zh-CN.md │ ├── demos │ │ ├── ColumnList │ │ │ ├── data.ts │ │ │ └── index.tsx │ │ ├── Redo │ │ │ ├── App.tsx │ │ │ ├── Toolbar.tsx │ │ │ ├── index.tsx │ │ │ └── store.ts │ │ └── editor-store │ │ │ ├── index.ts │ │ │ ├── initalState.ts │ │ │ ├── selectors.ts │ │ │ ├── slices │ │ │ └── crud │ │ │ │ ├── action.ts │ │ │ │ ├── index.ts │ │ │ │ ├── initalState.ts │ │ │ │ └── selectors.ts │ │ │ └── store.ts │ ├── intro.en-US.md │ ├── intro.zh-CN.md │ ├── nextjs.en-US.md │ ├── nextjs.zh-CN.md │ ├── redo-undo.en-US.md │ ├── redo-undo.zh-CN.md │ ├── umi.en-US.md │ ├── umi.zh-CN.md │ ├── why-pro-editor.en-US.md │ └── why-pro-editor.zh-CN.md ├── index.en-US.md ├── index.zh-CN.md └── pro-editor │ ├── component-assets.en-US.md │ ├── component-assets.zh-CN.md │ ├── data-flow.en-US.md │ ├── data-flow.zh-CN.md │ ├── demos │ ├── buttonAsset │ │ ├── _Component.tsx │ │ ├── _Panel.tsx │ │ ├── codeEmitter.tsx │ │ ├── format.ts │ │ ├── index.ts │ │ ├── models.ts │ │ └── store.ts │ ├── buttonAssets.tsx │ ├── controlledPresence.tsx │ ├── defaultAssets.tsx │ ├── empty.tsx │ └── realtimeCollaboration │ │ ├── SessionForm.tsx │ │ ├── demo.tsx │ │ └── store.ts │ ├── index.en-US.md │ ├── index.zh-CN.md │ ├── provider.en-US.md │ ├── provider.zh-CN.md │ ├── realtime-collaboration.en-US.md │ ├── realtime-collaboration.zh-CN.md │ ├── usePresenceAsset.en-US.md │ ├── usePresenceAsset.zh-CN.md │ ├── useProEditor.en-US.md │ └── useProEditor.zh-CN.md ├── package.json ├── public ├── CNAME ├── favicon.ico └── icon.png ├── src ├── ActionGroup │ ├── demos │ │ ├── _items.tsx │ │ ├── basic.tsx │ │ ├── config.tsx │ │ ├── custom.tsx │ │ ├── dropMenu.tsx │ │ ├── type.tsx │ │ └── withPanel.tsx │ ├── index.en-US.md │ ├── index.tsx │ ├── index.zh-CN.md │ └── style.ts ├── ActionIcon │ ├── ActionIcon.test.tsx │ ├── ActionIcon.tsx │ ├── Icons.tsx │ ├── __snapshots__ │ │ └── ActionIcon.test.tsx.snap │ ├── demos │ │ ├── basic.tsx │ │ └── preset.tsx │ ├── index.en-US.md │ ├── index.ts │ ├── index.zh-CN.md │ └── style.ts ├── Awareness │ ├── Avatars │ │ ├── Avatar.tsx │ │ └── index.tsx │ ├── Awareness.tsx │ ├── Cursors │ │ ├── Cursor.tsx │ │ ├── CursorSvg.tsx │ │ └── index.tsx │ ├── demos │ │ ├── Avatar.tsx │ │ └── Cursor.tsx │ ├── event.ts │ ├── index.en-US.md │ ├── index.tsx │ ├── index.zh-CN.md │ └── store.ts ├── ColumnList │ ├── ColumnItem.tsx │ ├── ColumnList.tsx │ ├── Header.tsx │ ├── demos │ │ ├── actions.tsx │ │ ├── column.tsx │ │ ├── controlled.tsx │ │ ├── creatorButtonProps.tsx │ │ ├── creatorButtonPropsFalse.tsx │ │ ├── customCreate.tsx │ │ ├── empty.tsx │ │ ├── mock_data │ │ │ └── options.ts │ │ └── normal.tsx │ ├── index.en-US.md │ ├── index.ts │ ├── index.zh-CN.md │ ├── renderItem │ │ ├── Input.tsx │ │ └── Select.tsx │ ├── style.ts │ └── types.ts ├── ComponentAsset │ ├── ComponentAsset.test.tsx │ ├── ComponentAsset.tsx │ ├── demoAssets │ │ ├── Component.tsx │ │ ├── index.ts │ │ ├── models.ts │ │ └── store.ts │ ├── index.ts │ ├── store │ │ ├── createAssetStore.ts │ │ ├── createTestAssetStore.ts │ │ └── index.ts │ └── types │ │ ├── asset.ts │ │ ├── code.ts │ │ ├── index.ts │ │ ├── model.ts │ │ └── render.ts ├── ConfigProvider │ └── index.tsx ├── ContextMenu │ ├── MenuItem │ │ ├── icons.tsx │ │ ├── index.tsx │ │ └── style.ts │ ├── demos │ │ └── index.tsx │ ├── index.en-US.md │ ├── index.tsx │ ├── index.zh-CN.md │ ├── style.ts │ └── types │ │ ├── index.ts │ │ └── menuItem.ts ├── DraggablePanel │ ├── DraggablePanel.tsx │ ├── FixMode.tsx │ ├── FloatMode.tsx │ ├── demos │ │ ├── basic.tsx │ │ ├── bottom.tsx │ │ ├── controlFixed.tsx │ │ ├── controlFloat.tsx │ │ ├── float.tsx │ │ ├── left.tsx │ │ └── top.tsx │ ├── index.en-US.md │ ├── index.ts │ ├── index.zh-CN.md │ └── style.ts ├── ErrorBoundary │ ├── demos │ │ ├── _dev.tsx │ │ └── _prod.tsx │ ├── index.en-US.md │ ├── index.tsx │ └── index.zh-CN.md ├── FreeCanvas │ ├── Artboard.tsx │ ├── ControlAction.tsx │ ├── demos │ │ └── basic.tsx │ ├── index.en-US.md │ ├── index.tsx │ ├── index.zh-CN.md │ └── style.ts ├── Highlight │ ├── components │ │ ├── CopyButton │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── HighLighter │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── HighlightCell │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ └── LanguageTag │ │ │ └── index.tsx │ ├── demos │ │ ├── auto.tsx │ │ ├── basic.tsx │ │ ├── config.js │ │ ├── lineNumber.tsx │ │ └── theme.tsx │ ├── hooks │ │ ├── useKeyDownCopyEvent.tsx │ │ └── useShiki.tsx │ ├── index.en-US.md │ ├── index.tsx │ ├── index.zh-CN.md │ ├── style.ts │ └── theme │ │ ├── colors.ts │ │ ├── config.ts │ │ ├── index.ts │ │ └── type.ts ├── IconPicker │ ├── components │ │ ├── IconList │ │ │ ├── IconThumbnail.tsx │ │ │ └── index.tsx │ │ ├── ScriptEditor.tsx │ │ └── SearchBar.tsx │ ├── container │ │ ├── App.tsx │ │ ├── StoreUpdater.tsx │ │ └── index.tsx │ ├── contents │ │ ├── antdIcons.ts │ │ └── customIcons.tsx │ ├── demos │ │ ├── controlled.tsx │ │ ├── normal.tsx │ │ └── scripts.tsx │ ├── features │ │ ├── Display.tsx │ │ ├── IconRender.tsx │ │ ├── IconfontScript.style.tsx │ │ ├── IconfontScript.tsx │ │ └── PickerPanel.tsx │ ├── index.en-US.md │ ├── index.ts │ ├── index.zh-CN.md │ ├── store │ │ ├── __mockData__ │ │ │ └── iconfont.js │ │ ├── __snapshots__ │ │ │ └── store.test.ts.snap │ │ ├── createStore.ts │ │ ├── index.ts │ │ ├── initialState.ts │ │ ├── selectors.test.ts │ │ ├── selectors.ts │ │ ├── store.test.ts │ │ └── store.ts │ ├── types │ │ └── index.ts │ └── utils │ │ ├── iconfont.ts │ │ └── index.ts ├── InteractContainer │ ├── __tests__ │ │ └── index.test.tsx │ ├── demos │ │ ├── Basic.tsx │ │ ├── Controlled.tsx │ │ └── WithContainer.tsx │ ├── hooks │ │ ├── InteractModel.test.ts │ │ ├── InteractModel.ts │ │ ├── useContainer.ts │ │ ├── useContextCanvas.test.ts │ │ ├── useContextCanvas.ts │ │ ├── useInteractModel.test.ts │ │ ├── useInteractModel.ts │ │ ├── useInteractStatus.test.ts │ │ ├── useInteractStatus.ts │ │ ├── useInteraction.ts │ │ ├── useRender.test.ts │ │ └── useRender.ts │ ├── index.tsx │ ├── style.ts │ ├── type.ts │ ├── utils.test.ts │ └── utils.ts ├── Layout │ ├── components │ │ ├── HeaderAndFooter.tsx │ │ ├── LayoutTypeContainer.tsx │ │ └── PannelDefault.tsx │ ├── demos │ │ ├── _defaultProps.tsx │ │ ├── basic.tsx │ │ ├── components │ │ │ └── sessinList.tsx │ │ ├── dingding.tsx │ │ ├── noLeftPannel.tsx │ │ ├── single.tsx │ │ ├── themeType.tsx │ │ └── types.tsx │ ├── index.en-US.md │ ├── index.tsx │ ├── index.zh-CN.md │ └── style.ts ├── Markdown │ ├── CodeBlock.tsx │ ├── demos │ │ ├── code.tsx │ │ ├── data.ts │ │ ├── htmlPlugin.tsx │ │ ├── index.tsx │ │ └── renderComponets.tsx │ ├── index.en-US.md │ ├── index.tsx │ ├── index.zh-CN.md │ ├── style.ts │ └── wrapper.tsx ├── ProBuilder │ ├── components │ │ ├── AssetEmpty │ │ │ └── index.tsx │ │ ├── AssetStoreUpdater │ │ │ └── index.tsx │ │ ├── Canvas │ │ │ ├── Component.tsx │ │ │ └── index.tsx │ │ ├── Code │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── ConfigPanel │ │ │ └── index.tsx │ │ ├── Controller │ │ │ └── index.tsx │ │ ├── NavBar │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ └── Stage │ │ │ ├── Empty.style.ts │ │ │ ├── Empty.tsx │ │ │ ├── index.tsx │ │ │ └── style.ts │ ├── container │ │ ├── App.tsx │ │ ├── Provider.tsx │ │ ├── StoreUpdater.tsx │ │ ├── index.tsx │ │ └── style.ts │ ├── hooks │ │ ├── __snapshots__ │ │ │ └── useProBuilder.test.ts.snap │ │ ├── useAssetAwareness.ts │ │ ├── useCanvasInteraction.ts │ │ ├── useEditorAwareness.ts │ │ ├── useHotkeyManager.ts │ │ ├── useProBuilder.test.ts │ │ └── useProBuilder.ts │ ├── index.ts │ ├── store │ │ ├── __snapshots__ │ │ │ └── createStore.test.ts.snap │ │ ├── __test__ │ │ │ └── config.test.ts │ │ ├── createStore.test.ts │ │ ├── createStore.ts │ │ ├── index.ts │ │ ├── selectors.test.ts │ │ ├── selectors.ts │ │ └── slices │ │ │ ├── awareness.ts │ │ │ ├── canvas.ts │ │ │ ├── config.ts │ │ │ ├── configPanel.ts │ │ │ └── general.ts │ ├── tests │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ └── index.test.tsx │ └── utils │ │ └── yjs.ts ├── ProEditor │ ├── ProEditorProvider │ │ ├── StoreUpdater.tsx │ │ ├── index.test.tsx │ │ └── index.tsx │ ├── hooks │ │ ├── useProEditor.test.ts │ │ └── useProEditor.ts │ ├── index.ts │ ├── middleware │ │ ├── index.ts │ │ ├── pro-editor │ │ │ ├── index.ts │ │ │ └── type.ts │ │ └── types │ │ │ └── utils.ts │ ├── store │ │ ├── __snapshots__ │ │ │ └── createStore.test.ts.snap │ │ ├── __test__ │ │ │ └── config.test.ts │ │ ├── createStore.test.ts │ │ ├── createStore.ts │ │ ├── index.ts │ │ └── slices │ │ │ ├── config.ts │ │ │ └── general.ts │ └── utils │ │ └── yjs.ts ├── Snippet │ ├── demos │ │ ├── index.tsx │ │ └── spotlight.tsx │ ├── index.en-US.md │ ├── index.tsx │ ├── index.zh-CN.md │ └── style.ts ├── SortableList │ ├── __tests__ │ │ └── index.test.tsx │ ├── components │ │ ├── Item │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── List │ │ │ ├── index.tsx │ │ │ └── style.ts │ │ ├── SortableItem.tsx │ │ └── index.ts │ ├── container │ │ ├── App.tsx │ │ ├── Provider.tsx │ │ ├── StoreUpdater.tsx │ │ └── index.tsx │ ├── demos │ │ ├── Basic.tsx │ │ ├── _ItemRender.tsx │ │ ├── controlled.tsx │ │ ├── creatorButtonProps.tsx │ │ ├── data.ts │ │ ├── empty.tsx │ │ ├── getItemStyles.tsx │ │ ├── handle.tsx │ │ ├── hideRemove.tsx │ │ ├── provider.tsx │ │ ├── ref.tsx │ │ ├── renderContent.tsx │ │ ├── renderItem.tsx │ │ └── useSortableList.tsx │ ├── features │ │ ├── DragOverlay.tsx │ │ └── SortList.tsx │ ├── hooks │ │ └── useSortableList.ts │ ├── index.en-US.md │ ├── index.ts │ ├── index.zh-CN.md │ ├── store │ │ ├── index.ts │ │ ├── initialState.ts │ │ ├── keyManagerReducer.ts │ │ ├── listDataReducer.test.ts │ │ ├── listDataReducer.ts │ │ └── store.ts │ ├── style.ts │ ├── type │ │ ├── action.ts │ │ ├── component.ts │ │ ├── index.ts │ │ └── store.ts │ └── utils │ │ ├── index.tsx │ │ └── useStoreUpdater.ts ├── SortableTree │ ├── components │ │ ├── TreeItem │ │ │ └── index.tsx │ │ └── index.ts │ ├── container │ │ ├── App.tsx │ │ ├── Provider.tsx │ │ ├── StoreUpdater.tsx │ │ └── index.tsx │ ├── demos │ │ ├── _multiSelect.tsx │ │ ├── controlled.tsx │ │ ├── data.ts │ │ ├── data │ │ │ └── virtual.ts │ │ ├── default.tsx │ │ ├── disableDrag.tsx │ │ ├── multiSelector.ts │ │ ├── renderContent.tsx │ │ ├── sortableRule.tsx │ │ └── virtual.tsx │ ├── features │ │ ├── DragOverlay.tsx │ │ └── TreeList.tsx │ ├── hooks │ │ └── useSortableTree.ts │ ├── index.en-US.md │ ├── index.ts │ ├── index.zh-CN.md │ ├── keyboardCoordinates.ts │ ├── store │ │ ├── index.ts │ │ ├── initialState.ts │ │ ├── selectors.ts │ │ ├── slices │ │ │ ├── crudSlice.ts │ │ │ ├── dndSlice.ts │ │ │ └── selectionSlice.ts │ │ ├── store.ts │ │ ├── treeDataReducer.test.ts │ │ └── treeDataReducer.ts │ ├── style.ts │ ├── types │ │ ├── custom.ts │ │ ├── data.ts │ │ └── index.ts │ └── utils │ │ ├── treeNode.test.ts │ │ ├── treeNode.ts │ │ └── utils.ts ├── antd │ ├── Input.tsx │ ├── InputNumber.tsx │ ├── Segmented.tsx │ ├── Select.tsx │ ├── Tabs.tsx │ ├── Tree.tsx │ ├── TreeSelect.tsx │ ├── demos │ │ ├── basic.tsx │ │ ├── inputNumber.tsx │ │ ├── segmented.tsx │ │ ├── select.tsx │ │ ├── tabs.tsx │ │ ├── tree.tsx │ │ └── treeselect.tsx │ ├── index.en-US.md │ ├── index.ts │ └── index.zh-CN.md ├── components │ ├── CopyButton │ │ └── index.tsx │ └── Spotlight │ │ ├── index.tsx │ │ └── style.ts ├── hooks │ └── useCopied.ts ├── index.ts ├── theme │ ├── index.ts │ └── themes │ │ ├── antdTheme.ts │ │ ├── darkAlgorithm.ts │ │ ├── index.ts │ │ ├── stylish.ts │ │ └── token.ts ├── types │ ├── c2d2c.ts │ ├── catogory.ts │ ├── index.ts │ └── schema.ts └── utils │ ├── autoId.ts │ ├── c2d2c.tsx │ ├── index.ts │ └── tests │ ├── c2d2c.test.tsx │ └── schema.ts ├── tests ├── __snapshots__ │ └── demo.test.tsx.snap ├── demo.test.tsx └── test-setup.ts ├── tsconfig-check.json ├── tsconfig.json └── vitest.config.ts /.changelogrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayTypes: ['feat', 'fix', 'style', 'pref'], 3 | titleLanguage: 'zh-CN', 4 | showAuthor: true, 5 | showAuthorAvatar: true, 6 | showSummary: true, 7 | reduceHeadingLevel: true, 8 | newlineTimestamp: true, 9 | addBackToTop: true, 10 | }; 11 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['gitmoji'], 3 | rules: { 4 | 'footer-leading-blank': [0, 'never'], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /.dumirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi'; 2 | 3 | export default defineConfig({ 4 | outputPath: 'docs-dist', 5 | mfsu: false, 6 | favicons: ['https://gw.alipayobjects.com/zos/antfincdn/upvrAjAPQX/Logo_Tech%252520UI.svg'], 7 | ssr: false, 8 | hash: true, 9 | ignoreMomentLocale: true, 10 | themeConfig: { 11 | socialLinks: { 12 | github: 'https://github.com/ant-design/pro-editor', 13 | }, 14 | footer: 'Made with ❤️ by 蚂蚁集团 - AFX & 数字科技', 15 | logo: 'https://gw.alipayobjects.com/zos/antfincdn/upvrAjAPQX/Logo_Tech%252520UI.svg', 16 | name: '@ant-design/pro-editor', 17 | }, 18 | extraBabelPlugins: ['antd-style'], 19 | locales: [ 20 | { name: 'English', id: 'en-US' }, 21 | { name: '简体中文', id: 'zh-CN' }, 22 | ], 23 | }); 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const config = require('@umijs/lint/dist/config/eslint'); 2 | 3 | module.exports = { 4 | ...config, 5 | rules: { 6 | ...config.rules, 7 | // 下述规则为判断后可以保留的部分 8 | 'array-callback-return': 'off', 9 | 'no-useless-escape': 'off', 10 | 'no-case-declarations': 'off', 11 | '@typescript-eslint/no-use-before-define': [2, { functions: false }], 12 | '@typescript-eslint/no-unused-vars': [ 13 | 2, 14 | { 15 | args: 'after-used', 16 | ignoreRestSiblings: true, 17 | argsIgnorePattern: '^_', 18 | varsIgnorePattern: '^_', 19 | }, 20 | ], 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | // more father config: https://github.com/umijs/father/blob/master/docs/config.md 5 | esm: { output: 'es' }, 6 | }); 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1_bug_report.yml: -------------------------------------------------------------------------------- 1 | name: '🐛 反馈缺陷 Bug Report' 2 | description: '反馈一个问题缺陷 | Report an bug' 3 | title: '[Bug] ' 4 | labels: '🐛 Bug' 5 | body: 6 | - type: dropdown 7 | attributes: 8 | label: '💻 系统环境 | Operating System' 9 | options: 10 | - Windows 11 | - macOS 12 | - Ubuntu 13 | - Other Linux 14 | - Other 15 | validations: 16 | required: true 17 | - type: dropdown 18 | attributes: 19 | label: '🌐 浏览器 | Browser' 20 | options: 21 | - Chrome 22 | - Edge 23 | - Safari 24 | - Firefox 25 | - Other 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: '🐛 问题描述 | Bug Description' 31 | description: A clear and concise description of the bug. 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: '🚦 期望结果 | Expected Behavior' 37 | description: A clear and concise description of what you expected to happen. 38 | - type: textarea 39 | attributes: 40 | label: '📷 复现步骤 | Recurrence Steps' 41 | description: A clear and concise description of how to recurrence. 42 | - type: textarea 43 | attributes: 44 | label: '📝 补充信息 | Additional Information' 45 | description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here. 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_feature_request.yml: -------------------------------------------------------------------------------- 1 | name: '🌠 功能需求 Feature Request' 2 | description: '需求或建议 | Suggest an idea' 3 | title: '[Request] ' 4 | labels: ['Feature Request'] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: '🥰 需求描述 | Feature Description' 9 | description: Please add a clear and concise description of the problem you are seeking to solve with this feature request. 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: '🧐 解决方案 | Proposed Solution' 15 | description: Describe the solution you'd like in a clear and concise manner. 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: '📝 补充信息 | Additional Information' 21 | description: Add any other context about the problem here. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3_question.yml: -------------------------------------------------------------------------------- 1 | name: '😇 疑问或帮助 Help Wanted' 2 | description: '疑问或需要帮助 | Need help' 3 | title: '[Question] ' 4 | labels: '😇 Help Wanted' 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: '🧐 问题描述 | Proposed Solution' 9 | description: A clear and concise description of the proplem. 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: '📝 补充信息 | Additional Information' 15 | description: Add any other context about the problem here. 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/4_other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '📝 其他 Other' 3 | about: '其他问题 | Other issues' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### 💻 变更类型 | Change Type 2 | 3 | 4 | 5 | - [ ] ✨ feat 6 | - [ ] 🐛 fix 7 | - [ ] ♻️ refactor 8 | - [ ] 💄 style 9 | - [ ] 🔨 chore 10 | - [ ] ⚡️ perf 11 | - [ ] 📝 docs 12 | 13 | #### 🔀 变更说明 | Description of Change 14 | 15 | 16 | 17 | #### 📝 补充信息 | Additional Information 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 🛎️ 13 | uses: actions/checkout@v3 14 | 15 | - name: Install pnpm 16 | uses: pnpm/action-setup@v2 17 | with: 18 | version: 8 19 | 20 | - name: Setup Node.js environment 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: '18' 24 | 25 | - name: Install and Build 🔧 26 | run: | 27 | pnpm i 28 | pnpm run lint 29 | pnpm run docs:build 30 | 31 | - name: Deploy 🚀 32 | uses: JamesIves/github-pages-deploy-action@4.1.1 33 | with: 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | branch: gh-pages # The branch the action should deploy to. 36 | folder: docs-dist # The folder the action should deploy. 37 | clean: true # Automatically remove deleted files from the deploy branch 38 | single-commit: true # Create a single commit rather than doing a full git history merge 39 | -------------------------------------------------------------------------------- /.github/workflows/preview.yml: -------------------------------------------------------------------------------- 1 | name: Surge PR Preview 2 | 3 | on: 4 | pull_request: 5 | 6 | workflow_dispatch: 7 | 8 | jobs: 9 | preview: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Install pnpm 15 | uses: pnpm/action-setup@v2 16 | with: 17 | version: 8 18 | 19 | - uses: afc163/surge-preview@v1 20 | id: preview_step 21 | with: 22 | surge_token: ${{ secrets.SURGE_TOKEN }} 23 | github_token: ${{ secrets.GITHUB_TOKEN }} 24 | build: | 25 | pnpm i 26 | pnpm run docs:preview 27 | dist: docs-dist 28 | 29 | - name: Get the preview_url 30 | run: echo "url => ${{ steps.preview_step.outputs.preview_url }}" 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | 7 | steps: 8 | - uses: actions/checkout@v3 9 | 10 | - name: Install pnpm 11 | uses: pnpm/action-setup@v2 12 | with: 13 | version: 8 14 | 15 | - name: Setup Node.js environment 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '20' 19 | 20 | - name: Install deps 21 | run: pnpm install 22 | 23 | - name: lint 24 | run: pnpm run ci 25 | 26 | - name: build 27 | run: pnpm run build 28 | 29 | - name: test and coverage 30 | run: pnpm run test:coverage 31 | 32 | - name: Upload coverage to Codecov 33 | uses: codecov/codecov-action@v3 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | es 4 | lib 5 | .dumi/tmp 6 | .dumi/tmp-test 7 | .dumi/tmp-production 8 | .DS_Store 9 | package-lock.json 10 | yarn.lock 11 | .idea 12 | docs-dist 13 | server 14 | .husky/prepare-commit-msg 15 | coverage 16 | .vscode 17 | bun.lockb -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit "${1}" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.i18nrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * find src -type f -name "*.en-US.md" -exec rm -f {} + 3 | * @type {import("@lobehub/i18n-cli").Config} 4 | */ 5 | module.exports = { 6 | markdown: { 7 | entry: ['docs/**/**.md', 'src/**/**.md'], 8 | entryLocale: 'zh-CN', 9 | entryExtension: '.zh-CN.md', 10 | outputLocales: ['en-US'], 11 | }, 12 | modelName: 'gpt-3.5-turbo-1106', 13 | }; 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | lockfile=false 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist 2 | *.yaml 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pluginSearchDirs: false, 3 | plugins: [ 4 | require.resolve('prettier-plugin-organize-imports'), 5 | require.resolve('prettier-plugin-packagejson'), 6 | ], 7 | printWidth: 100, 8 | proseWrap: 'never', 9 | singleQuote: true, 10 | endOfLine: 'lf', 11 | trailingComma: 'all', 12 | overrides: [ 13 | { 14 | files: '*.md', 15 | options: { 16 | proseWrap: 'preserve', 17 | }, 18 | }, 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /.releaserc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['semantic-release-config-gitmoji'], 3 | }; 4 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@umijs/lint/dist/config/stylelint" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) rdmclin2@163.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__mocks__/zustand/traditional.ts: -------------------------------------------------------------------------------- 1 | import { act } from 'react-dom/test-utils'; 2 | import { beforeEach } from 'vitest'; 3 | import { createWithEqualityFn as actualCreate } from 'zustand/traditional'; 4 | 5 | // a variable to hold reset functions for all stores declared in the app 6 | const storeResetFns = new Set<() => void>(); 7 | 8 | // when creating a store, we get its initial state, create a reset function and add it in the set 9 | const createImpl = (createState) => { 10 | const store = actualCreate(createState, Object.is); 11 | const initialState = store.getState(); 12 | storeResetFns.add(() => store.setState(initialState, true)); 13 | return store; 14 | }; 15 | 16 | // Reset all stores after each test run 17 | beforeEach(() => { 18 | act(() => 19 | storeResetFns.forEach((resetFn) => { 20 | resetFn(); 21 | }), 22 | ); 23 | }); 24 | 25 | export const createWithEqualityFn = (f) => (f === undefined ? createImpl : createImpl(f)); 26 | 27 | export { useStoreWithEqualityFn as useStore } from 'zustand/traditional'; 28 | -------------------------------------------------------------------------------- /docs/guide/demos/ColumnList/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ColumnItemList } from '@ant-design/pro-editor'; 2 | import { ColumnList } from '@ant-design/pro-editor'; 3 | import { useState } from 'react'; 4 | 5 | import { tableColumnValueOptions } from './data'; 6 | 7 | type SchemaItem = { 8 | title: string; 9 | valueType: string; 10 | dataIndex: string; 11 | }; 12 | 13 | const INIT_VALUES = [ 14 | { title: 'Index', valueType: 'indexBorder', dataIndex: 'index' }, 15 | { 16 | title: 'Enterprise', 17 | valueType: 'text', 18 | dataIndex: 'name', 19 | }, 20 | { title: 'Company', valueType: 'text', dataIndex: 'authCompany' }, 21 | ]; 22 | 23 | const columns: ColumnItemList = [ 24 | { 25 | title: 'Title', 26 | dataIndex: 'title', 27 | type: 'input', 28 | }, 29 | { 30 | title: 'ValueType', 31 | dataIndex: 'valueType', 32 | type: 'select', 33 | options: tableColumnValueOptions, 34 | }, 35 | { 36 | title: 'DataIndex', 37 | dataIndex: 'dataIndex', 38 | type: 'select', 39 | }, 40 | ]; 41 | 42 | export default () => { 43 | const [value, setValue] = useState(INIT_VALUES); 44 | 45 | return ( 46 | { 50 | setValue(values); 51 | console.log('onChange', values); 52 | }} 53 | /> 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /docs/guide/demos/Redo/App.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Card, Divider, Tabs } from 'antd'; 2 | import { useTheme } from 'antd-style'; 3 | import { Flexbox } from 'react-layout-kit'; 4 | 5 | import Toolbar from './Toolbar'; 6 | import { useStore } from './store'; 7 | 8 | const App = () => { 9 | const { data, plus, tabs, switchTabs, plusWithoutHistory } = useStore(); 10 | 11 | const theme = useTheme(); 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
data: {data}
22 |
23 | 24 |
25 | 33 | 34 |
下面的 +2 可使得 在历史记录外添加让 data +2
35 | 36 |
37 |
38 |
39 |
40 | ); 41 | }; 42 | 43 | export default App; 44 | -------------------------------------------------------------------------------- /docs/guide/demos/Redo/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | import { RedoOutlined, UndoOutlined } from '@ant-design/icons'; 2 | import { useProEditor } from '@ant-design/pro-editor'; 3 | import { Badge, Button } from 'antd'; 4 | import { Flexbox } from 'react-layout-kit'; 5 | 6 | const Toolbar = () => { 7 | const { undo, redo, undoStack, redoStack } = useProEditor(); 8 | 9 | const undoStackList = undoStack(); 10 | const redoStackList = redoStack(); 11 | 12 | const lastAction = undoStackList.at(-1); 13 | 14 | return ( 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 上次操作时间: 31 | {lastAction ? new Date(lastAction.timestamp).toLocaleTimeString() : '-'} 32 | 33 | 34 | 上次操作名称: 35 | {lastAction?.name ?? '-'} 36 | {' '} 37 | 38 | 上次操作类型: 39 | {lastAction?.type ?? '-'} 40 | 41 | 42 | ); 43 | }; 44 | 45 | export default Toolbar; 46 | -------------------------------------------------------------------------------- /docs/guide/demos/Redo/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { ProEditorProvider } from '@ant-design/pro-editor'; 5 | import App from './App'; 6 | 7 | import { useStore } from './store'; 8 | 9 | export default () => { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /docs/guide/demos/Redo/store.ts: -------------------------------------------------------------------------------- 1 | import { proEditorMiddleware, ProEditorOptions } from '@ant-design/pro-editor'; 2 | import { create, StateCreator } from 'zustand'; 3 | import { devtools, subscribeWithSelector } from 'zustand/middleware'; 4 | 5 | interface Store { 6 | tabs: string; 7 | plus: () => void; 8 | plusWithoutHistory: () => void; 9 | data: number; 10 | switchTabs: (key: string) => void; 11 | } 12 | 13 | const createStore: StateCreator = ( 14 | set, 15 | get, 16 | ) => ({ 17 | tabs: '1', 18 | switchTabs: (key) => { 19 | set({ tabs: key }); 20 | }, 21 | plusWithoutHistory: () => { 22 | set((s) => ({ ...s, data: s.data + 2 }), false, { 23 | type: 'plusWithoutHistory', 24 | recordHistory: false, 25 | }); 26 | }, 27 | 28 | plus: () => { 29 | const nextData = get().data + 1; 30 | 31 | set({ data: nextData }, false, { 32 | type: 'plus', 33 | payload: nextData, 34 | name: '+1', 35 | }); 36 | }, 37 | data: 3, 38 | }); 39 | 40 | interface ProEditorStore { 41 | data: number; 42 | } 43 | 44 | const storeName = 'redo-demo-app'; 45 | 46 | const proEditorOptions: ProEditorOptions = { 47 | name: storeName, 48 | partialize: (s) => ({ data: s.data }), 49 | }; 50 | 51 | export const useStore = create()( 52 | devtools(proEditorMiddleware(subscribeWithSelector(createStore), proEditorOptions), { 53 | name: storeName, 54 | }), 55 | ); 56 | 57 | useStore.subscribe((s) => s.data, console.log); 58 | -------------------------------------------------------------------------------- /docs/guide/demos/editor-store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './selectors'; 2 | export { createEditorStore } from './store'; 3 | -------------------------------------------------------------------------------- /docs/guide/demos/editor-store/initalState.ts: -------------------------------------------------------------------------------- 1 | import { CRUDState, initialCRUDState } from './slices/crud'; 2 | 3 | export type EditorStoreState = CRUDState; 4 | 5 | export const initialState: EditorStoreState = { 6 | ...initialCRUDState, 7 | }; 8 | -------------------------------------------------------------------------------- /docs/guide/demos/editor-store/selectors.ts: -------------------------------------------------------------------------------- 1 | export * from './slices/crud/selectors'; 2 | -------------------------------------------------------------------------------- /docs/guide/demos/editor-store/slices/crud/action.ts: -------------------------------------------------------------------------------- 1 | import { StateCreator } from 'zustand'; 2 | import { EditorStore } from '../../store'; 3 | 4 | export interface CrudSlice { 5 | increment: () => void; 6 | updateText: (text: string) => void; 7 | } 8 | 9 | export const createCrudSlice: StateCreator< 10 | EditorStore, 11 | [['zustand/devtools', never]], 12 | [], 13 | CrudSlice 14 | > = (set) => ({ 15 | increment: () => 16 | set((state) => ({ 17 | ...state, 18 | count: state.count + 1, 19 | })), 20 | updateText: (text) => set((state) => ({ ...state, text })), 21 | }); 22 | -------------------------------------------------------------------------------- /docs/guide/demos/editor-store/slices/crud/index.ts: -------------------------------------------------------------------------------- 1 | export * from './action'; 2 | export * from './initalState'; 3 | -------------------------------------------------------------------------------- /docs/guide/demos/editor-store/slices/crud/initalState.ts: -------------------------------------------------------------------------------- 1 | export interface CRUDState { 2 | count: number; 3 | text: string; 4 | } 5 | 6 | export const initialCRUDState = { 7 | count: 0, 8 | text: '', 9 | }; 10 | -------------------------------------------------------------------------------- /docs/guide/demos/editor-store/slices/crud/selectors.ts: -------------------------------------------------------------------------------- 1 | import type { EditorStore } from '../../store'; 2 | 3 | export const isDesignTest = (s: EditorStore) => s.text === 'design'; 4 | 5 | export const editorSelectors = { isDesignTest }; 6 | -------------------------------------------------------------------------------- /docs/guide/demos/editor-store/store.ts: -------------------------------------------------------------------------------- 1 | import isEqual from 'fast-deep-equal'; 2 | import type { StateCreator } from 'zustand'; 3 | import { devtools } from 'zustand/middleware'; 4 | import { createWithEqualityFn } from 'zustand/traditional'; 5 | 6 | import { EditorStoreState, initialState } from './initalState'; 7 | import { CrudSlice, createCrudSlice } from './slices/crud'; 8 | 9 | export type EditorStore = EditorStoreState & CrudSlice; 10 | 11 | const vanillaStore: StateCreator = (...params) => ({ 12 | ...initialState, 13 | ...createCrudSlice(...params), 14 | }); 15 | 16 | export const createEditorStore = createWithEqualityFn()( 17 | devtools(vanillaStore, { name: 'EditorStore' }), 18 | isEqual, 19 | ); 20 | -------------------------------------------------------------------------------- /docs/guide/intro.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 快速开始 3 | group: 4 | title: 快速上手 5 | order: 1 6 | nav: 7 | title: 文档 8 | order: 1 9 | --- 10 | 11 | # 快速开始 12 | 13 | ProEditor 定位编辑器 UI 框架,期望为「编辑」场景提供丰富、易用的基础组件与原子能力。 14 | 15 | ## 安装 16 | 17 | ```bash 18 | # @ant-design/pro-editor 基于 antd 和 antd-style,需要在项目中安装 19 | $ npm install antd antd-style -S 20 | $ npm install @ant-design/pro-editor -S 21 | ``` 22 | 23 | ### 使用组件 24 | 25 | ProEditor 提供了一系列针对「编辑」场景优化的组件,包括但不限于 「SortableList」、「SortableTree」、「DraggablePanel」、「Highlight」、「ContextMenu」等。完整的组件文档详见: [基础组件](/components/action-icon) 26 | 27 | 以下则是一个典型的数组对象编辑场景,我们提供的 ColumnList 可以帮助开发者快速实现一个高质量的数组编辑组件。 28 | 29 | 30 | 31 | ### 组件装配器 32 | 33 | ProEditor 最初的定位是作为组件的可视化配置框架。因此在 ProEditor 中提供了一系列便于可视化组件装配的容器与原子组件,帮助开发者快速实现一个可视化配置的组件。 34 | 35 | 详见:[ProEditor 装配器容器](/pro-editor) 36 | 37 | ## 工程化能力 38 | 39 | ### 按需加载 40 | 41 | ProEditor 默认支持基于 ES modules 的 tree shaking,直接引入 `import { ActionIcon } from '@ant-design/pro-editor`; 就会有按需加载的效果。 42 | 43 | ### TypeScript 44 | 45 | ProEditor 使用 TypeScript 进行开发,因此提供了完整的类型定义。 46 | -------------------------------------------------------------------------------- /docs/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | title: ProEditor 4 | description: 🌟 通用编辑器 UI 框架 5 | actions: 6 | - text: 快速开始 → 7 | link: /guide/intro 8 | - text: Github 9 | link: https://github.com/ant-design/pro-editor 10 | 11 | features: 12 | - title: 简单易用 13 | description: 在 Ant Design 上进行了自己的封装,更加易用 14 | image: https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/q48YQ5X4ytAAAAAAAAAAAAAAFl94AQBr 15 | 16 | - title: Ant Design 17 | description: 与 Ant Design 设计体系一脉相承,无缝对接 antd 项目 18 | image: https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg 19 | 20 | - title: 通用编辑器组件 21 | description: 提供完备的编辑器组件,方便使用者定制自己的编辑器 22 | image: https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/UKqDTIp55HYAAAAAAAAAAAAAFl94AQBr 23 | 24 | - title: 预设样式 25 | description: 样式风格与 antd 一脉相承,无需魔改,浑然天成,默认好用的主题系统 26 | image: https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/Y_NMQKxw7OgAAAAAAAAAAAAAFl94AQBr 27 | 28 | - title: 预设行为 29 | description: 更少的代码,更少的 Bug,更多的功能 30 | image: https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/U3XjS5IA1tUAAAAAAAAAAAAAFl94AQBr 31 | 32 | - title: TypeScript 33 | description: 使用 TypeScript 开发,提供完整的类型定义文件,无需频繁打开官网 34 | image: https://gw.alipayobjects.com/zos/antfincdn/Eb8IHpb9jE/Typescript_logo_2020.svg 35 | --- 36 | -------------------------------------------------------------------------------- /docs/pro-editor/component-assets.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 装配器组件元数据模型 3 | group: 基础框架 4 | --- 5 | 6 | # 复杂组件的元数据 7 | 8 | ### 木偶模型 9 | 10 | 高代码(props)和 低代码(配置器)的关系不是简单的一一映射,而是一种有机的组织形态,我们把它称为 —— 木偶模型。 props 就像是木偶的线,而配置器就是木偶的控制器,它们的关系如下图所示: 11 | 12 | ![image.png](https://mdn.alipayobjects.com/huamei_re70wt/afts/img/A*d8rTT4gXf-UAAAAAAAAAAAAADmuEAQ/original) 13 | 14 | 一个对于配置器来说好用的结构,大概率和组件的 props 不是一一对应的。因此我们会专门为配置器设计一层独立的数据模型,我们称之为 config。 15 | 16 | ## 元数据模型 17 | 18 | 我们把代码的属性叫 props,然后把配置器的属性叫 config,提供配置器属性定义的描述叫 schema。 那么他们之间的关系如下图所示: 19 | 20 | ![image.png](https://mdn.alipayobjects.com/huamei_re70wt/afts/img/A*kSZ-S6Pe0yUAAAAAAAAAAAAADmuEAQ/original) 21 | 22 | - Schema(config): 一份给到配置器的编辑器元数据 23 | - Emmiter(config -> props):将配置器转为代码属性 24 | - Parser(props -> config):将 props 解析为 config 25 | 26 | 基于这样一种数据模型,我们就能在理论上实现代码到装配器的双向转换。 27 | 28 | ### Model 结构 29 | 30 | 反映上述元数据模型的代码声明如下: 31 | 32 | ```typescript 33 | const model: Model = { 34 | key: '', 35 | schema: (config: Config, store: ProEditorStore): Schema => {}, 36 | parser: (props: Props): Config => {}, 37 | emitter: (config: Config, env: EmmiterEnv): Props => {}, 38 | }; 39 | ``` 40 | 41 | ## ComponentAssets 42 | 43 | 上述 Model 反映了组件数据流转的生命周期,接下来介绍组件资产元数据的描述模型 ComponentAssets。 44 | 45 | ```typescript 46 | export const tableAssetParams = { 47 | id: 'table', 48 | // ... 49 | 50 | // 输入模型 51 | models: [tableModel, dataModel, toolbarModel], 52 | }; 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/pro-editor/demos/buttonAsset/_Component.tsx: -------------------------------------------------------------------------------- 1 | import { useProBuilderStore } from '@ant-design/pro-editor'; 2 | import { Button } from 'antd'; 3 | import isEqual from 'fast-deep-equal'; 4 | import { memo } from 'react'; 5 | 6 | export const ButtonComponent = memo(() => { 7 | const data = useProBuilderStore((s) => s.config, isEqual); 8 | 9 | console.log(data); 10 | return 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | describe('ContextCanvas', () => { 27 | it('toggleClass', () => { 28 | const { queryByTestId } = render(); 29 | 30 | const btn = queryByTestId('btn'); 31 | 32 | expect(btn.className).toBe(''); 33 | 34 | // 点击激活 35 | act(() => { 36 | fireEvent.click(btn); 37 | }); 38 | 39 | expect(btn.className).toBe('ant-editor-context-canvas-click'); 40 | // 点击失焦 41 | act(() => { 42 | fireEvent.click(btn); 43 | }); 44 | expect(btn.className).toBe(''); 45 | // 再次激活 46 | act(() => { 47 | fireEvent.click(btn); 48 | }); 49 | expect(btn.className).toBe('ant-editor-context-canvas-click'); 50 | // 点击非btn区域失焦 51 | act(() => { 52 | fireEvent.click(document.body); 53 | }); 54 | expect(btn.className).toBe(''); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/InteractContainer/demos/Basic.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Space } from 'antd'; 2 | import InteractContainer from '../index'; 3 | 4 | export const Demo = () => { 5 | return ( 6 | 15 |
16 | 17 | 可选择元素 18 | 可选择元素 19 | 可选择元素 20 | 21 |
22 |
23 | ); 24 | }; 25 | 26 | export default Demo; 27 | -------------------------------------------------------------------------------- /src/InteractContainer/demos/WithContainer.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Divider } from 'antd'; 2 | import InteractContainer from '../index'; 3 | 4 | const WithContainer = () => { 5 | return ( 6 | 16 |
17 |
18 | 只有红色虚线框的内容可以选择(hover会有悬浮样式) 19 |
20 |
点击红色虚线框之外蓝色框之内的区域,可以取消选中
21 |
22 | 23 |
24 | 此区域无法选择 25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | export default WithContainer; 32 | -------------------------------------------------------------------------------- /src/InteractContainer/hooks/useContainer.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import type { ContainerNode, GetContainer } from '../type'; 3 | import { getContainerElement } from '../utils'; 4 | 5 | export const useContainer = (getContainer: GetContainer) => { 6 | const [container, setContainer] = useState(); 7 | 8 | // 在组件 mount 之后确定最终容器 9 | useEffect(() => { 10 | setContainer(getContainerElement(getContainer)); 11 | // eslint-disable-next-line react-hooks/exhaustive-deps 12 | }, []); 13 | 14 | return container; 15 | }; 16 | -------------------------------------------------------------------------------- /src/InteractContainer/hooks/useInteractModel.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import { useInteractModel } from './useInteractModel'; 3 | 4 | // 开始之前将所有节点清理掉 5 | beforeEach(() => { 6 | document.body.childNodes.forEach((n) => n.remove()); 7 | }); 8 | 9 | describe('useInteractModel', () => { 10 | it('识别到元素', () => { 11 | const div = document.createElement('div'); 12 | div.setAttribute('data-uxid', 'row'); 13 | document.body.appendChild(div); 14 | 15 | const { result } = renderHook(() => 16 | useInteractModel([ 17 | { id: 'container', actions: ['hover'], selectors: ['row'] }, 18 | ]), 19 | ); 20 | 21 | expect(result.current.models[0].elements).toEqual([div]); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/InteractContainer/hooks/useInteractModel.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import type { CanvasInteractRule, ContainerNode } from '../type'; 3 | 4 | import { InteractModel } from './InteractModel'; 5 | 6 | export const useInteractModel = ( 7 | rules: CanvasInteractRule[], 8 | container: ContainerNode = document, 9 | ) => { 10 | const interactModel = useRef(new InteractModel(rules, container)); 11 | 12 | // 任何时候进行刷新,都要重新 13 | useEffect(() => { 14 | interactModel.current.initModels(); 15 | }); 16 | 17 | useEffect(() => { 18 | interactModel.current = new InteractModel(rules, container); 19 | }, [rules, container]); 20 | 21 | return interactModel.current; 22 | }; 23 | -------------------------------------------------------------------------------- /src/InteractContainer/hooks/useRender.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import { useRender } from './useRender'; 3 | 4 | describe('useRender', () => { 5 | it('渲染点击后取消', () => { 6 | const { result } = renderHook(() => useRender()); 7 | 8 | const div = document.createElement('div'); 9 | 10 | result.current.renderSelected(div); 11 | expect(div.className).toEqual('ant-editor-context-canvas-click'); 12 | 13 | expect(result.current.currentSelectedElementRef.current).toEqual(div); 14 | expect(result.current.isSelected()).toBeTruthy(); 15 | 16 | result.current.renderUnselected(div); 17 | expect(div.className).toEqual(''); 18 | 19 | expect(result.current.isSelected()).toBeFalsy(); 20 | expect(result.current.currentSelectedElementRef.current).toBeNull(); 21 | }); 22 | 23 | it('渲染悬浮后取消', () => { 24 | const { result } = renderHook(() => useRender()); 25 | 26 | const div = document.createElement('div'); 27 | result.current.renderHover(div); 28 | expect(div.className).toEqual('ant-editor-context-canvas-hover'); 29 | result.current.renderUnHover(); 30 | expect(div.className).toEqual(''); 31 | }); 32 | 33 | test('isSelected 默认值', () => { 34 | const { result } = renderHook(() => useRender()); 35 | 36 | expect(result.current.isSelected()).toBeFalsy(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/InteractContainer/index.tsx: -------------------------------------------------------------------------------- 1 | import { withProvider } from '@ant-design/pro-editor'; 2 | import type { FC } from 'react'; 3 | import { useContextCanvas } from './hooks/useContextCanvas'; 4 | import { useStyle } from './style'; 5 | import type { ContextCanvasProps } from './type'; 6 | 7 | const ContextCanvas: FC = (props) => { 8 | useStyle(); 9 | useContextCanvas(props); 10 | return <>{props.children}; 11 | }; 12 | 13 | export default withProvider(ContextCanvas); 14 | 15 | export * from './type'; 16 | -------------------------------------------------------------------------------- /src/InteractContainer/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { getContainerElement } from './utils'; 2 | 3 | describe('getContainerElement', () => { 4 | it('获取document', () => { 5 | expect(getContainerElement()).toEqual(document); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/Layout/demos/basic.tsx: -------------------------------------------------------------------------------- 1 | import { Button, EditorLayout, Input } from '@ant-design/pro-editor'; 2 | import { Space } from 'antd'; 3 | import { DefaultLayoutProps } from './_defaultProps'; 4 | 5 | export default () => { 6 | return ( 7 | 20 | ), 21 | extra: ( 22 | 23 | 24 | 25 | 26 | ), 27 | }} 28 | footer={{ 29 | ...DefaultLayoutProps.footer, 30 | }} 31 | leftPannel={{ 32 | children:
Left Pannel
, 33 | }} 34 | /> 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/Layout/demos/noLeftPannel.tsx: -------------------------------------------------------------------------------- 1 | import { EditorLayout } from '@ant-design/pro-editor'; 2 | 3 | export default () => { 4 | return ( 5 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/Layout/demos/single.tsx: -------------------------------------------------------------------------------- 1 | import { EditorLayout } from '@ant-design/pro-editor'; 2 | 3 | export default () => { 4 | return ( 5 | Left Pannel, 14 | }} 15 | /> 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/Layout/demos/types.tsx: -------------------------------------------------------------------------------- 1 | import { EditorLayout, Input } from '@ant-design/pro-editor'; 2 | import { Segmented, Space } from 'antd'; 3 | import { useState } from 'react'; 4 | import { DefaultLayoutProps } from './_defaultProps'; 5 | 6 | export default () => { 7 | const [value, setValue] = useState('Bottom'); 8 | 9 | return ( 10 | 16 | setValue(e.toString())} 20 | /> 21 | 34 | ), 35 | }} 36 | type={value} 37 | footer={{ 38 | ...DefaultLayoutProps.footer, 39 | children:
Footer
, 40 | }} 41 | leftPannel={{ 42 | children:
Left Pannel
, 43 | }} 44 | /> 45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/Markdown/demos/code.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 代码渲染 3 | * description: Markdown 内置 `Highlight` 代码渲染 4 | */ 5 | import { Markdown } from '@ant-design/pro-editor'; 6 | import { codeContent } from './data'; 7 | 8 | export default () => { 9 | return {codeContent}; 10 | }; 11 | -------------------------------------------------------------------------------- /src/Markdown/demos/htmlPlugin.tsx: -------------------------------------------------------------------------------- 1 | import { Markdown } from '@ant-design/pro-editor'; 2 | import rehypeRaw from 'rehype-raw'; 3 | import { htmlContent } from './data'; 4 | 5 | export default () => { 6 | return {htmlContent}; 7 | }; 8 | -------------------------------------------------------------------------------- /src/Markdown/demos/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * title: 自定义操作 3 | * description: 可以通过自己传入 components 等 `React-Markdown` 的 Props 来进行自定义,多余的会透传过去。 4 | */ 5 | import { Markdown } from '@ant-design/pro-editor'; 6 | 7 | import { content } from './data'; 8 | 9 | export default () => { 10 | return {content}; 11 | }; 12 | -------------------------------------------------------------------------------- /src/Markdown/demos/renderComponets.tsx: -------------------------------------------------------------------------------- 1 | import { Markdown } from '@ant-design/pro-editor'; 2 | import { Button } from 'antd'; 3 | import { memo } from 'react'; 4 | 5 | export default () => { 6 | return ( 7 | ( 10 | 17 | )), 18 | }} 19 | > 20 | {` 21 | This is [an example](http://example.com/ "Title") inline link. 22 | 23 | 24 | `} 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/Markdown/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 组件 3 | group: 基础组件 4 | title: Markdown 文档展示 5 | atomId: Markdown 6 | description: 7 | --- 8 | 9 | # Markdown 文档展示 10 | 11 | Markdown 是一个用于渲染 Markdown 文本的 React 组件。它支持各种 Markdown 语法,如标题、列表、链接、图片、代码块等。它通常用于文档、博客和其他文本密集型应用中。 12 | 13 | ## 代码演示 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ## APIs 24 | 25 | | 属性名 | 类型 | 描述 | 26 | | ------------- | --------------------------------- | ------------------------ | 27 | | children | string | 要渲染的 Markdown 内容。 | 28 | | className | string | Markdown 组件的类名。 | 29 | | onDoubleClick | () => void | 双击事件处理函数。 | 30 | | style | CSSProperties | Markdown 组件的样式。 | 31 | | rehypePlugins | Markdown rehypePlugins Types | rehype 自定义插件 | 32 | | remarkPlugins | remarkPlugins rehypePlugins Types | remark 自定义插件 | 33 | -------------------------------------------------------------------------------- /src/ProBuilder/components/AssetEmpty/index.tsx: -------------------------------------------------------------------------------- 1 | import { Empty } from 'antd'; 2 | import type { FC } from 'react'; 3 | import { memo } from 'react'; 4 | import { Center } from 'react-layout-kit'; 5 | 6 | import { createStyles } from '../../../theme'; 7 | 8 | const useStyles = createStyles(({ token, css, cx, prefixCls }) => { 9 | const prefix = `${prefixCls}-${token.editorPrefix}-pro-builder`; 10 | return { 11 | cls: cx( 12 | `${prefix}-empty`, 13 | css` 14 | height: 100%; 15 | background: ${token.colorBgLayout}; 16 | `, 17 | ), 18 | }; 19 | }); 20 | 21 | const AssetEmpty: FC = memo(() => { 22 | const { styles } = useStyles(); 23 | 24 | return ( 25 |
26 | 27 |
28 | ); 29 | }); 30 | 31 | export default AssetEmpty; 32 | -------------------------------------------------------------------------------- /src/ProBuilder/components/Code/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from '../../../theme'; 2 | 3 | export const useStyles = createStyles(({ token, stylish, css }) => { 4 | return { 5 | container: css` 6 | border-top: 1px solid ${token.colorBorder}; 7 | display: flex; 8 | flex-direction: column; 9 | `, 10 | 11 | header: css` 12 | background: ${token.colorBgContainer}; 13 | cursor: pointer; 14 | `, 15 | collapse: css` 16 | background: ${token.colorFillQuaternary}; 17 | &:hover { 18 | background: ${token.colorFillTertiary}; 19 | } 20 | `, 21 | headerTitle: css` 22 | ${stylish.containerBgHover} 23 | ${stylish.textInfo}; 24 | 25 | padding: 2px 8px; 26 | 27 | color: ${token.colorTextSecondary}; 28 | border-radius: ${token.borderRadius}px; 29 | `, 30 | code: css` 31 | background: ${token.colorFillQuaternary}; 32 | height: 100%; 33 | `, 34 | }; 35 | }); 36 | -------------------------------------------------------------------------------- /src/ProBuilder/components/ConfigPanel/index.tsx: -------------------------------------------------------------------------------- 1 | import { DraggablePanel } from '@ant-design/pro-editor'; 2 | import isEqual from 'fast-deep-equal'; 3 | import { memo } from 'react'; 4 | import { shallow } from 'zustand/shallow'; 5 | 6 | import { useStore } from '../../store'; 7 | 8 | const ConfigPanel: React.FC = memo(() => { 9 | const [width, updatePosition, updatePanelSize, togglePanelExpand, componentAsset, isExpand] = 10 | useStore( 11 | (s) => [ 12 | s.editorAwareness.panelSize.width, 13 | s.updatePanelPosition, 14 | s.updatePanelSize, 15 | s.togglePanelExpand, 16 | s.componentAsset, 17 | s.editorAwareness.panelExpand, 18 | ], 19 | shallow, 20 | ); 21 | 22 | const panelPosition = useStore((s) => s.editorAwareness.panelPosition, isEqual); 23 | 24 | return ( 25 | { 31 | if (!size.width) return; 32 | 33 | updatePanelSize({ 34 | width: typeof size.width === 'string' ? parseInt(size.width) : size.width, 35 | }); 36 | }} 37 | onExpandChange={togglePanelExpand} 38 | minWidth={340} 39 | size={{ width, height: '100%' }} 40 | > 41 | 42 | 43 | ); 44 | }); 45 | 46 | export default ConfigPanel; 47 | -------------------------------------------------------------------------------- /src/ProBuilder/components/Controller/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { isDesignModeSelector, useStore } from '../../store'; 3 | 4 | /** 5 | * Stage 下方的控制区域 6 | */ 7 | export const Controller = memo(() => { 8 | const componentAsset = useStore((s) => s.componentAsset); 9 | const isDesignMode = useStore(isDesignModeSelector); 10 | 11 | const Controller = isDesignMode 12 | ? componentAsset.DesignController 13 | : componentAsset.DevelopController; 14 | 15 | return Controller ? : null; 16 | }); 17 | -------------------------------------------------------------------------------- /src/ProBuilder/components/NavBar/index.tsx: -------------------------------------------------------------------------------- 1 | import { Space } from 'antd'; 2 | import type { FC, ReactNode } from 'react'; 3 | import { memo } from 'react'; 4 | import { Flexbox } from 'react-layout-kit'; 5 | import { useStyle } from './style'; 6 | 7 | export interface NavBarProps { 8 | /** 9 | * 替换logo 10 | */ 11 | logo?: ReactNode; 12 | } 13 | 14 | const NavBar: FC = memo(({ logo }) => { 15 | const { styles } = useStyle(); 16 | 17 | const defaultLogo = ( 18 | 19 | ProBuilder 24 |
ProBuilder
25 |
26 | ); 27 | 28 | return ( 29 | 37 |
{logo ?? defaultLogo}
38 |
39 | ); 40 | }); 41 | 42 | export default NavBar; 43 | -------------------------------------------------------------------------------- /src/ProBuilder/components/NavBar/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from '../../../theme'; 2 | 3 | export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { 4 | const prefix = `${prefixCls}-${token.editorPrefix}-pro-builder-navbar`; 5 | return { 6 | container: cx( 7 | prefix, 8 | css` 9 | background-color: ${token.colorBgContainer}; 10 | `, 11 | ), 12 | logo: cx( 13 | `${prefix}-logo`, 14 | css` 15 | font-size: 16px; 16 | `, 17 | ), 18 | img: cx( 19 | `${prefix}-logo-img`, 20 | css` 21 | height: 24px; 22 | `, 23 | ), 24 | }; 25 | }); 26 | -------------------------------------------------------------------------------- /src/ProBuilder/components/Stage/Empty.style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from '../../../theme'; 2 | 3 | export const useStyle = createStyles(({ token, css, cx }) => ({ 4 | container: cx( 5 | css` 6 | height: 100%; 7 | `, 8 | ), 9 | default: cx( 10 | css` 11 | padding: ${token.paddingLG}px; 12 | 13 | color: ${token.colorTextTertiary}; 14 | `, 15 | ), 16 | })); 17 | -------------------------------------------------------------------------------- /src/ProBuilder/components/Stage/Empty.tsx: -------------------------------------------------------------------------------- 1 | import { Empty } from 'antd'; 2 | import type { FC } from 'react'; 3 | import { memo } from 'react'; 4 | 5 | import { useStore } from '../../store'; 6 | 7 | import { Center } from 'react-layout-kit'; 8 | import { shallow } from 'zustand/shallow'; 9 | import { useStyle } from './Empty.style'; 10 | 11 | const Starter: FC = memo(() => { 12 | const [componentAsset] = useStore((s) => [s.componentAsset], shallow); 13 | 14 | const { styles } = useStyle(); 15 | 16 | return ( 17 |
18 | {componentAsset.CanvasStarter ? ( 19 | 20 | ) : ( 21 | } 23 | imageStyle={{ 24 | height: 210, 25 | marginBottom: 32, 26 | }} 27 | className={styles.default} 28 | description={'暂无配置信息,请从右侧面板开始编辑'} 29 | /> 30 | )} 31 |
32 | ); 33 | }); 34 | 35 | export default Starter; 36 | -------------------------------------------------------------------------------- /src/ProBuilder/components/Stage/index.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import { memo } from 'react'; 3 | import { Flexbox } from 'react-layout-kit'; 4 | 5 | import Canvas from '../Canvas'; 6 | import Code from '../Code'; 7 | import { Controller } from '../Controller'; 8 | import Empty from './Empty'; 9 | 10 | import { isDesignModeSelector, useStore } from '../../store'; 11 | import { useStyles } from './style'; 12 | 13 | export const Stage: FC<{ 14 | hideNavbar: boolean; 15 | onCopy?: (children: any) => void; 16 | }> = memo(({ hideNavbar, onCopy }) => { 17 | const isStarter = useStore((x) => 18 | x.componentAsset.componentStore((s) => x.componentAsset.isStarterMode(s)), 19 | ); 20 | const isDesignMode = useStore(isDesignModeSelector); 21 | 22 | const { styles } = useStyles(); 23 | 24 | return ( 25 |
31 | 32 | {isStarter ? : } 33 | 34 | {isDesignMode ? null : } 35 | 36 |
37 | ); 38 | }); 39 | 40 | export default Stage; 41 | -------------------------------------------------------------------------------- /src/ProBuilder/components/Stage/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from '../../../theme'; 2 | 3 | export const useStyles = createStyles(({ token, css, cx, prefixCls }) => { 4 | const prefix = `${prefixCls}-${token.editorPrefix}-pro-builder-stage`; 5 | 6 | return { 7 | container: cx( 8 | `${prefix}-container`, 9 | css` 10 | overflow: auto; 11 | `, 12 | ), 13 | }; 14 | }); 15 | -------------------------------------------------------------------------------- /src/ProBuilder/container/Provider.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, ReactNode } from 'react'; 2 | import { DevtoolsOptions } from 'zustand/middleware'; 3 | 4 | import { createStore, Provider, useStoreApi } from '../store'; 5 | 6 | export const ProBuilderProvider: FC<{ 7 | children: ReactNode; 8 | devtoolOptions?: boolean | DevtoolsOptions; 9 | }> = ({ children, devtoolOptions }) => { 10 | let isWrapped = true; 11 | 12 | const Content = <>{children}; 13 | 14 | try { 15 | useStoreApi(); 16 | } catch (e) { 17 | isWrapped = false; 18 | } 19 | /* istanbul ignore if */ 20 | if (isWrapped) { 21 | return Content; 22 | } 23 | 24 | return createStore(devtoolOptions)}>{Content}; 25 | }; 26 | 27 | export default ProBuilderProvider; 28 | -------------------------------------------------------------------------------- /src/ProBuilder/container/index.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigProvider } from '@ant-design/pro-editor'; 2 | import { App } from 'antd'; 3 | import type { FC } from 'react'; 4 | import { memo } from 'react'; 5 | import { HotkeysProvider } from 'react-hotkeys-hook'; 6 | import { DevtoolsOptions } from 'zustand/middleware'; 7 | import type { ProBuilderAppProps } from './App'; 8 | import Content from './App'; 9 | import Provider from './Provider'; 10 | import type { StoreUpdaterProps } from './StoreUpdater'; 11 | import StoreUpdater from './StoreUpdater'; 12 | import { useStyle } from './style'; 13 | 14 | export type ProBuilderProps = ProBuilderAppProps & 15 | StoreUpdaterProps & { 16 | __EDITOR_STORE_DEVTOOLS__?: boolean | DevtoolsOptions; 17 | }; 18 | 19 | export const ProBuilder: FC = memo((props) => { 20 | const { style, __EDITOR_STORE_DEVTOOLS__, editorRef, ...res } = props; 21 | const { styles } = useStyle(); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | }); 36 | 37 | export { ProBuilderProvider } from './Provider'; 38 | export type { StoreUpdaterProps }; 39 | -------------------------------------------------------------------------------- /src/ProBuilder/container/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from '../../theme'; 2 | 3 | export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { 4 | const prefix = `${prefixCls}-${token.editorPrefix}-pro-builder`; 5 | return { 6 | app: css` 7 | height: 100%; 8 | `, 9 | main: cx( 10 | `${prefix}-main`, 11 | css` 12 | display: flex; 13 | flex-wrap: nowrap; 14 | height: 100%; 15 | position: relative; 16 | background-color: ${token.colorBgLayout}; 17 | `, 18 | ), 19 | left: cx( 20 | `${prefix}-left`, 21 | css` 22 | flex-grow: 1; 23 | `, 24 | ), 25 | }; 26 | }); 27 | -------------------------------------------------------------------------------- /src/ProBuilder/hooks/__snapshots__/useProBuilder.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`useProBuilder > 正确获取 presenceEditor 1`] = ` 4 | { 5 | "panelExpand": true, 6 | "panelPosition": { 7 | "x": 0, 8 | "y": 0, 9 | }, 10 | "panelSize": { 11 | "width": 340, 12 | }, 13 | "viewport": { 14 | "x": 0, 15 | "y": 0, 16 | "zoom": 1, 17 | }, 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /src/ProBuilder/hooks/useAssetAwareness.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useStore } from '../store'; 3 | 4 | export const useAssetAwareness = () => { 5 | const presenceAsset = useStore((s) => s.assetAwareness) as T; 6 | const internalUpdatePresenceAsset = useStore((s) => s.internalUpdateAssetAwareness); 7 | 8 | return useMemo(() => [presenceAsset, internalUpdatePresenceAsset] as const, [presenceAsset]); 9 | }; 10 | -------------------------------------------------------------------------------- /src/ProBuilder/hooks/useCanvasInteraction.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useStore } from '../store'; 3 | 4 | export const useCanvasInteraction = () => { 5 | const interaction = useStore((s) => s.interaction); 6 | const internalUpdateCanvasInteract = useStore((s) => s.internalUpdateCanvasInteract); 7 | 8 | return useMemo(() => [interaction, internalUpdateCanvasInteract] as const, [interaction]); 9 | }; 10 | -------------------------------------------------------------------------------- /src/ProBuilder/hooks/useEditorAwareness.ts: -------------------------------------------------------------------------------- 1 | import { useDebounceFn } from 'ahooks'; 2 | import { useMemo } from 'react'; 3 | 4 | import { PartialDeep } from 'type-fest'; 5 | import { AwarenessEditor, useStore } from '../store'; 6 | 7 | export const useUpdateEditorAwareness = (): { 8 | updateEditorAwareness: (awareness: PartialDeep) => void; 9 | } => { 10 | const updateEditorAwareness = useStore((s) => s.internalUpdateEditorAwareness); 11 | const { run } = useDebounceFn(updateEditorAwareness, { wait: 100 }); 12 | 13 | return useMemo( 14 | () => ({ 15 | updateEditorAwareness: run, 16 | }), 17 | [], 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/ProBuilder/hooks/useHotkeyManager.ts: -------------------------------------------------------------------------------- 1 | import { useHotkeys } from 'react-hotkeys-hook'; 2 | import { shallow } from 'zustand/shallow'; 3 | 4 | import { useStore } from '../store'; 5 | 6 | export const useHotkeyManager = () => { 7 | const [undo, redo] = useStore((s) => [s.undo, s.redo], shallow); 8 | 9 | useHotkeys('meta+z', (e) => { 10 | e.preventDefault(); 11 | undo(); 12 | }); 13 | 14 | useHotkeys('meta+shift+z', (e) => { 15 | e.preventDefault(); 16 | 17 | redo(); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/ProBuilder/index.ts: -------------------------------------------------------------------------------- 1 | export { ProBuilder, ProBuilderProvider } from './container'; 2 | export type { ProBuilderProps, StoreUpdaterProps } from './container'; 3 | 4 | // hooks 5 | export * from './hooks/useAssetAwareness'; 6 | export * from './hooks/useCanvasInteraction'; 7 | export * from './hooks/useEditorAwareness'; 8 | export * from './hooks/useProBuilder'; 9 | 10 | export { 11 | EditorMode, 12 | storeSelectors as proBuilderSelectors, 13 | useStore as useProBuilderStore, 14 | } from './store'; 15 | export type { AwarenessEditor, InternalProBuilderStore as ProBuilderStore } from './store'; 16 | -------------------------------------------------------------------------------- /src/ProBuilder/store/__snapshots__/createStore.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`proBuilderStore > 方法 > 修改配置 > 受控模式,有 componentAssets > 内部 config 更新时,内部 props 更新,同时外部值可接受到 props、config 和 isEmpty 值变化 1`] = `{}`; 4 | 5 | exports[`proBuilderStore > 方法 > 修改配置 > 受控模式,有 componentAssets > 内部 config 更新时,内部 props 更新,同时外部值可接受到 props、config 和 isEmpty 值变化 2`] = `undefined`; 6 | 7 | exports[`proBuilderStore > 方法 > 修改配置 > 受控模式,有 componentAssets > 外部设置 config 时,内部 props 会跟随 config 更新,不触发 onChange 1`] = `{}`; 8 | -------------------------------------------------------------------------------- /src/ProBuilder/store/index.ts: -------------------------------------------------------------------------------- 1 | import { StoreApi } from 'zustand'; 2 | import { createContext } from 'zustand-utils'; 3 | import { InternalProBuilderStore, createStore } from './createStore'; 4 | 5 | const { Provider, useStore, useStoreApi } = createContext>(); 6 | 7 | // ======== 导出 ======== // 8 | 9 | export { Provider, createStore, useStore, useStoreApi }; 10 | 11 | export type { InternalProBuilderStore, ProBuilderState } from './createStore'; 12 | 13 | export * from './selectors'; 14 | export type { AwarenessEditor } from './slices/awareness'; 15 | export { EditorMode, TabKey } from './slices/general'; 16 | -------------------------------------------------------------------------------- /src/ProBuilder/store/selectors.test.ts: -------------------------------------------------------------------------------- 1 | import type { InternalProBuilderStore } from './createStore'; 2 | import { isDesignModeSelector } from './selectors'; 3 | 4 | test('isDesignModeSelector', () => { 5 | const design = isDesignModeSelector({ mode: 'design' } as InternalProBuilderStore); 6 | expect(design).toBeTruthy(); 7 | 8 | const dev = isDesignModeSelector({ mode: 'develop' } as InternalProBuilderStore); 9 | expect(dev).toBeFalsy(); 10 | }); 11 | -------------------------------------------------------------------------------- /src/ProBuilder/store/selectors.ts: -------------------------------------------------------------------------------- 1 | import type { InternalProBuilderStore } from './createStore'; 2 | 3 | export const isDesignModeSelector = (s: InternalProBuilderStore) => s.mode === 'design'; 4 | 5 | export const storeSelectors = { 6 | isDesignMode: isDesignModeSelector, 7 | }; 8 | -------------------------------------------------------------------------------- /src/ProBuilder/tests/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | 3 | import { ProBuilder } from '@ant-design/pro-editor'; 4 | 5 | describe('ProBuilder', () => { 6 | it('没有传入 ComponentAsset 时,渲染为空状态', () => { 7 | const { container } = render(); 8 | expect(container).toMatchSnapshot(); 9 | }); 10 | it('传入 ComponentAsset 时,符合预期进行渲染', () => { 11 | expect(1).toEqual(1); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/ProEditor/ProEditorProvider/index.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, ReactNode } from 'react'; 2 | import { DevtoolsOptions } from 'zustand/middleware'; 3 | 4 | import { StoreApi } from 'zustand/esm'; 5 | import { UseBoundStore } from 'zustand/react'; 6 | import { createStore, Provider, useStoreApi } from '../store'; 7 | import StoreUpdater from './StoreUpdater'; 8 | 9 | export interface ProEditorProviderProps { 10 | children: ReactNode; 11 | devtoolOptions?: boolean | DevtoolsOptions; 12 | store?: UseBoundStore>[]; 13 | } 14 | 15 | export const ProEditorProvider: FC = ({ 16 | children, 17 | devtoolOptions, 18 | store, 19 | }) => { 20 | let isWrapped = true; 21 | 22 | const Content = ( 23 | <> 24 | {children} 25 | {store?.map((item, index) => ( 26 | 27 | ))} 28 | 29 | ); 30 | 31 | try { 32 | useStoreApi(); 33 | } catch (e) { 34 | isWrapped = false; 35 | } 36 | /* istanbul ignore if */ 37 | if (isWrapped) { 38 | return Content; 39 | } 40 | 41 | return createStore(devtoolOptions)}>{Content}; 42 | }; 43 | -------------------------------------------------------------------------------- /src/ProEditor/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProEditorProvider'; 2 | export * from './hooks/useProEditor'; 3 | export * from './middleware'; 4 | -------------------------------------------------------------------------------- /src/ProEditor/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pro-editor'; 2 | -------------------------------------------------------------------------------- /src/ProEditor/middleware/types/utils.ts: -------------------------------------------------------------------------------- 1 | export type Cast = T extends U ? T : U; 2 | 3 | export type TakeTwo = T extends { length: 0 } 4 | ? [undefined, undefined] 5 | : T extends { length: 1 } 6 | ? [...a0: Cast, a1: undefined] 7 | : T extends { length: 0 | 1 } 8 | ? [...a0: Cast, a1: undefined] 9 | : T extends { length: 2 } 10 | ? T 11 | : T extends { length: 1 | 2 } 12 | ? T 13 | : T extends { length: 0 | 1 | 2 } 14 | ? T 15 | : T extends [infer A0, infer A1, ...unknown[]] 16 | ? [A0, A1] 17 | : T extends [infer A0, (infer A1)?, ...unknown[]] 18 | ? [A0, A1?] 19 | : T extends [(infer A0)?, (infer A1)?, ...unknown[]] 20 | ? [A0?, A1?] 21 | : never; 22 | 23 | // 为 mutator 注入 'pro-editor' 类型,以支持第三个配置参数 24 | 25 | export type Write = Omit & U; 26 | -------------------------------------------------------------------------------- /src/ProEditor/store/__snapshots__/createStore.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`proEditorStore > 方法 > 修改配置 > 受控模式,有 componentAssets > 内部 config 更新时,内部 props 更新,同时外部值可接受到 props、config 值变化 1`] = `{}`; 4 | 5 | exports[`proEditorStore > 方法 > 修改配置 > 受控模式,有 componentAssets > 内部 config 更新时,内部 props 更新,同时外部值可接受到 props、config 值变化 2`] = `undefined`; 6 | 7 | exports[`proEditorStore > 方法 > 修改配置 > 受控模式,有 componentAssets > 外部设置 config 时,内部 props 会跟随 config 更新,不触发 onChange 1`] = `{}`; 8 | -------------------------------------------------------------------------------- /src/ProEditor/store/createStore.ts: -------------------------------------------------------------------------------- 1 | import isEqual from 'fast-deep-equal'; 2 | import type { StateCreator } from 'zustand'; 3 | import { optionalDevtools } from 'zustand-utils'; 4 | import { DevtoolsOptions } from 'zustand/middleware'; 5 | import { createWithEqualityFn } from 'zustand/traditional'; 6 | 7 | import { ConfigPublicState, ConfigSlice, configSlice } from './slices/config'; 8 | import { GeneralSlice, generalSlice } from './slices/general'; 9 | 10 | /** 11 | * ProEditorState 接口描述编辑器状态 12 | * @template Config - 编辑器配置属性类型 13 | */ 14 | export type ProEditorState = ConfigPublicState; 15 | 16 | export type InternalProEditorStore = ProEditorState & ConfigSlice & GeneralSlice; 17 | 18 | const vanillaStore: StateCreator = ( 19 | ...params 20 | ) => ({ 21 | ...generalSlice(...params), 22 | ...configSlice(...params), 23 | }); 24 | 25 | export const createStore = (options: boolean | DevtoolsOptions = false) => { 26 | const devtools = optionalDevtools(options !== false); 27 | 28 | const devtoolOptions = 29 | options === false ? undefined : options === true ? { name: 'ProEditorStore' } : options; 30 | 31 | return createWithEqualityFn()( 32 | devtools(vanillaStore, devtoolOptions), 33 | isEqual, 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/ProEditor/store/index.ts: -------------------------------------------------------------------------------- 1 | import { StoreApi } from 'zustand'; 2 | import { createContext } from 'zustand-utils'; 3 | import { InternalProEditorStore, createStore } from './createStore'; 4 | 5 | const { Provider, useStore, useStoreApi } = createContext>(); 6 | 7 | // ======== 导出 ======== // 8 | 9 | export { Provider, createStore, useStore, useStoreApi }; 10 | 11 | export type { InternalProEditorStore, ProEditorState } from './createStore'; 12 | -------------------------------------------------------------------------------- /src/Snippet/demos/index.tsx: -------------------------------------------------------------------------------- 1 | import { Snippet } from '@ant-design/pro-editor'; 2 | 3 | export default () => { 4 | return pnpm install @ant-design/pro-chat; 5 | }; 6 | -------------------------------------------------------------------------------- /src/Snippet/demos/spotlight.tsx: -------------------------------------------------------------------------------- 1 | import { Snippet } from '@ant-design/pro-editor'; 2 | 3 | export default () => { 4 | return ( 5 | 6 | pnpm install @ant-design/pro-chat 7 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/Snippet/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: Components 3 | group: Basic 4 | title: Snippet 5 | --- 6 | 7 | # Snippet Code Snippet 8 | 9 | The Snippet component is used to display code snippets with syntax highlighting. Customization can be done with symbols and syntax highlighting languages before the content. The component also supports copying through the included CopyButton by default. 10 | 11 | ## Code Demo 12 | 13 | 14 | 15 | 16 | 17 | ## APIs 18 | 19 | | Property | Description | Type | Default | 20 | | :-------- | :-------------------------------------------- | :----------------- | :------ | 21 | | children | Content displayed in the component | string | - | 22 | | copyable | Whether the content can be copied | boolean | true | 23 | | language | Language of the component content | string | 'tsx' | 24 | | spotlight | Whether to add a spotlight background effect | boolean | false | 25 | | symbol | Symbol displayed before the component content | string | - | 26 | | type | Rendering type of the component | 'ghost' \| 'block' | 'ghost' | 27 | -------------------------------------------------------------------------------- /src/Snippet/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 组件 3 | group: 基础组件 4 | title: Snippet 代码片段 5 | --- 6 | 7 | # Snippet 代码片段 8 | 9 | Snippet 组件用于显示带有语法突出显示的代码片段。可以在内容之前使用符号和语法突出显示的语言进行自定义。该组件还可以通过默认包含的 CopyButton 进行复制 10 | 11 | ## 代码演示 12 | 13 | 14 | 15 | 16 | 17 | ## APIs 18 | 19 | | 参数 | 说明 | 类型 | 默认值 | 20 | | :-------- | :--------------------- | :----------------- | :------ | 21 | | children | 组件内显示的内容 | string | - | 22 | | copyable | 内容是否可以复制 | boolean | true | 23 | | language | 组件内容的语言 | string | 'tsx' | 24 | | spotlight | 是否添加聚光灯背景效果 | boolean | false | 25 | | symbol | 组件内容前显示的符号 | string | - | 26 | | type | 组件的渲染类型 | 'ghost' \| 'block' | 'ghost' | 27 | -------------------------------------------------------------------------------- /src/SortableList/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { act, fireEvent, render } from '@testing-library/react'; 2 | import { Button } from 'antd'; 3 | import { useState } from 'react'; 4 | import { SortableList } from '../index'; 5 | 6 | const Demo = () => { 7 | const [list, setList] = useState(['hello', 'world']); 8 | 9 | return ( 10 | <> 11 | { 14 | console.log('change value', value); 15 | setList(value); 16 | }} 17 | SHOW_STORE_IN_DEVTOOLS 18 | /> 19 | 28 | 29 | ); 30 | }; 31 | 32 | describe('SortableList', () => { 33 | it('toggleSetData', () => { 34 | const { getByText } = render(); 35 | const setDataButton = getByText('Set Data'); 36 | act(() => { 37 | fireEvent.click(setDataButton); 38 | }); 39 | expect(getByText('foo')).toBeInTheDocument(); 40 | expect(getByText('bar')).toBeInTheDocument(); 41 | expect(getByText('yes')).toBeInTheDocument(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/SortableList/components/List/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { forwardRef } from 'react'; 3 | import { useStyle } from './style'; 4 | 5 | export interface Props { 6 | children: React.ReactNode; 7 | columns?: number; 8 | style?: React.CSSProperties; 9 | className?: string; 10 | horizontal?: boolean; 11 | } 12 | 13 | const List = forwardRef( 14 | ({ children, columns = 1, horizontal, style, className }: Props, ref) => { 15 | const { styles } = useStyle({ horizontal }); 16 | 17 | const listClassName = classNames(styles.container, className); 18 | 19 | return ( 20 |
    30 | {children} 31 |
32 | ); 33 | }, 34 | ); 35 | 36 | export default List; 37 | -------------------------------------------------------------------------------- /src/SortableList/components/List/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from '../../../theme'; 2 | 3 | export const useStyle = createStyles(({ css, token, cx, prefixCls }, { horizontal }) => { 4 | const prefix = `${prefixCls}-${token.editorPrefix}-sortable-list`; 5 | 6 | return { 7 | container: cx( 8 | `${prefix}-container`, 9 | css({ 10 | listStyle: 'none', 11 | display: 'grid', 12 | gridAutoRows: 'max-content', 13 | gridGap: '2px', 14 | gridTemplateColumns: 'repeat(var(--columns, 1), 1fr)', 15 | width: '100%', 16 | margin: '0', 17 | padding: '0', 18 | borderRadius: '4px', 19 | transition: 'background-color 350ms ease', 20 | gridAutoFlow: horizontal ? 'column' : undefined, 21 | }), 22 | ), 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /src/SortableList/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Item } from './Item'; 2 | export { default as List } from './List'; 3 | export { default as SortableItem } from './SortableItem'; 4 | -------------------------------------------------------------------------------- /src/SortableList/container/Provider.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | import { Provider, createStore, useStoreApi } from '../store'; 3 | 4 | export const SortableListProvider: FC<{ 5 | children: ReactNode; 6 | showDevtools?: boolean; 7 | }> = ({ children, showDevtools }) => { 8 | let isWrapped = true; 9 | const Content = <>{children}; 10 | try { 11 | useStoreApi(); 12 | } catch (e) { 13 | isWrapped = false; 14 | } 15 | /* istanbul ignore if */ 16 | if (isWrapped) { 17 | return Content; 18 | } 19 | return createStore(showDevtools)}>{Content}; 20 | }; 21 | -------------------------------------------------------------------------------- /src/SortableList/container/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC, forwardRef, memo } from 'react'; 2 | import { ConfigProvider } from '../../ConfigProvider'; 3 | import type { StoreUpdaterProps } from '../type'; 4 | import type { AppProps } from './App'; 5 | import App from './App'; 6 | import { SortableListProvider } from './Provider'; 7 | import StoreUpdater from './StoreUpdater'; 8 | 9 | export { SortableListProvider } from './Provider'; 10 | 11 | export interface SortableListProps extends StoreUpdaterProps, AppProps {} 12 | 13 | export const SortableList: (props: SortableListProps) => ReturnType = memo( 14 | forwardRef((props, ref) => { 15 | const { SHOW_STORE_IN_DEVTOOLS, className, style, ...res } = props; 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | }), 25 | ); 26 | -------------------------------------------------------------------------------- /src/SortableList/demos/Basic.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { SortableList } from '@ant-design/pro-editor'; 5 | import { useTheme } from 'antd-style'; 6 | import { Flexbox } from 'react-layout-kit'; 7 | 8 | const list = ['hello', 'world']; 9 | 10 | const Demo = () => { 11 | const token = useTheme(); 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default Demo; 20 | -------------------------------------------------------------------------------- /src/SortableList/demos/controlled.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { SortableList } from '@ant-design/pro-editor'; 5 | import { Button } from 'antd'; 6 | import { useTheme } from 'antd-style'; 7 | import { useState } from 'react'; 8 | import { Flexbox } from 'react-layout-kit'; 9 | 10 | const Demo = () => { 11 | const [list, setList] = useState(['hello', 'world']); 12 | 13 | const token = useTheme(); 14 | return ( 15 | 16 | { 19 | console.log('change value', value); 20 | setList(value); 21 | }} 22 | SHOW_STORE_IN_DEVTOOLS 23 | /> 24 | 33 | 34 | ); 35 | }; 36 | 37 | export default Demo; 38 | -------------------------------------------------------------------------------- /src/SortableList/demos/creatorButtonProps.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { SortableList } from '@ant-design/pro-editor'; 5 | import { useTheme } from 'antd-style'; 6 | import { Flexbox } from 'react-layout-kit'; 7 | 8 | const list = [{ text: 'hello' }, { text: 'world' }]; 9 | 10 | const Demo = () => { 11 | const token = useTheme(); 12 | return ( 13 | 14 | item.text} 17 | creatorButtonProps={{ 18 | creatorButtonText: 'Custom Create', 19 | record: () => ({ 20 | text: Math.ceil(Math.random() * 100000).toString(16), 21 | }), 22 | }} 23 | /> 24 | 25 | ); 26 | }; 27 | 28 | export default Demo; 29 | -------------------------------------------------------------------------------- /src/SortableList/demos/empty.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { SortableList } from '@ant-design/pro-editor'; 5 | import { useTheme } from 'antd-style'; 6 | import { Flexbox } from 'react-layout-kit'; 7 | 8 | const list = []; 9 | 10 | const Demo = () => { 11 | const token = useTheme(); 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default Demo; 20 | -------------------------------------------------------------------------------- /src/SortableList/demos/getItemStyles.tsx: -------------------------------------------------------------------------------- 1 | import { SortableList } from '@ant-design/pro-editor'; 2 | import { useState } from 'react'; 3 | 4 | const Demo = () => { 5 | const [list, setList] = useState(['关关雎鸠', '在河之洲', '窈窕淑女', '君子好逑']); 6 | 7 | return ( 8 | { 14 | return { 15 | padding: 24, 16 | // 拖拽项修改背景色 17 | background: isDragging ? 'rgb(74,135,82)' : 'pink', 18 | color: isDragging ? 'rgb(139,212,148)' : 'rgb(135,74,74)', 19 | // 在 拖拽过程中放大所有item的圆角 20 | borderRadius: isSorting ? 100 : 16, 21 | boxShadow: 'none', 22 | }; 23 | }} 24 | /> 25 | ); 26 | }; 27 | 28 | export default Demo; 29 | -------------------------------------------------------------------------------- /src/SortableList/demos/handle.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { SortableList } from '@ant-design/pro-editor'; 5 | import { useTheme } from 'antd-style'; 6 | import { Flexbox } from 'react-layout-kit'; 7 | 8 | const list = ['hello', 'world']; 9 | 10 | const Demo = () => { 11 | const token = useTheme(); 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default Demo; 20 | -------------------------------------------------------------------------------- /src/SortableList/demos/hideRemove.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { SortableList } from '@ant-design/pro-editor'; 5 | import { useTheme } from 'antd-style'; 6 | import { Flexbox } from 'react-layout-kit'; 7 | 8 | const list = ['hello', 'world']; 9 | 10 | const Demo = () => { 11 | const token = useTheme(); 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default Demo; 20 | -------------------------------------------------------------------------------- /src/SortableList/demos/renderContent.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { SortableList } from '@ant-design/pro-editor'; 5 | import { useTheme } from 'antd-style'; 6 | import { Flexbox } from 'react-layout-kit'; 7 | import ItemRender from './_ItemRender'; 8 | import { INIT_VALUES, SchemaItem } from './data'; 9 | 10 | export default () => { 11 | const token = useTheme(); 12 | return ( 13 | 14 | 15 | initialValues={INIT_VALUES} 16 | onChange={(data, event) => { 17 | console.log('data', data, event); 18 | }} 19 | renderContent={(item, index) => } 20 | /> 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/SortableList/demos/useSortableList.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * compact: true 3 | */ 4 | import { SortableList } from '@ant-design/pro-editor'; 5 | import { useTheme } from 'antd-style'; 6 | import { useState } from 'react'; 7 | import { Flexbox } from 'react-layout-kit'; 8 | import ItemRender from './_ItemRender'; 9 | import { INIT_VALUES, SchemaItem } from './data'; 10 | 11 | export default () => { 12 | const [listData, setListData] = useState(INIT_VALUES); 13 | const token = useTheme(); 14 | 15 | return ( 16 | 17 | 18 | value={listData} 19 | onChange={(data) => { 20 | console.log('data', data); 21 | setListData(data); 22 | }} 23 | renderContent={(item, index) => } 24 | /> 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/SortableList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './container'; 2 | 3 | // hooks 和相关类型定义 4 | export { useSortableList } from './hooks/useSortableList'; 5 | export type { SortableListInstance } from './hooks/useSortableList'; 6 | export type { CreatorButtonProps, SortableListDispatchPayload, SortableListRef } from './type'; 7 | -------------------------------------------------------------------------------- /src/SortableList/store/index.ts: -------------------------------------------------------------------------------- 1 | import isEqual from 'fast-deep-equal'; 2 | import type { StoreApi } from 'zustand'; 3 | import { createContext, optionalDevtools } from 'zustand-utils'; 4 | import { createWithEqualityFn } from 'zustand/traditional'; 5 | 6 | import type { Store } from './store'; 7 | import vanillaStore from './store'; 8 | 9 | const createStore = (showDevTools: boolean) => 10 | createWithEqualityFn( 11 | optionalDevtools(showDevTools)(vanillaStore, { name: 'SortableList' }), 12 | isEqual, 13 | ); 14 | 15 | const { useStore, useStoreApi, Provider } = createContext>(); 16 | 17 | // ========= 导出 ========= // 18 | export type { Store } from './store'; 19 | export { Provider, createStore, useStore, useStoreApi }; 20 | -------------------------------------------------------------------------------- /src/SortableList/store/initialState.ts: -------------------------------------------------------------------------------- 1 | import { SortableListState } from '../type'; 2 | 3 | export const initialState: SortableListState = { 4 | activeId: null, 5 | value: [], 6 | keyManager: [], 7 | hideRemove: false, 8 | handle: true, 9 | onChange: undefined, 10 | renderItem: undefined, 11 | actions: [], 12 | getItemStyles: () => ({}), 13 | }; 14 | -------------------------------------------------------------------------------- /src/SortableList/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles, getStudioStylish } from '../theme'; 2 | 3 | export const useStyle = createStyles((props) => { 4 | const { token, css, cx, prefixCls } = props; 5 | const common = getStudioStylish(props); 6 | 7 | const antCls = prefixCls; 8 | 9 | return { 10 | btnAdd: cx( 11 | `${antCls}-btn-add`, 12 | css` 13 | height: 24px; 14 | padding-block: 2px; 15 | margin-top: ${token.marginXXS}px; 16 | margin-bottom: ${token.marginXXS}px; 17 | `, 18 | common.defaultButton, 19 | ), 20 | }; 21 | }); 22 | -------------------------------------------------------------------------------- /src/SortableList/type/action.ts: -------------------------------------------------------------------------------- 1 | // 新增节点 2 | export interface AddItemAction { 3 | /** 4 | * 动作类型 5 | */ 6 | type: 'addItem'; 7 | /** 8 | * 新增的节点 9 | */ 10 | item: any; 11 | /** 12 | * 新增节点的位置 13 | * @default undefined 14 | */ 15 | index?: number; 16 | } 17 | 18 | // 移动节点 19 | export interface MoveItemAction { 20 | /** 21 | * 动作类型 22 | */ 23 | type: 'moveItem'; 24 | /** 25 | * 当前节点索引 26 | */ 27 | activeIndex: number; 28 | /** 29 | * 目标节点索引 30 | */ 31 | overIndex: number; 32 | } 33 | 34 | // 移除节点 35 | export interface RemoveItemAction { 36 | /** 37 | * 动作类型 38 | */ 39 | type: 'removeItem'; 40 | /** 41 | * 要移除的节点的位置 42 | */ 43 | index: number; 44 | } 45 | 46 | // 修改节点 47 | export interface UpdateItemAction { 48 | /** 49 | * 动作类型 50 | */ 51 | type: 'updateItem'; 52 | /** 53 | * 要修改的节点的位置 54 | */ 55 | index: number; 56 | /** 57 | * 修改后的节点内容 58 | */ 59 | item: any; 60 | } 61 | 62 | /** 63 | * 内部 Item 数据更新方法 64 | */ 65 | export type SortableListDispatchPayload = 66 | | MoveItemAction 67 | | AddItemAction 68 | | RemoveItemAction 69 | | UpdateItemAction; 70 | -------------------------------------------------------------------------------- /src/SortableList/type/index.ts: -------------------------------------------------------------------------------- 1 | export * from './action'; 2 | export * from './component'; 3 | export * from './store'; 4 | -------------------------------------------------------------------------------- /src/SortableList/utils/useStoreUpdater.ts: -------------------------------------------------------------------------------- 1 | import { devtools } from 'zustand/middleware'; 2 | import type { StateCreator } from 'zustand/vanilla'; 3 | 4 | export type ZustandStoreWithDevTools = StateCreator; 5 | 6 | /** 7 | * 将是否开启 devtools 变成可选方案 8 | * Refs: https://github.com/pmndrs/zustand/discussions/1266 9 | */ 10 | 11 | export const optionalDevtools = (showDevTools: boolean) => 12 | (showDevTools ? devtools : (f) => f) as typeof devtools; 13 | -------------------------------------------------------------------------------- /src/SortableTree/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SortableTreeItem } from './TreeItem'; 2 | -------------------------------------------------------------------------------- /src/SortableTree/container/Provider.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react'; 2 | 3 | import { createStore, Provider, useStoreApi } from '../store'; 4 | 5 | export const SortableTreeProvider: FC<{ 6 | children: ReactNode; 7 | showDevtools?: boolean; 8 | }> = ({ children, showDevtools }) => { 9 | let isWrapped = true; 10 | 11 | const Content = <>{children}; 12 | 13 | try { 14 | useStoreApi(); 15 | } catch (e) { 16 | isWrapped = false; 17 | } 18 | /* istanbul ignore if */ 19 | if (isWrapped) { 20 | return Content; 21 | } 22 | 23 | return ( 24 | createStore(showDevtools)}>{Content} 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/SortableTree/container/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC, memo } from 'react'; 2 | 3 | import { ConfigProvider } from '../../ConfigProvider'; 4 | import type { ControlledState } from '../store'; 5 | import type { AppProps } from './App'; 6 | import App from './App'; 7 | import { SortableTreeProvider } from './Provider'; 8 | import StoreUpdater from './StoreUpdater'; 9 | 10 | import type { StoreUpdaterProps } from './StoreUpdater'; 11 | 12 | export interface SortableTreeProps extends StoreUpdaterProps, ControlledState, AppProps {} 13 | 14 | export { SortableTreeProvider } from './Provider'; 15 | 16 | export const SortableTree = memo((props) => { 17 | const { SHOW_STORE_IN_DEVTOOLS, className, style, ...res } = props; 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }) as (props: SortableTreeProps) => ReturnType; 27 | -------------------------------------------------------------------------------- /src/SortableTree/demos/_multiSelect.tsx: -------------------------------------------------------------------------------- 1 | import { List } from 'antd'; 2 | 3 | import { useMultiSelect } from './multiSelector'; 4 | 5 | const data = [ 6 | 'Racing car sprays burning fuel into crowd.', 7 | 'Japanese princess to wed commoner.', 8 | 'Australian walks 100km after outback crash.', 9 | 'Man charged over missing wedding girl.', 10 | 'Los Angeles battles huge wildfires.', 11 | 'Japanese princess to wed commoner.', 12 | 'Australian walks 100km after outback crash.', 13 | 'Man charged over missing wedding girl.', 14 | 'Los Angeles battles huge wildfires.', 15 | ]; 16 | 17 | function App() { 18 | const { selectedIds } = useMultiSelect(); 19 | console.log('id', selectedIds); 20 | return ( 21 |
22 | { 27 | const selected = selectedIds.includes(index.toString()); 28 | return ( 29 | 33 | {item} 34 | 35 | ); 36 | }} 37 | /> 38 |
39 | ); 40 | } 41 | 42 | export default App; 43 | -------------------------------------------------------------------------------- /src/SortableTree/demos/controlled.tsx: -------------------------------------------------------------------------------- 1 | import type { TreeData } from '@ant-design/pro-editor'; 2 | import { Input, SortableTree, useSortableTree } from '@ant-design/pro-editor'; 3 | import { useState } from 'react'; 4 | 5 | import { initialData } from './data'; 6 | 7 | interface DataContent { 8 | title: string; 9 | } 10 | 11 | const Content = ({ value, id }) => { 12 | const instance = useSortableTree(); 13 | 14 | return ( 15 | { 18 | instance.updateNodeContent(id, { title: value }); 19 | }} 20 | /> 21 | ); 22 | }; 23 | 24 | export default () => { 25 | const [treeData, setTreeData] = useState>(initialData); 26 | 27 | return ( 28 |
29 | 30 | treeData={treeData} 31 | renderContent={(item) => } 32 | onTreeDataChange={(data, event) => { 33 | console.log('数据:', data); 34 | console.log('事件:', event); 35 | setTreeData(data); 36 | }} 37 | /> 38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/SortableTree/demos/default.tsx: -------------------------------------------------------------------------------- 1 | import { SortableTree } from '@ant-design/pro-editor'; 2 | 3 | import { initialData } from './data'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /src/SortableTree/demos/disableDrag.tsx: -------------------------------------------------------------------------------- 1 | import { SortableTree } from '@ant-design/pro-editor'; 2 | 3 | import { initialData } from './data'; 4 | 5 | export default () => ( 6 |
7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /src/SortableTree/demos/renderContent.tsx: -------------------------------------------------------------------------------- 1 | import type { TreeData } from '@ant-design/pro-editor'; 2 | import { IconPicker, SortableTree } from '@ant-design/pro-editor'; 3 | import { Input } from 'antd'; 4 | import { useState } from 'react'; 5 | import { Flexbox } from 'react-layout-kit'; 6 | 7 | import type { MenuContent } from './data'; 8 | import { menuData } from './data'; 9 | 10 | const NodeRender = ({ node }) => { 11 | const [text, setText] = useState(node.content.name); 12 | 13 | return ( 14 | 15 |
16 | 17 |
18 | { 24 | setText(e.target.value); 25 | }} 26 | /> 27 |
28 | ); 29 | }; 30 | 31 | export default () => { 32 | const [treeData, setTreeData] = useState>(menuData); 33 | 34 | return ( 35 |
36 | 37 | treeData={treeData} 38 | onTreeDataChange={(data) => { 39 | console.log('变更:', data); 40 | setTreeData(data); 41 | }} 42 | renderContent={(node) => node.content && } 43 | /> 44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/SortableTree/demos/sortableRule.tsx: -------------------------------------------------------------------------------- 1 | import { SortableTree } from '@ant-design/pro-editor'; 2 | 3 | import { message } from 'antd'; 4 | import { initialData } from './data'; 5 | 6 | export default () => ( 7 |
8 | { 11 | // 只允许平级拖动 12 | const { activeNode, projected } = data; 13 | const sortable = activeNode?.depth === projected?.depth; 14 | 15 | if (!sortable) message.warning('只允许同级拖动排序'); 16 | 17 | return sortable; 18 | }} 19 | /> 20 |
21 | ); 22 | -------------------------------------------------------------------------------- /src/SortableTree/demos/virtual.tsx: -------------------------------------------------------------------------------- 1 | import { SortableTree } from '@ant-design/pro-editor'; 2 | import { treeData } from './data/virtual'; 3 | 4 | interface DataContent { 5 | name: string; 6 | visible: boolean; 7 | isLeaf: boolean; 8 | } 9 | 10 | const LayerManager = () => { 11 | return ( 12 |
13 | 14 | treeData={treeData as any} 15 | renderContent={(item) =>
{item.id}
} 16 | SHOW_STORE_IN_DEVTOOLS 17 | virtual={{ 18 | // 滚动容器高度,必填 19 | height: 480, 20 | // 指定列表项高度,默认为 36,可选 21 | // itemHeight: (index: number) => number 22 | }} 23 | onTreeDataChange={(data) => { 24 | console.log('变更:', data); 25 | }} 26 | /> 27 |
28 | ); 29 | }; 30 | export default LayerManager; 31 | -------------------------------------------------------------------------------- /src/SortableTree/index.ts: -------------------------------------------------------------------------------- 1 | export * from './container'; 2 | // hooks 和相关类型定义 3 | export { useSortableTree, type SortableTreeInstance } from './hooks/useSortableTree'; 4 | export type { TreeNodeDispatchPayload } from './store'; 5 | export type { FlattenNode, Projected, TreeData, TreeNode } from './types'; 6 | -------------------------------------------------------------------------------- /src/SortableTree/store/index.ts: -------------------------------------------------------------------------------- 1 | import isEqual from 'fast-deep-equal'; 2 | import type { StoreApi } from 'zustand'; 3 | import { createContext, optionalDevtools } from 'zustand-utils'; 4 | import { createWithEqualityFn } from 'zustand/traditional'; 5 | 6 | import type { InternalSortableTreeStore } from './store'; 7 | import vanillaStore from './store'; 8 | 9 | const createStore = (showDevTools: boolean) => 10 | createWithEqualityFn( 11 | optionalDevtools(showDevTools)(vanillaStore, { name: 'SortableTree' }), 12 | isEqual, 13 | ); 14 | 15 | const { useStore, useStoreApi, Provider } = createContext>(); 16 | 17 | // ========= 导出 ========= // 18 | 19 | export type { ControlledState, OnTreeDataChange, State } from './initialState'; 20 | export * from './selectors'; 21 | export type { InternalSortableTreeStore } from './store'; 22 | export type { TreeNodeDispatchPayload } from './treeDataReducer'; 23 | export { Provider, createStore, useStore, useStoreApi }; 24 | -------------------------------------------------------------------------------- /src/SortableTree/store/selectors.ts: -------------------------------------------------------------------------------- 1 | import type { FlattenNode } from '../types'; 2 | import { getFlattenedData, getProjection } from '../utils/utils'; 3 | 4 | import type { InternalSortableTreeStore } from './store'; 5 | 6 | export const dataFlattenSelector = (s: InternalSortableTreeStore): FlattenNode[] => 7 | getFlattenedData(s.treeData, s.activeId); 8 | 9 | export const sortedIdsSelector = (s: InternalSortableTreeStore) => { 10 | return dataFlattenSelector(s).map(({ id }) => id); 11 | }; 12 | 13 | export const projectedSelector = (s: InternalSortableTreeStore) => { 14 | const { activeId, overId, offsetLeft, indentationWidth } = s; 15 | 16 | return activeId && overId 17 | ? getProjection(dataFlattenSelector(s), activeId, overId, offsetLeft, indentationWidth) 18 | : null; 19 | }; 20 | -------------------------------------------------------------------------------- /src/SortableTree/store/store.ts: -------------------------------------------------------------------------------- 1 | import type { StateCreator } from 'zustand/vanilla'; 2 | 3 | import type { State } from './initialState'; 4 | import { initialState } from './initialState'; 5 | import { TreeDataPublicAction, TreeDataSliceAction, crudSlice } from './slices/crudSlice'; 6 | import { DndAction, dndSlice } from './slices/dndSlice'; 7 | import { 8 | SelectionPublicAction, 9 | SelectionSliceAction, 10 | selectionSlice, 11 | } from './slices/selectionSlice'; 12 | 13 | export type InternalSortableTreeStore = State & 14 | TreeDataSliceAction & 15 | DndAction & 16 | SelectionSliceAction; 17 | 18 | // 对外暴露的实例中的方法 19 | export type PublicSortableTreeStore = TreeDataPublicAction & SelectionPublicAction; 20 | 21 | const vanillaStore: StateCreator = ( 22 | ...params 23 | ) => ({ 24 | ...initialState, 25 | ...dndSlice(...params), 26 | ...selectionSlice(...params), 27 | ...crudSlice(...params), 28 | }); 29 | 30 | export default vanillaStore; 31 | -------------------------------------------------------------------------------- /src/SortableTree/types/custom.ts: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import type { FlattenNode } from './data'; 3 | 4 | export type RenderNodeProps = (node: FlattenNode) => ReturnType; 5 | 6 | export type VirtualConfig = 7 | | { 8 | /** 9 | * @title 虚拟滚动的高度 10 | */ 11 | height: number; 12 | /** 13 | * @title 虚拟滚动的宽度 14 | */ 15 | width?: number; 16 | /** 17 | * @title 虚拟滚动的行高 18 | */ 19 | itemHeight?: (index: number) => number; 20 | } 21 | | false; 22 | -------------------------------------------------------------------------------- /src/SortableTree/types/data.ts: -------------------------------------------------------------------------------- 1 | import type { UniqueIdentifier } from '@dnd-kit/core'; 2 | import type { MutableRefObject } from 'react'; 3 | 4 | export type { UniqueIdentifier }; 5 | 6 | /** 7 | * 树节点 8 | */ 9 | export interface TreeNode { 10 | id: UniqueIdentifier; 11 | children: TreeNode[]; 12 | /** 13 | * 组是否折叠 14 | */ 15 | collapsed?: boolean; 16 | /** 17 | * 是否显示额外区域 18 | */ 19 | showExtra?: boolean; 20 | content?: T; 21 | } 22 | 23 | export type TreeData = TreeNode[]; 24 | 25 | /** 26 | * 展平的节点 27 | */ 28 | export interface FlattenNode extends TreeNode { 29 | parentId: UniqueIdentifier | null; 30 | depth: number; 31 | index: number; 32 | } 33 | 34 | export type SensorContext = MutableRefObject<{ 35 | items: FlattenNode[]; 36 | offset: number; 37 | }>; 38 | 39 | export interface Projected { 40 | depth: number; 41 | maxDepth: number; 42 | minDepth: number; 43 | parentId: UniqueIdentifier; 44 | } 45 | -------------------------------------------------------------------------------- /src/SortableTree/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './custom'; 2 | export * from './data'; 3 | -------------------------------------------------------------------------------- /src/antd/Tree.tsx: -------------------------------------------------------------------------------- 1 | import type { TreeProps as Props } from 'antd'; 2 | import { Tree as _Tree } from 'antd'; 3 | import type { FC, ReactNode } from 'react'; 4 | 5 | import { ConfigProvider } from '../ConfigProvider'; 6 | 7 | export type TreeProps = Props & { children?: ReactNode }; 8 | 9 | export const Tree: FC = (props) => { 10 | return ( 11 | 12 | <_Tree {...props} /> 13 | 14 | ); 15 | }; 16 | 17 | // @ts-ignore 18 | Tree.TreeNode = _Tree.TreeNode; 19 | -------------------------------------------------------------------------------- /src/antd/TreeSelect.tsx: -------------------------------------------------------------------------------- 1 | import type { TreeSelectProps as Props } from 'antd'; 2 | import { TreeSelect as _TreeSelect } from 'antd'; 3 | import type { FC, ReactNode } from 'react'; 4 | 5 | import { ConfigProvider } from '../ConfigProvider'; 6 | 7 | export type TreeSelectProps = Props & { children?: ReactNode }; 8 | 9 | export const TreeSelect: FC = (props) => ( 10 | 11 | <_TreeSelect {...props} /> 12 | 13 | ); 14 | 15 | // @ts-ignore 16 | TreeSelect.TreeNode = _TreeSelect.TreeNode; 17 | -------------------------------------------------------------------------------- /src/antd/demos/basic.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from '@ant-design/pro-editor'; 2 | import { Space } from 'antd'; 3 | 4 | export default () => ( 5 | 6 | Input 7 | 8 | { 11 | console.log(value); 12 | }} 13 | /> 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /src/antd/demos/inputNumber.tsx: -------------------------------------------------------------------------------- 1 | import { InputNumber } from '@ant-design/pro-editor'; 2 | import { Space } from 'antd'; 3 | 4 | export default () => ( 5 | 6 | InputNumber 7 | 8 | 9 | { 12 | console.log(value); 13 | }} 14 | addonAfter={'列'} 15 | /> 16 | 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /src/antd/demos/segmented.tsx: -------------------------------------------------------------------------------- 1 | import { Segmented } from '@ant-design/pro-editor'; 2 | import { Divider } from 'antd'; 3 | import { useState } from 'react'; 4 | 5 | enum TabKey { 6 | canvas, 7 | code, 8 | } 9 | 10 | const tabsList = [ 11 | { label: '画布', value: TabKey.canvas }, 12 | { label: '代码', value: TabKey.code }, 13 | ]; 14 | 15 | export default () => { 16 | const [tabKey, setTabKey] = useState(TabKey.canvas); 17 | 18 | return ( 19 |
20 | 21 | value={tabKey} 22 | options={tabsList} 23 | onChange={setTabKey} 24 | /> 25 | {tabKey === TabKey.canvas &&
canvas
} 26 | {tabKey === TabKey.code &&
code
} 27 | 28 | 29 | size={'small'} 30 | value={tabKey} 31 | options={tabsList} 32 | onChange={setTabKey} 33 | /> 34 | {tabKey === TabKey.canvas &&
canvas
} 35 | {tabKey === TabKey.code &&
code
} 36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/antd/demos/select.tsx: -------------------------------------------------------------------------------- 1 | import { Select } from '@ant-design/pro-editor'; 2 | import { Card } from 'antd'; 3 | import { Flexbox } from 'react-layout-kit'; 4 | 5 | export default () => ( 6 | 7 | 8 | 9 | 默认 10 | 12 | 13 | 14 | 小尺寸 15 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /src/antd/demos/tabs.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs } from '@ant-design/pro-editor'; 2 | 3 | const onChange = (key: string) => { 4 | console.log(key); 5 | }; 6 | 7 | const App: React.FC = () => ( 8 | 29 | ); 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /src/antd/demos/treeselect.tsx: -------------------------------------------------------------------------------- 1 | import { TreeSelect } from '@ant-design/pro-editor'; 2 | import { useState } from 'react'; 3 | 4 | const treeData = [ 5 | { 6 | value: 'parent 1', 7 | title: 'parent 1', 8 | children: [ 9 | { 10 | value: 'parent 1-0', 11 | title: 'parent 1-0', 12 | children: [ 13 | { 14 | value: 'leaf1', 15 | title: 'leaf1', 16 | }, 17 | { 18 | value: 'leaf2', 19 | title: 'leaf2', 20 | }, 21 | ], 22 | }, 23 | { 24 | value: 'parent 1-1', 25 | title: 'parent 1-1', 26 | children: [ 27 | { 28 | value: 'leaf3', 29 | title: leaf3, 30 | }, 31 | ], 32 | }, 33 | ], 34 | }, 35 | ]; 36 | const App: React.FC = () => { 37 | const [value, setValue] = useState(undefined); 38 | 39 | const onChange = (newValue: string) => { 40 | setValue(newValue); 41 | }; 42 | 43 | return ( 44 | 55 | ); 56 | }; 57 | 58 | export default App; 59 | -------------------------------------------------------------------------------- /src/antd/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic 3 | group: Basic 4 | demo: 5 | cols: 2 6 | --- 7 | 8 | # Basic Components for Adaptation in Editing Scenarios 9 | 10 | For the editing scenario, the basic styles and interactive feedback have been redefined, while the API remains unchanged. 11 | 12 | ## Code Demo 13 | 14 | ### Input Box Control 15 | 16 | For input box controls, the timing of data changes has been optimized, and updates will only be triggered when the input box loses focus or the Enter key is pressed; 17 | 18 | 19 | 20 | ### Select 21 | 22 | 23 | 24 | ### Segmented 25 | 26 | 27 | 28 | ### Tabs 29 | 30 | 31 | 32 | ### Tree 33 | 34 | 35 | 36 | ### TreeSelect 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/antd/index.ts: -------------------------------------------------------------------------------- 1 | // 先临时倒出暂未封装的 antd 组件 2 | export { 3 | Breadcrumb, 4 | Button, 5 | Card, 6 | Cascader, 7 | Checkbox, 8 | ColorPicker, 9 | DatePicker, 10 | Dropdown, 11 | Form, 12 | Layout, 13 | Menu, 14 | } from 'antd'; 15 | export * from './Input'; 16 | export * from './InputNumber'; 17 | export * from './Segmented'; 18 | export * from './Select'; 19 | export * from './Tabs'; 20 | export * from './Tree'; 21 | export * from './TreeSelect'; 22 | -------------------------------------------------------------------------------- /src/antd/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic 基础组件 3 | group: 基础组件 4 | demo: 5 | cols: 2 6 | --- 7 | 8 | # 适配编辑场景的基础组件 9 | 10 | 针对编辑器场景,重新定义了基础样式与交互反馈,API 保持不变。 11 | 12 | ## 代码演示 13 | 14 | ### 输入框控件 15 | 16 | 针对输入框类控件,重点优化了数据变更的时机,只有在输入框失焦或按回车后,才会触发更新; 17 | 18 | 19 | 20 | 21 | ### Select 22 | 23 | 24 | 25 | ### Segmented 26 | 27 | 28 | 29 | ### Tabs 30 | 31 | 32 | 33 | ### Tree 34 | 35 | 36 | 37 | ### TreeSelect 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/components/CopyButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { CopyOutlined } from '@ant-design/icons'; 2 | import copy from 'copy-to-clipboard'; 3 | import { memo } from 'react'; 4 | 5 | import ActionIcon from '@/ActionIcon'; 6 | import { useCopied } from '@/hooks/useCopied'; 7 | import { type TooltipProps } from 'antd'; 8 | import { DivProps } from 'react-layout-kit'; 9 | 10 | export interface CopyButtonProps extends DivProps { 11 | /** 12 | * @description Additional class name 13 | */ 14 | className?: string; 15 | /** 16 | * @description The text content to be copied 17 | */ 18 | content: string; 19 | /** 20 | * @description The placement of the tooltip 21 | * @enum ['top', 'left', 'right', 'bottom', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight', 'leftTop', 'leftBottom', 'rightTop', 'rightBottom'] 22 | * @default 'right' 23 | */ 24 | placement?: TooltipProps['placement']; 25 | } 26 | 27 | const CopyButton = memo( 28 | ({ content, className, placement = 'right', ...props }) => { 29 | const { copied, setCopied } = useCopied(); 30 | 31 | return ( 32 | } 36 | onClick={() => { 37 | copy(content); 38 | setCopied(); 39 | }} 40 | placement={placement} 41 | title={copied ? '✅ Success' : 'Copy'} 42 | /> 43 | ); 44 | }, 45 | ); 46 | 47 | export default CopyButton; 48 | -------------------------------------------------------------------------------- /src/components/Spotlight/style.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from '../../theme'; 2 | 3 | export const useStyles = createStyles( 4 | ( 5 | { css, token, isDarkMode }, 6 | { offset, outside, size }: { offset: { x: number; y: number }; outside: boolean; size: number }, 7 | ) => { 8 | const spotlightX = (offset?.x ?? 0) + 'px'; 9 | const spotlightY = (offset?.y ?? 0) + 'px'; 10 | const spotlightOpacity = outside ? '0' : '.1'; 11 | const spotlightSize = size + 'px'; 12 | return css` 13 | pointer-events: none; 14 | 15 | position: absolute; 16 | z-index: 1; 17 | inset: 0; 18 | 19 | opacity: ${spotlightOpacity}; 20 | background: radial-gradient( 21 | ${spotlightSize} circle at ${spotlightX} ${spotlightY}, 22 | ${isDarkMode ? token.colorText : '#fff'}, 23 | ${isDarkMode ? 'transparent' : token.colorTextQuaternary} 24 | ); 25 | border-radius: inherit; 26 | 27 | transition: all 0.2s; 28 | `; 29 | }, 30 | ); 31 | -------------------------------------------------------------------------------- /src/hooks/useCopied.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo, useState } from 'react'; 2 | 3 | export const useCopied = () => { 4 | const [copied, setCopy] = useState(false); 5 | 6 | useEffect(() => { 7 | if (!copied) return; 8 | 9 | const timer = setTimeout(() => { 10 | setCopy(false); 11 | }, 2000); 12 | 13 | return () => { 14 | clearTimeout(timer); 15 | }; 16 | }, [copied]); 17 | 18 | const setCopied = useCallback(() => setCopy(true), []); 19 | 20 | return useMemo(() => ({ copied, setCopied }), [copied]); 21 | }; 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as yjsMiddleware } from 'zustand-middleware-yjs'; 2 | export { ActionGroup } from './ActionGroup'; 3 | export type { ActionIconGroupItemType } from './ActionGroup'; 4 | export * from './ActionIcon'; 5 | export { default as Awareness } from './Awareness'; 6 | export type { AwarenessProps } from './Awareness'; 7 | export * from './ColumnList'; 8 | export * from './ComponentAsset'; 9 | export * from './ConfigProvider'; 10 | export { default as ContextMenu } from './ContextMenu'; 11 | export type { ContextMenuProps } from './ContextMenu'; 12 | export * from './DraggablePanel'; 13 | export { default as ErrorBoundary } from './ErrorBoundary'; 14 | export { default as FreeCanvas } from './FreeCanvas'; 15 | export type { FreeCanvasProps } from './FreeCanvas'; 16 | export * from './Highlight'; 17 | export * from './IconPicker'; 18 | export * from './InteractContainer'; 19 | export { Layout as EditorLayout } from './Layout'; 20 | export { default as Markdown, type MarkdownProps } from './Markdown'; 21 | export * from './ProBuilder'; 22 | export * from './ProEditor'; 23 | export * from './Snippet'; 24 | export * from './SortableList'; 25 | export * from './SortableTree'; 26 | export * from './antd'; 27 | export * from './theme'; 28 | export * from './types'; 29 | export * from './utils'; 30 | -------------------------------------------------------------------------------- /src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { createInstance } from 'antd-style'; 2 | import { StudioStylish, StudioThemeToken } from './themes'; 3 | 4 | type ProEditorToken = { 5 | editorPrefix: string; 6 | }; 7 | declare module 'antd-style' { 8 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 9 | export interface CustomToken extends StudioThemeToken {} 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 12 | export interface CustomStylish extends StudioStylish {} 13 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 14 | export interface CustomToken extends ProEditorToken {} 15 | } 16 | 17 | const { createStyles, ThemeProvider } = createInstance({ 18 | customToken: { 19 | editorPrefix: 'editor', 20 | }, 21 | }); 22 | 23 | export { 24 | createGlobalStyle, 25 | css, 26 | cx, 27 | injectGlobal, 28 | keyframes, 29 | useAntdToken as useToken, 30 | type AntdToken, 31 | } from 'antd-style'; 32 | export * from './themes'; 33 | export { ThemeProvider, createStyles }; 34 | -------------------------------------------------------------------------------- /src/theme/themes/antdTheme.ts: -------------------------------------------------------------------------------- 1 | import { theme, ThemeConfig } from 'antd'; 2 | import { ThemeAppearance } from 'antd-style'; 3 | import { studioDarkAlgorithm } from './darkAlgorithm'; 4 | 5 | export const createStudioAntdTheme = (appearance: ThemeAppearance) => { 6 | const themeConfig: ThemeConfig = { 7 | algorithm: [theme.compactAlgorithm], 8 | }; 9 | 10 | if (appearance === 'dark') { 11 | (themeConfig.algorithm as Array).push(studioDarkAlgorithm); 12 | } 13 | 14 | return themeConfig; 15 | }; 16 | -------------------------------------------------------------------------------- /src/theme/themes/darkAlgorithm.ts: -------------------------------------------------------------------------------- 1 | import { theme } from 'antd'; 2 | import type { MappingAlgorithm } from 'antd/es/theme/interface'; 3 | 4 | /** 5 | * studio 暗色模式下算法 6 | * @param seedToken 7 | * @param mapToken 8 | */ 9 | export const studioDarkAlgorithm: MappingAlgorithm = (seedToken, mapToken) => { 10 | const mergeToken = theme.darkAlgorithm(seedToken, mapToken); 11 | 12 | return { 13 | ...mergeToken, 14 | 15 | colorBgLayout: '#20252b', 16 | colorBgContainer: '#282c34', 17 | colorBgElevated: '#32363e', 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/theme/themes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './antdTheme'; 2 | export * from './darkAlgorithm'; 3 | export * from './stylish'; 4 | export * from './token'; 5 | -------------------------------------------------------------------------------- /src/theme/themes/token.ts: -------------------------------------------------------------------------------- 1 | import { GetCustomToken } from 'antd-style'; 2 | 3 | export interface StudioThemeToken { 4 | focusedOutlineColor: string; 5 | colorTypeBoolean: string; 6 | colorTypeNumber: string; 7 | colorTypeString: string; 8 | colorTypeBoolArray: string; 9 | colorTypeNumberArray: string; 10 | colorTypeStringArray: string; 11 | } 12 | 13 | export const getStudioToken: GetCustomToken = () => ({ 14 | focusedOutlineColor: '#4c9ffe', 15 | colorTypeBoolean: '#D8C152', 16 | colorTypeNumber: '#5295C4', 17 | colorTypeString: '#149E6D', 18 | colorTypeBoolArray: '#D8C152', 19 | colorTypeNumberArray: '#239BEF', 20 | colorTypeStringArray: '#62AE8D', 21 | }); 22 | 23 | export const themeToken = getStudioToken({} as any); 24 | -------------------------------------------------------------------------------- /src/types/c2d2c.ts: -------------------------------------------------------------------------------- 1 | export interface BoundBox { 2 | width: number; 3 | height: number; 4 | } 5 | 6 | export type SizeFollow = 'sketch' | 'self'; 7 | 8 | /** 9 | * 组件尺寸控制 10 | */ 11 | export interface ComponentSize extends Partial { 12 | widthFollow?: SizeFollow; 13 | heightFollow?: SizeFollow; 14 | } 15 | 16 | export type VerticalType = 'top' | 'center' | 'bottom'; 17 | export type HorizontalType = 'left' | 'center' | 'right'; 18 | 19 | export interface Alignment { 20 | /** 21 | * 横向位置 22 | */ 23 | vertical: VerticalType; 24 | /** 25 | * 纵向位置 26 | */ 27 | horizontal: HorizontalType; 28 | /** 29 | * 是否翻转相应的坐标系 30 | */ 31 | verticalFlipped?: boolean; 32 | } 33 | 34 | /** 35 | * C2D 组件预设值 36 | */ 37 | export interface C2DPresetValue { 38 | /** 39 | * 组件名 componentName 40 | */ 41 | componentName: string; 42 | /** 43 | * 组件属性 44 | */ 45 | props: any; 46 | /** 47 | * 对齐方式 48 | */ 49 | alignment?: Alignment; 50 | /** 51 | * 组件大小 52 | */ 53 | size?: ComponentSize; 54 | /** 55 | * 如果存在配置属性,那么存在这里持久化保存 56 | */ 57 | config?: any; 58 | } 59 | 60 | export interface ReactNodeElement { 61 | $$__type: 'element'; 62 | $$__body: C2DPresetValue; 63 | } 64 | -------------------------------------------------------------------------------- /src/types/catogory.ts: -------------------------------------------------------------------------------- 1 | export interface CategoryBaseField { 2 | title?: string; 3 | description?: string; 4 | defaultActive?: boolean; 5 | } 6 | /** 7 | * 目录分类 8 | * 默认包含 四个分类 9 | * type: '类型' 10 | * content: '内容' 11 | * style: '样式' 12 | * status: '状态' 13 | */ 14 | export type CategoryConfig = Record; 15 | 16 | export enum CategoryMap { 17 | type = 'type', 18 | content = 'content', 19 | function = 'function', 20 | style = 'style', 21 | status = 'status', 22 | bind = 'bind', 23 | table = 'table', 24 | customStyle = 'customStyle', 25 | } 26 | 27 | export const configCategoryMap: CategoryConfig = { 28 | [CategoryMap.type]: { title: '类型', defaultActive: true }, 29 | [CategoryMap.content]: { title: '内容与功能', defaultActive: true }, 30 | [CategoryMap.style]: { title: '样式', defaultActive: true }, 31 | [CategoryMap.status]: { title: '状态', defaultActive: true }, 32 | [CategoryMap.bind]: { title: '分页字段绑定', defaultActive: true }, 33 | [CategoryMap.table]: { title: '表格要素', defaultActive: true }, 34 | [CategoryMap.customStyle]: { title: '自定义样式', defaultActive: true }, 35 | }; 36 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './c2d2c'; 2 | export * from './catogory'; 3 | export * from './schema'; 4 | -------------------------------------------------------------------------------- /src/utils/autoId.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 生成的唯一 id 的字符串 3 | */ 4 | export const genUniqueId = (prefix?: string | number) => 5 | `${prefix}${Math.round(Math.random() * 1000).toString(16)}`; 6 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './autoId'; 2 | export * from './c2d2c'; 3 | -------------------------------------------------------------------------------- /tests/demo.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import * as fs from 'fs'; 3 | import { glob } from 'glob'; 4 | import * as path from 'path'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { vi } from 'vitest'; 7 | 8 | beforeEach(() => { 9 | vi.useFakeTimers(); 10 | // @ts-ignore 11 | global.fetch = vi.fn(() => 12 | Promise.resolve({ json: () => Promise.resolve({}), text: () => Promise.resolve('') }), 13 | ); 14 | }); 15 | 16 | afterEach(() => { 17 | vi.useRealTimers(); 18 | }); 19 | 20 | const baseDir = path.join(__dirname, '..'); 21 | 22 | const npmSrcDir = path.join(baseDir, `./src`); 23 | const dirs = fs.readdirSync(npmSrcDir); 24 | 25 | dirs.forEach((dir) => { 26 | // 支持 demos 下的所有非_开头的tsx文件 27 | const files = glob.sync(`${npmSrcDir}/${dir}/**/demos/**/[!_]*.tsx`); 28 | if (files.length === 0) return; 29 | 30 | describe(`<${dir} />`, () => { 31 | files.forEach((file) => { 32 | const demoName = file?.split('/').pop(); 33 | 34 | it(`renders ${demoName} correctly`, async () => { 35 | const Demo = await import(file); 36 | 37 | if (!demoName) return; 38 | // console.log(`测试组件${dir} DEMO:${demoName}`); 39 | const wrapper = render(); 40 | act(() => { 41 | vi.runAllTimers(); 42 | }); 43 | 44 | expect(wrapper.container).toMatchSnapshot(); 45 | wrapper.unmount(); 46 | }); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/test-setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest'; 2 | import { theme } from 'antd'; 3 | 4 | // Not use dynamic hashed for test env since version will change hash dynamically. 5 | theme.defaultConfig.hashed = false; 6 | 7 | import { createSerializer } from '@emotion/jest'; 8 | import { vi } from 'vitest'; 9 | expect.addSnapshotSerializer(createSerializer()); 10 | 11 | /* eslint-disable global-require */ 12 | if (typeof window !== 'undefined') { 13 | global.window.resizeTo = (width, height) => { 14 | // @ts-ignore-next-line 15 | global.window.innerWidth = width || global.window.innerWidth; 16 | // @ts-ignore-next-line 17 | global.window.innerHeight = height || global.window.innerHeight; 18 | global.window.dispatchEvent(new Event('resize')); 19 | }; 20 | global.window.scrollTo = () => {}; 21 | // ref: https://github.com/ant-design/ant-design/issues/18774 22 | if (!window.matchMedia) { 23 | Object.defineProperty(global.window, 'matchMedia', { 24 | value: vi.fn((query) => ({ 25 | matches: query.includes('max-width'), 26 | addListener: () => {}, 27 | addEventListener: () => {}, 28 | removeListener: () => {}, 29 | removeEventListener: () => {}, 30 | })), 31 | }); 32 | } 33 | 34 | window.ResizeObserver = 35 | window.ResizeObserver || 36 | vi.fn().mockImplementation(() => ({ 37 | disconnect: vi.fn(), 38 | observe: vi.fn(), 39 | unobserve: vi.fn(), 40 | })); 41 | 42 | global.window.HTMLElement.prototype.scrollIntoView = () => {}; 43 | } 44 | -------------------------------------------------------------------------------- /tsconfig-check.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "include": ["src", "tests"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "moduleResolution": "node", 5 | "skipLibCheck": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "sourceMap": true, 8 | "target": "ESNext", 9 | "module": "esnext", 10 | "jsx": "react-jsx", 11 | "types": ["vitest/globals", "@testing-library/jest-dom"], 12 | "declaration": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "@ant-design/pro-editor": ["src"], 16 | "@/*": ["src/*"] 17 | } 18 | }, 19 | "exclude": ["node_modules", "dist"], 20 | "include": ["src", "config/config.ts", "vitest.config.ts", "docs", "tests", ".dumirc.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | esbuild: { 6 | jsxInject: "import React from 'react'", 7 | }, 8 | test: { 9 | setupFiles: path.join(__dirname, './tests/test-setup.ts'), 10 | environment: 'jsdom', 11 | globals: true, 12 | testTimeout: 20000, 13 | coverage: { 14 | provider: 'v8', 15 | reporter: ['text', 'json', 'lcov', 'text-summary'], 16 | }, 17 | alias: { 18 | '@ant-design/pro-editor': path.join(__dirname, './src'), 19 | '@': path.join(__dirname, './src'), 20 | }, 21 | }, 22 | }); 23 | --------------------------------------------------------------------------------