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 +1 -1
- data/lib/assets/javascripts/jquery.atwho.js +556 -533
- data/lib/assets/stylesheets/jquery.atwho.css +8 -7
- data/lib/jquery-atwho-rails/version.rb +1 -1
- metadata +2 -2
data/README.md
CHANGED
@@ -24,557 +24,580 @@
|
|
24
24
|
*/
|
25
25
|
|
26
26
|
(function($) {
|
27
|
-
|
28
|
-
* 复制它的大小形状相关的样式. */
|
27
|
+
var At, AtView, Mirror, log, _DEFAULT_TPL, _evalTpl, _highlighter, _isNil, _objectify, _sorter, _unique;
|
29
28
|
Mirror = function($origin) {
|
30
|
-
|
31
|
-
|
29
|
+
this.init($origin);
|
30
|
+
return this;
|
31
|
+
};
|
32
32
|
Mirror.prototype = {
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
},
|
84
|
-
|
85
|
-
|
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, '<').replace(/>/g, '>').replace(/`/g, '`').replace(/"/g, '"').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
|
-
|
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
|
-
|
302
|
+
return null;
|
90
303
|
}
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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, '<')
|
118
|
-
.replace(/>/g, '>')
|
119
|
-
.replace(/`/g,'`')
|
120
|
-
.replace(/"/g,'"');
|
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
|
-
|
322
|
-
return items;
|
309
|
+
});
|
323
310
|
}
|
311
|
+
return items;
|
312
|
+
}
|
324
313
|
};
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
}
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
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
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
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
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
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
|
-
|
486
|
-
|
487
|
-
|
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
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
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
|
-
|
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
|
-
|
38
|
+
cursor: pointer;
|
38
39
|
/* border-top: 1px solid #C8C8C8; */
|
39
40
|
}
|
40
41
|
#at-view small {
|
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.
|
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-
|
12
|
+
date: 2012-04-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|