diff --git a/Gemfile b/Gemfile
index f433391..ad156a6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -11,8 +11,9 @@ gem 'hirb' # pretty console output
gem 'rb-readline'
gem 'rest-client'
gem 'activerecord-session_store'
-gem 'highlight_js-rails', :git => 'git://github.com/RedstonerServer/highlight_js-rails.git'
+gem 'highlight_js-rails', github: 'RedstonerServer/highlight_js-rails'
gem 'kaminari', github: 'jomo/kaminari', branch: 'patch-2' # pagination
+gem 'jquery-textcomplete-rails', github: 'RedstonerServer/jquery-textcomplete-rails' # @mentions
# Gems used only for assets and not required
# in production environments by default.
diff --git a/Gemfile.lock b/Gemfile.lock
index 04db23a..022c07d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -5,6 +5,15 @@ GIT
highlight_js-rails (8.4)
rails (>= 3.1.1)
+GIT
+ remote: git://github.com/RedstonerServer/jquery-textcomplete-rails.git
+ revision: 8bf23af2d8fa1c5226c2b6889c7796adfe1f8772
+ specs:
+ jquery-textcomplete-rails (0.1.4)
+ coffee-rails (>= 3.2.0)
+ railties (>= 3.2.0)
+ sass-rails (>= 3.2.0)
+
GIT
remote: git://github.com/jomo/kaminari.git
revision: e49066e94d77a6abb03a0819f3c4b0cc6923cb70
@@ -179,6 +188,7 @@ DEPENDENCIES
highlight_js-rails!
hirb
jquery-rails
+ jquery-textcomplete-rails!
kaminari!
mysql2
rails (= 4.1.0)
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index b1c4d1e..3b1499f 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -14,4 +14,4 @@
//= require app
//= require editor
//= require highlight
-//= require autocomplete
\ No newline at end of file
+//= require jquery-textcomplete
\ No newline at end of file
diff --git a/app/assets/javascripts/autocomplete.js b/app/assets/javascripts/autocomplete.js
deleted file mode 100644
index 376ced5..0000000
--- a/app/assets/javascripts/autocomplete.js
+++ /dev/null
@@ -1,612 +0,0 @@
-/**
- * Plugin Name: Autocomplete for Textarea
- * Author: Amir Harel
- * Copyright: amir harel (harel.amir1@gmail.com)
- * Twitter: @amir_harel
- * Version 1.4
- * Published at : http://www.amirharel.com/2011/03/07/implementing-autocomplete-jquery-plugin-for-textarea/
- */
-
-(function($){
- /**
- * @param obj
- * @attr wordCount {Number} the number of words the user want to for matching it with the dictionary
- * @attr mode {String} set "outter" for using an autocomplete that is being displayed in the outter layout of the textarea, as opposed to inner display
- * @attr on {Object} containing the followings:
- * @attr query {Function} will be called to query if there is any match for the user input
- */
- $.fn.autocomplete = function(obj){
- this.each(function(index,element){
- if( element.nodeName == 'TEXTAREA' ){
- makeAutoComplete(element,obj);
- }
- });
- };
-
- //var browser = {isChrome: $.browser.webkit };
-
- function getTextAreaSelectionEnd(ta) {
- var textArea = ta;//document.getElementById('textarea1');
- if (document.selection) { //IE
- var bm = document.selection.createRange().getBookmark();
- var sel = textArea.createTextRange();
- sel.moveToBookmark(bm);
- var sleft = textArea.createTextRange();
- sleft.collapse(true);
- sleft.setEndPoint("EndToStart", sel);
- return sleft.text.length + sel.text.length;
- }
- return textArea.selectionEnd; //ff & chrome
- }
-
- function getDefaultCharArray(){
- return {
- '`':0,
- '~':0,
- '1':0,
- '!':0,
- '2':0,
- '@':0,
- '3':0,
- '#':0,
- '4':0,
- '$':0,
- '5':0,
- '%':0,
- '6':0,
- '^':0,
- '7':0,
- '&':0,
- '8':0,
- '*':0,
- '9':0,
- '(':0,
- '0':0,
- ')':0,
- '-':0,
- '_':0,
- '=':0,
- '+':0,
- 'q':0,
- 'Q':0,
- 'w':0,
- 'W':0,
- 'e':0,
- 'E':0,
- 'r':0,
- 'R':0,
- 't':0,
- 'T':0,
- 'y':0,
- 'Y':0,
- 'u':0,
- 'U':0,
- 'i':0,
- 'I':0,
- 'o':0,
- 'O':0,
- 'p':0,
- 'P':0,
- '[':0,
- '{':0,
- ']':0,
- '}':0,
- 'a':0,
- 'A':0,
- 's':0,
- 'S':0,
- 'd':0,
- 'D':0,
- 'f':0,
- 'F':0,
- 'g':0,
- 'G':0,
- 'h':0,
- 'H':0,
- 'j':0,
- 'J':0,
- 'k':0,
- 'K':0,
- 'l':0,
- 'L':0,
- ';':0,
- ':':0,
- '\'':0,
- '"':0,
- '\\':0,
- '|':0,
- 'z':0,
- 'Z':0,
- 'x':0,
- 'X':0,
- 'c':0,
- 'C':0,
- 'v':0,
- 'V':0,
- 'b':0,
- 'B':0,
- 'n':0,
- 'N':0,
- 'm':0,
- 'M':0,
- ',':0,
- '<':0,
- '.':0,
- '>':0,
- '/':0,
- '?':0,
- ' ':0
- };
- }
-
-
- function setCharSize(data){
- for( var ch in data.chars ){
- if( ch == ' ' ) $(data.clone).html(" ");
- else $(data.clone).html(""+ch+"");
- var testWidth = $("#test-width_"+data.id).width();
- data.chars[ch] = testWidth;
- }
- }
-
- var _data = {};
- var _count = 0;
- function makeAutoComplete(ta,obj){
- _count++;
- _data[_count] = {
- id:"auto_"+_count,
- ta:ta,
- wordCount:obj.wordCount,
- on:obj.on,
- clone:null,
- lineHeight:0,
- list:null,
- charInLines:{},
- mode:obj.mode,
- chars:getDefaultCharArray()};
-
- var clone = createClone(_count);
-
- // we need to fix the width
- window.onresize = function() {
- clone.remove();
- clone = createClone(_count);
- console.debug('window resized!');
- };
- _data[_count].clone = clone;
- setCharSize(_data[_count]);
- //_data[_count].lineHeight = $(ta).css("font-size");
- _data[_count].list = createList(_data[_count]);
- registerEvents(_data[_count]);
- }
-
- function createList(data){
- var ul = document.createElement("ul");
- $(ul).addClass("auto-list");
- document.body.appendChild(ul);
- return ul;
- }
-
- function createClone(id) {
- var data = _data[id];
- var div = document.createElement("div");
- var offset = $(data.ta).offset();
- offset.top = offset.top - parseInt($(data.ta).css("margin-top"));
- offset.left = offset.left - parseInt($(data.ta).css("margin-left"));
- //console.log("createClone: offset.top=",offset.top," offset.left=",offset.left);
- $(div).css({
- position:"absolute",
- top: offset.top,
- left: offset.left,
- "border-collapse" : $(data.ta).css("border-collapse"),
- "border-bottom-style" : $(data.ta).css("border-bottom-style"),
- "border-bottom-width" : $(data.ta).css("border-bottom-width"),
- "border-left-style" : $(data.ta).css("border-left-style"),
- "border-left-width" : $(data.ta).css("border-left-width"),
- "border-right-style" : $(data.ta).css("border-right-style"),
- "border-right-width" : $(data.ta).css("border-right-width"),
- "border-spacing" : $(data.ta).css("border-spacing"),
- "border-top-style" : $(data.ta).css("border-top-style"),
- "border-top-width" : $(data.ta).css("border-top-width"),
- "direction" : $(data.ta).css("direction"),
- "font-size-adjust" : $(data.ta).css("font-size-adjust"),
- "font-size" : $(data.ta).css("font-size"),
- "font-stretch" : $(data.ta).css("font-stretch"),
- "font-style" : $(data.ta).css("font-style"),
- "font-family" : $(data.ta).css("font-family"),
- "font-variant" : $(data.ta).css("font-variant"),
- "font-weight" : $(data.ta).css("font-weight"),
- "width" : $(data.ta).css("width"),
- "height" : $(data.ta).css("height"),
- "letter-spacing" : $(data.ta).css("letter-spacing"),
- "margin-bottom" : $(data.ta).css("margin-bottom"),
- "margin-top" : $(data.ta).css("margin-top"),
- "margin-right" : $(data.ta).css("margin-right"),
- "margin-left" : $(data.ta).css("margin-left"),
- "padding-bottom" : $(data.ta).css("padding-bottom"),
- "padding-top" : $(data.ta).css("padding-top"),
- "padding-right" : $(data.ta).css("padding-right"),
- "padding-left" : $(data.ta).css("padding-left"),
- "overflow-x" : "hidden",
- "line-height" : $(data.ta).css("line-height"),
- "overflow-y" : "hidden",
- "z-index" : -10,
- "color" : "transparent",
- "visibility" : "hidden"
- });
-
- //console.log("createClone: ta width=",$(data.ta).css("width")," ta clientWidth=",data.ta.clientWidth, "scrollWidth=",data.ta.scrollWidth," offsetWidth=",data.ta.offsetWidth," jquery.width=",$(data.ta).width());
- //i don't know why by chrome adds some pixels to the clientWidth...
- data.chromeWidthFix = (data.ta.clientWidth - $(data.ta).width());
- data.lineHeight = $(data.ta).css("line-height");
- if( isNaN(parseInt(data.lineHeight)) ) data.lineHeight = parseInt($(data.ta).css("font-size"))+2;
-
- document.body.appendChild(div);
- return div;
- }
-
- /*function breakLongLines(lines,miror){
- var ret = [];
- for( var i=0; i"+line+"");
- var span = miror.children("span");
- if( span.width()+10 < miror.width()) return [line];
- while( span.width() >= miror.width()-10 ){
-
- count++;
- var words = line.split(" ");
- var left = words.slice(0,words.length-count);
- var right = words.slice(words.length-count,words.length);
- miror.html(""+left.join(" ")+"");
- var span = miror.children("span");
-
- ret = [left.join(" "),right.join(" ")];
- }
- var arr = breakLine(ret[1],miror);
- var ret2= [ret[0]];
- for( var i=0; i= 0 && text.charAt(pos) != '\n'){
- ret.unshift(text.charAt(pos));
- pos--;
- if( text.charAt(pos) == ' ' || pos < 0 ){
- wordsFound++;
- }
- }
- return ret.join("");
- }
-
-
- function showList(data,list,text){
- if( !data.listVisible ){
- data.listVisible = true;
- var pos = getCursorPosition(data);
- $(data.list).css({
- left: pos.left+"px",
- top: pos.top+"px",
- display: "block"
- });
-
- }
-
- var html = "";
- var regEx = new RegExp("("+text.slice(1)+")", "i");
- var taWidth = $(data.ta).width()-5;
- var width = data.mode == "outter" ? "style='width:"+taWidth+"px;'" : "";
- for( var i=0; i< list.length; i++ ){
-
- //var a = list[i].replace(regEx,"$1");
-
-
- html += ""+list[i].replace(regEx,"$1")+"";
- }
- $(data.list).html(html);
- }
-
- function breakLines(text,data){
- var lines = [];
-
- var width = $(data.clone).width();
-
- var line1 = "";
- var line1Width = 0;
- var line2Width = 0;
- var line2 = "";
- var chSize = data.chars;
-
-
- var len = text.length;
- for( var i=0; i"+lines[i]+"");
- }
- miror.append(""+lines[lines.length-1]+"");
-
- miror.append(""+restText.replace(/\n/g,"
")+" ");
-
- miror.get(0).scrollTop = ta.scrollTop;
-
- var span = miror.children("#"+data.id);
- var offset = span.offset();
-
- return {top:offset.top+span.height(),left:offset.left+span.width()};
-
- }
-
- function getOuterPosition(data){
- var offset = $(data.ta).offset();
- return {top:offset.top+$(data.ta).height()+8,left:offset.left};
- }
-
- function hideList(data){
- if( data.listVisible ){
- $(data.list).css("display","none");
- data.listVisible = false;
- }
- }
-
- function setSelected(dir,data){
- var selected = $(data.list).find("[data-selected=true]");
- if( selected.length != 1 ){
- if( dir > 0 ) $(data.list).find("li:first-child").attr("data-selected","true");
- else $(data.list).find("li:last-child").attr("data-selected","true");
- return;
- }
- selected.attr("data-selected","false");
- if( dir > 0 ){
- selected.next().attr("data-selected","true");
- }
- else{
- selected.prev().attr("data-selected","true");
- }
-
- }
-
- function getCurrentSelected(data){
- var selected = $(data.list).find("[data-selected=true]");
- if( selected.length == 1) return selected.get(0);
- return null;
- }
-
- function onUserSelected(li,data){
- var seletedText = $(li).attr("data-value");
-
-
- var selectionEnd = getTextAreaSelectionEnd(data.ta);//.selectionEnd;
- var text = data.ta.value;
- text = text.substr(0,selectionEnd);
- //if( text.charAt(text.length-1) == ' ' || text.charAt(text.length-1) == '\n' ) return "";
- //var ret = [];
- var wordsFound = 0;
- var pos = text.length-1;
-
- while( wordsFound < data.wordCount && pos >= 0 && text.charAt(pos) != '\n'){
- //ret.unshift(text.charAt(pos));
- pos--;
- if( text.charAt(pos) == ' ' || pos < 0 ){
- wordsFound++;
- }
- }
- var a = data.ta.value.substr(0,pos+1);
- var c = data.ta.value.substr(selectionEnd,data.ta.value.length);
- var scrollTop = data.ta.scrollTop;
- data.ta.value = a+seletedText+c;
- data.ta.scrollTop = scrollTop;
- data.ta.selectionEnd = pos+1+seletedText.length;
- hideList(data);
- $(data.ta).focus();
- }
-
- function registerEvents(data){
- $(data.list).delegate("li","click",function(e){
- var li = this;
- onUserSelected(li,data);
- e.stopPropagation();
- e.preventDefault();
- return false;
- });
-
-
-
- $(data.ta).blur(function(e){
- setTimeout(function(){
- hideList(data);
- },400);
-
- });
-
- $(data.ta).click(function(e){
- hideList(data);
- });
-
- $(data.ta).keydown(function(e){
- //console.log("keydown keycode="+e.keyCode);
- if( data.listVisible ){
- switch(e.keyCode){
- case 9:
- case 13:
- case 40:
- case 38:
- e.stopImmediatePropagation();
- e.preventDefault();
- return false;
- case 27: //esc
- hideList(data);
- }
-
- }
- });
-
- $(data.ta).keyup(function(e){
- if( data.listVisible ){
- //console.log("keCode=",e.keyCode);
- if( e.keyCode == 40 ){//down key
- setSelected(+1,data);
- e.stopImmediatePropagation();
- e.preventDefault();
- return false;
- }
- if( e.keyCode == 38 ){//up key
- setSelected(-1,data);
- e.stopImmediatePropagation();
- e.preventDefault();
- return false;
- }
- if( e.keyCode == 13 || e.keyCode == 9 ){//enter key or Tab key
- var li = getCurrentSelected(data);
- if( li ){
-
- e.stopImmediatePropagation();
- e.preventDefault();
- hideList(data);
- onUserSelected(li,data);
- return false;
- }
- hideList(data);
- }
- if( e.keyCode == 27 ){
- e.stopImmediatePropagation();
- e.preventDefault();
- return false;
- }
- }
- switch( e.keyCode ){
- case 27:
- return true;
- }
-
- var text = getWords(data);
- //console.log("getWords return ",text);
- if( text != "" ){
- data.on.query(text,function(list){
- //console.log("got list = ",list);
- if( list.length ){
- showList(data,list,text);
- }
- else{
- hideList(data);
- }
-
-
- });
- }
- else{
- hideList(data);
- }
- });
-
-
-
- $(data.ta).scroll(function(e){
- var ta = e.target;
- var miror = $(data.clone);
- miror.get(0).scrollTop = ta.scrollTop;
- });
- }
-})(jQuery);
\ No newline at end of file
diff --git a/app/assets/javascripts/editor.js b/app/assets/javascripts/editor.js
index 9d0e32f..f5f6da2 100644
--- a/app/assets/javascripts/editor.js
+++ b/app/assets/javascripts/editor.js
@@ -19,10 +19,10 @@ $(function() {
target.data('preview', 'true');
target.text('Edit');
var prev = target.parent().find('.preview');
- var editor = target.parent().find('.editor_field')
+ var editor = target.parent().find('.editor_field');
prev.html("(Loading ...)");
prev.show();
- editor.hide()
+ editor.hide();
if (target.parent().parent().hasClass('mini')) {
var url = '/tools/render_mini_markdown';
} else {
@@ -52,36 +52,45 @@ $(function() {
});
}
- var query_history = {};
- $('.md_editor .editor_field').autocomplete({
- wordCount: 1,
- mode: "inner",
- on: {
- query: function(text, callback) {
- if (text.length > 2 && text[0] == "@") {
- text = text.slice(1);
- if (query_history[text]) {
- callback(query_history[text]);
- } else {
- $.ajax("/users/suggestions", {
- type: 'post',
- data: {name: text},
- dataType: 'json',
- headers: {
- 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
- },
- success: function(data) {
- query_history[text] = data;
- callback(data);
- },
- error: function(xhr, status, err) {
- callback([]);
- }
- });
- }
+ $('.md_editor .editor_field').textcomplete([{
+ // match up to 2 words (everything except some special characters)
+ // each word can have up to 16 characters (up to 32 total)
+ // words must be separated by a single space
+ match: /(^|\s)@(([^!"§$%&\/()=?.,;+*@\s]{1,16} ?){0,1}[^!"§$%&\/()=?.,;+*@\s]{1,16})$/,
+ search: function (text, callback, match) {
+ console.log("Searching " + text);
+ text = text.toLowerCase();
+ $.ajax("/users/suggestions", {
+ type: "post",
+ data: {name: text},
+ dataType: "json",
+ headers: {
+ "X-CSRF-Token": $('meta[name="csrf-token"]').attr("content")
+ },
+ success: function(data) {
+ callback(data);
+ },
+ error: function(xhr, status, err) {
+ console.error(err);
+ callback([]);
}
+ });
+ },
+ template: function(user) {
+ var name = user[0];
+ var ign = user[1];
+ if (name != ign) {
+ return name + " (" + ign + ")";
+ } else {
+ return ign;
}
+ },
+ cache: true,
+ replace: function (word) {
+ return "$1@" + word[1] + " ";
}
+ }], {
+ debounce: 300
});
});
\ No newline at end of file
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index cac39f4..0ebd145 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -3,4 +3,5 @@
*= require style.css
*= require mobi.css
*= require highlight/default.css
+ *= require jquery-textcomplete
*/
\ No newline at end of file
diff --git a/app/assets/stylesheets/style.css.scss b/app/assets/stylesheets/style.css.scss
index 4f758e4..96d5f1a 100644
--- a/app/assets/stylesheets/style.css.scss
+++ b/app/assets/stylesheets/style.css.scss
@@ -485,20 +485,24 @@ blockquote p {
display: none;
padding: 4em 1em 1em;
}
+ }
+}
- .suggestions {
- background: #ddd;
- padding: 0.5em;
+ul.dropdown-menu {
+ background-color: #fff;
+ border: none;
+ border-radius: 4px;
- .name {
- display: inline-block;
- font-style: italic;
- padding: 1px 2px;
- margin-right: 1em;
- border: 1px solid #6cf;
- border-radius: 4px;
- font-weight: bold;
- }
+ li.textcomplete-item {
+ padding: 0.5em 1em;
+ border: none;
+
+ &.active, &:hover {
+ background-color: #6cf;
+ }
+
+ a {
+ color: #000;
}
}
}
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index c1b738a..4bbd41d 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -2,6 +2,7 @@ class UsersController < ApplicationController
require 'open-uri'
include MailerHelper
+ include ERB::Util
before_filter :set_user, except: [:index, :new, :create, :lost_password, :reset_password, :suggestions]
@@ -290,11 +291,14 @@ class UsersController < ApplicationController
def suggestions
query = params[:name]
- if current_user && query.present? && query =~ /\A[a-zA-Z0-9_]{1,16}\Z/
- @users = User.where("ign LIKE ?", "#{query}%").order(:ign).limit(7)
- @users = @users.to_a.map{|u| u.ign}
+ # same regex as the one used for textcomplete
+ if current_user && query.present? && query =~ /\A([^!"§$%&\/()=?.,;+*@\s]{1,16} ?){0,1}[^!"§$%&\/()=?.,;+*@\s]{1,16}\Z/
+ query.gsub!(/[_%]/) {|c|"\\#{c}"} # escape LIKE wildcard characters
+ @users = User.where("ign LIKE ? or name LIKE ?", "%#{query}%", "%#{query}%").order(:name, :ign).limit(7)
+ @users = @users.to_a.map{|u| [html_escape(u.name), html_escape(u.ign)]}
render json: @users
else
+ puts "'#{query}' does not match regex!"
render json: []
end
end