/*␊ |
* 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 class="' + record.originalTR[0].className + '">' + 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;␊ |
record["originalTR"] = $(this);␊ |
$(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);␊ |