super_finder 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+