mirror of https://github.com/pikami/palm-paste.git
1682 lines
57 KiB
JavaScript
1682 lines
57 KiB
JavaScript
/*
|
|
* jQuery Dynatable plugin 0.3.1
|
|
*
|
|
* Copyright (c) 2014 Steve Schwartz (JangoSteve)
|
|
*
|
|
* Dual licensed under the AGPL and Proprietary licenses:
|
|
* http://www.dynatable.com/license/
|
|
*
|
|
* Date: Tue Jan 02 2014
|
|
*/
|
|
//
|
|
|
|
(function($) {
|
|
var defaults,
|
|
mergeSettings,
|
|
dt,
|
|
Model,
|
|
modelPrototypes = {
|
|
dom: Dom,
|
|
domColumns: DomColumns,
|
|
records: Records,
|
|
recordsCount: RecordsCount,
|
|
processingIndicator: ProcessingIndicator,
|
|
state: State,
|
|
sorts: Sorts,
|
|
sortsHeaders: SortsHeaders,
|
|
queries: Queries,
|
|
inputsSearch: InputsSearch,
|
|
paginationPage: PaginationPage,
|
|
paginationPerPage: PaginationPerPage,
|
|
paginationLinks: PaginationLinks
|
|
},
|
|
utility,
|
|
build,
|
|
processAll,
|
|
initModel,
|
|
defaultRowWriter,
|
|
defaultCellWriter,
|
|
defaultAttributeWriter,
|
|
defaultAttributeReader;
|
|
|
|
//-----------------------------------------------------------------
|
|
// Cached plugin global defaults
|
|
//-----------------------------------------------------------------
|
|
|
|
defaults = {
|
|
features: {
|
|
paginate: true,
|
|
sort: true,
|
|
pushState: true,
|
|
search: true,
|
|
recordCount: true,
|
|
perPageSelect: true
|
|
},
|
|
table: {
|
|
defaultColumnIdStyle: 'camelCase',
|
|
columns: null,
|
|
headRowSelector: 'thead tr', // or e.g. tr:first-child
|
|
bodyRowSelector: 'tbody tr',
|
|
headRowClass: null
|
|
},
|
|
inputs: {
|
|
queries: null,
|
|
sorts: null,
|
|
multisort: ['ctrlKey', 'shiftKey', 'metaKey'],
|
|
page: null,
|
|
queryEvent: 'blur change',
|
|
recordCountTarget: null,
|
|
recordCountPlacement: 'after',
|
|
paginationLinkTarget: null,
|
|
paginationLinkPlacement: 'after',
|
|
paginationClass: 'dynatable-pagination-links',
|
|
paginationLinkClass: 'dynatable-page-link',
|
|
paginationPrevClass: 'dynatable-page-prev',
|
|
paginationNextClass: 'dynatable-page-next',
|
|
paginationActiveClass: 'dynatable-active-page',
|
|
paginationDisabledClass: 'dynatable-disabled-page',
|
|
paginationPrev: 'Previous',
|
|
paginationNext: 'Next',
|
|
paginationGap: [1,2,2,1],
|
|
searchTarget: null,
|
|
searchPlacement: 'before',
|
|
perPageTarget: null,
|
|
perPagePlacement: 'before',
|
|
perPageText: 'Show: ',
|
|
recordCountText: 'Showing ',
|
|
processingText: 'Processing...'
|
|
},
|
|
dataset: {
|
|
ajax: false,
|
|
ajaxUrl: null,
|
|
ajaxCache: null,
|
|
ajaxOnLoad: false,
|
|
ajaxMethod: 'GET',
|
|
ajaxDataType: 'json',
|
|
totalRecordCount: null,
|
|
queries: {},
|
|
queryRecordCount: null,
|
|
page: null,
|
|
perPageDefault: 10,
|
|
perPageOptions: [10,20,50,100],
|
|
sorts: {},
|
|
sortsKeys: null,
|
|
sortTypes: {},
|
|
records: null
|
|
},
|
|
writers: {
|
|
_rowWriter: defaultRowWriter,
|
|
_cellWriter: defaultCellWriter,
|
|
_attributeWriter: defaultAttributeWriter
|
|
},
|
|
readers: {
|
|
_rowReader: null,
|
|
_attributeReader: defaultAttributeReader
|
|
},
|
|
params: {
|
|
dynatable: 'dynatable',
|
|
queries: 'queries',
|
|
sorts: 'sorts',
|
|
page: 'page',
|
|
perPage: 'perPage',
|
|
offset: 'offset',
|
|
records: 'records',
|
|
record: null,
|
|
queryRecordCount: 'queryRecordCount',
|
|
totalRecordCount: 'totalRecordCount'
|
|
}
|
|
};
|
|
|
|
//-----------------------------------------------------------------
|
|
// Each dynatable instance inherits from this,
|
|
// set properties specific to instance
|
|
//-----------------------------------------------------------------
|
|
|
|
dt = {
|
|
init: function(element, options) {
|
|
this.settings = mergeSettings(options);
|
|
this.element = element;
|
|
this.$element = $(element);
|
|
|
|
// All the setup that doesn't require element or options
|
|
build.call(this);
|
|
|
|
return this;
|
|
},
|
|
|
|
process: function(skipPushState) {
|
|
processAll.call(this, skipPushState);
|
|
}
|
|
};
|
|
|
|
//-----------------------------------------------------------------
|
|
// Cached plugin global functions
|
|
//-----------------------------------------------------------------
|
|
|
|
mergeSettings = function(options) {
|
|
var newOptions = $.extend(true, {}, defaults, options);
|
|
|
|
// TODO: figure out a better way to do this.
|
|
// Doing `extend(true)` causes any elements that are arrays
|
|
// to merge the default and options arrays instead of overriding the defaults.
|
|
if (options) {
|
|
if (options.inputs) {
|
|
if (options.inputs.multisort) {
|
|
newOptions.inputs.multisort = options.inputs.multisort;
|
|
}
|
|
if (options.inputs.paginationGap) {
|
|
newOptions.inputs.paginationGap = options.inputs.paginationGap;
|
|
}
|
|
}
|
|
if (options.dataset && options.dataset.perPageOptions) {
|
|
newOptions.dataset.perPageOptions = options.dataset.perPageOptions;
|
|
}
|
|
}
|
|
|
|
return newOptions;
|
|
};
|
|
|
|
build = function() {
|
|
this.$element.trigger('dynatable:preinit', this);
|
|
|
|
for (model in modelPrototypes) {
|
|
if (modelPrototypes.hasOwnProperty(model)) {
|
|
var modelInstance = this[model] = new modelPrototypes[model](this, this.settings);
|
|
if (modelInstance.initOnLoad()) {
|
|
modelInstance.init();
|
|
}
|
|
}
|
|
}
|
|
|
|
this.$element.trigger('dynatable:init', this);
|
|
|
|
if (!this.settings.dataset.ajax || (this.settings.dataset.ajax && this.settings.dataset.ajaxOnLoad) || this.settings.features.paginate) {
|
|
this.process();
|
|
}
|
|
};
|
|
|
|
processAll = function(skipPushState) {
|
|
var data = {};
|
|
|
|
this.$element.trigger('dynatable:beforeProcess', data);
|
|
|
|
if (!$.isEmptyObject(this.settings.dataset.queries)) { data[this.settings.params.queries] = this.settings.dataset.queries; }
|
|
// TODO: Wrap this in a try/rescue block to hide the processing indicator and indicate something went wrong if error
|
|
this.processingIndicator.show();
|
|
|
|
if (this.settings.features.sort && !$.isEmptyObject(this.settings.dataset.sorts)) { data[this.settings.params.sorts] = this.settings.dataset.sorts; }
|
|
if (this.settings.features.paginate && this.settings.dataset.page) {
|
|
var page = this.settings.dataset.page,
|
|
perPage = this.settings.dataset.perPage;
|
|
data[this.settings.params.page] = page;
|
|
data[this.settings.params.perPage] = perPage;
|
|
data[this.settings.params.offset] = (page - 1) * perPage;
|
|
}
|
|
if (this.settings.dataset.ajaxData) { $.extend(data, this.settings.dataset.ajaxData); }
|
|
|
|
// If ajax, sends query to ajaxUrl with queries and sorts serialized and appended in ajax data
|
|
// otherwise, executes queries and sorts on in-page data
|
|
if (this.settings.dataset.ajax) {
|
|
var _this = this;
|
|
var options = {
|
|
type: _this.settings.dataset.ajaxMethod,
|
|
dataType: _this.settings.dataset.ajaxDataType,
|
|
data: data,
|
|
error: function(xhr, error) {
|
|
},
|
|
success: function(response) {
|
|
_this.$element.trigger('dynatable:ajax:success', response);
|
|
// Merge ajax results and meta-data into dynatables cached data
|
|
_this.records.updateFromJson(response);
|
|
// update table with new records
|
|
_this.dom.update();
|
|
|
|
if (!skipPushState && _this.state.initOnLoad()) {
|
|
_this.state.push(data);
|
|
}
|
|
},
|
|
complete: function() {
|
|
_this.processingIndicator.hide();
|
|
}
|
|
};
|
|
// Do not pass url to `ajax` options if blank
|
|
if (this.settings.dataset.ajaxUrl) {
|
|
options.url = this.settings.dataset.ajaxUrl;
|
|
|
|
// If ajaxUrl is blank, then we're using the current page URL,
|
|
// we need to strip out any query, sort, or page data controlled by dynatable
|
|
// that may have been in URL when page loaded, so that it doesn't conflict with
|
|
// what's passed in with the data ajax parameter
|
|
} else {
|
|
options.url = utility.refreshQueryString(window.location.href, {}, this.settings);
|
|
}
|
|
if (this.settings.dataset.ajaxCache !== null) { options.cache = this.settings.dataset.ajaxCache; }
|
|
|
|
$.ajax(options);
|
|
} else {
|
|
this.records.resetOriginal();
|
|
this.queries.run();
|
|
if (this.settings.features.sort) {
|
|
this.records.sort();
|
|
}
|
|
if (this.settings.features.paginate) {
|
|
this.records.paginate();
|
|
}
|
|
this.dom.update();
|
|
this.processingIndicator.hide();
|
|
|
|
if (!skipPushState && this.state.initOnLoad()) {
|
|
this.state.push(data);
|
|
}
|
|
}
|
|
this.$element.trigger('dynatable:afterProcess', data);
|
|
};
|
|
|
|
function defaultRowWriter(rowIndex, record, columns, cellWriter) {
|
|
var tr = '';
|
|
|
|
// grab the record's attribute for each column
|
|
for (var i = 0, len = columns.length; i < len; i++) {
|
|
tr += cellWriter(columns[i], record);
|
|
}
|
|
|
|
return '<tr>' + tr + '</tr>';
|
|
};
|
|
|
|
function defaultCellWriter(column, record) {
|
|
var html = column.attributeWriter(record),
|
|
td = '<td';
|
|
|
|
if (column.hidden || column.textAlign) {
|
|
td += ' style="';
|
|
|
|
// keep cells for hidden column headers hidden
|
|
if (column.hidden) {
|
|
td += 'display: none;';
|
|
}
|
|
|
|
// keep cells aligned as their column headers are aligned
|
|
if (column.textAlign) {
|
|
td += 'text-align: ' + column.textAlign + ';';
|
|
}
|
|
|
|
td += '"';
|
|
}
|
|
|
|
return td + '>' + html + '</td>';
|
|
};
|
|
|
|
function defaultAttributeWriter(record) {
|
|
// `this` is the column object in settings.columns
|
|
// TODO: automatically convert common types, such as arrays and objects, to string
|
|
return record[this.id];
|
|
};
|
|
|
|
function defaultAttributeReader(cell, record) {
|
|
return $(cell).html();
|
|
};
|
|
|
|
//-----------------------------------------------------------------
|
|
// Dynatable object model prototype
|
|
// (all object models get these default functions)
|
|
//-----------------------------------------------------------------
|
|
|
|
Model = {
|
|
initOnLoad: function() {
|
|
return true;
|
|
},
|
|
|
|
init: function() {}
|
|
};
|
|
|
|
for (model in modelPrototypes) {
|
|
if (modelPrototypes.hasOwnProperty(model)) {
|
|
var modelPrototype = modelPrototypes[model];
|
|
modelPrototype.prototype = Model;
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------
|
|
// Dynatable object models
|
|
//-----------------------------------------------------------------
|
|
|
|
function Dom(obj, settings) {
|
|
var _this = this;
|
|
|
|
// update table contents with new records array
|
|
// from query (whether ajax or not)
|
|
this.update = function() {
|
|
var rows = '',
|
|
columns = settings.table.columns,
|
|
rowWriter = settings.writers._rowWriter,
|
|
cellWriter = settings.writers._cellWriter;
|
|
|
|
obj.$element.trigger('dynatable:beforeUpdate', rows);
|
|
|
|
// loop through records
|
|
for (var i = 0, len = settings.dataset.records.length; i < len; i++) {
|
|
var record = settings.dataset.records[i],
|
|
tr = rowWriter(i, record, columns, cellWriter);
|
|
rows += tr;
|
|
}
|
|
|
|
// Appended dynatable interactive elements
|
|
if (settings.features.recordCount) {
|
|
$('#dynatable-record-count-' + obj.element.id).replaceWith(obj.recordsCount.create());
|
|
}
|
|
if (settings.features.paginate) {
|
|
$('#dynatable-pagination-links-' + obj.element.id).replaceWith(obj.paginationLinks.create());
|
|
if (settings.features.perPageSelect) {
|
|
$('#dynatable-per-page-' + obj.element.id).val(parseInt(settings.dataset.perPage));
|
|
}
|
|
}
|
|
|
|
// Sort headers functionality
|
|
if (settings.features.sort && columns) {
|
|
obj.sortsHeaders.removeAllArrows();
|
|
for (var i = 0, len = columns.length; i < len; i++) {
|
|
var column = columns[i],
|
|
sortedByColumn = utility.allMatch(settings.dataset.sorts, column.sorts, function(sorts, sort) { return sort in sorts; }),
|
|
value = settings.dataset.sorts[column.sorts[0]];
|
|
|
|
if (sortedByColumn) {
|
|
obj.$element.find('[data-dynatable-column="' + column.id + '"]').find('.dynatable-sort-header').each(function(){
|
|
if (value == 1) {
|
|
obj.sortsHeaders.appendArrowUp($(this));
|
|
} else {
|
|
obj.sortsHeaders.appendArrowDown($(this));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Query search functionality
|
|
if (settings.inputs.queries || settings.features.search) {
|
|
var allQueries = settings.inputs.queries || $();
|
|
if (settings.features.search) {
|
|
allQueries = allQueries.add('#dynatable-query-search-' + obj.element.id);
|
|
}
|
|
|
|
allQueries.each(function() {
|
|
var $this = $(this),
|
|
q = settings.dataset.queries[$this.data('dynatable-query')];
|
|
$this.val(q || '');
|
|
});
|
|
}
|
|
|
|
obj.$element.find(settings.table.bodyRowSelector).remove();
|
|
obj.$element.append(rows);
|
|
|
|
obj.$element.trigger('dynatable:afterUpdate', rows);
|
|
};
|
|
};
|
|
|
|
function DomColumns(obj, settings) {
|
|
var _this = this;
|
|
|
|
this.initOnLoad = function() {
|
|
return obj.$element.is('table');
|
|
};
|
|
|
|
this.init = function() {
|
|
settings.table.columns = [];
|
|
this.getFromTable();
|
|
};
|
|
|
|
// initialize table[columns] array
|
|
this.getFromTable = function() {
|
|
var $columns = obj.$element.find(settings.table.headRowSelector).children('th,td');
|
|
if ($columns.length) {
|
|
$columns.each(function(index){
|
|
_this.add($(this), index, true);
|
|
});
|
|
} else {
|
|
return $.error("Couldn't find any columns headers in '" + settings.table.headRowSelector + " th,td'. If your header row is different, specify the selector in the table: headRowSelector option.");
|
|
}
|
|
};
|
|
|
|
this.add = function($column, position, skipAppend, skipUpdate) {
|
|
var columns = settings.table.columns,
|
|
label = $column.text(),
|
|
id = $column.data('dynatable-column') || utility.normalizeText(label, settings.table.defaultColumnIdStyle),
|
|
dataSorts = $column.data('dynatable-sorts'),
|
|
sorts = dataSorts ? $.map(dataSorts.split(','), function(text) { return $.trim(text); }) : [id];
|
|
|
|
// If the column id is blank, generate an id for it
|
|
if ( !id ) {
|
|
this.generate($column);
|
|
id = $column.data('dynatable-column');
|
|
}
|
|
// Add column data to plugin instance
|
|
columns.splice(position, 0, {
|
|
index: position,
|
|
label: label,
|
|
id: id,
|
|
attributeWriter: settings.writers[id] || settings.writers._attributeWriter,
|
|
attributeReader: settings.readers[id] || settings.readers._attributeReader,
|
|
sorts: sorts,
|
|
hidden: $column.css('display') === 'none',
|
|
textAlign: $column.css('text-align')
|
|
});
|
|
|
|
// Modify header cell
|
|
$column
|
|
.attr('data-dynatable-column', id)
|
|
.addClass('dynatable-head');
|
|
if (settings.table.headRowClass) { $column.addClass(settings.table.headRowClass); }
|
|
|
|
// Append column header to table
|
|
if (!skipAppend) {
|
|
var domPosition = position + 1,
|
|
$sibling = obj.$element.find(settings.table.headRowSelector)
|
|
.children('th:nth-child(' + domPosition + '),td:nth-child(' + domPosition + ')').first(),
|
|
columnsAfter = columns.slice(position + 1, columns.length);
|
|
|
|
if ($sibling.length) {
|
|
$sibling.before($column);
|
|
// sibling column doesn't yet exist (maybe this is the last column in the header row)
|
|
} else {
|
|
obj.$element.find(settings.table.headRowSelector).append($column);
|
|
}
|
|
|
|
obj.sortsHeaders.attachOne($column.get());
|
|
|
|
// increment the index of all columns after this one that was just inserted
|
|
if (columnsAfter.length) {
|
|
for (var i = 0, len = columnsAfter.length; i < len; i++) {
|
|
columnsAfter[i].index += 1;
|
|
}
|
|
}
|
|
|
|
if (!skipUpdate) {
|
|
obj.dom.update();
|
|
}
|
|
}
|
|
|
|
return dt;
|
|
};
|
|
|
|
this.remove = function(columnIndexOrId) {
|
|
var columns = settings.table.columns,
|
|
length = columns.length;
|
|
|
|
if (typeof(columnIndexOrId) === "number") {
|
|
var column = columns[columnIndexOrId];
|
|
this.removeFromTable(column.id);
|
|
this.removeFromArray(columnIndexOrId);
|
|
} else {
|
|
// Traverse columns array in reverse order so that subsequent indices
|
|
// don't get messed up when we delete an item from the array in an iteration
|
|
for (var i = columns.length - 1; i >= 0; i--) {
|
|
var column = columns[i];
|
|
|
|
if (column.id === columnIndexOrId) {
|
|
this.removeFromTable(columnIndexOrId);
|
|
this.removeFromArray(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
obj.dom.update();
|
|
};
|
|
|
|
this.removeFromTable = function(columnId) {
|
|
obj.$element.find(settings.table.headRowSelector).children('[data-dynatable-column="' + columnId + '"]').first()
|
|
.remove();
|
|
};
|
|
|
|
this.removeFromArray = function(index) {
|
|
var columns = settings.table.columns,
|
|
adjustColumns;
|
|
columns.splice(index, 1);
|
|
adjustColumns = columns.slice(index, columns.length);
|
|
for (var i = 0, len = adjustColumns.length; i < len; i++) {
|
|
adjustColumns[i].index -= 1;
|
|
}
|
|
};
|
|
|
|
this.generate = function($cell) {
|
|
var cell = $cell === undefined ? $('<th></th>') : $cell;
|
|
return this.attachGeneratedAttributes(cell);
|
|
};
|
|
|
|
this.attachGeneratedAttributes = function($cell) {
|
|
// Use increment to create unique column name that is the same each time the page is reloaded,
|
|
// in order to avoid errors with mismatched attribute names when loading cached `dataset.records` array
|
|
var increment = obj.$element.find(settings.table.headRowSelector).children('th[data-dynatable-generated]').length;
|
|
return $cell
|
|
.attr('data-dynatable-column', 'dynatable-generated-' + increment) //+ utility.randomHash(),
|
|
.attr('data-dynatable-no-sort', 'true')
|
|
.attr('data-dynatable-generated', increment);
|
|
};
|
|
};
|
|
|
|
function Records(obj, settings) {
|
|
var _this = this;
|
|
|
|
this.initOnLoad = function() {
|
|
return !settings.dataset.ajax;
|
|
};
|
|
|
|
this.init = function() {
|
|
if (settings.dataset.records === null) {
|
|
settings.dataset.records = this.getFromTable();
|
|
|
|
if (!settings.dataset.queryRecordCount) {
|
|
settings.dataset.queryRecordCount = this.count();
|
|
}
|
|
|
|
if (!settings.dataset.totalRecordCount){
|
|
settings.dataset.totalRecordCount = settings.dataset.queryRecordCount;
|
|
}
|
|
}
|
|
|
|
// Create cache of original full recordset (unpaginated and unqueried)
|
|
settings.dataset.originalRecords = $.extend(true, [], settings.dataset.records);
|
|
};
|
|
|
|
// merge ajax response json with cached data including
|
|
// meta-data and records
|
|
this.updateFromJson = function(data) {
|
|
var records;
|
|
if (settings.params.records === "_root") {
|
|
records = data;
|
|
} else if (settings.params.records in data) {
|
|
records = data[settings.params.records];
|
|
}
|
|
if (settings.params.record) {
|
|
var len = records.length - 1;
|
|
for (var i = 0; i < len; i++) {
|
|
records[i] = records[i][settings.params.record];
|
|
}
|
|
}
|
|
if (settings.params.queryRecordCount in data) {
|
|
settings.dataset.queryRecordCount = data[settings.params.queryRecordCount];
|
|
}
|
|
if (settings.params.totalRecordCount in data) {
|
|
settings.dataset.totalRecordCount = data[settings.params.totalRecordCount];
|
|
}
|
|
settings.dataset.records = records;
|
|
};
|
|
|
|
// For really advanced sorting,
|
|
// see http://james.padolsey.com/javascript/sorting-elements-with-jquery/
|
|
this.sort = function() {
|
|
var sort = [].sort,
|
|
sorts = settings.dataset.sorts,
|
|
sortsKeys = settings.dataset.sortsKeys,
|
|
sortTypes = settings.dataset.sortTypes;
|
|
|
|
var sortFunction = function(a, b) {
|
|
var comparison;
|
|
if ($.isEmptyObject(sorts)) {
|
|
comparison = obj.sorts.functions['originalPlacement'](a, b);
|
|
} else {
|
|
for (var i = 0, len = sortsKeys.length; i < len; i++) {
|
|
var attr = sortsKeys[i],
|
|
direction = sorts[attr],
|
|
sortType = sortTypes[attr] || obj.sorts.guessType(a, b, attr);
|
|
comparison = obj.sorts.functions[sortType](a, b, attr, direction);
|
|
// Don't need to sort any further unless this sort is a tie between a and b,
|
|
// so break the for loop unless tied
|
|
if (comparison !== 0) { break; }
|
|
}
|
|
}
|
|
return comparison;
|
|
}
|
|
|
|
return sort.call(settings.dataset.records, sortFunction);
|
|
};
|
|
|
|
this.paginate = function() {
|
|
var bounds = this.pageBounds(),
|
|
first = bounds[0], last = bounds[1];
|
|
settings.dataset.records = settings.dataset.records.slice(first, last);
|
|
};
|
|
|
|
this.resetOriginal = function() {
|
|
settings.dataset.records = settings.dataset.originalRecords || [];
|
|
};
|
|
|
|
this.pageBounds = function() {
|
|
var page = settings.dataset.page || 1,
|
|
first = (page - 1) * settings.dataset.perPage,
|
|
last = Math.min(first + settings.dataset.perPage, settings.dataset.queryRecordCount);
|
|
return [first,last];
|
|
};
|
|
|
|
// get initial recordset to populate table
|
|
// if ajax, call ajaxUrl
|
|
// otherwise, initialize from in-table records
|
|
this.getFromTable = function() {
|
|
var records = [],
|
|
columns = settings.table.columns,
|
|
tableRecords = obj.$element.find(settings.table.bodyRowSelector);
|
|
|
|
tableRecords.each(function(index){
|
|
var record = {};
|
|
record['dynatable-original-index'] = index;
|
|
$(this).find('th,td').each(function(index) {
|
|
if (columns[index] === undefined) {
|
|
// Header cell didn't exist for this column, so let's generate and append
|
|
// a new header cell with a randomly generated name (so we can store and
|
|
// retrieve the contents of this column for each record)
|
|
obj.domColumns.add(obj.domColumns.generate(), columns.length, false, true); // don't skipAppend, do skipUpdate
|
|
}
|
|
var value = columns[index].attributeReader(this, record),
|
|
attr = columns[index].id;
|
|
|
|
// If value from table is HTML, let's get and cache the text equivalent for
|
|
// the default string sorting, since it rarely makes sense for sort headers
|
|
// to sort based on HTML tags.
|
|
if (typeof(value) === "string" && value.match(/\s*\<.+\>/)) {
|
|
if (! record['dynatable-sortable-text']) {
|
|
record['dynatable-sortable-text'] = {};
|
|
}
|
|
record['dynatable-sortable-text'][attr] = $.trim($('<div></div>').html(value).text());
|
|
}
|
|
|
|
record[attr] = value;
|
|
});
|
|
// Allow configuration function which alters record based on attributes of
|
|
// table row (e.g. from html5 data- attributes)
|
|
if (typeof(settings.readers._rowReader) === "function") {
|
|
settings.readers._rowReader(index, this, record);
|
|
}
|
|
records.push(record);
|
|
});
|
|
return records; // 1st row is header
|
|
};
|
|
|
|
// count records from table
|
|
this.count = function() {
|
|
return settings.dataset.records.length;
|
|
};
|
|
};
|
|
|
|
function RecordsCount(obj, settings) {
|
|
this.initOnLoad = function() {
|
|
return settings.features.recordCount;
|
|
};
|
|
|
|
this.init = function() {
|
|
this.attach();
|
|
};
|
|
|
|
this.create = function() {
|
|
var recordsShown = obj.records.count(),
|
|
recordsQueryCount = settings.dataset.queryRecordCount,
|
|
recordsTotal = settings.dataset.totalRecordCount,
|
|
text = settings.inputs.recordCountText,
|
|
collection_name = settings.params.records;
|
|
|
|
if (recordsShown < recordsQueryCount && settings.features.paginate) {
|
|
var bounds = obj.records.pageBounds();
|
|
text += "<span class='dynatable-record-bounds'>" + (bounds[0] + 1) + " to " + bounds[1] + "</span> of ";
|
|
} else if (recordsShown === recordsQueryCount && settings.features.paginate) {
|
|
text += recordsShown + " of ";
|
|
}
|
|
text += recordsQueryCount + " " + collection_name;
|
|
if (recordsQueryCount < recordsTotal) {
|
|
text += " (filtered from " + recordsTotal + " total records)";
|
|
}
|
|
|
|
return $('<span></span>', {
|
|
id: 'dynatable-record-count-' + obj.element.id,
|
|
'class': 'dynatable-record-count',
|
|
html: text
|
|
});
|
|
};
|
|
|
|
this.attach = function() {
|
|
var $target = settings.inputs.recordCountTarget ? $(settings.inputs.recordCountTarget) : obj.$element;
|
|
$target[settings.inputs.recordCountPlacement](this.create());
|
|
};
|
|
};
|
|
|
|
function ProcessingIndicator(obj, settings) {
|
|
this.init = function() {
|
|
this.attach();
|
|
};
|
|
|
|
this.create = function() {
|
|
var $processing = $('<div></div>', {
|
|
html: '<span>' + settings.inputs.processingText + '</span>',
|
|
id: 'dynatable-processing-' + obj.element.id,
|
|
'class': 'dynatable-processing',
|
|
style: 'position: absolute; display: none;'
|
|
});
|
|
|
|
return $processing;
|
|
};
|
|
|
|
this.position = function() {
|
|
var $processing = $('#dynatable-processing-' + obj.element.id),
|
|
$span = $processing.children('span'),
|
|
spanHeight = $span.outerHeight(),
|
|
spanWidth = $span.outerWidth(),
|
|
$covered = obj.$element,
|
|
offset = $covered.offset(),
|
|
height = $covered.outerHeight(), width = $covered.outerWidth();
|
|
|
|
$processing
|
|
.offset({left: offset.left, top: offset.top})
|
|
.width(width)
|
|
.height(height)
|
|
$span
|
|
.offset({left: offset.left + ( (width - spanWidth) / 2 ), top: offset.top + ( (height - spanHeight) / 2 )});
|
|
|
|
return $processing;
|
|
};
|
|
|
|
this.attach = function() {
|
|
obj.$element.before(this.create());
|
|
};
|
|
|
|
this.show = function() {
|
|
$('#dynatable-processing-' + obj.element.id).show();
|
|
this.position();
|
|
};
|
|
|
|
this.hide = function() {
|
|
$('#dynatable-processing-' + obj.element.id).hide();
|
|
};
|
|
};
|
|
|
|
function State(obj, settings) {
|
|
this.initOnLoad = function() {
|
|
// Check if pushState option is true, and if browser supports it
|
|
return settings.features.pushState && history.pushState;
|
|
};
|
|
|
|
this.init = function() {
|
|
window.onpopstate = function(event) {
|
|
if (event.state && event.state.dynatable) {
|
|
obj.state.pop(event);
|
|
}
|
|
}
|
|
};
|
|
|
|
this.push = function(data) {
|
|
var urlString = window.location.search,
|
|
urlOptions,
|
|
path,
|
|
params,
|
|
hash,
|
|
newParams,
|
|
cacheStr,
|
|
cache,
|
|
// replaceState on initial load, then pushState after that
|
|
firstPush = !(window.history.state && window.history.state.dynatable),
|
|
pushFunction = firstPush ? 'replaceState' : 'pushState';
|
|
|
|
if (urlString && /^\?/.test(urlString)) { urlString = urlString.substring(1); }
|
|
$.extend(urlOptions, data);
|
|
|
|
params = utility.refreshQueryString(urlString, data, settings);
|
|
if (params) { params = '?' + params; }
|
|
hash = window.location.hash;
|
|
path = window.location.pathname;
|
|
|
|
obj.$element.trigger('dynatable:push', data);
|
|
|
|
cache = { dynatable: { dataset: settings.dataset } };
|
|
if (!firstPush) { cache.dynatable.scrollTop = $(window).scrollTop(); }
|
|
cacheStr = JSON.stringify(cache);
|
|
|
|
// Mozilla has a 640k char limit on what can be stored in pushState.
|
|
// See "limit" in https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history#The_pushState().C2.A0method
|
|
// and "dataStr.length" in http://wine.git.sourceforge.net/git/gitweb.cgi?p=wine/wine-gecko;a=patch;h=43a11bdddc5fc1ff102278a120be66a7b90afe28
|
|
//
|
|
// Likewise, other browsers may have varying (undocumented) limits.
|
|
// Also, Firefox's limit can be changed in about:config as browser.history.maxStateObjectSize
|
|
// Since we don't know what the actual limit will be in any given situation, we'll just try caching and rescue
|
|
// any exceptions by retrying pushState without caching the records.
|
|
//
|
|
// I have absolutely no idea why perPageOptions suddenly becomes an array-like object instead of an array,
|
|
// but just recently, this started throwing an error if I don't convert it:
|
|
// 'Uncaught Error: DATA_CLONE_ERR: DOM Exception 25'
|
|
cache.dynatable.dataset.perPageOptions = $.makeArray(cache.dynatable.dataset.perPageOptions);
|
|
|
|
try {
|
|
window.history[pushFunction](cache, "Dynatable state", path + params + hash);
|
|
} catch(error) {
|
|
// Make cached records = null, so that `pop` will rerun process to retrieve records
|
|
cache.dynatable.dataset.records = null;
|
|
window.history[pushFunction](cache, "Dynatable state", path + params + hash);
|
|
}
|
|
};
|
|
|
|
this.pop = function(event) {
|
|
var data = event.state.dynatable;
|
|
settings.dataset = data.dataset;
|
|
|
|
if (data.scrollTop) { $(window).scrollTop(data.scrollTop); }
|
|
|
|
// If dataset.records is cached from pushState
|
|
if ( data.dataset.records ) {
|
|
obj.dom.update();
|
|
} else {
|
|
obj.process(true);
|
|
}
|
|
};
|
|
};
|
|
|
|
function Sorts(obj, settings) {
|
|
this.initOnLoad = function() {
|
|
return settings.features.sort;
|
|
};
|
|
|
|
this.init = function() {
|
|
var sortsUrl = window.location.search.match(new RegExp(settings.params.sorts + '[^&=]*=[^&]*', 'g'));
|
|
settings.dataset.sorts = sortsUrl ? utility.deserialize(sortsUrl)[settings.params.sorts] : {};
|
|
settings.dataset.sortsKeys = sortsUrl ? utility.keysFromObject(settings.dataset.sorts) : [];
|
|
};
|
|
|
|
this.add = function(attr, direction) {
|
|
var sortsKeys = settings.dataset.sortsKeys,
|
|
index = $.inArray(attr, sortsKeys);
|
|
settings.dataset.sorts[attr] = direction;
|
|
if (index === -1) { sortsKeys.push(attr); }
|
|
return dt;
|
|
};
|
|
|
|
this.remove = function(attr) {
|
|
var sortsKeys = settings.dataset.sortsKeys,
|
|
index = $.inArray(attr, sortsKeys);
|
|
delete settings.dataset.sorts[attr];
|
|
if (index !== -1) { sortsKeys.splice(index, 1); }
|
|
return dt;
|
|
};
|
|
|
|
this.clear = function() {
|
|
settings.dataset.sorts = {};
|
|
settings.dataset.sortsKeys.length = 0;
|
|
};
|
|
|
|
// Try to intelligently guess which sort function to use
|
|
// based on the type of attribute values.
|
|
// Consider using something more robust than `typeof` (http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/)
|
|
this.guessType = function(a, b, attr) {
|
|
var types = {
|
|
string: 'string',
|
|
number: 'number',
|
|
'boolean': 'number',
|
|
object: 'number' // dates and null values are also objects, this works...
|
|
},
|
|
attrType = a[attr] ? typeof(a[attr]) : typeof(b[attr]),
|
|
type = types[attrType] || 'number';
|
|
return type;
|
|
};
|
|
|
|
// Built-in sort functions
|
|
// (the most common use-cases I could think of)
|
|
this.functions = {
|
|
number: function(a, b, attr, direction) {
|
|
return a[attr] === b[attr] ? 0 : (direction > 0 ? a[attr] - b[attr] : b[attr] - a[attr]);
|
|
},
|
|
string: function(a, b, attr, direction) {
|
|
var aAttr = (a['dynatable-sortable-text'] && a['dynatable-sortable-text'][attr]) ? a['dynatable-sortable-text'][attr] : a[attr],
|
|
bAttr = (b['dynatable-sortable-text'] && b['dynatable-sortable-text'][attr]) ? b['dynatable-sortable-text'][attr] : b[attr],
|
|
comparison;
|
|
aAttr = aAttr.toLowerCase();
|
|
bAttr = bAttr.toLowerCase();
|
|
comparison = aAttr === bAttr ? 0 : (direction > 0 ? aAttr > bAttr : bAttr > aAttr);
|
|
// force false boolean value to -1, true to 1, and tie to 0
|
|
return comparison === false ? -1 : (comparison - 0);
|
|
},
|
|
originalPlacement: function(a, b) {
|
|
return a['dynatable-original-index'] - b['dynatable-original-index'];
|
|
}
|
|
};
|
|
};
|
|
|
|
// turn table headers into links which add sort to sorts array
|
|
function SortsHeaders(obj, settings) {
|
|
var _this = this;
|
|
|
|
this.initOnLoad = function() {
|
|
return settings.features.sort;
|
|
};
|
|
|
|
this.init = function() {
|
|
this.attach();
|
|
};
|
|
|
|
this.create = function(cell) {
|
|
var $cell = $(cell),
|
|
$link = $('<a></a>', {
|
|
'class': 'dynatable-sort-header',
|
|
href: '#',
|
|
html: $cell.html()
|
|
}),
|
|
id = $cell.data('dynatable-column'),
|
|
column = utility.findObjectInArray(settings.table.columns, {id: id});
|
|
|
|
$link.bind('click', function(e) {
|
|
_this.toggleSort(e, $link, column);
|
|
obj.process();
|
|
|
|
e.preventDefault();
|
|
});
|
|
|
|
if (this.sortedByColumn($link, column)) {
|
|
if (this.sortedByColumnValue(column) == 1) {
|
|
this.appendArrowUp($link);
|
|
} else {
|
|
this.appendArrowDown($link);
|
|
}
|
|
}
|
|
|
|
return $link;
|
|
};
|
|
|
|
this.removeAll = function() {
|
|
obj.$element.find(settings.table.headRowSelector).children('th,td').each(function(){
|
|
_this.removeAllArrows();
|
|
_this.removeOne(this);
|
|
});
|
|
};
|
|
|
|
this.removeOne = function(cell) {
|
|
var $cell = $(cell),
|
|
$link = $cell.find('.dynatable-sort-header');
|
|
if ($link.length) {
|
|
var html = $link.html();
|
|
$link.remove();
|
|
$cell.html($cell.html() + html);
|
|
}
|
|
};
|
|
|
|
this.attach = function() {
|
|
obj.$element.find(settings.table.headRowSelector).children('th,td').each(function(){
|
|
_this.attachOne(this);
|
|
});
|
|
};
|
|
|
|
this.attachOne = function(cell) {
|
|
var $cell = $(cell);
|
|
if (!$cell.data('dynatable-no-sort')) {
|
|
$cell.html(this.create(cell));
|
|
}
|
|
};
|
|
|
|
this.appendArrowUp = function($link) {
|
|
this.removeArrow($link);
|
|
$link.append("<span class='dynatable-arrow'> ▲</span>");
|
|
};
|
|
|
|
this.appendArrowDown = function($link) {
|
|
this.removeArrow($link);
|
|
$link.append("<span class='dynatable-arrow'> ▼</span>");
|
|
};
|
|
|
|
this.removeArrow = function($link) {
|
|
// Not sure why `parent()` is needed, the arrow should be inside the link from `append()` above
|
|
$link.find('.dynatable-arrow').remove();
|
|
};
|
|
|
|
this.removeAllArrows = function() {
|
|
obj.$element.find('.dynatable-arrow').remove();
|
|
};
|
|
|
|
this.toggleSort = function(e, $link, column) {
|
|
var sortedByColumn = this.sortedByColumn($link, column),
|
|
value = this.sortedByColumnValue(column);
|
|
// Clear existing sorts unless this is a multisort event
|
|
if (!settings.inputs.multisort || !utility.anyMatch(e, settings.inputs.multisort, function(evt, key) { return e[key]; })) {
|
|
this.removeAllArrows();
|
|
obj.sorts.clear();
|
|
}
|
|
|
|
// If sorts for this column are already set
|
|
if (sortedByColumn) {
|
|
// If ascending, then make descending
|
|
if (value == 1) {
|
|
for (var i = 0, len = column.sorts.length; i < len; i++) {
|
|
obj.sorts.add(column.sorts[i], -1);
|
|
}
|
|
this.appendArrowDown($link);
|
|
// If descending, remove sort
|
|
} else {
|
|
for (var i = 0, len = column.sorts.length; i < len; i++) {
|
|
obj.sorts.remove(column.sorts[i]);
|
|
}
|
|
this.removeArrow($link);
|
|
}
|
|
// Otherwise, if not already set, set to ascending
|
|
} else {
|
|
for (var i = 0, len = column.sorts.length; i < len; i++) {
|
|
obj.sorts.add(column.sorts[i], 1);
|
|
}
|
|
this.appendArrowUp($link);
|
|
}
|
|
};
|
|
|
|
this.sortedByColumn = function($link, column) {
|
|
return utility.allMatch(settings.dataset.sorts, column.sorts, function(sorts, sort) { return sort in sorts; });
|
|
};
|
|
|
|
this.sortedByColumnValue = function(column) {
|
|
return settings.dataset.sorts[column.sorts[0]];
|
|
};
|
|
};
|
|
|
|
function Queries(obj, settings) {
|
|
var _this = this;
|
|
|
|
this.initOnLoad = function() {
|
|
return settings.inputs.queries || settings.features.search;
|
|
};
|
|
|
|
this.init = function() {
|
|
var queriesUrl = window.location.search.match(new RegExp(settings.params.queries + '[^&=]*=[^&]*', 'g'));
|
|
|
|
settings.dataset.queries = queriesUrl ? utility.deserialize(queriesUrl)[settings.params.queries] : {};
|
|
if (settings.dataset.queries === "") { settings.dataset.queries = {}; }
|
|
|
|
if (settings.inputs.queries) {
|
|
this.setupInputs();
|
|
}
|
|
};
|
|
|
|
this.add = function(name, value) {
|
|
// reset to first page since query will change records
|
|
if (settings.features.paginate) {
|
|
settings.dataset.page = 1;
|
|
}
|
|
settings.dataset.queries[name] = value;
|
|
return dt;
|
|
};
|
|
|
|
this.remove = function(name) {
|
|
delete settings.dataset.queries[name];
|
|
return dt;
|
|
};
|
|
|
|
this.run = function() {
|
|
for (query in settings.dataset.queries) {
|
|
if (settings.dataset.queries.hasOwnProperty(query)) {
|
|
var value = settings.dataset.queries[query];
|
|
if (_this.functions[query] === undefined) {
|
|
// Try to lazily evaluate query from column names if not explicitly defined
|
|
var queryColumn = utility.findObjectInArray(settings.table.columns, {id: query});
|
|
if (queryColumn) {
|
|
_this.functions[query] = function(record, queryValue) {
|
|
return record[query] == queryValue;
|
|
};
|
|
} else {
|
|
$.error("Query named '" + query + "' called, but not defined in queries.functions");
|
|
continue; // to skip to next query
|
|
}
|
|
}
|
|
// collect all records that return true for query
|
|
settings.dataset.records = $.map(settings.dataset.records, function(record) {
|
|
return _this.functions[query](record, value) ? record : null;
|
|
});
|
|
}
|
|
}
|
|
settings.dataset.queryRecordCount = obj.records.count();
|
|
};
|
|
|
|
// Shortcut for performing simple query from built-in search
|
|
this.runSearch = function(q) {
|
|
var origQueries = $.extend({}, settings.dataset.queries);
|
|
if (q) {
|
|
this.add('search', q);
|
|
} else {
|
|
this.remove('search');
|
|
}
|
|
if (!utility.objectsEqual(settings.dataset.queries, origQueries)) {
|
|
obj.process();
|
|
}
|
|
};
|
|
|
|
this.setupInputs = function() {
|
|
settings.inputs.queries.each(function() {
|
|
var $this = $(this),
|
|
event = $this.data('dynatable-query-event') || settings.inputs.queryEvent,
|
|
query = $this.data('dynatable-query') || $this.attr('name') || this.id,
|
|
queryFunction = function(e) {
|
|
var q = $(this).val();
|
|
if (q === "") { q = undefined; }
|
|
if (q === settings.dataset.queries[query]) { return false; }
|
|
if (q) {
|
|
_this.add(query, q);
|
|
} else {
|
|
_this.remove(query);
|
|
}
|
|
obj.process();
|
|
e.preventDefault();
|
|
};
|
|
|
|
$this
|
|
.attr('data-dynatable-query', query)
|
|
.bind(event, queryFunction)
|
|
.bind('keypress', function(e) {
|
|
if (e.which == 13) {
|
|
queryFunction.call(this, e);
|
|
}
|
|
});
|
|
|
|
if (settings.dataset.queries[query]) { $this.val(decodeURIComponent(settings.dataset.queries[query])); }
|
|
});
|
|
};
|
|
|
|
// Query functions for in-page querying
|
|
// each function should take a record and a value as input
|
|
// and output true of false as to whether the record is a match or not
|
|
this.functions = {
|
|
search: function(record, queryValue) {
|
|
var contains = false;
|
|
// Loop through each attribute of record
|
|
for (attr in record) {
|
|
if (record.hasOwnProperty(attr)) {
|
|
var attrValue = record[attr];
|
|
if (typeof(attrValue) === "string" && attrValue.toLowerCase().indexOf(queryValue.toLowerCase()) !== -1) {
|
|
contains = true;
|
|
// Don't need to keep searching attributes once found
|
|
break;
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
return contains;
|
|
}
|
|
};
|
|
};
|
|
|
|
function InputsSearch(obj, settings) {
|
|
var _this = this;
|
|
|
|
this.initOnLoad = function() {
|
|
return settings.features.search;
|
|
};
|
|
|
|
this.init = function() {
|
|
this.attach();
|
|
};
|
|
|
|
this.create = function() {
|
|
var $search = $('<input />', {
|
|
type: 'search',
|
|
id: 'dynatable-query-search-' + obj.element.id,
|
|
'data-dynatable-query': 'search',
|
|
value: settings.dataset.queries.search
|
|
}),
|
|
$searchSpan = $('<span></span>', {
|
|
id: 'dynatable-search-' + obj.element.id,
|
|
'class': 'dynatable-search',
|
|
text: 'Search: '
|
|
}).append($search);
|
|
|
|
$search
|
|
.bind(settings.inputs.queryEvent, function() {
|
|
obj.queries.runSearch($(this).val());
|
|
})
|
|
.bind('keypress', function(e) {
|
|
if (e.which == 13) {
|
|
obj.queries.runSearch($(this).val());
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
return $searchSpan;
|
|
};
|
|
|
|
this.attach = function() {
|
|
var $target = settings.inputs.searchTarget ? $(settings.inputs.searchTarget) : obj.$element;
|
|
$target[settings.inputs.searchPlacement](this.create());
|
|
};
|
|
};
|
|
|
|
// provide a public function for selecting page
|
|
function PaginationPage(obj, settings) {
|
|
this.initOnLoad = function() {
|
|
return settings.features.paginate;
|
|
};
|
|
|
|
this.init = function() {
|
|
var pageUrl = window.location.search.match(new RegExp(settings.params.page + '=([^&]*)'));
|
|
// If page is present in URL parameters and pushState is enabled
|
|
// (meaning that it'd be possible for dynatable to have put the
|
|
// page parameter in the URL)
|
|
if (pageUrl && settings.features.pushState) {
|
|
this.set(pageUrl[1]);
|
|
} else {
|
|
this.set(1);
|
|
}
|
|
};
|
|
|
|
this.set = function(page) {
|
|
settings.dataset.page = parseInt(page, 10);
|
|
}
|
|
};
|
|
|
|
function PaginationPerPage(obj, settings) {
|
|
var _this = this;
|
|
|
|
this.initOnLoad = function() {
|
|
return settings.features.paginate;
|
|
};
|
|
|
|
this.init = function() {
|
|
var perPageUrl = window.location.search.match(new RegExp(settings.params.perPage + '=([^&]*)'));
|
|
|
|
// If perPage is present in URL parameters and pushState is enabled
|
|
// (meaning that it'd be possible for dynatable to have put the
|
|
// perPage parameter in the URL)
|
|
if (perPageUrl && settings.features.pushState) {
|
|
// Don't reset page to 1 on init, since it might override page
|
|
// set on init from URL
|
|
this.set(perPageUrl[1], true);
|
|
} else {
|
|
this.set(settings.dataset.perPageDefault, true);
|
|
}
|
|
|
|
if (settings.features.perPageSelect) {
|
|
this.attach();
|
|
}
|
|
};
|
|
|
|
this.create = function() {
|
|
var $select = $('<select>', {
|
|
id: 'dynatable-per-page-' + obj.element.id,
|
|
'class': 'dynatable-per-page-select'
|
|
});
|
|
|
|
for (var i = 0, len = settings.dataset.perPageOptions.length; i < len; i++) {
|
|
var number = settings.dataset.perPageOptions[i],
|
|
selected = settings.dataset.perPage == number ? 'selected="selected"' : '';
|
|
$select.append('<option value="' + number + '" ' + selected + '>' + number + '</option>');
|
|
}
|
|
|
|
$select.bind('change', function(e) {
|
|
_this.set($(this).val());
|
|
obj.process();
|
|
});
|
|
|
|
return $('<span />', {
|
|
'class': 'dynatable-per-page'
|
|
}).append("<span class='dynatable-per-page-label'>" + settings.inputs.perPageText + "</span>").append($select);
|
|
};
|
|
|
|
this.attach = function() {
|
|
var $target = settings.inputs.perPageTarget ? $(settings.inputs.perPageTarget) : obj.$element;
|
|
$target[settings.inputs.perPagePlacement](this.create());
|
|
};
|
|
|
|
this.set = function(number, skipResetPage) {
|
|
if (!skipResetPage) { obj.paginationPage.set(1); }
|
|
settings.dataset.perPage = parseInt(number);
|
|
};
|
|
};
|
|
|
|
// pagination links which update dataset.page attribute
|
|
function PaginationLinks(obj, settings) {
|
|
var _this = this;
|
|
|
|
this.initOnLoad = function() {
|
|
return settings.features.paginate;
|
|
};
|
|
|
|
this.init = function() {
|
|
this.attach();
|
|
};
|
|
|
|
this.create = function() {
|
|
var pageLinks = '<ul id="' + 'dynatable-pagination-links-' + obj.element.id + '" class="' + settings.inputs.paginationClass + '">',
|
|
pageLinkClass = settings.inputs.paginationLinkClass,
|
|
activePageClass = settings.inputs.paginationActiveClass,
|
|
disabledPageClass = settings.inputs.paginationDisabledClass,
|
|
pages = Math.ceil(settings.dataset.queryRecordCount / settings.dataset.perPage),
|
|
page = settings.dataset.page,
|
|
breaks = [
|
|
settings.inputs.paginationGap[0],
|
|
settings.dataset.page - settings.inputs.paginationGap[1],
|
|
settings.dataset.page + settings.inputs.paginationGap[2],
|
|
(pages + 1) - settings.inputs.paginationGap[3]
|
|
];
|
|
|
|
pageLinks += '<li><span>Pages: </span></li>';
|
|
|
|
for (var i = 1; i <= pages; i++) {
|
|
if ( (i > breaks[0] && i < breaks[1]) || (i > breaks[2] && i < breaks[3])) {
|
|
// skip to next iteration in loop
|
|
continue;
|
|
} else {
|
|
var li = obj.paginationLinks.buildLink(i, i, pageLinkClass, page == i, activePageClass),
|
|
breakIndex,
|
|
nextBreak;
|
|
|
|
// If i is not between one of the following
|
|
// (1 + (settings.paginationGap[0]))
|
|
// (page - settings.paginationGap[1])
|
|
// (page + settings.paginationGap[2])
|
|
// (pages - settings.paginationGap[3])
|
|
breakIndex = $.inArray(i, breaks);
|
|
nextBreak = breaks[breakIndex + 1];
|
|
if (breakIndex > 0 && i !== 1 && nextBreak && nextBreak > (i + 1)) {
|
|
var ellip = '<li><span class="dynatable-page-break">…</span></li>';
|
|
li = breakIndex < 2 ? ellip + li : li + ellip;
|
|
}
|
|
|
|
if (settings.inputs.paginationPrev && i === 1) {
|
|
var prevLi = obj.paginationLinks.buildLink(page - 1, settings.inputs.paginationPrev, pageLinkClass + ' ' + settings.inputs.paginationPrevClass, page === 1, disabledPageClass);
|
|
li = prevLi + li;
|
|
}
|
|
if (settings.inputs.paginationNext && i === pages) {
|
|
var nextLi = obj.paginationLinks.buildLink(page + 1, settings.inputs.paginationNext, pageLinkClass + ' ' + settings.inputs.paginationNextClass, page === pages, disabledPageClass);
|
|
li += nextLi;
|
|
}
|
|
|
|
pageLinks += li;
|
|
}
|
|
}
|
|
|
|
pageLinks += '</ul>';
|
|
|
|
// only bind page handler to non-active and non-disabled page links
|
|
var selector = '#dynatable-pagination-links-' + obj.element.id + ' a.' + pageLinkClass + ':not(.' + activePageClass + ',.' + disabledPageClass + ')';
|
|
// kill any existing delegated-bindings so they don't stack up
|
|
$(document).undelegate(selector, 'click.dynatable');
|
|
$(document).delegate(selector, 'click.dynatable', function(e) {
|
|
$this = $(this);
|
|
$this.closest(settings.inputs.paginationClass).find('.' + activePageClass).removeClass(activePageClass);
|
|
$this.addClass(activePageClass);
|
|
|
|
obj.paginationPage.set($this.data('dynatable-page'));
|
|
obj.process();
|
|
e.preventDefault();
|
|
});
|
|
|
|
return pageLinks;
|
|
};
|
|
|
|
this.buildLink = function(page, label, linkClass, conditional, conditionalClass) {
|
|
var link = '<a data-dynatable-page=' + page + ' class="' + linkClass,
|
|
li = '<li';
|
|
|
|
if (conditional) {
|
|
link += ' ' + conditionalClass;
|
|
li += ' class="' + conditionalClass + '"';
|
|
}
|
|
|
|
link += '">' + label + '</a>';
|
|
li += '>' + link + '</li>';
|
|
|
|
return li;
|
|
};
|
|
|
|
this.attach = function() {
|
|
// append page links *after* delegate-event-binding so it doesn't need to
|
|
// find and select all page links to bind event
|
|
var $target = settings.inputs.paginationLinkTarget ? $(settings.inputs.paginationLinkTarget) : obj.$element;
|
|
$target[settings.inputs.paginationLinkPlacement](obj.paginationLinks.create());
|
|
};
|
|
};
|
|
|
|
utility = dt.utility = {
|
|
normalizeText: function(text, style) {
|
|
text = this.textTransform[style](text);
|
|
return text;
|
|
},
|
|
textTransform: {
|
|
trimDash: function(text) {
|
|
return text.replace(/^\s+|\s+$/g, "").replace(/\s+/g, "-");
|
|
},
|
|
camelCase: function(text) {
|
|
text = this.trimDash(text);
|
|
return text
|
|
.replace(/(\-[a-zA-Z])/g, function($1){return $1.toUpperCase().replace('-','');})
|
|
.replace(/([A-Z])([A-Z]+)/g, function($1,$2,$3){return $2 + $3.toLowerCase();})
|
|
.replace(/^[A-Z]/, function($1){return $1.toLowerCase();});
|
|
},
|
|
dashed: function(text) {
|
|
text = this.trimDash(text);
|
|
return this.lowercase(text);
|
|
},
|
|
underscore: function(text) {
|
|
text = this.trimDash(text);
|
|
return this.lowercase(text.replace(/(-)/g, '_'));
|
|
},
|
|
lowercase: function(text) {
|
|
return text.replace(/([A-Z])/g, function($1){return $1.toLowerCase();});
|
|
}
|
|
},
|
|
// Deserialize params in URL to object
|
|
// see http://stackoverflow.com/questions/1131630/javascript-jquery-param-inverse-function/3401265#3401265
|
|
deserialize: function(query) {
|
|
if (!query) return {};
|
|
// modified to accept an array of partial URL strings
|
|
if (typeof(query) === "object") { query = query.join('&'); }
|
|
|
|
var hash = {},
|
|
vars = query.split("&");
|
|
|
|
for (var i = 0; i < vars.length; i++) {
|
|
var pair = vars[i].split("="),
|
|
k = decodeURIComponent(pair[0]),
|
|
v, m;
|
|
|
|
if (!pair[1]) { continue };
|
|
v = decodeURIComponent(pair[1].replace(/\+/g, ' '));
|
|
|
|
// modified to parse multi-level parameters (e.g. "hi[there][dude]=whatsup" => hi: {there: {dude: "whatsup"}})
|
|
while (m = k.match(/([^&=]+)\[([^&=]+)\]$/)) {
|
|
var origV = v;
|
|
k = m[1];
|
|
v = {};
|
|
|
|
// If nested param ends in '][', then the regex above erroneously included half of a trailing '[]',
|
|
// which indicates the end-value is part of an array
|
|
if (m[2].substr(m[2].length-2) == '][') { // must use substr for IE to understand it
|
|
v[m[2].substr(0,m[2].length-2)] = [origV];
|
|
} else {
|
|
v[m[2]] = origV;
|
|
}
|
|
}
|
|
|
|
// If it is the first entry with this name
|
|
if (typeof hash[k] === "undefined") {
|
|
if (k.substr(k.length-2) != '[]') { // not end with []. cannot use negative index as IE doesn't understand it
|
|
hash[k] = v;
|
|
} else {
|
|
hash[k] = [v];
|
|
}
|
|
// If subsequent entry with this name and not array
|
|
} else if (typeof hash[k] === "string") {
|
|
hash[k] = v; // replace it
|
|
// modified to add support for objects
|
|
} else if (typeof hash[k] === "object") {
|
|
hash[k] = $.extend({}, hash[k], v);
|
|
// If subsequent entry with this name and is array
|
|
} else {
|
|
hash[k].push(v);
|
|
}
|
|
}
|
|
return hash;
|
|
},
|
|
refreshQueryString: function(urlString, data, settings) {
|
|
var _this = this,
|
|
queryString = urlString.split('?'),
|
|
path = queryString.shift(),
|
|
urlOptions;
|
|
|
|
urlOptions = this.deserialize(urlString);
|
|
|
|
// Loop through each dynatable param and update the URL with it
|
|
for (attr in settings.params) {
|
|
if (settings.params.hasOwnProperty(attr)) {
|
|
var label = settings.params[attr];
|
|
// Skip over parameters matching attributes for disabled features (i.e. leave them untouched),
|
|
// because if the feature is turned off, then parameter name is a coincidence and it's unrelated to dynatable.
|
|
if (
|
|
(!settings.features.sort && attr == "sorts") ||
|
|
(!settings.features.paginate && _this.anyMatch(attr, ["page", "perPage", "offset"], function(attr, param) { return attr == param; }))
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// Delete page and offset from url params if on page 1 (default)
|
|
if ((attr === "page" || attr === "offset") && data["page"] === 1) {
|
|
if (urlOptions[label]) {
|
|
delete urlOptions[label];
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Delete perPage from url params if default perPage value
|
|
if (attr === "perPage" && data[label] == settings.dataset.perPageDefault) {
|
|
if (urlOptions[label]) {
|
|
delete urlOptions[label];
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// For queries, we're going to handle each possible query parameter individually here instead of
|
|
// handling the entire queries object below, since we need to make sure that this is a query controlled by dynatable.
|
|
if (attr == "queries" && data[label]) {
|
|
var queries = settings.inputs.queries || [],
|
|
inputQueries = $.makeArray(queries.map(function() { return $(this).attr('name') }));
|
|
|
|
if (settings.features.search) { inputQueries.push('search'); }
|
|
|
|
for (var i = 0, len = inputQueries.length; i < len; i++) {
|
|
var attr = inputQueries[i];
|
|
if (data[label][attr]) {
|
|
if (typeof urlOptions[label] === 'undefined') { urlOptions[label] = {}; }
|
|
urlOptions[label][attr] = data[label][attr];
|
|
} else {
|
|
delete urlOptions[label][attr];
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// If we haven't returned true by now, then we actually want to update the parameter in the URL
|
|
if (data[label]) {
|
|
urlOptions[label] = data[label];
|
|
} else {
|
|
delete urlOptions[label];
|
|
}
|
|
}
|
|
}
|
|
return decodeURI($.param(urlOptions));
|
|
},
|
|
// Get array of keys from object
|
|
// see http://stackoverflow.com/questions/208016/how-to-list-the-properties-of-a-javascript-object/208020#208020
|
|
keysFromObject: function(obj){
|
|
var keys = [];
|
|
for (var key in obj){
|
|
keys.push(key);
|
|
}
|
|
return keys;
|
|
},
|
|
// Find an object in an array of objects by attributes.
|
|
// E.g. find object with {id: 'hi', name: 'there'} in an array of objects
|
|
findObjectInArray: function(array, objectAttr) {
|
|
var _this = this,
|
|
foundObject;
|
|
for (var i = 0, len = array.length; i < len; i++) {
|
|
var item = array[i];
|
|
// For each object in array, test to make sure all attributes in objectAttr match
|
|
if (_this.allMatch(item, objectAttr, function(item, key, value) { return item[key] == value; })) {
|
|
foundObject = item;
|
|
break;
|
|
}
|
|
}
|
|
return foundObject;
|
|
},
|
|
// Return true if supplied test function passes for ALL items in an array
|
|
allMatch: function(item, arrayOrObject, test) {
|
|
// start off with true result by default
|
|
var match = true,
|
|
isArray = $.isArray(arrayOrObject);
|
|
// Loop through all items in array
|
|
$.each(arrayOrObject, function(key, value) {
|
|
var result = isArray ? test(item, value) : test(item, key, value);
|
|
// If a single item tests false, go ahead and break the array by returning false
|
|
// and return false as result,
|
|
// otherwise, continue with next iteration in loop
|
|
// (if we make it through all iterations without overriding match with false,
|
|
// then we can return the true result we started with by default)
|
|
if (!result) { return match = false; }
|
|
});
|
|
return match;
|
|
},
|
|
// Return true if supplied test function passes for ANY items in an array
|
|
anyMatch: function(item, arrayOrObject, test) {
|
|
var match = false,
|
|
isArray = $.isArray(arrayOrObject);
|
|
|
|
$.each(arrayOrObject, function(key, value) {
|
|
var result = isArray ? test(item, value) : test(item, key, value);
|
|
if (result) {
|
|
// As soon as a match is found, set match to true, and return false to stop the `$.each` loop
|
|
match = true;
|
|
return false;
|
|
}
|
|
});
|
|
return match;
|
|
},
|
|
// Return true if two objects are equal
|
|
// (i.e. have the same attributes and attribute values)
|
|
objectsEqual: function(a, b) {
|
|
for (attr in a) {
|
|
if (a.hasOwnProperty(attr)) {
|
|
if (!b.hasOwnProperty(attr) || a[attr] !== b[attr]) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
for (attr in b) {
|
|
if (b.hasOwnProperty(attr) && !a.hasOwnProperty(attr)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
// Taken from http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/105074#105074
|
|
randomHash: function() {
|
|
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
|
|
}
|
|
};
|
|
|
|
//-----------------------------------------------------------------
|
|
// Build the dynatable plugin
|
|
//-----------------------------------------------------------------
|
|
|
|
// Object.create support test, and fallback for browsers without it
|
|
if ( typeof Object.create !== "function" ) {
|
|
Object.create = function (o) {
|
|
function F() {}
|
|
F.prototype = o;
|
|
return new F();
|
|
};
|
|
}
|
|
|
|
//-----------------------------------------------------------------
|
|
// Global dynatable plugin setting defaults
|
|
//-----------------------------------------------------------------
|
|
|
|
$.dynatableSetup = function(options) {
|
|
defaults = mergeSettings(options);
|
|
};
|
|
|
|
// Create dynatable plugin based on a defined object
|
|
$.dynatable = function( object ) {
|
|
$.fn['dynatable'] = function( options ) {
|
|
return this.each(function() {
|
|
if ( ! $.data( this, 'dynatable' ) ) {
|
|
$.data( this, 'dynatable', Object.create(object).init(this, options) );
|
|
}
|
|
});
|
|
};
|
|
};
|
|
|
|
$.dynatable(dt);
|
|
|
|
})(jQuery);
|