diff --git a/apps/comments/.bowerrc b/apps/comments/.bowerrc
new file mode 100644
index 0000000000..faee137254
--- /dev/null
+++ b/apps/comments/.bowerrc
@@ -0,0 +1,3 @@
+{
+ "directory": "js/vendor"
+}
\ No newline at end of file
diff --git a/apps/comments/appinfo/app.php b/apps/comments/appinfo/app.php
index 067e01e47e..cfd695c516 100644
--- a/apps/comments/appinfo/app.php
+++ b/apps/comments/appinfo/app.php
@@ -27,6 +27,7 @@ $eventDispatcher->addListener(
function() {
\OCP\Util::addScript('oc-backbone-webdav');
\OCP\Util::addScript('comments', 'merged');
+ \OCP\Util::addStyle('comments', 'vendor/At.js/jquery.atwho.min');
\OCP\Util::addStyle('comments', 'comments');
}
);
diff --git a/apps/comments/css/vendor/At.js/jquery.atwho.min.css b/apps/comments/css/vendor/At.js/jquery.atwho.min.css
new file mode 100644
index 0000000000..f770dc73b3
--- /dev/null
+++ b/apps/comments/css/vendor/At.js/jquery.atwho.min.css
@@ -0,0 +1 @@
+.atwho-view{position:absolute;top:0;left:0;display:none;margin-top:18px;background:#fff;color:#000;border:1px solid #DDD;border-radius:3px;box-shadow:0 0 5px rgba(0,0,0,.1);min-width:120px;z-index:11110!important}.atwho-view .atwho-header{padding:5px;margin:5px;cursor:pointer;border-bottom:solid 1px #eaeff1;color:#6f8092;font-size:11px;font-weight:700}.atwho-view .atwho-header .small{color:#6f8092;float:right;padding-top:2px;margin-right:-5px;font-size:12px;font-weight:400}.atwho-view .atwho-header:hover{cursor:default}.atwho-view .cur{background:#36F;color:#fff}.atwho-view .cur small{color:#fff}.atwho-view strong{color:#36F}.atwho-view .cur strong{color:#fff;font:700}.atwho-view ul{list-style:none;padding:0;margin:auto;max-height:200px;overflow-y:auto}.atwho-view ul li{display:block;padding:5px 10px;border-bottom:1px solid #DDD;cursor:pointer}.atwho-view small{font-size:smaller;color:#777;font-weight:400}
\ No newline at end of file
diff --git a/apps/comments/js/merged.json b/apps/comments/js/merged.json
index 66a63e88d1..0202c7ff55 100644
--- a/apps/comments/js/merged.json
+++ b/apps/comments/js/merged.json
@@ -5,5 +5,7 @@
"commentsummarymodel.js",
"commentstabview.js",
"filesplugin.js",
- "activitytabviewplugin.js"
+ "activitytabviewplugin.js",
+ "vendor/Caret.js/dist/jquery.caret.min.js",
+ "vendor/At.js/dist/js/jquery.atwho.min.js"
]
diff --git a/apps/comments/js/vendor/At.js/.bower.json b/apps/comments/js/vendor/At.js/.bower.json
new file mode 100644
index 0000000000..d2d29c41fd
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/.bower.json
@@ -0,0 +1,43 @@
+{
+ "name": "At.js",
+ "version": "1.5.4",
+ "main": [
+ "dist/js/jquery.atwho.js",
+ "dist/css/jquery.atwho.css"
+ ],
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "components",
+ "libs",
+ "spec"
+ ],
+ "dependencies": {
+ "jquery": ">=1.7.0",
+ "Caret.js": "~0.2.2"
+ },
+ "devDependencies": {
+ "jasmine-jquery": "~2.0.2"
+ },
+ "keywords": [
+ "mention",
+ "mentions",
+ "autocomplete",
+ "autocompletion",
+ "autosuggest",
+ "autosuggestion",
+ "atjs",
+ "at.js"
+ ],
+ "homepage": "https://github.com/ichord/At.js",
+ "_release": "1.5.4",
+ "_resolution": {
+ "type": "version",
+ "tag": "v1.5.4",
+ "commit": "801c87dc804e37f134def2055b80cbc81ac98652"
+ },
+ "_source": "https://github.com/ichord/At.js.git",
+ "_target": "^1.5.4",
+ "_originalSource": "jquery.atwho",
+ "_direct": true
+}
\ No newline at end of file
diff --git a/apps/comments/js/vendor/At.js/CHANGELOG.md b/apps/comments/js/vendor/At.js/CHANGELOG.md
new file mode 100644
index 0000000000..bfc2458847
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/CHANGELOG.md
@@ -0,0 +1,314 @@
+### v1.5.0
+
+add `headerTpl` settings
+
+* 7a41d93 - #375 from vcekov/fix_scroll_position - Valentin Cekov
+* ecbf34f - #373 from vcekov/val/fix_key_navigation_interefence_with_mouse - Valentin Cekov
+* b68cf84 - #364 from WorktileTech/master - Harold.Luo
+* f836f04 - #372 from vcekov/fix_caret_for_space_after_@ - Harold.Luo
+* 06cf6bb - Properly set caret position after failed match - Valentin Cekov
+* c9ed2e2 - support header template. - htz
+
+### v1.4.0
+
+#### Contenteditable
+
+Pressing `Backspace` will turn the inserted element back to the origin query 'moment'.
+
+* 84edc9f - skip inserted element when moving left or right - ichord
+* 25a61d3 - the jQuery npm package is now called jquery. Fixes #338 - Mick Staugaard
+* 03ed71f - Merge pull request #351 from mociepka/master - Harold.Luo
+* ae00dc3 - Point main script in package json - Michał Ociepka
+* c5f31f5 - Merge branch 'dev' into HEAD - ichord
+* c399397 - fix contenteditable cursor bug when typing "a" into query - ichord
+* 7f4295a - fix previous replacements get clobbered when re-intering the inserted element - ichord
+* f00fabd - Merge pull request #354 from lvegerano/master - Harold.Luo
+* a42065e - Adds guard to event and dist file - Luis Vegerano
+* e4aaa30 - Add option to disable loopUp on click - Luis Vegerano
+* c9b7609 - Fix bug where callbacks would run before reaching minLen. Fixes #329. - Mike Leone
+* f8692dc - Add support for minLen. Connects to issue #316. - Mike Leone
+* fd7d298 - FIX: the value of `isSelecting` - ichord
+* c374c93 - FIX: IME typing error - ichord
+
+### v1.3.0
+
+* 7f2189d - fix #294 inserts "" suffix in contenteditable
+* bae95d9 - add `tabSelectsMatch` setting to make tab selection optional
+* e966aba - Merge pull request #298 from kkirsche/patch-1 - Harold.Luo
+* 9f78239 - Remove moot `version` property from bower.json - Kevin Kirsche
+
+### v1.2.0
+
+db09ac7 -> 886613f
+
+* 886613f - add `$.fn.atwho.debug = false` to trigger debug mode
+* 6567af9 - Enable default events when nothing is highlighted - Teemu
+* 752ad4a - Add scrollDuration option. - Takuru
+* bf17d43 - add parameter to allow for a spacebar in the middle of a search so that you can match a first + last name, for example - Feather Knee
+* a1d5fe7 - add `reposition` API - ichord
+* 9bcb06e - add "onInsert", "onDispaly" arguments to `tplEval` - ichord
+* db09ac7 - add `hide` api - ichord
+
+### v1.1.0
+
+* lisafeather/displyTplCallBack - #259
+* ADD: `editableAtwhoQueryAttrs` options
+* Added setting for 'spaceSelectsMatch' (default false/off)
+
+### v1.0.0
+
+**The naming convention are using camel case**.
+It means that every callback and setting's name are switched from underscope_naming to CamelNaming.
+Sorry about this.
+
+Future version's naming will follow the rules of http://semver.org constantly.
+
+#### Options:
+
+* Replaced `tpl` with `displayTpl`: display template of dropdown menu items.
+ In previous versions, At.js will fetch the value of `data-value` to insert; It stops doing it.
+ Please use the `insertTpl` option to manage the content to insert instead.
+ The default value is `"
${name}"`
+* The `insertTpl` option will be used in *textarea* as well.
+ The default value is `"${atwho-at}${name}"`
+
+#### Callbacks:
+
+* Added `afterMatchFailed` callback to *contentEditable*
+ It will be invoked after fail to match any query and stopping matching.
+ Open *examples/hashtas.html* to examine how it work.
+* Removed `inserting_wrapper` callback to *contentEditable*
+
+#### Internal changes:
+
+* refactor the `Controller`
+ Introduced `EditableController` class to control actions of `contenteditable` element.
+ Introduced `TextareaController` class to control actions of `textarea` element.
+ Both of them are inherit from the `Controller` class.
+
+* Refactored contentEditable mode
+ Inserted content are wrapped in a span: ``
+ Querying content are wrapped in a span: ``
+
+* Bring back auto-discovery to iframe.
+* Fix wrong offset in iframe
+* Replaced `iframeStandalone` with `iframeAdRoot`
+* All processed events are preventing default and stopping propagation.
+
+### v0.5.2
+
+* e1f6566 - fix error that doesn't display mention list on new line
+* 8fe3a54 - can insert multiple node from `inserting_wrapper`
+* 4080151 - scroll to top after showing
+* 01555f8 - scroll long dropdown list
+* 1b8999d - Add spm support
+* f2b8e9c - change name in package.json
+* b61bfdc - search on click
+* b1efd09 - Fixes error with selecting always first item on the list on iOS WebView when using https://github.com/ftlabs/fastclick
+* 7ed2890 - Allow accented characters in matcher
+
+### v0.5.1
+
+* 219de3d - fix Goes off screen / gets cropped if there isn't enough room
+* 1100c5b - No longer inherits text colour from document
+* ce60958 - on more boolean argument for `setIframe` api to work cross-document issues #199
+
+### v0.5.0
+
+* 593893c - refactor inserting of contenteditable
+ Adding `inserting_wrapper` for customize wrapping inserting content.
+ Not to insert item as a block in Firefox. check out issue #109.
+ Removing `getInsertedItems`, `getInsertedIDs` API. You have to collect them on your own.
+* 4d3fb8f - have to set IFRAME manually
+* 1f13a16 - change space_after to suffix
+* b099ebb - fix caret position error after inserting
+* 2c47d7a - fix #178 hide view while clicking somewhere else
+
+### v0.4.12
+
+* eeafab1 - fix error: will always call hidden atwho event
+* b0f6ceb - Highlighter finds the first occurrence
+* da256db - Adds possibility of having empty prefix (at keyword) in controllers
+* b884225 - add `space_after` option
+* 65d6273 - Passes esc/tab/return keyup events through to emitted hide event
+
+### v0.4.11
+
+* bf938db - add `delay` setting, support delay searching
+* a0b5a6f - fix bug: terminate if query out of max_len
+* 01d6d5b - add css min file
+
+### v0.4.10
+
+* update jquery dependence version
+
+### v0.4.9
+
+* f317bd7 not lowercase query, add `highlight_first` option
+
+### v0.4.8
+
+* 79bbef4 destroy atwho view container dom
+* 0372d65 update bower and component keywords
+* 52a41f5 add optional `before_repostion` callback
+* cc1c239 Fixes #143 - ichord
+
+### v0.4.7
+
+* resolved #133, #135, #137.
+* add `beforeDestroy` event
+* wouldn't concat `caret.js` into `dist/js/jquery.atwho.js` any more.
+* seperate `jquery.atwho.coffee` into pieces.
+* seperate testing.
+
+### v0.4.6
+
+* 2d9ab23 fix `wrong document` error in IE iframe
+
+### v0.4.5
+
+* 664a765 support iframe
+
+### v0.4.4
+
+* 9ac7e75 - improve contentEditable for IE 8
+
+ It's still some bugs in IE 8, just DON'T use it
+ I don't want to spend more time on IE 8.
+ So it would be the ending fixup. And i will still leave related code for
+ a while maybe in case anyone want to help to improve it.
+ Just encourge your users to upgrate the browers or just switch to a
+ batter one please !!
+
+* a8371b3 - move project page to master from gh-pages.
+* 24b6225 - fix bugs #122
+* 645e030 - update Caret.js to v0.0.5
+
+### v0.4.3
+
+* e8e7561 update `Caret.js` to `v0.0.4`
+
+### v0.4.2
+
+* 4169b74 - binding data storage to the inputor. issues #121
+* 11d053f - reduse querying twice. issues#112
+
+### v0.4.1
+
+* b7721be - fix bug at view id was not been assign. close issues #99
+* 407f069 - fix bug: Can not autofocus after click the at-list in FireFox. #95
+* 917f033 - fix bug: click do not work in div-contenteditable. close issues #93
+
+### v0.4.0
+
+* update `Caret.js` to `v0.0.2`
+* `contenteditable` support !!
+* change content of default item template `tpl`
+* new rule to insert the `at` : will always remove the `at` from inputor but will add it back from `tpl` in default.
+ so, if you are using your own `tpl` and want to show the `at` char, you have to do it yourself.
+* add `insert_tpl` setting for `contenteditable`.
+ it will insert `data-value` of li element that eval from `tpl` in default.
+* new APIs for `contenteditable`: `getInsertedItemsWithIDs`, `getInsertedItems`, `getInsertedIDs`
+
+### 2013-08-07 - v0.3.2
+
+* bower
+* remove `Caret.js` codes and add it as bower dependencies
+* remove `display_flag` settings.
+* add `start_with_space` settings, default `true`
+* change `super_call` function to `call_default`
+
+### 2013-04-28
+
+* release new api `load`, `run`
+* add `alias` setting for `load` data or as the view's id
+* matching key with a space before it
+* register key in settings `{at: "@", data: []}` instead of being a argument
+* `max_len` setting for max length to search
+* change the default matcher regrex rule: occur at start of line or after whitespace
+* will not sort the datay without valid query string
+
+### 2013-04-23
+
+* group all data handlers as `Model` class.
+* All callbacks's context would be current `Controller`
+
+### 2013-04-05
+
+* `data` setting will be used to load data either local or remote. If it's String as URL it will preload data from remote by launch a ajax request (every times At.js call `reg` to update settings)
+
+* remove default `remote_filter` from callbacks list.
+* add `get_data` and `save_data` function to contoller. They are used to get and save whole data for At.js
+* `save_data` will invoke `data_refactor` everytime
+
+* will filter local data which is set in `settings` first and if it get nothing then call `remote_filter` if it's exists in callbacks list that is set by user.
+
+### 2013-04
+
+* remove ability of changing common setting after inputor binded
+* can fix list view after matched query in IE now.
+* separated core function (get offset of inputor) as a jquery plugins.
+
+### v0.2.0 - 2012-12
+
+**No more testing in IEs browsers.**
+
+#### Note
+The name `atWho` was changed to `atwho`.
+
+#### New features
+
+* Customer data handlers(matcher, filter, sorter) and template renders(highlight, template eval) by a group of configurable callbacks.
+* Support **AMD**
+
+#### Removed features
+
+* Filter by local data and remote (by ajax) data at the same time.
+* Caching
+* Mouse event
+
+#### Changed settings
+
+`-` mean removed option
+`+` mean new added option
+The one that start without `-` or `+` mean not change.
+
+* `-` data: [],
+* `+` data: null,
+
+* `-` choose: "data-value",
+* `+` search_key: "name",
+
+* `-` callback: null,
+* `+` callbacks: DEFAULT_CALLBACKS,
+
+* `+` display_timeout: 300,
+
+* `-` tpl: _DEFAULT_TPL
+* `+` tpl: DEFAULT_TPL
+
+* `-` cache: false
+
+Not change settings
+
+* cache: true,
+* limit: 5,
+* display_flag: true,
+
+### v0.1.7
+
+同步 `jquery-atwho-rails` gem 的版本号
+这会是 `v0.1` 的固定版本. 不再有新功能更新.
+
+###v0.1.2 2012-3-23
+* box showing above instead of bottom when it get close to the bottom of window
+* coffeescript here is.
+* every registered character able to have thire own options such as template(`tpl`)
+* every inputor (textarea, input) able to have their own registered character and different behavior
+ even the same character to other inputor
+
+###v0.1.0
+* 可以監聽多個字符
+ multiple char listening.
+* 顯示缺省列表.
+ show default list.
diff --git a/apps/comments/js/vendor/At.js/CONTRIBUTING.md b/apps/comments/js/vendor/At.js/CONTRIBUTING.md
new file mode 100644
index 0000000000..629943f377
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/CONTRIBUTING.md
@@ -0,0 +1,37 @@
+## Contributing
+
+### Code style
+
+**Two** space indent
+
+### Modifying the code
+First, ensure that you have the latest [Node.js](http://nodejs.org/) and [npm](http://npmjs.org/) installed.
+
+Test that gulp is installed globally by running `grunt -v` at the command-line. If gulp isn't installed globally, run `npm install -g gulp` to install the latest version.
+
+* Fork and clone the repo.
+* Run `npm install` and `bower install` to install all dev dependencies (including grunt).
+* Modify the `*.coffee` file.
+* Run `gulp` to build this project.
+
+Assuming that you don't see any red, you're ready to go. Just be sure to run `gulp` after making any changes, to ensure that nothing is broken.
+
+### Submitting pull requests
+
+1. Create a new branch, please don't work in your `master` branch directly.
+1. Add failing tests for the change you want to make. Run `gulp` to see the tests fail.
+1. Fix stuff.
+1. Run `gulp` to see if the tests pass. Repeat steps 2-4 until done.
+1. Open `_SpecRunner.html` unit test file(s) in actual browser to ensure tests pass everywhere.
+1. Update the documentation to reflect any changes.
+1. Push to your fork and submit a pull request.
+
+### notes
+
+Please don't edit files in the `dist` subdirectory and *.js files in `src` as they are generated via gulp.
+You'll find source code in the `src` subdirectory!
+use `bower install` or `component install` to install dependencies first.
+
+
+### PhantomJS
+While gulp can run the included unit tests via [PhantomJS](http://phantomjs.org/), this shouldn't be considered a substitute for the real thing. Please be sure to test the `_SpecRunner.html` unit test file(s) in _actual_ browsers.
diff --git a/apps/comments/js/vendor/At.js/LICENSE-MIT b/apps/comments/js/vendor/At.js/LICENSE-MIT
new file mode 100644
index 0000000000..36cd1c1227
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/LICENSE-MIT
@@ -0,0 +1,22 @@
+Copyright (c) 2013 chord.luo@gmail.com
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/apps/comments/js/vendor/At.js/README.md b/apps/comments/js/vendor/At.js/README.md
new file mode 100644
index 0000000000..551820afbb
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/README.md
@@ -0,0 +1,67 @@
+**An autocompletion library to autocomplete mentions, smileys etc. just like on Github!**
+[![Build Status](https://travis-ci.org/ichord/At.js.png)](https://travis-ci.org/ichord/At.js)
+
+#### Notice
+
+At.js now **depends on** [Caret.js](https://github.com/ichord/Caret.js).
+Please read [**CHANGELOG.md**](CHANGELOG.md) for more details if you are going to update to new version.
+
+### Demo
+http://ichord.github.com/At.js
+
+### Documentation
+https://github.com/ichord/At.js/wiki
+
+### Compatibility
+
+* `textarea` - Chrome, Safari, Firefox, IE7+ (maybe IE6)
+* `contentEditable` - Chrome, Safari, Firefox, IE9+
+
+### Features Preview
+
+* Support IE 7+ for **textarea**.
+* Supports HTML5 [**contentEditable**](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_Editable) elements (NOT including IE 8)
+* Can listen to any character and not just '@'. Can set up multiple listeners for different characters with different behavior and data
+* Listener events can be bound to multiple inputors.
+* Format returned data using templates
+* Keyboard controls in addition to mouse
+ - `Tab` or `Enter` keys select the value
+ - `Up` and `Down` navigate between values (and `Ctrl-P` and `Ctrl-N` also)
+ - `Right` and `left` will re-search the keyword.
+* Custom data handlers and template renderers using a group of configurable callbacks
+* Supports AMD
+
+### Requirements
+
+* jQuery >= 1.7.0.
+* [Caret.js](https://github.com/ichord/Caret.js)
+ (You can use `Component` or `Bower` to install it.)
+
+### Integrating with your Application
+
+Simply include the following files in your HTML and you are good to go.
+
+```html
+
+
+
+
+```
+
+```javascript
+$('#inputor').atwho({
+ at: "@",
+ data:['Peter', 'Tom', 'Anne']
+})
+```
+
+#### Bower & Component
+For installing using Bower you can use `jquery.atwho` and for Component please use `ichord/At.js`.
+
+#### Rails
+You can include At.js in your `Rails` application using the gem [jquery-atwho-rails](https://github.com/ichord/jquery-atwho-rails).
+
+### Core Team Members
+
+* [@ichord](https://twitter.com/_ichord) (twitter)
+
diff --git a/apps/comments/js/vendor/At.js/bower.json b/apps/comments/js/vendor/At.js/bower.json
new file mode 100644
index 0000000000..57cc844b86
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/bower.json
@@ -0,0 +1,32 @@
+{
+ "name": "At.js",
+ "version": "1.5.4",
+ "main": [
+ "dist/js/jquery.atwho.js",
+ "dist/css/jquery.atwho.css"
+ ],
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "components",
+ "libs",
+ "spec"
+ ],
+ "dependencies": {
+ "jquery": ">=1.7.0",
+ "Caret.js": "~0.2.2"
+ },
+ "devDependencies": {
+ "jasmine-jquery": "~2.0.2"
+ },
+ "keywords": [
+ "mention",
+ "mentions",
+ "autocomplete",
+ "autocompletion",
+ "autosuggest",
+ "autosuggestion",
+ "atjs",
+ "at.js"
+ ]
+}
diff --git a/apps/comments/js/vendor/At.js/component.json b/apps/comments/js/vendor/At.js/component.json
new file mode 100644
index 0000000000..80951e9beb
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/component.json
@@ -0,0 +1,32 @@
+{
+ "name": "At.js",
+ "repo": "ichord/At.js",
+ "description": "Add Github like mentions autocomplete to your application.",
+ "version": "1.5.4",
+ "demo": "http://ichord.github.com/At.js",
+ "dependencies": {
+ "ichord/Caret.js": "~0.2.2",
+ "component/jquery": ">= 1.7.0"
+ },
+ "main": [
+ "dist/js/jquery.atwho.js"
+ ],
+ "scripts": [
+ "dist/js/jquery.atwho.js"
+ ],
+ "styles": [
+ "dist/css/jquery.atwho.css"
+ ],
+ "license": "MIT",
+ "keywords": [
+ "mentions",
+ "ui",
+ "mentions",
+ "autocomplete",
+ "autocompletion",
+ "autosuggest",
+ "autosuggestion",
+ "atjs",
+ "at.js"
+ ]
+}
\ No newline at end of file
diff --git a/apps/comments/js/vendor/At.js/dist/css/jquery.atwho.css b/apps/comments/js/vendor/At.js/dist/css/jquery.atwho.css
new file mode 100644
index 0000000000..dad94ed964
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/dist/css/jquery.atwho.css
@@ -0,0 +1,72 @@
+.atwho-view {
+ position:absolute;
+ top: 0;
+ left: 0;
+ display: none;
+ margin-top: 18px;
+ background: white;
+ color: black;
+ border: 1px solid #DDD;
+ border-radius: 3px;
+ box-shadow: 0 0 5px rgba(0,0,0,0.1);
+ min-width: 120px;
+ z-index: 11110 !important;
+}
+
+.atwho-view .atwho-header {
+ padding: 5px;
+ margin: 5px;
+ cursor: pointer;
+ border-bottom: solid 1px #eaeff1;
+ color: #6f8092;
+ font-size: 11px;
+ font-weight: bold;
+}
+
+.atwho-view .atwho-header .small {
+ color: #6f8092;
+ float: right;
+ padding-top: 2px;
+ margin-right: -5px;
+ font-size: 12px;
+ font-weight: normal;
+}
+
+.atwho-view .atwho-header:hover {
+ cursor: default;
+}
+
+.atwho-view .cur {
+ background: #3366FF;
+ color: white;
+}
+.atwho-view .cur small {
+ color: white;
+}
+.atwho-view strong {
+ color: #3366FF;
+}
+.atwho-view .cur strong {
+ color: white;
+ font:bold;
+}
+.atwho-view ul {
+ /* width: 100px; */
+ list-style:none;
+ padding:0;
+ margin:auto;
+ max-height: 200px;
+ overflow-y: auto;
+}
+.atwho-view ul li {
+ display: block;
+ padding: 5px 10px;
+ border-bottom: 1px solid #DDD;
+ cursor: pointer;
+ /* border-top: 1px solid #C8C8C8; */
+}
+.atwho-view small {
+ font-size: smaller;
+ color: #777;
+ font-weight: normal;
+}
diff --git a/apps/comments/js/vendor/At.js/dist/css/jquery.atwho.min.css b/apps/comments/js/vendor/At.js/dist/css/jquery.atwho.min.css
new file mode 100644
index 0000000000..f770dc73b3
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/dist/css/jquery.atwho.min.css
@@ -0,0 +1 @@
+.atwho-view{position:absolute;top:0;left:0;display:none;margin-top:18px;background:#fff;color:#000;border:1px solid #DDD;border-radius:3px;box-shadow:0 0 5px rgba(0,0,0,.1);min-width:120px;z-index:11110!important}.atwho-view .atwho-header{padding:5px;margin:5px;cursor:pointer;border-bottom:solid 1px #eaeff1;color:#6f8092;font-size:11px;font-weight:700}.atwho-view .atwho-header .small{color:#6f8092;float:right;padding-top:2px;margin-right:-5px;font-size:12px;font-weight:400}.atwho-view .atwho-header:hover{cursor:default}.atwho-view .cur{background:#36F;color:#fff}.atwho-view .cur small{color:#fff}.atwho-view strong{color:#36F}.atwho-view .cur strong{color:#fff;font:700}.atwho-view ul{list-style:none;padding:0;margin:auto;max-height:200px;overflow-y:auto}.atwho-view ul li{display:block;padding:5px 10px;border-bottom:1px solid #DDD;cursor:pointer}.atwho-view small{font-size:smaller;color:#777;font-weight:400}
\ No newline at end of file
diff --git a/apps/comments/js/vendor/At.js/dist/js/jquery.atwho.js b/apps/comments/js/vendor/At.js/dist/js/jquery.atwho.js
new file mode 100644
index 0000000000..795b6c67dc
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/dist/js/jquery.atwho.js
@@ -0,0 +1,1212 @@
+/**
+ * at.js - 1.5.4
+ * Copyright (c) 2017 chord.luo ;
+ * Homepage: http://ichord.github.com/At.js
+ * License: MIT
+ */
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module unless amdModuleId is set
+ define(["jquery"], function (a0) {
+ return (factory(a0));
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+}(this, function ($) {
+var DEFAULT_CALLBACKS, KEY_CODE;
+
+KEY_CODE = {
+ ESC: 27,
+ TAB: 9,
+ ENTER: 13,
+ CTRL: 17,
+ A: 65,
+ P: 80,
+ N: 78,
+ LEFT: 37,
+ UP: 38,
+ RIGHT: 39,
+ DOWN: 40,
+ BACKSPACE: 8,
+ SPACE: 32
+};
+
+DEFAULT_CALLBACKS = {
+ beforeSave: function(data) {
+ return Controller.arrayToDefaultHash(data);
+ },
+ matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
+ var _a, _y, match, regexp, space;
+ flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ if (should_startWithSpace) {
+ flag = '(?:^|\\s)' + flag;
+ }
+ _a = decodeURI("%C3%80");
+ _y = decodeURI("%C3%BF");
+ space = acceptSpaceBar ? "\ " : "";
+ regexp = new RegExp(flag + "([A-Za-z" + _a + "-" + _y + "0-9_" + space + "\'\.\+\-]*)$|" + flag + "([^\\x00-\\xff]*)$", 'gi');
+ match = regexp.exec(subtext);
+ if (match) {
+ return match[2] || match[1];
+ } else {
+ return null;
+ }
+ },
+ filter: function(query, data, searchKey) {
+ var _results, i, item, len;
+ _results = [];
+ for (i = 0, len = data.length; i < len; i++) {
+ item = data[i];
+ if (~new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase())) {
+ _results.push(item);
+ }
+ }
+ return _results;
+ },
+ remoteFilter: null,
+ sorter: function(query, items, searchKey) {
+ var _results, i, item, len;
+ if (!query) {
+ return items;
+ }
+ _results = [];
+ for (i = 0, len = items.length; i < len; i++) {
+ item = items[i];
+ item.atwho_order = new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase());
+ if (item.atwho_order > -1) {
+ _results.push(item);
+ }
+ }
+ return _results.sort(function(a, b) {
+ return a.atwho_order - b.atwho_order;
+ });
+ },
+ tplEval: function(tpl, map) {
+ var error, error1, template;
+ template = tpl;
+ try {
+ if (typeof tpl !== 'string') {
+ template = tpl(map);
+ }
+ return template.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) {
+ return map[key];
+ });
+ } catch (error1) {
+ error = error1;
+ return "";
+ }
+ },
+ highlighter: function(li, query) {
+ var regexp;
+ if (!query) {
+ return li;
+ }
+ regexp = new RegExp(">\\s*([^\<]*?)(" + query.replace("+", "\\+") + ")([^\<]*)\\s*<", 'ig');
+ return li.replace(regexp, function(str, $1, $2, $3) {
+ return '> ' + $1 + '' + $2 + '' + $3 + ' <';
+ });
+ },
+ beforeInsert: function(value, $li, e) {
+ return value;
+ },
+ beforeReposition: function(offset) {
+ return offset;
+ },
+ afterMatchFailed: function(at, el) {}
+};
+
+var App;
+
+App = (function() {
+ function App(inputor) {
+ this.currentFlag = null;
+ this.controllers = {};
+ this.aliasMaps = {};
+ this.$inputor = $(inputor);
+ this.setupRootElement();
+ this.listen();
+ }
+
+ App.prototype.createContainer = function(doc) {
+ var ref;
+ if ((ref = this.$el) != null) {
+ ref.remove();
+ }
+ return $(doc.body).append(this.$el = $(""));
+ };
+
+ App.prototype.setupRootElement = function(iframe, asRoot) {
+ var error, error1;
+ if (asRoot == null) {
+ asRoot = false;
+ }
+ if (iframe) {
+ this.window = iframe.contentWindow;
+ this.document = iframe.contentDocument || this.window.document;
+ this.iframe = iframe;
+ } else {
+ this.document = this.$inputor[0].ownerDocument;
+ this.window = this.document.defaultView || this.document.parentWindow;
+ try {
+ this.iframe = this.window.frameElement;
+ } catch (error1) {
+ error = error1;
+ this.iframe = null;
+ if ($.fn.atwho.debug) {
+ throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n" + error);
+ }
+ }
+ }
+ return this.createContainer((this.iframeAsRoot = asRoot) ? this.document : document);
+ };
+
+ App.prototype.controller = function(at) {
+ var c, current, currentFlag, ref;
+ if (this.aliasMaps[at]) {
+ current = this.controllers[this.aliasMaps[at]];
+ } else {
+ ref = this.controllers;
+ for (currentFlag in ref) {
+ c = ref[currentFlag];
+ if (currentFlag === at) {
+ current = c;
+ break;
+ }
+ }
+ }
+ if (current) {
+ return current;
+ } else {
+ return this.controllers[this.currentFlag];
+ }
+ };
+
+ App.prototype.setContextFor = function(at) {
+ this.currentFlag = at;
+ return this;
+ };
+
+ App.prototype.reg = function(flag, setting) {
+ var base, controller;
+ controller = (base = this.controllers)[flag] || (base[flag] = this.$inputor.is('[contentEditable]') ? new EditableController(this, flag) : new TextareaController(this, flag));
+ if (setting.alias) {
+ this.aliasMaps[setting.alias] = flag;
+ }
+ controller.init(setting);
+ return this;
+ };
+
+ App.prototype.listen = function() {
+ return this.$inputor.on('compositionstart', (function(_this) {
+ return function(e) {
+ var ref;
+ if ((ref = _this.controller()) != null) {
+ ref.view.hide();
+ }
+ _this.isComposing = true;
+ return null;
+ };
+ })(this)).on('compositionend', (function(_this) {
+ return function(e) {
+ _this.isComposing = false;
+ setTimeout(function(e) {
+ return _this.dispatch(e);
+ });
+ return null;
+ };
+ })(this)).on('keyup.atwhoInner', (function(_this) {
+ return function(e) {
+ return _this.onKeyup(e);
+ };
+ })(this)).on('keydown.atwhoInner', (function(_this) {
+ return function(e) {
+ return _this.onKeydown(e);
+ };
+ })(this)).on('blur.atwhoInner', (function(_this) {
+ return function(e) {
+ var c;
+ if (c = _this.controller()) {
+ c.expectedQueryCBId = null;
+ return c.view.hide(e, c.getOpt("displayTimeout"));
+ }
+ };
+ })(this)).on('click.atwhoInner', (function(_this) {
+ return function(e) {
+ return _this.dispatch(e);
+ };
+ })(this)).on('scroll.atwhoInner', (function(_this) {
+ return function() {
+ var lastScrollTop;
+ lastScrollTop = _this.$inputor.scrollTop();
+ return function(e) {
+ var currentScrollTop, ref;
+ currentScrollTop = e.target.scrollTop;
+ if (lastScrollTop !== currentScrollTop) {
+ if ((ref = _this.controller()) != null) {
+ ref.view.hide(e);
+ }
+ }
+ lastScrollTop = currentScrollTop;
+ return true;
+ };
+ };
+ })(this)());
+ };
+
+ App.prototype.shutdown = function() {
+ var _, c, ref;
+ ref = this.controllers;
+ for (_ in ref) {
+ c = ref[_];
+ c.destroy();
+ delete this.controllers[_];
+ }
+ this.$inputor.off('.atwhoInner');
+ return this.$el.remove();
+ };
+
+ App.prototype.dispatch = function(e) {
+ var _, c, ref, results;
+ ref = this.controllers;
+ results = [];
+ for (_ in ref) {
+ c = ref[_];
+ results.push(c.lookUp(e));
+ }
+ return results;
+ };
+
+ App.prototype.onKeyup = function(e) {
+ var ref;
+ switch (e.keyCode) {
+ case KEY_CODE.ESC:
+ e.preventDefault();
+ if ((ref = this.controller()) != null) {
+ ref.view.hide();
+ }
+ break;
+ case KEY_CODE.DOWN:
+ case KEY_CODE.UP:
+ case KEY_CODE.CTRL:
+ case KEY_CODE.ENTER:
+ $.noop();
+ break;
+ case KEY_CODE.P:
+ case KEY_CODE.N:
+ if (!e.ctrlKey) {
+ this.dispatch(e);
+ }
+ break;
+ default:
+ this.dispatch(e);
+ }
+ };
+
+ App.prototype.onKeydown = function(e) {
+ var ref, view;
+ view = (ref = this.controller()) != null ? ref.view : void 0;
+ if (!(view && view.visible())) {
+ return;
+ }
+ switch (e.keyCode) {
+ case KEY_CODE.ESC:
+ e.preventDefault();
+ view.hide(e);
+ break;
+ case KEY_CODE.UP:
+ e.preventDefault();
+ view.prev();
+ break;
+ case KEY_CODE.DOWN:
+ e.preventDefault();
+ view.next();
+ break;
+ case KEY_CODE.P:
+ if (!e.ctrlKey) {
+ return;
+ }
+ e.preventDefault();
+ view.prev();
+ break;
+ case KEY_CODE.N:
+ if (!e.ctrlKey) {
+ return;
+ }
+ e.preventDefault();
+ view.next();
+ break;
+ case KEY_CODE.TAB:
+ case KEY_CODE.ENTER:
+ case KEY_CODE.SPACE:
+ if (!view.visible()) {
+ return;
+ }
+ if (!this.controller().getOpt('spaceSelectsMatch') && e.keyCode === KEY_CODE.SPACE) {
+ return;
+ }
+ if (!this.controller().getOpt('tabSelectsMatch') && e.keyCode === KEY_CODE.TAB) {
+ return;
+ }
+ if (view.highlighted()) {
+ e.preventDefault();
+ view.choose(e);
+ } else {
+ view.hide(e);
+ }
+ break;
+ default:
+ $.noop();
+ }
+ };
+
+ return App;
+
+})();
+
+var Controller,
+ slice = [].slice;
+
+Controller = (function() {
+ Controller.prototype.uid = function() {
+ return (Math.random().toString(16) + "000000000").substr(2, 8) + (new Date().getTime());
+ };
+
+ function Controller(app, at1) {
+ this.app = app;
+ this.at = at1;
+ this.$inputor = this.app.$inputor;
+ this.id = this.$inputor[0].id || this.uid();
+ this.expectedQueryCBId = null;
+ this.setting = null;
+ this.query = null;
+ this.pos = 0;
+ this.range = null;
+ if ((this.$el = $("#atwho-ground-" + this.id, this.app.$el)).length === 0) {
+ this.app.$el.append(this.$el = $(""));
+ }
+ this.model = new Model(this);
+ this.view = new View(this);
+ }
+
+ Controller.prototype.init = function(setting) {
+ this.setting = $.extend({}, this.setting || $.fn.atwho["default"], setting);
+ this.view.init();
+ return this.model.reload(this.setting.data);
+ };
+
+ Controller.prototype.destroy = function() {
+ this.trigger('beforeDestroy');
+ this.model.destroy();
+ this.view.destroy();
+ return this.$el.remove();
+ };
+
+ Controller.prototype.callDefault = function() {
+ var args, error, error1, funcName;
+ funcName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
+ try {
+ return DEFAULT_CALLBACKS[funcName].apply(this, args);
+ } catch (error1) {
+ error = error1;
+ return $.error(error + " Or maybe At.js doesn't have function " + funcName);
+ }
+ };
+
+ Controller.prototype.trigger = function(name, data) {
+ var alias, eventName;
+ if (data == null) {
+ data = [];
+ }
+ data.push(this);
+ alias = this.getOpt('alias');
+ eventName = alias ? name + "-" + alias + ".atwho" : name + ".atwho";
+ return this.$inputor.trigger(eventName, data);
+ };
+
+ Controller.prototype.callbacks = function(funcName) {
+ return this.getOpt("callbacks")[funcName] || DEFAULT_CALLBACKS[funcName];
+ };
+
+ Controller.prototype.getOpt = function(at, default_value) {
+ var e, error1;
+ try {
+ return this.setting[at];
+ } catch (error1) {
+ e = error1;
+ return null;
+ }
+ };
+
+ Controller.prototype.insertContentFor = function($li) {
+ var data, tpl;
+ tpl = this.getOpt('insertTpl');
+ data = $.extend({}, $li.data('item-data'), {
+ 'atwho-at': this.at
+ });
+ return this.callbacks("tplEval").call(this, tpl, data, "onInsert");
+ };
+
+ Controller.prototype.renderView = function(data) {
+ var searchKey;
+ searchKey = this.getOpt("searchKey");
+ data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), searchKey);
+ return this.view.render(data.slice(0, this.getOpt('limit')));
+ };
+
+ Controller.arrayToDefaultHash = function(data) {
+ var i, item, len, results;
+ if (!$.isArray(data)) {
+ return data;
+ }
+ results = [];
+ for (i = 0, len = data.length; i < len; i++) {
+ item = data[i];
+ if ($.isPlainObject(item)) {
+ results.push(item);
+ } else {
+ results.push({
+ name: item
+ });
+ }
+ }
+ return results;
+ };
+
+ Controller.prototype.lookUp = function(e) {
+ var query, wait;
+ if (e && e.type === 'click' && !this.getOpt('lookUpOnClick')) {
+ return;
+ }
+ if (this.getOpt('suspendOnComposing') && this.app.isComposing) {
+ return;
+ }
+ query = this.catchQuery(e);
+ if (!query) {
+ this.expectedQueryCBId = null;
+ return query;
+ }
+ this.app.setContextFor(this.at);
+ if (wait = this.getOpt('delay')) {
+ this._delayLookUp(query, wait);
+ } else {
+ this._lookUp(query);
+ }
+ return query;
+ };
+
+ Controller.prototype._delayLookUp = function(query, wait) {
+ var now, remaining;
+ now = Date.now ? Date.now() : new Date().getTime();
+ this.previousCallTime || (this.previousCallTime = now);
+ remaining = wait - (now - this.previousCallTime);
+ if ((0 < remaining && remaining < wait)) {
+ this.previousCallTime = now;
+ this._stopDelayedCall();
+ return this.delayedCallTimeout = setTimeout((function(_this) {
+ return function() {
+ _this.previousCallTime = 0;
+ _this.delayedCallTimeout = null;
+ return _this._lookUp(query);
+ };
+ })(this), wait);
+ } else {
+ this._stopDelayedCall();
+ if (this.previousCallTime !== now) {
+ this.previousCallTime = 0;
+ }
+ return this._lookUp(query);
+ }
+ };
+
+ Controller.prototype._stopDelayedCall = function() {
+ if (this.delayedCallTimeout) {
+ clearTimeout(this.delayedCallTimeout);
+ return this.delayedCallTimeout = null;
+ }
+ };
+
+ Controller.prototype._generateQueryCBId = function() {
+ return {};
+ };
+
+ Controller.prototype._lookUp = function(query) {
+ var _callback;
+ _callback = function(queryCBId, data) {
+ if (queryCBId !== this.expectedQueryCBId) {
+ return;
+ }
+ if (data && data.length > 0) {
+ return this.renderView(this.constructor.arrayToDefaultHash(data));
+ } else {
+ return this.view.hide();
+ }
+ };
+ this.expectedQueryCBId = this._generateQueryCBId();
+ return this.model.query(query.text, $.proxy(_callback, this, this.expectedQueryCBId));
+ };
+
+ return Controller;
+
+})();
+
+var TextareaController,
+ 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; },
+ hasProp = {}.hasOwnProperty;
+
+TextareaController = (function(superClass) {
+ extend(TextareaController, superClass);
+
+ function TextareaController() {
+ return TextareaController.__super__.constructor.apply(this, arguments);
+ }
+
+ TextareaController.prototype.catchQuery = function() {
+ var caretPos, content, end, isString, query, start, subtext;
+ content = this.$inputor.val();
+ caretPos = this.$inputor.caret('pos', {
+ iframe: this.app.iframe
+ });
+ subtext = content.slice(0, caretPos);
+ query = this.callbacks("matcher").call(this, this.at, subtext, this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar"));
+ isString = typeof query === 'string';
+ if (isString && query.length < this.getOpt('minLen', 0)) {
+ return;
+ }
+ if (isString && query.length <= this.getOpt('maxLen', 20)) {
+ start = caretPos - query.length;
+ end = start + query.length;
+ this.pos = start;
+ query = {
+ 'text': query,
+ 'headPos': start,
+ 'endPos': end
+ };
+ this.trigger("matched", [this.at, query.text]);
+ } else {
+ query = null;
+ this.view.hide();
+ }
+ return this.query = query;
+ };
+
+ TextareaController.prototype.rect = function() {
+ var c, iframeOffset, scaleBottom;
+ if (!(c = this.$inputor.caret('offset', this.pos - 1, {
+ iframe: this.app.iframe
+ }))) {
+ return;
+ }
+ if (this.app.iframe && !this.app.iframeAsRoot) {
+ iframeOffset = $(this.app.iframe).offset();
+ c.left += iframeOffset.left;
+ c.top += iframeOffset.top;
+ }
+ scaleBottom = this.app.document.selection ? 0 : 2;
+ return {
+ left: c.left,
+ top: c.top,
+ bottom: c.top + c.height + scaleBottom
+ };
+ };
+
+ TextareaController.prototype.insert = function(content, $li) {
+ var $inputor, source, startStr, suffix, text;
+ $inputor = this.$inputor;
+ source = $inputor.val();
+ startStr = source.slice(0, Math.max(this.query.headPos - this.at.length, 0));
+ suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || " ";
+ content += suffix;
+ text = "" + startStr + content + (source.slice(this.query['endPos'] || 0));
+ $inputor.val(text);
+ $inputor.caret('pos', startStr.length + content.length, {
+ iframe: this.app.iframe
+ });
+ if (!$inputor.is(':focus')) {
+ $inputor.focus();
+ }
+ return $inputor.change();
+ };
+
+ return TextareaController;
+
+})(Controller);
+
+var EditableController,
+ 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; },
+ hasProp = {}.hasOwnProperty;
+
+EditableController = (function(superClass) {
+ extend(EditableController, superClass);
+
+ function EditableController() {
+ return EditableController.__super__.constructor.apply(this, arguments);
+ }
+
+ EditableController.prototype._getRange = function() {
+ var sel;
+ sel = this.app.window.getSelection();
+ if (sel.rangeCount > 0) {
+ return sel.getRangeAt(0);
+ }
+ };
+
+ EditableController.prototype._setRange = function(position, node, range) {
+ if (range == null) {
+ range = this._getRange();
+ }
+ if (!(range && node)) {
+ return;
+ }
+ node = $(node)[0];
+ if (position === 'after') {
+ range.setEndAfter(node);
+ range.setStartAfter(node);
+ } else {
+ range.setEndBefore(node);
+ range.setStartBefore(node);
+ }
+ range.collapse(false);
+ return this._clearRange(range);
+ };
+
+ EditableController.prototype._clearRange = function(range) {
+ var sel;
+ if (range == null) {
+ range = this._getRange();
+ }
+ sel = this.app.window.getSelection();
+ if (this.ctrl_a_pressed == null) {
+ sel.removeAllRanges();
+ return sel.addRange(range);
+ }
+ };
+
+ EditableController.prototype._movingEvent = function(e) {
+ var ref;
+ return e.type === 'click' || ((ref = e.which) === KEY_CODE.RIGHT || ref === KEY_CODE.LEFT || ref === KEY_CODE.UP || ref === KEY_CODE.DOWN);
+ };
+
+ EditableController.prototype._unwrap = function(node) {
+ var next;
+ node = $(node).unwrap().get(0);
+ if ((next = node.nextSibling) && next.nodeValue) {
+ node.nodeValue += next.nodeValue;
+ $(next).remove();
+ }
+ return node;
+ };
+
+ EditableController.prototype.catchQuery = function(e) {
+ var $inserted, $query, _range, index, inserted, isString, lastNode, matched, offset, query, query_content, range;
+ if (!(range = this._getRange())) {
+ return;
+ }
+ if (!range.collapsed) {
+ return;
+ }
+ if (e.which === KEY_CODE.ENTER) {
+ ($query = $(range.startContainer).closest('.atwho-query')).contents().unwrap();
+ if ($query.is(':empty')) {
+ $query.remove();
+ }
+ ($query = $(".atwho-query", this.app.document)).text($query.text()).contents().last().unwrap();
+ this._clearRange();
+ return;
+ }
+ if (/firefox/i.test(navigator.userAgent)) {
+ if ($(range.startContainer).is(this.$inputor)) {
+ this._clearRange();
+ return;
+ }
+ if (e.which === KEY_CODE.BACKSPACE && range.startContainer.nodeType === document.ELEMENT_NODE && (offset = range.startOffset - 1) >= 0) {
+ _range = range.cloneRange();
+ _range.setStart(range.startContainer, offset);
+ if ($(_range.cloneContents()).contents().last().is('.atwho-inserted')) {
+ inserted = $(range.startContainer).contents().get(offset);
+ this._setRange('after', $(inserted).contents().last());
+ }
+ } else if (e.which === KEY_CODE.LEFT && range.startContainer.nodeType === document.TEXT_NODE) {
+ $inserted = $(range.startContainer.previousSibling);
+ if ($inserted.is('.atwho-inserted') && range.startOffset === 0) {
+ this._setRange('after', $inserted.contents().last());
+ }
+ }
+ }
+ $(range.startContainer).closest('.atwho-inserted').addClass('atwho-query').siblings().removeClass('atwho-query');
+ if (($query = $(".atwho-query", this.app.document)).length > 0 && $query.is(':empty') && $query.text().length === 0) {
+ $query.remove();
+ }
+ if (!this._movingEvent(e)) {
+ $query.removeClass('atwho-inserted');
+ }
+ if ($query.length > 0) {
+ switch (e.which) {
+ case KEY_CODE.LEFT:
+ this._setRange('before', $query.get(0), range);
+ $query.removeClass('atwho-query');
+ return;
+ case KEY_CODE.RIGHT:
+ this._setRange('after', $query.get(0).nextSibling, range);
+ $query.removeClass('atwho-query');
+ return;
+ }
+ }
+ if ($query.length > 0 && (query_content = $query.attr('data-atwho-at-query'))) {
+ $query.empty().html(query_content).attr('data-atwho-at-query', null);
+ this._setRange('after', $query.get(0), range);
+ }
+ _range = range.cloneRange();
+ _range.setStart(range.startContainer, 0);
+ matched = this.callbacks("matcher").call(this, this.at, _range.toString(), this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar"));
+ isString = typeof matched === 'string';
+ if ($query.length === 0 && isString && (index = range.startOffset - this.at.length - matched.length) >= 0) {
+ range.setStart(range.startContainer, index);
+ $query = $('', this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass('atwho-query');
+ range.surroundContents($query.get(0));
+ lastNode = $query.contents().last().get(0);
+ if (lastNode) {
+ if (/firefox/i.test(navigator.userAgent)) {
+ range.setStart(lastNode, lastNode.length);
+ range.setEnd(lastNode, lastNode.length);
+ this._clearRange(range);
+ } else {
+ this._setRange('after', lastNode, range);
+ }
+ }
+ }
+ if (isString && matched.length < this.getOpt('minLen', 0)) {
+ return;
+ }
+ if (isString && matched.length <= this.getOpt('maxLen', 20)) {
+ query = {
+ text: matched,
+ el: $query
+ };
+ this.trigger("matched", [this.at, query.text]);
+ return this.query = query;
+ } else {
+ this.view.hide();
+ this.query = {
+ el: $query
+ };
+ if ($query.text().indexOf(this.at) >= 0) {
+ if (this._movingEvent(e) && $query.hasClass('atwho-inserted')) {
+ $query.removeClass('atwho-query');
+ } else if (false !== this.callbacks('afterMatchFailed').call(this, this.at, $query)) {
+ this._setRange("after", this._unwrap($query.text($query.text()).contents().first()));
+ }
+ }
+ return null;
+ }
+ };
+
+ EditableController.prototype.rect = function() {
+ var $iframe, iframeOffset, rect;
+ rect = this.query.el.offset();
+ if (!(rect && this.query.el[0].getClientRects().length)) {
+ return;
+ }
+ if (this.app.iframe && !this.app.iframeAsRoot) {
+ iframeOffset = ($iframe = $(this.app.iframe)).offset();
+ rect.left += iframeOffset.left - this.$inputor.scrollLeft();
+ rect.top += iframeOffset.top - this.$inputor.scrollTop();
+ }
+ rect.bottom = rect.top + this.query.el.height();
+ return rect;
+ };
+
+ EditableController.prototype.insert = function(content, $li) {
+ var data, overrides, range, suffix, suffixNode;
+ if (!this.$inputor.is(':focus')) {
+ this.$inputor.focus();
+ }
+ overrides = this.getOpt('functionOverrides');
+ if (overrides.insert) {
+ return overrides.insert.call(this, content, $li);
+ }
+ suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || "\u00A0";
+ data = $li.data('item-data');
+ this.query.el.removeClass('atwho-query').addClass('atwho-inserted').html(content).attr('data-atwho-at-query', "" + data['atwho-at'] + this.query.text).attr('contenteditable', "false");
+ if (range = this._getRange()) {
+ if (this.query.el.length) {
+ range.setEndAfter(this.query.el[0]);
+ }
+ range.collapse(false);
+ range.insertNode(suffixNode = this.app.document.createTextNode("" + suffix));
+ this._setRange('after', suffixNode, range);
+ }
+ if (!this.$inputor.is(':focus')) {
+ this.$inputor.focus();
+ }
+ return this.$inputor.change();
+ };
+
+ return EditableController;
+
+})(Controller);
+
+var Model;
+
+Model = (function() {
+ function Model(context) {
+ this.context = context;
+ this.at = this.context.at;
+ this.storage = this.context.$inputor;
+ }
+
+ Model.prototype.destroy = function() {
+ return this.storage.data(this.at, null);
+ };
+
+ Model.prototype.saved = function() {
+ return this.fetch() > 0;
+ };
+
+ Model.prototype.query = function(query, callback) {
+ var _remoteFilter, data, searchKey;
+ data = this.fetch();
+ searchKey = this.context.getOpt("searchKey");
+ data = this.context.callbacks('filter').call(this.context, query, data, searchKey) || [];
+ _remoteFilter = this.context.callbacks('remoteFilter');
+ if (data.length > 0 || (!_remoteFilter && data.length === 0)) {
+ return callback(data);
+ } else {
+ return _remoteFilter.call(this.context, query, callback);
+ }
+ };
+
+ Model.prototype.fetch = function() {
+ return this.storage.data(this.at) || [];
+ };
+
+ Model.prototype.save = function(data) {
+ return this.storage.data(this.at, this.context.callbacks("beforeSave").call(this.context, data || []));
+ };
+
+ Model.prototype.load = function(data) {
+ if (!(this.saved() || !data)) {
+ return this._load(data);
+ }
+ };
+
+ Model.prototype.reload = function(data) {
+ return this._load(data);
+ };
+
+ Model.prototype._load = function(data) {
+ if (typeof data === "string") {
+ return $.ajax(data, {
+ dataType: "json"
+ }).done((function(_this) {
+ return function(data) {
+ return _this.save(data);
+ };
+ })(this));
+ } else {
+ return this.save(data);
+ }
+ };
+
+ return Model;
+
+})();
+
+var View;
+
+View = (function() {
+ function View(context) {
+ this.context = context;
+ this.$el = $("");
+ this.$elUl = this.$el.children();
+ this.timeoutID = null;
+ this.context.$el.append(this.$el);
+ this.bindEvent();
+ }
+
+ View.prototype.init = function() {
+ var header_tpl, id;
+ id = this.context.getOpt("alias") || this.context.at.charCodeAt(0);
+ header_tpl = this.context.getOpt("headerTpl");
+ if (header_tpl && this.$el.children().length === 1) {
+ this.$el.prepend(header_tpl);
+ }
+ return this.$el.attr({
+ 'id': "at-view-" + id
+ });
+ };
+
+ View.prototype.destroy = function() {
+ return this.$el.remove();
+ };
+
+ View.prototype.bindEvent = function() {
+ var $menu, lastCoordX, lastCoordY;
+ $menu = this.$el.find('ul');
+ lastCoordX = 0;
+ lastCoordY = 0;
+ return $menu.on('mousemove.atwho-view', 'li', (function(_this) {
+ return function(e) {
+ var $cur;
+ if (lastCoordX === e.clientX && lastCoordY === e.clientY) {
+ return;
+ }
+ lastCoordX = e.clientX;
+ lastCoordY = e.clientY;
+ $cur = $(e.currentTarget);
+ if ($cur.hasClass('cur')) {
+ return;
+ }
+ $menu.find('.cur').removeClass('cur');
+ return $cur.addClass('cur');
+ };
+ })(this)).on('click.atwho-view', 'li', (function(_this) {
+ return function(e) {
+ $menu.find('.cur').removeClass('cur');
+ $(e.currentTarget).addClass('cur');
+ _this.choose(e);
+ return e.preventDefault();
+ };
+ })(this));
+ };
+
+ View.prototype.visible = function() {
+ return $.expr.filters.visible(this.$el[0]);
+ };
+
+ View.prototype.highlighted = function() {
+ return this.$el.find(".cur").length > 0;
+ };
+
+ View.prototype.choose = function(e) {
+ var $li, content;
+ if (($li = this.$el.find(".cur")).length) {
+ content = this.context.insertContentFor($li);
+ this.context._stopDelayedCall();
+ this.context.insert(this.context.callbacks("beforeInsert").call(this.context, content, $li, e), $li);
+ this.context.trigger("inserted", [$li, e]);
+ this.hide(e);
+ }
+ if (this.context.getOpt("hideWithoutSuffix")) {
+ return this.stopShowing = true;
+ }
+ };
+
+ View.prototype.reposition = function(rect) {
+ var _window, offset, overflowOffset, ref;
+ _window = this.context.app.iframeAsRoot ? this.context.app.window : window;
+ if (rect.bottom + this.$el.height() - $(_window).scrollTop() > $(_window).height()) {
+ rect.bottom = rect.top - this.$el.height();
+ }
+ if (rect.left > (overflowOffset = $(_window).width() - this.$el.width() - 5)) {
+ rect.left = overflowOffset;
+ }
+ offset = {
+ left: rect.left,
+ top: rect.bottom
+ };
+ if ((ref = this.context.callbacks("beforeReposition")) != null) {
+ ref.call(this.context, offset);
+ }
+ this.$el.offset(offset);
+ return this.context.trigger("reposition", [offset]);
+ };
+
+ View.prototype.next = function() {
+ var cur, next, nextEl, offset;
+ cur = this.$el.find('.cur').removeClass('cur');
+ next = cur.next();
+ if (!next.length) {
+ next = this.$el.find('li:first');
+ }
+ next.addClass('cur');
+ nextEl = next[0];
+ offset = nextEl.offsetTop + nextEl.offsetHeight + (nextEl.nextSibling ? nextEl.nextSibling.offsetHeight : 0);
+ return this.scrollTop(Math.max(0, offset - this.$el.height()));
+ };
+
+ View.prototype.prev = function() {
+ var cur, offset, prev, prevEl;
+ cur = this.$el.find('.cur').removeClass('cur');
+ prev = cur.prev();
+ if (!prev.length) {
+ prev = this.$el.find('li:last');
+ }
+ prev.addClass('cur');
+ prevEl = prev[0];
+ offset = prevEl.offsetTop + prevEl.offsetHeight + (prevEl.nextSibling ? prevEl.nextSibling.offsetHeight : 0);
+ return this.scrollTop(Math.max(0, offset - this.$el.height()));
+ };
+
+ View.prototype.scrollTop = function(scrollTop) {
+ var scrollDuration;
+ scrollDuration = this.context.getOpt('scrollDuration');
+ if (scrollDuration) {
+ return this.$elUl.animate({
+ scrollTop: scrollTop
+ }, scrollDuration);
+ } else {
+ return this.$elUl.scrollTop(scrollTop);
+ }
+ };
+
+ View.prototype.show = function() {
+ var rect;
+ if (this.stopShowing) {
+ this.stopShowing = false;
+ return;
+ }
+ if (!this.visible()) {
+ this.$el.show();
+ this.$el.scrollTop(0);
+ this.context.trigger('shown');
+ }
+ if (rect = this.context.rect()) {
+ return this.reposition(rect);
+ }
+ };
+
+ View.prototype.hide = function(e, time) {
+ var callback;
+ if (!this.visible()) {
+ return;
+ }
+ if (isNaN(time)) {
+ this.$el.hide();
+ return this.context.trigger('hidden', [e]);
+ } else {
+ callback = (function(_this) {
+ return function() {
+ return _this.hide();
+ };
+ })(this);
+ clearTimeout(this.timeoutID);
+ return this.timeoutID = setTimeout(callback, time);
+ }
+ };
+
+ View.prototype.render = function(list) {
+ var $li, $ul, i, item, len, li, tpl;
+ if (!($.isArray(list) && list.length > 0)) {
+ this.hide();
+ return;
+ }
+ this.$el.find('ul').empty();
+ $ul = this.$el.find('ul');
+ tpl = this.context.getOpt('displayTpl');
+ for (i = 0, len = list.length; i < len; i++) {
+ item = list[i];
+ item = $.extend({}, item, {
+ 'atwho-at': this.context.at
+ });
+ li = this.context.callbacks("tplEval").call(this.context, tpl, item, "onDisplay");
+ $li = $(this.context.callbacks("highlighter").call(this.context, li, this.context.query.text));
+ $li.data("item-data", item);
+ $ul.append($li);
+ }
+ this.show();
+ if (this.context.getOpt('highlightFirst')) {
+ return $ul.find("li:first").addClass("cur");
+ }
+ };
+
+ return View;
+
+})();
+
+var Api;
+
+Api = {
+ load: function(at, data) {
+ var c;
+ if (c = this.controller(at)) {
+ return c.model.load(data);
+ }
+ },
+ isSelecting: function() {
+ var ref;
+ return !!((ref = this.controller()) != null ? ref.view.visible() : void 0);
+ },
+ hide: function() {
+ var ref;
+ return (ref = this.controller()) != null ? ref.view.hide() : void 0;
+ },
+ reposition: function() {
+ var c;
+ if (c = this.controller()) {
+ return c.view.reposition(c.rect());
+ }
+ },
+ setIframe: function(iframe, asRoot) {
+ this.setupRootElement(iframe, asRoot);
+ return null;
+ },
+ run: function() {
+ return this.dispatch();
+ },
+ destroy: function() {
+ this.shutdown();
+ return this.$inputor.data('atwho', null);
+ }
+};
+
+$.fn.atwho = function(method) {
+ var _args, result;
+ _args = arguments;
+ result = null;
+ this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function() {
+ var $this, app;
+ if (!(app = ($this = $(this)).data("atwho"))) {
+ $this.data('atwho', (app = new App(this)));
+ }
+ if (typeof method === 'object' || !method) {
+ return app.reg(method.at, method);
+ } else if (Api[method] && app) {
+ return result = Api[method].apply(app, Array.prototype.slice.call(_args, 1));
+ } else {
+ return $.error("Method " + method + " does not exist on jQuery.atwho");
+ }
+ });
+ if (result != null) {
+ return result;
+ } else {
+ return this;
+ }
+};
+
+$.fn.atwho["default"] = {
+ at: void 0,
+ alias: void 0,
+ data: null,
+ displayTpl: "${name}",
+ insertTpl: "${atwho-at}${name}",
+ headerTpl: null,
+ callbacks: DEFAULT_CALLBACKS,
+ functionOverrides: {},
+ searchKey: "name",
+ suffix: void 0,
+ hideWithoutSuffix: false,
+ startWithSpace: true,
+ acceptSpaceBar: false,
+ highlightFirst: true,
+ limit: 5,
+ maxLen: 20,
+ minLen: 0,
+ displayTimeout: 300,
+ delay: null,
+ spaceSelectsMatch: false,
+ tabSelectsMatch: true,
+ editableAtwhoQueryAttrs: {},
+ scrollDuration: 150,
+ suspendOnComposing: true,
+ lookUpOnClick: true
+};
+
+$.fn.atwho.debug = false;
+
+}));
diff --git a/apps/comments/js/vendor/At.js/dist/js/jquery.atwho.min.js b/apps/comments/js/vendor/At.js/dist/js/jquery.atwho.min.js
new file mode 100644
index 0000000000..d1e60152b4
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/dist/js/jquery.atwho.min.js
@@ -0,0 +1 @@
+!function(t,e){"function"==typeof define&&define.amd?define(["jquery"],function(t){return e(t)}):"object"==typeof exports?module.exports=e(require("jquery")):e(jQuery)}(this,function(t){var e,i;i={ESC:27,TAB:9,ENTER:13,CTRL:17,A:65,P:80,N:78,LEFT:37,UP:38,RIGHT:39,DOWN:40,BACKSPACE:8,SPACE:32},e={beforeSave:function(t){return r.arrayToDefaultHash(t)},matcher:function(t,e,i,n){var r,o,s,a,h;return t=t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&"),i&&(t="(?:^|\\s)"+t),r=decodeURI("%C3%80"),o=decodeURI("%C3%BF"),h=n?" ":"",a=new RegExp(t+"([A-Za-z"+r+"-"+o+"0-9_"+h+"'.+-]*)$|"+t+"([^\\x00-\\xff]*)$","gi"),s=a.exec(e),s?s[2]||s[1]:null},filter:function(t,e,i){var n,r,o,s;for(n=[],r=0,s=e.length;s>r;r++)o=e[r],~new String(o[i]).toLowerCase().indexOf(t.toLowerCase())&&n.push(o);return n},remoteFilter:null,sorter:function(t,e,i){var n,r,o,s;if(!t)return e;for(n=[],r=0,s=e.length;s>r;r++)o=e[r],o.atwho_order=new String(o[i]).toLowerCase().indexOf(t.toLowerCase()),o.atwho_order>-1&&n.push(o);return n.sort(function(t,e){return t.atwho_order-e.atwho_order})},tplEval:function(t,e){var i,n,r;r=t;try{return"string"!=typeof t&&(r=t(e)),r.replace(/\$\{([^\}]*)\}/g,function(t,i,n){return e[i]})}catch(n){return i=n,""}},highlighter:function(t,e){var i;return e?(i=new RegExp(">\\s*([^<]*?)("+e.replace("+","\\+")+")([^<]*)\\s*<","ig"),t.replace(i,function(t,e,i,n){return"> "+e+""+i+""+n+" <"})):t},beforeInsert:function(t,e,i){return t},beforeReposition:function(t){return t},afterMatchFailed:function(t,e){}};var n;n=function(){function e(e){this.currentFlag=null,this.controllers={},this.aliasMaps={},this.$inputor=t(e),this.setupRootElement(),this.listen()}return e.prototype.createContainer=function(e){var i;return null!=(i=this.$el)&&i.remove(),t(e.body).append(this.$el=t(""))},e.prototype.setupRootElement=function(e,i){var n,r;if(null==i&&(i=!1),e)this.window=e.contentWindow,this.document=e.contentDocument||this.window.document,this.iframe=e;else{this.document=this.$inputor[0].ownerDocument,this.window=this.document.defaultView||this.document.parentWindow;try{this.iframe=this.window.frameElement}catch(r){if(n=r,this.iframe=null,t.fn.atwho.debug)throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n"+n)}}return this.createContainer((this.iframeAsRoot=i)?this.document:document)},e.prototype.controller=function(t){var e,i,n,r;if(this.aliasMaps[t])i=this.controllers[this.aliasMaps[t]];else{r=this.controllers;for(n in r)if(e=r[n],n===t){i=e;break}}return i?i:this.controllers[this.currentFlag]},e.prototype.setContextFor=function(t){return this.currentFlag=t,this},e.prototype.reg=function(t,e){var i,n;return n=(i=this.controllers)[t]||(i[t]=this.$inputor.is("[contentEditable]")?new l(this,t):new s(this,t)),e.alias&&(this.aliasMaps[e.alias]=t),n.init(e),this},e.prototype.listen=function(){return this.$inputor.on("compositionstart",function(t){return function(e){var i;return null!=(i=t.controller())&&i.view.hide(),t.isComposing=!0,null}}(this)).on("compositionend",function(t){return function(e){return t.isComposing=!1,setTimeout(function(e){return t.dispatch(e)}),null}}(this)).on("keyup.atwhoInner",function(t){return function(e){return t.onKeyup(e)}}(this)).on("keydown.atwhoInner",function(t){return function(e){return t.onKeydown(e)}}(this)).on("blur.atwhoInner",function(t){return function(e){var i;return(i=t.controller())?(i.expectedQueryCBId=null,i.view.hide(e,i.getOpt("displayTimeout"))):void 0}}(this)).on("click.atwhoInner",function(t){return function(e){return t.dispatch(e)}}(this)).on("scroll.atwhoInner",function(t){return function(){var e;return e=t.$inputor.scrollTop(),function(i){var n,r;return n=i.target.scrollTop,e!==n&&null!=(r=t.controller())&&r.view.hide(i),e=n,!0}}}(this)())},e.prototype.shutdown=function(){var t,e,i;i=this.controllers;for(t in i)e=i[t],e.destroy(),delete this.controllers[t];return this.$inputor.off(".atwhoInner"),this.$el.remove()},e.prototype.dispatch=function(t){var e,i,n,r;n=this.controllers,r=[];for(e in n)i=n[e],r.push(i.lookUp(t));return r},e.prototype.onKeyup=function(e){var n;switch(e.keyCode){case i.ESC:e.preventDefault(),null!=(n=this.controller())&&n.view.hide();break;case i.DOWN:case i.UP:case i.CTRL:case i.ENTER:t.noop();break;case i.P:case i.N:e.ctrlKey||this.dispatch(e);break;default:this.dispatch(e)}},e.prototype.onKeydown=function(e){var n,r;if(r=null!=(n=this.controller())?n.view:void 0,r&&r.visible())switch(e.keyCode){case i.ESC:e.preventDefault(),r.hide(e);break;case i.UP:e.preventDefault(),r.prev();break;case i.DOWN:e.preventDefault(),r.next();break;case i.P:if(!e.ctrlKey)return;e.preventDefault(),r.prev();break;case i.N:if(!e.ctrlKey)return;e.preventDefault(),r.next();break;case i.TAB:case i.ENTER:case i.SPACE:if(!r.visible())return;if(!this.controller().getOpt("spaceSelectsMatch")&&e.keyCode===i.SPACE)return;if(!this.controller().getOpt("tabSelectsMatch")&&e.keyCode===i.TAB)return;r.highlighted()?(e.preventDefault(),r.choose(e)):r.hide(e);break;default:t.noop()}},e}();var r,o=[].slice;r=function(){function i(e,i){this.app=e,this.at=i,this.$inputor=this.app.$inputor,this.id=this.$inputor[0].id||this.uid(),this.expectedQueryCBId=null,this.setting=null,this.query=null,this.pos=0,this.range=null,0===(this.$el=t("#atwho-ground-"+this.id,this.app.$el)).length&&this.app.$el.append(this.$el=t("")),this.model=new u(this),this.view=new c(this)}return i.prototype.uid=function(){return(Math.random().toString(16)+"000000000").substr(2,8)+(new Date).getTime()},i.prototype.init=function(e){return this.setting=t.extend({},this.setting||t.fn.atwho["default"],e),this.view.init(),this.model.reload(this.setting.data)},i.prototype.destroy=function(){return this.trigger("beforeDestroy"),this.model.destroy(),this.view.destroy(),this.$el.remove()},i.prototype.callDefault=function(){var i,n,r,s;s=arguments[0],i=2<=arguments.length?o.call(arguments,1):[];try{return e[s].apply(this,i)}catch(r){return n=r,t.error(n+" Or maybe At.js doesn't have function "+s)}},i.prototype.trigger=function(t,e){var i,n;return null==e&&(e=[]),e.push(this),i=this.getOpt("alias"),n=i?t+"-"+i+".atwho":t+".atwho",this.$inputor.trigger(n,e)},i.prototype.callbacks=function(t){return this.getOpt("callbacks")[t]||e[t]},i.prototype.getOpt=function(t,e){var i,n;try{return this.setting[t]}catch(n){return i=n,null}},i.prototype.insertContentFor=function(e){var i,n;return n=this.getOpt("insertTpl"),i=t.extend({},e.data("item-data"),{"atwho-at":this.at}),this.callbacks("tplEval").call(this,n,i,"onInsert")},i.prototype.renderView=function(t){var e;return e=this.getOpt("searchKey"),t=this.callbacks("sorter").call(this,this.query.text,t.slice(0,1001),e),this.view.render(t.slice(0,this.getOpt("limit")))},i.arrayToDefaultHash=function(e){var i,n,r,o;if(!t.isArray(e))return e;for(o=[],i=0,r=e.length;r>i;i++)n=e[i],t.isPlainObject(n)?o.push(n):o.push({name:n});return o},i.prototype.lookUp=function(t){var e,i;if((!t||"click"!==t.type||this.getOpt("lookUpOnClick"))&&(!this.getOpt("suspendOnComposing")||!this.app.isComposing))return(e=this.catchQuery(t))?(this.app.setContextFor(this.at),(i=this.getOpt("delay"))?this._delayLookUp(e,i):this._lookUp(e),e):(this.expectedQueryCBId=null,e)},i.prototype._delayLookUp=function(t,e){var i,n;return i=Date.now?Date.now():(new Date).getTime(),this.previousCallTime||(this.previousCallTime=i),n=e-(i-this.previousCallTime),n>0&&e>n?(this.previousCallTime=i,this._stopDelayedCall(),this.delayedCallTimeout=setTimeout(function(e){return function(){return e.previousCallTime=0,e.delayedCallTimeout=null,e._lookUp(t)}}(this),e)):(this._stopDelayedCall(),this.previousCallTime!==i&&(this.previousCallTime=0),this._lookUp(t))},i.prototype._stopDelayedCall=function(){return this.delayedCallTimeout?(clearTimeout(this.delayedCallTimeout),this.delayedCallTimeout=null):void 0},i.prototype._generateQueryCBId=function(){return{}},i.prototype._lookUp=function(e){var i;return i=function(t,e){return t===this.expectedQueryCBId?e&&e.length>0?this.renderView(this.constructor.arrayToDefaultHash(e)):this.view.hide():void 0},this.expectedQueryCBId=this._generateQueryCBId(),this.model.query(e.text,t.proxy(i,this,this.expectedQueryCBId))},i}();var s,a=function(t,e){function i(){this.constructor=t}for(var n in e)h.call(e,n)&&(t[n]=e[n]);return i.prototype=e.prototype,t.prototype=new i,t.__super__=e.prototype,t},h={}.hasOwnProperty;s=function(e){function i(){return i.__super__.constructor.apply(this,arguments)}return a(i,e),i.prototype.catchQuery=function(){var t,e,i,n,r,o,s;return e=this.$inputor.val(),t=this.$inputor.caret("pos",{iframe:this.app.iframe}),s=e.slice(0,t),r=this.callbacks("matcher").call(this,this.at,s,this.getOpt("startWithSpace"),this.getOpt("acceptSpaceBar")),n="string"==typeof r,n&&r.length0?t.getRangeAt(0):void 0},n.prototype._setRange=function(e,i,n){return null==n&&(n=this._getRange()),n&&i?(i=t(i)[0],"after"===e?(n.setEndAfter(i),n.setStartAfter(i)):(n.setEndBefore(i),n.setStartBefore(i)),n.collapse(!1),this._clearRange(n)):void 0},n.prototype._clearRange=function(t){var e;return null==t&&(t=this._getRange()),e=this.app.window.getSelection(),null==this.ctrl_a_pressed?(e.removeAllRanges(),e.addRange(t)):void 0},n.prototype._movingEvent=function(t){var e;return"click"===t.type||(e=t.which)===i.RIGHT||e===i.LEFT||e===i.UP||e===i.DOWN},n.prototype._unwrap=function(e){var i;return e=t(e).unwrap().get(0),(i=e.nextSibling)&&i.nodeValue&&(e.nodeValue+=i.nodeValue,t(i).remove()),e},n.prototype.catchQuery=function(e){var n,r,o,s,a,h,l,u,c,p,f,d;if((d=this._getRange())&&d.collapsed){if(e.which===i.ENTER)return(r=t(d.startContainer).closest(".atwho-query")).contents().unwrap(),r.is(":empty")&&r.remove(),(r=t(".atwho-query",this.app.document)).text(r.text()).contents().last().unwrap(),void this._clearRange();if(/firefox/i.test(navigator.userAgent)){if(t(d.startContainer).is(this.$inputor))return void this._clearRange();e.which===i.BACKSPACE&&d.startContainer.nodeType===document.ELEMENT_NODE&&(c=d.startOffset-1)>=0?(o=d.cloneRange(),o.setStart(d.startContainer,c),t(o.cloneContents()).contents().last().is(".atwho-inserted")&&(a=t(d.startContainer).contents().get(c),this._setRange("after",t(a).contents().last()))):e.which===i.LEFT&&d.startContainer.nodeType===document.TEXT_NODE&&(n=t(d.startContainer.previousSibling),n.is(".atwho-inserted")&&0===d.startOffset&&this._setRange("after",n.contents().last()))}if(t(d.startContainer).closest(".atwho-inserted").addClass("atwho-query").siblings().removeClass("atwho-query"),(r=t(".atwho-query",this.app.document)).length>0&&r.is(":empty")&&0===r.text().length&&r.remove(),this._movingEvent(e)||r.removeClass("atwho-inserted"),r.length>0)switch(e.which){case i.LEFT:return this._setRange("before",r.get(0),d),void r.removeClass("atwho-query");case i.RIGHT:return this._setRange("after",r.get(0).nextSibling,d),void r.removeClass("atwho-query")}if(r.length>0&&(f=r.attr("data-atwho-at-query"))&&(r.empty().html(f).attr("data-atwho-at-query",null),this._setRange("after",r.get(0),d)),o=d.cloneRange(),o.setStart(d.startContainer,0),u=this.callbacks("matcher").call(this,this.at,o.toString(),this.getOpt("startWithSpace"),this.getOpt("acceptSpaceBar")),h="string"==typeof u,0===r.length&&h&&(s=d.startOffset-this.at.length-u.length)>=0&&(d.setStart(d.startContainer,s),r=t("",this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass("atwho-query"),d.surroundContents(r.get(0)),l=r.contents().last().get(0),l&&(/firefox/i.test(navigator.userAgent)?(d.setStart(l,l.length),d.setEnd(l,l.length),this._clearRange(d)):this._setRange("after",l,d))),!(h&&u.length=0&&(this._movingEvent(e)&&r.hasClass("atwho-inserted")?r.removeClass("atwho-query"):!1!==this.callbacks("afterMatchFailed").call(this,this.at,r)&&this._setRange("after",this._unwrap(r.text(r.text()).contents().first()))),null)}},n.prototype.rect=function(){var e,i,n;return n=this.query.el.offset(),n&&this.query.el[0].getClientRects().length?(this.app.iframe&&!this.app.iframeAsRoot&&(i=(e=t(this.app.iframe)).offset(),n.left+=i.left-this.$inputor.scrollLeft(),n.top+=i.top-this.$inputor.scrollTop()),n.bottom=n.top+this.query.el.height(),n):void 0},n.prototype.insert=function(t,e){var i,n,r,o,s;return this.$inputor.is(":focus")||this.$inputor.focus(),n=this.getOpt("functionOverrides"),n.insert?n.insert.call(this,t,e):(o=""===(o=this.getOpt("suffix"))?o:o||" ",i=e.data("item-data"),this.query.el.removeClass("atwho-query").addClass("atwho-inserted").html(t).attr("data-atwho-at-query",""+i["atwho-at"]+this.query.text).attr("contenteditable","false"),(r=this._getRange())&&(this.query.el.length&&r.setEndAfter(this.query.el[0]),r.collapse(!1),r.insertNode(s=this.app.document.createTextNode(""+o)),this._setRange("after",s,r)),this.$inputor.is(":focus")||this.$inputor.focus(),this.$inputor.change())},n}(r);var u;u=function(){function e(t){this.context=t,this.at=this.context.at,this.storage=this.context.$inputor}return e.prototype.destroy=function(){return this.storage.data(this.at,null)},e.prototype.saved=function(){return this.fetch()>0},e.prototype.query=function(t,e){var i,n,r;return n=this.fetch(),r=this.context.getOpt("searchKey"),n=this.context.callbacks("filter").call(this.context,t,n,r)||[],i=this.context.callbacks("remoteFilter"),n.length>0||!i&&0===n.length?e(n):i.call(this.context,t,e)},e.prototype.fetch=function(){return this.storage.data(this.at)||[]},e.prototype.save=function(t){return this.storage.data(this.at,this.context.callbacks("beforeSave").call(this.context,t||[]))},e.prototype.load=function(t){return!this.saved()&&t?this._load(t):void 0},e.prototype.reload=function(t){return this._load(t)},e.prototype._load=function(e){return"string"==typeof e?t.ajax(e,{dataType:"json"}).done(function(t){return function(e){return t.save(e)}}(this)):this.save(e)},e}();var c;c=function(){function e(e){this.context=e,this.$el=t(""),this.$elUl=this.$el.children(),this.timeoutID=null,this.context.$el.append(this.$el),this.bindEvent()}return e.prototype.init=function(){var t,e;return e=this.context.getOpt("alias")||this.context.at.charCodeAt(0),t=this.context.getOpt("headerTpl"),t&&1===this.$el.children().length&&this.$el.prepend(t),this.$el.attr({id:"at-view-"+e})},e.prototype.destroy=function(){return this.$el.remove()},e.prototype.bindEvent=function(){var e,i,n;return e=this.$el.find("ul"),i=0,n=0,e.on("mousemove.atwho-view","li",function(r){return function(r){var o;if((i!==r.clientX||n!==r.clientY)&&(i=r.clientX,n=r.clientY,o=t(r.currentTarget),!o.hasClass("cur")))return e.find(".cur").removeClass("cur"),o.addClass("cur")}}(this)).on("click.atwho-view","li",function(i){return function(n){return e.find(".cur").removeClass("cur"),t(n.currentTarget).addClass("cur"),i.choose(n),n.preventDefault()}}(this))},e.prototype.visible=function(){return t.expr.filters.visible(this.$el[0])},e.prototype.highlighted=function(){return this.$el.find(".cur").length>0},e.prototype.choose=function(t){var e,i;return(e=this.$el.find(".cur")).length&&(i=this.context.insertContentFor(e),this.context._stopDelayedCall(),this.context.insert(this.context.callbacks("beforeInsert").call(this.context,i,e,t),e),this.context.trigger("inserted",[e,t]),this.hide(t)),this.context.getOpt("hideWithoutSuffix")?this.stopShowing=!0:void 0},e.prototype.reposition=function(e){var i,n,r,o;return i=this.context.app.iframeAsRoot?this.context.app.window:window,e.bottom+this.$el.height()-t(i).scrollTop()>t(i).height()&&(e.bottom=e.top-this.$el.height()),e.left>(r=t(i).width()-this.$el.width()-5)&&(e.left=r),n={left:e.left,top:e.bottom},null!=(o=this.context.callbacks("beforeReposition"))&&o.call(this.context,n),this.$el.offset(n),this.context.trigger("reposition",[n])},e.prototype.next=function(){var t,e,i,n;return t=this.$el.find(".cur").removeClass("cur"),e=t.next(),e.length||(e=this.$el.find("li:first")),e.addClass("cur"),i=e[0],n=i.offsetTop+i.offsetHeight+(i.nextSibling?i.nextSibling.offsetHeight:0),this.scrollTop(Math.max(0,n-this.$el.height()))},e.prototype.prev=function(){var t,e,i,n;return t=this.$el.find(".cur").removeClass("cur"),i=t.prev(),i.length||(i=this.$el.find("li:last")),i.addClass("cur"),n=i[0],e=n.offsetTop+n.offsetHeight+(n.nextSibling?n.nextSibling.offsetHeight:0),this.scrollTop(Math.max(0,e-this.$el.height()))},e.prototype.scrollTop=function(t){var e;return e=this.context.getOpt("scrollDuration"),e?this.$elUl.animate({scrollTop:t},e):this.$elUl.scrollTop(t)},e.prototype.show=function(){var t;return this.stopShowing?void(this.stopShowing=!1):(this.visible()||(this.$el.show(),this.$el.scrollTop(0),this.context.trigger("shown")),(t=this.context.rect())?this.reposition(t):void 0)},e.prototype.hide=function(t,e){var i;if(this.visible())return isNaN(e)?(this.$el.hide(),this.context.trigger("hidden",[t])):(i=function(t){return function(){return t.hide()}}(this),clearTimeout(this.timeoutID),this.timeoutID=setTimeout(i,e))},e.prototype.render=function(e){var i,n,r,o,s,a,h;if(!(t.isArray(e)&&e.length>0))return void this.hide();for(this.$el.find("ul").empty(),n=this.$el.find("ul"),h=this.context.getOpt("displayTpl"),r=0,s=e.length;s>r;r++)o=e[r],o=t.extend({},o,{"atwho-at":this.context.at}),a=this.context.callbacks("tplEval").call(this.context,h,o,"onDisplay"),i=t(this.context.callbacks("highlighter").call(this.context,a,this.context.query.text)),i.data("item-data",o),n.append(i);return this.show(),this.context.getOpt("highlightFirst")?n.find("li:first").addClass("cur"):void 0},e}();var p;p={load:function(t,e){var i;return(i=this.controller(t))?i.model.load(e):void 0},isSelecting:function(){var t;return!!(null!=(t=this.controller())?t.view.visible():void 0)},hide:function(){var t;return null!=(t=this.controller())?t.view.hide():void 0},reposition:function(){var t;return(t=this.controller())?t.view.reposition(t.rect()):void 0},setIframe:function(t,e){return this.setupRootElement(t,e),null},run:function(){return this.dispatch()},destroy:function(){return this.shutdown(),this.$inputor.data("atwho",null)}},t.fn.atwho=function(e){var i,r;return i=arguments,r=null,this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function(){var o,s;return(s=(o=t(this)).data("atwho"))||o.data("atwho",s=new n(this)),"object"!=typeof e&&e?p[e]&&s?r=p[e].apply(s,Array.prototype.slice.call(i,1)):t.error("Method "+e+" does not exist on jQuery.atwho"):s.reg(e.at,e)}),null!=r?r:this},t.fn.atwho["default"]={at:void 0,alias:void 0,data:null,displayTpl:"${name}",insertTpl:"${atwho-at}${name}",headerTpl:null,callbacks:e,functionOverrides:{},searchKey:"name",suffix:void 0,hideWithoutSuffix:!1,startWithSpace:!0,acceptSpaceBar:!1,highlightFirst:!0,limit:5,maxLen:20,minLen:0,displayTimeout:300,delay:null,spaceSelectsMatch:!1,tabSelectsMatch:!0,editableAtwhoQueryAttrs:{},scrollDuration:150,suspendOnComposing:!0,lookUpOnClick:!0},t.fn.atwho.debug=!1});
\ No newline at end of file
diff --git a/apps/comments/js/vendor/At.js/examples/cross_document/dataFrame.html b/apps/comments/js/vendor/At.js/examples/cross_document/dataFrame.html
new file mode 100644
index 0000000000..4a1adabecc
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/examples/cross_document/dataFrame.html
@@ -0,0 +1,30 @@
+
+
+
+ Data Iframe
+
+
+
+
+
+
+
+
diff --git a/apps/comments/js/vendor/At.js/examples/cross_document/index.html b/apps/comments/js/vendor/At.js/examples/cross_document/index.html
new file mode 100644
index 0000000000..05428b3ee0
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/examples/cross_document/index.html
@@ -0,0 +1,90 @@
+
+
+
+
+ At.js
+
+
+
+
+
+
+
+
Cross-Document
+
+
+
+
+
+
diff --git a/apps/comments/js/vendor/At.js/examples/cross_document/viewFrame.html b/apps/comments/js/vendor/At.js/examples/cross_document/viewFrame.html
new file mode 100644
index 0000000000..84ee67d8a2
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/examples/cross_document/viewFrame.html
@@ -0,0 +1,10 @@
+
+
+
+ View Iframe
+
+
+
+ hello!
+
+
diff --git a/apps/comments/js/vendor/At.js/examples/hashtags.html b/apps/comments/js/vendor/At.js/examples/hashtags.html
new file mode 100644
index 0000000000..3894ceea99
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/examples/hashtags.html
@@ -0,0 +1,61 @@
+
+
+
+
+ At.js
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Type `#` to autocomplete tags
+
+
+
+
+
diff --git a/apps/comments/js/vendor/At.js/examples/medium-editor.html b/apps/comments/js/vendor/At.js/examples/medium-editor.html
new file mode 100644
index 0000000000..f1cedf9b06
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/examples/medium-editor.html
@@ -0,0 +1,44 @@
+
+
+
+
+ At.js
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Example for medium-editor
+
+
+
Easy! You should check out MoxieManager!
+
+
+
+
+
+
+
diff --git a/apps/comments/js/vendor/At.js/examples/style.css b/apps/comments/js/vendor/At.js/examples/style.css
new file mode 100644
index 0000000000..9e78ffbf74
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/examples/style.css
@@ -0,0 +1,57 @@
+html, body {
+ background:#F9F9F9;
+ padding: 0;
+ margin: 0;
+ font: 14px/1.6 "Lucida Grande", "Helvetica", sans-serif;
+ color: #333;
+}
+h1,h2,h3,h4 {
+ font-family: 'PT Sans', sans-serif;
+ line-height: 40px;
+ color: inherit;
+ font-weight: bold;
+ margin: 10px 0;
+ text-rendering: optimizelegibility;
+}
+h2,h3 {
+ color: gray;
+}
+strong {
+ color: #424242;
+}
+
+a {
+ color: #4183C4;
+ text-decoration: none;
+}
+a:hover {
+ text-decoration: underline;
+}
+.wrapper {
+ width: 750px;
+ padding: 20px;
+ margin: 0 auto;
+}
+header {
+ margin-top:30px;
+}
+
+.inputor {
+ height: 160px;
+ width: 90%;
+ border: 1px solid #dadada;
+ border-radius: 4px;
+ padding: 5px 8px;
+ outline: 0 none;
+ margin: 10px 0;
+ background: white;
+ font-size: inherit;
+ overflow-y: scroll;
+}
+.inputor:focus {
+ border: 1px solid rgb(6, 150, 247);
+}
+
+footer {
+ margin: 30px 0;
+}
diff --git a/apps/comments/js/vendor/At.js/examples/tinyMCE.html b/apps/comments/js/vendor/At.js/examples/tinyMCE.html
new file mode 100644
index 0000000000..fca6815298
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/examples/tinyMCE.html
@@ -0,0 +1,53 @@
+
+
+
+
+ At.js
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Example for tinyMCE editor
+
+
+
+
+
+
diff --git a/apps/comments/js/vendor/At.js/examples/ueditor.html b/apps/comments/js/vendor/At.js/examples/ueditor.html
new file mode 100644
index 0000000000..34de7c9292
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/examples/ueditor.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+ ueditor
+
+
+
+
+
+
+
+
+
diff --git a/apps/comments/js/vendor/At.js/gulpfile.js b/apps/comments/js/vendor/At.js/gulpfile.js
new file mode 100644
index 0000000000..e9699eab8d
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/gulpfile.js
@@ -0,0 +1,103 @@
+var gulp = require('gulp'),
+ coffee = require('gulp-coffee'),
+ concat = require('gulp-concat'),
+ umd = require('gulp-umd'),
+ uglify = require('gulp-uglify'),
+ rename = require("gulp-rename"),
+ cssmin = require('gulp-cssmin'),
+ jasmine = require('gulp-jasmine-phantom'),
+ bump = require('gulp-bump'),
+ header = require('gulp-header'),
+ debug = require('gulp-debug'),
+ util = require('gulp-util');
+
+var name = 'jquery.atwho';
+
+gulp.task('coffee', function() {
+ gulp.src('src/*.coffee')
+ .pipe(coffee({bare: true}).on('error', util.log))
+ .pipe(gulp.dest('./build/js'));
+});
+
+gulp.task('concat', function() {
+ fileList = [
+ 'build/js/default.js',
+ 'build/js/app.js',
+ 'build/js/controller.js',
+ 'build/js/textareaController.js',
+ 'build/js/editableController.js',
+ 'build/js/model.js',
+ 'build/js/view.js',
+ 'build/js/api.js'
+ ]
+ gulp.src(fileList)
+ .pipe(concat(name + ".js"))
+ .pipe(gulp.dest('build'));
+});
+
+gulp.task('umd', function() {
+ gulp.src('build/' + name + ".js")
+ .pipe(umd({template: "umd.template.js"}))
+ .pipe(gulp.dest('build/js'));
+});
+
+gulp.task('bump', function() {
+ gulp.src(['bower.json', 'component.json', 'package.json'])
+ .pipe(bump({version: "1.5.4"}))
+ .pipe(gulp.dest('./'));
+});
+
+gulp.task("mark", function() {
+ var pkg = require('./package.json');
+ var banner = ['/**',
+ ' * <%= pkg.name %> - <%= pkg.version %>',
+ ' * Copyright (c) <%= year %> <%= pkg.author.name %> <<%= pkg.author.email %>>;',
+ ' * Homepage: <%= pkg.homepage %>',
+ ' * License: <%= pkg.license %>',
+ ' */',
+ ''].join('\n');
+
+ gulp.src('build/js/' + name + '.js')
+ .pipe(header(banner, { pkg : pkg, year: (new Date).getFullYear()}))
+ .pipe(gulp.dest('dist/js/'))
+});
+
+gulp.task('compress', function() {
+ gulp.src('dist/js/' + name + '.js')
+ .pipe(uglify())
+ .pipe(rename({suffix: '.min'}))
+ .pipe(gulp.dest('dist/js'));
+
+ gulp.src('src/jquery.atwho.css').pipe(gulp.dest('dist/css'))
+ gulp.src('dist/css/' + name + '.css')
+ .pipe(cssmin())
+ .pipe(rename({suffix: '.min'}))
+ .pipe(gulp.dest('dist/css'));
+});
+
+gulp.task('test', function () {
+ gulp.src('spec/**/*.coffee')
+ .pipe(coffee({bare: true}).on('error', util.log))
+ .pipe(debug({title: "compiled specs"}))
+ .pipe(gulp.dest('spec/build'))
+
+ gulp.src('spec/build/javascripts/*.spec.js')
+ .pipe(jasmine({
+ integration: true,
+ specHtml: "specRunner.html"
+ /* TODO: have to add css to spec
+ vendor: [
+ 'bower_components/jquery/dist/jquery.js',
+ 'bower_components/Caret.js/dist/jquery.caret.js',
+ 'dist/js/jquery.atwho.js',
+ 'node_modules/jasmine-jquery/lib/*.js',
+ 'node_modules/jasmine-ajax/lib/*.js',
+ 'spec/helpers/*.js',
+ 'spec/build/spec_helper.js'
+ ],
+ */
+ }));
+});
+
+gulp.task('compile', ['coffee', 'umd', 'concat']);
+gulp.task('default', ['compile', 'bump', 'mark', 'compress']);
diff --git a/apps/comments/js/vendor/At.js/index.html b/apps/comments/js/vendor/At.js/index.html
new file mode 100644
index 0000000000..a2654e0b8e
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/index.html
@@ -0,0 +1,205 @@
+
+
+
+
+ At.js
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/comments/js/vendor/At.js/package.json b/apps/comments/js/vendor/At.js/package.json
new file mode 100644
index 0000000000..46d8d41cfa
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "at.js",
+ "main": "dist/js/jquery.atwho.js",
+ "author": {
+ "name": "chord.luo",
+ "email": "chord.luo@gmail.com"
+ },
+ "homepage": "http://ichord.github.com/At.js",
+ "license": "MIT",
+ "version": "1.5.4",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/ichord/At.js"
+ },
+ "engines": {
+ "node": ">= 0.6.0"
+ },
+ "scripts": {
+ "test": "gulp test"
+ },
+ "peerDependencies": {
+ "jquery": ">=1.7.0 <4.0.0"
+ },
+ "devDependencies": {
+ "gulp": "^3.9.0",
+ "gulp-bump": "^1.0.0",
+ "gulp-coffee": "^2.3.1",
+ "gulp-concat": "^2.6.0",
+ "gulp-cssmin": "^0.1.7",
+ "gulp-debug": "^2.1.2",
+ "gulp-header": "^1.7.1",
+ "gulp-jasmine": "^2.2.1",
+ "gulp-jasmine-phantom": "^2.0.1",
+ "gulp-rename": "^1.2.2",
+ "gulp-uglify": "^1.5.1",
+ "gulp-umd": "^0.2.0",
+ "gulp-util": "^3.0.7",
+ "jasmine-ajax": "^3.2.0",
+ "jasmine-jquery": "^2.1.1",
+ "phantomjs": "^1.9.19"
+ },
+ "spm": {
+ "main": "dist/js/jquery.atwho.js",
+ "dependencies": {
+ "jquery": ">=1.7.2",
+ "caret.js": "~0.2.2"
+ },
+ "ignore": [
+ "examples",
+ "spec",
+ "src"
+ ]
+ }
+}
diff --git a/apps/comments/js/vendor/At.js/specRunner.html b/apps/comments/js/vendor/At.js/specRunner.html
new file mode 100644
index 0000000000..d0a5e63e8b
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/specRunner.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/comments/js/vendor/At.js/src/api.coffee b/apps/comments/js/vendor/At.js/src/api.coffee
new file mode 100644
index 0000000000..2e208d3e65
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/src/api.coffee
@@ -0,0 +1,59 @@
+Api =
+ # load a flag's data
+ #
+ # @params at[String] the flag
+ # @params data [Array] data to storage.
+ load: (at, data) -> c.model.load data if c = this.controller(at)
+ isSelecting: () -> !!this.controller()?.view.visible()
+ hide: () -> this.controller()?.view.hide()
+ reposition: () ->
+ if c = this.controller()
+ c.view.reposition(c.rect())
+ setIframe: (iframe, asRoot) -> this.setupRootElement(iframe, asRoot); null;
+ run: -> this.dispatch()
+ destroy: ->
+ this.shutdown()
+ @$inputor.data('atwho', null)
+
+$.fn.atwho = (method) ->
+ _args = arguments
+ result = null
+ this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each ->
+ if not app = ($this = $ this).data "atwho"
+ $this.data 'atwho', (app = new App this)
+ if typeof method is 'object' || !method
+ app.reg method.at, method
+ else if Api[method] and app
+ result = Api[method].apply app, Array::slice.call(_args, 1)
+ else
+ $.error "Method #{method} does not exist on jQuery.atwho"
+ if result? then result else this
+
+$.fn.atwho.default =
+ at: undefined
+ alias: undefined
+ data: null
+ displayTpl: "${name}"
+ insertTpl: "${atwho-at}${name}"
+ headerTpl: null
+ callbacks: DEFAULT_CALLBACKS
+ functionOverrides: {}
+ searchKey: "name"
+ suffix: undefined
+ hideWithoutSuffix: no
+ startWithSpace: yes
+ acceptSpaceBar: false
+ highlightFirst: yes
+ limit: 5
+ maxLen: 20
+ minLen: 0
+ displayTimeout: 300
+ delay: null
+ spaceSelectsMatch: no
+ tabSelectsMatch: yes
+ editableAtwhoQueryAttrs: {}
+ scrollDuration: 150
+ suspendOnComposing: true
+ lookUpOnClick: true
+
+$.fn.atwho.debug = false
diff --git a/apps/comments/js/vendor/At.js/src/app.coffee b/apps/comments/js/vendor/At.js/src/app.coffee
new file mode 100644
index 0000000000..fd601b5161
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/src/app.coffee
@@ -0,0 +1,158 @@
+# At.js central contoller(searching, matching, evaluating and rendering.)
+class App
+
+ # @param inputor [HTML DOM Object] `input` or `textarea`
+ constructor: (inputor) ->
+ @currentFlag = null
+ @controllers = {}
+ @aliasMaps = {}
+ @$inputor = $(inputor)
+ this.setupRootElement()
+ this.listen()
+
+ createContainer: (doc) ->
+ @$el?.remove()
+ $ doc.body
+ .append @$el = $ ""
+
+ setupRootElement: (iframe, asRoot=false) ->
+ if iframe
+ @window = iframe.contentWindow
+ @document = iframe.contentDocument || @window.document
+ @iframe = iframe
+ else
+ @document = @$inputor[0].ownerDocument
+ @window = @document.defaultView || @document.parentWindow
+ try
+ @iframe = @window.frameElement
+ catch error
+ @iframe = null
+ if $.fn.atwho.debug
+ throw new Error """
+ iframe auto-discovery is failed.
+ Please use `setIframe` to set the target iframe manually.
+ #{error}
+ """
+ this.createContainer if @iframeAsRoot = asRoot then @document else document
+
+ controller: (at) ->
+ if @aliasMaps[at]
+ current = @controllers[@aliasMaps[at]]
+ else
+ for currentFlag, c of @controllers
+ if currentFlag is at
+ current = c
+ break
+
+ if current then current else @controllers[@currentFlag]
+
+ setContextFor: (at) ->
+ @currentFlag = at
+ this
+
+ # At.js can register multiple at char (flag) to every inputor such as "@" and ":"
+ # Along with their own `settings` so that it works differently.
+ # After register, we still can update their `settings` such as updating `data`
+ #
+ # @param flag [String] at char (flag)
+ # @param settings [Hash] the settings
+ reg: (flag, setting) ->
+ controller = @controllers[flag] ||=
+ if @$inputor.is '[contentEditable]'
+ new EditableController this, flag
+ else
+ new TextareaController this, flag
+ # TODO: it will produce rubbish alias map, reduse this.
+ @aliasMaps[setting.alias] = flag if setting.alias
+ controller.init setting
+ this
+
+ # binding jQuery events of `inputor`'s
+ listen: ->
+ @$inputor
+ .on 'compositionstart', (e) =>
+ this.controller()?.view.hide()
+ @isComposing = true
+ null
+ .on 'compositionend', (e) =>
+ @isComposing = false
+ setTimeout((e) => @dispatch(e))
+ null
+ .on 'keyup.atwhoInner', (e) =>
+ this.onKeyup(e)
+ .on 'keydown.atwhoInner', (e) =>
+ this.onKeydown(e)
+ .on 'blur.atwhoInner', (e) =>
+ if c = this.controller()
+ c.expectedQueryCBId = null
+ c.view.hide(e,c.getOpt("displayTimeout"))
+ .on 'click.atwhoInner', (e) =>
+ this.dispatch e
+ .on 'scroll.atwhoInner', do =>
+ # make returned handler handle the very first call properly
+ lastScrollTop = @$inputor.scrollTop()
+ (e) =>
+ currentScrollTop = e.target.scrollTop
+ if lastScrollTop != currentScrollTop
+ @controller()?.view.hide(e)
+ lastScrollTop = currentScrollTop
+ true # ensure we don't stop bubbling
+
+ shutdown: ->
+ for _, c of @controllers
+ c.destroy()
+ delete @controllers[_]
+ @$inputor.off '.atwhoInner'
+ @$el.remove()
+
+ dispatch: (e) ->
+ c.lookUp(e) for _, c of @controllers
+
+ onKeyup: (e) ->
+ switch e.keyCode
+ when KEY_CODE.ESC
+ e.preventDefault()
+ this.controller()?.view.hide()
+ when KEY_CODE.DOWN, KEY_CODE.UP, KEY_CODE.CTRL, KEY_CODE.ENTER
+ $.noop()
+ when KEY_CODE.P, KEY_CODE.N
+ this.dispatch e if not e.ctrlKey
+ else
+ this.dispatch e
+ # coffeescript will return everywhere!!
+ return
+
+ onKeydown: (e) ->
+ # return if not (view = this.controller().view).visible()
+ view = this.controller()?.view
+ return if not (view and view.visible())
+ switch e.keyCode
+ when KEY_CODE.ESC
+ e.preventDefault()
+ view.hide(e)
+ when KEY_CODE.UP
+ e.preventDefault()
+ view.prev()
+ when KEY_CODE.DOWN
+ e.preventDefault()
+ view.next()
+ when KEY_CODE.P
+ return if not e.ctrlKey
+ e.preventDefault()
+ view.prev()
+ when KEY_CODE.N
+ return if not e.ctrlKey
+ e.preventDefault()
+ view.next()
+ when KEY_CODE.TAB, KEY_CODE.ENTER, KEY_CODE.SPACE
+ return if not view.visible()
+ return if not this.controller().getOpt('spaceSelectsMatch') and e.keyCode == KEY_CODE.SPACE
+ return if not this.controller().getOpt('tabSelectsMatch') and e.keyCode == KEY_CODE.TAB
+ if view.highlighted()
+ e.preventDefault()
+ view.choose(e)
+ else
+ view.hide(e)
+ else
+ $.noop()
+ return
diff --git a/apps/comments/js/vendor/At.js/src/controller.coffee b/apps/comments/js/vendor/At.js/src/controller.coffee
new file mode 100644
index 0000000000..22e263b002
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/src/controller.coffee
@@ -0,0 +1,142 @@
+class Controller
+ uid: ->
+ (Math.random().toString(16)+"000000000").substr(2,8) + (new Date().getTime())
+
+ constructor: (@app, @at) ->
+ @$inputor = @app.$inputor
+ @id = @$inputor[0].id || this.uid()
+ @expectedQueryCBId = null
+
+ @setting = null
+ @query = null
+ @pos = 0
+ @range = null
+ if (@$el = $("#atwho-ground-#{@id}", @app.$el)).length == 0
+ @app.$el.append @$el = $("")
+
+ @model = new Model(this)
+ @view = new View(this)
+
+ init: (setting) ->
+ @setting = $.extend {}, @setting || $.fn.atwho.default, setting
+ @view.init()
+ @model.reload @setting.data
+
+ destroy: ->
+ this.trigger 'beforeDestroy'
+ @model.destroy()
+ @view.destroy()
+ @$el.remove()
+
+ callDefault: (funcName, args...) ->
+ try
+ DEFAULT_CALLBACKS[funcName].apply this, args
+ catch error
+ $.error "#{error} Or maybe At.js doesn't have function #{funcName}"
+
+ # Delegate custom `jQueryEvent` to the inputor
+ # This function will add `atwho` as namespace to every jQuery event
+ # and pass current context as the last param to it.
+ #
+ # @example
+ # this.trigger "roll_n_rock", [1,2,3,4]
+ #
+ # $inputor.on "rool_n_rock", (e, one, two, three, four) ->
+ # console.log one, two, three, four
+ #
+ # @param name [String] Event name
+ # @param data [Array] data to callback
+ trigger: (name, data=[]) ->
+ data.push this
+ alias = this.getOpt('alias')
+ eventName = if alias then "#{name}-#{alias}.atwho" else "#{name}.atwho"
+ @$inputor.trigger eventName, data
+
+ # Get callback either in settings which was set by plugin user or in default callbacks list.
+ #
+ # @param funcName [String] callback's name
+ # @return [Function] The callback.
+ callbacks: (funcName)->
+ this.getOpt("callbacks")[funcName] || DEFAULT_CALLBACKS[funcName]
+
+ # Because different registered at chars have different settings.
+ # so we should give their own for them.
+ #
+ # @param at [String] setting's at name
+ # @param default_value [?] return this if nothing is returned from current settings.
+ # @return [?] setting's value
+ getOpt: (at, default_value) ->
+ try
+ @setting[at]
+ catch e
+ null
+
+ insertContentFor: ($li) ->
+ tpl = this.getOpt('insertTpl')
+ data = $.extend {}, $li.data('item-data'), {'atwho-at': @at}
+ this.callbacks("tplEval").call(this, tpl, data, "onInsert")
+
+ # Render list view
+ #
+ # @param data [Array] The data
+ renderView: (data) ->
+ searchKey = this.getOpt("searchKey")
+ data = this.callbacks("sorter").call(this, @query.text, data[0..1000] , searchKey)
+ @view.render data[0...this.getOpt('limit')]
+
+ @arrayToDefaultHash: (data) ->
+ return data if not $.isArray data
+ for item in data
+ if $.isPlainObject item then item else name:item
+
+ # Searching!
+ lookUp: (e) ->
+ return if e && e.type == 'click' && !@getOpt('lookUpOnClick')
+ return if @getOpt('suspendOnComposing') and @app.isComposing
+ query = @catchQuery e
+ if not query
+ @expectedQueryCBId = null
+ return query
+ @app.setContextFor @at
+ if wait = this.getOpt('delay')
+ @_delayLookUp query, wait
+ else
+ @_lookUp query
+ query
+
+ _delayLookUp: (query, wait) ->
+ now = if Date.now then Date.now() else new Date().getTime()
+ @previousCallTime ||= now
+ remaining = wait - (now - @previousCallTime)
+ if 0 < remaining < wait
+ @previousCallTime = now
+ @_stopDelayedCall()
+ @delayedCallTimeout = setTimeout(=>
+ @previousCallTime = 0
+ @delayedCallTimeout = null
+ @_lookUp query
+ , wait)
+ else
+ @_stopDelayedCall()
+ @previousCallTime = 0 if @previousCallTime isnt now
+ @_lookUp query
+
+ _stopDelayedCall: ->
+ if @delayedCallTimeout
+ clearTimeout @delayedCallTimeout
+ @delayedCallTimeout = null
+
+ _generateQueryCBId: ->
+ return {};
+
+ _lookUp: (query) ->
+ _callback = (queryCBId, data) ->
+ # ensure only the latest instance of this function perform actions
+ if queryCBId isnt @expectedQueryCBId
+ return
+ if data and data.length > 0
+ this.renderView @constructor.arrayToDefaultHash data
+ else
+ @view.hide()
+ @expectedQueryCBId = @_generateQueryCBId()
+ @model.query query.text, $.proxy(_callback, this, @expectedQueryCBId)
diff --git a/apps/comments/js/vendor/At.js/src/default.coffee b/apps/comments/js/vendor/At.js/src/default.coffee
new file mode 100644
index 0000000000..f8c64a9fe1
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/src/default.coffee
@@ -0,0 +1,147 @@
+KEY_CODE =
+ ESC: 27
+ TAB: 9
+ ENTER: 13
+ CTRL: 17
+ A: 65
+ P: 80
+ N: 78
+ LEFT: 37
+ UP:38
+ RIGHT: 39
+ DOWN: 40
+ BACKSPACE: 8
+ SPACE: 32
+
+# Functions set for handling and rendering the data.
+# Others developers can override these methods to tweak At.js such as matcher.
+# We can override them in `callbacks` settings.
+#
+# @mixin
+#
+# The context of these functions is `$.atwho.Controller` object and they are called in this sequences:
+#
+# [beforeSave, matcher, filter, remoteFilter, sorter, tplEvl, highlighter, beforeInsert, afterMatchFailed]
+#
+DEFAULT_CALLBACKS =
+
+ # It would be called to restructure the data before At.js invokes `Model#save` to save data
+ # By default, At.js will convert it to a Hash Array.
+ #
+ # @param data [Array] data to refacotor.
+ # @return [Array] Data after refactor.
+ beforeSave: (data) ->
+ Controller.arrayToDefaultHash data
+
+ # It would be called to match the `flag`.
+ # It will match at start of line or after whitespace
+ #
+ # @param flag [String] current `flag` ("@", etc)
+ # @param subtext [String] Text from start to current caret position.
+ # @param should_startWithSpace [boolean] accept white space as beginning of match.
+ # @param acceptSpaceBar [boolean] accept a space bar in the center of match,
+ # so you can match a first and last name, for ex.
+ #
+ # @return [String | null] Matched result.
+ matcher: (flag, subtext, should_startWithSpace, acceptSpaceBar) ->
+ # escape RegExp
+ flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")
+ flag = '(?:^|\\s)' + flag if should_startWithSpace
+
+ # À
+ _a = decodeURI("%C3%80")
+ # ÿ
+ _y = decodeURI("%C3%BF")
+ space = if acceptSpaceBar then "\ " else ""
+ regexp = new RegExp "#{flag}([A-Za-z#{_a}-#{_y}0-9_#{space}\'\.\+\-]*)$|#{flag}([^\\x00-\\xff]*)$",'gi'
+ match = regexp.exec subtext
+ if match then match[2] || match[1] else null
+
+ # ---------------------
+
+ # Filter data by matched string.
+ #
+ # @param query [String] Matched string.
+ # @param data [Array] data list
+ # @param searchKey [String] at char for searching.
+ #
+ # @return [Array] result data.
+ filter: (query, data, searchKey) ->
+ # !!null #=> false; !!undefined #=> false; !!'' #=> false;
+ _results = []
+ for item in data
+ _results.push item if ~new String(item[searchKey]).toLowerCase().indexOf query.toLowerCase()
+ _results
+
+ # If a function is given, At.js will invoke it if local filter can not find any data
+ #
+ # @param params [String] matched query
+ # @param callback [Function] callback to render page.
+ remoteFilter: null
+ # remoteFilter: (query, callback) ->
+ # $.ajax url,
+ # data: params
+ # success: (data) ->
+ # callback(data)
+
+ # Sorter data of course.
+ #
+ # @param query [String] matched string
+ # @param items [Array] data that was refactored
+ # @param searchKey [String] at char to search
+ #
+ # @return [Array] sorted data
+ sorter: (query, items, searchKey) ->
+ return items unless query
+
+ _results = []
+ for item in items
+ item.atwho_order = new String(item[searchKey]).toLowerCase().indexOf query.toLowerCase()
+ _results.push item if item.atwho_order > -1
+
+ _results.sort (a,b) -> a.atwho_order - b.atwho_order
+
+ # Evaluate the template either as a string or as a function
+ # this allows someone to pass in a set of data that needs a
+ # different template for different data results
+ #
+ # @param tpl [function] the template function or string
+ # @param map [Hash] Data map to eval.
+ tplEval: (tpl, map) ->
+ template = tpl
+ try
+ template = tpl(map) unless typeof tpl == 'string'
+ template.replace /\$\{([^\}]*)\}/g, (tag, key, pos) -> map[key]
+ catch error
+ ""
+
+
+ # Highlight the `matched query` string.
+ #
+ # @param li [String] HTML String after eval.
+ # @param query [String] matched query.
+ #
+ # @return [String] highlighted string.
+ highlighter: (li, query) ->
+ return li if not query
+ regexp = new RegExp(">\\s*([^\<]*?)(" + query.replace("+","\\+") + ")([^\<]*)\\s*<", 'ig')
+ li.replace regexp, (str, $1, $2, $3) -> '> '+$1+'' + $2 + ''+$3+' <'
+
+ # What to do before inserting item's value into inputor.
+ #
+ # @param value [String] content to insert
+ # @param $li [jQuery Object] the chosen item
+ # @param e [event Object] from the user selection (keyDown or click)
+ beforeInsert: (value, $li, e) ->
+ value
+
+ # You can adjust the menu's offset here.
+ #
+ # @param offset [Hash] offset will be applied to menu
+ # beforeReposition: (offset) ->
+ # offset.left += 10
+ # offset.top += 10
+ # offset
+ beforeReposition: (offset) -> offset
+
+ afterMatchFailed: (at, el) ->
diff --git a/apps/comments/js/vendor/At.js/src/editableController.coffee b/apps/comments/js/vendor/At.js/src/editableController.coffee
new file mode 100644
index 0000000000..d6fcaeb799
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/src/editableController.coffee
@@ -0,0 +1,174 @@
+class EditableController extends Controller
+
+ _getRange: ->
+ sel = @app.window.getSelection()
+ sel.getRangeAt(0) if sel.rangeCount > 0
+
+ _setRange: (position, node, range=@_getRange()) ->
+ return unless range and node
+ node = $(node)[0]
+ if position == 'after'
+ range.setEndAfter node
+ range.setStartAfter node
+ else
+ range.setEndBefore node
+ range.setStartBefore node
+ range.collapse false
+ @_clearRange range
+
+ _clearRange: (range=@_getRange()) ->
+ sel = @app.window.getSelection()
+ #ctrl+a remove defaults using the flag
+ if !@ctrl_a_pressed?
+ sel.removeAllRanges()
+ sel.addRange range
+
+ _movingEvent: (e) ->
+ e.type == 'click' or e.which in [KEY_CODE.RIGHT, KEY_CODE.LEFT, KEY_CODE.UP, KEY_CODE.DOWN]
+
+ _unwrap: (node) ->
+ node = $(node).unwrap().get 0
+ if (next = node.nextSibling) and next.nodeValue
+ node.nodeValue += next.nodeValue
+ $(next).remove()
+ node
+
+ catchQuery: (e) ->
+ return unless range = @_getRange()
+ return unless range.collapsed
+
+ if e.which == KEY_CODE.ENTER
+ ($query = $(range.startContainer).closest '.atwho-query')
+ .contents().unwrap()
+ $query.remove() if $query.is ':empty'
+ ($query = $ ".atwho-query", @app.document)
+ .text $query.text()
+ .contents().last().unwrap()
+ @_clearRange()
+ return
+
+ # absorb range
+ # The range at the end of an element is not inside in firefox but not others browsers including IE.
+ # To normolize them, we have to move the range inside the element while deleting content or moving caret right after .atwho-inserted
+ if /firefox/i.test(navigator.userAgent)
+ if $(range.startContainer).is @$inputor
+ @_clearRange()
+ return
+ if e.which == KEY_CODE.BACKSPACE and range.startContainer.nodeType == document.ELEMENT_NODE \
+ and (offset = range.startOffset - 1) >= 0
+ _range = range.cloneRange()
+ _range.setStart range.startContainer, offset
+ if $(_range.cloneContents()).contents().last().is '.atwho-inserted'
+ inserted = $(range.startContainer).contents().get(offset)
+ @_setRange 'after', $(inserted).contents().last()
+ else if e.which == KEY_CODE.LEFT and range.startContainer.nodeType == document.TEXT_NODE
+ $inserted = $ range.startContainer.previousSibling
+ if $inserted.is('.atwho-inserted') and range.startOffset == 0
+ @_setRange 'after', $inserted.contents().last()
+
+ # modifying inserted element
+ $(range.startContainer)
+ .closest '.atwho-inserted'
+ .addClass 'atwho-query'
+ .siblings().removeClass 'atwho-query'
+
+ if ($query = $ ".atwho-query", @app.document).length > 0 \
+ and $query.is(':empty') and $query.text().length == 0
+ $query.remove()
+
+ if not @_movingEvent e
+ $query.removeClass 'atwho-inserted'
+
+ if $query.length > 0
+ switch e.which
+ when KEY_CODE.LEFT
+ @_setRange 'before', $query.get(0), range
+ $query.removeClass 'atwho-query'
+ return
+ when KEY_CODE.RIGHT
+ @_setRange 'after', $query.get(0).nextSibling, range
+ $query.removeClass 'atwho-query'
+ return
+
+ # matching
+ if $query.length > 0 and query_content = $query.attr('data-atwho-at-query')
+ $query.empty().html(query_content).attr('data-atwho-at-query', null)
+ @_setRange 'after', $query.get(0), range
+ _range = range.cloneRange()
+ _range.setStart range.startContainer, 0
+ matched = @callbacks("matcher").call(this, @at, _range.toString(), @getOpt('startWithSpace'), @getOpt("acceptSpaceBar"))
+ isString = typeof matched is 'string'
+
+ # wrapping query with .atwho-query
+ if $query.length == 0 and isString \
+ and (index = range.startOffset - @at.length - matched.length) >= 0
+ range.setStart range.startContainer, index
+ $query = $ '', @app.document
+ .attr @getOpt "editableAtwhoQueryAttrs"
+ .addClass 'atwho-query'
+ range.surroundContents $query.get 0
+ lastNode = $query.contents().last().get(0)
+ if lastNode
+ if /firefox/i.test navigator.userAgent
+ range.setStart lastNode, lastNode.length
+ range.setEnd lastNode, lastNode.length
+ @_clearRange range
+ else
+ @_setRange 'after', lastNode, range
+
+ return if isString and matched.length < @getOpt('minLen', 0)
+
+ # handle the matched result
+ if isString and matched.length <= @getOpt('maxLen', 20)
+ query = text: matched, el: $query
+ @trigger "matched", [@at, query.text]
+ @query = query
+ else
+ @view.hide()
+ @query = el: $query
+ if $query.text().indexOf(this.at) >= 0
+ if @_movingEvent(e) and $query.hasClass 'atwho-inserted'
+ $query.removeClass('atwho-query')
+ else if false != @callbacks('afterMatchFailed').call this, @at, $query
+ @_setRange "after", @_unwrap $query.text($query.text()).contents().first()
+ null
+
+ # Get offset of current at char(`flag`)
+ #
+ # @return [Hash] the offset which look likes this: {top: y, left: x, bottom: bottom}
+ rect: ->
+ rect = @query.el.offset()
+ # do not use {top: 0, left: 0} from jQuery when element is hidden
+ # happens every other time the menu is displayed on click in contenteditable
+ return unless rect and @query.el[0].getClientRects().length
+ if @app.iframe and not @app.iframeAsRoot
+ iframeOffset = ($iframe = $ @app.iframe).offset()
+ rect.left += iframeOffset.left - @$inputor.scrollLeft()
+ rect.top += iframeOffset.top - @$inputor.scrollTop()
+ rect.bottom = rect.top + @query.el.height()
+ rect
+
+ # Insert value of `data-value` attribute of chosen item into inputor
+ #
+ # @param content [String] string to insert
+ insert: (content, $li) ->
+ @$inputor.focus() unless @$inputor.is ':focus'
+ overrides = @getOpt 'functionOverrides'
+ if overrides.insert
+ return overrides.insert.call this, content, $li
+ suffix = if (suffix = @getOpt 'suffix') == "" then suffix else suffix or "\u00A0"
+ data = $li.data('item-data')
+ @query.el
+ .removeClass 'atwho-query'
+ .addClass 'atwho-inserted'
+ .html content
+ .attr 'data-atwho-at-query', "" + data['atwho-at'] + @query.text
+ .attr 'contenteditable', "false"
+ if range = @_getRange()
+ if @query.el.length
+ range.setEndAfter @query.el[0]
+ range.collapse false
+ range.insertNode suffixNode = @app.document.createTextNode "" + suffix
+ @_setRange 'after', suffixNode, range
+ @$inputor.focus() unless @$inputor.is ':focus'
+ @$inputor.change()
diff --git a/apps/comments/js/vendor/At.js/src/jquery.atwho.css b/apps/comments/js/vendor/At.js/src/jquery.atwho.css
new file mode 100644
index 0000000000..dad94ed964
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/src/jquery.atwho.css
@@ -0,0 +1,72 @@
+.atwho-view {
+ position:absolute;
+ top: 0;
+ left: 0;
+ display: none;
+ margin-top: 18px;
+ background: white;
+ color: black;
+ border: 1px solid #DDD;
+ border-radius: 3px;
+ box-shadow: 0 0 5px rgba(0,0,0,0.1);
+ min-width: 120px;
+ z-index: 11110 !important;
+}
+
+.atwho-view .atwho-header {
+ padding: 5px;
+ margin: 5px;
+ cursor: pointer;
+ border-bottom: solid 1px #eaeff1;
+ color: #6f8092;
+ font-size: 11px;
+ font-weight: bold;
+}
+
+.atwho-view .atwho-header .small {
+ color: #6f8092;
+ float: right;
+ padding-top: 2px;
+ margin-right: -5px;
+ font-size: 12px;
+ font-weight: normal;
+}
+
+.atwho-view .atwho-header:hover {
+ cursor: default;
+}
+
+.atwho-view .cur {
+ background: #3366FF;
+ color: white;
+}
+.atwho-view .cur small {
+ color: white;
+}
+.atwho-view strong {
+ color: #3366FF;
+}
+.atwho-view .cur strong {
+ color: white;
+ font:bold;
+}
+.atwho-view ul {
+ /* width: 100px; */
+ list-style:none;
+ padding:0;
+ margin:auto;
+ max-height: 200px;
+ overflow-y: auto;
+}
+.atwho-view ul li {
+ display: block;
+ padding: 5px 10px;
+ border-bottom: 1px solid #DDD;
+ cursor: pointer;
+ /* border-top: 1px solid #C8C8C8; */
+}
+.atwho-view small {
+ font-size: smaller;
+ color: #777;
+ font-weight: normal;
+}
diff --git a/apps/comments/js/vendor/At.js/src/model.coffee b/apps/comments/js/vendor/At.js/src/model.coffee
new file mode 100644
index 0000000000..7d09fa7943
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/src/model.coffee
@@ -0,0 +1,59 @@
+# Class to process data
+class Model
+
+ constructor: (@context) ->
+ @at = @context.at
+ # NOTE: bind data storage to inputor maybe App class can handle it.
+ @storage = @context.$inputor
+
+ destroy: ->
+ @storage.data(@at, null)
+
+ saved: ->
+ this.fetch() > 0
+
+ # fetch data from storage by query.
+ # will invoke `callback` to return data
+ #
+ # @param query [String] catched string for searching
+ # @param callback [Function] for receiving data
+ query: (query, callback) ->
+ data = this.fetch()
+ searchKey = @context.getOpt("searchKey")
+ data = @context.callbacks('filter').call(@context, query, data, searchKey) || []
+ _remoteFilter = @context.callbacks('remoteFilter')
+ if data.length > 0 or (!_remoteFilter and data.length == 0)
+ callback data
+ else
+ _remoteFilter.call(@context, query, callback)
+
+ # get or set current data which would be shown on the list view.
+ #
+ # @param data [Array] set data
+ # @return [Array|undefined] current data that are showing on the list view.
+ fetch: ->
+ @storage.data(@at) || []
+
+ # save special flag's data to storage
+ #
+ # @param data [Array] data to save
+ save: (data) ->
+ @storage.data @at, @context.callbacks("beforeSave").call(@context, data || [])
+
+ # load data. It wouldn't load for a second time if it has been loaded.
+ #
+ # @param data [Array] data to load
+ load: (data) ->
+ this._load(data) unless this.saved() or not data
+
+ reload: (data) ->
+ this._load(data)
+
+ # load data from local or remote with callback
+ #
+ # @param data [Array|String] data to load.
+ _load: (data) ->
+ if typeof data is "string"
+ $.ajax(data, dataType: "json").done (data) => this.save(data)
+ else
+ this.save data
diff --git a/apps/comments/js/vendor/At.js/src/textareaController.coffee b/apps/comments/js/vendor/At.js/src/textareaController.coffee
new file mode 100644
index 0000000000..95f94bf8dc
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/src/textareaController.coffee
@@ -0,0 +1,51 @@
+class TextareaController extends Controller
+ # Catch query string behind the at char
+ #
+ # @return [Hash] Info of the query. Look likes this: {'text': "hello", 'headPos': 0, 'endPos': 0}
+ catchQuery: ->
+ content = @$inputor.val()
+ caretPos = @$inputor.caret('pos', {iframe: @app.iframe})
+ subtext = content.slice(0, caretPos)
+ query = this.callbacks("matcher").call(this, @at, subtext, this.getOpt('startWithSpace'), @getOpt("acceptSpaceBar"))
+ isString = typeof query is 'string'
+
+ return if isString and query.length < this.getOpt('minLen', 0)
+
+ if isString and query.length <= this.getOpt('maxLen', 20)
+ start = caretPos - query.length
+ end = start + query.length
+ @pos = start
+ query = {'text': query, 'headPos': start, 'endPos': end}
+ this.trigger "matched", [@at, query.text]
+ else
+ query = null
+ @view.hide()
+
+ @query = query
+
+ # Get offset of current at char(`flag`)
+ #
+ # @return [Hash] the offset which look likes this: {top: y, left: x, bottom: bottom}
+ rect: ->
+ return if not c = @$inputor.caret('offset', @pos - 1, {iframe: @app.iframe})
+ if @app.iframe and not @app.iframeAsRoot
+ iframeOffset = $(@app.iframe).offset()
+ c.left += iframeOffset.left
+ c.top += iframeOffset.top
+ scaleBottom = if @app.document.selection then 0 else 2
+ {left: c.left, top: c.top, bottom: c.top + c.height + scaleBottom}
+
+ # Insert value of `data-value` attribute of chosen item into inputor
+ #
+ # @param content [String] string to insert
+ insert: (content, $li) ->
+ $inputor = @$inputor
+ source = $inputor.val()
+ startStr = source.slice 0, Math.max(@query.headPos - @at.length, 0)
+ suffix = if (suffix = @getOpt 'suffix') == "" then suffix else suffix or " "
+ content += suffix
+ text = "#{startStr}#{content}#{source.slice @query['endPos'] || 0}"
+ $inputor.val text
+ $inputor.caret('pos', startStr.length + content.length, {iframe: @app.iframe})
+ $inputor.focus() unless $inputor.is ':focus'
+ $inputor.change()
diff --git a/apps/comments/js/vendor/At.js/src/view.coffee b/apps/comments/js/vendor/At.js/src/view.coffee
new file mode 100644
index 0000000000..08785d7373
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/src/view.coffee
@@ -0,0 +1,136 @@
+# View class to control how At.js's view showing.
+# All classes share the same DOM view.
+class View
+
+ # @param controller [Object] The Controller.
+ constructor: (@context) ->
+ @$el = $("")
+ @$elUl = @$el.children();
+ @timeoutID = null
+ # create HTML DOM of list view if it does not exist
+ @context.$el.append(@$el)
+ this.bindEvent()
+
+ init: ->
+ id = @context.getOpt("alias") || @context.at.charCodeAt(0)
+ header_tpl = this.context.getOpt("headerTpl")
+ if (header_tpl && this.$el.children().length == 1)
+ this.$el.prepend(header_tpl)
+ @$el.attr('id': "at-view-#{id}")
+
+ destroy: ->
+ @$el.remove()
+
+ bindEvent: ->
+ $menu = @$el.find('ul')
+ lastCoordX = 0
+ lastCoordY = 0
+ $menu.on 'mousemove.atwho-view','li', (e) =>
+ # If the mouse hasn't actually moved then exit.
+ return if lastCoordX == e.clientX and lastCoordY == e.clientY
+ lastCoordX = e.clientX
+ lastCoordY = e.clientY
+ $cur = $(e.currentTarget)
+ return if $cur.hasClass('cur')
+ $menu.find('.cur').removeClass 'cur'
+ $cur.addClass 'cur'
+ .on 'click.atwho-view', 'li', (e) =>
+ $menu.find('.cur').removeClass 'cur'
+ $(e.currentTarget).addClass 'cur'
+ this.choose(e)
+ e.preventDefault()
+
+ # Check if view is visible
+ #
+ # @return [Boolean]
+ visible: ->
+ $.expr.filters.visible(@$el[0])
+
+ highlighted: ->
+ @$el.find(".cur").length > 0
+
+ choose: (e) ->
+ if ($li = @$el.find ".cur").length
+ content = @context.insertContentFor $li
+
+ @context._stopDelayedCall()
+ @context.insert @context.callbacks("beforeInsert").call(@context, content, $li, e), $li
+ @context.trigger "inserted", [$li, e]
+ this.hide(e)
+ @stopShowing = yes if @context.getOpt("hideWithoutSuffix")
+
+ reposition: (rect) ->
+ _window = if @context.app.iframeAsRoot then @context.app.window else window
+ if rect.bottom + @$el.height() - $(_window).scrollTop() > $(_window).height()
+ rect.bottom = rect.top - @$el.height()
+ if rect.left > overflowOffset = $(_window).width() - @$el.width() - 5
+ rect.left = overflowOffset
+ offset = {left:rect.left, top:rect.bottom}
+ @context.callbacks("beforeReposition")?.call(@context, offset)
+ @$el.offset offset
+ @context.trigger "reposition", [offset]
+
+ next: ->
+ cur = @$el.find('.cur').removeClass('cur')
+ next = cur.next()
+ next = @$el.find('li:first') if not next.length
+ next.addClass 'cur'
+ nextEl = next[0]
+ offset = nextEl.offsetTop + nextEl.offsetHeight + (if nextEl.nextSibling then nextEl.nextSibling.offsetHeight else 0)
+ @scrollTop Math.max(0, offset - this.$el.height())
+
+ prev: ->
+ cur = @$el.find('.cur').removeClass('cur')
+ prev = cur.prev()
+ prev = @$el.find('li:last') if not prev.length
+ prev.addClass 'cur'
+ prevEl = prev[0]
+ offset = prevEl.offsetTop + prevEl.offsetHeight + (if prevEl.nextSibling then prevEl.nextSibling.offsetHeight else 0)
+ @scrollTop Math.max(0, offset - this.$el.height())
+
+ scrollTop: (scrollTop) ->
+ scrollDuration = @context.getOpt('scrollDuration')
+ if scrollDuration
+ @$elUl.animate {scrollTop: scrollTop}, scrollDuration
+ else
+ @$elUl.scrollTop(scrollTop)
+
+ show: ->
+ if @stopShowing
+ @stopShowing = false
+ return
+ if not this.visible()
+ @$el.show()
+ @$el.scrollTop 0
+ @context.trigger 'shown'
+ this.reposition(rect) if rect = @context.rect()
+
+ hide: (e, time) ->
+ return if not this.visible()
+ if isNaN(time)
+ @$el.hide()
+ @context.trigger 'hidden', [e]
+ else
+ callback = => this.hide()
+ clearTimeout @timeoutID
+ @timeoutID = setTimeout callback, time
+
+ # render list view
+ render: (list) ->
+ if not ($.isArray(list) and list.length > 0)
+ this.hide()
+ return
+
+ @$el.find('ul').empty()
+ $ul = @$el.find('ul')
+ tpl = @context.getOpt('displayTpl')
+
+ for item in list
+ item = $.extend {}, item, {'atwho-at': @context.at}
+ li = @context.callbacks("tplEval").call(@context, tpl, item, "onDisplay")
+ $li = $ @context.callbacks("highlighter").call(@context, li, @context.query.text)
+ $li.data("item-data", item)
+ $ul.append $li
+
+ this.show()
+ $ul.find("li:first").addClass "cur" if @context.getOpt('highlightFirst')
diff --git a/apps/comments/js/vendor/At.js/umd.template.js b/apps/comments/js/vendor/At.js/umd.template.js
new file mode 100644
index 0000000000..9df3d0b5d6
--- /dev/null
+++ b/apps/comments/js/vendor/At.js/umd.template.js
@@ -0,0 +1,17 @@
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module unless amdModuleId is set
+ define(["jquery"], function (a0) {
+ return (factory(a0));
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+}(this, function ($) {
+<%= contents %>
+}));
diff --git a/apps/comments/js/vendor/Caret.js/.bower.json b/apps/comments/js/vendor/Caret.js/.bower.json
new file mode 100644
index 0000000000..dae9e91f6a
--- /dev/null
+++ b/apps/comments/js/vendor/Caret.js/.bower.json
@@ -0,0 +1,30 @@
+{
+ "name": "Caret.js",
+ "version": "0.2.2",
+ "main": "src/jquery.caret.js",
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "test",
+ "tests",
+ "spec",
+ "index.html"
+ ],
+ "dependencies": {
+ "jquery": ">=1.7.0"
+ },
+ "devDependencies": {
+ "jasmine-jquery": "~1.5.8"
+ },
+ "homepage": "https://github.com/ichord/Caret.js",
+ "_release": "0.2.2",
+ "_resolution": {
+ "type": "version",
+ "tag": "v0.2.2",
+ "commit": "b435c7049f1bce1c4db10526d956e24e8b484a52"
+ },
+ "_source": "https://github.com/ichord/Caret.js.git",
+ "_target": "~0.2.2",
+ "_originalSource": "Caret.js"
+}
\ No newline at end of file
diff --git a/apps/comments/js/vendor/Caret.js/CHANGELOG.md b/apps/comments/js/vendor/Caret.js/CHANGELOG.md
new file mode 100644
index 0000000000..2b127e4433
--- /dev/null
+++ b/apps/comments/js/vendor/Caret.js/CHANGELOG.md
@@ -0,0 +1,52 @@
+### v0.2.1
+
+* f66a1eb - can get offset at the benginning of a line
+* 4885ddd - fix wrong position of textarea
+
+### v0.2.0
+
+* 12119d2 - calculating in iframe's coordinate
+* 959436d - implement `position` api for contentEditable
+* d051ffc - fix html escaping while mirroring caret
+
+### v0.1.0
+
+* b1f8f53 - fix Mirror div does not reset its CSS
+* e88e40e - fix Bad positioning in long words
+* 37d4c5e - disable auto decovery iframe
+
+### v0.0.7
+
+* cf94271 - Added suport for .caret(pos, 0) - Nicolas Donna
+* 2de2b0f - Fixed error when checking the pos arg when setting the position - Nicolas Donna
+* 34ac7fa - catch error thrown in cross-domain iframe - jiyinyiyong
+
+* 01f1fa1 - add minified file in dist.
+
+### v0.0.6
+
+* 287b5d8 working in iframe
+
+### v0.0.5
+
+* aef0aa4 fix IE input position error
+* 4a4f7f7 fix contenteditable null value bug
+
+### v0.0.4
+
+* fix scrolling problem
+
+### v0.0.2
+
+* support `contentEditable` mode
+* fix ie bugs, and support IE > 6 to all mode
+
+### 2013-08-07
+
+* fix bug: error position at beginning of textarea
+* Bower
+* jasmine test
+
+### 2013-03-31
+
+* support IE browsers.
diff --git a/apps/comments/js/vendor/Caret.js/Gruntfile.coffee b/apps/comments/js/vendor/Caret.js/Gruntfile.coffee
new file mode 100644
index 0000000000..a645110b59
--- /dev/null
+++ b/apps/comments/js/vendor/Caret.js/Gruntfile.coffee
@@ -0,0 +1,79 @@
+module.exports = (grunt) ->
+ grunt.initConfig
+ pkg: grunt.file.readJSON 'package.json'
+ bower_path: 'bower_components'
+
+ jasmine:
+ src: 'src/*.js'
+ options:
+ vendor: [
+ '<%= bower_path %>/jquery/dist/jquery.min.js',
+ '<%= bower_path %>/jasmine-jquery/lib/jasmine-jquery.js'
+ ]
+ specs: 'spec/javascripts/*.js'
+ # keepRunner: true
+
+ uglify:
+ options:
+ banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
+ build:
+ files:
+ 'dist/<%= pkg.name %>.min.js': ['src/<%= pkg.name %>.js']
+
+ coffee:
+ withMaps:
+ options:
+ bare: true
+ sourceMap: true
+ files:
+ 'src/<%= pkg.name %>.js': 'src/<%= pkg.name %>.coffee'
+ withoutMaps:
+ options:
+ bare: true
+ sourceMap: false
+ files:
+ 'dist/<%= pkg.name %>.js': 'src/<%= pkg.name %>.coffee'
+
+ watch:
+ scripts:
+ files: ['src/*.coffee']
+ tasks: ['coffee', 'umd']
+
+ umd:
+ options:
+ template: 'umd'
+ deps:
+ 'default': ['$']
+ amd: ['jquery']
+ cjs: ['jquery']
+ global:
+ items: ['jQuery']
+ prefix: ''
+ src:
+ src: 'src/<%= pkg.name %>.js'
+ dist:
+ src: 'dist/<%= pkg.name %>.js'
+
+
+ 'json-replace':
+ options:
+ space: " ",
+ replace:
+ version: "<%= pkg.version %>"
+ 'update-version':
+ files:[{
+ 'bower.json': 'bower.json',
+ 'component.json': 'component.json'
+ }]
+
+ grunt.loadNpmTasks 'grunt-contrib-coffee'
+ grunt.loadNpmTasks 'grunt-contrib-uglify'
+ grunt.loadNpmTasks 'grunt-contrib-jasmine'
+ grunt.loadNpmTasks 'grunt-json-replace'
+ grunt.loadNpmTasks 'grunt-contrib-watch'
+ grunt.loadNpmTasks 'grunt-umd'
+
+ grunt.registerTask 'update-version', 'json-replace'
+
+ grunt.registerTask 'default', ['coffee', 'umd', 'jasmine','update-version', 'uglify', 'watch']
+ grunt.registerTask 'test', ['coffee', 'umd', 'jasmine']
diff --git a/apps/comments/js/vendor/Caret.js/LICENSE-MIT b/apps/comments/js/vendor/Caret.js/LICENSE-MIT
new file mode 100644
index 0000000000..36cd1c1227
--- /dev/null
+++ b/apps/comments/js/vendor/Caret.js/LICENSE-MIT
@@ -0,0 +1,22 @@
+Copyright (c) 2013 chord.luo@gmail.com
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/apps/comments/js/vendor/Caret.js/README.md b/apps/comments/js/vendor/Caret.js/README.md
new file mode 100644
index 0000000000..ececb2cc2f
--- /dev/null
+++ b/apps/comments/js/vendor/Caret.js/README.md
@@ -0,0 +1,54 @@
+Caret.js
+========
+
+Get caret postion or offset from inputor
+
+This is the core function that working in [At.js](http://ichord.github.com/At.js).
+Now, It just become an simple jquery plugin so that everybody can use it.
+And, of course, **At.js** is using this plugin too.
+
+* support iframe context
+
+Live Demo
+=========
+
+http://ichord.github.com/Caret.js/
+
+
+Usage
+=====
+
+```javascript
+
+// Get caret position
+$('#inputor').caret('position'); // => {left: 15, top: 30, height: 20}
+
+// Get caret offset
+$('#inputor').caret('offset'); // => {left: 300, top: 400, height: 20}
+
+var fixPos = 20
+// Get position of the 20th char in the inputor.
+// not working in `contentEditable` mode
+$('#inputor').caret('position', fixPos);
+
+// Get offset of the 20th char.
+// not working in `contentEditable` mode
+$('#inputor').caret('offset', fixPos);
+
+// more
+
+// Get caret position from the first char in the inputor.
+$('#inputor').caret('pos'); // => 15
+
+// Set caret position in the inputor
+// not working in contentEditable mode
+$('#inputor').caret('pos', 15);
+
+// set iframe context
+// NOTE: Related to the iframe's cooridinate.
+// You might want to get the iframe's offset/position on your own
+$('#inputor').caret('offset', {iframe: theIframe});
+$('#inputor').caret('position', {iframe: theIframe});
+$('#inputor').caret('pos', 15, {iframe: theIframe});
+
+```
diff --git a/apps/comments/js/vendor/Caret.js/bower.json b/apps/comments/js/vendor/Caret.js/bower.json
new file mode 100644
index 0000000000..c732905da1
--- /dev/null
+++ b/apps/comments/js/vendor/Caret.js/bower.json
@@ -0,0 +1,20 @@
+{
+ "name": "Caret.js",
+ "version": "0.2.2",
+ "main": "src/jquery.caret.js",
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "test",
+ "tests",
+ "spec",
+ "index.html"
+ ],
+ "dependencies": {
+ "jquery": ">=1.7.0"
+ },
+ "devDependencies": {
+ "jasmine-jquery": "~1.5.8"
+ }
+}
\ No newline at end of file
diff --git a/apps/comments/js/vendor/Caret.js/component.json b/apps/comments/js/vendor/Caret.js/component.json
new file mode 100644
index 0000000000..b81815e5dc
--- /dev/null
+++ b/apps/comments/js/vendor/Caret.js/component.json
@@ -0,0 +1,20 @@
+{
+ "name": "Caret.js",
+ "repo": "ichord/Caret.js",
+ "description": "Add Github like mentions autocomplete to your application.",
+ "version": "0.2.2",
+ "keywords": [
+ "At.js",
+ "caret",
+ "ui"
+ ],
+ "dependencies": {
+ "component/jquery": "*"
+ },
+ "demo": "http://ichord.github.com/Caret.js",
+ "main": "src/jquery.caret.js",
+ "scripts": [
+ "src/jquery.caret.js"
+ ],
+ "license": "MIT"
+}
\ No newline at end of file
diff --git a/apps/comments/js/vendor/Caret.js/dist/jquery.caret.js b/apps/comments/js/vendor/Caret.js/dist/jquery.caret.js
new file mode 100644
index 0000000000..e0de4bc824
--- /dev/null
+++ b/apps/comments/js/vendor/Caret.js/dist/jquery.caret.js
@@ -0,0 +1,405 @@
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(["jquery"], function ($) {
+ return (root.returnExportsGlobal = factory($));
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like enviroments that support module.exports,
+ // like Node.
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+}(this, function ($) {
+
+/*
+ Implement Github like autocomplete mentions
+ http://ichord.github.com/At.js
+
+ Copyright (c) 2013 chord.luo@gmail.com
+ Licensed under the MIT license.
+*/
+
+/*
+本插件操作 textarea 或者 input 内的插入符
+只实现了获得插入符在文本框中的位置,我设置
+插入符的位置.
+*/
+
+"use strict";
+var EditableCaret, InputCaret, Mirror, Utils, discoveryIframeOf, methods, oDocument, oFrame, oWindow, pluginName, setContextBy;
+
+pluginName = 'caret';
+
+EditableCaret = (function() {
+ function EditableCaret($inputor) {
+ this.$inputor = $inputor;
+ this.domInputor = this.$inputor[0];
+ }
+
+ EditableCaret.prototype.setPos = function(pos) {
+ return this.domInputor;
+ };
+
+ EditableCaret.prototype.getIEPosition = function() {
+ return this.getPosition();
+ };
+
+ EditableCaret.prototype.getPosition = function() {
+ var inputor_offset, offset;
+ offset = this.getOffset();
+ inputor_offset = this.$inputor.offset();
+ offset.left -= inputor_offset.left;
+ offset.top -= inputor_offset.top;
+ return offset;
+ };
+
+ EditableCaret.prototype.getOldIEPos = function() {
+ var preCaretTextRange, textRange;
+ textRange = oDocument.selection.createRange();
+ preCaretTextRange = oDocument.body.createTextRange();
+ preCaretTextRange.moveToElementText(this.domInputor);
+ preCaretTextRange.setEndPoint("EndToEnd", textRange);
+ return preCaretTextRange.text.length;
+ };
+
+ EditableCaret.prototype.getPos = function() {
+ var clonedRange, pos, range;
+ if (range = this.range()) {
+ clonedRange = range.cloneRange();
+ clonedRange.selectNodeContents(this.domInputor);
+ clonedRange.setEnd(range.endContainer, range.endOffset);
+ pos = clonedRange.toString().length;
+ clonedRange.detach();
+ return pos;
+ } else if (oDocument.selection) {
+ return this.getOldIEPos();
+ }
+ };
+
+ EditableCaret.prototype.getOldIEOffset = function() {
+ var range, rect;
+ range = oDocument.selection.createRange().duplicate();
+ range.moveStart("character", -1);
+ rect = range.getBoundingClientRect();
+ return {
+ height: rect.bottom - rect.top,
+ left: rect.left,
+ top: rect.top
+ };
+ };
+
+ EditableCaret.prototype.getOffset = function(pos) {
+ var clonedRange, offset, range, rect, shadowCaret;
+ if (oWindow.getSelection && (range = this.range())) {
+ if (range.endOffset - 1 > 0 && range.endContainer === !this.domInputor) {
+ clonedRange = range.cloneRange();
+ clonedRange.setStart(range.endContainer, range.endOffset - 1);
+ clonedRange.setEnd(range.endContainer, range.endOffset);
+ rect = clonedRange.getBoundingClientRect();
+ offset = {
+ height: rect.height,
+ left: rect.left + rect.width,
+ top: rect.top
+ };
+ clonedRange.detach();
+ }
+ if (!offset || (offset != null ? offset.height : void 0) === 0) {
+ clonedRange = range.cloneRange();
+ shadowCaret = $(oDocument.createTextNode("|"));
+ clonedRange.insertNode(shadowCaret[0]);
+ clonedRange.selectNode(shadowCaret[0]);
+ rect = clonedRange.getBoundingClientRect();
+ offset = {
+ height: rect.height,
+ left: rect.left,
+ top: rect.top
+ };
+ shadowCaret.remove();
+ clonedRange.detach();
+ }
+ } else if (oDocument.selection) {
+ offset = this.getOldIEOffset();
+ }
+ if (offset) {
+ offset.top += $(oWindow).scrollTop();
+ offset.left += $(oWindow).scrollLeft();
+ }
+ return offset;
+ };
+
+ EditableCaret.prototype.range = function() {
+ var sel;
+ if (!oWindow.getSelection) {
+ return;
+ }
+ sel = oWindow.getSelection();
+ if (sel.rangeCount > 0) {
+ return sel.getRangeAt(0);
+ } else {
+ return null;
+ }
+ };
+
+ return EditableCaret;
+
+})();
+
+InputCaret = (function() {
+ function InputCaret($inputor) {
+ this.$inputor = $inputor;
+ this.domInputor = this.$inputor[0];
+ }
+
+ InputCaret.prototype.getIEPos = function() {
+ var endRange, inputor, len, normalizedValue, pos, range, textInputRange;
+ inputor = this.domInputor;
+ range = oDocument.selection.createRange();
+ pos = 0;
+ if (range && range.parentElement() === inputor) {
+ normalizedValue = inputor.value.replace(/\r\n/g, "\n");
+ len = normalizedValue.length;
+ textInputRange = inputor.createTextRange();
+ textInputRange.moveToBookmark(range.getBookmark());
+ endRange = inputor.createTextRange();
+ endRange.collapse(false);
+ if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
+ pos = len;
+ } else {
+ pos = -textInputRange.moveStart("character", -len);
+ }
+ }
+ return pos;
+ };
+
+ InputCaret.prototype.getPos = function() {
+ if (oDocument.selection) {
+ return this.getIEPos();
+ } else {
+ return this.domInputor.selectionStart;
+ }
+ };
+
+ InputCaret.prototype.setPos = function(pos) {
+ var inputor, range;
+ inputor = this.domInputor;
+ if (oDocument.selection) {
+ range = inputor.createTextRange();
+ range.move("character", pos);
+ range.select();
+ } else if (inputor.setSelectionRange) {
+ inputor.setSelectionRange(pos, pos);
+ }
+ return inputor;
+ };
+
+ InputCaret.prototype.getIEOffset = function(pos) {
+ var h, textRange, x, y;
+ textRange = this.domInputor.createTextRange();
+ pos || (pos = this.getPos());
+ textRange.move('character', pos);
+ x = textRange.boundingLeft;
+ y = textRange.boundingTop;
+ h = textRange.boundingHeight;
+ return {
+ left: x,
+ top: y,
+ height: h
+ };
+ };
+
+ InputCaret.prototype.getOffset = function(pos) {
+ var $inputor, offset, position;
+ $inputor = this.$inputor;
+ if (oDocument.selection) {
+ offset = this.getIEOffset(pos);
+ offset.top += $(oWindow).scrollTop() + $inputor.scrollTop();
+ offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft();
+ return offset;
+ } else {
+ offset = $inputor.offset();
+ position = this.getPosition(pos);
+ return offset = {
+ left: offset.left + position.left - $inputor.scrollLeft(),
+ top: offset.top + position.top - $inputor.scrollTop(),
+ height: position.height
+ };
+ }
+ };
+
+ InputCaret.prototype.getPosition = function(pos) {
+ var $inputor, at_rect, end_range, format, html, mirror, start_range;
+ $inputor = this.$inputor;
+ format = function(value) {
+ value = value.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g, "
");
+ if (/firefox/i.test(navigator.userAgent)) {
+ value = value.replace(/\s/g, ' ');
+ }
+ return value;
+ };
+ if (pos === void 0) {
+ pos = this.getPos();
+ }
+ start_range = $inputor.val().slice(0, pos);
+ end_range = $inputor.val().slice(pos);
+ html = "" + format(start_range) + "";
+ html += "|";
+ html += "" + format(end_range) + "";
+ mirror = new Mirror($inputor);
+ return at_rect = mirror.create(html).rect();
+ };
+
+ InputCaret.prototype.getIEPosition = function(pos) {
+ var h, inputorOffset, offset, x, y;
+ offset = this.getIEOffset(pos);
+ inputorOffset = this.$inputor.offset();
+ x = offset.left - inputorOffset.left;
+ y = offset.top - inputorOffset.top;
+ h = offset.height;
+ return {
+ left: x,
+ top: y,
+ height: h
+ };
+ };
+
+ return InputCaret;
+
+})();
+
+Mirror = (function() {
+ 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"];
+
+ function Mirror($inputor) {
+ this.$inputor = $inputor;
+ }
+
+ Mirror.prototype.mirrorCss = function() {
+ var css,
+ _this = this;
+ css = {
+ position: 'absolute',
+ left: -9999,
+ top: 0,
+ zIndex: -20000
+ };
+ if (this.$inputor.prop('tagName') === 'TEXTAREA') {
+ this.css_attr.push('width');
+ }
+ $.each(this.css_attr, function(i, p) {
+ return css[p] = _this.$inputor.css(p);
+ });
+ return css;
+ };
+
+ Mirror.prototype.create = function(html) {
+ this.$mirror = $('');
+ this.$mirror.css(this.mirrorCss());
+ this.$mirror.html(html);
+ this.$inputor.after(this.$mirror);
+ return this;
+ };
+
+ Mirror.prototype.rect = function() {
+ var $flag, pos, rect;
+ $flag = this.$mirror.find("#caret");
+ pos = $flag.position();
+ rect = {
+ left: pos.left,
+ top: pos.top,
+ height: $flag.height()
+ };
+ this.$mirror.remove();
+ return rect;
+ };
+
+ return Mirror;
+
+})();
+
+Utils = {
+ contentEditable: function($inputor) {
+ return !!($inputor[0].contentEditable && $inputor[0].contentEditable === 'true');
+ }
+};
+
+methods = {
+ pos: function(pos) {
+ if (pos || pos === 0) {
+ return this.setPos(pos);
+ } else {
+ return this.getPos();
+ }
+ },
+ position: function(pos) {
+ if (oDocument.selection) {
+ return this.getIEPosition(pos);
+ } else {
+ return this.getPosition(pos);
+ }
+ },
+ offset: function(pos) {
+ var offset;
+ offset = this.getOffset(pos);
+ return offset;
+ }
+};
+
+oDocument = null;
+
+oWindow = null;
+
+oFrame = null;
+
+setContextBy = function(settings) {
+ var iframe;
+ if (iframe = settings != null ? settings.iframe : void 0) {
+ oFrame = iframe;
+ oWindow = iframe.contentWindow;
+ return oDocument = iframe.contentDocument || oWindow.document;
+ } else {
+ oFrame = void 0;
+ oWindow = window;
+ return oDocument = document;
+ }
+};
+
+discoveryIframeOf = function($dom) {
+ var error;
+ oDocument = $dom[0].ownerDocument;
+ oWindow = oDocument.defaultView || oDocument.parentWindow;
+ try {
+ return oFrame = oWindow.frameElement;
+ } catch (_error) {
+ error = _error;
+ }
+};
+
+$.fn.caret = function(method, value, settings) {
+ var caret;
+ if (methods[method]) {
+ if ($.isPlainObject(value)) {
+ setContextBy(value);
+ value = void 0;
+ } else {
+ setContextBy(settings);
+ }
+ caret = Utils.contentEditable(this) ? new EditableCaret(this) : new InputCaret(this);
+ return methods[method].apply(caret, [value]);
+ } else {
+ return $.error("Method " + method + " does not exist on jQuery.caret");
+ }
+};
+
+$.fn.caret.EditableCaret = EditableCaret;
+
+$.fn.caret.InputCaret = InputCaret;
+
+$.fn.caret.Utils = Utils;
+
+$.fn.caret.apis = methods;
+
+
+}));
diff --git a/apps/comments/js/vendor/Caret.js/dist/jquery.caret.min.js b/apps/comments/js/vendor/Caret.js/dist/jquery.caret.min.js
new file mode 100644
index 0000000000..a4d02eae24
--- /dev/null
+++ b/apps/comments/js/vendor/Caret.js/dist/jquery.caret.min.js
@@ -0,0 +1,2 @@
+/*! jquery.caret 2015-02-01 */
+!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(c){return a.returnExportsGlobal=b(c)}):"object"==typeof exports?module.exports=b(require("jquery")):b(jQuery)}(this,function(a){"use strict";var b,c,d,e,f,g,h,i,j,k,l;k="caret",b=function(){function b(a){this.$inputor=a,this.domInputor=this.$inputor[0]}return b.prototype.setPos=function(){return this.domInputor},b.prototype.getIEPosition=function(){return this.getPosition()},b.prototype.getPosition=function(){var a,b;return b=this.getOffset(),a=this.$inputor.offset(),b.left-=a.left,b.top-=a.top,b},b.prototype.getOldIEPos=function(){var a,b;return b=h.selection.createRange(),a=h.body.createTextRange(),a.moveToElementText(this.domInputor),a.setEndPoint("EndToEnd",b),a.text.length},b.prototype.getPos=function(){var a,b,c;return(c=this.range())?(a=c.cloneRange(),a.selectNodeContents(this.domInputor),a.setEnd(c.endContainer,c.endOffset),b=a.toString().length,a.detach(),b):h.selection?this.getOldIEPos():void 0},b.prototype.getOldIEOffset=function(){var a,b;return a=h.selection.createRange().duplicate(),a.moveStart("character",-1),b=a.getBoundingClientRect(),{height:b.bottom-b.top,left:b.left,top:b.top}},b.prototype.getOffset=function(){var b,c,d,e,f;return j.getSelection&&(d=this.range())?(d.endOffset-1>0&&d.endContainer===!this.domInputor&&(b=d.cloneRange(),b.setStart(d.endContainer,d.endOffset-1),b.setEnd(d.endContainer,d.endOffset),e=b.getBoundingClientRect(),c={height:e.height,left:e.left+e.width,top:e.top},b.detach()),c&&0!==(null!=c?c.height:void 0)||(b=d.cloneRange(),f=a(h.createTextNode("|")),b.insertNode(f[0]),b.selectNode(f[0]),e=b.getBoundingClientRect(),c={height:e.height,left:e.left,top:e.top},f.remove(),b.detach())):h.selection&&(c=this.getOldIEOffset()),c&&(c.top+=a(j).scrollTop(),c.left+=a(j).scrollLeft()),c},b.prototype.range=function(){var a;if(j.getSelection)return a=j.getSelection(),a.rangeCount>0?a.getRangeAt(0):null},b}(),c=function(){function b(a){this.$inputor=a,this.domInputor=this.$inputor[0]}return b.prototype.getIEPos=function(){var a,b,c,d,e,f,g;return b=this.domInputor,f=h.selection.createRange(),e=0,f&&f.parentElement()===b&&(d=b.value.replace(/\r\n/g,"\n"),c=d.length,g=b.createTextRange(),g.moveToBookmark(f.getBookmark()),a=b.createTextRange(),a.collapse(!1),e=g.compareEndPoints("StartToEnd",a)>-1?c:-g.moveStart("character",-c)),e},b.prototype.getPos=function(){return h.selection?this.getIEPos():this.domInputor.selectionStart},b.prototype.setPos=function(a){var b,c;return b=this.domInputor,h.selection?(c=b.createTextRange(),c.move("character",a),c.select()):b.setSelectionRange&&b.setSelectionRange(a,a),b},b.prototype.getIEOffset=function(a){var b,c,d,e;return c=this.domInputor.createTextRange(),a||(a=this.getPos()),c.move("character",a),d=c.boundingLeft,e=c.boundingTop,b=c.boundingHeight,{left:d,top:e,height:b}},b.prototype.getOffset=function(b){var c,d,e;return c=this.$inputor,h.selection?(d=this.getIEOffset(b),d.top+=a(j).scrollTop()+c.scrollTop(),d.left+=a(j).scrollLeft()+c.scrollLeft(),d):(d=c.offset(),e=this.getPosition(b),d={left:d.left+e.left-c.scrollLeft(),top:d.top+e.top-c.scrollTop(),height:e.height})},b.prototype.getPosition=function(a){var b,c,e,f,g,h,i;return b=this.$inputor,f=function(a){return a=a.replace(/<|>|`|"|&/g,"?").replace(/\r\n|\r|\n/g,"
"),/firefox/i.test(navigator.userAgent)&&(a=a.replace(/\s/g," ")),a},void 0===a&&(a=this.getPos()),i=b.val().slice(0,a),e=b.val().slice(a),g=""+f(i)+"",g+="|",g+=""+f(e)+"",h=new d(b),c=h.create(g).rect()},b.prototype.getIEPosition=function(a){var b,c,d,e,f;return d=this.getIEOffset(a),c=this.$inputor.offset(),e=d.left-c.left,f=d.top-c.top,b=d.height,{left:e,top:f,height:b}},b}(),d=function(){function b(a){this.$inputor=a}return b.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"],b.prototype.mirrorCss=function(){var b,c=this;return b={position:"absolute",left:-9999,top:0,zIndex:-2e4},"TEXTAREA"===this.$inputor.prop("tagName")&&this.css_attr.push("width"),a.each(this.css_attr,function(a,d){return b[d]=c.$inputor.css(d)}),b},b.prototype.create=function(b){return this.$mirror=a(""),this.$mirror.css(this.mirrorCss()),this.$mirror.html(b),this.$inputor.after(this.$mirror),this},b.prototype.rect=function(){var a,b,c;return a=this.$mirror.find("#caret"),b=a.position(),c={left:b.left,top:b.top,height:a.height()},this.$mirror.remove(),c},b}(),e={contentEditable:function(a){return!(!a[0].contentEditable||"true"!==a[0].contentEditable)}},g={pos:function(a){return a||0===a?this.setPos(a):this.getPos()},position:function(a){return h.selection?this.getIEPosition(a):this.getPosition(a)},offset:function(a){var b;return b=this.getOffset(a)}},h=null,j=null,i=null,l=function(a){var b;return(b=null!=a?a.iframe:void 0)?(i=b,j=b.contentWindow,h=b.contentDocument||j.document):(i=void 0,j=window,h=document)},f=function(a){var b;h=a[0].ownerDocument,j=h.defaultView||h.parentWindow;try{return i=j.frameElement}catch(c){b=c}},a.fn.caret=function(d,f,h){var i;return g[d]?(a.isPlainObject(f)?(l(f),f=void 0):l(h),i=e.contentEditable(this)?new b(this):new c(this),g[d].apply(i,[f])):a.error("Method "+d+" does not exist on jQuery.caret")},a.fn.caret.EditableCaret=b,a.fn.caret.InputCaret=c,a.fn.caret.Utils=e,a.fn.caret.apis=g});
\ No newline at end of file
diff --git a/apps/comments/js/vendor/Caret.js/package.json b/apps/comments/js/vendor/Caret.js/package.json
new file mode 100644
index 0000000000..ee04f5a52b
--- /dev/null
+++ b/apps/comments/js/vendor/Caret.js/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "jquery.caret",
+ "version": "0.2.2",
+ "description": "Get caret position and offset from inputor",
+ "main": "index.js",
+ "dependencies": {
+ "grunt": "~0.4.1"
+ },
+ "devDependencies": {
+ "grunt-contrib-coffee": "~0.6.4",
+ "grunt-contrib-jasmine": "~0.5.1",
+ "grunt-contrib-uglify": "~0.2.0",
+ "grunt-contrib-watch": "^0.6.1",
+ "grunt-json-replace": "~0.1.2",
+ "grunt-umd": "^2.2.1"
+ },
+ "scripts": {
+ "test": "grunt test --verbose"
+ },
+ "repository": "",
+ "keywords": [
+ "jquery",
+ "caret",
+ "offset",
+ "position"
+ ],
+ "author": "Harold.luo ",
+ "license": "MIT"
+}
diff --git a/apps/comments/js/vendor/Caret.js/src/jquery.caret.coffee b/apps/comments/js/vendor/Caret.js/src/jquery.caret.coffee
new file mode 100644
index 0000000000..51f698d1ee
--- /dev/null
+++ b/apps/comments/js/vendor/Caret.js/src/jquery.caret.coffee
@@ -0,0 +1,311 @@
+###
+ Implement Github like autocomplete mentions
+ http://ichord.github.com/At.js
+
+ Copyright (c) 2013 chord.luo@gmail.com
+ Licensed under the MIT license.
+###
+
+###
+本插件操作 textarea 或者 input 内的插入符
+只实现了获得插入符在文本框中的位置,我设置
+插入符的位置.
+###
+"use strict";
+
+pluginName = 'caret'
+
+class EditableCaret
+ constructor: (@$inputor) ->
+ @domInputor = @$inputor[0]
+
+ # NOTE: Duck type
+ setPos: (pos) -> @domInputor
+ getIEPosition: -> this.getPosition()
+ getPosition: ->
+ offset = this.getOffset()
+ inputor_offset = @$inputor.offset()
+ offset.left -= inputor_offset.left
+ offset.top -= inputor_offset.top
+ offset
+
+ getOldIEPos: ->
+ textRange = oDocument.selection.createRange()
+ preCaretTextRange = oDocument.body.createTextRange()
+ preCaretTextRange.moveToElementText(@domInputor)
+ preCaretTextRange.setEndPoint("EndToEnd", textRange)
+ preCaretTextRange.text.length
+
+ getPos: ->
+ if range = this.range() # Major Browser and IE > 10
+ clonedRange = range.cloneRange()
+ clonedRange.selectNodeContents(@domInputor)
+ clonedRange.setEnd(range.endContainer, range.endOffset)
+ pos = clonedRange.toString().length
+ clonedRange.detach()
+ pos
+ else if oDocument.selection #IE < 9
+ this.getOldIEPos()
+
+ getOldIEOffset: ->
+ range = oDocument.selection.createRange().duplicate()
+ range.moveStart "character", -1
+ rect = range.getBoundingClientRect()
+ { height: rect.bottom - rect.top, left: rect.left, top: rect.top }
+
+ getOffset: (pos) ->
+ if oWindow.getSelection and range = this.range()
+ # endContainer would be the inputor in Firefox at the begnning of a line
+ if range.endOffset - 1 > 0 and range.endContainer is not @domInputor
+ clonedRange = range.cloneRange()
+ clonedRange.setStart(range.endContainer, range.endOffset - 1)
+ clonedRange.setEnd(range.endContainer, range.endOffset)
+ rect = clonedRange.getBoundingClientRect()
+ offset = { height: rect.height, left: rect.left + rect.width, top: rect.top }
+ clonedRange.detach()
+ # At the begnning of the inputor, the offset height is 0 in Chrome and Safari
+ # This work fine in all browers but except while the inputor break a line into two (wrapped line).
+ # so we can't use it in all cases.
+ if !offset or offset?.height == 0
+ clonedRange = range.cloneRange()
+ shadowCaret = $ oDocument.createTextNode "|"
+ clonedRange.insertNode shadowCaret[0]
+ clonedRange.selectNode shadowCaret[0]
+ rect = clonedRange.getBoundingClientRect()
+ offset = {height: rect.height, left: rect.left, top: rect.top }
+ shadowCaret.remove()
+ clonedRange.detach()
+ else if oDocument.selection # ie < 9
+ offset = this.getOldIEOffset()
+
+ if offset
+ offset.top += $(oWindow).scrollTop()
+ offset.left += $(oWindow).scrollLeft()
+
+ offset
+
+ range: ->
+ return unless oWindow.getSelection
+ sel = oWindow.getSelection()
+ if sel.rangeCount > 0 then sel.getRangeAt(0) else null
+
+
+class InputCaret
+
+ constructor: (@$inputor) ->
+ @domInputor = @$inputor[0]
+
+ getIEPos: ->
+ # https://github.com/ichord/Caret.js/wiki/Get-pos-of-caret-in-IE
+ inputor = @domInputor
+ range = oDocument.selection.createRange()
+ pos = 0
+ # selection should in the inputor.
+ if range and range.parentElement() is inputor
+ normalizedValue = inputor.value.replace /\r\n/g, "\n"
+ len = normalizedValue.length
+ textInputRange = inputor.createTextRange()
+ textInputRange.moveToBookmark range.getBookmark()
+ endRange = inputor.createTextRange()
+ endRange.collapse false
+ if textInputRange.compareEndPoints("StartToEnd", endRange) > -1
+ pos = len
+ else
+ pos = -textInputRange.moveStart "character", -len
+ pos
+
+ getPos: ->
+ if oDocument.selection then this.getIEPos() else @domInputor.selectionStart
+
+ setPos: (pos) ->
+ inputor = @domInputor
+ if oDocument.selection #IE
+ range = inputor.createTextRange()
+ range.move "character", pos
+ range.select()
+ else if inputor.setSelectionRange
+ inputor.setSelectionRange pos, pos
+ inputor
+
+ getIEOffset: (pos) ->
+ textRange = @domInputor.createTextRange()
+ pos ||= this.getPos()
+ textRange.move('character', pos)
+
+ x = textRange.boundingLeft
+ y = textRange.boundingTop
+ h = textRange.boundingHeight
+
+ {left: x, top: y, height: h}
+
+ getOffset: (pos) ->
+ $inputor = @$inputor
+ if oDocument.selection
+ offset = this.getIEOffset(pos)
+ offset.top += $(oWindow).scrollTop() + $inputor.scrollTop()
+ offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft()
+ offset
+ else
+ offset = $inputor.offset()
+ position = this.getPosition(pos)
+ offset =
+ left: offset.left + position.left - $inputor.scrollLeft()
+ top: offset.top + position.top - $inputor.scrollTop()
+ height: position.height
+
+ getPosition: (pos)->
+ $inputor = @$inputor
+ format = (value) ->
+ value = value.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g,"
")
+ if /firefox/i.test navigator.userAgent
+ value = value.replace(/\s/g, ' ')
+ value
+ pos = this.getPos() if pos is undefined
+ start_range = $inputor.val().slice(0, pos)
+ end_range = $inputor.val().slice(pos)
+ html = ""+format(start_range)+""
+ html += "|"
+ html += ""+format(end_range)+""
+
+ mirror = new Mirror($inputor)
+ at_rect = mirror.create(html).rect()
+
+ getIEPosition: (pos) ->
+ offset = this.getIEOffset pos
+ inputorOffset = @$inputor.offset()
+ x = offset.left - inputorOffset.left
+ y = offset.top - inputorOffset.top
+ h = offset.height
+
+ {left: x, top: y, height: h}
+
+# @example
+# mirror = new Mirror($("textarea#inputor"))
+# html = "We will get the rect of @icho
"
+# mirror.create(html).rect()
+class Mirror
+ 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",
+ ]
+
+ constructor: (@$inputor) ->
+
+ mirrorCss: ->
+ css =
+ position: 'absolute'
+ left: -9999
+ top: 0
+ zIndex: -20000
+ if @$inputor.prop( 'tagName' ) == 'TEXTAREA'
+ @css_attr.push( 'width' )
+ $.each @css_attr, (i,p) =>
+ css[p] = @$inputor.css p
+ css
+
+ create: (html) ->
+ @$mirror = $('')
+ @$mirror.css this.mirrorCss()
+ @$mirror.html(html)
+ @$inputor.after(@$mirror)
+ this
+
+ # 获得标记的位置
+ #
+ # @return [Object] 标记的坐标
+ # {left: 0, top: 0, bottom: 0}
+ rect: ->
+ $flag = @$mirror.find "#caret"
+ pos = $flag.position()
+ rect = {left: pos.left, top: pos.top, height: $flag.height() }
+ @$mirror.remove()
+ rect
+
+Utils =
+ contentEditable: ($inputor)->
+ !!($inputor[0].contentEditable && $inputor[0].contentEditable == 'true')
+
+methods =
+ pos: (pos) ->
+ if pos or pos == 0
+ this.setPos pos
+ else
+ this.getPos()
+
+ position: (pos) ->
+ if oDocument.selection then this.getIEPosition pos else this.getPosition pos
+
+ offset: (pos) ->
+ offset = this.getOffset(pos)
+ offset
+
+oDocument = null
+oWindow = null
+oFrame = null
+setContextBy = (settings) ->
+ if iframe = settings?.iframe
+ oFrame = iframe
+ oWindow = iframe.contentWindow
+ oDocument = iframe.contentDocument || oWindow.document
+ else
+ oFrame = undefined
+ oWindow = window
+ oDocument = document
+discoveryIframeOf = ($dom) ->
+ oDocument = $dom[0].ownerDocument
+ oWindow = oDocument.defaultView || oDocument.parentWindow
+ try
+ oFrame = oWindow.frameElement
+ catch error
+ # throws error in cross-domain iframes
+
+$.fn.caret = (method, value, settings) ->
+ # http://stackoverflow.com/questions/16010204/get-reference-of-window-object-from-a-dom-element
+ if methods[method]
+ if $.isPlainObject(value)
+ setContextBy value
+ value = undefined
+ else
+ setContextBy settings
+ caret = if Utils.contentEditable(this) then new EditableCaret(this) else new InputCaret(this)
+ methods[method].apply caret, [value]
+ else
+ $.error "Method #{method} does not exist on jQuery.caret"
+
+
+
+$.fn.caret.EditableCaret = EditableCaret
+$.fn.caret.InputCaret = InputCaret
+$.fn.caret.Utils = Utils
+$.fn.caret.apis = methods
diff --git a/apps/comments/js/vendor/Caret.js/src/jquery.caret.js b/apps/comments/js/vendor/Caret.js/src/jquery.caret.js
new file mode 100644
index 0000000000..1cdde1d0c9
--- /dev/null
+++ b/apps/comments/js/vendor/Caret.js/src/jquery.caret.js
@@ -0,0 +1,406 @@
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(["jquery"], function ($) {
+ return (root.returnExportsGlobal = factory($));
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like enviroments that support module.exports,
+ // like Node.
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+}(this, function ($) {
+
+//@ sourceMappingURL=jquery.caret.map
+/*
+ Implement Github like autocomplete mentions
+ http://ichord.github.com/At.js
+
+ Copyright (c) 2013 chord.luo@gmail.com
+ Licensed under the MIT license.
+*/
+
+/*
+本插件操作 textarea 或者 input 内的插入符
+只实现了获得插入符在文本框中的位置,我设置
+插入符的位置.
+*/
+
+"use strict";
+var EditableCaret, InputCaret, Mirror, Utils, discoveryIframeOf, methods, oDocument, oFrame, oWindow, pluginName, setContextBy;
+
+pluginName = 'caret';
+
+EditableCaret = (function() {
+ function EditableCaret($inputor) {
+ this.$inputor = $inputor;
+ this.domInputor = this.$inputor[0];
+ }
+
+ EditableCaret.prototype.setPos = function(pos) {
+ return this.domInputor;
+ };
+
+ EditableCaret.prototype.getIEPosition = function() {
+ return this.getPosition();
+ };
+
+ EditableCaret.prototype.getPosition = function() {
+ var inputor_offset, offset;
+ offset = this.getOffset();
+ inputor_offset = this.$inputor.offset();
+ offset.left -= inputor_offset.left;
+ offset.top -= inputor_offset.top;
+ return offset;
+ };
+
+ EditableCaret.prototype.getOldIEPos = function() {
+ var preCaretTextRange, textRange;
+ textRange = oDocument.selection.createRange();
+ preCaretTextRange = oDocument.body.createTextRange();
+ preCaretTextRange.moveToElementText(this.domInputor);
+ preCaretTextRange.setEndPoint("EndToEnd", textRange);
+ return preCaretTextRange.text.length;
+ };
+
+ EditableCaret.prototype.getPos = function() {
+ var clonedRange, pos, range;
+ if (range = this.range()) {
+ clonedRange = range.cloneRange();
+ clonedRange.selectNodeContents(this.domInputor);
+ clonedRange.setEnd(range.endContainer, range.endOffset);
+ pos = clonedRange.toString().length;
+ clonedRange.detach();
+ return pos;
+ } else if (oDocument.selection) {
+ return this.getOldIEPos();
+ }
+ };
+
+ EditableCaret.prototype.getOldIEOffset = function() {
+ var range, rect;
+ range = oDocument.selection.createRange().duplicate();
+ range.moveStart("character", -1);
+ rect = range.getBoundingClientRect();
+ return {
+ height: rect.bottom - rect.top,
+ left: rect.left,
+ top: rect.top
+ };
+ };
+
+ EditableCaret.prototype.getOffset = function(pos) {
+ var clonedRange, offset, range, rect, shadowCaret;
+ if (oWindow.getSelection && (range = this.range())) {
+ if (range.endOffset - 1 > 0 && range.endContainer === !this.domInputor) {
+ clonedRange = range.cloneRange();
+ clonedRange.setStart(range.endContainer, range.endOffset - 1);
+ clonedRange.setEnd(range.endContainer, range.endOffset);
+ rect = clonedRange.getBoundingClientRect();
+ offset = {
+ height: rect.height,
+ left: rect.left + rect.width,
+ top: rect.top
+ };
+ clonedRange.detach();
+ }
+ if (!offset || (offset != null ? offset.height : void 0) === 0) {
+ clonedRange = range.cloneRange();
+ shadowCaret = $(oDocument.createTextNode("|"));
+ clonedRange.insertNode(shadowCaret[0]);
+ clonedRange.selectNode(shadowCaret[0]);
+ rect = clonedRange.getBoundingClientRect();
+ offset = {
+ height: rect.height,
+ left: rect.left,
+ top: rect.top
+ };
+ shadowCaret.remove();
+ clonedRange.detach();
+ }
+ } else if (oDocument.selection) {
+ offset = this.getOldIEOffset();
+ }
+ if (offset) {
+ offset.top += $(oWindow).scrollTop();
+ offset.left += $(oWindow).scrollLeft();
+ }
+ return offset;
+ };
+
+ EditableCaret.prototype.range = function() {
+ var sel;
+ if (!oWindow.getSelection) {
+ return;
+ }
+ sel = oWindow.getSelection();
+ if (sel.rangeCount > 0) {
+ return sel.getRangeAt(0);
+ } else {
+ return null;
+ }
+ };
+
+ return EditableCaret;
+
+})();
+
+InputCaret = (function() {
+ function InputCaret($inputor) {
+ this.$inputor = $inputor;
+ this.domInputor = this.$inputor[0];
+ }
+
+ InputCaret.prototype.getIEPos = function() {
+ var endRange, inputor, len, normalizedValue, pos, range, textInputRange;
+ inputor = this.domInputor;
+ range = oDocument.selection.createRange();
+ pos = 0;
+ if (range && range.parentElement() === inputor) {
+ normalizedValue = inputor.value.replace(/\r\n/g, "\n");
+ len = normalizedValue.length;
+ textInputRange = inputor.createTextRange();
+ textInputRange.moveToBookmark(range.getBookmark());
+ endRange = inputor.createTextRange();
+ endRange.collapse(false);
+ if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
+ pos = len;
+ } else {
+ pos = -textInputRange.moveStart("character", -len);
+ }
+ }
+ return pos;
+ };
+
+ InputCaret.prototype.getPos = function() {
+ if (oDocument.selection) {
+ return this.getIEPos();
+ } else {
+ return this.domInputor.selectionStart;
+ }
+ };
+
+ InputCaret.prototype.setPos = function(pos) {
+ var inputor, range;
+ inputor = this.domInputor;
+ if (oDocument.selection) {
+ range = inputor.createTextRange();
+ range.move("character", pos);
+ range.select();
+ } else if (inputor.setSelectionRange) {
+ inputor.setSelectionRange(pos, pos);
+ }
+ return inputor;
+ };
+
+ InputCaret.prototype.getIEOffset = function(pos) {
+ var h, textRange, x, y;
+ textRange = this.domInputor.createTextRange();
+ pos || (pos = this.getPos());
+ textRange.move('character', pos);
+ x = textRange.boundingLeft;
+ y = textRange.boundingTop;
+ h = textRange.boundingHeight;
+ return {
+ left: x,
+ top: y,
+ height: h
+ };
+ };
+
+ InputCaret.prototype.getOffset = function(pos) {
+ var $inputor, offset, position;
+ $inputor = this.$inputor;
+ if (oDocument.selection) {
+ offset = this.getIEOffset(pos);
+ offset.top += $(oWindow).scrollTop() + $inputor.scrollTop();
+ offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft();
+ return offset;
+ } else {
+ offset = $inputor.offset();
+ position = this.getPosition(pos);
+ return offset = {
+ left: offset.left + position.left - $inputor.scrollLeft(),
+ top: offset.top + position.top - $inputor.scrollTop(),
+ height: position.height
+ };
+ }
+ };
+
+ InputCaret.prototype.getPosition = function(pos) {
+ var $inputor, at_rect, end_range, format, html, mirror, start_range;
+ $inputor = this.$inputor;
+ format = function(value) {
+ value = value.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g, "
");
+ if (/firefox/i.test(navigator.userAgent)) {
+ value = value.replace(/\s/g, ' ');
+ }
+ return value;
+ };
+ if (pos === void 0) {
+ pos = this.getPos();
+ }
+ start_range = $inputor.val().slice(0, pos);
+ end_range = $inputor.val().slice(pos);
+ html = "" + format(start_range) + "";
+ html += "|";
+ html += "" + format(end_range) + "";
+ mirror = new Mirror($inputor);
+ return at_rect = mirror.create(html).rect();
+ };
+
+ InputCaret.prototype.getIEPosition = function(pos) {
+ var h, inputorOffset, offset, x, y;
+ offset = this.getIEOffset(pos);
+ inputorOffset = this.$inputor.offset();
+ x = offset.left - inputorOffset.left;
+ y = offset.top - inputorOffset.top;
+ h = offset.height;
+ return {
+ left: x,
+ top: y,
+ height: h
+ };
+ };
+
+ return InputCaret;
+
+})();
+
+Mirror = (function() {
+ 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"];
+
+ function Mirror($inputor) {
+ this.$inputor = $inputor;
+ }
+
+ Mirror.prototype.mirrorCss = function() {
+ var css,
+ _this = this;
+ css = {
+ position: 'absolute',
+ left: -9999,
+ top: 0,
+ zIndex: -20000
+ };
+ if (this.$inputor.prop('tagName') === 'TEXTAREA') {
+ this.css_attr.push('width');
+ }
+ $.each(this.css_attr, function(i, p) {
+ return css[p] = _this.$inputor.css(p);
+ });
+ return css;
+ };
+
+ Mirror.prototype.create = function(html) {
+ this.$mirror = $('');
+ this.$mirror.css(this.mirrorCss());
+ this.$mirror.html(html);
+ this.$inputor.after(this.$mirror);
+ return this;
+ };
+
+ Mirror.prototype.rect = function() {
+ var $flag, pos, rect;
+ $flag = this.$mirror.find("#caret");
+ pos = $flag.position();
+ rect = {
+ left: pos.left,
+ top: pos.top,
+ height: $flag.height()
+ };
+ this.$mirror.remove();
+ return rect;
+ };
+
+ return Mirror;
+
+})();
+
+Utils = {
+ contentEditable: function($inputor) {
+ return !!($inputor[0].contentEditable && $inputor[0].contentEditable === 'true');
+ }
+};
+
+methods = {
+ pos: function(pos) {
+ if (pos || pos === 0) {
+ return this.setPos(pos);
+ } else {
+ return this.getPos();
+ }
+ },
+ position: function(pos) {
+ if (oDocument.selection) {
+ return this.getIEPosition(pos);
+ } else {
+ return this.getPosition(pos);
+ }
+ },
+ offset: function(pos) {
+ var offset;
+ offset = this.getOffset(pos);
+ return offset;
+ }
+};
+
+oDocument = null;
+
+oWindow = null;
+
+oFrame = null;
+
+setContextBy = function(settings) {
+ var iframe;
+ if (iframe = settings != null ? settings.iframe : void 0) {
+ oFrame = iframe;
+ oWindow = iframe.contentWindow;
+ return oDocument = iframe.contentDocument || oWindow.document;
+ } else {
+ oFrame = void 0;
+ oWindow = window;
+ return oDocument = document;
+ }
+};
+
+discoveryIframeOf = function($dom) {
+ var error;
+ oDocument = $dom[0].ownerDocument;
+ oWindow = oDocument.defaultView || oDocument.parentWindow;
+ try {
+ return oFrame = oWindow.frameElement;
+ } catch (_error) {
+ error = _error;
+ }
+};
+
+$.fn.caret = function(method, value, settings) {
+ var caret;
+ if (methods[method]) {
+ if ($.isPlainObject(value)) {
+ setContextBy(value);
+ value = void 0;
+ } else {
+ setContextBy(settings);
+ }
+ caret = Utils.contentEditable(this) ? new EditableCaret(this) : new InputCaret(this);
+ return methods[method].apply(caret, [value]);
+ } else {
+ return $.error("Method " + method + " does not exist on jQuery.caret");
+ }
+};
+
+$.fn.caret.EditableCaret = EditableCaret;
+
+$.fn.caret.InputCaret = InputCaret;
+
+$.fn.caret.Utils = Utils;
+
+$.fn.caret.apis = methods;
+
+
+}));
diff --git a/apps/comments/js/vendor/jquery/.bower.json b/apps/comments/js/vendor/jquery/.bower.json
new file mode 100644
index 0000000000..2af10aeb13
--- /dev/null
+++ b/apps/comments/js/vendor/jquery/.bower.json
@@ -0,0 +1,25 @@
+{
+ "name": "jquery",
+ "main": "dist/jquery.js",
+ "license": "MIT",
+ "ignore": [
+ "package.json"
+ ],
+ "keywords": [
+ "jquery",
+ "javascript",
+ "browser",
+ "library"
+ ],
+ "homepage": "https://github.com/jquery/jquery-dist",
+ "version": "3.2.1",
+ "_release": "3.2.1",
+ "_resolution": {
+ "type": "version",
+ "tag": "3.2.1",
+ "commit": "77d2a51d0520d2ee44173afdf4e40a9201f5964e"
+ },
+ "_source": "https://github.com/jquery/jquery-dist.git",
+ "_target": ">=1.7.0",
+ "_originalSource": "jquery"
+}
\ No newline at end of file
diff --git a/apps/comments/js/vendor/jquery/AUTHORS.txt b/apps/comments/js/vendor/jquery/AUTHORS.txt
new file mode 100644
index 0000000000..c32c25f9c3
--- /dev/null
+++ b/apps/comments/js/vendor/jquery/AUTHORS.txt
@@ -0,0 +1,301 @@
+Authors ordered by first contribution.
+
+John Resig
+Gilles van den Hoven
+Michael Geary
+Stefan Petre
+Yehuda Katz
+Corey Jewett
+Klaus Hartl
+Franck Marcia
+Jörn Zaefferer
+Paul Bakaus
+Brandon Aaron
+Mike Alsup
+Dave Methvin
+Ed Engelhardt
+Sean Catchpole
+Paul Mclanahan
+David Serduke
+Richard D. Worth
+Scott González
+Ariel Flesler
+Jon Evans
+TJ Holowaychuk
+Michael Bensoussan
+Robert Katić
+Louis-Rémi Babé
+Earle Castledine
+Damian Janowski
+Rich Dougherty
+Kim Dalsgaard
+Andrea Giammarchi
+Mark Gibson
+Karl Swedberg
+Justin Meyer
+Ben Alman
+James Padolsey
+David Petersen
+Batiste Bieler
+Alexander Farkas
+Rick Waldron
+Filipe Fortes
+Neeraj Singh
+Paul Irish
+Iraê Carvalho
+Matt Curry
+Michael Monteleone
+Noah Sloan
+Tom Viner
+Douglas Neiner
+Adam J. Sontag
+Dave Reed
+Ralph Whitbeck
+Carl Fürstenberg
+Jacob Wright
+J. Ryan Stinnett
+unknown
+temp01
+Heungsub Lee
+Colin Snover
+Ryan W Tenney
+Pinhook
+Ron Otten
+Jephte Clain
+Anton Matzneller
+Alex Sexton
+Dan Heberden
+Henri Wiechers
+Russell Holbrook
+Julian Aubourg
+Gianni Alessandro Chiappetta
+Scott Jehl
+James Burke
+Jonas Pfenniger
+Xavi Ramirez
+Jared Grippe
+Sylvester Keil
+Brandon Sterne
+Mathias Bynens
+Timmy Willison <4timmywil@gmail.com>
+Corey Frang
+Digitalxero
+Anton Kovalyov
+David Murdoch
+Josh Varner
+Charles McNulty
+Jordan Boesch
+Jess Thrysoee
+Michael Murray
+Lee Carpenter
+Alexis Abril
+Rob Morgan
+John Firebaugh
+Sam Bisbee
+Gilmore Davidson
+Brian Brennan
+Xavier Montillet
+Daniel Pihlstrom
+Sahab Yazdani
+avaly
+Scott Hughes
+Mike Sherov
+Greg Hazel
+Schalk Neethling
+Denis Knauf
+Timo Tijhof
+Steen Nielsen
+Anton Ryzhov
+Shi Chuan
+Berker Peksag
+Toby Brain
+Matt Mueller
+Justin
+Daniel Herman
+Oleg Gaidarenko
+Richard Gibson
+Rafaël Blais Masson
+cmc3cn <59194618@qq.com>
+Joe Presbrey
+Sindre Sorhus
+Arne de Bree
+Vladislav Zarakovsky
+Andrew E Monat
+Oskari
+Joao Henrique de Andrade Bruni
+tsinha
+Matt Farmer
+Trey Hunner
+Jason Moon
+Jeffery To
+Kris Borchers
+Vladimir Zhuravlev
+Jacob Thornton
+Chad Killingsworth
+Nowres Rafid
+David Benjamin
+Uri Gilad
+Chris Faulkner
+Elijah Manor
+Daniel Chatfield
+Nikita Govorov
+Wesley Walser
+Mike Pennisi
+Markus Staab
+Dave Riddle
+Callum Macrae
+Benjamin Truyman
+James Huston
+Erick Ruiz de Chávez
+David Bonner
+Akintayo Akinwunmi
+MORGAN
+Ismail Khair
+Carl Danley
+Mike Petrovich
+Greg Lavallee
+Daniel Gálvez
+Sai Lung Wong
+Tom H Fuertes
+Roland Eckl
+Jay Merrifield
+Allen J Schmidt Jr
+Jonathan Sampson
+Marcel Greter
+Matthias Jäggli
+David Fox
+Yiming He
+Devin Cooper
+Paul Ramos
+Rod Vagg
+Bennett Sorbo
+Sebastian Burkhard
+Zachary Adam Kaplan
+nanto_vi
+nanto
+Danil Somsikov
+Ryunosuke SATO
+Jean Boussier
+Adam Coulombe
+Andrew Plummer
+Mark Raddatz
+Isaac Z. Schlueter
+Karl Sieburg
+Pascal Borreli
+Nguyen Phuc Lam
+Dmitry Gusev
+Michał Gołębiowski
+Li Xudong
+Steven Benner
+Tom H Fuertes
+Renato Oliveira dos Santos
+ros3cin
+Jason Bedard
+Kyle Robinson Young
+Chris Talkington
+Eddie Monge
+Terry Jones
+Jason Merino
+Jeremy Dunck
+Chris Price
+Guy Bedford
+Amey Sakhadeo
+Mike Sidorov
+Anthony Ryan
+Dominik D. Geyer
+George Kats
+Lihan Li
+Ronny Springer
+Chris Antaki
+Marian Sollmann
+njhamann
+Ilya Kantor
+David Hong
+John Paul
+Jakob Stoeck
+Christopher Jones
+Forbes Lindesay
+S. Andrew Sheppard
+Leonardo Balter
+Roman Reiß
+Benjy Cui
+Rodrigo Rosenfeld Rosas
+John Hoven
+Philip Jägenstedt
+Christian Kosmowski
+Liang Peng
+TJ VanToll
+Senya Pugach
+Aurelio De Rosa
+Nazar Mokrynskyi
+Amit Merchant
+Jason Bedard
+Arthur Verschaeve
+Dan Hart
+Bin Xin
+David Corbacho
+Veaceslav Grimalschi
+Daniel Husar
+Frederic Hemberger
+Ben Toews
+Aditya Raghavan
+Victor Homyakov
+Shivaji Varma
+Nicolas HENRY
+Anne-Gaelle Colom
+George Mauer
+Leonardo Braga
+Stephen Edgar
+Thomas Tortorini
+Winston Howes
+Jon Hester
+Alexander O'Mara
+Bastian Buchholz
+Arthur Stolyar