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