├── src ├── js │ ├── index.js │ ├── utilities │ │ ├── identity.js │ │ ├── regexp.js │ │ ├── string-replace.js │ │ ├── ajax.js │ │ ├── dom.js │ │ └── extend.js │ ├── defaults │ │ └── defaults.js │ ├── keyboard.js │ ├── controller.js │ ├── view.js │ └── mentions.js └── scss │ └── style.scss ├── .gitignore ├── docs ├── scripts │ ├── linenumber.js │ └── prettify │ │ ├── lang-css.js │ │ └── Apache-License-2.0.txt ├── index.js.html ├── styles │ ├── prettify-jsdoc.css │ ├── prettify-tomorrow.css │ └── jsdoc-default.css ├── format.js.html ├── index.html ├── module-search.html ├── identity.js.html ├── string-replace.js.html ├── ajax.js.html ├── ajax.html ├── identity.html ├── extend.html ├── module-mentions.html ├── extend.js.html ├── defaults.html ├── module-controller-Controller.html ├── search.js.html ├── defaults.js.html ├── keyboard.js.html ├── defaults-ajaxDefaults.html ├── KEYS.html ├── keydown.html ├── controller.js.html ├── module-controller.html ├── view.js.html ├── module-controller-AJAXController.html └── mentions.js.html ├── dist ├── style.css ├── quill-mentions.css.map ├── quill-mentions.css └── quill-mentions.min.js ├── package.json ├── lp ├── utilities.js ├── style.css └── quill.base.css ├── LICENSE ├── gruntfile.js ├── README.md └── index.html /src/js/index.js: -------------------------------------------------------------------------------- 1 | global.QuillMentions = require("./mentions"); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | favicon.ico 2 | .sass-cache 3 | node_modules 4 | plugin -------------------------------------------------------------------------------- /src/js/utilities/identity.js: -------------------------------------------------------------------------------- 1 | /** @module utilities/identity */ 2 | 3 | module.exports = identity; 4 | 5 | /** @function identity */ 6 | function identity(d) { 7 | return d; 8 | } -------------------------------------------------------------------------------- /src/js/utilities/regexp.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | escapeRegExp: function escapeRegExp(str) { 3 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 4 | } 5 | }; -------------------------------------------------------------------------------- /src/js/utilities/string-replace.js: -------------------------------------------------------------------------------- 1 | var escapeRegExp = require("./regexp").escapeRegExp; 2 | 3 | module.exports = { 4 | all: replaceAll, 5 | }; 6 | 7 | /** 8 | * @param {stirng} [options] - RegExp options (like "i"). 9 | **/ 10 | function replaceAll(string, toReplace, replaceWith, options) { 11 | options = options || ""; 12 | var reOpts = "g" + options, 13 | re = new RegExp(escapeRegExp(toReplace), reOpts); 14 | 15 | return string.replace(re, replaceWith); 16 | } -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var counter = 0; 3 | var numbered; 4 | var source = document.getElementsByClassName('prettyprint source'); 5 | 6 | if (source && source[0]) { 7 | source = source[0].getElementsByTagName('code')[0]; 8 | 9 | numbered = source.innerHTML.split('\n'); 10 | numbered = numbered.map(function(item) { 11 | counter++; 12 | return '' + item; 13 | }); 14 | 15 | source.innerHTML = numbered.join('\n'); 16 | } 17 | })(); 18 | -------------------------------------------------------------------------------- /dist/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Helvetica', 'Arial', san-serif; 3 | font-size: 13px; 4 | padding: 25px; 5 | } 6 | #content-container { 7 | margin: auto; 8 | width: 960px; 9 | } 10 | #formatting-container { 11 | background-color: #f5f5f5; 12 | border-bottom: 1px solid #ccc; 13 | padding: 5px 12px; 14 | } 15 | #formatting-container .ql-active, 16 | #formatting-container button:hover { 17 | color: #008000; 18 | font-weight: bold; 19 | } 20 | #editor-container { 21 | height: 600px; 22 | } 23 | #editor-wrapper { 24 | border: 1px solid #aaa; 25 | box-shadow: 0 0 2px 2px #ddd; 26 | } 27 | -------------------------------------------------------------------------------- /dist/quill-mentions.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AASA,YAAkB;EAChB,UAAU,EAAE,qCACkB;EAC9B,OAAO,EAAE,IAAI;;AAEb,eAAG;EACD,eAAe,EAAE,IAAI;EACrB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEV,kBAAG;EACD,UAAU,EAAE,IAAI;EAChB,aAAa,EAAE,eAAe;EAC9B,MAAM,EAAE,OAAO;EACf,OAAO,EAAE,QAAQ;EAlBrB,iBAAiB,EAAE,eAAc;EACjC,SAAS,EAAE,eAAc;;AAqBrB,uEACQ;EACN,UAAU,EAAE,IAAI;EAChB,KAAK,EAAE,KAAK;;AAGd,6BAAa;EACX,aAAa,EAAE,IAAI;;AAIzB,6BAAmB;EACjB,OAAO,EAAE,KAAK;;;AAIlB,YAAa;EACX,MAAM,EAAE,cAAc;EACtB,SAAS,EAAE,IAAI;EACf,OAAO,EAAE,CAAC;;;AAKZ,oBAAqB;EACnB,UAAU,EAAE,IAAI;EAChB,UAAU,EAAE,4EAGkB;EAC9B,OAAO,EAAE,gBAAgB;EACzB,cAAc,EAAE,IAAI;EArDpB,iBAAiB,EAAE,eAAc;EACjC,SAAS,EAAE,eAAc;;AAwDzB,sBAAE;EACA,WAAW,EAAE,iBAAwB;EACrC,YAAY,EAAE,GAAG", 4 | "sources": ["../src/scss/style.scss"], 5 | "names": [], 6 | "file": "quill-mentions.css" 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quill-mentions", 3 | "version": "0.2.4", 4 | "description": "a module for integrating at-style mentions into quill.js", 5 | "keywords": [ 6 | "quill", 7 | "mentions", 8 | "supergoodcodeiswear", 9 | "bugsgalorenojkbutseriously" 10 | ], 11 | "main": "src/js/mentions.js", 12 | "directories": { 13 | "doc": "docs" 14 | }, 15 | "scripts": { 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "author": "boots", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "grunt": "~0.4.5", 22 | "grunt-browserify": "~3.3.0", 23 | "grunt-contrib-sass": "~0.9.2", 24 | "grunt-contrib-uglify": "~0.7.0", 25 | "matchdep": "~0.3.0", 26 | "grunt-jsdoc": "~0.5.8", 27 | "grunt-contrib-watch": "~0.6.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /src/js/utilities/ajax.js: -------------------------------------------------------------------------------- 1 | /** @module utilities/ajax */ 2 | module.exports = { 3 | 4 | // from stackoverflow 5 | // https://stackoverflow.com/questions/9838812/how-can-i-open-a-json-file-in-javascript-without-jquery 6 | /** 7 | * @function loadJSON 8 | */ 9 | loadJSON: function loadJSON(path, success, error) { 10 | var xhr = new XMLHttpRequest(); 11 | xhr.onreadystatechange = function() 12 | { 13 | if (xhr.readyState === XMLHttpRequest.DONE) { 14 | if (xhr.status === 200) { 15 | if (success) 16 | success(JSON.parse(xhr.responseText)); 17 | } else { 18 | if (error) 19 | error(xhr); 20 | } 21 | } 22 | }; 23 | xhr.open("GET", path, true); 24 | xhr.send(); 25 | return xhr; 26 | }, 27 | }; -------------------------------------------------------------------------------- /lp/utilities.js: -------------------------------------------------------------------------------- 1 | 2 | function loadJSON(path, success, error) { 3 | var xhr = new XMLHttpRequest(); 4 | xhr.onreadystatechange = function() 5 | { 6 | if (xhr.readyState === XMLHttpRequest.DONE) { 7 | if (xhr.status === 200) { 8 | if (success) 9 | success(JSON.parse(xhr.responseText)); 10 | } else { 11 | if (error) 12 | error(xhr); 13 | } 14 | } 15 | }; 16 | xhr.open("GET", path, true); 17 | xhr.send(); 18 | return xhr; 19 | } 20 | 21 | function addClass(node, className) { 22 | if (!node) return; 23 | if (!node.className) node.className = className; 24 | else if (node.className.indexOf(className) === -1) { 25 | node.className += " "+className; 26 | } 27 | } 28 | function removeClass(node, className) { 29 | if (!node) return; 30 | while (node.className.indexOf(className) !== -1) { 31 | node.className = node.className.replace(className, ""); 32 | } 33 | } -------------------------------------------------------------------------------- /src/js/utilities/dom.js: -------------------------------------------------------------------------------- 1 | module.exports.addClass = addClass; 2 | module.exports.getOlderSiblingsInclusive = getOlderSiblingsInclusive; 3 | module.exports.hasClass = hasClass; 4 | module.exports.removeClass = removeClass; 5 | 6 | function addClass(node, className) { 7 | if (!hasClass(node, className)) { 8 | node.className += " "+className; 9 | } 10 | } 11 | 12 | function getOlderSiblingsInclusive(node) { 13 | var result = [node]; 14 | if (!node) return []; 15 | while (node.previousSibling) { 16 | result.push(node.previousSibling); 17 | node = node.previousSibling; 18 | } 19 | return result; 20 | } 21 | 22 | function hasClass(node, className) { 23 | if (!node) return console.log("Called hasClass on an empty node"); 24 | return node.className.indexOf(className) !== -1; 25 | } 26 | 27 | function removeClass(node, className) { 28 | if (!hasClass(node, className)) return; 29 | while (hasClass(node, className)) { 30 | node.className = node.className.replace(className, ""); 31 | } 32 | } -------------------------------------------------------------------------------- /lp/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | color: #222; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | h1 { 8 | font-family: sans-serif; 9 | font-size: 42px; 10 | line-height: 56px; 11 | text-align: center; 12 | margin: 40px 0; 13 | } 14 | 15 | h1 small { 16 | display: block; 17 | color: #666; 18 | font-size: 24px; 19 | line-height: 32px; 20 | margin: 0 auto; 21 | max-width: 600px; 22 | padding: 0 40px; 23 | } 24 | 25 | .quill-wrapper { 26 | border-bottom: solid 5px #ddd; 27 | border-left: solid 2px #ddd; 28 | border-right: solid 2px #ddd; 29 | border-top: solid 2px #ddd; 30 | box-sizing: border-box; 31 | margin: 0 auto; 32 | max-width: 500px; 33 | } 34 | .quill-wrapper.focus { 35 | border-color: #2ba6cb; 36 | } 37 | 38 | .toolbar, 39 | .quill-wrapper .ql-container { 40 | margin: 0 auto; 41 | max-width: 500px; 42 | 43 | } 44 | 45 | .quill-wrapper .toolbar { 46 | padding: 14px 0 14px 14px; 47 | } 48 | 49 | .quill-wrapper .ql-container { 50 | border-top: solid 1px #ddd; 51 | box-sizing: border-box; 52 | height: 300px; 53 | max-width: 500px; 54 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Brett Beutell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/js/utilities/extend.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Extend module 3 | * @module utilities/extend 4 | */ 5 | module.exports = extend; 6 | 7 | /** 8 | * Shallow-copies an arbitrary number of objects' properties into the first argument. Applies "last-in-wins" policy to conflicting property names. 9 | * @function extend 10 | * @param {...Object} o 11 | */ 12 | function extend(o) { 13 | var args = [].slice.call(arguments, 0), 14 | result = args[0]; 15 | 16 | for (var i=1; i < args.length; i++) { 17 | result = extendHelper(result, args[i]); 18 | } 19 | 20 | return result; 21 | } 22 | 23 | /** 24 | * Shallow-copies one object into another. 25 | * @function extendHelper 26 | * @param {Object} destination - Object into which `source` properties will be copied. 27 | * @param {Object} source - Object whose properties will be copied into `destination`. 28 | */ 29 | function extendHelper(destination, source) { 30 | // thanks be to angus kroll 31 | // https://javascriptweblog.wordpress.com/2011/05/31/a-fresh-look-at-javascript-mixins/ 32 | for (var k in source) { 33 | if (source.hasOwnProperty(k)) { 34 | destination[k] = source[k]; 35 | } 36 | } 37 | return destination; 38 | } -------------------------------------------------------------------------------- /dist/quill-mentions.css: -------------------------------------------------------------------------------- 1 | .ql-mentions { 2 | box-shadow: -1px -1px 2px #ddd, 1px 1px 2px #ddd; 3 | display: none; 4 | } 5 | .ql-mentions ul { 6 | list-style-type: none; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | .ql-mentions ul li { 11 | background: #fff; 12 | border-bottom: solid thin #ddd; 13 | cursor: pointer; 14 | padding: 8px 12px; 15 | -webkit-transform: translateZ(1px); 16 | transform: translateZ(1px); 17 | } 18 | .ql-mentions ul li.ql-mention-choice-selected, .ql-mentions ul li:hover { 19 | background: #888; 20 | color: white; 21 | } 22 | .ql-mentions ul li:last-child { 23 | border-bottom: none; 24 | } 25 | .ql-is-mentioning.ql-mentions { 26 | display: table; 27 | } 28 | 29 | .ql-mentions { 30 | border: solid 1px #ddd; 31 | font-size: 13px; 32 | padding: 0; 33 | } 34 | 35 | .ql-mention-no-match { 36 | background: #fff; 37 | box-shadow: -1px -1px 1px #bbb, 1px 1px 1px #bbb, -1px 1px 1px #bbb, 1px -1px 1px #bbb; 38 | padding: 8px 12px 8px 8px; 39 | pointer-events: none; 40 | -webkit-transform: translateZ(1px); 41 | transform: translateZ(1px); 42 | } 43 | .ql-mention-no-match i { 44 | border-left: solid 2px #DF928E; 45 | padding-left: 6px; 46 | } 47 | 48 | /*# sourceMappingURL=quill-mentions.css.map */ 49 | -------------------------------------------------------------------------------- /src/scss/style.scss: -------------------------------------------------------------------------------- 1 | // mentions styles 2 | $faux-link: #2ba6cb; 3 | $warning-color: #DF928E; 4 | 5 | @mixin translate-z($n) { 6 | -webkit-transform: translateZ($n); // hack... makes the overlay _actually_ overlay text inside the editor. tried using z-index but no dice. 7 | transform: translateZ($n); 8 | } 9 | 10 | %ql-mentions-base { 11 | box-shadow: -1px -1px 2px #ddd, 12 | 1px 1px 2px #ddd; 13 | display: none; 14 | 15 | ul { 16 | list-style-type: none; 17 | margin: 0; 18 | padding: 0; 19 | 20 | li { 21 | background: #fff; 22 | border-bottom: solid thin #ddd; 23 | cursor: pointer; 24 | padding: 8px 12px; 25 | 26 | @include translate-z(1px); 27 | 28 | &.ql-mention-choice-selected, 29 | &:hover { 30 | background: #888; 31 | color: white; 32 | } 33 | 34 | &:last-child { 35 | border-bottom: none; 36 | } 37 | } 38 | } 39 | &.ql-is-mentioning { 40 | display: table; 41 | } 42 | } 43 | 44 | .ql-mentions { 45 | border: solid 1px #ddd; 46 | font-size: 13px; 47 | padding: 0; 48 | 49 | @extend %ql-mentions-base; 50 | } 51 | 52 | .ql-mention-no-match { 53 | background: #fff; 54 | box-shadow: -1px -1px 1px #bbb, 55 | 1px 1px 1px #bbb, 56 | -1px 1px 1px #bbb, 57 | 1px -1px 1px #bbb; 58 | padding: 8px 12px 8px 8px; 59 | pointer-events: none; 60 | 61 | @include translate-z(1px); 62 | 63 | i { 64 | border-left: solid 2px $warning-color; 65 | padding-left: 6px; 66 | } 67 | } 68 | 69 | [class^="ql-mention-item"] { 70 | // apply custom styles to the way the item looks 71 | } 72 | 73 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | "use strict"; 3 | require("matchdep").filterDev("grunt-*").forEach(grunt.loadNpmTasks); 4 | 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | watch: { 8 | js: { 9 | files: ['src/js/**/*.js'], 10 | tasks: ['buildjs'], 11 | options: { 12 | interrupt: true, 13 | }, 14 | }, 15 | css: { 16 | files: ['src/scss/**/*.scss'], 17 | tasks: ['sass'], 18 | options: { 19 | interrupt: true, 20 | }, 21 | }, 22 | }, 23 | browserify: { 24 | dist: { 25 | files: { 26 | 'dist/quill-mentions.js': ['src/js/**/*.js'], 27 | }, 28 | }, 29 | }, 30 | uglify: { 31 | build: { 32 | files: { 33 | 'dist/quill-mentions.min.js': ['dist/quill-mentions.js'], 34 | } 35 | } 36 | }, 37 | sass: { 38 | dist: { 39 | options: { 40 | style: 'expanded' 41 | }, 42 | files: { 43 | 'dist/quill-mentions.css': 'src/scss/style.scss', 44 | } 45 | } 46 | }, 47 | jsdoc: { 48 | dist: { 49 | src: ['src/js/**/*.js'], 50 | options: { 51 | destination: 'docs' 52 | } 53 | }, 54 | }, 55 | }); 56 | 57 | grunt.registerTask('default', ['browserify', 'uglify', 'sass', 'jsdoc']); 58 | grunt.registerTask('buildjs', ['browserify', 'uglify']); // don't build sass or docs bc they're sloowwwww 59 | }; -------------------------------------------------------------------------------- /docs/index.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: index.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: index.js

21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
/** @global */
29 | global.QuillMentions = require("./mentions");
30 | if (window.Quill) {
31 |     Quill.registerModule('mentions', QuillMentions);
32 | }
33 | else {
34 |     throw new Error("Quill is not defined in the global scope.");
35 | }
36 | 
37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 | 45 | 48 | 49 |
50 | 51 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /docs/styles/prettify-jsdoc.css: -------------------------------------------------------------------------------- 1 | /* JSDoc prettify.js theme */ 2 | 3 | /* plain text */ 4 | .pln { 5 | color: #000000; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | /* string content */ 11 | .str { 12 | color: #006400; 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /* a keyword */ 18 | .kwd { 19 | color: #000000; 20 | font-weight: bold; 21 | font-style: normal; 22 | } 23 | 24 | /* a comment */ 25 | .com { 26 | font-weight: normal; 27 | font-style: italic; 28 | } 29 | 30 | /* a type name */ 31 | .typ { 32 | color: #000000; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | /* a literal value */ 38 | .lit { 39 | color: #006400; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | /* punctuation */ 45 | .pun { 46 | color: #000000; 47 | font-weight: bold; 48 | font-style: normal; 49 | } 50 | 51 | /* lisp open bracket */ 52 | .opn { 53 | color: #000000; 54 | font-weight: bold; 55 | font-style: normal; 56 | } 57 | 58 | /* lisp close bracket */ 59 | .clo { 60 | color: #000000; 61 | font-weight: bold; 62 | font-style: normal; 63 | } 64 | 65 | /* a markup tag name */ 66 | .tag { 67 | color: #006400; 68 | font-weight: normal; 69 | font-style: normal; 70 | } 71 | 72 | /* a markup attribute name */ 73 | .atn { 74 | color: #006400; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | /* a markup attribute value */ 80 | .atv { 81 | color: #006400; 82 | font-weight: normal; 83 | font-style: normal; 84 | } 85 | 86 | /* a declaration */ 87 | .dec { 88 | color: #000000; 89 | font-weight: bold; 90 | font-style: normal; 91 | } 92 | 93 | /* a variable name */ 94 | .var { 95 | color: #000000; 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | /* a function name */ 101 | .fun { 102 | color: #000000; 103 | font-weight: bold; 104 | font-style: normal; 105 | } 106 | 107 | /* Specify class=linenums on a pre to get line numbering */ 108 | ol.linenums { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | } 112 | -------------------------------------------------------------------------------- /docs/format.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: format.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: format.js

21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
module.exports = addFormat;
29 | 
30 | function addFormat(QuillMentions) {
31 |     /**
32 |      * @method
33 |      */
34 |     QuillMentions.prototype.addFormat = function(className) {
35 |         this.quill.addFormat('mention', { tag: 'SPAN', "class": "ql-", });
36 |     };
37 | }
38 |
39 |
40 | 41 | 42 | 43 | 44 |
45 | 46 | 49 | 50 |
51 | 52 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Index 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Index

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |

29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 | 50 | 53 | 54 |
55 | 56 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/styles/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Menlo, Monaco, Consolas, monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /docs/module-search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Module: search 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Module: search

21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 |
29 |

30 | search 31 |

32 | 33 |
34 | 35 |
36 |
37 | 38 | 39 | 40 | 41 |
Search module
42 | 43 | 44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
Source:
66 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 | 78 | 79 | 80 | 81 |
82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 | 102 |
103 | 104 | 105 | 106 | 107 |
108 | 109 | 112 | 113 |
114 | 115 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /docs/identity.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: utilities/identity.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: utilities/identity.js

21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
/** @module utilities/identity */
29 | 
30 | module.exports = identity;
31 | 
32 | /** @function identity */
33 | function identity(d) {
34 |     return d;
35 | }
36 |
37 |
38 | 39 | 40 | 41 | 42 |
43 | 44 | 47 | 48 |
49 | 50 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/js/defaults/defaults.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module defaults/defaults 3 | */ 4 | 5 | var extend = require("../utilities/extend"), 6 | identity = require("../utilities/identity"); 7 | 8 | /** 9 | * @namespace 10 | * @prop {object} ajax - The default ajax configuration. 11 | * @prop {number} choiceMax - The maximum number of possible matches to display. 12 | * @prop {object[]} choices - A static array of possible choices. Ignored if `ajax` is truthy. 13 | * @prop {string} choiceTemplate - A string used as a template for possible choices. 14 | * @prop {string} containerClassName - The class attached to the mentions view container. 15 | * @prop {function} format - Function used by a Controller instance to munge data into expected form. 16 | * @prop {boolean} hotkeys - If false, disables navigating the popover with the keyboard. 17 | * @prop {boolean} includeTrigger - Whether to prepend triggerSymbol to the inserted mention. 18 | * @prop {number} marginTop - Amount of margin to place on top of the popover. (Controls space, in px, between the line and the popover) 19 | * @prop {RegExp} matcher - The regular expression used to trigger Controller#search 20 | * @prop {string} mentionClass - Prefixed with `ql-` for now because of how quill handles custom formats. The class given to inserted mention. 21 | * @prop {string} noMatchMessage - A message to display 22 | * @prop {string} noMatchTemplate - A template in which to display error message 23 | * @prop {string} template - A template for the popover, into which possible choices are inserted. 24 | * @prop {string} triggerSymbol - Symbol that triggers the mentioning state. 25 | */ 26 | var defaults = { 27 | ajax: false, 28 | choiceMax: 6, 29 | choices: [], 30 | choiceTemplate: "
  • {{value}}
  • ", 31 | containerClassName: "ql-mentions", 32 | format: identity, 33 | hotkeys: true, 34 | includeTrigger: false, 35 | marginTop: 10, 36 | matcher: /@\w+$/i, 37 | mentionClass: "mention-item", 38 | noMatchMessage: "Ruh Roh Raggy!", 39 | noMatchTemplate: "
    {{message}}
    ", 40 | template: '', 41 | triggerSymbol: "@", 42 | }; 43 | 44 | /** 45 | * @namespace 46 | * @prop {function} format - Mapped onto the array of possible matches returned by call to `path`. Should yield the expected interface for data, which is an object with `name` and `data` properties. 47 | * @prop {string} path - The path to endpoint we should query for possible matches. 48 | * @prop {string} queryParameter - The name of the query paramater in the url sent to `path`. 49 | */ 50 | var ajaxDefaults = { 51 | format: identity, 52 | path: null, 53 | queryParameter: "q", 54 | }; 55 | 56 | /** 57 | * Returns a configuration object for QuillMentions constructor. 58 | * @name defaultFactory 59 | */ 60 | function defaultFactory(options) { 61 | var result = extend({}, defaults, options); 62 | if (options.ajax) { 63 | result.ajax = extend({}, ajaxDefaults, options.ajax); 64 | } 65 | return result; 66 | } 67 | module.exports = defaultFactory; -------------------------------------------------------------------------------- /docs/string-replace.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: utilities/string-replace.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Source: utilities/string-replace.js

    21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 |
    var escapeRegExp = require("./regexp").escapeRegExp;
    29 | 
    30 | module.exports = {
    31 |     all: replaceAll,
    32 | };
    33 | 
    34 | /**
    35 |  * @param {stirng} [options] - RegExp options (like "i").
    36 |  **/
    37 | function replaceAll(string, toReplace, replaceWith, options) {
    38 |     options = options || "";
    39 |     var reOpts = "g" + options,
    40 |         re     = new RegExp(escapeRegExp(toReplace), reOpts);
    41 | 
    42 |     return string.replace(re, replaceWith);
    43 | }
    44 |
    45 |
    46 | 47 | 48 | 49 | 50 |
    51 | 52 | 55 | 56 |
    57 | 58 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/js/keyboard.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var DOM = require("./utilities/dom"), 4 | addClass = DOM.addClass, 5 | removeClass = DOM.removeClass; 6 | 7 | var SELECTED_CLASS = "ql-mention-choice-selected"; 8 | 9 | /** 10 | * Dispatches keyboard events to handlers 11 | * @namespace 12 | * @prop {function} 13 | */ 14 | /** 15 | * @namespace 16 | * @prop {Number} 13 - Handler for the enter key. 17 | * @prop {Number} 27 - Handler for the escape key. 18 | * @prop {Number} 38 - Handler for the up arrow key. 19 | * @prop {Number} 40 - Handler for the down arrow key. 20 | */ 21 | var keydown = { 22 | 27: keydownEscape, 23 | 38: keydownUpKey, 24 | 40: keydownDownKey, 25 | }; 26 | 27 | var keyup = { 28 | 13: keyupEnter, 29 | }; 30 | 31 | /** 32 | * @method 33 | * @this {QuillMentions} 34 | */ 35 | function keydownDownKey() { 36 | if (this.view.isHidden()) return; 37 | _moveSelection.call(this, 1); 38 | } 39 | 40 | /** 41 | * @method 42 | * @this {QuillMentions} 43 | */ 44 | function keydownUpKey() { 45 | if (this.view.isHidden()) return; 46 | _moveSelection.call(this, -1); 47 | } 48 | 49 | /** 50 | * @method 51 | * @this {QuillMentions} 52 | */ 53 | function keyupEnter() { 54 | var nodes, 55 | currIndex = this.selectedChoiceIndex, 56 | currNode; 57 | 58 | if (currIndex === -1) return; 59 | if (!this.view.hasMatches()) return; 60 | 61 | this.quill.setSelection(this._cachedRange); 62 | nodes = this.view.getMatches(); 63 | currNode = nodes[currIndex]; 64 | this.addMention(currNode); 65 | this.selectedChoiceIndex = -1; 66 | } 67 | 68 | /** 69 | * @method 70 | * @this {QuillMentions} 71 | */ 72 | function keydownEscape() { 73 | this.view.hide(); 74 | this.selectedChoiceIndex = -1; 75 | this.quill.focus(); 76 | } 77 | 78 | /** 79 | * Moves the selected list item up or down. (+steps means down, -steps means up) PUT THIS IN THE VIEW 80 | * @method 81 | * @private 82 | * @this {QuillMentions} 83 | */ 84 | function _moveSelection(steps) { 85 | var nodes, 86 | currIndex = this.selectedChoiceIndex, 87 | currNode, 88 | nextIndex, 89 | nextNode; 90 | 91 | nodes = this.view.container.querySelectorAll("li"); 92 | 93 | if (nodes.length === 0) { 94 | this.selectedChoiceIndex = -1; 95 | return; 96 | } 97 | if (currIndex !== -1) { 98 | currNode = nodes[currIndex]; 99 | removeClass(currNode, SELECTED_CLASS); 100 | } 101 | 102 | nextIndex = _normalizeIndex(currIndex + steps, nodes.length); 103 | nextNode = nodes[nextIndex]; 104 | 105 | if (nextNode) { 106 | addClass(nextNode, SELECTED_CLASS); 107 | this.selectedChoiceIndex = nextIndex; 108 | } 109 | else { 110 | console.log("Indexing error on node returned by querySelectorAll"); 111 | } 112 | 113 | } 114 | 115 | function _normalizeIndex(i, modulo) { 116 | if (modulo <= 0) throw new Error("TF are you doing? _normalizeIndex needs a nonnegative, nonzero modulo."); 117 | while (i < 0) { 118 | i += modulo; 119 | } 120 | return i % modulo; 121 | } 122 | 123 | module.exports = {keyup: keyup, keydown: keydown}; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :construction: quill-mentions 2 | _Under construction_ 3 | 4 | An at-style mentions module for quilljs. 5 | 6 | ## Get it. 7 | You can get `quill-mentions` through npm 8 | ```bash 9 | $ npm install quill-modules 10 | ``` 11 | Or you can just take the files from the `dist` folder. That works too. 12 | 13 | ## Use it. 14 | `quill-mentions` exposes a single global variable, `QuillMentions`. 15 | 16 | To include mentions in your Quill project, simply add the stylesheet and all the Javascripts to your page. 17 | 18 | Pass the global `QuillMentions` contsructor to Quill's [registerModule](http://quilljs.com/docs/api/#quillregistermodule) function, and add `mentions` to your module config when you instantiate your editor(s). 19 | 20 | ```html 21 | 22 | ... 23 | 24 | ... 25 | 26 | 27 | ... 28 | 29 | 30 | 38 | 39 | 40 | ``` 41 | 42 | ## Docs 43 | 44 | The docs are not as exhaustive as they should be, but they live (nonetheless) in the `docs` folder. 45 | 46 | :warning: If you build the docs, be warned that your build will fail if the _full path_ to your clone/fork of the repo includes any folders with an underscore. See the issue and fix [here](https://github.com/brettimus/quill-mentions/issues/1). 47 | 48 | 49 | ## Style Dependencies... 50 | **Not Yet Written** 51 | 52 | 53 | # v-3 goals 54 | * ~~inject choices (as array)~~ 55 | * ~~parse contents~~ 56 | * ~~use `@` to summon popover with possible choices matched to text~~ 57 | * ~~vertically align popover to position of calling `@`~~ 58 | 59 | # v-2 goals 60 | * ~~Customizable no-match-found messages~~ TODO - figre out how to configure so there's _no message_. Was running into issues trying this out. 61 | * ~~Keyboard events for up and down arrows~~ Also for escape and enter! 62 | * ~~Insert data with mention into markup (this might require deviating from custom quill format because custom formats are too nascent :confused:)~~ Currently, the `data-mention` attribute from a matching `li` is appended to the class of a mention `span`. This is hacky, but it avoids having to manually insert HTML... 63 | * Horizontally align the popover 64 | 65 | # V-1 goals 66 | * ~~Refactor with MVC~~ 67 | * Break out defaultFactory (the main default object has toooo much :shit: on it.) 68 | * **Horizontally align the popover** 69 | * Refactor styles to not rely on dom elements 70 | 71 | # V-0 goals 72 | * More flexible templates, allow custom `value` accessor functions 73 | * Allow config to turn of quill custom format 74 | * Customizable hotkeys 75 | 76 | # TODO 77 | * Determine horizontal rendering of mentions container. 78 | * Find alternative to current use of `transform: translateZ` on the popover list items... 79 | * ~~Write more robust regex for parsing names (separate for work)~~ 80 | * ~~Hide view after insert~~ 81 | * ~~Add keyboard events for up and down arrows~~ 82 | * ~~Don't allow 'womp womp' message to be clicked~~ 83 | * ~~Render ql-mentions container in a more logical position~~ 84 | -------------------------------------------------------------------------------- /docs/ajax.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: utilities/ajax.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Source: utilities/ajax.js

    21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 |
    /** @module utilities/ajax */
    29 | module.exports = {
    30 | 
    31 |     // from stackoverflow 
    32 |     // https://stackoverflow.com/questions/9838812/how-can-i-open-a-json-file-in-javascript-without-jquery
    33 |     /**
    34 |      * @function loadJSON
    35 |      */
    36 |     loadJSON: function loadJSON(path, success, error) {
    37 |         var xhr = new XMLHttpRequest();
    38 |         xhr.onreadystatechange = function()
    39 |         {
    40 |             if (xhr.readyState === XMLHttpRequest.DONE) {
    41 |                 if (xhr.status === 200) {
    42 |                     if (success)
    43 |                         success(JSON.parse(xhr.responseText));
    44 |                 } else {
    45 |                     if (error)
    46 |                         error(xhr);
    47 |                 }
    48 |             }
    49 |         };
    50 |         xhr.open("GET", path, true);
    51 |         xhr.send();
    52 |         return xhr;
    53 |     },
    54 | };
    55 |
    56 |
    57 | 58 | 59 | 60 | 61 |
    62 | 63 | 66 | 67 |
    68 | 69 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /docs/ajax.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Module: utilities/ajax 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Module: utilities/ajax

    21 | 22 | 23 | 24 | 25 | 26 |
    27 | 28 |
    29 |

    30 | utilities/ajax 31 |

    32 | 33 |
    34 | 35 |
    36 |
    37 | 38 | 39 | 40 | 41 | 42 | 43 |
    44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
    Source:
    64 |
    67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
    75 | 76 | 77 | 78 | 79 |
    80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 |
    99 | 100 |
    101 | 102 | 103 | 104 | 105 |
    106 | 107 | 110 | 111 |
    112 | 113 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /docs/identity.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Module: utilities/identity 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Module: utilities/identity

    21 | 22 | 23 | 24 | 25 | 26 |
    27 | 28 |
    29 |

    30 | utilities/identity 31 |

    32 | 33 |
    34 | 35 |
    36 |
    37 | 38 | 39 | 40 | 41 | 42 | 43 |
    44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
    Source:
    64 |
    67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
    75 | 76 | 77 | 78 | 79 |
    80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 |
    99 | 100 |
    101 | 102 | 103 | 104 | 105 |
    106 | 107 | 110 | 111 |
    112 | 113 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /docs/extend.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Module: utilities/extend 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Module: utilities/extend

    21 | 22 | 23 | 24 | 25 | 26 |
    27 | 28 |
    29 |

    30 | utilities/extend 31 |

    32 | 33 |
    34 | 35 |
    36 |
    37 | 38 | 39 | 40 | 41 |
    Extend module
    42 | 43 | 44 | 45 |
    46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
    Source:
    66 |
    69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
    77 | 78 | 79 | 80 | 81 |
    82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
    101 | 102 |
    103 | 104 | 105 | 106 | 107 |
    108 | 109 | 112 | 113 |
    114 | 115 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /docs/module-mentions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Module: mentions 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Module: mentions

    21 | 22 | 23 | 24 | 25 | 26 |
    27 | 28 |
    29 |

    30 | mentions 31 |

    32 | 33 |
    34 | 35 |
    36 |
    37 | 38 | 39 | 40 | 41 | 42 | 43 |
    44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
    Source:
    64 |
    67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
    75 | 76 | 77 | 78 | 79 |
    80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |

    Classes

    89 | 90 |
    91 |
    QuillMentions
    92 |
    93 |
    94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
    106 | 107 |
    108 | 109 | 110 | 111 | 112 |
    113 | 114 | 117 | 118 |
    119 | 120 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /docs/extend.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: utilities/extend.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Source: utilities/extend.js

    21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 |
    /**
    29 |  * Extend module
    30 |  * @module utilities/extend
    31 |  */
    32 | module.exports = extend;
    33 | 
    34 | /**
    35 |  * Shallow-copies an arbitrary number of objects' properties into the first argument. Applies "last-in-wins" policy to conflicting property names.
    36 |  * @function extend
    37 |  * @param {...Object} o
    38 |  */
    39 | function extend(o) {
    40 |     var args   = [].slice.call(arguments, 0),
    41 |         result = args[0];
    42 | 
    43 |     for (var i=1; i < args.length; i++) {
    44 |         result = extendHelper(result, args[i]);
    45 |     }
    46 | 
    47 |     return result;
    48 | }
    49 | 
    50 | /**
    51 |  * Shallow-copies one object into another.
    52 |  * @function extendHelper
    53 |  * @param {Object} destination - Object into which `source` properties will be copied.
    54 |  * @param {Object} source - Object whose properties will be copied into `destination`.
    55 |  */
    56 | function extendHelper(destination, source) {
    57 |     // thanks be to angus kroll
    58 |     // https://javascriptweblog.wordpress.com/2011/05/31/a-fresh-look-at-javascript-mixins/
    59 |     for (var k in source) {
    60 |         if (source.hasOwnProperty(k)) {
    61 |           destination[k] = source[k];
    62 |         }
    63 |     }
    64 |     return destination;
    65 | }
    66 |
    67 |
    68 | 69 | 70 | 71 | 72 |
    73 | 74 | 77 | 78 |
    79 | 80 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /docs/defaults.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Module: defaults/defaults 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Module: defaults/defaults

    21 | 22 | 23 | 24 | 25 | 26 |
    27 | 28 |
    29 |

    30 | defaults/defaults 31 |

    32 | 33 |
    34 | 35 |
    36 |
    37 | 38 | 39 | 40 | 41 | 42 | 43 |
    44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
    Source:
    64 |
    67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
    75 | 76 | 77 | 78 | 79 |
    80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 |

    Namespaces

    91 | 92 |
    93 |
    ajaxDefaults
    94 |
    95 | 96 |
    defaults
    97 |
    98 |
    99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |
    109 | 110 |
    111 | 112 | 113 | 114 | 115 |
    116 | 117 | 120 | 121 |
    122 | 123 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/js/controller.js: -------------------------------------------------------------------------------- 1 | // TODO - factor out data munging into separate object 2 | 3 | /** @module controller */ 4 | 5 | var loadJSON = require("./utilities/ajax").loadJSON, 6 | escapeRegExp = require("./utilities/regexp").escapeRegExp; 7 | 8 | module.exports = { 9 | Controller: Controller, 10 | AJAXController: AJAXController, 11 | }; 12 | 13 | /** 14 | * @callback searchCallback 15 | * @param {Object[]} data - An array of objects that represent possible matches to data. The data are mapped over a formatter to provide a consistent interface. 16 | */ 17 | 18 | 19 | function Controller(formatter, view, options) { 20 | this.format = formatter; 21 | this.view = view; 22 | this.database = this.munge(options.data); 23 | this.max = options.max; 24 | } 25 | 26 | 27 | /** 28 | * @interface 29 | * @param {function} formatter - Munges data 30 | * @param {View} view 31 | * @param {object} options 32 | * @prop {function} format - Munges data 33 | * @prop {View} view 34 | * @prop {number} max - Maximum number of matches to pass to the View. 35 | */ 36 | function AbstractController(formatter, view, options) { 37 | this.format = formatter; 38 | this.view = view; 39 | this.max = options.max; 40 | } 41 | 42 | /** 43 | * @abstract 44 | */ 45 | AbstractController.prototype.search = function search() { 46 | throw new Error("NYI"); 47 | }; 48 | 49 | /** 50 | * Transforms data to conform to config. 51 | * @method 52 | * @param {string} qry 53 | * @param {searchCallback} callback 54 | */ 55 | AbstractController.prototype.munge = function(data) { 56 | return data.map(this.format); 57 | }; 58 | 59 | 60 | /** 61 | * @constructor 62 | * @prop {object[]} database - All possible choices for a given mention. 63 | */ 64 | function Controller(formatter, view, options) { 65 | AbstractController.call(this, formatter, view, options); 66 | this.database = this.munge(options.data); 67 | } 68 | Controller.prototype = Object.create(AbstractController.prototype); 69 | 70 | Controller.prototype.search = function search(qry, callback) { 71 | var qryRE = new RegExp(escapeRegExp(qry), "i"), 72 | data; 73 | 74 | data = this.database.filter(function(d) { 75 | return qryRE.test(d.value); 76 | }).sort(function(d1, d2) { 77 | return d1.value.indexOf(qry) - d2.value.indexOf(qry); 78 | }); 79 | 80 | this.view.render(data.slice(0, this.max)); 81 | if (callback) callback(); 82 | }; 83 | 84 | 85 | /** 86 | * @constructor 87 | * @augments Controller 88 | * @prop {string} path - The path from which to request data. 89 | * @prop {string} queryParameter - The name of the paramter in the request to Controller~path 90 | * @prop {Object} _latestCall - Cached ajax call. Aborted if a new search is made. 91 | */ 92 | function AJAXController(formatter, view, options) { 93 | AbstractController.call(this, formatter, view, options); 94 | this.path = options.path; 95 | this.queryParameter = options.queryParameter; 96 | this._latestCall = null; 97 | } 98 | AJAXController.prototype = Object.create(AbstractController.prototype); 99 | 100 | /** 101 | * @method 102 | * @param {String} qry 103 | * @param {searchCallback} callback 104 | */ 105 | AJAXController.prototype.search = function search(qry, callback) { 106 | 107 | if (this._latestCall) this._latestCall.abort(); // caches ajax calls so we can cancel them as the input is updated 108 | var qryString = this.path + 109 | "?" + this.queryParameter + 110 | "=" + encodeURIComponent(qry); 111 | 112 | this._latestCall = loadJSON(qryString, success.bind(this), ajaxError); 113 | 114 | function success(data) { 115 | this._callback(data); 116 | if (callback) callback(); 117 | } 118 | }; 119 | 120 | /** 121 | * Munges the callback data 122 | * @method 123 | * @private 124 | * @param {array} data 125 | */ 126 | AJAXController.prototype._callback = function(data) { 127 | data = this.munge(data).slice(0, this.max); 128 | this.view.render(data); 129 | }; 130 | 131 | function ajaxError(error) { 132 | console.log("Loading json errored! Likely due to aborted request, but there's the error: ", error); 133 | } -------------------------------------------------------------------------------- /lp/quill.base.css: -------------------------------------------------------------------------------- 1 | /*! Quill Editor v0.19.12 2 | * https://quilljs.com/ 3 | * Copyright (c) 2014, Jason Chen 4 | * Copyright (c) 2013, salesforce.com 5 | */ 6 | .ql-image-tooltip { 7 | padding: 10px; 8 | width: 300px; 9 | } 10 | .ql-image-tooltip:after { 11 | clear: both; 12 | content: ""; 13 | display: table; 14 | } 15 | .ql-image-tooltip a { 16 | border: 1px solid #000; 17 | box-sizing: border-box; 18 | display: inline-block; 19 | float: left; 20 | padding: 5px; 21 | text-align: center; 22 | width: 50%; 23 | } 24 | .ql-image-tooltip img { 25 | bottom: 0; 26 | left: 0; 27 | margin: auto; 28 | max-height: 100%; 29 | max-width: 100%; 30 | position: absolute; 31 | right: 0; 32 | top: 0; 33 | } 34 | .ql-image-tooltip .input { 35 | box-sizing: border-box; 36 | width: 100%; 37 | } 38 | .ql-image-tooltip .preview { 39 | margin: 10px 0px; 40 | position: relative; 41 | border: 1px dashed #000; 42 | height: 200px; 43 | } 44 | .ql-image-tooltip .preview span { 45 | display: inline-block; 46 | position: absolute; 47 | text-align: center; 48 | top: 40%; 49 | width: 100%; 50 | } 51 | .ql-link-tooltip { 52 | padding: 5px 10px; 53 | } 54 | .ql-link-tooltip input.input { 55 | width: 170px; 56 | } 57 | .ql-link-tooltip input.input, 58 | .ql-link-tooltip a.done { 59 | display: none; 60 | } 61 | .ql-link-tooltip a.change { 62 | margin-right: 4px; 63 | } 64 | .ql-link-tooltip.editing input.input, 65 | .ql-link-tooltip.editing a.done { 66 | display: inline-block; 67 | } 68 | .ql-link-tooltip.editing a.url, 69 | .ql-link-tooltip.editing a.change, 70 | .ql-link-tooltip.editing a.remove { 71 | display: none; 72 | } 73 | .ql-multi-cursor { 74 | position: absolute; 75 | left: 0; 76 | top: 0; 77 | z-index: 1000; 78 | } 79 | .ql-multi-cursor .cursor { 80 | margin-left: -1px; 81 | position: absolute; 82 | } 83 | .ql-multi-cursor .cursor-flag { 84 | bottom: 100%; 85 | position: absolute; 86 | white-space: nowrap; 87 | } 88 | .ql-multi-cursor .cursor-name { 89 | display: inline-block; 90 | color: #fff; 91 | padding: 2px 8px; 92 | } 93 | .ql-multi-cursor .cursor-caret { 94 | height: 100%; 95 | position: absolute; 96 | width: 2px; 97 | } 98 | .ql-multi-cursor .cursor.hidden .cursor-flag { 99 | display: none; 100 | } 101 | .ql-multi-cursor .cursor.top .cursor-flag { 102 | bottom: auto; 103 | top: 100%; 104 | } 105 | .ql-multi-cursor .cursor.right .cursor-flag { 106 | right: -2px; 107 | } 108 | .ql-paste-manager { 109 | left: -100000px; 110 | position: absolute; 111 | top: 50%; 112 | } 113 | .ql-toolbar { 114 | box-sizing: border-box; 115 | } 116 | .ql-tooltip { 117 | background-color: #fff; 118 | border: 1px solid #000; 119 | box-sizing: border-box; 120 | position: absolute; 121 | top: 0px; 122 | white-space: nowrap; 123 | z-index: 2000; 124 | } 125 | .ql-tooltip a { 126 | cursor: pointer; 127 | text-decoration: none; 128 | } 129 | .ql-container { 130 | box-sizing: border-box; 131 | cursor: text; 132 | font-family: Helvetica, 'Arial', sans-serif; 133 | font-size: 13px; 134 | height: 100%; 135 | line-height: 1.42; 136 | margin: 0px; 137 | overflow-x: hidden; 138 | overflow-y: auto; 139 | padding: 12px 15px; 140 | position: relative; 141 | } 142 | .ql-editor { 143 | box-sizing: border-box; 144 | min-height: 100%; 145 | outline: none; 146 | tab-size: 4; 147 | white-space: pre-wrap; 148 | } 149 | .ql-editor div { 150 | margin: 0; 151 | padding: 0; 152 | } 153 | .ql-editor a { 154 | text-decoration: underline; 155 | } 156 | .ql-editor b { 157 | font-weight: bold; 158 | } 159 | .ql-editor i { 160 | font-style: italic; 161 | } 162 | .ql-editor s { 163 | text-decoration: line-through; 164 | } 165 | .ql-editor u { 166 | text-decoration: underline; 167 | } 168 | .ql-editor a, 169 | .ql-editor b, 170 | .ql-editor i, 171 | .ql-editor s, 172 | .ql-editor u, 173 | .ql-editor span { 174 | background-color: inherit; 175 | } 176 | .ql-editor img { 177 | max-width: 100%; 178 | } 179 | .ql-editor blockquote, 180 | .ql-editor ol, 181 | .ql-editor ul { 182 | margin: 0 0 0 2em; 183 | padding: 0; 184 | } 185 | .ql-editor ol { 186 | list-style-type: decimal; 187 | } 188 | .ql-editor ul { 189 | list-style-type: disc; 190 | } 191 | .ql-editor.ql-ie-9 br, 192 | .ql-editor.ql-ie-10 br { 193 | display: none; 194 | } 195 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Quill Mentions 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 |

    21 | Quill Mentions 22 | 23 | Try this totally badass example with emoji
    24 | :metal: 25 |
    26 |

    27 | 28 |
    29 |
    30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
    48 |
    49 |
    hello there! 😅
    50 |

    51 |
    enter a colon and at least two letters to search for emoji
    52 |
    53 |
    54 | 55 | 56 | 57 | 58 | 108 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /docs/module-controller-Controller.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Class: Controller 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Class: Controller

    21 | 22 | 23 | 24 | 25 | 26 |
    27 | 28 |
    29 |

    30 | controller~ 31 | 32 | Controller 33 |

    34 | 35 |
    36 | 37 |
    38 |
    39 | 40 | 41 | 42 | 43 |
    44 |

    new Controller()

    45 | 46 | 47 |
    48 |
    49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
    60 | 61 | 62 |
    Properties:
    63 | 64 |
    65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
    NameTypeDescription
    database 92 | 93 | 94 | object[] 95 | 96 | 97 | 98 | All possible choices for a given mention.
    110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
    Source:
    131 |
    134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |
    142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 |
    156 | 157 | 158 |
    159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 |
    178 | 179 |
    180 | 181 | 182 | 183 | 184 |
    185 | 186 | 189 | 190 |
    191 | 192 | 195 | 196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /docs/search.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: search.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Source: search.js

    21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 |
    // TODO - rename to "model"
     29 | var loadJSON = require("./utilities/ajax").loadJSON;
     30 | /**
     31 |  * @callback searchCallback
     32 |  * @param {Object[]} data - An array of objects that represent possible matches to data. The data are mapped over a formatter to provide a consistent interface.
     33 |  */
     34 | 
     35 | function search(qry, callback) {
     36 |     var searcher = this.options.ajax ? this.ajaxSearch : this.staticSearch;
     37 |     searcher.call(this, qry, callback);
     38 | }
     39 | 
     40 | module.exports = function addSearch(QuillMentions) {
     41 |     /**
     42 |      * Dispatches search for possible matches to a query.
     43 |      * @method 
     44 |      * @param {string} qry
     45 |      * @param {searchCallback} callback - Callback that handles the possible matches
     46 |      */
     47 |     QuillMentions.prototype.search = search;
     48 | 
     49 |     /**
     50 |      * @method
     51 |      * @param {string} qry
     52 |      * @param {searchCallback} callback - Callback that handles possible matches
     53 |      */
     54 |     QuillMentions.prototype.staticSearch = function staticSearch(qry, callback) {
     55 |         console.log("Static Search Query", qry);
     56 |         var data = this.options.choices.filter(staticFilter(qry).bind(this));
     57 |         if (!callback) noCallbackError("staticSearch");
     58 |         callback(data);
     59 |     };
     60 | 
     61 |     /**
     62 |      * @method
     63 |      * @param {string} qry
     64 |      * @param {searchCallback} callback - Callback that handles possible matches
     65 |      */
     66 |     QuillMentions.prototype.ajaxSearch = function ajaxSearch(qry, callback) {
     67 |         // TODO - remember last ajax request, and if it's still pending, cancel it.
     68 |         //       ... to that end, just use promises.
     69 | 
     70 |         if (ajaxSearch.latest) ajaxSearch.latest.abort();
     71 | 
     72 |         var path = this.options.ajax.path,
     73 |             formatData = this.options.ajax.format,
     74 |             queryParameter = this.options.ajax.queryParameter,
     75 |             qryString = path + "?" + queryParameter + "=" + encodeURIComponent(qry);
     76 | 
     77 |         ajaxSearch.latest = loadJSON(qryString, ajaxSuccess(callback, formatData), ajaxError);
     78 |     };
     79 | };
     80 | 
     81 | function staticFilter(qry) {
     82 |     return function(choice) {
     83 |         var formatter = this.options.format;
     84 |         return choice.name.toLowerCase().indexOf(qry.toLowerCase()) !== -1;
     85 |     };
     86 | }
     87 | 
     88 | function ajaxSuccess(callback, formatter) {
     89 |     return function(data) {
     90 |         if (callback) callback(data.map(formatter));
     91 |         else noCallbackError("ajaxSearch");
     92 |     };
     93 | }
     94 | 
     95 | function ajaxError(error) {
     96 |     console.log("Loading json errored...", error);
     97 | }
     98 | 
     99 | function noCallbackError(functionName) {
    100 |     console.log("Warning!", functionName, "was not provided a callback. Don't be a ding-dong.");
    101 | }
    102 |
    103 |
    104 | 105 | 106 | 107 | 108 |
    109 | 110 | 113 | 114 |
    115 | 116 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /docs/styles/jsdoc-default.css: -------------------------------------------------------------------------------- 1 | html 2 | { 3 | overflow: auto; 4 | background-color: #fff; 5 | } 6 | 7 | body 8 | { 9 | font: 14px "DejaVu Sans Condensed", "Liberation Sans", "Nimbus Sans L", Tahoma, Geneva, "Helvetica Neue", Helvetica, Arial, sans serif; 10 | line-height: 130%; 11 | color: #000; 12 | background-color: #fff; 13 | } 14 | 15 | a { 16 | color: #444; 17 | } 18 | 19 | a:visited { 20 | color: #444; 21 | } 22 | 23 | a:active { 24 | color: #444; 25 | } 26 | 27 | header 28 | { 29 | display: block; 30 | padding: 6px 4px; 31 | } 32 | 33 | .class-description { 34 | font-style: italic; 35 | font-family: Palatino, 'Palatino Linotype', serif; 36 | font-size: 130%; 37 | line-height: 140%; 38 | margin-bottom: 1em; 39 | margin-top: 1em; 40 | } 41 | 42 | #main { 43 | float: left; 44 | width: 100%; 45 | } 46 | 47 | section 48 | { 49 | display: block; 50 | 51 | background-color: #fff; 52 | padding: 12px 24px; 53 | border-bottom: 1px solid #ccc; 54 | margin-right: 240px; 55 | } 56 | 57 | .variation { 58 | display: none; 59 | } 60 | 61 | .optional:after { 62 | content: "opt"; 63 | font-size: 60%; 64 | color: #aaa; 65 | font-style: italic; 66 | font-weight: lighter; 67 | } 68 | 69 | nav 70 | { 71 | display: block; 72 | float: left; 73 | margin-left: -230px; 74 | margin-top: 28px; 75 | width: 220px; 76 | border-left: 1px solid #ccc; 77 | padding-left: 9px; 78 | } 79 | 80 | nav ul { 81 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif; 82 | font-size: 100%; 83 | line-height: 17px; 84 | padding:0; 85 | margin:0; 86 | list-style-type:none; 87 | } 88 | 89 | nav h2 a, nav h2 a:visited { 90 | color: #A35A00; 91 | text-decoration: none; 92 | } 93 | 94 | nav h3 { 95 | margin-top: 12px; 96 | } 97 | 98 | nav li { 99 | margin-top: 6px; 100 | } 101 | 102 | nav a { 103 | color: #5C5954; 104 | } 105 | 106 | nav a:visited { 107 | color: #5C5954; 108 | } 109 | 110 | nav a:active { 111 | color: #5C5954; 112 | } 113 | 114 | footer { 115 | display: block; 116 | padding: 6px; 117 | margin-top: 12px; 118 | font-style: italic; 119 | font-size: 90%; 120 | } 121 | 122 | h1 123 | { 124 | font-size: 200%; 125 | font-weight: bold; 126 | letter-spacing: -0.01em; 127 | margin: 6px 0 9px 0; 128 | } 129 | 130 | h2 131 | { 132 | font-size: 170%; 133 | font-weight: bold; 134 | letter-spacing: -0.01em; 135 | margin: 6px 0 3px 0; 136 | } 137 | 138 | h3 139 | { 140 | font-size: 150%; 141 | font-weight: bold; 142 | letter-spacing: -0.01em; 143 | margin-top: 16px; 144 | margin: 6px 0 3px 0; 145 | } 146 | 147 | h4 148 | { 149 | font-size: 130%; 150 | font-weight: bold; 151 | letter-spacing: -0.01em; 152 | margin-top: 16px; 153 | margin: 18px 0 3px 0; 154 | color: #A35A00; 155 | } 156 | 157 | h5, .container-overview .subsection-title 158 | { 159 | font-size: 120%; 160 | font-weight: bold; 161 | letter-spacing: -0.01em; 162 | margin: 8px 0 3px -16px; 163 | } 164 | 165 | h6 166 | { 167 | font-size: 100%; 168 | letter-spacing: -0.01em; 169 | margin: 6px 0 3px 0; 170 | font-style: italic; 171 | } 172 | 173 | .ancestors { color: #999; } 174 | .ancestors a 175 | { 176 | color: #999 !important; 177 | text-decoration: none; 178 | } 179 | 180 | .important 181 | { 182 | font-weight: bold; 183 | color: #950B02; 184 | } 185 | 186 | .yes-def { 187 | text-indent: -1000px; 188 | } 189 | 190 | .type-signature { 191 | color: #aaa; 192 | } 193 | 194 | .name, .signature { 195 | font-family: Consolas, "Lucida Console", Monaco, monospace; 196 | } 197 | 198 | .details { margin-top: 14px; border-left: 2px solid #DDD; } 199 | .details dt { width:100px; float:left; padding-left: 10px; padding-top: 6px; } 200 | .details dd { margin-left: 50px; } 201 | .details ul { margin: 0; } 202 | .details ul { list-style-type: none; } 203 | .details li { margin-left: 30px; padding-top: 6px; } 204 | .details pre.prettyprint { margin: 0 } 205 | .details .object-value { padding-top: 0; } 206 | 207 | .description { 208 | margin-bottom: 1em; 209 | margin-left: -16px; 210 | margin-top: 1em; 211 | } 212 | 213 | .code-caption 214 | { 215 | font-style: italic; 216 | font-family: Palatino, 'Palatino Linotype', serif; 217 | font-size: 107%; 218 | margin: 0; 219 | } 220 | 221 | .prettyprint 222 | { 223 | border: 1px solid #ddd; 224 | width: 80%; 225 | overflow: auto; 226 | } 227 | 228 | .prettyprint.source { 229 | width: inherit; 230 | } 231 | 232 | .prettyprint code 233 | { 234 | font-family: Consolas, 'Lucida Console', Monaco, monospace; 235 | font-size: 100%; 236 | line-height: 18px; 237 | display: block; 238 | padding: 4px 12px; 239 | margin: 0; 240 | background-color: #fff; 241 | color: #000; 242 | border-left: 3px #ddd solid; 243 | } 244 | 245 | .prettyprint code span.line 246 | { 247 | display: inline-block; 248 | } 249 | 250 | .params, .props 251 | { 252 | border-spacing: 0; 253 | border: 0; 254 | border-collapse: collapse; 255 | } 256 | 257 | .params .name, .props .name, .name code { 258 | color: #A35A00; 259 | font-family: Consolas, 'Lucida Console', Monaco, monospace; 260 | font-size: 100%; 261 | } 262 | 263 | .params td, .params th, .props td, .props th 264 | { 265 | border: 1px solid #ddd; 266 | margin: 0px; 267 | text-align: left; 268 | vertical-align: top; 269 | padding: 4px 6px; 270 | display: table-cell; 271 | } 272 | 273 | .params thead tr, .props thead tr 274 | { 275 | background-color: #ddd; 276 | font-weight: bold; 277 | } 278 | 279 | .params .params thead tr, .props .props thead tr 280 | { 281 | background-color: #fff; 282 | font-weight: bold; 283 | } 284 | 285 | .params th, .props th { border-right: 1px solid #aaa; } 286 | .params thead .last, .props thead .last { border-right: 1px solid #ddd; } 287 | 288 | .disabled { 289 | color: #454545; 290 | } 291 | -------------------------------------------------------------------------------- /docs/defaults.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: defaults/defaults.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Source: defaults/defaults.js

    21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 |
    /**
     29 |  * @module defaults/defaults
     30 |  */
     31 | 
     32 | var extend = require("../utilities/extend"),
     33 |     identity = require("../utilities/identity");
     34 | 
     35 | /**
     36 |  * @namespace
     37 |  * @prop {object} ajax - The default ajax configuration.
     38 |  * @prop {number} choiceMax - The maximum number of possible matches to display.
     39 |  * @prop {object[]} choices - A static array of possible choices. Ignored if `ajax` is truthy.
     40 |  * @prop {string} choiceTemplate - A string used as a template for possible choices.
     41 |  * @prop {string} containerClassName - The class attached to the mentions view container.
     42 |  * @prop {function} format - Function used by a Controller instance to munge data into expected form.
     43 |  * @prop {boolean} hotkeys - If false, disables navigating the popover with the keyboard.
     44 |  * @prop {boolean} includeTrigger - Whether to prepend triggerSymbol to the inserted mention.
     45 |  * @prop {number} marginTop - Amount of margin to place on top of the popover. (Controls space, in px, between the line and the popover) 
     46 |  * @prop {RegExp} matcher - The regular expression used to trigger Controller#search
     47 |  * @prop {string} mentionClass - Prefixed with `ql-` for now because of how quill handles custom formats. The class given to inserted mention. 
     48 |  * @prop {string} noMatchMessage - A message to display 
     49 |  * @prop {string} noMatchTemplate - A template in which to display error message
     50 |  * @prop {string} template - A template for the popover, into which possible choices are inserted.
     51 |  * @prop {string} triggerSymbol - Symbol that triggers the mentioning state.
     52 |  */
     53 | var defaults = {
     54 |     ajax: false,
     55 |     choiceMax: 6,
     56 |     choices: [],
     57 |     choiceTemplate: "<li data-display=\"{{value}}\" data-mention=\"{{data}}\">{{value}}</li>",
     58 |     containerClassName: "ql-mentions",
     59 |     format: identity,
     60 |     hotkeys: true,
     61 |     includeTrigger: false,
     62 |     marginTop: 10,
     63 |     matcher: /@\w+$/i,
     64 |     mentionClass: "mention-item",
     65 |     noMatchMessage: "Ruh Roh Raggy!",
     66 |     noMatchTemplate: "<div class='ql-mention-no-match'><i>{{message}}</i></div>",
     67 |     template: '<ul>{{choices}}</ul>',
     68 |     triggerSymbol: "@",
     69 | };
     70 | 
     71 | /**
     72 |  * @namespace
     73 |  * @prop {function} format - Mapped onto the array of possible matches returned by call to `path`. Should yield the expected interface for data, which is an object with `name` and `data` properties.
     74 |  * @prop {string} path - The path to endpoint we should query for possible matches.
     75 |  * @prop {string} queryParameter - The name of the query paramater in the url sent to `path`.
     76 |  */
     77 | var ajaxDefaults = {
     78 |     format: identity,
     79 |     path: null,
     80 |     queryParameter: "q",
     81 | };
     82 | 
     83 | /**
     84 |  * Returns a configuration object for QuillMentions constructor.
     85 |  * @name defaultFactory
     86 |  */
     87 | function defaultFactory(options) {
     88 |     var result = extend({}, defaults, options);
     89 |     if (options.ajax) {
     90 |         result.ajax = extend({}, ajaxDefaults, options.ajax);
     91 |     }
     92 |     return result;
     93 | }
     94 | module.exports = defaultFactory;
    95 |
    96 |
    97 | 98 | 99 | 100 | 101 |
    102 | 103 | 106 | 107 |
    108 | 109 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /docs/keyboard.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: keyboard.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Source: keyboard.js

    21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 |
    
     29 | 
     30 | var DOM = require("./utilities/dom"),
     31 |     addClass = DOM.addClass,
     32 |     removeClass = DOM.removeClass;
     33 | 
     34 | var SELECTED_CLASS = "ql-mention-choice-selected";
     35 | 
     36 | /**
     37 |  * Dispatches keyboard events to handlers
     38 |  * @namespace
     39 |  * @prop {function} 
     40 |  */
     41 |  /**
     42 |   * @namespace
     43 |   * @prop {Number} 13 - Handler for the enter key.
     44 |   * @prop {Number} 27 - Handler for the escape key.
     45 |   * @prop {Number} 38 - Handler for the up arrow key.
     46 |   * @prop {Number} 40 - Handler for the down arrow key.
     47 |   */
     48 | var keydown = {
     49 |     27: keydownEscape,
     50 |     38: keydownUpKey,
     51 |     40: keydownDownKey,
     52 | };
     53 | 
     54 | var keyup = {
     55 |     13: keyupEnter,
     56 | };
     57 | 
     58 | /**
     59 |  * @method
     60 |  * @this {QuillMentions}
     61 |  */
     62 | function keydownDownKey() {
     63 |     if (this.view.isHidden()) return;
     64 |     _moveSelection.call(this, 1);
     65 | }
     66 | 
     67 | /**
     68 |  * @method
     69 |  * @this {QuillMentions}
     70 |  */
     71 | function keydownUpKey() {
     72 |     if (this.view.isHidden()) return;
     73 |     _moveSelection.call(this, -1);
     74 | }
     75 | 
     76 | /**
     77 |  * @method
     78 |  * @this {QuillMentions}
     79 |  */
     80 | function keyupEnter() {
     81 |     var nodes,
     82 |         currIndex = this.selectedChoiceIndex,
     83 |         currNode;
     84 | 
     85 |     if (currIndex === -1) return;
     86 |     if (!this.view.hasMatches()) return;
     87 | 
     88 |     this.quill.setSelection(this._cachedRange);
     89 |     nodes = this.view.getMatches();
     90 |     currNode = nodes[currIndex];
     91 |     this.addMention(currNode);
     92 |     this.selectedChoiceIndex = -1;
     93 | }
     94 | 
     95 | /**
     96 |  * @method
     97 |  * @this {QuillMentions}
     98 |  */
     99 | function keydownEscape() {
    100 |     this.view.hide();
    101 |     this.selectedChoiceIndex = -1;
    102 |     this.quill.focus();
    103 | }
    104 | 
    105 | /**
    106 |  * Moves the selected list item up or down. (+steps means down, -steps means up) PUT THIS IN THE VIEW
    107 |  * @method
    108 |  * @private
    109 |  * @this {QuillMentions}
    110 |  */
    111 | function _moveSelection(steps) {
    112 |     var nodes,
    113 |         currIndex = this.selectedChoiceIndex,
    114 |         currNode,
    115 |         nextIndex,
    116 |         nextNode;
    117 | 
    118 |     nodes = this.view.container.querySelectorAll("li");
    119 | 
    120 |     if (nodes.length === 0) {
    121 |         this.selectedChoiceIndex = -1;
    122 |         return;
    123 |     }
    124 |     if (currIndex !== -1) {
    125 |         currNode = nodes[currIndex];
    126 |         removeClass(currNode, SELECTED_CLASS);
    127 |     }
    128 | 
    129 |     nextIndex = _normalizeIndex(currIndex + steps, nodes.length);
    130 |     nextNode = nodes[nextIndex];
    131 | 
    132 |     if (nextNode) {
    133 |         addClass(nextNode, SELECTED_CLASS);
    134 |         this.selectedChoiceIndex = nextIndex;
    135 |     }
    136 |     else {
    137 |         console.log("Indexing error on node returned by querySelectorAll");
    138 |     }
    139 | 
    140 | }
    141 | 
    142 | function _normalizeIndex(i, modulo) {
    143 |     if (modulo <= 0) throw new Error("TF are you doing? _normalizeIndex needs a nonnegative, nonzero modulo.");
    144 |     while (i < 0) {
    145 |         i += modulo;
    146 |     }
    147 |     return i % modulo;
    148 | }
    149 | 
    150 | module.exports = {keyup: keyup, keydown: keydown};
    151 |
    152 |
    153 | 154 | 155 | 156 | 157 |
    158 | 159 | 162 | 163 |
    164 | 165 | 168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /docs/defaults-ajaxDefaults.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Namespace: ajaxDefaults 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Namespace: ajaxDefaults

    21 | 22 | 23 | 24 | 25 | 26 |
    27 | 28 |
    29 |

    30 | defaults/defaults~ 31 | 32 | ajaxDefaults 33 |

    34 | 35 |
    36 | 37 |
    38 |
    39 | 40 | 41 | 42 | 43 | 44 | 45 |
    46 | 47 | 48 |
    Properties:
    49 | 50 |
    51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |
    NameTypeDescription
    format 78 | 79 | 80 | function 81 | 82 | 83 | 84 | Mapped onto the array of possible matches returned by call to `path`. Should yield the expected interface for data, which is an object with `name` and `data` properties.
    path 101 | 102 | 103 | string 104 | 105 | 106 | 107 | The path to endpoint we should query for possible matches.
    queryParameter 124 | 125 | 126 | string 127 | 128 | 129 | 130 | The name of the query paramater in the url sent to `path`.
    142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 |
    Source:
    163 |
    166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 |
    174 | 175 | 176 | 177 | 178 |
    179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 |
    198 | 199 |
    200 | 201 | 202 | 203 | 204 |
    205 | 206 | 209 | 210 |
    211 | 212 | 215 | 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /docs/KEYS.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Namespace: KEYS 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Namespace: KEYS

    21 | 22 | 23 | 24 | 25 | 26 |
    27 | 28 |
    29 |

    30 | KEYS 31 |

    32 | 33 |
    34 | 35 |
    36 |
    37 | 38 | 39 | 40 | 41 | 42 | 43 |
    44 | 45 | 46 |
    Properties:
    47 | 48 |
    49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 |
    NameTypeDescription
    13 76 | 77 | 78 | Number 79 | 80 | 81 | 82 | Handler for the enter key.
    27 99 | 100 | 101 | Number 102 | 103 | 104 | 105 | Handler for the escape key.
    38 122 | 123 | 124 | Number 125 | 126 | 127 | 128 | Handler for the up arrow key.
    40 145 | 146 | 147 | Number 148 | 149 | 150 | 151 | Handler for the down arrow key.
    163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 |
    Source:
    184 |
    187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 |
    195 | 196 | 197 | 198 | 199 |
    200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 |
    219 | 220 |
    221 | 222 | 223 | 224 | 225 |
    226 | 227 | 230 | 231 |
    232 | 233 | 236 | 237 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /docs/keydown.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Namespace: keydown 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Namespace: keydown

    21 | 22 | 23 | 24 | 25 | 26 |
    27 | 28 |
    29 |

    30 | keydown 31 |

    32 | 33 |
    34 | 35 |
    36 |
    37 | 38 | 39 | 40 | 41 | 42 | 43 |
    44 | 45 | 46 |
    Properties:
    47 | 48 |
    49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 |
    NameTypeDescription
    13 76 | 77 | 78 | Number 79 | 80 | 81 | 82 | Handler for the enter key.
    27 99 | 100 | 101 | Number 102 | 103 | 104 | 105 | Handler for the escape key.
    38 122 | 123 | 124 | Number 125 | 126 | 127 | 128 | Handler for the up arrow key.
    40 145 | 146 | 147 | Number 148 | 149 | 150 | 151 | Handler for the down arrow key.
    163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 |
    Source:
    184 |
    187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 |
    195 | 196 | 197 | 198 | 199 |
    200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 |
    219 | 220 |
    221 | 222 | 223 | 224 | 225 |
    226 | 227 | 230 | 231 |
    232 | 233 | 236 | 237 | 238 | 239 | 240 | -------------------------------------------------------------------------------- /docs/controller.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: controller.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Source: controller.js

    21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 |
    // TODO - factor out data munging into separate object
     29 | 
     30 | /** @module controller */
     31 | 
     32 | var loadJSON = require("./utilities/ajax").loadJSON,
     33 |     escapeRegExp = require("./utilities/regexp").escapeRegExp;
     34 | 
     35 | module.exports = {
     36 |     Controller: Controller,
     37 |     AJAXController: AJAXController,
     38 | };
     39 | 
     40 | /**
     41 |  * @callback searchCallback
     42 |  * @param {Object[]} data - An array of objects that represent possible matches to data. The data are mapped over a formatter to provide a consistent interface.
     43 |  */
     44 | 
     45 | 
     46 |  function Controller(formatter, view, options) {
     47 |      this.format   = formatter;
     48 |      this.view     = view;
     49 |      this.database = this.munge(options.data);
     50 |      this.max      = options.max;
     51 |  }
     52 | 
     53 | 
     54 |  /**
     55 |   * @interface
     56 |   * @param {function} formatter - Munges data
     57 |   * @param {View} view
     58 |   * @param {object} options
     59 |   * @prop {function} format - Munges data 
     60 |   * @prop {View} view
     61 |   * @prop {number} max - Maximum number of matches to pass to the View.  
     62 |   */
     63 | function AbstractController(formatter, view, options) {
     64 |     this.format   = formatter;
     65 |     this.view     = view;
     66 |     this.max      = options.max;
     67 | }
     68 | 
     69 |  /**
     70 |   * @abstract
     71 |   */
     72 |  AbstractController.prototype.search = function search() {
     73 |     throw new Error("NYI");
     74 |  };
     75 | 
     76 |  /**
     77 |   * Transforms data to conform to config.
     78 |   * @method
     79 |   * @param {string} qry
     80 |   * @param {searchCallback} callback
     81 |   */
     82 |  AbstractController.prototype.munge = function(data) {
     83 |      return data.map(this.format);
     84 |  };
     85 | 
     86 | 
     87 | /**
     88 |  * @constructor
     89 |  * @prop {object[]} database - All possible choices for a given mention. 
     90 |  */
     91 | function Controller(formatter, view, options) {
     92 |     AbstractController.call(this, formatter, view, options);
     93 |     this.database = this.munge(options.data);
     94 | }
     95 | Controller.prototype = Object.create(AbstractController.prototype);
     96 | 
     97 | Controller.prototype.search = function search(qry, callback) {
     98 |     var qryRE = new RegExp(escapeRegExp(qry), "i"),
     99 |         data;
    100 | 
    101 |     data = this.database.filter(function(d) {
    102 |         return qryRE.test(d.value);
    103 |     }).sort(function(d1, d2) {
    104 |         return d1.value.indexOf(qry) - d2.value.indexOf(qry);
    105 |     });
    106 | 
    107 |     this.view.render(data.slice(0, this.max));
    108 |     if (callback) callback();
    109 | };
    110 | 
    111 | 
    112 | /**
    113 |  * @constructor
    114 |  * @augments Controller
    115 |  * @prop {string} path - The path from which to request data.
    116 |  * @prop {string} queryParameter - The name of the paramter in the request to Controller~path
    117 |  * @prop {Object} _latestCall - Cached ajax call. Aborted if a new search is made.
    118 |  */
    119 | function AJAXController(formatter, view, options) {
    120 |     AbstractController.call(this, formatter, view, options);
    121 |     this.path = options.path;
    122 |     this.queryParameter = options.queryParameter;
    123 |     this._latestCall = null;
    124 | }
    125 | AJAXController.prototype = Object.create(AbstractController.prototype);
    126 | 
    127 | /**
    128 |  * @method
    129 |  * @param {String} qry
    130 |  * @param {searchCallback} callback
    131 |  */
    132 | AJAXController.prototype.search = function search(qry, callback) {
    133 | 
    134 |     if (this._latestCall) this._latestCall.abort();  // caches ajax calls so we can cancel them as the input is updated
    135 |     var qryString = this.path +
    136 |                      "?" + this.queryParameter +
    137 |                      "=" + encodeURIComponent(qry);
    138 | 
    139 |     this._latestCall = loadJSON(qryString, success.bind(this), ajaxError);
    140 | 
    141 |     function success(data) {
    142 |         this._callback(data);
    143 |         if (callback) callback();
    144 |     }
    145 | };
    146 | 
    147 | /**
    148 |  * Munges the callback data
    149 |  * @method
    150 |  * @private
    151 |  * @param {array} data
    152 |  */
    153 | AJAXController.prototype._callback = function(data) {
    154 |     data = this.munge(data).slice(0, this.max);
    155 |     this.view.render(data);
    156 | };
    157 | 
    158 | function ajaxError(error) {
    159 |     console.log("Loading json errored! Likely due to aborted request, but there's the error: ", error);
    160 | }
    161 |
    162 |
    163 | 164 | 165 | 166 | 167 |
    168 | 169 | 172 | 173 |
    174 | 175 | 178 | 179 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /src/js/view.js: -------------------------------------------------------------------------------- 1 | var DOM = require("./utilities/dom"), 2 | extend = require("./utilities/extend"), 3 | replaceAll = require("./utilities/string-replace").all, 4 | escapeRegExp = require("./utilities/regexp").escapeRegExp; 5 | 6 | module.exports = View; 7 | 8 | 9 | /** 10 | * @constructor 11 | * @param {HTMLElement} container 12 | * @param {Object} templates - a set of templates into which we render munged data 13 | * @param {Object} options 14 | */ 15 | function View(container, templates, options) { 16 | this.container = container; 17 | this.templates = extend({}, templates); 18 | this.marginTop = options.marginTop; 19 | this.errMessage = options.errMessage; 20 | } 21 | 22 | /** 23 | * Creates view from data and calls View~_renderSuccess. If there are no data, calls View~_renderError. 24 | * @method 25 | * @param {array} data 26 | */ 27 | View.prototype.render = function(data) { 28 | var templates = this.templates, 29 | items, 30 | err, 31 | toRender; 32 | if (!data || !data.length) { 33 | err = templates.error.replace("{{message}}", this.errMessage); 34 | this.container.innerHTML = err; 35 | } 36 | else { 37 | items = data.map(this._renderLI, this).join(""); 38 | this.container.innerHTML = templates.list.replace("{{choices}}", items); 39 | } 40 | return this; 41 | }; 42 | 43 | /** 44 | * Renders listItem template with a datum as the context 45 | * @method 46 | * @private 47 | * @param {object} datum - A piece of data 48 | */ 49 | View.prototype._renderLI = function(datum) { 50 | var template = this.templates.listItem; 51 | return this._renderWithContext(template, datum); 52 | }; 53 | 54 | /** 55 | * Renders a template given the context of an object 56 | * @method 57 | * @private 58 | * @param {string} template 59 | * @param {object} o - Context for a template string. 60 | */ 61 | View.prototype._renderWithContext = function(template, o) { 62 | var prop, 63 | result = template; 64 | 65 | for (prop in o) { 66 | if (o.hasOwnProperty(prop)) { 67 | result = replaceAll(result, "{{"+prop+"}}", o[prop]); 68 | } 69 | } 70 | 71 | return result; 72 | }; 73 | 74 | /** 75 | * Makes the popover disappear 76 | * @method 77 | * @param {Quill} quill 78 | * @param {Object} range 79 | */ 80 | View.prototype.hide = function hide(quill, range) { 81 | DOM.removeClass(this.container, "ql-is-mentioning"); 82 | this.container.style.marginTop = "0"; 83 | if (range) quill.setSelection(range); 84 | return this; 85 | }; 86 | 87 | /** 88 | * @method 89 | * @returns {HTMLElement[]} 90 | */ 91 | View.prototype.getMatches = function getMatches() { 92 | return this.container.querySelectorAll("li"); 93 | }; 94 | 95 | /** 96 | * @method 97 | * @returns {HTMLElement[]} 98 | */ 99 | View.prototype.hasMatches = function hasMatches() { 100 | return this.getMatches().length > 0; 101 | }; 102 | 103 | /** 104 | * Returns whether the popover is in view. I had bad feels about this method but it's coming in hand re: keyboard events right now. 105 | * @method 106 | * @returns {boolean} 107 | */ 108 | View.prototype.isHidden = function isHidden() { 109 | return !DOM.hasClass(this.container, "ql-is-mentioning"); 110 | }; 111 | 112 | /** 113 | * Adds an active class to the mentions popover and sits it beneath the cursor. 114 | * [TODO - add active class to config] 115 | * @method 116 | * @param {Quill} quill 117 | */ 118 | View.prototype.show = function show(quill) { 119 | 120 | this.container.style.marginTop = this._getTopMargin(quill); 121 | this.container.style.marginLeft = this._getLeftMargin(quill); 122 | DOM.addClass(this.container, "ql-is-mentioning"); // TODO - config active class 123 | this.container.focus(); // Does this even do anything? It would if we were using form elements instead of LIs prob 124 | 125 | return this; 126 | }; 127 | 128 | /** 129 | * Return an array of dom nodes corresponding to all lines at or before the line corresponding to the current range. 130 | * @method 131 | * @private 132 | * @param {Range} range 133 | * @return {Node[]} 134 | */ 135 | View.prototype._findOffsetLines = function(quill) { 136 | var node = this._findMentionContainerNode(quill); 137 | return DOM.getOlderSiblingsInclusive(node); 138 | }; 139 | 140 | /** 141 | * Return the DOM node that encloses the line on which current mention is being typed. 142 | * @method 143 | * @private 144 | * @param {Range} range 145 | * @return {Node|null} 146 | */ 147 | View.prototype._findMentionContainerNode = function _findMentionContainerNode(quill) { 148 | var range = quill.getSelection(), 149 | leafAndOffset, 150 | leaf, 151 | offset, 152 | node; 153 | 154 | 155 | leafAndOffset = quill.editor.doc.findLeafAt(range.start, true); 156 | leaf = leafAndOffset[0]; 157 | offset = leafAndOffset[1]; // how many chars in front of current range 158 | if (leaf) node = leaf.node; 159 | while (node) { 160 | if (node.tagName === "DIV") break; 161 | node = node.parentNode; 162 | } 163 | if (!node) return null; 164 | return node; 165 | }; 166 | 167 | /** 168 | * Return the (hopefully inline-positioned) DOM node that encloses the mention itself. 169 | * @method 170 | * @private 171 | * @param {Range} range 172 | * @return {Node|null} 173 | */ 174 | View.prototype._findMentionNode = function _findMentionNode(quill) { 175 | var range = quill.getSelection(), 176 | leafAndOffset, 177 | leaf, 178 | offset, 179 | node; 180 | 181 | 182 | leafAndOffset = quill.editor.doc.findLeafAt(range.start, true); 183 | leaf = leafAndOffset[0]; 184 | offset = leafAndOffset[1]; // how many chars in front of current range 185 | if (leaf) node = leaf.node; 186 | while (node) { 187 | if (node.tagName === "SPAN") break; 188 | node = node.parentNode; 189 | } 190 | if (!node) return null; 191 | return node; 192 | }; 193 | 194 | View.prototype._getLeftMargin = function(quill) { 195 | var mentionNode = this._findMentionNode(quill), 196 | mentionParent = this._findMentionContainerNode(quill); 197 | 198 | var mentionRect = mentionNode.getBoundingClientRect(), 199 | parentRect = mentionParent.getBoundingClientRect(), 200 | editorRect = quill.container.getBoundingClientRect(); 201 | 202 | var marginLeft = mentionRect.left - parentRect.left; 203 | 204 | var overflow = marginLeft + mentionRect.width - editorRect.width; 205 | 206 | if (overflow > 0) { 207 | marginLeft -= (overflow); 208 | } 209 | 210 | return marginLeft + "px"; 211 | }; 212 | 213 | /** 214 | * @method 215 | * @private 216 | */ 217 | View.prototype._getTopMargin = function(quill) { 218 | var qlEditor = quill.editor.root, 219 | qlLines, 220 | marginTop = this.marginTop, 221 | negMargin = -marginTop, 222 | range; 223 | 224 | qlLines = this._findOffsetLines(quill); 225 | 226 | negMargin += this._nodeHeight(qlEditor); 227 | negMargin -= qlLines.reduce(function(total, line) { 228 | return total + this._nodeHeight(line); 229 | }.bind(this), 0); 230 | 231 | return "-" + negMargin + "px"; 232 | }; 233 | 234 | /** 235 | * @method 236 | * @private 237 | */ 238 | View.prototype._nodeHeight = function(node) { 239 | return node.getBoundingClientRect().height; 240 | }; 241 | 242 | 243 | // TODO - write QuillEditor View 244 | function QuillEditorView() { 245 | throw new Error("NYI"); 246 | } -------------------------------------------------------------------------------- /docs/module-controller.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Module: controller 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Module: controller

    21 | 22 | 23 | 24 | 25 | 26 |
    27 | 28 |
    29 |

    30 | controller 31 |

    32 | 33 |
    34 | 35 |
    36 |
    37 | 38 | 39 | 40 | 41 | 42 | 43 |
    44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
    Source:
    64 |
    67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
    75 | 76 | 77 | 78 | 79 |
    80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |

    Classes

    89 | 90 |
    91 |
    AJAXController
    92 |
    93 | 94 |
    Controller
    95 |
    96 |
    97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |

    Methods

    105 | 106 |
    107 | 108 |
    109 |

    <inner> AbstractController(formatter, view, options)

    110 | 111 | 112 |
    113 |
    114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |
    Parameters:
    123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 |
    NameTypeDescription
    formatter 151 | 152 | 153 | function 154 | 155 | 156 | 157 | Munges data
    view 174 | 175 | 176 | View 177 | 178 | 179 | 180 |
    options 197 | 198 | 199 | object 200 | 201 | 202 | 203 |
    215 | 216 | 217 | 218 |
    219 | 220 | 221 |
    Properties:
    222 | 223 |
    224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 |
    NameTypeDescription
    format 251 | 252 | 253 | function 254 | 255 | 256 | 257 | Munges data
    view 274 | 275 | 276 | View 277 | 278 | 279 | 280 |
    max 297 | 298 | 299 | number 300 | 301 | 302 | 303 | Maximum number of matches to pass to the View.
    315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 |
    Source:
    336 |
    339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 |
    347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 |
    361 | 362 |
    363 | 364 | 365 | 366 | 367 | 368 |
    369 | 370 |
    371 | 372 | 373 | 374 | 375 |
    376 | 377 | 380 | 381 |
    382 | 383 | 386 | 387 | 388 | 389 | 390 | -------------------------------------------------------------------------------- /src/js/mentions.js: -------------------------------------------------------------------------------- 1 | /** @module mentions */ 2 | 3 | var AJAXController = require("./controller").AJAXController, 4 | Controller = require("./controller").Controller, 5 | View = require("./view"); 6 | 7 | var extend = require("./utilities/extend"), 8 | defaultFactory = require("./defaults/defaults"), // keep in defaults so we can write specific defaults for each object 9 | KEYUP = require("./keyboard").keyup, 10 | KEYDOWN = require("./keyboard").keydown; 11 | 12 | module.exports = QuillMentions; 13 | 14 | /** 15 | * @constructor 16 | * @param {Object} quill - An instance of `Quill`. 17 | * @param {Object} [options] - User configuration passed to the mentions module. It's mixed in with defaults. 18 | * @prop {Quill} quill 19 | * @prop {HTMLElement} container - Container for the popover (a.k.a. the View) 20 | * @prop {RegExp} matcher - Used to scan contents of editor for mentions. 21 | */ 22 | function QuillMentions(quill, options) { 23 | 24 | this.quill = quill; 25 | var modOptions = defaultFactory(options), 26 | container = this.quill.addContainer(modOptions.containerClassName); 27 | 28 | this.triggerSymbol = modOptions.triggerSymbol; 29 | this.includeTrigger = modOptions.includeTrigger; 30 | this.matcher = modOptions.matcher; 31 | this.mentionClass = modOptions.mentionClass; 32 | this.currentMention = null; 33 | 34 | this.selectedChoiceIndex = -1; 35 | 36 | this.setView(container, modOptions) 37 | .setController(modOptions) 38 | .listenTextChange(quill) 39 | .listenSelectionChange(quill) 40 | .listenClick(container) 41 | .addFormat(); 42 | 43 | if (modOptions.hotkeys) { 44 | this.listenHotKeys(quill); 45 | } 46 | 47 | this._cachedRange = null; 48 | this.charSinceMention = 0; 49 | } 50 | 51 | /** 52 | * Sets QuillMentions.view to a View object 53 | * @method 54 | * @private 55 | * @param {HTMLElement} container 56 | * @param {Object} options - Configuration for the view 57 | */ 58 | QuillMentions.prototype.setView = function(container, options) { 59 | var templates = {}, 60 | errMessage = options.noMatchMessage, 61 | marginTop = options.marginTop; 62 | templates.list = options.template; 63 | templates.listItem = options.choiceTemplate; 64 | templates.error = options.noMatchTemplate; 65 | this.view = new View(container, templates, {errMessage: errMessage, marginTop: marginTop }); 66 | return this; 67 | }; 68 | 69 | /** 70 | * Sets QuillMentions.controller to a Controller or AJAXController object (depending on options). 71 | * @method 72 | * @private 73 | * @param {Object} options - Configuration for the controller. 74 | */ 75 | QuillMentions.prototype.setController = function(options) { 76 | if (!this.view) throw new Error("Must set view before controller."); 77 | 78 | var formatter, 79 | config = { 80 | max: options.choiceMax, 81 | }; 82 | if (!options.ajax) { 83 | formatter = options.format; 84 | config.data = options.choices; 85 | this.controller = new Controller(formatter, this.view, config); 86 | } else { 87 | formatter = options.ajax.format; 88 | extend(config, options.ajax); 89 | this.controller = new AJAXController(formatter, this.view, config); 90 | } 91 | return this; 92 | }; 93 | 94 | /** 95 | * Sets a listener for text-change events on the given Quill instance 96 | * @method 97 | * @param {Quill} quill - An instance of Quill 98 | */ 99 | QuillMentions.prototype.listenTextChange = function listenTextChange(quill) { 100 | var eventName = this.quill.constructor.events.TEXT_CHANGE; 101 | quill.on(eventName, textChangeHandler.bind(this)); 102 | return this; 103 | 104 | function textChangeHandler(delta, source) { 105 | if (source === "api") return; 106 | var mention = this.findMention(), 107 | query, 108 | _this; 109 | 110 | if (mention) { 111 | _this = this; 112 | 113 | this.charSinceMention = 0; 114 | this._cachedRange = quill.getSelection(); 115 | this.currentMention = mention; 116 | this._addTemporaryMentionSpan(); 117 | 118 | query = mention[0].replace(this.triggerSymbol, ""); 119 | 120 | this.controller.search(query, function() { 121 | _this.view.show(_this.quill); 122 | }); 123 | } 124 | else { 125 | this.charSinceMention++; 126 | this.view.hide(); 127 | } 128 | } 129 | }; 130 | 131 | 132 | /** 133 | * Sets a listener for selection-change events on the given Quill instance 134 | * @method 135 | * @param {Quill} quill - An instance of Quill 136 | */ 137 | QuillMentions.prototype.listenSelectionChange = function(quill) { 138 | var eventName = quill.constructor.events.SELECTION_CHANGE; 139 | quill.on(eventName, selectionChangeHandler.bind(this)); 140 | return this; 141 | 142 | function selectionChangeHandler(range) { 143 | if (!range) this.view.hide(); 144 | } 145 | }; 146 | 147 | /** 148 | * Sets a listener for keyboard events on the given container. 149 | * Events are dispatched through the KEYS object. 150 | * @method 151 | * @param {Quill} quill - An instance of Quill 152 | */ 153 | QuillMentions.prototype.listenHotKeys = function(quill) { 154 | quill.container 155 | .addEventListener('keydown', 156 | keydownHandler.bind(this)); // TIL keypress is intended for keys that normally produce a character 157 | 158 | quill.container 159 | .addEventListener('keyup', 160 | keyupHandler.bind(this)); 161 | 162 | return this; 163 | 164 | function keydownHandler(event) { 165 | var code = event.keyCode || event.which; 166 | if (!this.view.isHidden()) { // need special logic for enter key :sob: 167 | if (KEYDOWN[code]) { 168 | KEYDOWN[code].call(this); 169 | event.stopPropagation(); 170 | event.preventDefault(); 171 | } 172 | } 173 | } 174 | function keyupHandler(event) { 175 | var code = event.keyCode || event.which; 176 | if (!this.view.isHidden() || this.charSinceMention === 1) { // this weird if condition solve an issue where hitting enter would hide the view and we wouldn't be able to insert the mention... 177 | if (KEYUP[code]) { 178 | KEYUP[code].call(this); 179 | } 180 | } 181 | } 182 | }; 183 | 184 | /** 185 | * Listens for a click or touchend event on the View. 186 | * @method 187 | * @param {HTMLElement} elt 188 | */ 189 | QuillMentions.prototype.listenClick = function(elt) { 190 | 191 | elt.addEventListener("click", addMention.bind(this)); 192 | elt.addEventListener("touchend", addMention.bind(this)); 193 | return this; 194 | 195 | /** Wraps the QuillMentions~addMention method */ 196 | function addMention(event) { 197 | var target = event.target || event.srcElement; 198 | if (target.tagName.toLowerCase() === "li") { 199 | this.addMention(target); 200 | } 201 | event.stopPropagation(); 202 | } 203 | }; 204 | 205 | /** 206 | * Looks for a string in the editor (up to the cursor's current position) for a match to QuillMentions~matcher 207 | * @method 208 | * @return {Match} 209 | */ 210 | QuillMentions.prototype.findMention = function findMention() { 211 | var range = this.quill.getSelection() || this._cachedRange; 212 | if (!range) return; 213 | var cursor = range.end, 214 | contents = this.quill.getText(0, cursor); 215 | 216 | return this.matcher.exec(contents); 217 | }; 218 | 219 | /** 220 | * Needs to be refactored! QuillMention should be responsible for this action. 221 | * @method 222 | * @param {HTMLElement} 223 | */ 224 | QuillMentions.prototype.addMention = function addMention(node) { 225 | var insertAt = this.currentMention.index, 226 | toInsert = (this.includeTrigger ? this.triggerSymbol : "") + node.dataset.display, 227 | toFocus = insertAt + toInsert.length + 1; 228 | 229 | 230 | this.quill.deleteText(insertAt, insertAt + this.currentMention[0].length); 231 | this.quill.insertText(insertAt, toInsert, "mention", this.mentionClass+"-"+node.dataset.mention); 232 | this.quill.insertText(insertAt + toInsert.length, " "); 233 | this.quill.setSelection(toFocus, toFocus); 234 | 235 | this.view.hide(); 236 | }; 237 | 238 | QuillMentions.prototype._addTemporaryMentionSpan = function(range) { 239 | var insertAt = this.currentMention.index, 240 | toInsert = this.currentMention[0]; 241 | 242 | this.quill.deleteText(insertAt, insertAt + this.currentMention[0].length); 243 | this.quill.insertText(insertAt, toInsert, "mention", "is-typing-mention"); 244 | }; 245 | 246 | 247 | /** 248 | * Refactor. 249 | * @method 250 | */ 251 | QuillMentions.prototype.hasSelection = function() { 252 | return this.selectedChoiceIndex !== -1; 253 | }; 254 | 255 | /** 256 | * Waiting on new custom formats in Quill to beef this up. 257 | * @method 258 | * @private 259 | */ 260 | QuillMentions.prototype.addFormat = function(className) { 261 | this.quill.addFormat('mention', { tag: 'SPAN', "class": "ql-", }); // a mention is a span with the class prefix "ql-". the naming is an artifact of the current custom formats implementation 262 | return this; 263 | }; 264 | 265 | -------------------------------------------------------------------------------- /docs/view.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: view.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Source: view.js

    21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 |
    var DOM = require("./utilities/dom"),
     29 |     extend = require("./utilities/extend"),
     30 |     replaceAll = require("./utilities/string-replace").all,
     31 |     escapeRegExp = require("./utilities/regexp").escapeRegExp;
     32 | 
     33 | module.exports = View;
     34 | 
     35 | 
     36 | /**
     37 |  * @constructor
     38 |  * @param {HTMLElement} container
     39 |  * @param {Object} templates - a set of templates into which we render munged data
     40 |  * @param {Object} options
     41 |  */
     42 | function View(container, templates, options) {
     43 |     this.container = container;
     44 |     this.templates = extend({}, templates);
     45 |     this.marginTop = options.marginTop;
     46 |     this.errMessage = options.errMessage;
     47 | }
     48 | 
     49 | /**
     50 |  * Creates view from data and calls View~_renderSuccess. If there are no data, calls View~_renderError.
     51 |  * @method
     52 |  * @param {array} data
     53 |  */
     54 | View.prototype.render = function(data) {
     55 |     var templates = this.templates,
     56 |         items,
     57 |         err,
     58 |         toRender;
     59 |     if (!data || !data.length) {
     60 |         err = templates.error.replace("{{message}}", this.errMessage);
     61 |         this.container.innerHTML = err;
     62 |     }
     63 |     else {
     64 |         items = data.map(this._renderLI, this).join("");
     65 |         this.container.innerHTML = templates.list.replace("{{choices}}", items);
     66 |     }
     67 |     return this;
     68 | };
     69 | 
     70 | /**
     71 |  * Renders listItem template with a datum as the context
     72 |  * @method
     73 |  * @private
     74 |  * @param {object} datum - A piece of data 
     75 |  */
     76 | View.prototype._renderLI = function(datum) {
     77 |     var template = this.templates.listItem;
     78 |     return this._renderWithContext(template, datum);
     79 | };
     80 | 
     81 | /**
     82 |  * Renders a template given the context of an object
     83 |  * @method
     84 |  * @private
     85 |  * @param {string} template
     86 |  * @param {object} o - Context for a template string.
     87 |  */
     88 | View.prototype._renderWithContext = function(template, o) {
     89 |     var prop,
     90 |         result = template;
     91 | 
     92 |     for (prop in o) {
     93 |         if (o.hasOwnProperty(prop)) {
     94 |             result = replaceAll(result, "{{"+prop+"}}", o[prop]);
     95 |         }
     96 |     }
     97 | 
     98 |     return result;
     99 | };
    100 | 
    101 | /**
    102 |  * Makes the popover disappear
    103 |  * @method
    104 |  * @param {Quill} quill
    105 |  * @param {Object} range
    106 |  */
    107 | View.prototype.hide = function hide(quill, range) {
    108 |     DOM.removeClass(this.container, "ql-is-mentioning");
    109 |     this.container.style.marginTop = "0";
    110 |     if (range) quill.setSelection(range);
    111 |     return this;
    112 | };
    113 | 
    114 | /**
    115 |  * @method
    116 |  * @returns {HTMLElement[]}
    117 |  */
    118 | View.prototype.getMatches = function getMatches() {
    119 |     return this.container.querySelectorAll("li");
    120 | };
    121 | 
    122 | /**
    123 |  * @method
    124 |  * @returns {HTMLElement[]}
    125 |  */
    126 | View.prototype.hasMatches = function hasMatches() {
    127 |     return this.getMatches().length > 0;
    128 | };
    129 | 
    130 | /**
    131 |  * Returns whether the popover is in view. I had bad feels about this method but it's coming in hand re: keyboard events right now.
    132 |  * @method
    133 |  * @returns {boolean}
    134 |  */
    135 | View.prototype.isHidden = function isHidden() {
    136 |     return !DOM.hasClass(this.container, "ql-is-mentioning");
    137 | };
    138 | 
    139 | /**
    140 |  * Adds an active class to the mentions popover and sits it beneath the cursor.
    141 |  * [TODO - add active class to config]
    142 |  * @method
    143 |  * @param {Quill} quill
    144 |  */
    145 | View.prototype.show = function show(quill) {
    146 | 
    147 |     this.container.style.marginTop = this._getTopMargin(quill);
    148 |     this.container.style.marginLeft = this._getLeftMargin(quill);
    149 |     DOM.addClass(this.container, "ql-is-mentioning"); // TODO - config active class
    150 |     this.container.focus(); // Does this even do anything? It would if we were using form elements instead of LIs prob
    151 | 
    152 |     return this;
    153 | };
    154 | 
    155 | /**
    156 |  * Return an array of dom nodes corresponding to all lines at or before the line corresponding to the current range.
    157 |  * @method
    158 |  * @private
    159 |  * @param {Range} range
    160 |  * @return {Node[]}
    161 |  */
    162 | View.prototype._findOffsetLines = function(quill) {
    163 |     var node = this._findMentionContainerNode(quill);
    164 |     return DOM.getOlderSiblingsInclusive(node);
    165 | };
    166 | 
    167 | /**
    168 |  * Return the DOM node that encloses the line on which current mention is being typed.
    169 |  * @method
    170 |  * @private
    171 |  * @param {Range} range
    172 |  * @return {Node|null}
    173 |  */
    174 | View.prototype._findMentionContainerNode = function _findMentionContainerNode(quill) {
    175 |     var range = quill.getSelection(),
    176 |         leafAndOffset,
    177 |         leaf,
    178 |         offset,
    179 |         node;
    180 |        
    181 |                 
    182 |     leafAndOffset = quill.editor.doc.findLeafAt(range.start, true);
    183 |     leaf = leafAndOffset[0];
    184 |     offset = leafAndOffset[1]; // how many chars in front of current range
    185 |     if (leaf) node = leaf.node;
    186 |     while (node) {
    187 |         if (node.tagName === "DIV") break;
    188 |         node = node.parentNode;
    189 |     }
    190 |     if (!node) return null;
    191 |     return node;
    192 | };
    193 | 
    194 | /**
    195 |  * Return the (hopefully inline-positioned) DOM node that encloses the mention itself.
    196 |  * @method
    197 |  * @private
    198 |  * @param {Range} range
    199 |  * @return {Node|null}
    200 |  */
    201 | View.prototype._findMentionNode = function _findMentionNode(quill) {
    202 |     var range = quill.getSelection(),
    203 |         leafAndOffset,
    204 |         leaf,
    205 |         offset,
    206 |         node;
    207 |        
    208 |                 
    209 |     leafAndOffset = quill.editor.doc.findLeafAt(range.start, true);
    210 |     leaf = leafAndOffset[0];
    211 |     offset = leafAndOffset[1]; // how many chars in front of current range
    212 |     if (leaf) node = leaf.node;
    213 |     while (node) {
    214 |         if (node.tagName === "SPAN") break;
    215 |         node = node.parentNode;
    216 |     }
    217 |     if (!node) return null;
    218 |     return node;
    219 | };
    220 | 
    221 | View.prototype._getLeftMargin = function(quill) {
    222 |     var mentionNode = this._findMentionNode(quill),
    223 |         mentionParent = this._findMentionContainerNode(quill);
    224 | 
    225 |     var mentionRect = mentionNode.getBoundingClientRect(),
    226 |         parentRect = mentionParent.getBoundingClientRect(),
    227 |         editorRect = quill.container.getBoundingClientRect();
    228 | 
    229 |     var marginLeft = mentionRect.left - parentRect.left;
    230 | 
    231 |     var overflow = marginLeft + mentionRect.width - editorRect.width;
    232 | 
    233 |     if (overflow > 0) {
    234 |         marginLeft -= (overflow);
    235 |     }
    236 | 
    237 |     return marginLeft + "px";
    238 | };
    239 | 
    240 | /**
    241 |  * @method
    242 |  * @private
    243 |  */
    244 | View.prototype._getTopMargin = function(quill) {
    245 |     var qlEditor = quill.editor.root,
    246 |         qlLines,
    247 |         marginTop = this.marginTop,
    248 |         negMargin = -marginTop,
    249 |         range;
    250 | 
    251 |     qlLines = this._findOffsetLines(quill);
    252 | 
    253 |     negMargin += this._nodeHeight(qlEditor);
    254 |     negMargin -= qlLines.reduce(function(total, line) {
    255 |         return total + this._nodeHeight(line);
    256 |     }.bind(this), 0);
    257 | 
    258 |     return "-" + negMargin + "px";
    259 | };
    260 | 
    261 | /**
    262 |  * @method
    263 |  * @private
    264 |  */
    265 | View.prototype._nodeHeight = function(node) {
    266 |     return node.getBoundingClientRect().height;
    267 | };
    268 | 
    269 | 
    270 | // TODO - write QuillEditor View
    271 | function QuillEditorView() {
    272 |     throw new Error("NYI");
    273 | }
    274 |
    275 |
    276 | 277 | 278 | 279 | 280 |
    281 | 282 | 285 | 286 |
    287 | 288 | 291 | 292 | 293 | 294 | 295 | 296 | -------------------------------------------------------------------------------- /dist/quill-mentions.min.js: -------------------------------------------------------------------------------- 1 | !function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g{{value}}',containerClassName:"ql-mentions",format:f,hotkeys:!0,includeTrigger:!1,marginTop:10,matcher:/@\w+$/i,mentionClass:"mention-item",noMatchMessage:"Ruh Roh Raggy!",noMatchTemplate:"
    {{message}}
    ",template:"",triggerSymbol:"@"},h={format:f,path:null,queryParameter:"q"};b.exports=d},{"../utilities/extend":8,"../utilities/identity":9}],3:[function(a,b,c){(function(b){b.QuillMentions=a("./mentions")}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./mentions":5}],4:[function(a,b,c){function d(){this.view.isHidden()||h.call(this,1)}function e(){this.view.isHidden()||h.call(this,-1)}function f(){var a,b,c=this.selectedChoiceIndex;-1!==c&&this.view.hasMatches()&&(this.quill.setSelection(this._cachedRange),a=this.view.getMatches(),b=a[c],this.addMention(b),this.selectedChoiceIndex=-1)}function g(){this.view.hide(),this.selectedChoiceIndex=-1,this.quill.focus()}function h(a){var b,c,d,e,f=this.selectedChoiceIndex;return b=this.view.container.querySelectorAll("li"),0===b.length?void(this.selectedChoiceIndex=-1):(-1!==f&&(c=b[f],l(c,m)),d=i(f+a,b.length),e=b[d],void(e?(k(e,m),this.selectedChoiceIndex=d):console.log("Indexing error on node returned by querySelectorAll")))}function i(a,b){if(0>=b)throw new Error("TF are you doing? _normalizeIndex needs a nonnegative, nonzero modulo.");for(;0>a;)a+=b;return a%b}var j=a("./utilities/dom"),k=j.addClass,l=j.removeClass,m="ql-mention-choice-selected",n={27:g,38:e,40:d},o={13:f};b.exports={keyup:o,keydown:n}},{"./utilities/dom":7}],5:[function(a,b,c){function d(a,b){this.quill=a;var c=i(b),d=this.quill.addContainer(c.containerClassName);this.triggerSymbol=c.triggerSymbol,this.includeTrigger=c.includeTrigger,this.matcher=c.matcher,this.mentionClass=c.mentionClass,this.currentMention=null,this.selectedChoiceIndex=-1,this.setView(d,c).setController(c).listenTextChange(a).listenSelectionChange(a).listenClick(d).addFormat(),c.hotkeys&&this.listenHotKeys(a),this._cachedRange=null,this.charSinceMention=0}var e=a("./controller").AJAXController,f=a("./controller").Controller,g=a("./view"),h=a("./utilities/extend"),i=a("./defaults/defaults"),j=a("./keyboard").keyup,k=a("./keyboard").keydown;b.exports=d,d.prototype.setView=function(a,b){var c={},d=b.noMatchMessage,e=b.marginTop;return c.list=b.template,c.listItem=b.choiceTemplate,c.error=b.noMatchTemplate,this.view=new g(a,c,{errMessage:d,marginTop:e}),this},d.prototype.setController=function(a){if(!this.view)throw new Error("Must set view before controller.");var b,c={max:a.choiceMax};return a.ajax?(b=a.ajax.format,h(c,a.ajax),this.controller=new e(b,this.view,c)):(b=a.format,c.data=a.choices,this.controller=new f(b,this.view,c)),this},d.prototype.listenTextChange=function(a){function b(b,c){if("api"!==c){var d,e,f=this.findMention();f?(e=this,this.charSinceMention=0,this._cachedRange=a.getSelection(),this.currentMention=f,this._addTemporaryMentionSpan(),d=f[0].replace(this.triggerSymbol,""),this.controller.search(d,function(){e.view.show(e.quill)})):(this.charSinceMention++,this.view.hide())}}var c=this.quill.constructor.events.TEXT_CHANGE;return a.on(c,b.bind(this)),this},d.prototype.listenSelectionChange=function(a){function b(a){a||this.view.hide()}var c=a.constructor.events.SELECTION_CHANGE;return a.on(c,b.bind(this)),this},d.prototype.listenHotKeys=function(a){function b(a){var b=a.keyCode||a.which;this.view.isHidden()||k[b]&&(k[b].call(this),a.stopPropagation(),a.preventDefault())}function c(a){var b=a.keyCode||a.which;this.view.isHidden()&&1!==this.charSinceMention||j[b]&&j[b].call(this)}return a.container.addEventListener("keydown",b.bind(this)),a.container.addEventListener("keyup",c.bind(this)),this},d.prototype.listenClick=function(a){function b(a){var b=a.target||a.srcElement;"li"===b.tagName.toLowerCase()&&this.addMention(b),a.stopPropagation()}return a.addEventListener("click",b.bind(this)),a.addEventListener("touchend",b.bind(this)),this},d.prototype.findMention=function(){var a=this.quill.getSelection()||this._cachedRange;if(a){var b=a.end,c=this.quill.getText(0,b);return this.matcher.exec(c)}},d.prototype.addMention=function(a){var b=this.currentMention.index,c=(this.includeTrigger?this.triggerSymbol:"")+a.dataset.display,d=b+c.length+1;this.quill.deleteText(b,b+this.currentMention[0].length),this.quill.insertText(b,c,"mention",this.mentionClass+"-"+a.dataset.mention),this.quill.insertText(b+c.length," "),this.quill.setSelection(d,d),this.view.hide()},d.prototype._addTemporaryMentionSpan=function(a){var b=this.currentMention.index,c=this.currentMention[0];this.quill.deleteText(b,b+this.currentMention[0].length),this.quill.insertText(b,c,"mention","is-typing-mention")},d.prototype.hasSelection=function(){return-1!==this.selectedChoiceIndex},d.prototype.addFormat=function(a){return this.quill.addFormat("mention",{tag:"SPAN","class":"ql-"}),this}},{"./controller":1,"./defaults/defaults":2,"./keyboard":4,"./utilities/extend":8,"./view":12}],6:[function(a,b,c){b.exports={loadJSON:function(a,b,c){var d=new XMLHttpRequest;return d.onreadystatechange=function(){d.readyState===XMLHttpRequest.DONE&&(200===d.status?b&&b(JSON.parse(d.responseText)):c&&c(d))},d.open("GET",a,!0),d.send(),d}}},{}],7:[function(a,b,c){function d(a,b){f(a,b)||(a.className+=" "+b)}function e(a){var b=[a];if(!a)return[];for(;a.previousSibling;)b.push(a.previousSibling),a=a.previousSibling;return b}function f(a,b){return a?-1!==a.className.indexOf(b):console.log("Called hasClass on an empty node")}function g(a,b){if(f(a,b))for(;f(a,b);)a.className=a.className.replace(b,"")}b.exports.addClass=d,b.exports.getOlderSiblingsInclusive=e,b.exports.hasClass=f,b.exports.removeClass=g},{}],8:[function(a,b,c){function d(a){for(var b=[].slice.call(arguments,0),c=b[0],d=1;d0},d.prototype.isHidden=function(){return!e.hasClass(this.container,"ql-is-mentioning")},d.prototype.show=function(a){return this.container.style.marginTop=this._getTopMargin(a),this.container.style.marginLeft=this._getLeftMargin(a),e.addClass(this.container,"ql-is-mentioning"),this.container.focus(),this},d.prototype._findOffsetLines=function(a){var b=this._findMentionContainerNode(a);return e.getOlderSiblingsInclusive(b)},d.prototype._findMentionContainerNode=function(a){var b,c,d,e,f=a.getSelection();for(b=a.editor.doc.findLeafAt(f.start,!0),c=b[0],d=b[1],c&&(e=c.node);e&&"DIV"!==e.tagName;)e=e.parentNode;return e?e:null},d.prototype._findMentionNode=function(a){var b,c,d,e,f=a.getSelection();for(b=a.editor.doc.findLeafAt(f.start,!0),c=b[0],d=b[1],c&&(e=c.node);e&&"SPAN"!==e.tagName;)e=e.parentNode;return e?e:null},d.prototype._getLeftMargin=function(a){var b=this._findMentionNode(a),c=this._findMentionContainerNode(a),d=b.getBoundingClientRect(),e=c.getBoundingClientRect(),f=a.container.getBoundingClientRect(),g=d.left-e.left,h=g+d.width-f.width;return h>0&&(g-=h),g+"px"},d.prototype._getTopMargin=function(a){var b,c=a.editor.root,d=this.marginTop,e=-d;return b=this._findOffsetLines(a),e+=this._nodeHeight(c),e-=b.reduce(function(a,b){return a+this._nodeHeight(b)}.bind(this),0),"-"+e+"px"},d.prototype._nodeHeight=function(a){return a.getBoundingClientRect().height}},{"./utilities/dom":7,"./utilities/extend":8,"./utilities/regexp":10,"./utilities/string-replace":11}]},{},[1,2,3,4,5,6,7,8,9,10,11,12]); -------------------------------------------------------------------------------- /docs/module-controller-AJAXController.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Class: AJAXController 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Class: AJAXController

    21 | 22 | 23 | 24 | 25 | 26 |
    27 | 28 |
    29 |

    30 | controller~ 31 | 32 | AJAXController 33 |

    34 | 35 |
    36 | 37 |
    38 |
    39 | 40 | 41 | 42 | 43 |
    44 |

    new AJAXController()

    45 | 46 | 47 |
    48 |
    49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
    60 | 61 | 62 |
    Properties:
    63 | 64 |
    65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 |
    NameTypeDescription
    path 92 | 93 | 94 | string 95 | 96 | 97 | 98 | The path from which to request data.
    queryParameter 115 | 116 | 117 | string 118 | 119 | 120 | 121 | The name of the paramter in the request to Controller~path
    _latestCall 138 | 139 | 140 | Object 141 | 142 | 143 | 144 | Cached ajax call. Aborted if a new search is made.
    156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 |
    Source:
    177 |
    180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 |
    188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 |
    202 | 203 | 204 |
    205 | 206 | 207 |

    Extends

    208 | 209 |
      210 |
    • Controller
    • 211 |
    212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 |

    Methods

    226 | 227 |
    228 | 229 |
    230 |

    <private> _callback(data)

    231 | 232 | 233 |
    234 |
    235 | 236 | 237 |
    238 | Munges the callback data 239 |
    240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 |
    Parameters:
    248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 |
    NameTypeDescription
    data 276 | 277 | 278 | array 279 | 280 | 281 | 282 |
    294 | 295 | 296 | 297 |
    298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 |
    Source:
    318 |
    321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 |
    329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 |
    343 | 344 | 345 | 346 |
    347 | 348 | 349 | 350 |
    351 |
    352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 |
    Parameters:
    361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 |
    NameTypeDescription
    qry 389 | 390 | 391 | String 392 | 393 | 394 | 395 |
    callback 412 | 413 | 414 | searchCallback 415 | 416 | 417 | 418 |
    430 | 431 | 432 | 433 |
    434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 |
    Source:
    454 |
    457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 |
    465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 |
    479 | 480 |
    481 | 482 | 483 | 484 | 485 | 486 |
    487 | 488 |
    489 | 490 | 491 | 492 | 493 |
    494 | 495 | 498 | 499 |
    500 | 501 |
    502 | Documentation generated by JSDoc 3.2.2 on Wed Jun 10 2015 15:59:13 GMT-0700 (PDT) 503 |
    504 | 505 | 506 | 507 | 508 | -------------------------------------------------------------------------------- /docs/scripts/prettify/Apache-License-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /docs/mentions.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: mentions.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 |

    Source: mentions.js

    21 | 22 | 23 | 24 | 25 | 26 |
    27 |
    28 |
    /** @module mentions */
     29 | 
     30 | var AJAXController = require("./controller").AJAXController,
     31 |     Controller = require("./controller").Controller,
     32 |     View = require("./view");
     33 | 
     34 | var extend = require("./utilities/extend"),
     35 |     defaultFactory = require("./defaults/defaults"), // keep in defaults so we can write specific defaults for each object
     36 |     KEYUP = require("./keyboard").keyup,
     37 |     KEYDOWN = require("./keyboard").keydown;
     38 | 
     39 | module.exports = QuillMentions;
     40 | 
     41 | /**
     42 |  * @constructor
     43 |  * @param {Object} quill - An instance of `Quill`.
     44 |  * @param {Object} [options] - User configuration passed to the mentions module. It's mixed in with defaults.
     45 |  * @prop {Quill} quill
     46 |  * @prop {HTMLElement} container - Container for the popover (a.k.a. the View)
     47 |  * @prop {RegExp} matcher - Used to scan contents of editor for mentions.
     48 |  */
     49 | function QuillMentions(quill, options) {
     50 | 
     51 |     this.quill = quill;
     52 |     var modOptions = defaultFactory(options),
     53 |         container = this.quill.addContainer(modOptions.containerClassName);
     54 | 
     55 |     this.triggerSymbol = modOptions.triggerSymbol;
     56 |     this.includeTrigger = modOptions.includeTrigger;
     57 |     this.matcher = modOptions.matcher;
     58 |     this.mentionClass = modOptions.mentionClass;
     59 |     this.currentMention = null;
     60 | 
     61 |     this.selectedChoiceIndex = -1;
     62 | 
     63 |     this.setView(container, modOptions)
     64 |         .setController(modOptions)
     65 |         .listenTextChange(quill)
     66 |         .listenSelectionChange(quill)
     67 |         .listenClick(container)
     68 |         .addFormat();
     69 | 
     70 |     if (modOptions.hotkeys) {
     71 |         this.listenHotKeys(quill);
     72 |     }
     73 | 
     74 |     this._cachedRange = null;
     75 |     this.charSinceMention = 0;
     76 | }
     77 | 
     78 | /**
     79 |  * Sets QuillMentions.view to a View object
     80 |  * @method
     81 |  * @private
     82 |  * @param {HTMLElement} container
     83 |  * @param {Object} options - Configuration for the view
     84 |  */
     85 | QuillMentions.prototype.setView = function(container, options) {
     86 |     var templates = {},
     87 |         errMessage = options.noMatchMessage,
     88 |         marginTop = options.marginTop;
     89 |     templates.list = options.template;
     90 |     templates.listItem = options.choiceTemplate;
     91 |     templates.error = options.noMatchTemplate;
     92 |     this.view = new View(container, templates, {errMessage: errMessage, marginTop: marginTop });
     93 |     return this;
     94 | };
     95 | 
     96 | /**
     97 |  * Sets QuillMentions.controller to a Controller or AJAXController object (depending on options).
     98 |  * @method
     99 |  * @private
    100 |  * @param {Object} options - Configuration for the controller.
    101 |  */
    102 | QuillMentions.prototype.setController = function(options) {
    103 |     if (!this.view) throw new Error("Must set view before controller.");
    104 | 
    105 |     var formatter,
    106 |         config = {
    107 |             max: options.choiceMax,
    108 |         };
    109 |     if (!options.ajax) {
    110 |         formatter = options.format;
    111 |         config.data = options.choices;
    112 |         this.controller = new Controller(formatter, this.view, config);
    113 |     } else {
    114 |         formatter = options.ajax.format;
    115 |         extend(config, options.ajax);
    116 |         this.controller = new AJAXController(formatter, this.view, config);
    117 |     }
    118 |     return this;
    119 | };
    120 | 
    121 | /**
    122 |  * Sets a listener for text-change events on the given Quill instance
    123 |  * @method
    124 |  * @param {Quill} quill - An instance of Quill
    125 |  */
    126 | QuillMentions.prototype.listenTextChange = function listenTextChange(quill) {
    127 |     var eventName = this.quill.constructor.events.TEXT_CHANGE;
    128 |     quill.on(eventName, textChangeHandler.bind(this));
    129 |     return this;
    130 | 
    131 |     function textChangeHandler(delta, source) {
    132 |         if (source === "api") return;
    133 |         var mention = this.findMention(),
    134 |             query,
    135 |             _this;
    136 | 
    137 |         if (mention) {
    138 |             _this = this;
    139 | 
    140 |             this.charSinceMention = 0;
    141 |             this._cachedRange = quill.getSelection();
    142 |             this.currentMention = mention;
    143 |             this._addTemporaryMentionSpan();
    144 | 
    145 |             query = mention[0].replace(this.triggerSymbol, "");
    146 | 
    147 |             this.controller.search(query, function() {
    148 |                 _this.view.show(_this.quill);
    149 |             });
    150 |         }
    151 |         else {
    152 |             this.charSinceMention++;
    153 |             this.view.hide();
    154 |         }
    155 |     }
    156 | };
    157 | 
    158 | 
    159 | /**
    160 |  * Sets a listener for selection-change events on the given Quill instance
    161 |  * @method
    162 |  * @param {Quill} quill - An instance of Quill
    163 |  */
    164 | QuillMentions.prototype.listenSelectionChange = function(quill) {
    165 |     var eventName = quill.constructor.events.SELECTION_CHANGE;
    166 |     quill.on(eventName, selectionChangeHandler.bind(this));
    167 |     return this;
    168 | 
    169 |     function selectionChangeHandler(range) {
    170 |         if (!range) this.view.hide();
    171 |     }
    172 | };
    173 | 
    174 | /**
    175 |  * Sets a listener for keyboard events on the given container.
    176 |  * Events are dispatched through the KEYS object.
    177 |  * @method
    178 |  * @param {Quill} quill - An instance of Quill
    179 |  */
    180 | QuillMentions.prototype.listenHotKeys = function(quill) {
    181 |     quill.container
    182 |         .addEventListener('keydown',
    183 |                            keydownHandler.bind(this)); // TIL keypress is intended for keys that normally produce a character
    184 | 
    185 |     quill.container
    186 |         .addEventListener('keyup',
    187 |                            keyupHandler.bind(this));
    188 | 
    189 |     return this;
    190 | 
    191 |     function keydownHandler(event) {
    192 |         var code = event.keyCode || event.which;
    193 |         if (!this.view.isHidden()) { // need special logic for enter key :sob:
    194 |             if (KEYDOWN[code]) {
    195 |                 KEYDOWN[code].call(this);
    196 |                 event.stopPropagation();
    197 |                 event.preventDefault();
    198 |             }
    199 |         }
    200 |     }
    201 |     function keyupHandler(event) {
    202 |         var code = event.keyCode || event.which;
    203 |         if (!this.view.isHidden() || this.charSinceMention === 1) { // this weird if condition solve an issue where hitting enter would hide the view and we wouldn't be able to insert the mention...
    204 |             if (KEYUP[code]) {
    205 |                 KEYUP[code].call(this);
    206 |             }
    207 |         }
    208 |     }
    209 | };
    210 | 
    211 | /**
    212 |  * Listens for a click or touchend event on the View.
    213 |  * @method
    214 |  * @param {HTMLElement} elt
    215 |  */
    216 | QuillMentions.prototype.listenClick = function(elt) {
    217 | 
    218 |     elt.addEventListener("click", addMention.bind(this));
    219 |     elt.addEventListener("touchend", addMention.bind(this));
    220 |     return this;
    221 | 
    222 |     /** Wraps the QuillMentions~addMention method */
    223 |     function addMention(event) {
    224 |         var target = event.target || event.srcElement;
    225 |         if (target.tagName.toLowerCase() === "li") {
    226 |             this.addMention(target);
    227 |         }
    228 |         event.stopPropagation();
    229 |     }
    230 | };
    231 | 
    232 | /**
    233 |  * Looks for a string in the editor (up to the cursor's current position) for a match to QuillMentions~matcher
    234 |  * @method
    235 |  * @return {Match}
    236 |  */
    237 | QuillMentions.prototype.findMention = function findMention() {
    238 |     var range = this.quill.getSelection() || this._cachedRange;
    239 |     if (!range) return;
    240 |     var cursor = range.end,
    241 |         contents = this.quill.getText(0, cursor);
    242 | 
    243 |     return this.matcher.exec(contents);
    244 | };
    245 | 
    246 | /**
    247 |  * Needs to be refactored! QuillMention should be responsible for this action.
    248 |  * @method
    249 |  * @param {HTMLElement}
    250 |  */
    251 | QuillMentions.prototype.addMention = function addMention(node) {
    252 |     var insertAt = this.currentMention.index,
    253 |         toInsert = (this.includeTrigger ? this.triggerSymbol : "") + node.dataset.display,
    254 |         toFocus = insertAt + toInsert.length + 1;
    255 | 
    256 | 
    257 |     this.quill.deleteText(insertAt, insertAt + this.currentMention[0].length);
    258 |     this.quill.insertText(insertAt, toInsert, "mention", this.mentionClass+"-"+node.dataset.mention);
    259 |     this.quill.insertText(insertAt + toInsert.length, " ");
    260 |     this.quill.setSelection(toFocus, toFocus);
    261 | 
    262 |     this.view.hide();
    263 | };
    264 | 
    265 | QuillMentions.prototype._addTemporaryMentionSpan = function(range) {
    266 |     var insertAt = this.currentMention.index,
    267 |         toInsert = this.currentMention[0];
    268 | 
    269 |     this.quill.deleteText(insertAt, insertAt + this.currentMention[0].length);
    270 |     this.quill.insertText(insertAt, toInsert, "mention", "is-typing-mention");
    271 | };
    272 | 
    273 | 
    274 |  /**
    275 |   * Refactor.
    276 |   * @method
    277 |   */
    278 |  QuillMentions.prototype.hasSelection = function() {
    279 |      return this.selectedChoiceIndex !== -1;
    280 |  };
    281 | 
    282 | /**
    283 |  * Waiting on new custom formats in Quill to beef this up.
    284 |  * @method
    285 |  * @private
    286 |  */
    287 |  QuillMentions.prototype.addFormat = function(className) {
    288 |      this.quill.addFormat('mention', { tag: 'SPAN', "class": "ql-", }); // a mention is a span with the class prefix "ql-". the naming is an artifact of the current custom formats implementation
    289 |      return this;
    290 |  };
    291 | 
    292 | 
    293 |
    294 |
    295 | 296 | 297 | 298 | 299 |
    300 | 301 | 304 | 305 |
    306 | 307 |
    308 | Documentation generated by JSDoc 3.2.2 on Wed Jun 10 2015 15:59:13 GMT-0700 (PDT) 309 |
    310 | 311 | 312 | 313 | 314 | 315 | --------------------------------------------------------------------------------