jquery-atwho-rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ *.swp
6
+ tmp/*
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ #source "http://rubygems.org"
2
+ source "http://ruby.taobao.org"
3
+
4
+ # Specify your gem's dependencies in jquery-atwho-rails.gemspec
5
+ gemspec
@@ -0,0 +1,33 @@
1
+ ### Usage
2
+ ---
3
+ bind your textarea
4
+
5
+ ```javascript
6
+ data = ['tom','john'];
7
+ $('textarea').atwho({'debug':true,'data':data});
8
+ ```
9
+
10
+ that's it, check it out!
11
+ more details in [At.js](https://github.com/ichord/At.js) on github.
12
+
13
+ ### Installtion
14
+ ---
15
+ #### Rails 3.0.x
16
+ issue commnd line bellow:
17
+ `rails generate atwho:install`
18
+ then It will show in `public/[javascript|stylesheets]/` directory.
19
+
20
+ #### Rails 3.1.x
21
+ add this gem in `Gemfile` like this:
22
+ `gem jquery-atwho-rails`
23
+
24
+ then add
25
+ ` //= require jquery.atwho `
26
+ to `app/assets/javascripts/application.js`
27
+ and `app/assets/stylesheets/applications.css`
28
+
29
+ ### development
30
+ ---
31
+ #### Test generator
32
+ just issue
33
+ `bundle` then `rspec`
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "jquery-atwho-rails/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "jquery-atwho-rails"
7
+ s.version = Jquery::Atwho::Rails::VERSION
8
+ s.authors = ["ichord"]
9
+ s.email = ["chord.luo@gmail.com"]
10
+ s.homepage = "http://ichord.github.com/jquery-atwho-rails"
11
+ s.summary = %q{jquery plugin: @mentions}
12
+ s.description = %q{This is a jQuery plugin
13
+ that implement Twitter/Weibo like @ mentions.}
14
+
15
+ s.rubyforge_project = "jquery-atwho-rails"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ # specify any dependencies here; for example:
23
+ s.add_development_dependency "rspec"
24
+ s.add_development_dependency "generator_spec"
25
+ # s.add_runtime_dependency "rest-client"
26
+ end
@@ -0,0 +1,547 @@
1
+ /*
2
+ Implement Twitter/Weibo @ mentions
3
+
4
+ Copyright (c) 2012 chord.luo@gmail.com
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ */
25
+
26
+ (function($) {
27
+ /* 克隆(镜像) inputor. 用于获得@在输入框中的位置
28
+ * 复制它的大小形状相关的样式. */
29
+ Mirror = function($origin) {
30
+ this.init($origin);
31
+ }
32
+ Mirror.prototype = {
33
+ $mirror: null,
34
+ css : ["overflowY", "height", "width", "paddingTop", "paddingLeft", "paddingRight", "paddingBottom", "marginTop", "marginLeft", "marginRight", "marginBottom",'fontFamily', 'borderStyle', 'borderWidth','wordWrap', 'fontSize', 'lineHeight', 'overflowX'],
35
+ init: function($origin) {
36
+ $mirror = $('<div></div>');
37
+ var css = {
38
+ opacity: 0,
39
+ position: 'absolute',
40
+ left: 0,
41
+ top:0,
42
+ zIndex: -20000,
43
+ 'word-wrap':'break-word',
44
+ /* wrap long line as textarea do. not work in ie < 8 */
45
+ 'white-space':'pre-wrap'
46
+ }
47
+ $.each(this.css,function(i,p){
48
+ css[p] = $origin.css(p);
49
+ });
50
+ $mirror.css(css);
51
+ $('body').append($mirror);
52
+ this.$mirror = $mirror;
53
+ },
54
+ setContent: function(html) {
55
+ this.$mirror.html(html);
56
+ },
57
+ getFlagPos:function() {
58
+ return this.$mirror.find("span#flag").position();
59
+ },
60
+ height: function() {
61
+ return this.$mirror.height();
62
+ }
63
+ };
64
+ At = {
65
+ keyword : {'text':"",'start':0,'stop':0},
66
+ search_word: "",
67
+ _cache : {},
68
+ // textarea, input.
69
+ $inputor : null,
70
+ // prevent from duplicate binding.
71
+ inputor_keys: [],
72
+ lenght : 0,
73
+ /* @ position in inputor */
74
+ pos: 0,
75
+ /* @ offset*/
76
+ offset: function() {
77
+ $inputor = this.$inputor;
78
+ mirror = $inputor.data("mirror");
79
+ if (isNil(mirror)) {
80
+ mirror = new Mirror($inputor);
81
+ $inputor.data("mirror",mirror);
82
+ }
83
+
84
+ /* 为了将textarea中文字的排版模拟到镜像div中
85
+ * 我们需要做一些字符处理.由于div元素中不认多余的空格.
86
+ * 我们需要将多余的空格用元素块包裹.
87
+ × 换行符直接替换成<br/>就可以了.
88
+ * NOTE: “\r\n” 用于ie的textarea.
89
+ */
90
+ function format(value) {
91
+ value = value.replace(/</g, '&lt;')
92
+ .replace(/>/g, '&gt;')
93
+ .replace(/`/g,'&#96;')
94
+ .replace(/"/g,'&quot;');
95
+ if ($.browser.msie) {
96
+ rep_str = parseInt($.browser.version) < 8 ? "&nbsp;" : "<span> </span>"
97
+ value = value.replace(/ /g,rep_str);
98
+ }
99
+ return value.replace(/\r\n|\r|\n/g,"<br />");
100
+ }
101
+ /* 克隆完inputor后将原来的文本内容根据
102
+ * @的位置进行分块,以获取@块在inputor(输入框)里的position
103
+ * */
104
+ text = $inputor.val();
105
+ start_range = text.slice(0,this.pos - 1);
106
+ end_range = text.slice(this.pos + 1);
107
+ html = "<span>"+format(start_range)+"</span>";
108
+ html += "<span id='flag'>@</span>";
109
+ html += "<span>"+format(end_range)+"</span>";
110
+ mirror.setContent(html);
111
+
112
+ /* 将inputor的 offset(相对于document)
113
+ * 和@在inputor里的position相加
114
+ * 就得到了@相对于document的offset.
115
+ * 当然,还要加上行高和滚动条的偏移量.
116
+ * */
117
+ offset = $inputor.offset();
118
+ at_pos = mirror.getFlagPos();
119
+ line_height = $inputor.css("line-height");
120
+ line_height = isNaN(line_height) ? 20 : line_height;
121
+ //FIXME: -$(window).scrollTop() get "wrong" offset.
122
+ // but is good for $inputor.scrollTop();
123
+ // jquey 1. + 07.1 fixed the scrollTop problem!?
124
+ y = offset.top + at_pos.top + line_height
125
+ - $inputor.scrollTop();
126
+ x = offset.left + at_pos.left - $inputor.scrollLeft();
127
+
128
+ return {'top':y,'left':x};
129
+ },
130
+ cache: function(key,value) {
131
+ if (!settings['cache']) return null;
132
+ log("cacheing",key,value);
133
+ if (value)
134
+ this._cache[key] = value;
135
+ return this._cache[key];
136
+ },
137
+ getKey: function() {
138
+ $inputor = this.$inputor;
139
+ text = $inputor.val();
140
+ //获得inputor中插入符的position.
141
+ caret_pos = $inputor.caretPos();
142
+ /* 向在插入符前的的文本进行正则匹配
143
+ * 考虑会有多个 @ 的存在, 匹配离插入符最近的一个*/
144
+ subtext = text.slice(0,caret_pos);
145
+ // word = subtext.exec(/@(\w+)$|@[^\x00-\xff]+$/g);
146
+ matched = /@(\w+)$|@([^\x00-\xff]+)$/g.exec(subtext);
147
+ key = null;
148
+ if (matched && (word = matched[1]).length < 20) {
149
+ start = caret_pos - word.length;
150
+ end = start + word.length;
151
+ this.pos = start;
152
+ key = {'text':word, 'start':start, 'end':end};
153
+ } else
154
+ this.view.hide();
155
+ this.keyword = key;
156
+ log("getKey",key);
157
+ return key;
158
+ },
159
+ /* 捕捉inputor的上下回车键.
160
+ * 在列表框做相应的操作,上下滚动,回车选择
161
+ * 返回 false 阻止冒泡事件以捕捉inputor对应的事件
162
+ * */
163
+ onkeydown:function(e) {
164
+ view = this.view;
165
+ // 当列表没显示时不捕捉inputor相关事件.
166
+ if (!view.running()) return true;
167
+ last_idx = view.items.length - 1;
168
+ var return_val = false;
169
+ switch (e.keyCode) {
170
+ case 27:
171
+ this.choose();
172
+ break;
173
+ // UP
174
+ case 38:
175
+ // if put this line outside the switch
176
+ // the view will flash when key down.
177
+ $(view.id + " ul li.cur").removeClass("cur");
178
+ view.cur_li_idx--;
179
+ // 到达顶端时高亮效果跳到最后
180
+ if (view.cur_li_idx < 0)
181
+ view.cur_li_idx = last_idx;
182
+ $(view.id + " li:eq(" + view.cur_li_idx + ")")
183
+ .addClass('cur');
184
+ break;
185
+ // DOWN
186
+ case 40:
187
+ $(view.id + " ul li.cur").removeClass("cur");
188
+ view.cur_li_idx++;
189
+ if (view.cur_li_idx > last_idx)
190
+ view.cur_li_idx = 0;
191
+ $(view.id + " li:eq(" + view.cur_li_idx + ")")
192
+ .addClass('cur');
193
+ break;
194
+ //TAB or ENTER
195
+ case 9:
196
+ case 13:
197
+ $(view.id + " ul li.cur").removeClass("cur");
198
+ // 如果列表为空,则不捕捉回车事件
199
+ $cur_li = $(view.id + " li:eq("+view.cur_li_idx+")");
200
+ this.choose($cur_li);
201
+ break;
202
+ default:
203
+ return_val = true;
204
+ }
205
+ return return_val;
206
+ },
207
+ replaceStr: function(str) {
208
+ /* $inputor.replaceStr(str,start,end)*/
209
+ key = this.keyword;
210
+ source = this.$inputor.val();
211
+ start_str = source.slice(0, key.start);
212
+ text = start_str + str + source.slice(key.end);
213
+ this.$inputor.val(text);
214
+ this.$inputor.caretPos(start_str.length + str.length);
215
+ },
216
+ choose: function($li) {
217
+ str = isNil($li) ? this.keyword.text+" " : $li.attr("data-insert")+" ";
218
+ this.replaceStr(str);
219
+ this.view.hide();
220
+ },
221
+ reg: function(inputor) {
222
+ $inputor = $(inputor);
223
+
224
+ /* 防止对同一个inputor进行多次绑定
225
+ * 在每个已经绑定过的inputor设置一个key.
226
+ * 注册过的key将不再进行绑定
227
+ * */
228
+ key = $inputor.data("@reg-key");
229
+ log("reg",inputor,key);
230
+ if ($.inArray(key,this.inputor_keys) >= 0)
231
+ return null;
232
+ key = "@-"+$.now();
233
+ this.inputor_keys[key];
234
+ // 捕捉inputor事件
235
+ var self = this;
236
+ $inputor.bind("keydown",function(e) {
237
+ return self.onkeydown(e);
238
+ })
239
+ .scroll(function(e){
240
+ self.view.hide();
241
+ })
242
+ .blur(function(e){
243
+ self.view.timeout_id = setTimeout("At.view.hide()",100);
244
+ });
245
+ return key;
246
+ },
247
+ run: function(inputor) {
248
+ this.$inputor = $(inputor);
249
+ key = this.getKey();
250
+ if (!key) return false;
251
+ /*
252
+ * 支持多渠道获得用户数据.
253
+ * 可以设置静态数据的同时从服务器动态获取.
254
+ * 获取级别从先到后: cache -> statis data -> ajax.
255
+ */
256
+ if (!isNil(names = this.cache(this.keyword.text))) {
257
+ log("cache data",names);
258
+ this.view.load(names,false);
259
+ } else if (!isNil(names = this.runWithData(key,settings['data']))) {
260
+ log("statis data",names);
261
+ this.view.load(names,false);
262
+ } else {
263
+ callback = settings['callback'];
264
+ log("callbacking",callback);
265
+ if($.isFunction(callback)) {
266
+ callback(At);
267
+ }
268
+ }
269
+ },
270
+ runWithData:function(key,data) {
271
+ var items = null;
272
+ var self = this;
273
+ if($.isArray(data) && data.length != 0) {
274
+ items = $.map(data,function(item,i) {
275
+ //support plain object also
276
+ var name = $.isPlainObject(item) ? item[self.search_word] : item;
277
+ match = name.match((new RegExp(key.text,"i")));
278
+ return match ? item : null;
279
+ });
280
+ }
281
+ return items;
282
+ }
283
+ };
284
+
285
+ /* 弹出的用户列表框相关的操作 */
286
+ At.view = {
287
+ //当前高亮的条目
288
+ cur_li_idx : 0,
289
+ timeout_id : null,
290
+ id : '#at-view',
291
+ //at view jquery object
292
+ jqo : null,
293
+ items : [],
294
+ // 列表框是否显示中.
295
+ running :function() {
296
+ return $(this.id).is(":visible");
297
+ },
298
+ evalTpl: function(tpl,map) {
299
+ if(isNil(tpl)) return;
300
+ el = tpl.replace(/\$\{([^\}]*)\}/g,function(tag,key,pos){
301
+ return map[key];
302
+ });
303
+ log("evalTpl",el);
304
+ return el;
305
+ },
306
+ jqObject : function(o) {
307
+ if (!isNil(o)) this.jqo = o;
308
+ return isNil(this.jqo) ? $(this.id) : this.jqo;
309
+ },
310
+ onLoaded: function($view) {
311
+ $view.find('li').live('click',function(e) {
312
+ At.choose($(this));
313
+ });
314
+ $view.mousemove(function(e) {
315
+ if (e.target.tagName == "LI") {
316
+ $(this).find("li.cur").removeClass("cur");
317
+ $(e.target).addClass("cur");
318
+ At.cur_li_idx = $(this).find("li").index(e.target)
319
+ }
320
+ });
321
+ },
322
+ rePosition:function($view) {
323
+ $view.offset(At.offset());
324
+ },
325
+ show: function(){
326
+ if (!this.running())
327
+ $view = $(this.id).show();
328
+ this.rePosition($view);
329
+ },
330
+ hide: function() {
331
+ if (!this.running()) return;
332
+ this.cur_li_idx = 0;
333
+ $(this.id).hide();
334
+ },
335
+ load: function(list,cacheable) {
336
+ // 是否已经加载了列表视图
337
+ if (isNil(this.jqObject())) {
338
+ tpl = "<div id='"+this.id.slice(1)+"' class='at-view'><span id='title'>@who?</span><ul id='"+this.id.slice(1)+"-ul'></ul></div>";
339
+ $at_view = $(tpl);
340
+ $('body').append($at_view);
341
+ this.jqObject($at_view = $(this.id));
342
+ this.onLoaded($at_view);
343
+ }
344
+ return this.update(list,cacheable);
345
+ },
346
+ clear: function(clear_all) {
347
+ if (clear_all == true)
348
+ this._cache = {};
349
+ this.items = [];
350
+ this.jqObject().find('ul').empty();
351
+ },
352
+ update: function(list,cacheable) {
353
+ if (!$.isArray(list)) return false;
354
+ if (cacheable != false) At.cache(At.keyword.text,list);
355
+
356
+ $ul = this.jqObject().find('ul');
357
+ this.clear();
358
+ $.merge(this.items,list);
359
+ var tpl = settings['tpl'];
360
+ var self = this;
361
+ var list = unique(list,At.search_word);
362
+ $.each(list.splice(0,settings['limit']), function(i,item) {
363
+ if (!$.isPlainObject(item)) {
364
+ item = {'id':i,'name':item};
365
+ tpl = DEFAULT_TPL;
366
+ }
367
+ $ul.append(self.evalTpl(tpl,item));
368
+ });
369
+ this.show();
370
+ $ul.find("li:eq(0)").addClass("cur");
371
+ }
372
+ };
373
+
374
+ /* maybe we can use $.unique.
375
+ * But i don't know it will delete li element frequently or not.
376
+ * I think we should not change DOM element frequently.
377
+ * more, It seems batter not to call evalTpl function too much times.
378
+ * */
379
+ function unique(list,keyword) {
380
+ var record = [];
381
+ log(list,keyword);
382
+ return $.map(list,function(v,idx){
383
+ var value = $.isPlainObject(v) ? v[keyword] : v;
384
+ if ($.inArray(value,record) < 0) {
385
+ record.push(value);
386
+ return v;
387
+ }
388
+ });
389
+ }
390
+
391
+ function isNil(target) {
392
+ return !target
393
+ //empty_object =
394
+ || ($.isPlainObject(target) && $.isEmptyObject(target))
395
+ //empty_array =
396
+ || ($.isArray(target) && target.length == 0)
397
+ // nil_jquery =
398
+ || (target instanceof $ && target.length == 0)
399
+ || target === undefined;
400
+ }
401
+
402
+ function log() {
403
+ if (!settings['debug'] || $.browser.msie)
404
+ return;
405
+ console.log(arguments);
406
+ }
407
+
408
+ function setSettings(options) {
409
+ opt = {};
410
+ if ($.isFunction(options))
411
+ opt['callback'] = options;
412
+ else
413
+ opt = options;
414
+ return $.extend({
415
+ //must return array;
416
+ 'callback': function(context) {return []},
417
+ 'cache' : true,
418
+ 'debug' : false,
419
+ 'tpl' : DEFAULT_TPL,
420
+ 'limit' : 5,
421
+ 'data':[]
422
+ },opt);
423
+ }
424
+
425
+ DEFAULT_TPL = "<li id='${id}' data-insert='${name}'>${name}</li>";
426
+
427
+ $.fn.atWho = function (options) {
428
+ settings = setSettings(options);
429
+ log("settings",settings);
430
+ // just used in At.runWithData
431
+ var match = /data-insert=['?]\$\{(\w+)\}/g.exec(settings['tpl']);
432
+ At.search_word = match[1];
433
+ return this.filter('textarea, input').each(function() {
434
+ if (!At.reg(this)) return;
435
+ $(this).bind("keyup",function(e) {
436
+ /* 当用户列表框显示时, 上下键不触发查询 */
437
+ var stop_key = e.keyCode == 40 || e.keyCode == 38;
438
+ run = !(At.view.running() && stop_key);
439
+ if (run) At.run(this);
440
+ })
441
+ .mouseup(function(e) {
442
+ At.run(this);
443
+ });
444
+ });
445
+ }
446
+ })(jQuery);
447
+
448
+ /* 本插件操作 textarea 或者 input 内的插入符
449
+ * 只实现了获得插入符在文本框中的位置,我设置
450
+ * 插入符的位置.
451
+ * */
452
+ (function($) {
453
+ function getCaretPos(inputor) {
454
+ if ("selection" in document) { // IE
455
+ inputor.focus();
456
+ /*
457
+ * reference: http://tinyurl.com/86pyc4s
458
+ */
459
+ var start = 0, end = 0, normalizedValue, range,
460
+ textInputRange, len, endRange;
461
+ var el = inputor;
462
+ /* assume we select "HATE" in the inputor such as textarea -> { }.
463
+ * start end-point.
464
+ * /
465
+ * < I really [HATE] IE > between the brackets is the selection range.
466
+ * \
467
+ * end end-point.
468
+ */
469
+ range = document.selection.createRange();
470
+ pos = 0;
471
+ // selection should in the inputor.
472
+ if (range && range.parentElement() == el) {
473
+ normalizedValue = el.value.replace(/\r\n/g, "\n");
474
+ /* SOMETIME !!!
475
+ *"/r/n" is counted as two char.
476
+ * one line is two, two will be four. balalala.
477
+ * so we have to using the normalized one's length.;
478
+ */
479
+ len = normalizedValue.length;
480
+
481
+ /*<[ I really HATE IE ]>:
482
+ * the whole content in the inputor will be the textInputRange.
483
+ */
484
+ textInputRange = el.createTextRange();
485
+
486
+ /* _here must be the position of bookmark.
487
+ * /
488
+ * <[ I really [HATE] IE ]>
489
+ * [---------->[ ] : this is what moveToBookmark do.
490
+ * < I really [[HATE] IE ]> : here is result.
491
+ * \ two brackets in should be in line.
492
+ */
493
+ textInputRange.moveToBookmark(range.getBookmark());
494
+ // IE don't want to let "createTextRange" and "collapse" get together. It's so bad
495
+ endRange = el.createTextRange();
496
+
497
+ /* [--------------------->[] : if set false all end-point goto end.
498
+ * < I really [[HATE] IE []]>
499
+ */
500
+ endRange.collapse(false);
501
+ /* ___VS____
502
+ * / \
503
+ * < I really [[HATE] IE []]>
504
+ * \_endRange end-point.
505
+ *
506
+ * " > -1" mean the start end-point will be the same or right to the end end-point
507
+ * simplelly, all in the end.
508
+ */
509
+ if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
510
+ // TextRange object will miss "\r\n". So, we count it ourself.
511
+ //line_counter = normalizedValue.slice(0, start).split("\n").length -1;
512
+ start = end = len;
513
+ } else {
514
+ /* I really |HATE] IE ]>
515
+ * <-|
516
+ * I really[ [HATE] IE ]>
517
+ * <-[
518
+ * I reall[y [HATE] IE ]>
519
+ *
520
+ * will return how many unit have moved.
521
+ */
522
+ start = -textInputRange.moveStart("character", -len);
523
+ end = -textInputRange.moveEnd("character", -len);
524
+ }
525
+ }
526
+ } else {
527
+ start = inputor.selectionStart;
528
+ }
529
+ return start;
530
+ }
531
+ function setCaretPos(inputor, pos) {
532
+ if ("selection" in document) { //IE
533
+ range = inputor.createTextRange();
534
+ range.move('character',pos);
535
+ range.select();
536
+ } else
537
+ inputor.setSelectionRange(pos,pos);
538
+ }
539
+ $.fn.caretPos = function(pos) {
540
+ var inputor = this[0];
541
+ if (pos) {
542
+ return setCaretPos(inputor,pos);
543
+ } else {
544
+ return getCaretPos(inputor);
545
+ }
546
+ }
547
+ })(jQuery);
@@ -0,0 +1,46 @@
1
+ #at-view {
2
+ position:absolute;
3
+ top: 0;
4
+ left: 0;
5
+ display: none;
6
+ margin-top: 18px;
7
+ background: white;
8
+ border: 1px solid #DDD;
9
+ border-radius: 3px;
10
+ box-shadow: 0 0 5px rgba(0,0,0,0.1);
11
+ min-width: 120px;
12
+ }
13
+ #at-view span#title {
14
+ font-size:13px;
15
+ color: gray;
16
+ padding: 3px 5px;
17
+ cursor: default;
18
+ }
19
+
20
+ #at-view .cur {
21
+ background: #3366FF;
22
+ color: white;
23
+ }
24
+ #at-view .cur small {
25
+ color: white;
26
+ }
27
+ #at-view ul {
28
+ /* width: 100px; */
29
+ list-style:none;
30
+ padding:0;
31
+ margin:auto;
32
+ }
33
+ #at-view ul li {
34
+ display: block;
35
+ padding: 5px 10px;
36
+ border-bottom: 1px solid #DDD;
37
+ font-weight: bold; cursor: pointer;
38
+ /* border-top: 1px solid #C8C8C8; */
39
+ }
40
+ #at-view small {
41
+ font-size: smaller;
42
+ color: #777;
43
+ font-weight: normal;
44
+ }
45
+
46
+
@@ -0,0 +1,44 @@
1
+ require 'rails'
2
+ require 'jquery-atwho-rails/version'
3
+
4
+ # Supply generator for Rails 3.0.x or if asset pipeline is not enabled
5
+ if ::Rails.version < "3.1" || !::Rails.application.config.assets.enabled
6
+ module Atwho
7
+ module Generators
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+
10
+ desc "This generator installs jquery.atwho.js #{Jquery::Atwho::Rails::VERSION}"
11
+ source_root File.expand_path('../../../assets', __FILE__)
12
+
13
+ def copy_js
14
+ say_status("copying js", " jquery.atwho.js (#{Jquery::Atwho::Rails::VERSION})", :green)
15
+ copy_file "javascripts/jquery.atwho.js", "public/javascripts/jquery.atwho.js"
16
+ #copy_file "javascripts/jquery.atwho.min.js", "public/javascripts/jquery.atwho.min.js"
17
+ end
18
+
19
+ def copy_css
20
+ say_status("copying css"," jquery.atwho.css",:green)
21
+ copy_file "stylesheets/jquery.atwho.css", "public/stylesheets/jquery.atwho.css"
22
+ #copy_file "stylesheets/jquery.atwho.min.css", "public/stylesheets/jquery.atwho.min.css"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ else
28
+ module Atwho
29
+ module Generators
30
+ class InstallGenerator < ::Rails::Generators::Base
31
+ desc "Just show instructions so people will know what to do when mistakenly using generator for Rails 3.1 apps"
32
+
33
+ def do_nothing
34
+ say_status("deprecated", "You are using Rails 3.1 with the asset pipeline enabled, so this generator is not needed.")
35
+ say_status("", "The necessary files are already in your asset pipeline.")
36
+ say_status("", "Just add `//= require jquery.atwho` to your app/assets/javascripts/application.js and app/assets/stylesheets/application.css")
37
+ say_status("", "If you upgraded your app from Rails 3.0 and still have jquery.atwho.js in your javascripts, be sure to remove them.")
38
+ say_status("", "If you do not want the asset pipeline enabled, you may turn it off in application.rb and re-run this generator.")
39
+ # ok, nothing
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,10 @@
1
+ require "jquery-atwho-rails/version"
2
+
3
+ module Jquery
4
+ module Atwho
5
+ module Rails
6
+ class Engine < ::Rails::Engine
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module Jquery
2
+ module Atwho
3
+ module Rails
4
+ VERSION = "0.0.1"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Atwho::Generators::InstallGenerator do
4
+ include GeneratorSpec::TestCase
5
+
6
+ destination File.expand_path("../../../tmp/", __FILE__)
7
+ before(:all) do
8
+ prepare_destination
9
+ run_generator
10
+ end
11
+
12
+ it "generate assets files" do
13
+ js_prefix = "public/javascripts"
14
+ css_prefix = "public/stylesheets"
15
+
16
+ assert_file "#{js_prefix}/jquery.atwho.js"
17
+ #assert_file "#{js_prefix}/jquery.atwho.min.js"
18
+
19
+ assert_file "#{css_prefix}/jquery.atwho.css"
20
+ #assert_file "#{css_prefix}/jquery.atwho.min.css"
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ # Set up generator tests
2
+ require 'rails'
3
+ require 'rails/all'
4
+ require 'rails/generators'
5
+
6
+ class TestApp < Rails::Application
7
+ config.root = File.dirname(__FILE__)
8
+ end
9
+ Rails.application = TestApp
10
+
11
+ module Rails
12
+ def self.root
13
+ @root ||= File.expand_path("../../tmp/", __FILE__)
14
+ end
15
+ end
16
+ Rails.application.config.root = Rails.root
17
+ # Call configure to load the settings from
18
+ # Rails.application.config.generators to Rails::Generators
19
+ #Rails::Generators.configure!
20
+
21
+ #Rails.application.config.assets.enabled = true
22
+ require 'generators/atwho/install_generator'
23
+ require 'generator_spec/test_case'
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jquery-atwho-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - ichord
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &76937320 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *76937320
25
+ - !ruby/object:Gem::Dependency
26
+ name: generator_spec
27
+ requirement: &76937010 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *76937010
36
+ description: ! "This is a jQuery plugin \n that implement Twitter/Weibo like @
37
+ mentions."
38
+ email:
39
+ - chord.luo@gmail.com
40
+ executables: []
41
+ extensions: []
42
+ extra_rdoc_files: []
43
+ files:
44
+ - .gitignore
45
+ - Gemfile
46
+ - README.md
47
+ - Rakefile
48
+ - jquery-atwho-rails.gemspec
49
+ - lib/assets/javascripts/jquery.atwho.js
50
+ - lib/assets/stylesheets/jquery.atwho.css
51
+ - lib/generators/atwho/install_generator.rb
52
+ - lib/jquery-atwho-rails.rb
53
+ - lib/jquery-atwho-rails/version.rb
54
+ - spec/generators/install_generator_spec.rb
55
+ - spec/spec_helper.rb
56
+ homepage: http://ichord.github.com/jquery-atwho-rails
57
+ licenses: []
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubyforge_project: jquery-atwho-rails
76
+ rubygems_version: 1.8.12
77
+ signing_key:
78
+ specification_version: 3
79
+ summary: ! 'jquery plugin: @mentions'
80
+ test_files:
81
+ - spec/generators/install_generator_spec.rb
82
+ - spec/spec_helper.rb