├── .gitignore ├── resources ├── config │ └── mentions.php ├── views │ └── assets.blade.php └── assets │ └── laravel.mentions.js ├── src ├── Http │ ├── routes.php │ └── Controllers │ │ └── ApiController.php ├── helper.php ├── MentionServiceProvider.php └── Factory │ └── MentionBuilder.php ├── tests └── ExampleTest.php ├── CHANGELOG.md ├── .travis.yml ├── dist ├── jquery.atwho.min.css ├── jquery.caret.js └── jquery.atwho.js ├── phpunit.xml.dist ├── LICENSE.md ├── CONTRIBUTING.md ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /vendor 3 | /build 4 | composer.lock -------------------------------------------------------------------------------- /resources/config/mentions.php: -------------------------------------------------------------------------------- 1 | 'App\User', 6 | 7 | ]; 8 | -------------------------------------------------------------------------------- /resources/views/assets.blade.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/Http/routes.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All Notable changes to laravel-mentions will be documented in this file 4 | 5 | ## 2015-11-04 6 | 7 | ### Added 8 | - Nothing 9 | 10 | ### Deprecated 11 | - Nothing 12 | 13 | ### Fixed 14 | - Changed namespace from Busayo to Unicodeveloper 15 | 16 | ### Removed 17 | - Nothing 18 | 19 | ### Security 20 | - Nothing 21 | -------------------------------------------------------------------------------- /src/helper.php: -------------------------------------------------------------------------------- 1 | get('q'); 22 | $column = $request->get('c'); 23 | 24 | $model = app()->make(config('mentions.' . $type)); 25 | 26 | $records = $model->where($column, 'LIKE', "%$query%") 27 | ->get([$column]); 28 | 29 | foreach ($records as $record) { 30 | $resultColumns[] = $record->$column; 31 | } 32 | 33 | return response()->json($resultColumns); 34 | } catch (\ReflectionException $e) { 35 | return response()->json('Not Found', 404); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Prosper Otemuyiwa 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 13 | > all 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 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/thephpleague/:package_name). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unicodeveloper/laravel-mentions", 3 | "description": "Laravel 5 package that provides facebook-like mention functionality", 4 | "keywords": [ 5 | "laravel", 6 | "laravel 5", 7 | "laravel-mentions", 8 | "facebook-mentions", 9 | "autocomplete", 10 | "smart search" 11 | ], 12 | "homepage": "https://github.com/unicodeveloper/laravel-mentions", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Prosper Otemuyiwa", 17 | "email": "prosperotemuyiwa@gmail.com", 18 | "homepage": "https://twitter.com/unicodeveloper", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php" : ">=5.5.9", 24 | "laravelcollective/html": "~5.0|~5.1" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit" : "4.*", 28 | "scrutinizer/ocular": "~1.1" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Unicodeveloper\\Mention\\": "src" 33 | }, 34 | "files": [ 35 | "src/helper.php" 36 | ] 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Unicodeveloper\\Mention\\Test\\": "tests" 41 | } 42 | }, 43 | "scripts": { 44 | "test": "phpunit" 45 | }, 46 | "extra": { 47 | "branch-alias": { 48 | "dev-master": "1.0-dev" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/MentionServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 29 | $config => config_path('mentions.php'), 30 | $views => base_path('resources/views/vendor/mentions'), 31 | $script => public_path('js'), 32 | ]); 33 | 34 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'mentions'); 35 | 36 | if (! $this->app->routesAreCached()) { 37 | require __DIR__.'/Http/routes.php'; 38 | } 39 | } 40 | 41 | 42 | /** 43 | * Register the application services 44 | * @return void 45 | */ 46 | public function register() 47 | { 48 | $this->app->singleton('mentionBuilder', function ($app) { 49 | $form = new MentionBuilder($app['html'], $app['url'], $app['session.store']->getToken()); 50 | 51 | return $form->setSessionStore($app['session.store']); 52 | }); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/Factory/MentionBuilder.php: -------------------------------------------------------------------------------- 1 | text($name, $value, [ 40 | 'id' => 'mention-' . $name, 41 | 'class' => $class 42 | ]); 43 | 44 | 45 | $scriptTag = <<< EOT 46 | 51 | EOT; 52 | 53 | return $scriptTag.$input; 54 | } 55 | 56 | /** 57 | * Create a textarea input field. 58 | * 59 | * @param string $name 60 | * @param string $value 61 | * @param string $type 62 | * @param string $column 63 | * @param string $class 64 | * 65 | * @return string 66 | */ 67 | public function asTextArea($name, $value, $type, $column, $class = '') 68 | { 69 | $input = $this->textarea($name, $value, [ 70 | 'id' => 'mention-' . $name, 71 | 'class' => $class 72 | ]); 73 | 74 | $scriptTag = <<< EOT 75 | 80 | EOT; 81 | 82 | return $scriptTag.$input; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravel-mentions 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/unicodeveloper/laravel-mentions/v/stable.svg)](https://packagist.org/packages/unicodeveloper/laravel-mentions) 4 | ![](https://img.shields.io/badge/unicodeveloper-approved-brightgreen.svg) 5 | [![License](https://poser.pugx.org/unicodeveloper/laravel-mentions/license.svg)](LICENSE.md) 6 | [![Build Status](https://img.shields.io/travis/unicodeveloper/laravel-mentions.svg)](https://travis-ci.org/unicodeveloper/laravel-mentions) 7 | [![Quality Score](https://img.shields.io/scrutinizer/g/unicodeveloper/laravel-mentions.svg?style=flat-square)](https://scrutinizer-ci.com/g/unicodeveloper/laravel-mentions) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/unicodeveloper/laravel-mentions.svg?style=flat-square)](https://packagist.org/packages/unicodeveloper/laravel-mentions) 9 | 10 | This package makes it possible to create text/textarea fields that enable **mentioning** by using [At.js](https://github.com/ichord/At.js). 11 | 12 | The data for the autocomplete is loaded from a route which will load data based on predefined key-value pairs of an alias and a model in the config. 13 | 14 | ## Installation 15 | 16 | First, pull in the package through Composer. 17 | 18 | ```js 19 | "require": { 20 | "unicodeveloper/laravel-mentions": "1.1.*" 21 | } 22 | ``` 23 | 24 | And then include these service providers within `config/app.php`. 25 | 26 | ```php 27 | 'providers' => [ 28 | Unicodeveloper\Mention\MentionServiceProvider::class, 29 | Collective\Html\HtmlServiceProvider::class, 30 | ]; 31 | ``` 32 | 33 | If you need to modify the configuration or the views, you can run: 34 | 35 | ```bash 36 | php artisan vendor:publish 37 | ``` 38 | 39 | The package views will now be located in the `app/resources/views/vendor/mentions/` directory and the configuration will be located at `config/mentions.php`. 40 | 41 | ## Configuration 42 | 43 | To make it possible for At.js to load data we need to define key-value pairs that consist of an alias and a corresponding model. 44 | 45 | ```php 46 | return [ 47 | 48 | 'users' => 'App\User', // responds to /api/mentions/users 49 | 'friends' => 'App\Friend', // responds to /api/mentions/friends 50 | 'clients' => 'App\Client', // responds to /api/mentions/clients 51 | 'supports' => 'App\Supporter', // responds to /api/mentions/supports 52 | 53 | ]; 54 | ``` 55 | 56 | So now with these `aliases` configured we could create a new textfield which will send a request to the `users` route and search for matching data in the `name` column. 57 | 58 | ```php 59 | {!! mention()->asText('recipient', old('recipient'), 'users', 'name') !!} 60 | ``` 61 | 62 | You can also add a class name for styling of the text and textareas, that's the last argument. In this example, it is `user-form` 63 | 64 | ```php 65 | {!! mention()->asText('recipient', old('recipient'), 'users', 'name', 'user-form') !!} 66 | ``` 67 | 68 | ## Example 69 | 70 | ```html 71 | 72 | 73 | 74 | 75 | Laravel PHP Framework 76 | 77 | 78 | 79 | 80 | 81 | 82 | @include('mentions::assets') 83 | 84 | 85 | 86 |
87 | {!! mention()->asText('recipient', old('recipient'), 'users', 'name') !!} 88 | {!! mention()->asTextArea('message', old('message'), 'users', 'name') !!} 89 |
90 | 91 | 92 | ``` 93 | 94 | 95 | ## Install 96 | 97 | Via Composer 98 | 99 | ``` bash 100 | $ composer require unicodeveloper/laravel-mentions 101 | ``` 102 | 103 | ## Change log 104 | 105 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 106 | 107 | ## Testing 108 | 109 | ``` bash 110 | $ composer test 111 | ``` 112 | 113 | ## Contributing 114 | 115 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 116 | 117 | ## How can I thank you? 118 | 119 | Why not star the github repo? I'd love the attention! Why not share the link for this repository on Twitter or HackerNews? Spread the word! 120 | 121 | Don't forget to [follow me on twitter](https://twitter.com/unicodeveloper)! 122 | 123 | Thanks! 124 | Prosper Otemuyiwa. 125 | 126 | ## License 127 | 128 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 129 | 130 | -------------------------------------------------------------------------------- /dist/jquery.caret.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module. 4 | define(["jquery"], function ($) { 5 | return (root.returnExportsGlobal = factory($)); 6 | }); 7 | } else if (typeof exports === 'object') { 8 | // Node. Does not work with strict CommonJS, but 9 | // only CommonJS-like enviroments that support module.exports, 10 | // like Node. 11 | module.exports = factory(require("jquery")); 12 | } else { 13 | factory(jQuery); 14 | } 15 | }(this, function ($) { 16 | 17 | /* 18 | Implement Github like autocomplete mentions 19 | http://ichord.github.com/At.js 20 | 21 | Copyright (c) 2013 chord.luo@gmail.com 22 | Licensed under the MIT license. 23 | */ 24 | 25 | /* 26 | 本插件操作 textarea 或者 input 内的插入符 27 | 只实现了获得插入符在文本框中的位置,我设置 28 | 插入符的位置. 29 | */ 30 | 31 | "use strict"; 32 | var EditableCaret, InputCaret, Mirror, Utils, discoveryIframeOf, methods, oDocument, oFrame, oWindow, pluginName, setContextBy; 33 | 34 | pluginName = 'caret'; 35 | 36 | EditableCaret = (function() { 37 | function EditableCaret($inputor) { 38 | this.$inputor = $inputor; 39 | this.domInputor = this.$inputor[0]; 40 | } 41 | 42 | EditableCaret.prototype.setPos = function(pos) { 43 | return this.domInputor; 44 | }; 45 | 46 | EditableCaret.prototype.getIEPosition = function() { 47 | return this.getPosition(); 48 | }; 49 | 50 | EditableCaret.prototype.getPosition = function() { 51 | var inputor_offset, offset; 52 | offset = this.getOffset(); 53 | inputor_offset = this.$inputor.offset(); 54 | offset.left -= inputor_offset.left; 55 | offset.top -= inputor_offset.top; 56 | return offset; 57 | }; 58 | 59 | EditableCaret.prototype.getOldIEPos = function() { 60 | var preCaretTextRange, textRange; 61 | textRange = oDocument.selection.createRange(); 62 | preCaretTextRange = oDocument.body.createTextRange(); 63 | preCaretTextRange.moveToElementText(this.domInputor); 64 | preCaretTextRange.setEndPoint("EndToEnd", textRange); 65 | return preCaretTextRange.text.length; 66 | }; 67 | 68 | EditableCaret.prototype.getPos = function() { 69 | var clonedRange, pos, range; 70 | if (range = this.range()) { 71 | clonedRange = range.cloneRange(); 72 | clonedRange.selectNodeContents(this.domInputor); 73 | clonedRange.setEnd(range.endContainer, range.endOffset); 74 | pos = clonedRange.toString().length; 75 | clonedRange.detach(); 76 | return pos; 77 | } else if (oDocument.selection) { 78 | return this.getOldIEPos(); 79 | } 80 | }; 81 | 82 | EditableCaret.prototype.getOldIEOffset = function() { 83 | var range, rect; 84 | range = oDocument.selection.createRange().duplicate(); 85 | range.moveStart("character", -1); 86 | rect = range.getBoundingClientRect(); 87 | return { 88 | height: rect.bottom - rect.top, 89 | left: rect.left, 90 | top: rect.top 91 | }; 92 | }; 93 | 94 | EditableCaret.prototype.getOffset = function(pos) { 95 | var clonedRange, offset, range, rect, shadowCaret; 96 | if (oWindow.getSelection && (range = this.range())) { 97 | if (range.endOffset - 1 > 0 && range.endContainer === !this.domInputor) { 98 | clonedRange = range.cloneRange(); 99 | clonedRange.setStart(range.endContainer, range.endOffset - 1); 100 | clonedRange.setEnd(range.endContainer, range.endOffset); 101 | rect = clonedRange.getBoundingClientRect(); 102 | offset = { 103 | height: rect.height, 104 | left: rect.left + rect.width, 105 | top: rect.top 106 | }; 107 | clonedRange.detach(); 108 | } 109 | if (!offset || (offset != null ? offset.height : void 0) === 0) { 110 | clonedRange = range.cloneRange(); 111 | shadowCaret = $(oDocument.createTextNode("|")); 112 | clonedRange.insertNode(shadowCaret[0]); 113 | clonedRange.selectNode(shadowCaret[0]); 114 | rect = clonedRange.getBoundingClientRect(); 115 | offset = { 116 | height: rect.height, 117 | left: rect.left, 118 | top: rect.top 119 | }; 120 | shadowCaret.remove(); 121 | clonedRange.detach(); 122 | } 123 | } else if (oDocument.selection) { 124 | offset = this.getOldIEOffset(); 125 | } 126 | if (offset) { 127 | offset.top += $(oWindow).scrollTop(); 128 | offset.left += $(oWindow).scrollLeft(); 129 | } 130 | return offset; 131 | }; 132 | 133 | EditableCaret.prototype.range = function() { 134 | var sel; 135 | if (!oWindow.getSelection) { 136 | return; 137 | } 138 | sel = oWindow.getSelection(); 139 | if (sel.rangeCount > 0) { 140 | return sel.getRangeAt(0); 141 | } else { 142 | return null; 143 | } 144 | }; 145 | 146 | return EditableCaret; 147 | 148 | })(); 149 | 150 | InputCaret = (function() { 151 | function InputCaret($inputor) { 152 | this.$inputor = $inputor; 153 | this.domInputor = this.$inputor[0]; 154 | } 155 | 156 | InputCaret.prototype.getIEPos = function() { 157 | var endRange, inputor, len, normalizedValue, pos, range, textInputRange; 158 | inputor = this.domInputor; 159 | range = oDocument.selection.createRange(); 160 | pos = 0; 161 | if (range && range.parentElement() === inputor) { 162 | normalizedValue = inputor.value.replace(/\r\n/g, "\n"); 163 | len = normalizedValue.length; 164 | textInputRange = inputor.createTextRange(); 165 | textInputRange.moveToBookmark(range.getBookmark()); 166 | endRange = inputor.createTextRange(); 167 | endRange.collapse(false); 168 | if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) { 169 | pos = len; 170 | } else { 171 | pos = -textInputRange.moveStart("character", -len); 172 | } 173 | } 174 | return pos; 175 | }; 176 | 177 | InputCaret.prototype.getPos = function() { 178 | if (oDocument.selection) { 179 | return this.getIEPos(); 180 | } else { 181 | return this.domInputor.selectionStart; 182 | } 183 | }; 184 | 185 | InputCaret.prototype.setPos = function(pos) { 186 | var inputor, range; 187 | inputor = this.domInputor; 188 | if (oDocument.selection) { 189 | range = inputor.createTextRange(); 190 | range.move("character", pos); 191 | range.select(); 192 | } else if (inputor.setSelectionRange) { 193 | inputor.setSelectionRange(pos, pos); 194 | } 195 | return inputor; 196 | }; 197 | 198 | InputCaret.prototype.getIEOffset = function(pos) { 199 | var h, textRange, x, y; 200 | textRange = this.domInputor.createTextRange(); 201 | pos || (pos = this.getPos()); 202 | textRange.move('character', pos); 203 | x = textRange.boundingLeft; 204 | y = textRange.boundingTop; 205 | h = textRange.boundingHeight; 206 | return { 207 | left: x, 208 | top: y, 209 | height: h 210 | }; 211 | }; 212 | 213 | InputCaret.prototype.getOffset = function(pos) { 214 | var $inputor, offset, position; 215 | $inputor = this.$inputor; 216 | if (oDocument.selection) { 217 | offset = this.getIEOffset(pos); 218 | offset.top += $(oWindow).scrollTop() + $inputor.scrollTop(); 219 | offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft(); 220 | return offset; 221 | } else { 222 | offset = $inputor.offset(); 223 | position = this.getPosition(pos); 224 | return offset = { 225 | left: offset.left + position.left - $inputor.scrollLeft(), 226 | top: offset.top + position.top - $inputor.scrollTop(), 227 | height: position.height 228 | }; 229 | } 230 | }; 231 | 232 | InputCaret.prototype.getPosition = function(pos) { 233 | var $inputor, at_rect, end_range, format, html, mirror, start_range; 234 | $inputor = this.$inputor; 235 | format = function(value) { 236 | value = value.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g, "
"); 237 | if (/firefox/i.test(navigator.userAgent)) { 238 | value = value.replace(/\s/g, ' '); 239 | } 240 | return value; 241 | }; 242 | if (pos === void 0) { 243 | pos = this.getPos(); 244 | } 245 | start_range = $inputor.val().slice(0, pos); 246 | end_range = $inputor.val().slice(pos); 247 | html = "" + format(start_range) + ""; 248 | html += "|"; 249 | html += "" + format(end_range) + ""; 250 | mirror = new Mirror($inputor); 251 | return at_rect = mirror.create(html).rect(); 252 | }; 253 | 254 | InputCaret.prototype.getIEPosition = function(pos) { 255 | var h, inputorOffset, offset, x, y; 256 | offset = this.getIEOffset(pos); 257 | inputorOffset = this.$inputor.offset(); 258 | x = offset.left - inputorOffset.left; 259 | y = offset.top - inputorOffset.top; 260 | h = offset.height; 261 | return { 262 | left: x, 263 | top: y, 264 | height: h 265 | }; 266 | }; 267 | 268 | return InputCaret; 269 | 270 | })(); 271 | 272 | Mirror = (function() { 273 | Mirror.prototype.css_attr = ["borderBottomWidth", "borderLeftWidth", "borderRightWidth", "borderTopStyle", "borderRightStyle", "borderBottomStyle", "borderLeftStyle", "borderTopWidth", "boxSizing", "fontFamily", "fontSize", "fontWeight", "height", "letterSpacing", "lineHeight", "marginBottom", "marginLeft", "marginRight", "marginTop", "outlineWidth", "overflow", "overflowX", "overflowY", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textAlign", "textOverflow", "textTransform", "whiteSpace", "wordBreak", "wordWrap"]; 274 | 275 | function Mirror($inputor) { 276 | this.$inputor = $inputor; 277 | } 278 | 279 | Mirror.prototype.mirrorCss = function() { 280 | var css, 281 | _this = this; 282 | css = { 283 | position: 'absolute', 284 | left: -9999, 285 | top: 0, 286 | zIndex: -20000 287 | }; 288 | if (this.$inputor.prop('tagName') === 'TEXTAREA') { 289 | this.css_attr.push('width'); 290 | } 291 | $.each(this.css_attr, function(i, p) { 292 | return css[p] = _this.$inputor.css(p); 293 | }); 294 | return css; 295 | }; 296 | 297 | Mirror.prototype.create = function(html) { 298 | this.$mirror = $('
'); 299 | this.$mirror.css(this.mirrorCss()); 300 | this.$mirror.html(html); 301 | this.$inputor.after(this.$mirror); 302 | return this; 303 | }; 304 | 305 | Mirror.prototype.rect = function() { 306 | var $flag, pos, rect; 307 | $flag = this.$mirror.find("#caret"); 308 | pos = $flag.position(); 309 | rect = { 310 | left: pos.left, 311 | top: pos.top, 312 | height: $flag.height() 313 | }; 314 | this.$mirror.remove(); 315 | return rect; 316 | }; 317 | 318 | return Mirror; 319 | 320 | })(); 321 | 322 | Utils = { 323 | contentEditable: function($inputor) { 324 | return !!($inputor[0].contentEditable && $inputor[0].contentEditable === 'true'); 325 | } 326 | }; 327 | 328 | methods = { 329 | pos: function(pos) { 330 | if (pos || pos === 0) { 331 | return this.setPos(pos); 332 | } else { 333 | return this.getPos(); 334 | } 335 | }, 336 | position: function(pos) { 337 | if (oDocument.selection) { 338 | return this.getIEPosition(pos); 339 | } else { 340 | return this.getPosition(pos); 341 | } 342 | }, 343 | offset: function(pos) { 344 | var offset; 345 | offset = this.getOffset(pos); 346 | return offset; 347 | } 348 | }; 349 | 350 | oDocument = null; 351 | 352 | oWindow = null; 353 | 354 | oFrame = null; 355 | 356 | setContextBy = function(settings) { 357 | var iframe; 358 | if (iframe = settings != null ? settings.iframe : void 0) { 359 | oFrame = iframe; 360 | oWindow = iframe.contentWindow; 361 | return oDocument = iframe.contentDocument || oWindow.document; 362 | } else { 363 | oFrame = void 0; 364 | oWindow = window; 365 | return oDocument = document; 366 | } 367 | }; 368 | 369 | discoveryIframeOf = function($dom) { 370 | var error; 371 | oDocument = $dom[0].ownerDocument; 372 | oWindow = oDocument.defaultView || oDocument.parentWindow; 373 | try { 374 | return oFrame = oWindow.frameElement; 375 | } catch (_error) { 376 | error = _error; 377 | } 378 | }; 379 | 380 | $.fn.caret = function(method, value, settings) { 381 | var caret; 382 | if (methods[method]) { 383 | if ($.isPlainObject(value)) { 384 | setContextBy(value); 385 | value = void 0; 386 | } else { 387 | setContextBy(settings); 388 | } 389 | caret = Utils.contentEditable(this) ? new EditableCaret(this) : new InputCaret(this); 390 | return methods[method].apply(caret, [value]); 391 | } else { 392 | return $.error("Method " + method + " does not exist on jQuery.caret"); 393 | } 394 | }; 395 | 396 | $.fn.caret.EditableCaret = EditableCaret; 397 | 398 | $.fn.caret.InputCaret = InputCaret; 399 | 400 | $.fn.caret.Utils = Utils; 401 | 402 | $.fn.caret.apis = methods; 403 | 404 | 405 | })); 406 | -------------------------------------------------------------------------------- /dist/jquery.atwho.js: -------------------------------------------------------------------------------- 1 | /*! jquery.atwho - v1.4.0 %> 2 | * Copyright (c) 2015 chord.luo ; 3 | * homepage: http://ichord.github.com/At.js 4 | * Licensed MIT 5 | */ 6 | (function (root, factory) { 7 | if (typeof define === 'function' && define.amd) { 8 | // AMD. Register as an anonymous module unless amdModuleId is set 9 | define(["jquery"], function (a0) { 10 | return (factory(a0)); 11 | }); 12 | } else if (typeof exports === 'object') { 13 | // Node. Does not work with strict CommonJS, but 14 | // only CommonJS-like environments that support module.exports, 15 | // like Node. 16 | module.exports = factory(require("jquery")); 17 | } else { 18 | factory(jQuery); 19 | } 20 | }(this, function (jquery) { 21 | 22 | var $, Api, App, Controller, DEFAULT_CALLBACKS, EditableController, KEY_CODE, Model, TextareaController, View, 23 | slice = [].slice, 24 | extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, 25 | hasProp = {}.hasOwnProperty; 26 | 27 | $ = jquery; 28 | 29 | App = (function() { 30 | function App(inputor) { 31 | this.currentFlag = null; 32 | this.controllers = {}; 33 | this.aliasMaps = {}; 34 | this.$inputor = $(inputor); 35 | this.setupRootElement(); 36 | this.listen(); 37 | } 38 | 39 | App.prototype.createContainer = function(doc) { 40 | var ref; 41 | if ((ref = this.$el) != null) { 42 | ref.remove(); 43 | } 44 | return $(doc.body).append(this.$el = $("
")); 45 | }; 46 | 47 | App.prototype.setupRootElement = function(iframe, asRoot) { 48 | var error; 49 | if (asRoot == null) { 50 | asRoot = false; 51 | } 52 | if (iframe) { 53 | this.window = iframe.contentWindow; 54 | this.document = iframe.contentDocument || this.window.document; 55 | this.iframe = iframe; 56 | } else { 57 | this.document = this.$inputor[0].ownerDocument; 58 | this.window = this.document.defaultView || this.document.parentWindow; 59 | try { 60 | this.iframe = this.window.frameElement; 61 | } catch (_error) { 62 | error = _error; 63 | this.iframe = null; 64 | if ($.fn.atwho.debug) { 65 | throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n" + error); 66 | } 67 | } 68 | } 69 | return this.createContainer((this.iframeAsRoot = asRoot) ? this.document : document); 70 | }; 71 | 72 | App.prototype.controller = function(at) { 73 | var c, current, currentFlag, ref; 74 | if (this.aliasMaps[at]) { 75 | current = this.controllers[this.aliasMaps[at]]; 76 | } else { 77 | ref = this.controllers; 78 | for (currentFlag in ref) { 79 | c = ref[currentFlag]; 80 | if (currentFlag === at) { 81 | current = c; 82 | break; 83 | } 84 | } 85 | } 86 | if (current) { 87 | return current; 88 | } else { 89 | return this.controllers[this.currentFlag]; 90 | } 91 | }; 92 | 93 | App.prototype.setContextFor = function(at) { 94 | this.currentFlag = at; 95 | return this; 96 | }; 97 | 98 | App.prototype.reg = function(flag, setting) { 99 | var base, controller; 100 | controller = (base = this.controllers)[flag] || (base[flag] = this.$inputor.is('[contentEditable]') ? new EditableController(this, flag) : new TextareaController(this, flag)); 101 | if (setting.alias) { 102 | this.aliasMaps[setting.alias] = flag; 103 | } 104 | controller.init(setting); 105 | return this; 106 | }; 107 | 108 | App.prototype.listen = function() { 109 | return this.$inputor.on('compositionstart', (function(_this) { 110 | return function(e) { 111 | var ref; 112 | if ((ref = _this.controller()) != null) { 113 | ref.view.hide(); 114 | } 115 | _this.isComposing = true; 116 | return null; 117 | }; 118 | })(this)).on('compositionend', (function(_this) { 119 | return function(e) { 120 | _this.isComposing = false; 121 | return null; 122 | }; 123 | })(this)).on('keyup.atwhoInner', (function(_this) { 124 | return function(e) { 125 | return _this.onKeyup(e); 126 | }; 127 | })(this)).on('keydown.atwhoInner', (function(_this) { 128 | return function(e) { 129 | return _this.onKeydown(e); 130 | }; 131 | })(this)).on('blur.atwhoInner', (function(_this) { 132 | return function(e) { 133 | var c; 134 | if (c = _this.controller()) { 135 | c.expectedQueryCBId = null; 136 | return c.view.hide(e, c.getOpt("displayTimeout")); 137 | } 138 | }; 139 | })(this)).on('click.atwhoInner', (function(_this) { 140 | return function(e) { 141 | return _this.dispatch(e); 142 | }; 143 | })(this)).on('scroll.atwhoInner', (function(_this) { 144 | return function() { 145 | var lastScrollTop; 146 | lastScrollTop = _this.$inputor.scrollTop(); 147 | return function(e) { 148 | var currentScrollTop, ref; 149 | currentScrollTop = e.target.scrollTop; 150 | if (lastScrollTop !== currentScrollTop) { 151 | if ((ref = _this.controller()) != null) { 152 | ref.view.hide(e); 153 | } 154 | } 155 | lastScrollTop = currentScrollTop; 156 | return true; 157 | }; 158 | }; 159 | })(this)()); 160 | }; 161 | 162 | App.prototype.shutdown = function() { 163 | var _, c, ref; 164 | ref = this.controllers; 165 | for (_ in ref) { 166 | c = ref[_]; 167 | c.destroy(); 168 | delete this.controllers[_]; 169 | } 170 | this.$inputor.off('.atwhoInner'); 171 | return this.$el.remove(); 172 | }; 173 | 174 | App.prototype.dispatch = function(e) { 175 | var _, c, ref, results; 176 | ref = this.controllers; 177 | results = []; 178 | for (_ in ref) { 179 | c = ref[_]; 180 | results.push(c.lookUp(e)); 181 | } 182 | return results; 183 | }; 184 | 185 | App.prototype.onKeyup = function(e) { 186 | var ref; 187 | switch (e.keyCode) { 188 | case KEY_CODE.ESC: 189 | e.preventDefault(); 190 | if ((ref = this.controller()) != null) { 191 | ref.view.hide(); 192 | } 193 | break; 194 | case KEY_CODE.DOWN: 195 | case KEY_CODE.UP: 196 | case KEY_CODE.CTRL: 197 | case KEY_CODE.ENTER: 198 | $.noop(); 199 | break; 200 | case KEY_CODE.P: 201 | case KEY_CODE.N: 202 | if (!e.ctrlKey) { 203 | this.dispatch(e); 204 | } 205 | break; 206 | default: 207 | this.dispatch(e); 208 | } 209 | }; 210 | 211 | App.prototype.onKeydown = function(e) { 212 | var ref, view; 213 | view = (ref = this.controller()) != null ? ref.view : void 0; 214 | if (!(view && view.visible())) { 215 | return; 216 | } 217 | switch (e.keyCode) { 218 | case KEY_CODE.ESC: 219 | e.preventDefault(); 220 | view.hide(e); 221 | break; 222 | case KEY_CODE.UP: 223 | e.preventDefault(); 224 | view.prev(); 225 | break; 226 | case KEY_CODE.DOWN: 227 | e.preventDefault(); 228 | view.next(); 229 | break; 230 | case KEY_CODE.P: 231 | if (!e.ctrlKey) { 232 | return; 233 | } 234 | e.preventDefault(); 235 | view.prev(); 236 | break; 237 | case KEY_CODE.N: 238 | if (!e.ctrlKey) { 239 | return; 240 | } 241 | e.preventDefault(); 242 | view.next(); 243 | break; 244 | case KEY_CODE.TAB: 245 | case KEY_CODE.ENTER: 246 | case KEY_CODE.SPACE: 247 | if (!view.visible()) { 248 | return; 249 | } 250 | if (!this.controller().getOpt('spaceSelectsMatch') && e.keyCode === KEY_CODE.SPACE) { 251 | return; 252 | } 253 | if (!this.controller().getOpt('tabSelectsMatch') && e.keyCode === KEY_CODE.TAB) { 254 | return; 255 | } 256 | if (view.highlighted()) { 257 | e.preventDefault(); 258 | view.choose(e); 259 | } else { 260 | view.hide(e); 261 | } 262 | break; 263 | default: 264 | $.noop(); 265 | } 266 | }; 267 | 268 | return App; 269 | 270 | })(); 271 | 272 | Controller = (function() { 273 | Controller.prototype.uid = function() { 274 | return (Math.random().toString(16) + "000000000").substr(2, 8) + (new Date().getTime()); 275 | }; 276 | 277 | function Controller(app1, at1) { 278 | this.app = app1; 279 | this.at = at1; 280 | this.$inputor = this.app.$inputor; 281 | this.id = this.$inputor[0].id || this.uid(); 282 | this.expectedQueryCBId = null; 283 | this.setting = null; 284 | this.query = null; 285 | this.pos = 0; 286 | this.range = null; 287 | if ((this.$el = $("#atwho-ground-" + this.id, this.app.$el)).length === 0) { 288 | this.app.$el.append(this.$el = $("
")); 289 | } 290 | this.model = new Model(this); 291 | this.view = new View(this); 292 | } 293 | 294 | Controller.prototype.init = function(setting) { 295 | this.setting = $.extend({}, this.setting || $.fn.atwho["default"], setting); 296 | this.view.init(); 297 | return this.model.reload(this.setting.data); 298 | }; 299 | 300 | Controller.prototype.destroy = function() { 301 | this.trigger('beforeDestroy'); 302 | this.model.destroy(); 303 | this.view.destroy(); 304 | return this.$el.remove(); 305 | }; 306 | 307 | Controller.prototype.callDefault = function() { 308 | var args, error, funcName; 309 | funcName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; 310 | try { 311 | return DEFAULT_CALLBACKS[funcName].apply(this, args); 312 | } catch (_error) { 313 | error = _error; 314 | return $.error(error + " Or maybe At.js doesn't have function " + funcName); 315 | } 316 | }; 317 | 318 | Controller.prototype.trigger = function(name, data) { 319 | var alias, eventName; 320 | if (data == null) { 321 | data = []; 322 | } 323 | data.push(this); 324 | alias = this.getOpt('alias'); 325 | eventName = alias ? name + "-" + alias + ".atwho" : name + ".atwho"; 326 | return this.$inputor.trigger(eventName, data); 327 | }; 328 | 329 | Controller.prototype.callbacks = function(funcName) { 330 | return this.getOpt("callbacks")[funcName] || DEFAULT_CALLBACKS[funcName]; 331 | }; 332 | 333 | Controller.prototype.getOpt = function(at, default_value) { 334 | var e; 335 | try { 336 | return this.setting[at]; 337 | } catch (_error) { 338 | e = _error; 339 | return null; 340 | } 341 | }; 342 | 343 | Controller.prototype.insertContentFor = function($li) { 344 | var data, tpl; 345 | tpl = this.getOpt('insertTpl'); 346 | data = $.extend({}, $li.data('item-data'), { 347 | 'atwho-at': this.at 348 | }); 349 | return this.callbacks("tplEval").call(this, tpl, data, "onInsert"); 350 | }; 351 | 352 | Controller.prototype.renderView = function(data) { 353 | var searchKey; 354 | searchKey = this.getOpt("searchKey"); 355 | data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), searchKey); 356 | return this.view.render(data.slice(0, this.getOpt('limit'))); 357 | }; 358 | 359 | Controller.arrayToDefaultHash = function(data) { 360 | var i, item, len, results; 361 | if (!$.isArray(data)) { 362 | return data; 363 | } 364 | results = []; 365 | for (i = 0, len = data.length; i < len; i++) { 366 | item = data[i]; 367 | if ($.isPlainObject(item)) { 368 | results.push(item); 369 | } else { 370 | results.push({ 371 | name: item 372 | }); 373 | } 374 | } 375 | return results; 376 | }; 377 | 378 | Controller.prototype.lookUp = function(e) { 379 | var query, wait; 380 | if (e && e.type === 'click' && !this.getOpt('lookUpOnClick')) { 381 | return; 382 | } 383 | if (this.getOpt('suspendOnComposing') && this.app.isComposing) { 384 | return; 385 | } 386 | query = this.catchQuery(e); 387 | if (!query) { 388 | this.expectedQueryCBId = null; 389 | return query; 390 | } 391 | this.app.setContextFor(this.at); 392 | if (wait = this.getOpt('delay')) { 393 | this._delayLookUp(query, wait); 394 | } else { 395 | this._lookUp(query); 396 | } 397 | return query; 398 | }; 399 | 400 | Controller.prototype._delayLookUp = function(query, wait) { 401 | var now, remaining; 402 | now = Date.now ? Date.now() : new Date().getTime(); 403 | this.previousCallTime || (this.previousCallTime = now); 404 | remaining = wait - (now - this.previousCallTime); 405 | if ((0 < remaining && remaining < wait)) { 406 | this.previousCallTime = now; 407 | this._stopDelayedCall(); 408 | return this.delayedCallTimeout = setTimeout((function(_this) { 409 | return function() { 410 | _this.previousCallTime = 0; 411 | _this.delayedCallTimeout = null; 412 | return _this._lookUp(query); 413 | }; 414 | })(this), wait); 415 | } else { 416 | this._stopDelayedCall(); 417 | if (this.previousCallTime !== now) { 418 | this.previousCallTime = 0; 419 | } 420 | return this._lookUp(query); 421 | } 422 | }; 423 | 424 | Controller.prototype._stopDelayedCall = function() { 425 | if (this.delayedCallTimeout) { 426 | clearTimeout(this.delayedCallTimeout); 427 | return this.delayedCallTimeout = null; 428 | } 429 | }; 430 | 431 | Controller.prototype._generateQueryCBId = function() { 432 | return {}; 433 | }; 434 | 435 | Controller.prototype._lookUp = function(query) { 436 | var _callback; 437 | _callback = function(queryCBId, data) { 438 | if (queryCBId !== this.expectedQueryCBId) { 439 | return; 440 | } 441 | if (data && data.length > 0) { 442 | return this.renderView(this.constructor.arrayToDefaultHash(data)); 443 | } else { 444 | return this.view.hide(); 445 | } 446 | }; 447 | this.expectedQueryCBId = this._generateQueryCBId(); 448 | return this.model.query(query.text, $.proxy(_callback, this, this.expectedQueryCBId)); 449 | }; 450 | 451 | return Controller; 452 | 453 | })(); 454 | 455 | TextareaController = (function(superClass) { 456 | extend(TextareaController, superClass); 457 | 458 | function TextareaController() { 459 | return TextareaController.__super__.constructor.apply(this, arguments); 460 | } 461 | 462 | TextareaController.prototype.catchQuery = function() { 463 | var caretPos, content, end, isString, query, start, subtext; 464 | content = this.$inputor.val(); 465 | caretPos = this.$inputor.caret('pos', { 466 | iframe: this.app.iframe 467 | }); 468 | subtext = content.slice(0, caretPos); 469 | query = this.callbacks("matcher").call(this, this.at, subtext, this.getOpt('startWithSpace')); 470 | isString = typeof query === 'string'; 471 | if (isString && query.length < this.getOpt('minLen', 0)) { 472 | return; 473 | } 474 | if (isString && query.length <= this.getOpt('maxLen', 20)) { 475 | start = caretPos - query.length; 476 | end = start + query.length; 477 | this.pos = start; 478 | query = { 479 | 'text': query, 480 | 'headPos': start, 481 | 'endPos': end 482 | }; 483 | this.trigger("matched", [this.at, query.text]); 484 | } else { 485 | query = null; 486 | this.view.hide(); 487 | } 488 | return this.query = query; 489 | }; 490 | 491 | TextareaController.prototype.rect = function() { 492 | var c, iframeOffset, scaleBottom; 493 | if (!(c = this.$inputor.caret('offset', this.pos - 1, { 494 | iframe: this.app.iframe 495 | }))) { 496 | return; 497 | } 498 | if (this.app.iframe && !this.app.iframeAsRoot) { 499 | iframeOffset = $(this.app.iframe).offset(); 500 | c.left += iframeOffset.left; 501 | c.top += iframeOffset.top; 502 | } 503 | scaleBottom = this.app.document.selection ? 0 : 2; 504 | return { 505 | left: c.left, 506 | top: c.top, 507 | bottom: c.top + c.height + scaleBottom 508 | }; 509 | }; 510 | 511 | TextareaController.prototype.insert = function(content, $li) { 512 | var $inputor, source, startStr, suffix, text; 513 | $inputor = this.$inputor; 514 | source = $inputor.val(); 515 | startStr = source.slice(0, Math.max(this.query.headPos - this.at.length, 0)); 516 | suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || " "; 517 | content += suffix; 518 | text = "" + startStr + content + (source.slice(this.query['endPos'] || 0)); 519 | $inputor.val(text); 520 | $inputor.caret('pos', startStr.length + content.length, { 521 | iframe: this.app.iframe 522 | }); 523 | if (!$inputor.is(':focus')) { 524 | $inputor.focus(); 525 | } 526 | return $inputor.change(); 527 | }; 528 | 529 | return TextareaController; 530 | 531 | })(Controller); 532 | 533 | EditableController = (function(superClass) { 534 | extend(EditableController, superClass); 535 | 536 | function EditableController() { 537 | return EditableController.__super__.constructor.apply(this, arguments); 538 | } 539 | 540 | EditableController.prototype._getRange = function() { 541 | var sel; 542 | sel = this.app.window.getSelection(); 543 | if (sel.rangeCount > 0) { 544 | return sel.getRangeAt(0); 545 | } 546 | }; 547 | 548 | EditableController.prototype._setRange = function(position, node, range) { 549 | if (range == null) { 550 | range = this._getRange(); 551 | } 552 | if (!range) { 553 | return; 554 | } 555 | node = $(node)[0]; 556 | if (position === 'after') { 557 | range.setEndAfter(node); 558 | range.setStartAfter(node); 559 | } else { 560 | range.setEndBefore(node); 561 | range.setStartBefore(node); 562 | } 563 | range.collapse(false); 564 | return this._clearRange(range); 565 | }; 566 | 567 | EditableController.prototype._clearRange = function(range) { 568 | var sel; 569 | if (range == null) { 570 | range = this._getRange(); 571 | } 572 | sel = this.app.window.getSelection(); 573 | if (this.ctrl_a_pressed == null) { 574 | sel.removeAllRanges(); 575 | return sel.addRange(range); 576 | } 577 | }; 578 | 579 | EditableController.prototype._movingEvent = function(e) { 580 | var ref; 581 | return e.type === 'click' || ((ref = e.which) === KEY_CODE.RIGHT || ref === KEY_CODE.LEFT || ref === KEY_CODE.UP || ref === KEY_CODE.DOWN); 582 | }; 583 | 584 | EditableController.prototype._unwrap = function(node) { 585 | var next; 586 | node = $(node).unwrap().get(0); 587 | if ((next = node.nextSibling) && next.nodeValue) { 588 | node.nodeValue += next.nodeValue; 589 | $(next).remove(); 590 | } 591 | return node; 592 | }; 593 | 594 | EditableController.prototype.catchQuery = function(e) { 595 | var $inserted, $query, _range, index, inserted, isString, lastNode, matched, offset, query, query_content, range; 596 | if (!(range = this._getRange())) { 597 | return; 598 | } 599 | if (!range.collapsed) { 600 | return; 601 | } 602 | if (e.which === KEY_CODE.ENTER) { 603 | ($query = $(range.startContainer).closest('.atwho-query')).contents().unwrap(); 604 | if ($query.is(':empty')) { 605 | $query.remove(); 606 | } 607 | ($query = $(".atwho-query", this.app.document)).text($query.text()).contents().last().unwrap(); 608 | this._clearRange(); 609 | return; 610 | } 611 | if (/firefox/i.test(navigator.userAgent)) { 612 | if ($(range.startContainer).is(this.$inputor)) { 613 | this._clearRange(); 614 | return; 615 | } 616 | if (e.which === KEY_CODE.BACKSPACE && range.startContainer.nodeType === document.ELEMENT_NODE && (offset = range.startOffset - 1) >= 0) { 617 | _range = range.cloneRange(); 618 | _range.setStart(range.startContainer, offset); 619 | if ($(_range.cloneContents()).contents().last().is('.atwho-inserted')) { 620 | inserted = $(range.startContainer).contents().get(offset); 621 | this._setRange('after', $(inserted).contents().last()); 622 | } 623 | } else if (e.which === KEY_CODE.LEFT && range.startContainer.nodeType === document.TEXT_NODE) { 624 | $inserted = $(range.startContainer.previousSibling); 625 | if ($inserted.is('.atwho-inserted') && range.startOffset === 0) { 626 | this._setRange('after', $inserted.contents().last()); 627 | } 628 | } 629 | } 630 | $(range.startContainer).closest('.atwho-inserted').addClass('atwho-query').siblings().removeClass('atwho-query'); 631 | if (($query = $(".atwho-query", this.app.document)).length > 0 && $query.is(':empty') && $query.text().length === 0) { 632 | $query.remove(); 633 | } 634 | if (!this._movingEvent(e)) { 635 | $query.removeClass('atwho-inserted'); 636 | } 637 | if ($query.length > 0) { 638 | switch (e.which) { 639 | case KEY_CODE.LEFT: 640 | this._setRange('before', $query.get(0), range); 641 | $query.removeClass('atwho-query'); 642 | return; 643 | case KEY_CODE.RIGHT: 644 | this._setRange('after', $query.get(0).nextSibling, range); 645 | $query.removeClass('atwho-query'); 646 | return; 647 | } 648 | } 649 | if ($query.length > 0 && (query_content = $query.attr('data-atwho-at-query'))) { 650 | $query.empty().html(query_content).attr('data-atwho-at-query', null); 651 | this._setRange('after', $query.get(0), range); 652 | } 653 | _range = range.cloneRange(); 654 | _range.setStart(range.startContainer, 0); 655 | matched = this.callbacks("matcher").call(this, this.at, _range.toString(), this.getOpt('startWithSpace')); 656 | isString = typeof matched === 'string'; 657 | if ($query.length === 0 && isString && (index = range.startOffset - this.at.length - matched.length) >= 0) { 658 | range.setStart(range.startContainer, index); 659 | $query = $('', this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass('atwho-query'); 660 | range.surroundContents($query.get(0)); 661 | lastNode = $query.contents().last().get(0); 662 | if (/firefox/i.test(navigator.userAgent)) { 663 | range.setStart(lastNode, lastNode.length); 664 | range.setEnd(lastNode, lastNode.length); 665 | this._clearRange(range); 666 | } else { 667 | this._setRange('after', lastNode, range); 668 | } 669 | } 670 | if (isString && matched.length < this.getOpt('minLen', 0)) { 671 | return; 672 | } 673 | if (isString && matched.length <= this.getOpt('maxLen', 20)) { 674 | query = { 675 | text: matched, 676 | el: $query 677 | }; 678 | this.trigger("matched", [this.at, query.text]); 679 | return this.query = query; 680 | } else { 681 | this.view.hide(); 682 | this.query = { 683 | el: $query 684 | }; 685 | if ($query.text().indexOf(this.at) >= 0) { 686 | if (this._movingEvent(e) && $query.hasClass('atwho-inserted')) { 687 | $query.removeClass('atwho-query'); 688 | } else if (false !== this.callbacks('afterMatchFailed').call(this, this.at, $query)) { 689 | this._setRange("after", this._unwrap($query.text($query.text()).contents().first())); 690 | } 691 | } 692 | return null; 693 | } 694 | }; 695 | 696 | EditableController.prototype.rect = function() { 697 | var $iframe, iframeOffset, rect; 698 | rect = this.query.el.offset(); 699 | if (this.app.iframe && !this.app.iframeAsRoot) { 700 | iframeOffset = ($iframe = $(this.app.iframe)).offset(); 701 | rect.left += iframeOffset.left - this.$inputor.scrollLeft(); 702 | rect.top += iframeOffset.top - this.$inputor.scrollTop(); 703 | } 704 | rect.bottom = rect.top + this.query.el.height(); 705 | return rect; 706 | }; 707 | 708 | EditableController.prototype.insert = function(content, $li) { 709 | var data, range, suffix, suffixNode; 710 | suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || "\u00A0"; 711 | data = $li.data('item-data'); 712 | this.query.el.removeClass('atwho-query').addClass('atwho-inserted').html(content).attr('data-atwho-at-query', "" + data['atwho-at'] + this.query.text); 713 | if (range = this._getRange()) { 714 | range.setEndAfter(this.query.el[0]); 715 | range.collapse(false); 716 | range.insertNode(suffixNode = this.app.document.createTextNode("\u2060" + suffix)); 717 | this._setRange('after', suffixNode, range); 718 | } 719 | if (!this.$inputor.is(':focus')) { 720 | this.$inputor.focus(); 721 | } 722 | return this.$inputor.change(); 723 | }; 724 | 725 | return EditableController; 726 | 727 | })(Controller); 728 | 729 | Model = (function() { 730 | function Model(context) { 731 | this.context = context; 732 | this.at = this.context.at; 733 | this.storage = this.context.$inputor; 734 | } 735 | 736 | Model.prototype.destroy = function() { 737 | return this.storage.data(this.at, null); 738 | }; 739 | 740 | Model.prototype.saved = function() { 741 | return this.fetch() > 0; 742 | }; 743 | 744 | Model.prototype.query = function(query, callback) { 745 | var _remoteFilter, data, searchKey; 746 | data = this.fetch(); 747 | searchKey = this.context.getOpt("searchKey"); 748 | data = this.context.callbacks('filter').call(this.context, query, data, searchKey) || []; 749 | _remoteFilter = this.context.callbacks('remoteFilter'); 750 | if (data.length > 0 || (!_remoteFilter && data.length === 0)) { 751 | return callback(data); 752 | } else { 753 | return _remoteFilter.call(this.context, query, callback); 754 | } 755 | }; 756 | 757 | Model.prototype.fetch = function() { 758 | return this.storage.data(this.at) || []; 759 | }; 760 | 761 | Model.prototype.save = function(data) { 762 | return this.storage.data(this.at, this.context.callbacks("beforeSave").call(this.context, data || [])); 763 | }; 764 | 765 | Model.prototype.load = function(data) { 766 | if (!(this.saved() || !data)) { 767 | return this._load(data); 768 | } 769 | }; 770 | 771 | Model.prototype.reload = function(data) { 772 | return this._load(data); 773 | }; 774 | 775 | Model.prototype._load = function(data) { 776 | if (typeof data === "string") { 777 | return $.ajax(data, { 778 | dataType: "json" 779 | }).done((function(_this) { 780 | return function(data) { 781 | return _this.save(data); 782 | }; 783 | })(this)); 784 | } else { 785 | return this.save(data); 786 | } 787 | }; 788 | 789 | return Model; 790 | 791 | })(); 792 | 793 | View = (function() { 794 | function View(context) { 795 | this.context = context; 796 | this.$el = $("
    "); 797 | this.timeoutID = null; 798 | this.context.$el.append(this.$el); 799 | this.bindEvent(); 800 | } 801 | 802 | View.prototype.init = function() { 803 | var id; 804 | id = this.context.getOpt("alias") || this.context.at.charCodeAt(0); 805 | return this.$el.attr({ 806 | 'id': "at-view-" + id 807 | }); 808 | }; 809 | 810 | View.prototype.destroy = function() { 811 | return this.$el.remove(); 812 | }; 813 | 814 | View.prototype.bindEvent = function() { 815 | var $menu; 816 | $menu = this.$el.find('ul'); 817 | return $menu.on('mouseenter.atwho-view', 'li', function(e) { 818 | $menu.find('.cur').removeClass('cur'); 819 | return $(e.currentTarget).addClass('cur'); 820 | }).on('click.atwho-view', 'li', (function(_this) { 821 | return function(e) { 822 | $menu.find('.cur').removeClass('cur'); 823 | $(e.currentTarget).addClass('cur'); 824 | _this.choose(e); 825 | return e.preventDefault(); 826 | }; 827 | })(this)); 828 | }; 829 | 830 | View.prototype.visible = function() { 831 | return this.$el.is(":visible"); 832 | }; 833 | 834 | View.prototype.highlighted = function() { 835 | return this.$el.find(".cur").length > 0; 836 | }; 837 | 838 | View.prototype.choose = function(e) { 839 | var $li, content; 840 | if (($li = this.$el.find(".cur")).length) { 841 | content = this.context.insertContentFor($li); 842 | this.context._stopDelayedCall(); 843 | this.context.insert(this.context.callbacks("beforeInsert").call(this.context, content, $li), $li); 844 | this.context.trigger("inserted", [$li, e]); 845 | this.hide(e); 846 | } 847 | if (this.context.getOpt("hideWithoutSuffix")) { 848 | return this.stopShowing = true; 849 | } 850 | }; 851 | 852 | View.prototype.reposition = function(rect) { 853 | var _window, offset, overflowOffset, ref; 854 | _window = this.context.app.iframeAsRoot ? this.context.app.window : window; 855 | if (rect.bottom + this.$el.height() - $(_window).scrollTop() > $(_window).height()) { 856 | rect.bottom = rect.top - this.$el.height(); 857 | } 858 | if (rect.left > (overflowOffset = $(_window).width() - this.$el.width() - 5)) { 859 | rect.left = overflowOffset; 860 | } 861 | offset = { 862 | left: rect.left, 863 | top: rect.bottom 864 | }; 865 | if ((ref = this.context.callbacks("beforeReposition")) != null) { 866 | ref.call(this.context, offset); 867 | } 868 | this.$el.offset(offset); 869 | return this.context.trigger("reposition", [offset]); 870 | }; 871 | 872 | View.prototype.next = function() { 873 | var cur, next; 874 | cur = this.$el.find('.cur').removeClass('cur'); 875 | next = cur.next(); 876 | if (!next.length) { 877 | next = this.$el.find('li:first'); 878 | } 879 | next.addClass('cur'); 880 | return this.scrollTop(Math.max(0, cur.innerHeight() * (next.index() + 2) - this.$el.height())); 881 | }; 882 | 883 | View.prototype.prev = function() { 884 | var cur, prev; 885 | cur = this.$el.find('.cur').removeClass('cur'); 886 | prev = cur.prev(); 887 | if (!prev.length) { 888 | prev = this.$el.find('li:last'); 889 | } 890 | prev.addClass('cur'); 891 | return this.scrollTop(Math.max(0, cur.innerHeight() * (prev.index() + 2) - this.$el.height())); 892 | }; 893 | 894 | View.prototype.scrollTop = function(scrollTop) { 895 | var scrollDuration; 896 | scrollDuration = this.context.getOpt('scrollDuration'); 897 | if (scrollDuration) { 898 | return this.$el.animate({ 899 | scrollTop: scrollTop 900 | }, scrollDuration); 901 | } else { 902 | return this.$el.scrollTop(scrollTop); 903 | } 904 | }; 905 | 906 | View.prototype.show = function() { 907 | var rect; 908 | if (this.stopShowing) { 909 | this.stopShowing = false; 910 | return; 911 | } 912 | if (!this.visible()) { 913 | this.$el.show(); 914 | this.$el.scrollTop(0); 915 | this.context.trigger('shown'); 916 | } 917 | if (rect = this.context.rect()) { 918 | return this.reposition(rect); 919 | } 920 | }; 921 | 922 | View.prototype.hide = function(e, time) { 923 | var callback; 924 | if (!this.visible()) { 925 | return; 926 | } 927 | if (isNaN(time)) { 928 | this.$el.hide(); 929 | return this.context.trigger('hidden', [e]); 930 | } else { 931 | callback = (function(_this) { 932 | return function() { 933 | return _this.hide(); 934 | }; 935 | })(this); 936 | clearTimeout(this.timeoutID); 937 | return this.timeoutID = setTimeout(callback, time); 938 | } 939 | }; 940 | 941 | View.prototype.render = function(list) { 942 | var $li, $ul, i, item, len, li, tpl; 943 | if (!($.isArray(list) && list.length > 0)) { 944 | this.hide(); 945 | return; 946 | } 947 | this.$el.find('ul').empty(); 948 | $ul = this.$el.find('ul'); 949 | tpl = this.context.getOpt('displayTpl'); 950 | for (i = 0, len = list.length; i < len; i++) { 951 | item = list[i]; 952 | item = $.extend({}, item, { 953 | 'atwho-at': this.context.at 954 | }); 955 | li = this.context.callbacks("tplEval").call(this.context, tpl, item, "onDisplay"); 956 | $li = $(this.context.callbacks("highlighter").call(this.context, li, this.context.query.text)); 957 | $li.data("item-data", item); 958 | $ul.append($li); 959 | } 960 | this.show(); 961 | if (this.context.getOpt('highlightFirst')) { 962 | return $ul.find("li:first").addClass("cur"); 963 | } 964 | }; 965 | 966 | return View; 967 | 968 | })(); 969 | 970 | KEY_CODE = { 971 | DOWN: 40, 972 | UP: 38, 973 | ESC: 27, 974 | TAB: 9, 975 | ENTER: 13, 976 | CTRL: 17, 977 | A: 65, 978 | P: 80, 979 | N: 78, 980 | LEFT: 37, 981 | UP: 38, 982 | RIGHT: 39, 983 | DOWN: 40, 984 | BACKSPACE: 8, 985 | SPACE: 32 986 | }; 987 | 988 | DEFAULT_CALLBACKS = { 989 | beforeSave: function(data) { 990 | return Controller.arrayToDefaultHash(data); 991 | }, 992 | matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { 993 | var _a, _y, match, regexp, space; 994 | flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 995 | if (should_startWithSpace) { 996 | flag = '(?:^|\\s)' + flag; 997 | } 998 | _a = decodeURI("%C3%80"); 999 | _y = decodeURI("%C3%BF"); 1000 | space = acceptSpaceBar ? "\ " : ""; 1001 | regexp = new RegExp(flag + "([A-Za-z" + _a + "-" + _y + "0-9_" + space + "\'\.\+\-]*)$|" + flag + "([^\\x00-\\xff]*)$", 'gi'); 1002 | match = regexp.exec(subtext); 1003 | if (match) { 1004 | return match[2] || match[1]; 1005 | } else { 1006 | return null; 1007 | } 1008 | }, 1009 | filter: function(query, data, searchKey) { 1010 | var _results, i, item, len; 1011 | _results = []; 1012 | for (i = 0, len = data.length; i < len; i++) { 1013 | item = data[i]; 1014 | if (~new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase())) { 1015 | _results.push(item); 1016 | } 1017 | } 1018 | return _results; 1019 | }, 1020 | remoteFilter: null, 1021 | sorter: function(query, items, searchKey) { 1022 | var _results, i, item, len; 1023 | if (!query) { 1024 | return items; 1025 | } 1026 | _results = []; 1027 | for (i = 0, len = items.length; i < len; i++) { 1028 | item = items[i]; 1029 | item.atwho_order = new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase()); 1030 | if (item.atwho_order > -1) { 1031 | _results.push(item); 1032 | } 1033 | } 1034 | return _results.sort(function(a, b) { 1035 | return a.atwho_order - b.atwho_order; 1036 | }); 1037 | }, 1038 | tplEval: function(tpl, map) { 1039 | var error, template; 1040 | template = tpl; 1041 | try { 1042 | if (typeof tpl !== 'string') { 1043 | template = tpl(map); 1044 | } 1045 | return template.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) { 1046 | return map[key]; 1047 | }); 1048 | } catch (_error) { 1049 | error = _error; 1050 | return ""; 1051 | } 1052 | }, 1053 | highlighter: function(li, query) { 1054 | var regexp; 1055 | if (!query) { 1056 | return li; 1057 | } 1058 | regexp = new RegExp(">\\s*(\\w*?)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig'); 1059 | return li.replace(regexp, function(str, $1, $2, $3) { 1060 | return '> ' + $1 + '' + $2 + '' + $3 + ' <'; 1061 | }); 1062 | }, 1063 | beforeInsert: function(value, $li) { 1064 | return value; 1065 | }, 1066 | beforeReposition: function(offset) { 1067 | return offset; 1068 | }, 1069 | afterMatchFailed: function(at, el) {} 1070 | }; 1071 | 1072 | Api = { 1073 | load: function(at, data) { 1074 | var c; 1075 | if (c = this.controller(at)) { 1076 | return c.model.load(data); 1077 | } 1078 | }, 1079 | isSelecting: function() { 1080 | var ref; 1081 | return !!((ref = this.controller()) != null ? ref.view.visible() : void 0); 1082 | }, 1083 | hide: function() { 1084 | var ref; 1085 | return (ref = this.controller()) != null ? ref.view.hide() : void 0; 1086 | }, 1087 | reposition: function() { 1088 | var c; 1089 | if (c = this.controller()) { 1090 | return c.view.reposition(c.rect()); 1091 | } 1092 | }, 1093 | setIframe: function(iframe, asRoot) { 1094 | this.setupRootElement(iframe, asRoot); 1095 | return null; 1096 | }, 1097 | run: function() { 1098 | return this.dispatch(); 1099 | }, 1100 | destroy: function() { 1101 | this.shutdown(); 1102 | return this.$inputor.data('atwho', null); 1103 | } 1104 | }; 1105 | 1106 | $.fn.atwho = function(method) { 1107 | var _args, result; 1108 | _args = arguments; 1109 | result = null; 1110 | this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function() { 1111 | var $this, app; 1112 | if (!(app = ($this = $(this)).data("atwho"))) { 1113 | $this.data('atwho', (app = new App(this))); 1114 | } 1115 | if (typeof method === 'object' || !method) { 1116 | return app.reg(method.at, method); 1117 | } else if (Api[method] && app) { 1118 | return result = Api[method].apply(app, Array.prototype.slice.call(_args, 1)); 1119 | } else { 1120 | return $.error("Method " + method + " does not exist on jQuery.atwho"); 1121 | } 1122 | }); 1123 | if (result != null) { 1124 | return result; 1125 | } else { 1126 | return this; 1127 | } 1128 | }; 1129 | 1130 | $.fn.atwho["default"] = { 1131 | at: void 0, 1132 | alias: void 0, 1133 | data: null, 1134 | displayTpl: "
  • ${name}
  • ", 1135 | insertTpl: "${atwho-at}${name}", 1136 | callbacks: DEFAULT_CALLBACKS, 1137 | searchKey: "name", 1138 | suffix: void 0, 1139 | hideWithoutSuffix: false, 1140 | startWithSpace: true, 1141 | highlightFirst: true, 1142 | limit: 5, 1143 | maxLen: 20, 1144 | minLen: 0, 1145 | displayTimeout: 300, 1146 | delay: null, 1147 | spaceSelectsMatch: false, 1148 | tabSelectsMatch: true, 1149 | editableAtwhoQueryAttrs: {}, 1150 | scrollDuration: 150, 1151 | suspendOnComposing: true, 1152 | lookUpOnClick: true 1153 | }; 1154 | 1155 | $.fn.atwho.debug = false; 1156 | 1157 | 1158 | })); 1159 | --------------------------------------------------------------------------------