jquery-atwho-rails 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -4,7 +4,7 @@ bind your textarea
4
4
 
5
5
  ```javascript
6
6
  data = ['tom','john'];
7
- $('textarea').atwho("@",{'debug':true,'data':data});
7
+ $('textarea').atwho("@",{'data':data});
8
8
  ```
9
9
 
10
10
  that's it, check it out!
@@ -24,557 +24,580 @@
24
24
  */
25
25
 
26
26
  (function($) {
27
- /* 克隆(镜像) inputor. 用于获得@在输入框中的位置
28
- * 复制它的大小形状相关的样式. */
27
+ var At, AtView, Mirror, log, _DEFAULT_TPL, _evalTpl, _highlighter, _isNil, _objectify, _sorter, _unique;
29
28
  Mirror = function($origin) {
30
- this.init($origin);
31
- }
29
+ this.init($origin);
30
+ return this;
31
+ };
32
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
- 'white-space':'pre-wrap'
44
- }
45
- $.each(this.css,function(i,p){
46
- css[p] = $origin.css(p);
47
- });
48
- $mirror.css(css);
49
- $('body').append($mirror);
50
- this.$mirror = $mirror;
51
- },
52
- setContent: function(html) {
53
- this.$mirror.html(html);
54
- },
55
- getFlagPos:function() {
56
- return this.$mirror.find("span#flag").position();
57
- },
58
- height: function() {
59
- return this.$mirror.height();
60
- }
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
+ var $mirror, css;
37
+ $mirror = $('<div></div>');
38
+ css = {
39
+ opacity: 0,
40
+ position: 'absolute',
41
+ left: 0,
42
+ top: 0,
43
+ zIndex: -20000,
44
+ 'white-space': 'pre-wrap'
45
+ };
46
+ $.each(this.css, function(i, p) {
47
+ return css[p] = $origin.css(p);
48
+ });
49
+ $mirror.css(css);
50
+ $('body').append($mirror);
51
+ return this.$mirror = $mirror;
52
+ },
53
+ setContent: function(html) {
54
+ return this.$mirror.html(html);
55
+ },
56
+ getFlagRect: function() {
57
+ var $flag, pos;
58
+ $flag = this.$mirror.find("span#flag");
59
+ pos = $flag.position();
60
+ return {
61
+ left: pos.left,
62
+ top: pos.top,
63
+ bottom: $flag.height() + pos.top
64
+ };
65
+ },
66
+ height: function() {
67
+ return this.$mirror.height();
68
+ }
69
+ };
70
+ At = function(inputor) {
71
+ var $inputor,
72
+ _this = this;
73
+ $inputor = this.$inputor = $(inputor);
74
+ this.options = {};
75
+ this.query = {
76
+ text: "",
77
+ start: 0,
78
+ stop: 0
79
+ };
80
+ this._cache = {};
81
+ this.pos = 0;
82
+ this.flags = {};
83
+ this.theflag = null;
84
+ this.search_word = {};
85
+ this.view = AtView;
86
+ this.mirror = new Mirror($inputor);
87
+ $inputor.on("keyup.inputor", function(e) {
88
+ var lookup, stop;
89
+ stop = e.keyCode === 40 || e.keyCode === 38;
90
+ lookup = !(stop && _this.view.isShowing());
91
+ if (lookup) return _this.lookup();
92
+ }).on("mouseup.inputor", function(e) {
93
+ return _this.lookup();
94
+ });
95
+ this.init();
96
+ log("At.new", $inputor[0]);
97
+ return this;
61
98
  };
62
- At = {
63
- keyword : {'text':"",'start':0,'stop':0},
64
- _cache : {},
65
- // textarea, input.
66
- $inputor : null,
67
- // prevent from duplicate binding.
68
- inputor_keys: [],
69
- lenght : 0,
70
- /* @ position in inputor */
71
- pos: 0,
72
- flags:{},
73
- theflag:null,
74
- options:{},
75
- search_word:{},
76
- searchWord:function() {
77
- // just used in At.watchWithData
78
- search_word = this.search_word[this.theflag];
79
- if (search_word)
80
- return search_word;
81
- var match = /data-keyname=['?]\$\{(\w+)\}/g.exec(this.getOpt('tpl'));
82
- return this.search_word[this.theflag] = !_isNil(match) ? match[1] : null;
83
- },
84
- getOpt: function(key) {
85
- var flag = this.theflag;
99
+ At.prototype = {
100
+ constructor: At,
101
+ init: function() {
102
+ var _this = this;
103
+ this.$inputor.on('keydown.inputor', function(e) {
104
+ return _this.onkeydown(e);
105
+ }).on('scroll.inputor', function(e) {
106
+ return _this.view.hide();
107
+ }).on('blur.inputor', function(e) {
108
+ return _this.view.hide(1000);
109
+ });
110
+ return log("At.init", this.$inputor[0]);
111
+ },
112
+ reg: function(flag, options) {
113
+ var opt;
114
+ opt = {};
115
+ if ($.isFunction(options)) {
116
+ opt['callback'] = options;
117
+ } else {
118
+ opt = options;
119
+ }
120
+ this.options[flag] = $.extend({}, $.fn.atWho["default"], opt);
121
+ return log("At.reg", this.$inputor[0], flag, options);
122
+ },
123
+ dataValue: function() {
124
+ var match, search_word;
125
+ search_word = this.search_word[this.theflag];
126
+ if (search_word) return search_word;
127
+ match = /data-value=['?]\$\{(\w+)\}/g.exec(this.getOpt('tpl'));
128
+ return this.search_word[this.theflag] = !_isNil(match) ? match[1] : null;
129
+ },
130
+ getOpt: function(key) {
131
+ try {
132
+ return this.options[this.theflag][key];
133
+ } catch (error) {
134
+ return null;
135
+ }
136
+ },
137
+ rect: function() {
138
+ var $inputor, Sel, at_rect, bottom, end_range, format, html, mirror, offset, start_range, text, x, y;
139
+ $inputor = this.$inputor;
140
+ if (document.selection) {
141
+ Sel = document.selection.createRange();
142
+ x = Sel.boundingLeft + $inputor.scrollLeft();
143
+ y = Sel.boundingTop + $(window).scrollTop() + $inputor.scrollTop();
144
+ bottom = y + Sel.boundingHeight;
145
+ return {
146
+ top: y,
147
+ left: x,
148
+ bottom: bottom
149
+ };
150
+ }
151
+ mirror = this.mirror;
152
+ format = function(value) {
153
+ return value.replace(/</g, '&lt').replace(/>/g, '&gt').replace(/`/g, '&#96').replace(/"/g, '&quot').replace(/\r\n|\r|\n/g, "<br />");
154
+ };
155
+ /* 克隆完inputor后将原来的文本内容根据
156
+ @的位置进行分块,以获取@块在inputor(输入框)里的position
157
+ */
158
+ text = $inputor.val();
159
+ start_range = text.slice(0, this.pos - 1);
160
+ end_range = text.slice(this.pos + 1);
161
+ html = "<span>" + format(start_range) + "</span>";
162
+ html += "<span id='flag'>@</span>";
163
+ html += "<span>" + format(end_range) + "</span>";
164
+ mirror.setContent(html);
165
+ /*
166
+ 将inputor的 offset(相对于document)
167
+ 和@在inputor里的position相加
168
+ 就得到了@相对于document的offset.
169
+ 当然,还要加上行高和滚动条的偏移量.
170
+ */
171
+ offset = $inputor.offset();
172
+ at_rect = mirror.getFlagRect();
173
+ /*
174
+ FIXME: -$(window).scrollTop() get "wrong" offset.
175
+ but is good for $inputor.scrollTop()
176
+ jquey 1. + 07.1 fixed the scrollTop problem!?
177
+ */
178
+ x = offset.left + at_rect.left - $inputor.scrollLeft();
179
+ y = offset.top - $inputor.scrollTop();
180
+ bottom = y + at_rect.bottom;
181
+ y += at_rect.top;
182
+ return {
183
+ top: y,
184
+ left: x,
185
+ bottom: bottom
186
+ };
187
+ },
188
+ cache: function(value) {
189
+ var key, _base;
190
+ key = this.query.text;
191
+ if (!this.getOpt("cache") || !key) return null;
192
+ return (_base = this._cache)[key] || (_base[key] = value);
193
+ },
194
+ getKeyname: function() {
195
+ var $inputor, caret_pos, end, key, matched, start, subtext, text,
196
+ _this = this;
197
+ $inputor = this.$inputor;
198
+ text = $inputor.val();
199
+ caret_pos = $inputor.caretPos();
200
+ /* 向在插入符前的的文本进行正则匹配
201
+ * 考虑会有多个 @ 的存在, 匹配离插入符最近的一个
202
+ */
203
+ subtext = text.slice(0, caret_pos);
204
+ matched = null;
205
+ $.each(this.options, function(flag) {
206
+ var match, regexp;
207
+ regexp = new RegExp(flag + '([A-Za-z0-9_\+\-]*)$|' + flag + '([^\\x00-\\xff]*)$', 'gi');
208
+ match = regexp.exec(subtext);
209
+ if (!_isNil(match)) {
210
+ matched = match[1] === 'undefined' ? match[2] : match[1];
211
+ _this.theflag = flag;
212
+ return false;
213
+ }
214
+ });
215
+ if (typeof matched === 'string' && matched.length <= 20) {
216
+ start = caret_pos - matched.length;
217
+ end = start + matched.length;
218
+ this.pos = start;
219
+ key = {
220
+ 'text': matched.toLowerCase(),
221
+ 'start': start,
222
+ 'end': end
223
+ };
224
+ } else {
225
+ this.view.hide();
226
+ }
227
+ log("At.getKeyname", key);
228
+ return this.query = key;
229
+ },
230
+ replaceStr: function(str) {
231
+ var $inputor, key, source, start_str, text;
232
+ $inputor = this.$inputor;
233
+ key = this.query;
234
+ source = $inputor.val();
235
+ start_str = source.slice(0, key.start);
236
+ text = start_str + str + source.slice(key.end);
237
+ $inputor.val(text);
238
+ $inputor.caretPos(start_str.length + str.length);
239
+ $inputor.change();
240
+ return log("At.replaceStr", text);
241
+ },
242
+ onkeydown: function(e) {
243
+ var view;
244
+ view = this.view;
245
+ if (!view.isShowing()) return;
246
+ switch (e.keyCode) {
247
+ case 38:
248
+ e.preventDefault();
249
+ view.prev();
250
+ break;
251
+ case 40:
252
+ e.preventDefault();
253
+ view.next();
254
+ break;
255
+ case 9:
256
+ case 13:
257
+ if (!view.isShowing()) return;
258
+ e.preventDefault();
259
+ view.choose();
260
+ break;
261
+ default:
262
+ $.noop();
263
+ }
264
+ return e.stopPropagation();
265
+ },
266
+ renderView: function(datas) {
267
+ log("At.renderView", this, datas);
268
+ datas = datas.splice(0, this.getOpt('limit'));
269
+ datas = _unique(datas, this.dataValue());
270
+ datas = _objectify(datas);
271
+ datas = _sorter.call(this, datas);
272
+ return this.view.render(this, datas);
273
+ },
274
+ lookup: function() {
275
+ var callback, datas, key;
276
+ key = this.getKeyname();
277
+ if (!key) return false;
278
+ log("At.lookup.key", key);
279
+ if (!_isNil(datas = this.cache())) {
280
+ this.renderView(datas);
281
+ } else if (!_isNil(datas = this.lookupWithData(key))) {
282
+ this.renderView(datas);
283
+ } else if ($.isFunction(callback = this.getOpt('callback'))) {
284
+ callback(key.text, $.proxy(this.renderView, this));
285
+ } else {
286
+ this.view.hide();
287
+ }
288
+ return $.noop();
289
+ },
290
+ lookupWithData: function(key) {
291
+ var data, items,
292
+ _this = this;
293
+ data = this.getOpt("data");
294
+ if ($.isArray(data) && data.length !== 0) {
295
+ items = $.map(data, function(item, i) {
296
+ var match, name, regexp;
86
297
  try {
87
- return this.options[flag][key];
298
+ name = $.isPlainObject(item) ? item[_this.dataValue()] : item;
299
+ regexp = new RegExp(key.text.replace("+", "\\+"), 'i');
300
+ match = name.match(regexp);
88
301
  } catch (e) {
89
- return null
302
+ return null;
90
303
  }
91
- },
92
- /* @ offset*/
93
- offset: function() {
94
- $inputor = this.$inputor;
95
-
96
- if (document.selection) {// IE!!!
97
- var Sel = document.selection.createRange();
98
- x = Sel.boundingLeft + $inputor.scrollLeft();
99
- y = Sel.boundingTop + Sel.boundingHeight
100
- + $(window).scrollTop() + $inputor.scrollTop();
101
- return {'top':y,'left':x};
102
- }
103
-
104
- mirror = $inputor.data("mirror");
105
- if (_isNil(mirror)) {
106
- mirror = new Mirror($inputor);
107
- $inputor.data("mirror",mirror);
108
- }
109
-
110
- /* 为了将textarea中文字的排版模拟到镜像div中
111
- * 我们需要做一些字符处理.由于div元素中不认多余的空格.
112
- * 我们需要将多余的空格用元素块包裹.
113
- × 换行符直接替换成<br/>就可以了.
114
- * NOTE: “\r\n” 用于ie的textarea.
115
- */
116
- function format(value) {
117
- value = value.replace(/</g, '&lt;')
118
- .replace(/>/g, '&gt;')
119
- .replace(/`/g,'&#96;')
120
- .replace(/"/g,'&quot;');
121
- return value.replace(/\r\n|\r|\n/g,"<br />");
122
- }
123
- /* 克隆完inputor后将原来的文本内容根据
124
- * @的位置进行分块,以获取@块在inputor(输入框)里的position
125
- * */
126
- text = $inputor.val();
127
- start_range = text.slice(0,this.pos - 1);
128
- end_range = text.slice(this.pos + 1);
129
- html = "<span>"+format(start_range)+"</span>";
130
- html += "<span id='flag'>@</span>";
131
- html += "<span>"+format(end_range)+"</span>";
132
- mirror.setContent(html);
133
-
134
- /* 将inputor的 offset(相对于document)
135
- * 和@在inputor里的position相加
136
- * 就得到了@相对于document的offset.
137
- * 当然,还要加上行高和滚动条的偏移量.
138
- * */
139
- offset = $inputor.offset();
140
- at_pos = mirror.getFlagPos();
141
- line_height = $inputor.css("line-height");
142
- line_height = isNaN(line_height) ? 20 : line_height;
143
- //FIXME: -$(window).scrollTop() get "wrong" offset.
144
- // but is good for $inputor.scrollTop();
145
- // jquey 1. + 07.1 fixed the scrollTop problem!?
146
- y = offset.top + at_pos.top + line_height - $inputor.scrollTop();
147
- x = offset.left + at_pos.left - $inputor.scrollLeft();
148
-
149
- return {'top':y,'left':x};
150
- },
151
- cache: function(key,value) {
152
- if (!this.getOpt('cache')) return null;
153
- _log("cacheing",key,value);
154
- if (value)
155
- this._cache[key] = value;
156
- return this._cache[key];
157
- },
158
- getKeyname: function() {
159
- $inputor = this.$inputor;
160
- text = $inputor.val();
161
- //获得inputor中插入符的position.
162
- caret_pos = $inputor.caretPos();
163
- /* 向在插入符前的的文本进行正则匹配
164
- * 考虑会有多个 @ 的存在, 匹配离插入符最近的一个*/
165
- subtext = text.slice(0,caret_pos);
166
- // word = subtext.exec(/@(\w+)$|@[^\x00-\xff]+$/g);
167
- var self = this;
168
- var matched = null;
169
- $.each(this.options,function(flag) {
170
- regexp = new RegExp(flag+'([A-Za-z0-9_\+\-]*)$|'+flag+'([^\\x00-\\xff]*)$','gi');
171
- match = regexp.exec(subtext);
172
- if (!_isNil(match)) {
173
- matched = match[1] == undefined ? match[2] : match[1];
174
- self.theflag = flag;
175
- return false;
176
- }
177
- });
178
- var key = null;
179
- _log("matched",matched);
180
- if (typeof matched == "string" && matched.length <= 20) {
181
- start = caret_pos - matched.length;
182
- end = start + matched.length;
183
- this.pos = start;
184
- key = {'text':matched, 'start':start, 'end':end};
185
- } else
186
- this.view.hide();
187
-
188
- this.keyword = key;
189
- _log("getKeyname",key);
190
- return key;
191
- },
192
- /* 捕捉inputor的上下回车键.
193
- * 在列表框做相应的操作,上下滚动,回车选择
194
- * 返回 false 阻止冒泡事件以捕捉inputor对应的事件
195
- * */
196
- onkeydown:function(e) {
197
- view = this.view;
198
- // 当列表没显示时不捕捉inputor相关事件.
199
- if (!view.watching()) return true;
200
- last_idx = view.items.length - 1;
201
- var return_val = false;
202
- switch (e.keyCode) {
203
- case 27:
204
- this.choose();
205
- break;
206
- // UP
207
- case 38:
208
- // if put this line outside the switch
209
- // the view will flash when key down.
210
- $(view.id + " ul li.cur").removeClass("cur");
211
- view.cur_li_idx--;
212
- // 到达顶端时高亮效果跳到最后
213
- if (view.cur_li_idx < 0)
214
- view.cur_li_idx = last_idx;
215
- $(view.id + " li:eq(" + view.cur_li_idx + ")")
216
- .addClass('cur');
217
- break;
218
- // DOWN
219
- case 40:
220
- $(view.id + " ul li.cur").removeClass("cur");
221
- view.cur_li_idx++;
222
- if (view.cur_li_idx > last_idx)
223
- view.cur_li_idx = 0;
224
- $(view.id + " li:eq(" + view.cur_li_idx + ")")
225
- .addClass('cur');
226
- break;
227
- //TAB or ENTER
228
- case 9:
229
- case 13:
230
- $(view.id + " ul li.cur").removeClass("cur");
231
- // 如果列表为空,则不捕捉回车事件
232
- $cur_li = $(view.id + " li:eq("+view.cur_li_idx+")");
233
- this.choose($cur_li);
234
- break;
235
- default:
236
- return_val = true;
237
- }
238
- return return_val;
239
- },
240
- replaceStr: function(str) {
241
- /* $inputor.replaceStr(str,start,end)*/
242
- key = this.keyword;
243
- source = this.$inputor.val();
244
- start_str = source.slice(0, key.start);
245
- text = start_str + str + source.slice(key.end);
246
- this.$inputor.val(text);
247
- this.$inputor.caretPos(start_str.length + str.length);
248
- },
249
- choose: function($li) {
250
- str = _isNil($li) ? this.keyword.text+" " : $li.attr("data-keyname")+" ";
251
- this.replaceStr(str);
252
- this.view.hide();
253
- },
254
- reg: function(inputor) {
255
- $inputor = $(inputor);
256
-
257
- /* 防止对同一个inputor进行多次绑定
258
- * 在每个已经绑定过的inputor设置一个key.
259
- * 注册过的key将不再进行绑定
260
- * */
261
- key = $inputor.data("@reg-key");
262
- if ($.inArray(key,this.inputor_keys) >= 0)
263
- return null;
264
- _log("reg",inputor,key);
265
-
266
- key = "@-"+$.now();
267
- $inputor.data("@reg-key",key);
268
- this.inputor_keys.push(key);
269
- // 捕捉inputor事件
270
- var self = this;
271
- $inputor.bind("keydown",function(e) {
272
- return self.onkeydown(e);
273
- })
274
- .scroll(function(e){
275
- self.view.hide();
276
- })
277
- .blur(function(e){
278
- self.view.timeout_id = setTimeout("At.view.hide()",100);
279
- });
280
- return key;
281
- },
282
- watch: function(inputor) {
283
- this.$inputor = $(inputor);
284
- key = this.getKeyname();
285
- if (!key) return false;
286
- /*
287
- * 支持多渠道获得用户数据.
288
- * 可以设置静态数据的同时从服务器动态获取.
289
- * 获取级别从先到后: cache -> statis data -> ajax.
290
- */
291
- if (!_isNil(names = this.cache(this.keyword.text))) {
292
- _log("cache data",names);
293
- this.view.load(names,false);
294
- } else if (!_isNil(names = this.watchWithData(key))) {
295
- _log("statis data",names);
296
- this.view.load(names,false);
297
- } else if ($.isFunction(callback = this.getOpt('callback'))){
298
- _log("callbacking",callback);
299
- callback(At);
300
- } else
301
- this.view.hide();
302
- },
303
- watchWithData:function(key) {
304
- data = this.getOpt("data");
305
- _log("watchWithData",data);
306
- var items = null;
307
- var self = this;
308
- if($.isArray(data) && data.length != 0) {
309
- items = $.map(data,function(item,i) {
310
- //support plain object also
311
- var name = $.isPlainObject(item) ? item[self.searchWord()] : item;
312
- try {
313
- match = name.match((new RegExp(key.text.replace("+","\\+"),"i")));
314
- } catch(e) {
315
- _log("watchWithData.error",e);
316
- return null;
317
- }
318
- return match ? item : null;
319
- });
304
+ if (match) {
305
+ return item;
306
+ } else {
307
+ return null;
320
308
  }
321
- _log("watch with data.item",items)
322
- return items;
309
+ });
323
310
  }
311
+ return items;
312
+ }
324
313
  };
325
-
326
- /* 弹出的用户列表框相关的操作 */
327
- At.view = {
328
- //当前高亮的条目
329
- cur_li_idx : 0,
330
- timeout_id : null,
331
- id : '#at-view',
332
- items : [],
333
- // 列表框是否显示中.
334
- watching :function() {
335
- return $(this.id).is(":visible");
336
- },
337
- freshItems : function() {
338
- this.items = $(this.id).find('#'+this.id.slice(1)+'-ul li');
339
- this.cur_li_idx = 0;
340
- },
341
- evalTpl: function(tpl,map) {
342
- if(_isNil(tpl)) return;
343
- el = tpl.replace(/\$\{([^\}]*)\}/g,function(tag,key,pos){
344
- return map[key];
345
- });
346
- _log("evalTpl",el);
347
- return el;
348
- },
349
- onLoaded: function($view) {
350
- $view.find('li').live('click',function(e) {
351
- At.choose($(this));
352
- });
353
- $view.mousemove(function(e) {
354
- if (e.target.tagName == "LI") {
355
- $(this).find("li.cur").removeClass("cur");
356
- $(e.target).addClass("cur");
357
- At.cur_li_idx = $(this).find("li").index(e.target)
358
- }
359
- });
360
- },
361
- rePosition:function($view) {
362
- $view.offset(At.offset());
363
- },
364
- show: function(){
365
- if (!this.watching())
366
- $view = $(this.id).show();
367
- this.freshItems();
368
- this.rePosition($view);
369
- },
370
- hide: function() {
371
- if (!this.watching()) return;
372
- this.cur_li_idx = 0;
373
- $(this.id).hide();
374
- },
375
- load: function(list,cacheable) {
376
- // 是否已经加载了列表视图
377
- if (_isNil($(this.id))) {
378
- tpl = "<div id='"+this.id.slice(1)+"' class='at-view'><ul id='"+this.id.slice(1)+"-ul'></ul></div>";
379
- $('body').append(tpl);
380
- this.onLoaded($(this.id));
381
- }
382
- return this.update(list,cacheable);
383
- },
384
- clear: function(clear_all) {
385
- if (clear_all == true)
386
- this._cache = {};
387
- $(this.id).find('ul').empty();
388
- },
389
- update: function(list,cacheable) {
390
- if (!$.isArray(list)) return false;
391
- if (cacheable != false) At.cache(At.keyword.text,list);
392
-
393
- this.clear();
394
- var tpl = At.getOpt('tpl');
395
- var list = _unique(list,At.searchWord());
396
-
397
- var self = this;
398
- $ul = $(this.id).find('ul');
399
- $.each(list.splice(0, At.getOpt('limit')), function(i,item) {
400
- if (!$.isPlainObject(item)) {
401
- item = {'id':i,'name':item};
402
- tpl = DEFAULT_TPL;
403
- }
404
- $ul.append(self.evalTpl(tpl,item));
405
- });
406
- this.show();
407
- $ul.find("li:eq(0)").addClass("cur");
314
+ AtView = {
315
+ timeout_id: null,
316
+ id: '#at-view',
317
+ holder: null,
318
+ _jqo: null,
319
+ jqo: function() {
320
+ var jqo;
321
+ jqo = this._jqo;
322
+ return jqo = _isNil(jqo) ? (this._jqo = $(this.id)) : jqo;
323
+ },
324
+ init: function() {
325
+ var $menu, tpl,
326
+ _this = this;
327
+ if (!_isNil(this.jqo())) return;
328
+ tpl = "<div id='" + this.id.slice(1) + "' class='at-view'><ul id='" + this.id.slice(1) + "-ul'></ul></div>";
329
+ $("body").append(tpl);
330
+ $menu = this.jqo().find('ul');
331
+ return $menu.on('mouseenter.view', 'li', function(e) {
332
+ $menu.find('.cur').removeClass('cur');
333
+ return $(e.currentTarget).addClass('cur');
334
+ }).on('click', function(e) {
335
+ e.stopPropagation();
336
+ e.preventDefault();
337
+ return _this.choose();
338
+ });
339
+ },
340
+ isShowing: function() {
341
+ return this.jqo().is(":visible");
342
+ },
343
+ choose: function() {
344
+ var $li, str;
345
+ $li = this.jqo().find(".cur");
346
+ str = _isNil($li) ? this.holder.query.text + " " : $li.attr(this.holder.getOpt("choose")) + " ";
347
+ this.holder.replaceStr(str);
348
+ return this.hide();
349
+ },
350
+ rePosition: function() {
351
+ var rect;
352
+ rect = this.holder.rect();
353
+ if (rect.bottom + this.jqo().height() - $(window).scrollTop() > $(window).height()) {
354
+ rect.bottom = rect.top - this.jqo().height();
355
+ }
356
+ log("AtView.rePosition", {
357
+ left: rect.left,
358
+ top: rect.bottom
359
+ });
360
+ return this.jqo().offset({
361
+ left: rect.left,
362
+ top: rect.bottom
363
+ });
364
+ },
365
+ next: function() {
366
+ var cur, next;
367
+ cur = this.jqo().find('.cur').removeClass('cur');
368
+ next = cur.next();
369
+ if (!cur.length) next = $(this.jqo().find('li')[0]);
370
+ return next.addClass('cur');
371
+ },
372
+ prev: function() {
373
+ var cur, prev;
374
+ cur = this.jqo().find('.cur').removeClass('cur');
375
+ prev = cur.prev();
376
+ if (!prev.length) prev = this.jqo().find('li').last();
377
+ return prev.addClass('cur');
378
+ },
379
+ show: function() {
380
+ if (!this.isShowing()) this.jqo().show();
381
+ return this.rePosition();
382
+ },
383
+ hide: function(time) {
384
+ var callback,
385
+ _this = this;
386
+ if (isNaN(time)) {
387
+ if (this.isShowing()) return this.jqo().hide();
388
+ } else {
389
+ callback = function() {
390
+ return _this.hide();
391
+ };
392
+ clearTimeout(this.timeout_id);
393
+ return this.timeout_id = setTimeout(callback, 300);
408
394
  }
395
+ },
396
+ clear: function(clear_all) {
397
+ if (clear_all === true) this._cache = {};
398
+ return this.jqo().find('ul').empty();
399
+ },
400
+ render: function(holder, list) {
401
+ var $ul, tpl;
402
+ if (!$.isArray(list)) return false;
403
+ this.holder = holder;
404
+ holder.cache(list);
405
+ this.clear();
406
+ $ul = this.jqo().find('ul');
407
+ tpl = holder.getOpt('tpl');
408
+ $.each(list, function(i, item) {
409
+ var li;
410
+ tpl || (tpl = _DEFAULT_TPL);
411
+ li = _evalTpl(tpl, item);
412
+ log("AtView.render", li);
413
+ return $ul.append(_highlighter(li, holder.query.text));
414
+ });
415
+ this.show();
416
+ return $ul.find("li:eq(0)").addClass("cur");
417
+ }
409
418
  };
410
-
411
- /* maybe we can use $._unique.
412
- * But i don't know it will delete li element frequently or not.
413
- * I think we should not change DOM element frequently.
414
- * more, It seems batter not to call evalTpl function too much times.
415
- * */
416
- function _unique(list,keyword) {
417
- var record = [];
418
- _log(list,keyword);
419
- return $.map(list,function(v,idx){
420
- var value = $.isPlainObject(v) ? v[keyword] : v;
421
- if ($.inArray(value,record) < 0) {
422
- record.push(value);
423
- return v;
424
- }
419
+ _objectify = function(list) {
420
+ return $.map(list, function(item, k) {
421
+ if (!$.isPlainObject(item)) {
422
+ item = {
423
+ id: k,
424
+ name: item
425
+ };
426
+ }
427
+ return item;
428
+ });
429
+ };
430
+ _evalTpl = function(tpl, map) {
431
+ var el;
432
+ try {
433
+ return el = tpl.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) {
434
+ return map[key];
425
435
  });
426
- }
427
-
428
- function _isNil(target) {
429
- return !target
430
- //empty_object =
431
- || ($.isPlainObject(target) && $.isEmptyObject(target))
432
- //empty_array =
433
- || ($.isArray(target) && target.length == 0)
434
- // nil_jquery =
435
- || (target instanceof $ && target.length == 0)
436
- || target === undefined;
437
- }
438
-
439
- function _log() {
440
- if (!At.getOpt('debug') || $.browser.msie)
441
- return;
442
- console.log(arguments);
443
- }
444
-
445
- function _setSettings(options) {
446
- opt = {};
447
- if ($.isFunction(options))
448
- opt['callback'] = options;
449
- else
450
- opt = options;
451
- return $.extend({
452
- 'cache' : true,
453
- 'debug' : false,
454
- 'limit' : 5,
455
- 'tpl' : DEFAULT_TPL
456
- },opt);
457
- }
458
-
459
- DEFAULT_TPL = "<li id='${id}' data-keyname='${name}'>${name}</li>";
436
+ } catch (error) {
437
+ return "";
438
+ }
439
+ };
440
+ _highlighter = function(li, query) {
441
+ if (_isNil(query)) return li;
442
+ return li.replace(new RegExp(">\\s*(\\w*)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig'), function(str, $1, $2, $3) {
443
+ return '> ' + $1 + '<strong>' + $2 + '</strong>' + $3 + ' <';
444
+ });
445
+ };
446
+ _sorter = function(items) {
447
+ var data_value, item, query, results, text, _i, _len;
448
+ data_value = this.dataValue();
449
+ query = this.query.text;
450
+ results = [];
451
+ for (_i = 0, _len = items.length; _i < _len; _i++) {
452
+ item = items[_i];
453
+ text = item[data_value];
454
+ if (text.toLowerCase().indexOf(query) === -1) continue;
455
+ item.order = text.toLowerCase().indexOf(query);
456
+ results.push(item);
457
+ }
458
+ results.sort(function(a, b) {
459
+ return a.order - b.order;
460
+ });
461
+ return results;
462
+ };
463
+ /*
464
+ maybe we can use $._unique.
465
+ But i don't know it will delete li element frequently or not.
466
+ I think we should not change DOM element frequently.
467
+ more, It seems batter not to call evalTpl function too much times.
468
+ */
469
+ _unique = function(list, query) {
470
+ var record;
471
+ record = [];
472
+ return $.map(list, function(v, id) {
473
+ var value;
474
+ value = $.isPlainObject(v) ? v[query] : v;
475
+ if ($.inArray(value, record) < 0) {
476
+ record.push(value);
477
+ return v;
478
+ }
479
+ });
480
+ };
481
+ _isNil = function(target) {
482
+ return !target || ($.isPlainObject(target) && $.isEmptyObject(target)) || ($.isArray(target) && target.length === 0) || (target instanceof $ && target.length === 0) || target === void 0;
483
+ };
484
+ _DEFAULT_TPL = "<li id='${id}' data-value='${name}'>${name}</li>";
485
+ log = function() {};
486
+ $.fn.atWho = function(flag, options) {
487
+ AtView.init();
488
+ return this.filter('textarea, input').each(function() {
489
+ var $this, data;
490
+ $this = $(this);
491
+ data = $this.data("AtWho");
492
+ if (!data) $this.data('AtWho', (data = new At(this)));
493
+ return data.reg(flag, options);
494
+ });
495
+ };
496
+ return $.fn.atWho["default"] = {
497
+ data: [],
498
+ choose: "data-value",
499
+ callback: null,
500
+ cache: true,
501
+ limit: 5,
502
+ tpl: _DEFAULT_TPL
503
+ };
504
+ })(window.jQuery);
460
505
 
461
- $.fn.atWho = function (flag,options) {
462
- At.options[flag] = _setSettings(options);
463
- _log("options",At.options);
464
- return this.filter('textarea, input').each(function() {
465
- if (!At.reg(this)) return;
466
- $(this).bind("keyup",function(e) {
467
- /* 当用户列表框显示时, 上下键不触发查询 */
468
- var stop_key = e.keyCode == 40 || e.keyCode == 38;
469
- watch = !(At.view.watching() && stop_key);
470
- if (watch) At.watch(this);
471
- })
472
- .mouseup(function(e) {
473
- At.watch(this);
474
- });
475
- });
476
- return At;
477
- }
478
- })(window.jQuery);
479
506
 
480
507
  /* 本插件操作 textarea 或者 input 内的插入符
481
508
  * 只实现了获得插入符在文本框中的位置,我设置
482
509
  * 插入符的位置.
483
510
  * */
484
511
  (function($) {
485
- function getCaretPos(inputor) {
486
- if ("selection" in document) { // IE
487
- inputor.focus();
512
+ var getCaretPos, setCaretPos;
513
+ getCaretPos = function(inputor) {
514
+ var end, endRange, len, normalizedValue, pos, range, start, textInputRange;
515
+ if (document.selection) {
516
+ /*
517
+ #assume we select "HATE" in the inputor such as textarea -> { }.
518
+ * start end-point.
519
+ * /
520
+ * < I really [HATE] IE > between the brackets is the selection range.
521
+ * \
522
+ * end end-point.
523
+ */
524
+ range = document.selection.createRange();
525
+ pos = 0;
526
+ if (range && range.parentElement() === inputor) {
527
+ normalizedValue = inputor.value.replace(/\r\n/g, "\n");
528
+ /* SOMETIME !!!
529
+ "/r/n" is counted as two char.
530
+ one line is two, two will be four. balalala.
531
+ so we have to using the normalized one's length.;
532
+ */
533
+ len = normalizedValue.length;
534
+ /*
535
+ <[ I really HATE IE ]>:
536
+ the whole content in the inputor will be the textInputRange.
537
+ */
538
+ textInputRange = inputor.createTextRange();
539
+ /* _here must be the position of bookmark.
540
+ /
541
+ <[ I really [HATE] IE ]>
542
+ [---------->[ ] : this is what moveToBookmark do.
543
+ < I really [[HATE] IE ]> : here is result.
544
+ \ two brackets in should be in line.
545
+ */
546
+ textInputRange.moveToBookmark(range.getBookmark());
547
+ endRange = inputor.createTextRange();
548
+ /* [--------------------->[] : if set false all end-point goto end.
549
+ < I really [[HATE] IE []]>
550
+ */
551
+ endRange.collapse(false);
552
+ /*
553
+ ___VS____
554
+ / \
555
+ < I really [[HATE] IE []]>
556
+ \_endRange end-point.
557
+
558
+ " > -1" mean the start end-point will be the same or right to the end end-point
559
+ * simplelly, all in the end.
560
+ */
561
+ if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
562
+ start = end = len;
563
+ } else {
488
564
  /*
489
- * reference: http://tinyurl.com/86pyc4s
490
- */
491
- var start = 0, end = 0, normalizedValue, range,
492
- textInputRange, len, endRange;
493
- var el = inputor;
494
- /* assume we select "HATE" in the inputor such as textarea -> { }.
495
- * start end-point.
496
- * /
497
- * < I really [HATE] IE > between the brackets is the selection range.
498
- * \
499
- * end end-point.
500
- */
501
- range = document.selection.createRange();
502
- pos = 0;
503
- // selection should in the inputor.
504
- if (range && range.parentElement() == el) {
505
- normalizedValue = el.value.replace(/\r\n/g, "\n");
506
- /* SOMETIME !!!
507
- *"/r/n" is counted as two char.
508
- * one line is two, two will be four. balalala.
509
- * so we have to using the normalized one's length.;
510
- */
511
- len = normalizedValue.length;
512
-
513
- /*<[ I really HATE IE ]>:
514
- * the whole content in the inputor will be the textInputRange.
515
- */
516
- textInputRange = el.createTextRange();
517
-
518
- /* _here must be the position of bookmark.
519
- * /
520
- * <[ I really [HATE] IE ]>
521
- * [---------->[ ] : this is what moveToBookmark do.
522
- * < I really [[HATE] IE ]> : here is result.
523
- * \ two brackets in should be in line.
524
- */
525
- textInputRange.moveToBookmark(range.getBookmark());
526
- // IE don't want to let "createTextRange" and "collapse" get together. It's so bad
527
- endRange = el.createTextRange();
528
-
529
- /* [--------------------->[] : if set false all end-point goto end.
530
- * < I really [[HATE] IE []]>
531
- */
532
- endRange.collapse(false);
533
- /* ___VS____
534
- * / \
535
- * < I really [[HATE] IE []]>
536
- * \_endRange end-point.
537
- *
538
- * " > -1" mean the start end-point will be the same or right to the end end-point
539
- * simplelly, all in the end.
540
- */
541
- if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
542
- // TextRange object will miss "\r\n". So, we count it ourself.
543
- //line_counter = normalizedValue.slice(0, start).split("\n").length -1;
544
- start = end = len;
545
- } else {
546
- /* I really |HATE] IE ]>
547
- * <-|
548
- * I really[ [HATE] IE ]>
549
- * <-[
550
- * I reall[y [HATE] IE ]>
551
- *
552
- * will return how many unit have moved.
553
- */
554
- start = -textInputRange.moveStart("character", -len);
555
- end = -textInputRange.moveEnd("character", -len);
556
- }
557
- }
558
- } else {
559
- start = inputor.selectionStart;
560
- }
561
- return start;
562
- }
563
- function setCaretPos(inputor, pos) {
564
- if ("selection" in document) { //IE
565
- range = inputor.createTextRange();
566
- range.move('character',pos);
567
- range.select();
568
- } else
569
- inputor.setSelectionRange(pos,pos);
570
- }
571
- $.fn.caretPos = function(pos) {
572
- var inputor = this[0];
573
- inputor.focus();
574
- if (pos) {
575
- return setCaretPos(inputor,pos);
576
- } else {
577
- return getCaretPos(inputor);
565
+ I really |HATE] IE ]>
566
+ <-|
567
+ I really[ [HATE] IE ]>
568
+ <-[
569
+ I reall[y [HATE] IE ]>
570
+
571
+ will return how many unit have moved.
572
+ */
573
+ start = -textInputRange.moveStart("character", -len);
574
+ end = -textInputRange.moveEnd("character", -len);
575
+ }
578
576
  }
579
- }
580
- })(window.jQuery);
577
+ } else {
578
+ start = inputor.selectionStart;
579
+ }
580
+ return start;
581
+ };
582
+ setCaretPos = function(inputor, pos) {
583
+ var range;
584
+ if (document.selection) {
585
+ range = inputor.createTextRange();
586
+ range.move("character", pos);
587
+ return range.select();
588
+ } else {
589
+ return inputor.setSelectionRange(pos, pos);
590
+ }
591
+ };
592
+ return $.fn.caretPos = function(pos) {
593
+ var inputor;
594
+ inputor = this[0];
595
+ inputor.focus();
596
+ if (pos) {
597
+ return setCaretPos(inputor, pos);
598
+ } else {
599
+ return getCaretPos(inputor);
600
+ }
601
+ };
602
+ })(window.jQuery);
603
+
@@ -10,12 +10,6 @@
10
10
  box-shadow: 0 0 5px rgba(0,0,0,0.1);
11
11
  min-width: 120px;
12
12
  }
13
- #at-view span#title {
14
- font-size:13px;
15
- color: gray;
16
- padding: 3px 5px;
17
- cursor: default;
18
- }
19
13
 
20
14
  #at-view .cur {
21
15
  background: #3366FF;
@@ -24,6 +18,13 @@
24
18
  #at-view .cur small {
25
19
  color: white;
26
20
  }
21
+ #at-view strong {
22
+ color: #3366FF;
23
+ }
24
+ #at-view .cur strong {
25
+ color: white;
26
+ font:bold;
27
+ }
27
28
  #at-view ul {
28
29
  /* width: 100px; */
29
30
  list-style:none;
@@ -34,7 +35,7 @@
34
35
  display: block;
35
36
  padding: 5px 10px;
36
37
  border-bottom: 1px solid #DDD;
37
- font-weight: bold; cursor: pointer;
38
+ cursor: pointer;
38
39
  /* border-top: 1px solid #C8C8C8; */
39
40
  }
40
41
  #at-view small {
@@ -1,7 +1,7 @@
1
1
  module Jquery
2
2
  module Atwho
3
3
  module Rails
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.4"
5
5
  end
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jquery-atwho-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-21 00:00:00.000000000 Z
12
+ date: 2012-04-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec