├── .gitignore ├── .prettierrc.cjs ├── LICENSE ├── README.md ├── README.zh.md ├── package-lock.json ├── package.json ├── src ├── imagePlaceholder.ts ├── imageUploadExtension.ts ├── imageUploader.ts ├── imgPlaceholder.vue └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .idea 107 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, // 使用单引号代替双引号 3 | printWidth: 120, // 超过最大值换行 4 | semi: false, // 结尾不用分号 5 | useTabs: false, // 缩进使用tab, 不使用空格 6 | tabWidth: 2, // tab 样式宽度 7 | bracketSpacing: true, // 对象数组, 文字间加空格 {a: 1} => { a: 1 } 8 | arrowParens: 'avoid', // 如果可以, 自动去除括号 (x) => x 变为 x => x 9 | proseWrap: 'preserve', 10 | // htmlWhitespaceSensitivity: "ignore", 11 | trailingComma: 'all', 12 | }; 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alonso 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tiptap-extension-image-upload 2 | 3 | Image upload extension for tiptap, support image preview. 4 | 5 | [中文 readme](./README.zh.md) 6 | 7 | ## Introduction 8 | 9 | Upload support: 10 | 11 | - File type data upload (such as the image selected by input:file) 12 | - Image drop handling 13 | - Screenshot in the clipboard 14 | - Third party pictures in the clipboard (the image url will be read and converted to File) 15 | 16 | Image preview(.image-placeholder): 17 | 18 | - The base64 of the image is used as the preview by default 19 | 20 | ## Instalation 21 | 22 | `npm i tiptap-extension-image-upload -S` 23 | 24 | ## Usage 25 | 26 | Add it as any extension in `useEditor()` 27 | 28 | ```ts 29 | import { ImageUploadExtension, ImagePlaceholder } from 'tiptap-extension-image-upload' 30 | 31 | extensions: { 32 | ImageUploadExtension.configure({ 33 | acceptMimes: ['image/jpeg', 'image/gif', 'image/png', 'image/jpg'], 34 | upload: (file: File, id: string) => { 35 | // your upload ajax 36 | return Promise.resolve('https://avatars.githubusercontent.com/u/112541088') 37 | }, 38 | ignoreDomains: ['www.xxx.com'] 39 | }), 40 | ImagePlaceholder.configure({ 41 | inline: false 42 | }), 43 | ``` 44 | 45 | ### ImageUploadExtension Configuration 46 | 47 | ```ts 48 | export interface ImageUploaderPluginOptions { 49 | /** Image types allowed to upload */ 50 | acceptMimes: string[] 51 | /** 52 | * Image File upload function 53 | * @param {File} file - File waiting to be uploaded 54 | * @param {string} id - Automatically generated unique key 55 | */ 56 | upload(file: File | string, id: string): Promise 57 | /** Do not upload if the src domain is in this array */ 58 | ignoreDomains: string[] 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # tiptap-extension-image-upload 2 | 3 | tiptap 的图片上传扩展,支持图片预览或占位 4 | 5 | ## Introduction 6 | 7 | 上传支持: 8 | 9 | - File数据上传(比如 input:file 选择的图片) 10 | - 拖拽上传的文件 11 | - 剪切板中的截图 12 | - 剪切板中的第三方图片(会读取图片数据转为File) 13 | 14 | 图片预览或占位(.image-placeholder): 15 | 16 | - 默认使用图片的 base64 做为预览 17 | 18 | ## Instalation 19 | 20 | `npm i tiptap-extension-image-upload -S` 21 | 22 | ## Usage 23 | 24 | 添加在 `useEditor()` 的扩展中: 25 | 26 | ```ts 27 | import { ImageUploadExtension, ImagePlaceholder } from 'tiptap-extension-image-upload' 28 | 29 | extensions: { 30 | ImageUploadExtension.configure({ 31 | acceptMimes: ['image/jpeg', 'image/gif', 'image/png', 'image/jpg'], 32 | upload: (file: File, id: string) => { 33 | // 你的上传方法 34 | return Promise.resolve('https://avatars.githubusercontent.com/u/112541088') 35 | }, 36 | }), 37 | ImagePlaceholder.configure({ 38 | inline: false 39 | }), 40 | ``` 41 | 42 | ImageUploadExtension 配置项: 43 | 44 | ```ts 45 | export interface ImageUploaderPluginOptions { 46 | /** 允许上传的图片类型 */ 47 | acceptMimes: string[]; 48 | /** 49 | * 图片上传方法 50 | * @param {File} file 待上传的文件 51 | * @param {string} id 自动生成的唯一key 52 | */ 53 | upload(file: File | string, id: string): Promise; 54 | } 55 | ``` 56 | 57 | ## Commands 58 | 59 | ```js 60 | 61 | 62 | function onFileInputChoose({ target }) { 63 | const file = target.files[0] 64 | target.value = '' 65 | editor.commands.uploadImage({ file }) 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiptap-extension-image-upload", 3 | "author": "coolswitch", 4 | "description": "Image upload extension for tiptap, support image preview(tiptap的图片上传扩展,支持图片预览或占位)", 5 | "version": "0.1.6", 6 | "homepage": "https://github.com/coolswitch/tiptap-extension-image-upload#readme", 7 | "keywords": [ 8 | "tiptap", 9 | "tiptap extension", 10 | "tiptap extension upload", 11 | "tiptap extension img", 12 | "tiptap extension image" 13 | ], 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">=10" 17 | }, 18 | "main": "src/index.ts", 19 | "files": [ 20 | "dist", 21 | "src" 22 | ], 23 | "husky": { 24 | "hooks": { 25 | "pre-commit": "tsdx lint" 26 | } 27 | }, 28 | "dependencies": { 29 | "prosemirror-replaceattrs": "^1.0.0" 30 | }, 31 | "devDependencies": { 32 | "husky": "^7.0.4", 33 | "tsdx": "^0.14.1", 34 | "tslib": "^2.4.0", 35 | "typescript": "^4.6.4", 36 | "prosemirror-state": "^1.2.0", 37 | "prosemirror-model": "^1.8.0", 38 | "prosemirror-view": "^1.14.0", 39 | "@tiptap/core": "^2.0.0-beta.193", 40 | "@tiptap/vue-3": "^2.0.0-beta.195" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/coolswitch/tiptap-extension-image-upload/issues" 44 | }, 45 | "repository": "git+https://github.com/coolswitch/tiptap-extension-image-upload.git" 46 | } 47 | -------------------------------------------------------------------------------- /src/imagePlaceholder.ts: -------------------------------------------------------------------------------- 1 | import { Node, nodeInputRule } from '@tiptap/core' 2 | import { VueNodeViewRenderer } from '@tiptap/vue-3' 3 | import imgPlaceholder from './imgPlaceholder.vue' 4 | 5 | export interface ImagePlaceholderOptions { 6 | inline: boolean 7 | HTMLAttributes: Record 8 | } 9 | 10 | export const inputRegex = 11 | /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/ 12 | 13 | export const ImagePlaceholder = Node.create({ 14 | name: 'imagePlaceholder', 15 | draggable: false, 16 | 17 | addOptions() { 18 | return { 19 | inline: false, 20 | HTMLAttributes: {} 21 | } 22 | }, 23 | 24 | inline() { 25 | return this.options.inline 26 | }, 27 | 28 | group() { 29 | return this.options.inline ? 'inline' : 'block' 30 | }, 31 | 32 | addAttributes() { 33 | return { 34 | uploadId: { 35 | default: '' 36 | }, 37 | width: { 38 | default: undefined, 39 | }, 40 | } 41 | }, 42 | 43 | parseHTML() { 44 | return [{ tag: 'image-placeholder' }] 45 | }, 46 | 47 | renderHTML() { 48 | return ['div'] 49 | }, 50 | 51 | addNodeView() { 52 | return VueNodeViewRenderer(imgPlaceholder) 53 | }, 54 | 55 | addInputRules() { 56 | return [ 57 | nodeInputRule({ 58 | find: inputRegex, 59 | type: this.type, 60 | getAttributes: (match) => { 61 | const [, , uploadId] = match 62 | 63 | return { uploadId } 64 | } 65 | }) 66 | ] 67 | } 68 | }) 69 | -------------------------------------------------------------------------------- /src/imageUploadExtension.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '@tiptap/core' 2 | // import { imageUploader } from 'prosemirror-image-uploader' 3 | import { imageUploader, getFileCache } from './imageUploader' 4 | 5 | export interface ImageUploaderPluginOptions { 6 | acceptMimes: string[] 7 | 8 | upload(file: File | string, id: string): Promise 9 | 10 | id(): string 11 | 12 | ignoreDomains: string[] 13 | } 14 | 15 | declare module '@tiptap/core' { 16 | interface Commands { 17 | imageUploadExtension: { 18 | /** Add an image */ 19 | uploadImage: (options: { file: File }) => ReturnType 20 | } 21 | } 22 | } 23 | 24 | export const ImageUploadExtension = Extension.create({ 25 | name: 'imageUploadExtension', 26 | 27 | addOptions() { 28 | return { 29 | id: () => 30 | Math.random() 31 | .toString(36) 32 | .substring(7), 33 | acceptMimes: ['image/jpeg', 'image/gif', 'image/png', 'image/jpg'], 34 | upload: () => Promise.reject('【ImageUploadExtension】参数 upload 为必填项'), 35 | ignoreDomains: [], 36 | } 37 | }, 38 | 39 | addCommands() { 40 | return { 41 | uploadImage: options => ({ tr }) => { 42 | // const plugin = getPluginInstances() 43 | // plugin?.beforeUpload(options.file, -1) 44 | tr.setMeta('uploadImages', options.file) 45 | return true 46 | }, 47 | getFileCache: (key: string) => () => { 48 | return getFileCache(key) 49 | }, 50 | } 51 | }, 52 | 53 | addProseMirrorPlugins() { 54 | const options = this.options 55 | return [imageUploader(options)] 56 | }, 57 | }) 58 | -------------------------------------------------------------------------------- /src/imageUploader.ts: -------------------------------------------------------------------------------- 1 | import { Fragment, Node, Slice } from 'prosemirror-model'; 2 | import 'prosemirror-replaceattrs'; /// register it 3 | import { Plugin } from 'prosemirror-state'; 4 | import { EditorView } from 'prosemirror-view'; 5 | import type { ImageUploaderPluginOptions } from './imageUploadExtension' 6 | 7 | let plugin: ImageUploaderPlugin | null = null 8 | let fileCache: { [key: string]: File | string } = {} 9 | 10 | export function imageUploader(options: ImageUploaderPluginOptions) { 11 | plugin = new ImageUploaderPlugin(options); 12 | const dummy = {}; 13 | 14 | return new Plugin({ 15 | props: { 16 | handleDOMEvents: { 17 | keydown(view) { 18 | return !plugin?.setView(view); 19 | }, 20 | 21 | drop(view) { 22 | return !plugin?.setView(view); 23 | }, 24 | 25 | focus(view) { 26 | return !plugin?.setView(view); 27 | } 28 | }, 29 | 30 | handlePaste(view, event) { 31 | return plugin?.setView(view).handlePaste(event) || false; 32 | }, 33 | 34 | transformPasted(slice) { 35 | /// Workaround for missing view is provided above. 36 | return plugin?.transformPasted(slice) || slice; 37 | }, 38 | 39 | handleDrop(view, event) { 40 | return plugin?.setView(view).handleDrop(event as DragEvent) || false; 41 | } 42 | }, 43 | 44 | state: { 45 | init() { 46 | return dummy; 47 | }, 48 | 49 | apply(tr, _value, _oldState, newState) { 50 | const filesOrUrls = tr.getMeta('uploadImages'); 51 | 52 | if (filesOrUrls) { 53 | const arr: Array = 54 | typeof filesOrUrls === 'string' || filesOrUrls instanceof File 55 | ? [filesOrUrls] 56 | : Array.from(filesOrUrls); /// Probably a FileList or an array of files/urls 57 | 58 | // give some time for editor, otherwise history plugin forgets history 59 | setTimeout(() => { 60 | arr.forEach((item, i) => 61 | plugin?.beforeUpload(item, newState.selection.from + i) 62 | ) 63 | tr.setMeta('uploadImages', undefined); 64 | }, 10); 65 | } 66 | 67 | return dummy; 68 | } 69 | } 70 | }); 71 | } 72 | 73 | export class ImageUploaderPlugin { 74 | public view!: EditorView; 75 | 76 | constructor(public config: ImageUploaderPluginOptions) { } 77 | 78 | public handleDrop(event: DragEvent) { 79 | if (!event.dataTransfer?.files.length) return; 80 | 81 | const coordinates = this.view.posAtCoords({ 82 | left: event.clientX, 83 | top: event.clientY 84 | }); 85 | if (!coordinates) return; 86 | 87 | const imageFiles = Array.from(event.dataTransfer.files).filter(file => 88 | this.config.acceptMimes.includes(file.type) 89 | ); 90 | if (!imageFiles.length) return; 91 | 92 | imageFiles.forEach((file, i) => { 93 | this.beforeUpload(file, coordinates.pos + i); 94 | }); 95 | 96 | return true; 97 | } 98 | 99 | // 网页 100 | public transformPasted(slice: Slice) { 101 | const imageNodes: Array<{ url: string; id: string }> = []; 102 | 103 | const children: Node[] = []; 104 | slice.content.forEach(child => { 105 | let newChild = child; 106 | 107 | /// if the node itself is image 108 | if (child.type.name === 'image' && !this.isOurOwnPic(child.attrs)) { 109 | newChild = this.newUploadingImageNode(child.attrs); 110 | imageNodes.push({ 111 | id: newChild.attrs.uploadId, 112 | url: child.attrs.src || child.attrs['data-src'] 113 | }); 114 | } else { 115 | child.descendants((node, pos) => { 116 | if (node.type.name === 'image' && !this.isOurOwnPic(node.attrs)) { 117 | const imageNode = this.newUploadingImageNode(node.attrs); 118 | newChild = newChild.replace( 119 | pos, 120 | pos + 1, 121 | new Slice(Fragment.from(imageNode), 0, 0) 122 | ); 123 | imageNodes.push({ 124 | id: imageNode.attrs.uploadId, 125 | url: node.attrs.src || node.attrs['data-src'] 126 | }); 127 | } 128 | }); 129 | } 130 | 131 | children.push(newChild); 132 | }); 133 | 134 | imageNodes.forEach(({ url, id }) => this.uploadImageForId(url, id)); 135 | 136 | return new Slice( 137 | Fragment.fromArray(children), 138 | slice.openStart, 139 | slice.openEnd 140 | ); 141 | } 142 | 143 | // 截图 144 | public handlePaste(event: ClipboardEvent) { 145 | const items = Array.from(event.clipboardData?.items || []); 146 | 147 | /// Clipboard may contain both html and image items (like when pasting from ms word, excel) 148 | /// in that case (if there is any html), don't handle images. 149 | if (items.some(x => x.type === 'text/html')) { 150 | return false; 151 | } 152 | 153 | const image = items.find(item => this.config.acceptMimes.includes(item.type)); 154 | 155 | if (image) { 156 | this.beforeUpload(image.getAsFile()!, this.view.state.selection.from); 157 | return true; 158 | } 159 | 160 | return false; 161 | } 162 | 163 | public beforeUpload(fileOrUrl: File | string, at: number) { 164 | const tr = this.view.state.tr; 165 | if (!tr.selection.empty) { 166 | tr.deleteSelection(); 167 | } 168 | 169 | if (at < 0) { 170 | at = this.view.state.selection.from 171 | } 172 | 173 | /// insert image node. 174 | const node = this.newUploadingImageNode({ src: fileOrUrl }); 175 | tr.replaceWith(at, at, node) 176 | this.view.dispatch(tr); 177 | 178 | /// upload image for above node 179 | this.uploadImageForId(fileOrUrl, node.attrs.uploadId); 180 | } 181 | 182 | public newUploadingImageNode(attrs?: any): Node { 183 | // const empty_baseb4 = "data:image/svg+xml,%3Csvg width='20' height='20' xmlns='http://www.w3.org/2000/svg'/%3E\n"; 184 | const uploadId = this.config.id() 185 | fileCache[uploadId] = attrs.src || attrs['data-src'] 186 | return this.view.state.schema.nodes.imagePlaceholder.create({ 187 | ...attrs, 188 | src: '',//attrs.src, 189 | uploadId 190 | }) 191 | } 192 | 193 | public async uploadImageForId(fileOrUrl: File | string, id: string) { 194 | const getImagePositions = () => { 195 | const positions: Array<{ node: Node; pos: number }> = []; 196 | this.view.state.doc.descendants((node, pos) => { 197 | if (node.type.name === 'imagePlaceholder' && node.attrs.uploadId === id) { 198 | positions.push({ node, pos }); 199 | } 200 | }); 201 | 202 | return positions; 203 | }; 204 | 205 | let file: string | File | null = fileOrUrl 206 | if (typeof file === 'string') { 207 | file = await webImg2File(file) 208 | } 209 | 210 | const url = file && (await this.config 211 | .upload(file, id) 212 | // tslint:disable-next-line:no-console 213 | .catch(console.warn)) as string | undefined; 214 | 215 | const imageNodes = getImagePositions(); 216 | if (!imageNodes.length) { 217 | return; 218 | } 219 | 220 | /// disallow user from undoing back to 'uploading' state. 221 | // let tr = this.view.state.tr.setMeta('addToHistory', false); 222 | let tr = this.view.state.tr; 223 | 224 | imageNodes.forEach(({ node, pos }) => { 225 | const newNode = this.view.state.schema.nodes.image.create({ 226 | ...node.attrs, 227 | width: node.attrs.width, 228 | src: url || '' 229 | }) 230 | tr.replaceWith(pos, pos + 1, newNode) 231 | }); 232 | 233 | this.view.dispatch(tr); 234 | fileCache[id] = '' 235 | } 236 | 237 | public setView(view: EditorView): this { 238 | this.view = view; 239 | return this; 240 | } 241 | 242 | private isOurOwnPic(attrs: { src?: string, ['data-src']?: string }): boolean { 243 | const src = attrs.src || attrs['data-src'] || '' 244 | return (this.config.ignoreDomains || []).some((domain) => src.includes(domain)) 245 | } 246 | } 247 | 248 | 249 | function webImg2File(imgUrl: string): Promise { 250 | function imgToBase64(url: string): Promise { 251 | let canvas = document.createElement('canvas'), 252 | ctx = canvas.getContext('2d'), 253 | img = new Image; 254 | 255 | img.crossOrigin = 'Anonymous'; 256 | img.setAttribute('referrerpolicy', 'no-referrer') 257 | img.src = url; 258 | return new Promise((resolve, reject) => { 259 | img.onload = function () { 260 | canvas.height = img.height; 261 | canvas.width = img.width; 262 | ctx?.drawImage(img, 0, 0); 263 | var dataURL = canvas.toDataURL('image/png'); 264 | resolve(dataURL); 265 | // @ts-ignore 266 | canvas = null; 267 | }; 268 | img.onerror = reject 269 | }) 270 | } 271 | 272 | function base64toFile(base: string, filename: string): File { 273 | var arr = base.split(','); 274 | // @ts-ignore 275 | var mime = arr[0].match(/:(.*?);/)[1]; 276 | var suffix = mime.split("/")[1]; 277 | var bstr = atob(arr[1]); 278 | var n = bstr.length; 279 | var u8arr = new Uint8Array(n); 280 | while (n--) { 281 | u8arr[n] = bstr.charCodeAt(n); 282 | } 283 | //转换成file对象 284 | return new File([u8arr], `${filename}.${suffix}`, { type: mime }); 285 | } 286 | 287 | return imgToBase64(imgUrl).then(base => { 288 | return base64toFile(base, '网络图片') 289 | }).catch(() => { 290 | return null 291 | }) 292 | } 293 | 294 | 295 | // export function getPluginInstances() { 296 | // return plugin 297 | // } 298 | export function getFileCache(key: string) { 299 | return fileCache[key] 300 | } -------------------------------------------------------------------------------- /src/imgPlaceholder.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 52 | 53 | 87 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ImagePlaceholder as Placeholder } from './imagePlaceholder' 2 | import { ImageUploadExtension as UploadExtension } from './imageUploadExtension' 3 | 4 | export const ImagePlaceholder = Placeholder 5 | export const ImageUploadExtension = UploadExtension 6 | 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | } 35 | } --------------------------------------------------------------------------------