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
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
pkg/
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 [name of plugin creator]
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
h1. SuperFinder
|
2
|
+
|
3
|
+
TextMate’s "cmd-T" functionality right in your ruby on rails application. Results in the popup come from models you choose. Almost everything can be re-defined to match your needs.
|
4
|
+
|
5
|
+
I strongly suggest you to check our demo application to see how it looks.
|
6
|
+
|
7
|
+
h2. Pre-requisities
|
8
|
+
|
9
|
+
ActiveRecord as ORM, jQuery and jQuery boxy plugin.
|
10
|
+
|
11
|
+
h2. Installation
|
12
|
+
|
13
|
+
For now, only installation as a plugin is available.
|
14
|
+
Even if there are multiple steps to pass before using it, the installation is pretty simple.
|
15
|
+
|
16
|
+
<pre>
|
17
|
+
script/plugin install git://github.com/did/super_finder.git
|
18
|
+
</pre>
|
19
|
+
|
20
|
+
Then, in your config/routes.rb, just add at the end of your file the following statement:
|
21
|
+
|
22
|
+
<pre>
|
23
|
+
SuperFinder::Routes.draw(map)
|
24
|
+
</pre>
|
25
|
+
|
26
|
+
Register an new observer in config/environment.rb (or add it to your existing observers list). This observer is responsible for sweeping the cache.
|
27
|
+
|
28
|
+
<pre>
|
29
|
+
config.active_record.observers = 'super_finder/cache_sweeper'
|
30
|
+
</pre>
|
31
|
+
|
32
|
+
Define the behavior of the plugin by creating an initializer file (initializers/super_finder.rb for instance)
|
33
|
+
|
34
|
+
<pre>
|
35
|
+
SuperFinder::Initializer.run do |config|
|
36
|
+
config.models = [
|
37
|
+
{ :klass => Project, :column => :title },
|
38
|
+
{ :klass => Person, :column => :nickname }
|
39
|
+
]
|
40
|
+
end
|
41
|
+
</pre>
|
42
|
+
|
43
|
+
Next, inside your application layout, just before the closing BODY tag, add this:
|
44
|
+
|
45
|
+
<pre>
|
46
|
+
<%= super_finder_tag %>
|
47
|
+
</pre>
|
48
|
+
|
49
|
+
Include the javascript and stylesheets files (do not forget to include jQuery as well):
|
50
|
+
|
51
|
+
<pre>
|
52
|
+
<%= stylesheet_link_tag 'super_finder' %>
|
53
|
+
<%= javascript_include_tag 'boxy', 'super_finder' %>
|
54
|
+
</pre>
|
55
|
+
|
56
|
+
Finally, put into your application.js
|
57
|
+
|
58
|
+
<pre>
|
59
|
+
$(document).ready(function() {
|
60
|
+
new SuperFinder({ button: $('#your-button a') })
|
61
|
+
});
|
62
|
+
</pre>
|
63
|
+
|
64
|
+
_Note: button is optional since we set up a shortcut *(ALT-T)*
|
65
|
+
|
66
|
+
That's it !!!
|
67
|
+
|
68
|
+
h2. Usage
|
69
|
+
|
70
|
+
There are 2 ways to display the popup and begin to search for something
|
71
|
+
|
72
|
+
* Press ALT and t in your browser.
|
73
|
+
* if you provide a css selector for *button* (see previous section), just click the button
|
74
|
+
|
75
|
+
Then, it's pretty straigth forward, type some letters and use your keyboard arrows to select your item. Once you're done, press ENTER.
|
76
|
+
|
77
|
+
You may want to change the color assigned to a label. Just add the following lines in one of your stylesheet files
|
78
|
+
|
79
|
+
<pre>
|
80
|
+
div#superfinder li.project label { background-color: #64992c; }
|
81
|
+
div#superfinder li.task label { background-color: #5a6986; }
|
82
|
+
div#superfinder li.person label { background-color: #ec7000; }
|
83
|
+
div#superfinder li.account label { background-color: #5229a3; }
|
84
|
+
</pre>
|
85
|
+
|
86
|
+
h2. How it works ?
|
87
|
+
|
88
|
+
Thru its own controller, SuperFinder generates on the fly a js file storing by model all the records it found.
|
89
|
+
It means some important things:
|
90
|
+
|
91
|
+
* It best fits for small collections of records. Not to fetch all the customers of an important e-commerce back-office.
|
92
|
+
* Authentication and before_filter constraints can be applied. It prevents not authorized people to sneak at data.
|
93
|
+
|
94
|
+
The javascript output is just a single statement; it assigns the previous collection to a global javascript variable.
|
95
|
+
Then, a jquery uses this variable to emulate the same behaviour of the TextMate’s "cmd-T" functionality
|
96
|
+
|
97
|
+
|
98
|
+
h2. More settings
|
99
|
+
|
100
|
+
Instead of writing hundred or thousands lines, I prefer to show you some examples:
|
101
|
+
|
102
|
+
h3. Simple one
|
103
|
+
|
104
|
+
<pre>
|
105
|
+
SuperFinder::Initializer.run do |config|
|
106
|
+
config.models = [
|
107
|
+
{ :klass => Project, :column => :title },
|
108
|
+
{ :klass => Person, :column => :nickname }
|
109
|
+
]
|
110
|
+
end
|
111
|
+
</pre>
|
112
|
+
|
113
|
+
Let's say you have a project in db whose title is "Ruby on Rails" and id is 42. If you type, "ru" in the popup, "Ruby on Rails" will appear and if you press ENTER, you will be redirected to *project_url(42)*.
|
114
|
+
Simple, isn't it ?
|
115
|
+
|
116
|
+
|
117
|
+
h3. I18n
|
118
|
+
|
119
|
+
For each entry displayed in the popup, a little label precising the model type is also displayed as well.
|
120
|
+
By default, SuperFinder will find the translation of *activerecord.models.<your model>* and if it's missing humanize the model class name. But you can also define it !
|
121
|
+
|
122
|
+
<pre>
|
123
|
+
SuperFinder::Initializer.run do |config|
|
124
|
+
config.models = [
|
125
|
+
{ :klass => Project, :label => 'My fancy projects', :column => :title },
|
126
|
+
{ :klass => Task, :label => Proc.new { |controller| "blablabla" }, :column => :title },
|
127
|
+
{ :klass => Person, :column => Proc.new { |p| p.full_name } }
|
128
|
+
]
|
129
|
+
end
|
130
|
+
</pre>
|
131
|
+
|
132
|
+
h3. Urls
|
133
|
+
|
134
|
+
When pressing enter, you are redirected to the url of the selected entry. For each model, you can define your own url strategy.
|
135
|
+
|
136
|
+
<pre>
|
137
|
+
SuperFinder::Initializer.run do |config|
|
138
|
+
config.url = {
|
139
|
+
:name_prefix => 'admin',
|
140
|
+
:action => :edit # by default, :show
|
141
|
+
}
|
142
|
+
config.models = [
|
143
|
+
{ :klass => Project, :column => :title, :url => { :action => :show, :name_prefix =>nil } },
|
144
|
+
{ :klass => Task, :column => :title },
|
145
|
+
{ :klass => Person, :column => :nickname, :url => Proc.new { |controller, person| controller.my_url(person) } }
|
146
|
+
]
|
147
|
+
end
|
148
|
+
</pre>
|
149
|
+
|
150
|
+
Generated urls will be:
|
151
|
+
|
152
|
+
* *project_url(<project_id>)*
|
153
|
+
* *edit_admin_task_url(<project_id>)*
|
154
|
+
* *edit_admin_task_url(<project_id>)*
|
155
|
+
* *my_url(<project_id>)*
|
156
|
+
|
157
|
+
h3. Scoping / finder
|
158
|
+
|
159
|
+
Sometimes, one of your model is scoped and bound to an account for instance. The scoper object is generally retrieved from the controller based on subdomain.
|
160
|
+
SuperFinder allows you to use the scoper to find entries.
|
161
|
+
|
162
|
+
<pre>
|
163
|
+
SuperFinder::Initializer.run do |config|
|
164
|
+
config.scoper = {
|
165
|
+
:column_id => :account_id,
|
166
|
+
:getter => :current_account # name of controller method returning the scoper instance
|
167
|
+
}
|
168
|
+
config.models = [
|
169
|
+
{ :klass => Project, :column => :title },
|
170
|
+
{ :klass => Person, :column => :nickname, :scoper => false }
|
171
|
+
{ :klass => Task, :column => :title, :finder => Proc.new { |controller| Task.active.all } }
|
172
|
+
]
|
173
|
+
end
|
174
|
+
</pre>
|
175
|
+
|
176
|
+
h3. Controller filtering
|
177
|
+
|
178
|
+
Of course, you do not want bad people sneak at your data. No problems, here is the solution
|
179
|
+
|
180
|
+
<pre>
|
181
|
+
SuperFinder::Initializer.run do |config|
|
182
|
+
config.before_filters = [:must_be_authenticated]
|
183
|
+
config.models = [
|
184
|
+
{ :klass => Project, :column => :title },
|
185
|
+
{ :klass => Person, :column => :nickname }
|
186
|
+
]
|
187
|
+
end
|
188
|
+
</pre>
|
189
|
+
|
190
|
+
_Note: before_filters does not work in development mode for some class reloading issues.
|
191
|
+
|
192
|
+
h2. Demo
|
193
|
+
|
194
|
+
A big thank you at Heroku's people for their awesome service.
|
195
|
+
|
196
|
+
"http://superfinder.heroku.com":http://superfinder.heroku.com
|
197
|
+
|
198
|
+
h2. Tests / Bugs / Evolutions
|
199
|
+
|
200
|
+
The plugin is fully tests with rspec (unit / functional tests). Into the plugin folder, type
|
201
|
+
|
202
|
+
<pre>
|
203
|
+
rake
|
204
|
+
</pre>
|
205
|
+
|
206
|
+
You may find bugs, sure you will actually. If you have time to investigate and solve them, just apply the classic procedure (fork, fix, test and submit).
|
207
|
+
|
208
|
+
For evolutions, you're welcome to suggest your ideas. Contact me at didier at nocoffee dot fr.
|
209
|
+
|
210
|
+
|
211
|
+
Copyright (c) 2010 NoCoffee, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'jeweler'
|
8
|
+
Jeweler::Tasks.new do |gem|
|
9
|
+
gem.name = "super_finder"
|
10
|
+
gem.summary = %Q{TextMate's "cmd-T" functionality in a web app}
|
11
|
+
gem.description = %Q{TextMate's "cmd-T" functionality in a web app}
|
12
|
+
gem.email = "didier@nocoffee.fr"
|
13
|
+
gem.homepage = "http://github.com/did/super_finder"
|
14
|
+
gem.authors = ["Didier Lafforgue"]
|
15
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
16
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
17
|
+
end
|
18
|
+
Jeweler::GemcutterTasks.new
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'Test the super_finder plugin.'
|
24
|
+
Spec::Rake::SpecTask.new('spec:unit') do |spec|
|
25
|
+
spec.libs << 'lib' << 'spec'
|
26
|
+
spec.spec_files = FileList['spec/unit/**/*_spec.rb']
|
27
|
+
end
|
28
|
+
|
29
|
+
Spec::Rake::SpecTask.new('spec:functionals') do |spec|
|
30
|
+
spec.libs << 'lib' << 'spec'
|
31
|
+
spec.spec_files = FileList['spec/functional/**/*_spec.rb']
|
32
|
+
end
|
33
|
+
|
34
|
+
task :spec => ['spec:unit', 'spec:functionals']
|
35
|
+
|
36
|
+
desc 'Default: run rspec tests.'
|
37
|
+
task :default => :spec
|
38
|
+
|
39
|
+
desc 'Generate documentation for the super_finder plugin.'
|
40
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
41
|
+
rdoc.rdoc_dir = 'rdoc'
|
42
|
+
rdoc.title = 'SuperFinder'
|
43
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
44
|
+
rdoc.rdoc_files.include('README')
|
45
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
46
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,570 @@
|
|
1
|
+
/**
|
2
|
+
* Boxy 0.1.4 - Facebook-style dialog, with frills
|
3
|
+
*
|
4
|
+
* (c) 2008 Jason Frame
|
5
|
+
* Licensed under the MIT License (LICENSE)
|
6
|
+
*/
|
7
|
+
|
8
|
+
/*
|
9
|
+
* jQuery plugin
|
10
|
+
*
|
11
|
+
* Options:
|
12
|
+
* message: confirmation message for form submit hook (default: "Please confirm:")
|
13
|
+
*
|
14
|
+
* Any other options - e.g. 'clone' - will be passed onto the boxy constructor (or
|
15
|
+
* Boxy.load for AJAX operations)
|
16
|
+
*/
|
17
|
+
jQuery.fn.boxy = function(options) {
|
18
|
+
options = options || {};
|
19
|
+
return this.each(function() {
|
20
|
+
var node = this.nodeName.toLowerCase(), self = this;
|
21
|
+
if (node == 'a') {
|
22
|
+
jQuery(this).click(function() {
|
23
|
+
var active = Boxy.linkedTo(this),
|
24
|
+
href = this.getAttribute('href'),
|
25
|
+
localOptions = jQuery.extend({actuator: this, title: this.title}, options);
|
26
|
+
|
27
|
+
if (active) {
|
28
|
+
active.show();
|
29
|
+
} else if (href.indexOf('#') >= 0) {
|
30
|
+
var content = jQuery(href.substr(href.indexOf('#'))),
|
31
|
+
newContent = content.clone(true);
|
32
|
+
content.remove();
|
33
|
+
localOptions.unloadOnHide = false;
|
34
|
+
new Boxy(newContent, localOptions);
|
35
|
+
} else { // fall back to AJAX; could do with a same-origin check
|
36
|
+
if (!localOptions.cache) localOptions.unloadOnHide = true;
|
37
|
+
Boxy.load(this.href, localOptions);
|
38
|
+
}
|
39
|
+
|
40
|
+
return false;
|
41
|
+
});
|
42
|
+
} else if (node == 'form') {
|
43
|
+
jQuery(this).bind('submit.boxy', function() {
|
44
|
+
Boxy.confirm(options.message || 'Please confirm:', function() {
|
45
|
+
jQuery(self).unbind('submit.boxy').submit();
|
46
|
+
});
|
47
|
+
return false;
|
48
|
+
});
|
49
|
+
}
|
50
|
+
});
|
51
|
+
};
|
52
|
+
|
53
|
+
//
|
54
|
+
// Boxy Class
|
55
|
+
|
56
|
+
function Boxy(element, options) {
|
57
|
+
|
58
|
+
this.boxy = jQuery(Boxy.WRAPPER);
|
59
|
+
jQuery.data(this.boxy[0], 'boxy', this);
|
60
|
+
|
61
|
+
this.visible = false;
|
62
|
+
this.options = jQuery.extend({}, Boxy.DEFAULTS, options || {});
|
63
|
+
|
64
|
+
if (this.options.modal) {
|
65
|
+
this.options = jQuery.extend(this.options, {center: true, draggable: false});
|
66
|
+
}
|
67
|
+
|
68
|
+
// options.actuator == DOM element that opened this boxy
|
69
|
+
// association will be automatically deleted when this boxy is remove()d
|
70
|
+
if (this.options.actuator) {
|
71
|
+
jQuery.data(this.options.actuator, 'active.boxy', this);
|
72
|
+
}
|
73
|
+
|
74
|
+
this.setContent(element || "<div></div>");
|
75
|
+
this._setupTitleBar();
|
76
|
+
|
77
|
+
this.boxy.css('display', 'none').appendTo(document.body);
|
78
|
+
this.toTop();
|
79
|
+
|
80
|
+
if (this.options.fixed) {
|
81
|
+
if (jQuery.browser.msie && jQuery.browser.version < 7) {
|
82
|
+
this.options.fixed = false; // IE6 doesn't support fixed positioning
|
83
|
+
} else {
|
84
|
+
this.boxy.addClass('fixed');
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
if (this.options.center && Boxy._u(this.options.x, this.options.y)) {
|
89
|
+
this.center();
|
90
|
+
} else {
|
91
|
+
this.moveTo(
|
92
|
+
Boxy._u(this.options.x) ? this.options.x : Boxy.DEFAULT_X,
|
93
|
+
Boxy._u(this.options.y) ? this.options.y : Boxy.DEFAULT_Y
|
94
|
+
);
|
95
|
+
}
|
96
|
+
|
97
|
+
if (this.options.show) this.show();
|
98
|
+
|
99
|
+
};
|
100
|
+
|
101
|
+
Boxy.EF = function() {};
|
102
|
+
|
103
|
+
jQuery.extend(Boxy, {
|
104
|
+
|
105
|
+
WRAPPER: "<table cellspacing='0' cellpadding='0' border='0' class='boxy-wrapper'>" +
|
106
|
+
"<tr><td class='top-left'></td><td class='top'></td><td class='top-right'></td></tr>" +
|
107
|
+
"<tr><td class='left'></td><td class='boxy-inner'></td><td class='right'></td></tr>" +
|
108
|
+
"<tr><td class='bottom-left'></td><td class='bottom'></td><td class='bottom-right'></td></tr>" +
|
109
|
+
"</table>",
|
110
|
+
|
111
|
+
DEFAULTS: {
|
112
|
+
title: null, // titlebar text. titlebar will not be visible if not set.
|
113
|
+
closeable: true, // display close link in titlebar?
|
114
|
+
draggable: true, // can this dialog be dragged?
|
115
|
+
clone: false, // clone content prior to insertion into dialog?
|
116
|
+
actuator: null, // element which opened this dialog
|
117
|
+
center: true, // center dialog in viewport?
|
118
|
+
show: true, // show dialog immediately?
|
119
|
+
modal: false, // make dialog modal?
|
120
|
+
fixed: true, // use fixed positioning, if supported? absolute positioning used otherwise
|
121
|
+
closeText: '[close]', // text to use for default close link
|
122
|
+
unloadOnHide: false, // should this dialog be removed from the DOM after being hidden?
|
123
|
+
clickToFront: false, // bring dialog to foreground on any click (not just titlebar)?
|
124
|
+
behaviours: Boxy.EF, // function used to apply behaviours to all content embedded in dialog.
|
125
|
+
afterDrop: Boxy.EF, // callback fired after dialog is dropped. executes in context of Boxy instance.
|
126
|
+
afterShow: Boxy.EF, // callback fired after dialog becomes visible. executes in context of Boxy instance.
|
127
|
+
afterHide: Boxy.EF, // callback fired after dialog is hidden. executed in context of Boxy instance.
|
128
|
+
beforeUnload: Boxy.EF // callback fired after dialog is unloaded. executed in context of Boxy instance.
|
129
|
+
},
|
130
|
+
|
131
|
+
DEFAULT_X: 50,
|
132
|
+
DEFAULT_Y: 50,
|
133
|
+
zIndex: 1337,
|
134
|
+
dragConfigured: false, // only set up one drag handler for all boxys
|
135
|
+
resizeConfigured: false,
|
136
|
+
dragging: null,
|
137
|
+
|
138
|
+
// load a URL and display in boxy
|
139
|
+
// url - url to load
|
140
|
+
// options keys (any not listed below are passed to boxy constructor)
|
141
|
+
// type: HTTP method, default: GET
|
142
|
+
// cache: cache retrieved content? default: false
|
143
|
+
// filter: jQuery selector used to filter remote content
|
144
|
+
load: function(url, options) {
|
145
|
+
|
146
|
+
options = options || {};
|
147
|
+
|
148
|
+
var ajax = {
|
149
|
+
url: url, type: 'GET', dataType: 'html', cache: false, success: function(html) {
|
150
|
+
html = jQuery(html);
|
151
|
+
if (options.filter) html = jQuery(options.filter, html);
|
152
|
+
new Boxy(html, options);
|
153
|
+
}
|
154
|
+
};
|
155
|
+
|
156
|
+
jQuery.each(['type', 'cache'], function() {
|
157
|
+
if (this in options) {
|
158
|
+
ajax[this] = options[this];
|
159
|
+
delete options[this];
|
160
|
+
}
|
161
|
+
});
|
162
|
+
|
163
|
+
jQuery.ajax(ajax);
|
164
|
+
|
165
|
+
},
|
166
|
+
|
167
|
+
// allows you to get a handle to the containing boxy instance of any element
|
168
|
+
// e.g. <a href='#' onclick='alert(Boxy.get(this));'>inspect!</a>.
|
169
|
+
// this returns the actual instance of the boxy 'class', not just a DOM element.
|
170
|
+
// Boxy.get(this).hide() would be valid, for instance.
|
171
|
+
get: function(ele) {
|
172
|
+
var p = jQuery(ele).parents('.boxy-wrapper');
|
173
|
+
return p.length ? jQuery.data(p[0], 'boxy') : null;
|
174
|
+
},
|
175
|
+
|
176
|
+
// returns the boxy instance which has been linked to a given element via the
|
177
|
+
// 'actuator' constructor option.
|
178
|
+
linkedTo: function(ele) {
|
179
|
+
return jQuery.data(ele, 'active.boxy');
|
180
|
+
},
|
181
|
+
|
182
|
+
// displays an alert box with a given message, calling optional callback
|
183
|
+
// after dismissal.
|
184
|
+
alert: function(message, callback, options) {
|
185
|
+
return Boxy.ask(message, ['OK'], callback, options);
|
186
|
+
},
|
187
|
+
|
188
|
+
// displays an alert box with a given message, calling after callback iff
|
189
|
+
// user selects OK.
|
190
|
+
confirm: function(message, after, options) {
|
191
|
+
return Boxy.ask(message, ['OK', 'Cancel'], function(response) {
|
192
|
+
if (response == 'OK') after();
|
193
|
+
}, options);
|
194
|
+
},
|
195
|
+
|
196
|
+
// asks a question with multiple responses presented as buttons
|
197
|
+
// selected item is returned to a callback method.
|
198
|
+
// answers may be either an array or a hash. if it's an array, the
|
199
|
+
// the callback will received the selected value. if it's a hash,
|
200
|
+
// you'll get the corresponding key.
|
201
|
+
ask: function(question, answers, callback, options) {
|
202
|
+
|
203
|
+
options = jQuery.extend({modal: true, closeable: false},
|
204
|
+
options || {},
|
205
|
+
{show: true, unloadOnHide: true});
|
206
|
+
|
207
|
+
var body = jQuery('<div></div>').append(jQuery('<div class="question"></div>').html(question));
|
208
|
+
|
209
|
+
// ick
|
210
|
+
var map = {}, answerStrings = [];
|
211
|
+
if (answers instanceof Array) {
|
212
|
+
for (var i = 0; i < answers.length; i++) {
|
213
|
+
map[answers[i]] = answers[i];
|
214
|
+
answerStrings.push(answers[i]);
|
215
|
+
}
|
216
|
+
} else {
|
217
|
+
for (var k in answers) {
|
218
|
+
map[answers[k]] = k;
|
219
|
+
answerStrings.push(answers[k]);
|
220
|
+
}
|
221
|
+
}
|
222
|
+
|
223
|
+
var buttons = jQuery('<form class="answers"></form>');
|
224
|
+
buttons.html(jQuery.map(answerStrings, function(v) {
|
225
|
+
return "<input type='button' value='" + v + "' />";
|
226
|
+
}).join(' '));
|
227
|
+
|
228
|
+
jQuery('input[type=button]', buttons).click(function() {
|
229
|
+
var clicked = this;
|
230
|
+
Boxy.get(this).hide(function() {
|
231
|
+
if (callback) callback(map[clicked.value]);
|
232
|
+
});
|
233
|
+
});
|
234
|
+
|
235
|
+
body.append(buttons);
|
236
|
+
|
237
|
+
new Boxy(body, options);
|
238
|
+
|
239
|
+
},
|
240
|
+
|
241
|
+
// returns true if a modal boxy is visible, false otherwise
|
242
|
+
isModalVisible: function() {
|
243
|
+
return jQuery('.boxy-modal-blackout').length > 0;
|
244
|
+
},
|
245
|
+
|
246
|
+
_u: function() {
|
247
|
+
for (var i = 0; i < arguments.length; i++)
|
248
|
+
if (typeof arguments[i] != 'undefined') return false;
|
249
|
+
return true;
|
250
|
+
},
|
251
|
+
|
252
|
+
_handleResize: function(evt) {
|
253
|
+
var d = jQuery(document);
|
254
|
+
jQuery('.boxy-modal-blackout').css('display', 'none').css({
|
255
|
+
width: d.width(), height: d.height()
|
256
|
+
}).css('display', 'block');
|
257
|
+
},
|
258
|
+
|
259
|
+
_handleDrag: function(evt) {
|
260
|
+
var d;
|
261
|
+
if (d = Boxy.dragging) {
|
262
|
+
d[0].boxy.css({left: evt.pageX - d[1], top: evt.pageY - d[2]});
|
263
|
+
}
|
264
|
+
},
|
265
|
+
|
266
|
+
_nextZ: function() {
|
267
|
+
return Boxy.zIndex++;
|
268
|
+
},
|
269
|
+
|
270
|
+
_viewport: function() {
|
271
|
+
var d = document.documentElement, b = document.body, w = window;
|
272
|
+
return jQuery.extend(
|
273
|
+
jQuery.browser.msie ?
|
274
|
+
{ left: b.scrollLeft || d.scrollLeft, top: b.scrollTop || d.scrollTop } :
|
275
|
+
{ left: w.pageXOffset, top: w.pageYOffset },
|
276
|
+
!Boxy._u(w.innerWidth) ?
|
277
|
+
{ width: w.innerWidth, height: w.innerHeight } :
|
278
|
+
(!Boxy._u(d) && !Boxy._u(d.clientWidth) && d.clientWidth != 0 ?
|
279
|
+
{ width: d.clientWidth, height: d.clientHeight } :
|
280
|
+
{ width: b.clientWidth, height: b.clientHeight }) );
|
281
|
+
}
|
282
|
+
|
283
|
+
});
|
284
|
+
|
285
|
+
Boxy.prototype = {
|
286
|
+
|
287
|
+
// Returns the size of this boxy instance without displaying it.
|
288
|
+
// Do not use this method if boxy is already visible, use getSize() instead.
|
289
|
+
estimateSize: function() {
|
290
|
+
this.boxy.css({visibility: 'hidden', display: 'block'});
|
291
|
+
var dims = this.getSize();
|
292
|
+
this.boxy.css('display', 'none').css('visibility', 'visible');
|
293
|
+
return dims;
|
294
|
+
},
|
295
|
+
|
296
|
+
// Returns the dimensions of the entire boxy dialog as [width,height]
|
297
|
+
getSize: function() {
|
298
|
+
return [this.boxy.width(), this.boxy.height()];
|
299
|
+
},
|
300
|
+
|
301
|
+
// Returns the dimensions of the content region as [width,height]
|
302
|
+
getContentSize: function() {
|
303
|
+
var c = this.getContent();
|
304
|
+
return [c.width(), c.height()];
|
305
|
+
},
|
306
|
+
|
307
|
+
// Returns the position of this dialog as [x,y]
|
308
|
+
getPosition: function() {
|
309
|
+
var b = this.boxy[0];
|
310
|
+
return [b.offsetLeft, b.offsetTop];
|
311
|
+
},
|
312
|
+
|
313
|
+
// Returns the center point of this dialog as [x,y]
|
314
|
+
getCenter: function() {
|
315
|
+
var p = this.getPosition();
|
316
|
+
var s = this.getSize();
|
317
|
+
return [Math.floor(p[0] + s[0] / 2), Math.floor(p[1] + s[1] / 2)];
|
318
|
+
},
|
319
|
+
|
320
|
+
// Returns a jQuery object wrapping the inner boxy region.
|
321
|
+
// Not much reason to use this, you're probably more interested in getContent()
|
322
|
+
getInner: function() {
|
323
|
+
return jQuery('.boxy-inner', this.boxy);
|
324
|
+
},
|
325
|
+
|
326
|
+
// Returns a jQuery object wrapping the boxy content region.
|
327
|
+
// This is the user-editable content area (i.e. excludes titlebar)
|
328
|
+
getContent: function() {
|
329
|
+
return jQuery('.boxy-content', this.boxy);
|
330
|
+
},
|
331
|
+
|
332
|
+
// Replace dialog content
|
333
|
+
setContent: function(newContent) {
|
334
|
+
newContent = jQuery(newContent).css({display: 'block'}).addClass('boxy-content');
|
335
|
+
if (this.options.clone) newContent = newContent.clone(true);
|
336
|
+
this.getContent().remove();
|
337
|
+
this.getInner().append(newContent);
|
338
|
+
this._setupDefaultBehaviours(newContent);
|
339
|
+
this.options.behaviours.call(this, newContent);
|
340
|
+
return this;
|
341
|
+
},
|
342
|
+
|
343
|
+
// Move this dialog to some position, funnily enough
|
344
|
+
moveTo: function(x, y) {
|
345
|
+
this.moveToX(x).moveToY(y);
|
346
|
+
return this;
|
347
|
+
},
|
348
|
+
|
349
|
+
// Move this dialog (x-coord only)
|
350
|
+
moveToX: function(x) {
|
351
|
+
if (typeof x == 'number') this.boxy.css({left: x});
|
352
|
+
else this.centerX();
|
353
|
+
return this;
|
354
|
+
},
|
355
|
+
|
356
|
+
// Move this dialog (y-coord only)
|
357
|
+
moveToY: function(y) {
|
358
|
+
if (typeof y == 'number') this.boxy.css({top: y});
|
359
|
+
else this.centerY();
|
360
|
+
return this;
|
361
|
+
},
|
362
|
+
|
363
|
+
// Move this dialog so that it is centered at (x,y)
|
364
|
+
centerAt: function(x, y) {
|
365
|
+
var s = this[this.visible ? 'getSize' : 'estimateSize']();
|
366
|
+
if (typeof x == 'number') this.moveToX(x - s[0] / 2);
|
367
|
+
if (typeof y == 'number') this.moveToY(y - s[1] / 2);
|
368
|
+
return this;
|
369
|
+
},
|
370
|
+
|
371
|
+
centerAtX: function(x) {
|
372
|
+
return this.centerAt(x, null);
|
373
|
+
},
|
374
|
+
|
375
|
+
centerAtY: function(y) {
|
376
|
+
return this.centerAt(null, y);
|
377
|
+
},
|
378
|
+
|
379
|
+
// Center this dialog in the viewport
|
380
|
+
// axis is optional, can be 'x', 'y'.
|
381
|
+
center: function(axis) {
|
382
|
+
var v = Boxy._viewport();
|
383
|
+
var o = this.options.fixed ? [0, 0] : [v.left, v.top];
|
384
|
+
if (!axis || axis == 'x') this.centerAt(o[0] + v.width / 2, null);
|
385
|
+
if (!axis || axis == 'y') this.centerAt(null, o[1] + v.height / 2);
|
386
|
+
return this;
|
387
|
+
},
|
388
|
+
|
389
|
+
// Center this dialog in the viewport (x-coord only)
|
390
|
+
centerX: function() {
|
391
|
+
return this.center('x');
|
392
|
+
},
|
393
|
+
|
394
|
+
// Center this dialog in the viewport (y-coord only)
|
395
|
+
centerY: function() {
|
396
|
+
return this.center('y');
|
397
|
+
},
|
398
|
+
|
399
|
+
// Resize the content region to a specific size
|
400
|
+
resize: function(width, height, after) {
|
401
|
+
if (!this.visible) return;
|
402
|
+
var bounds = this._getBoundsForResize(width, height);
|
403
|
+
this.boxy.css({left: bounds[0], top: bounds[1]});
|
404
|
+
this.getContent().css({width: bounds[2], height: bounds[3]});
|
405
|
+
if (after) after(this);
|
406
|
+
return this;
|
407
|
+
},
|
408
|
+
|
409
|
+
// Tween the content region to a specific size
|
410
|
+
tween: function(width, height, after) {
|
411
|
+
if (!this.visible) return;
|
412
|
+
var bounds = this._getBoundsForResize(width, height);
|
413
|
+
var self = this;
|
414
|
+
this.boxy.stop().animate({left: bounds[0], top: bounds[1]});
|
415
|
+
this.getContent().stop().animate({width: bounds[2], height: bounds[3]}, function() {
|
416
|
+
if (after) after(self);
|
417
|
+
});
|
418
|
+
return this;
|
419
|
+
},
|
420
|
+
|
421
|
+
// Returns true if this dialog is visible, false otherwise
|
422
|
+
isVisible: function() {
|
423
|
+
return this.visible;
|
424
|
+
},
|
425
|
+
|
426
|
+
// Make this boxy instance visible
|
427
|
+
show: function() {
|
428
|
+
if (this.visible) return;
|
429
|
+
if (this.options.modal) {
|
430
|
+
var self = this;
|
431
|
+
if (!Boxy.resizeConfigured) {
|
432
|
+
Boxy.resizeConfigured = true;
|
433
|
+
jQuery(window).resize(function() { Boxy._handleResize(); });
|
434
|
+
}
|
435
|
+
this.modalBlackout = jQuery('<div class="boxy-modal-blackout"></div>')
|
436
|
+
.css({zIndex: Boxy._nextZ(),
|
437
|
+
opacity: 0.7,
|
438
|
+
width: jQuery(document).width(),
|
439
|
+
height: jQuery(document).height()})
|
440
|
+
.appendTo(document.body);
|
441
|
+
this.toTop();
|
442
|
+
if (this.options.closeable) {
|
443
|
+
jQuery(document.body).bind('keypress.boxy', function(evt) {
|
444
|
+
var key = evt.which || evt.keyCode;
|
445
|
+
if (key == 27) {
|
446
|
+
self.hide();
|
447
|
+
jQuery(document.body).unbind('keypress.boxy');
|
448
|
+
}
|
449
|
+
});
|
450
|
+
}
|
451
|
+
}
|
452
|
+
this.boxy.stop().css({opacity: 1}).show();
|
453
|
+
this.visible = true;
|
454
|
+
this._fire('afterShow');
|
455
|
+
return this;
|
456
|
+
},
|
457
|
+
|
458
|
+
// Hide this boxy instance
|
459
|
+
hide: function(after) {
|
460
|
+
if (!this.visible) return;
|
461
|
+
var self = this;
|
462
|
+
if (this.options.modal) {
|
463
|
+
jQuery(document.body).unbind('keypress.boxy');
|
464
|
+
this.modalBlackout.animate({opacity: 0}, function() {
|
465
|
+
jQuery(this).remove();
|
466
|
+
});
|
467
|
+
}
|
468
|
+
this.boxy.stop().animate({opacity: 0}, 300, function() {
|
469
|
+
self.boxy.css({display: 'none'});
|
470
|
+
self.visible = false;
|
471
|
+
self._fire('afterHide');
|
472
|
+
if (after) after(self);
|
473
|
+
if (self.options.unloadOnHide) self.unload();
|
474
|
+
});
|
475
|
+
return this;
|
476
|
+
},
|
477
|
+
|
478
|
+
toggle: function() {
|
479
|
+
this[this.visible ? 'hide' : 'show']();
|
480
|
+
return this;
|
481
|
+
},
|
482
|
+
|
483
|
+
hideAndUnload: function(after) {
|
484
|
+
this.options.unloadOnHide = true;
|
485
|
+
this.hide(after);
|
486
|
+
return this;
|
487
|
+
},
|
488
|
+
|
489
|
+
unload: function() {
|
490
|
+
this._fire('beforeUnload');
|
491
|
+
this.boxy.remove();
|
492
|
+
if (this.options.actuator) {
|
493
|
+
jQuery.data(this.options.actuator, 'active.boxy', false);
|
494
|
+
}
|
495
|
+
},
|
496
|
+
|
497
|
+
// Move this dialog box above all other boxy instances
|
498
|
+
toTop: function() {
|
499
|
+
this.boxy.css({zIndex: Boxy._nextZ()});
|
500
|
+
return this;
|
501
|
+
},
|
502
|
+
|
503
|
+
// Returns the title of this dialog
|
504
|
+
getTitle: function() {
|
505
|
+
return jQuery('> .title-bar h2', this.getInner()).html();
|
506
|
+
},
|
507
|
+
|
508
|
+
// Sets the title of this dialog
|
509
|
+
setTitle: function(t) {
|
510
|
+
jQuery('> .title-bar h2', this.getInner()).html(t);
|
511
|
+
return this;
|
512
|
+
},
|
513
|
+
|
514
|
+
//
|
515
|
+
// Don't touch these privates
|
516
|
+
|
517
|
+
_getBoundsForResize: function(width, height) {
|
518
|
+
var csize = this.getContentSize();
|
519
|
+
var delta = [width - csize[0], height - csize[1]];
|
520
|
+
var p = this.getPosition();
|
521
|
+
return [Math.max(p[0] - delta[0] / 2, 0),
|
522
|
+
Math.max(p[1] - delta[1] / 2, 0), width, height];
|
523
|
+
},
|
524
|
+
|
525
|
+
_setupTitleBar: function() {
|
526
|
+
if (this.options.title) {
|
527
|
+
var self = this;
|
528
|
+
var tb = jQuery("<div class='title-bar'></div>").html("<h2>" + this.options.title + "</h2>");
|
529
|
+
if (this.options.closeable) {
|
530
|
+
tb.append(jQuery("<a href='#' class='close'></a>").html(this.options.closeText));
|
531
|
+
}
|
532
|
+
if (this.options.draggable) {
|
533
|
+
tb[0].onselectstart = function() { return false; }
|
534
|
+
tb[0].unselectable = 'on';
|
535
|
+
tb[0].style.MozUserSelect = 'none';
|
536
|
+
if (!Boxy.dragConfigured) {
|
537
|
+
jQuery(document).mousemove(Boxy._handleDrag);
|
538
|
+
Boxy.dragConfigured = true;
|
539
|
+
}
|
540
|
+
tb.mousedown(function(evt) {
|
541
|
+
self.toTop();
|
542
|
+
Boxy.dragging = [self, evt.pageX - self.boxy[0].offsetLeft, evt.pageY - self.boxy[0].offsetTop];
|
543
|
+
jQuery(this).addClass('dragging');
|
544
|
+
}).mouseup(function() {
|
545
|
+
jQuery(this).removeClass('dragging');
|
546
|
+
Boxy.dragging = null;
|
547
|
+
self._fire('afterDrop');
|
548
|
+
});
|
549
|
+
}
|
550
|
+
this.getInner().prepend(tb);
|
551
|
+
this._setupDefaultBehaviours(tb);
|
552
|
+
}
|
553
|
+
},
|
554
|
+
|
555
|
+
_setupDefaultBehaviours: function(root) {
|
556
|
+
var self = this;
|
557
|
+
if (this.options.clickToFront) {
|
558
|
+
root.click(function() { self.toTop(); });
|
559
|
+
}
|
560
|
+
jQuery('.close', root).click(function() {
|
561
|
+
self.hide();
|
562
|
+
return false;
|
563
|
+
}).mousedown(function(evt) { evt.stopPropagation(); });
|
564
|
+
},
|
565
|
+
|
566
|
+
_fire: function(event) {
|
567
|
+
this.options[event].call(this);
|
568
|
+
}
|
569
|
+
|
570
|
+
};
|