From ffdc83dc26df6f7cb44dbdbf9513b2bd9d98699d Mon Sep 17 00:00:00 2001 From: Albert Santoni Date: Tue, 15 Sep 2015 14:18:35 -0400 Subject: [PATCH] Reusable datatable control that hooks up to any of our REST APIs * Implements item selection (single, shift, ctrl), pagination, and sorting. To be used in the future. * Added example code for how to use the table widget * Temporarily added a table test page to the DashboardController --- .../application/common/widgets/Table.php | 30 ++ .../controllers/DashboardController.php | 15 + .../rest/controllers/MediaController.php | 11 +- .../views/scripts/dashboard/table-test.phtml | 7 + .../public/js/airtime/library/library.js | 19 - .../js/airtime/widgets/table-example.js | 24 ++ .../public/js/airtime/widgets/table.js | 326 ++++++++++++++++++ 7 files changed, 411 insertions(+), 21 deletions(-) create mode 100644 airtime_mvc/application/common/widgets/Table.php create mode 100644 airtime_mvc/application/views/scripts/dashboard/table-test.phtml create mode 100644 airtime_mvc/public/js/airtime/widgets/table-example.js create mode 100644 airtime_mvc/public/js/airtime/widgets/table.js diff --git a/airtime_mvc/application/common/widgets/Table.php b/airtime_mvc/application/common/widgets/Table.php new file mode 100644 index 000000000..0692debff --- /dev/null +++ b/airtime_mvc/application/common/widgets/Table.php @@ -0,0 +1,30 @@ +appendFile($baseUrl . $deps[$i] .'?'. $airtimeVersion, 'text/javascript'); + } + } +} \ No newline at end of file diff --git a/airtime_mvc/application/controllers/DashboardController.php b/airtime_mvc/application/controllers/DashboardController.php index 660a6b61c..dbf273f03 100644 --- a/airtime_mvc/application/controllers/DashboardController.php +++ b/airtime_mvc/application/controllers/DashboardController.php @@ -1,5 +1,7 @@ view->airtime_version = Application_Model_Preference::GetAirtimeVersion(); } + public function tableTestAction() + { + Zend_Layout::getMvcInstance()->assign('parent_page', 'Help'); + + $CC_CONFIG = Config::getConfig(); + + $baseUrl = Application_Common_OsPath::getBaseDir(); + + $headScript = $this->view->headScript(); + AirtimeTableView::injectTableJavaScriptDependencies($headScript, $baseUrl, $CC_CONFIG['airtime_version']); + $this->view->headScript()->appendFile($baseUrl.'js/airtime/widgets/table-example.js?'.$CC_CONFIG['airtime_version']); + + } } diff --git a/airtime_mvc/application/modules/rest/controllers/MediaController.php b/airtime_mvc/application/modules/rest/controllers/MediaController.php index b43bac3c0..d276e829b 100644 --- a/airtime_mvc/application/modules/rest/controllers/MediaController.php +++ b/airtime_mvc/application/modules/rest/controllers/MediaController.php @@ -21,13 +21,20 @@ class Rest_MediaController extends Zend_Rest_Controller $offset = $this->_getParam('offset', 0); $limit = $this->_getParam('limit', $totalFileCount); + //Sorting parameters + $sortColumn = $this->_getParam('sort', CcFilesPeer::ID); + $sortDir = $this->_getParam('sort_dir', Criteria::ASC); + $query = CcFilesQuery::create() ->filterByDbHidden(false) ->filterByDbFileExists(true) ->filterByDbImportStatus(0) ->setLimit($limit) ->setOffset($offset) - ->orderByDbId(); + ->orderBy($sortColumn, $sortDir); + //->orderByDbId(); + + $queryCount = $query->count(); $queryResult = $query->find(); @@ -39,7 +46,7 @@ class Rest_MediaController extends Zend_Rest_Controller $this->getResponse() ->setHttpResponseCode(200) - ->setHeader('X-TOTAL-COUNT', $queryCount) + ->setHeader('X-TOTAL-COUNT', $totalFileCount) ->appendBody(json_encode($files_array)); /** TODO: Use this simpler code instead after we upgrade to Propel 1.7 (Airtime 2.6.x branch): diff --git a/airtime_mvc/application/views/scripts/dashboard/table-test.phtml b/airtime_mvc/application/views/scripts/dashboard/table-test.phtml new file mode 100644 index 000000000..63c849b6d --- /dev/null +++ b/airtime_mvc/application/views/scripts/dashboard/table-test.phtml @@ -0,0 +1,7 @@ +
+

+

+ Hello +

+
+
diff --git a/airtime_mvc/public/js/airtime/library/library.js b/airtime_mvc/public/js/airtime/library/library.js index 91c954e6d..cdf460bcd 100644 --- a/airtime_mvc/public/js/airtime/library/library.js +++ b/airtime_mvc/public/js/airtime/library/library.js @@ -1174,25 +1174,6 @@ var AIRTIME = (function(AIRTIME) { }(AIRTIME || {})); -function buildEditMetadataDialog (json){ - var dialog = $(json.dialog); - - dialog.dialog({ - autoOpen: false, - title: $.i18n._("Edit Metadata"), - width: 460, - height: 660, - modal: true, - close: closeDialogLibrary - }); - - dialog.dialog('open'); -} - -function closeDialogLibrary(event, ui) { - $(this).remove(); -} - /* * This function is called from dataTables.columnFilter.js */ diff --git a/airtime_mvc/public/js/airtime/widgets/table-example.js b/airtime_mvc/public/js/airtime/widgets/table-example.js new file mode 100644 index 000000000..fa5a21835 --- /dev/null +++ b/airtime_mvc/public/js/airtime/widgets/table-example.js @@ -0,0 +1,24 @@ +/** + * Created by asantoni on 11/09/15. + */ + + +$(document).ready(function() { + var aoColumns = [ + /* Title */ { "sTitle" : $.i18n._("Title") , "mDataProp" : "track_title" , "sClass" : "library_title" , "sWidth" : "170px" }, + /* Creator */ { "sTitle" : $.i18n._("Creator") , "mDataProp" : "artist_name" , "sClass" : "library_creator" , "sWidth" : "160px" }, + /* Upload Time */ { "sTitle" : $.i18n._("Uploaded") , "mDataProp" : "utime" , "bVisible" : false , "sClass" : "library_upload_time" , "sWidth" : "155px" }, + /* Website */ { "sTitle" : $.i18n._("Website") , "mDataProp" : "info_url" , "bVisible" : false , "sClass" : "library_url" , "sWidth" : "150px" }, + /* Year */ { "sTitle" : $.i18n._("Year") , "mDataProp" : "year" , "bVisible" : false , "sClass" : "library_year" , "sWidth" : "60px" }, + ]; + var ajaxSourceURL = baseUrl+"rest/media"; + + //Set up the div with id "example-table" as a datatable. + var table = AIRTIME.widgets.table.init( + $('#example-table'), //DOM node to create the table inside. + true, //Enable item selection + { //Datatables overrides. + 'aoColumns' : aoColumns, + 'sAjaxSource' : ajaxSourceURL + }); +}); diff --git a/airtime_mvc/public/js/airtime/widgets/table.js b/airtime_mvc/public/js/airtime/widgets/table.js new file mode 100644 index 000000000..ccdf8fffb --- /dev/null +++ b/airtime_mvc/public/js/airtime/widgets/table.js @@ -0,0 +1,326 @@ +/** + * Created by asantoni on 11/09/15. + */ + +var AIRTIME = (function(AIRTIME) { + + //Module initialization + if (AIRTIME.widgets === undefined) { + AIRTIME.widgets = {}; + } + if (AIRTIME.widgets.table === undefined) { + AIRTIME.widgets.table = {}; + } + + var self; + var self = AIRTIME.widgets.table; + + //Constants + self.SELECTION_MODE = { + SINGLE : 0, + MULTI_SHIFT : 1, + MULTI_CTRL : 2 + } + + self.HUGE_INT = Math.pow(2, 53) - 1; + + //Member variables + self._datatable = null; + self._selectedRows = []; //An array containing the underlying objects for each selected row. (Easy to use!) + //self._selectedRowVisualIdxMap = []; //A map of the visual index of a selected rows onto the actual row data. + self._selectedRowVisualIdxMin = self.HUGE_INT; + self._selectedRowVisualIdxMax = -1; + self._$wrapperDOMNode = null; + + + //Member functions + self.init = function(wrapperDOMNode, bItemSelection, dataTablesOptions) { + self._$wrapperDOMNode = $(wrapperDOMNode); + + //TODO: If selection is enabled, add in the checkbox column. + if (bItemSelection) { + dataTablesOptions["aoColumns"].unshift( + /* Checkbox */ { "sTitle" : "", "mData" : self._datatablesCheckboxDataDelegate, "bSortable" : false , "bSearchable" : false , "sWidth" : "16px" , "sClass" : "library_checkbox" } + ); + + dataTablesOptions["fnRowCallback"] = self._rowCreatedCallback; + } + + var options = { + "aoColumns": [ + /* Title */ { "sTitle" : $.i18n._("Make sure to override me") , "mDataProp" : "track_title" , "sClass" : "library_title" , "sWidth" : "170px" }, + ], + "bProcessing": true, + "bServerSide": true, + "sAjaxSource": baseUrl+"rest/media", //Override me + "sAjaxDataProp": "aaData", + "bScrollCollapse": false, + "sPaginationType": "full_numbers", + "bJQueryUI": true, + "bAutoWidth": false, + "aaSorting": [], + "oLanguage" : getDatatablesStrings({ + "sEmptyTable": $.i18n._(""), + "sZeroRecords": $.i18n._("No matching results found.") + }), + "oColVis": { + "sAlign": "right", + "buttonText": $.i18n._("Columns"), + "iOverlayFade": 0 + }, + // z = ColResize, R = ColReorder, C = ColVis + "sDom": 'Rf<"dt-process-rel"r><"H"<"library_toolbar"C>><"dataTables_scrolling"t<"#library_empty"<"#library_empty_image"><"#library_empty_text">>><"F"lip>>', + + "fnServerData": self._fetchData, + "fnDrawCallback" : self._tableDrawCallback + }; + + //Override any options with those passed in as arguments to this constructor. + for (var key in dataTablesOptions) + { + options[key] = dataTablesOptions[key]; + } + + self._datatable = self._$wrapperDOMNode.dataTable(options); + + }; + + self._handleAjaxError = function(r) { + // If the request was denied due to permissioning + if (r.status === 403) { + // Hide the processing div + /* + $("#library_display_wrapper").find(".dt-process-rel").hide(); + $.getJSON( "ajax/library_placeholders.json", function( data ) { + $('#library_empty_text').text($.i18n._(data.unauthorized)); + }) ; + + $('#library_empty').show(); + */ + } + }; + + // + self._fetchData = function ( sSource, aoData, fnCallback, oSettings ) { + + var echo = aoData[0].value; //Datatables state tracking. Must be included. + + //getUsabilityHint(); + var sortColName = ""; + var sortDir = ""; + if (oSettings.aaSorting.length > 0) { + var sortColIdx = oSettings.aaSorting[0][0]; + sortColName = oSettings.aoColumns[sortColIdx].mDataProp; + sortDir = oSettings.aaSorting[0][1].toUpperCase(); + } + + $.ajax({ + "dataType": 'json', + "type": "GET", + "url": sSource, + "data": { + "limit": oSettings._iDisplayLength, + "offset": oSettings._iDisplayStart, + "sort": sortColName, + 'sort_dir': sortDir, + }, + "success": function (json, textStatus, jqXHR) { + var rawResponseJSON = json; + json = []; + json.aaData = rawResponseJSON; + json.iTotalRecords = jqXHR.getResponseHeader('X-TOTAL-COUNT'); + json.iTotalDisplayRecords = json.iTotalRecords;//rawResponseJSON.length; + json.sEcho = echo; + + //Pass it along to datatables. + fnCallback(json); + }, + "error": self._handleAjaxError + }).done(function (data) { + /* + if (data.iTotalRecords > data.iTotalDisplayRecords) { + $('#filter_message').text( + $.i18n._("Filtering out ") + (data.iTotalRecords - data.iTotalDisplayRecords) + + $.i18n._(" of ") + data.iTotalRecords + + $.i18n._(" records") + ); + $('#library_empty').hide(); + $('#library_display').find('tr:has(td.dataTables_empty)').show(); + } else { + $('#filter_message').text(""); + } + $('#library_content').find('.dataTables_filter input[type="text"]') + .css('padding-right', $('#advanced-options').find('button').outerWidth()); + */ + }); + }; + + self._datatablesCheckboxDataDelegate = function(rowData, callType, dataToSave) { + + + if (callType == undefined) { + //Supposed to return the raw data for the type here. + return null; + } else if (callType == 'display') { + return ""; + } else if (callType == 'sort') { + return null; + } else if (callType == 'type') { + return "input"; + } else if (callType == 'set') { + //The data to set is in dataToSave. + return; + } else if (callType == 'filter') { + return null; + } + + //For all other calls, just return the data as this: + return "check"; + }; + + self._rowCreatedCallback = function(nRow, aData, iDisplayIndex) { + + // Bind click event + $(nRow).click(function(e) { + e.stopPropagation(); + e.preventDefault(); + document.getSelection().removeAllRanges(); + //alert( 'You clicked on '+aData.track_title+'\'s row' + iDisplayIndex); + var selectionMode = self.SELECTION_MODE.SINGLE; + if (e.shiftKey) { + selectionMode = self.SELECTION_MODE.MULTI_SHIFT; + } else if (e.ctrlKey) { + selectionMode = self.SELECTION_MODE.MULTI_CTRL; + } + self.selectRow(nRow, aData, selectionMode, iDisplayIndex); + }); + + return nRow; + }; + + self._tableDrawCallback = function(oSettings) { + + $('input.airtime_table_checkbox').click(function(e) { + $this = $(this); + + var iVisualRowIdx = $this.parent().parent().index(); + self.selectRow($this.parent().parent(), null, self.SELECTION_MODE.MULTI_CTRL, iVisualRowIdx); //Always multiselect for checkboxes + e.stopPropagation(); + return true; + }); + }; + + /** @param nRow is a tr DOM node (non-jQuery) + * @param aData is an array containing the raw data for the row. Can be null if you don't have it. + * @param selectionMode is an SELECT_MODE enum. Specify what selection mode you want to use for this action. + * @param iVisualRowIdx is an integer which corresponds to the index of the clicked row, as it appears to the user. + * eg. The 5th row in the table will have an iVisualRowIdx of 4 (0-based). + */ + self.selectRow = function(nRow, aData, selectionMode, iVisualRowIdx) { + + //Default to single item selection. + if (selectionMode == undefined) { + selectionMode = self.SELECTION_MODE.SINGLE; + } + + var $nRow = $(nRow); + + /* + var foundAtIdx = $.inArray(aData, self._selectedRows) + + if (foundAtIdx >= 0 && self._selectedRows.length > 1) { + self._selectedRows.splice(foundAtIdx, 1); + $nRow.removeClass('selected'); + $nRow.find('input.airtime_table_checkbox').attr('checked', false); + */ + if (false) { + } else { + //Regular single left-click mode + if (selectionMode == self.SELECTION_MODE.SINGLE) { + + self._clearSelection(); + + self._selectedRows.push(aData); + self._selectedRowVisualIdxMin = iVisualRowIdx; + self._selectedRowVisualIdxMax = iVisualRowIdx; + //self._selectedRowVisualIdxMap[iVisualRowIdx] = aData; + + $nRow.addClass('selected'); + $nRow.find('input.airtime_table_checkbox').attr('checked', true); + } + //Ctrl-click multi row selection mode + else if (selectionMode == self.SELECTION_MODE.MULTI_CTRL) { + + + var foundAtIdx = $.inArray(aData, self._selectedRows) + if (foundAtIdx >= 0 && self._selectedRows.length > 1) { + self._selectedRows.splice(foundAtIdx, 1); + $nRow.removeClass('selected'); + $nRow.find('input.airtime_table_checkbox').attr('checked', false); + } + else { + self._selectedRows.push(aData); + + self._selectedRowVisualIdxMin = iVisualRowIdx; + self._selectedRowVisualIdxMax = iVisualRowIdx; + + $nRow.addClass('selected'); + $nRow.find('input.airtime_table_checkbox').attr('checked', true); + } + } + //Shift-click multi row selection mode + else if (selectionMode == self.SELECTION_MODE.MULTI_SHIFT) { + + //If there's no rows selected, just behave like single selection. + if (self._selectedRows.length == 0) { + return self.selectRow(nRow, aData, self.SELECTION_MODE.SINGLE, iVisualRowIdx); + } + + if (iVisualRowIdx > self._selectedRowVisualIdxMax) { + self._selectedRowVisualIdxMax = iVisualRowIdx; + } + if (iVisualRowIdx < self._selectedRowVisualIdxMin) { + self._selectedRowVisualIdxMin = iVisualRowIdx; + } + + var selectionStartRowIdx = Math.min(iVisualRowIdx, self._selectedRowVisualIdxMin); + var selectionEndRowIdx = Math.min(iVisualRowIdx, self._selectedRowVisualIdxMax); + + + //We can assume there's at least 1 row already selected now. + var allRows = self._datatable.fnGetData(); + + self._selectedRows = []; + for (var i = self._selectedRowVisualIdxMin; i <= self._selectedRowVisualIdxMax; i++) + { + self._selectedRows.push(allRows[i]); + $row = $($nRow.parent().children()[i]); + $row.addClass('selected'); + $row.find('input.airtime_table_checkbox').attr('checked', true); + } + + } + else { + console.log("Unimplemented selection mode"); + } + } + }; + + self._clearSelection = function() { + self._selectedRows = []; + //self._selectedRowVisualIdxMap = []; + self._selectedRowVisualIdxMin = self.HUGE_INT; + self._selectedRowVisualIdxMax = -1; + self._$wrapperDOMNode.find('.selected').removeClass('selected'); + self._$wrapperDOMNode.find('input.airtime_table_checkbox').attr('checked', false); + }; + + self.getSelectedRows = function() { + return self._selectedRows; + } + + return AIRTIME; + +}(AIRTIME || {})); + +