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,67 @@
1
+ (function() {
2
+
3
+ var $ = jQuery; // Handle namespaced jQuery
4
+
5
+ // The model that holds individual search facets and their categories.
6
+ // Held in a collection by `VS.app.searchQuery`.
7
+ VS.model.SearchFacet = Backbone.Model.extend({
8
+
9
+ // Extract the category and value and serialize it in preparation for
10
+ // turning the entire searchBox into a search query that can be sent
11
+ // to the server for parsing and searching.
12
+ serialize : function() {
13
+ var category = this.quoteCategory(this.get('category'));
14
+ var value = VS.utils.inflector.trim(this.get('value'));
15
+ var remainder = this.get("app").options.remainder;
16
+
17
+ if (!value) return '';
18
+
19
+ if (!_.contains(this.get("app").options.unquotable || [], category) && category != remainder) {
20
+ value = this.quoteValue(value);
21
+ }
22
+
23
+ if (category != remainder) {
24
+ category = category + ': ';
25
+ } else {
26
+ category = "";
27
+ }
28
+ return category + value;
29
+ },
30
+
31
+ // Wrap categories that have spaces or any kind of quote with opposite matching
32
+ // quotes to preserve the complex category during serialization.
33
+ quoteCategory : function(category) {
34
+ var hasDoubleQuote = (/"/).test(category);
35
+ var hasSingleQuote = (/'/).test(category);
36
+ var hasSpace = (/\s/).test(category);
37
+
38
+ if (hasDoubleQuote && !hasSingleQuote) {
39
+ return "'" + category + "'";
40
+ } else if (hasSpace || (hasSingleQuote && !hasDoubleQuote)) {
41
+ return '"' + category + '"';
42
+ } else {
43
+ return category;
44
+ }
45
+ },
46
+
47
+ // Wrap values that have quotes in opposite matching quotes. If a value has
48
+ // both single and double quotes, just use the double quotes.
49
+ quoteValue : function(value) {
50
+ var hasDoubleQuote = (/"/).test(value);
51
+ var hasSingleQuote = (/'/).test(value);
52
+
53
+ if (hasDoubleQuote && !hasSingleQuote) {
54
+ return "'" + value + "'";
55
+ } else {
56
+ return '"' + value + '"';
57
+ }
58
+ },
59
+
60
+ // If provided, use a custom label instead of the raw value.
61
+ label : function() {
62
+ return this.get('label') || this.get('value');
63
+ }
64
+
65
+ });
66
+
67
+ })();
@@ -0,0 +1,70 @@
1
+ (function() {
2
+
3
+ var $ = jQuery; // Handle namespaced jQuery
4
+
5
+ // Collection which holds all of the individual facets (category: value).
6
+ // Used for finding and removing specific facets.
7
+ VS.model.SearchQuery = Backbone.Collection.extend({
8
+
9
+ // Model holds the category and value of the facet.
10
+ model : VS.model.SearchFacet,
11
+
12
+ // Turns all of the facets into a single serialized string.
13
+ serialize : function() {
14
+ return this.map(function(facet){ return facet.serialize(); }).join(' ');
15
+ },
16
+
17
+ facets : function() {
18
+ return this.map(function(facet) {
19
+ var value = {};
20
+ value[facet.get('category')] = facet.get('value');
21
+ return value;
22
+ });
23
+ },
24
+
25
+ // Find a facet by its category. Multiple facets with the same category
26
+ // is fine, but only the first is returned.
27
+ find : function(category) {
28
+ var facet = this.detect(function(facet) {
29
+ return facet.get('category').toLowerCase() == category.toLowerCase();
30
+ });
31
+ return facet && facet.get('value');
32
+ },
33
+
34
+ // Counts the number of times a specific category is in the search query.
35
+ count : function(category) {
36
+ return this.select(function(facet) {
37
+ return facet.get('category').toLowerCase() == category.toLowerCase();
38
+ }).length;
39
+ },
40
+
41
+ // Returns an array of extracted values from each facet in a category.
42
+ values : function(category) {
43
+ var facets = this.select(function(facet) {
44
+ return facet.get('category').toLowerCase() == category.toLowerCase();
45
+ });
46
+ return _.map(facets, function(facet) { return facet.get('value'); });
47
+ },
48
+
49
+ // Checks all facets for matches of either a category or both category and value.
50
+ has : function(category, value) {
51
+ return this.any(function(facet) {
52
+ var categoryMatched = facet.get('category').toLowerCase() == category.toLowerCase();
53
+ if (!value) return categoryMatched;
54
+ return categoryMatched && facet.get('value') == value;
55
+ });
56
+ },
57
+
58
+ // Used to temporarily hide specific categories and serialize the search query.
59
+ withoutCategory : function() {
60
+ var categories = _.map(_.toArray(arguments), function(cat) { return cat.toLowerCase(); });
61
+ return this.map(function(facet) {
62
+ if (!_.include(categories, facet.get('category').toLowerCase())) {
63
+ return facet.serialize();
64
+ };
65
+ }).join(' ');
66
+ }
67
+
68
+ });
69
+
70
+ })();
@@ -0,0 +1,8 @@
1
+ <div class="VS-search">
2
+ <div class="VS-search-box-wrapper VS-search-box">
3
+ <div class="VS-icon VS-icon-search"></div>
4
+ <div class="VS-placeholder"></div>
5
+ <div class="VS-search-inner"></div>
6
+ <div class="VS-icon VS-icon-cancel VS-cancel-search-box" title="clear search"></div>
7
+ </div>
8
+ </div>
@@ -0,0 +1,9 @@
1
+ <% if (model.has('category')) { %>
2
+ <div class="category"><%= model.get('category') %>:</div>
3
+ <% } %>
4
+
5
+ <div class="search_facet_input_container">
6
+ <input type="text" class="search_facet_input ui-menu VS-interface" value="" />
7
+ </div>
8
+
9
+ <div class="search_facet_remove VS-icon VS-icon-cancel"></div>
@@ -0,0 +1 @@
1
+ <input type="text" class="ui-menu" />
@@ -0,0 +1,7 @@
1
+ (function(){
2
+ window.JST = window.JST || {};
3
+
4
+ window.JST['search_box'] = _.template('<div class="VS-search">\n <div class="VS-search-box-wrapper VS-search-box">\n <div class="VS-icon VS-icon-search"></div>\n <div class="VS-placeholder"></div>\n <div class="VS-search-inner"></div>\n <div class="VS-icon VS-icon-cancel VS-cancel-search-box" title="clear search"></div>\n </div>\n</div>');
5
+ window.JST['search_facet'] = _.template('<% if (model.has(\'category\')) { %>\n <div class="category"><%= model.get(\'category\') %>:</div>\n<% } %>\n\n<div class="search_facet_input_container">\n <input type="text" class="search_facet_input ui-menu VS-interface" value="" />\n</div>\n\n<div class="search_facet_remove VS-icon VS-icon-cancel"></div>');
6
+ window.JST['search_input'] = _.template('<input type="text" class="ui-menu" />');
7
+ })();
@@ -0,0 +1,17 @@
1
+ (function(){
2
+
3
+ var $ = jQuery; // Handle namespaced jQuery
4
+
5
+ // Makes the view enter a mode. Modes have both a 'mode' and a 'group',
6
+ // and are mutually exclusive with any other modes in the same group.
7
+ // Setting will update the view's modes hash, as well as set an HTML class
8
+ // of *[mode]_[group]* on the view's element. Convenient way to swap styles
9
+ // and behavior.
10
+ Backbone.View.prototype.setMode = function(mode, group) {
11
+ this.modes || (this.modes = {});
12
+ if (this.modes[group] === mode) return;
13
+ $(this.el).setMode(mode, group);
14
+ this.modes[group] = mode;
15
+ };
16
+
17
+ })();
@@ -0,0 +1,99 @@
1
+ (function() {
2
+
3
+ var $ = jQuery; // Handle namespaced jQuery
4
+
5
+ // DocumentCloud workspace hotkeys. To tell if a key is currently being pressed,
6
+ // just ask `VS.app.hotkeys.[key]` on `keypress`, or ask `VS.app.hotkeys.key(e)`
7
+ // on `keydown`.
8
+ //
9
+ // For the most headache-free way to use this utility, check modifier keys,
10
+ // like shift and command, with `VS.app.hotkeys.shift`, and check every other
11
+ // key with `VS.app.hotkeys.key(e) == 'key_name'`.
12
+ VS.app.hotkeys = {
13
+
14
+ // Keys that will be mapped to the `hotkeys` namespace.
15
+ KEYS: {
16
+ '16': 'shift',
17
+ '17': 'command',
18
+ '91': 'command',
19
+ '93': 'command',
20
+ '224': 'command',
21
+ '13': 'enter',
22
+ '37': 'left',
23
+ '38': 'upArrow',
24
+ '39': 'right',
25
+ '40': 'downArrow',
26
+ '46': 'delete',
27
+ '8': 'backspace',
28
+ '35': 'end',
29
+ '36': 'home',
30
+ '9': 'tab',
31
+ '188': 'comma'
32
+ },
33
+
34
+ // Binds global keydown and keyup events to listen for keys that match `this.KEYS`.
35
+ initialize : function() {
36
+ _.bindAll(this, 'down', 'up', 'blur');
37
+ $(document).bind('keydown', this.down);
38
+ $(document).bind('keyup', this.up);
39
+ $(window).bind('blur', this.blur);
40
+ },
41
+
42
+ // On `keydown`, turn on all keys that match.
43
+ down : function(e) {
44
+ var key = this.KEYS[e.which];
45
+ if (key) this[key] = true;
46
+ },
47
+
48
+ // On `keyup`, turn off all keys that match.
49
+ up : function(e) {
50
+ var key = this.KEYS[e.which];
51
+ if (key) this[key] = false;
52
+ },
53
+
54
+ // If an input is blurred, all keys need to be turned off, since they are no longer
55
+ // able to modify the document.
56
+ blur : function(e) {
57
+ for (var key in this.KEYS) this[this.KEYS[key]] = false;
58
+ },
59
+
60
+ // Check a key from an event and return the common english name.
61
+ key : function(e) {
62
+ return this.KEYS[e.which];
63
+ },
64
+
65
+ // Colon is special, since the value is different between browsers.
66
+ colon : function(e) {
67
+ var charCode = e.which;
68
+ return charCode && String.fromCharCode(charCode) == ":";
69
+ },
70
+
71
+ // Check a key from an event and match it against any known characters.
72
+ // The `keyCode` is different depending on the event type: `keydown` vs. `keypress`.
73
+ //
74
+ // These were determined by looping through every `keyCode` and `charCode` that
75
+ // resulted from `keydown` and `keypress` events and counting what was printable.
76
+ printable : function(e) {
77
+ var code = e.which;
78
+ if (e.type == 'keydown') {
79
+ if (code == 32 || // space
80
+ (code >= 48 && code <= 90) || // 0-1a-z
81
+ (code >= 96 && code <= 111) || // 0-9+-/*.
82
+ (code >= 186 && code <= 192) || // ;=,-./^
83
+ (code >= 219 && code <= 222)) { // (\)'
84
+ return true;
85
+ }
86
+ } else {
87
+ // [space]!"#$%&'()*+,-.0-9:;<=>?@A-Z[\]^_`a-z{|} and unicode characters
88
+ if ((code >= 32 && code <= 126) ||
89
+ (code >= 160 && code <= 500) ||
90
+ (String.fromCharCode(code) == ":")) {
91
+ return true;
92
+ }
93
+ }
94
+ return false;
95
+ }
96
+
97
+ };
98
+
99
+ })();
@@ -0,0 +1,21 @@
1
+ (function() {
2
+
3
+ var $ = jQuery; // Handle namespaced jQuery
4
+
5
+ // Naive English transformations on words. Only used for a few transformations
6
+ // in VisualSearch.js.
7
+ VS.utils.inflector = {
8
+
9
+ // Delegate to the ECMA5 String.prototype.trim function, if available.
10
+ trim : function(s) {
11
+ return s.trim ? s.trim() : s.replace(/^\s+|\s+$/g, '');
12
+ },
13
+
14
+ // Escape strings that are going to be used in a regex. Escapes punctuation
15
+ // that would be incorrect in a regex.
16
+ escapeRegExp : function(s) {
17
+ return s.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1');
18
+ }
19
+ };
20
+
21
+ })();
@@ -0,0 +1,197 @@
1
+ (function() {
2
+
3
+ var $ = jQuery; // Handle namespaced jQuery
4
+
5
+ $.fn.extend({
6
+
7
+ // Makes the selector enter a mode. Modes have both a 'mode' and a 'group',
8
+ // and are mutually exclusive with any other modes in the same group.
9
+ // Setting will update the view's modes hash, as well as set an HTML class
10
+ // of *[mode]_[group]* on the view's element. Convenient way to swap styles
11
+ // and behavior.
12
+ setMode : function(state, group) {
13
+ group = group || 'mode';
14
+ var re = new RegExp("\\w+_" + group + "(\\s|$)", 'g');
15
+ var mode = (state === null) ? "" : state + "_" + group;
16
+ this.each(function() {
17
+ this.className = (this.className.replace(re, '')+' '+mode)
18
+ .replace(/\s\s/g, ' ');
19
+ });
20
+ return mode;
21
+ },
22
+
23
+ // When attached to an input element, this will cause the width of the input
24
+ // to match its contents. This calculates the width of the contents of the input
25
+ // by measuring a hidden shadow div that should match the styling of the input.
26
+ autoGrowInput: function() {
27
+ return this.each(function() {
28
+ var $input = $(this);
29
+ var $tester = $('<div />').css({
30
+ opacity : 0,
31
+ top : -9999,
32
+ left : -9999,
33
+ position : 'absolute',
34
+ whiteSpace : 'nowrap'
35
+ }).addClass('VS-input-width-tester').addClass('VS-interface');
36
+
37
+ // Watch for input value changes on all of these events. `resize`
38
+ // event is called explicitly when the input has been changed without
39
+ // a single keypress.
40
+ var events = 'keydown.autogrow keypress.autogrow ' +
41
+ 'resize.autogrow change.autogrow';
42
+ $input.next('.VS-input-width-tester').remove();
43
+ $input.after($tester);
44
+ $input.unbind(events).bind(events, function(e, realEvent) {
45
+ if (realEvent) e = realEvent;
46
+ var value = $input.val();
47
+
48
+ // Watching for the backspace key is tricky because it may not
49
+ // actually be deleting the character, but instead the key gets
50
+ // redirected to move the cursor from facet to facet.
51
+ if (VS.app.hotkeys.key(e) == 'backspace') {
52
+ var position = $input.getCursorPosition();
53
+ if (position > 0) value = value.slice(0, position-1) +
54
+ value.slice(position, value.length);
55
+ } else if (VS.app.hotkeys.printable(e) &&
56
+ !VS.app.hotkeys.command) {
57
+ value += String.fromCharCode(e.which);
58
+ }
59
+ value = value.replace(/&/g, '&amp;')
60
+ .replace(/\s/g,'&nbsp;')
61
+ .replace(/</g, '&lt;')
62
+ .replace(/>/g, '&gt;');
63
+
64
+ $tester.html(value);
65
+
66
+ $input.width($tester.width() + 3 + parseInt($input.css('min-width')));
67
+ $input.trigger('updated.autogrow');
68
+ });
69
+
70
+ // Sets the width of the input on initialization.
71
+ $input.trigger('resize.autogrow');
72
+ });
73
+ },
74
+
75
+
76
+ // Cross-browser method used for calculating where the cursor is in an
77
+ // input field.
78
+ getCursorPosition: function() {
79
+ var position = 0;
80
+ var input = this.get(0);
81
+
82
+ if (document.selection) { // IE
83
+ input.focus();
84
+ var sel = document.selection.createRange();
85
+ var selLen = document.selection.createRange().text.length;
86
+ sel.moveStart('character', -input.value.length);
87
+ position = sel.text.length - selLen;
88
+ } else if (input && $(input).is(':visible') &&
89
+ input.selectionStart != null) { // Firefox/Safari
90
+ position = input.selectionStart;
91
+ }
92
+
93
+ return position;
94
+ },
95
+
96
+ // A simple proxy for `selectRange` that sets the cursor position in an
97
+ // input field.
98
+ setCursorPosition: function(position) {
99
+ return this.each(function() {
100
+ return $(this).selectRange(position, position);
101
+ });
102
+ },
103
+
104
+ // Cross-browser way to select text in an input field.
105
+ selectRange: function(start, end) {
106
+ return this.filter(':visible').each(function() {
107
+ if (this.setSelectionRange) { // FF/Webkit
108
+ this.focus();
109
+ this.setSelectionRange(start, end);
110
+ } else if (this.createTextRange) { // IE
111
+ var range = this.createTextRange();
112
+ range.collapse(true);
113
+ range.moveEnd('character', end);
114
+ range.moveStart('character', start);
115
+ if (end - start >= 0) range.select();
116
+ }
117
+ });
118
+ },
119
+
120
+ // Returns an object that contains the text selection range values for
121
+ // an input field.
122
+ getSelection: function() {
123
+ var input = this[0];
124
+
125
+ if (input.selectionStart != null) { // FF/Webkit
126
+ var start = input.selectionStart;
127
+ var end = input.selectionEnd;
128
+ return {
129
+ start : start,
130
+ end : end,
131
+ length : end-start,
132
+ text : input.value.substr(start, end-start)
133
+ };
134
+ } else if (document.selection) { // IE
135
+ var range = document.selection.createRange();
136
+ if (range) {
137
+ var textRange = input.createTextRange();
138
+ var copyRange = textRange.duplicate();
139
+ textRange.moveToBookmark(range.getBookmark());
140
+ copyRange.setEndPoint('EndToStart', textRange);
141
+ var start = copyRange.text.length;
142
+ var end = start + range.text.length;
143
+ return {
144
+ start : start,
145
+ end : end,
146
+ length : end-start,
147
+ text : range.text
148
+ };
149
+ }
150
+ }
151
+ return {start: 0, end: 0, length: 0};
152
+ }
153
+
154
+ });
155
+
156
+ // Debugging in Internet Explorer. This allows you to use
157
+ // `console.log(['message', var1, var2, ...])`. Just remove the `false` and
158
+ // add your console.logs. This will automatically stringify objects using
159
+ // `JSON.stringify', so you can read what's going out. Think of this as a
160
+ // *Diet Firebug Lite Zero with Lemon*.
161
+ if (false) {
162
+ window.console = {};
163
+ var _$ied;
164
+ window.console.log = function(msg) {
165
+ if (_.isArray(msg)) {
166
+ var message = msg[0];
167
+ var vars = _.map(msg.slice(1), function(arg) {
168
+ return JSON.stringify(arg);
169
+ }).join(' - ');
170
+ }
171
+ if(!_$ied){
172
+ _$ied = $('<div><ol></ol></div>').css({
173
+ 'position': 'fixed',
174
+ 'bottom': 10,
175
+ 'left': 10,
176
+ 'zIndex': 20000,
177
+ 'width': $('body').width() - 80,
178
+ 'border': '1px solid #000',
179
+ 'padding': '10px',
180
+ 'backgroundColor': '#fff',
181
+ 'fontFamily': 'arial,helvetica,sans-serif',
182
+ 'fontSize': '11px'
183
+ });
184
+ $('body').append(_$ied);
185
+ }
186
+ var $message = $('<li>'+message+' - '+vars+'</li>').css({
187
+ 'borderBottom': '1px solid #999999'
188
+ });
189
+ _$ied.find('ol').append($message);
190
+ _.delay(function() {
191
+ $message.fadeOut(500);
192
+ }, 5000);
193
+ };
194
+
195
+ }
196
+
197
+ })();