/**
* Copyright Franson Technology AB, Sweden, 2009
* http://gpsgate.com, http://franson.com
*
* author Fredrik Blomqvist
*
*/
/**
*
* @module UI
*/
var Franson = Franson || {};
/*
* namespace
* //class Franson.UI
* //static
*/
Franson.UI = Franson.UI || {};
/**
* todo: expose CSS styling
* todo: handle hierarchial menus?
* @class Franson.UI.PopupMenu
* @constructor
* @param {string} menuContent
* @param {string} [mode='stateless'] one of: [ 'stateless', 'checkbox', 'radiobutton' ]
*/
Franson.UI.PopupMenu = function(settings, menuContent)
{
settings = MochiKit.Base.setdefault(settings, {
id: 'popupMenu_' + Franson.Util.getUniqueId()
});
/** @private @type string */
this.id = settings.id;
/** @private */
this._mode = settings.mode;
/** @private */
this._menuContent = menuContent;
/** @private @type DOM|string */
this._targetElem = null; // set in attach()
this._events = [];
};
/*
// todo: better def "language"? (simplified DOM-lang)
menuContent = [
{ 'text': 'row one' },
{ 'radio-group': [ // todo: id
{ 'text': 'radio row 1' },
{ 'text': 'radio row 2' }
]},
{ 'text': 'baasdfa', 'type': 'checkbox' },
{ 'separator' },
{ 'text: 'asffa' }
{ 'checkbox-group': [
{ 'text': 'asdf' },
{ 'text': 'saddf' }
]}
];
*/
Franson.UI.PopupMenu.prototype =
{
hide: function()
{
MochiKit.Style.hideElement(this.id);
},
show: function()
{
var targetBounds = Franson.DOM.getElementBounds(this._targetElem);
MochiKit.Style.showElement(this.id);
var ourBounds = Franson.DOM.getElementBounds(this.id); // or just store this at init time?
// align on right side (todo: take align flags?)
MochiKit.Style.setElementPosition(this.id, {
x: targetBounds.x + targetBounds.w - ourBounds.w,
y: targetBounds.y + targetBounds.h
});
},
/* @private */
_popUp: function(e)
{
e.stop(); // to prevent 'onoutsideclick' to fire immediately
// toggle visibility
if (Franson.DOM.isBlockVisible(this.id))
this.hide()
else
this.show();
},
_createRows: function()
{
forEach(this._events, disconnect);
this._events = [];
MochiKit.DOM.replaceChildNodes($(this.id).firstChild,
MochiKit.Base.map(
method(this, function(index_menuRow)
{
var index = index_menuRow[0];
var menuRow = index_menuRow[1];
return MochiKit.DOM.TR({
'class': 'clockRow' // use popupclock's CSS for now (why doesn't mouseover/hover work?)
},
method(this, function()
{
var col0 = null;
switch (this._mode)
{
case 'radio':
col0 = MochiKit.DOM.INPUT({
'type': 'radio',
'id': 'popupMenuRb_' + this.id + '_' + index,
'name': 'popupMenuStateGroupRb_' + this.id
});
if (menuRow.state)
col0.defaultChecked = menuRow.state; // ok?
this._events.push(connect(col0, 'onclick', this, function(e)
{
e.stopPropagation(); // to allow seeing the change
for (var i = 0; i < this._menuContent.length; ++i)
this._menuContent[i].state = false;
this._menuContent[index].state = e.src().checked;
signal(this, 'onselect', this._menuContent);
}));
break;
case 'checkbox':
col0 = MochiKit.DOM.INPUT({
'id': 'popupMenuCb_' + this.id + '_' + index,
'type': 'checkbox',
'name': 'popupMenuStateGroupCb_' + this.id
});
col0.defaultChecked = menuRow.state; // why can't this be set directly above? (browser bug? or Mochi?)
this._events.push(connect(col0, 'onclick', this, function(e)
{
e.stopPropagation(); // to allow seeing the change
this._menuContent[index].state = e.src().checked;
signal(this, 'onselect', this._menuContent);
}));
break;
case 'stateless':
default:
col0 = null;
break;
}
try // IE leak trick
{
return [
MochiKit.DOM.TD({'style': { 'padding': '2px 5px 0px 2px' }},
col0
),
MochiKit.DOM.TD({'style': { 'padding': '2px 5px 0px 2px' }},
menuRow.text
)
];
}
finally
{
col0 = null; // prevent IE leak
}
})
) // TR
}),
MochiKit.Iter.izip(
MochiKit.Iter.count(),
this._menuContent
)
)
);
},
destroy: function()
{
forEach(this._events, disconnect);
this._events = null;
disconnectAll(this);
MochiKit.DOM.removeElement(this.id);
},
setMenuContent: function(menuContent)
{
this._menuContent = menuContent;
},
getMenuContent: function()
{
return this._menuContent;
},
/**
* only run once!
* @method attach
* @param {DOM} element
*/
attach: function(element)
{
this._targetElem = element;
var menuTable = MochiKit.DOM.TABLE({
'id': this.id,
'style': {
// todo: move to CSS..
'position': 'absolute', // or use relative? (we're set as child of element anyway, take an offsetPos as param)
'z-index': 100,
'background': '#ffffff',
'border': '1px solid #777',
'class': 'clockTable', // use popupclock's CSS for now
'cellspacing': 1,
'cellpadding': 1,
'cursor': 'default', // ok?
'display': 'none' // initially hidden
}},
MochiKit.DOM.TBODY(null//,
//this._createRows()
) // TBODY
);
var body = Franson.DOM.body();
body.insertBefore(menuTable, body.firstChild);
this._createRows();
this._attachEvents();
},
// currently assumed to be called after _createRows
_attachEvents: function()
{
MochiKit.Base.extend(this._events, [
connect(this._targetElem, 'onclick', this, this._popUp),
connect(this.id, 'onclick', this, function(e)
{
this.hide();
signal(this, 'onselect', this._menuContent);
}),
Franson.Event.onOutsideClick(this.id, method(this, function(e)
{
if (e.target() != $(this._targetElem))
this.hide();
})),
connect(document, 'onkeydown', this, function(e)
{
if (e.key().string == 'KEY_ESCAPE') // add more keys? tab?
this.hide();
})
]);
},
//-----
_saveState: function()
{
return {
id: this.id,
mode: this._mode,
rows: this._menuContent
};
},
_restoreState: function(state)
{
if (this._mode == state.mode)
{
forEach(state.rows, function(rowState)
{
var found = false;
forEach(this._menuContent, function(row)
{
if (row.text == rowState.text) // would be nice to be able to use the pharse-keys here...
{
found = true;
row.state = rowState.state;
throw MochiKit.Iter.StopIteration; // break
}
}, this);
if (!found)
{
logWarning('Matching row state not found');
}
}, this);
// .. should rather create a refresh() (shouldn't rely on destroy keeping stuff.. (in this case targetElem)
if (this._targetElem != null)
{
this.attach(this._targetElem);
signal(this, 'onselect', this._menuContent); // notify observers about new state (always fire?)
}
}
}
};