From fe685c2fae4657bafc98305a483bf7557b5a39c2 Mon Sep 17 00:00:00 2001 From: luo jiyin Date: Fri, 12 Jul 2024 20:05:54 +0800 Subject: [PATCH] [migrate] replace Markdown IME with HTML editor (#14) --- components/Form/HTMLEditor/index.tsx | 22 ++++++ components/Git/ArticleEditor.tsx | 100 ++++++++++----------------- package.json | 1 + pnpm-lock.yaml | 50 ++++++++++++++ translation/en-US.ts | 10 +++ translation/zh-CN.ts | 10 +++ translation/zh-TW.ts | 10 +++ 7 files changed, 139 insertions(+), 64 deletions(-) create mode 100644 components/Form/HTMLEditor/index.tsx diff --git a/components/Form/HTMLEditor/index.tsx b/components/Form/HTMLEditor/index.tsx new file mode 100644 index 0000000..c720965 --- /dev/null +++ b/components/Form/HTMLEditor/index.tsx @@ -0,0 +1,22 @@ +import { FC } from 'react'; +import { + AudioTool, + Editor, + EditorProps, + IFrameTool, + ImageTool, + OriginalTools, + VideoTool, +} from 'react-bootstrap-editor'; +import { Constructor } from 'web-utility'; + +const ExcludeTools = [IFrameTool, AudioTool, VideoTool]; + +const CustomTools = OriginalTools.filter( + Tool => !ExcludeTools.includes(Tool as Constructor), +); + +const HTMLEditor: FC = props => ( + +); +export default HTMLEditor; diff --git a/components/Git/ArticleEditor.tsx b/components/Git/ArticleEditor.tsx index 235f83c..08680ef 100644 --- a/components/Git/ArticleEditor.tsx +++ b/components/Git/ArticleEditor.tsx @@ -4,23 +4,21 @@ import { computed, observable } from 'mobx'; import { GitContent } from 'mobx-github'; import { observer } from 'mobx-react'; import { DataObject } from 'mobx-restful'; -import { - ChangeEvent, - Component, - createRef, - FormEvent, - MouseEvent, -} from 'react'; +import dynamic from 'next/dynamic'; +import { ChangeEvent, Component, FormEvent, MouseEvent } from 'react'; import { Button, Col, Form } from 'react-bootstrap'; import { blobOf, formatDate, uniqueID } from 'web-utility'; import YAML from 'yaml'; import { GitRepositoryModel, userStore } from '../../models/Repository'; +import { i18n } from '../../models/Translation'; import { ListField } from '../Form/JSONEditor'; -import { MarkdownEditor } from '../Form/MarkdownEditor'; import { PathSelect } from './PathSelect'; import { RepositorySelect } from './RepositorySelect'; +const { t } = i18n; +const HTMLEditor = dynamic(() => import('../Form/HTMLEditor'), { ssr: false }); + export const fileType = { MarkDown: ['md', 'markdown'], JSON: ['json'], @@ -39,6 +37,7 @@ export type HyperLink = HTMLAnchorElement | HTMLImageElement; export class ArticleEditor extends Component { @observable accessor repository = ''; + editorContent = ''; @computed get currentRepository() { @@ -59,18 +58,9 @@ export class ArticleEditor extends Component { path = ''; URL = ''; - private Core = createRef(); - - get core() { - return this.Core.current; - } - @observable accessor meta: PostMeta | null = null; - @observable - accessor copied = false; - static contentFilter({ type, name }: GitContent) { return ( type === 'dir' || @@ -123,31 +113,25 @@ export class ArticleEditor extends Component { if (meta[1]) this.setPostMeta(meta[1]); } - if (this.core) this.core.raw = content; + this.editorContent = content; }; reset = () => { this.meta = null; - - if (this.core) this.core.raw = ''; + this.editorContent = ''; }; onPathClear = ({ target: { value } }: ChangeEvent) => { - if (value.trim()) return; - - this.meta = null; - - if (this.core) this.core.raw = ''; + if (!value.trim()) this.reset(); }; fixURL = debounce(() => { const { repository } = this, - pageURL = window.location.href.split('?')[0]; + pageURL = window.location.href.split('?')[0], + root = document.querySelector('div[contenteditable]'); - if (this.core && this.core.root) - for (let element of this.core.root.querySelectorAll( - '[href], [src]', - )) { + if (root) + for (let element of root.querySelectorAll('[href], [src]')) { let URI = element instanceof HTMLAnchorElement ? element.href : element.src; @@ -166,14 +150,14 @@ export class ArticleEditor extends Component { getContent() { const type = this.URL.split('.').slice(-1)[0], - { meta, core } = this; + { meta, editorContent } = this; if (fileType.JSON.includes(type)) return JSON.stringify(meta); if (fileType.YAML.includes(type)) return YAML.stringify(meta); - if (fileType.MarkDown.includes(type) && core) { - if (!meta) return core.raw; + if (fileType.MarkDown.includes(type) && editorContent) { + if (!meta) return editorContent; // @ts-ignore meta.updated = formatDate(); @@ -181,21 +165,22 @@ export class ArticleEditor extends Component { ${YAML.stringify(meta)} --- - ${core.raw}`; + ${editorContent}`; } } submit = async (event: FormEvent) => { event.preventDefault(); - const { currentRepository, repositoryStore, core } = this, + const { currentRepository, repositoryStore, editorContent } = this, // @ts-ignore { message } = event.currentTarget.elements; - if (!core?.root) return; + if (!editorContent) return; + const root = document.querySelector('div[contenteditable]'); const media: HTMLMediaElement[] = [].filter.call( - core.root.querySelectorAll('img[src], audio[src], video[src]'), + root!.querySelectorAll('img[src], audio[src], video[src]'), ({ src }) => new URL(src).protocol === 'blob:', ); @@ -224,16 +209,6 @@ export class ArticleEditor extends Component { window.alert('Submitted'); }; - copyMarkdown = async (event: MouseEvent) => { - event.preventDefault(); - - if (this.core) { - await navigator.clipboard.writeText(this.core.raw); - - this.copied = true; - } - }; - loadFile = async (path: string) => { const type = path.split('.').at(-1)?.toLowerCase(); @@ -251,7 +226,7 @@ export class ArticleEditor extends Component { }; render() { - const { repository, meta, copied } = this; + const { repository, meta } = this; return (
- + (this.repository = `${owner}/${name}`) @@ -268,14 +243,16 @@ export class ArticleEditor extends Component { /> - + {repository && ( )} - + @@ -283,16 +260,16 @@ export class ArticleEditor extends Component { sm={3} className="d-flex flex-wrap gap-2 justify-content-around align-items-center" > - + {meta && ( - + @@ -303,17 +280,12 @@ export class ArticleEditor extends Component { )}
- - +
- + (this.editorContent = value)} + />
); diff --git a/package.json b/package.json index 08eff4d..57ccc8a 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "primereact": "^10.6.6", "react": "^18.3.1", "react-bootstrap": "^2.10.2", + "react-bootstrap-editor": "^2.0.4", "react-dom": "^18.3.1", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c87d4ab..fe010f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: react-bootstrap: specifier: ^2.10.2 version: 2.10.2(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-bootstrap-editor: + specifier: ^2.0.4 + version: 2.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.4.5) react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) @@ -1827,6 +1830,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browser-fs-access@0.35.0: + resolution: {integrity: sha512-sLoadumpRfsjprP8XzVjpQc0jK8yqHBx0PtUTGYj2fftT+P/t+uyDAQdMgGAPKD011in/O+YYGh7fIs0oG/viw==} + browserslist@4.23.0: resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -2090,6 +2096,9 @@ packages: editorjs-html@3.4.3: resolution: {integrity: sha512-HMqQ3BCE98uhSpJsbfH0c3CoMctUMCHlap2Eq/7/VjaHas+g3IJqyf+ERtMByoQCzvcW22ISYaZEeE7rGkd8Xg==} + edkit@1.2.1: + resolution: {integrity: sha512-Mqixc9JdCC7oFFeQ+Vn8GmK3jaGkI7jEGighQQt3PZmKKXmlMovwRoMXFV2glev7pyiA48GP89sBXapZcQhgXQ==} + ejs@3.1.10: resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} @@ -3106,6 +3115,11 @@ packages: engines: {node: '>= 18'} hasBin: true + marked@13.0.1: + resolution: {integrity: sha512-7kBohS6GrZKvCsNXZyVVXSW7/hGBHe49ng99YPkDCckSUrrG7MSFLCexsRxptzOmyW2eT5dySh4Md1V6my52fA==} + engines: {node: '>= 18'} + hasBin: true + marked@4.3.0: resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} engines: {node: '>= 12'} @@ -3756,6 +3770,12 @@ packages: randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + react-bootstrap-editor@2.0.4: + resolution: {integrity: sha512-hKJMhCriJ1/V4QWq+FWccxYCtq6IYYIr79wBsyWlXSmwHypms79CQGNGL78F19MWQQn613AvltBMWaxwi7DEmw==} + peerDependencies: + react: '>=16' + react-dom: '>=16' + react-bootstrap@2.10.2: resolution: {integrity: sha512-UvB7mRqQjivdZNxJNEA2yOQRB7L9N43nBnKc33K47+cH90/ujmnMwatTCwQLu83gLhrzAl8fsa6Lqig/KLghaA==} peerDependencies: @@ -6645,6 +6665,8 @@ snapshots: dependencies: fill-range: 7.1.1 + browser-fs-access@0.35.0: {} + browserslist@4.23.0: dependencies: caniuse-lite: 1.0.30001625 @@ -6909,6 +6931,19 @@ snapshots: editorjs-html@3.4.3: {} + edkit@1.2.1(typescript@5.4.5): + dependencies: + '@swc/helpers': 0.5.11 + '@types/turndown': 5.0.4 + browser-fs-access: 0.35.0 + marked: 13.0.1 + regenerator-runtime: 0.14.1 + turndown: 7.2.0 + turndown-plugin-gfm: 1.0.2 + web-utility: 4.4.0(typescript@5.4.5) + transitivePeerDependencies: + - typescript + ejs@3.1.10: dependencies: jake: 10.9.1 @@ -8174,6 +8209,8 @@ snapshots: marked@12.0.2: {} + marked@13.0.1: {} + marked@4.3.0: {} mdast-util-find-and-replace@3.0.1: @@ -9094,6 +9131,19 @@ snapshots: dependencies: safe-buffer: 5.2.1 + react-bootstrap-editor@2.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.4.5): + dependencies: + '@swc/helpers': 0.5.11 + edkit: 1.2.1(typescript@5.4.5) + mobx: 6.12.3 + mobx-react: 9.1.1(mobx@6.12.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + web-utility: 4.4.0(typescript@5.4.5) + transitivePeerDependencies: + - react-native + - typescript + react-bootstrap@2.10.2(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.24.6 diff --git a/translation/en-US.ts b/translation/en-US.ts index e901fb4..150db61 100644 --- a/translation/en-US.ts +++ b/translation/en-US.ts @@ -109,6 +109,16 @@ export default { range_module: 'module', last_step: 'back', + // Git pager + repository: 'repository', + file_path: 'file path', + commit_message: 'commit message', + commit: 'commit', + clear: 'clear', + meta: 'meta', + content: 'content', + copy_MarkDown: 'copy Markdown', + //Polyfill page select_compatible_browser: 'select Compatible Browser', select_features: 'select features', diff --git a/translation/zh-CN.ts b/translation/zh-CN.ts index b2f51c3..d64876e 100644 --- a/translation/zh-CN.ts +++ b/translation/zh-CN.ts @@ -106,6 +106,16 @@ export default { range_module: '模块', last_step: '上一步', + // Git pager + repository: '代码仓库', + file_path: '文件路径', + commit_message: '提交信息', + commit: '提交', + clear: '清空', + meta: '元数据', + content: '内容', + copy_MarkDown: '复制 Markdown', + //Polyfill page select_compatible_browser: '选择兼容浏览器', select_features: '选择特性', diff --git a/translation/zh-TW.ts b/translation/zh-TW.ts index bd87cc1..1e6ab02 100644 --- a/translation/zh-TW.ts +++ b/translation/zh-TW.ts @@ -106,6 +106,16 @@ export default { range_module: '模組', last_step: '上一步', + // Git pager + repository: '程式碼倉庫', + file_path: '檔案路徑', + commit_message: '提交訊息', + commit: '提交', + clear: '清空', + meta: '詮釋資料', + content: '内容', + copy_MarkDown: '複製 Markdown', + //Polyfill page select_compatible_browser: '選擇相容瀏覽器', select_features: '選擇特性',