├── .browserslistrc ├── .eslintrc.js ├── .fatherrc.ts ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── lerna.json ├── package.json ├── packages ├── audio │ ├── README.md │ ├── package.json │ ├── src │ │ ├── component │ │ │ ├── index.css │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── locales │ │ │ ├── en-US.ts │ │ │ ├── index.ts │ │ │ └── zh-CN.ts │ │ └── uploader.ts │ └── tsconfig.json ├── codeblock │ ├── .babelrc │ ├── .fatherrc.ts │ ├── README.md │ ├── package.json │ ├── src │ │ ├── component │ │ │ ├── editor.ts │ │ │ ├── index.css │ │ │ ├── index.ts │ │ │ ├── lang.ts │ │ │ ├── mode.ts │ │ │ ├── select │ │ │ │ ├── component.vue │ │ │ │ └── index.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── locales │ │ │ ├── en-US.ts │ │ │ ├── index.ts │ │ │ └── zh-CN.ts │ │ ├── shims-vue.d.ts │ │ └── types.ts │ └── tsconfig.json ├── link │ ├── .babelrc │ ├── .fatherrc.ts │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.css │ │ ├── index.ts │ │ ├── locales │ │ │ ├── en-US.ts │ │ │ ├── index.ts │ │ │ └── zh-CN.ts │ │ ├── shims-vue.d.ts │ │ └── toolbar │ │ │ ├── editor.vue │ │ │ ├── index.ts │ │ │ └── preview.vue │ └── tsconfig.json ├── map │ ├── README.md │ ├── package.json │ ├── src │ │ ├── component │ │ │ ├── index.less │ │ │ └── index.ts │ │ ├── index.ts │ │ └── locales │ │ │ ├── en-US.ts │ │ │ ├── index.ts │ │ │ └── zh-CN.ts │ └── tsconfig.json └── toolbar │ ├── .babelrc │ ├── .fatherrc.ts │ ├── README.md │ ├── package.json │ ├── src │ ├── components │ │ ├── button.vue │ │ ├── collapse │ │ │ ├── collapse.vue │ │ │ ├── group.vue │ │ │ └── item.vue │ │ ├── color │ │ │ ├── color.vue │ │ │ └── picker │ │ │ │ ├── group.vue │ │ │ │ ├── item.vue │ │ │ │ ├── palette.ts │ │ │ │ └── picker.vue │ │ ├── dropdown-list.vue │ │ ├── dropdown.vue │ │ ├── group.vue │ │ ├── table.vue │ │ └── toolbar.vue │ ├── config │ │ ├── fontfamily.ts │ │ ├── index.css │ │ └── index.ts │ ├── index.ts │ ├── locales │ │ ├── en-US.ts │ │ ├── index.ts │ │ └── zh-CN.ts │ ├── plugin │ │ ├── component │ │ │ ├── collapse.ts │ │ │ ├── index.css │ │ │ ├── index.ts │ │ │ └── popup.ts │ │ └── index.ts │ ├── shims-vue.d.ts │ ├── types.ts │ └── utils.ts │ └── tsconfig.json ├── public ├── favicon.ico └── index.html ├── scripts ├── build.js ├── rollup-build.js └── rollup.js ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ ├── Editor.vue │ ├── Loading.vue │ ├── Map │ │ ├── Content.vue │ │ └── Modal.vue │ ├── MentionHover.vue │ └── config.ts ├── main.ts ├── router │ ├── index.js │ └── index.ts ├── shims-tsx.d.ts ├── shims-vue.d.ts ├── store │ └── index.ts ├── utils │ └── index.ts └── views │ ├── About.vue │ └── Home.vue ├── tsconfig.json └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | "plugin:vue/essential", 8 | "eslint:recommended", 9 | "@vue/typescript/recommended", 10 | "@vue/prettier", 11 | "@vue/prettier/@typescript-eslint", 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 2020, 15 | }, 16 | rules: { 17 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 18 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 19 | }, 20 | globals: { 21 | "NodeJS": true 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | esm: 'rollup', 3 | cjs: 'rollup', 4 | runtimeHelpers: true, 5 | extraBabelPlugins:[ 6 | ['babel-plugin-import', { 7 | libraryName: 'ant-design-vue', 8 | libraryDirectory: 'es', 9 | style: true, 10 | }], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | lerna-debug.log* 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | *.lock 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-present AoMao (me@yanmao.cc) 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # am-editor-vue2 2 | 3 | [AM-EDITOR](https://github.com/yanmao-cc/am-editor) VUE2案例及部分插件 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | yarn 10 | 11 | lerna bootstrap 12 | 13 | # serve with hot reload at localhost:8080 14 | yarn serve 15 | 16 | # build for production with minification 17 | yarn build 18 | 19 | # build for production width docs 20 | yarn docs:build 21 | ``` -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | plugins: [ 4 | ["import", { "libraryName": "ant-design-vue", "libraryDirectory": "es", "style": "css" }] // `style: true` 会加载 less 文件 5 | ] 6 | }; 7 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "independent", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "ignoreChanges": [ 6 | "**/*.md", 7 | "**/*.test.ts", 8 | "**/*.e2e.ts", 9 | "**/fixtures/**", 10 | "**/test/**" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "am-editor-vue2", 3 | "version": "1.0.0", 4 | "keyword": "editor-vue2", 5 | "description": "A Vue.js project", 6 | "author": "张彬 ", 7 | "private": true, 8 | "license": "MIT", 9 | "workspaces": [ 10 | "packages/*" 11 | ], 12 | "scripts": { 13 | "serve": "vue-cli-service serve", 14 | "docs:build": "vue-cli-service build", 15 | "build": "node ./scripts/build", 16 | "lerna-publish": "yarn build & lerna publish", 17 | "lint": "vue-cli-service lint" 18 | }, 19 | "dependencies": { 20 | "@aomao/engine": "^2.9", 21 | "@aomao/plugin-alignment": "^2.9", 22 | "@aomao/plugin-backcolor": "^2.9", 23 | "@aomao/plugin-bold": "^2.9", 24 | "@aomao/plugin-code": "^2.9", 25 | "@aomao/plugin-file": "^2.9", 26 | "@aomao/plugin-fontcolor": "^2.9", 27 | "@aomao/plugin-fontfamily": "^2.9", 28 | "@aomao/plugin-fontsize": "^2.9", 29 | "@aomao/plugin-heading": "^2.9", 30 | "@aomao/plugin-hr": "^2.9", 31 | "@aomao/plugin-image": "^2.9", 32 | "@aomao/plugin-indent": "^2.9", 33 | "@aomao/plugin-italic": "^2.9", 34 | "@aomao/plugin-line-height": "^2.9", 35 | "@aomao/plugin-mark-range": "^2.9", 36 | "@aomao/plugin-math": "^2.9", 37 | "@aomao/plugin-mention": "^2.9", 38 | "@aomao/plugin-orderedlist": "^2.9", 39 | "@aomao/plugin-paintformat": "^2.9", 40 | "@aomao/plugin-quote": "^2.9", 41 | "@aomao/plugin-redo": "^2.9", 42 | "@aomao/plugin-removeformat": "^2.9", 43 | "@aomao/plugin-selectall": "^2.9", 44 | "@aomao/plugin-status": "^2.9", 45 | "@aomao/plugin-strikethrough": "^2.9", 46 | "@aomao/plugin-sub": "^2.9", 47 | "@aomao/plugin-sup": "^2.9", 48 | "@aomao/plugin-table": "^2.9", 49 | "@aomao/plugin-tasklist": "^2.9", 50 | "@aomao/plugin-underline": "^2.9", 51 | "@aomao/plugin-undo": "^2.9", 52 | "@aomao/plugin-unorderedlist": "^2.9", 53 | "@aomao/plugin-video": "^2.9", 54 | "ant-design-vue": "^1.7.8", 55 | "codemirror": "^5.63.3", 56 | "core-js": "^3.6.5", 57 | "vue": "^2.6.11", 58 | "vue-class-component": "^7.2.3", 59 | "vue-property-decorator": "^9.1.2", 60 | "vue-router": "^3.2.0", 61 | "vuex": "^3.4.0" 62 | }, 63 | "devDependencies": { 64 | "@typescript-eslint/eslint-plugin": "^4.18.0", 65 | "@typescript-eslint/parser": "^4.18.0", 66 | "@vue/cli-plugin-babel": "~4.5.0", 67 | "@vue/cli-plugin-eslint": "~4.5.0", 68 | "@vue/cli-plugin-router": "~4.5.0", 69 | "@vue/cli-plugin-typescript": "~4.5.0", 70 | "@vue/cli-plugin-vuex": "~4.5.0", 71 | "@vue/cli-service": "~4.5.0", 72 | "@vue/compiler-sfc": "^3.2.20", 73 | "@vue/eslint-config-prettier": "^6.0.0", 74 | "@vue/eslint-config-typescript": "^7.0.0", 75 | "babel-loader": "^8.2.2", 76 | "babel-plugin-import": "^1.13.3", 77 | "eslint": "^6.7.2", 78 | "eslint-plugin-prettier": "^3.3.1", 79 | "eslint-plugin-vue": "^6.2.2", 80 | "father-build": "^1.20.1", 81 | "lerna": "^4.0.0", 82 | "less": "^3.0.4", 83 | "less-loader": "^5.0.0", 84 | "lint-staged": "^11.2.3", 85 | "prettier": "^2.2.1", 86 | "rollup-plugin-typescript2": "^0.30.0", 87 | "rollup-plugin-vue": "^5.0.0", 88 | "typescript": "~4.1.5", 89 | "vue-template-compiler": "^2.6.11" 90 | }, 91 | "eslintConfig": { 92 | "root": true, 93 | "env": { 94 | "node": true 95 | }, 96 | "extends": [ 97 | "plugin:vue/essential", 98 | "eslint:recommended", 99 | "@vue/typescript/recommended", 100 | "@vue/prettier", 101 | "@vue/prettier/@typescript-eslint" 102 | ], 103 | "parserOptions": { 104 | "ecmaVersion": 2020 105 | }, 106 | "rules": {} 107 | }, 108 | "gitHooks": { 109 | "pre-commit": "lint-staged" 110 | }, 111 | "lint-staged": { 112 | "src/**/*.{js,jsx,vue,ts,tsx}": [ 113 | "vue-cli-service lint", 114 | "git add" 115 | ] 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /packages/audio/README.md: -------------------------------------------------------------------------------- 1 | # @aomao/plugin-video 2 | 3 | 音频插件 4 | 5 | ## 安装 6 | 7 | ```bash 8 | $ yarn add @aomao/plugin-video 9 | ``` 10 | 11 | 添加到引擎 12 | 13 | ```ts 14 | import Engine, { EngineInterface } from '@aomao/engine'; 15 | import Video , { VideoComponent , VideoUploader } from '@aomao/plugin-video'; 16 | 17 | new Engine(...,{ plugins:[ Video , VideoUploader ] , cards:[ VideoComponent ]}) 18 | ``` 19 | 20 | `VideoUploader` 插件主要功能:选择音频文件、上传音频文件 21 | 22 | ## `Video` 可选项 23 | 24 | `onBeforeRender` 设置音频地址前可或者下载音频时可对地址修改。另外还可以对音频的主图修改地址。 25 | 26 | ```ts 27 | onBeforeRender?: (action: 'download' | 'query' | 'cover', url: string) => string; 28 | ``` 29 | 30 | ## `VideoUploader` 可选项 31 | 32 | ```ts 33 | //使用配置 34 | new Engine(...,{ 35 | config:{ 36 | [VideoUploader.pluginName]:{ 37 | //...相关配置 38 | } 39 | } 40 | }) 41 | ``` 42 | 43 | ### 文件上传 44 | 45 | `action`: 上传地址,始终使用 `POST` 请求 46 | 47 | `crossOrigin`: 是否跨域 48 | 49 | `headers`: 请求头 50 | 51 | `contentType`: 文件上传默认以 `multipart/form-data;` 类型上传 52 | 53 | `accept`: 限制用户文件选择框选择的文件类型,默认 `mp4` 格式 54 | 55 | `limitSize`: 限制用户选择的文件大小,超过限制将不请求上传。默认:`1024 * 1024 * 5` 5M 56 | 57 | `multiple`: `false` 一次只能上传一个文件,`true` 默认一次最多 100 个文件。可以指定具体数量,但是文件选择框无法限制,只能上传的时候限制上传最前面的张数 58 | 59 | `data`: 文件上传时同时将这些数据一起`POST`到服务端 60 | 61 | `name`: 文件上传请求时,请求参数在 `FormData` 中的名称,默认 `file` 62 | 63 | ```ts 64 | /** 65 | * 文件上传地址 66 | */ 67 | action:string 68 | /** 69 | * 是否跨域 70 | */ 71 | crossOrigin?: boolean; 72 | /** 73 | * 请求头 74 | */ 75 | headers?: { [key: string]: string } | (() => { [key: string]: string }); 76 | /** 77 | * 数据返回类型,默认 json 78 | */ 79 | type?: '*' | 'json' | 'xml' | 'html' | 'text' | 'js'; 80 | /** 81 | * 音频文件上传时 FormData 的名称,默认 file 82 | */ 83 | name?: string 84 | /** 85 | * 额外携带数据上传 86 | */ 87 | data?: {}; 88 | /** 89 | * 请求类型,默认 multipart/form-data; 90 | */ 91 | contentType?:string 92 | /** 93 | * 文件接收的格式,默认 "*" 所有的 94 | */ 95 | accept?: string | Array; 96 | /** 97 | * 文件选择限制数量 98 | */ 99 | multiple?: boolean | number; 100 | /** 101 | * 上传大小限制,默认 1024 * 1024 * 5 就是5M 102 | */ 103 | limitSize?: number; 104 | 105 | ``` 106 | 107 | ### 查询音频信息 108 | 109 | 在对音频有播放权限或限制、对其它无法使用 html5 直接播放需要转码后才能播放的音频文件、需要对音频进行其它处理的音频文件都可能需要这个配置 110 | 111 | 以上的音频文件上传处理流程: 112 | 113 | - 选择文件上传后需要返回 `status` 字段并标明值为 `transcoding`,并且需要返回这个音频文件在服务端的唯一标识 `id` ,这个标识能够在后续查询中辨别这个音频文件以或得音频文件处理信息,否则一律视为 `done` 直接传输给 video 标签播放 114 | - 插件获取到 `status` 字段值为 `transcoding` 时,会展示等待 `转码中...` 信息,并且每 3 秒通过 `id` 参数调用查询接口获取音频文件处理状态,直到 `status` 的值不为 `transcoding` 时终止轮询 115 | 116 | 除此之外,在有配置 `查询音频信息接口` 后,每次展示音频时都会调用 `查询音频信息接口` 查询一次,接口返回的结果将作为展示音频信息的参数 117 | 118 | ```ts 119 | /** 120 | * 查询音频信息 121 | */ 122 | query?: { 123 | /** 124 | * 查询地址 125 | */ 126 | action: string; 127 | /** 128 | * 数据返回类型,默认 json 129 | */ 130 | type?: '*' | 'json' | 'xml' | 'html' | 'text' | 'js'; 131 | /** 132 | * 额外携带数据上传 133 | */ 134 | data?: {}; 135 | /** 136 | * 请求类型,默认 multipart/form-data; 137 | */ 138 | contentType?: string; 139 | } 140 | ``` 141 | 142 | ### 解析服务端响应数据 143 | 144 | 默认会查找 145 | 146 | 音频文件地址:response.url || response.data && response.data.url 一般为可播放的 mp4 地址 147 | 音频文件标识:response.id || response.data && response.data.id 可选参数,配置了`音频查询接口`必须 148 | 音频文件封面图片地址:response.cover || response.cover && response.data.cover 可选参数 149 | 音频文件处理状态:response.status || response.status && response.data.status 可选参数,配置了`音频查询接口`必须,否则一律视为 `done` 150 | 音频下载地址:response.download || response.data && response.data.download 音频文件的下载地址,可以加权限、时间限制等等,如果有可以返回地址 151 | 152 | `result`: true 上传成功,data 为音频信息。false 上传失败,data 为错误消息 153 | 154 | ```ts 155 | /** 156 | * 解析上传后的Respone,返回 result:是否成功,data:成功:音频信息,失败:错误信息 157 | */ 158 | parse?: ( 159 | response: any, 160 | ) => { 161 | result: boolean; 162 | data: { 163 | url: string, 164 | id?: string, 165 | cover?: string 166 | status?: string 167 | } | string; 168 | }; 169 | ``` 170 | 171 | ## 命令 172 | 173 | ### `Video` 插件命令 174 | 175 | 插入一个文件 176 | 177 | 参数 1:文件状态`uploading` | `done` | `transcoding` | `error` 上传中、上传完成、转码中、上传错误 178 | 179 | 参数 2:在状态非 `error` 下,为展示文件,否则展示错误消息 180 | 181 | ```ts 182 | //'uploading' | 'done' | `transcoding` | 'error' 183 | engine.command.execute( 184 | Video.pluginName, 185 | 'done', 186 | '音频地址', 187 | '音频名称', //可选,默认为音频地址 188 | '音频标识', //可选,配置了 查询音频信息接口 必须 189 | '音频封面', //可选 190 | '音频大小', //可选 191 | '下载地址', //可选 192 | ); 193 | ``` 194 | 195 | ### `VideoUploader` 插件命令 196 | 197 | 弹出文件选择框,并执行上传 198 | 199 | 可选参数 1: 传入文件列表,将上传这些文件。否则弹出文件选择框并,选择文件后执行上传。或者传入 `query` 命令,查询音频文件状态 200 | 可选参数 2: 查询音频文件信息状态的参数,0.音频文件标识,1.成功处理后的回调,2.失败处理后的回调 201 | 202 | ```ts 203 | //方法签名 204 | async execute(files?: Array | MouseEvent | string,...args:any):void 205 | //执行命令 206 | engine.command.execute(VideoUploader.pluginName,file); 207 | //查询 208 | engine.command.execute(VideoUploader.pluginName,"query","标识",success: (data?:{ url: string, name?: string, cover?: string, download?: string, status?: string }) => void, failed: (message: string) => void = () => {}); 209 | ``` 210 | -------------------------------------------------------------------------------- /packages/audio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "am-editor-audio", 3 | "version": "1.1.12", 4 | "main": "dist/index.js", 5 | "module": "dist/index.esm.js", 6 | "typings": "dist/index.d.ts", 7 | "files": [ 8 | "dist", 9 | "lib", 10 | "src" 11 | ], 12 | "author": "me@yanmao.cc", 13 | "license": "MIT", 14 | "homepage": "https://github.com/yanmao-cc/am-editor#readme", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/yanmao-cc/am-editor.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/yanmao-cc/am-editor/issues" 21 | }, 22 | "dependencies": { 23 | "@babel/runtime": "^7.13.10" 24 | }, 25 | "peerDependencies": { 26 | "@aomao/engine": "^2.9" 27 | }, 28 | "gitHead": "898fe923d8a408cf59ae2e92a67b0b5aefc44378" 29 | } 30 | -------------------------------------------------------------------------------- /packages/audio/src/component/index.css: -------------------------------------------------------------------------------- 1 | [data-card-key="audio"] { 2 | outline: 1px solid #ddd; 3 | border-radius: 54px; 4 | } 5 | .data-audio-content { 6 | position: relative; 7 | height: 54px; 8 | background: #f7f7f7; 9 | } 10 | .data-audio-content audio { 11 | width: 100%; 12 | outline: none; 13 | } 14 | .data-audio-uploading, 15 | .data-audio-uploaded, 16 | .data-audio-error { 17 | border: 1px solid #e6e6e6; 18 | border-radius: 54px; 19 | background: #f6f6f6; 20 | } 21 | .data-audio-done { 22 | height: auto; 23 | border: none; 24 | background: none; 25 | line-height: 0; 26 | } 27 | .data-audio-active { 28 | outline: 1px solid #d9d9d9; 29 | border-radius: 54px; 30 | } 31 | .data-audio-center { 32 | position: absolute; 33 | top: 50%; 34 | margin-top: -48px; 35 | width: 100%; 36 | height: 96px; 37 | } 38 | .data-audio-center .data-audio-icon, 39 | .data-audio-center .data-audio-name, 40 | .data-audio-center .data-audio-message, 41 | .data-audio-center .data-audio-progress, 42 | .data-audio-center .data-audio-transcoding { 43 | text-align: center; 44 | } 45 | .data-audio-center .data-audio-icon { 46 | font-size: 24px; 47 | color: #BFBFBF; 48 | margin-bottom: 12px; 49 | } 50 | .data-audio-center .data-audio-name { 51 | color: #595959; 52 | margin-bottom: 12px; 53 | } 54 | .data-audio-center .data-audio-message { 55 | color: #595959; 56 | } 57 | .data-audio-center .data-audio-anticon { 58 | display: inline-block; 59 | font-style: normal; 60 | vertical-align: -0.125em; 61 | text-align: center; 62 | text-transform: none; 63 | line-height: 0; 64 | text-rendering: optimizeLegibility; 65 | -webkit-font-smoothing: antialiased; 66 | margin-right: 5px; 67 | } 68 | .data-audio-center .data-audio-anticon .data-audio-anticon-spin { 69 | display: inline-block; 70 | -webkit-animation: loadingCircle 1s infinite linear; 71 | animation: loadingCircle 1s infinite linear; 72 | } 73 | .data-audio-center .data-error-icon { 74 | width: 16px; 75 | height: 16px; 76 | display: inline-block; 77 | background: #F5222D; 78 | text-align: center; 79 | font-size: 12px; 80 | color: #ffffff; 81 | padding: 1px 0 0 0; 82 | line-height: 16px; 83 | border-radius: 100%; 84 | vertical-align: middle; 85 | margin: -2px 5px 0 0; 86 | } -------------------------------------------------------------------------------- /packages/audio/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | $, 3 | CardEntry, 4 | CardInterface, 5 | CARD_KEY, 6 | decodeCardValue, 7 | encodeCardValue, 8 | isEngine, 9 | NodeInterface, 10 | Plugin, 11 | PluginEntry, 12 | PluginOptions, 13 | sanitizeUrl, 14 | SchemaInterface, 15 | } from '@aomao/engine'; 16 | import AudioComponent, { AudioValue } from './component'; 17 | import AudioUploader from './uploader'; 18 | import locales from './locales'; 19 | 20 | export interface AudioOptions extends PluginOptions { 21 | onBeforeRender?: ( 22 | action: 'download' | 'query' | 'cover', 23 | url: string, 24 | ) => string; 25 | } 26 | export default class AudioPlugin extends Plugin { 27 | static get pluginName() { 28 | return 'audio'; 29 | } 30 | 31 | init() { 32 | this.editor.language.add(locales); 33 | if (!isEngine(this.editor)) return; 34 | this.editor.on('parse:html', (node) => this.parseHtml(node)); 35 | this.editor.on('paste:each', (child) => this.pasteHtml(child)); 36 | this.editor.on('paste:schema', (schema: SchemaInterface) => 37 | this.pasteSchema(schema), 38 | ); 39 | } 40 | 41 | execute( 42 | status: 'uploading' | 'transcoding' | 'done' | 'error', 43 | url: string, 44 | name?: string, 45 | audio_id?: string, 46 | size?: number, 47 | download?: string, 48 | ): void { 49 | const value: AudioValue = { 50 | status, 51 | audio_id, 52 | url, 53 | name: name || url, 54 | size, 55 | download, 56 | }; 57 | if (status === 'error') { 58 | value.url = ''; 59 | value.message = url; 60 | } 61 | this.editor.card.insert('audio', value); 62 | } 63 | 64 | async waiting( 65 | callback?: ( 66 | name: string, 67 | card?: CardInterface, 68 | ...args: any 69 | ) => boolean | number | void, 70 | ): Promise { 71 | const { card } = this.editor; 72 | // 检测单个组件 73 | const check = (component: CardInterface) => { 74 | return ( 75 | component.root.inEditor() && 76 | (component.constructor as CardEntry).cardName === 77 | AudioComponent.cardName && 78 | (component as AudioComponent).getValue()?.status === 'uploading' 79 | ); 80 | }; 81 | // 找到不合格的组件 82 | const find = (): CardInterface | undefined => { 83 | return card.components.find(check); 84 | }; 85 | const waitCheck = (component: CardInterface): Promise => { 86 | let time = 60000; 87 | return new Promise((resolve, reject) => { 88 | if (callback) { 89 | const result = callback( 90 | (this.constructor as PluginEntry).pluginName, 91 | component, 92 | ); 93 | if (result === false) { 94 | return reject({ 95 | name: (this.constructor as PluginEntry).pluginName, 96 | card: component, 97 | }); 98 | } else if (typeof result === 'number') { 99 | time = result; 100 | } 101 | } 102 | const beginTime = new Date().getTime(); 103 | const now = new Date().getTime(); 104 | const timeout = () => { 105 | if (now - beginTime >= time) return resolve(); 106 | setTimeout(() => { 107 | if (check(component)) timeout(); 108 | else resolve(); 109 | }, 10); 110 | }; 111 | timeout(); 112 | }); 113 | }; 114 | return new Promise((resolve, reject) => { 115 | const component = find(); 116 | const wait = (component: CardInterface) => { 117 | waitCheck(component) 118 | .then(() => { 119 | const next = find(); 120 | if (next) wait(next); 121 | else resolve(); 122 | }) 123 | .catch(reject); 124 | }; 125 | if (component) wait(component); 126 | else resolve(); 127 | }); 128 | } 129 | 130 | pasteSchema(schema: SchemaInterface) { 131 | schema.add({ 132 | type: 'block', 133 | name: 'div', 134 | attributes: { 135 | 'data-value': '*', 136 | 'data-type': { 137 | required: true, 138 | value: AudioComponent.cardName, 139 | }, 140 | }, 141 | }); 142 | } 143 | 144 | pasteHtml(node: NodeInterface) { 145 | if (!isEngine(this.editor)) return; 146 | if (node.isElement()) { 147 | const type = node.attributes('data-type'); 148 | if (type === AudioComponent.cardName) { 149 | const value = node.attributes('data-value'); 150 | const cardValue = decodeCardValue(value) as AudioValue; 151 | if (!cardValue.url) return; 152 | this.editor.card.replaceNode( 153 | node, 154 | AudioComponent.cardName, 155 | cardValue, 156 | ); 157 | node.remove(); 158 | return false; 159 | } 160 | } 161 | return true; 162 | } 163 | 164 | parseHtml(root: NodeInterface) { 165 | root.find(`[${CARD_KEY}=${AudioComponent.cardName}`).each( 166 | (cardNode) => { 167 | const node = $(cardNode); 168 | const card = this.editor.card.find(node); 169 | const value = card?.getValue(); 170 | if (value?.url && value.status === 'done') { 171 | const { onBeforeRender } = this.options; 172 | const { url } = value; 173 | const html = `
`; 180 | node.empty(); 181 | node.replaceWith($(html)); 182 | } else node.remove(); 183 | }, 184 | ); 185 | } 186 | } 187 | 188 | export { AudioComponent, AudioUploader }; 189 | -------------------------------------------------------------------------------- /packages/audio/src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | audio: { 3 | errorMessageCopy: 'Copy error message', 4 | loadError: 'The audio failed to load!', 5 | uploadError: 'The audio failed to upload!', 6 | uploadLimitError: 'Upload audio size is limited to $size', 7 | download: 'Download', 8 | preview: 'Preview', 9 | loading: 'Loading...', 10 | transcoding: 'Transcoding...', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/audio/src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import en from './en-US'; 2 | import cn from './zh-CN'; 3 | 4 | export default { 5 | 'en-US': en, 6 | 'zh-CN': cn, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/audio/src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | audio: { 3 | errorMessageCopy: '复制错误信息', 4 | loadError: '音频加载失败!', 5 | uploadError: '上传音频失败!', 6 | uploadLimitError: '上传音频大小限制为 $size', 7 | download: '下载', 8 | preview: '预览', 9 | loading: '加载中...', 10 | transcoding: '转码中...', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/audio/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "react", 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "experimentalDecorators": true, 11 | "noImplicitReturns": true, 12 | "declaration": true, 13 | "suppressImplicitAnyIndexErrors": true, 14 | "esModuleInterop": true, 15 | "sourceMap": true, 16 | "baseUrl": "./", 17 | "strict": true, 18 | "paths": { 19 | "@/*": ["src/*"] 20 | }, 21 | "allowSyntheticDefaultImports": true 22 | }, 23 | "exclude": [ 24 | "node_modules", 25 | "lib", 26 | "es", 27 | "dist", 28 | "docs-dist", 29 | "typings", 30 | "**/__test__", 31 | "test", 32 | "fixtures" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/codeblock/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["import", { "libraryName": "ant-design-vue", "libraryDirectory": "es", "style": "css" }] // `style: true` 会加载 less 文件 4 | ] 5 | } -------------------------------------------------------------------------------- /packages/codeblock/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | 3 | export default { 4 | extraRollupPlugins: [ 5 | commonjs(), 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /packages/codeblock/README.md: -------------------------------------------------------------------------------- 1 | # am-editor-codeblock-vue2 2 | 3 | 代码块插件 4 | 5 | ## 安装 6 | 7 | ```bash 8 | $ yarn add am-editor-codeblock-vue2 9 | ``` 10 | 11 | 添加到引擎 12 | 13 | ```ts 14 | import Engine, { EngineInterface } from '@aomao/engine'; 15 | import CodeBlock , { CodeBlockComponent } from '@aomao/plugin-codeblock'; 16 | 17 | new Engine(...,{ plugins:[CodeBlock] , cards:[CodeBlockComponent]}) 18 | ``` 19 | 20 | ## 可选项 21 | 22 | ### 快捷键 23 | 24 | 默认无快捷键 25 | 26 | ```ts 27 | //快捷键,key 组合键,args,执行参数,[mode?: string, value?: string] 语言模式:可选,代码文本:可选 28 | hotkey?:string | {key:string,args:Array};//默认无 29 | 30 | //使用配置 31 | new Engine(...,{ 32 | config:{ 33 | "codeblock":{ 34 | //修改快捷键 35 | hotkey:{ 36 | key:"mod+b", 37 | args:["javascript","const test = 123;"] 38 | } 39 | } 40 | } 41 | }) 42 | ``` 43 | 44 | ### Markdown 45 | 46 | 默认支持 markdown,传入`false`关闭 47 | 48 | CodeBlock 插件 markdown 语法为` ``` ` 49 | 50 | ```ts 51 | markdown?: boolean;//默认开启,false 关闭 52 | //使用配置 53 | new Engine(...,{ 54 | config:{ 55 | "codeblock":{ 56 | //关闭markdown 57 | markdown:false 58 | } 59 | } 60 | }) 61 | ``` 62 | 63 | ## 命令 64 | 65 | ```ts 66 | //可携带两个参数,语言类型,默认文本,都是可选的 67 | engine.command.execute('codeblock', 'javascript', 'const test = 123;'); 68 | ``` 69 | -------------------------------------------------------------------------------- /packages/codeblock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "am-editor-codeblock-vue2", 3 | "version": "1.1.17", 4 | "keyword": "am-editor-codeblock-vue2", 5 | "description": "> TODO: description", 6 | "author": "zhangbin yanmao ", 7 | "homepage": "https://github.com/zb201307/am-editor-vue2#readme", 8 | "license": "MIT", 9 | "main": "dist/index.js", 10 | "module": "dist/index.esm.js", 11 | "typings": "dist/index.d.ts", 12 | "private": false, 13 | "files": [ 14 | "dist", 15 | "lib", 16 | "src" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/zb201307/am-editor-vue2.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/zb201307/am-editor-vue2/issues" 24 | }, 25 | "dependencies": { 26 | "@babel/runtime": "^7.13.10", 27 | "codemirror": "^5.63.3", 28 | "lodash": "^4.17.21" 29 | }, 30 | "devDependencies": { 31 | "@types/codemirror": "^5.60.5", 32 | "@types/lodash": "^4.14.178", 33 | "@types/markdown-it": "^12.2.3" 34 | }, 35 | "peerDependencies": { 36 | "@aomao/engine": "^2.9", 37 | "ant-design-vue": "^1.7.8", 38 | "vue": "^2.6.14" 39 | }, 40 | "gitHead": "bc52c5d6caf2d168a714f12d1cbbdea28d376ac0" 41 | } 42 | -------------------------------------------------------------------------------- /packages/codeblock/src/component/lang.ts: -------------------------------------------------------------------------------- 1 | import 'codemirror/mode/shell/shell'; 2 | import 'codemirror/mode/clike/clike'; 3 | import 'codemirror/mode/css/css'; 4 | import 'codemirror/mode/dart/dart'; 5 | import 'codemirror/mode/diff/diff'; 6 | import 'codemirror/mode/dockerfile/dockerfile'; 7 | import 'codemirror/mode/erlang/erlang'; 8 | import 'codemirror/mode/go/go'; 9 | import 'codemirror/mode/groovy/groovy'; 10 | import 'codemirror/mode/htmlmixed/htmlmixed'; 11 | import 'codemirror/mode/http/http'; 12 | import 'codemirror/mode/javascript/javascript'; 13 | import 'codemirror/mode/jsx/jsx'; 14 | import 'codemirror/mode/cmake/cmake'; 15 | import 'codemirror/mode/markdown/markdown'; 16 | import 'codemirror/mode/octave/octave'; 17 | import 'codemirror/mode/nginx/nginx'; 18 | import 'codemirror/mode/pascal/pascal'; 19 | import 'codemirror/mode/perl/perl'; 20 | import 'codemirror/mode/php/php'; 21 | import 'codemirror/mode/powershell/powershell'; 22 | import 'codemirror/mode/protobuf/protobuf'; 23 | import 'codemirror/mode/python/python'; 24 | import 'codemirror/mode/r/r'; 25 | import 'codemirror/mode/ruby/ruby'; 26 | import 'codemirror/mode/rust/rust'; 27 | import 'codemirror/mode/sql/sql'; 28 | import 'codemirror/mode/swift/swift'; 29 | import 'codemirror/mode/vb/vb'; 30 | import 'codemirror/mode/velocity/velocity'; 31 | import 'codemirror/mode/xml/xml'; 32 | import 'codemirror/mode/yaml/yaml'; 33 | import 'codemirror/addon/scroll/simplescrollbars'; 34 | import 'codemirror/addon/scroll/simplescrollbars.css'; 35 | -------------------------------------------------------------------------------- /packages/codeblock/src/component/mode.ts: -------------------------------------------------------------------------------- 1 | import "./lang"; 2 | 3 | const datas = [ 4 | { 5 | value: "plain", 6 | syntax: "simplemode", 7 | name: "Plain Text", 8 | }, 9 | { 10 | value: "bash", 11 | syntax: "shell", 12 | name: "Bash", 13 | }, 14 | { 15 | value: "basic", 16 | syntax: "vbscript", 17 | name: "Basic", 18 | }, 19 | { 20 | value: "c", 21 | syntax: "text/x-csrc", 22 | name: "C", 23 | }, // text/x-csrc 24 | { 25 | value: "cpp", 26 | syntax: "text/x-c++src", 27 | alias: ["c++"], 28 | name: "C++", 29 | }, 30 | { 31 | value: "csharp", 32 | syntax: "text/x-csharp", 33 | alias: ["c#"], 34 | name: "C#", 35 | }, 36 | { 37 | value: "css", 38 | syntax: "css", 39 | name: "CSS", 40 | }, 41 | { 42 | value: "dart", 43 | syntax: "dart", 44 | name: "Dart", 45 | }, 46 | { 47 | value: "diff", 48 | syntax: "diff", 49 | name: "Diff", 50 | }, 51 | { 52 | value: "dockerfile", 53 | syntax: "dockerfile", 54 | name: "Dockerfile", 55 | }, 56 | { 57 | value: "erlang", 58 | syntax: "erlang", 59 | name: "Erlang", 60 | }, 61 | { 62 | value: "git", 63 | syntax: "shell", 64 | name: "Git", 65 | }, 66 | { 67 | value: "go", 68 | syntax: "go", 69 | alias: ["golang"], 70 | name: "Go", 71 | }, 72 | { 73 | value: "graphql", 74 | syntax: "simplemode", 75 | name: "GraphQL", 76 | }, 77 | { 78 | value: "groovy", 79 | syntax: "groovy", 80 | name: "Groovy", 81 | }, 82 | { 83 | value: "html", 84 | syntax: "htmlmixed", 85 | name: "HTML", 86 | alias: ["html5"], 87 | }, 88 | { 89 | value: "http", 90 | syntax: "http", 91 | name: "HTTP", 92 | }, 93 | { 94 | value: "java", 95 | syntax: "text/x-java", 96 | name: "Java", 97 | }, 98 | { 99 | value: "javascript", 100 | syntax: "text/javascript", 101 | name: "JavaScript", 102 | alias: ["js"], 103 | }, 104 | { 105 | value: "json", 106 | syntax: "application/json", 107 | name: "JSON", 108 | }, 109 | { 110 | value: "jsx", 111 | syntax: "jsx", 112 | name: "JSX", 113 | }, 114 | { 115 | value: "katex", 116 | syntax: "simplemode", 117 | name: "KaTeX", 118 | }, 119 | { 120 | value: "kotlin", 121 | syntax: "text/x-kotlin", 122 | name: "Kotlin", 123 | }, 124 | { 125 | value: "less", 126 | syntax: "css", 127 | name: "Less", 128 | }, 129 | { 130 | value: "makefile", 131 | syntax: "cmake", 132 | name: "Makefile", 133 | }, 134 | { 135 | value: "markdown", 136 | syntax: "markdown", 137 | name: "Markdown", 138 | }, 139 | { 140 | value: "matlab", 141 | syntax: "octave", 142 | name: "MATLAB", 143 | }, 144 | { 145 | value: "nginx", 146 | syntax: "nginx", 147 | name: "Nginx", 148 | }, 149 | { 150 | value: "objectivec", 151 | syntax: "text/x-objectivec", 152 | name: "Objective-C", 153 | }, 154 | { 155 | value: "pascal", 156 | syntax: "pascal", 157 | name: "Pascal", 158 | }, 159 | { 160 | value: "perl", 161 | syntax: "perl", 162 | name: "Perl", 163 | }, 164 | { 165 | value: "php", 166 | syntax: "php", 167 | name: "PHP", 168 | }, 169 | { 170 | value: "powershell", 171 | syntax: "powershell", 172 | name: "PowerShell", 173 | }, 174 | { 175 | value: "protobuf", 176 | syntax: "protobuf", 177 | name: "Protobuf", 178 | }, 179 | { 180 | value: "python", 181 | syntax: "python", 182 | name: "Python", 183 | alias: ["py"], 184 | }, 185 | { 186 | value: "r", 187 | syntax: "r", 188 | name: "R", 189 | }, 190 | { 191 | value: "ruby", 192 | syntax: "ruby", 193 | name: "Ruby", 194 | }, 195 | { 196 | value: "rust", 197 | syntax: "rust", 198 | name: "Rust", 199 | }, 200 | { 201 | value: "scala", 202 | syntax: "text/x-scala", 203 | name: "Scala", 204 | }, 205 | { 206 | value: "shell", 207 | syntax: "shell", 208 | name: "Shell", 209 | }, 210 | { 211 | value: "sql", 212 | syntax: "text/x-sql", 213 | name: "SQL", 214 | }, 215 | { 216 | value: "swift", 217 | syntax: "swift", 218 | name: "Swift", 219 | }, 220 | { 221 | value: "typescript", 222 | syntax: "text/typescript", 223 | name: "TypeScript", 224 | alias: ["ts"], 225 | }, 226 | { 227 | value: "vbnet", 228 | syntax: "vb", 229 | name: "VB.net", 230 | }, 231 | { 232 | value: "velocity", 233 | syntax: "velocity", 234 | name: "Velocity", 235 | }, 236 | { 237 | value: "xml", 238 | syntax: "xml", 239 | name: "XML", 240 | }, 241 | { 242 | value: "yaml", 243 | syntax: "yaml", 244 | name: "YAML", 245 | }, 246 | ]; 247 | 248 | export default datas; 249 | -------------------------------------------------------------------------------- /packages/codeblock/src/component/select/component.vue: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /packages/codeblock/src/component/select/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Select from './component.vue'; 3 | 4 | export { Select }; 5 | 6 | export default ( 7 | container: HTMLElement, 8 | modeDatas: { value: string; syntax: string; name: string }[], 9 | defaultValue: string, 10 | onSelect?: (value: string) => void, 11 | ) => { 12 | const vm = new Vue({ 13 | render: h => { 14 | return h(Select, { 15 | props: { 16 | modeDatas, 17 | defaultValue, 18 | getContainer: container ? () => container : undefined, 19 | onSelect, 20 | } 21 | }) 22 | } 23 | }); 24 | container.append(vm.$mount().$el); 25 | return vm; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/codeblock/src/component/types.ts: -------------------------------------------------------------------------------- 1 | import { Editor, EditorConfiguration } from 'codemirror'; 2 | import { EditorInterface, NodeInterface } from '@aomao/engine'; 3 | 4 | export type Options = { 5 | styleMap?: Record; 6 | onSave?: (mode: string, value: string) => void; 7 | onFocus?: () => void; 8 | onBlur?: () => void; 9 | onMouseDown?: (event: MouseEvent | TouchEvent) => void; 10 | container?: NodeInterface; 11 | synatxMap: { [key: string]: string }; 12 | onDownFocus?: (event: KeyboardEvent) => void; 13 | onUpFocus?: (event: KeyboardEvent) => void; 14 | onLeftFocus?: (event: KeyboardEvent) => void; 15 | onRightFocus?: (event: KeyboardEvent) => void; 16 | }; 17 | 18 | export interface CodeBlockEditor { 19 | new (editor: EditorInterface, options: Options): CodeBlockEditorInterface; 20 | } 21 | 22 | export interface CodeBlockEditorInterface { 23 | codeMirror?: Editor; 24 | mode: string; 25 | container: NodeInterface; 26 | renderTemplate(): string; 27 | getConfig(value: string, mode?: string): EditorConfiguration; 28 | getSyntax(mode: string): string; 29 | create(mode: string, value: string, options?: EditorConfiguration): Editor; 30 | update(mode: string, value?: string): void; 31 | setAutoWrap(value: boolean): void; 32 | render(mode: string, value: string, options?: EditorConfiguration): void; 33 | save(): void; 34 | focus(): void; 35 | select(start?: boolean): void; 36 | /** 37 | * 代码来自 runmode addon 38 | * 支持行号需要考虑复制粘贴问题 39 | * 40 | * runmode 本身不支持行号,见 https://github.com/codemirror/CodeMirror/issues/3364 41 | * 可参考的解法 https://stackoverflow.com/questions/14237361/use-codemirror-for-standalone-syntax-highlighting-with-line-numbers 42 | * 43 | * ref: 44 | * - https://codemirror.net/doc/manual.html#addons 45 | * - https://codemirror.net/addon/runmode/runmode.js 46 | */ 47 | runMode( 48 | string: string, 49 | modespec: string, 50 | callback: any, 51 | options: any, 52 | ): void; 53 | destroy(): void; 54 | } 55 | -------------------------------------------------------------------------------- /packages/codeblock/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | $, 3 | Plugin, 4 | isEngine, 5 | NodeInterface, 6 | CARD_KEY, 7 | isServer, 8 | CARD_VALUE_KEY, 9 | Parser, 10 | SchemaInterface, 11 | unescape, 12 | CARD_TYPE_KEY, 13 | READY_CARD_KEY, 14 | decodeCardValue, 15 | } from "@aomao/engine"; 16 | import type MarkdownIt from "markdown-it"; 17 | import CodeBlockComponent, { 18 | CodeBlockEditor, 19 | CodeBlockValue, 20 | } from "./component"; 21 | import locales from "./locales"; 22 | import { CodeBlockOptions } from "./types"; 23 | 24 | const DATA_SYNTAX = "data-syntax"; 25 | export default class< 26 | T extends CodeBlockOptions = CodeBlockOptions 27 | > extends Plugin { 28 | static get pluginName() { 29 | return "codeblock"; 30 | } 31 | 32 | init() { 33 | this.editor.language.add(locales); 34 | 35 | this.editor.on("parse:html", (node) => this.parseHtml(node)); 36 | this.editor.on("paste:schema", (schema) => this.pasteSchema(schema)); 37 | this.editor.on("paste:each", (child) => this.pasteHtml(child)); 38 | if (isEngine(this.editor)) { 39 | this.editor.on("markdown-it", this.markdownIt); 40 | } 41 | } 42 | 43 | execute(mode: string, value: string) { 44 | if (!isEngine(this.editor)) return; 45 | const { card } = this.editor; 46 | const component = card.insert< 47 | CodeBlockValue, 48 | CodeBlockComponent 49 | >(CodeBlockComponent.cardName, { 50 | mode, 51 | code: value, 52 | }); 53 | setTimeout(() => { 54 | component.focusEditor(); 55 | }, 200); 56 | } 57 | 58 | hotkey() { 59 | return this.options.hotkey || ""; 60 | } 61 | 62 | pasteSchema(schema: SchemaInterface) { 63 | schema.add([ 64 | { 65 | type: "block", 66 | name: "pre", 67 | attributes: { 68 | [DATA_SYNTAX]: "*", 69 | class: "*", 70 | language: "*", 71 | "auto-wrap": "*", 72 | }, 73 | }, 74 | { 75 | type: "block", 76 | name: "code", 77 | attributes: { 78 | [DATA_SYNTAX]: { 79 | required: true, 80 | value: "*", 81 | }, 82 | "auto-wrap": "*", 83 | }, 84 | }, 85 | { 86 | type: "block", 87 | name: "code", 88 | attributes: { 89 | language: { 90 | required: true, 91 | value: "*", 92 | }, 93 | }, 94 | }, 95 | { 96 | type: "block", 97 | name: "code", 98 | attributes: { 99 | class: { 100 | required: true, 101 | value: "*", 102 | }, 103 | }, 104 | }, 105 | { 106 | type: "block", 107 | name: "div", 108 | attributes: { 109 | [DATA_SYNTAX]: { 110 | required: true, 111 | value: "*", 112 | }, 113 | "auto-wrap": "*", 114 | }, 115 | }, 116 | ]); 117 | } 118 | 119 | pasteHtml(node: NodeInterface) { 120 | if (!isEngine(this.editor) || node.isText()) return; 121 | if ( 122 | node.get()?.hasAttribute(DATA_SYNTAX) || 123 | node.name === "pre" 124 | ) { 125 | let syntax: string | undefined = node.attributes(DATA_SYNTAX); 126 | if (!syntax) { 127 | const getSyntaxForClass = (node: NodeInterface) => { 128 | const classList = node?.get()?.classList; 129 | if (!classList) return; 130 | for (let i = 0; i < classList.length; i++) { 131 | const className = classList.item(i); 132 | if (className && className.startsWith("language-")) { 133 | const classArray = className.split("-"); 134 | classArray.shift(); 135 | return classArray.join("-"); 136 | } 137 | } 138 | return undefined; 139 | }; 140 | if (node.name === "pre") { 141 | syntax = node.attributes("language"); 142 | if (!syntax) { 143 | syntax = getSyntaxForClass(node); 144 | } 145 | } 146 | const code = node.find("code"); 147 | if (!syntax && code.length > 0) { 148 | syntax = code.attributes(DATA_SYNTAX) || code.attributes("language"); 149 | if (!syntax) { 150 | syntax = getSyntaxForClass(code); 151 | } 152 | } 153 | } 154 | let code = new Parser(node, this.editor).toText( 155 | undefined, 156 | undefined, 157 | false 158 | ); 159 | code = unescape(code.replace(/\u200b/g, "")); 160 | if (code.endsWith("\n")) { 161 | code = code.slice(0, -1); 162 | } 163 | this.editor.card.replaceNode(node, "codeblock", { 164 | mode: syntax || "plain", 165 | code, 166 | autoWrap: node.attributes("auto-wrap") === "true", 167 | }); 168 | node.remove(); 169 | return false; 170 | } 171 | return true; 172 | } 173 | 174 | markdownIt = (mardown: MarkdownIt) => { 175 | if (this.options.markdown !== false) { 176 | mardown.enable("code"); 177 | mardown.enable("fence"); 178 | } 179 | }; 180 | 181 | parseHtml(root: NodeInterface) { 182 | if (isServer) return; 183 | 184 | root 185 | .find( 186 | `[${CARD_KEY}="${CodeBlockComponent.cardName}"],[${READY_CARD_KEY}="${CodeBlockComponent.cardName}"]` 187 | ) 188 | .each((cardNode) => { 189 | const node = $(cardNode); 190 | const card = this.editor.card.find( 191 | node 192 | ) as CodeBlockComponent; 193 | const value = 194 | card?.getValue() || decodeCardValue(node.attributes(CARD_VALUE_KEY)); 195 | if (value) { 196 | node.empty(); 197 | const synatxMap: { [key: string]: string } = {}; 198 | CodeBlockComponent.getModes().forEach((item) => { 199 | synatxMap[item.value] = item.syntax; 200 | }); 201 | const codeEditor = new CodeBlockEditor(this.editor, { 202 | synatxMap, 203 | styleMap: this.options.styleMap, 204 | }); 205 | 206 | const content = codeEditor.container.find(".data-codeblock-content"); 207 | content.css({ 208 | border: "1px solid #e8e8e8", 209 | "max-width": "750px", 210 | }); 211 | codeEditor.render(value.mode || "plain", value.code || ""); 212 | content.addClass("am-engine-view"); 213 | content.hide(); 214 | document.body.appendChild(content[0]); 215 | content.traverse((node) => { 216 | if ( 217 | node.type === Node.ELEMENT_NODE && 218 | (node.get()?.classList?.length || 0) > 0 219 | ) { 220 | const element = node.get()!; 221 | const style = window.getComputedStyle(element); 222 | ["color", "margin", "padding", "background"].forEach((attr) => { 223 | (element.style as any)[attr] = style.getPropertyValue(attr); 224 | }); 225 | } 226 | }); 227 | content.show(); 228 | content.css("background", "#f9f9f9"); 229 | node.append(content); 230 | node.removeAttributes(CARD_KEY); 231 | node.removeAttributes(CARD_TYPE_KEY); 232 | node.removeAttributes(CARD_VALUE_KEY); 233 | node.attributes(DATA_SYNTAX, value.mode || "plain"); 234 | node.attributes("auto-wrap", value.autoWrap ? "true" : "false"); 235 | content.removeClass("am-engine-view"); 236 | } else node.remove(); 237 | }); 238 | } 239 | } 240 | export { CodeBlockComponent }; 241 | export type { CodeBlockValue }; 242 | -------------------------------------------------------------------------------- /packages/codeblock/src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | codeblock: { 3 | autoWrap: 'Auto Wrap', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/codeblock/src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import en from './en-US'; 2 | import cn from './zh-CN'; 3 | 4 | export default { 5 | 'en-US': en, 6 | 'zh-CN': cn, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/codeblock/src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | codeblock: { 3 | autoWrap: '自动换行', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/codeblock/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /packages/codeblock/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CardToolbarItemOptions, 3 | PluginOptions, 4 | ToolbarItemOptions, 5 | } from "@aomao/engine"; 6 | 7 | export interface CodeBlockOptions extends PluginOptions { 8 | hotkey?: string | Array; 9 | markdown?: boolean; 10 | alias?: Record; 11 | styleMap?: Record; 12 | cardToolbars?: ( 13 | items: (ToolbarItemOptions | CardToolbarItemOptions)[], 14 | ) => (ToolbarItemOptions | CardToolbarItemOptions)[]; 15 | }; -------------------------------------------------------------------------------- /packages/codeblock/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "preserve", 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "experimentalDecorators": true, 11 | "noImplicitReturns": true, 12 | "declaration": true, 13 | "suppressImplicitAnyIndexErrors": true, 14 | "esModuleInterop": true, 15 | "sourceMap": true, 16 | "baseUrl": "./", 17 | "strict": true, 18 | "paths": { 19 | "@/*": ["src/*"] 20 | }, 21 | "allowSyntheticDefaultImports": true, 22 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 23 | }, 24 | "include": ["src/*.ts", "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], 25 | "exclude": [ 26 | "node_modules", 27 | "lib", 28 | "es", 29 | "dist", 30 | "docs-dist", 31 | "typings", 32 | "**/__test__", 33 | "test", 34 | "fixtures" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /packages/link/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["import", { "libraryName": "ant-design-vue", "libraryDirectory": "es", "style": "css" }] // `style: true` 会加载 less 文件 4 | ] 5 | } -------------------------------------------------------------------------------- /packages/link/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | 3 | export default { 4 | extraRollupPlugins: [ 5 | commonjs(), 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /packages/link/README.md: -------------------------------------------------------------------------------- 1 | # am-editor-link-vue2 2 | 3 | 链接插件 4 | 5 | ## 安装 6 | 7 | ```bash 8 | $ yarn add am-editor-link-vue2 9 | ``` 10 | 11 | 添加到引擎 12 | 13 | ```ts 14 | import Engine, { EngineInterface } from '@aomao/engine'; 15 | import Link from '@aomao/plugin-link'; 16 | 17 | new Engine(...,{ plugins:[Link] }) 18 | ``` 19 | 20 | ## 可选项 21 | 22 | ### 快捷键 23 | 24 | 默认快捷键为 `mod+k`,默认参数为 ["_blank"] 25 | 26 | ```ts 27 | //快捷键,key 组合键,args,执行参数,[target?:string,href?:string,text?:string] 打开模式:可选,默认链接:可选,默认文本:可选 28 | hotkey?:string | {key:string,args:Array}; 29 | 30 | //使用配置 31 | new Engine(...,{ 32 | config:{ 33 | "link":{ 34 | //修改快捷键 35 | hotkey:{ 36 | key:"mod+k", 37 | args:["_balnk_","https://www.yanmao.cc","ITELLYOU"] 38 | } 39 | } 40 | } 41 | }) 42 | ``` 43 | 44 | ### Markdown 45 | 46 | 默认支持 markdown,传入`false`关闭 47 | 48 | Link 插件 markdown 语法为`[文本](链接地址)` 回车后触发 49 | 50 | ```ts 51 | markdown?: boolean;//默认开启,false 关闭 52 | //使用配置 53 | new Engine(...,{ 54 | config:{ 55 | "link":{ 56 | //关闭markdown 57 | markdown:false 58 | } 59 | } 60 | }) 61 | ``` 62 | 63 | ## 命令 64 | 65 | 可传入三个参数[target?:string,href?:string,text?:string] 打开模式:可选,默认链接:可选,默认文本:可选 66 | 67 | ```ts 68 | //target:'_blank', '_parent', '_top', '_self',href:链接,text:文字 69 | engine.command.execute('link', '_blank', 'https://www.yanmao.cc', 'ITELLYOU'); 70 | //使用 command 执行查询当前状态,返回 boolean | undefined 71 | engine.command.queryState('link'); 72 | ``` 73 | -------------------------------------------------------------------------------- /packages/link/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "am-editor-link-vue2", 3 | "version": "1.1.17", 4 | "keyword": "am-editor-link-vue2", 5 | "description": "> TODO: description", 6 | "author": "zhangbin yanmao ", 7 | "homepage": "https://github.com/zb201307/am-editor-vue2#readme", 8 | "license": "MIT", 9 | "main": "dist/index.js", 10 | "module": "dist/index.esm.js", 11 | "typings": "dist/index.d.ts", 12 | "private": false, 13 | "files": [ 14 | "dist", 15 | "lib", 16 | "src" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/zb201307/am-editor-vue2.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/zb201307/am-editor-vue2/issues" 24 | }, 25 | "dependencies": { 26 | "@babel/runtime": "^7.13.10" 27 | }, 28 | "devDependencies": { 29 | "@types/markdown-it": "^12.2.3" 30 | }, 31 | "peerDependencies": { 32 | "@aomao/engine": "^2.9", 33 | "ant-design-vue": "^1.7.8", 34 | "vue": "^2.6.14" 35 | }, 36 | "gitHead": "bc52c5d6caf2d168a714f12d1cbbdea28d376ac0" 37 | } 38 | -------------------------------------------------------------------------------- /packages/link/src/index.css: -------------------------------------------------------------------------------- 1 | .data-link-container { 2 | max-width: 398px; 3 | display: inline-block; 4 | border: 1px solid #E8E8E8; 5 | border-radius: 4px; 6 | box-shadow: rgba(221, 221, 221, 0.5) 0px 1px 3px; 7 | background: white; 8 | } 9 | 10 | .data-link-container-mobile { 11 | max-width: calc(100vw - 20px);; 12 | } 13 | 14 | .data-link-container .data-link-editor { 15 | min-width: 365px; 16 | padding: 16px 12px; 17 | padding-bottom: 4px; 18 | } 19 | 20 | .data-link-container-mobile .data-link-editor { 21 | min-width: calc(100vw - 40px); 22 | padding: 8px 6px; 23 | } 24 | 25 | .data-link-container p { 26 | margin-top: 0; 27 | margin-bottom: 14px; 28 | } 29 | 30 | .data-link-container .itellyou-icon { 31 | color: #8590A6; 32 | font-size: 16px; 33 | } 34 | .data-link-preview { 35 | line-height: 16px; 36 | padding: 6px 8px; 37 | vertical-align: middle; 38 | white-space: nowrap; 39 | display: flex; 40 | justify-content:space-between; 41 | } 42 | .data-link-preview > * { 43 | display: block; 44 | } 45 | 46 | .data-link-preview a { 47 | display: inline-block; 48 | color: #595959; 49 | margin: 0px 0px 0px 8px; 50 | padding: 4px; 51 | } 52 | .data-link-preview a:hover { 53 | background: #F4F4F4; 54 | cursor: pointer; 55 | } 56 | .data-link-preview a.data-link-preview-open { 57 | color: #1890FF; 58 | max-width: 292px; 59 | overflow: hidden; 60 | text-overflow: ellipsis; 61 | white-space: nowrap; 62 | text-decoration: none; 63 | font-size: 14px; 64 | letter-spacing: 1.2px; 65 | vertical-align: middle; 66 | margin: 0; 67 | } 68 | .data-link-container-mobile .data-link-preview a.data-link-preview-open { 69 | max-width: 70%; 70 | } 71 | .data-link-preview a.data-link-preview-open::before 72 | { 73 | vertical-align: middle; 74 | margin-right: 2px; 75 | } 76 | 77 | .data-link-preview a.data-link-preview-open:hover{ 78 | background: transparent; 79 | } -------------------------------------------------------------------------------- /packages/link/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | $, 3 | NodeInterface, 4 | InlinePlugin, 5 | isEngine, 6 | PluginOptions, 7 | } from "@aomao/engine"; 8 | import type MarkdownIt from "markdown-it"; 9 | import Toolbar from "./toolbar"; 10 | import locales from "./locales"; 11 | 12 | import "./index.css"; 13 | 14 | export interface LinkOptions extends PluginOptions { 15 | hotkey?: string | Array; 16 | markdown?: boolean; 17 | onConfirm?: ( 18 | text: string, 19 | link: string 20 | ) => Promise<{ text: string; link: string }>; 21 | enableToolbar?: boolean; 22 | onLinkClick?: (e: MouseEvent, link: string) => void; 23 | } 24 | 25 | export default class< 26 | T extends LinkOptions = LinkOptions 27 | > extends InlinePlugin { 28 | private toolbar?: Toolbar; 29 | 30 | static get pluginName() { 31 | return "link"; 32 | } 33 | 34 | attributes = { 35 | target: "@var0", 36 | href: "@var1", 37 | }; 38 | 39 | variable = { 40 | "@var0": ["_blank", "_parent", "_top", "_self"], 41 | "@var1": { 42 | required: true, 43 | value: "*", 44 | }, 45 | }; 46 | 47 | tagName = "a"; 48 | 49 | init() { 50 | super.init(); 51 | 52 | const editor = this.editor; 53 | if (isEngine(editor)) { 54 | if (this.options.enableToolbar !== false) { 55 | this.toolbar = new Toolbar(editor, { 56 | onConfirm: this.options.onConfirm, 57 | }); 58 | } 59 | editor.container.on("click", this.handleClick); 60 | editor.on("markdown-it", this.markdownIt); 61 | editor.on("paste:each", this.pasteHtml); 62 | } 63 | editor.on("parse:html", this.parseHtml); 64 | editor.on("select", this.bindQuery); 65 | editor.language.add(locales); 66 | } 67 | 68 | handleClick = (e: MouseEvent) => { 69 | if (!e.target) return; 70 | const { onLinkClick } = this.options; 71 | if (!onLinkClick) return; 72 | const target = $(e.target).closest(`${this.tagName}`); 73 | if (target.name === this.tagName) { 74 | onLinkClick(e, target.attributes("href")); 75 | } 76 | }; 77 | 78 | hotkey() { 79 | return this.options.hotkey || { key: "mod+k", args: ["_blank"] }; 80 | } 81 | 82 | execute(...args: any[]) { 83 | if (!isEngine(this.editor)) return; 84 | const { inline, change } = this.editor; 85 | if (!this.queryState()) { 86 | const inlineNode = $(`<${this.tagName} />`); 87 | this.setStyle(inlineNode, ...args); 88 | this.setAttributes(inlineNode, ...args); 89 | const text = args.length > 2 ? args[2] : ""; 90 | 91 | if (text) { 92 | inlineNode.text(text); 93 | inline.insert(inlineNode); 94 | } else { 95 | inline.wrap(inlineNode); 96 | } 97 | const range = change.range.get(); 98 | if (!range.collapsed && change.inlines.length > 0) { 99 | this.toolbar?.show(change.inlines[0]); 100 | } 101 | } else { 102 | const inlineNode = change.inlines.find((node) => this.isSelf(node)); 103 | if (inlineNode && inlineNode.length > 0) { 104 | inline.unwrap(inlineNode); 105 | } 106 | } 107 | } 108 | 109 | bindQuery = () => { 110 | this.query(); 111 | }; 112 | 113 | query = () => { 114 | if (!isEngine(this.editor)) return; 115 | const { change } = this.editor; 116 | const inlineNode = change.inlines.find((node) => this.isSelf(node)); 117 | this.toolbar?.hide(inlineNode); 118 | if (inlineNode && inlineNode.length > 0 && !inlineNode.isCard()) { 119 | const range = change.range.get(); 120 | if ( 121 | range.collapsed || 122 | (inlineNode.contains(range.startNode) && 123 | inlineNode.contains(range.endNode)) 124 | ) { 125 | this.toolbar?.show(inlineNode); 126 | return true; 127 | } else { 128 | this.toolbar?.hide(); 129 | } 130 | } 131 | return false; 132 | }; 133 | 134 | queryState() { 135 | return this.query(); 136 | } 137 | markdownIt = (mardown: MarkdownIt) => { 138 | if (this.options.markdown !== false) { 139 | mardown.enable("link"); 140 | mardown.enable("linkify"); 141 | } 142 | }; 143 | 144 | pasteHtml = (child: NodeInterface) => { 145 | if (child.isText()) { 146 | const text = child.text(); 147 | const { node, inline } = this.editor; 148 | if ( 149 | /^https?:\/\/\S+$/.test(text.toLowerCase().trim()) && 150 | inline.closest(child).equal(child) 151 | ) { 152 | const newNode = node.wrap( 153 | child, 154 | $(`<${this.tagName} target="_blank" href="${text}">`) 155 | ); 156 | inline.repairCursor(newNode); 157 | return false; 158 | } 159 | } else if (child.name === "a") { 160 | const href = child.attributes("href"); 161 | child.attributes("target", "_blank"); 162 | child.attributes( 163 | "href", 164 | decodeURI(href) 165 | .trim() 166 | .replace(/\u200b/g, "") 167 | ); 168 | } 169 | return true; 170 | }; 171 | 172 | parseHtml = (root: NodeInterface) => { 173 | root.find(this.tagName).css({ 174 | "font-family": "monospace", 175 | "font-size": "inherit", 176 | "background-color": "rgba(0,0,0,.06)", 177 | padding: "0 2px", 178 | border: "1px solid rgba(0,0,0,.08)", 179 | "border-radius": "2px 2px", 180 | "line-height": "inherit", 181 | "overflow-wrap": "break-word", 182 | "text-indent": "0", 183 | }); 184 | }; 185 | 186 | destroy(): void { 187 | const editor = this.editor; 188 | editor.container.off("click", this.handleClick); 189 | editor.off("paste:each", this.pasteHtml); 190 | editor.off("parse:html", this.parseHtml); 191 | editor.off("select", this.bindQuery); 192 | editor.off("markdown-it", this.markdownIt); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /packages/link/src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | link: { 3 | text: 'Text', 4 | link: 'Link', 5 | text_placeholder: 'Description text', 6 | link_placeholder: 'Link address', 7 | link_open: 'Open link', 8 | link_edit: 'Edit link', 9 | link_remove: 'Remove link', 10 | ok_button: 'OK', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/link/src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import en from './en-US'; 2 | import cn from './zh-CN'; 3 | 4 | export default { 5 | 'en-US': en, 6 | 'zh-CN': cn, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/link/src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | link: { 3 | text: '文本', 4 | link: '链接', 5 | text_placeholder: '描述文本', 6 | link_placeholder: '链接地址', 7 | link_open: '打开链接', 8 | link_edit: '编辑链接', 9 | link_remove: '移除链接', 10 | ok_button: '确定', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/link/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /packages/link/src/toolbar/editor.vue: -------------------------------------------------------------------------------- 1 | 36 | 97 | -------------------------------------------------------------------------------- /packages/link/src/toolbar/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | $, 3 | EngineInterface, 4 | isMobile, 5 | NodeInterface, 6 | Position, 7 | } from "@aomao/engine"; 8 | import Vue from "vue"; 9 | import LinkEditor from "./editor.vue"; 10 | import LinkPreview from "./preview.vue"; 11 | 12 | export type ToolbarOptions = { 13 | onConfirm?: ( 14 | text: string, 15 | link: string 16 | ) => Promise<{ text: string; link: string }>; 17 | }; 18 | export { LinkEditor, LinkPreview }; 19 | class Toolbar { 20 | private engine: EngineInterface; 21 | private root?: NodeInterface; 22 | private target?: NodeInterface; 23 | private options?: ToolbarOptions; 24 | private mouseInContainer = false; 25 | private vm?: Vue; 26 | position: Position; 27 | 28 | constructor(engine: EngineInterface, options?: ToolbarOptions) { 29 | this.engine = engine; 30 | const { change } = this.engine; 31 | this.options = options; 32 | this.position = new Position(this.engine); 33 | change.event.onWindow("mousedown", (event: MouseEvent) => { 34 | if (!event.target) return; 35 | const target = $(event.target); 36 | const container = target.closest(".data-link-container"); 37 | this.mouseInContainer = container && container.length > 0; 38 | if (!target.inEditor() && !this.mouseInContainer) this.hide(); 39 | }); 40 | } 41 | 42 | private create() { 43 | if (!this.target) return; 44 | let root = $(".data-link-container"); 45 | if (root.length === 0) { 46 | root = $( 47 | `` 50 | ); 51 | } 52 | this.root = root; 53 | const rect = this.target.get()?.getBoundingClientRect(); 54 | if (!rect) return; 55 | this.root.css({ 56 | top: `${window.pageYOffset + rect.bottom + 4}px`, 57 | left: `${window.pageXOffset}px`, 58 | position: "absolute", 59 | "z-index": 125, 60 | }); 61 | } 62 | 63 | private async onOk(text: string, link: string) { 64 | if (!this.target) return; 65 | const { change } = this.engine; 66 | const range = change.range.get(); 67 | if (!change.rangePathBeforeCommand) { 68 | if (!range.startNode.inEditor()) { 69 | range.select(this.target, true); 70 | change.range.select(range); 71 | } 72 | change.cacheRangeBeforeCommand(); 73 | } 74 | const { onConfirm } = this.options || {}; 75 | if (onConfirm) { 76 | const result = await onConfirm(text, link); 77 | text = result.text; 78 | link = result.link; 79 | } 80 | this.target.attributes("href", link); 81 | text = text.trim() === "" ? link : text; 82 | const oldText = this.target.text(); 83 | if (oldText !== text) { 84 | const children = this.target.children(); 85 | // 左右两侧有零宽字符 86 | if (children.length < 3) { 87 | this.target.text(text); 88 | } 89 | // 中间节点是文本字符 90 | else if (children.length === 3 && children.eq(1)?.isText()) { 91 | this.target.text(text); 92 | } 93 | // 中间节点是非文本节点 94 | else if (children.length === 3) { 95 | let element = children.eq(1); 96 | while (element) { 97 | const child = element.children(); 98 | // 有多个子节点就直接设置文本,覆盖里面的mark样式 99 | if (child.length > 1 || child.length === 0) { 100 | element.text(text); 101 | break; 102 | } 103 | // 里面的子节点是文本就设置文本 104 | else if (child.eq(0)?.isText()) { 105 | element.text(text); 106 | break; 107 | } 108 | // 里面的子节点非文本节点就继续循环 109 | else { 110 | element = child; 111 | } 112 | } 113 | } 114 | // 多个其它节点 115 | else { 116 | this.target.text(text); 117 | } 118 | } 119 | this.engine.inline.repairCursor(this.target); 120 | range.setStart(this.target.next()!, 1); 121 | range.setEnd(this.target.next()!, 1); 122 | change.apply(range); 123 | this.mouseInContainer = false; 124 | this.hide(); 125 | } 126 | 127 | editor(text: string, href: string, callback?: () => void) { 128 | const vm = new Vue({ 129 | render: (h) => { 130 | return h(LinkEditor, { 131 | props: { 132 | language: this.engine.language, 133 | defaultText: text, 134 | defaultLink: href, 135 | onLoad: () => { 136 | this.mouseInContainer = true; 137 | if (callback) callback(); 138 | }, 139 | onOk: (text: string, link: string) => this.onOk(text, link), 140 | }, 141 | }); 142 | }, 143 | }); 144 | return vm; 145 | } 146 | 147 | preview(href: string, callback?: () => void) { 148 | const { change, inline, language } = this.engine; 149 | const vm = new Vue({ 150 | render: (h) => { 151 | return h(LinkPreview, { 152 | props: { 153 | language, 154 | href, 155 | readonly: this.engine.readonly, 156 | onLoad: () => { 157 | if (callback) callback(); 158 | }, 159 | onEdit: () => { 160 | if (!this.target) return; 161 | this.mouseInContainer = false; 162 | this.hide(undefined, false); 163 | this.show(this.target, true); 164 | }, 165 | onRemove: () => { 166 | if (!this.target) return; 167 | const range = change.range.get(); 168 | range.select(this.target, true); 169 | inline.repairRange(range); 170 | change.range.select(range); 171 | change.cacheRangeBeforeCommand(); 172 | inline.unwrap(); 173 | this.mouseInContainer = false; 174 | this.target = undefined; 175 | this.hide(); 176 | }, 177 | }, 178 | }); 179 | }, 180 | }); 181 | return vm; 182 | } 183 | 184 | show(target: NodeInterface, forceEdit?: boolean) { 185 | if (this.target?.equal(target) && !!this.root?.parent()?.length) return; 186 | this.target = target; 187 | this.create(); 188 | const text = target.text().replace(/\u200B/g, ""); 189 | const href = target.attributes("href"); 190 | const container = this.root!.get()!; 191 | 192 | const name = 193 | (!href || forceEdit) && !this.engine.readonly 194 | ? "data-link-editor" 195 | : "data-link-preview"; 196 | 197 | if (this.vm && $(this.vm.$el).hasClass(name)) { 198 | if (!this.root || !this.target) return; 199 | this.position?.destroy(); 200 | this.position?.bind(this.root, this.target); 201 | return; 202 | } else if (this.vm) { 203 | this.vm.$destroy(); 204 | this.vm = undefined; 205 | this.position?.destroy(); 206 | } 207 | 208 | setTimeout(() => { 209 | this.position?.destroy(); 210 | this.position?.bind(this.root!, this.target!); 211 | this.vm = 212 | (!href || forceEdit) && !this.engine.readonly 213 | ? this.editor(text, href, () => { 214 | this.position?.update(); 215 | }) 216 | : this.preview(href, () => { 217 | this.position?.update(); 218 | }); 219 | container.append(this.vm.$mount().$el); 220 | }, 20); 221 | } 222 | 223 | hide(target?: NodeInterface, clearTarget?: boolean) { 224 | if (target && this.target && target.equal(this.target)) return; 225 | const element = this.root?.get(); 226 | if (element && !this.mouseInContainer) { 227 | if (this.vm) { 228 | this.vm.$destroy(); 229 | this.vm = undefined; 230 | this.position?.destroy(); 231 | } 232 | this.root = undefined; 233 | if (this.target && !this.target.attributes("href")) { 234 | const { change, inline } = this.engine; 235 | const range = change.range.get(); 236 | range.select(this.target, true); 237 | inline.unwrap(range); 238 | change.apply(range.collapse(true)); 239 | } 240 | if (clearTarget !== false) this.target = undefined; 241 | } 242 | } 243 | } 244 | export default Toolbar; 245 | -------------------------------------------------------------------------------- /packages/link/src/toolbar/preview.vue: -------------------------------------------------------------------------------- 1 | 22 | 54 | -------------------------------------------------------------------------------- /packages/link/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "preserve", 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "experimentalDecorators": true, 11 | "noImplicitReturns": true, 12 | "declaration": true, 13 | "suppressImplicitAnyIndexErrors": true, 14 | "esModuleInterop": true, 15 | "sourceMap": true, 16 | "baseUrl": "./", 17 | "strict": true, 18 | "paths": { 19 | "@/*": ["src/*"] 20 | }, 21 | "allowSyntheticDefaultImports": true, 22 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 23 | }, 24 | "include": ["src/*.ts", "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], 25 | "exclude": [ 26 | "node_modules", 27 | "lib", 28 | "es", 29 | "dist", 30 | "docs-dist", 31 | "typings", 32 | "**/__test__", 33 | "test", 34 | "fixtures" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /packages/map/README.md: -------------------------------------------------------------------------------- 1 | # `map` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const map = require('map'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/map/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "am-editor-map", 3 | "version": "1.1.12", 4 | "keyword": "am-editor-map", 5 | "description": "> TODO: description", 6 | "author": "zhangbin yanmao ", 7 | "homepage": "https://github.com/zb201307/am-editor-vue2#readme", 8 | "license": "MIT", 9 | "main": "dist/index.js", 10 | "module": "dist/index.esm.js", 11 | "typings": "dist/index.d.ts", 12 | "private": false, 13 | "files": [ 14 | "dist", 15 | "lib", 16 | "src" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/zb201307/am-editor-vue2.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/zb201307/am-editor-vue2/issues" 24 | }, 25 | "dependencies": { 26 | "@babel/runtime": "^7.13.10", 27 | "autosize": "^5.0.1" 28 | }, 29 | "peerDependencies": { 30 | "@aomao/engine": "^2.9" 31 | }, 32 | "gitHead": "898fe923d8a408cf59ae2e92a67b0b5aefc44378", 33 | "devDependencies": { 34 | "@types/autosize": "^4.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/map/src/component/index.less: -------------------------------------------------------------------------------- 1 | .map-container { 2 | background-color: rgb(247, 247, 247); 3 | border-radius: 8px; 4 | padding: 12px 8px; 5 | font-size: 14px; 6 | line-height: 1.4; 7 | max-width: 65%; 8 | margin: 0px auto; 9 | cursor: pointer; 10 | 11 | &::selection { 12 | background: transparent !important; 13 | } 14 | 15 | .title, .address { 16 | overflow: hidden; 17 | text-overflow: ellipsis; 18 | white-space: nowrap; 19 | } 20 | 21 | .title { 22 | font-size: 17px; 23 | line-height: 1.3; 24 | padding: 0px 4px; 25 | 26 | textarea { 27 | background: transparent; 28 | border: none; 29 | width: 100%; 30 | resize: none; 31 | line-height: 28px; 32 | height: 28px; 33 | 34 | &:focus { 35 | outline: none; 36 | } 37 | } 38 | } 39 | 40 | .address { 41 | display: block; 42 | color: rgb(154, 154, 154); 43 | line-height: 1.3; 44 | padding: 4px 4px 12px; 45 | font-size: 14px; 46 | } 47 | 48 | .img img { 49 | width: 100%; 50 | } 51 | } 52 | 53 | .map-container-inline { 54 | a { 55 | span { 56 | 57 | &:first-child { 58 | position: relative; 59 | top: 2px; 60 | } 61 | } 62 | } 63 | } 64 | 65 | .am-engine [data-card-key="map"].card-selected [data-card-element="center"]{ 66 | outline: 0 none; 67 | 68 | .map-container { 69 | border: 2px solid #1890FF; 70 | } 71 | } -------------------------------------------------------------------------------- /packages/map/src/component/index.ts: -------------------------------------------------------------------------------- 1 | import { $, Card, CardToolbarItemOptions, CardType, CardValue, isEngine, isMobile, NodeInterface, ToolbarItemOptions } from '@aomao/engine' 2 | import autosize from 'autosize' 3 | import './index.less' 4 | 5 | export interface MapValue extends CardValue { 6 | title: string 7 | address: string 8 | url?: string 9 | province?: string 10 | city?: string 11 | staticUrl?: string 12 | point: { 13 | lat: number 14 | lng: number 15 | } 16 | } 17 | 18 | class MapComponent extends Card { 19 | 20 | static get cardName () { 21 | return 'map' 22 | } 23 | 24 | static get cardType () { 25 | return CardType.BLOCK 26 | } 27 | 28 | static get autoSelected() { 29 | return false; 30 | } 31 | 32 | static get singleSelectable(){ 33 | return false 34 | } 35 | 36 | #container?: NodeInterface 37 | 38 | toolbar(): Array { 39 | if (!isEngine(this.editor) || this.editor.readonly) return []; 40 | const { language } = this.editor; 41 | const items: Array = [ 42 | { 43 | type: 'copy', 44 | }, 45 | { 46 | type: 'delete', 47 | }, 48 | ]; 49 | 50 | return items.concat([ 51 | { 52 | type: 'button', 53 | content: '', 54 | title: language.get('map', 'displayBlockTitle'), 55 | onClick: () => { 56 | this.type = CardType.BLOCK; 57 | }, 58 | }, 59 | { 60 | type: 'button', 61 | content: '', 62 | title: language.get('map', 'displayBlockTitle'), 63 | onClick: () => { 64 | this.type = CardType.INLINE; 65 | }, 66 | }, 67 | ]); 68 | } 69 | 70 | render(){ 71 | const value = this.getValue() 72 | 73 | const container = this.type === CardType.BLOCK ? $(`
74 |
75 |
${value?.address}
76 |
77 |
`) : $(` 78 | 79 | ${value?.title}`) 80 | const textarea = container.find('textarea') 81 | autosize(textarea.get()!) 82 | textarea.on('input', () => { 83 | this.setValue({ 84 | title: textarea.get()?.value 85 | } as V) 86 | }) 87 | this.#container = container 88 | return this.#container 89 | } 90 | 91 | didRender(){ 92 | super.didRender() 93 | if(this.type === CardType.BLOCK) this.toolbarModel?.setOffset([-120,0]) 94 | else this.toolbarModel?.setOffset([0,0]) 95 | } 96 | 97 | destroy(){ 98 | const textarea = this.#container?.find('textarea') 99 | if(textarea) { 100 | autosize.destroy(textarea.get()!) 101 | } 102 | super.destroy() 103 | } 104 | } 105 | 106 | export default MapComponent -------------------------------------------------------------------------------- /packages/map/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | $, 3 | Plugin, 4 | isEngine, 5 | NodeInterface, 6 | CARD_KEY, 7 | SchemaInterface, 8 | PluginOptions, 9 | encodeCardValue, 10 | decodeCardValue, 11 | CardType, 12 | } from '@aomao/engine'; 13 | import MapComponent from './component'; 14 | import type { MapValue } from './component'; 15 | import locales from './locales'; 16 | 17 | export interface MapOptions extends PluginOptions { 18 | hotkey?: string | Array; 19 | } 20 | 21 | export default class extends Plugin { 22 | static get pluginName() { 23 | return 'map'; 24 | } 25 | 26 | init() { 27 | this.editor.language.add(locales); 28 | this.editor.on('paser:html', (node) => this.parseHtml(node)); 29 | this.editor.on('paste:schema', (schema) => this.pasteSchema(schema)); 30 | this.editor.on('paste:each', (child) => this.pasteHtml(child)); 31 | } 32 | 33 | execute(item: MapValue) { 34 | if (!isEngine(this.editor)) return; 35 | const { card } = this.editor; 36 | card.insert(MapComponent.cardName, { 37 | ...item 38 | }); 39 | } 40 | 41 | hotkey() { 42 | return this.options.hotkey || ''; 43 | } 44 | 45 | 46 | pasteSchema(schema: SchemaInterface) { 47 | schema.add({ 48 | type: 'block', 49 | name: 'div', 50 | attributes: { 51 | 'data-type': { 52 | required: true, 53 | value: MapComponent.cardName, 54 | }, 55 | 'data-value': '*', 56 | }, 57 | }); 58 | schema.add({ 59 | type: 'inline', 60 | name: 'span', 61 | attributes: { 62 | 'data-type': { 63 | required: true, 64 | value: MapComponent.cardName, 65 | }, 66 | 'data-value': '*', 67 | }, 68 | }); 69 | } 70 | 71 | pasteHtml(node: NodeInterface) { 72 | if (!isEngine(this.editor)) return; 73 | if (node.isElement()) { 74 | const type = node.attributes('data-type'); 75 | if (type === MapComponent.cardName) { 76 | const value = node.attributes('data-value'); 77 | const cardValue = decodeCardValue(value); 78 | if (!cardValue.url) return; 79 | this.editor.card.replaceNode( 80 | node, 81 | MapComponent.cardName, 82 | cardValue, 83 | ); 84 | node.remove(); 85 | return false; 86 | } 87 | } 88 | return true; 89 | } 90 | 91 | parseHtml(root: NodeInterface) { 92 | root.find(`[${CARD_KEY}=${MapComponent.cardName}`).each((cardNode) => { 93 | const node = $(cardNode); 94 | const card = this.editor.card.find(node) as MapComponent; 95 | const value = card?.getValue(); 96 | if (value) { 97 | const tagName = card.type === CardType.INLINE ? 'span' : 'div' 98 | const html = `<${tagName} data-type="${ 99 | card.type 100 | }" data-value="${encodeCardValue(value)}">`; 101 | node.empty(); 102 | node.replaceWith($(html)); 103 | } else node.remove(); 104 | }); 105 | } 106 | } 107 | export { MapComponent, MapValue }; 108 | -------------------------------------------------------------------------------- /packages/map/src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | map: { 3 | displayBlockTitle: 'Block', 4 | displayInlineTitle: 'In line', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/map/src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import en from './en-US'; 2 | import cn from './zh-CN'; 3 | 4 | export default { 5 | 'en-US': en, 6 | 'zh-CN': cn, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/map/src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | map: { 3 | displayBlockTitle: '独占一行', 4 | displayInlineTitle: '嵌入行内', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/map/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "preserve", 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "experimentalDecorators": true, 11 | "noImplicitReturns": true, 12 | "declaration": true, 13 | "suppressImplicitAnyIndexErrors": true, 14 | "esModuleInterop": true, 15 | "sourceMap": true, 16 | "baseUrl": "./", 17 | "strict": true, 18 | "paths": { 19 | "@/*": ["src/*"] 20 | }, 21 | "allowSyntheticDefaultImports": true, 22 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 23 | }, 24 | "include": ["src/*.ts", "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], 25 | "exclude": [ 26 | "node_modules", 27 | "lib", 28 | "es", 29 | "dist", 30 | "docs-dist", 31 | "typings", 32 | "**/__test__", 33 | "test", 34 | "fixtures" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /packages/toolbar/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["import", { "libraryName": "ant-design-vue", "libraryDirectory": "es", "style": "css" }] // `style: true` 会加载 less 文件 4 | ] 5 | } -------------------------------------------------------------------------------- /packages/toolbar/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | 3 | export default { 4 | extraRollupPlugins: [ 5 | commonjs(), 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /packages/toolbar/README.md: -------------------------------------------------------------------------------- 1 | # `toolbar` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const toolbar = require('toolbar'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/toolbar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "am-editor-toolbar-vue2", 3 | "version": "1.1.20", 4 | "description": "am-editor toolbar for vue2", 5 | "keywords": [ 6 | "toolbar" 7 | ], 8 | "keyword": "am-editor-toolbar-vue2", 9 | "author": "zhangbin yanmao ", 10 | "homepage": "https://github.com/zb201307/am-editor-vue2#readme", 11 | "license": "MIT", 12 | "main": "dist/index.js", 13 | "module": "dist/index.esm.js", 14 | "typings": "dist/index.d.ts", 15 | "private": false, 16 | "files": [ 17 | "dist", 18 | "lib", 19 | "src" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/zb201307/am-editor-vue2.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/zb201307/am-editor-vue2/issues" 27 | }, 28 | "dependencies": { 29 | "@babel/runtime": "^7.13.10", 30 | "keymaster": "^1.6.2", 31 | "lodash": "^4.17.21", 32 | "tinycolor2": "^1.4.2" 33 | }, 34 | "devDependencies": { 35 | "@types/keymaster": "^1.6.30", 36 | "@types/lodash": "^4.14.178", 37 | "@types/tinycolor2": "^1.4.3" 38 | }, 39 | "peerDependencies": { 40 | "@aomao/engine": "^2.9", 41 | "ant-design-vue": "^1.7.8", 42 | "vue": "^2.6.14" 43 | }, 44 | "gitHead": "bc52c5d6caf2d168a714f12d1cbbdea28d376ac0" 45 | } 46 | -------------------------------------------------------------------------------- /packages/toolbar/src/components/button.vue: -------------------------------------------------------------------------------- 1 | 40 | 148 | 197 | -------------------------------------------------------------------------------- /packages/toolbar/src/components/collapse/collapse.vue: -------------------------------------------------------------------------------- 1 | 31 | 107 | -------------------------------------------------------------------------------- /packages/toolbar/src/components/collapse/group.vue: -------------------------------------------------------------------------------- 1 | 13 | 48 | -------------------------------------------------------------------------------- /packages/toolbar/src/components/collapse/item.vue: -------------------------------------------------------------------------------- 1 | 37 | 116 | 121 | -------------------------------------------------------------------------------- /packages/toolbar/src/components/color/color.vue: -------------------------------------------------------------------------------- 1 | 61 | 195 | 261 | -------------------------------------------------------------------------------- /packages/toolbar/src/components/color/picker/group.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /packages/toolbar/src/components/color/picker/item.vue: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /packages/toolbar/src/components/color/picker/palette.ts: -------------------------------------------------------------------------------- 1 | class Palette { 2 | static colors: Array>; 3 | static _map: { [k: string]: { x: number; y: number } }; 4 | /** 5 | * 获取描边颜色 6 | * 默认为当前 color,浅色不明显区域:第 3 组、第 4 组的第 3、4 个用第 5 组的颜色描边 7 | * 8 | * @param {string} color 颜色 9 | * @return {string} 描边颜色 10 | */ 11 | static getStroke: (color: string) => string; 12 | static getColors: () => Array>; 13 | } 14 | 15 | Palette.colors = [ 16 | [ 17 | '#000000', 18 | '#262626', 19 | '#595959', 20 | '#8C8C8C', 21 | '#BFBFBF', 22 | '#D9D9D9', 23 | '#E9E9E9', 24 | '#F5F5F5', 25 | '#FAFAFA', 26 | '#FFFFFF', 27 | ], 28 | [ 29 | '#F5222D', 30 | '#FA541C', 31 | '#FA8C16', 32 | '#FADB14', 33 | '#52C41A', 34 | '#13C2C2', 35 | '#1890FF', 36 | '#2F54EB', 37 | '#722ED1', 38 | '#EB2F96', 39 | ], 40 | [ 41 | '#FFE8E6', 42 | '#FFECE0', 43 | '#FFEFD1', 44 | '#FCFCCA', 45 | '#E4F7D2', 46 | '#D3F5F0', 47 | '#D4EEFC', 48 | '#DEE8FC', 49 | '#EFE1FA', 50 | '#FAE1EB', 51 | ], 52 | [ 53 | '#FFA39E', 54 | '#FFBB96', 55 | '#FFD591', 56 | '#FFFB8F', 57 | '#B7EB8F', 58 | '#87E8DE', 59 | '#91D5FF', 60 | '#ADC6FF', 61 | '#D3ADF7', 62 | '#FFADD2', 63 | ], 64 | [ 65 | '#FF4D4F', 66 | '#FF7A45', 67 | '#FFA940', 68 | '#FFEC3D', 69 | '#73D13D', 70 | '#36CFC9', 71 | '#40A9FF', 72 | '#597EF7', 73 | '#9254DE', 74 | '#F759AB', 75 | ], 76 | [ 77 | '#CF1322', 78 | '#D4380D', 79 | '#D46B08', 80 | '#D4B106', 81 | '#389E0D', 82 | '#08979C', 83 | '#096DD9', 84 | '#1D39C4', 85 | '#531DAB', 86 | '#C41D7F', 87 | ], 88 | [ 89 | '#820014', 90 | '#871400', 91 | '#873800', 92 | '#614700', 93 | '#135200', 94 | '#00474F', 95 | '#003A8C', 96 | '#061178', 97 | '#22075E', 98 | '#780650', 99 | ], 100 | ]; 101 | 102 | Palette._map = (function() { 103 | const map: { [k: string]: { x: number; y: number } } = {}; 104 | const colors = Palette.colors; 105 | for (let i = 0, l1 = colors.length; i < l1; i++) { 106 | const group = colors[i]; 107 | for (let k = 0, l2 = group.length; k < l2; k++) { 108 | const color = colors[i][k]; 109 | map[color] = { 110 | y: i, 111 | x: k, 112 | }; 113 | } 114 | } 115 | return map; 116 | })(); 117 | 118 | /** 119 | * 获取描边颜色 120 | * 默认为当前 color,浅色不明显区域:第 3 组、第 4 组的第 3、4 个用第 5 组的颜色描边 121 | * 122 | * @param {string} color 颜色 123 | * @return {string} 描边颜色 124 | */ 125 | Palette.getStroke = function(color: string): string { 126 | const pos = Palette._map[color]; 127 | if (!pos) return color; 128 | 129 | if (pos.y === 2 || (pos.y === 3 && pos.x > 2 && pos.x < 5)) { 130 | return this.colors[4][pos.x]; 131 | } 132 | 133 | return color; 134 | }; 135 | 136 | Palette.getColors = function() { 137 | return this.colors; 138 | }; 139 | 140 | export default Palette; 141 | -------------------------------------------------------------------------------- /packages/toolbar/src/components/color/picker/picker.vue: -------------------------------------------------------------------------------- 1 | 30 | 89 | -------------------------------------------------------------------------------- /packages/toolbar/src/components/dropdown-list.vue: -------------------------------------------------------------------------------- 1 | 59 | 140 | -------------------------------------------------------------------------------- /packages/toolbar/src/components/group.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 74 | -------------------------------------------------------------------------------- /packages/toolbar/src/components/table.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 62 | 86 | -------------------------------------------------------------------------------- /packages/toolbar/src/config/fontfamily.ts: -------------------------------------------------------------------------------- 1 | import { DropdownListItem } from '../types'; 2 | import { isSupportFontFamily } from '../utils'; 3 | 4 | export const defaultData = [ 5 | { 6 | key: 'default', 7 | value: '', 8 | }, 9 | { 10 | key: 'arial', 11 | value: 'Arial', 12 | }, 13 | { 14 | key: 'comicSansMS', 15 | value: '"Comic Sans MS"', 16 | }, 17 | { 18 | key: 'courierNew', 19 | value: '"Courier New"', 20 | }, 21 | { 22 | key: 'georgia', 23 | value: 'Georgia', 24 | }, 25 | { 26 | key: 'helvetica', 27 | value: 'Helvetica', 28 | }, 29 | { 30 | key: 'impact', 31 | value: 'Impact', 32 | }, 33 | { 34 | key: 'timesNewRoman', 35 | value: '"Times New Roman"', 36 | }, 37 | { 38 | key: 'trebuchetMS', 39 | value: '"Trebuchet MS"', 40 | }, 41 | { 42 | key: 'verdana', 43 | value: 'Verdana', 44 | }, 45 | { 46 | key: 'fangSong', 47 | value: 'FangSong, 仿宋, FZFangSong-Z02S, STFangsong, fangsong', 48 | }, 49 | { 50 | key: 'stFangsong', 51 | value: 'STFangsong, 华文仿宋, FangSong, FZFangSong-Z02S, fangsong', 52 | }, 53 | { 54 | key: 'stSong', 55 | value: 'STSong, 华文宋体, SimSun, "Songti SC", NSimSun, serif', 56 | }, 57 | { 58 | key: 'stKaiti', 59 | value: 'STKaiti, 华文楷体, KaiTi, "Kaiti SC", cursive', 60 | }, 61 | { 62 | key: 'simSun', 63 | value: 'SimSun, 宋体, "Songti SC", NSimSun, STSong, serif', 64 | }, 65 | { 66 | key: 'microsoftYaHei', 67 | value: '"Microsoft YaHei", 微软雅黑, "PingFang SC", SimHei, STHeiti, sans-serif', 68 | }, 69 | { 70 | key: 'kaiTi', 71 | value: 'KaiTi, 楷体, STKaiti, "Kaiti SC", cursive', 72 | }, 73 | { 74 | key: 'kaitiSC', 75 | value: '"Kaiti SC"', 76 | }, 77 | { 78 | key: 'simHei', 79 | value: 'SimHei, 黑体, "Microsoft YaHei", "PingFang SC", STHeiti, sans-serif', 80 | }, 81 | { 82 | key: 'heitiSC', 83 | value: '"Heiti SC"', 84 | }, 85 | { 86 | key: 'fzHei', 87 | value: 'FZHei-B01S', 88 | }, 89 | { 90 | key: 'fzKai', 91 | value: 'FZKai-Z03S', 92 | }, 93 | { 94 | key: 'fzFangSong', 95 | value: 'FZFangSong-Z02S', 96 | }, 97 | ]; 98 | /** 99 | * 生成字体下拉列表项 100 | * @param data key-value 键值对数据,key 名称,如果有传语言则是语言键值对的key否则就直接显示 101 | * @param language 语言,可选 102 | */ 103 | export default ( 104 | data: Array<{ key: string; value: string }>, 105 | language?: { [key: string]: string }, 106 | ): Array => { 107 | return data.map(({ key, value }) => { 108 | const disabled = 109 | key !== 'default' 110 | ? !value.split(',').some((v) => isSupportFontFamily(v.trim())) 111 | : false; 112 | return { 113 | key: value, 114 | faimlyName: language ? language[key] : key, 115 | content: `${ 116 | language ? language[key] : key 117 | }`, 118 | hotkey: false, 119 | disabled, 120 | title: disabled 121 | ? (language && language['notInstalled']) || 122 | 'The font may not be installed' 123 | : undefined, 124 | }; 125 | }); 126 | }; 127 | -------------------------------------------------------------------------------- /packages/toolbar/src/config/index.css: -------------------------------------------------------------------------------- 1 | .editor-toolbar .toolbar-dropdown.toolbar-dropdown-heading .toolbar-button { 2 | font-weight: bold; 3 | min-width: 73px; 4 | } 5 | 6 | .editor-toolbar .toolbar-dropdown.toolbar-dropdown-heading .heading-item-h1, 7 | .editor-toolbar .toolbar-dropdown.toolbar-dropdown-heading .heading-item-h2, 8 | .editor-toolbar .toolbar-dropdown.toolbar-dropdown-heading .heading-item-h3, 9 | .editor-toolbar .toolbar-dropdown.toolbar-dropdown-heading .heading-item-h4, 10 | .editor-toolbar .toolbar-dropdown.toolbar-dropdown-heading .heading-item-h5, 11 | .editor-toolbar .toolbar-dropdown.toolbar-dropdown-heading .heading-item-h6 { 12 | line-height: 1.6; 13 | font-weight: bold; 14 | color: #262626; 15 | } 16 | 17 | .heading-item-h1 { 18 | font-size: 28px; 19 | } 20 | 21 | .heading-item-h2 { 22 | font-size: 24px; 23 | } 24 | 25 | .heading-item-h3 { 26 | font-size: 20px; 27 | } 28 | 29 | .heading-item-h4 { 30 | font-size: 16px; 31 | } 32 | 33 | .heading-item-h5 { 34 | font-size: 14px; 35 | } 36 | 37 | .editor-toolbar .toolbar-dropdown.toolbar-dropdown-heading .heading-item-h6 { 38 | font-size: 14px; 39 | font-weight: normal; 40 | } 41 | 42 | .editor-toolbar .toolbar-dropdown.toolbar-dropdown-fontsize .toolbar-button { 43 | font-weight: bold; 44 | min-width: 58px; 45 | } 46 | 47 | .editor-toolbar .toolbar-dropdown.toolbar-dropdown-fontfamily .toolbar-button { 48 | font-size: 12px; 49 | } -------------------------------------------------------------------------------- /packages/toolbar/src/index.ts: -------------------------------------------------------------------------------- 1 | import Toolbar from './components/toolbar.vue'; 2 | import { 3 | getToolbarDefaultConfig, 4 | fontFamilyDefaultData, 5 | fontfamily, 6 | } from './config'; 7 | import ToolbarPlugin, { ToolbarComponent } from './plugin'; 8 | import type { ToolbarOptions } from './plugin'; 9 | import type { ToolbarProps, GroupItemProps, ToolbarItemProps } from './types'; 10 | 11 | export default Toolbar; 12 | export { 13 | ToolbarPlugin, 14 | ToolbarComponent, 15 | getToolbarDefaultConfig, 16 | fontFamilyDefaultData, 17 | fontfamily, 18 | }; 19 | export type { ToolbarOptions, ToolbarProps, GroupItemProps, ToolbarItemProps }; 20 | -------------------------------------------------------------------------------- /packages/toolbar/src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | import { isMacos } from '@aomao/engine'; 2 | 3 | export default { 4 | toolbar: { 5 | collapse: { 6 | title: `Type ${ 7 | isMacos ? '⌘' : 'Ctrl' 8 | } + / to quickly insert a card`, 9 | }, 10 | undo: { 11 | title: 'Undo', 12 | }, 13 | redo: { 14 | title: 'Redo', 15 | }, 16 | paintformat: { 17 | title: 'Format brush', 18 | }, 19 | removeformat: { 20 | title: 'Clear format', 21 | }, 22 | heading: { 23 | title: 'Text and title', 24 | p: 'Text', 25 | h1: 'Heading 1', 26 | h2: 'Heading 2', 27 | h3: 'Heading 3', 28 | h4: 'Heading 4', 29 | h5: 'Heading 5', 30 | h6: 'Heading 6', 31 | }, 32 | fontfamily: { 33 | title: 'Font family', 34 | notInstalled: 'The font may not be installed', 35 | items: { 36 | default: 'Default', 37 | arial: 'Arial', 38 | comicSansMS: 'Comic Sans MS', 39 | courierNew: 'Courier New', 40 | georgia: 'Georgia', 41 | helvetica: 'Helvetica', 42 | impact: 'Impact', 43 | timesNewRoman: 'Times New Roman', 44 | trebuchetMS: 'Trebuchet MS', 45 | verdana: 'Verdana', 46 | fangSong: 'FangSong', 47 | stFangsong: 'STFangsong', 48 | stSong: 'STSong', 49 | stKaiti: 'STKaiti', 50 | simSun: 'SimSum', 51 | microsoftYaHei: 'Microsoft YaHei', 52 | kaiTi: 'KaiTi', 53 | kaitiSC: 'KaiTi SC', 54 | simHei: 'SimHei', 55 | heitiSC: 'Heiti SC', 56 | fzHei: 'FZHeiTi', 57 | fzKai: 'FZKaiTi', 58 | fzFangSong: 'FZFangSong', 59 | }, 60 | }, 61 | fontsize: { 62 | title: 'Font size', 63 | }, 64 | fontcolor: { 65 | title: 'Font color', 66 | more: 'More colors', 67 | }, 68 | backcolor: { 69 | title: 'Background color', 70 | more: 'More colors', 71 | }, 72 | bold: { 73 | title: 'Bold', 74 | }, 75 | italic: { 76 | title: 'Italic', 77 | }, 78 | strikethrough: { 79 | title: 'Strikethrough', 80 | }, 81 | underline: { 82 | title: 'Underline', 83 | }, 84 | moremark: { 85 | title: 'More text styles', 86 | sup: 'Sup', 87 | sub: 'Sub', 88 | code: 'Inline code', 89 | }, 90 | alignment: { 91 | title: 'Alignment', 92 | left: 'Align left', 93 | center: 'Align center', 94 | right: 'Align right', 95 | justify: 'Align justify', 96 | }, 97 | unorderedlist: { 98 | title: 'Unordered list', 99 | }, 100 | orderedlist: { 101 | title: 'Ordered list', 102 | }, 103 | tasklist: { 104 | title: 'Task list', 105 | }, 106 | indent: { 107 | title: 'Ident', 108 | in: 'Increase indent', 109 | out: 'Reduce indent', 110 | }, 111 | 'line-height': { 112 | title: 'Line height', 113 | default: 'Default', 114 | }, 115 | link: { 116 | title: 'Insert Link', 117 | }, 118 | quote: { 119 | title: 'Insert reference', 120 | }, 121 | hr: { 122 | title: 'Insert dividing line', 123 | }, 124 | colorPicker: { 125 | defaultText: 'Default Color', 126 | nonFillText: 'No fill color', 127 | '#000000': 'Black', 128 | '#262626': 'Dark Gray 3', 129 | '#595959': 'Dark Gray 2', 130 | '#8C8C8C': 'Dark Gray 1', 131 | '#BFBFBF': 'Gray', 132 | '#D9D9D9': 'Light Gray 4', 133 | '#E9E9E9': 'Light Gray 3', 134 | '#F5F5F5': 'Light Gray 2', 135 | '#FAFAFA': 'Light Gray 1', 136 | '#FFFFFF': 'White', 137 | '#F5222D': 'Red', 138 | '#FA541C': 'Chinese Red', 139 | '#FA8C16': 'Orange', 140 | '#FADB14': 'Yellow', 141 | '#52C41A': 'Green', 142 | '#13C2C2': 'Cyan', 143 | '#1890FF': 'Light Blue', 144 | '#2F54EB': 'Blue', 145 | '#722ED1': 'Purple', 146 | '#EB2F96': 'Magenta', 147 | '#FFE8E6': 'Red 1', 148 | '#FFECE0': 'Chinese Red 1', 149 | '#FFEFD1': 'Orange 1', 150 | '#FCFCCA': 'Yellow 1', 151 | '#E4F7D2': 'Green 1', 152 | '#D3F5F0': 'Cyan 1', 153 | '#D4EEFC': 'Light Blue 1', 154 | '#DEE8FC': 'Blue 1', 155 | '#EFE1FA': 'Purple 1', 156 | '#FAE1EB': 'Magenta 1', 157 | '#FFA39E': 'Red 2', 158 | '#FFBB96': 'Chinese Red 2', 159 | '#FFD591': 'Orange 2', 160 | '#FFFB8F': 'Yellow 2', 161 | '#B7EB8F': 'Green 2', 162 | '#87E8DE': 'Cyan 2', 163 | '#91D5FF': 'Light Blue 2', 164 | '#ADC6FF': 'Blue 2', 165 | '#D3ADF7': 'Purple 2', 166 | '#FFADD2': 'Magenta 2', 167 | '#FF4D4F': 'Red 3', 168 | '#FF7A45': 'Chinese Red 3', 169 | '#FFA940': 'Orange 3', 170 | '#FFEC3D': 'Yellow 3', 171 | '#73D13D': 'Green 3', 172 | '#36CFC9': 'Cyan 3', 173 | '#40A9FF': 'Light Blue 3', 174 | '#597EF7': 'Blue 3', 175 | '#9254DE': 'Purple 3', 176 | '#F759AB': 'Magenta 3', 177 | '#CF1322': 'Red 4', 178 | '#D4380D': 'Chinese Red 4', 179 | '#D46B08': 'Orange 4', 180 | '#D4B106': 'Yellow 4', 181 | '#389E0D': 'Green 4', 182 | '#08979C': 'Cyan 4', 183 | '#096DD9': 'Light Blue 4', 184 | '#1D39C4': 'Blue 4', 185 | '#531DAB': 'Purple 4', 186 | '#C41D7F': 'Magenta 4', 187 | '#820014': 'Red 5', 188 | '#871400': 'Chinese Red 5', 189 | '#873800': 'Orange 5', 190 | '#614700': 'Yellow 5', 191 | '#135200': 'Green 5', 192 | '#00474F': 'Cyan 5', 193 | '#003A8C': 'Light Blue 5', 194 | '#061178': 'Blue 5', 195 | '#22075E': 'Purple 5', 196 | '#780650': 'Magenta 5', 197 | }, 198 | component: { 199 | placeholder: 'Card name', 200 | }, 201 | image: { 202 | title: 'Image', 203 | }, 204 | codeblock: { 205 | title: 'Codeblock', 206 | }, 207 | table: { 208 | title: 'Table', 209 | }, 210 | file: { 211 | title: 'File', 212 | }, 213 | video: { 214 | title: 'Video', 215 | }, 216 | math: { 217 | title: 'Formula', 218 | }, 219 | status: { 220 | title: 'Status', 221 | }, 222 | mind: { 223 | title: 'Mind Map', 224 | }, 225 | commonlyUsed: { 226 | title: 'Commonly used', 227 | }, 228 | searchEmtpy: { 229 | title: 'No matching card', 230 | }, 231 | }, 232 | }; 233 | -------------------------------------------------------------------------------- /packages/toolbar/src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import en from './en-US'; 2 | import cn from './zh-CN'; 3 | 4 | export default { 5 | 'en-US': en, 6 | 'zh-CN': cn, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/toolbar/src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import { isMacos } from '@aomao/engine'; 2 | 3 | export default { 4 | toolbar: { 5 | collapse: { 6 | title: `输入 ${ 7 | isMacos ? '⌘' : 'Ctrl' 8 | } + / 快速插入卡片`, 9 | }, 10 | undo: { 11 | title: '撤销', 12 | }, 13 | redo: { 14 | title: '重做', 15 | }, 16 | paintformat: { 17 | title: '格式刷', 18 | }, 19 | removeformat: { 20 | title: '清除格式', 21 | }, 22 | heading: { 23 | title: '正文与标题', 24 | p: '正文', 25 | h1: '标题 1', 26 | h2: '标题 2', 27 | h3: '标题 3', 28 | h4: '标题 4', 29 | h5: '标题 5', 30 | h6: '标题 6', 31 | }, 32 | fontfamily: { 33 | title: '字体', 34 | notInstalled: '可能未安装该字体', 35 | items: { 36 | default: '默认', 37 | arial: 'Arial', 38 | comicSansMS: 'Comic Sans MS', 39 | courierNew: 'Courier New', 40 | georgia: 'Georgia', 41 | helvetica: 'Helvetica', 42 | impact: 'Impact', 43 | timesNewRoman: 'Times New Roman', 44 | trebuchetMS: 'Trebuchet MS', 45 | verdana: 'Verdana', 46 | fangSong: '仿宋', 47 | stFangsong: '华文仿宋', 48 | stSong: '华文宋体', 49 | stKaiti: '华文楷体', 50 | simSun: '宋体', 51 | microsoftYaHei: '微软雅黑', 52 | kaiTi: '楷体', 53 | kaitiSC: '楷体-简', 54 | simHei: '黑体', 55 | heitiSC: '黑体-简', 56 | fzHei: '方正黑体', 57 | fzKai: '方正楷体', 58 | fzFangSong: '方正仿宋', 59 | }, 60 | }, 61 | fontsize: { 62 | title: '字号', 63 | }, 64 | fontcolor: { 65 | title: '字体颜色', 66 | more: '更多颜色', 67 | }, 68 | backcolor: { 69 | title: '背景颜色', 70 | more: '更多颜色', 71 | }, 72 | bold: { 73 | title: '粗体', 74 | }, 75 | italic: { 76 | title: '斜体', 77 | }, 78 | strikethrough: { 79 | title: '删除线', 80 | }, 81 | underline: { 82 | title: '下划线', 83 | }, 84 | moremark: { 85 | title: '更多文本样式', 86 | sup: '上标', 87 | sub: '下标', 88 | code: '行内代码', 89 | }, 90 | alignment: { 91 | title: '对齐方式', 92 | left: '左对齐', 93 | center: '居中对齐', 94 | right: '右对齐', 95 | justify: '两端对齐', 96 | }, 97 | unorderedlist: { 98 | title: '无序列表', 99 | }, 100 | orderedlist: { 101 | title: '有序列表', 102 | }, 103 | tasklist: { 104 | title: '任务列表', 105 | }, 106 | indent: { 107 | title: '缩进', 108 | in: '增加缩进', 109 | out: '减少缩进', 110 | }, 111 | 'line-height': { 112 | title: '行高', 113 | default: '默认', 114 | }, 115 | link: { 116 | title: '链接', 117 | }, 118 | quote: { 119 | title: '插入引用', 120 | }, 121 | hr: { 122 | title: '插入分割线', 123 | }, 124 | colorPicker: { 125 | defaultText: '默认', 126 | nonFillText: '无填充色', 127 | '#000000': '黑色', 128 | '#262626': '深灰 3', 129 | '#595959': '深灰 2', 130 | '#8C8C8C': '深灰 1', 131 | '#BFBFBF': '灰色', 132 | '#D9D9D9': '浅灰 4', 133 | '#E9E9E9': '浅灰 3', 134 | '#F5F5F5': '浅灰 2', 135 | '#FAFAFA': '浅灰 1', 136 | '#FFFFFF': '白色', 137 | '#F5222D': '红色', 138 | '#FA541C': '朱红', 139 | '#FA8C16': '橙色', 140 | '#FADB14': '黄色', 141 | '#52C41A': '绿色', 142 | '#13C2C2': '青色', 143 | '#1890FF': '浅蓝', 144 | '#2F54EB': '蓝色', 145 | '#722ED1': '紫色', 146 | '#EB2F96': '玫红', 147 | '#FFE8E6': '红色 1', 148 | '#FFECE0': '朱红 1', 149 | '#FFEFD1': '橙色 1', 150 | '#FCFCCA': '黄色 1', 151 | '#E4F7D2': '绿色 1', 152 | '#D3F5F0': '青色 1', 153 | '#D4EEFC': '浅蓝 1', 154 | '#DEE8FC': '蓝色 1', 155 | '#EFE1FA': '紫色 1', 156 | '#FAE1EB': '玫红 1', 157 | '#FFA39E': '红色 2', 158 | '#FFBB96': '朱红 2', 159 | '#FFD591': '橙色 2', 160 | '#FFFB8F': '黄色 2', 161 | '#B7EB8F': '绿色 2', 162 | '#87E8DE': '青色 2', 163 | '#91D5FF': '浅蓝 2', 164 | '#ADC6FF': '蓝色 2', 165 | '#D3ADF7': '紫色 2', 166 | '#FFADD2': '玫红 2', 167 | '#FF4D4F': '红色 3', 168 | '#FF7A45': '朱红 3', 169 | '#FFA940': '橙色 3', 170 | '#FFEC3D': '黄色 3', 171 | '#73D13D': '绿色 3', 172 | '#36CFC9': '青色 3', 173 | '#40A9FF': '浅蓝 3', 174 | '#597EF7': '蓝色 3', 175 | '#9254DE': '紫色 3', 176 | '#F759AB': '玫红 3', 177 | '#CF1322': '红色 4', 178 | '#D4380D': '朱红 4', 179 | '#D46B08': '橙色 4', 180 | '#D4B106': '黄色 4', 181 | '#389E0D': '绿色 4', 182 | '#08979C': '青色 4', 183 | '#096DD9': '浅蓝 4', 184 | '#1D39C4': '蓝色 4', 185 | '#531DAB': '紫色 4', 186 | '#C41D7F': '玫红 4', 187 | '#820014': '红色 5', 188 | '#871400': '朱红 5', 189 | '#873800': '橙色 5', 190 | '#614700': '黄色 5', 191 | '#135200': '绿色 5', 192 | '#00474F': '青色 5', 193 | '#003A8C': '浅蓝 5', 194 | '#061178': '蓝色 5', 195 | '#22075E': '紫色 5', 196 | '#780650': '玫红 5', 197 | }, 198 | component: { 199 | placeholder: '卡片名称', 200 | }, 201 | image: { 202 | title: '图片', 203 | }, 204 | codeblock: { 205 | title: '代码块', 206 | }, 207 | table: { 208 | title: '表格', 209 | }, 210 | file: { 211 | title: '附件', 212 | }, 213 | video: { 214 | title: '视频', 215 | }, 216 | math: { 217 | title: '公式', 218 | }, 219 | status: { 220 | title: '状态', 221 | }, 222 | mind: { 223 | title: '脑图', 224 | }, 225 | commonlyUsed: { 226 | title: '常用', 227 | }, 228 | searchEmtpy: { 229 | title: '无匹配卡片', 230 | }, 231 | }, 232 | }; 233 | -------------------------------------------------------------------------------- /packages/toolbar/src/plugin/component/collapse.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Keymaster, { deleteScope, setScope, unbind } from 'keymaster'; 3 | import { $, EngineInterface, NodeInterface, Position } from '@aomao/engine'; 4 | import Collapse from '../../components/collapse/collapse.vue'; 5 | import { CollapseGroupProps } from '../../types'; 6 | 7 | export type Options = { 8 | onCancel?: () => void; 9 | onSelect?: (event: MouseEvent, name: string) => void; 10 | }; 11 | 12 | export interface CollapseComponentInterface { 13 | select(index: number): void; 14 | scroll(direction: 'up' | 'down'): void; 15 | unbindEvents(): void; 16 | bindEvents(): void; 17 | remove(): void; 18 | render( 19 | container: NodeInterface, 20 | target: NodeInterface, 21 | data: Array, 22 | ): void; 23 | } 24 | 25 | class CollapseComponent implements CollapseComponentInterface { 26 | private engine: EngineInterface; 27 | private root?: NodeInterface; 28 | private otpions: Options; 29 | private vm?: Vue; 30 | #position?: Position; 31 | private readonly SCOPE_NAME = 'data-toolbar-component'; 32 | 33 | constructor(engine: EngineInterface, options: Options) { 34 | this.otpions = options; 35 | this.engine = engine; 36 | this.#position = new Position(engine); 37 | } 38 | 39 | handlePreventDefault = (event: Event) => { 40 | // Card已被删除 41 | if (this.root?.closest('body').length !== 0) { 42 | event.preventDefault(); 43 | return false; 44 | } 45 | return; 46 | }; 47 | 48 | select(index: number) { 49 | this.root 50 | ?.find('.toolbar-collapse-item-active') 51 | .removeClass('toolbar-collapse-item-active'); 52 | this.root 53 | ?.find('.toolbar-collapse-item') 54 | .eq(index) 55 | ?.addClass('toolbar-collapse-item-active'); 56 | } 57 | 58 | scroll(direction: 'up' | 'down') { 59 | if (!this.root) return; 60 | const items = this.root.find('.toolbar-collapse-item').toArray(); 61 | let activeNode = this.root.find('.toolbar-collapse-item-active'); 62 | const activeIndex = items.findIndex((item) => item.equal(activeNode)); 63 | 64 | let index = direction === 'up' ? activeIndex - 1 : activeIndex + 1; 65 | if (index < 0) { 66 | index = items.length - 1; 67 | } 68 | if (index >= items.length) index = 0; 69 | activeNode = items[index]; 70 | this.select(index); 71 | let offset = 0; 72 | this.root 73 | .find('.toolbar-collapse-group-title,.toolbar-collapse-item') 74 | .each((node) => { 75 | if (activeNode.equal(node)) return false; 76 | offset += (node as Element).clientHeight; 77 | return; 78 | }); 79 | const rootElement = this.root.get()!; 80 | rootElement.scrollTop = offset - rootElement.clientHeight / 2; 81 | } 82 | 83 | unbindEvents() { 84 | deleteScope(this.SCOPE_NAME); 85 | unbind('enter', this.SCOPE_NAME); 86 | unbind('up', this.SCOPE_NAME); 87 | unbind('down', this.SCOPE_NAME); 88 | unbind('esc', this.SCOPE_NAME); 89 | this.engine.off('keydown:enter', this.handlePreventDefault); 90 | } 91 | 92 | bindEvents() { 93 | this.unbindEvents(); 94 | setScope(this.SCOPE_NAME); 95 | //回车 96 | Keymaster('enter', this.SCOPE_NAME, (event) => { 97 | // Card 已被删除 98 | if (this.root?.closest('body').length === 0) { 99 | return; 100 | } 101 | event.preventDefault(); 102 | const active = this.root?.find('.toolbar-collapse-item-active'); 103 | active?.get()?.click(); 104 | }); 105 | 106 | Keymaster('up', this.SCOPE_NAME, (event) => { 107 | // Card 已被删除 108 | if (this.root?.closest('body').length === 0) { 109 | return; 110 | } 111 | event.preventDefault(); 112 | this.scroll('up'); 113 | }); 114 | Keymaster('down', this.SCOPE_NAME, (e) => { 115 | // Card 已被删除 116 | if (this.root?.closest('body').length === 0) { 117 | return; 118 | } 119 | e.preventDefault(); 120 | this.scroll('down'); 121 | }); 122 | Keymaster('esc', this.SCOPE_NAME, (event) => { 123 | event.preventDefault(); 124 | this.unbindEvents(); 125 | const { onCancel } = this.otpions; 126 | if (onCancel) onCancel(); 127 | }); 128 | this.engine.on('keydown:enter', this.handlePreventDefault); 129 | } 130 | 131 | remove() { 132 | if (!this.root || this.root.length === 0) return; 133 | this.#position?.destroy(); 134 | if (this.vm) { 135 | this.vm.$destroy(); 136 | this.vm = undefined 137 | } 138 | this.root.remove(); 139 | this.root = undefined; 140 | } 141 | 142 | render( 143 | container: NodeInterface, 144 | target: NodeInterface, 145 | data: Array, 146 | ) { 147 | this.unbindEvents(); 148 | this.remove(); 149 | this.root = $('
'); 150 | container.append(this.root); 151 | 152 | const rootElement = this.root.get()!; 153 | 154 | const { onSelect } = this.otpions; 155 | if (data.length > 0) { 156 | const engine = this.engine 157 | this.vm = new Vue({ 158 | render: (h) => { 159 | return h(Collapse, { 160 | props: { 161 | engine, 162 | groups: data, 163 | onSelect, 164 | } 165 | }) 166 | } 167 | }) 168 | rootElement.append(this.vm!.$mount().$el) 169 | } else { 170 | this.root.append( 171 | `
${this.engine.language.get( 172 | 'toolbar', 173 | 'searchEmtpy', 174 | 'title', 175 | )}
`, 176 | ); 177 | } 178 | this.bindEvents(); 179 | this.#position?.bind(this.root!, target); 180 | 181 | setTimeout(() => { 182 | this.select(0); 183 | }, 0); 184 | } 185 | } 186 | 187 | export default CollapseComponent; 188 | -------------------------------------------------------------------------------- /packages/toolbar/src/plugin/component/index.css: -------------------------------------------------------------------------------- 1 | .data-toolbar-component-list { 2 | position: absolute; 3 | min-height: 0px; 4 | top: 10px; 5 | left: 0; 6 | } 7 | 8 | .data-toolbar-component-list .toolbar-dropdown-list { 9 | top:0px; 10 | position: relative; 11 | } 12 | 13 | .data-toolbar-component-placeholder { 14 | color: rgba(0,0,0,0.25); 15 | pointer-events: none; 16 | width: 76px; 17 | } 18 | 19 | .data-toolbar-component-list-empty { 20 | position: relative; 21 | font-size: 14px; 22 | background: #ffffff; 23 | border: 1px solid #e8e8e8; 24 | border-radius: 3px 3px; 25 | box-shadow: 0 2px 10px rgb(0 0 0 / 12%); 26 | padding: 5px 16px; 27 | line-height: 32px; 28 | min-width: 200px; 29 | height: auto; 30 | transition: all 0.25s cubic-bezier(0.3, 1.2, 0.2, 1); 31 | z-index: 999; 32 | max-height: calc(80vh); 33 | overflow: auto; 34 | } 35 | /** ------------------- popup ---------------------- **/ 36 | .data-toolbar-popup-wrapper { 37 | position: absolute; 38 | z-index: 9999; 39 | padding: 4px; 40 | background-color: #fff; 41 | border-radius: 4px; 42 | border: 1px solid #dee0e3; 43 | box-shadow: 0 4px 8px 0 rgba(31, 35, 41, 0.1); 44 | } 45 | 46 | .data-toolbar-popup-wrapper .editor-toolbar-popover { 47 | width: max-content; 48 | } 49 | 50 | .data-toolbar-popup-wrapper .editor-toolbar-popover .ant-popover-inner-content { 51 | padding: 4px; 52 | background-color: #fff; 53 | border-radius: 4px; 54 | border: 1px solid #dee0e3; 55 | } 56 | 57 | .data-toolbar-popup-wrapper .editor-toolbar-popover .ant-popover-arrow { 58 | display: none; 59 | } -------------------------------------------------------------------------------- /packages/toolbar/src/plugin/component/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | $, 3 | Card, 4 | isEngine, 5 | NodeInterface, 6 | isHotkey, 7 | CardType, 8 | isServer, 9 | CardValue, 10 | } from "@aomao/engine"; 11 | import { 12 | CollapseGroupProps, 13 | CollapseItemProps, 14 | CollapseProps, 15 | } from "../../types"; 16 | import { getToolbarDefaultConfig } from "../../config"; 17 | import CollapseComponent, { CollapseComponentInterface } from "./collapse"; 18 | import ToolbarPopup from "./popup"; 19 | import "./index.css"; 20 | 21 | type Data = Array; 22 | 23 | export interface ToolbarValue extends CardValue { 24 | data: Data; 25 | } 26 | 27 | class ToolbarComponent extends Card { 28 | private keyword?: NodeInterface; 29 | private placeholder?: NodeInterface; 30 | private component?: CollapseComponentInterface; 31 | #collapseData?: Data; 32 | #data?: any; 33 | 34 | static get cardName() { 35 | return "toolbar"; 36 | } 37 | 38 | static get cardType() { 39 | return CardType.INLINE; 40 | } 41 | 42 | static get singleSelectable() { 43 | return false; 44 | } 45 | 46 | static get autoSelected() { 47 | return false; 48 | } 49 | 50 | init() { 51 | if (!isEngine(this.editor) || isServer) { 52 | return; 53 | } 54 | 55 | this.component = new CollapseComponent(this.editor, { 56 | onCancel: () => { 57 | this.changeToText(); 58 | }, 59 | onSelect: () => { 60 | this.remove(); 61 | }, 62 | }); 63 | } 64 | 65 | setData(_data: any) { 66 | this.#data = _data; 67 | } 68 | 69 | getData(): Data { 70 | if (!isEngine(this.editor)) { 71 | return []; 72 | } 73 | const data: 74 | | Data 75 | | { title: any; items: Omit[] }[] = []; 76 | const defaultConfig = getToolbarDefaultConfig(this.editor); 77 | const collapseConfig = defaultConfig.find( 78 | ({ type }) => type === "collapse" 79 | ); 80 | let collapseGroups: Array = []; 81 | if (collapseConfig) 82 | collapseGroups = (collapseConfig as CollapseProps).groups; 83 | const collapseItems: Array> = []; 84 | collapseGroups.forEach((group) => { 85 | collapseItems.push(...group.items); 86 | }); 87 | const value = this.getValue(); 88 | (this.#data || (value ? value.data : []) || []).forEach((group: any) => { 89 | const title = group.title; 90 | const items: Array> = []; 91 | group.items.forEach((item: any) => { 92 | let name = item; 93 | if (typeof item !== "string") name = item.name; 94 | const collapseItem = collapseItems.find((item) => item.name === name); 95 | if (collapseItem) { 96 | items.push({ 97 | ...collapseItem, 98 | ...(typeof item !== "string" ? item : {}), 99 | disabled: collapseItem.onDisabled 100 | ? collapseItem.onDisabled() 101 | : !this.editor.command.queryEnabled(name), 102 | }); 103 | } else if (typeof item === 'object') items.push(item); 104 | }); 105 | data.push({ 106 | title, 107 | items, 108 | }); 109 | }); 110 | return data; 111 | } 112 | 113 | /** 114 | * 查询 115 | * @param keyword 关键字 116 | * @returns 117 | */ 118 | search(keyword: string) { 119 | const items: Array> = []; 120 | // search with case insensitive 121 | if (typeof keyword === "string") keyword = keyword.toLowerCase(); 122 | // 已经有值了就不用赋值了,不然会影响禁用状态 123 | 124 | if (!this.#collapseData) this.#collapseData = []; 125 | this.#collapseData.forEach((group) => { 126 | group.items.forEach((item) => { 127 | if (item.search && item.search.toLowerCase().indexOf(keyword) >= 0) { 128 | if (!items.find(({ name }) => name === item.name)) { 129 | items.push({ ...item }); 130 | } 131 | } 132 | }); 133 | }); 134 | const data = []; 135 | if (items.length > 0) { 136 | data.push({ 137 | title: "", 138 | items: items, 139 | }); 140 | } 141 | return data; 142 | } 143 | 144 | remove() { 145 | if (!isEngine(this.editor)) return; 146 | this.component?.remove(); 147 | this.editor.card.remove(this.id); 148 | } 149 | 150 | changeToText() { 151 | if (!this.root.inEditor() || !isEngine(this.editor)) { 152 | return; 153 | } 154 | 155 | const content = this.keyword?.get()?.innerText || ""; 156 | this.remove(); 157 | this.editor.node.insertText(content); 158 | } 159 | 160 | destroy() { 161 | this.component?.unbindEvents(); 162 | this.component?.remove(); 163 | } 164 | 165 | activate(activated: boolean) { 166 | super.activate(activated); 167 | if (!activated) { 168 | this.component?.unbindEvents(); 169 | this.changeToText(); 170 | } 171 | } 172 | 173 | handleInput() { 174 | if (!isEngine(this.editor)) return; 175 | const { change, card } = this.editor; 176 | if (change.isComposing()) { 177 | return; 178 | } 179 | const content = 180 | this.keyword?.get()?.innerText.replace(/[\r\n]/g, "") || ""; 181 | // 内容为空 182 | if (content === "") { 183 | this.component?.remove(); 184 | card.remove(this.id); 185 | return; 186 | } 187 | 188 | const keyword = content.substr(1); 189 | // 搜索关键词为空 190 | if (keyword === "") { 191 | this.component?.render( 192 | this.editor.root, 193 | this.root, 194 | this.#collapseData || [] 195 | ); 196 | return; 197 | } 198 | const data = this.search(keyword); 199 | this.component?.render(this.editor.root, this.root, data); 200 | } 201 | 202 | resetPlaceHolder() { 203 | if ("/" === this.keyword?.get()?.innerText) 204 | this.placeholder?.show(); 205 | else this.placeholder?.hide(); 206 | } 207 | 208 | render(data?: any): string | void | NodeInterface { 209 | this.setData(data); 210 | const editor = this.editor; 211 | if (!isEngine(editor) || isServer) return; 212 | const language = editor.language.get<{ placeholder: string }>( 213 | "toolbar", 214 | "component" 215 | ); 216 | this.root.attributes("data-transient", "true"); 217 | this.root.attributes("contenteditable", "false"); 218 | // 编辑模式 219 | const container = $( 220 | `/${language["placeholder"]}` 221 | ); 222 | const center = this.getCenter(); 223 | center.empty().append(container); 224 | this.keyword = center.find(".data-toolbar-component-keyword"); 225 | this.placeholder = center.find(".data-toolbar-component-placeholder"); 226 | // 监听输入事件 227 | this.keyword?.on("keydown", (e) => { 228 | if (isHotkey("enter", e)) { 229 | e.preventDefault(); 230 | } 231 | }); 232 | const renderTime = Date.now(); 233 | this.keyword?.on("input", () => { 234 | this.resetPlaceHolder(); 235 | // 在 Windows 上使用中文输入法,在 keydown 事件里无法阻止用户的输入,所以在这里删除用户的输入 236 | if (Date.now() - renderTime < 200) { 237 | const textNode = this.keyword?.first(); 238 | if ( 239 | (textNode && textNode.isText() && textNode[0].nodeValue === "/、") || 240 | textNode?.get()?.nodeValue === "//" 241 | ) { 242 | const text = textNode.get()?.splitText(1); 243 | text?.remove(); 244 | } 245 | } 246 | 247 | setTimeout(() => { 248 | this.handleInput(); 249 | }, 10); 250 | }); 251 | if (!this.#collapseData) this.#collapseData = this.getData(); 252 | // 显示下拉列表 253 | this.component?.render(editor.root, this.root, this.#collapseData); 254 | } 255 | } 256 | 257 | export default ToolbarComponent; 258 | export { ToolbarPopup }; 259 | -------------------------------------------------------------------------------- /packages/toolbar/src/plugin/component/popup.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { $, isEngine, isMobile, Range, UI_SELECTOR } from '@aomao/engine'; 3 | import type { NodeInterface, EditorInterface } from '@aomao/engine'; 4 | import Toolbar from '../../components/toolbar.vue'; 5 | import type { GroupItemProps } from '../../types'; 6 | 7 | type PopupOptions = { 8 | items?: GroupItemProps[]; 9 | }; 10 | 11 | export default class Popup { 12 | #editor: EditorInterface; 13 | #root: NodeInterface; 14 | #point: Record<'left' | 'top', number> = { left: 0, top: -9999 }; 15 | #align: 'top' | 'bottom' = 'bottom'; 16 | #options: PopupOptions = {}; 17 | #vm?: Vue; 18 | 19 | constructor(editor: EditorInterface, options: PopupOptions = {}) { 20 | this.#options = options; 21 | this.#editor = editor; 22 | this.#root = $(`
`); 23 | (this.#editor.scrollNode?.get() || document.body).appendChild( 24 | this.#root[0] 25 | ); 26 | if (isEngine(editor)) { 27 | this.#editor.on('selectEnd', this.onSelect); 28 | } else { 29 | document.addEventListener('selectionchange', this.onSelect); 30 | } 31 | if (!isMobile) window.addEventListener('scroll', this.onSelect); 32 | window.addEventListener('resize', this.onSelect); 33 | this.#editor.scrollNode?.on('scroll', this.onSelect); 34 | document.addEventListener('mousedown', this.hide); 35 | } 36 | 37 | onSelect = () => { 38 | const range = Range.from(this.#editor)?.cloneRange().shrinkToTextNode(); 39 | const selection = window.getSelection(); 40 | if ( 41 | !range || 42 | !selection || 43 | !selection.focusNode || 44 | range.collapsed || 45 | this.#editor.card.getSingleSelectedCard(range) || 46 | (!range.commonAncestorNode.inEditor(this.#editor.container) && 47 | !range.commonAncestorNode.isRoot(this.#editor.container)) 48 | ) { 49 | this.hide(); 50 | return; 51 | } 52 | const next = range.startNode.next(); 53 | if ( 54 | next?.isElement() && 55 | Math.abs(range.endOffset - range.startOffset) === 1 56 | ) { 57 | const component = this.#editor.card.closest(next); 58 | if (component) { 59 | this.hide(); 60 | return; 61 | } 62 | } 63 | const prev = range.startNode.prev(); 64 | if ( 65 | prev?.isElement() && 66 | Math.abs(range.startOffset - range.endOffset) === 1 67 | ) { 68 | const component = this.#editor.card.closest(prev); 69 | if (component) { 70 | this.hide(); 71 | return; 72 | } 73 | } 74 | const subRanges = range.getSubRanges(); 75 | if ( 76 | subRanges.length === 0 || 77 | (this.#editor.card.active && !this.#editor.card.active.isEditable) 78 | ) { 79 | this.hide(); 80 | return; 81 | } 82 | const topRange = subRanges[0]; 83 | const bottomRange = subRanges[subRanges.length - 1]; 84 | const topRect = topRange 85 | .cloneRange() 86 | .collapse(true) 87 | .getBoundingClientRect(); 88 | const bottomRect = bottomRange 89 | .cloneRange() 90 | .collapse(false) 91 | .getBoundingClientRect(); 92 | 93 | let rootRect: DOMRect | undefined = undefined; 94 | this.showContent(() => { 95 | rootRect = this.#root.get()?.getBoundingClientRect(); 96 | if (!rootRect) { 97 | this.hide(); 98 | return; 99 | } 100 | this.#align = 101 | bottomRange.startNode.equal(selection.focusNode!) && 102 | (!topRange.startNode.equal(selection.focusNode!) || 103 | selection.focusOffset > selection.anchorOffset) 104 | ? 'bottom' 105 | : 'top'; 106 | const space = 12; 107 | let targetRect = this.#align === 'bottom' ? bottomRect : topRect; 108 | if ( 109 | this.#align === 'top' && 110 | targetRect.top - rootRect.height - space < 111 | window.innerHeight - (this.#editor.scrollNode?.height() || 0) 112 | ) { 113 | this.#align = 'bottom'; 114 | } else if ( 115 | this.#align === 'bottom' && 116 | targetRect.bottom + rootRect.height + space > window.innerHeight 117 | ) { 118 | this.#align = 'top'; 119 | } 120 | targetRect = this.#align === 'bottom' ? bottomRect : topRect; 121 | const scrollElement = this.#editor.scrollNode?.get(); 122 | const scrollNodeRect = scrollElement?.getBoundingClientRect(); 123 | const top = 124 | this.#align === 'top' 125 | ? targetRect.top - 126 | rootRect.height - 127 | space - 128 | (scrollNodeRect?.top || 0) + 129 | (scrollElement?.scrollTop || 0) 130 | : targetRect.bottom + 131 | space - 132 | (scrollNodeRect?.top || 0) + 133 | (scrollElement?.scrollTop || 0); 134 | 135 | let left = 136 | targetRect.left - 137 | (scrollNodeRect?.left || 0) + 138 | (scrollElement?.scrollLeft || 0) + 139 | targetRect.width - 140 | rootRect.width / 2; 141 | if (left < 0) left = 16; 142 | this.#point = { 143 | left, 144 | top, 145 | }; 146 | this.#root.css({ 147 | left: `${this.#point.left}px`, 148 | top: `${this.#point.top}px`, 149 | }); 150 | }); 151 | }; 152 | 153 | showContent(callback?: () => void) { 154 | const result = this.#editor.trigger('toolbar-render', this.#options); 155 | if (!result && (this.#options.items || []).length === 0) { 156 | this.#vm?.$destroy(); 157 | this.#vm = undefined; 158 | this.hide(); 159 | return; 160 | } 161 | let content = Toolbar; 162 | if (typeof result === 'object') { 163 | this.#vm?.$destroy(); 164 | this.#vm = undefined; 165 | content = result; 166 | } 167 | if (!this.#vm) { 168 | this.#vm = new Vue({ 169 | render: (h) => { 170 | return h(content, { 171 | props: { 172 | ...this.#options, 173 | engine: this.#editor, 174 | popup: true, 175 | }, 176 | }); 177 | }, 178 | }); 179 | this.#root.empty().append(this.#vm.$mount().$el); 180 | } 181 | setTimeout(() => { 182 | if (callback) callback(); 183 | }, 200); 184 | } 185 | 186 | hide = (event?: MouseEvent) => { 187 | if (event?.target) { 188 | const target = $(event.target); 189 | if ( 190 | target.closest('.data-toolbar-popup-wrapper').length > 0 || 191 | target.closest(UI_SELECTOR).length > 0 192 | ) 193 | return; 194 | } 195 | this.#root.css({ 196 | left: '0px', 197 | top: '-9999px', 198 | }); 199 | }; 200 | 201 | destroy() { 202 | this.#root.remove(); 203 | if (isEngine(this.#editor)) { 204 | this.#editor.off('select', this.onSelect); 205 | } else { 206 | document.removeEventListener('selectionchange', this.onSelect); 207 | } 208 | if (!isMobile) window.removeEventListener('scroll', this.onSelect); 209 | window.removeEventListener('resize', this.onSelect); 210 | this.#editor.scrollNode?.off('scroll', this.onSelect); 211 | document.removeEventListener('mousedown', this.hide); 212 | if (this.#vm) { 213 | this.#vm.$destroy(); 214 | this.#vm = undefined; 215 | } 216 | } 217 | } 218 | export type { GroupItemProps }; 219 | -------------------------------------------------------------------------------- /packages/toolbar/src/plugin/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EditorInterface, 3 | isEngine, 4 | isSafari, 5 | NodeInterface, 6 | Plugin, 7 | PluginOptions, 8 | } from '@aomao/engine'; 9 | import { CollapseItemProps, GroupItemProps } from '../types'; 10 | import locales from '../locales'; 11 | import ToolbarComponent, { ToolbarPopup } from './component'; 12 | import type { ToolbarValue } from './component'; 13 | 14 | type Config = Array<{ 15 | title: string; 16 | items: Array | string>; 17 | }>; 18 | export interface ToolbarOptions extends PluginOptions { 19 | config?: Config; 20 | popup?: { 21 | items: GroupItemProps[]; 22 | }; 23 | } 24 | 25 | const defaultConfig = (editor: EditorInterface): Config => { 26 | return [ 27 | { 28 | title: editor.language.get( 29 | 'toolbar', 30 | 'commonlyUsed', 31 | 'title', 32 | ), 33 | items: [ 34 | 'image-uploader', 35 | 'codeblock', 36 | 'table', 37 | 'file-uploader', 38 | 'video-uploader', 39 | 'math', 40 | 'status', 41 | ], 42 | }, 43 | ]; 44 | }; 45 | 46 | class ToolbarPlugin< 47 | T extends ToolbarOptions = ToolbarOptions, 48 | > extends Plugin { 49 | static get pluginName() { 50 | return 'toolbar'; 51 | } 52 | private popup?: ToolbarPopup; 53 | 54 | init() { 55 | if (isEngine(this.editor)) { 56 | this.editor.on('keydown:slash', this.onSlash); 57 | this.editor.on('parse:value', this.paserValue); 58 | } 59 | this.editor.language.add(locales); 60 | if (this.options.popup) { 61 | this.popup = new ToolbarPopup(this.editor, { 62 | items: this.options.popup.items, 63 | }); 64 | } 65 | } 66 | 67 | paserValue = (node: NodeInterface) => { 68 | if ( 69 | node.isCard() && 70 | node.attributes('name') === ToolbarComponent.cardName 71 | ) { 72 | return false; 73 | } 74 | return true; 75 | }; 76 | 77 | onSlash = (event: KeyboardEvent) => { 78 | if (!isEngine(this.editor)) return; 79 | const { change } = this.editor; 80 | let range = change.range.get(); 81 | const block = this.editor.block.closest(range.startNode); 82 | const text = block.text().trim(); 83 | if (text === '/' && isSafari) { 84 | block.empty(); 85 | } 86 | 87 | if ( 88 | '' === text || 89 | ('/' === text && isSafari) || 90 | event.ctrlKey || 91 | event.metaKey 92 | ) { 93 | range = change.range.get(); 94 | if (range.collapsed) { 95 | event.preventDefault(); 96 | const data = this.options.config || defaultConfig(this.editor); 97 | const card = this.editor.card.insert( 98 | ToolbarComponent.cardName, 99 | {}, 100 | data, 101 | ) as ToolbarComponent; 102 | card.setData(data); 103 | this.editor.card.activate(card.root); 104 | range = change.range.get(); 105 | //选中关键词输入节点 106 | const keyword = card.find('.data-toolbar-component-keyword'); 107 | range.select(keyword, true); 108 | range.collapse(false); 109 | change.range.select(range); 110 | } 111 | } 112 | }; 113 | 114 | execute(...args: any): void { 115 | throw new Error('Method not implemented.'); 116 | } 117 | 118 | destroy() { 119 | this.popup?.destroy(); 120 | this.editor.off('keydown:slash', this.onSlash); 121 | this.editor.off('parse:value', this.paserValue); 122 | } 123 | } 124 | export { ToolbarComponent }; 125 | export type { ToolbarValue }; 126 | export default ToolbarPlugin; 127 | -------------------------------------------------------------------------------- /packages/toolbar/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /packages/toolbar/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { EngineInterface, Placement } from '@aomao/engine'; 2 | import { PropType, VNode } from 'vue'; 3 | 4 | //命令 5 | export type Command = 6 | | { name: string; args: Array } 7 | | Array 8 | | undefined; 9 | //按钮 10 | export const buttonProps = { 11 | engine: Object as PropType, 12 | name: { 13 | type: String, 14 | required: true, 15 | } as const, 16 | icon: String, 17 | content: [String, Function] as PropType string) | VNode>, 18 | title: String, 19 | placement: String as PropType, 20 | hotkey: [String, Object] as PropType, 21 | command: Object as PropType, 22 | autoExecute: { 23 | type: [Boolean, undefined] as PropType, 24 | default: undefined, 25 | }, 26 | className: String, 27 | active: { 28 | type: [Boolean, undefined] as PropType, 29 | default: undefined, 30 | }, 31 | disabled: { 32 | type: [Boolean, undefined] as PropType, 33 | default: undefined, 34 | }, 35 | onClick: Function as PropType<(event: MouseEvent) => void | boolean>, 36 | onMouseDown: Function as PropType<(event: MouseEvent) => void | boolean>, 37 | onMouseEnter: Function as PropType<(event: MouseEvent) => void | boolean>, 38 | onMouseLevel: Function as PropType<(event: MouseEvent) => void | boolean>, 39 | }; 40 | //按钮 41 | export type ButtonProps = { 42 | engine?: EngineInterface 43 | name: string 44 | icon?: string 45 | content?: string | (() => string) | VNode 46 | title?: string, 47 | placement?: Placement, 48 | hotkey?: boolean | string, 49 | command?: Command, 50 | autoExecute?: boolean, 51 | className?: string, 52 | active?: boolean, 53 | disabled?: boolean, 54 | onClick?: (event: MouseEvent) => void | boolean 55 | onMouseDown?: (event: MouseEvent) => void | boolean 56 | onMouseEnter?: (event: MouseEvent) => void | boolean 57 | onMouseLevel?: (event: MouseEvent) => void | boolean 58 | }; 59 | 60 | //增加type 61 | export type GroupButtonProps = { 62 | type: 'button'; 63 | values?: any; 64 | } & Omit; 65 | //下拉项 66 | export type DropdownListItem = { 67 | key: string; 68 | icon?: string; 69 | content?: string | (() => string); 70 | hotkey?: boolean | string; 71 | isDefault?: boolean; 72 | disabled?: boolean; 73 | title?: string; 74 | placement?: Placement; 75 | className?: string; 76 | command?: { name: string; args: any[] } | any[]; 77 | autoExecute?: boolean; 78 | }; 79 | //下拉列表 80 | export type DropdownListProps = { 81 | engine?: EngineInterface 82 | name: string 83 | direction?: 'vertical' | 'horizontal' 84 | items: DropdownListItem[] 85 | values: string | number | string[] 86 | className?: string 87 | onSelect?: (event: MouseEvent, key: string) => void | boolean 88 | hasDot?: boolean 89 | }; 90 | //下拉 91 | export type DropdownProps = { 92 | engine?: EngineInterface 93 | name: string 94 | values?: string | number | string[] 95 | items: DropdownListItem[] 96 | icon?: string 97 | content?: string | (() => string) 98 | title?: string 99 | disabled?: boolean 100 | single?: boolean 101 | className?: string 102 | direction?: 'vertical' | 'horizontal' 103 | onSelect?: (event: MouseEvent, key: string) => void | boolean 104 | hasArrow?: boolean 105 | hasDot?: boolean 106 | placement?: Placement 107 | }; 108 | 109 | export type GroupDropdownProps = { 110 | type: 'dropdown'; 111 | } & Omit; 112 | 113 | //颜色 114 | export type ColorPickerItemProps = { 115 | engine: EngineInterface 116 | color: string 117 | active: boolean 118 | setStroke?: boolean 119 | onSelect?: (color: string, event: MouseEvent) => void 120 | } 121 | //颜色分组 122 | export type ColorPickerGroupProps = { 123 | engine: EngineInterface 124 | colors: { value: string; active: boolean }[] 125 | setStroke?: boolean 126 | onSelect?: (color: string, event: MouseEvent) => void 127 | }; 128 | 129 | 130 | //picker 131 | export type ColorPickerProps = { 132 | engine: EngineInterface 133 | colors?: { value: string; active: boolean }[] 134 | defaultColor: string 135 | defaultActiveColor: string 136 | setStroke?: boolean 137 | placement?: Placement 138 | onSelect?: (color: string, event: MouseEvent) => void 139 | }; 140 | 141 | //color 142 | export type ColorProps = { 143 | engine?: EngineInterface 144 | name: string 145 | content: string | ((color: string, stroke: string, disabled?: boolean) => string) 146 | buttonTitle?: string 147 | dropdownTitle?: string 148 | command?: Command, 149 | autoExecute?: boolean, 150 | disabled?: boolean, 151 | } & Omit 152 | 153 | export type GroupColorProps = { 154 | type: 'color'; 155 | } & Omit; 156 | 157 | //collapse item 158 | export type CollapseItemProps = { 159 | engine?: EngineInterface 160 | name: string 161 | icon?: string 162 | search: string, 163 | description?: string | (() => string) | VNode 164 | disabled?: boolean 165 | prompt?: string | ((props: any) => string) | ((props: any) => VNode) | VNode 166 | title?: string; 167 | placement?: Placement; 168 | className?: string; 169 | command?: { name: string; args: any[] } | any[]; 170 | autoExecute?: boolean; 171 | onClick?: (event: MouseEvent, name: string, engine?: EngineInterface) => boolean | void 172 | onMouseDown?: (event: MouseEvent) => void 173 | onDisabled?: () => boolean; 174 | }; 175 | 176 | //collapse group 177 | export type CollapseGroupProps = { 178 | engine?: EngineInterface 179 | title?: string 180 | items: Omit[] 181 | onSelect?: (event: MouseEvent, name: string) => boolean | void 182 | }; 183 | 184 | //collapse 185 | export type CollapseProps = { 186 | engine?: EngineInterface 187 | header?: string, 188 | groups: CollapseGroupProps[], 189 | disabled?: boolean 190 | className?: string 191 | icon?: string 192 | content?: string | (() => string) | VNode 193 | onSelect?: (event: MouseEvent, name: string) => boolean | void 194 | }; 195 | 196 | export type ToolbarCollapseGroupProps = { 197 | type: 'collapse'; 198 | } & Omit; 199 | 200 | export type GroupProps = { 201 | engine: EngineInterface 202 | items?: (GroupButtonProps 203 | | GroupDropdownProps 204 | | GroupColorProps 205 | | ToolbarCollapseGroupProps | string)[] 206 | icon?: string 207 | content: string | (() => string) | VNode 208 | }; 209 | 210 | export type ToolbarButtonProps = { 211 | onActive?: () => boolean; 212 | onDisabled?: () => boolean; 213 | } & GroupButtonProps; 214 | 215 | export type ToolbarDropdownProps = { 216 | onActive?: (items: Array) => string | Array; 217 | onDisabled?: () => boolean; 218 | } & GroupDropdownProps; 219 | 220 | export type ToolbarColorProps = { 221 | onActive?: () => string | Array; 222 | onDisabled?: () => boolean; 223 | } & GroupColorProps; 224 | 225 | export type ToolbarItemProps = 226 | | ToolbarButtonProps 227 | | ToolbarDropdownProps 228 | | ToolbarColorProps 229 | | ToolbarCollapseGroupProps; 230 | 231 | export type GroupItemDataProps = { 232 | icon?: string; 233 | content?: string | (() => string) | VNode; 234 | items: Array; 235 | }; 236 | 237 | export type GroupItemProps = 238 | | Array< 239 | | ToolbarItemProps 240 | | string 241 | | (Omit & { 242 | groups: Array< 243 | Omit & { 244 | items: Array< 245 | string | Omit 246 | >; 247 | } 248 | >; 249 | }) 250 | > 251 | | GroupItemDataProps; 252 | 253 | export type GroupDataProps = Omit & { 254 | items: Array; 255 | }; 256 | 257 | export type ToolbarProps = { 258 | engine: EngineInterface 259 | items: Array 260 | className?: string 261 | } 262 | -------------------------------------------------------------------------------- /packages/toolbar/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { EngineInterface } from '@aomao/engine'; 2 | 3 | export const autoGetHotkey = ( 4 | engine: EngineInterface, 5 | name: string, 6 | itemKey?: string, 7 | ) => { 8 | const plugin = engine?.plugin.components[name]; 9 | if (plugin && plugin.hotkey) { 10 | let key = plugin.hotkey(); 11 | if (key) { 12 | if (Array.isArray(key)) { 13 | if (itemKey) { 14 | const index = key.findIndex( 15 | (k: any) => typeof k === 'object' && k.args === itemKey, 16 | ); 17 | key = key[index > -1 ? index : 0]; 18 | } else { 19 | key = key[0]; 20 | } 21 | } 22 | if (typeof key === 'object') { 23 | key = key.key; 24 | } 25 | return key; 26 | } 27 | } 28 | return; 29 | }; 30 | 31 | /** 32 | * 是否支持字体 33 | * @param font 字体名称 34 | * @returns 35 | */ 36 | export const isSupportFontFamily = (font: string) => { 37 | if (typeof font !== 'string') { 38 | console.log('Font name is not legal !'); 39 | return false; 40 | } 41 | 42 | let width; 43 | const body = document.body; 44 | 45 | const container = document.createElement('span'); 46 | container.innerHTML = Array(10).join('wi'); 47 | container.style.cssText = [ 48 | 'position:absolute', 49 | 'width:auto', 50 | 'font-size:128px', 51 | 'left:-99999px', 52 | ].join(' !important;'); 53 | 54 | const getWidth = (fontFamily: string) => { 55 | container.style.fontFamily = fontFamily; 56 | body.appendChild(container); 57 | width = container.clientWidth; 58 | body.removeChild(container); 59 | 60 | return width; 61 | }; 62 | 63 | const monoWidth = getWidth('monospace'); 64 | const serifWidth = getWidth('serif'); 65 | const sansWidth = getWidth('sans-serif'); 66 | 67 | return ( 68 | monoWidth !== getWidth(font + ',monospace') || 69 | sansWidth !== getWidth(font + ',sans-serif') || 70 | serifWidth !== getWidth(font + ',serif') 71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /packages/toolbar/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "preserve", 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "experimentalDecorators": true, 11 | "noImplicitReturns": true, 12 | "declaration": true, 13 | "suppressImplicitAnyIndexErrors": true, 14 | "esModuleInterop": true, 15 | "sourceMap": true, 16 | "baseUrl": "./", 17 | "strict": true, 18 | "paths": { 19 | "@/*": ["src/*"] 20 | }, 21 | "allowSyntheticDefaultImports": true, 22 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 23 | }, 24 | "include": ["src/*.ts", "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], 25 | "exclude": [ 26 | "node_modules", 27 | "lib", 28 | "es", 29 | "dist", 30 | "docs-dist", 31 | "typings", 32 | "**/__test__", 33 | "test", 34 | "fixtures" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zb201307/am-editor-vue2/a15c9fdf4bcb43ccc1b9345159785d387ee3b346/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { existsSync } = require('fs'); 4 | const { join } = require('path'); 5 | const yParser = require('yargs-parser'); 6 | const chalk = require('chalk'); 7 | const signale = require('signale'); 8 | 9 | // print version and @local 10 | const args = yParser(process.argv.slice(2)); 11 | if (args.v || args.version) { 12 | console.log(require('father-build/package').version); 13 | if (existsSync(join(__dirname, '../.local'))) { 14 | console.log(chalk.cyan('@local')); 15 | } 16 | process.exit(0); 17 | } 18 | 19 | // Notify update when process exits 20 | const updater = require('update-notifier'); 21 | const pkg = require('father-build/package.json'); 22 | updater({ pkg }).notify({ defer: true }); 23 | 24 | function stripEmptyKeys(obj) { 25 | Object.keys(obj).forEach((key) => { 26 | if (!obj[key] || (Array.isArray(obj[key]) && !obj[key].length)) { 27 | delete obj[key]; 28 | } 29 | }); 30 | return obj; 31 | } 32 | 33 | function build() { 34 | // Parse buildArgs from cli 35 | const buildArgs = stripEmptyKeys({ 36 | esm: args.esm && { type: args.esm === true ? 'rollup' : args.esm }, 37 | cjs: args.cjs && { type: args.cjs === true ? 'rollup' : args.cjs }, 38 | umd: args.umd && { name: args.umd === true ? undefined : args.umd }, 39 | file: args.file, 40 | target: args.target, 41 | entry: args._, 42 | }); 43 | 44 | if (buildArgs.file && buildArgs.entry && buildArgs.entry.length > 1) { 45 | signale.error(new Error( 46 | `Cannot specify file when have multiple entries (${buildArgs.entry.join(', ')})` 47 | )); 48 | process.exit(1); 49 | } 50 | 51 | require('./rollup-build')({ 52 | cwd: args.root || process.cwd(), 53 | watch: args.w || args.watch, 54 | buildArgs, 55 | }).catch(e => { 56 | signale.error(e); 57 | process.exit(1); 58 | }); 59 | } 60 | 61 | build(); 62 | -------------------------------------------------------------------------------- /scripts/rollup-build.js: -------------------------------------------------------------------------------- 1 | const { existsSync, readFileSync } = require('fs'); 2 | const { join, basename } = require('path'); 3 | const rimraf = require('rimraf'); 4 | const assert = require('assert'); 5 | const { merge } = require('lodash'); 6 | const signale = require('signale'); 7 | const chalk = require('chalk'); 8 | const babel = require('father-build/lib/babel'); 9 | const rollup = require('./rollup'); 10 | const registerBabel = require('father-build/lib/registerBabel').default; 11 | const { getExistFile, getLernaPackages } = require('father-build/lib/utils'); 12 | const UserConfig = require('father-build/lib/getUserConfig'); 13 | const randomColor = require("father-build/lib/randomColor").default; 14 | const getUserConfig = UserConfig.default 15 | const { CONFIG_FILES } = UserConfig 16 | 17 | function getBundleOpts(opts) { 18 | const { cwd, buildArgs = {}, rootConfig = {} } = opts; 19 | const entry = getExistFile({ 20 | cwd, 21 | files: ['src/index.tsx', 'src/index.ts', 'src/index.jsx', 'src/index.js'], 22 | returnRelative: true, 23 | }); 24 | const userConfig = getUserConfig({ cwd }); 25 | const userConfigs = Array.isArray(userConfig) ? userConfig : [userConfig]; 26 | return userConfigs.map(userConfig => { 27 | const bundleOpts = merge( 28 | { 29 | entry, 30 | }, 31 | rootConfig, 32 | userConfig, 33 | buildArgs, 34 | ); 35 | 36 | // Support config esm: 'rollup' and cjs: 'rollup' 37 | if (typeof bundleOpts.esm === 'string') { 38 | bundleOpts.esm = { type: bundleOpts.esm }; 39 | } 40 | if (typeof bundleOpts.cjs === 'string') { 41 | bundleOpts.cjs = { type: bundleOpts.cjs }; 42 | } 43 | 44 | return bundleOpts; 45 | }); 46 | } 47 | 48 | function validateBundleOpts(bundleOpts, { cwd, rootPath }) { 49 | if (bundleOpts.runtimeHelpers) { 50 | const pkgPath = join(cwd, 'package.json'); 51 | assert.ok(existsSync(pkgPath), `@babel/runtime dependency is required to use runtimeHelpers`); 52 | const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); 53 | assert.ok( 54 | (pkg.dependencies || {})['@babel/runtime'], 55 | `@babel/runtime dependency is required to use runtimeHelpers`, 56 | ); 57 | } 58 | if (bundleOpts.cjs && (bundleOpts.cjs).lazy && (bundleOpts.cjs).type === 'rollup') { 59 | throw new Error(` 60 | cjs.lazy don't support rollup. 61 | `.trim()); 62 | } 63 | if (!bundleOpts.esm && !bundleOpts.cjs && !bundleOpts.umd) { 64 | throw new Error( 65 | ` 66 | None format of ${chalk.cyan( 67 | 'cjs | esm | umd', 68 | )} is configured, checkout https://github.com/umijs/father for usage details. 69 | `.trim(), 70 | ); 71 | } 72 | if (bundleOpts.entry) { 73 | const tsConfigPath = join(cwd, 'tsconfig.json'); 74 | const tsConfig = existsSync(tsConfigPath) 75 | || (rootPath && existsSync(join(rootPath, 'tsconfig.json'))); 76 | if ( 77 | !tsConfig && ( 78 | (Array.isArray(bundleOpts.entry) && bundleOpts.entry.some(isTypescriptFile)) || 79 | (!Array.isArray(bundleOpts.entry) && isTypescriptFile(bundleOpts.entry)) 80 | ) 81 | ) { 82 | signale.info( 83 | `Project using ${chalk.cyan('typescript')} but tsconfig.json not exists. Use default config.` 84 | ); 85 | } 86 | } 87 | } 88 | 89 | function isTypescriptFile(filePath) { 90 | return filePath.endsWith('.ts') || filePath.endsWith('.tsx') 91 | } 92 | 93 | async function build(opts, extraOpts = {}) { 94 | const { cwd, rootPath, watch } = opts; 95 | const { pkg } = extraOpts; 96 | 97 | const dispose = []; 98 | 99 | // register babel for config files 100 | registerBabel({ 101 | cwd, 102 | only: CONFIG_FILES, 103 | }); 104 | 105 | const pkgName = (typeof pkg === 'string' ? pkg : pkg.name) || 'unknown'; 106 | 107 | function log(msg) { 108 | console.log(`${pkg ? `${randomColor(`${pkgName}`)}: ` : ''}${msg}`); 109 | } 110 | 111 | // Get user config 112 | const bundleOptsArray = getBundleOpts(opts); 113 | 114 | for (const bundleOpts of bundleOptsArray) { 115 | validateBundleOpts(bundleOpts, { cwd, rootPath }); 116 | 117 | // Clean dist 118 | log(chalk.gray(`Clean dist directory`)); 119 | rimraf.sync(join(cwd, 'dist')); 120 | 121 | // Build umd 122 | if (bundleOpts.umd) { 123 | log(`Build umd`); 124 | await rollup({ 125 | cwd, 126 | rootPath, 127 | log, 128 | type: 'umd', 129 | entry: bundleOpts.entry, 130 | watch, 131 | dispose, 132 | bundleOpts, 133 | }); 134 | } 135 | 136 | // Build cjs 137 | if (bundleOpts.cjs) { 138 | const cjs = bundleOpts.cjs; 139 | log(`Build cjs with ${cjs.type}`); 140 | if (cjs.type === 'babel') { 141 | await babel({ cwd, rootPath, watch, dispose, type: 'cjs', log, bundleOpts }); 142 | } else { 143 | await rollup({ 144 | cwd, 145 | rootPath, 146 | log, 147 | type: 'cjs', 148 | entry: bundleOpts.entry, 149 | watch, 150 | dispose, 151 | bundleOpts, 152 | }); 153 | } 154 | } 155 | 156 | // Build esm 157 | if (bundleOpts.esm) { 158 | const esm = bundleOpts.esm; 159 | log(`Build esm with ${esm.type}`); 160 | const importLibToEs = esm && esm.importLibToEs; 161 | if (esm && esm.type === 'babel') { 162 | await babel({ cwd, rootPath, watch, dispose, type: 'esm', importLibToEs, log, bundleOpts }); 163 | } else { 164 | await rollup({ 165 | cwd, 166 | rootPath, 167 | log, 168 | type: 'esm', 169 | entry: bundleOpts.entry, 170 | importLibToEs, 171 | watch, 172 | dispose, 173 | bundleOpts, 174 | }); 175 | } 176 | } 177 | } 178 | 179 | return dispose; 180 | } 181 | 182 | async function buildForLerna(opts) { 183 | const { cwd, rootConfig = {}, buildArgs = {} } = opts; 184 | 185 | // register babel for config files 186 | registerBabel({ 187 | cwd, 188 | only: CONFIG_FILES, 189 | }); 190 | 191 | const userConfig = merge(getUserConfig({ cwd }), rootConfig, buildArgs); 192 | 193 | let pkgs = await getLernaPackages(cwd, userConfig.pkgFilter); 194 | 195 | // support define pkgs in lerna 196 | // TODO: 使用lerna包解决依赖编译问题 197 | if (userConfig.pkgs) { 198 | pkgs = userConfig.pkgs 199 | .map((item) => { 200 | return pkgs.find(pkg => basename(pkg.contents) === item); 201 | }) 202 | .filter(Boolean); 203 | } 204 | 205 | const dispose = []; 206 | for (const pkg of pkgs) { 207 | if (process.env.PACKAGE && basename(pkg.contents) !== process.env.PACKAGE) continue; 208 | // build error when .DS_Store includes in packages root 209 | const pkgPath = pkg.contents; 210 | assert.ok( 211 | existsSync(join(pkgPath, 'package.json')), 212 | `package.json not found in packages/${pkg}`, 213 | ); 214 | process.chdir(pkgPath); 215 | dispose.push(...await build( 216 | { 217 | // eslint-disable-line 218 | ...opts, 219 | buildArgs: opts.buildArgs, 220 | rootConfig: userConfig, 221 | cwd: pkgPath, 222 | rootPath: cwd, 223 | }, 224 | { 225 | pkg, 226 | }, 227 | )); 228 | } 229 | return dispose; 230 | } 231 | 232 | module.exports = async function(opts) { 233 | const useLerna = existsSync(join(opts.cwd, 'lerna.json')); 234 | const isLerna = useLerna && process.env.LERNA !== 'none'; 235 | 236 | const dispose = isLerna ? await buildForLerna(opts) : await build(opts); 237 | return () => dispose.forEach(e => e()); 238 | } 239 | -------------------------------------------------------------------------------- /scripts/rollup.js: -------------------------------------------------------------------------------- 1 | const { rollup, watch } = require('rollup'); 2 | const signale = require('signale'); 3 | const vue = require('rollup-plugin-vue'); 4 | const tsconfig = require('../tsconfig.json') 5 | const typescript2 = require("rollup-plugin-typescript2"); 6 | const getRollupConfig = require('father-build/lib/getRollupConfig').default; 7 | const normalizeBundleOpts = require('father-build/lib/normalizeBundleOpts').default; 8 | 9 | async function build(entry, opts) { 10 | const { cwd, rootPath, type, log, bundleOpts, importLibToEs, dispose } = opts; 11 | const rollupConfigs = getRollupConfig({ 12 | cwd, 13 | rootPath:rootPath || cwd, 14 | type, 15 | entry, 16 | importLibToEs, 17 | bundleOpts: normalizeBundleOpts(entry, bundleOpts), 18 | }); 19 | rollupConfigs.forEach(config => { 20 | const { plugins } = config 21 | plugins.splice(plugins.findIndex(p => p.name === 'rpt2'), 1) 22 | const index = plugins.findIndex(p => p.name === 'postcss') 23 | if(index < plugins.length) plugins.splice(index, 0, typescript2({ 24 | tsconfigOverride: { 25 | compilerOptions: { 26 | declaration: true, 27 | }, 28 | tsconfig: tsconfig 29 | } 30 | }),vue({ 31 | typescript: tsconfig })) 32 | else plugins.push(typescript2({tsconfigOverride: { 33 | compilerOptions: { 34 | declaration: true, 35 | } 36 | }}),vue({ preprocessStyles: true })) 37 | }) 38 | for (const rollupConfig of rollupConfigs) { 39 | if (opts.watch) { 40 | const watcher = watch([ 41 | { 42 | ...rollupConfig, 43 | watch: {}, 44 | }, 45 | ]); 46 | watcher.on('event', event => { 47 | if (event.error) { 48 | signale.error(event.error); 49 | } else if (event.code === 'START') { 50 | log(`[${type}] Rebuild since file changed`); 51 | } 52 | }); 53 | process.once('SIGINT', () => { 54 | watcher.close(); 55 | }); 56 | if(dispose) dispose.push(() => watcher.close()); 57 | } else { 58 | const { output, ...input } = rollupConfig; 59 | const bundle = await rollup(input); // eslint-disable-line 60 | await bundle.write(output); // eslint-disable-line 61 | } 62 | } 63 | } 64 | 65 | module.exports = async function(opts) { 66 | if (Array.isArray(opts.entry)) { 67 | const { entry: entries } = opts; 68 | for (const entry of entries) { 69 | await build(entry, opts); 70 | } 71 | } else { 72 | await build(opts.entry, opts); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zb201307/am-editor-vue2/a15c9fdf4bcb43ccc1b9345159785d387ee3b346/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | 26 | -------------------------------------------------------------------------------- /src/components/Map/Content.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 152 | 194 | -------------------------------------------------------------------------------- /src/components/Map/Modal.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 50 | -------------------------------------------------------------------------------- /src/components/MentionHover.vue: -------------------------------------------------------------------------------- 1 | 7 | 14 | 20 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import store from "./store"; 5 | 6 | Vue.config.productionTip = false; 7 | 8 | new Vue({ 9 | router, 10 | store, 11 | render: (h) => h(App), 12 | }).$mount("#app"); 13 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import amEditorVue2 from '@/components/amEditorVue2' 4 | 5 | Vue.use(Router) 6 | 7 | export default new Router({ 8 | routes: [ 9 | { 10 | path: '/', 11 | name: 'amEditorVue2', 12 | component: amEditorVue2 13 | } 14 | ] 15 | }) 16 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter, { RouteConfig } from "vue-router"; 3 | import Home from "../views/Home.vue"; 4 | 5 | Vue.use(VueRouter); 6 | 7 | const routes: Array = [ 8 | { 9 | path: "/", 10 | name: "Home", 11 | component: Home, 12 | }, 13 | { 14 | path: "/about", 15 | name: "About", 16 | // route level code-splitting 17 | // this generates a separate chunk (about.[hash].js) for this route 18 | // which is lazy-loaded when the route is visited. 19 | component: () => 20 | import(/* webpackChunkName: "about" */ "../views/About.vue"), 21 | }, 22 | ]; 23 | 24 | const router = new VueRouter({ 25 | mode: "history", 26 | base: process.env.BASE_URL, 27 | routes, 28 | }); 29 | 30 | export default router; 31 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from "vue"; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | 4 | Vue.use(Vuex); 5 | 6 | export default new Vuex.Store({ 7 | state: {}, 8 | mutations: {}, 9 | actions: {}, 10 | modules: {}, 11 | }); 12 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import Vue, { Component } from "vue"; 2 | function creatComponent( 3 | component: Component, 4 | containter: HTMLElement, 5 | props: { [key: string]: any } = {}, 6 | emitEvent: { [key: string]: () => void } = {} 7 | ): Vue { 8 | const comp: Vue = new Vue({ 9 | render(createElement) { 10 | return createElement(component, { 11 | props, 12 | on: emitEvent, 13 | }); 14 | }, 15 | }).$mount(); 16 | 17 | containter.appendChild(comp.$el); 18 | 19 | // comp.remove = (): void => { 20 | // containter.removeChild(comp.$el); 21 | // comp.$destroy(); 22 | // }; 23 | return comp; 24 | } 25 | 26 | export { creatComponent }; 27 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "tests/**/*.ts", 35 | "tests/**/*.tsx" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | css: { 3 | loaderOptions: { 4 | // 向 CSS 相关的 loader 传递选项 5 | less: { 6 | javascriptEnabled: true, 7 | }, 8 | }, 9 | }, 10 | }; 11 | --------------------------------------------------------------------------------