super_finder 0.0.1

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.
Files changed (42) hide show
  1. data/.gitignore +1 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.textile +211 -0
  4. data/Rakefile +46 -0
  5. data/VERSION +1 -0
  6. data/assets/images/boxy-ne.png +0 -0
  7. data/assets/images/boxy-nw.png +0 -0
  8. data/assets/images/boxy-se.png +0 -0
  9. data/assets/images/boxy-sw.png +0 -0
  10. data/assets/images/close.png +0 -0
  11. data/assets/images/magnify.png +0 -0
  12. data/assets/javascripts/boxy.js +570 -0
  13. data/assets/javascripts/super_finder.js +193 -0
  14. data/assets/stylesheets/boxy.css +49 -0
  15. data/assets/stylesheets/super_finder.css +60 -0
  16. data/init.rb +2 -0
  17. data/install.rb +18 -0
  18. data/lib/super_finder.rb +17 -0
  19. data/lib/super_finder/cache_manager.rb +38 -0
  20. data/lib/super_finder/cache_sweeper.rb +41 -0
  21. data/lib/super_finder/config.rb +45 -0
  22. data/lib/super_finder/filters.rb +33 -0
  23. data/lib/super_finder/generator_controller.rb +94 -0
  24. data/lib/super_finder/helper.rb +18 -0
  25. data/lib/super_finder/initializer.rb +31 -0
  26. data/lib/super_finder/routes.rb +10 -0
  27. data/spec/assets/locales/en.yml +4 -0
  28. data/spec/functional/cache_spec.rb +45 -0
  29. data/spec/functional/controller_spec.rb +61 -0
  30. data/spec/functional/functional_spec_helper.rb +82 -0
  31. data/spec/functional/routes_spec.rb +29 -0
  32. data/spec/spec.opts +1 -0
  33. data/spec/spec_helper.rb +37 -0
  34. data/spec/unit/cache_manager_spec.rb +41 -0
  35. data/spec/unit/config_spec.rb +24 -0
  36. data/spec/unit/controller_spec.rb +76 -0
  37. data/spec/unit/initializer_spec.rb +44 -0
  38. data/spec/unit/unit_spec_helper.rb +1 -0
  39. data/super_finder.gemspec +92 -0
  40. data/tasks/super_finder_tasks.rake +4 -0
  41. data/uninstall.rb +1 -0
  42. metadata +124 -0
@@ -0,0 +1,193 @@
1
+ // Some portions of the code (related to keypress events) are inspired
2
+ // one of the jquery autocomplete plugin
3
+ // (http://www.pengoworks.com/workshop/jquery/autocomplete.htm)
4
+
5
+ function SuperFinder(options) {
6
+ var self = this;
7
+
8
+ this.options = jQuery.extend({}, SuperFinder.DEFAULTS, options || {});
9
+
10
+ this.input = jQuery('#superfinder input[name=q]');
11
+ this.list = jQuery("#superfinder ul");
12
+
13
+ // Find -> Command T
14
+ jQuery(window).keypress(function(event) {
15
+ // console.log("keypressed " + event.which + ", " + event.altKey);
16
+ if (!(event.which == 8224 && event.altKey)) return true;
17
+ self.open();
18
+ event.preventDefault();
19
+ return false;
20
+ });
21
+
22
+ jQuery(this.options.button).click(function(event) {
23
+ self.open();
24
+ event.preventDefault();
25
+ return false;
26
+ });
27
+
28
+ this.input.attr("autocomplete", "off")
29
+ .keydown(function(e) {
30
+ // track last key pressed
31
+ self.lastKeyPressCode = e.keyCode;
32
+ switch(e.keyCode) {
33
+ case 27: // ESC
34
+ e.preventDefault();
35
+ self.input.blur();
36
+ self.close();
37
+ case 38: // up
38
+ e.preventDefault();
39
+ self.moveSelect(-1);
40
+ break;
41
+ case 40: // down
42
+ e.preventDefault();
43
+ self.moveSelect(1);
44
+ break;
45
+ case 9: // tab
46
+ e.preventDefault();
47
+ break;
48
+ case 13: // return
49
+ self.input.blur();
50
+ self.selectCurrent();
51
+ e.preventDefault();
52
+ break;
53
+ default:
54
+ self.active = -1;
55
+ if (self.timeout) clearTimeout(self.timeout);
56
+ self.timeout = setTimeout(function(){ self.onChange(); }, 400);
57
+ break;
58
+ }
59
+ });
60
+ };
61
+
62
+ jQuery.extend(SuperFinder, {
63
+
64
+ DEFAULTS: {
65
+ title: 'Finder',
66
+ limit: 6,
67
+ maxWordSize: 15,
68
+ button: []
69
+ }
70
+
71
+ });
72
+
73
+ SuperFinder.prototype = {
74
+
75
+ open: function() {
76
+ if (this.boxy == null) {
77
+ this.boxy = new Boxy(jQuery('#superfinder'), {
78
+ title: this.options.title,
79
+ closeText: "<img src=\"/images/super_finder/close.png\" alt=\"[close]\" />"
80
+ });
81
+ this.input.focus();
82
+ } else {
83
+ this.boxy.show();
84
+ this.reset();
85
+ }
86
+ },
87
+
88
+ close: function() {
89
+ this.boxy.hide();
90
+ },
91
+
92
+ search: function(val) {
93
+ // console.log("looking for '" + val + "'");
94
+ var matches = [];
95
+
96
+ if (val.length != 0) {
97
+ var regexp = this.buildRegExp(val);
98
+ for (var key in SuperFinderResources) {
99
+ var tmp = jQuery.grep(SuperFinderResources[key], function(resource) {
100
+ var matched = regexp.test(resource.value.toLowerCase());
101
+ // console.log("resource.value=" + resource.value + ", label = " + key + ", matched =" + matched);
102
+ if (matched) resource.label = key;
103
+ return matched;
104
+ });
105
+ jQuery.merge(matches, tmp);
106
+ }
107
+ }
108
+
109
+ this.update(matches.slice(0, this.options.limit), val);
110
+ },
111
+
112
+ selectCurrent: function() {
113
+ var current = jQuery('li.on', this.list);
114
+
115
+ if (current.size() != 0)
116
+ window.location.href = current.find('a').attr('href');
117
+ },
118
+
119
+ update: function(matches, val) {
120
+ var self = this;
121
+ this.list.empty();
122
+
123
+ jQuery.each(matches, function() {
124
+ var className = this.label.toLowerCase().replace(/ /g, "-").replace(/_/g, "-");
125
+ var name = self.highlight(val, this.value);
126
+
127
+ var li = "<li class=\"" + className + "\"><label>" + this.label + "</label><a href=\"" + this.url + "\">" + name + "</a></li>";
128
+ self.list.append(li);
129
+ });
130
+
131
+ if (matches.length > 0)
132
+ this.moveSelect(1);
133
+ },
134
+
135
+ moveSelect: function(step) {
136
+ var children = this.list.children();
137
+
138
+ if (children.length == 0) return ;
139
+
140
+ if (this.active >= 0)
141
+ jQuery(children[this.active]).find('label').animate({ marginLeft: '0px' }, 'fast');
142
+
143
+ this.active += step;
144
+
145
+ if (this.active < 0) {
146
+ this.active = 0;
147
+ } else if (this.active >= children.size()) {
148
+ this.active = children.size() - 1;
149
+ }
150
+
151
+ children.removeClass('on');
152
+ jQuery(children[this.active]).addClass('on').find('label').animate({ marginLeft: '10px' }, 'fast');
153
+ },
154
+
155
+ onChange: function() {
156
+ if (this.lastKeyPressCode == 46 || (this.lastKeyPressCode > 8 && this.lastKeyPressCode < 32)) return ;
157
+ var val = this.input.val();
158
+ if (val == this.prev) return ;
159
+ this.prev = val;
160
+ this.search(val);
161
+ },
162
+
163
+ reset: function() {
164
+ this.list.empty();
165
+ this.input.val('').focus();
166
+ this.active = -1;
167
+ this.prev = null;
168
+ },
169
+
170
+ highlight: function(val, word) {
171
+ var text = "";
172
+ for (var i = 0; i < val.length; i++) {
173
+ var pos = word.toLowerCase().indexOf(val[i]);
174
+ if (pos != -1) {
175
+ text += word.slice(0, pos) + "<em>" + word[pos] + "</em>";
176
+ if (i == val.length - 1)
177
+ text += word.slice(pos + 1);
178
+ else
179
+ word = word.slice(pos + 1);
180
+ }
181
+ }
182
+ return text;
183
+ },
184
+
185
+ buildRegExp: function(val) {
186
+ var exp = "";
187
+ for (var i = 0; i < val.length; i++) {
188
+ exp += val[i];
189
+ if (i < val.length - 1) exp += ".*";
190
+ }
191
+ return new RegExp(exp);
192
+ }
193
+ };
@@ -0,0 +1,49 @@
1
+ .boxy-wrapper { position: absolute; }
2
+ .boxy-wrapper.fixed { position: fixed; }
3
+
4
+ /* Modal */
5
+
6
+ .boxy-modal-blackout { position: absolute; background-color: black; left: 0; top: 0; }
7
+
8
+ /* Border */
9
+
10
+ .boxy-wrapper { empty-cells: show; width: auto; }
11
+ .boxy-wrapper .top-left,
12
+ .boxy-wrapper .top-right,
13
+ .boxy-wrapper .bottom-right,
14
+ .boxy-wrapper .bottom-left { width: 10px; height: 10px; padding: 0 }
15
+
16
+ .boxy-wrapper .top-left { background: url('/images/super_finder/boxy-nw.png'); }
17
+ .boxy-wrapper .top-right { background: url('/images/super_finder/boxy-ne.png'); }
18
+ .boxy-wrapper .bottom-right { background: url('/images/super_finder/boxy-se.png'); }
19
+ .boxy-wrapper .bottom-left { background: url('/images/super_finder/boxy-sw.png'); }
20
+
21
+ /* IE6+7 hacks for the border. IE7 should support this natively but fails in conjuction with modal blackout bg. */
22
+ /* NB: these must be absolute paths or URLs to your images */
23
+ .boxy-wrapper .top-left { #background: none; #filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/images/super_finder/boxy-nw.png'); }
24
+ .boxy-wrapper .top-right { #background: none; #filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/images/super_finder/boxy-ne.png'); }
25
+ .boxy-wrapper .bottom-right { #background: none; #filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/images/super_finder/boxy-se.png'); }
26
+ .boxy-wrapper .bottom-left { #background: none; #filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/images/super_finder/boxy-sw.png'); }
27
+
28
+ .boxy-wrapper .top,
29
+ .boxy-wrapper .bottom { height: 10px; background-color: black; opacity: 0.6; filter: alpha(opacity=60); padding: 0 }
30
+
31
+ .boxy-wrapper .left,
32
+ .boxy-wrapper .right { width: 10px; background-color: black; opacity: 0.6; filter: alpha(opacity=60); padding: 0 }
33
+
34
+ /* Title bar */
35
+
36
+ .boxy-wrapper .title-bar { background-color: black; padding: 6px; position: relative; }
37
+ .boxy-wrapper .title-bar.dragging { cursor: move; }
38
+ .boxy-wrapper .title-bar h2 { font-size: 12px; color: white; line-height: 1; margin: 0; padding: 0; font-weight: normal; }
39
+ .boxy-wrapper .title-bar .close { color: white; position: absolute; top: 6px; right: 6px; font-size: 90%; line-height: 1; }
40
+
41
+ /* Content Region */
42
+
43
+ .boxy-inner { background-color: white; padding: 0 }
44
+ .boxy-content { padding: 15px; }
45
+
46
+ /* Question Boxes */
47
+
48
+ .boxy-wrapper .question { width: 350px; min-height: 80px; }
49
+ .boxy-wrapper .answers { text-align: right; }
@@ -0,0 +1,60 @@
1
+ @import url(boxy.css);
2
+
3
+ div#superfinder { width: 300px; min-height: 200px; }
4
+
5
+ div#superfinder p {
6
+ margin: 0px;
7
+ background: transparent url(/images/super_finder/magnify.png) no-repeat right 3px;
8
+ }
9
+
10
+ div#superfinder p input {
11
+ width: 260px;
12
+ border: 1px solid #bbb;
13
+ font-size: 16px;
14
+ color: #222;
15
+ padding: 5px;
16
+ background: white;
17
+ }
18
+
19
+ div#superfinder ul { list-style: none; margin: 10px 0 0 0px; padding: 0px; }
20
+
21
+ div#superfinder li {
22
+ background: #dff5f8;
23
+ padding: 5px;
24
+ margin-bottom: 2px;
25
+ overflow: hidden;
26
+ }
27
+
28
+ div#superfinder li.on {
29
+ background-color: #ccc;
30
+ }
31
+
32
+ div#superfinder li label {
33
+ position: relative;
34
+ top: -2px;
35
+ background: #333;
36
+ padding: 2px 6px;
37
+ color: white;
38
+ font-weight: normal;
39
+ font-size: 12px;
40
+
41
+ -moz-border-radius: 5px;
42
+ -webkit-border-radius: 5px;
43
+ }
44
+
45
+ div#superfinder li a {
46
+ margin-left: 15px;
47
+ color: #333;
48
+ text-decoration: none;
49
+ font-size: 16px;
50
+ }
51
+
52
+ div#superfinder li a em {
53
+ font-style: normal;
54
+ font-weight: bold;
55
+ }
56
+
57
+
58
+
59
+
60
+
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ $:.unshift "#{File.dirname(__FILE__)}/lib"
2
+ require 'super_finder'
@@ -0,0 +1,18 @@
1
+ require 'fileutils'
2
+
3
+ unless defined?(RAILS_ROOT)
4
+ RAILS_ROOT = File.expand_path( File.join(File.dirname(__FILE__), '../../../') )
5
+ end
6
+
7
+ ['images', 'stylesheets', 'javascripts'].each do |folder|
8
+ if folder == 'images'
9
+ unless FileTest.exist? File.join(RAILS_ROOT, 'public', 'images', 'super_finder')
10
+ FileUtils.mkdir( File.join(RAILS_ROOT, 'public', 'images', 'super_finder') )
11
+ end
12
+ end
13
+
14
+ FileUtils.cp(
15
+ Dir[File.join(File.dirname(__FILE__), 'assets', folder, '*')],
16
+ File.join([RAILS_ROOT, 'public', folder, folder == 'images' ? 'super_finder' : nil].compact)
17
+ )
18
+ end
@@ -0,0 +1,17 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__))
2
+
3
+ require 'super_finder/routes'
4
+ require 'super_finder/config'
5
+ require 'super_finder/initializer'
6
+ require 'super_finder/cache_manager'
7
+ require 'super_finder/cache_sweeper'
8
+ require 'super_finder/helper'
9
+ require 'super_finder/filters'
10
+
11
+ ActionView::Base.send :include, SuperFinder::Helper
12
+
13
+ # ActionController::Base.class_eval { include SuperFinder::Filters } # dirty hack for dev env
14
+
15
+ require 'super_finder/generator_controller'
16
+
17
+
@@ -0,0 +1,38 @@
1
+ module SuperFinder
2
+
3
+ class CacheManager
4
+
5
+ include Singleton
6
+
7
+ @@caching_keys = {}
8
+
9
+ def fetch(klass, scoper = nil, &block)
10
+ ::Rails.cache.fetch(self.key(klass, scoper), &block)
11
+ end
12
+
13
+ def key(klass, scoper = nil)
14
+ key = internal_key(klass, scoper)
15
+
16
+ @@caching_keys[key] ||= Time.now
17
+
18
+ "superfinder_#{key}_#{@@caching_keys[key].to_i}"
19
+ end
20
+
21
+ def refresh!(klass, scoper = nil)
22
+ @@caching_keys[internal_key(klass, scoper)] = nil
23
+ end
24
+
25
+ protected
26
+
27
+ def internal_key(klass, scoper = nil)
28
+ scope_id = (if scoper
29
+ scoper.is_a?(Integer) ? scoper : scoper.id
30
+ else
31
+ :all
32
+ end)
33
+ "#{scope_id}_#{klass.name.tableize}"
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,41 @@
1
+ module SuperFinder
2
+
3
+ class CacheSweeper < ActiveRecord::Observer
4
+
5
+ unloadable
6
+
7
+ def after_create(record)
8
+ refresh_cache!(record)
9
+ end
10
+
11
+ def after_update(record)
12
+ refresh_cache!(record)
13
+ end
14
+
15
+ def after_destroy(record)
16
+ refresh_cache!(record)
17
+ end
18
+
19
+ def refresh_cache!(record)
20
+ models, scoper = SuperFinder::Config.instance.models, SuperFinder::Config.instance.scoper
21
+
22
+ options = models.find { |m| m[:klass].name == record.class.name } # do not rely on class in dev mode
23
+
24
+ scope_id = nil
25
+
26
+ if options[:scope].nil?
27
+ if scoper && scoper[:column] # looking for a global scope
28
+ scope_id = record.attributes[scoper[:column].to_s]
29
+ end
30
+ end
31
+
32
+ SuperFinder::CacheManager.instance.refresh!(record.class, scope_id)
33
+ end
34
+
35
+ def observed_classes
36
+ SuperFinder::Config.instance.models.map { |m| eval(m[:klass].name) }
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,45 @@
1
+ require 'singleton'
2
+
3
+ module SuperFinder
4
+
5
+ class Config
6
+
7
+ include Singleton
8
+
9
+ @@default_options = {
10
+ :url => {
11
+ :name_prefix => nil,
12
+ :action => :show
13
+ },
14
+ :models => [],
15
+ :scoper => {
16
+ :column => nil,
17
+ :getter => nil
18
+ },
19
+ :before_filters => []
20
+ }
21
+
22
+ def attributes
23
+ @attributes ||= @@default_options.clone # only called once
24
+ end
25
+
26
+ def reset_attributes
27
+ @attributes = nil
28
+ end
29
+
30
+ def method_missing(method, *args)
31
+ if method.to_s.ends_with?('=')
32
+ attributes[method.to_s[0..-2].to_sym] = args.first
33
+ else
34
+ attributes[method]
35
+ end
36
+ end
37
+
38
+ def to_s
39
+ attributes.inspect
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+