rostra 0.0.15 → 0.0.16
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.
- data/app/assets/javascripts/rostra/application.js +18 -0
- data/app/assets/javascripts/rostra/jquery.autoSuggest.js +419 -0
- data/app/assets/javascripts/rostra/rails.validations.custom.js +37 -0
- data/app/assets/stylesheets/rostra/application.css +0 -5
- data/app/assets/stylesheets/rostra/autoSuggest.css +218 -0
- data/app/controllers/rostra/base/questions_controller.rb +9 -1
- data/app/models/rostra/question.rb +6 -1
- data/app/views/rostra/questions/_form.html.erb +1 -1
- data/app/views/rostra/questions/index.html.erb +1 -1
- data/config/routes.rb +3 -0
- data/db/migrate/20111024223022_add_send_email_notifications_to_question_followings.rb +5 -0
- data/lib/rostra/email_notifier.rb +1 -1
- data/lib/rostra/engine.rb +1 -1
- data/lib/rostra/version.rb +1 -1
- metadata +6 -2
@@ -7,8 +7,10 @@
|
|
7
7
|
//= require jquery
|
8
8
|
//= require jquery_ujs
|
9
9
|
//= require rails.validations
|
10
|
+
//= require rostra/rails.validations.custom
|
10
11
|
//= require rostra/jquery.jeditable
|
11
12
|
//= require rostra/tiny_mce/jquery.tinymce
|
13
|
+
//= require rostra/jquery.autoSuggest
|
12
14
|
|
13
15
|
$(document).ready(function() {
|
14
16
|
|
@@ -23,6 +25,22 @@ $(document).ready(function() {
|
|
23
25
|
}();
|
24
26
|
|
25
27
|
|
28
|
+
$('input.tokenize').each(function() {
|
29
|
+
var input = $(this);
|
30
|
+
var ajax_url = input.attr('data-ajax_url');
|
31
|
+
var name = input.attr('name');
|
32
|
+
var prefill = input.val();
|
33
|
+
|
34
|
+
input.autoSuggest(ajax_url, {
|
35
|
+
startText: '',
|
36
|
+
emptyText:'Press comma or tab to add this tag',
|
37
|
+
preFill: prefill
|
38
|
+
});
|
39
|
+
|
40
|
+
$('input.as-values').attr('name', name); // force name to not
|
41
|
+
});
|
42
|
+
|
43
|
+
|
26
44
|
var answer_comment_form = function() {
|
27
45
|
$('a.leave_comment').click(function() {
|
28
46
|
$(this).closest('.answer').find('form.comment').fadeIn();
|
@@ -0,0 +1,419 @@
|
|
1
|
+
/*
|
2
|
+
* AutoSuggest
|
3
|
+
* Copyright 2009-2010 Drew Wilson
|
4
|
+
* www.drewwilson.com
|
5
|
+
* code.drewwilson.com/entry/autosuggest-jquery-plugin
|
6
|
+
*
|
7
|
+
* Forked by Wu Yuntao
|
8
|
+
* github.com/wuyuntao/jquery-autosuggest
|
9
|
+
*
|
10
|
+
* Version 1.6.2
|
11
|
+
*
|
12
|
+
* This Plug-In will auto-complete or auto-suggest completed search queries
|
13
|
+
* for you as you type. You can add multiple selections and remove them on
|
14
|
+
* the fly. It supports keybord navigation (UP + DOWN + RETURN), as well
|
15
|
+
* as multiple AutoSuggest fields on the same page.
|
16
|
+
*
|
17
|
+
* Inspied by the Autocomplete plugin by: J歳n Zaefferer
|
18
|
+
* and the Facelist plugin by: Ian Tearle (iantearle.com)
|
19
|
+
*
|
20
|
+
* This AutoSuggest jQuery plug-in is dual licensed under the MIT and GPL licenses:
|
21
|
+
* http://www.opensource.org/licenses/mit-license.php
|
22
|
+
* http://www.gnu.org/licenses/gpl.html
|
23
|
+
*/
|
24
|
+
|
25
|
+
(function($){
|
26
|
+
$.fn.autoSuggest = function(data, options) {
|
27
|
+
var defaults = {
|
28
|
+
asHtmlID: false,
|
29
|
+
startText: "Enter Name Here",
|
30
|
+
usePlaceholder: false,
|
31
|
+
emptyText: "No Results Found",
|
32
|
+
preFill: {},
|
33
|
+
limitText: "No More Selections Are Allowed",
|
34
|
+
selectedItemProp: "value", //name of object property
|
35
|
+
selectedValuesProp: "value", //name of object property
|
36
|
+
searchObjProps: "value", //comma separated list of object property names
|
37
|
+
queryParam: "q",
|
38
|
+
retrieveLimit: false, //number for 'limit' param on ajax request
|
39
|
+
extraParams: "",
|
40
|
+
matchCase: false,
|
41
|
+
minChars: 1,
|
42
|
+
keyDelay: 400,
|
43
|
+
resultsHighlight: true,
|
44
|
+
neverSubmit: false,
|
45
|
+
selectionLimit: false,
|
46
|
+
showResultList: true,
|
47
|
+
showResultListWhenNoMatch: false,
|
48
|
+
start: function(){},
|
49
|
+
selectionClick: function(elem){},
|
50
|
+
selectionAdded: function(elem){},
|
51
|
+
selectionRemoved: function(elem){ elem.remove(); },
|
52
|
+
formatList: false, //callback function
|
53
|
+
beforeRetrieve: function(string){ return string; },
|
54
|
+
retrieveComplete: function(data){ return data; },
|
55
|
+
resultClick: function(data){},
|
56
|
+
resultsComplete: function(){}
|
57
|
+
};
|
58
|
+
var opts = $.extend(defaults, options);
|
59
|
+
|
60
|
+
function countValidItems(data) { var n = 0; for (k in data) if (data.hasOwnProperty(k)) n++; return n; }
|
61
|
+
|
62
|
+
var d_fetcher;
|
63
|
+
if(typeof data == "function") {
|
64
|
+
d_fetcher = data;
|
65
|
+
} else if(typeof data == "string") {
|
66
|
+
d_fetcher = function(query, next) {
|
67
|
+
var limit = "";
|
68
|
+
if(opts.retrieveLimit){
|
69
|
+
limit = "&limit="+encodeURIComponent(opts.retrieveLimit);
|
70
|
+
}
|
71
|
+
$.getJSON(data+"?"+opts.queryParam+"="+encodeURIComponent(query)+limit+opts.extraParams, function(data){
|
72
|
+
var new_data = opts.retrieveComplete.call(this, data);
|
73
|
+
next(new_data, query);
|
74
|
+
});
|
75
|
+
};
|
76
|
+
} else if(typeof data == "object" && countValidItems(data) > 0) {
|
77
|
+
d_fetcher = function(query, next) { next(data, query); };
|
78
|
+
}
|
79
|
+
|
80
|
+
if(d_fetcher) {
|
81
|
+
return this.each(function(x){
|
82
|
+
if(!opts.asHtmlID){
|
83
|
+
x = x+""+Math.floor(Math.random()*100); //this ensures there will be unique IDs on the page if autoSuggest() is called multiple times
|
84
|
+
var x_id = "as-input-"+x;
|
85
|
+
} else {
|
86
|
+
x = opts.asHtmlID;
|
87
|
+
var x_id = x;
|
88
|
+
}
|
89
|
+
opts.start.call(this, {
|
90
|
+
add: function(data) {
|
91
|
+
add_selected_item(data, 'u' + $('li', selections_holder).length).addClass('blur');
|
92
|
+
},
|
93
|
+
remove: function(value) {
|
94
|
+
values_input.val(values_input.val().replace(","+value+",",","));
|
95
|
+
selections_holder.find('li[data-value = "' + value + '"]').remove();
|
96
|
+
},
|
97
|
+
});
|
98
|
+
var input = $(this);
|
99
|
+
input.attr("autocomplete","off").addClass("as-input").attr("id",x_id);
|
100
|
+
if (opts.usePlaceholder) {
|
101
|
+
input.attr('placeholder', opts.startText);
|
102
|
+
} else {
|
103
|
+
input.val(opts.startText);
|
104
|
+
}
|
105
|
+
var input_focus = false;
|
106
|
+
|
107
|
+
// Setup basic elements and render them to the DOM
|
108
|
+
input.wrap('<ul class="as-selections" id="as-selections-'+x+'"></ul>').wrap('<li class="as-original" id="as-original-'+x+'"></li>');
|
109
|
+
var selections_holder = $("#as-selections-"+x);
|
110
|
+
var org_li = $("#as-original-"+x);
|
111
|
+
var results_holder = $('<div class="as-results" id="as-results-'+x+'"></div>').hide();
|
112
|
+
var results_ul = $('<ul class="as-list"></ul>');
|
113
|
+
var values_input = $('<input type="hidden" class="as-values" name="as_values_'+x+'" id="as-values-'+x+'" />');
|
114
|
+
var prefill_value = "";
|
115
|
+
if(typeof opts.preFill == "string"){
|
116
|
+
var vals = opts.preFill.split(",");
|
117
|
+
for(var i=0; i < vals.length; i++){
|
118
|
+
var v_data = {};
|
119
|
+
v_data[opts.selectedValuesProp] = vals[i];
|
120
|
+
if(vals[i] != ""){
|
121
|
+
add_selected_item(v_data, "000"+i);
|
122
|
+
}
|
123
|
+
}
|
124
|
+
prefill_value = opts.preFill;
|
125
|
+
} else {
|
126
|
+
prefill_value = "";
|
127
|
+
var prefill_count = 0;
|
128
|
+
for (k in opts.preFill) if (opts.preFill.hasOwnProperty(k)) prefill_count++;
|
129
|
+
if(prefill_count > 0){
|
130
|
+
for(var i=0; i < prefill_count; i++){
|
131
|
+
var new_v = opts.preFill[i][opts.selectedValuesProp];
|
132
|
+
if(new_v == undefined){ new_v = ""; }
|
133
|
+
prefill_value = prefill_value+new_v+",";
|
134
|
+
if(new_v != ""){
|
135
|
+
add_selected_item(opts.preFill[i], "000"+i);
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}
|
139
|
+
}
|
140
|
+
if(prefill_value != ""){
|
141
|
+
input.val("");
|
142
|
+
var lastChar = prefill_value.substring(prefill_value.length-1);
|
143
|
+
if(lastChar != ","){ prefill_value = prefill_value+","; }
|
144
|
+
values_input.val(","+prefill_value);
|
145
|
+
$("li.as-selection-item", selections_holder).addClass("blur").removeClass("selected");
|
146
|
+
}
|
147
|
+
input.after(values_input);
|
148
|
+
selections_holder.click(function(){
|
149
|
+
input_focus = true;
|
150
|
+
input.focus();
|
151
|
+
}).mousedown(function(){ input_focus = false; }).after(results_holder);
|
152
|
+
|
153
|
+
var interval = null;
|
154
|
+
var timeout = null;
|
155
|
+
var prev = "";
|
156
|
+
var totalSelections = 0;
|
157
|
+
var tab_press = false;
|
158
|
+
var lastKeyPressCode = null;
|
159
|
+
var request = null;
|
160
|
+
|
161
|
+
// Handle input field events
|
162
|
+
input.focus(function(){
|
163
|
+
if(!opts.usePlaceholder && $(this).val() == opts.startText && values_input.val() == ""){
|
164
|
+
$(this).val("");
|
165
|
+
} else if(input_focus){
|
166
|
+
$("li.as-selection-item", selections_holder).removeClass("blur");
|
167
|
+
if($(this).val() != ""){
|
168
|
+
results_ul.css("width",selections_holder.outerWidth());
|
169
|
+
results_holder.show();
|
170
|
+
}
|
171
|
+
}
|
172
|
+
if (interval) clearInterval(interval);
|
173
|
+
interval = setInterval(function() {
|
174
|
+
if(opts.showResultList){
|
175
|
+
if(opts.selectionLimit && $("li.as-selection-item", selections_holder).length >= opts.selectionLimit){
|
176
|
+
results_ul.html('<li class="as-message">'+opts.limitText+'</li>');
|
177
|
+
results_holder.show();
|
178
|
+
} else {
|
179
|
+
keyChange();
|
180
|
+
}
|
181
|
+
}
|
182
|
+
}, opts.keyDelay);
|
183
|
+
input_focus = true;
|
184
|
+
if (opts.minChars == 0){
|
185
|
+
processRequest($(this).val());
|
186
|
+
}
|
187
|
+
return true;
|
188
|
+
}).blur(function(){
|
189
|
+
if (!opts.usePlaceholder && $(this).val() == "" && values_input.val() == "" && prefill_value == "" && opts.minChars > 0) {
|
190
|
+
$(this).val(opts.startText);
|
191
|
+
} else if(input_focus){
|
192
|
+
$("li.as-selection-item", selections_holder).addClass("blur").removeClass("selected");
|
193
|
+
results_holder.hide();
|
194
|
+
}
|
195
|
+
if (interval) clearInterval(interval);
|
196
|
+
}).keydown(function(e) {
|
197
|
+
// track last key pressed
|
198
|
+
lastKeyPressCode = e.keyCode;
|
199
|
+
first_focus = false;
|
200
|
+
switch(e.keyCode) {
|
201
|
+
case 38: // up
|
202
|
+
e.preventDefault();
|
203
|
+
moveSelection("up");
|
204
|
+
break;
|
205
|
+
case 40: // down
|
206
|
+
e.preventDefault();
|
207
|
+
moveSelection("down");
|
208
|
+
break;
|
209
|
+
case 8: // delete
|
210
|
+
if(input.val() == ""){
|
211
|
+
var last = values_input.val().split(",");
|
212
|
+
last = last[last.length - 2];
|
213
|
+
selections_holder.children().not(org_li.prev()).removeClass("selected");
|
214
|
+
if(org_li.prev().hasClass("selected")){
|
215
|
+
values_input.val(values_input.val().replace(","+last+",",","));
|
216
|
+
opts.selectionRemoved.call(this, org_li.prev());
|
217
|
+
} else {
|
218
|
+
opts.selectionClick.call(this, org_li.prev());
|
219
|
+
org_li.prev().addClass("selected");
|
220
|
+
}
|
221
|
+
}
|
222
|
+
if(input.val().length == 1){
|
223
|
+
results_holder.hide();
|
224
|
+
prev = "";
|
225
|
+
abortRequest();
|
226
|
+
}
|
227
|
+
if($(":visible",results_holder).length > 0){
|
228
|
+
if (timeout){ clearTimeout(timeout); }
|
229
|
+
timeout = setTimeout(function(){ keyChange(); }, opts.keyDelay);
|
230
|
+
}
|
231
|
+
break;
|
232
|
+
case 9: case 188: // tab or comma
|
233
|
+
tab_press = true;
|
234
|
+
var i_input = input.val().replace(/(,)/g, "");
|
235
|
+
var active = $("li.active:first", results_holder);
|
236
|
+
// Generate a new bubble with text when no suggestion selected
|
237
|
+
if(i_input !== "" && values_input.val().search(","+i_input+",") < 0 && i_input.length >= opts.minChars && active.length === 0){
|
238
|
+
e.preventDefault();
|
239
|
+
var n_data = {};
|
240
|
+
n_data[opts.selectedItemProp] = i_input;
|
241
|
+
n_data[opts.selectedValuesProp] = i_input;
|
242
|
+
var lis = $("li", selections_holder).length;
|
243
|
+
add_selected_item(n_data, "00"+(lis+1));
|
244
|
+
input.val("");
|
245
|
+
// Cancel previous request when new tag is added
|
246
|
+
abortRequest();
|
247
|
+
break;
|
248
|
+
}
|
249
|
+
case 13: // return
|
250
|
+
tab_press = false;
|
251
|
+
var active = $("li.active:first", results_holder);
|
252
|
+
if(active.length > 0){
|
253
|
+
active.click();
|
254
|
+
results_holder.hide();
|
255
|
+
}
|
256
|
+
if(opts.neverSubmit || active.length > 0){
|
257
|
+
e.preventDefault();
|
258
|
+
}
|
259
|
+
break;
|
260
|
+
// ignore if the following keys are pressed: [escape] [shift] [capslock]
|
261
|
+
case 27: // escape
|
262
|
+
case 16: // shift
|
263
|
+
case 20: // capslock
|
264
|
+
abortRequest();
|
265
|
+
results_holder.hide();
|
266
|
+
break;
|
267
|
+
}
|
268
|
+
});
|
269
|
+
|
270
|
+
function keyChange() {
|
271
|
+
// Since most IME does not trigger any key events, if we press [del]
|
272
|
+
// and type some chinese character, `lastKeyPressCode` will still be [del].
|
273
|
+
// This might cause problem so we move the line to key events section;
|
274
|
+
// ignore if the following keys are pressed: [del] [shift] [capslock]
|
275
|
+
// if( lastKeyPressCode == 46 || (lastKeyPressCode > 8 && lastKeyPressCode < 32) ){ return results_holder.hide(); }
|
276
|
+
var string = input.val().replace(/[\\]+|[\/]+/g,"");
|
277
|
+
if (string == prev) return;
|
278
|
+
prev = string;
|
279
|
+
if (string.length >= opts.minChars) {
|
280
|
+
selections_holder.addClass("loading");
|
281
|
+
processRequest(string);
|
282
|
+
} else {
|
283
|
+
selections_holder.removeClass("loading");
|
284
|
+
results_holder.hide();
|
285
|
+
}
|
286
|
+
}
|
287
|
+
function processRequest(string){
|
288
|
+
if(opts.beforeRetrieve){
|
289
|
+
string = opts.beforeRetrieve.call(this, string);
|
290
|
+
}
|
291
|
+
abortRequest();
|
292
|
+
d_fetcher(string, processData);
|
293
|
+
}
|
294
|
+
var num_count = 0;
|
295
|
+
function processData(data, query){
|
296
|
+
if (!opts.matchCase){ query = query.toLowerCase(); }
|
297
|
+
var matchCount = 0;
|
298
|
+
results_holder.html(results_ul.html("")).hide();
|
299
|
+
var d_count = countValidItems(data);
|
300
|
+
for(var i=0;i<d_count;i++){
|
301
|
+
var num = i;
|
302
|
+
num_count++;
|
303
|
+
var forward = false;
|
304
|
+
if(opts.searchObjProps == "value") {
|
305
|
+
var str = data[num].value;
|
306
|
+
} else {
|
307
|
+
var str = "";
|
308
|
+
var names = opts.searchObjProps.split(",");
|
309
|
+
for(var y=0;y<names.length;y++){
|
310
|
+
var name = $.trim(names[y]);
|
311
|
+
str = str+data[num][name]+" ";
|
312
|
+
}
|
313
|
+
}
|
314
|
+
if(str){
|
315
|
+
if (!opts.matchCase){ str = str.toLowerCase(); }
|
316
|
+
if(str.search(query) != -1 && values_input.val().search(","+data[num][opts.selectedValuesProp]+",") == -1){
|
317
|
+
forward = true;
|
318
|
+
}
|
319
|
+
}
|
320
|
+
if(forward){
|
321
|
+
var formatted = $('<li class="as-result-item" id="as-result-item-'+num+'"></li>').click(function(){
|
322
|
+
var raw_data = $(this).data("data");
|
323
|
+
var number = raw_data.num;
|
324
|
+
if($("#as-selection-"+number, selections_holder).length <= 0 && !tab_press){
|
325
|
+
var data = raw_data.attributes;
|
326
|
+
input.val("").focus();
|
327
|
+
prev = "";
|
328
|
+
add_selected_item(data, number);
|
329
|
+
opts.resultClick.call(this, raw_data);
|
330
|
+
results_holder.hide();
|
331
|
+
}
|
332
|
+
tab_press = false;
|
333
|
+
}).mousedown(function(){ input_focus = false; }).mouseover(function(){
|
334
|
+
$("li", results_ul).removeClass("active");
|
335
|
+
$(this).addClass("active");
|
336
|
+
}).data("data",{attributes: data[num], num: num_count});
|
337
|
+
var this_data = $.extend({},data[num]);
|
338
|
+
if (!opts.matchCase){
|
339
|
+
var regx = new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + query + ")(?![^<>]*>)(?![^&;]+;)", "gi");
|
340
|
+
} else {
|
341
|
+
var regx = new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + query + ")(?![^<>]*>)(?![^&;]+;)", "g");
|
342
|
+
}
|
343
|
+
|
344
|
+
if(opts.resultsHighlight && query.length > 0){
|
345
|
+
this_data[opts.selectedItemProp] = this_data[opts.selectedItemProp].replace(regx,"<em>$1</em>");
|
346
|
+
}
|
347
|
+
if(!opts.formatList){
|
348
|
+
formatted = formatted.html(this_data[opts.selectedItemProp]);
|
349
|
+
} else {
|
350
|
+
formatted = opts.formatList.call(this, this_data, formatted);
|
351
|
+
}
|
352
|
+
results_ul.append(formatted);
|
353
|
+
delete this_data;
|
354
|
+
matchCount++;
|
355
|
+
if(opts.retrieveLimit && opts.retrieveLimit == matchCount ){ break; }
|
356
|
+
}
|
357
|
+
}
|
358
|
+
selections_holder.removeClass("loading");
|
359
|
+
if(matchCount <= 0){
|
360
|
+
results_ul.html('<li class="as-message">'+opts.emptyText+'</li>');
|
361
|
+
}
|
362
|
+
results_ul.css("width", selections_holder.outerWidth());
|
363
|
+
if (matchCount > 0 || !opts.showResultListWhenNoMatch) {
|
364
|
+
results_holder.show();
|
365
|
+
}
|
366
|
+
opts.resultsComplete.call(this);
|
367
|
+
}
|
368
|
+
|
369
|
+
function add_selected_item(data, num){
|
370
|
+
values_input.val((values_input.val()||",")+data[opts.selectedValuesProp]+",");
|
371
|
+
var item = $('<li class="as-selection-item" id="as-selection-'+num+'" data-value="' + data[opts.selectedValuesProp] + '"></li>').click(function(){
|
372
|
+
opts.selectionClick.call(this, $(this));
|
373
|
+
selections_holder.children().removeClass("selected");
|
374
|
+
$(this).addClass("selected");
|
375
|
+
}).mousedown(function(){ input_focus = false; });
|
376
|
+
var close = $('<a class="as-close">×</a>').click(function(){
|
377
|
+
values_input.val(values_input.val().replace(","+data[opts.selectedValuesProp]+",",","));
|
378
|
+
opts.selectionRemoved.call(this, item);
|
379
|
+
input_focus = true;
|
380
|
+
input.focus();
|
381
|
+
return false;
|
382
|
+
});
|
383
|
+
org_li.before(item.html(data[opts.selectedItemProp]).prepend(close));
|
384
|
+
opts.selectionAdded.call(this, org_li.prev(), data[opts.selectedValuesProp]);
|
385
|
+
return org_li.prev();
|
386
|
+
}
|
387
|
+
|
388
|
+
function moveSelection(direction){
|
389
|
+
if($(":visible",results_holder).length > 0){
|
390
|
+
var lis = $("li", results_holder);
|
391
|
+
if(direction == "down"){
|
392
|
+
var start = lis.eq(0);
|
393
|
+
} else {
|
394
|
+
var start = lis.filter(":last");
|
395
|
+
}
|
396
|
+
var active = $("li.active:first", results_holder);
|
397
|
+
if(active.length > 0){
|
398
|
+
if(direction == "down"){
|
399
|
+
start = active.next();
|
400
|
+
} else {
|
401
|
+
start = active.prev();
|
402
|
+
}
|
403
|
+
}
|
404
|
+
lis.removeClass("active");
|
405
|
+
start.addClass("active");
|
406
|
+
}
|
407
|
+
}
|
408
|
+
|
409
|
+
function abortRequest() {
|
410
|
+
if (request) {
|
411
|
+
request.abort();
|
412
|
+
request = null;
|
413
|
+
}
|
414
|
+
}
|
415
|
+
|
416
|
+
});
|
417
|
+
}
|
418
|
+
};
|
419
|
+
})(jQuery);
|
@@ -0,0 +1,37 @@
|
|
1
|
+
var custom_validation_callbacks = {
|
2
|
+
// Taglist inputs are built using the autoSuggest jQuery plugin, which dynamically generates markup to create a nice
|
3
|
+
// facebook-like tag field. The plugin uses two inputs:
|
4
|
+
//
|
5
|
+
// 1) An input where the user types tag names. After clicking tab/comma, the plugin adds a new tag pill (`li`) to the
|
6
|
+
// list of tags (`ul`) and clears the input so the user can continue adding tags.
|
7
|
+
// 2) A hidden input which contains the value all the tags as a comma separated list. Tags are automatically appended
|
8
|
+
// whenever a the user enters a new tag.
|
9
|
+
//
|
10
|
+
// Rails uses the second input when writing to the database. This happens automatically since both inputs have the same
|
11
|
+
// `name` attribute, meaning only the second, hidden input gets passed along with the form parameters.
|
12
|
+
//
|
13
|
+
// Because of this markup, validation can be tricky since we really only care about the hidden input. Unfortunately,
|
14
|
+
// the `client_side_validation` gem adds the validaiton behavior to the first input. This callback essentially overrides
|
15
|
+
// the default behavior to ensure validations are ran against the second/hidden input.
|
16
|
+
//
|
17
|
+
taglist: function(element, event_data) {
|
18
|
+
var tag_list_input = element.hasClass('as-input');
|
19
|
+
if (tag_list_input) {
|
20
|
+
var taglist_valid = $.trim( element.next().val() ).match(/[^,]/); // has at least one tag and it's not just whitespace
|
21
|
+
var field = element.closest('.field');
|
22
|
+
field.append('<span class="error"></span>'); // Cuase sometimes it doesn't have a error span. Why?
|
23
|
+
|
24
|
+
if (taglist_valid) {
|
25
|
+
element.data('valid', true);
|
26
|
+
field.find('.error').text('');
|
27
|
+
} else {
|
28
|
+
element.data('valid', false);
|
29
|
+
field.find('.error').text("can't be blank");
|
30
|
+
}
|
31
|
+
}
|
32
|
+
}
|
33
|
+
};
|
34
|
+
|
35
|
+
clientSideValidations.callbacks.element.after = function(element, event_data) {
|
36
|
+
custom_validation_callbacks.taglist(element, event_data);
|
37
|
+
}
|
@@ -15,9 +15,6 @@
|
|
15
15
|
.text {
|
16
16
|
line-height: 18px;
|
17
17
|
}
|
18
|
-
nav {
|
19
|
-
float: right;
|
20
|
-
}
|
21
18
|
#page_content {
|
22
19
|
width: 630px;
|
23
20
|
border-right: 1px solid #ccc;
|
@@ -40,8 +37,6 @@ body#questions.show #page_content {
|
|
40
37
|
list-style: none;
|
41
38
|
padding: 0;
|
42
39
|
}
|
43
|
-
|
44
|
-
|
45
40
|
img.avatar {
|
46
41
|
float: right;
|
47
42
|
box-shadow: 0 1px 4px #666;
|
@@ -0,0 +1,218 @@
|
|
1
|
+
/* AutoSuggest CSS - Version 1.2 */
|
2
|
+
|
3
|
+
ul.as-selections {
|
4
|
+
list-style-type: none;
|
5
|
+
border-bottom: 1px solid #b6b6b6;
|
6
|
+
border-left: 1px solid #aaa;
|
7
|
+
border-right: 1px solid #aaa;
|
8
|
+
padding: 5px 5px 1px;
|
9
|
+
margin: 0;
|
10
|
+
overflow: auto;
|
11
|
+
background-color: #fff;
|
12
|
+
box-shadow:inset 0 1px 2px #888;
|
13
|
+
-webkit-box-shadow:inset 0 1px 2px #888;
|
14
|
+
-moz-box-shadow:inset 0 1px 2px #888;
|
15
|
+
}
|
16
|
+
|
17
|
+
ul.as-selections li {
|
18
|
+
float: left;
|
19
|
+
margin: 1px 4px 1px 0;
|
20
|
+
}
|
21
|
+
|
22
|
+
ul.as-selections li.as-selection-item {
|
23
|
+
color: #2b3840;
|
24
|
+
font-size: 13px;
|
25
|
+
font-family: "Lucida Grande", arial, sans-serif;
|
26
|
+
text-shadow: 0 1px 1px #fff;
|
27
|
+
background-color: #ddeefe;
|
28
|
+
background-image: -moz-linear-gradient(top, #ddeefe, #bfe0f1);
|
29
|
+
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#ddeefe), to(#bfe0f1));
|
30
|
+
border: 1px solid #acc3ec;
|
31
|
+
border-top-color: #c0d9e9;
|
32
|
+
padding: 2px 7px 2px 10px;
|
33
|
+
border-radius: 12px;
|
34
|
+
-webkit-border-radius: 12px;
|
35
|
+
-moz-border-radius: 12px;
|
36
|
+
box-shadow: 0 1px 1px #e4edf2;
|
37
|
+
-webkit-box-shadow: 0 1px 1px #e4edf2;
|
38
|
+
-moz-box-shadow: 0 1px 1px #e4edf2;
|
39
|
+
}
|
40
|
+
|
41
|
+
ul.as-selections li.as-selection-item:last-child {
|
42
|
+
margin-left: 30px;
|
43
|
+
}
|
44
|
+
|
45
|
+
ul.as-selections li.as-selection-item a.as-close {
|
46
|
+
float: right;
|
47
|
+
margin: 1px 0 0 7px;
|
48
|
+
padding: 0 2px;
|
49
|
+
cursor: pointer;
|
50
|
+
color: #5491be;
|
51
|
+
font-family: "Helvetica", helvetica, arial, sans-serif;
|
52
|
+
font-size: 14px;
|
53
|
+
font-weight: bold;
|
54
|
+
text-shadow: 0 1px 1px #fff;
|
55
|
+
-webkit-transition: color .1s ease-in;
|
56
|
+
}
|
57
|
+
|
58
|
+
ul.as-selections li.as-selection-item.blur {
|
59
|
+
color: #666666;
|
60
|
+
background-color: #f4f4f4;
|
61
|
+
background-image: -moz-linear-gradient(top, #f4f4f4, #d5d5d5);
|
62
|
+
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#f4f4f4), to(#d5d5d5));
|
63
|
+
border-color: #bbb;
|
64
|
+
border-top-color: #ccc;
|
65
|
+
box-shadow: 0 1px 1px #e9e9e9;
|
66
|
+
-webkit-box-shadow: 0 1px 1px #e9e9e9;
|
67
|
+
-moz-box-shadow: 0 1px 1px #e9e9e9;
|
68
|
+
}
|
69
|
+
|
70
|
+
ul.as-selections li.as-selection-item.blur a.as-close {
|
71
|
+
color: #999;
|
72
|
+
}
|
73
|
+
|
74
|
+
ul.as-selections li:hover.as-selection-item {
|
75
|
+
color: #2b3840;
|
76
|
+
background-color: #bbd4f1;
|
77
|
+
background-image: -moz-linear-gradient(top, #bbd4f1, #a3c2e5);
|
78
|
+
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#bbd4f1), to(#a3c2e5));
|
79
|
+
border-color: #6da0e0;
|
80
|
+
border-top-color: #8bb7ed;
|
81
|
+
}
|
82
|
+
|
83
|
+
ul.as-selections li:hover.as-selection-item a.as-close {
|
84
|
+
color: #4d70b0;
|
85
|
+
}
|
86
|
+
|
87
|
+
ul.as-selections li.as-selection-item.selected {
|
88
|
+
border-color: #1f30e4;
|
89
|
+
}
|
90
|
+
|
91
|
+
ul.as-selections li.as-selection-item a:hover.as-close {
|
92
|
+
color: #1b3c65;
|
93
|
+
background: transparent;
|
94
|
+
}
|
95
|
+
|
96
|
+
ul.as-selections li.as-selection-item a:active.as-close {
|
97
|
+
color: #4d70b0;
|
98
|
+
}
|
99
|
+
|
100
|
+
ul.as-selections li.as-original {
|
101
|
+
margin-left: 0;
|
102
|
+
}
|
103
|
+
|
104
|
+
ul.as-selections li.as-original input {
|
105
|
+
border: none;
|
106
|
+
outline: none;
|
107
|
+
font-size: 13px;
|
108
|
+
width: 120px;
|
109
|
+
height: 18px;
|
110
|
+
padding-top: 3px;
|
111
|
+
background: white;
|
112
|
+
}
|
113
|
+
|
114
|
+
ul.as-list {
|
115
|
+
position: absolute;
|
116
|
+
list-style-type: none;
|
117
|
+
margin: 2px 0 0 0;
|
118
|
+
padding: 0;
|
119
|
+
font-size: 14px;
|
120
|
+
color: #000;
|
121
|
+
font-family: "Lucida Grande", arial, sans-serif;
|
122
|
+
background-color: #fff;
|
123
|
+
background-color: rgba(255,255,255,0.95);
|
124
|
+
z-index: 2;
|
125
|
+
box-shadow: 0 2px 12px #222;
|
126
|
+
-webkit-box-shadow: 0 2px 12px #222;
|
127
|
+
-moz-box-shadow: 0 2px 12px #222;
|
128
|
+
border-radius: 5px;
|
129
|
+
-webkit-border-radius: 5px;
|
130
|
+
-moz-border-radius: 5px;
|
131
|
+
}
|
132
|
+
|
133
|
+
li.as-result-item, li.as-message {
|
134
|
+
margin: 0 0 0 0;
|
135
|
+
padding: 5px 12px;
|
136
|
+
background-color: transparent;
|
137
|
+
border: 1px solid #fff;
|
138
|
+
border-bottom: 1px solid #ddd;
|
139
|
+
cursor: pointer;
|
140
|
+
border-radius: 5px;
|
141
|
+
-webkit-border-radius: 5px;
|
142
|
+
-moz-border-radius: 5px;
|
143
|
+
}
|
144
|
+
|
145
|
+
li:first-child.as-result-item {
|
146
|
+
margin: 0;
|
147
|
+
}
|
148
|
+
|
149
|
+
li.as-message {
|
150
|
+
margin: 0;
|
151
|
+
cursor: default;
|
152
|
+
}
|
153
|
+
|
154
|
+
li.as-result-item.active {
|
155
|
+
background-color: #3668d9;
|
156
|
+
background-image: -moz-linear-gradient(top, rgb(110, 129, 245), rgb(62, 82, 242));
|
157
|
+
background-image: -webkit-gradient(linear, 0% 0%, 0% 64%, from(rgb(110, 129, 245)), to(rgb(62, 82, 242)));
|
158
|
+
border-color: #3342e8;
|
159
|
+
color: #fff;
|
160
|
+
text-shadow: 0 1px 2px #122042;
|
161
|
+
}
|
162
|
+
|
163
|
+
li.as-result-item em {
|
164
|
+
font-style: normal;
|
165
|
+
background: #444;
|
166
|
+
padding: 0 2px;
|
167
|
+
color: #fff;
|
168
|
+
}
|
169
|
+
|
170
|
+
li.as-result-item.active em {
|
171
|
+
background: #253f7a;
|
172
|
+
color: #fff;
|
173
|
+
}
|
174
|
+
|
175
|
+
/* Webkit Hacks */
|
176
|
+
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
177
|
+
ul.as-selections {
|
178
|
+
border-top-width: 2px;
|
179
|
+
}
|
180
|
+
ul.as-selections li.as-selection-item {
|
181
|
+
padding-top: 0;
|
182
|
+
padding-bottom: 2px;
|
183
|
+
}
|
184
|
+
ul.as-selections li.as-selection-item a.as-close {
|
185
|
+
margin-top: -1px;
|
186
|
+
}
|
187
|
+
ul.as-selections li.as-original input {
|
188
|
+
height: 19px;
|
189
|
+
}
|
190
|
+
}
|
191
|
+
|
192
|
+
/* Opera Hacks */
|
193
|
+
@media all and (-webkit-min-device-pixel-ratio:10000), not all and (-webkit-min-device-pixel-ratio:0) {
|
194
|
+
ul.as-list {
|
195
|
+
border: 1px solid #888;
|
196
|
+
}
|
197
|
+
ul.as-selections li.as-selection-item a.as-close {
|
198
|
+
margin-left: 4px;
|
199
|
+
margin-top: 0;
|
200
|
+
}
|
201
|
+
}
|
202
|
+
|
203
|
+
/* IE Hacks */
|
204
|
+
ul.as-list {
|
205
|
+
border: 1px solid #888\9;
|
206
|
+
}
|
207
|
+
ul.as-selections li.as-selection-item a.as-close {
|
208
|
+
margin-left: 4px\9;
|
209
|
+
margin-top: 0\9;
|
210
|
+
}
|
211
|
+
|
212
|
+
/* Firefox 3.0 Hacks */
|
213
|
+
ul.as-list, x:-moz-any-link, x:default {
|
214
|
+
border: 1px solid #888;
|
215
|
+
}
|
216
|
+
BODY:first-of-type ul.as-list, x:-moz-any-link, x:default { /* Target FF 3.5+ */
|
217
|
+
border: none;
|
218
|
+
}
|
@@ -16,7 +16,7 @@ module Rostra
|
|
16
16
|
if rostra_user.following?(@question)
|
17
17
|
rostra_user.followed_questions.delete(@question)
|
18
18
|
else
|
19
|
-
rostra_user.
|
19
|
+
rostra_user.question_followings.create(question: @question, send_email_notifications: false)
|
20
20
|
end
|
21
21
|
|
22
22
|
respond_to do |format|
|
@@ -25,6 +25,14 @@ module Rostra
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
+
def tags
|
29
|
+
tags = Question.tag_counts.where("name like ?", "%#{params[:q]}%").limit(10)
|
30
|
+
respond_to do |format|
|
31
|
+
format.js { render :js => tags.map { |tag| {value: tag.name} }.to_json }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
28
36
|
def index
|
29
37
|
if params[:tag_search].present?
|
30
38
|
@questions = Question.tagged_with(params[:tag_search]).order('created_at desc').page(params[:page])
|
@@ -4,6 +4,8 @@ module Rostra
|
|
4
4
|
has_many :answers
|
5
5
|
has_many :question_followings
|
6
6
|
has_many :followers, through: :question_followings, source: :user
|
7
|
+
has_many :notified_followers, through: :question_followings, source: :user,
|
8
|
+
conditions: { 'rostra_question_followings.send_email_notifications' => true }
|
7
9
|
|
8
10
|
acts_as_taggable
|
9
11
|
acts_as_voteable
|
@@ -18,13 +20,16 @@ module Rostra
|
|
18
20
|
|
19
21
|
# Set number of questions per page for will paginate
|
20
22
|
#
|
21
|
-
|
23
|
+
paginates_per Rostra::Config.number_of_question_per_page
|
22
24
|
|
23
25
|
# Finds questions asked within the last 15 days ordered by non-unique page views.
|
24
26
|
#
|
25
27
|
def self.trending(limit = 5)
|
26
28
|
Question.where(created_at: (15.days.ago)..(Time.now)).limit(limit)
|
27
29
|
|
30
|
+
# Maybe try to use a subquery?
|
31
|
+
# User.where(:id => accounts.project(:user_id).where(accounts[:user_id].not_eq(6)))
|
32
|
+
|
28
33
|
# This code doesn't work in postgres.
|
29
34
|
# Question
|
30
35
|
# .where(created_at: (15.days.ago)..(Time.now))
|
@@ -2,7 +2,7 @@
|
|
2
2
|
<%= f.hidden_field :user_id, value: rostra_user.id %>
|
3
3
|
<%= f.input :title %>
|
4
4
|
<%= f.input :details, input_html: {class: 'wysiwyg'} %>
|
5
|
-
<%= f.input :tag_list, label: 'Tags', hint: 'Separate tags with commas' %>
|
5
|
+
<%= f.input :tag_list, label: 'Tags', hint: 'Separate tags with commas', input_html: { class: 'tokenize', 'data-ajax_url' => tags_questions_path } %>
|
6
6
|
<%= f.input :follow_by_email, as: :boolean, input_html: { checked: true }, label: "I want to follow answers and comments on this question" %>
|
7
7
|
<%= f.submit @question.new_record? ? 'Post your question' : 'Update' %>
|
8
8
|
<% end %>
|
data/config/routes.rb
CHANGED
@@ -2,9 +2,12 @@ Rostra::Engine.routes.draw do
|
|
2
2
|
resources :questions, except: [:destroy] do
|
3
3
|
put :vote, on: :member
|
4
4
|
put :toggle_following, on: :member
|
5
|
+
get :tags, on: :collection
|
6
|
+
|
5
7
|
resources :answers, only: [:create, :edit, :update] do
|
6
8
|
put :vote, on: :member
|
7
9
|
end
|
10
|
+
|
8
11
|
resources :comments, only: [:create, :update, :destroy]
|
9
12
|
end
|
10
13
|
|
@@ -11,7 +11,7 @@ module Rostra
|
|
11
11
|
# don't send notification to the person that created the answer/comment.
|
12
12
|
#
|
13
13
|
def notify_followers
|
14
|
-
question.
|
14
|
+
question.notified_followers.each do |user|
|
15
15
|
next if self.user == user
|
16
16
|
ApplicationMailer.notification(user, question).deliver
|
17
17
|
end
|
data/lib/rostra/engine.rb
CHANGED
data/lib/rostra/version.rb
CHANGED
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: rostra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.0.
|
5
|
+
version: 0.0.16
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Cory Schires
|
@@ -113,7 +113,7 @@ dependencies:
|
|
113
113
|
type: :runtime
|
114
114
|
version_requirements: *id009
|
115
115
|
- !ruby/object:Gem::Dependency
|
116
|
-
name:
|
116
|
+
name: kaminari
|
117
117
|
prerelease: false
|
118
118
|
requirement: &id010 !ruby/object:Gem::Requirement
|
119
119
|
none: false
|
@@ -235,7 +235,9 @@ files:
|
|
235
235
|
- app/assets/images/rostra/anonymous_avatar.png
|
236
236
|
- app/assets/images/rostra/vote_arrows.png
|
237
237
|
- app/assets/javascripts/rostra/application.js
|
238
|
+
- app/assets/javascripts/rostra/jquery.autoSuggest.js
|
238
239
|
- app/assets/javascripts/rostra/jquery.jeditable.js
|
240
|
+
- app/assets/javascripts/rostra/rails.validations.custom.js
|
239
241
|
- app/assets/javascripts/rostra/tiny_mce/jquery.tinymce.js
|
240
242
|
- app/assets/javascripts/rostra/tiny_mce/langs/en.js
|
241
243
|
- app/assets/javascripts/rostra/tiny_mce/plugins/advhr/css/advhr.css
|
@@ -490,6 +492,7 @@ files:
|
|
490
492
|
- app/assets/javascripts/rostra/tiny_mce/utils/mctabs.js
|
491
493
|
- app/assets/javascripts/rostra/tiny_mce/utils/validate.js
|
492
494
|
- app/assets/stylesheets/rostra/application.css
|
495
|
+
- app/assets/stylesheets/rostra/autoSuggest.css
|
493
496
|
- app/assets/stylesheets/rostra/buttons.css
|
494
497
|
- app/assets/stylesheets/rostra/flash_messages.css
|
495
498
|
- app/assets/stylesheets/rostra/forms.css
|
@@ -541,6 +544,7 @@ files:
|
|
541
544
|
- db/migrate/20111012020025_create_comments.rb
|
542
545
|
- db/migrate/20111016021105_create_impressions_table.rb
|
543
546
|
- db/migrate/20111019162723_create_rostra_question_followings.rb
|
547
|
+
- db/migrate/20111024223022_add_send_email_notifications_to_question_followings.rb
|
544
548
|
- lib/generators/rostra/install_generator.rb
|
545
549
|
- lib/generators/rostra/templates/app/helpers/rostra/application_helper.rb
|
546
550
|
- lib/generators/rostra/templates/config/initializers/rostra.rb
|