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.
- 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
|
+
|