1
/* Copyright (c) 2008, Canonical Ltd. All rights reserved. */
3
YUI.add('lazr.choiceedit', function(Y) {
6
* This class provides the ability to allow a specific field to be
7
* chosen from an enum, similar to a dropdown.
9
* This can be thought of as a rather pretty Ajax-enhanced dropdown menu.
11
* @module lazr.choiceedit
14
var CHOICESOURCE = 'ichoicesource',
15
CHOICELIST = 'ichoicelist',
16
NULLCHOICESOURCE = 'inullchoicesource',
17
C_EDITICON = 'editicon',
18
C_VALUELOCATION = 'value',
19
C_NULLTEXTLOCATION = 'nulltext',
20
C_ADDICON = 'addicon',
22
LEFT_MOUSE_BUTTON = 1,
23
RENDERUI = "renderUI",
26
NOTHING = new Object();
29
* This class provides the ability to allow a specific field to be
30
* chosen from an enum, similar to a dropdown.
37
var ChoiceSource = function() {
38
ChoiceSource.superclass.constructor.apply(this, arguments);
39
Y.after(this._bindUIChoiceSource, this, BINDUI);
40
Y.after(this._syncUIChoiceSource, this, SYNCUI);
43
ChoiceSource.NAME = CHOICESOURCE;
46
* Dictionary of selectors to define subparts of the widget that we care about.
47
* YUI calls ATTRS.set(foo) for each foo defined here
49
* @property InlineEditor.HTML_PARSER
53
ChoiceSource.HTML_PARSER = {
54
value_location: '.' + C_VALUELOCATION,
55
editicon: '.' + C_EDITICON
58
ChoiceSource.ATTRS = {
60
* Possible values of the enum that the user chooses from.
70
* Current value of enum
81
* List header displayed in the popup
92
* Y.Node displaying the current value of the field. Should be
93
* automatically calculated by HTML_PARSER.
94
* Setter function returns Y.one(parameter) so that you can pass
95
* either a Node (as expected) or a selector.
97
* @attribute value_location
102
setter: function(v) {
108
* Y.Node (img) displaying the editicon, which is exchanged for a spinner
109
* while saving happens. Should be automatically calculated by HTML_PARSER.
110
* Setter function returns Y.one(parameter) so that you can pass
111
* either a Node (as expected) or a selector.
113
* @attribute value_location
118
setter: function(v) {
124
* Y.Node display the action icon. The default implementation just returns
125
* the edit icon, but it can be customized to return other elements in
127
* @attribute actionicon
132
return this.get('editicon');
138
setter: function(v) {
152
Y.extend(ChoiceSource, Y.Widget, {
153
initializer: function(cfg) {
155
* Fires when the user selects an item
158
* @preventable _saveData
162
var editicon = this.get('editicon');
163
editicon.original_src = editicon.get("src");
169
* This method is invoked after bindUI is invoked for the Widget class
170
* using YUI's aop infrastructure.
173
* @method _bindUIChoiceSource
176
_bindUIChoiceSource: function() {
178
if (this.get('clickable_content')) {
179
var clickable_element = this.get('contentBox');
181
var clickable_element = this.get('editicon');
183
clickable_element.on("click", this.onClick, this);
185
this.after("valueChange", function(e) {
187
this._showSucceeded();
192
* Update in-page HTML with current value of the field
194
* This method is invoked after syncUI is invoked for the Widget class
195
* using YUI's aop infrastructure.
198
* @method _syncUIChoiceSource
201
_syncUIChoiceSource: function() {
202
var items = this.get("items");
203
var value = this.get("value");
204
var node = this.get("value_location");
205
for (var i=0; i<items.length; i++) {
206
if (items[i].value == value) {
207
node.set("innerHTML", items[i].source_name || items[i].name);
212
_chosen_value: NOTHING,
215
* Get the currently chosen value.
217
* Compatible with the Launchpad PATCH plugin.
221
getInput: function() {
222
if (this._chosen_value !== NOTHING) {
223
return this._chosen_value;
225
return this.get("value");
230
* Handle click and create the ChoiceList to allow user to
236
onClick: function(e) {
238
// Only continue if the down button is the left one.
239
if (e.button != LEFT_MOUSE_BUTTON) {
243
this._choice_list = new Y.ChoiceList({
244
value: this.get("value"),
245
title: this.get("title"),
246
items: this.get("items"),
247
value_location: this.get("value_location"),
252
this._choice_list.on("valueChosen", function(e) {
253
that._chosen_value = e.details[0];
254
that._saveData(e.details[0]);
257
// Stuff the mouse coordinates into the list object,
258
// by the time we'll need them, they won't be available.
259
this._choice_list._mouseX = e.clientX + window.pageXOffset;
260
this._choice_list._mouseY = e.clientY + window.pageYOffset;
262
this._choice_list.render();
273
_saveData: function(newvalue) {
274
this.set("value", newvalue);
279
* Called when save has succeeded to flash the in-page HTML green.
282
* @method _showSucceeded
284
_showSucceeded: function() {
285
this._uiAnimateFlash(Y.lazr.anim.green_flash);
289
* Called when save has failed to flash the in-page HTML red.
292
* @method _showFailed
294
_showFailed: function() {
295
this._uiAnimateFlash(Y.lazr.anim.red_flash);
299
* Run a flash-in animation on the editable text node.
301
* @method _uiAnimateFlash
302
* @param flash_fn {Function} A lazr.anim flash-in function.
305
_uiAnimateFlash: function(flash_fn) {
306
var node = this.get('elementToFlash');
308
node = this.get('contentBox');
310
var cfg = { node: node };
311
if (this.get('backgroundColor') !== null) {
312
cfg.to = {backgroundColor: this.get('backgroundColor')};
314
var anim = flash_fn(cfg);
319
* Set the 'waiting' user-interface state. Be sure to call
320
* _uiClearWaiting() when you are done.
322
* @method _uiSetWaiting
325
_uiSetWaiting: function() {
326
var actionicon = this.get("actionicon");
327
actionicon.original_src = actionicon.get("src");
328
actionicon.set("src", "https://launchpad.net/@@/spinner");
332
* Clear the 'waiting' user-interface state.
334
* @method _uiClearWaiting
337
_uiClearWaiting: function() {
338
var actionicon = this.get("actionicon");
339
actionicon.set("src", actionicon.original_src);
345
Y.ChoiceSource = ChoiceSource;
347
var ChoiceList = function() {
348
ChoiceList.superclass.constructor.apply(this, arguments);
351
ChoiceList.NAME = CHOICELIST;
355
* Possible values of the enum that the user chooses from.
365
* Current value of enum
376
* List header displayed in the popup
387
* Node currently containing the value, around which we need to
390
* @attribute value_location
398
* List of clickable enum values
400
* @attribute display_items_list
403
display_items_list: {
412
Y.extend(ChoiceList, Y.lazr.PrettyOverlay, {
413
initializer: function(cfg) {
415
* Fires when the user selects an item
419
this.publish("valueChosen");
420
this.after("renderedChange", this._positionCorrectly);
421
Y.after(this._renderUIChoiceList, this, RENDERUI);
422
Y.after(this._bindUIChoiceList, this, BINDUI);
426
* Render the popup menu
428
* This method is invoked after renderUI is invoked for the Widget class
429
* using YUI's aop infrastructure.
432
* @method _renderUIChoiceList
435
_renderUIChoiceList: function() {
437
node: this.get("value_location"),
438
points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.TL]
440
this.set("headerContent", "<h2>" + this.get("title") + "</h2>");
441
this.set("display_items_list", Y.Node.create("<ul>"));
442
var display_items_list = this.get("display_items_list");
443
var items = this.get("items");
444
var value = this.get("value");
446
for (var i=0; i<items.length; i++) {
447
if (items[i].disabled) {
448
li = Y.Node.create('<li><span class="disabled">' +
449
items[i].name + '</span></li>');
450
} else if (items[i].value == value) {
451
li = Y.Node.create('<li><span class="current">' +
452
items[i].name + '</span></li>');
454
li = Y.Node.create('<li><a href="#' + items[i].value +
455
'">' + items[i].name + '</a></li>');
456
li.one('a')._value = items[i].value;
458
if (items[i].css_class !== undefined) {
459
li.addClass(items[i].css_class);
461
li.addClass('unstyled');
463
display_items_list.appendChild(li);
466
this.setStdModContent(
467
Y.WidgetStdMod.BODY, display_items_list, Y.WidgetStdMod.REPLACE);
468
this.move(-10000, 0);
474
* This method is invoked after bindUI is invoked for the Widget class
475
* using YUI's aop infrastructure.
478
* @method _bindUIChoiceList
481
_bindUIChoiceList: function() {
482
var display_items_list = this.get("display_items_list");
484
Y.delegate("click", function(e) {
485
var target = e.currentTarget;
486
var value = target._value;
487
var items = that.get("items");
488
for (var i=0; i<items.length; i++) {
489
if (items[i].value == value) {
490
that.fire("valueChosen", items[i].value);
496
}, display_items_list, "li a");
500
* Destroy the widget (remove its HTML from the page)
504
destructor: function() {
505
var bb = this.get("boundingBox");
506
var parent = bb.get("parentNode");
508
parent.removeChild(bb);
513
* Calculate correct position for popup and move it there.
515
* This is needed so that we have the correct height of the overlay,
516
* with the content, when we position it. This solution is not very
517
* elegant - in the future we'd like to be able to use YUI's positioning,
518
* thought it doesn't seem to work correctly right now.
521
* @method _positionCorrectly
523
_positionCorrectly: function(e) {
524
var boundingBox = this.get('boundingBox');
525
var selectedListItem = boundingBox.one('span.current');
526
valueX = this._mouseX - (boundingBox.get('offsetWidth') / 2);
528
if (Y.Lang.isValue(selectedListItem)) {
529
valueY = (this._mouseY -
530
this.get("headerContent").get('offsetHeight') -
531
selectedListItem.get('offsetTop') -
532
(selectedListItem.get('offsetHeight') / 2));
534
valueY = this._mouseY - (boundingBox.get('offsetHeight') / 2);
540
document.body.clientWidth - boundingBox.get('offsetWidth')) &&
541
(document.body.clientWidth > boundingBox.get('offsetWidth'))) {
542
valueX = document.body.clientWidth - boundingBox.get('offsetWidth');
548
this.move(valueX, valueY);
550
var bb = this.get('boundingBox');
551
bb.on('focus', function(e) {
552
bb.one('.close-button').focus();
554
bb.one('.close-button').focus();
558
* Return the absolute position of any node
561
* @method _findPosition
563
_findPosition: function(obj) {
566
if (obj.get("offsetParent")) {
568
curleft += obj.get("offsetLeft");
569
curtop += obj.get("offsetTop");
570
} while ((obj = obj.get("offsetParent")));
572
return [curleft,curtop];
578
Y.augment(ChoiceList, Y.Event.Target);
579
Y.ChoiceList = ChoiceList;
583
* This class provides a specialised implementation of ChoiceSource
584
* displaying a custom UI for null items.
586
* @class NullChoiceSource
587
* @extends ChoiceSource
590
var NullChoiceSource = function() {
591
NullChoiceSource.superclass.constructor.apply(this, arguments);
594
NullChoiceSource.NAME = NULLCHOICESOURCE;
596
NullChoiceSource.HTML_PARSER = {
597
value_location: '.' + C_VALUELOCATION,
598
editicon: '.' + C_EDITICON,
599
null_text_location: '.' + C_NULLTEXTLOCATION,
600
addicon: '.' + C_ADDICON
603
NullChoiceSource.ATTRS = {
604
null_text_location: {},
607
* Action icon returns either the add icon or the edit icon, depending
608
* on whether the currently selected value is null.
610
* @attribute actionicon
614
if (Y.Lang.isValue(this.get('value'))) {
615
return this.get('editicon');
617
return this.get('addicon');
622
* The specialised version of the items attirbute is cloned and the name
623
* of the null value is modified to add a remove icon next to it. If the
624
* currently selected value is null, the null item is not displayed.
630
getter: function(v) {
631
if (!Y.Lang.isValue(this.get("value"))) {
632
v = Y.Array(v).filter(function(item) {
633
return (Y.Lang.isValue(item.value));
636
for (var i = 0; i < v.length; i++) {
637
if (!Y.Lang.isValue(v[i].value) &&
638
v[i].name.indexOf('<img') == -1) {
639
// Only append the icon if the value for this item is
640
// null, and the img tag is not already found.
642
'<img src="https://launchpad.net/@@/remove" ',
643
' style="margin-right: 0.5em; border: none; ',
644
' vertical-align: middle" />',
645
'<span style="text-decoration: underline; ',
658
Y.extend(NullChoiceSource, ChoiceSource, {
659
initializer: function(cfg) {
660
var addicon = this.get('addicon');
661
addicon.original_src = addicon.get("src");
662
var old_uiClearWaiting = this._uiClearWaiting;
663
this._uiClearWaiting = function() {
664
old_uiClearWaiting.call(this);
665
if (Y.Lang.isValue(this.get('value'))) {
666
this.get('null_text_location').setStyle('display', 'none');
667
this.get('addicon').setStyle('display', 'none');
668
this.get('value_location').setStyle('display', 'inline');
669
this.get('editicon').setStyle('display', 'inline');
671
this.get('null_text_location').setStyle('display', 'inline');
672
this.get('addicon').setStyle('display', 'inline');
673
this.get('value_location').setStyle('display', 'none');
674
this.get('editicon').setStyle('display', 'none');
680
Y.NullChoiceSource = NullChoiceSource;
682
},"0.2", {"skinnable": true,
683
"requires": ["oop", "event", "event-delegate", "node",
684
"widget", "widget-position", "widget-stdmod",
685
"overlay", "lazr.overlay", "lazr.anim", "lazr.base"]});