jquery-atwho-rails 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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