├── .gitignore ├── .prettierrc ├── README.md ├── babel.config.js ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ ├── logo.png │ ├── md-toolbar.scss │ └── vue3-editor.scss ├── components │ └── VueEditor.vue ├── helpers │ ├── axios.js │ ├── custom-link.js │ ├── default-toolbar.js │ ├── fullToolbar.js │ ├── markdown-shortcuts.js │ ├── merge-deep.js │ └── old-api.js ├── index.js ├── main.js └── plugin.js ├── vue.config.js └── yarn.lock /.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 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue3Editor 2 | 3 | > An easy-to-use but yet powerful and customizable rich text editor powered by Quill.js and Vue.js 4 | 5 | > This project is built on `vue2-editor`, please see its doc. 6 | 7 | [vue2-editor](https://github.com/davidroyer/vue2-editor) 8 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-editor", 3 | "version": "0.1.1", 4 | "description": "HTML editor using Vue.js 3, and Quill.js, an open source editor", 5 | "license": "MIT", 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build --target lib --name vue3-editor ./src/index.js", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "main": "dist/vue3-editor.common.js", 12 | "unpkg": "dist/vue3-editor.umd.min.js", 13 | "files": [ 14 | "dist" 15 | ], 16 | "peerDependencies": { 17 | "vue": "^3.0.0" 18 | }, 19 | "dependencies": { 20 | "core-js": "^3.6.5", 21 | "quill": "^1.3.7" 22 | }, 23 | "devDependencies": { 24 | "@vue/cli-plugin-babel": "~4.5.0", 25 | "@vue/cli-plugin-eslint": "~4.5.0", 26 | "@vue/cli-service": "~4.5.0", 27 | "@vue/compiler-sfc": "^3.0.0-0", 28 | "axios": "^0.20.0", 29 | "babel-eslint": "^10.1.0", 30 | "eslint": "^6.7.2", 31 | "eslint-plugin-vue": "^7.0.0-0", 32 | "sass": "^1.26.11", 33 | "sass-loader": "^10.0.2", 34 | "vue": "^3.0.0" 35 | }, 36 | "eslintConfig": { 37 | "root": true, 38 | "env": { 39 | "node": true 40 | }, 41 | "extends": [ 42 | "plugin:vue/vue3-essential", 43 | "eslint:recommended" 44 | ], 45 | "parserOptions": { 46 | "parser": "babel-eslint" 47 | }, 48 | "rules": {} 49 | }, 50 | "browserslist": [ 51 | "> 1%", 52 | "last 2 versions", 53 | "not dead" 54 | ], 55 | "keywords": [ 56 | "vue", 57 | "vue-component", 58 | "quill", 59 | "html editor", 60 | "text editor" 61 | ], 62 | "repository": { 63 | "type": "git", 64 | "url": "https://github.com/sunkint/vue3-editor.git" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunkint/vue3-editor/ed0ef422318a0a8f20ac4f1388054ea265266124/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 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 101 | 102 | 112 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunkint/vue3-editor/ed0ef422318a0a8f20ac4f1388054ea265266124/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/md-toolbar.scss: -------------------------------------------------------------------------------- 1 | svg { 2 | width: 24px !important; 3 | height: 24px !important; 4 | fill: rgba(65, 65, 65, 0.9); 5 | 6 | } 7 | 8 | button.ql-active svg { 9 | fill: white; 10 | fill: rgba(97, 97, 97, 0.98) !important; 11 | } 12 | 13 | .quillWrapper .ql-snow.ql-toolbar button { 14 | margin: 1px 2px; 15 | display: flex; 16 | padding: 3px; 17 | justify-content: center; 18 | align-items: center; 19 | } 20 | 21 | .ql-snow.ql-toolbar button, .ql-snow .ql-toolbar button { 22 | display: flex; 23 | padding: 3px; 24 | margin: 1px 3px !important; 25 | justify-content: center; 26 | width: 30px !important; 27 | height: 30px !important; 28 | border-radius: 3px; 29 | } 30 | 31 | .ql-toolbar button.ql-active { 32 | background: rgba(85, 85, 97, 0.9) !important; 33 | background: rgba(203, 201, 201, 0.9) !important; 34 | } 35 | 36 | .quillWrapper .ql-toolbar { 37 | padding-bottom: 4px; 38 | display: flex; 39 | align-items: center; 40 | flex-flow: row wrap; 41 | } 42 | -------------------------------------------------------------------------------- /src/assets/vue3-editor.scss: -------------------------------------------------------------------------------- 1 | .ql-editor { 2 | min-height: 200px; 3 | font-size: 16px; 4 | } 5 | 6 | .ql-snow .ql-stroke.ql-thin, 7 | .ql-snow .ql-thin { 8 | stroke-width: 1px !important; 9 | } 10 | 11 | .quillWrapper .ql-snow.ql-toolbar { 12 | padding-top: 8px; 13 | padding-bottom: 4px; 14 | } 15 | /* .quillWrapper .ql-snow.ql-toolbar button { 16 | margin: 1px 4px; 17 | } */ 18 | .quillWrapper .ql-snow.ql-toolbar .ql-formats { 19 | margin-bottom: 10px; 20 | } 21 | 22 | .ql-snow .ql-toolbar button svg, 23 | .quillWrapper .ql-snow.ql-toolbar button svg { 24 | width: 22px; 25 | height: 22px; 26 | } 27 | 28 | .quillWrapper .ql-editor ul[data-checked=false] > li::before, 29 | .quillWrapper .ql-editor ul[data-checked=true] > li::before { 30 | font-size: 1.35em; 31 | vertical-align: baseline; 32 | bottom: -0.065em; 33 | font-weight: 900; 34 | color: #222; 35 | } 36 | 37 | .quillWrapper .ql-snow .ql-stroke { 38 | stroke: rgba(63, 63, 63, 0.95); 39 | stroke-linecap: square; 40 | stroke-linejoin: initial; 41 | stroke-width: 1.7px; 42 | } 43 | 44 | .quillWrapper .ql-picker-label { 45 | font-size: 15px; 46 | } 47 | 48 | .quillWrapper .ql-snow .ql-active .ql-stroke { 49 | stroke-width: 2.25px; 50 | } 51 | 52 | .quillWrapper .ql-toolbar.ql-snow .ql-formats { 53 | vertical-align: top; 54 | } 55 | 56 | .ql-picker { 57 | &:not(.ql-background) { 58 | position: relative; 59 | top: 2px; 60 | } 61 | 62 | &.ql-color-picker { 63 | svg { 64 | width: 22px !important; 65 | height: 22px !important; 66 | } 67 | } 68 | } 69 | 70 | .quillWrapper { 71 | & .imageResizeActive { 72 | img { 73 | display: block; 74 | cursor: pointer; 75 | } 76 | 77 | & ~ div svg { 78 | cursor: pointer; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/components/VueEditor.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 234 | 235 | 236 | 237 | -------------------------------------------------------------------------------- /src/helpers/axios.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | const CLIENT_ID = "993793b1d8d3e2e"; 3 | 4 | // We create our own axios instance and set a custom base URL. 5 | // Note that if we wouldn't set any config here we do not need 6 | // a named export, as we could just `import axios from 'axios'` 7 | export const axiosInstance = axios.create({ 8 | baseURL: `https://api.imgur.com/3/`, 9 | headers: { Authorization: "Client-ID " + CLIENT_ID } 10 | }); 11 | // export default ({ Vue }) => { 12 | // Vue.prototype.$axios = axiosInstance; 13 | // }; 14 | 15 | // Here we define a named export 16 | // that we can later use inside .js files: 17 | // export default axiosInstance; 18 | 19 | // axios({ 20 | // url: "https://api.imgur.com/3/image", 21 | // method: "POST", 22 | // headers: { Authorization: "Client-ID " + CLIENT_ID }, 23 | 24 | // }); 25 | -------------------------------------------------------------------------------- /src/helpers/custom-link.js: -------------------------------------------------------------------------------- 1 | import Quill from 'quill'; 2 | const Link = Quill.import('formats/link'); 3 | 4 | export default class CustomLink extends Link { 5 | static sanitize(url) { 6 | const value = super.sanitize(url); 7 | if (value) { 8 | for (let i = 0; i < this.PROTOCOL_WHITELIST.length; i++) 9 | if (value.startsWith(this.PROTOCOL_WHITELIST[i])) { 10 | return value; 11 | } 12 | return `https://${value}`; 13 | } 14 | return value; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/helpers/default-toolbar.js: -------------------------------------------------------------------------------- 1 | let defaultToolbar = [ 2 | [{ header: [false, 1, 2, 3, 4, 5, 6] }], 3 | ["bold", "italic", "underline", "strike"], // toggled buttons 4 | [ 5 | { align: "" }, 6 | { align: "center" }, 7 | { align: "right" }, 8 | { align: "justify" } 9 | ], 10 | ["blockquote", "code-block"], 11 | [{ list: "ordered" }, { list: "bullet" }, { list: "check" }], 12 | [{ indent: "-1" }, { indent: "+1" }], // outdent/indent 13 | [{ color: [] }, { background: [] }], // dropdown with defaults from theme 14 | ["link", "image", "video"], 15 | ["clean"] // remove formatting button 16 | ]; 17 | export default defaultToolbar; 18 | -------------------------------------------------------------------------------- /src/helpers/fullToolbar.js: -------------------------------------------------------------------------------- 1 | var fullToolbar = [ 2 | [{ font: [] }], 3 | 4 | [{ header: [false, 1, 2, 3, 4, 5, 6] }], 5 | 6 | [{ size: ["small", false, "large", "huge"] }], 7 | 8 | ["bold", "italic", "underline", "strike"], 9 | 10 | [ 11 | { align: "" }, 12 | { align: "center" }, 13 | { align: "right" }, 14 | { align: "justify" } 15 | ], 16 | 17 | [{ header: 1 }, { header: 2 }], 18 | 19 | ["blockquote", "code-block"], 20 | 21 | [{ list: "ordered" }, { list: "bullet" }, { list: "check" }], 22 | 23 | [{ script: "sub" }, { script: "super" }], 24 | 25 | [{ indent: "-1" }, { indent: "+1" }], 26 | 27 | [{ color: [] }, { background: [] }], 28 | 29 | ["link", "image", "video", "formula"], 30 | 31 | [{ direction: "rtl" }], 32 | ["clean"] 33 | ]; 34 | 35 | export default fullToolbar; 36 | -------------------------------------------------------------------------------- /src/helpers/markdown-shortcuts.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import Quill from "quill"; 3 | let BlockEmbed = Quill.import("blots/block/embed"); 4 | class HorizontalRule extends BlockEmbed {} 5 | HorizontalRule.blotName = "hr"; 6 | HorizontalRule.tagName = "hr"; 7 | Quill.register("formats/horizontal", HorizontalRule); 8 | 9 | class MarkdownShortcuts { 10 | constructor(quill, options) { 11 | this.quill = quill; 12 | this.options = options; 13 | 14 | this.ignoreTags = ["PRE"]; 15 | this.matches = [ 16 | { 17 | name: "header", 18 | pattern: /^(#){1,6}\s/g, 19 | action: (text, selection, pattern) => { 20 | var match = pattern.exec(text); 21 | if (!match) return; 22 | const size = match[0].length; 23 | // Need to defer this action https://github.com/quilljs/quill/issues/1134 24 | setTimeout(() => { 25 | this.quill.formatLine(selection.index, 0, "header", size - 1); 26 | this.quill.deleteText(selection.index - size, size); 27 | }, 0); 28 | } 29 | }, 30 | { 31 | name: "blockquote", 32 | pattern: /^(>)\s/g, 33 | action: (_text, selection) => { 34 | // Need to defer this action https://github.com/quilljs/quill/issues/1134 35 | setTimeout(() => { 36 | this.quill.formatLine(selection.index, 1, "blockquote", true); 37 | this.quill.deleteText(selection.index - 2, 2); 38 | }, 0); 39 | } 40 | }, 41 | { 42 | name: "code-block", 43 | pattern: /^`{3}(?:\s|\n)/g, 44 | action: (_text, selection) => { 45 | // Need to defer this action https://github.com/quilljs/quill/issues/1134 46 | setTimeout(() => { 47 | this.quill.formatLine(selection.index, 1, "code-block", true); 48 | this.quill.deleteText(selection.index - 4, 4); 49 | }, 0); 50 | } 51 | }, 52 | { 53 | name: "bolditalic", 54 | pattern: /(?:\*|_){3}(.+?)(?:\*|_){3}/g, 55 | action: (text, _selection, pattern, lineStart) => { 56 | let match = pattern.exec(text); 57 | 58 | const annotatedText = match[0]; 59 | const matchedText = match[1]; 60 | const startIndex = lineStart + match.index; 61 | 62 | if (text.match(/^([*_ \n]+)$/g)) return; 63 | 64 | setTimeout(() => { 65 | this.quill.deleteText(startIndex, annotatedText.length); 66 | this.quill.insertText(startIndex, matchedText, { 67 | bold: true, 68 | italic: true 69 | }); 70 | this.quill.format("bold", false); 71 | }, 0); 72 | } 73 | }, 74 | { 75 | name: "bold", 76 | pattern: /(?:\*|_){2}(.+?)(?:\*|_){2}/g, 77 | action: (text, _selection, pattern, lineStart) => { 78 | let match = pattern.exec(text); 79 | 80 | const annotatedText = match[0]; 81 | const matchedText = match[1]; 82 | const startIndex = lineStart + match.index; 83 | 84 | if (text.match(/^([*_ \n]+)$/g)) return; 85 | 86 | setTimeout(() => { 87 | this.quill.deleteText(startIndex, annotatedText.length); 88 | this.quill.insertText(startIndex, matchedText, { bold: true }); 89 | this.quill.format("bold", false); 90 | }, 0); 91 | } 92 | }, 93 | { 94 | name: "italic", 95 | pattern: /(?:\*|_){1}(.+?)(?:\*|_){1}/g, 96 | action: (text, _selection, pattern, lineStart) => { 97 | let match = pattern.exec(text); 98 | 99 | const annotatedText = match[0]; 100 | const matchedText = match[1]; 101 | const startIndex = lineStart + match.index; 102 | 103 | if (text.match(/^([*_ \n]+)$/g)) return; 104 | 105 | setTimeout(() => { 106 | this.quill.deleteText(startIndex, annotatedText.length); 107 | this.quill.insertText(startIndex, matchedText, { italic: true }); 108 | this.quill.format("italic", false); 109 | }, 0); 110 | } 111 | }, 112 | { 113 | name: "strikethrough", 114 | pattern: /(?:~~)(.+?)(?:~~)/g, 115 | action: (text, _selection, pattern, lineStart) => { 116 | let match = pattern.exec(text); 117 | 118 | const annotatedText = match[0]; 119 | const matchedText = match[1]; 120 | const startIndex = lineStart + match.index; 121 | 122 | if (text.match(/^([*_ \n]+)$/g)) return; 123 | 124 | setTimeout(() => { 125 | this.quill.deleteText(startIndex, annotatedText.length); 126 | this.quill.insertText(startIndex, matchedText, { strike: true }); 127 | this.quill.format("strike", false); 128 | }, 0); 129 | } 130 | }, 131 | { 132 | name: "code", 133 | pattern: /(?:`)(.+?)(?:`)/g, 134 | action: (text, _selection, pattern, lineStart) => { 135 | let match = pattern.exec(text); 136 | 137 | const annotatedText = match[0]; 138 | const matchedText = match[1]; 139 | const startIndex = lineStart + match.index; 140 | 141 | if (text.match(/^([*_ \n]+)$/g)) return; 142 | 143 | setTimeout(() => { 144 | this.quill.deleteText(startIndex, annotatedText.length); 145 | this.quill.insertText(startIndex, matchedText, { code: true }); 146 | this.quill.format("code", false); 147 | this.quill.insertText(this.quill.getSelection(), " "); 148 | }, 0); 149 | } 150 | }, 151 | { 152 | name: "hr", 153 | pattern: /^([-*]\s?){3}/g, 154 | action: (text, selection) => { 155 | const startIndex = selection.index - text.length; 156 | setTimeout(() => { 157 | this.quill.deleteText(startIndex, text.length); 158 | 159 | this.quill.insertEmbed( 160 | startIndex + 1, 161 | "hr", 162 | true, 163 | Quill.sources.USER 164 | ); 165 | this.quill.insertText(startIndex + 2, "\n", Quill.sources.SILENT); 166 | this.quill.setSelection(startIndex + 2, Quill.sources.SILENT); 167 | }, 0); 168 | } 169 | }, 170 | { 171 | name: "asterisk-ul", 172 | pattern: /^(\*|\+)\s$/g, 173 | action: (_text, selection, _pattern) => { 174 | setTimeout(() => { 175 | this.quill.formatLine(selection.index, 1, "list", "unordered"); 176 | this.quill.deleteText(selection.index - 2, 2); 177 | }, 0); 178 | } 179 | }, 180 | { 181 | name: "image", 182 | pattern: /(?:!\[(.+?)\])(?:\((.+?)\))/g, 183 | action: (text, selection, pattern) => { 184 | const startIndex = text.search(pattern); 185 | const matchedText = text.match(pattern)[0]; 186 | // const hrefText = text.match(/(?:!\[(.*?)\])/g)[0] 187 | const hrefLink = text.match(/(?:\((.*?)\))/g)[0]; 188 | const start = selection.index - matchedText.length - 1; 189 | if (startIndex !== -1) { 190 | setTimeout(() => { 191 | this.quill.deleteText(start, matchedText.length); 192 | this.quill.insertEmbed( 193 | start, 194 | "image", 195 | hrefLink.slice(1, hrefLink.length - 1) 196 | ); 197 | }, 0); 198 | } 199 | } 200 | }, 201 | { 202 | name: "link", 203 | pattern: /(?:\[(.+?)\])(?:\((.+?)\))/g, 204 | action: (text, selection, pattern) => { 205 | const startIndex = text.search(pattern); 206 | const matchedText = text.match(pattern)[0]; 207 | const hrefText = text.match(/(?:\[(.*?)\])/g)[0]; 208 | const hrefLink = text.match(/(?:\((.*?)\))/g)[0]; 209 | const start = selection.index - matchedText.length - 1; 210 | if (startIndex !== -1) { 211 | setTimeout(() => { 212 | this.quill.deleteText(start, matchedText.length); 213 | this.quill.insertText( 214 | start, 215 | hrefText.slice(1, hrefText.length - 1), 216 | "link", 217 | hrefLink.slice(1, hrefLink.length - 1) 218 | ); 219 | }, 0); 220 | } 221 | } 222 | } 223 | ]; 224 | 225 | // Handler that looks for insert deltas that match specific characters 226 | this.quill.on("text-change", (delta, _oldContents, _source) => { 227 | for (let i = 0; i < delta.ops.length; i++) { 228 | // eslint-disable-next-line no-prototype-builtins 229 | if (delta.ops[i].hasOwnProperty("insert")) { 230 | if (delta.ops[i].insert === " ") { 231 | this.onSpace(); 232 | } else if (delta.ops[i].insert === "\n") { 233 | this.onEnter(); 234 | } 235 | } 236 | } 237 | }); 238 | } 239 | 240 | isValid(text, tagName) { 241 | return ( 242 | typeof text !== "undefined" && 243 | text && 244 | this.ignoreTags.indexOf(tagName) === -1 245 | ); 246 | } 247 | 248 | onSpace() { 249 | const selection = this.quill.getSelection(); 250 | if (!selection) return; 251 | const [line, offset] = this.quill.getLine(selection.index); 252 | const text = line.domNode.textContent; 253 | const lineStart = selection.index - offset; 254 | if (this.isValid(text, line.domNode.tagName)) { 255 | for (let match of this.matches) { 256 | const matchedText = text.match(match.pattern); 257 | if (matchedText) { 258 | // We need to replace only matched text not the whole line 259 | console.log("matched:", match.name, text); 260 | match.action(text, selection, match.pattern, lineStart); 261 | return; 262 | } 263 | } 264 | } 265 | } 266 | 267 | onEnter() { 268 | let selection = this.quill.getSelection(); 269 | if (!selection) return; 270 | const [line, offset] = this.quill.getLine(selection.index); 271 | const text = line.domNode.textContent + " "; 272 | const lineStart = selection.index - offset; 273 | selection.length = selection.index++; 274 | if (this.isValid(text, line.domNode.tagName)) { 275 | for (let match of this.matches) { 276 | const matchedText = text.match(match.pattern); 277 | if (matchedText) { 278 | console.log("matched", match.name, text); 279 | match.action(text, selection, match.pattern, lineStart); 280 | return; 281 | } 282 | } 283 | } 284 | } 285 | } 286 | 287 | // module.exports = MarkdownShortcuts; 288 | export default MarkdownShortcuts; 289 | -------------------------------------------------------------------------------- /src/helpers/merge-deep.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Performs a deep merge of `source` into `target`. 3 | * Mutates `target` only but not its objects and arrays. 4 | * 5 | */ 6 | 7 | export default function mergeDeep(target, source) { 8 | const isObject = obj => obj && typeof obj === "object"; 9 | 10 | if (!isObject(target) || !isObject(source)) { 11 | return source; 12 | } 13 | 14 | Object.keys(source).forEach(key => { 15 | const targetValue = target[key]; 16 | const sourceValue = source[key]; 17 | 18 | if (Array.isArray(targetValue) && Array.isArray(sourceValue)) { 19 | target[key] = targetValue.concat(sourceValue); 20 | } else if (isObject(targetValue) && isObject(sourceValue)) { 21 | target[key] = mergeDeep(Object.assign({}, targetValue), sourceValue); 22 | } else { 23 | target[key] = sourceValue; 24 | } 25 | }); 26 | 27 | return target; 28 | } 29 | -------------------------------------------------------------------------------- /src/helpers/old-api.js: -------------------------------------------------------------------------------- 1 | export default { 2 | props: { 3 | customModules: Array 4 | }, 5 | methods: { 6 | registerCustomModules(Quill) { 7 | if (this.customModules !== undefined) { 8 | this.customModules.forEach(customModule => { 9 | Quill.register("modules/" + customModule.alias, customModule.module); 10 | }); 11 | } 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Quill from "quill"; 2 | import VueEditor from "./components/VueEditor.vue"; 3 | 4 | const version = "0.1.0-alpha.2"; 5 | 6 | // Declare install function executed by Vue.use() 7 | export function install(app) { 8 | if (install.installed) return; 9 | install.installed = true; 10 | 11 | app.component("VueEditor", VueEditor); 12 | } 13 | 14 | const VPlugin = { 15 | install, 16 | version, 17 | Quill, 18 | VueEditor, 19 | }; 20 | 21 | // Auto-install when vue is found (eg. in browser via