super_finder 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/MIT-LICENSE +20 -0
- data/README.textile +211 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/assets/images/boxy-ne.png +0 -0
- data/assets/images/boxy-nw.png +0 -0
- data/assets/images/boxy-se.png +0 -0
- data/assets/images/boxy-sw.png +0 -0
- data/assets/images/close.png +0 -0
- data/assets/images/magnify.png +0 -0
- data/assets/javascripts/boxy.js +570 -0
- data/assets/javascripts/super_finder.js +193 -0
- data/assets/stylesheets/boxy.css +49 -0
- data/assets/stylesheets/super_finder.css +60 -0
- data/init.rb +2 -0
- data/install.rb +18 -0
- data/lib/super_finder.rb +17 -0
- data/lib/super_finder/cache_manager.rb +38 -0
- data/lib/super_finder/cache_sweeper.rb +41 -0
- data/lib/super_finder/config.rb +45 -0
- data/lib/super_finder/filters.rb +33 -0
- data/lib/super_finder/generator_controller.rb +94 -0
- data/lib/super_finder/helper.rb +18 -0
- data/lib/super_finder/initializer.rb +31 -0
- data/lib/super_finder/routes.rb +10 -0
- data/spec/assets/locales/en.yml +4 -0
- data/spec/functional/cache_spec.rb +45 -0
- data/spec/functional/controller_spec.rb +61 -0
- data/spec/functional/functional_spec_helper.rb +82 -0
- data/spec/functional/routes_spec.rb +29 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +37 -0
- data/spec/unit/cache_manager_spec.rb +41 -0
- data/spec/unit/config_spec.rb +24 -0
- data/spec/unit/controller_spec.rb +76 -0
- data/spec/unit/initializer_spec.rb +44 -0
- data/spec/unit/unit_spec_helper.rb +1 -0
- data/super_finder.gemspec +92 -0
- data/tasks/super_finder_tasks.rake +4 -0
- data/uninstall.rb +1 -0
- 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
data/install.rb
ADDED
@@ -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
|
data/lib/super_finder.rb
ADDED
@@ -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
|
+
|