~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/app/javascript/picker.js

  • Committer: Danilo Segan
  • Date: 2011-04-22 14:02:29 UTC
  • mto: This revision was merged to the branch mainline in revision 12910.
  • Revision ID: danilo@canonical.com-20110422140229-zhq4d4c2k8jpglhf
Ignore hidden files when building combined JS file.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
/* Copyright 2011 Canonical Ltd.  This software is licensed under the
2
 
 * GNU Affero General Public License version 3 (see the file LICENSE).
3
 
 */
4
 
 
5
1
YUI.add('lp.app.picker', function(Y) {
6
2
 
7
3
var namespace = Y.namespace('lp.app.picker');
18
14
 * @param {String} attribute_name The attribute on the resource being
19
15
 *                                modified.
20
16
 * @param {String} content_box_id
21
 
 * @param {Object} config Object literal of config name/value pairs. The
22
 
 *                        values listed below are common for all picker types.
23
 
 *     config.picker_type: the type of picker to create (default or person).
 
17
 * @param {Object} config Object literal of config name/value pairs.
24
18
 *     config.header: a line of text at the top of the widget.
25
19
 *     config.step_title: overrides the subtitle.
 
20
 *     config.remove_button_text: Override the default 'Remove' text.
26
21
 *     config.null_display_value: Override the default 'None' text.
 
22
 *     config.show_remove_button: Should the remove button be shown?
 
23
 *         Defaults to false, should be a boolean.
 
24
 *     config.show_assign_me_botton: Should the 'assign me' button be shown?
 
25
 *         Defaults to false, should be a boolean.
27
26
 *     config.show_search_box: Should the search box be shown.
28
27
 *         Vocabularies that are not huge should not have a search box.
29
28
 */
35
34
        return;
36
35
    }
37
36
 
 
37
    var show_remove_button = false;
 
38
    var show_assign_me_button = false;
 
39
    var remove_button_text = 'Remove';
38
40
    var null_display_value = 'None';
39
41
    var show_search_box = true;
40
 
    var vocabulary_filters;
41
 
 
42
42
    resource_uri = Y.lp.client.normalize_uri(resource_uri);
43
43
 
44
44
    if (config !== undefined) {
 
45
        if (config.remove_button_text !== undefined) {
 
46
            remove_button_text = config.remove_button_text;
 
47
        }
 
48
 
45
49
        if (config.null_display_value !== undefined) {
46
50
            null_display_value = config.null_display_value;
47
51
        }
 
52
 
 
53
        if (config.show_remove_button !== undefined) {
 
54
            show_remove_button = config.show_remove_button;
 
55
        }
 
56
 
 
57
        if (config.show_assign_me_button !== undefined) {
 
58
            show_assign_me_button = config.show_assign_me_button;
 
59
        }
 
60
 
48
61
        if (config.show_search_box !== undefined) {
49
62
            show_search_box = config.show_search_box;
50
63
        }
51
 
        vocabulary_filters = config.vocabulary_filters;
52
64
    }
53
65
 
54
66
    var content_box = Y.one('#' + content_box_id);
64
76
                '</div>'));
65
77
    };
66
78
 
 
79
    var show_hide_buttons = function () {
 
80
        var link = content_box.one('.yui3-activator-data-box a');
 
81
        if (remove_button) {
 
82
            if (link === null || !show_remove_button) {
 
83
                remove_button.addClass('yui-picker-hidden');
 
84
            } else {
 
85
                remove_button.removeClass('yui-picker-hidden');
 
86
            }
 
87
        }
 
88
 
 
89
        if (assign_me_button) {
 
90
            if (link !== null
 
91
                && link.get('href').indexOf(LP.links.me + '/') != -1) {
 
92
                assign_me_button.addClass('yui-picker-hidden');
 
93
            } else {
 
94
                assign_me_button.removeClass('yui-picker-hidden');
 
95
            }
 
96
        }
 
97
    };
 
98
 
67
99
    var save = function (picker_result) {
68
100
        activator.renderProcessing();
69
101
        var success_handler = function (entry) {
70
 
          var to_render = null_display_value;
71
 
          var selected_value = null;
72
 
          if (entry.get(attribute_name) !== null) {
73
 
              to_render = entry.getHTML(attribute_name);
74
 
              selected_value = picker_result.api_uri;
75
 
          }
76
 
          // NB We need to set the selected_value_metadata attribute first
77
 
          // because we listen for changes to selected_value.
78
 
          picker.set('selected_value_metadata', picker_result.metadata);
79
 
          picker.set('selected_value', selected_value);
80
 
          activator.renderSuccess(to_render);
81
 
        };
82
 
 
83
 
        var patch_payload = {};
84
 
        if (Y.Lang.isValue(picker_result.api_uri)) {
85
 
            patch_payload[attribute_name] = Y.lp.client.get_absolute_uri(
86
 
                picker_result.api_uri);
87
 
        } else {
88
 
            patch_payload[attribute_name] = null;
89
 
        }
90
 
 
91
 
        var client = new Y.lp.client.Launchpad();
 
102
          activator.renderSuccess(entry.getHTML(attribute_name));
 
103
          show_hide_buttons();
 
104
        };
 
105
 
 
106
        var patch_payload = {};
 
107
        patch_payload[attribute_name] = Y.lp.client.get_absolute_uri(
 
108
            picker_result.api_uri);
 
109
 
 
110
        var client = new Y.lp.client.Launchpad();
 
111
        client.patch(picker._resource_uri, patch_payload, {
 
112
            accept: 'application/json;include=lp_html',
 
113
            on: {
 
114
                success: success_handler,
 
115
                failure: failure_handler
 
116
            }
 
117
        });
 
118
    };
 
119
 
 
120
    var assign_me = function () {
 
121
        picker.hide();
 
122
        save({
 
123
            image: '/@@/person',
 
124
            title: 'Me',
 
125
            api_uri: LP.links.me
 
126
        });
 
127
    };
 
128
 
 
129
    var remove = function () {
 
130
        picker.hide();
 
131
        activator.renderProcessing();
 
132
        var success_handler = function (entry) {
 
133
            activator.renderSuccess(Y.Node.create(null_display_value));
 
134
            show_hide_buttons();
 
135
        };
 
136
 
 
137
        var patch_payload = {};
 
138
        patch_payload[attribute_name] = null;
 
139
 
 
140
        var client = new Y.lp.client.Launchpad();
 
141
        // Use picker._resource_uri, since it might have been changed
 
142
        // from the outside after the widget has already been initialized.
92
143
        client.patch(picker._resource_uri, patch_payload, {
93
144
            accept: 'application/json;include=lp_html',
94
145
            on: {
99
150
    };
100
151
 
101
152
    config.save = save;
102
 
    var picker = namespace.create(
103
 
        vocabulary, config, undefined, vocabulary_filters);
 
153
    var picker = namespace.create(vocabulary, config, activator);
104
154
    picker._resource_uri = resource_uri;
 
155
    var extra_buttons = Y.Node.create('<div class="extra-form-buttons"/>');
 
156
    var remove_button, assign_me_button;
 
157
    if (show_remove_button) {
 
158
        remove_button = Y.Node.create(
 
159
            '<a class="yui-picker-remove-button bg-image" ' +
 
160
            'href="javascript:void(0)" ' +
 
161
            'style="background-image: url(/@@/remove); padding-right: 1em">' +
 
162
            remove_button_text + '</a>');
 
163
        remove_button.on('click', remove);
 
164
        extra_buttons.appendChild(remove_button);
 
165
    }
 
166
    if (show_assign_me_button) {
 
167
        assign_me_button = Y.Node.create(
 
168
            '<a class="yui-picker-assign-me-button bg-image" ' +
 
169
            'href="javascript:void(0)" ' +
 
170
            'style="background-image: url(/@@/person)">' +
 
171
            'Assign Me</a>');
 
172
        assign_me_button.on('click', assign_me);
 
173
        extra_buttons.appendChild(assign_me_button);
 
174
    }
 
175
    picker.set('footer_slot', extra_buttons);
105
176
 
106
177
    // If we are to pre-load the vocab, we need a spinner.
107
178
    // We set it up here because we only want to do it once and the
116
187
        if (!show_search_box) {
117
188
          config.temp_spinner.removeClass('unseen');
118
189
          picker.set('min_search_chars', 0);
119
 
          picker.fire(Y.lazr.picker.Picker.SEARCH, '');
120
 
          picker.get('contentBox').one(
121
 
              '.yui3-picker-search-box').addClass('unseen');
 
190
          picker.fire('search', '');
 
191
          picker.get('contentBox').one('.yui3-picker-search-box').addClass('unseen');
122
192
        }
123
193
        picker.show();
124
194
    });
125
195
    activator.render();
 
196
 
 
197
    show_hide_buttons();
 
198
 
126
199
    return picker;
127
200
};
128
201
 
143
216
 * Remove the Loading.... spinner (if it exists).
144
217
 */
145
218
function hide_temporary_spinner(temp_spinner) {
146
 
    if (temp_spinner !== undefined && temp_spinner !== null) {
 
219
    if( temp_spinner != null ) {
147
220
        temp_spinner.addClass('unseen');
148
221
    }
149
222
}
197
270
};
198
271
 
199
272
/*
200
 
 * A specific instantiation of the yesno warning for private teams, used in
201
 
 * multiple places.
202
 
 */
203
 
namespace.public_private_warning = function(
204
 
    picker, picker_result, do_save, do_cancel) {
205
 
 
206
 
    var client = new Y.lp.client.Launchpad();
207
 
    config = {
208
 
        on: {
209
 
            success: function (person) {
210
 
                var private_person = person.get('private');
211
 
                var public_context = true;
212
 
                if (LP.cache.context.private !== undefined) {
213
 
                    public_context = !(LP.cache.context.private);
214
 
                }
215
 
 
216
 
                if (private_person && public_context) {
217
 
                    var yesno_content =
218
 
                        "<p>This action will reveal this team's name to " +
219
 
                        "the public.</p>";
220
 
                    Y.lp.app.picker.yesno_save_confirmation(
221
 
                        picker, yesno_content, 'Continue', 'Choose Again',
222
 
                        do_save, do_cancel);
223
 
                } else {
224
 
                    do_save();
225
 
                }
226
 
            }
227
 
        }
228
 
    };
229
 
    client.get(picker_result.api_uri, config);
230
 
};
231
 
 
232
 
/*
233
273
 * Insert the validation content into the form and animate its appearance.
234
274
 */
235
275
function animate_validation_content(picker, validation_content) {
250
290
    picker.get('contentBox').all('.steps').show();
251
291
    var validation_node = picker.get('contentBox').one('.validation-node');
252
292
    var content_node = picker.get('contentBox').one('.yui3-widget-bd');
253
 
    if (validation_node !== null) {
 
293
    if (validation_node != null) {
254
294
        validation_node.get('parentNode').removeChild(validation_node);
255
295
        content_node.addClass('transparent');
256
296
        content_node.setStyle('opacity', 0);
268
308
    }
269
309
}
270
310
 
271
 
 
272
 
/*
273
 
 * Connect the onchange event of the select menu to copy the selected value
274
 
 * to the text input.
275
 
 *
276
 
 * @param {Node} select_menu The select menu with suggested matches.
277
 
 * @param {Node} text_input The input field to copy the selected match too.
278
 
 */
279
 
namespace.connect_select_menu = function (select_menu, text_input) {
280
 
    if (Y.Lang.isValue(select_menu)) {
281
 
        var copy_selected_value = function(e) {
282
 
            text_input.value = select_menu.value;
283
 
        };
284
 
        Y.on('change', copy_selected_value, select_menu);
285
 
    }
286
 
};
287
 
 
288
311
/**
289
312
  * Creates a picker widget that has already been rendered and hidden.
290
313
  *
291
 
  * @requires dom, dump, lazr.overlay
 
314
  * @requires dom, dump, lazr.overlay, lazr.picker
292
315
  * @method create
293
316
  * @param {String} vocabulary Name of the vocabulary to query.
294
317
  * @param {Object} config Optional Object literal of config name/value pairs.
297
320
  *                        config.step_title overrides the subtitle.
298
321
  *                        config.save is a Function (optional) which takes
299
322
  *                        a single string argument.
300
 
  *                        config.show_search_box: Should the search box be
301
 
  *                        shown.
302
 
  * @param {String} associated_field_id Optional Id of the text field to
303
 
  *                        to be updated with the value selected by the
304
 
  *                        picker.
305
 
  * @param {Object} vocabulary_filters Optional List of filters which are
306
 
  *                        supported by the vocabulary. Filter objects are a
307
 
 *                         dict of name, title, description values.
308
 
  *
309
323
  */
310
 
namespace.create = function (vocabulary, config, associated_field_id,
311
 
                             vocabulary_filters) {
 
324
namespace.create = function (vocabulary, config, activator) {
312
325
    if (Y.UA.ie) {
313
326
        return;
314
327
    }
315
328
 
316
329
    var header = 'Choose an item.';
317
330
    var step_title = "Enter search terms";
318
 
    var show_search_box = true;
319
 
    var picker_type = "default";
320
331
    if (config !== undefined) {
321
332
        if (config.header !== undefined) {
322
333
            header = config.header;
325
336
        if (config.step_title !== undefined) {
326
337
            step_title = config.step_title;
327
338
        }
328
 
 
329
 
        if (config.show_search_box !== undefined) {
330
 
            show_search_box = config.show_search_box;
331
 
        }
332
 
 
333
 
        if (config.picker_type !== undefined) {
334
 
            picker_type = config.picker_type;
335
 
        }
336
 
    } else {
337
 
        config = {};
338
339
    }
339
340
 
340
 
    if (typeof vocabulary !== 'string' && typeof vocabulary !== 'object') {
 
341
    if (typeof vocabulary != 'string' && typeof vocabulary != 'object') {
341
342
        throw new TypeError(
342
343
            "vocabulary argument for Y.lp.picker.create() must be a " +
343
344
            "string or associative array: " + vocabulary);
344
345
    }
345
346
 
346
347
    var new_config = Y.merge(config, {
347
 
        associated_field_id: associated_field_id,
348
348
        align: {
349
349
            points: [Y.WidgetPositionAlign.CC,
350
350
                     Y.WidgetPositionAlign.CC]
354
354
        headerContent: "<h2>" + header + "</h2>",
355
355
        steptitle: step_title,
356
356
        zIndex: 1000,
357
 
        visible: false,
358
 
        filter_options: vocabulary_filters
 
357
        visible: false
359
358
        });
360
 
 
361
 
    var picker = null;
362
 
    if (picker_type === 'person') {
363
 
        picker = new Y.lazr.picker.PersonPicker(new_config);
364
 
    } else {
365
 
        picker = new Y.lazr.picker.Picker(new_config);
366
 
    }
367
 
 
368
 
    // We don't want the default save to fire since this hides
 
359
    var picker = new Y.lazr.Picker(new_config);
 
360
 
 
361
    // We don't want the Y.lazr.Picker default save to fire since this hides
369
362
    // the form. We want to do this ourselves after any validation has had a
370
363
    // chance to be performed.
371
364
    picker.publish('save', { defaultFn: function(){} } );
372
 
 
373
 
    // Has the user performed a search yet?
374
 
    var user_has_searched = false;
375
 
 
 
365
    
376
366
    picker.subscribe('save', function (e) {
377
367
        Y.log('Got save event.');
378
 
        var picker_result = e.details[Y.lazr.picker.Picker.SAVE_RESULT];
 
368
        var picker_result = e.details[Y.lazr.Picker.SAVE_RESULT];
379
369
        var do_save = function() {
380
 
            user_has_searched = false;
381
 
            picker.hide();
382
370
            if (Y.Lang.isFunction(config.save)) {
383
371
                config.save(picker_result);
384
372
            }
396
384
    picker.subscribe('cancel', function (e) {
397
385
        Y.log('Got cancel event.');
398
386
        reset_form(picker);
399
 
        user_has_searched = false;
400
387
    });
401
388
 
402
 
    if (Y.Lang.isValue(config.extra_no_results_message)) {
403
 
        picker.before('resultsChange', function (e) {
404
 
            var new_results = e.details[0].newVal;
405
 
            if (new_results.length === 0) {
406
 
                picker.set('footer_slot',
407
 
                    Y.Node.create(config.extra_no_results_message));
408
 
            } else {
409
 
                picker.set('footer_slot', null);
410
 
            }
411
 
        });
412
 
    }
413
 
 
414
389
    // Search for results, create batches and update the display.
415
390
    // in the widget.
416
391
    var search_handler = function (e) {
417
392
        Y.log('Got search event:' + Y.dump(e.details));
418
393
        var search_text = e.details[0];
419
394
        var selected_batch = e.details[1] || 0;
420
 
        // Was this search initiated automatically, perhaps to load
421
 
        // suggestions?
422
 
        var automated_search = e.details[2] || false;
423
 
        var search_filter = e.details[3];
424
395
        var start = BATCH_SIZE * selected_batch;
425
 
        var batch = 0;
426
 
 
427
 
        // Record whether or not the user has initiated a search yet.
428
 
        user_has_searched = user_has_searched || !automated_search;
429
 
 
430
396
        var display_vocabulary = function(results, total_size, start) {
431
 
            var max_size = MAX_BATCHES * BATCH_SIZE;
432
 
            if (show_search_box && total_size > max_size)  {
 
397
            if (total_size > (MAX_BATCHES * BATCH_SIZE))  {
433
398
                picker.set('error',
434
399
                    'Too many matches. Please try to narrow your search.');
435
400
                // Display a single empty result item so that the picker
444
409
                    var batches = [];
445
410
                    var stop = Math.ceil(total_size / BATCH_SIZE);
446
411
                    if (stop > 1) {
447
 
                        for (batch = 0; batch < stop; batch++) {
 
412
                        for (var i=0; i<stop; i++) {
448
413
                            batches.push({
449
 
                                    name: batch+1,
450
 
                                    value: batch
 
414
                                    name: i+1,
 
415
                                    value: i
451
416
                                });
452
417
                        }
453
418
                    }
457
422
        };
458
423
 
459
424
        // We can pass in a vocabulary name
460
 
        if (typeof vocabulary === 'string') {
 
425
        if (typeof vocabulary == 'string') {
461
426
            var success_handler = function (ignore, response, args) {
462
427
                var entry = Y.JSON.parse(response.responseText);
463
428
                var total_size = entry.total_size;
464
429
                var start = entry.start;
465
430
                var results = entry.entries;
466
431
                hide_temporary_spinner(config.temp_spinner);
467
 
                // If this was an automated (preemptive) search and the user
468
 
                // has not subsequently initiated their own search, display
469
 
                // the results of the search.
470
 
                if (user_has_searched !== automated_search) {
471
 
                    display_vocabulary(results, total_size, start);
472
 
                }
473
 
            };
474
 
 
475
 
            var failure_handler = function (ignore, response, args) {
476
 
                Y.log("Loading " + uri + " failed.");
477
 
                hide_temporary_spinner(config.temp_spinner);
478
 
                // If this was an automated (preemptive) search and the user
479
 
                // has subsequently initiated their own search, don't bother
480
 
                // showing an error message about something the user didn't
481
 
                // initiate and now doesn't care about.
482
 
                if (user_has_searched === automated_search) {
483
 
                    return;
484
 
                }
485
 
                var base_error =
486
 
                    "Sorry, something went wrong with your search.";
487
 
                if (response.status === 500) {
488
 
                    base_error +=
489
 
                        " We've recorded what happened, and we'll fix it " +
490
 
                        "as soon as possible.";
491
 
                } else if (response.status >= 502 && response.status <= 504) {
492
 
                    base_error +=
493
 
                        " Trying again in a couple of minutes might work.";
494
 
                }
495
 
                var oops_id = response.getResponseHeader('X-Lazr-OopsId');
496
 
                if (oops_id) {
497
 
                    base_error += ' (Error ID: ' + oops_id + ')';
498
 
                }
499
 
                picker.set('error', base_error);
500
 
                picker.set('search_mode', false);
501
 
            };
502
 
 
 
432
                display_vocabulary(results, total_size, start);
 
433
            };
503
434
 
504
435
            var qs = '';
505
436
            qs = Y.lp.client.append_qs(qs, 'name', vocabulary);
506
437
            qs = Y.lp.client.append_qs(qs, 'search_text', search_text);
507
 
            if (Y.Lang.isValue(search_filter)) {
508
 
                qs = Y.lp.client.append_qs(
509
 
                    qs, 'search_filter', search_filter);
510
 
            }
511
438
            qs = Y.lp.client.append_qs(qs, 'batch', BATCH_SIZE);
512
439
            qs = Y.lp.client.append_qs(qs, 'start', start);
513
440
 
516
443
            // use the context to limit the results to the same project.
517
444
 
518
445
            var uri = '';
519
 
            if (Y.Lang.isValue(config.context)){
520
 
                uri += Y.lp.get_url_path(
521
 
                    config.context.get('web_link')) + '/';
522
 
            }
 
446
            if (Y.Lang.isValue(config['context'])){
 
447
                uri += Y.lp.get_url_path(config['context'].get('web_link')) + '/';
 
448
            }            
523
449
            uri += '@@+huge-vocabulary?' + qs;
524
450
 
525
 
            var yio = (config.yio !== undefined) ? config.yio : Y;
526
 
            yio.io(uri, {
 
451
            Y.io(uri, {
527
452
                headers: {'Accept': 'application/json'},
528
453
                timeout: 20000,
529
454
                on: {
530
455
                    success: success_handler,
531
 
                    failure: failure_handler
 
456
                    failure: function (arg) {
 
457
                        hide_temporary_spinner(config.temp_spinner);
 
458
                        picker.set('error', 'Loading results failed.');
 
459
                        picker.set('search_mode', false);
 
460
                        Y.log("Loading " + uri + " failed.");
 
461
                    }
532
462
                }
533
463
            });
534
464
        // Or we can pass in a vocabulary directly.
537
467
        }
538
468
    };
539
469
 
540
 
    picker.after(Y.lazr.picker.Picker.SEARCH, search_handler);
 
470
    picker.after('search', search_handler);
541
471
 
542
472
    picker.render();
543
473
    picker.hide();
545
475
};
546
476
 
547
477
}, "0.1", {"requires": [
548
 
    "io", "dom", "dump", "event", "lazr.activator", "json-parse",
549
 
    "lp.client", "lazr.picker", "lazr.person-picker"
 
478
    "io", "dom", "dump", "lazr.picker", "lazr.activator", "json-parse",
 
479
    "lp.client"
550
480
    ]});