var AIRTIME = (function (AIRTIME) { /** * AIRTIME module namespace object */ var mod, /** * Tab counter to use as unique tab IDs that can be * retrieved from the DOM * * @type {number} */ $tabCount = 0, /** * Map of Tab IDs (by tabCount) to object UIDs so * Tabs can be referenced either by ID (from the DOM) * or by UID (from object data) * * @type {{}} */ $tabMap = {}, /** * Map of object UIDs to currently open Tab objects * * @type {{}} */ $openTabs = {}, /** * The currently active (open) Tab object * * @type {Tab} */ $activeTab, /** * Singleton object used to reference the schedule tab * * @type {ScheduleTab} */ $scheduleTab; if (AIRTIME.tabs === undefined) { AIRTIME.tabs = {}; } mod = AIRTIME.tabs; /* ##################################################### Object Initialization and Functions ##################################################### */ /** * Tab object constructor * * @param {string} html the HTML to render as the tab contents * @param {string} uid the unique ID for the tab. Uses the values in * AIRTIME.library.MediaTypeStringEnum and the object ID * to create a string of the form TYPE_ID. * @returns {Tab} the created Tab object * @constructor */ var Tab = function (html, uid) { var self = this; AIRTIME.library.selectNone(); var existingTab = $openTabs[uid]; if (existingTab) { existingTab.switchTo(); return existingTab; } self.id = ++$tabCount; self.uid = uid; // TODO: clean this up a bit and use js instead of strings to create elements var wrapper = "
", t = $("#show_builder") .append(wrapper) .find("#pl-tab-content-" + self.id), pane = $(".editor_pane_wrapper:last").append(html), name = pane.find("#track_title").length > 0 ? pane.find("#track_title").val() + $.i18n._(" - Metadata Editor") : pane.find(".playlist_name_display").val(), tab = "", tabs = $(".nav.nav-tabs"); $(".nav.nav-tabs li").removeClass("active"); tabs.append(tab); var newTab = $("#pl-tab-" + self.id); self.wrapper = pane; self.contents = t; self.tab = newTab; $openTabs[uid] = self; $tabMap[self.id] = uid; self._init(); self.switchTo(); return self; }; /** * Private initialization function for Tab objects * * Assigns default action handlers to the tab DOM element * * @private */ Tab.prototype._init = function () { var self = this; self.assignTabClickHandler(function (e) { if (!$(this).hasClass("active")) { self.switchTo(); } }); self.assignTabCloseClickHandler(function (e) { e.preventDefault(); e.stopPropagation(); $(this).unbind("click"); // Prevent repeated clicks in quick succession from closing multiple tabs // We need to update the text on the add button AIRTIME.library.checkAddButton(); // We also need to run the draw callback to update how dragged items are drawn AIRTIME.library.fnDrawCallback(); self.close(); }); self.contents.on("click", ".toggle-editor-form", function (event) { self.contents.find(".inner_editor_wrapper").slideToggle(200); var buttonIcon = $(this).find(".icon-white"); buttonIcon.toggleClass("icon-chevron-up"); buttonIcon.toggleClass("icon-chevron-down"); }); }; /** * Internal destructor. Can be assigned via assignOnCloseHandler * * @private */ Tab.prototype._destroy = function () {}; /** * Assign the given function f as the click handler for the tab * * @param {function} f the function to call when the tab is clicked */ Tab.prototype.assignTabClickHandler = function (f) { var self = this; self.tab.unbind("click").on("click", function (e) { // Always close on middle mouse press if (e.which == 2) { // Simulate a click on the close tab button so any // additional on-close behaviour is executed self.tab.find(".lib_pl_close").click(); return; } f(); }); }; /** * Assign the given function f as the click handler for the tab close button * * @param {function} f the function to call when the tab's close button is clicked */ Tab.prototype.assignTabCloseClickHandler = function (f) { this.tab.find(".lib_pl_close").unbind("click").click(f); }; /** * Assign an implicit destructor * * @param {function} fn function to run when this Tab is destroyed */ Tab.prototype.assignOnCloseHandler = function (fn) { this._destroy = fn; }; /** * Open this tab in the right-hand pane and set it as the currently active tab */ Tab.prototype.switchTo = function () { var self = this; $activeTab.contents.hide().removeClass("active-tab"); self.contents.addClass("active-tab").show(); $activeTab.tab.removeClass("active"); self.tab.addClass("active"); mod.updateActiveTab(); // In case we're adding a tab that wraps to the next row // It's better to call this here so we don't have to call it in multiple places mod.onResize(); return this; // For chaining }; /** * Close the tab. Switches to the nearest open tab, prioritizing the * more recent (rightmost) tabs */ Tab.prototype.close = function () { var self = this; var ascTabs = Object.keys($openTabs).sort(function (a, b) { return a - b; }), pos = ascTabs.indexOf(self.uid), toTab = pos < ascTabs.length - 1 ? $openTabs[ascTabs[++pos]] : $openTabs[ascTabs[--pos]]; delete $openTabs[self.uid]; // Remove this tab from the open tab array delete $tabMap[self.id]; // Remove this tab from the internal tab mapping // Remove the relevant DOM elements (the tab and its contents) if (self.uid !== 0) { self.tab.remove(); self.contents.remove(); } else { // only hide scheduled shows tab so we can still interact with it. self.tab.hide(); self.contents.hide(); } if (self.isActive() && toTab) { // Closing the current tab, otherwise we don't need to switch tabs toTab.switchTo(); } else { mod.onResize(); } if (Object.keys($openTabs).length < 1) { $("#show_builder").hide(); } self._destroy(); }; /** * Set the visible Tab name to the given string * * @param {string} name the name to set */ Tab.prototype.setName = function (name) { this.tab.find(".tab-name").text(name); return this; // For chaining }; /** * Check if the Tab object is the currently active (open) Tab * * @returns {boolean} true if the Tab is the currently active Tab */ Tab.prototype.isActive = function () { return this.contents.get(0) == $activeTab.contents.get(0); }; /** * ScheduledTab object constructor * * The schedule tab is present in the DOM already on load, and we * need to be able to reference it in the same way as other tabs * (to avoid duplication and confusion) so we define it statically * * @constructor */ var ScheduleTab = function () { var self = this, uid = 0, tab = $("#schedule-tab"), pane = $("#show_builder"), contents = pane.find(".outer-datatable-wrapper"); self.id = 0; self.uid = uid; tab.data("tab-id", self.id); self.wrapper = pane; self.contents = contents; self.tab = tab; self.assignTabClickHandler(function (e) { if (!self.isActive()) { self.switchTo(); } }); self.assignTabCloseClickHandler(function (e) { self.close(); }); $openTabs[uid] = self; $tabMap[self.id] = uid; }; /** * Subclass the Tab object * @type {Tab} */ ScheduleTab.prototype = Object.create(Tab.prototype); ScheduleTab.prototype.constructor = ScheduleTab; /* ##################################################### Module Functions ##################################################### */ /** * Initialize the singleton ScheduleTab object on startup */ mod.initScheduleTab = function () { $scheduleTab = new ScheduleTab(); $activeTab = $scheduleTab; }; /** * Create a new Tab object and open it in the ShowBuilder pane * * @param {string} html the HTML to render as the tab contents * @param {string} uid the unique ID for the tab. Uses the values in * AIRTIME.library.MediaTypeStringEnum and the object ID * @param {function} callback an optional callback function to call once the * Tab object is initialized * @returns {Tab} the created Tab object */ mod.openTab = function (html, uid, callback) { $("#show_builder").show(); var newTab = new Tab(html, uid); if (callback) callback(newTab); return newTab; }; /** * open the schedule tab if if was closed * * @returns {Tab} */ mod.openScheduleTab = function () { var $scheduleTab = this.getScheduleTab(); $("#show_builder").show(); $openTabs[0] = $scheduleTab; $scheduleTab.tab.show(); $scheduleTab.contents.show(); $scheduleTab.switchTo(); $scheduleTab.assignTabCloseClickHandler(function (e) { $scheduleTab.close(); }); }; /** * Updates the currently active tab * * Called when the user switches tabs for any reason * * NOTE: this function updates the currently active playlist * as a side-effect, which is necessary for playlist tabs * but not for other types of tabs... would be good to * get rid of this dependency at some point */ mod.updateActiveTab = function () { var t = $(".nav.nav-tabs .active"); $activeTab = mod.get(t.data("tab-id")); if (!$activeTab) $activeTab = $scheduleTab; if ($activeTab.contents.hasClass("pl-content")) { AIRTIME.playlist.setCurrent($activeTab.contents); } }; /** * Get the ScheduleTab object * * @returns {ScheduleTab} */ mod.getScheduleTab = function () { return $scheduleTab; }; /** * Get the currently active (open) Tab object * * @returns {Tab} the currently active tab */ mod.getActiveTab = function () { return $activeTab; }; /** * Given a tab id, get the corresponding Tab object * * @param {int|string} id the tab or object ID of the Tab to retrieve * @returns {Tab|undefined} the Tab object with the given ID, or undefined * if no Tab with the given ID exists */ mod.get = function (id) { return $.isNumeric(id) ? $openTabs[$tabMap[id]] : $openTabs[id]; }; /** * Adjust the margins on the right-hand pane when we have multiple rows of tabs */ mod.onResize = function () { var h = $(".panel-header .nav").height(); $(".pl-content").css("margin-top", h + 5); // 8px extra for padding $("#show_builder_table_wrapper").css("top", h + 5); }; /** * Expose the Tab object so it can be subclassed * * @type {Function} */ mod.Tab = Tab; return AIRTIME; })(AIRTIME || {}); $(document).ready(function () { var sb = $("#show_builder"); // Add text scrolling to tab names sb.addTitles(".tab-name"); sb.find(".nav.nav-tabs").sortable({ containment: "parent", distance: 25, }); // Initialize the ScheduleTab AIRTIME.tabs.initScheduleTab(); }); $(window).resize(AIRTIME.tabs.onResize);