From 44a5e5a240d761c75017b1f964b75693079dd4dd Mon Sep 17 00:00:00 2001 From: Duncan Sommerville Date: Tue, 22 Sep 2015 17:34:24 -0400 Subject: [PATCH] Improve tabs functionality and add comments --- .../controllers/LibraryController.php | 1 - .../public/js/airtime/library/library.js | 2 +- .../public/js/airtime/library/podcast.js | 25 +- airtime_mvc/public/js/airtime/library/spl.js | 82 +++- .../public/js/airtime/showbuilder/tabs.js | 355 +++++++++++------- 5 files changed, 291 insertions(+), 174 deletions(-) diff --git a/airtime_mvc/application/controllers/LibraryController.php b/airtime_mvc/application/controllers/LibraryController.php index ba5a56d2c..2e3061a1c 100644 --- a/airtime_mvc/application/controllers/LibraryController.php +++ b/airtime_mvc/application/controllers/LibraryController.php @@ -377,7 +377,6 @@ class LibraryController extends Zend_Controller_Action $this->view->form = $form; $this->view->id = $file_id; $this->view->title = $file->getPropelOrm()->getDbTrackTitle(); - $this->view->type = "md"; $this->view->html = $this->view->render('library/edit-file-md.phtml'); } diff --git a/airtime_mvc/public/js/airtime/library/library.js b/airtime_mvc/public/js/airtime/library/library.js index 65494fa69..dafa6e7b8 100644 --- a/airtime_mvc/public/js/airtime/library/library.js +++ b/airtime_mvc/public/js/airtime/library/library.js @@ -456,7 +456,7 @@ var AIRTIME = (function(AIRTIME) { openTabObjectIds.each(function(i, el) { var v = parseInt($(el).val()); if ($.inArray(v, mediaIds) > -1) { - AIRTIME.tabs.closeTab($(el).closest(".pl-content").attr("data-tab-id")); + AIRTIME.tabs.get($(el).closest(".pl-content").attr("data-tab-id")).close(); } }); diff --git a/airtime_mvc/public/js/airtime/library/podcast.js b/airtime_mvc/public/js/airtime/library/podcast.js index 2f9efc396..5ec3be2a4 100644 --- a/airtime_mvc/public/js/airtime/library/podcast.js +++ b/airtime_mvc/public/js/airtime/library/podcast.js @@ -1,9 +1,9 @@ var endpoint = 'rest/podcast/'; var podcastApp = angular.module('podcast', []) - .controller('RestController', function($scope, $http, podcast) { + .controller('RestController', function($scope, $http, podcast, tab) { $scope.podcast = podcast; - AIRTIME.tabs.setActiveTabName($scope.podcast.title); + tab.setName($scope.podcast.title); $scope.savePodcast = function() { $http.put(endpoint + $scope.podcast.id, { csrf_token: jQuery("#csrf").val(), podcast: $scope.podcast }) @@ -13,7 +13,7 @@ var podcastApp = angular.module('podcast', []) }; $scope.discard = function() { - AIRTIME.tabs.getActiveTab().close(); + tab.close(); $scope.podcast = {}; }; }); @@ -39,9 +39,10 @@ var AIRTIME = (function (AIRTIME) { $.post(endpoint + "bulk", { csrf_token: $("#csrf").val(), method: method, ids: ids }, callback); } - function _bootstrapAngularApp(podcast) { + function _bootstrapAngularApp(podcast, tab) { podcastApp.value('podcast', JSON.parse(podcast)); - var wrapper = AIRTIME.tabs.getActiveTab().contents.find(".editor_pane_wrapper"); + podcastApp.value('tab', tab); + var wrapper = tab.contents.find(".editor_pane_wrapper"); wrapper.attr("ng-controller", "RestController"); angular.bootstrap(wrapper.get(0), ["podcast"]); } @@ -61,11 +62,10 @@ var AIRTIME = (function (AIRTIME) { mod.addPodcast = function() { $.post(endpoint, $("#podcast_url_dialog").find("form").serialize(), function(json) { - var uid = AIRTIME.library.MediaTypeStringEnum.PODCAST+"_"+json.id; - AIRTIME.tabs.openTab(json, uid); - _bootstrapAngularApp(json.podcast); + var uid = AIRTIME.library.MediaTypeStringEnum.PODCAST+"_"+json.id, + tab = AIRTIME.tabs.openTab(json, uid); + _bootstrapAngularApp(json.podcast, tab); $("#podcast_url_dialog").dialog("close"); - console.log(json); mod.initPodcastEpisodeDatatable(JSON.parse(json.podcast).episodes); }); }; @@ -73,9 +73,9 @@ var AIRTIME = (function (AIRTIME) { mod.editSelectedPodcasts = function() { _bulkAction("GET", function(json) { json.forEach(function(el) { - var uid = AIRTIME.library.MediaTypeStringEnum.PODCAST+"_"+el.id; - AIRTIME.tabs.openTab(el, uid, AIRTIME.podcast.init); - _bootstrapAngularApp(el.podcast); + var uid = AIRTIME.library.MediaTypeStringEnum.PODCAST+"_"+el.id, + tab = AIRTIME.tabs.openTab(el, uid, AIRTIME.podcast.init); + _bootstrapAngularApp(el.podcast, tab); mod.initPodcastEpisodeDatatable(JSON.parse(el.podcast).episodes); }); }); @@ -90,7 +90,6 @@ var AIRTIME = (function (AIRTIME) { }; mod.initPodcastEpisodeDatatable = function(episodes) { - console.log(episodes); var aoColumns = [ /* Title */ { "sTitle" : $.i18n._("Title") , "mDataProp" : "title" , "sClass" : "podcast_episodes_title" , "sWidth" : "170px" }, /* Author */ { "sTitle" : $.i18n._("Author") , "mDataProp" : "author" , "sClass" : "podcast_episodes_author" , "sWidth" : "170px" }, diff --git a/airtime_mvc/public/js/airtime/library/spl.js b/airtime_mvc/public/js/airtime/library/spl.js index 1197536b0..cdce25adb 100644 --- a/airtime_mvc/public/js/airtime/library/spl.js +++ b/airtime_mvc/public/js/airtime/library/spl.js @@ -381,7 +381,7 @@ var AIRTIME = (function(AIRTIME){ setCueEvents(); setFadeEvents(); mod.setModified(json.modified); - AIRTIME.tabs.setActiveTabName(json.name); + AIRTIME.tabs.getActiveTab().setName(json.name); AIRTIME.playlist.validatePlaylistElements(); redrawLib(); @@ -788,7 +788,7 @@ var AIRTIME = (function(AIRTIME){ setTimeout(function(){$status.fadeOut("slow", function(){$status.empty()})}, 5000); $pl.find(".title_obj_name").val(name); - AIRTIME.tabs.setActiveTabName(name); + AIRTIME.tabs.getActiveTab().setName(json.name); var $ws_id = $(".active-tab .obj_id"); $ws_id.attr("value", json.streamId); @@ -889,7 +889,7 @@ var AIRTIME = (function(AIRTIME){ } else { setTitleLabel(json.name); - AIRTIME.tabs.setActiveTabName(json.name); + AIRTIME.tabs.getActiveTab().setName(json.name); mod.setModified(json.modified); if (obj_type == "block") { @@ -1016,6 +1016,56 @@ var AIRTIME = (function(AIRTIME){ AIRTIME.playlist.validatePlaylistElements(); } + mod._initPlaylistTabEvents = function(newTab) { + newTab.assignTabClickHandler(function() { + if (!$(this).hasClass('active')) { + newTab.switchTo(); + $.post(baseUrl+'playlist/edit', { + format: "json", + id: newTab.pane.find(".obj_id").val(), + type: newTab.pane.find(".obj_type").val() + }); + } + }); + + mod.init(); + + // functions in smart_blockbuilder.js + setupUI(); + appendAddButton(); + appendModAddButton(); + removeButtonCheck(); + mod.setFadeIcon(); + }; + + mod._initFileMdEvents = function(newTab) { + newTab.contents.find(".md-cancel").on("click", function() { + newTab.close(); + }); + + newTab.contents.find(".md-save").on("click", function() { + var file_id = newTab.wrapper.find('#file_id').val(), + data = newTab.wrapper.find("#edit-md-dialog form").serializeArray(); + $.post(baseUrl+'library/edit-file-md', {format: "json", id: file_id, data: data}, function() { + // don't redraw the library table if we are on calendar page + // we would be on calendar if viewing recorded file metadata + if ($("#schedule_calendar").length === 0) { + oTable.fnStandingRedraw(); + } + }); + + newTab.close(); + }); + + newTab.wrapper.find('#edit-md-dialog').on("keyup", function(event) { + if (event.keyCode === 13) { + newTab.wrapper.find('.md-save').click(); + } + }); + + mod.setupEventListeners(); + }; + mod.fnNew = function() { var url = baseUrl+'playlist/new'; @@ -1025,7 +1075,7 @@ var AIRTIME = (function(AIRTIME){ {format: "json", type: 'playlist'}, function(json) { var uid = AIRTIME.library.MediaTypeStringEnum.PLAYLIST+"_"+json.id; - AIRTIME.tabs.openPlaylistTab(json, uid); + AIRTIME.tabs.openTab(json, uid, AIRTIME.playlist._initPlaylistTabEvents); redrawLib(); }); }; @@ -1039,7 +1089,7 @@ var AIRTIME = (function(AIRTIME){ {format: "json"}, function(json) { var uid = AIRTIME.library.MediaTypeStringEnum.WEBSTREAM+"_"+json.id; - AIRTIME.tabs.openPlaylistTab(json, uid); + AIRTIME.tabs.openTab(json, uid, AIRTIME.playlist._initPlaylistTabEvents); redrawLib(); }); }; @@ -1054,13 +1104,13 @@ var AIRTIME = (function(AIRTIME){ {format: "json", type: 'block'}, function(json){ var uid = AIRTIME.library.MediaTypeStringEnum.BLOCK+"_"+json.id; - AIRTIME.tabs.openPlaylistTab(json, uid); + AIRTIME.tabs.openTab(json, uid, AIRTIME.playlist._initPlaylistTabEvents); redrawLib(); }); }; mod.fileMdEdit = function(json, uid) { - AIRTIME.tabs.openFileMdEditorTab(json, uid); + AIRTIME.tabs.openTab(json, uid, AIRTIME.playlist._initFileMdEvents); }; mod.fnEdit = function(id, type, url) { @@ -1071,7 +1121,7 @@ var AIRTIME = (function(AIRTIME){ {format: "json", id: id, type: type}, function(json) { var uid = AIRTIME.library.MediaTypeFullToStringEnum.type+"_"+id; - AIRTIME.tabs.openPlaylistTab(json, uid); + AIRTIME.tabs.openTab(json, uid, AIRTIME.playlist._initPlaylistTabEvents); redrawLib(); }); }; @@ -1107,7 +1157,7 @@ var AIRTIME = (function(AIRTIME){ {format: "json", ids: id, modified: lastMod, type: type}, function(json){ var uid = AIRTIME.library.MediaTypeStringEnum.WEBSTREAM+"_"+id; - AIRTIME.tabs.openPlaylistTab(json, uid); + AIRTIME.tabs.openTab(json, uid, AIRTIME.playlist._initPlaylistTabEvents); redrawLib(); }); }; @@ -1155,7 +1205,7 @@ var AIRTIME = (function(AIRTIME){ mod.replaceForm = function(json){ $pl.find('.editor_pane_wrapper').html(json.html); var uid = AIRTIME.library.MediaTypeStringEnum.BLOCK+"_"+json.id; - AIRTIME.tabs.openPlaylistTab(json, uid); + AIRTIME.tabs.openTab(json, uid, AIRTIME.playlist._initPlaylistTabEvents); }; @@ -1432,6 +1482,10 @@ var AIRTIME = (function(AIRTIME){ mod.setCurrent = function(pl) { $pl = pl; + $.post(baseUrl + "playlist/change-playlist", { + "id": mod.getId($pl), + "type": $pl.find('.obj_type').val() + }); }; mod.init = function() { @@ -1485,16 +1539,8 @@ var AIRTIME = (function(AIRTIME){ AIRTIME.playlist.init(); }; - 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); - }; - return AIRTIME; }(AIRTIME || {})); - $(document).ready(AIRTIME.playlist.onReady); -$(window).resize(AIRTIME.playlist.onResize); diff --git a/airtime_mvc/public/js/airtime/showbuilder/tabs.js b/airtime_mvc/public/js/airtime/showbuilder/tabs.js index e205e9975..3e6ef9e67 100644 --- a/airtime_mvc/public/js/airtime/showbuilder/tabs.js +++ b/airtime_mvc/public/js/airtime/showbuilder/tabs.js @@ -1,9 +1,40 @@ 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) { @@ -12,9 +43,22 @@ var AIRTIME = (function(AIRTIME){ mod = AIRTIME.tabs; /* ##################################################### - Internal Functions + Object Initialization and Functions ##################################################### */ + /** + * Tab object constructor + * + * @param {{}} json a javascript object of the form + * { + * '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(json, uid) { var self = this; @@ -28,17 +72,18 @@ var AIRTIME = (function(AIRTIME){ self.id = ++$tabCount; self.uid = uid; - var wrapper = "
", + // 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(json.html), name = pane.find("#track_title").length > 0 ? pane.find("#track_title").val() + $.i18n._(" - Metadata Editor") - : pane.find(".playlist_name_display").val(), + : pane.find(".playlist_name_display").val(), tab = - "", tabs = $(".nav.nav-tabs"); @@ -54,47 +99,18 @@ var AIRTIME = (function(AIRTIME){ $openTabs[uid] = self; $tabMap[self.id] = uid; + self._init(); self.switchTo(); return self; }; - Tab.prototype.switchTo = function() { - _switchTab(this.contents, this.tab); - }; - - Tab.prototype.close = function() { - var self = this; - - var toPane = self.contents.next().length > 0 ? self.contents.next() : self.contents.prev(), - toTab = self.tab.next().length > 0 ? self.tab.next() : self.tab.prev(); - 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) - self.tab.remove(); - self.contents.remove(); - - if (self.isActive()) { // Closing the current tab, otherwise we don't need to switch tabs - _switchTab(toPane, toTab); - } - - // If we close a tab that was causing tabs to wrap to the next row - // we need to resize to change the margin for the tab nav - AIRTIME.playlist.onResize(); - - }; - - Tab.prototype.isActive = function() { - return this.contents.get(0) == $activeTab.contents.get(0); - }; - - Tab.prototype.assignTabClickHandler = function(f) { - this.tab.unbind("click").on("click", f); - }; - Tab.prototype.assignTabCloseClickHandler = function(f) { - this.tab.find(".lib_pl_close").unbind("click").click(f); - }; - + /** + * 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() { @@ -116,6 +132,92 @@ var AIRTIME = (function(AIRTIME){ }); }; + /** + * 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) { + this.tab.unbind("click").on("click", 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); + }; + + /** + * 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(); + AIRTIME.library.fnRedraw(); + }; + + /** + * 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) + self.tab.remove(); + self.contents.remove(); + + if (self.isActive()) { // Closing the current tab, otherwise we don't need to switch tabs + toTab.switchTo(); + } + }; + + /** + * 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); + }; + + /** + * 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"), @@ -124,143 +226,113 @@ var AIRTIME = (function(AIRTIME){ self.id = 0; tab.data("tab-id", self.id); - tab.on("click", function() { - if (!$(this).hasClass('active')) { - _switchTab(contents, $(this)); - } - }); self.wrapper = pane; self.contents = contents; self.tab = tab; + tab.on("click", function() { + if (!$(this).hasClass('active')) { + self.switchTo(); + } + }); + $openTabs[uid] = self; $tabMap[self.id] = uid; }; + /** + * Subclass the Tab object + * @type {Tab} + */ ScheduleTab.prototype = Object.create(Tab.prototype); ScheduleTab.prototype.constructor = ScheduleTab; - function _initFileMdEvents(newTab) { - newTab.contents.find(".md-cancel").on("click", function() { - newTab.close(); - }); - - newTab.contents.find(".md-save").on("click", function() { - var file_id = newTab.wrapper.find('#file_id').val(), - data = newTab.wrapper.find("#edit-md-dialog form").serializeArray(); - $.post(baseUrl+'library/edit-file-md', {format: "json", id: file_id, data: data}, function() { - // don't redraw the library table if we are on calendar page - // we would be on calendar if viewing recorded file metadata - if ($("#schedule_calendar").length === 0) { - oTable.fnStandingRedraw(); - } - }); - - newTab.close(); - }); - - newTab.wrapper.find('#edit-md-dialog').on("keyup", function(event) { - if (event.keyCode === 13) { - newTab.wrapper.find('.md-save').click(); - } - }); - - AIRTIME.playlist.setupEventListeners(); - } - - function _initPlaylistEvents(newTab) { - newTab.assignTabClickHandler(function() { - if (!$(this).hasClass('active')) { - newTab.switchTo(); - $.post(baseUrl+'playlist/edit', { - format: "json", - id: newTab.pane.find(".obj_id").val(), - type: newTab.pane.find(".obj_type").val() - }); - } - }); - - AIRTIME.playlist.init(); - - // functions in smart_blockbuilder.js - setupUI(); - appendAddButton(); - appendModAddButton(); - removeButtonCheck(); - AIRTIME.playlist.setFadeIcon(); - } - - function _switchTab(tabPane, tab) { - $activeTab.contents.hide().removeClass("active-tab"); - tabPane.addClass("active-tab").show(); - - $activeTab.tab.removeClass("active"); - tab.addClass("active"); - - mod.updateActiveTab(); - - AIRTIME.playlist.onResize(); - AIRTIME.library.fnRedraw(); - } - /* ##################################################### External Functions ##################################################### */ + /** + * Initialize the singleton ScheduleTab object on startup + */ mod.init = function() { - $scheduleTab = new ScheduleTab(); - }; - - mod.openFileMdEditorTab = function(json, uid) { - mod.openTab(json, uid, _initFileMdEvents); - }; - - mod.openPlaylistTab = function(json, uid) { - mod.openTab(json, uid, _initPlaylistEvents); + $scheduleTab = Object.freeze(new ScheduleTab()); }; + /** + * Create a new Tab object and open it in the ShowBuilder pane + * + * @param {{}} json a javascript object of the form + * { + * '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(json, uid, callback) { var newTab = new Tab(json, uid); - newTab._init(); if (callback) callback(newTab); + return newTab; }; - mod.closeTab = function(id) { - $openTabs[$tabMap[id]].close(); - }; - - mod.setActiveTabName = function(name) { - $activeTab.tab.find(".tab-name").text(name); - }; - + /** + * 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.contents.hasClass("pl-content")) { - mod.updatePlaylist(); + AIRTIME.playlist.setCurrent($activeTab.contents); } }; - mod.updatePlaylist = function() { - AIRTIME.playlist.setCurrent($activeTab.contents); - $.post(baseUrl + "playlist/change-playlist", { - "id": AIRTIME.playlist.getId($activeTab.contents), - "type": $activeTab.contents.find('.obj_type').val() - }); - }; - + /** + * 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} id the tab 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 $openTabs[$tabMap[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); + }; + return AIRTIME; }(AIRTIME || {})); @@ -268,4 +340,5 @@ var AIRTIME = (function(AIRTIME){ $(document).ready(function() { $("#show_builder").textScroll(".tab-name"); AIRTIME.tabs.init(); -}); \ No newline at end of file +}); +$(window).resize(AIRTIME.tabs.onResize);