ares-ide
Version:
A browser-based code editor and UI designer for Enyo 2 projects
594 lines (513 loc) • 13.3 kB
JavaScript
/*global enyo */
/**
enyo.TabBar is a scrolled set of radio buttons that is used by TabPanels. This bar may
be used by other kinds to provide a similar layout. By default, a tap on a tab will
immediately switch tab and fire a "onTabChanged" event.
Here's an example:
enyo.kind({
name: "myStuff"
});
enyo.kind({
name: "App",
fit: true,
components: [
{name:"bar",kind: "onyx.TabBar"},
{kind: "MyStuff"}
],
handlers: {
onTabChanged: "switchStuff"
},
rendered: function() {
this.inherited(arguments);
this.$.bar.addTab(
{
'caption': 'greetings',
'data' : { 'msg': 'Hello World !' } // arbitrary user data
}
) ;
},
switchStuff: function(inSender,inEvent) {
this.log("Tapped tab with caption "+ inEvent.caption
+ " and message " + inEvent.data.msg );
}
});
Tabs must be created after construction, i.e. in rendered function.
If tabs are created in 'create' function, the last created tabs will
not be selected.
You can also setup the TabBar so a tap on a tab will fire a
"onTabChangeRequest" event:
enyo.kind({
name: "App",
fit: true,
components: [
{name:"bar",kind: "onyx.TabBar", checkBeforeChanging: true },
{kind: "MyStuff"}
],
handlers: {
onTabChangeRequest: "switchStuff"
},
// same rendered function as above
switchStuff: function(inSender,inEvent) {
this.log("Tapped tab with caption "+ inEvent.caption
+ " and message " + inEvent.data.msg );
// do switch
inEvent.next();
}
});
In this mode, no event is firedt *after* the actual switch.
*/
enyo.kind ({
name: 'onyx.TabBar',
kind: "enyo.FittableColumns",
isPanel: true,
classes: "onyx-tab-bar",
checkBeforeClosing: false,
checkBeforeChanging: false,
debug: false,
events: {
/**
Fired when a tab different from the one currently selected is tapped.
inEvent contains :
{
'index': 3, // index of tab in tab bar
'userId': 1234, // unique id in tab managed by user
'caption': 'bar.js', // tab label
'data': { 'lang': 'javascript' },
'next': callback // call with error message if problem
}
*/
onTabChanged: "",
/**
Fired when a tab different from the one currently selected is tapped
when checkBeforeChanging is true.
inEvent contains the same structure as onTabChanged event. Call next()
when the tab change can be completed.
*/
onTabChangeRequested: "",
/**
* Fired when a tab is about to be removed. inEvent
* contains the same data as onTabChanged.
*
* if (removeOk) { inEvent.next() ;}
* else ( inEvent.next('not now') ;}
*
*/
onTabRemoveRequested: "",
/**
* Fired when a tab is removed. inEvent contains the same
* data as onTabChanged (minus the next callback)
*/
onTabRemoved: ""
},
/**
* Set a maximum height for the scrollable menu that can be raised on the right of
* the tab bar.
*/
published: {
maxMenuHeight: 600
},
handlers: {
onShowTooltip: "showTooltip",
onHideTooltip: "hideTooltip"
},
components: [
{
fit:true,
components: [
{
name: "scroller",
kind: "enyo.Scroller",
maxHeight: "100px",
touch: true,
thumb: false,
vertical: "hidden",
horizontal: "auto",
classes: "onyx-tab-bar-scroller",
components: [
{
classes: "onyx-tab-wrapper",
components: [
{
// double level of components is required to add padding
// at this level. This avoid "> div" in selectors
components: [
{
name: "tabs",
classes: 'onyx-tab-holder',
kind: "onyx.RadioGroup",
defaultKind: "onyx.TabBar.Item",
style: "text-align: left; white-space: nowrap;",
onTabCloseRequest: "requestTabClose",
onTabSwitchRequest: 'requestTabSwitch'
},
{ classes: "onyx-tab-line"},
{ classes: "onyx-tab-rug"}
]
}
]
}
]
},
{kind: "onyx.TooltipDecorator", components:[
{kind: "onyx.Tooltip", classes: "onyx-tab-tooltip"}
]}
]
},
{
kind: "onyx.MenuDecorator",
name: "tabPicker",
onSelect: "popupButtonTapped",
components: [
{
kind: "onyx.IconButton",
classes: "onyx-more-button",
ontap: "showPopupAtEvent"
},
{
kind: "onyx.Menu",
name: "popup"
}
]
}
],
// lastIndex is required to avoid duplicate index in the tab bar.
lastIndex: 0,
//* @protected
clientTransitionStart: function(inSender, inEvent) {
var i = inEvent.toIndex;
var t = this.$.tabs.getClientControls()[i];
if (t && t.hasNode()) {
this.$.tabs.setActive(t);
var tn = t.node;
var tl = tn.offsetLeft;
var tr = tl + tn.offsetWidth;
var sb = this.$.scroller.getScrollBounds();
if (tr < sb.left || tr > sb.left + sb.clientWidth) {
this.$.scroller.scrollToControl(t);
}
}
return true;
},
create: function () {
this.inherited(arguments);
this.maxMenuHeightChanged();
},
maxMenuHeightChanged: function() {
this.$.popup.setMaxHeight(this.getMaxMenuHeight());
},
rendered: function() {
this.inherited(arguments);
this.resetWidth();
},
//* @public
/**
*
* Append a new tab to the tab bar. inControl is an object
* with optional caption and data attributes. When not
* specified the tab will have a generated caption like
* 'Tab 0', 'Tab 1'. etc... data is an arbitrary object that will
* be given back with onTabChanged events
*
*/
addTab: function(inControl) {
var c = inControl.caption || ("Tab " + this.lastIndex);
this.selectedId = this.lastIndex++ ;
var t = this.$.tabs.createComponent(
{
content: c,
userData: inControl.data || { },
tooltipMsg: inControl.tooltipMsg, //may be null
userId: inControl.userId, // may be null
tabIndex: this.selectedId,
addBefore: this.$.line
}
);
t.render();
this.resetWidth();
t.raise();
t.setActive(true);
return t;
},
//* @public
/**
*
* Remove a tab from the tab bar. target is an object with
* either a caption attribute or an index. The tab(s) matching
* the caption will be destroyed or the tab with matching
* index will be destroyed.
*
* Example:
myTab.removeTab({'index':0}); // remove the leftmost tab
myTab.removeTab({'caption':'foo.js'});
*/
removeTab: function(target) {
var tab = this.resolveTab(target,'removeTab');
if (! tab) { return; }
var activeTab = this.$.tabs.active ;
var keepActiveTab = activeTab !== tab ;
var gonerIndex = tab.indexInContainer();
var tabData = {
index: tab.tabIndex,
caption: tab.content,
userId: tab.userId,
data: tab.userData
} ;
tab.destroy();
this.resetWidth();
var ctrls = this.$.tabs.controls;
var ctrlLength = ctrls.length ;
var replacementTab
= keepActiveTab ? activeTab
: gonerIndex < ctrlLength ? ctrls[gonerIndex]
: ctrls[ ctrlLength - 1 ];
// replacementTab may be undef if all tabs were removed
if (replacementTab) {
replacementTab.setActive(true) ;
replacementTab.raise();
this.$.scroller.scrollIntoView(replacementTab);
this.doTabChanged(
{
index: replacementTab.index,
caption: replacementTab.caption,
tooltipMsg: replacementTab.tooltipMsg,
data: replacementTab.userData,
userId: replacementTab.userId
}
);
}
this.doTabRemoved(tabData);
},
//* @public
/**
*
* Request to remove a tab from the tab bar. This is a bit
* like removeTab, except that a onTabRemoveRequested event is
* fired to let the application the possibility to cancel the
* request.
*
*/
requestRemoveTab: function(target) {
var tab = this.resolveTab(target,'removeTab');
var tabData = {
index: tab.tabIndex,
caption: tab.content,
tooltipMsg: tab.tooltipMsg,
userId: tab.userId,
data: tab.userData
} ;
var that = this ;
if (tab) {
tabData.next = function(err) {
if (err) { throw new Error(err); }
else { that.removeTab(target); }
} ;
this.doTabRemoveRequested( tabData ) ;
}
},
//* @protected
resolveTab: function(target,action_name){
var targetTab ;
if (target.userId) {
enyo.forEach(
this.$.tabs.controls,
function(tab){
if (tab.userId === target.userId) {
targetTab = tab;
}
}
);
}
else if (target.caption) {
enyo.forEach(
this.$.tabs.controls,
function(tab){
if (tab.content === target.caption) {
targetTab = tab;
}
}
);
}
else if (typeof target.index !== 'undefined') {
enyo.forEach(
this.$.tabs.controls,
function(tab){
if (tab.tabIndex === target.index) {
targetTab = tab;
}
}
);
}
else {
throw new Error("internal: " + action_name+ " called without index or caption");
}
return targetTab ;
},
//@ protected
requestTabClose: function(inSender,inEvent) {
if (this.checkBeforeClosing) {
this.requestRemoveTab(inEvent) ;
}
else {
this.removeTab(inEvent);
}
},
//* @public
/**
*
* Activate a tab in the tab bar. target is an object with
* either a caption attribute or an index. The tab(s) matching
* the caption will be activated or the tab with matching
* index will be activated
*
* Example:
myTab.activate({'index':0}); // activate the leftmost tab
myTab.activate({'caption':'foo.js'});
* Note that tabActivated event will be fired.
*
*/
activate: function(target) {
var tab = this.resolveTab(target,'activate');
if (tab) {
this.raiseTab(tab);
}
},
raiseTab: function(tab) {
tab.setActive(true) ;
this.$.scroller.scrollIntoView(tab);
},
//@ protected
requestTabSwitch: function(inSender,inEvent) {
var event, next;
var tab = inEvent.originator;
if (this.checkBeforeChanging) {
// polite mode, ask before
event = 'onTabChangeRequested';
// then change the tab
next = enyo.bind(tab, tab.setActiveTrue);
} else {
// rough mode, change the tab
tab.setActiveTrue();
event = 'onTabChanged';
// and then undo if necessary
next = enyo.bind(this,'undoSwitchOnError', oldIndex);
}
var oldIndex = this.selectedId ;
this.selectedId = inEvent.index;
if ( this.selectedId != oldIndex ) {
this.bubble(
event,
{
index: inEvent.index,
caption: inEvent.caption,
tooltipMsg: inEvent.tooltipMsg,
data: inEvent.userData,
userId: inEvent.userId,
next: next
}
);
}
else {
// when clicking on a tab, the tab always deactivated even
// if user clicks on the active tab. So the activation
// must be put back.
tab.setActiveTrue();
}
return true;
},
showTooltip: function(inSender, inEvent) {
var t = inEvent.tooltipContent;
var bounds = inEvent.bounds;
if(t){
if(!this.$.tooltip.showing){
this.$.tooltip.setContent(t);
var leftSpace = bounds.left + ( bounds.width / 2 );
this.$.tooltipDecorator.applyStyle("left", leftSpace + "px");
this.$.tooltip.show();
}
}
return true ;
},
hideTooltip: function() {
this.$.tooltip.hide();
return true ;
},
//* @protected
undoSwitchOnError: function(oldIndex, err) {
if (err) {
this.activate({ 'index': oldIndex } ) ;
}
},
// resize stuff:
// use scroller's getScrollBounds to get scroll boundaries
resizeHandler: function() {
this.inherited(arguments);
this.adjustTabWidth() ;
},
// compute tab width by adding width of tabs contained in tab bar.
computeOrigTabWidth: function() {
var result = 0;
enyo.forEach(
this.$.tabs.getControls(),
function(tab){
var w = tab.origWidth() ;
// must add margin and padding of inner button and outer tab-item
result += w + 18 ;
}
);
return result;
},
origTabWidth: null,
adjustTabWidth: function(inSender, inEvent) {
var scrolledWidth = this.$.scroller.getBounds().width;
var tabsWidth = this.origTabWidth ;
var coeff = scrolledWidth > tabsWidth ? 1 : scrolledWidth / tabsWidth ;
coeff = coeff < 0.5 ? 0.5 : coeff;
this.applyCoeff(coeff) ;
},
applyCoeff: function(coeff) {
enyo.forEach(
this.$.tabs.getControls(),
function(tab){
tab.reduce(coeff) ;
}
);
},
resetWidth: function() {
this.applyCoeff(1) ; // restore original size to all tabs
this.origTabWidth = this.computeOrigTabWidth(); // measure tab width
this.adjustTabWidth();
},
isEmpty: function() {
return ! this.$.tabs.getControls().length ;
},
// Since action buttons of Contextual Popups are not dynamic, this
// kind is created on the fly and destroyed once the user clicks
// on a button
showPopupAtEvent: function(inSender, inEvent) {
var that = this ;
var popup = this.$.popup;
for (var name in popup.$) {
if (popup.$.hasOwnProperty(name) && /menuItem/.test(name)) {
popup.$[name].destroy();
}
}
//popup.render();
enyo.forEach(
this.$.tabs.getControls(),
function(tab){
that.$.popup.createComponent({
content: tab.content,
value: tab.tabIndex
}) ;
}
);
popup.maxHeightChanged();
popup.showAtPosition({top: 30, right:30});
this.render();
this.resized(); // required for IE10 to work correctly
return ;
},
popupButtonTapped: function(inSender, inEvent) {
this.activate({ index: inEvent.originator.value } );
}
});