'use strict';
function tableInit(ctx){
if (ctx.url.length === 0) {
throw "No url supplied for retreiving data";
}
var tableChromeDone = false;
var tableTotal = 0;
var tableParams = {
limit : 25,
page : 1,
orderby : null,
filter : null,
search : null,
default_orderby: null,
};
var defaultHiddenCols = [];
var table = $("#" + ctx.tableName);
/* if we're loading clean from a url use it's parameters as the default */
var urlParams = libtoaster.parseUrlParams();
/* Merge the tableParams and urlParams object properties */
tableParams = $.extend(tableParams, urlParams);
/* Now fix the types that .extend changed for us */
tableParams.limit = Number(tableParams.limit);
tableParams.page = Number(tableParams.page);
loadData(tableParams);
// clicking on this set of elements removes the search
var clearSearchElements = $('.remove-search-btn-'+ctx.tableName +
', .show-all-'+ctx.tableName);
function loadData(tableParams){
table.trigger("table-loading");
$.ajax({
type: "GET",
url: ctx.url,
data: tableParams,
headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
success: function(tableData) {
updateTable(tableData);
window.history.replaceState(null, null,
libtoaster.dumpsUrlParams(tableParams));
}
});
}
function updateTable(tableData) {
var tableBody = table.children("tbody");
var pagination = $('#pagination-'+ctx.tableName);
var paginationBtns = pagination.children('ul');
var tableContainer = $("#table-container-"+ctx.tableName);
tableContainer.css("visibility", "hidden");
/* To avoid page re-layout flicker when paging set fixed height */
table.css("padding-bottom", table.height());
/* Reset table components */
tableBody.html("");
paginationBtns.html("");
if (tableParams.search)
clearSearchElements.show();
else
clearSearchElements.hide();
$('.table-count-' + ctx.tableName).text(tableData.total);
tableTotal = tableData.total;
if (tableData.total === 0){
tableContainer.hide();
/* No results caused by a search returning nothing */
if (tableParams.search) {
if ($("#no-results-special-"+ctx.tableName).length > 0) {
/* use this page's special no-results form instead of the default */
$("#no-results-search-input-"+ctx.tableName).val(tableParams.search);
$("#no-results-special-"+ctx.tableName).show();
$("#results-found-"+ctx.tableName).hide();
} else {
$("#new-search-input-"+ctx.tableName).val(tableParams.search);
$("#no-results-"+ctx.tableName).show();
}
}
else {
/* No results caused by there being no data */
$("#empty-state-"+ctx.tableName).show();
}
table.trigger("table-done", [tableData.total, tableParams]);
return;
} else {
tableContainer.show();
$("#no-results-"+ctx.tableName).hide();
$("#empty-state-"+ctx.tableName).hide();
}
setupTableChrome(tableData);
/* Add table data rows */
var column_index;
for (var i in tableData.rows){
/* only display if the column is display-able */
var row = $("
");
column_index = -1;
for (var key_j in tableData.rows[i]){
var td = $("
");
td.prop("class", key_j);
if (tableData.rows[i][key_j]){
td.html(tableData.rows[i][key_j]);
}
row.append(td);
}
tableBody.append(row);
/* If we have layerbtns then initialise them */
layerBtnsInit();
/* If we have popovers initialise them now */
$('td > a.btn').popover({
html:true,
placement:'left',
container:'body',
trigger:'manual'
}).click(function(e){
$('td > a.btn').not(this).popover('hide');
/* ideally we would use 'toggle' here
* but it seems buggy in our Bootstrap version
*/
$(this).popover('show');
e.stopPropagation();
});
/* enable help information tooltip */
$(".get-help").tooltip({container:'body', html:true, delay:{show:300}});
}
/* Setup the pagination controls */
var start = tableParams.page - 2;
var end = tableParams.page + 2;
var numPages = Math.ceil(tableData.total/tableParams.limit);
if (numPages > 1){
if (tableParams.page < 3)
end = 5;
for (var page_i=1; page_i <= numPages; page_i++){
if (page_i >= start && page_i <= end){
var btn = $('
');
if (page_i === tableParams.page){
btn.addClass("active");
}
/* Add the click handler */
btn.click(pageButtonClicked);
paginationBtns.append(btn);
}
}
}
loadColumnsPreference();
table.css("padding-bottom", 0);
tableContainer.css("visibility", "visible");
/* If we have a hash in the url try and highlight that item in the table */
if (window.location.hash){
var highlight = $("table a[name="+window.location.hash.replace('#',''));
if (highlight.length > 0){
highlight.parents("tr").addClass('highlight');
window.scroll(0, highlight.position().top - 50);
}
}
table.trigger("table-done", [tableData.total, tableParams]);
}
function setupTableChrome(tableData){
if (tableChromeDone === true)
return;
var tableHeadRow = table.find("thead > tr");
var editColMenu = $("#table-chrome-"+ctx.tableName).find(".editcol");
tableHeadRow.html("");
editColMenu.html("");
tableParams.default_orderby = tableData.default_orderby;
if (!tableParams.orderby && tableData.default_orderby){
tableParams.orderby = tableData.default_orderby;
}
/* Add table header and column toggle menu */
var column_edit_entries = [];
for (var i in tableData.columns){
var col = tableData.columns[i];
if (col.displayable === false) {
continue;
}
var header = $("
");
header.prop("class", col.field_name);
/* Setup the help text */
if (col.help_text.length > 0) {
var help_text = $('');
help_text.tooltip({title: col.help_text});
header.append(help_text);
}
/* Setup the orderable title */
if (col.orderable) {
var title = $('');
title.data('field-name', col.field_name);
title.attr('data-sort-field', col.field_name);
title.text(col.title);
title.click(sortColumnClicked);
header.append(title);
header.append(' ');
header.append(' ');
/* If we're currently ordered setup the visual indicator */
if (col.field_name === tableParams.orderby ||
'-' + col.field_name === tableParams.orderby){
header.children("a").addClass("sorted");
if (tableParams.orderby.indexOf("-") === -1){
header.find('.icon-caret-down').show();
} else {
header.find('.icon-caret-up').show();
}
}
if (col.field_name === tableData.default_orderby){
title.addClass("default-orderby");
}
} else {
/* Not orderable */
header.css("font-weight", "normal");
header.append('' + col.title + ' ');
}
/* Setup the filter button */
if (col.filter_name){
var filterBtn = $('');
filterBtn.data('filter-name', col.filter_name);
filterBtn.prop('id', col.filter_name);
filterBtn.click(filterOpenClicked);
/* If we're currently being filtered setup the visial indicator */
if (tableParams.filter &&
tableParams.filter.match('^'+col.filter_name)) {
filterBtnActive(filterBtn, true);
}
header.append(filterBtn);
}
/* Done making the header now add it */
tableHeadRow.append(header);
/* Now setup the checkbox state and click handler */
var toggler = $('
');
var togglerInput = toggler.find("input");
togglerInput.attr("checked","checked");
/* If we can hide the column enable the checkbox action */
if (col.hideable){
togglerInput.click(colToggleClicked);
} else {
toggler.find("label").addClass("text-muted");
toggler.find("label").parent().addClass("disabled");
togglerInput.attr("disabled", "disabled");
}
if (col.hidden) {
defaultHiddenCols.push(col.field_name);
}
/* Gather the Edit Column entries */
column_edit_entries.push({'title':col.title,'html':toggler});
} /* End for each column */
/* Append the sorted Edit Column toggler entries */
column_edit_entries.sort(function(a,b) {return (a.title > b.title) ? 1 : ((b.title > a.title) ? -1 : 0);} );
for (var col in column_edit_entries){
editColMenu.append(column_edit_entries[col].html);
}
tableChromeDone = true;
}
/* Toggles the active state of the filter button */
function filterBtnActive(filterBtn, active){
if (active) {
filterBtn.removeClass("btn-link");
filterBtn.addClass("btn-primary");
filterBtn.tooltip({
html: true,
title: '',
placement: 'bottom',
delay: {
hide: 1500,
show: 400,
},
});
} else {
filterBtn.removeClass("btn-primary");
filterBtn.addClass("btn-link");
filterBtn.tooltip('destroy');
}
}
/* Display or hide table columns based on the cookie preference or defaults */
function loadColumnsPreference(){
var cookie_data = $.cookie("cols");
if (cookie_data) {
var cols_hidden = JSON.parse($.cookie("cols"));
/* For each of the columns check if we should hide them
* also update the checked status in the Edit columns menu
*/
$("#"+ctx.tableName+" th").each(function(){
for (var i in cols_hidden){
if ($(this).hasClass(cols_hidden[i])){
table.find("."+cols_hidden[i]).hide();
$("#checkbox-"+cols_hidden[i]).removeAttr("checked");
}
}
});
} else {
/* Disable these columns by default when we have no columns
* user setting.
*/
for (var i in defaultHiddenCols) {
table.find("."+defaultHiddenCols[i]).hide();
$("#checkbox-"+defaultHiddenCols[i]).removeAttr("checked");
}
}
}
/* Apply an ordering to the current table.
*
* 1. Find the column heading matching the sortSpecifier
* 2. Set its up/down arrow and add .sorted
*
* orderby: e.g. "-started_on", "completed_on"
* colHeading: column heading element to activate (by showing the caret
* up/down, depending on sort order); if not set, the correct column
* heading is selected from the DOM using orderby as a key
*/
function applyOrderby(orderby, colHeading) {
if (!orderby) {
return;
}
// We only have one sort at a time so remove existing sort indicators
$("#" + ctx.tableName + " th .icon-caret-down").hide();
$("#" + ctx.tableName + " th .icon-caret-up").hide();
$("#" + ctx.tableName + " th a").removeClass("sorted");
// normalise the orderby so we can use it to find the link we want
// to style
var fieldName = orderby;
if (fieldName.indexOf('-') === 0) {
fieldName = fieldName.slice(1);
}
// find the table header element which corresponds to the sort field
// (if we don't already have it)
if (!colHeading) {
colHeading = $('[data-sort-field="' + fieldName + '"]');
}
colHeading.addClass("sorted");
var parent = colHeading.parent();
if (orderby.indexOf('-') === 0) {
parent.children('.icon-caret-up').show();
}
else {
parent.children('.icon-caret-down').show();
}
tableParams.orderby = orderby;
loadData(tableParams);
}
function sortColumnClicked(e){
e.preventDefault();
/* if we're already sorted sort the other way */
var orderby = $(this).data('field-name');
if (tableParams.orderby === orderby &&
tableParams.orderby.indexOf('-') === -1) {
orderby = '-' + orderby;
}
applyOrderby(orderby, $(this));
}
function pageButtonClicked(e) {
tableParams.page = Number($(this).text());
loadData(tableParams);
/* Stop page jumps when clicking on # links */
e.preventDefault();
}
/* Toggle a table column */
function colToggleClicked (){
var col = $(this).val();
var disabled_cols = [];
if ($(this).prop("checked")) {
table.find("."+col).show();
} else {
table.find("."+col).hide();
// If we're ordered by the column we're hiding remove the order by
// and apply the default one instead
if (col === tableParams.orderby ||
'-' + col === tableParams.orderby){
tableParams.orderby = null;
applyOrderby(tableParams.default_orderby);
}
}
/* Update the cookie with the unchecked columns */
$(".col-toggle").not(":checked").map(function(){
disabled_cols.push($(this).val());
});
$.cookie("cols", JSON.stringify(disabled_cols));
}
/**
* Create the DOM/JS for the client side of a TableFilterActionToggle
* or TableFilterActionDay
*
* filterName: (string) internal name for the filter action
* filterActionData: (object)
* filterActionData.count: (number) The number of items this filter will
* show when selected
*
* NB this triggers a filtervalue event each time its radio button is checked
*/
function createActionRadio(filterName, filterActionData) {
var hasNoRecords = (Number(filterActionData.count) == 0);
var actionStr = '
' +
'' +
'
';
var action = $(actionStr);
// fire the filtervalue event from this action when the radio button
// is active so that the apply button can be enabled
action.find('[type="radio"]').change(function () {
if ($(this).is(':checked')) {
action.trigger('filtervalue', 'on');
}
});
return action;
}
/**
* Create the DOM/JS for the client side of a TableFilterActionDateRange
*
* filterName: (string) internal name for the filter action
* filterValue: (string) from,to date range in format yyyy-mm-dd,yyyy-mm-dd;
* used to select the current values for the from/to datepickers;
* if this is partial (e.g. "yyyy-mm-dd,") only the applicable datepicker
* will have a date pre-selected; if empty, neither will
* filterActionData: (object) data for generating the action's HTML
* filterActionData.title: label for the radio button
* filterActionData.max: (string) maximum date for the pickers, in ISO 8601
* datetime format
* filterActionData.min: (string) minimum date for the pickers, ISO 8601
* datetime
*
* NB this triggers a filtervalue event each time its radio button is checked
*/
function createActionDateRange(filterName, filterValue, filterActionData) {
var action = $('
' +
'' +
'
' +
'' +
'to' +
'' +
'(yyyy-mm-dd)' +
'
');
var radio = action.find('[type="radio"]');
var value = action.find('[data-value-for]');
// make the datepickers for the range
var options = {
dateFormat: 'yy-mm-dd',
maxDate: new Date(filterActionData.max),
minDate: new Date(filterActionData.min)
};
// create date pickers, setting currently-selected from and to dates
var selectedFrom = null;
var selectedTo = null;
var selectedFromAndTo = [];
if (filterValue) {
selectedFromAndTo = filterValue.split(',');
}
if (selectedFromAndTo.length == 2) {
selectedFrom = selectedFromAndTo[0];
selectedTo = selectedFromAndTo[1];
}
options.defaultDate = selectedFrom;
var inputFrom =
action.find('[data-date-from-for]').datepicker(options);
inputFrom.val(selectedFrom);
options.defaultDate = selectedTo;
var inputTo =
action.find('[data-date-to-for]').datepicker(options);
inputTo.val(selectedTo);
// set filter_value based on date pickers when
// one of their values changes; if either from or to are unset,
// the new value is null;
// this triggers a 'filter_value-change' event on the action's element,
// which is used to determine the disabled/enabled state of the "Apply"
// button
var changeHandler = function () {
var fromValue = inputFrom.val();
var toValue = inputTo.val();
var newValue = undefined;
if (fromValue !== '' && toValue !== '') {
newValue = fromValue + ',' + toValue;
}
value.val(newValue);
// if this action is selected, fire an event for the new range
if (radio.is(':checked')) {
action.trigger('filtervalue', newValue);
}
};
inputFrom.change(changeHandler);
inputTo.change(changeHandler);
// check the associated radio button on clicking a date picker
var checkRadio = function () {
radio.prop('checked', 'checked');
// checking the radio button this way doesn't cause the "change"
// event to fire, so we manually call the changeHandler
changeHandler();
};
inputFrom.focus(checkRadio);
inputTo.focus(checkRadio);
// selecting a date in a picker constrains the date you can
// set in the other picker
inputFrom.change(function () {
inputTo.datepicker('option', 'minDate', inputFrom.val());
});
inputTo.change(function () {
inputFrom.datepicker('option', 'maxDate', inputTo.val());
});
// checking the radio input causes the "Apply" button disabled state to
// change, depending on which from/to dates are supplied
radio.change(changeHandler);
return action;
}
function filterOpenClicked(){
var filterName = $(this).data('filter-name');
/* We need to pass in the current search so that the filter counts take
* into account the current search term
*/
var params = {
'name' : filterName,
'search': tableParams.search,
'cmd': 'filterinfo',
};
$.ajax({
type: "GET",
url: ctx.url,
data: params,
headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
success: function (filterData) {
/*
filterData structure:
{
title: '',
filter_actions: [
{
title: '