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