├── 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 |
13 |
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 | /******/ ]);
--------------------------------------------------------------------------------