From 2b1e8acee30d5aba5479e49fe5561db6ec081c7d Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 6 Jul 2014 02:38:44 +0200 Subject: [PATCH] add username suggestions to editor --- app/assets/javascripts/application.js | 3 +- app/assets/javascripts/autocomplete.js | 603 +++++++++++++++++++++++++ app/assets/javascripts/editor.js | 33 ++ app/assets/stylesheets/style.css.scss | 51 ++- app/controllers/users_controller.rb | 13 +- app/views/application/_mdhelp.html.erb | 2 +- config/routes.rb | 1 + 7 files changed, 702 insertions(+), 4 deletions(-) create mode 100644 app/assets/javascripts/autocomplete.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 89cdb16..4a58cd9 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,4 +14,5 @@ //= require app //= require editor //= require ago -//= require highlight \ No newline at end of file +//= require highlight +//= require autocomplete \ No newline at end of file diff --git a/app/assets/javascripts/autocomplete.js b/app/assets/javascripts/autocomplete.js new file mode 100644 index 0000000..40df641 --- /dev/null +++ b/app/assets/javascripts/autocomplete.js @@ -0,0 +1,603 @@ +/** + * 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); + _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 + }); + + //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)+")"); + 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 f82b7f7..4e91eda 100644 --- a/app/assets/javascripts/editor.js +++ b/app/assets/javascripts/editor.js @@ -52,4 +52,37 @@ $(function() { }); } + var query_history = {}; + $('.md_editor .editor_field').autocomplete({ + wordCount: 1, + mode: "inner", + on: { + query: function(text, callback) { + console.log(query_history) + 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([]); + } + }); + } + } + } + } + }); + }); \ No newline at end of file diff --git a/app/assets/stylesheets/style.css.scss b/app/assets/stylesheets/style.css.scss index 851eb9c..e7f2fbc 100644 --- a/app/assets/stylesheets/style.css.scss +++ b/app/assets/stylesheets/style.css.scss @@ -467,15 +467,64 @@ blockquote p { left: 1em; z-index: 10; } + .editor_field { padding-top: 4em; min-height: 5em; + margin: 0; } .preview { display: none; padding: 4em 1em 1em; } + + .suggestions { + background: #ddd; + padding: 0.5em; + + .name { + display: inline-block; + font-style: italic; + padding: 1px 2px; + margin-right: 1em; + border: 1px solid #6cf; + border-radius: 4px; + font-weight: bold; + } + } + } +} + +ul.auto-list { + display: none; + position: absolute; + top: 0; + left: 0; + border: 1px solid #000; + background-color: #363636; + padding: 0; + margin: 0; + list-style: none; + z-index: 999; + color: #fff; + + li { + cursor: pointer; + padding: 2px; + + &:hover, &[data-selected=true] { + background-color: #f66; + } + + mark { + font-weight: bold; + margin: 0; + padding: 0; + background: inherit; + text-decoration: underline; + color: inherit; + } } } @@ -484,7 +533,7 @@ blockquote p { } .markdown-help { - margin: 4px 0 -4px; + margin: 4px 0 0; background: #ddd; padding: 0.5em 1em; border-bottom: 1px solid; diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 7cd3c49..061ddc6 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -3,7 +3,7 @@ class UsersController < ApplicationController require 'open-uri' include MailerHelper - before_filter :set_user, except: [:index, :new, :create, :lost_password, :reset_password] + before_filter :set_user, except: [:index, :new, :create, :lost_password, :reset_password, :suggestions] def index if params[:role] @@ -286,6 +286,17 @@ class UsersController < ApplicationController end end + 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} + render json: @users + else + render json: [] + end + end + private def validate_token(uuid, email, token) diff --git a/app/views/application/_mdhelp.html.erb b/app/views/application/_mdhelp.html.erb index cb21e63..6979bfd 100644 --- a/app/views/application/_mdhelp.html.erb +++ b/app/views/application/_mdhelp.html.erb @@ -7,6 +7,6 @@ ==mark== | [link](https://example.com) - <%= link_to "more...", "/info/1", target: "_blank", class: "right", tabindex: -1 %> + <%= link_to "formatting help", "/info/1", target: "_blank", class: "right", tabindex: -1 %>
    \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 3868199..4128402 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,6 +24,7 @@ Redstoner::Application.routes.draw do collection do get 'lost_password' post 'reset_password' + post 'suggestions' end end