visualsearch-rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +7 -0
  2. data/.travis.yml +4 -0
  3. data/Changelog.md +4 -0
  4. data/Gemfile +2 -0
  5. data/Rakefile +5 -0
  6. data/Readme.md +24 -0
  7. data/app/assets/css/icons.css +19 -0
  8. data/app/assets/css/reset.css +30 -0
  9. data/app/assets/css/workspace.css +290 -0
  10. data/app/assets/images/cancel_search.png +0 -0
  11. data/app/assets/images/search_glyph.png +0 -0
  12. data/app/assets/javascripts/backbone-0.9.10.js +1498 -0
  13. data/app/assets/javascripts/dependencies.js +14843 -0
  14. data/app/assets/javascripts/jquery.ui.autocomplete.js +614 -0
  15. data/app/assets/javascripts/jquery.ui.core.js +324 -0
  16. data/app/assets/javascripts/jquery.ui.datepicker.js +5 -0
  17. data/app/assets/javascripts/jquery.ui.menu.js +621 -0
  18. data/app/assets/javascripts/jquery.ui.position.js +497 -0
  19. data/app/assets/javascripts/jquery.ui.widget.js +521 -0
  20. data/app/assets/javascripts/underscore-1.4.3.js +1221 -0
  21. data/app/assets/javascripts/visualsearch/js/models/search_facets.js +67 -0
  22. data/app/assets/javascripts/visualsearch/js/models/search_query.js +70 -0
  23. data/app/assets/javascripts/visualsearch/js/templates/search_box.jst +8 -0
  24. data/app/assets/javascripts/visualsearch/js/templates/search_facet.jst +9 -0
  25. data/app/assets/javascripts/visualsearch/js/templates/search_input.jst +1 -0
  26. data/app/assets/javascripts/visualsearch/js/templates/templates.js +7 -0
  27. data/app/assets/javascripts/visualsearch/js/utils/backbone_extensions.js +17 -0
  28. data/app/assets/javascripts/visualsearch/js/utils/hotkeys.js +99 -0
  29. data/app/assets/javascripts/visualsearch/js/utils/inflector.js +21 -0
  30. data/app/assets/javascripts/visualsearch/js/utils/jquery_extensions.js +197 -0
  31. data/app/assets/javascripts/visualsearch/js/utils/search_parser.js +87 -0
  32. data/app/assets/javascripts/visualsearch/js/views/search_box.js +447 -0
  33. data/app/assets/javascripts/visualsearch/js/views/search_facet.js +444 -0
  34. data/app/assets/javascripts/visualsearch/js/views/search_input.js +409 -0
  35. data/app/assets/javascripts/visualsearch/js/visualsearch.js +77 -0
  36. data/lib/generators/visual_search_install.rb +30 -0
  37. data/lib/visualsearch-rails.rb +2 -0
  38. data/lib/visualsearch/rails.rb +6 -0
  39. data/lib/visualsearch/version.rb +3 -0
  40. data/visualsearch-rails.gemspec +26 -0
  41. metadata +165 -0
@@ -0,0 +1,614 @@
1
+ /*!
2
+ * jQuery UI Autocomplete 1.10.0
3
+ * http://jqueryui.com
4
+ *
5
+ * Copyright 2013 jQuery Foundation and other contributors
6
+ * Released under the MIT license.
7
+ * http://jquery.org/license
8
+ *
9
+ * http://api.jqueryui.com/autocomplete/
10
+ *
11
+ * Depends:
12
+ * jquery.ui.core.js
13
+ * jquery.ui.widget.js
14
+ * jquery.ui.position.js
15
+ * jquery.ui.menu.js
16
+ */
17
+ (function( $, undefined ) {
18
+
19
+ // used to prevent race conditions with remote data sources
20
+ var requestIndex = 0;
21
+
22
+ $.widget( "ui.autocomplete", {
23
+ version: "1.10.0",
24
+ defaultElement: "<input>",
25
+ options: {
26
+ appendTo: null,
27
+ autoFocus: false,
28
+ delay: 300,
29
+ minLength: 1,
30
+ position: {
31
+ my: "left top",
32
+ at: "left bottom",
33
+ collision: "none"
34
+ },
35
+ source: null,
36
+
37
+ // callbacks
38
+ change: null,
39
+ close: null,
40
+ focus: null,
41
+ open: null,
42
+ response: null,
43
+ search: null,
44
+ select: null
45
+ },
46
+
47
+ pending: 0,
48
+
49
+ _create: function() {
50
+ // Some browsers only repeat keydown events, not keypress events,
51
+ // so we use the suppressKeyPress flag to determine if we've already
52
+ // handled the keydown event. #7269
53
+ // Unfortunately the code for & in keypress is the same as the up arrow,
54
+ // so we use the suppressKeyPressRepeat flag to avoid handling keypress
55
+ // events when we know the keydown event was used to modify the
56
+ // search term. #7799
57
+ var suppressKeyPress, suppressKeyPressRepeat, suppressInput;
58
+
59
+ this.isMultiLine = this._isMultiLine();
60
+ this.valueMethod = this.element[ this.element.is( "input,textarea" ) ? "val" : "text" ];
61
+ this.isNewMenu = true;
62
+
63
+ this.element
64
+ .addClass( "ui-autocomplete-input" )
65
+ .attr( "autocomplete", "off" );
66
+
67
+ this._on( this.element, {
68
+ keydown: function( event ) {
69
+ /*jshint maxcomplexity:15*/
70
+ if ( this.element.prop( "readOnly" ) ) {
71
+ suppressKeyPress = true;
72
+ suppressInput = true;
73
+ suppressKeyPressRepeat = true;
74
+ return;
75
+ }
76
+
77
+ suppressKeyPress = false;
78
+ suppressInput = false;
79
+ suppressKeyPressRepeat = false;
80
+ var keyCode = $.ui.keyCode;
81
+ switch( event.keyCode ) {
82
+ case keyCode.PAGE_UP:
83
+ suppressKeyPress = true;
84
+ this._move( "previousPage", event );
85
+ break;
86
+ case keyCode.PAGE_DOWN:
87
+ suppressKeyPress = true;
88
+ this._move( "nextPage", event );
89
+ break;
90
+ case keyCode.UP:
91
+ suppressKeyPress = true;
92
+ this._keyEvent( "previous", event );
93
+ break;
94
+ case keyCode.DOWN:
95
+ suppressKeyPress = true;
96
+ this._keyEvent( "next", event );
97
+ break;
98
+ case keyCode.ENTER:
99
+ case keyCode.NUMPAD_ENTER:
100
+ // when menu is open and has focus
101
+ if ( this.menu.active ) {
102
+ // #6055 - Opera still allows the keypress to occur
103
+ // which causes forms to submit
104
+ suppressKeyPress = true;
105
+ event.preventDefault();
106
+ this.menu.select( event );
107
+ }
108
+ break;
109
+ case keyCode.TAB:
110
+ if ( this.menu.active ) {
111
+ this.menu.select( event );
112
+ }
113
+ break;
114
+ case keyCode.ESCAPE:
115
+ if ( this.menu.element.is( ":visible" ) ) {
116
+ this._value( this.term );
117
+ this.close( event );
118
+ // Different browsers have different default behavior for escape
119
+ // Single press can mean undo or clear
120
+ // Double press in IE means clear the whole form
121
+ event.preventDefault();
122
+ }
123
+ break;
124
+ default:
125
+ suppressKeyPressRepeat = true;
126
+ // search timeout should be triggered before the input value is changed
127
+ this._searchTimeout( event );
128
+ break;
129
+ }
130
+ },
131
+ keypress: function( event ) {
132
+ if ( suppressKeyPress ) {
133
+ suppressKeyPress = false;
134
+ event.preventDefault();
135
+ return;
136
+ }
137
+ if ( suppressKeyPressRepeat ) {
138
+ return;
139
+ }
140
+
141
+ // replicate some key handlers to allow them to repeat in Firefox and Opera
142
+ var keyCode = $.ui.keyCode;
143
+ switch( event.keyCode ) {
144
+ case keyCode.PAGE_UP:
145
+ this._move( "previousPage", event );
146
+ break;
147
+ case keyCode.PAGE_DOWN:
148
+ this._move( "nextPage", event );
149
+ break;
150
+ case keyCode.UP:
151
+ this._keyEvent( "previous", event );
152
+ break;
153
+ case keyCode.DOWN:
154
+ this._keyEvent( "next", event );
155
+ break;
156
+ }
157
+ },
158
+ input: function( event ) {
159
+ if ( suppressInput ) {
160
+ suppressInput = false;
161
+ event.preventDefault();
162
+ return;
163
+ }
164
+ this._searchTimeout( event );
165
+ },
166
+ focus: function() {
167
+ this.selectedItem = null;
168
+ this.previous = this._value();
169
+ },
170
+ blur: function( event ) {
171
+ if ( this.cancelBlur ) {
172
+ delete this.cancelBlur;
173
+ return;
174
+ }
175
+
176
+ clearTimeout( this.searching );
177
+ this.close( event );
178
+ this._change( event );
179
+ }
180
+ });
181
+
182
+ this._initSource();
183
+ this.menu = $( "<ul>" )
184
+ .addClass( "ui-autocomplete" )
185
+ .appendTo( this._appendTo() )
186
+ .menu({
187
+ // custom key handling for now
188
+ input: $(),
189
+ // disable ARIA support, the live region takes care of that
190
+ role: null
191
+ })
192
+ .zIndex( this.element.zIndex() + 1 )
193
+ .hide()
194
+ .data( "ui-menu" );
195
+
196
+ this._on( this.menu.element, {
197
+ mousedown: function( event ) {
198
+ // prevent moving focus out of the text field
199
+ event.preventDefault();
200
+
201
+ // IE doesn't prevent moving focus even with event.preventDefault()
202
+ // so we set a flag to know when we should ignore the blur event
203
+ this.cancelBlur = true;
204
+ this._delay(function() {
205
+ delete this.cancelBlur;
206
+ });
207
+
208
+ // clicking on the scrollbar causes focus to shift to the body
209
+ // but we can't detect a mouseup or a click immediately afterward
210
+ // so we have to track the next mousedown and close the menu if
211
+ // the user clicks somewhere outside of the autocomplete
212
+ var menuElement = this.menu.element[ 0 ];
213
+ if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
214
+ this._delay(function() {
215
+ var that = this;
216
+ this.document.one( "mousedown", function( event ) {
217
+ if ( event.target !== that.element[ 0 ] &&
218
+ event.target !== menuElement &&
219
+ !$.contains( menuElement, event.target ) ) {
220
+ that.close();
221
+ }
222
+ });
223
+ });
224
+ }
225
+ },
226
+ menufocus: function( event, ui ) {
227
+ // #7024 - Prevent accidental activation of menu items in Firefox
228
+ if ( this.isNewMenu ) {
229
+ this.isNewMenu = false;
230
+ if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) {
231
+ this.menu.blur();
232
+
233
+ this.document.one( "mousemove", function() {
234
+ $( event.target ).trigger( event.originalEvent );
235
+ });
236
+
237
+ return;
238
+ }
239
+ }
240
+
241
+ var item = ui.item.data( "ui-autocomplete-item" );
242
+ if ( false !== this._trigger( "focus", event, { item: item } ) ) {
243
+ // use value to match what will end up in the input, if it was a key event
244
+ if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) {
245
+ this._value( item.value );
246
+ }
247
+ } else {
248
+ // Normally the input is populated with the item's value as the
249
+ // menu is navigated, causing screen readers to notice a change and
250
+ // announce the item. Since the focus event was canceled, this doesn't
251
+ // happen, so we update the live region so that screen readers can
252
+ // still notice the change and announce it.
253
+ this.liveRegion.text( item.value );
254
+ }
255
+ },
256
+ menuselect: function( event, ui ) {
257
+ var item = ui.item.data( "ui-autocomplete-item" ),
258
+ previous = this.previous;
259
+
260
+ // only trigger when focus was lost (click on menu)
261
+ if ( this.element[0] !== this.document[0].activeElement ) {
262
+ this.element.focus();
263
+ this.previous = previous;
264
+ // #6109 - IE triggers two focus events and the second
265
+ // is asynchronous, so we need to reset the previous
266
+ // term synchronously and asynchronously :-(
267
+ this._delay(function() {
268
+ this.previous = previous;
269
+ this.selectedItem = item;
270
+ });
271
+ }
272
+
273
+ if ( false !== this._trigger( "select", event, { item: item } ) ) {
274
+ this._value( item.value );
275
+ }
276
+ // reset the term after the select event
277
+ // this allows custom select handling to work properly
278
+ this.term = this._value();
279
+
280
+ this.close( event );
281
+ this.selectedItem = item;
282
+ }
283
+ });
284
+
285
+ this.liveRegion = $( "<span>", {
286
+ role: "status",
287
+ "aria-live": "polite"
288
+ })
289
+ .addClass( "ui-helper-hidden-accessible" )
290
+ .insertAfter( this.element );
291
+
292
+ // turning off autocomplete prevents the browser from remembering the
293
+ // value when navigating through history, so we re-enable autocomplete
294
+ // if the page is unloaded before the widget is destroyed. #7790
295
+ this._on( this.window, {
296
+ beforeunload: function() {
297
+ this.element.removeAttr( "autocomplete" );
298
+ }
299
+ });
300
+ },
301
+
302
+ _destroy: function() {
303
+ clearTimeout( this.searching );
304
+ this.element
305
+ .removeClass( "ui-autocomplete-input" )
306
+ .removeAttr( "autocomplete" );
307
+ this.menu.element.remove();
308
+ this.liveRegion.remove();
309
+ },
310
+
311
+ _setOption: function( key, value ) {
312
+ this._super( key, value );
313
+ if ( key === "source" ) {
314
+ this._initSource();
315
+ }
316
+ if ( key === "appendTo" ) {
317
+ this.menu.element.appendTo( this._appendTo() );
318
+ }
319
+ if ( key === "disabled" && value && this.xhr ) {
320
+ this.xhr.abort();
321
+ }
322
+ },
323
+
324
+ _appendTo: function() {
325
+ var element = this.options.appendTo;
326
+
327
+ if ( element ) {
328
+ element = element.jquery || element.nodeType ?
329
+ $( element ) :
330
+ this.document.find( element ).eq( 0 );
331
+ }
332
+
333
+ if ( !element ) {
334
+ element = this.element.closest( ".ui-front" );
335
+ }
336
+
337
+ if ( !element.length ) {
338
+ element = this.document[0].body;
339
+ }
340
+
341
+ return element;
342
+ },
343
+
344
+ _isMultiLine: function() {
345
+ // Textareas are always multi-line
346
+ if ( this.element.is( "textarea" ) ) {
347
+ return true;
348
+ }
349
+ // Inputs are always single-line, even if inside a contentEditable element
350
+ // IE also treats inputs as contentEditable
351
+ if ( this.element.is( "input" ) ) {
352
+ return false;
353
+ }
354
+ // All other element types are determined by whether or not they're contentEditable
355
+ return this.element.prop( "isContentEditable" );
356
+ },
357
+
358
+ _initSource: function() {
359
+ var array, url,
360
+ that = this;
361
+ if ( $.isArray(this.options.source) ) {
362
+ array = this.options.source;
363
+ this.source = function( request, response ) {
364
+ response( $.ui.autocomplete.filter( array, request.term ) );
365
+ };
366
+ } else if ( typeof this.options.source === "string" ) {
367
+ url = this.options.source;
368
+ this.source = function( request, response ) {
369
+ if ( that.xhr ) {
370
+ that.xhr.abort();
371
+ }
372
+ that.xhr = $.ajax({
373
+ url: url,
374
+ data: request,
375
+ dataType: "json",
376
+ success: function( data ) {
377
+ response( data );
378
+ },
379
+ error: function() {
380
+ response( [] );
381
+ }
382
+ });
383
+ };
384
+ } else {
385
+ this.source = this.options.source;
386
+ }
387
+ },
388
+
389
+ _searchTimeout: function( event ) {
390
+ clearTimeout( this.searching );
391
+ this.searching = this._delay(function() {
392
+ // only search if the value has changed
393
+ if ( this.term !== this._value() ) {
394
+ this.selectedItem = null;
395
+ this.search( null, event );
396
+ }
397
+ }, this.options.delay );
398
+ },
399
+
400
+ search: function( value, event ) {
401
+ value = value != null ? value : this._value();
402
+
403
+ // always save the actual value, not the one passed as an argument
404
+ this.term = this._value();
405
+
406
+ if ( value.length < this.options.minLength ) {
407
+ return this.close( event );
408
+ }
409
+
410
+ if ( this._trigger( "search", event ) === false ) {
411
+ return;
412
+ }
413
+
414
+ return this._search( value );
415
+ },
416
+
417
+ _search: function( value ) {
418
+ this.pending++;
419
+ this.element.addClass( "ui-autocomplete-loading" );
420
+ this.cancelSearch = false;
421
+
422
+ this.source( { term: value }, this._response() );
423
+ },
424
+
425
+ _response: function() {
426
+ var that = this,
427
+ index = ++requestIndex;
428
+
429
+ return function( content ) {
430
+ if ( index === requestIndex ) {
431
+ that.__response( content );
432
+ }
433
+
434
+ that.pending--;
435
+ if ( !that.pending ) {
436
+ that.element.removeClass( "ui-autocomplete-loading" );
437
+ }
438
+ };
439
+ },
440
+
441
+ __response: function( content ) {
442
+ if ( content ) {
443
+ content = this._normalize( content );
444
+ }
445
+ this._trigger( "response", null, { content: content } );
446
+ if ( !this.options.disabled && content && content.length && !this.cancelSearch ) {
447
+ this._suggest( content );
448
+ this._trigger( "open" );
449
+ } else {
450
+ // use ._close() instead of .close() so we don't cancel future searches
451
+ this._close();
452
+ }
453
+ },
454
+
455
+ close: function( event ) {
456
+ this.cancelSearch = true;
457
+ this._close( event );
458
+ },
459
+
460
+ _close: function( event ) {
461
+ if ( this.menu.element.is( ":visible" ) ) {
462
+ this.menu.element.hide();
463
+ this.menu.blur();
464
+ this.isNewMenu = true;
465
+ this._trigger( "close", event );
466
+ }
467
+ },
468
+
469
+ _change: function( event ) {
470
+ if ( this.previous !== this._value() ) {
471
+ this._trigger( "change", event, { item: this.selectedItem } );
472
+ }
473
+ },
474
+
475
+ _normalize: function( items ) {
476
+ // assume all items have the right format when the first item is complete
477
+ if ( items.length && items[0].label && items[0].value ) {
478
+ return items;
479
+ }
480
+ return $.map( items, function( item ) {
481
+ if ( typeof item === "string" ) {
482
+ return {
483
+ label: item,
484
+ value: item
485
+ };
486
+ }
487
+ return $.extend({
488
+ label: item.label || item.value,
489
+ value: item.value || item.label
490
+ }, item );
491
+ });
492
+ },
493
+
494
+ _suggest: function( items ) {
495
+ var ul = this.menu.element
496
+ .empty()
497
+ .zIndex( this.element.zIndex() + 1 );
498
+ this._renderMenu( ul, items );
499
+ this.menu.refresh();
500
+
501
+ // size and position menu
502
+ ul.show();
503
+ this._resizeMenu();
504
+ ul.position( $.extend({
505
+ of: this.element
506
+ }, this.options.position ));
507
+
508
+ if ( this.options.autoFocus ) {
509
+ this.menu.next();
510
+ }
511
+ },
512
+
513
+ _resizeMenu: function() {
514
+ var ul = this.menu.element;
515
+ ul.outerWidth( Math.max(
516
+ // Firefox wraps long text (possibly a rounding bug)
517
+ // so we add 1px to avoid the wrapping (#7513)
518
+ ul.width( "" ).outerWidth() + 1,
519
+ this.element.outerWidth()
520
+ ) );
521
+ },
522
+
523
+ _renderMenu: function( ul, items ) {
524
+ var that = this;
525
+ $.each( items, function( index, item ) {
526
+ that._renderItemData( ul, item );
527
+ });
528
+ },
529
+
530
+ _renderItemData: function( ul, item ) {
531
+ return this._renderItem( ul, item ).data( "ui-autocomplete-item", item );
532
+ },
533
+
534
+ _renderItem: function( ul, item ) {
535
+ return $( "<li>" )
536
+ .append( $( "<a>" ).text( item.label ) )
537
+ .appendTo( ul );
538
+ },
539
+
540
+ _move: function( direction, event ) {
541
+ if ( !this.menu.element.is( ":visible" ) ) {
542
+ this.search( null, event );
543
+ return;
544
+ }
545
+ if ( this.menu.isFirstItem() && /^previous/.test( direction ) ||
546
+ this.menu.isLastItem() && /^next/.test( direction ) ) {
547
+ this._value( this.term );
548
+ this.menu.blur();
549
+ return;
550
+ }
551
+ this.menu[ direction ]( event );
552
+ },
553
+
554
+ widget: function() {
555
+ return this.menu.element;
556
+ },
557
+
558
+ _value: function() {
559
+ return this.valueMethod.apply( this.element, arguments );
560
+ },
561
+
562
+ _keyEvent: function( keyEvent, event ) {
563
+ if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
564
+ this._move( keyEvent, event );
565
+
566
+ // prevents moving cursor to beginning/end of the text field in some browsers
567
+ event.preventDefault();
568
+ }
569
+ }
570
+ });
571
+
572
+ $.extend( $.ui.autocomplete, {
573
+ escapeRegex: function( value ) {
574
+ return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
575
+ },
576
+ filter: function(array, term) {
577
+ var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
578
+ return $.grep( array, function(value) {
579
+ return matcher.test( value.label || value.value || value );
580
+ });
581
+ }
582
+ });
583
+
584
+
585
+ // live region extension, adding a `messages` option
586
+ // NOTE: This is an experimental API. We are still investigating
587
+ // a full solution for string manipulation and internationalization.
588
+ $.widget( "ui.autocomplete", $.ui.autocomplete, {
589
+ options: {
590
+ messages: {
591
+ noResults: "No search results.",
592
+ results: function( amount ) {
593
+ return amount + ( amount > 1 ? " results are" : " result is" ) +
594
+ " available, use up and down arrow keys to navigate.";
595
+ }
596
+ }
597
+ },
598
+
599
+ __response: function( content ) {
600
+ var message;
601
+ this._superApply( arguments );
602
+ if ( this.options.disabled || this.cancelSearch ) {
603
+ return;
604
+ }
605
+ if ( content && content.length ) {
606
+ message = this.options.messages.results( content.length );
607
+ } else {
608
+ message = this.options.messages.noResults;
609
+ }
610
+ this.liveRegion.text( message );
611
+ }
612
+ });
613
+
614
+ }( jQuery ));