├── demo ├── .gitkeep └── index.html ├── dist ├── .gitkeep ├── quill-mentions.css └── quill-mentions.js ├── .babelrc ├── src ├── scss │ ├── base.scss │ └── core │ │ ├── _variables.scss │ │ └── _styles.scss ├── quill-mentions.js └── module-mentions.js ├── .npmignore ├── .gitignore ├── .eslintrc.json ├── bower.json ├── README.md ├── package.json └── webpack.config.js /demo/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["es2015"] } 2 | -------------------------------------------------------------------------------- /src/scss/base.scss: -------------------------------------------------------------------------------- 1 | @import 'core/styles'; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | *.log 4 | .DS_Store -------------------------------------------------------------------------------- /src/quill-mentions.js: -------------------------------------------------------------------------------- 1 | import css from "./scss/base.scss"; 2 | import {Mentions} from "../src/module-mentions"; -------------------------------------------------------------------------------- /src/scss/core/_variables.scss: -------------------------------------------------------------------------------- 1 | $white: #FFF; 2 | $blue: #0366d6; 3 | $cyan: #2D9EE0; 4 | $blue: #1d7bde; 5 | $light-blue: #84a8cc; 6 | $light-gray: #ddd; 7 | 8 | $gray-80: #64707B; 9 | $gray-20: #CBD6E0; 10 | $gray-5: #F2F4F6; 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "indent": [ 12 | "error", 13 | 2 14 | ], 15 | "linebreak-style": [ 16 | "error", 17 | "unix" 18 | ], 19 | "quotes": [ 20 | "error", 21 | "double" 22 | ], 23 | "semi": [ 24 | "error", 25 | "always" 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quill-mentions", 3 | "version": "1.0.5", 4 | "description": "Quill Extension for mentions", 5 | "main": "webpack.config.js", 6 | "authors": [ 7 | "contentco" 8 | ], 9 | "license": "MIT", 10 | "keywords": [ 11 | "content", 12 | "mentions", 13 | "quill", 14 | "editor" 15 | ], 16 | "homepage": "https://github.com/contentco/quill-mentions", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower_components", 21 | "test", 22 | "tests" 23 | ], 24 | "dependencies": { 25 | "quill": "https://github.com/contentco/quill/raw/master/.release/quill.tar.gz" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /dist/quill-mentions.css: -------------------------------------------------------------------------------- 1 | .completions{list-style:none;margin:0;padding:0;background:#fff;border-radius:2px;box-shadow:2px 2px 2px rgba(0,0,0,.25);max-height:215px;max-width:250px;overflow:auto;border:1px solid #cbd6e0;border-radius:0 0 3px 3px;box-shadow:0 8px 20px -2px rgba(0,0,0,.25)}.completions>li{margin:0;padding:0;border-bottom:1px solid #f2f4f6;display:block}.completions>li>button{box-sizing:border-box;margin:0;display:block;width:100%;text-align:left;border:none;background:none;cursor:pointer;padding:8px 12px}.completions>li.active>button{background:#1d7bde}.completions>li.active>button span{color:#fff}.completions>li>button>.mention--username{font-weight:600;font-size:15px}.completions>li>button>.mention--name{color:#64707b;margin-left:4px}.completions>li>button>.matched{font-weight:700;color:#000}.completions>li>button>*{vertical-align:"middle"}.textarea-mention-control{width:25px;height:25px;right:10px;top:7px;z-index:4} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quill Mentions 2 | 3 | Custom module for [Quill.js](https://github.com/quilljs/quill) to allow mentions. 4 | 5 | ## Usage 6 | 7 | ### Getting Started 8 | 9 | To use mentions, initiate a quill editor and add the ```mentions``` when defining your quill ```modules```. 10 | 11 | ```javascript 12 | var users = [{ 13 | id: 11, 14 | fullName: 'Aron Hunt', 15 | username: 'aronhunt' 16 | }, 17 | { 18 | label: 23, 19 | fullName: 'Bobby Johnson', 20 | username: 'bobbyjohnson' 21 | }, 22 | { 23 | label: 58, 24 | fullName: 'Dennis', 25 | username: 'dennis' 26 | } 27 | ] 28 | 29 | 30 | var quill = new Quill('#quill-editor', { 31 | modules:{ 32 | mentions: { 33 | users: users 34 | } 35 | }, 36 | theme: 'snow' 37 | }); 38 | ``` 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quill-mentions", 3 | "version": "1.0.5", 4 | "description": "Quill Extension for mentions", 5 | "main": "webpack.config.js", 6 | "devDependencies": { 7 | "babel-core": "^6.26.3", 8 | "babel-loader": "^6.4.1", 9 | "babel-preset-env": "^1.7.0", 10 | "babel-preset-es2015": "^6.22.0", 11 | "css-loader": "^0.27.3", 12 | "eslint": "^4.10.0", 13 | "extract-text-webpack-plugin": "^2.1.0", 14 | "node-sass": "^4.11.0", 15 | "sass-loader": "^6.0.3", 16 | "webpack": "^2.3.2" 17 | }, 18 | "scripts": { 19 | "test": "npm test", 20 | "build": "webpack" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/contentco/quill-mentions.git" 25 | }, 26 | "keywords": [ 27 | "content", 28 | "mentions", 29 | "quill", 30 | "editor" 31 | ], 32 | "author": "contentco.co", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/contentco/quill-mentions/issues" 36 | }, 37 | "homepage": "https://github.com/contentco/quill-mentions#readme", 38 | "dependencies": { 39 | "babel": "^6.23.0", 40 | "fuse.js": "^2.6.2", 41 | "preact": "^7.2.0", 42 | "quill": "^1.2.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | // const autoprefixer = requre('autoprefixer'); 4 | 5 | const config = { 6 | entry: './src/quill-mentions.js', 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'quill-mentions.js' 10 | }, 11 | module: { 12 | rules: [{ 13 | test: /\.scss$/, 14 | use: ExtractTextPlugin.extract({ 15 | use: [{ 16 | loader: 'css-loader', 17 | options: { 18 | minimize: true || {/* CSSNano Options */} 19 | } 20 | }, { 21 | loader: 'sass-loader', 22 | }] 23 | }) 24 | }, 25 | { 26 | test: /\.js$/, 27 | include: [ 28 | path.resolve(__dirname, "src/") 29 | ], 30 | exclude: /(node_modules)/, 31 | use: { 32 | loader: 'babel-loader', 33 | options: { 34 | presets: [['es2015', {modules: false}],] 35 | } 36 | } 37 | } 38 | ] 39 | }, 40 | plugins: [ 41 | new ExtractTextPlugin('quill-mentions.css'), 42 | ] 43 | }; 44 | 45 | module.exports = config; -------------------------------------------------------------------------------- /src/scss/core/_styles.scss: -------------------------------------------------------------------------------- 1 | @import 'core/variables'; /* sass-loader doesnt like absolute imports*/ 2 | .mention { 3 | // color: $blue; 4 | } 5 | .completions { 6 | list-style: none; 7 | margin: 0; 8 | padding: 0; 9 | background: $white; 10 | border-radius: 2px; 11 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.25); 12 | max-height: 215px; 13 | max-width: 250px; 14 | // width: 250px; 15 | overflow: auto; 16 | border: 1px solid $gray-20; 17 | border-radius: 0 0 3px 3px; 18 | box-shadow: 0 8px 20px -2px rgba(0,0,0,.25); 19 | } 20 | .completions > li { 21 | margin: 0; 22 | padding: 0; 23 | border-bottom: 1px solid $gray-5; 24 | display: block; 25 | } 26 | .completions > li > button { 27 | box-sizing: border-box; 28 | margin: 0; 29 | display: block; 30 | width: 100%; 31 | text-align: left; 32 | border: none; 33 | background: none; 34 | cursor: pointer; 35 | padding: 8px 12px; 36 | } 37 | .completions > li.active > button { 38 | background: $blue; 39 | span { 40 | color: $white; 41 | } 42 | } 43 | /*.completions > li > button:hover { 44 | background: $blue; 45 | span { 46 | color: $white; 47 | } 48 | } 49 | .completions > li > button:focus { 50 | background: $blue; 51 | span { 52 | color: $white; 53 | } 54 | outline: none; 55 | }*/ 56 | .completions > li > button > .mention--username { 57 | font-weight: 600; 58 | font-size: 15px; 59 | } 60 | 61 | .completions > li > button > .mention--name { 62 | color: $gray-80; 63 | margin-left: 4px; 64 | } 65 | 66 | .completions > li > button > .matched { 67 | font-weight: bold; 68 | color: black; 69 | } 70 | .completions > li > button > * { 71 | vertical-align: "middle"; 72 | } 73 | 74 | .matched,.unmatched{ 75 | //display:none; 76 | } 77 | 78 | .textarea-mention-control { 79 | width: 25px; 80 | height: 25px; 81 | right: 10px; 82 | top: 7px; 83 | z-index:4; 84 | } 85 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Quill Mentions 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 | 17 | 18 | 19 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/module-mentions.js: -------------------------------------------------------------------------------- 1 | const h = (tag, attrs, ...children) => { 2 | const elem = document.createElement(tag); 3 | Object.keys(attrs).forEach(key => elem[key] = attrs[key]); 4 | children.forEach(child => { 5 | if (typeof child === "string") { 6 | child = document.createTextNode(child); 7 | } 8 | elem.appendChild(child); 9 | }); 10 | return elem; 11 | }; 12 | 13 | const Inline = Quill.import("blots/inline"); 14 | 15 | class MentionBlot extends Inline { 16 | static create(label) { 17 | const node = super.create(); 18 | node.dataset.label = label; 19 | return node; 20 | } 21 | static formats(node) { 22 | return node.dataset.label; 23 | } 24 | format(name, value) { 25 | if (name === "mention" && value) { 26 | this.domNode.dataset.label = value; 27 | } else { 28 | super.format(name, value); 29 | } 30 | } 31 | 32 | formats() { 33 | const formats = super.formats(); 34 | formats["mention"] = MentionBlot.formats(this.domNode); 35 | return formats; 36 | } 37 | } 38 | 39 | MentionBlot.blotName = "mention"; 40 | MentionBlot.tagName = "SPAN"; 41 | MentionBlot.className = "mention"; 42 | 43 | Quill.register({ 44 | "formats/mention": MentionBlot 45 | }); 46 | 47 | class Mentions { 48 | constructor(quill, props) { 49 | this.quill = quill; 50 | this.onClose = props.onClose; 51 | this.onOpen = props.onOpen; 52 | this.users = props.users; 53 | if (!this.users || (this.users && this.users.length < 1)){ 54 | return; 55 | } 56 | this.quill.root.setAttribute("data-gramm", false); 57 | this.container = this.quill.container.parentNode.querySelector(props.container); 58 | this.container = document.createElement("ul"); 59 | this.container.classList.add("completions"); 60 | this.quill.container.appendChild(this.container); 61 | this.container.style.position = "absolute"; 62 | this.container.style.display = "none"; 63 | this.onSelectionChange = this.maybeUnfocus.bind(this); 64 | this.onTextChange = this.update.bind(this); 65 | 66 | this.mentionBtnControl = document.createElement("div"); 67 | this.mentionBtnControl.classList.add("textarea-mention-control"); 68 | this.mentionBtnControl.style.position = "absolute"; 69 | this.mentionBtnControl.innerHTML = ''; 70 | this.quill.container.appendChild(this.mentionBtnControl); 71 | this.mentionBtnControl.addEventListener("click", this.clickMentionBtn.bind(this),false); 72 | 73 | this.open = false; 74 | this.atIndex = null; 75 | this.focusedButton = null; 76 | this.currentPosition = null; 77 | this.prevUsers = null; 78 | 79 | quill.keyboard.addBinding({ 80 | key: 50, 81 | shiftKey: true, 82 | }, this.onAtKey.bind(this)); 83 | 84 | quill.keyboard.addBinding({ 85 | key: 40, 86 | collapsed: true, 87 | format: ["mention"] 88 | }, this.handleArrow.bind(this, "ArrowDown")); 89 | 90 | quill.keyboard.addBinding({ 91 | key: 38, 92 | collapsed: true, 93 | format: ["mention"] 94 | }, this.handleArrow.bind(this, "ArrowUp")); 95 | 96 | quill.keyboard.addBinding({ 97 | key: 27, 98 | collapsed: true, 99 | format: ["mention"] 100 | }, this.handleEsc.bind(this, "ArrowUp")); 101 | 102 | quill.keyboard.addBinding({ 103 | key: 13, 104 | collapsed: true 105 | }, this.handleEnter.bind(this)); 106 | 107 | quill.keyboard.bindings[13].unshift(quill.keyboard.bindings[13].pop()); 108 | } 109 | 110 | clickMentionBtn(){ 111 | const users = this.users; 112 | if (!this.open) { 113 | this.quill.insertText(this.quill.selection.savedRange.index, "@", "", "0", Quill.sources.USER); 114 | this.quill.setSelection(this.quill.selection.savedRange.index + 1, 0, Quill.sources.SILENT); 115 | } 116 | 117 | this.renderMentionBox(users); 118 | } 119 | 120 | renderMentionBox(users) { 121 | this.open = !this.open; 122 | 123 | if (!this.open) { 124 | this.quill.deleteText(this.quill.selection.savedRange.index-1, 1, Quill.sources.USER); 125 | this.quill.setSelection(this.quill.selection.savedRange.index-1, 0, Quill.sources.SILENT); 126 | } 127 | 128 | this.isBoxRender = true; 129 | let atSignBounds = this.quill.getBounds(this.quill.selection.savedRange.index); 130 | 131 | if ((atSignBounds.left + 230) > this.quill.container.offsetWidth) { 132 | this.container.style.left = "auto"; 133 | this.container.style.right = 0; 134 | } else { 135 | this.container.style.left = atSignBounds.left + "px"; 136 | } 137 | 138 | let windowHeight = window.innerHeight; 139 | let editorPos = this.quill.container.getBoundingClientRect().top; 140 | 141 | if (editorPos > windowHeight / 2) { 142 | this.container.style.top = "auto"; 143 | this.container.style.bottom = atSignBounds.top + atSignBounds.height + 15 + "px"; 144 | } else { 145 | this.container.style.top = atSignBounds.top + atSignBounds.height + 15 + "px"; 146 | this.container.style.bottom = "auto"; 147 | } 148 | this.container.style.zIndex = 99; 149 | this.renderCompletions(this.users, true); 150 | 151 | } 152 | 153 | handleEsc() { 154 | this.close(null); 155 | } 156 | 157 | handleEnter() { 158 | if (this.open) return false; 159 | return true; 160 | } 161 | 162 | onAtKey(range) { 163 | let prevText = this.quill.getText(range.index-1, 1).trim(); 164 | let nextText = this.quill.getText(range.index, 1).trim(); 165 | // if (this.open) return true; 166 | if (this.open) { 167 | close(null); 168 | } 169 | 170 | if (range.length > 0) { 171 | this.quill.deleteText(range.index, range.length, Quill.sources.USER); 172 | } 173 | 174 | if (prevText || nextText) { 175 | this.quill.insertText(range.index, "@"); 176 | } else { 177 | this.isBoxRender = false; 178 | this.quill.insertText(range.index, "@", "mention", "0", Quill.sources.USER); 179 | let atSignBounds = this.quill.getBounds(range.index); 180 | this.quill.setSelection(range.index + 1, Quill.sources.SILENT); 181 | 182 | this.atIndex = range.index; 183 | 184 | if ((atSignBounds.left + 230) > this.quill.container.offsetWidth) { 185 | this.container.style.left = "auto"; 186 | this.container.style.right = 0; 187 | } else { 188 | this.container.style.left = atSignBounds.left + "px"; 189 | } 190 | 191 | let windowHeight = window.innerHeight; 192 | let editorPos = this.quill.container.getBoundingClientRect().top; 193 | 194 | if (editorPos > windowHeight / 2) { 195 | this.container.style.top = "auto"; 196 | this.container.style.bottom = atSignBounds.top + atSignBounds.height + 15 + "px"; 197 | } else { 198 | this.container.style.top = atSignBounds.top + atSignBounds.height + 15 + "px"; 199 | this.container.style.bottom = "auto"; 200 | } 201 | 202 | this.container.style.zIndex = 99; 203 | //this.open = true; 204 | this.quill.on("text-change", this.onTextChange); 205 | this.quill.once("selection-change", this.onSelectionChange); 206 | this.update(); 207 | this.onOpen && this.onOpen(); 208 | } 209 | } 210 | 211 | handleArrow(keyType) { 212 | if (!this.open) return true; 213 | 214 | if (this.currentPosition >= 0) { 215 | if (keyType === "ArrowDown") { 216 | this.currentPosition = Math.min(this.list.length - 1, this.currentPosition + 1); 217 | if (this.list[Math.min(this.list.length - 1, this.currentPosition) - 1]) { 218 | this.list[Math.min(this.list.length - 1, this.currentPosition) - 1].classList.remove("active"); 219 | } 220 | this.list[Math.min(this.list.length - 1, this.currentPosition)].classList.add("active"); 221 | var top = this.list[Math.min(this.list.length - 1, this.currentPosition)].offsetTop + this.list[Math.min(this.list.length - 1, this.currentPosition)].offsetHeight; 222 | if (top > this.container.offsetHeight) { 223 | this.container.scrollTop = top - this.container.offsetHeight; 224 | } 225 | } else if (keyType === "ArrowUp") { 226 | this.currentPosition = Math.max(0, this.currentPosition - 1); 227 | if (this.list[Math.max(0, this.currentPosition) + 1]) { 228 | this.list[Math.max(0, this.currentPosition) + 1].classList.remove("active"); 229 | } 230 | this.list[Math.max(0, this.currentPosition)].classList.add("active"); 231 | var top = this.list[Math.max(0, this.currentPosition)].offsetTop; 232 | if (this.container.offsetHeight < (this.container.scrollHeight - top)) { 233 | this.container.scrollTop = top; 234 | } 235 | } 236 | } 237 | } 238 | 239 | update(val) { 240 | const sel = this.quill.getSelection().index; 241 | if (this.atIndex >= sel) { 242 | return this.close(null); 243 | } 244 | this.query = this.quill.getText(this.atIndex + 1, sel - this.atIndex - 1); 245 | const users = this.users 246 | .filter(u => { 247 | if (u.username.indexOf(this.query) != -1){ 248 | u.searchKey = "username"; 249 | return u; 250 | } else if (u.fullName.indexOf(this.query) != -1) { 251 | u.searchKey = "name"; 252 | return u; 253 | } 254 | }) 255 | .sort((u1, u2) => u1.username > u2.username); 256 | this.renderCompletions(users); 257 | } 258 | 259 | maybeUnfocus() { 260 | if (this.container.querySelector("*:focus")) return; 261 | this.close(null); 262 | } 263 | 264 | renderCompletions(users) { 265 | this.list = this.container.childNodes; 266 | while (this.container.firstChild) this.container.removeChild(this.container.firstChild); 267 | const buttons = Array(users.length); 268 | this.buttons = buttons; 269 | const handler = () => event => { 270 | if (event.key === "Enter" || event.keyCode === 13 271 | || event.key === "Tab" || event.keyCode === 9 272 | || event.type === "click") { 273 | 274 | event.preventDefault(); 275 | users.forEach((user, i) => { 276 | if(this.list[this.currentPosition] && this.list[this.currentPosition].id && user.id == this.list[this.currentPosition].id) { 277 | if (this.isBoxRender) { 278 | this.mentionBoxClose(user,(event.key === "Enter" || event.keyCode === 13) ? true: false, 279 | this.quill.getSelection(), 280 | (event.key === "Tab" || event.keyCode === 9) ? true: false); 281 | } else { 282 | this.close(user, (event.key === "Enter" || event.keyCode === 13) ? true: false, (event.key === "Tab" || event.keyCode === 9) ? true: false); 283 | } 284 | } 285 | }); 286 | } 287 | }; 288 | 289 | const mouseHandler = (i, user) => event => { 290 | this.currentPosition = i; 291 | this.list.forEach((list, i) => { 292 | if (list.classList.contains('active')) { 293 | list.classList.remove("active"); 294 | } 295 | }); 296 | this.list[i].classList.add("active"); 297 | }; 298 | 299 | users.forEach((user, i) => { 300 | const li = h("li", {}, 301 | h("button", {type: "button"}, 302 | h("span", {className: "matched"}, "@" + user.username), 303 | h("span", {className: "mention--name"}, user.fullName) 304 | // h("span", {className: "matched"}, "@" + (user.searchKey === 'username' ? (this.query + user.username.slice(this.query.length)) : user.username)), 305 | // h("span", {className: "mention--name"}, ' '+ (user.searchKey === 'name' ? (this.query + user.fullName.slice(this.query.length)) : user.fullName)) 306 | ) 307 | ); 308 | this.container.appendChild(li); 309 | li.setAttribute("id", user.id); 310 | this.list[i].addEventListener("mouseenter", mouseHandler(i, user)); 311 | }); 312 | 313 | 314 | if (!this.open || !this.prevUsers || this.prevUsers.length !== users.length || this.currentPosition === null) { 315 | this.currentPosition = 0; 316 | } 317 | 318 | if (this.currentPosition >= 0 && this.list[this.currentPosition]) { 319 | this.list[this.currentPosition].classList.add("active"); 320 | } 321 | 322 | 323 | if (!users.length) { 324 | this.open = false; 325 | } else if (!this.isBoxRender) { 326 | this.open = true; 327 | } 328 | 329 | this.list = this.container.childNodes; 330 | this.quill.container.addEventListener("keydown", handler(this)); 331 | this.container.addEventListener("click", handler(this)); 332 | if (this.open) { 333 | this.container.style.display = "block"; 334 | } 335 | else{ 336 | this.container.style.display = "none"; 337 | } 338 | this.prevUsers = users; 339 | } 340 | 341 | close(value, isEnter, isTab) { 342 | this.container.scrollTop = 0; 343 | this.container.style.display = "none"; 344 | while (this.container.firstChild) this.container.removeChild(this.container.firstChild); 345 | this.quill.off("selection-change", this.onSelectionChange); 346 | this.quill.off("text-change", this.onTextChange); 347 | 348 | if (value) { 349 | const {label, username} = value; 350 | 351 | this.quill.deleteText(this.atIndex, this.query.length + 1, Quill.sources.USER); 352 | this.quill.insertText(this.atIndex, "@" + username, "mention", label, Quill.sources.USER); 353 | // this.quill.insertText(this.atIndex + username.length + 1, " ", "mention", false, Quill.sources.USER); 354 | this.quill.setSelection(this.atIndex + username.length + 1, 0, Quill.sources.SILENT); 355 | 356 | if (isTab) { 357 | this.quill.deleteText(this.atIndex + username.length + 1, 1, Quill.sources.USER); 358 | } 359 | 360 | } 361 | this.open = false; 362 | this.onClose && this.onClose(value); 363 | } 364 | 365 | mentionBoxClose(value, isEnter, range, isTab){ 366 | this.container.scrollTop = 0; 367 | this.container.style.display = "none"; 368 | while (this.container.firstChild) this.container.removeChild(this.container.firstChild); 369 | this.quill.off("selection-change", this.onSelectionChange); 370 | this.quill.off("text-change", this.onTextChange); 371 | 372 | if (value) { 373 | const {label, username} = value; 374 | 375 | // this.quill.deleteText(range.index, 1, Quill.sources.USER); 376 | this.quill.insertText(range.index, username + ' ', "mention", label, Quill.sources.USER); 377 | // this.quill.insertText(range.index + username.length + 1, " ", "mention", false, Quill.sources.USER); 378 | this.quill.setSelection(range.index + username.length + 1, 0, Quill.sources.SILENT); 379 | 380 | if (isTab) { 381 | this.quill.deleteText(range.index-1, 1, Quill.sources.USER); 382 | } 383 | 384 | } 385 | 386 | this.open = false; 387 | this.onClose && this.onClose(value); 388 | } 389 | } 390 | 391 | Quill.register("modules/mentions", Mentions); 392 | -------------------------------------------------------------------------------- /dist/quill-mentions.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // identity function for calling harmony imports with the correct context 37 | /******/ __webpack_require__.i = function(value) { return value; }; 38 | /******/ 39 | /******/ // define getter function for harmony exports 40 | /******/ __webpack_require__.d = function(exports, name, getter) { 41 | /******/ if(!__webpack_require__.o(exports, name)) { 42 | /******/ Object.defineProperty(exports, name, { 43 | /******/ configurable: false, 44 | /******/ enumerable: true, 45 | /******/ get: getter 46 | /******/ }); 47 | /******/ } 48 | /******/ }; 49 | /******/ 50 | /******/ // getDefaultExport function for compatibility with non-harmony modules 51 | /******/ __webpack_require__.n = function(module) { 52 | /******/ var getter = module && module.__esModule ? 53 | /******/ function getDefault() { return module['default']; } : 54 | /******/ function getModuleExports() { return module; }; 55 | /******/ __webpack_require__.d(getter, 'a', getter); 56 | /******/ return getter; 57 | /******/ }; 58 | /******/ 59 | /******/ // Object.prototype.hasOwnProperty.call 60 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 61 | /******/ 62 | /******/ // __webpack_public_path__ 63 | /******/ __webpack_require__.p = ""; 64 | /******/ 65 | /******/ // Load entry module and return exports 66 | /******/ return __webpack_require__(__webpack_require__.s = 2); 67 | /******/ }) 68 | /************************************************************************/ 69 | /******/ ([ 70 | /* 0 */ 71 | /***/ (function(module, exports, __webpack_require__) { 72 | 73 | "use strict"; 74 | 75 | 76 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 77 | 78 | var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; 79 | 80 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 81 | 82 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 83 | 84 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 85 | 86 | var h = function h(tag, attrs) { 87 | for (var _len = arguments.length, children = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { 88 | children[_key - 2] = arguments[_key]; 89 | } 90 | 91 | var elem = document.createElement(tag); 92 | Object.keys(attrs).forEach(function (key) { 93 | return elem[key] = attrs[key]; 94 | }); 95 | children.forEach(function (child) { 96 | if (typeof child === "string") { 97 | child = document.createTextNode(child); 98 | } 99 | elem.appendChild(child); 100 | }); 101 | return elem; 102 | }; 103 | 104 | var Inline = Quill.import("blots/inline"); 105 | 106 | var MentionBlot = function (_Inline) { 107 | _inherits(MentionBlot, _Inline); 108 | 109 | function MentionBlot() { 110 | _classCallCheck(this, MentionBlot); 111 | 112 | return _possibleConstructorReturn(this, (MentionBlot.__proto__ || Object.getPrototypeOf(MentionBlot)).apply(this, arguments)); 113 | } 114 | 115 | _createClass(MentionBlot, [{ 116 | key: "format", 117 | value: function format(name, value) { 118 | if (name === "mention" && value) { 119 | this.domNode.dataset.label = value; 120 | } else { 121 | _get(MentionBlot.prototype.__proto__ || Object.getPrototypeOf(MentionBlot.prototype), "format", this).call(this, name, value); 122 | } 123 | } 124 | }, { 125 | key: "formats", 126 | value: function formats() { 127 | var formats = _get(MentionBlot.prototype.__proto__ || Object.getPrototypeOf(MentionBlot.prototype), "formats", this).call(this); 128 | formats["mention"] = MentionBlot.formats(this.domNode); 129 | return formats; 130 | } 131 | }], [{ 132 | key: "create", 133 | value: function create(label) { 134 | var node = _get(MentionBlot.__proto__ || Object.getPrototypeOf(MentionBlot), "create", this).call(this); 135 | node.dataset.label = label; 136 | return node; 137 | } 138 | }, { 139 | key: "formats", 140 | value: function formats(node) { 141 | return node.dataset.label; 142 | } 143 | }]); 144 | 145 | return MentionBlot; 146 | }(Inline); 147 | 148 | MentionBlot.blotName = "mention"; 149 | MentionBlot.tagName = "SPAN"; 150 | MentionBlot.className = "mention"; 151 | 152 | Quill.register({ 153 | "formats/mention": MentionBlot 154 | }); 155 | 156 | var Mentions = function () { 157 | function Mentions(quill, props) { 158 | _classCallCheck(this, Mentions); 159 | 160 | this.quill = quill; 161 | this.onClose = props.onClose; 162 | this.onOpen = props.onOpen; 163 | this.users = props.users; 164 | if (!this.users || this.users && this.users.length < 1) { 165 | return; 166 | } 167 | this.quill.root.setAttribute("data-gramm", false); 168 | this.container = this.quill.container.parentNode.querySelector(props.container); 169 | this.container = document.createElement("ul"); 170 | this.container.classList.add("completions"); 171 | this.quill.container.appendChild(this.container); 172 | this.container.style.position = "absolute"; 173 | this.container.style.display = "none"; 174 | this.onSelectionChange = this.maybeUnfocus.bind(this); 175 | this.onTextChange = this.update.bind(this); 176 | 177 | this.mentionBtnControl = document.createElement("div"); 178 | this.mentionBtnControl.classList.add("textarea-mention-control"); 179 | this.mentionBtnControl.style.position = "absolute"; 180 | this.mentionBtnControl.innerHTML = ''; 181 | this.quill.container.appendChild(this.mentionBtnControl); 182 | this.mentionBtnControl.addEventListener("click", this.clickMentionBtn.bind(this), false); 183 | 184 | this.open = false; 185 | this.atIndex = null; 186 | this.focusedButton = null; 187 | this.currentPosition = null; 188 | this.prevUsers = null; 189 | 190 | quill.keyboard.addBinding({ 191 | key: 50, 192 | shiftKey: true 193 | }, this.onAtKey.bind(this)); 194 | 195 | quill.keyboard.addBinding({ 196 | key: 40, 197 | collapsed: true, 198 | format: ["mention"] 199 | }, this.handleArrow.bind(this, "ArrowDown")); 200 | 201 | quill.keyboard.addBinding({ 202 | key: 38, 203 | collapsed: true, 204 | format: ["mention"] 205 | }, this.handleArrow.bind(this, "ArrowUp")); 206 | 207 | quill.keyboard.addBinding({ 208 | key: 27, 209 | collapsed: true, 210 | format: ["mention"] 211 | }, this.handleEsc.bind(this, "ArrowUp")); 212 | 213 | quill.keyboard.addBinding({ 214 | key: 13, 215 | collapsed: true 216 | }, this.handleEnter.bind(this)); 217 | 218 | quill.keyboard.bindings[13].unshift(quill.keyboard.bindings[13].pop()); 219 | } 220 | 221 | _createClass(Mentions, [{ 222 | key: "clickMentionBtn", 223 | value: function clickMentionBtn() { 224 | var users = this.users; 225 | if (!this.open) { 226 | this.quill.insertText(this.quill.selection.savedRange.index, "@", "", "0", Quill.sources.USER); 227 | this.quill.setSelection(this.quill.selection.savedRange.index + 1, 0, Quill.sources.SILENT); 228 | } 229 | 230 | this.renderMentionBox(users); 231 | } 232 | }, { 233 | key: "renderMentionBox", 234 | value: function renderMentionBox(users) { 235 | this.open = !this.open; 236 | 237 | if (!this.open) { 238 | this.quill.deleteText(this.quill.selection.savedRange.index - 1, 1, Quill.sources.USER); 239 | this.quill.setSelection(this.quill.selection.savedRange.index - 1, 0, Quill.sources.SILENT); 240 | } 241 | 242 | this.isBoxRender = true; 243 | var atSignBounds = this.quill.getBounds(this.quill.selection.savedRange.index); 244 | 245 | if (atSignBounds.left + 230 > this.quill.container.offsetWidth) { 246 | this.container.style.left = "auto"; 247 | this.container.style.right = 0; 248 | } else { 249 | this.container.style.left = atSignBounds.left + "px"; 250 | } 251 | 252 | var windowHeight = window.innerHeight; 253 | var editorPos = this.quill.container.getBoundingClientRect().top; 254 | 255 | if (editorPos > windowHeight / 2) { 256 | this.container.style.top = "auto"; 257 | this.container.style.bottom = atSignBounds.top + atSignBounds.height + 15 + "px"; 258 | } else { 259 | this.container.style.top = atSignBounds.top + atSignBounds.height + 15 + "px"; 260 | this.container.style.bottom = "auto"; 261 | } 262 | this.container.style.zIndex = 99; 263 | this.renderCompletions(this.users, true); 264 | } 265 | }, { 266 | key: "handleEsc", 267 | value: function handleEsc() { 268 | this.close(null); 269 | } 270 | }, { 271 | key: "handleEnter", 272 | value: function handleEnter() { 273 | if (this.open) return false; 274 | return true; 275 | } 276 | }, { 277 | key: "onAtKey", 278 | value: function onAtKey(range) { 279 | var prevText = this.quill.getText(range.index - 1, 1).trim(); 280 | var nextText = this.quill.getText(range.index, 1).trim(); 281 | // if (this.open) return true; 282 | if (this.open) { 283 | close(null); 284 | } 285 | 286 | if (range.length > 0) { 287 | this.quill.deleteText(range.index, range.length, Quill.sources.USER); 288 | } 289 | 290 | if (prevText || nextText) { 291 | this.quill.insertText(range.index, "@"); 292 | } else { 293 | this.isBoxRender = false; 294 | this.quill.insertText(range.index, "@", "mention", "0", Quill.sources.USER); 295 | var atSignBounds = this.quill.getBounds(range.index); 296 | this.quill.setSelection(range.index + 1, Quill.sources.SILENT); 297 | 298 | this.atIndex = range.index; 299 | 300 | if (atSignBounds.left + 230 > this.quill.container.offsetWidth) { 301 | this.container.style.left = "auto"; 302 | this.container.style.right = 0; 303 | } else { 304 | this.container.style.left = atSignBounds.left + "px"; 305 | } 306 | 307 | var windowHeight = window.innerHeight; 308 | var editorPos = this.quill.container.getBoundingClientRect().top; 309 | 310 | if (editorPos > windowHeight / 2) { 311 | this.container.style.top = "auto"; 312 | this.container.style.bottom = atSignBounds.top + atSignBounds.height + 15 + "px"; 313 | } else { 314 | this.container.style.top = atSignBounds.top + atSignBounds.height + 15 + "px"; 315 | this.container.style.bottom = "auto"; 316 | } 317 | 318 | this.container.style.zIndex = 99; 319 | //this.open = true; 320 | this.quill.on("text-change", this.onTextChange); 321 | this.quill.once("selection-change", this.onSelectionChange); 322 | this.update(); 323 | this.onOpen && this.onOpen(); 324 | } 325 | } 326 | }, { 327 | key: "handleArrow", 328 | value: function handleArrow(keyType) { 329 | if (!this.open) return true; 330 | 331 | if (this.currentPosition >= 0) { 332 | if (keyType === "ArrowDown") { 333 | this.currentPosition = Math.min(this.list.length - 1, this.currentPosition + 1); 334 | if (this.list[Math.min(this.list.length - 1, this.currentPosition) - 1]) { 335 | this.list[Math.min(this.list.length - 1, this.currentPosition) - 1].classList.remove("active"); 336 | } 337 | this.list[Math.min(this.list.length - 1, this.currentPosition)].classList.add("active"); 338 | var top = this.list[Math.min(this.list.length - 1, this.currentPosition)].offsetTop + this.list[Math.min(this.list.length - 1, this.currentPosition)].offsetHeight; 339 | if (top > this.container.offsetHeight) { 340 | this.container.scrollTop = top - this.container.offsetHeight; 341 | } 342 | } else if (keyType === "ArrowUp") { 343 | this.currentPosition = Math.max(0, this.currentPosition - 1); 344 | if (this.list[Math.max(0, this.currentPosition) + 1]) { 345 | this.list[Math.max(0, this.currentPosition) + 1].classList.remove("active"); 346 | } 347 | this.list[Math.max(0, this.currentPosition)].classList.add("active"); 348 | var top = this.list[Math.max(0, this.currentPosition)].offsetTop; 349 | if (this.container.offsetHeight < this.container.scrollHeight - top) { 350 | this.container.scrollTop = top; 351 | } 352 | } 353 | } 354 | } 355 | }, { 356 | key: "update", 357 | value: function update(val) { 358 | var _this2 = this; 359 | 360 | var sel = this.quill.getSelection().index; 361 | if (this.atIndex >= sel) { 362 | return this.close(null); 363 | } 364 | this.query = this.quill.getText(this.atIndex + 1, sel - this.atIndex - 1); 365 | var users = this.users.filter(function (u) { 366 | if (u.username.indexOf(_this2.query) != -1) { 367 | u.searchKey = "username"; 368 | return u; 369 | } else if (u.fullName.indexOf(_this2.query) != -1) { 370 | u.searchKey = "name"; 371 | return u; 372 | } 373 | }).sort(function (u1, u2) { 374 | return u1.username > u2.username; 375 | }); 376 | this.renderCompletions(users); 377 | } 378 | }, { 379 | key: "maybeUnfocus", 380 | value: function maybeUnfocus() { 381 | if (this.container.querySelector("*:focus")) return; 382 | this.close(null); 383 | } 384 | }, { 385 | key: "renderCompletions", 386 | value: function renderCompletions(users) { 387 | var _this3 = this; 388 | 389 | this.list = this.container.childNodes; 390 | while (this.container.firstChild) { 391 | this.container.removeChild(this.container.firstChild); 392 | }var buttons = Array(users.length); 393 | this.buttons = buttons; 394 | var handler = function handler() { 395 | return function (event) { 396 | if (event.key === "Enter" || event.keyCode === 13 || event.key === "Tab" || event.keyCode === 9 || event.type === "click") { 397 | 398 | event.preventDefault(); 399 | users.forEach(function (user, i) { 400 | if (_this3.list[_this3.currentPosition] && _this3.list[_this3.currentPosition].id && user.id == _this3.list[_this3.currentPosition].id) { 401 | if (_this3.isBoxRender) { 402 | _this3.mentionBoxClose(user, event.key === "Enter" || event.keyCode === 13 ? true : false, _this3.quill.getSelection(), event.key === "Tab" || event.keyCode === 9 ? true : false); 403 | } else { 404 | _this3.close(user, event.key === "Enter" || event.keyCode === 13 ? true : false, event.key === "Tab" || event.keyCode === 9 ? true : false); 405 | } 406 | } 407 | }); 408 | } 409 | }; 410 | }; 411 | 412 | var mouseHandler = function mouseHandler(i, user) { 413 | return function (event) { 414 | _this3.currentPosition = i; 415 | _this3.list.forEach(function (list, i) { 416 | if (list.classList.contains('active')) { 417 | list.classList.remove("active"); 418 | } 419 | }); 420 | _this3.list[i].classList.add("active"); 421 | }; 422 | }; 423 | 424 | users.forEach(function (user, i) { 425 | var li = h("li", {}, h("button", { type: "button" }, h("span", { className: "matched" }, "@" + user.username), h("span", { className: "mention--name" }, user.fullName) 426 | // h("span", {className: "matched"}, "@" + (user.searchKey === 'username' ? (this.query + user.username.slice(this.query.length)) : user.username)), 427 | // h("span", {className: "mention--name"}, ' '+ (user.searchKey === 'name' ? (this.query + user.fullName.slice(this.query.length)) : user.fullName)) 428 | )); 429 | _this3.container.appendChild(li); 430 | li.setAttribute("id", user.id); 431 | _this3.list[i].addEventListener("mouseenter", mouseHandler(i, user)); 432 | }); 433 | 434 | if (!this.open || !this.prevUsers || this.prevUsers.length !== users.length || this.currentPosition === null) { 435 | this.currentPosition = 0; 436 | } 437 | 438 | if (this.currentPosition >= 0 && this.list[this.currentPosition]) { 439 | this.list[this.currentPosition].classList.add("active"); 440 | } 441 | 442 | if (!users.length) { 443 | this.open = false; 444 | } else if (!this.isBoxRender) { 445 | this.open = true; 446 | } 447 | 448 | this.list = this.container.childNodes; 449 | this.quill.container.addEventListener("keydown", handler(this)); 450 | this.container.addEventListener("click", handler(this)); 451 | if (this.open) { 452 | this.container.style.display = "block"; 453 | } else { 454 | this.container.style.display = "none"; 455 | } 456 | this.prevUsers = users; 457 | } 458 | }, { 459 | key: "close", 460 | value: function close(value, isEnter, isTab) { 461 | this.container.scrollTop = 0; 462 | this.container.style.display = "none"; 463 | while (this.container.firstChild) { 464 | this.container.removeChild(this.container.firstChild); 465 | }this.quill.off("selection-change", this.onSelectionChange); 466 | this.quill.off("text-change", this.onTextChange); 467 | 468 | if (value) { 469 | var label = value.label, 470 | username = value.username; 471 | 472 | 473 | this.quill.deleteText(this.atIndex, this.query.length + 1, Quill.sources.USER); 474 | this.quill.insertText(this.atIndex, "@" + username, "mention", label, Quill.sources.USER); 475 | // this.quill.insertText(this.atIndex + username.length + 1, " ", "mention", false, Quill.sources.USER); 476 | this.quill.setSelection(this.atIndex + username.length + 1, 0, Quill.sources.SILENT); 477 | 478 | if (isTab) { 479 | this.quill.deleteText(this.atIndex + username.length + 1, 1, Quill.sources.USER); 480 | } 481 | } 482 | this.open = false; 483 | this.onClose && this.onClose(value); 484 | } 485 | }, { 486 | key: "mentionBoxClose", 487 | value: function mentionBoxClose(value, isEnter, range, isTab) { 488 | this.container.scrollTop = 0; 489 | this.container.style.display = "none"; 490 | while (this.container.firstChild) { 491 | this.container.removeChild(this.container.firstChild); 492 | }this.quill.off("selection-change", this.onSelectionChange); 493 | this.quill.off("text-change", this.onTextChange); 494 | 495 | if (value) { 496 | var label = value.label, 497 | username = value.username; 498 | 499 | // this.quill.deleteText(range.index, 1, Quill.sources.USER); 500 | 501 | this.quill.insertText(range.index, username + ' ', "mention", label, Quill.sources.USER); 502 | // this.quill.insertText(range.index + username.length + 1, " ", "mention", false, Quill.sources.USER); 503 | this.quill.setSelection(range.index + username.length + 1, 0, Quill.sources.SILENT); 504 | 505 | if (isTab) { 506 | this.quill.deleteText(range.index - 1, 1, Quill.sources.USER); 507 | } 508 | } 509 | 510 | this.open = false; 511 | this.onClose && this.onClose(value); 512 | } 513 | }]); 514 | 515 | return Mentions; 516 | }(); 517 | 518 | Quill.register("modules/mentions", Mentions); 519 | 520 | /***/ }), 521 | /* 1 */ 522 | /***/ (function(module, exports) { 523 | 524 | // removed by extract-text-webpack-plugin 525 | 526 | /***/ }), 527 | /* 2 */ 528 | /***/ (function(module, exports, __webpack_require__) { 529 | 530 | "use strict"; 531 | 532 | 533 | var _base = __webpack_require__(1); 534 | 535 | var _base2 = _interopRequireDefault(_base); 536 | 537 | var _moduleMentions = __webpack_require__(0); 538 | 539 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 540 | 541 | /***/ }) 542 | /******/ ]); --------------------------------------------------------------------------------