recordselect 3.0.4
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/CHANGELOG +25 -0
- data/MIT-LICENSE +20 -0
- data/README +15 -0
- data/app/views/record_select/_browse.html.erb +8 -0
- data/app/views/record_select/_list.html.erb +31 -0
- data/app/views/record_select/_search.html.erb +20 -0
- data/app/views/record_select/browse.js.rjs +1 -0
- data/assets/images/cross.gif +0 -0
- data/assets/images/next.gif +0 -0
- data/assets/images/previous.gif +0 -0
- data/assets/javascripts/jquery/record_select.js +499 -0
- data/assets/javascripts/prototype/record_select.js +369 -0
- data/assets/stylesheets/record_select.css +133 -0
- data/lib/record_select.rb +37 -0
- data/lib/record_select/actions.rb +68 -0
- data/lib/record_select/conditions.rb +100 -0
- data/lib/record_select/config.rb +99 -0
- data/lib/record_select/engine.rb +4 -0
- data/lib/record_select/extensions/active_record.rb +9 -0
- data/lib/record_select/extensions/localization.rb +8 -0
- data/lib/record_select/extensions/routing_mapper.rb +20 -0
- data/lib/record_select/form_builder.rb +25 -0
- data/lib/record_select/helpers/record_select_helper.rb +185 -0
- data/lib/record_select/version.rb +9 -0
- data/lib/record_select_assets.rb +28 -0
- data/lib/recordselect.rb +23 -0
- data/test/recordselect_test.rb +10 -0
- metadata +154 -0
@@ -0,0 +1,369 @@
|
|
1
|
+
document.observe("dom:loaded", function() {
|
2
|
+
RecordSelect.document_loaded = true;
|
3
|
+
document.on('ajax:before', 'div.record-select * li.record a', function(event) {
|
4
|
+
var link = event.findElement();
|
5
|
+
if (link) {
|
6
|
+
if (RecordSelect.notify(link) == false) {
|
7
|
+
event.stop();
|
8
|
+
} else {
|
9
|
+
link.toggleClassName("selected");
|
10
|
+
}
|
11
|
+
}
|
12
|
+
return true;
|
13
|
+
});
|
14
|
+
});
|
15
|
+
|
16
|
+
Form.Element.AfterActivity = function(element, callback, delay) {
|
17
|
+
element = $(element);
|
18
|
+
if (!delay) delay = 0.25;
|
19
|
+
new Form.Element.Observer(element, delay, function(element, value) {
|
20
|
+
// TODO: display loading indicator
|
21
|
+
if (element.activity_timer) clearTimeout(element.activity_timer);
|
22
|
+
element.activity_timer = setTimeout(function() {
|
23
|
+
callback(element.value);
|
24
|
+
}, delay * 1000 + 50);
|
25
|
+
});
|
26
|
+
}
|
27
|
+
|
28
|
+
var RecordSelect = new Object();
|
29
|
+
RecordSelect.document_loaded = false;
|
30
|
+
|
31
|
+
RecordSelect.notify = function(item) {
|
32
|
+
var e = Element.up(item, '.record-select-handler');
|
33
|
+
var onselect = e.onselect || e.getAttribute('onselect');
|
34
|
+
if (typeof onselect != 'function') onselect = eval(onselect);
|
35
|
+
if (onselect)
|
36
|
+
{
|
37
|
+
try {
|
38
|
+
onselect(item.parentNode.id.substr(2), (item.down('label') || item).innerHTML.unescapeHTML(), e);
|
39
|
+
} catch(e) {
|
40
|
+
alert(e);
|
41
|
+
}
|
42
|
+
return false;
|
43
|
+
}
|
44
|
+
else return true;
|
45
|
+
};
|
46
|
+
|
47
|
+
RecordSelect.render_page = function(record_select_id, page) {
|
48
|
+
var page_element = $$('#' + record_select_id + ' ol')[0];
|
49
|
+
if (page_element) Element.replace(page_element, page);
|
50
|
+
};
|
51
|
+
|
52
|
+
RecordSelect.Abstract = Class.create();
|
53
|
+
Object.extend(RecordSelect.Abstract.prototype, {
|
54
|
+
/**
|
55
|
+
* obj - the id or element that will anchor the recordselect to the page
|
56
|
+
* url - the url to run the recordselect
|
57
|
+
* options - ??? (check concrete classes)
|
58
|
+
*/
|
59
|
+
initialize: function(obj, url, options) {
|
60
|
+
this.obj = $(obj);
|
61
|
+
this.url = url;
|
62
|
+
this.options = options;
|
63
|
+
this.container;
|
64
|
+
|
65
|
+
if (RecordSelect.document_loaded) this.onload();
|
66
|
+
else Event.observe(window, 'load', this.onload.bind(this));
|
67
|
+
},
|
68
|
+
|
69
|
+
/**
|
70
|
+
* Finish the setup - IE doesn't like doing certain things before the page loads
|
71
|
+
* --override--
|
72
|
+
*/
|
73
|
+
onload: function() {},
|
74
|
+
|
75
|
+
/**
|
76
|
+
* the onselect event handler - when someone clicks on a record
|
77
|
+
* --override--
|
78
|
+
*/
|
79
|
+
onselect: function(id, value) {
|
80
|
+
alert(id + ': ' + value);
|
81
|
+
},
|
82
|
+
|
83
|
+
/**
|
84
|
+
* opens the recordselect
|
85
|
+
*/
|
86
|
+
open: function() {
|
87
|
+
if (this.is_open()) return;
|
88
|
+
|
89
|
+
new Ajax.Updater(this.container, this.url, {
|
90
|
+
method: 'get',
|
91
|
+
evalScripts: true,
|
92
|
+
asynchronous: true,
|
93
|
+
onComplete: function() {
|
94
|
+
this.show();
|
95
|
+
// needs to be mousedown so the event doesn't get canceled by other code (see issue #26)
|
96
|
+
Element.observe(document.body, 'mousedown', this.onbodyclick.bindAsEventListener(this));
|
97
|
+
}.bind(this)
|
98
|
+
});
|
99
|
+
},
|
100
|
+
|
101
|
+
/**
|
102
|
+
* positions and reveals the recordselect
|
103
|
+
*/
|
104
|
+
show: function() {
|
105
|
+
var offset = Position.cumulativeOffset(this.obj);
|
106
|
+
this.container.style.left = offset[0] + 'px';
|
107
|
+
this.container.style.top = (Element.getHeight(this.obj) + offset[1]) + 'px';
|
108
|
+
|
109
|
+
if (this._use_iframe_mask()) {
|
110
|
+
this.container.insertAdjacentHTML('afterEnd', '<iframe src="javascript:false;" class="record-select-mask" />');
|
111
|
+
var mask = this.container.next('iframe');
|
112
|
+
mask.style.left = this.container.style.left;
|
113
|
+
mask.style.top = this.container.style.top;
|
114
|
+
}
|
115
|
+
|
116
|
+
this.container.show();
|
117
|
+
|
118
|
+
if (this._use_iframe_mask()) {
|
119
|
+
var dimensions = this.container.immediateDescendants().first().getDimensions();
|
120
|
+
mask.style.width = dimensions.width + 'px';
|
121
|
+
mask.style.height = dimensions.height + 'px';
|
122
|
+
}
|
123
|
+
},
|
124
|
+
|
125
|
+
/**
|
126
|
+
* closes the recordselect by emptying the container
|
127
|
+
*/
|
128
|
+
close: function() {
|
129
|
+
if (this._use_iframe_mask()) {
|
130
|
+
this.container.next('iframe').remove();
|
131
|
+
}
|
132
|
+
|
133
|
+
this.container.hide();
|
134
|
+
// hopefully by using remove() instead of innerHTML we won't leak memory
|
135
|
+
this.container.immediateDescendants().invoke('remove');
|
136
|
+
},
|
137
|
+
|
138
|
+
/**
|
139
|
+
* returns true/false for whether the recordselect is open
|
140
|
+
*/
|
141
|
+
is_open: function() {
|
142
|
+
return (!this.container.innerHTML.blank())
|
143
|
+
},
|
144
|
+
|
145
|
+
/**
|
146
|
+
* when the user clicks outside the dropdown
|
147
|
+
*/
|
148
|
+
onbodyclick: function(ev) {
|
149
|
+
if (!this.is_open()) return;
|
150
|
+
var elem = $(Event.element(ev));
|
151
|
+
var ancestors = elem.ancestors();
|
152
|
+
ancestors.push(elem);
|
153
|
+
if (ancestors.include(this.container) || ancestors.include(this.obj)) return;
|
154
|
+
this.close();
|
155
|
+
},
|
156
|
+
|
157
|
+
/**
|
158
|
+
* creates and initializes (and returns) the recordselect container
|
159
|
+
*/
|
160
|
+
create_container: function() {
|
161
|
+
new Insertion.Bottom(document.body, '<div class="record-select-container record-select-handler"></div>');
|
162
|
+
e = document.body.childNodes[document.body.childNodes.length - 1];
|
163
|
+
e.onselect = this.onselect.bind(this);
|
164
|
+
e.style.display = 'none';
|
165
|
+
|
166
|
+
return $(e);
|
167
|
+
},
|
168
|
+
|
169
|
+
/**
|
170
|
+
* all the behavior to respond to a text field as a search box
|
171
|
+
*/
|
172
|
+
_respond_to_text_field: function(text_field) {
|
173
|
+
// attach the events to start this party
|
174
|
+
text_field.observe('focus', this.open.bind(this));
|
175
|
+
|
176
|
+
// the autosearch event - needs to happen slightly late (keyup is later than keypress)
|
177
|
+
text_field.observe('keyup', function() {
|
178
|
+
if (!this.is_open()) return;
|
179
|
+
this.container.down('.text-input').value = text_field.value;
|
180
|
+
}.bind(this));
|
181
|
+
|
182
|
+
// keyboard navigation, if available
|
183
|
+
if (this.onkeypress) {
|
184
|
+
text_field.observe('keypress', this.onkeypress.bind(this));
|
185
|
+
}
|
186
|
+
},
|
187
|
+
|
188
|
+
_use_iframe_mask: function() {
|
189
|
+
return this.container.insertAdjacentHTML ? true : false;
|
190
|
+
}
|
191
|
+
});
|
192
|
+
|
193
|
+
/**
|
194
|
+
* Adds keyboard navigation to RecordSelect objects
|
195
|
+
*/
|
196
|
+
Object.extend(RecordSelect.Abstract.prototype, {
|
197
|
+
current: null,
|
198
|
+
|
199
|
+
/**
|
200
|
+
* keyboard navigation - where to intercept the keys is up to the concrete class
|
201
|
+
*/
|
202
|
+
onkeypress: function(ev) {
|
203
|
+
var elem;
|
204
|
+
switch (ev.keyCode) {
|
205
|
+
case Event.KEY_UP:
|
206
|
+
if (this.current && this.current.up('.record-select')) elem = this.current.previous();
|
207
|
+
if (!elem) elem = this.container.getElementsBySelector('ol li.record').last();
|
208
|
+
this.highlight(elem);
|
209
|
+
break;
|
210
|
+
case Event.KEY_DOWN:
|
211
|
+
if (this.current && this.current.up('.record-select')) elem = this.current.next();
|
212
|
+
if (!elem) elem = this.container.getElementsBySelector('ol li.record').first();
|
213
|
+
this.highlight(elem);
|
214
|
+
break;
|
215
|
+
case Event.KEY_SPACE:
|
216
|
+
case Event.KEY_RETURN:
|
217
|
+
if (this.current) this.current.down('a').onclick();
|
218
|
+
break;
|
219
|
+
case Event.KEY_RIGHT:
|
220
|
+
elem = this.container.down('li.pagination.next');
|
221
|
+
if (elem) elem.down('a').onclick();
|
222
|
+
break;
|
223
|
+
case Event.KEY_LEFT:
|
224
|
+
elem = this.container.down('li.pagination.previous');
|
225
|
+
if (elem) elem.down('a').onclick();
|
226
|
+
break;
|
227
|
+
case Event.KEY_ESC:
|
228
|
+
this.close();
|
229
|
+
break;
|
230
|
+
default:
|
231
|
+
return;
|
232
|
+
}
|
233
|
+
Event.stop(ev); // so "enter" doesn't submit the form, among other things(?)
|
234
|
+
},
|
235
|
+
|
236
|
+
/**
|
237
|
+
* moves the highlight to a new object
|
238
|
+
*/
|
239
|
+
highlight: function(obj) {
|
240
|
+
if (this.current) this.current.removeClassName('current');
|
241
|
+
this.current = $(obj);
|
242
|
+
obj.addClassName('current');
|
243
|
+
}
|
244
|
+
});
|
245
|
+
|
246
|
+
/**
|
247
|
+
* Used by link_to_record_select
|
248
|
+
* The options hash should contain a onselect: key, with a javascript function as value
|
249
|
+
*/
|
250
|
+
RecordSelect.Dialog = Class.create();
|
251
|
+
RecordSelect.Dialog.prototype = Object.extend(new RecordSelect.Abstract(), {
|
252
|
+
onload: function() {
|
253
|
+
this.container = this.create_container();
|
254
|
+
this.obj.observe('click', this.toggle.bind(this));
|
255
|
+
|
256
|
+
if (this.onkeypress) this.obj.observe('keypress', this.onkeypress.bind(this));
|
257
|
+
},
|
258
|
+
|
259
|
+
onselect: function(id, value) {
|
260
|
+
if (this.options.onselect(id, value) != false) this.close();
|
261
|
+
},
|
262
|
+
|
263
|
+
toggle: function() {
|
264
|
+
if (this.is_open()) this.close();
|
265
|
+
else this.open();
|
266
|
+
}
|
267
|
+
});
|
268
|
+
|
269
|
+
/**
|
270
|
+
* Used by record_select_field helper
|
271
|
+
* The options hash may contain id: and label: keys, designating the current value
|
272
|
+
* The options hash may also include an onchange: key, where the value is a javascript function (or eval-able string) for an callback routine.
|
273
|
+
*/
|
274
|
+
RecordSelect.Single = Class.create();
|
275
|
+
RecordSelect.Single.prototype = Object.extend(new RecordSelect.Abstract(), {
|
276
|
+
onload: function() {
|
277
|
+
// initialize the container
|
278
|
+
this.container = this.create_container();
|
279
|
+
this.container.addClassName('record-select-autocomplete');
|
280
|
+
|
281
|
+
// create the hidden input
|
282
|
+
new Insertion.After(this.obj, '<input type="hidden" name="" value="" />')
|
283
|
+
this.hidden_input = this.obj.next();
|
284
|
+
|
285
|
+
// transfer the input name from the text input to the hidden input
|
286
|
+
this.hidden_input.name = this.obj.name;
|
287
|
+
this.obj.name = '';
|
288
|
+
|
289
|
+
// initialize the values
|
290
|
+
this.set(this.options.id, this.options.label);
|
291
|
+
|
292
|
+
this._respond_to_text_field(this.obj);
|
293
|
+
if (this.obj.focused) this.open(); // if it was focused before we could attach observers
|
294
|
+
},
|
295
|
+
|
296
|
+
close: function() {
|
297
|
+
// if they close the dialog with the text field empty, then delete the id value
|
298
|
+
if (this.obj.value == '') this.set('', '');
|
299
|
+
|
300
|
+
RecordSelect.Abstract.prototype.close.call(this);
|
301
|
+
},
|
302
|
+
|
303
|
+
onselect: function(id, value) {
|
304
|
+
this.set(id, value);
|
305
|
+
if (this.options.onchange) this.options.onchange.call(this, id, value);
|
306
|
+
this.close();
|
307
|
+
},
|
308
|
+
|
309
|
+
/**
|
310
|
+
* sets the id/label
|
311
|
+
*/
|
312
|
+
set: function(id, label) {
|
313
|
+
this.obj.value = label.unescapeHTML();
|
314
|
+
this.hidden_input.value = id;
|
315
|
+
}
|
316
|
+
});
|
317
|
+
|
318
|
+
/**
|
319
|
+
* Used by record_multi_select_field helper.
|
320
|
+
* Options:
|
321
|
+
* list - the id (or object) of the <ul> to contain the <li>s of selected entries
|
322
|
+
* current - an array of id:/label: keys designating the currently selected entries
|
323
|
+
*/
|
324
|
+
RecordSelect.Multiple = Class.create();
|
325
|
+
RecordSelect.Multiple.prototype = Object.extend(new RecordSelect.Abstract(), {
|
326
|
+
onload: function() {
|
327
|
+
// initialize the container
|
328
|
+
this.container = this.create_container();
|
329
|
+
this.container.addClassName('record-select-autocomplete');
|
330
|
+
|
331
|
+
// decide where the <li> entries should be placed
|
332
|
+
if (this.options.list) this.list_container = $(this.options.list);
|
333
|
+
else this.list_container = this.obj.next('ul');
|
334
|
+
|
335
|
+
// take the input name from the text input, and store it for this.add()
|
336
|
+
this.input_name = this.obj.name;
|
337
|
+
this.obj.name = '';
|
338
|
+
|
339
|
+
// initialize the list
|
340
|
+
$A(this.options.current).each(function(c) {
|
341
|
+
this.add(c.id, c.label);
|
342
|
+
}.bind(this));
|
343
|
+
|
344
|
+
this._respond_to_text_field(this.obj);
|
345
|
+
if (this.obj.focused) this.open(); // if it was focused before we could attach observers
|
346
|
+
},
|
347
|
+
|
348
|
+
onselect: function(id, value) {
|
349
|
+
this.add(id, value);
|
350
|
+
},
|
351
|
+
|
352
|
+
/**
|
353
|
+
* Adds a record to the selected list
|
354
|
+
*/
|
355
|
+
add: function(id, label) {
|
356
|
+
// return silently if this value has already been selected
|
357
|
+
var already_selected = this.list_container.getElementsBySelector('input').any(function(i) {
|
358
|
+
return i.value == id
|
359
|
+
});
|
360
|
+
if (already_selected) return;
|
361
|
+
|
362
|
+
var entry = '<li>'
|
363
|
+
+ '<a href="#" onclick="$(this.parentNode).remove(); return false;" class="remove">remove</a>'
|
364
|
+
+ '<input type="hidden" name="' + this.input_name + '" value="' + id + '" />'
|
365
|
+
+ '<label>' + label + '</label>'
|
366
|
+
+ '</li>';
|
367
|
+
new Insertion.Top(this.list_container, entry);
|
368
|
+
}
|
369
|
+
});
|
@@ -0,0 +1,133 @@
|
|
1
|
+
.record-select {
|
2
|
+
width: 300px;
|
3
|
+
border: 1px solid #afd0f5;
|
4
|
+
font-family: sans-serif;
|
5
|
+
background-color: #fff;
|
6
|
+
font-size: 11px;
|
7
|
+
}
|
8
|
+
|
9
|
+
.record-select img {
|
10
|
+
border-width: 0px;
|
11
|
+
}
|
12
|
+
|
13
|
+
.record-select form {
|
14
|
+
display: inline;
|
15
|
+
}
|
16
|
+
|
17
|
+
.record-select form .text-input {
|
18
|
+
width: 294px;
|
19
|
+
margin: 2px auto 1px auto;
|
20
|
+
display: block;
|
21
|
+
border: 1px solid #999;
|
22
|
+
}
|
23
|
+
|
24
|
+
.record-select form input.example {
|
25
|
+
color: #999;
|
26
|
+
text-align: center;
|
27
|
+
}
|
28
|
+
|
29
|
+
.record-select form .search_submit {
|
30
|
+
display: none;
|
31
|
+
}
|
32
|
+
|
33
|
+
.record-select ol,
|
34
|
+
.record-select li {
|
35
|
+
margin: 0px;
|
36
|
+
padding: 0px;
|
37
|
+
list-style: none;
|
38
|
+
clear: both;
|
39
|
+
}
|
40
|
+
|
41
|
+
.record-select a {
|
42
|
+
color: #0066cc;
|
43
|
+
text-decoration: none;
|
44
|
+
}
|
45
|
+
|
46
|
+
.record-select ol a {
|
47
|
+
display: block;
|
48
|
+
zoom: 1;
|
49
|
+
background-color: #e6f2ff;
|
50
|
+
padding: 2px 4px;
|
51
|
+
}
|
52
|
+
|
53
|
+
.record-select ol .even a {
|
54
|
+
background-color: #ffffff;
|
55
|
+
}
|
56
|
+
|
57
|
+
.record-select ol .pagination a {
|
58
|
+
background-color: #eee;
|
59
|
+
}
|
60
|
+
|
61
|
+
.record-select ol .previous a {
|
62
|
+
border-bottom: 1px solid #afd0f5;
|
63
|
+
}
|
64
|
+
|
65
|
+
.record-select ol .next a {
|
66
|
+
border-top: 1px solid #afd0f5;
|
67
|
+
}
|
68
|
+
|
69
|
+
.record-select ol .pagination a img {
|
70
|
+
vertical-align: middle;
|
71
|
+
}
|
72
|
+
|
73
|
+
.record-select ol .found {
|
74
|
+
text-align: center;
|
75
|
+
font-style: italic;
|
76
|
+
color: #999;
|
77
|
+
padding: 1px 4px;
|
78
|
+
border-bottom: 1px solid #afd0f5;
|
79
|
+
}
|
80
|
+
|
81
|
+
.record-select ol .current a,
|
82
|
+
.record-select ol a:hover {
|
83
|
+
background-color: #ffff88;
|
84
|
+
}
|
85
|
+
|
86
|
+
.record-select ol a.selected {
|
87
|
+
background-color: #666;
|
88
|
+
color: #fff;
|
89
|
+
}
|
90
|
+
|
91
|
+
.record-select-container {
|
92
|
+
position: absolute;
|
93
|
+
z-index: 100;
|
94
|
+
}
|
95
|
+
|
96
|
+
iframe.record-select-mask {
|
97
|
+
/* to mask windowed elements in IE6 */
|
98
|
+
position: absolute;
|
99
|
+
z-index: 99;
|
100
|
+
filter: progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0);
|
101
|
+
}
|
102
|
+
|
103
|
+
.record-select-autocomplete form .text-input {
|
104
|
+
display: none;
|
105
|
+
}
|
106
|
+
|
107
|
+
.record-select-list {
|
108
|
+
padding: 0px;
|
109
|
+
margin: 0px;
|
110
|
+
list-style: none;
|
111
|
+
}
|
112
|
+
|
113
|
+
.record-select-list li {
|
114
|
+
overflow: auto;
|
115
|
+
zoom: 1;
|
116
|
+
margin-left: 10px;
|
117
|
+
font-size: 80%;
|
118
|
+
}
|
119
|
+
|
120
|
+
.record-select-list label {
|
121
|
+
float: left;
|
122
|
+
}
|
123
|
+
|
124
|
+
.record-select-list a.remove {
|
125
|
+
display: block;
|
126
|
+
width: 0px;
|
127
|
+
height: 16px;
|
128
|
+
padding-left: 16px;
|
129
|
+
background: url('../../images/record_select/cross.gif') no-repeat 0 0;
|
130
|
+
overflow: hidden;
|
131
|
+
float: left;
|
132
|
+
margin-right: 5px;
|
133
|
+
}
|