/* global Marionette, Handlebars */
/**
*
* @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*
*/
(function(OCA, Marionette, Handlebars) {
'use strict';
OCA.SpreedMe = OCA.SpreedMe || {};
OCA.SpreedMe.Views = OCA.SpreedMe.Views || {};
var TEMPLATE_TAB_HEADER_VIEW =
'{{label}}';
var TEMPLATE_TAB_VIEW =
'
' +
'';
var TabHeaderView = Marionette.View.extend({
tagName: 'li',
className: 'tabHeader',
template: Handlebars.compile(TEMPLATE_TAB_HEADER_VIEW),
templateContext: function() {
return {
label: this.getOption('label')
};
},
events: {
'click': function() {
this.triggerMethod('click:tabHeader', this.getOption('tabId'));
}
},
setSelected: function(selected) {
if (selected) {
this.$el.addClass('selected');
} else {
this.$el.removeClass('selected');
}
}
});
var TabHeadersView = Marionette.View.extend({
tagName: 'ul',
className: 'tabHeaders',
// The tab headers are added dynamically using regions, so there is
// nothing to be rendered with a template.
template: _.noop,
childViewTriggers: {
// Propagate the event to the parent view.
'click:tabHeader': 'click:tabHeader'
},
addTabHeader: function(tabId, tabHeaderOptions) {
var tabHeaderId = 'tabHeader' + tabId.charAt(0).toUpperCase() + tabId.substr(1);
tabHeaderOptions.id = tabHeaderId;
// The "tabId" will be passed by the TabHeaderView when triggering
// "click:tabHeader" events.
tabHeaderOptions.tabId = tabId;
tabHeaderOptions.priority = tabHeaderOptions.priority || 0;
var tabHeaderView = new TabHeaderView(tabHeaderOptions);
var tabHeaderIndex = this._getIndexForTabHeaderPriority(tabHeaderOptions.priority);
// When adding a region and showing a view on it the target element
// of the region must exist in the parent view. Therefore, a dummy
// target element, which will be replaced with the tab header
// itself, has to be added to the parent view.
var dummyElement = '';
if (tabHeaderIndex === 0) {
this.$el.prepend(dummyElement);
} else {
// When two tab headers have the same priority the new one is
// added after the existing one.
this.$el.children().eq(tabHeaderIndex-1).after(dummyElement);
}
this.addRegion(tabId, { el: '#' + tabHeaderId, replaceElement: true });
this.showChildView(tabId, tabHeaderView);
},
/**
* Return the insertion index for a tab header based on its priority.
*
* Tab headers with higher priorities go before tab headers with lower
* priorities; if the priority is the same as one or more of the current
* tab headers the new tab header goes after the last of them.
*
* @param int priority the priority to get its insertion index.
* @return int the insertion index.
*/
_getIndexForTabHeaderPriority: function(priority) {
// this.getRegions() returns an object that acts as a map, but it
// has no "length" property; _.map creates an array, thus ensuring
// that there is a "length" property to know the current number of
// tab headers.
var currentPriorities = _.map(this.getRegions(), function(region) {
return region.currentView.getOption('priority');
});
// By default sort() converts the values to strings and sorts them
// in ascending order using their Unicode value; a custom function
// must be used to sort them by their numerical value instead.
currentPriorities.sort(function(a, b) {
return a - b;
}).reverse();
var index = _.findIndex(currentPriorities, function(currentPriority) {
return priority > currentPriority;
});
if (index === -1) {
return currentPriorities.length;
}
return index;
},
selectTabHeader: function(tabId) {
if (this._currentTabId !== undefined) {
this.getChildView(this._currentTabId).setSelected(false);
}
this._currentTabId = tabId;
this.getChildView(this._currentTabId).setSelected(true);
}
});
/**
* View for tabs (headers and content).
*
* A TabView contains a set of tab headers and a content area. When a header
* is selected its associated content view is shown in the content area;
* otherwise its content is hidden (although the header is always shown).
*/
var TabView = Marionette.View.extend({
tagName: 'div',
className: 'tabs',
regions: {
tabHeaders: '.tabHeaders',
tabContent: '.tab'
},
template: Handlebars.compile(TEMPLATE_TAB_VIEW),
initialize: function() {
this._tabHeadersView = null;
this._tabContentViews = {};
},
onDestroy: function() {
_.each(this._tabContentViews, function(tabContentView) {
// Explicitly destroy all the tab content views, as some of them
// may be detached from the TabView.
tabContentView.destroy();
});
},
/**
* Adds a new tab.
*
* The tabHeaderOptions must provide a 'label' string which will be
* rendered as the tab header. Optionally, it can provide a 'priority'
* integer to set the order of the tab header with respect to the other
* tab headers (tabs with higher priorities appear before tabs with
* lower priorities; tabs with the same priority are sorted based on
* their insertion order); if it is not explicitly set the value 0 is
* used. If needed, the tabHeaderOptions can provide other values that
* will override the default TabHeaderView properties (for example, it
* can provide an 'onRender' function to extend the default rendering of
* the header).
*
* The TabView takes ownership of the given content view, and it will
* destroy it when the TabView is destroyed.
*
* @param string tabId the ID of the tab.
* @param Object tabHeaderOptions the options for the constructor of the
* TabHeaderView that will be added as the header of the tab.
* @param Marionette.View tabContentView the View to be shown when the
* tab is selected.
*/
addTab: function(tabId, tabHeaderOptions, tabContentView) {
if (this._tabHeadersView === null) {
this._tabHeadersView = new TabHeadersView();
this.showChildView('tabHeaders', this._tabHeadersView, { replaceElement: true });
}
this._tabHeadersView.addTabHeader(tabId, tabHeaderOptions);
this._tabContentViews[tabId] = tabContentView;
if (Object.keys(this._tabContentViews).length === 1) {
this.selectTab(tabId);
}
},
onChildviewClickTabHeader: function(tabId) {
this.selectTab(tabId);
},
/**
* Select the tab associated to the given tabId.
*
* @param string tabId the ID of the tab to select.
*/
selectTab: function(tabId) {
if (!this._tabContentViews.hasOwnProperty(tabId)) {
return;
}
this._tabHeadersView.selectTabHeader(tabId);
// With Marionette 3.1 "this.detachChildView('tabContent')" would be
// used instead of the "preventDestroy" option.
this.showChildView('tabContent', this._tabContentViews[tabId], { preventDestroy: true } );
}
});
OCA.SpreedMe.Views.TabView = TabView;
})(OCA, Marionette, Handlebars);