(
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;
defaults = {
features: {
paginate:
true
,
sort:
true
,
pushState:
true
,
search:
true
,
recordCount:
true
,
perPageSelect:
true
},
table: {
defaultColumnIdStyle:
'camelCase'
,
columns:
null
,
headRowSelector:
'thead tr'
,
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'
}
};
dt = {
init:
function
(element, options) {
this
.settings = mergeSettings(options);
this
.element = element;
this
.$element = $(element);
build.call(
this
);
return
this
;
},
process:
function
(skipPushState) {
processAll.call(
this
, skipPushState);
}
};
mergeSettings =
function
(options) {
var
newOptions = $.extend(
true
, {}, defaults, options);
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,
}
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 =
''
;
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));
}
}
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
( !id ) {
this
.generate($column);
id = $column.data(
'dynatable-column'
);
}
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'
)
});
$column
.attr(
'data-dynatable-column'
, id)
.addClass(
'dynatable-head'
);
if
(settings.table.headRowClass) { $column.addClass(settings.table.headRowClass); }
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);
}
else
{
obj.$element.find(settings.table.headRowSelector).append($column);
}
obj.sortsHeaders.attachOne($column.get());
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
{
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) {
var
increment = obj.$element.find(settings.table.headRowSelector).children(
'th[data-dynatable-generated]'
).length;
return
$cell
.attr(
'data-dynatable-column'
,
'dynatable-generated-'
+ increment)
.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;
}
}
settings.dataset.originalRecords = $.extend(
true
, [], settings.dataset.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;
};
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);
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];
};
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
(
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
() {
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,
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.
//
// Likewise, other browsers may have varying (undocumented) limits.
// Also, Firefox'
s limit can be changed
in
about:config as browser.history.maxStateObjectSize
cache.dynatable.dataset.perPageOptions = $.makeArray(cache.dynatable.dataset.perPageOptions);
try
{
window.history[pushFunction](cache,
"Dynatable state"
, path + params + hash);
}
catch
(error) {
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
( 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.
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
;
}
}
settings.dataset.records = $.map(settings.dataset.records,
function
(record) {
return
_this.functions[query](record, value) ? record :
null
;
});
}
}
settings.dataset.queryRecordCount = obj.records.count();
};
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());
};
};
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
(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
if
(perPageUrl && settings.features.pushState) {
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 + ')';
$(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
() {
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
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];
}
}
else
if
(
typeof
hash[k] ===
"string"
) {
hash[k] = v;
}
else
if
(
typeof
hash[k] ===
"object"
) {
hash[k] = $.extend({}, hash[k], v);
}
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);
for
(attr
in
settings.params) {
if
(settings.params.hasOwnProperty(attr)) {
var
label = settings.params[attr];
if
(
(!settings.features.sort && attr ==
"sorts"
) ||
(!settings.features.paginate && _this.anyMatch(attr, [
"page"
,
"perPage"
,
"offset"
],
function
(attr, param) {
return
attr == param; }))
) {
continue
;
}
if
((attr ===
"page"
|| attr ===
"offset"
) && data[
"page"
] === 1) {
if
(urlOptions[label]) {
delete
urlOptions[label];
}
continue
;
}
if
(attr ===
"perPage"
&& data[label] == settings.dataset.perPageDefault) {
if
(urlOptions[label]) {
delete
urlOptions[label];
}
continue
;
}
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
(data[label]) {
urlOptions[label] = data[label];
}
else
{
delete
urlOptions[label];
}
}
}
return
decodeURI($.param(urlOptions));
},
keysFromObject:
function
(obj){
var
keys = [];
for
(
var
key
in
obj){
keys.push(key);
}
return
keys;
},
findObjectInArray:
function
(array, objectAttr) {
var
_this =
this
,
foundObject;
for
(
var
i = 0, len = array.length; i < len; i++) {
var
item = array[i];
if
(_this.allMatch(item, objectAttr,
function
(item, key, value) {
return
item[key] == value; })) {
foundObject = item;
break
;
}
}
return
foundObject;
},
allMatch:
function
(item, arrayOrObject, test) {
var
match =
true
,
isArray = $.isArray(arrayOrObject);
$.each(arrayOrObject,
function
(key, value) {
var
result = isArray ? test(item, value) : test(item, key, value);
if
(!result) {
return
match =
false
; }
});
return
match;
},
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) {
match =
true
;
return
false
;
}
});
return
match;
},
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
;
},
randomHash:
function
() {
return
(((1+Math.random())*0x10000)|0).toString(16).substring(1);
}
};
if
(
typeof
Object.create !==
"function"
) {
Object.create =
function
(o) {
function
F() {}
F.prototype = o;
return
new
F();
};
}
$.dynatableSetup =
function
(options) {
defaults = mergeSettings(options);
};
$.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);