diff --git a/ReadMe.md b/ReadMe.md index 22611d8..63357ca 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -27,12 +27,13 @@ A **Pagination Table** & **Scroll List** component suite for [CRUD operation][1] 5. [Form Field](https://idea2app.github.io/MobX-RESTful-table/functions/FormField-1.html) 6. [Range Input](https://idea2app.github.io/MobX-RESTful-table/classes/RangeInput.html) 7. [Badge Input](https://idea2app.github.io/MobX-RESTful-table/classes/BadgeInput.html) -8. [REST Form](https://idea2app.github.io/MobX-RESTful-table/classes/RestForm.html) -9. [Pager](https://idea2app.github.io/MobX-RESTful-table/functions/Pager-1.html) -10. [REST Table](https://idea2app.github.io/MobX-RESTful-table/classes/RestTable.html) -11. [Scroll Boundary](https://idea2app.github.io/MobX-RESTful-table/functions/ScrollBoundary-1.html) -12. [Scroll List](https://idea2app.github.io/MobX-RESTful-table/classes/ScrollList.html) -13. [Searchable Input](https://idea2app.github.io/MobX-RESTful-table/classes/SearchableInput.html) +8. [Array Field](https://idea2app.github.io/MobX-RESTful-table/classes/ArrayField.html) +9. [REST Form](https://idea2app.github.io/MobX-RESTful-table/classes/RestForm.html) +10. [Pager](https://idea2app.github.io/MobX-RESTful-table/functions/Pager-1.html) +11. [REST Table](https://idea2app.github.io/MobX-RESTful-table/classes/RestTable.html) +12. [Scroll Boundary](https://idea2app.github.io/MobX-RESTful-table/functions/ScrollBoundary-1.html) +13. [Scroll List](https://idea2app.github.io/MobX-RESTful-table/classes/ScrollList.html) +14. [Searchable Input](https://idea2app.github.io/MobX-RESTful-table/classes/SearchableInput.html) ## Installation diff --git a/package.json b/package.json index add3dbf..27930e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-restful-table", - "version": "2.3.0", + "version": "2.4.0", "license": "LGPL-3.0", "author": "shiy2008@gmail.com", "description": "A Pagination Table & Scroll List component suite for CRUD operation, which is based on MobX RESTful & React.", @@ -34,6 +34,7 @@ "mobx-react-helper": "^0.4.1", "mobx-restful": "^2.1.0", "react-bootstrap": "^2.10.10", + "react-bootstrap-editor": "^2.1.1", "regenerator-runtime": "^0.14.1", "web-utility": "^4.4.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02cacbb..30ad21a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: react-bootstrap: specifier: ^2.10.10 version: 2.10.10(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-bootstrap-editor: + specifier: ^2.1.1 + version: 2.1.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) regenerator-runtime: specifier: ^0.14.1 version: 0.14.1 @@ -180,6 +183,9 @@ packages: resolution: {integrity: sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w==} engines: {node: '>=12.0.0'} + '@mixmark-io/domino@2.2.0': + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} cpu: [arm64] @@ -770,6 +776,9 @@ packages: '@types/react@19.1.5': resolution: {integrity: sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==} + '@types/turndown@5.0.5': + resolution: {integrity: sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -816,6 +825,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browser-fs-access@0.37.0: + resolution: {integrity: sha512-MKpvZrKtv6pBJ2ACd+VwfS9XauBKTMVZg2UBibypuK1gfiXM7euZjbdKmvRsyxeQRhfzNVQrzCSVGXs19/LP8Q==} + browserslist@4.24.5: resolution: {integrity: sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -918,8 +930,11 @@ packages: editorjs-html@4.0.5: resolution: {integrity: sha512-ImQYxB3fNCJcd+nJ+Vbne/6PxidO1cYByNpu9nBDStVabfjVrMW65BuR+IEZfOii8VKYH+CW/lYDb2GDlzZtDg==} - electron-to-chromium@1.5.156: - resolution: {integrity: sha512-QeOqv11TSASsY/3Ft3LUyDqEiEOph5/85srEPFUo9wuGFNTb0/z5fGE/+ZzTrYvSTGoXNkdQLTw3MKRmgyOyGA==} + edkit@1.2.7: + resolution: {integrity: sha512-dCOBN9MMbCaCdSqhnZTSHPe7lu53TQttttjVBxLE/TehsQasuxmqW3ckimVODFaJVci1A6w429j9bebpiU3zKg==} + + electron-to-chromium@1.5.157: + resolution: {integrity: sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==} element-internals-polyfill@1.3.13: resolution: {integrity: sha512-viZ7wJsvh6eFwGQX512zEaccK/c6RRFSerJsdkfe3DW/ZtruvNeOR33fpPZgfXxvqRdU2lK33KM4S6GqaTgVKQ==} @@ -1159,6 +1174,11 @@ packages: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true + marked@15.0.12: + resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} + engines: {node: '>= 18'} + hasBin: true + mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -1327,6 +1347,12 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} + react-bootstrap-editor@2.1.1: + resolution: {integrity: sha512-vnAy1MSn4mAZp418cz3R7IiUWXGGwNfmWQYlPUa2MWGm6hTTxJVeKTp2jhrH3n8sbbtg2MN4/co44OP+sZn1pQ==} + peerDependencies: + react: '>=16' + react-dom: '>=16' + react-bootstrap@2.10.10: resolution: {integrity: sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ==} peerDependencies: @@ -1460,6 +1486,12 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + turndown-plugin-gfm@1.0.2: + resolution: {integrity: sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==} + + turndown@7.2.0: + resolution: {integrity: sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==} + type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -1613,6 +1645,8 @@ snapshots: '@lezer/lr': 1.4.2 json5: 2.2.3 + '@mixmark-io/domino@2.2.0': {} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': optional: true @@ -2467,6 +2501,8 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/turndown@5.0.5': {} + '@types/unist@3.0.3': {} '@types/warning@3.0.3': {} @@ -2503,10 +2539,12 @@ snapshots: dependencies: fill-range: 7.1.1 + browser-fs-access@0.37.0: {} + browserslist@4.24.5: dependencies: caniuse-lite: 1.0.30001718 - electron-to-chromium: 1.5.156 + electron-to-chromium: 1.5.157 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.5) @@ -2583,7 +2621,20 @@ snapshots: editorjs-html@4.0.5: {} - electron-to-chromium@1.5.156: {} + edkit@1.2.7(typescript@5.8.3): + dependencies: + '@swc/helpers': 0.5.17 + '@types/turndown': 5.0.5 + browser-fs-access: 0.37.0 + marked: 15.0.12 + regenerator-runtime: 0.14.1 + turndown: 7.2.0 + turndown-plugin-gfm: 1.0.2 + web-utility: 4.4.3(typescript@5.8.3) + transitivePeerDependencies: + - typescript + + electron-to-chromium@1.5.157: {} element-internals-polyfill@1.3.13: {} @@ -2829,6 +2880,8 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 + marked@15.0.12: {} + mdurl@2.0.0: {} micromatch@4.0.8: @@ -3015,6 +3068,20 @@ snapshots: punycode.js@2.3.1: {} + react-bootstrap-editor@2.1.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3): + dependencies: + '@swc/helpers': 0.5.17 + edkit: 1.2.7(typescript@5.8.3) + mobx: 6.13.7 + mobx-react: 9.2.0(mobx@6.13.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + mobx-react-helper: 0.4.1(mobx@6.13.7)(react@19.1.0)(typescript@5.8.3) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + web-utility: 4.4.3(typescript@5.8.3) + transitivePeerDependencies: + - react-native + - typescript + react-bootstrap@2.10.10(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@babel/runtime': 7.27.1 @@ -3157,6 +3224,12 @@ snapshots: tslib@2.8.1: {} + turndown-plugin-gfm@1.0.2: {} + + turndown@7.2.0: + dependencies: + '@mixmark-io/domino': 2.2.0 + type-fest@0.20.2: {} typedoc-plugin-mdn-links@5.0.2(typedoc@0.28.4(typescript@5.8.3)): diff --git a/preview/content.tsx b/preview/content.tsx index d60f3d0..6c5a6f7 100644 --- a/preview/content.tsx +++ b/preview/content.tsx @@ -1,9 +1,10 @@ import { text2color } from 'idea-react'; import { GitRepository } from 'mobx-github'; import { FC } from 'react'; -import { Badge } from 'react-bootstrap'; +import { Badge, Form, InputGroup } from 'react-bootstrap'; import { + ArrayField, BadgeInput, Column, FileModel, @@ -20,6 +21,11 @@ import { import { i18n, repositoryStore, topicStore } from './model'; import { CodeExample, Section } from './utility'; +interface Price { + currency: 'USD' | 'CNY'; + amount: number; +} + class MyFileModel extends FileModel {} const columns: Column[] = [ @@ -55,6 +61,16 @@ const columns: Column[] = [ ), }, { key: 'stargazers_count', type: 'number', renderHead: 'Star Count' }, + { + key: 'description', + contentEditable: true, + renderHead: 'Description', + renderBody: ({ description }) => ( +

+ {description} +

+ ), + }, ]; export const Content: FC = () => ( @@ -117,6 +133,31 @@ export const Content: FC = () => ( +
+ + ( + + + + + + + + )} + onChange={console.log} + /> + +
+
diff --git a/source/ArrayField.tsx b/source/ArrayField.tsx new file mode 100644 index 0000000..c5a391b --- /dev/null +++ b/source/ArrayField.tsx @@ -0,0 +1,81 @@ +import { toJS } from 'mobx'; +import { observer } from 'mobx-react'; +import { FormComponent, FormComponentProps } from 'mobx-react-helper'; +import { DataObject } from 'mobx-restful'; +import { ChangeEvent, HTMLAttributes, ReactNode } from 'react'; +import { Button, ButtonGroup } from 'react-bootstrap'; +import { formToJSON, isEmpty } from 'web-utility'; + +export type ArrayFieldProps = Pick< + HTMLAttributes, + 'className' | 'style' +> & + FormComponentProps & { + renderItem: (item: T, index: number) => ReactNode; + }; + +@observer +export class ArrayField extends FormComponent< + ArrayFieldProps +> { + componentDidMount() { + super.componentDidMount(); + + if (isEmpty(this.value)) this.add(); + } + + add = () => (this.innerValue = [...(this.innerValue || []), {} as T]); + + remove = (index: number) => (this.innerValue = this.innerValue?.filter((_, i) => i !== index)); + + handleChange = + (index: number) => + ({ currentTarget }: ChangeEvent) => { + const item = formToJSON(currentTarget as HTMLFieldSetElement), + { innerValue } = this; + + const list = [...innerValue!.slice(0, index), item, ...innerValue!.slice(index + 1)].map( + item => toJS(item), + ); + this.props.onChange?.(list); + }; + + handleUpdate = + (index: number) => + ({ currentTarget }: ChangeEvent) => + (this.innerValue![index] = formToJSON(currentTarget as HTMLFieldSetElement)); + + render() { + const { className = '', style, name, renderItem } = this.props; + + return ( + <> + {this.value?.map((item, index, { length }) => ( +
+
{renderItem(item, index)}
+ + + + +
+ ))} + + ); + } +} diff --git a/source/RestForm.tsx b/source/RestForm.tsx index 3f6b783..67a255f 100644 --- a/source/RestForm.tsx +++ b/source/RestForm.tsx @@ -3,8 +3,9 @@ import { TranslationModel } from 'mobx-i18n'; import { observer } from 'mobx-react'; import { ObservedComponent } from 'mobx-react-helper'; import { DataObject, Filter, IDType, ListModel } from 'mobx-restful'; -import { FormEvent, Fragment, InputHTMLAttributes, ReactNode, TextareaHTMLAttributes } from 'react'; +import { FormEvent, Fragment, InputHTMLAttributes, ReactNode } from 'react'; import { Button, Form, FormGroupProps, FormProps } from 'react-bootstrap'; +import { Editor, EditorProps } from 'react-bootstrap-editor'; import { formatDate, formToJSON, isEmpty } from 'web-utility'; import { FilePreview } from './FilePreview'; @@ -27,7 +28,8 @@ export interface Field | 'accept' | 'placeholder' >, - Pick { + Pick, + Pick { key?: keyof T; renderLabel?: ReactNode | ((data: keyof T) => ReactNode); renderInput?: (data: T, meta: Field) => ReactNode; @@ -112,8 +114,10 @@ export class RestForm< ? this.renderFile(meta) : (meta.type === 'radio' || meta.type === 'checkbox') && meta.options ? this.renderCheckGroup(meta) - : meta.key && - this.renderField(meta, meta.rows && !meta.options ? { as: 'textarea' } : {})), + : meta.contentEditable + ? this.renderHTMLEditor(meta) + : meta.key && + this.renderField(meta, meta.rows && !meta.options ? { as: 'textarea' } : {})), })); } @@ -151,11 +155,11 @@ export class RestForm< }; renderCheckGroup = ({ key, type, options, renderLabel }: Field) => - (data: D) => - this.fieldReady && ( - -
- {options.map(({ value, text = value }) => ( + (data: D) => ( + +
+ {this.fieldReady && + options.map(({ value, text = value }) => ( ))} -
-
- ); +
+
+ ); + + renderHTMLEditor = + ({ key, renderLabel }: Field) => + (data: D) => ( + + {this.fieldReady && } + + ); renderField = ( { key, type, step, renderLabel, renderInput, ...meta }: Field, @@ -205,7 +217,7 @@ export class RestForm< onReset={() => store.clearCurrent()} > {fields.map(({ renderInput, ...meta }) => ( - {renderInput?.(currentOne, meta)} + {renderInput?.(currentOne, meta)} ))} {!readOnly && (