627 lines
19 KiB
JavaScript
627 lines
19 KiB
JavaScript
/**
|
|
* Created by asantoni on 11/09/15.
|
|
*/
|
|
|
|
var AIRTIME = (function (AIRTIME) {
|
|
//Module initialization
|
|
if (AIRTIME.widgets === undefined) {
|
|
AIRTIME.widgets = {};
|
|
}
|
|
|
|
//Table widget constructor
|
|
/**
|
|
*
|
|
*
|
|
* @param wrapperDOMNode
|
|
* @param {boolean} bItemSelection
|
|
* @param {Object} toolbarButtons
|
|
* @param {Object} dataTablesOptions
|
|
* @param {Object} [emptyPlaceholder]
|
|
* @param {string} emptyPlaceholder.html
|
|
* @param {string} emptyPlaceholder.iconClass
|
|
*
|
|
* @returns {Table}
|
|
* @constructor
|
|
*/
|
|
var Table = function (
|
|
wrapperDOMNode,
|
|
bItemSelection,
|
|
toolbarButtons,
|
|
dataTablesOptions,
|
|
emptyPlaceholder,
|
|
) {
|
|
var self = this;
|
|
|
|
self.HUGE_INT = Math.pow(2, 53) - 1;
|
|
|
|
//Constants and enumerations
|
|
self.SELECTION_MODE = {
|
|
SINGLE: 0,
|
|
MULTI_SHIFT: 1,
|
|
MULTI_CTRL: 2,
|
|
};
|
|
|
|
//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;
|
|
self._toolbarButtons = null;
|
|
//Save some of the constructor parameters
|
|
self._$wrapperDOMNode = $(wrapperDOMNode);
|
|
self._toolbarButtons = toolbarButtons;
|
|
self._emptyPlaceholder = emptyPlaceholder;
|
|
|
|
// Exclude the leftmost column if we're implementing item selection
|
|
self._colVisExcludeColumns = bItemSelection ? [0] : [];
|
|
|
|
//Finish initialization of the datatable since everything is declared by now.
|
|
|
|
// If selection is enabled, add in the checkbox column.
|
|
if (bItemSelection) {
|
|
dataTablesOptions["aoColumns"].unshift(
|
|
/* Checkbox */ {
|
|
sTitle: "",
|
|
mData: self._datatablesCheckboxDataDelegate.bind(this),
|
|
bSortable: false,
|
|
bSearchable: false,
|
|
sWidth: "24px",
|
|
sClass: "airtime_table_checkbox",
|
|
},
|
|
);
|
|
}
|
|
|
|
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,
|
|
deferLoading: 1, //0 tells it there's zero elements loaded and disables the automatic AJAX. We don't want to load until after we bind all our event handlers, to prevent a race condition with the "init" event callback.
|
|
sPaginationType: "full_numbers",
|
|
bJQueryUI: true,
|
|
bAutoWidth: false,
|
|
aaSorting: [],
|
|
iDisplayLength: 25,
|
|
aLengthMenu: [25, 50, 100],
|
|
oLanguage: getDatatablesStrings({
|
|
sEmptyTable: $.i18n._(""),
|
|
sZeroRecords: $.i18n._("No matching results found."),
|
|
}),
|
|
oColVis: {
|
|
sAlign: "right",
|
|
aiExclude: self._colVisExcludeColumns,
|
|
buttonText: $.i18n._("Columns"),
|
|
iOverlayFade: 0,
|
|
},
|
|
// z = ColResize, R = ColReorder, C = ColVis
|
|
sDom: 'Rf<"dt-process-rel"r><"H"<"table_toolbar"C>><"dataTables_scrolling"t<".empty_placeholder"<".empty_placeholder_image"><".empty_placeholder_text">>><"F"lip>>',
|
|
|
|
fnPreDrawCallback: function () {
|
|
$("#draggingContainer").remove();
|
|
},
|
|
fnServerData: self._fetchData.bind(self),
|
|
//"fnInitComplete" : function() { self._setupEventHandlers(bItemSelection) }
|
|
fnDrawCallback: function () {
|
|
self.clearSelection();
|
|
},
|
|
};
|
|
|
|
//Override any options with those passed in as arguments to this constructor.
|
|
for (var key in dataTablesOptions) {
|
|
options[key] = dataTablesOptions[key];
|
|
}
|
|
|
|
if (options.fnCreatedRow) {
|
|
options.fnCreatedRow = options.fnCreatedRow.bind(self);
|
|
}
|
|
if (options.fnDrawCallback) {
|
|
options.fnDrawCallback = options.fnDrawCallback.bind(self);
|
|
}
|
|
|
|
self._datatable = self._$wrapperDOMNode.dataTable(options);
|
|
// self._datatable.fnDraw(); //Load the AJAX data now that our event handlers have been bound.
|
|
self._setupEventHandlers(bItemSelection);
|
|
|
|
//return self._datatable;
|
|
return self;
|
|
};
|
|
|
|
Table.prototype.assignDblClickHandler = function (fn) {
|
|
$(this._datatable, "tbody tr").on(
|
|
"dblclick",
|
|
this._SELECTORS.SELECTION_TABLE_ROW,
|
|
fn,
|
|
);
|
|
};
|
|
|
|
/* Set up global event handlers for the datatable.
|
|
* @param bItemSelection Whether or not row selection behaviour should be enabled for this widget.
|
|
* */
|
|
Table.prototype._setupEventHandlers = function (bItemSelection) {
|
|
var self = this;
|
|
|
|
/** This table row event handler is created once and catches events for any row. (It's less resource intensive
|
|
* than having a per-row callback...)
|
|
*/
|
|
if (bItemSelection) {
|
|
$(self._datatable, "tbody tr").on(
|
|
"click contextmenu",
|
|
self._SELECTORS.SELECTION_TABLE_ROW,
|
|
function (e) {
|
|
var aData = self._datatable.fnGetData(this);
|
|
var iDisplayIndex = $(this).index(); // The index of the row in the current page in the table.
|
|
var nRow = this;
|
|
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
document.getSelection().removeAllRanges();
|
|
|
|
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;
|
|
}
|
|
|
|
if (e.button == 2) {
|
|
selectionMode = self.SELECTION_MODE.SINGLE;
|
|
}
|
|
self.selectRow(nRow, aData, selectionMode, iDisplayIndex);
|
|
},
|
|
);
|
|
|
|
$(self._datatable, "tbody tr").on(
|
|
"click",
|
|
self._SELECTORS.SELECTION_CHECKBOX,
|
|
function (e) {
|
|
$this = $(this);
|
|
|
|
var iVisualRowIdx = $this.parent().index();
|
|
var aData = self._datatable.fnGetData(iVisualRowIdx);
|
|
var selectionMode = self.SELECTION_MODE.MULTI_CTRL; //Behaviour for checkboxes.
|
|
if (e.shiftKey) {
|
|
selectionMode = self.SELECTION_MODE.MULTI_SHIFT;
|
|
}
|
|
self.selectRow($this.parent(), aData, selectionMode, iVisualRowIdx); //Always multiselect for checkboxes
|
|
e.stopPropagation();
|
|
return true;
|
|
},
|
|
);
|
|
|
|
// Clear selection when switching pages
|
|
$(self._datatable).on("page", function () {
|
|
self.clearSelection();
|
|
});
|
|
}
|
|
|
|
// On filter, display the number of total and filtered results in the search bar
|
|
$(self._datatable).on("filter", function () {
|
|
var dt = self._datatable,
|
|
f = dt.closest(".dataTables_wrapper").find(".filter-message"),
|
|
totalRecords = dt.fnSettings().fnRecordsTotal(),
|
|
totalDisplayRecords = dt.fnSettings().fnRecordsDisplay();
|
|
|
|
if (f.length === 0) {
|
|
var el = document.createElement("span");
|
|
el.setAttribute("class", "filter-message");
|
|
f = dt
|
|
.closest(".dataTables_wrapper")
|
|
.find(".dataTables_filter")
|
|
.append(el)
|
|
.find(".filter-message");
|
|
}
|
|
|
|
f.text(
|
|
totalRecords > totalDisplayRecords
|
|
? $.i18n._("Filtering out ") +
|
|
(totalRecords - totalDisplayRecords) +
|
|
$.i18n._(" of ") +
|
|
totalRecords +
|
|
$.i18n._(" records")
|
|
: "",
|
|
);
|
|
|
|
dt.closest(".dataTables_wrapper")
|
|
.find('.dataTables_filter input[type="text"]')
|
|
.css("padding-right", f.outerWidth());
|
|
});
|
|
|
|
//Since this function is already called when the datatables initialization is complete, we know the DOM
|
|
//structure for the datatable exists and can just proceed to setup the toolbar DOM elements now.
|
|
self._setupToolbarButtons(self._toolbarButtons);
|
|
};
|
|
|
|
/**
|
|
* Member functions
|
|
*
|
|
*/
|
|
|
|
/** Populate the toolbar with buttons.
|
|
*
|
|
* @param buttons A list of objects which contain button definitions. See self.TOOLBAR_BUTTON_ROLES for an example, or use getStandardToolbarButtons() to get a list of them.
|
|
* @private
|
|
*/
|
|
Table.prototype._setupToolbarButtons = function (buttons) {
|
|
var self = this;
|
|
var $menu = self._$wrapperDOMNode
|
|
.parent()
|
|
.parent()
|
|
.find("div.table_toolbar");
|
|
$menu.addClass("btn-toolbar");
|
|
|
|
//Create the toolbar buttons.
|
|
$.each(buttons, function (idx, btn) {
|
|
var buttonElement = self._createToolbarButton(
|
|
btn.title,
|
|
btn.iconClass,
|
|
btn.extraBtnClass,
|
|
btn.elementId,
|
|
);
|
|
$menu.append(buttonElement);
|
|
btn.element = buttonElement; //Save this guy in case you need it later.
|
|
//Bind event handlers to each button
|
|
$.each(btn.eventHandlers, function (eventName, eventCallback) {
|
|
$(buttonElement).on(eventName, function () {
|
|
if ($(buttonElement).find("button").is(":disabled")) {
|
|
return;
|
|
}
|
|
eventCallback();
|
|
});
|
|
});
|
|
});
|
|
|
|
self._checkToolbarButtons();
|
|
};
|
|
|
|
/**
|
|
* Check each of the toolbar buttons for the table and disable them if their constraints are invalid.
|
|
*
|
|
* Passes current Table object context to function calls.
|
|
*/
|
|
Table.prototype._checkToolbarButtons = function () {
|
|
var self = this;
|
|
$.each(self._toolbarButtons, function (idx, btn) {
|
|
var btnNode = $(btn.element).find("button").get(0);
|
|
btnNode.disabled = btn.disabled = !btn.validateConstraints.call(self);
|
|
});
|
|
};
|
|
|
|
/** Create the DOM element for a toolbar button and return it. */
|
|
Table.prototype._createToolbarButton = function (
|
|
title,
|
|
iconClass,
|
|
extraBtnClass,
|
|
elementId,
|
|
) {
|
|
if (!iconClass) {
|
|
iconClass = "icon-plus";
|
|
}
|
|
|
|
// var title = $.i18n._('Delete');
|
|
var outerDiv = document.createElement("div");
|
|
outerDiv.className = "btn-group";
|
|
outerDiv.title = title;
|
|
var innerButton = document.createElement("button");
|
|
//innerButton.className = 'btn btn-small ' + extraBtnClass;
|
|
innerButton.className = "btn " + extraBtnClass;
|
|
innerButton.id = elementId;
|
|
var innerIcon = document.createElement("i");
|
|
innerIcon.className = "icon-white " + iconClass;
|
|
var innerTextSpan = document.createElement("span");
|
|
var innerText = document.createTextNode(title);
|
|
innerTextSpan.appendChild(innerText);
|
|
innerButton.appendChild(innerIcon);
|
|
innerButton.appendChild(innerTextSpan);
|
|
outerDiv.appendChild(innerButton);
|
|
|
|
/* Here's an example of what the button HTML should look like:
|
|
"<div class='btn-group' title=" + $.i18n._('Delete') + ">" +
|
|
"<button class='btn btn-small btn-danger' id='sb-trash'>" +
|
|
"<i class='icon-white icon-trash'></i>" +
|
|
"<span>" + $.i18n._('Delete') + "</span>" +
|
|
"</button>" +
|
|
"</div>"*/
|
|
return outerDiv;
|
|
};
|
|
|
|
Table.prototype.clearSelection = function () {
|
|
this._selectedRows = [];
|
|
//self._selectedRowVisualIdxMap = [];
|
|
this._selectedRowVisualIdxMin = self.HUGE_INT;
|
|
this._selectedRowVisualIdxMax = -1;
|
|
this._$wrapperDOMNode.find(".selected").removeClass("selected");
|
|
this._$wrapperDOMNode
|
|
.find(this._SELECTORS.SELECTION_CHECKBOX)
|
|
.find("input")
|
|
.attr("checked", false);
|
|
this._checkToolbarButtons();
|
|
};
|
|
|
|
/** @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).
|
|
*/
|
|
Table.prototype.selectRow = function (
|
|
nRow,
|
|
aData,
|
|
selectionMode,
|
|
iVisualRowIdx,
|
|
) {
|
|
var self = this;
|
|
|
|
//Default to single item selection.
|
|
if (selectionMode == undefined) {
|
|
selectionMode = self.SELECTION_MODE.SINGLE;
|
|
}
|
|
|
|
var $nRow = $(nRow);
|
|
|
|
//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(self._SELECTORS.SELECTION_CHECKBOX)
|
|
.find("input")
|
|
.attr("checked", true);
|
|
}
|
|
//Ctrl-click multi row selection mode
|
|
else if (selectionMode == self.SELECTION_MODE.MULTI_CTRL) {
|
|
var foundAtIdx = $.inArray(aData, self._selectedRows);
|
|
|
|
//console.log('checkbox mouse', iVisualRowIdx, foundAtIdx);
|
|
|
|
//If the clicked row is already selected, deselect it.
|
|
if (foundAtIdx >= 0 && self._selectedRows.length >= 1) {
|
|
self._selectedRows.splice(foundAtIdx, 1);
|
|
$nRow.removeClass("selected");
|
|
$nRow
|
|
.find(self._SELECTORS.SELECTION_CHECKBOX)
|
|
.find("input")
|
|
.attr("checked", false);
|
|
} else {
|
|
self._selectedRows.push(aData);
|
|
|
|
self._selectedRowVisualIdxMin = iVisualRowIdx;
|
|
self._selectedRowVisualIdxMax = iVisualRowIdx;
|
|
|
|
$nRow.addClass("selected");
|
|
$nRow
|
|
.find(self._SELECTORS.SELECTION_CHECKBOX)
|
|
.find("input")
|
|
.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(self._SELECTORS.SELECTION_CHECKBOX)
|
|
.find("input")
|
|
.attr("checked", true);
|
|
}
|
|
} else {
|
|
console.log("Unimplemented selection mode");
|
|
}
|
|
|
|
self._checkToolbarButtons();
|
|
};
|
|
|
|
Table.prototype.getSelectedRows = function () {
|
|
return this._selectedRows;
|
|
};
|
|
|
|
Table.prototype.getEmptyPlaceholder = function () {
|
|
return this._emptyPlaceholder;
|
|
};
|
|
|
|
Table.prototype._handleAjaxError = function (r) {
|
|
// If the request was denied due to permissioning
|
|
if (r.status === 403) {
|
|
$(".dt-process-rel").hide();
|
|
$(".empty_placeholder_text").text(
|
|
$.i18n._("You don't have permission to view this resource."),
|
|
);
|
|
$(".empty_placeholder").show();
|
|
}
|
|
};
|
|
|
|
/** Grab data from a REST API and format so that DataTables can display it.
|
|
* This is the DataTables REST adapter function, basically.
|
|
* */
|
|
Table.prototype._fetchData = function (
|
|
sSource,
|
|
aoData,
|
|
fnCallback,
|
|
oSettings,
|
|
) {
|
|
var self = this;
|
|
var echo = aoData[0].value; //Datatables state tracking. Must be included.
|
|
|
|
var sortColName = "";
|
|
var sortDir = "";
|
|
var search = self._$wrapperDOMNode
|
|
.closest(".dataTables_wrapper")
|
|
.find(".dataTables_filter")
|
|
.find("input")
|
|
.val();
|
|
if (oSettings.aaSorting.length > 0) {
|
|
var sortColIdx = oSettings.aaSorting[0][0];
|
|
sortColName = oSettings.aoColumns[sortColIdx].mDataProp;
|
|
sortDir = oSettings.aaSorting[0][1].toUpperCase();
|
|
}
|
|
|
|
// FIXME: We should probably just be sending aoData back here..?
|
|
$.ajax({
|
|
dataType: "json",
|
|
type: "GET",
|
|
url: sSource,
|
|
data: {
|
|
limit: oSettings._iDisplayLength,
|
|
offset: oSettings._iDisplayStart,
|
|
sort: sortColName,
|
|
sort_dir: sortDir,
|
|
search: search,
|
|
},
|
|
success: function (json, textStatus, jqXHR) {
|
|
var rawResponseJSON = json;
|
|
json = [];
|
|
json.aaData = rawResponseJSON;
|
|
json.iTotalRecords = jqXHR.getResponseHeader("X-TOTAL-COUNT");
|
|
json.iTotalDisplayRecords = json.iTotalRecords;
|
|
json.sEcho = echo;
|
|
|
|
//Pass it along to datatables.
|
|
fnCallback(json);
|
|
},
|
|
error: self._handleAjaxError,
|
|
});
|
|
};
|
|
|
|
Table.prototype._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 "<input type='checkbox'>";
|
|
} 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";
|
|
};
|
|
|
|
//Accessors / Mutators
|
|
|
|
Table.prototype.getDatatable = function () {
|
|
return this._datatable;
|
|
};
|
|
|
|
//Static initializers / Class variables
|
|
|
|
Table.prototype._SELECTORS = Object.freeze({
|
|
SELECTION_CHECKBOX: ".airtime_table_checkbox",
|
|
SELECTION_TABLE_ROW: "tr",
|
|
});
|
|
|
|
Table.TOOLBAR_BUTTON_ROLES = {
|
|
NEW: 0,
|
|
EDIT: 1,
|
|
DELETE: 2,
|
|
};
|
|
Object.freeze(Table.TOOLBAR_BUTTON_ROLES);
|
|
|
|
//Set of standard buttons. Use getStandardToolbarButtons() to grab these and pass them to the init() function.
|
|
Table._STANDARD_TOOLBAR_BUTTONS = {};
|
|
Table._STANDARD_TOOLBAR_BUTTONS[Table.TOOLBAR_BUTTON_ROLES.NEW] = {
|
|
title: $.i18n._("New"),
|
|
iconClass: "icon-plus",
|
|
extraBtnClass: "btn-small btn-new",
|
|
elementId: "",
|
|
eventHandlers: {},
|
|
validateConstraints: function () {
|
|
return true;
|
|
},
|
|
};
|
|
Table._STANDARD_TOOLBAR_BUTTONS[Table.TOOLBAR_BUTTON_ROLES.EDIT] = {
|
|
title: $.i18n._("Edit"),
|
|
iconClass: "icon-pencil",
|
|
extraBtnClass: "btn-small",
|
|
elementId: "",
|
|
eventHandlers: {},
|
|
validateConstraints: function () {
|
|
return true;
|
|
},
|
|
};
|
|
Table._STANDARD_TOOLBAR_BUTTONS[Table.TOOLBAR_BUTTON_ROLES.DELETE] = {
|
|
title: $.i18n._("Delete"),
|
|
iconClass: "icon-trash",
|
|
extraBtnClass: "btn-small btn-danger",
|
|
elementId: "",
|
|
eventHandlers: {},
|
|
validateConstraints: function () {
|
|
return true;
|
|
},
|
|
};
|
|
Object.freeze(Table._STANDARD_TOOLBAR_BUTTONS);
|
|
|
|
//Static method
|
|
Table.getStandardToolbarButtons = function () {
|
|
//Return a deep copy
|
|
return jQuery.extend(true, {}, Table._STANDARD_TOOLBAR_BUTTONS);
|
|
};
|
|
|
|
//Add Table to the widgets namespace
|
|
AIRTIME.widgets.Table = Table;
|
|
|
|
return AIRTIME;
|
|
})(AIRTIME || {});
|