diff --git a/.gitignore b/.gitignore
index bbc031e..02a1bef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,6 @@ __MACOSX
*~
Thumbs.db
desktop.ini
+
+node_modules/*
+bower_components/*
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b802d92
--- /dev/null
+++ b/README.md
@@ -0,0 +1,72 @@
+# Overview
+
+Updated, backwards incompatible version of [redactor-mentions](https://github.com/tr42/redactor-mentions) plugin that supports [Redactor 10](http://imperavi.com/redactor/) and some enhancements (like floating div by the underneath the cursor). My goal is to eventually support searching through AJAX URLs.
+
+Note that this project is under heavy development so no guarantees that it will work for you yet.
+
+# Installation
+
+## Manual
+
+Copy `redactor-mentions.min.css` and `redactor-mentions.min.js` from the `dist` folder.
+
+## Bower
+
+I am just getting started with this so I haven't published to bower yet. I will do that soon.
+
+# Usage
+
+1. Copy `redactor-mentions.min.css` and `redactor-mentions.min.js` somewhere in your assets directory.
+
+2. Add them to your markup after redactor stuff.
+
+```html
+
+
+
+
+```
+
+3. Add the mention plugins to your initialization:
+
+```javascript
+$('.post').redactor({
+ plugins: ['mentions'],
+ mentions {
+ url: "users.json", // user data for mentions plugin
+ maxUsers: 5, // maximum users to show in user select dialog
+ urlPrefix: "/user/", // optional url prefix for user
+
+ // Optional. Pass in a function to format each user li. This should return
+ // a jQuery object.
+ formatUserListItem: function(user) {
+ return '' + user.username + ' (' + user.name + ')';
+ },
+
+ // Optional. Pass in a function to format or modify the link that will be
+ // displayed in redactor. You can use this to add any additional properties to
+ // the link.
+ alterUserLink: function($mentionHref, user) {
+ $mentionHref.text("@" + user.name);
+ $mentionHref.attr("data-hovercard", "/e/" + user.username + "/_hovercard");
+ }
+ }
+});
+```
+
+The users JSON data should look like:
+
+```javascript
+[
+ {
+ "icon": "/icons/bob.gif",
+ "name": "Bob",
+ "username": "bob"
+ },
+ {
+ "icon": "/icons/alice.gif",
+ "name": "Alice",
+ "username": "alice"
+ }
+]
+```
diff --git a/README.rst b/README.rst
deleted file mode 100644
index 1fcf0a7..0000000
--- a/README.rst
+++ /dev/null
@@ -1,34 +0,0 @@
-Getting Started
-===============
-
-#. Copy ``mentions.css`` and ``mentions.js`` somewhere into your assets directory.
-#. Add them to your markup after redactor stuff::
-
-
-
-
-
-
-#. Add the mention plugins to your initialization::
-
- $('.post').redactor({
- plugins: ['mentions'],
- usersUrl: "users.json", // user data for mentions plugin
- maxUsers: 5, // maximum users to show in user select dialog
- userUrlPrefix: "/user/" // optional url prefix for user
- });
-
-The users JSON data should look like::
-
- [
- {
- "icon": "/icons/bob.gif",
- "name": "Bob",
- "username": "bob"
- },
- {
- "icon": "/icons/alice.gif",
- "name": "Alice",
- "username": "alice"
- }
- ]
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..d12c072
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,23 @@
+{
+ "name": "redactor-mentions",
+ "version": "0.2",
+ "homepage": "https://github.com/thebitguru/redactor-mentions",
+ "authors": [
+ "Jason Keene (https://github.com/jasonkeene)",
+ "Farhan Ahmad "
+ ],
+ "description": "Mentions plugin for redactor editor.",
+ "keywords": [
+ "redactor",
+ "editor",
+ "mentions"
+ ],
+ "license": "MIT",
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "test",
+ "tests"
+ ]
+}
diff --git a/dist/redactor-mentions.css b/dist/redactor-mentions.css
new file mode 100644
index 0000000..2a5a9b2
--- /dev/null
+++ b/dist/redactor-mentions.css
@@ -0,0 +1,28 @@
+/*! redactor-mentions - compiled at Tue Mar 31 2015 02:58:41 GMT-0500 (CDT) */
+.redactor-box .redactor-mentions-container {
+ position: absolute;
+ border: 2px solid #aaa;
+ background: #fff;
+ z-index: 5000;
+}
+.redactor-box .mention {
+ text-decoration: none !important;
+ cursor: default;
+}
+.redactor-box .user-select {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+.redactor-box .user-select li {
+ padding: 3px;
+ cursor: pointer;
+}
+.redactor-box .user-select li img {
+ margin: 0 4px 0 0;
+ vertical-align: bottom;
+}
+.redactor-box .user-select .selected {
+ background-color: #3577b9;
+ color: #ffffff;
+}
diff --git a/dist/redactor-mentions.js b/dist/redactor-mentions.js
new file mode 100644
index 0000000..21f99eb
--- /dev/null
+++ b/dist/redactor-mentions.js
@@ -0,0 +1,343 @@
+(function() {
+ var $, plugins, ref, ref1, root, users, utils;
+
+ root = typeof exports !== "undefined" && exports !== null ? exports : this;
+
+ $ = root.jQuery;
+
+ utils = root.RedactorUtils = (ref = root.RedactorUtils) != null ? ref : {};
+
+ plugins = root.RedactorPlugins = (ref1 = root.RedactorPlugins) != null ? ref1 : {};
+
+ users = null;
+
+ $.extend(utils, (function() {
+ var once;
+ once = function(func) {
+ func._ran = false;
+ func._return = null;
+ return function() {
+ if (!func._ran) {
+ func._ran = true;
+ func._return = func.apply(this, arguments);
+ }
+ return func._return;
+ };
+ };
+ return {
+ once: once,
+ any: function(arr) {
+ var element, j, len;
+ for (j = 0, len = arr.length; j < len; j++) {
+ element = arr[j];
+ if (element) {
+ return true;
+ }
+ }
+ return false;
+ },
+ deadLink: function(e) {
+ return e.preventDefault();
+ },
+ getCursorInfo: function() {
+ var range, selection;
+ selection = window.getSelection();
+ range = selection.getRangeAt(0);
+ return {
+ selection: selection,
+ range: range,
+ offset: range.startOffset,
+ container: range.startContainer
+ };
+ },
+ loadUsers: once(function(url, formatUserListItem) {
+ return $.getJSON(url, function(data) {
+ var i, j, len, results, user;
+ users = data;
+ results = [];
+ for (i = j = 0, len = data.length; j < len; i = ++j) {
+ user = data[i];
+ if (formatUserListItem) {
+ user.$element = $('' + formatUserListItem(user) + '');
+ } else {
+ user.$element = $("\n " + user.username + " (" + user.name + ")\n");
+ }
+ results.push(user.$element[0].user = user);
+ }
+ return results;
+ });
+ }),
+ filterTest: function(user, filter_string) {
+ var test_strings;
+ filter_string = filter_string.toLowerCase();
+ test_strings = [user.username.toLowerCase(), user.name.toLowerCase()];
+ return utils.any(test_strings.map(function(el) {
+ return el.indexOf(filter_string) !== -1;
+ }));
+ },
+ createMention: function() {
+ var cursor_info, left, mention, new_range, right;
+ cursor_info = utils.getCursorInfo();
+ mention = $('@\u200b');
+ mention.click(utils.deadLink);
+ left = cursor_info.container.data.slice(0, cursor_info.offset);
+ right = cursor_info.container.data.slice(cursor_info.offset);
+ left = left.slice(0, -1);
+ cursor_info.container.data = left;
+ mention.insertAfter(cursor_info.container);
+ mention.after(right);
+ new_range = document.createRange();
+ new_range.setStart(mention[0].firstChild, 1);
+ new_range.setEnd(mention[0].firstChild, 1);
+ cursor_info.selection.removeAllRanges();
+ return cursor_info.selection.addRange(new_range);
+ },
+ cursorAfterMentionStart: function() {
+ var cursor_info, left, ref2;
+ cursor_info = utils.getCursorInfo();
+ if (cursor_info.container.nodeName !== "#text") {
+ return false;
+ }
+ left = cursor_info.container.data.slice(0, cursor_info.offset);
+ left = left.replace(/\u00a0/g, ' ');
+ left = left.replace(/\u200b/g, '');
+ return (ref2 = left.slice(-2)) === '@' || ref2 === ' @';
+ }
+ };
+ })());
+
+ plugins.mentions = function() {
+ return {
+ init: function() {
+ this.mentions.select_state = null;
+ this.mentions.selected = null;
+ this.mentions.$userSelect = null;
+ this.mentions.validateOptions();
+ utils.loadUsers(this.opts.mentions.url, this.opts.mentions.formatUserListItem);
+ this.mentions.setupUserSelect();
+ return this.mentions.setupEditor();
+ },
+ validateOptions: function() {
+ var j, len, name, required, results;
+ required = ["url", "maxUsers"];
+ results = [];
+ for (j = 0, len = required.length; j < len; j++) {
+ name = required[j];
+ if (!this.opts.mentions[name]) {
+ throw "Mention plugin requires option: " + name;
+ } else {
+ results.push(void 0);
+ }
+ }
+ return results;
+ },
+ setupUserSelect: function() {
+ this.mentions.select_state = false;
+ this.mentions.$containerDiv = $('');
+ this.mentions.$containerDiv.hide();
+ this.mentions.$userSelect = $('
');
+ this.mentions.$containerDiv.append(this.mentions.$userSelect);
+ this.mentions.$userSelect.mousemove($.proxy(this.mentions.selectMousemove, this));
+ this.mentions.$userSelect.mousedown($.proxy(this.mentions.selectClick, this));
+ return this.$editor.after(this.mentions.$containerDiv);
+ },
+ setupEditor: function() {
+ this.$editor.on("keydown.mentions", $.proxy(this.mentions.editorKeydown, this));
+ return this.$editor.on("mousedown.mentions", $.proxy(this.mentions.selectClick, this));
+ },
+ selectMousemove: function(e) {
+ var $target;
+ $target = $(e.target);
+ if ($target.hasClass('user')) {
+ this.mentions.selected = this.mentions.$userSelect.children().index($target);
+ return this.mentions.paintSelected();
+ }
+ },
+ selectClick: function(e) {
+ if (this.mentions.select_state) {
+ e.preventDefault();
+ this.mentions.chooseUser();
+ this.mentions.closeMention();
+ this.mentions.setCursorAfterMention();
+ return this.mentions.disableSelect();
+ }
+ },
+ editorKeydown: function(e) {
+ var tabFocus, that;
+ that = this;
+ if (this.mentions.cursorInMention()) {
+ switch (e.which) {
+ case 27:
+ this.mentions.closeMention();
+ this.mentions.disableSelect();
+ break;
+ case 9:
+ case 13:
+ e.preventDefault();
+ tabFocus = this.opts.tabFocus;
+ this.opts.tabFocus = false;
+ if (this.mentions.select_state && this.mentions.$userSelect.children().length > 0) {
+ this.mentions.chooseUser();
+ }
+ this.mentions.closeMention();
+ this.mentions.setCursorAfterMention();
+ this.mentions.disableSelect();
+ setTimeout(function() {
+ return that.opts.tabFocus = tabFocus;
+ }, 0);
+ break;
+ case 38:
+ e.preventDefault();
+ this.mentions.moveSelectUp();
+ break;
+ case 40:
+ e.preventDefault();
+ this.mentions.moveSelectDown();
+ }
+ } else if (utils.cursorAfterMentionStart()) {
+ utils.createMention();
+ this.mentions.enableSelect();
+ }
+ return setTimeout($.proxy(this.mentions.updateSelect, this), 0);
+ },
+ editorMousedown: function() {
+ return setTimeout($.proxy(this.mentions.updateSelect, this), 0);
+ },
+ positionContainerDiv: function() {
+ var $firstNode, boxOffset, nodeOffset;
+ $firstNode = $(this.selection.getNodes()[0]);
+ boxOffset = this.$box.offset();
+ nodeOffset = $firstNode.offset();
+ return this.mentions.$containerDiv.css({
+ left: nodeOffset.left - boxOffset.left,
+ top: nodeOffset.top - boxOffset.top + parseFloat($firstNode.css("line-height"))
+ });
+ },
+ updateSelect: function() {
+ if (this.mentions.cursorInMention()) {
+ this.mentions.filterUsers();
+ this.mentions.positionContainerDiv();
+ return this.mentions.$containerDiv.show();
+ } else {
+ return this.mentions.$containerDiv.hide();
+ }
+ },
+ moveSelectUp: function() {
+ if (this.mentions.selected > 0) {
+ this.mentions.selected -= 1;
+ }
+ return this.mentions.paintSelected();
+ },
+ moveSelectDown: function() {
+ if (this.mentions.selected < this.mentions.$userSelect.children().length - 1) {
+ this.mentions.selected += 1;
+ }
+ return this.mentions.paintSelected();
+ },
+ enableSelect: function() {
+ var i, j, ref2;
+ this.mentions.select_state = true;
+ this.mentions.selected = 0;
+ for (i = j = 0, ref2 = this.opts.mentions.maxUsers; 0 <= ref2 ? j < ref2 : j > ref2; i = 0 <= ref2 ? ++j : --j) {
+ this.mentions.$userSelect.append(users[i].$element);
+ }
+ this.mentions.paintSelected();
+ this.mentions.positionContainerDiv();
+ return this.mentions.$containerDiv.show();
+ },
+ disableSelect: function() {
+ this.mentions.select_state = false;
+ this.mentions.selected = null;
+ this.mentions.$userSelect.children().detach();
+ return this.mentions.$containerDiv.hide();
+ },
+ paintSelected: function() {
+ var $elements;
+ $elements = $('li', this.mentions.$userSelect);
+ $elements.removeClass('selected');
+ return $elements.eq(this.mentions.selected).addClass('selected');
+ },
+ chooseUser: function() {
+ var $mention, prefix, user;
+ user = this.mentions.userFromSelected();
+ prefix = this.opts.mentions.urlPrefix || '/user/';
+ $mention = this.mentions.getCurrentMention();
+ $mention.attr("href", prefix + user.username);
+ $mention.text("@" + user.username);
+ if (this.opts.mentions.alterUserLink) {
+ return this.opts.mentions.alterUserLink($mention, user);
+ }
+ },
+ userFromSelected: function() {
+ return this.mentions.$userSelect.children('li')[this.mentions.selected].user;
+ },
+ filterUsers: function() {
+ var count, filter_string, j, len, user;
+ this.mentions.$userSelect.children().detach();
+ filter_string = this.mentions.getFilterString();
+ count = 0;
+ for (j = 0, len = users.length; j < len; j++) {
+ user = users[j];
+ if (count >= this.opts.mentions.maxUsers) {
+ break;
+ }
+ if (utils.filterTest(user, filter_string)) {
+ this.mentions.$userSelect.append(user.$element);
+ count++;
+ }
+ }
+ return this.mentions.paintSelected();
+ },
+ getFilterString: function() {
+ var filter_str, mention;
+ mention = this.mentions.getCurrentMention();
+ filter_str = mention.text();
+ filter_str = filter_str.slice(1);
+ filter_str = filter_str.replace(/\u00a0/g, ' ');
+ return filter_str.replace(/\u200b/g, '');
+ },
+ closeMention: function() {
+ var mention;
+ mention = this.mentions.getCurrentMention();
+ return mention.attr("contenteditable", "false");
+ },
+ getCurrentMention: function() {
+ var current, parents;
+ current = $(this.selection.getCurrent());
+ if (current.hasClass('mention')) {
+ return current;
+ }
+ parents = current.parents('.mention');
+ if (parents.length > 0) {
+ return parents.eq(0);
+ }
+ throw "There is no current mention.";
+ },
+ cursorInMention: function() {
+ var e;
+ try {
+ this.mentions.getCurrentMention().length > 0;
+ } catch (_error) {
+ e = _error;
+ if (e === "There is no current mention.") {
+ return false;
+ }
+ throw e;
+ }
+ return true;
+ },
+ setCursorAfterMention: function() {
+ var mention, new_range, selection;
+ mention = this.mentions.getCurrentMention();
+ mention.after("\u00a0");
+ selection = window.getSelection();
+ new_range = document.createRange();
+ new_range.setStart(mention[0].nextSibling, 1);
+ new_range.setEnd(mention[0].nextSibling, 1);
+ selection.removeAllRanges();
+ return selection.addRange(new_range);
+ }
+ };
+ };
+
+}).call(this);
diff --git a/dist/redactor-mentions.min.css b/dist/redactor-mentions.min.css
new file mode 100644
index 0000000..14e7d8a
--- /dev/null
+++ b/dist/redactor-mentions.min.css
@@ -0,0 +1,2 @@
+/*! redactor-mentions - compiled at Tue Mar 31 2015 02:58:41 GMT-0500 (CDT) */.redactor-box .redactor-mentions-container{position:absolute;border:2px solid #aaa;background:#fff;z-index:5000}.redactor-box .mention{text-decoration:none!important;cursor:default}.redactor-box .user-select{list-style-type:none;margin:0;padding:0}.redactor-box .user-select li{padding:3px;cursor:pointer}.redactor-box .user-select li img{margin:0 4px 0 0;vertical-align:bottom}.redactor-box .user-select .selected{background-color:#3577b9;color:#fff}
+/*# sourceMappingURL=redactor-mentions.min.css.map */
\ No newline at end of file
diff --git a/dist/redactor-mentions.min.css.map b/dist/redactor-mentions.min.css.map
new file mode 100644
index 0000000..361239f
--- /dev/null
+++ b/dist/redactor-mentions.min.css.map
@@ -0,0 +1 @@
+{"version":3,"sources":["/Users/thebitguru/Desktop/Work/Vonlay/Edison/working_copy/vonlayedison/manual_components/redactor-mentions/redactor-mentions.less"],"names":[],"mappings":"AAAA,aACI;EACI,kBAAA;EACA,sBAAA;EACA,gBAAA;EACA,aAAA;;AALR,aASI;EACI,gCAAA;EACA,eAAA;;AAXR,aAcI;EACI,qBAAA;EACA,SAAA;EACA,UAAA;;AAjBR,aAcI,aAKI;EACI,YAAA;EACA,eAAA;;AArBZ,aAcI,aAKI,GAII;EACI,iBAAA;EACA,sBAAA;;AAzBhB,aAcI,aAeI;EACI,yBAAA;EACA,cAAA","file":"redactor-mentions.min.css","sourcesContent":[".redactor-box {\n .redactor-mentions-container {\n position: absolute;\n border: 2px solid #aaa;\n background: #fff;\n z-index: 5000;\n // box-shadow: 5px 5px 15px #888;\n }\n\n .mention {\n text-decoration: none !important;\n cursor: default;\n }\n \n .user-select {\n list-style-type: none;\n margin: 0;\n padding: 0;\n\n li {\n padding: 3px;\n cursor: pointer;\n\n img {\n margin: 0 4px 0 0;\n vertical-align: bottom;\n }\n }\n\n .selected {\n background-color: #3577b9;\n color: #ffffff;\n }\n }\n}\n"],"sourceRoot":"/source/"}
\ No newline at end of file
diff --git a/dist/redactor-mentions.min.js b/dist/redactor-mentions.min.js
new file mode 100644
index 0000000..52b4ec2
--- /dev/null
+++ b/dist/redactor-mentions.min.js
@@ -0,0 +1 @@
+(function(){var e,t,n,s,i,o,r;i="undefined"!=typeof exports&&null!==exports?exports:this,e=i.jQuery,r=i.RedactorUtils=null!=(n=i.RedactorUtils)?n:{},t=i.RedactorPlugins=null!=(s=i.RedactorPlugins)?s:{},o=null,e.extend(r,function(){var t;return t=function(e){return e._ran=!1,e._return=null,function(){return e._ran||(e._ran=!0,e._return=e.apply(this,arguments)),e._return}},{once:t,any:function(e){var t,n,s;for(n=0,s=e.length;s>n;n++)if(t=e[n])return!0;return!1},deadLink:function(e){return e.preventDefault()},getCursorInfo:function(){var e,t;return t=window.getSelection(),e=t.getRangeAt(0),{selection:t,range:e,offset:e.startOffset,container:e.startContainer}},loadUsers:t(function(t,n){return e.getJSON(t,function(t){var s,i,r,c,l;for(o=t,c=[],s=i=0,r=t.length;r>i;s=++i)l=t[s],l.$element=e(n?''+n(l)+"":'\n '+l.username+" ("+l.name+")\n"),c.push(l.$element[0].user=l);return c})}),filterTest:function(e,t){var n;return t=t.toLowerCase(),n=[e.username.toLowerCase(),e.name.toLowerCase()],r.any(n.map(function(e){return-1!==e.indexOf(t)}))},createMention:function(){var t,n,s,i,o;return t=r.getCursorInfo(),s=e('@'),s.click(r.deadLink),n=t.container.data.slice(0,t.offset),o=t.container.data.slice(t.offset),n=n.slice(0,-1),t.container.data=n,s.insertAfter(t.container),s.after(o),i=document.createRange(),i.setStart(s[0].firstChild,1),i.setEnd(s[0].firstChild,1),t.selection.removeAllRanges(),t.selection.addRange(i)},cursorAfterMentionStart:function(){var e,t,n;return e=r.getCursorInfo(),"#text"!==e.container.nodeName?!1:(t=e.container.data.slice(0,e.offset),t=t.replace(/\u00a0/g," "),t=t.replace(/\u200b/g,""),"@"===(n=t.slice(-2))||" @"===n)}}}()),t.mentions=function(){return{init:function(){return this.mentions.select_state=null,this.mentions.selected=null,this.mentions.$userSelect=null,this.mentions.validateOptions(),r.loadUsers(this.opts.mentions.url,this.opts.mentions.formatUserListItem),this.mentions.setupUserSelect(),this.mentions.setupEditor()},validateOptions:function(){var e,t,n,s,i;for(s=["url","maxUsers"],i=[],e=0,t=s.length;t>e;e++){if(n=s[e],!this.opts.mentions[n])throw"Mention plugin requires option: "+n;i.push(void 0)}return i},setupUserSelect:function(){return this.mentions.select_state=!1,this.mentions.$containerDiv=e(''),this.mentions.$containerDiv.hide(),this.mentions.$userSelect=e('
'),this.mentions.$containerDiv.append(this.mentions.$userSelect),this.mentions.$userSelect.mousemove(e.proxy(this.mentions.selectMousemove,this)),this.mentions.$userSelect.mousedown(e.proxy(this.mentions.selectClick,this)),this.$editor.after(this.mentions.$containerDiv)},setupEditor:function(){return this.$editor.on("keydown.mentions",e.proxy(this.mentions.editorKeydown,this)),this.$editor.on("mousedown.mentions",e.proxy(this.mentions.selectClick,this))},selectMousemove:function(t){var n;return n=e(t.target),n.hasClass("user")?(this.mentions.selected=this.mentions.$userSelect.children().index(n),this.mentions.paintSelected()):void 0},selectClick:function(e){return this.mentions.select_state?(e.preventDefault(),this.mentions.chooseUser(),this.mentions.closeMention(),this.mentions.setCursorAfterMention(),this.mentions.disableSelect()):void 0},editorKeydown:function(t){var n,s;if(s=this,this.mentions.cursorInMention())switch(t.which){case 27:this.mentions.closeMention(),this.mentions.disableSelect();break;case 9:case 13:t.preventDefault(),n=this.opts.tabFocus,this.opts.tabFocus=!1,this.mentions.select_state&&this.mentions.$userSelect.children().length>0&&this.mentions.chooseUser(),this.mentions.closeMention(),this.mentions.setCursorAfterMention(),this.mentions.disableSelect(),setTimeout(function(){return s.opts.tabFocus=n},0);break;case 38:t.preventDefault(),this.mentions.moveSelectUp();break;case 40:t.preventDefault(),this.mentions.moveSelectDown()}else r.cursorAfterMentionStart()&&(r.createMention(),this.mentions.enableSelect());return setTimeout(e.proxy(this.mentions.updateSelect,this),0)},editorMousedown:function(){return setTimeout(e.proxy(this.mentions.updateSelect,this),0)},positionContainerDiv:function(){var t,n,s;return t=e(this.selection.getNodes()[0]),n=this.$box.offset(),s=t.offset(),this.mentions.$containerDiv.css({left:s.left-n.left,top:s.top-n.top+parseFloat(t.css("line-height"))})},updateSelect:function(){return this.mentions.cursorInMention()?(this.mentions.filterUsers(),this.mentions.positionContainerDiv(),this.mentions.$containerDiv.show()):this.mentions.$containerDiv.hide()},moveSelectUp:function(){return this.mentions.selected>0&&(this.mentions.selected-=1),this.mentions.paintSelected()},moveSelectDown:function(){return this.mentions.selected=0?n>t:t>n;e=n>=0?++t:--t)this.mentions.$userSelect.append(o[e].$element);return this.mentions.paintSelected(),this.mentions.positionContainerDiv(),this.mentions.$containerDiv.show()},disableSelect:function(){return this.mentions.select_state=!1,this.mentions.selected=null,this.mentions.$userSelect.children().detach(),this.mentions.$containerDiv.hide()},paintSelected:function(){var t;return t=e("li",this.mentions.$userSelect),t.removeClass("selected"),t.eq(this.mentions.selected).addClass("selected")},chooseUser:function(){var e,t,n;return n=this.mentions.userFromSelected(),t=this.opts.mentions.urlPrefix||"/user/",e=this.mentions.getCurrentMention(),e.attr("href",t+n.username),e.text("@"+n.username),this.opts.mentions.alterUserLink?this.opts.mentions.alterUserLink(e,n):void 0},userFromSelected:function(){return this.mentions.$userSelect.children("li")[this.mentions.selected].user},filterUsers:function(){var e,t,n,s,i;for(this.mentions.$userSelect.children().detach(),t=this.mentions.getFilterString(),e=0,n=0,s=o.length;s>n&&(i=o[n],!(e>=this.opts.mentions.maxUsers));n++)r.filterTest(i,t)&&(this.mentions.$userSelect.append(i.$element),e++);return this.mentions.paintSelected()},getFilterString:function(){var e,t;return t=this.mentions.getCurrentMention(),e=t.text(),e=e.slice(1),e=e.replace(/\u00a0/g," "),e.replace(/\u200b/g,"")},closeMention:function(){var e;return e=this.mentions.getCurrentMention(),e.attr("contenteditable","false")},getCurrentMention:function(){var t,n;if(t=e(this.selection.getCurrent()),t.hasClass("mention"))return t;if(n=t.parents(".mention"),n.length>0)return n.eq(0);throw"There is no current mention."},cursorInMention:function(){var e;try{this.mentions.getCurrentMention().length>0}catch(t){if(e=t,"There is no current mention."===e)return!1;throw e}return!0},setCursorAfterMention:function(){var e,t,n;return e=this.mentions.getCurrentMention(),e.after(" "),n=window.getSelection(),t=document.createRange(),t.setStart(e[0].nextSibling,1),t.setEnd(e[0].nextSibling,1),n.removeAllRanges(),n.addRange(t)}}}}).call(this);
\ No newline at end of file
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 0000000..68323b2
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,41 @@
+var gulp = require('gulp');
+var coffee = require('gulp-coffee');
+var less = require('gulp-less');
+var cssmin = require('gulp-cssmin');
+var uglify = require('gulp-uglify');
+var sourcemaps = require('gulp-sourcemaps');
+var header = require('gulp-header');
+var rename = require('gulp-rename');
+var pkg = require('./package.json');
+
+gulp.task('default', ['compile-coffeescript', 'minify-and-copy-css']);
+
+gulp.task('watch', function() {
+ gulp.watch('redactor-mentions.coffee', ['compile-coffeescript']);
+ gulp.watch('redactor-mentions.less', ['compile-and-minify-css']);
+});
+
+gulp.task('compile-coffeescript', function() {
+ gulp.src('redactor-mentions.coffee')
+ .pipe(coffee({bare: false}))
+ .pipe(gulp.dest('dist'))
+
+ .pipe(uglify())
+ .pipe(rename({ suffix: '.min' }))
+ .pipe(gulp.dest('dist'));
+});
+
+gulp.task('compile-and-minify-css', function() {
+ gulp.src('redactor-mentions.less')
+ .pipe(sourcemaps.init())
+ .pipe(less())
+ .pipe(header(
+ '/*! <%= pkg.name %> - compiled at <%= new Date() %> */\n', { pkg: pkg }
+ ))
+ .pipe(gulp.dest('dist/'))
+
+ .pipe(cssmin())
+ .pipe(rename({ suffix: '.min' }))
+ .pipe(sourcemaps.write('./'))
+ .pipe(gulp.dest('dist/'));
+});
diff --git a/mentions.coffee b/mentions.coffee
deleted file mode 100644
index aebf53d..0000000
--- a/mentions.coffee
+++ /dev/null
@@ -1,355 +0,0 @@
-# grab global object
-root = exports ? this
-
-# grab jQuery/redactor globals
-$ = root.jQuery
-utils = root.RedactorUtils = root.RedactorUtils ? {}
-plugins = root.RedactorPlugins = root.RedactorPlugins ? {}
-users = null # where we store all the user data
-
-
-# extend utils with stuff that isn't concerned with redactor instance
-$.extend utils, do ->
- # needed in other parts in utils
- once = (func) ->
- # only run func once even if it's called multiple times
- func._ran = false
- func._return = null
- ->
- if not func._ran
- func._ran = true
- func._return = func.apply this, arguments
- func._return
- once: once
-
- any: (arr) ->
- # if any elements of arr are truthy then return true, else false
- for element in arr
- return true if element
- false
-
- deadLink: (e) ->
- # event handler to kill a link (prevent event from propagating)
- e.preventDefault()
-
- getCursorInfo: ->
- # return current cursor information
- selection = window.getSelection()
- range = selection.getRangeAt 0
-
- selection: selection
- range: range
- offset: range.startOffset
- container: range.startContainer
-
- loadUsers: once (url) ->
- # async call to get user data and assign it into module global
- $.getJSON url, (data) ->
- users = data
-
- for user, i in data
- # create actual dom node for userSelect
- user.$element = $ """
-
- #{ user.username } (#{ user.name })
- """
-
- # put a pointer back to user object
- user.$element[0].user = user
-
- filterTest: (user, filter_string) ->
- # test if user passes through the filter given by filter_string
- filter_string = filter_string.toLowerCase()
- test_strings = [
- user.username.toLowerCase()
- user.name.toLowerCase()
- ]
- utils.any test_strings.map (el) ->
- el.indexOf(filter_string) != -1
-
- createMention: ->
- # create a new mention and insert it at cursor position
- cursor_info = utils.getCursorInfo()
- mention = $ '@\u200b'
-
- # make sure mention links aren't clickable
- mention.click utils.deadLink
-
- # insert mention where cursor is at
- # figure out what text is left and right of the cursor
- left = cursor_info.container.data.slice 0, cursor_info.offset
- right = cursor_info.container.data.slice cursor_info.offset
-
- # slice off the @ sign
- left = left.slice 0, -1
-
- # insert the mention inbetween left and right
- cursor_info.container.data = left
- mention.insertAfter cursor_info.container
- mention.after right
-
- # set cursor positon into mention
- new_range = document.createRange()
- new_range.setStart mention[0].firstChild, 1
- new_range.setEnd mention[0].firstChild, 1
- cursor_info.selection.removeAllRanges()
- cursor_info.selection.addRange new_range
-
- cursorAfterMentionStart: ->
- # test to see if the cursor is at a place where a mention can be inserted
-
- # get cursor element and offset
- cursor_info = utils.getCursorInfo()
- # if cursor isn't on a text element return false
- return false if cursor_info.container.nodeName != "#text"
-
- # figure out what is left of the cursor
- left = cursor_info.container.data.slice 0, cursor_info.offset
- # replace A0 with 20
- left = left.replace /\u00a0/g, ' '
- # remove zero width spaces
- left = left.replace /\u200b/g, ''
- # slice off last two characters and test them
- left.slice(-2) in ['@', ' @']
-
-
-# extend plugins with stuff that is concerned with redactor instance
-$.extend plugins, do ->
- mentions:
-
- #########
- # setup #
- #########
-
- init: ->
- this.select_state = null # state of display of user select
- this.selected = null # current user select index
- this.$userSelect = null # user select element
-
- this.validateOptions()
- utils.loadUsers(this.opts.usersUrl)
- this.setupUserSelect()
- this.setupEditor()
-
- validateOptions: ->
- # make sure options are set to valid values
- required = [
- "usersUrl"
- "maxUsers"
- ]
- for name in required
- if not this.opts[name]
- throw "Mention plugin requires option: #{ name }"
-
-
- setupUserSelect: ->
- # init it's state to false
- this.select_state = false
- # create dom node
- this.$userSelect = $ '
'
- # hide it by default
- this.$userSelect.hide()
- # setup event handlers
- this.$userSelect.mousemove $.proxy(this.selectMousemove, this)
- this.$userSelect.mousedown $.proxy(this.selectMousedown, this)
- # insert it into active dom tree
- this.$editor.after this.$userSelect
-
- setupEditor: ->
- # setup event handlers
- this.$editor.keydown $.proxy(this.editorKeydown, this)
- this.$editor.mousedown $.proxy(this.editorMousedown, this)
-
- ##################
- # event handlers #
- ##################
-
- # select event handlers
- selectMousemove: (e) ->
- $target = $ e.target
- if $target.hasClass 'user'
- this.selected = this.$userSelect.children().index $target
- this.paintSelected()
-
- selectMousedown: (e) ->
- if this.select_state
- e.preventDefault()
- this.chooseUser()
- this.closeMention()
- this.setCursorAfterMention()
- this.disableSelect()
-
- # editor event handlers
- editorKeydown: (e) ->
- that = this
-
- if this.cursorInMention()
- switch e.keyCode
- when 27 # escape
- this.closeMention()
- this.disableSelect()
-
- when 9, 13 # tab, return
- e.preventDefault()
-
- # work around to prevent tabs being inser
- tabFocus = this.opts.tabFocus
- this.opts.tabFocus = false
-
- if this.select_state and this.$userSelect.children().length > 0
- this.chooseUser()
-
- this.closeMention()
- this.setCursorAfterMention()
- this.disableSelect()
-
- # reset tabFocus when you return to the event loop
- setTimeout ->
- that.opts.tabFocus = tabFocus
- , 0
-
- when 38 # up
- e.preventDefault()
- this.moveSelectUp()
-
- when 40 # down
- e.preventDefault()
- this.moveSelectDown()
-
- else if utils.cursorAfterMentionStart()
- utils.createMention()
- this.enableSelect()
-
- # after every key press, make sure that select state is correct
- setTimeout $.proxy(this.updateSelect, this), 0
-
- editorMousedown: ->
- # after every mousepress, make sure that select state is correct
- setTimeout $.proxy(this.updateSelect, this), 0
-
- ########################
- # select functionality #
- ########################
-
- updateSelect: ->
- if this.cursorInMention()
- this.filterUsers()
- this.$userSelect.show()
- else
- this.$userSelect.hide()
-
- moveSelectUp: ->
- if this.selected > 0
- this.selected -= 1
- this.paintSelected()
-
- moveSelectDown: ->
- if this.selected < this.$userSelect.children().length - 1
- this.selected += 1
- this.paintSelected()
-
- enableSelect: ->
- this.select_state = true
- this.selected = 0
-
- # build initial user select
- for i in [0...this.opts.maxUsers]
- this.$userSelect.append users[i].$element
-
- this.paintSelected()
- this.$userSelect.show()
-
- disableSelect: ->
- this.select_state = false
- this.selected = null
- this.$userSelect.children().detach()
- this.$userSelect.hide()
-
- paintSelected: ->
- $elements = $ 'li', this.$userSelect
- $elements.removeClass 'selected'
- $elements.eq(this.selected).addClass 'selected'
-
- chooseUser: ->
- user = this.userFromSelected()
- mention = this.getCurrentMention()
- prefix = this.opts.userUrlPrefix or '/user/'
- mention.attr "href", prefix + user.username
- mention.text "@#{ user.username }"
-
- userFromSelected: ->
- this.$userSelect.children('li')[this.selected].user
-
- filterUsers: ->
- # empty out userSelect
- this.$userSelect.children().detach()
-
- # query for filter_string once
- filter_string = this.getFilterString()
-
- # build filtered users list
- count = 0
- for user in users
- # break on max filter users
- break if count >= this.opts.maxUsers
-
- if utils.filterTest user, filter_string
- this.$userSelect.append user.$element
- count++
-
- this.paintSelected()
-
- getFilterString: ->
- mention = this.getCurrentMention()
- filter_str = mention.text()
- # remove @ from the begining
- filter_str = filter_str.slice 1
- # replace A0 with 20
- filter_str = filter_str.replace /\u00a0/g, ' '
- # remove zero width spaces
- filter_str.replace /\u200b/g, ''
-
- #########################
- # mention functionality #
- #########################
-
- closeMention: ->
- mention = this.getCurrentMention()
- mention.attr "contenteditable", "false"
-
- getCurrentMention: ->
- # return the current mention based on cursor position, if there
- # isn't one then return false
-
- # first check the current element, if it is a mention return it
- current = $ this.getCurrent()
- return current if current.hasClass 'mention'
-
- # else select from parents
- parents = current.parents '.mention'
- return parents.eq 0 if parents.length > 0
-
- # throw if there isn't a current mention
- throw "There is no current mention."
-
- cursorInMention: ->
- try
- this.getCurrentMention().length > 0
- catch e
- return false if e == "There is no current mention."
- throw e
- true
-
- setCursorAfterMention: ->
- mention = this.getCurrentMention()
-
- # insert space after mention
- mention.after "\u00a0"
-
- # set cursor
- selection = window.getSelection()
- new_range = document.createRange()
- new_range.setStart mention[0].nextSibling, 1
- new_range.setEnd mention[0].nextSibling, 1
- selection.removeAllRanges()
- selection.addRange new_range
diff --git a/mentions.css b/mentions.css
deleted file mode 100644
index 48f4fab..0000000
--- a/mentions.css
+++ /dev/null
@@ -1,25 +0,0 @@
-.redactor_box .mention {
- text-decoration: none !important;
- color: #e03c00 !important;
- cursor: default;
-}
-.redactor_box .user_select {
- list-style-type: none;
- margin: 0;
- padding: 3px 0 0 0;
- font-family: sans-serif;
- font-size: 14px;
- min-height: 20px;
-}
-.redactor_box .user_select li {
- padding: 3px;
- margin: 2px 0;
-}
-.redactor_box .user_select .selected {
- background-color: #e03c00;
- color: #ffffff;
-}
-.redactor_box .user_select li img {
- margin: 0 4px 0 0;
- vertical-align: bottom;
-}
diff --git a/mentions.js b/mentions.js
deleted file mode 100644
index 00e0f30..0000000
--- a/mentions.js
+++ /dev/null
@@ -1,325 +0,0 @@
-// Generated by CoffeeScript 1.7.1
-(function() {
- var $, plugins, root, users, utils, _ref, _ref1;
-
- root = typeof exports !== "undefined" && exports !== null ? exports : this;
-
- $ = root.jQuery;
-
- utils = root.RedactorUtils = (_ref = root.RedactorUtils) != null ? _ref : {};
-
- plugins = root.RedactorPlugins = (_ref1 = root.RedactorPlugins) != null ? _ref1 : {};
-
- users = null;
-
- $.extend(utils, (function() {
- var once;
- once = function(func) {
- func._ran = false;
- func._return = null;
- return function() {
- if (!func._ran) {
- func._ran = true;
- func._return = func.apply(this, arguments);
- }
- return func._return;
- };
- };
- return {
- once: once,
- any: function(arr) {
- var element, _i, _len;
- for (_i = 0, _len = arr.length; _i < _len; _i++) {
- element = arr[_i];
- if (element) {
- return true;
- }
- }
- return false;
- },
- deadLink: function(e) {
- return e.preventDefault();
- },
- getCursorInfo: function() {
- var range, selection;
- selection = window.getSelection();
- range = selection.getRangeAt(0);
- return {
- selection: selection,
- range: range,
- offset: range.startOffset,
- container: range.startContainer
- };
- },
- loadUsers: once(function(url) {
- return $.getJSON(url, function(data) {
- var i, user, _i, _len, _results;
- users = data;
- _results = [];
- for (i = _i = 0, _len = data.length; _i < _len; i = ++_i) {
- user = data[i];
- user.$element = $("\n " + user.username + " (" + user.name + ")\n");
- _results.push(user.$element[0].user = user);
- }
- return _results;
- });
- }),
- filterTest: function(user, filter_string) {
- var test_strings;
- filter_string = filter_string.toLowerCase();
- test_strings = [user.username.toLowerCase(), user.name.toLowerCase()];
- return utils.any(test_strings.map(function(el) {
- return el.indexOf(filter_string) !== -1;
- }));
- },
- createMention: function() {
- var cursor_info, left, mention, new_range, right;
- cursor_info = utils.getCursorInfo();
- mention = $('@\u200b');
- mention.click(utils.deadLink);
- left = cursor_info.container.data.slice(0, cursor_info.offset);
- right = cursor_info.container.data.slice(cursor_info.offset);
- left = left.slice(0, -1);
- cursor_info.container.data = left;
- mention.insertAfter(cursor_info.container);
- mention.after(right);
- new_range = document.createRange();
- new_range.setStart(mention[0].firstChild, 1);
- new_range.setEnd(mention[0].firstChild, 1);
- cursor_info.selection.removeAllRanges();
- return cursor_info.selection.addRange(new_range);
- },
- cursorAfterMentionStart: function() {
- var cursor_info, left, _ref2;
- cursor_info = utils.getCursorInfo();
- if (cursor_info.container.nodeName !== "#text") {
- return false;
- }
- left = cursor_info.container.data.slice(0, cursor_info.offset);
- left = left.replace(/\u00a0/g, ' ');
- left = left.replace(/\u200b/g, '');
- return (_ref2 = left.slice(-2)) === '@' || _ref2 === ' @';
- }
- };
- })());
-
- $.extend(plugins, (function() {
- return {
- mentions: {
- init: function() {
- this.select_state = null;
- this.selected = null;
- this.$userSelect = null;
- this.validateOptions();
- utils.loadUsers(this.opts.usersUrl);
- this.setupUserSelect();
- return this.setupEditor();
- },
- validateOptions: function() {
- var name, required, _i, _len, _results;
- required = ["usersUrl", "maxUsers"];
- _results = [];
- for (_i = 0, _len = required.length; _i < _len; _i++) {
- name = required[_i];
- if (!this.opts[name]) {
- throw "Mention plugin requires option: " + name;
- } else {
- _results.push(void 0);
- }
- }
- return _results;
- },
- setupUserSelect: function() {
- this.select_state = false;
- this.$userSelect = $('
');
- this.$userSelect.hide();
- this.$userSelect.mousemove($.proxy(this.selectMousemove, this));
- this.$userSelect.mousedown($.proxy(this.selectMousedown, this));
- return this.$editor.after(this.$userSelect);
- },
- setupEditor: function() {
- this.$editor.keydown($.proxy(this.editorKeydown, this));
- return this.$editor.mousedown($.proxy(this.editorMousedown, this));
- },
- selectMousemove: function(e) {
- var $target;
- $target = $(e.target);
- if ($target.hasClass('user')) {
- this.selected = this.$userSelect.children().index($target);
- return this.paintSelected();
- }
- },
- selectMousedown: function(e) {
- if (this.select_state) {
- e.preventDefault();
- this.chooseUser();
- this.closeMention();
- this.setCursorAfterMention();
- return this.disableSelect();
- }
- },
- editorKeydown: function(e) {
- var tabFocus, that;
- that = this;
- if (this.cursorInMention()) {
- switch (e.keyCode) {
- case 27:
- this.closeMention();
- this.disableSelect();
- break;
- case 9:
- case 13:
- e.preventDefault();
- tabFocus = this.opts.tabFocus;
- this.opts.tabFocus = false;
- if (this.select_state && this.$userSelect.children().length > 0) {
- this.chooseUser();
- }
- this.closeMention();
- this.setCursorAfterMention();
- this.disableSelect();
- setTimeout(function() {
- return that.opts.tabFocus = tabFocus;
- }, 0);
- break;
- case 38:
- e.preventDefault();
- this.moveSelectUp();
- break;
- case 40:
- e.preventDefault();
- this.moveSelectDown();
- }
- } else if (utils.cursorAfterMentionStart()) {
- utils.createMention();
- this.enableSelect();
- }
- return setTimeout($.proxy(this.updateSelect, this), 0);
- },
- editorMousedown: function() {
- return setTimeout($.proxy(this.updateSelect, this), 0);
- },
- updateSelect: function() {
- if (this.cursorInMention()) {
- this.filterUsers();
- return this.$userSelect.show();
- } else {
- return this.$userSelect.hide();
- }
- },
- moveSelectUp: function() {
- if (this.selected > 0) {
- this.selected -= 1;
- }
- return this.paintSelected();
- },
- moveSelectDown: function() {
- if (this.selected < this.$userSelect.children().length - 1) {
- this.selected += 1;
- }
- return this.paintSelected();
- },
- enableSelect: function() {
- var i, _i, _ref2;
- this.select_state = true;
- this.selected = 0;
- for (i = _i = 0, _ref2 = this.opts.maxUsers; 0 <= _ref2 ? _i < _ref2 : _i > _ref2; i = 0 <= _ref2 ? ++_i : --_i) {
- this.$userSelect.append(users[i].$element);
- }
- this.paintSelected();
- return this.$userSelect.show();
- },
- disableSelect: function() {
- this.select_state = false;
- this.selected = null;
- this.$userSelect.children().detach();
- return this.$userSelect.hide();
- },
- paintSelected: function() {
- var $elements;
- $elements = $('li', this.$userSelect);
- $elements.removeClass('selected');
- return $elements.eq(this.selected).addClass('selected');
- },
- chooseUser: function() {
- var mention, prefix, user;
- user = this.userFromSelected();
- mention = this.getCurrentMention();
- prefix = this.opts.userUrlPrefix || '/user/';
- mention.attr("href", prefix + user.username);
- return mention.text("@" + user.username);
- },
- userFromSelected: function() {
- return this.$userSelect.children('li')[this.selected].user;
- },
- filterUsers: function() {
- var count, filter_string, user, _i, _len;
- this.$userSelect.children().detach();
- filter_string = this.getFilterString();
- count = 0;
- for (_i = 0, _len = users.length; _i < _len; _i++) {
- user = users[_i];
- if (count >= this.opts.maxUsers) {
- break;
- }
- if (utils.filterTest(user, filter_string)) {
- this.$userSelect.append(user.$element);
- count++;
- }
- }
- return this.paintSelected();
- },
- getFilterString: function() {
- var filter_str, mention;
- mention = this.getCurrentMention();
- filter_str = mention.text();
- filter_str = filter_str.slice(1);
- filter_str = filter_str.replace(/\u00a0/g, ' ');
- return filter_str.replace(/\u200b/g, '');
- },
- closeMention: function() {
- var mention;
- mention = this.getCurrentMention();
- return mention.attr("contenteditable", "false");
- },
- getCurrentMention: function() {
- var current, parents;
- current = $(this.getCurrent());
- if (current.hasClass('mention')) {
- return current;
- }
- parents = current.parents('.mention');
- if (parents.length > 0) {
- return parents.eq(0);
- }
- throw "There is no current mention.";
- },
- cursorInMention: function() {
- var e;
- try {
- this.getCurrentMention().length > 0;
- } catch (_error) {
- e = _error;
- if (e === "There is no current mention.") {
- return false;
- }
- throw e;
- }
- return true;
- },
- setCursorAfterMention: function() {
- var mention, new_range, selection;
- mention = this.getCurrentMention();
- mention.after("\u00a0");
- selection = window.getSelection();
- new_range = document.createRange();
- new_range.setStart(mention[0].nextSibling, 1);
- new_range.setEnd(mention[0].nextSibling, 1);
- selection.removeAllRanges();
- return selection.addRange(new_range);
- }
- }
- };
- })());
-
-}).call(this);
diff --git a/notes.txt b/notes.txt
new file mode 100644
index 0000000..37d74ea
--- /dev/null
+++ b/notes.txt
@@ -0,0 +1,11 @@
+Commit Notes
+
+
+
+--------------------------------------------------------------------------------
+TODO
+
+- Add support for server side search.
+ "dependencies": {
+ "jquery-throttle-debounce": "*"
+ }
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..3fe092d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "redactor-mentions",
+ "version": "0.2.0",
+ "description": "Mentions plugin for redactor editor.",
+ "main": "mentions.js",
+ "directories": {
+ "doc": "docs"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thebitguru/redactor-mentions.git"
+ },
+ "keywords": [
+ "redactor",
+ "mentions",
+ "plugin"
+ ],
+ "author": "Farhan Ahmad ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/thebitguru/redactor-mentions/issues"
+ },
+ "homepage": "https://github.com/thebitguru/redactor-mentions",
+ "dependencies": {
+ "gulp": "^3.8.11",
+ "gulp-coffee": "^2.3.1",
+ "gulp-cssmin": "^0.1.6",
+ "gulp-header": "^1.2.2",
+ "gulp-less": "^3.0.2",
+ "gulp-rename": "^1.2.0",
+ "gulp-sourcemaps": "^1.5.1",
+ "gulp-uglify": "^1.1.0"
+ }
+}
diff --git a/redactor-mentions.coffee b/redactor-mentions.coffee
new file mode 100644
index 0000000..7d7fb85
--- /dev/null
+++ b/redactor-mentions.coffee
@@ -0,0 +1,373 @@
+# grab global object
+root = exports ? this
+
+# grab jQuery/redactor globals
+$ = root.jQuery
+utils = root.RedactorUtils = root.RedactorUtils ? {}
+plugins = root.RedactorPlugins = root.RedactorPlugins ? {}
+users = null # where we store all the user data
+
+
+# extend utils with stuff that isn't concerned with redactor instance
+$.extend utils, do ->
+ # needed in other parts in utils
+ once = (func) ->
+ # only run func once even if it's called multiple times
+ func._ran = false
+ func._return = null
+ ->
+ if not func._ran
+ func._ran = true
+ func._return = func.apply this, arguments
+ func._return
+ once: once
+
+ any: (arr) ->
+ # if any elements of arr are truthy then return true, else false
+ for element in arr
+ return true if element
+ false
+
+ deadLink: (e) ->
+ # event handler to kill a link (prevent event from propagating)
+ e.preventDefault()
+
+ getCursorInfo: ->
+ # return current cursor information
+ selection = window.getSelection()
+ range = selection.getRangeAt 0
+
+ selection: selection
+ range: range
+ offset: range.startOffset
+ container: range.startContainer
+
+ loadUsers: once (url, formatUserListItem) ->
+ # async call to get user data and assign it into module global
+ $.getJSON url, (data) ->
+ users = data
+
+ for user, i in data
+ # create actual dom node for userSelect
+ if formatUserListItem
+ user.$element = $ '' + formatUserListItem(user) + ''
+ else
+ user.$element = $ """
+
+ #{ user.username } (#{ user.name })
+ """
+
+ # put a pointer back to user object
+ user.$element[0].user = user
+
+
+ filterTest: (user, filter_string) ->
+ # test if user passes through the filter given by filter_string
+ filter_string = filter_string.toLowerCase()
+ test_strings = [
+ user.username.toLowerCase()
+ user.name.toLowerCase()
+ ]
+ utils.any test_strings.map (el) ->
+ el.indexOf(filter_string) != -1
+
+ createMention: ->
+ # create a new mention and insert it at cursor position
+ cursor_info = utils.getCursorInfo()
+ mention = $ '@\u200b'
+
+ # make sure mention links aren't clickable
+ mention.click utils.deadLink
+
+ # insert mention where cursor is at
+ # figure out what text is left and right of the cursor
+ left = cursor_info.container.data.slice 0, cursor_info.offset
+ right = cursor_info.container.data.slice cursor_info.offset
+
+ # slice off the @ sign
+ left = left.slice 0, -1
+
+ # insert the mention inbetween left and right
+ cursor_info.container.data = left
+ mention.insertAfter cursor_info.container
+ mention.after right
+
+ # set cursor positon into mention
+ new_range = document.createRange()
+ new_range.setStart mention[0].firstChild, 1
+ new_range.setEnd mention[0].firstChild, 1
+ cursor_info.selection.removeAllRanges()
+ cursor_info.selection.addRange new_range
+
+ cursorAfterMentionStart: ->
+ # test to see if the cursor is at a place where a mention can be inserted
+
+ # get cursor element and offset
+ cursor_info = utils.getCursorInfo()
+ # if cursor isn't on a text element return false
+ return false if cursor_info.container.nodeName != "#text"
+
+ # figure out what is left of the cursor
+ left = cursor_info.container.data.slice 0, cursor_info.offset
+ # replace A0 with 20
+ left = left.replace /\u00a0/g, ' '
+ # remove zero width spaces
+ left = left.replace /\u200b/g, ''
+ # slice off last two characters and test them
+ left.slice(-2) in ['@', ' @']
+
+
+# extend plugins with stuff that is concerned with redactor instance
+plugins.mentions = ->
+ #########
+ # setup #
+ #########
+
+ init: ->
+ this.mentions.select_state = null # state of display of user select
+ this.mentions.selected = null # current user select index
+ this.mentions.$userSelect = null # user select element
+
+ this.mentions.validateOptions()
+ utils.loadUsers(this.opts.mentions.url, this.opts.mentions.formatUserListItem)
+ this.mentions.setupUserSelect()
+ this.mentions.setupEditor()
+
+ validateOptions: ->
+ # make sure options are set to valid values
+ required = [
+ "url"
+ "maxUsers"
+ ]
+ for name in required
+ if not this.opts.mentions[name]
+ throw "Mention plugin requires option: #{ name }"
+
+
+ setupUserSelect: ->
+ # init it's state to false
+ this.mentions.select_state = false
+ # create dom nodes
+ this.mentions.$containerDiv = $ ''
+ # hide it by default
+ this.mentions.$containerDiv.hide()
+
+ this.mentions.$userSelect = $ '
'
+ this.mentions.$containerDiv.append this.mentions.$userSelect
+ # setup event handlers
+ this.mentions.$userSelect.mousemove $.proxy(this.mentions.selectMousemove, this)
+ this.mentions.$userSelect.mousedown $.proxy(this.mentions.selectClick, this)
+ # insert it into active dom tree
+ this.$editor.after this.mentions.$containerDiv
+
+ setupEditor: ->
+ # setup event handlers
+ this.$editor.on "keydown.mentions", $.proxy(this.mentions.editorKeydown, this)
+ this.$editor.on "mousedown.mentions", $.proxy(this.mentions.selectClick, this)
+
+ ##################
+ # event handlers #
+ ##################
+
+ # select event handlers
+ selectMousemove: (e) ->
+ $target = $ e.target
+ if $target.hasClass 'user'
+ this.mentions.selected = this.mentions.$userSelect.children().index $target
+ this.mentions.paintSelected()
+
+ selectClick: (e) ->
+ if this.mentions.select_state
+ e.preventDefault()
+ this.mentions.chooseUser()
+ this.mentions.closeMention()
+ this.mentions.setCursorAfterMention()
+ this.mentions.disableSelect()
+
+ # editor event handlers
+ editorKeydown: (e) ->
+ that = this
+
+ if this.mentions.cursorInMention()
+ switch e.which
+ when 27 # escape
+ this.mentions.closeMention()
+ this.mentions.disableSelect()
+
+ when 9, 13 # tab, return
+ e.preventDefault()
+
+ # work around to prevent tabs being inserted
+ tabFocus = this.opts.tabFocus
+ this.opts.tabFocus = false
+
+ if this.mentions.select_state and this.mentions.$userSelect.children().length > 0
+ this.mentions.chooseUser()
+
+ this.mentions.closeMention()
+ this.mentions.setCursorAfterMention()
+ this.mentions.disableSelect()
+
+ # reset tabFocus when you return to the event loop
+ setTimeout ->
+ that.opts.tabFocus = tabFocus
+ , 0
+
+ when 38 # up
+ e.preventDefault()
+ this.mentions.moveSelectUp()
+
+ when 40 # down
+ e.preventDefault()
+ this.mentions.moveSelectDown()
+
+ else if utils.cursorAfterMentionStart()
+ utils.createMention()
+ this.mentions.enableSelect()
+
+ # after every key press, make sure that select state is correct
+ setTimeout $.proxy(this.mentions.updateSelect, this), 0
+
+ editorMousedown: ->
+ # after every mousepress, make sure that select state is correct
+ setTimeout $.proxy(this.mentions.updateSelect, this), 0
+
+ ########################
+ # select functionality #
+ ########################
+
+ positionContainerDiv: ->
+ $firstNode = $ this.selection.getNodes()[0]
+ boxOffset = this.$box.offset()
+ nodeOffset = $firstNode.offset()
+ this.mentions.$containerDiv.css(
+ left: nodeOffset.left - boxOffset.left,
+ top: nodeOffset.top - boxOffset.top + parseFloat($firstNode.css("line-height"))
+ )
+
+ updateSelect: ->
+ if this.mentions.cursorInMention()
+ this.mentions.filterUsers()
+ this.mentions.positionContainerDiv()
+ this.mentions.$containerDiv.show()
+ else
+ this.mentions.$containerDiv.hide()
+
+ moveSelectUp: ->
+ if this.mentions.selected > 0
+ this.mentions.selected -= 1
+ this.mentions.paintSelected()
+
+ moveSelectDown: ->
+ if this.mentions.selected < this.mentions.$userSelect.children().length - 1
+ this.mentions.selected += 1
+ this.mentions.paintSelected()
+
+ enableSelect: ->
+ this.mentions.select_state = true
+ this.mentions.selected = 0
+
+ # build initial user select
+ for i in [0...this.opts.mentions.maxUsers]
+ this.mentions.$userSelect.append users[i].$element
+
+ this.mentions.paintSelected()
+ this.mentions.positionContainerDiv()
+ this.mentions.$containerDiv.show()
+
+ disableSelect: ->
+ this.mentions.select_state = false
+ this.mentions.selected = null
+ this.mentions.$userSelect.children().detach()
+ this.mentions.$containerDiv.hide()
+
+ paintSelected: ->
+ $elements = $ 'li', this.mentions.$userSelect
+ $elements.removeClass 'selected'
+ $elements.eq(this.mentions.selected).addClass 'selected'
+
+ chooseUser: ->
+ user = this.mentions.userFromSelected()
+ prefix = this.opts.mentions.urlPrefix or '/user/'
+ $mention = this.mentions.getCurrentMention()
+ $mention.attr "href", prefix + user.username
+ $mention.text "@#{ user.username }"
+ if this.opts.mentions.alterUserLink
+ this.opts.mentions.alterUserLink $mention, user
+
+ userFromSelected: ->
+ this.mentions.$userSelect.children('li')[this.mentions.selected].user
+
+ filterUsers: ->
+ # empty out userSelect
+ this.mentions.$userSelect.children().detach()
+
+ # query for filter_string once
+ filter_string = this.mentions.getFilterString()
+
+ # build filtered users list
+ count = 0
+ for user in users
+ # break on max filter users
+ break if count >= this.opts.mentions.maxUsers
+
+ if utils.filterTest user, filter_string
+ this.mentions.$userSelect.append user.$element
+ count++
+
+ this.mentions.paintSelected()
+
+ getFilterString: ->
+ mention = this.mentions.getCurrentMention()
+ filter_str = mention.text()
+ # remove @ from the begining
+ filter_str = filter_str.slice 1
+ # replace A0 with 20
+ filter_str = filter_str.replace /\u00a0/g, ' '
+ # remove zero width spaces
+ filter_str.replace /\u200b/g, ''
+
+ #########################
+ # mention functionality #
+ #########################
+
+ closeMention: ->
+ mention = this.mentions.getCurrentMention()
+ mention.attr "contenteditable", "false"
+
+ getCurrentMention: ->
+ # return the current mention based on cursor position, if there
+ # isn't one then return false
+
+ # first check the current element, if it is a mention return it
+ current = $ this.selection.getCurrent()
+ return current if current.hasClass 'mention'
+
+ # else select from parents
+ parents = current.parents '.mention'
+ return parents.eq 0 if parents.length > 0
+
+ # throw if there isn't a current mention
+ throw "There is no current mention."
+
+ cursorInMention: ->
+ try
+ this.mentions.getCurrentMention().length > 0
+ catch e
+ return false if e == "There is no current mention."
+ throw e
+ true
+
+ setCursorAfterMention: ->
+ mention = this.mentions.getCurrentMention()
+
+ # insert space after mention
+ mention.after "\u00a0"
+
+ # set cursor
+ selection = window.getSelection()
+ new_range = document.createRange()
+ new_range.setStart mention[0].nextSibling, 1
+ new_range.setEnd mention[0].nextSibling, 1
+ selection.removeAllRanges()
+ selection.addRange new_range
diff --git a/redactor-mentions.less b/redactor-mentions.less
new file mode 100644
index 0000000..40d22c7
--- /dev/null
+++ b/redactor-mentions.less
@@ -0,0 +1,35 @@
+.redactor-box {
+ .redactor-mentions-container {
+ position: absolute;
+ border: 2px solid #aaa;
+ background: #fff;
+ z-index: 5000;
+ // box-shadow: 5px 5px 15px #888;
+ }
+
+ .mention {
+ text-decoration: none !important;
+ cursor: default;
+ }
+
+ .user-select {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+
+ li {
+ padding: 3px;
+ cursor: pointer;
+
+ img {
+ margin: 0 4px 0 0;
+ vertical-align: bottom;
+ }
+ }
+
+ .selected {
+ background-color: #3577b9;
+ color: #ffffff;
+ }
+ }
+}