Browse files

Merge pull request #245 from ichord/feature

@mention 更新, 改用 gem.
  • Loading branch information...
2 parents c23bdbb + 7df58d9 commit bf952d571f628c14eab5977e1595756f206a6ce8 @huacnlee huacnlee committed Mar 18, 2012
View
1 Gemfile
@@ -4,6 +4,7 @@ gem "rails", "3.2.2"
gem "rails-i18n","0.1.8"
gem "jquery-rails", "1.0.16"
gem "rails_autolink", ">= 1.0.4"
+gem "jquery-atwho-rails"
group :assets do
gem 'sass-rails', " ~> 3.2.3"
View
11 app/assets/javascripts/app.coffee
@@ -15,8 +15,7 @@
#= require jquery.chosen
#= require jquery.autogrow-textarea
#= require social-share-button
-#= require jquery.at.js
-#= require jquery.caret.js
+#= require jquery.atwho
#= require_self
window.App =
loading : () ->
@@ -67,10 +66,10 @@ window.App =
# 绑定 @ 回复功能
at_replyable : (el, logins) ->
- $(el).atWho
- debug : false
+ $(el).atWho "@"
+ debug : true
data : logins
- tpl : "<li data-insert='${login}'>${login} <small>${name}</small></li>"
+ tpl : "<li data-keyname='${login}'>${login} <small>${name}</small></li>"
initForDesktopView : () ->
return if typeof(app_mobile) != "undefined"
@@ -141,4 +140,4 @@ $(document).ready ->
# Choose 样式
$("select").chosen()
-
+
View
2 app/assets/stylesheets/application.scss
@@ -5,7 +5,7 @@
*= require jquery.chosen
*= require highlight
*= require social-share-button
- *= require jquery.at
+ *= require jquery.atwho
*= require_tree ./sprites/
*= require_self
*/
View
427 vendor/assets/javascripts/jquery.at.js
@@ -1,427 +0,0 @@
-/*
- Implement Twitter/Weibo @ mentions
-
- Copyright (c) 2012 chord.luo@gmail.com
-
- Permission is hereby granted, free of charge, to any person obtaining
- a copy of this software and associated documentation files (the
- "Software"), to deal in the Software without restriction, including
- without limitation the rights to use, copy, modify, merge, publish,
- distribute, sublicense, and/or sell copies of the Software, and to
- permit persons to whom the Software is furnished to do so, subject to
- the following conditions:
-
- The above copyright notice and this permission notice shall be
- included in all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-*/
-
-(function($) {
- /* 克隆(镜像) inputor. 用于获得@在输入框中的位置
- * 复制它的大小形状相关的样式. */
- Mirror = function($origin) {
- this.init($origin);
- }
- Mirror.prototype = {
- $mirror: null,
- css : ["overflowY", "height", "width", "paddingTop", "paddingLeft", "paddingRight", "paddingBottom", "marginTop", "marginLeft", "marginRight", "marginBottom",'fontFamily', 'borderStyle', 'borderWidth','wordWrap', 'fontSize', 'lineHeight', 'overflowX'],
- init: function($origin) {
- $mirror = $('<div></div>');
- var css = {
- opacity: 0,
- position: 'absolute',
- left: 0,
- top:0,
- zIndex: -20000,
- 'word-wrap':'break-word',
- /* wrap long line as textarea do. not work in ie < 8 */
- 'white-space':'pre-wrap'
- }
- $.each(this.css,function(i,p){
- css[p] = $origin.css(p);
- });
- $mirror.css(css);
- $('body').append($mirror);
- this.$mirror = $mirror;
- },
- setContent: function(html) {
- this.$mirror.html(html);
- },
- getFlagPos:function() {
- return this.$mirror.find("span#flag").position();
- },
- height: function() {
- return this.$mirror.height();
- }
- };
- At = {
- keyword : {'text':"",'start':0,'stop':0},
- search_word: "",
- _cache : {},
- // textarea, input.
- $inputor : null,
- // prevent from duplicate binding.
- inputor_keys: [],
- lenght : 0,
- /* @ position in inputor */
- pos: 0,
- /* @ offset*/
- offset: function() {
- $inputor = this.$inputor;
- mirror = $inputor.data("mirror");
- if (isNil(mirror)) {
- mirror = new Mirror($inputor);
- $inputor.data("mirror",mirror);
- }
-
- /* 为了将textarea中文字的排版模拟到镜像div中
- * 我们需要做一些字符处理.由于div元素中不认多余的空格.
- * 我们需要将多余的空格用元素块包裹.
- × 换行符直接替换成<br/>就可以了.
- * NOTE: “\r\n” 用于ie的textarea.
- */
- function format(value) {
- value = value.replace(/</g, '&lt;')
- .replace(/>/g, '&gt;')
- .replace(/`/g,'&#96;')
- .replace(/"/g,'&quot;');
- if ($.browser.msie) {
- rep_str = parseInt($.browser.version) < 8 ? "&nbsp;" : "<span> </span>"
- value = value.replace(/ /g,rep_str);
- }
- return value.replace(/\r\n|\r|\n/g,"<br />");
- }
- /* 克隆完inputor后将原来的文本内容根据
- * @的位置进行分块,以获取@块在inputor(输入框)里的position
- * */
- text = $inputor.val();
- start_range = text.slice(0,this.pos - 1);
- end_range = text.slice(this.pos + 1);
- html = "<span>"+format(start_range)+"</span>";
- html += "<span id='flag'>@</span>";
- html += "<span>"+format(end_range)+"</span>";
- mirror.setContent(html);
-
- /* 将inputor的 offset(相对于document)
- * 和@在inputor里的position相加
- * 就得到了@相对于document的offset.
- * 当然,还要加上行高和滚动条的偏移量.
- * */
- offset = $inputor.offset();
- at_pos = mirror.getFlagPos();
- line_height = $inputor.css("line-height");
- line_height = isNaN(line_height) ? 20 : line_height;
- //FIXME: -$(window).scrollTop() get "wrong" offset.
- // but is good for $inputor.scrollTop();
- // jquey 1. + 07.1 fixed the scrollTop problem!?
- y = offset.top + at_pos.top + line_height
- - $inputor.scrollTop();
- x = offset.left + at_pos.left - $inputor.scrollLeft();
-
- return {'top':y,'left':x};
- },
- cache: function(key,value) {
- if (!settings['cache']) return null;
- log("cacheing",key,value);
- if (value)
- this._cache[key] = value;
- return this._cache[key];
- },
- getKey: function() {
- $inputor = this.$inputor;
- text = $inputor.val();
- //获得inputor中插入符的position.
- caret_pos = $inputor.caretPos();
- /* 向在插入符前的的文本进行正则匹配
- * 考虑会有多个 @ 的存在, 匹配离插入符最近的一个*/
- subtext = text.slice(0,caret_pos);
- // word = subtext.exec(/@(\w+)$|@[^\x00-\xff]+$/g);
- matched = /@(\w+)$|@([^\x00-\xff]+)$/g.exec(subtext);
- key = null;
- if (matched && (word = matched[1]).length < 20) {
- start = caret_pos - word.length;
- end = start + word.length;
- this.pos = start;
- key = {'text':word, 'start':start, 'end':end};
- } else
- this.view.hide();
- this.keyword = key;
- log("getKey",key);
- return key;
- },
- /* 捕捉inputor的上下回车键.
- * 在列表框做相应的操作,上下滚动,回车选择
- * 返回 false 阻止冒泡事件以捕捉inputor对应的事件
- * */
- onkeydown:function(e) {
- view = this.view;
- // 当列表没显示时不捕捉inputor相关事件.
- if (!view.running()) return true;
- last_idx = view.items.length - 1;
- var return_val = false;
- switch (e.keyCode) {
- case 27:
- this.choose();
- break;
- // UP
- case 38:
- // if put this line outside the switch
- // the view will flash when key down.
- $(view.id + " ul li.cur").removeClass("cur");
- view.cur_li_idx--;
- // 到达顶端时高亮效果跳到最后
- if (view.cur_li_idx < 0)
- view.cur_li_idx = last_idx;
- $(view.id + " li:eq(" + view.cur_li_idx + ")")
- .addClass('cur');
- break;
- // DOWN
- case 40:
- $(view.id + " ul li.cur").removeClass("cur");
- view.cur_li_idx++;
- if (view.cur_li_idx > last_idx)
- view.cur_li_idx = 0;
- $(view.id + " li:eq(" + view.cur_li_idx + ")")
- .addClass('cur');
- break;
- //TAB or ENTER
- case 9:
- case 13:
- $(view.id + " ul li.cur").removeClass("cur");
- // 如果列表为空,则不捕捉回车事件
- $cur_li = $(view.id + " li:eq("+view.cur_li_idx+")");
- this.choose($cur_li);
- break;
- default:
- return_val = true;
- }
- return return_val;
- },
- replaceStr: function(str) {
- /* $inputor.replaceStr(str,start,end)*/
- key = this.keyword;
- source = this.$inputor.val();
- start_str = source.slice(0, key.start);
- text = start_str + str + source.slice(key.end);
- this.$inputor.val(text);
- this.$inputor.caretPos(start_str.length + str.length);
- },
- choose: function($li) {
- str = isNil($li) ? this.keyword.text+" " : $li.attr("data-insert")+" ";
- this.replaceStr(str);
- this.view.hide();
- },
- reg: function(inputor) {
- $inputor = $(inputor);
-
- /* 防止对同一个inputor进行多次绑定
- * 在每个已经绑定过的inputor设置一个key.
- * 注册过的key将不再进行绑定
- * */
- key = $inputor.data("@reg-key");
- log("reg",inputor,key);
- if ($.inArray(key,this.inputor_keys) >= 0)
- return null;
- key = "@-"+$.now();
- this.inputor_keys[key];
- // 捕捉inputor事件
- var self = this;
- $inputor.bind("keydown",function(e) {
- return self.onkeydown(e);
- })
- .scroll(function(e){
- self.view.hide();
- })
- .blur(function(e){
- self.view.timeout_id = setTimeout("At.view.hide()",100);
- });
- return key;
- },
- run: function(inputor) {
- this.$inputor = $(inputor);
- key = this.getKey();
- if (!key) return false;
- /*
- * 支持多渠道获得用户数据.
- * 可以设置静态数据的同时从服务器动态获取.
- * 获取级别从先到后: cache -> statis data -> ajax.
- */
- if (!isNil(names = this.cache(this.keyword.text))) {
- log("cache data",names);
- this.view.load(names,false);
- } else if (!isNil(names = this.runWithData(key,settings['data']))) {
- log("statis data",names);
- this.view.load(names,false);
- } else {
- callback = settings['callback'];
- log("callbacking",callback);
- if($.isFunction(callback)) {
- callback(At);
- }
- }
- },
- runWithData:function(key,data) {
- var items = null;
- var self = this;
- if($.isArray(data) && data.length != 0) {
- items = $.map(data,function(item,i) {
- //support plain object also
- var name = $.isPlainObject(item) ? item[self.search_word] : item;
- match = name.match((new RegExp(key.text,"i")));
- return match ? item : null;
- });
- }
- return items;
- }
- };
-
- /* 弹出的用户列表框相关的操作 */
- At.view = {
- //当前高亮的条目
- cur_li_idx : 0,
- timeout_id : null,
- id : '#at-view',
- //at view jquery object
- jqo : null,
- items : [],
- // 列表框是否显示中.
- running :function() {
- return $(this.id).is(":visible");
- },
- evalTpl: function(tpl,map) {
- if(isNil(tpl)) return;
- el = tpl.replace(/\$\{([^\}]*)\}/g,function(tag,key,pos){
- return map[key];
- });
- log("evalTpl",el);
- return el;
- },
- jqObject : function(o) {
- if (!isNil(o)) this.jqo = o;
- return isNil(this.jqo) ? $(this.id) : this.jqo;
- },
- onLoaded: function($view) {
- $view.find('li').live('click',function(e) {
- At.choose($(this));
- });
- $view.mousemove(function(e) {
- if (e.target.tagName == "LI") {
- $(this).find("li.cur").removeClass("cur");
- $(e.target).addClass("cur");
- At.cur_li_idx = $(this).find("li").index(e.target)
- }
- });
- },
- rePosition:function($view) {
- $view.offset(At.offset());
- },
- show: function(){
- if (!this.running())
- $view = $(this.id).show();
- this.rePosition($view);
- },
- hide: function() {
- if (!this.running()) return;
- this.cur_li_idx = 0;
- $(this.id).hide();
- },
- load: function(list,cacheable) {
- // 是否已经加载了列表视图
- if (isNil(this.jqObject())) {
- tpl = "<div id='"+this.id.slice(1)+"' class='at-view'><span id='title'>@who?</span><ul id='"+this.id.slice(1)+"-ul'></ul></div>";
- $at_view = $(tpl);
- $('body').append($at_view);
- this.jqObject($at_view = $(this.id));
- this.onLoaded($at_view);
- }
- return this.update(list,cacheable);
- },
- clear: function(clear_all) {
- if (clear_all == true)
- this._cache = {};
- this.items = [];
- this.jqObject().find('ul').empty();
- },
- update: function(list,cacheable) {
- if (!$.isArray(list)) return false;
- if (cacheable != false) At.cache(At.keyword.text,list);
-
- $ul = this.jqObject().find('ul');
- this.clear();
- $.merge(this.items,list);
- var tpl = settings['tpl'];
- var self = this;
- $.each(list,function(i,item) {
- if (!$.isPlainObject(item)) {
- item = {'id':i,'name':item};
- tpl = DEFAULT_TPL;
- }
- $ul.append(self.evalTpl(tpl,item));
- });
- this.show();
- $ul.find("li:eq(0)").addClass("cur");
- }
- };
-
- function isNil(target) {
- return !target
- //empty_object =
- || ($.isPlainObject(target) && $.isEmptyObject(target))
- //empty_array =
- || ($.isArray(target) && target.length == 0)
- // nil_jquery =
- || (target instanceof $ && target.length == 0)
- || target === undefined;
- }
-
- function log() {
- if (!settings['debug'] || $.browser.msie)
- return;
- console.log(arguments);
- }
-
- function setSettings(options) {
- opt = {};
- if ($.isFunction(options))
- opt['callback'] = options;
- else
- opt = options;
- return $.extend({
- //must return array;
- 'callback': function(context) {return []},
- 'cache' : true,
- 'debug' : false,
- 'tpl' : DEFAULT_TPL,
- 'data':[]
- },opt);
- }
-
- DEFAULT_TPL = "<li id='${id}' data-insert='${name}'>${name}</li>";
-
- $.fn.atWho = function (options) {
- settings = setSettings(options);
- log("settings",settings);
- // just used in At.runWithData
- var match = /data-insert=['?]\$\{(\w+)\}/g.exec(settings['tpl']);
- At.search_word = match[1];
- return this.filter('textarea, input').each(function() {
- if (!At.reg(this)) return;
- $(this).bind("keyup",function(e) {
- /* 当用户列表框显示时, 上下键不触发查询 */
- var stop_key = e.keyCode == 40 || e.keyCode == 38;
- run = !(At.view.running() && stop_key);
- if (run) At.run(this);
- })
- .mouseup(function(e) {
- At.run(this);
- });
- });
- }
-})(jQuery);
View
126 vendor/assets/javascripts/jquery.caret.js
@@ -1,126 +0,0 @@
-/*
- Implement Twitter/Weibo @ mentions
-
- Copyright (c) 2012 chord.luo@gmail.com
-
- Permission is hereby granted, free of charge, to any person obtaining
- a copy of this software and associated documentation files (the
- "Software"), to deal in the Software without restriction, including
- without limitation the rights to use, copy, modify, merge, publish,
- distribute, sublicense, and/or sell copies of the Software, and to
- permit persons to whom the Software is furnished to do so, subject to
- the following conditions:
-
- The above copyright notice and this permission notice shall be
- included in all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-*/
-
-/* 本插件操作 textarea 或者 input 内的插入符
- * 只实现了获得插入符在文本框中的位置,我设置
- * 插入符的位置.
- * */
-(function($) {
- function getCaretPos(inputor) {
- if ("selection" in document) { // IE
- inputor.focus();
- /*
- * reference: http://tinyurl.com/86pyc4s
- */
- var start = 0, end = 0, normalizedValue, range,
- textInputRange, len, endRange;
- var el = inputor;
- /* assume we select "HATE" in the inputor such as textarea -> { }.
- * start end-point.
- * /
- * < I really [HATE] IE > between the brackets is the selection range.
- * \
- * end end-point.
- */
- range = document.selection.createRange();
- pos = 0;
- // selection should in the inputor.
- if (range && range.parentElement() == el) {
- normalizedValue = el.value.replace(/\r\n/g, "\n");
- /* SOMETIME !!!
- *"/r/n" is counted as two char.
- * one line is two, two will be four. balalala.
- * so we have to using the normalized one's length.;
- */
- len = normalizedValue.length;
-
- /*<[ I really HATE IE ]>:
- * the whole content in the inputor will be the textInputRange.
- */
- textInputRange = el.createTextRange();
-
- /* _here must be the position of bookmark.
- * /
- * <[ I really [HATE] IE ]>
- * [---------->[ ] : this is what moveToBookmark do.
- * < I really [[HATE] IE ]> : here is result.
- * \ two brackets in should be in line.
- */
- textInputRange.moveToBookmark(range.getBookmark());
- // IE don't want to let "createTextRange" and "collapse" get together. It's so bad
- endRange = el.createTextRange();
-
- /* [--------------------->[] : if set false all end-point goto end.
- * < I really [[HATE] IE []]>
- */
- endRange.collapse(false);
- /* ___VS____
- * / \
- * < I really [[HATE] IE []]>
- * \_endRange end-point.
- *
- * " > -1" mean the start end-point will be the same or right to the end end-point
- * simplelly, all in the end.
- */
- if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
- // TextRange object will miss "\r\n". So, we count it ourself.
- //line_counter = normalizedValue.slice(0, start).split("\n").length -1;
- start = end = len;
- } else {
- /* I really |HATE] IE ]>
- * <-|
- * I really[ [HATE] IE ]>
- * <-[
- * I reall[y [HATE] IE ]>
- *
- * will return how many unit have moved.
- */
- start = -textInputRange.moveStart("character", -len);
- end = -textInputRange.moveEnd("character", -len);
- }
- }
- } else {
- start = inputor.selectionStart;
- }
- return start;
- }
- function setCaretPos(inputor, pos) {
- if ("selection" in document) { //IE
- range = inputor.createTextRange();
- range.move('character',pos);
- range.select();
- } else
- inputor.setSelectionRange(pos,pos);
- }
- $.fn.caretPos = function(pos) {
- var inputor = this[0];
- if (pos) {
- return setCaretPos(inputor,pos);
- } else {
- return getCaretPos(inputor);
- }
- }
-})(jQuery);
View
50 vendor/assets/stylesheets/jquery.at.css
@@ -1,50 +0,0 @@
-#at-view {
- position:absolute;
- top: 0;
- left: 0;
- display: none;
- margin-top: 18px;
- background: white;
- border: 1px solid #DDD;
- border-radius: 3px;
- box-shadow: 0 0 5px rgba(0,0,0,0.1);
- min-width: 120px;
-}
-#at-view span#title {
- display: none;
- font-size:13px;
- color: #999;
- border-bottom:1px solid #ddd;
- padding: 3px 5px;
- cursor: default;
-}
-
-#at-view .cur {
- background: #0064CD;
- color: white;
-}
-#at-view .cur small {
- color: white;
-}
-#at-view ul {
- /* width: 100px; */
- list-style:none;
- padding:0;
- margin:auto;
-}
-#at-view ul li {
- display: block;
- font-weight:bold;
- color:#333;
- padding: 5px 10px;
- border-bottom: 1px solid #DDD;
- cursor: pointer;
-}
-#at-view small {
- font-size: 12px;
- color: #999;
- margin-left:5px;
- font-weight: normal;
-}
-
-

0 comments on commit bf952d5

Please sign in to comment.