/jquery.dynatable.js

https://github.com/joseanluo/jquery-dynatable · JavaScript · 1733 lines · 1325 code · 220 blank · 188 comment · 248 complexity · 4f80482e3547b2371583a96f8c365d8f MD5 · raw file

  1. /*
  2. * jQuery Dynatable plugin 0.3.1
  3. *
  4. * Copyright (c) 2014 Steve Schwartz (JangoSteve)
  5. *
  6. * Dual licensed under the AGPL and Proprietary licenses:
  7. * http://www.dynatable.com/license/
  8. *
  9. * Date: Tue Jan 02 2014
  10. */
  11. //
  12. (function($) {
  13. var defaults,
  14. mergeSettings,
  15. dt,
  16. Model,
  17. modelPrototypes = {
  18. dom: Dom,
  19. domColumns: DomColumns,
  20. records: Records,
  21. recordsCount: RecordsCount,
  22. processingIndicator: ProcessingIndicator,
  23. state: State,
  24. sorts: Sorts,
  25. sortsHeaders: SortsHeaders,
  26. queries: Queries,
  27. inputsSearch: InputsSearch,
  28. paginationPage: PaginationPage,
  29. paginationPerPage: PaginationPerPage,
  30. paginationLinks: PaginationLinks
  31. },
  32. utility,
  33. build,
  34. processAll,
  35. initModel,
  36. defaultRowWriter,
  37. defaultCellWriter,
  38. defaultAttributeWriter,
  39. defaultAttributeReader;
  40. //-----------------------------------------------------------------
  41. // Cached plugin global defaults
  42. //-----------------------------------------------------------------
  43. defaults = {
  44. features: {
  45. paginate: true,
  46. sort: true,
  47. pushState: true,
  48. search: true,
  49. recordCount: true,
  50. perPageSelect: true
  51. },
  52. table: {
  53. defaultColumnIdStyle: 'camelCase',
  54. columns: null,
  55. headRowSelector: 'thead tr', // or e.g. tr:first-child
  56. bodyRowSelector: 'tbody tr',
  57. headRowClass: null,
  58. copyHeaderAlignment: true,
  59. copyHeaderClass: false
  60. },
  61. inputs: {
  62. queries: null,
  63. sorts: null,
  64. multisort: ['ctrlKey', 'shiftKey', 'metaKey'],
  65. page: null,
  66. queryEvent: 'blur change',
  67. recordCountTarget: null,
  68. recordCountPlacement: 'after',
  69. paginationLinkTarget: null,
  70. paginationLinkPlacement: 'after',
  71. paginationClass: 'dynatable-pagination-links',
  72. paginationLinkClass: 'dynatable-page-link',
  73. paginationPrevClass: 'dynatable-page-prev',
  74. paginationNextClass: 'dynatable-page-next',
  75. paginationActiveClass: 'dynatable-active-page',
  76. paginationDisabledClass: 'dynatable-disabled-page',
  77. paginationPrev: 'Previous',
  78. paginationNext: 'Next',
  79. paginationGap: [1,2,2,1],
  80. searchTarget: null,
  81. searchPlacement: 'before',
  82. searchText: 'Search: ',
  83. perPageTarget: null,
  84. perPagePlacement: 'before',
  85. perPageText: 'Show: ',
  86. pageText: 'Pages: ',
  87. recordCountPageBoundTemplate: '{pageLowerBound} to {pageUpperBound} of',
  88. recordCountPageUnboundedTemplate: '{recordsShown} of',
  89. recordCountTotalTemplate: '{recordsQueryCount} {collectionName}',
  90. recordCountFilteredTemplate: ' (filtered from {recordsTotal} total records)',
  91. recordCountText: 'Showing',
  92. recordCountTextTemplate: '{text} {pageTemplate} {totalTemplate} {filteredTemplate}',
  93. recordCountTemplate: '<span id="dynatable-record-count-{elementId}" class="dynatable-record-count">{textTemplate}</span>',
  94. processingText: 'Processing...'
  95. },
  96. dataset: {
  97. ajax: false,
  98. ajaxUrl: null,
  99. ajaxCache: null,
  100. ajaxOnLoad: false,
  101. ajaxMethod: 'GET',
  102. ajaxDataType: 'json',
  103. totalRecordCount: null,
  104. queries: {},
  105. queryRecordCount: null,
  106. page: null,
  107. perPageDefault: 10,
  108. perPageOptions: [10,20,50,100],
  109. sorts: {},
  110. sortsKeys: [],
  111. sortTypes: {},
  112. records: null
  113. },
  114. writers: {
  115. _rowWriter: defaultRowWriter,
  116. _cellWriter: defaultCellWriter,
  117. _attributeWriter: defaultAttributeWriter
  118. },
  119. readers: {
  120. _rowReader: null,
  121. _attributeReader: defaultAttributeReader
  122. },
  123. params: {
  124. dynatable: 'dynatable',
  125. queries: 'queries',
  126. sorts: 'sorts',
  127. page: 'page',
  128. perPage: 'perPage',
  129. offset: 'offset',
  130. records: 'records',
  131. record: null,
  132. queryRecordCount: 'queryRecordCount',
  133. totalRecordCount: 'totalRecordCount'
  134. }
  135. };
  136. //-----------------------------------------------------------------
  137. // Each dynatable instance inherits from this,
  138. // set properties specific to instance
  139. //-----------------------------------------------------------------
  140. dt = {
  141. init: function(element, options) {
  142. this.settings = mergeSettings(options);
  143. this.element = element;
  144. this.$element = $(element);
  145. // All the setup that doesn't require element or options
  146. build.call(this);
  147. return this;
  148. },
  149. process: function(skipPushState) {
  150. processAll.call(this, skipPushState);
  151. }
  152. };
  153. //-----------------------------------------------------------------
  154. // Cached plugin global functions
  155. //-----------------------------------------------------------------
  156. mergeSettings = function(options) {
  157. var newOptions = $.extend(true, {}, defaults, options);
  158. // TODO: figure out a better way to do this.
  159. // Doing `extend(true)` causes any elements that are arrays
  160. // to merge the default and options arrays instead of overriding the defaults.
  161. if (options) {
  162. if (options.inputs) {
  163. if (options.inputs.multisort) {
  164. newOptions.inputs.multisort = options.inputs.multisort;
  165. }
  166. if (options.inputs.paginationGap) {
  167. newOptions.inputs.paginationGap = options.inputs.paginationGap;
  168. }
  169. }
  170. if (options.dataset && options.dataset.perPageOptions) {
  171. newOptions.dataset.perPageOptions = options.dataset.perPageOptions;
  172. }
  173. }
  174. return newOptions;
  175. };
  176. build = function() {
  177. this.$element.trigger('dynatable:preinit', this);
  178. for (model in modelPrototypes) {
  179. if (modelPrototypes.hasOwnProperty(model)) {
  180. var modelInstance = this[model] = new modelPrototypes[model](this, this.settings);
  181. if (modelInstance.initOnLoad()) {
  182. modelInstance.init();
  183. }
  184. }
  185. }
  186. this.$element.trigger('dynatable:init', this);
  187. if (!this.settings.dataset.ajax || (this.settings.dataset.ajax && this.settings.dataset.ajaxOnLoad) || this.settings.features.paginate || (this.settings.features.sort && !$.isEmptyObject(this.settings.dataset.sorts))) {
  188. this.process();
  189. }
  190. };
  191. processAll = function(skipPushState) {
  192. var data = {};
  193. this.$element.trigger('dynatable:beforeProcess', data);
  194. if (!$.isEmptyObject(this.settings.dataset.queries)) { data[this.settings.params.queries] = this.settings.dataset.queries; }
  195. // TODO: Wrap this in a try/rescue block to hide the processing indicator and indicate something went wrong if error
  196. this.processingIndicator.show();
  197. if (this.settings.features.sort && !$.isEmptyObject(this.settings.dataset.sorts)) { data[this.settings.params.sorts] = this.settings.dataset.sorts; }
  198. if (this.settings.features.paginate && this.settings.dataset.page) {
  199. var page = this.settings.dataset.page,
  200. perPage = this.settings.dataset.perPage;
  201. data[this.settings.params.page] = page;
  202. data[this.settings.params.perPage] = perPage;
  203. data[this.settings.params.offset] = (page - 1) * perPage;
  204. }
  205. if (this.settings.dataset.ajaxData) { $.extend(data, this.settings.dataset.ajaxData); }
  206. // If ajax, sends query to ajaxUrl with queries and sorts serialized and appended in ajax data
  207. // otherwise, executes queries and sorts on in-page data
  208. if (this.settings.dataset.ajax) {
  209. var _this = this;
  210. var options = {
  211. type: _this.settings.dataset.ajaxMethod,
  212. dataType: _this.settings.dataset.ajaxDataType,
  213. data: data,
  214. error: function(xhr, error) {
  215. },
  216. success: function(response) {
  217. _this.$element.trigger('dynatable:ajax:success', response);
  218. // Merge ajax results and meta-data into dynatables cached data
  219. _this.records.updateFromJson(response);
  220. // update table with new records
  221. _this.dom.update();
  222. if (!skipPushState && _this.state.initOnLoad()) {
  223. _this.state.push(data);
  224. }
  225. },
  226. complete: function() {
  227. _this.processingIndicator.hide();
  228. }
  229. };
  230. // Do not pass url to `ajax` options if blank
  231. if (this.settings.dataset.ajaxUrl) {
  232. options.url = this.settings.dataset.ajaxUrl;
  233. // If ajaxUrl is blank, then we're using the current page URL,
  234. // we need to strip out any query, sort, or page data controlled by dynatable
  235. // that may have been in URL when page loaded, so that it doesn't conflict with
  236. // what's passed in with the data ajax parameter
  237. } else {
  238. options.url = utility.refreshQueryString(window.location.href, {}, this.settings);
  239. }
  240. if (this.settings.dataset.ajaxCache !== null) { options.cache = this.settings.dataset.ajaxCache; }
  241. $.ajax(options);
  242. } else {
  243. this.records.resetOriginal();
  244. this.queries.run();
  245. if (this.settings.features.sort) {
  246. this.records.sort();
  247. }
  248. if (this.settings.features.paginate) {
  249. this.records.paginate();
  250. }
  251. this.dom.update();
  252. this.processingIndicator.hide();
  253. if (!skipPushState && this.state.initOnLoad()) {
  254. this.state.push(data);
  255. }
  256. }
  257. this.$element.addClass('dynatable-loaded');
  258. this.$element.trigger('dynatable:afterProcess', data);
  259. };
  260. function defaultRowWriter(rowIndex, record, columns, cellWriter) {
  261. var tr = '';
  262. // grab the record's attribute for each column
  263. for (var i = 0, len = columns.length; i < len; i++) {
  264. tr += cellWriter(columns[i], record);
  265. }
  266. return '<tr>' + tr + '</tr>';
  267. };
  268. function defaultCellWriter(column, record) {
  269. var html = column.attributeWriter(record),
  270. td = '<td';
  271. if (column.hidden || column.textAlign) {
  272. td += ' style="';
  273. // keep cells for hidden column headers hidden
  274. if (column.hidden) {
  275. td += 'display: none;';
  276. }
  277. // keep cells aligned as their column headers are aligned
  278. if (column.textAlign) {
  279. td += 'text-align: ' + column.textAlign + ';';
  280. }
  281. td += '"';
  282. }
  283. if (column.cssClass) {
  284. td += ' class="' + column.cssClass + '"';
  285. }
  286. return td + '>' + html + '</td>';
  287. };
  288. function defaultAttributeWriter(record) {
  289. // `this` is the column object in settings.columns
  290. // TODO: automatically convert common types, such as arrays and objects, to string
  291. return record[this.id];
  292. };
  293. function defaultAttributeReader(cell, record) {
  294. return $(cell).html();
  295. };
  296. //-----------------------------------------------------------------
  297. // Dynatable object model prototype
  298. // (all object models get these default functions)
  299. //-----------------------------------------------------------------
  300. Model = {
  301. initOnLoad: function() {
  302. return true;
  303. },
  304. init: function() {}
  305. };
  306. for (model in modelPrototypes) {
  307. if (modelPrototypes.hasOwnProperty(model)) {
  308. var modelPrototype = modelPrototypes[model];
  309. modelPrototype.prototype = Model;
  310. }
  311. }
  312. //-----------------------------------------------------------------
  313. // Dynatable object models
  314. //-----------------------------------------------------------------
  315. function Dom(obj, settings) {
  316. var _this = this;
  317. // update table contents with new records array
  318. // from query (whether ajax or not)
  319. this.update = function() {
  320. var rows = '',
  321. columns = settings.table.columns,
  322. rowWriter = settings.writers._rowWriter,
  323. cellWriter = settings.writers._cellWriter;
  324. obj.$element.trigger('dynatable:beforeUpdate', rows);
  325. // loop through records
  326. for (var i = 0, len = settings.dataset.records.length; i < len; i++) {
  327. var record = settings.dataset.records[i],
  328. tr = rowWriter(i, record, columns, cellWriter);
  329. rows += tr;
  330. }
  331. // Appended dynatable interactive elements
  332. if (settings.features.recordCount) {
  333. $('#dynatable-record-count-' + obj.element.id).replaceWith(obj.recordsCount.create());
  334. }
  335. if (settings.features.paginate) {
  336. $('#dynatable-pagination-links-' + obj.element.id).replaceWith(obj.paginationLinks.create());
  337. if (settings.features.perPageSelect) {
  338. $('#dynatable-per-page-' + obj.element.id).val(parseInt(settings.dataset.perPage));
  339. }
  340. }
  341. // Sort headers functionality
  342. if (settings.features.sort && columns) {
  343. obj.sortsHeaders.removeAllArrows();
  344. for (var i = 0, len = columns.length; i < len; i++) {
  345. var column = columns[i],
  346. sortedByColumn = utility.allMatch(settings.dataset.sorts, column.sorts, function(sorts, sort) { return sort in sorts; }),
  347. value = settings.dataset.sorts[column.sorts[0]];
  348. if (sortedByColumn) {
  349. obj.$element.find('[data-dynatable-column="' + column.id + '"]').find('.dynatable-sort-header').each(function(){
  350. if (value == 1) {
  351. obj.sortsHeaders.appendArrowUp($(this));
  352. } else {
  353. obj.sortsHeaders.appendArrowDown($(this));
  354. }
  355. });
  356. }
  357. }
  358. }
  359. // Query search functionality
  360. if (settings.inputs.queries || settings.features.search) {
  361. var allQueries = settings.inputs.queries || $();
  362. if (settings.features.search) {
  363. allQueries = allQueries.add('#dynatable-query-search-' + obj.element.id);
  364. }
  365. allQueries.each(function() {
  366. var $this = $(this),
  367. q = settings.dataset.queries[$this.data('dynatable-query')];
  368. $this.val(q || '');
  369. });
  370. }
  371. obj.$element.find(settings.table.bodyRowSelector).remove();
  372. obj.$element.append(rows);
  373. obj.$element.trigger('dynatable:afterUpdate', rows);
  374. };
  375. };
  376. function DomColumns(obj, settings) {
  377. var _this = this;
  378. this.initOnLoad = function() {
  379. return obj.$element.is('table');
  380. };
  381. this.init = function() {
  382. settings.table.columns = [];
  383. this.getFromTable();
  384. };
  385. // initialize table[columns] array
  386. this.getFromTable = function() {
  387. var $columns = obj.$element.find(settings.table.headRowSelector).children('th,td');
  388. if ($columns.length) {
  389. $columns.each(function(index){
  390. _this.add($(this), index, true);
  391. });
  392. } else {
  393. 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.");
  394. }
  395. };
  396. this.add = function($column, position, skipAppend, skipUpdate) {
  397. var columns = settings.table.columns,
  398. label = $column.text(),
  399. id = $column.data('dynatable-column') || utility.normalizeText(label, settings.table.defaultColumnIdStyle),
  400. dataSorts = $column.data('dynatable-sorts'),
  401. sorts = dataSorts ? $.map(dataSorts.split(','), function(text) { return $.trim(text); }) : [id];
  402. // If the column id is blank, generate an id for it
  403. if ( !id ) {
  404. this.generate($column);
  405. id = $column.data('dynatable-column');
  406. }
  407. // Add column data to plugin instance
  408. columns.splice(position, 0, {
  409. index: position,
  410. label: label,
  411. id: id,
  412. attributeWriter: settings.writers[id] || settings.writers._attributeWriter,
  413. attributeReader: settings.readers[id] || settings.readers._attributeReader,
  414. sorts: sorts,
  415. hidden: $column.css('display') === 'none',
  416. textAlign: settings.table.copyHeaderAlignment && $column.css('text-align'),
  417. cssClass: settings.table.copyHeaderClass && $column.attr('class')
  418. });
  419. // Modify header cell
  420. $column
  421. .attr('data-dynatable-column', id)
  422. .addClass('dynatable-head');
  423. if (settings.table.headRowClass) { $column.addClass(settings.table.headRowClass); }
  424. // Append column header to table
  425. if (!skipAppend) {
  426. var domPosition = position + 1,
  427. $sibling = obj.$element.find(settings.table.headRowSelector)
  428. .children('th:nth-child(' + domPosition + '),td:nth-child(' + domPosition + ')').first(),
  429. columnsAfter = columns.slice(position + 1, columns.length);
  430. if ($sibling.length) {
  431. $sibling.before($column);
  432. // sibling column doesn't yet exist (maybe this is the last column in the header row)
  433. } else {
  434. obj.$element.find(settings.table.headRowSelector).append($column);
  435. }
  436. obj.sortsHeaders.attachOne($column.get());
  437. // increment the index of all columns after this one that was just inserted
  438. if (columnsAfter.length) {
  439. for (var i = 0, len = columnsAfter.length; i < len; i++) {
  440. columnsAfter[i].index += 1;
  441. }
  442. }
  443. if (!skipUpdate) {
  444. obj.dom.update();
  445. }
  446. }
  447. return dt;
  448. };
  449. this.remove = function(columnIndexOrId) {
  450. var columns = settings.table.columns,
  451. length = columns.length;
  452. if (typeof(columnIndexOrId) === "number") {
  453. var column = columns[columnIndexOrId];
  454. this.removeFromTable(column.id);
  455. this.removeFromArray(columnIndexOrId);
  456. } else {
  457. // Traverse columns array in reverse order so that subsequent indices
  458. // don't get messed up when we delete an item from the array in an iteration
  459. for (var i = columns.length - 1; i >= 0; i--) {
  460. var column = columns[i];
  461. if (column.id === columnIndexOrId) {
  462. this.removeFromTable(columnIndexOrId);
  463. this.removeFromArray(i);
  464. }
  465. }
  466. }
  467. obj.dom.update();
  468. };
  469. this.removeFromTable = function(columnId) {
  470. obj.$element.find(settings.table.headRowSelector).children('[data-dynatable-column="' + columnId + '"]').first()
  471. .remove();
  472. };
  473. this.removeFromArray = function(index) {
  474. var columns = settings.table.columns,
  475. adjustColumns;
  476. columns.splice(index, 1);
  477. adjustColumns = columns.slice(index, columns.length);
  478. for (var i = 0, len = adjustColumns.length; i < len; i++) {
  479. adjustColumns[i].index -= 1;
  480. }
  481. };
  482. this.generate = function($cell) {
  483. var cell = $cell === undefined ? $('<th></th>') : $cell;
  484. return this.attachGeneratedAttributes(cell);
  485. };
  486. this.attachGeneratedAttributes = function($cell) {
  487. // Use increment to create unique column name that is the same each time the page is reloaded,
  488. // in order to avoid errors with mismatched attribute names when loading cached `dataset.records` array
  489. var increment = obj.$element.find(settings.table.headRowSelector).children('th[data-dynatable-generated]').length;
  490. return $cell
  491. .attr('data-dynatable-column', 'dynatable-generated-' + increment) //+ utility.randomHash(),
  492. .attr('data-dynatable-no-sort', 'true')
  493. .attr('data-dynatable-generated', increment);
  494. };
  495. };
  496. function Records(obj, settings) {
  497. var _this = this;
  498. this.initOnLoad = function() {
  499. return !settings.dataset.ajax;
  500. };
  501. this.init = function() {
  502. if (settings.dataset.records === null) {
  503. settings.dataset.records = this.getFromTable();
  504. if (!settings.dataset.queryRecordCount) {
  505. settings.dataset.queryRecordCount = this.count();
  506. }
  507. if (!settings.dataset.totalRecordCount){
  508. settings.dataset.totalRecordCount = settings.dataset.queryRecordCount;
  509. }
  510. }
  511. // Create cache of original full recordset (unpaginated and unqueried)
  512. settings.dataset.originalRecords = $.extend(true, [], settings.dataset.records);
  513. };
  514. // merge ajax response json with cached data including
  515. // meta-data and records
  516. this.updateFromJson = function(data) {
  517. var records;
  518. if (settings.params.records === "_root") {
  519. records = data;
  520. } else if (settings.params.records in data) {
  521. records = data[settings.params.records];
  522. }
  523. if (settings.params.record) {
  524. var len = records.length - 1;
  525. for (var i = 0; i < len; i++) {
  526. records[i] = records[i][settings.params.record];
  527. }
  528. }
  529. if (settings.params.queryRecordCount in data) {
  530. settings.dataset.queryRecordCount = data[settings.params.queryRecordCount];
  531. }
  532. if (settings.params.totalRecordCount in data) {
  533. settings.dataset.totalRecordCount = data[settings.params.totalRecordCount];
  534. }
  535. settings.dataset.records = records;
  536. };
  537. // For really advanced sorting,
  538. // see http://james.padolsey.com/javascript/sorting-elements-with-jquery/
  539. this.sort = function() {
  540. var sort = [].sort,
  541. sorts = settings.dataset.sorts,
  542. sortsKeys = settings.dataset.sortsKeys,
  543. sortTypes = settings.dataset.sortTypes;
  544. var sortFunction = function(a, b) {
  545. var comparison;
  546. if ($.isEmptyObject(sorts)) {
  547. comparison = obj.sorts.functions['originalPlacement'](a, b);
  548. } else {
  549. for (var i = 0, len = sortsKeys.length; i < len; i++) {
  550. var attr = sortsKeys[i],
  551. direction = sorts[attr],
  552. sortType = sortTypes[attr] || obj.sorts.guessType(a, b, attr);
  553. comparison = obj.sorts.functions[sortType](a, b, attr, direction);
  554. // Don't need to sort any further unless this sort is a tie between a and b,
  555. // so break the for loop unless tied
  556. if (comparison !== 0) { break; }
  557. }
  558. }
  559. return comparison;
  560. }
  561. return sort.call(settings.dataset.records, sortFunction);
  562. };
  563. this.paginate = function() {
  564. var bounds = this.pageBounds(),
  565. first = bounds[0], last = bounds[1];
  566. settings.dataset.records = settings.dataset.records.slice(first, last);
  567. };
  568. this.resetOriginal = function() {
  569. settings.dataset.records = settings.dataset.originalRecords || [];
  570. };
  571. this.pageBounds = function() {
  572. var page = settings.dataset.page || 1,
  573. first = (page - 1) * settings.dataset.perPage,
  574. last = Math.min(first + settings.dataset.perPage, settings.dataset.queryRecordCount);
  575. return [first,last];
  576. };
  577. // get initial recordset to populate table
  578. // if ajax, call ajaxUrl
  579. // otherwise, initialize from in-table records
  580. this.getFromTable = function() {
  581. var records = [],
  582. columns = settings.table.columns,
  583. tableRecords = obj.$element.find(settings.table.bodyRowSelector);
  584. tableRecords.each(function(index){
  585. var record = {};
  586. record['dynatable-original-index'] = index;
  587. $(this).find('th,td').each(function(index) {
  588. if (columns[index] === undefined) {
  589. // Header cell didn't exist for this column, so let's generate and append
  590. // a new header cell with a randomly generated name (so we can store and
  591. // retrieve the contents of this column for each record)
  592. obj.domColumns.add(obj.domColumns.generate(), columns.length, false, true); // don't skipAppend, do skipUpdate
  593. }
  594. var value = columns[index].attributeReader(this, record),
  595. attr = columns[index].id;
  596. // If value from table is HTML, let's get and cache the text equivalent for
  597. // the default string sorting, since it rarely makes sense for sort headers
  598. // to sort based on HTML tags.
  599. if (typeof(value) === "string" && value.match(/\s*\<.+\>/)) {
  600. if (! record['dynatable-sortable-text']) {
  601. record['dynatable-sortable-text'] = {};
  602. }
  603. record['dynatable-sortable-text'][attr] = $.trim($('<div></div>').html(value).text());
  604. }
  605. record[attr] = value;
  606. });
  607. // Allow configuration function which alters record based on attributes of
  608. // table row (e.g. from html5 data- attributes)
  609. if (typeof(settings.readers._rowReader) === "function") {
  610. settings.readers._rowReader(index, this, record);
  611. }
  612. records.push(record);
  613. });
  614. return records; // 1st row is header
  615. };
  616. // count records from table
  617. this.count = function() {
  618. return settings.dataset.records.length;
  619. };
  620. };
  621. function RecordsCount(obj, settings) {
  622. this.initOnLoad = function() {
  623. return settings.features.recordCount;
  624. };
  625. this.init = function() {
  626. this.attach();
  627. };
  628. this.create = function() {
  629. var pageTemplate = '',
  630. filteredTemplate = '',
  631. options = {
  632. elementId: obj.element.id,
  633. recordsShown: obj.records.count(),
  634. recordsQueryCount: settings.dataset.queryRecordCount,
  635. recordsTotal: settings.dataset.totalRecordCount,
  636. collectionName: settings.params.records === "_root" ? "records" : settings.params.records,
  637. text: settings.inputs.recordCountText
  638. };
  639. if (settings.features.paginate) {
  640. // If currently displayed records are a subset (page) of the entire collection
  641. if (options.recordsShown < options.recordsQueryCount) {
  642. var bounds = obj.records.pageBounds();
  643. options.pageLowerBound = bounds[0] + 1;
  644. options.pageUpperBound = bounds[1];
  645. pageTemplate = settings.inputs.recordCountPageBoundTemplate;
  646. // Else if currently displayed records are the entire collection
  647. } else if (options.recordsShown === options.recordsQueryCount) {
  648. pageTemplate = settings.inputs.recordCountPageUnboundedTemplate;
  649. }
  650. }
  651. // If collection for table is queried subset of collection
  652. if (options.recordsQueryCount < options.recordsTotal) {
  653. filteredTemplate = settings.inputs.recordCountFilteredTemplate;
  654. }
  655. // Populate templates with options
  656. options.pageTemplate = utility.template(pageTemplate, options);
  657. options.filteredTemplate = utility.template(filteredTemplate, options);
  658. options.totalTemplate = utility.template(settings.inputs.recordCountTotalTemplate, options);
  659. options.textTemplate = utility.template(settings.inputs.recordCountTextTemplate, options);
  660. return utility.template(settings.inputs.recordCountTemplate, options);
  661. };
  662. this.attach = function() {
  663. var $target = settings.inputs.recordCountTarget ? $(settings.inputs.recordCountTarget) : obj.$element;
  664. $target[settings.inputs.recordCountPlacement](this.create());
  665. };
  666. };
  667. function ProcessingIndicator(obj, settings) {
  668. this.init = function() {
  669. this.attach();
  670. };
  671. this.create = function() {
  672. var $processing = $('<div></div>', {
  673. html: '<span>' + settings.inputs.processingText + '</span>',
  674. id: 'dynatable-processing-' + obj.element.id,
  675. 'class': 'dynatable-processing',
  676. style: 'position: absolute; display: none;'
  677. });
  678. return $processing;
  679. };
  680. this.position = function() {
  681. var $processing = $('#dynatable-processing-' + obj.element.id),
  682. $span = $processing.children('span'),
  683. spanHeight = $span.outerHeight(),
  684. spanWidth = $span.outerWidth(),
  685. $covered = obj.$element,
  686. offset = $covered.offset(),
  687. height = $covered.outerHeight(), width = $covered.outerWidth();
  688. $processing
  689. .offset({left: offset.left, top: offset.top})
  690. .width(width)
  691. .height(height)
  692. $span
  693. .offset({left: offset.left + ( (width - spanWidth) / 2 ), top: offset.top + ( (height - spanHeight) / 2 )});
  694. return $processing;
  695. };
  696. this.attach = function() {
  697. obj.$element.before(this.create());
  698. };
  699. this.show = function() {
  700. $('#dynatable-processing-' + obj.element.id).show();
  701. this.position();
  702. };
  703. this.hide = function() {
  704. $('#dynatable-processing-' + obj.element.id).hide();
  705. };
  706. };
  707. function State(obj, settings) {
  708. this.initOnLoad = function() {
  709. // Check if pushState option is true, and if browser supports it
  710. return settings.features.pushState && history.pushState;
  711. };
  712. this.init = function() {
  713. window.onpopstate = function(event) {
  714. if (event.state && event.state.dynatable) {
  715. obj.state.pop(event);
  716. }
  717. }
  718. };
  719. this.push = function(data) {
  720. var urlString = window.location.search,
  721. urlOptions,
  722. path,
  723. params,
  724. hash,
  725. newParams,
  726. cacheStr,
  727. cache,
  728. // replaceState on initial load, then pushState after that
  729. firstPush = !(window.history.state && window.history.state.dynatable),
  730. pushFunction = firstPush ? 'replaceState' : 'pushState';
  731. if (urlString && /^\?/.test(urlString)) { urlString = urlString.substring(1); }
  732. $.extend(urlOptions, data);
  733. params = utility.refreshQueryString(urlString, data, settings);
  734. if (params) { params = '?' + params; }
  735. hash = window.location.hash;
  736. path = window.location.pathname;
  737. obj.$element.trigger('dynatable:push', data);
  738. cache = { dynatable: { dataset: settings.dataset } };
  739. if (!firstPush) { cache.dynatable.scrollTop = $(window).scrollTop(); }
  740. cacheStr = JSON.stringify(cache);
  741. // Mozilla has a 640k char limit on what can be stored in pushState.
  742. // See "limit" in https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history#The_pushState().C2.A0method
  743. // and "dataStr.length" in http://wine.git.sourceforge.net/git/gitweb.cgi?p=wine/wine-gecko;a=patch;h=43a11bdddc5fc1ff102278a120be66a7b90afe28
  744. //
  745. // Likewise, other browsers may have varying (undocumented) limits.
  746. // Also, Firefox's limit can be changed in about:config as browser.history.maxStateObjectSize
  747. // Since we don't know what the actual limit will be in any given situation, we'll just try caching and rescue
  748. // any exceptions by retrying pushState without caching the records.
  749. //
  750. // I have absolutely no idea why perPageOptions suddenly becomes an array-like object instead of an array,
  751. // but just recently, this started throwing an error if I don't convert it:
  752. // 'Uncaught Error: DATA_CLONE_ERR: DOM Exception 25'
  753. cache.dynatable.dataset.perPageOptions = $.makeArray(cache.dynatable.dataset.perPageOptions);
  754. try {
  755. window.history[pushFunction](cache, "Dynatable state", path + params + hash);
  756. } catch(error) {
  757. // Make cached records = null, so that `pop` will rerun process to retrieve records
  758. cache.dynatable.dataset.records = null;
  759. window.history[pushFunction](cache, "Dynatable state", path + params + hash);
  760. }
  761. };
  762. this.pop = function(event) {
  763. var data = event.state.dynatable;
  764. settings.dataset = data.dataset;
  765. if (data.scrollTop) { $(window).scrollTop(data.scrollTop); }
  766. // If dataset.records is cached from pushState
  767. if ( data.dataset.records ) {
  768. obj.dom.update();
  769. } else {
  770. obj.process(true);
  771. }
  772. };
  773. };
  774. function Sorts(obj, settings) {
  775. this.initOnLoad = function() {
  776. return settings.features.sort;
  777. };
  778. this.init = function() {
  779. var sortsUrl = window.location.search.match(new RegExp(settings.params.sorts + '[^&=]*=[^&]*', 'g'));
  780. if (sortsUrl) {
  781. settings.dataset.sorts = utility.deserialize(sortsUrl)[settings.params.sorts];
  782. }
  783. if (!settings.dataset.sortsKeys.length) {
  784. settings.dataset.sortsKeys = utility.keysFromObject(settings.dataset.sorts);
  785. }
  786. };
  787. this.add = function(attr, direction) {
  788. var sortsKeys = settings.dataset.sortsKeys,
  789. index = $.inArray(attr, sortsKeys);
  790. settings.dataset.sorts[attr] = direction;
  791. obj.$element.trigger('dynatable:sorts:added', [attr, direction]);
  792. if (index === -1) { sortsKeys.push(attr); }
  793. return dt;
  794. };
  795. this.remove = function(attr) {
  796. var sortsKeys = settings.dataset.sortsKeys,
  797. index = $.inArray(attr, sortsKeys);
  798. delete settings.dataset.sorts[attr];
  799. obj.$element.trigger('dynatable:sorts:removed', attr);
  800. if (index !== -1) { sortsKeys.splice(index, 1); }
  801. return dt;
  802. };
  803. this.clear = function() {
  804. settings.dataset.sorts = {};
  805. settings.dataset.sortsKeys.length = 0;
  806. obj.$element.trigger('dynatable:sorts:cleared');
  807. };
  808. // Try to intelligently guess which sort function to use
  809. // based on the type of attribute values.
  810. // Consider using something more robust than `typeof` (http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/)
  811. this.guessType = function(a, b, attr) {
  812. var types = {
  813. string: 'string',
  814. number: 'number',
  815. 'boolean': 'number',
  816. object: 'number' // dates and null values are also objects, this works...
  817. },
  818. attrType = a[attr] ? typeof(a[attr]) : typeof(b[attr]),
  819. type = types[attrType] || 'number';
  820. return type;
  821. };
  822. // Built-in sort functions
  823. // (the most common use-cases I could think of)
  824. this.functions = {
  825. number: function(a, b, attr, direction) {
  826. return a[attr] === b[attr] ? 0 : (direction > 0 ? a[attr] - b[attr] : b[attr] - a[attr]);
  827. },
  828. string: function(a, b, attr, direction) {
  829. var aAttr = (a['dynatable-sortable-text'] && a['dynatable-sortable-text'][attr]) ? a['dynatable-sortable-text'][attr] : a[attr],
  830. bAttr = (b['dynatable-sortable-text'] && b['dynatable-sortable-text'][attr]) ? b['dynatable-sortable-text'][attr] : b[attr],
  831. comparison;
  832. aAttr = aAttr.toLowerCase();
  833. bAttr = bAttr.toLowerCase();
  834. comparison = aAttr === bAttr ? 0 : (direction > 0 ? aAttr > bAttr : bAttr > aAttr);
  835. // force false boolean value to -1, true to 1, and tie to 0
  836. return comparison === false ? -1 : (comparison - 0);
  837. },
  838. originalPlacement: function(a, b) {
  839. return a['dynatable-original-index'] - b['dynatable-original-index'];
  840. }
  841. };
  842. };
  843. // turn table headers into links which add sort to sorts array
  844. function SortsHeaders(obj, settings) {
  845. var _this = this;
  846. this.initOnLoad = function() {
  847. return settings.features.sort;
  848. };
  849. this.init = function() {
  850. this.attach();
  851. };
  852. this.create = function(cell) {
  853. var $cell = $(cell),
  854. $link = $('<a></a>', {
  855. 'class': 'dynatable-sort-header',
  856. href: '#',
  857. html: $cell.html()
  858. }),
  859. id = $cell.data('dynatable-column'),
  860. column = utility.findObjectInArray(settings.table.columns, {id: id});
  861. $link.bind('click', function(e) {
  862. _this.toggleSort(e, $link, column);
  863. obj.process();
  864. e.preventDefault();
  865. });
  866. if (this.sortedByColumn($link, column)) {
  867. if (this.sortedByColumnValue(column) == 1) {
  868. this.appendArrowUp($link);
  869. } else {
  870. this.appendArrowDown($link);
  871. }
  872. }
  873. return $link;
  874. };
  875. this.removeAll = function() {
  876. obj.$element.find(settings.table.headRowSelector).children('th,td').each(function(){
  877. _this.removeAllArrows();
  878. _this.removeOne(this);
  879. });
  880. };
  881. this.removeOne = function(cell) {
  882. var $cell = $(cell),
  883. $link = $cell.find('.dynatable-sort-header');
  884. if ($link.length) {
  885. var html = $link.html();
  886. $link.remove();
  887. $cell.html($cell.html() + html);
  888. }
  889. };
  890. this.attach = function() {
  891. obj.$element.find(settings.table.headRowSelector).children('th,td').each(function(){
  892. _this.attachOne(this);
  893. });
  894. };
  895. this.attachOne = function(cell) {
  896. var $cell = $(cell);
  897. if (!$cell.data('dynatable-no-sort')) {
  898. $cell.html(this.create(cell));
  899. }
  900. };
  901. this.appendArrowUp = function($link) {
  902. this.removeArrow($link);
  903. $link.append("<span class='dynatable-arrow'> &#9650;</span>");
  904. };
  905. this.appendArrowDown = function($link) {
  906. this.removeArrow($link);
  907. $link.append("<span class='dynatable-arrow'> &#9660;</span>");
  908. };
  909. this.removeArrow = function($link) {
  910. // Not sure why `parent()` is needed, the arrow should be inside the link from `append()` above
  911. $link.find('.dynatable-arrow').remove();
  912. };
  913. this.removeAllArrows = function() {
  914. obj.$element.find('.dynatable-arrow').remove();
  915. };
  916. this.toggleSort = function(e, $link, column) {
  917. var sortedByColumn = this.sortedByColumn($link, column),
  918. value = this.sortedByColumnValue(column);
  919. // Clear existing sorts unless this is a multisort event
  920. if (!settings.inputs.multisort || !utility.anyMatch(e, settings.inputs.multisort, function(evt, key) { return e[key]; })) {
  921. this.removeAllArrows();
  922. obj.sorts.clear();
  923. }
  924. // If sorts for this column are already set
  925. if (sortedByColumn) {
  926. // If ascending, then make descending
  927. if (value == 1) {
  928. for (var i = 0, len = column.sorts.length; i < len; i++) {
  929. obj.sorts.add(column.sorts[i], -1);
  930. }
  931. this.appendArrowDown($link);
  932. // If descending, remove sort
  933. } else {
  934. for (var i = 0, len = column.sorts.length; i < len; i++) {
  935. obj.sorts.remove(column.sorts[i]);
  936. }
  937. this.removeArrow($link);
  938. }
  939. // Otherwise, if not already set, set to ascending
  940. } else {
  941. for (var i = 0, len = column.sorts.length; i < len; i++) {
  942. obj.sorts.add(column.sorts[i], 1);
  943. }
  944. this.appendArrowUp($link);
  945. }
  946. };
  947. this.sortedByColumn = function($link, column) {
  948. return utility.allMatch(settings.dataset.sorts, column.sorts, function(sorts, sort) { return sort in sorts; });
  949. };
  950. this.sortedByColumnValue = function(column) {
  951. return settings.dataset.sorts[column.sorts[0]];
  952. };
  953. };
  954. function Queries(obj, settings) {
  955. var _this = this;
  956. this.initOnLoad = function() {
  957. return settings.inputs.queries || settings.features.search;
  958. };
  959. this.init = function() {
  960. var queriesUrl = window.location.search.match(new RegExp(settings.params.queries + '[^&=]*=[^&]*', 'g'));
  961. settings.dataset.queries = queriesUrl ? utility.deserialize(queriesUrl)[settings.params.queries] : {};
  962. if (settings.dataset.queries === "") { settings.dataset.queries = {}; }
  963. if (settings.inputs.queries) {
  964. this.setupInputs();
  965. }
  966. };
  967. this.add = function(name, value) {
  968. // reset to first page since query will change records
  969. if (settings.features.paginate) {
  970. settings.dataset.page = 1;
  971. }
  972. settings.dataset.queries[name] = value;
  973. obj.$element.trigger('dynatable:queries:added', [name, value]);
  974. return dt;
  975. };
  976. this.remove = function(name) {
  977. delete settings.dataset.queries[name];
  978. obj.$element.trigger('dynatable:queries:removed', name);
  979. return dt;
  980. };
  981. this.run = function() {
  982. for (query in settings.dataset.queries) {
  983. if (settings.dataset.queries.hasOwnProperty(query)) {
  984. var value = settings.dataset.queries[query];
  985. if (_this.functions[query] === undefined) {
  986. // Try to lazily evaluate query from column names if not explicitly defined
  987. var queryColumn = utility.findObjectInArray(settings.table.columns, {id: query});
  988. if (queryColumn) {
  989. _this.functions[query] = function(record, queryValue) {
  990. return record[query] == queryValue;
  991. };
  992. } else {
  993. $.error("Query named '" + query + "' called, but not defined in queries.functions");
  994. continue; // to skip to next query
  995. }
  996. }
  997. // collect all records that return true for query
  998. settings.dataset.records = $.map(settings.dataset.records, function(record) {
  999. return _this.functions[query](record, value) ? record : null;
  1000. });
  1001. }
  1002. }
  1003. settings.dataset.queryRecordCount = obj.records.count();
  1004. };
  1005. // Shortcut for performing simple query from built-in search
  1006. this.runSearch = function(q) {
  1007. var origQueries = $.extend({}, settings.dataset.queries);
  1008. if (q) {
  1009. this.add('search', q);
  1010. } else {
  1011. this.remove('search');
  1012. }
  1013. if (!utility.objectsEqual(settings.dataset.queries, origQueries)) {
  1014. obj.process();
  1015. }
  1016. };
  1017. this.setupInputs = function() {
  1018. settings.inputs.queries.each(function() {
  1019. var $this = $(this),
  1020. event = $this.data('dynatable-query-event') || settings.inputs.queryEvent,
  1021. query = $this.data('dynatable-query') || $this.attr('name') || this.id,
  1022. queryFunction = function(e) {
  1023. var q = $(this).val();
  1024. if (q === "") { q = undefined; }
  1025. if (q === settings.dataset.queries[query]) { return false; }
  1026. if (q) {
  1027. _this.add(query, q);
  1028. } else {
  1029. _this.remove(query);
  1030. }
  1031. obj.process();
  1032. e.preventDefault();
  1033. };
  1034. $this
  1035. .attr('data-dynatable-query', query)
  1036. .bind(event, queryFunction)
  1037. .bind('keypress', function(e) {
  1038. if (e.which == 13) {
  1039. queryFunction.call(this, e);
  1040. }
  1041. });
  1042. if (settings.dataset.queries[query]) { $this.val(decodeURIComponent(settings.dataset.queries[query])); }
  1043. });
  1044. };
  1045. // Query functions for in-page querying
  1046. // each function should take a record and a value as input
  1047. // and output true of false as to whether the record is a match or not
  1048. this.functions = {
  1049. search: function(record, queryValue) {
  1050. var contains = false;
  1051. // Loop through each attribute of record
  1052. for (attr in record) {
  1053. if (record.hasOwnProperty(attr)) {
  1054. var attrValue = record[attr];
  1055. if (typeof(attrValue) === "string" && attrValue.toLowerCase().indexOf(queryValue.toLowerCase()) !== -1) {
  1056. contains = true;
  1057. // Don't need to keep searching attributes once found
  1058. break;
  1059. } else {
  1060. continue;
  1061. }
  1062. }
  1063. }
  1064. return contains;
  1065. }
  1066. };
  1067. };
  1068. function InputsSearch(obj, settings) {
  1069. var _this = this;
  1070. this.initOnLoad = function() {
  1071. return settings.features.search;
  1072. };
  1073. this.init = function() {
  1074. this.attach();
  1075. };
  1076. this.create = function() {
  1077. var $search = $('<input />', {
  1078. type: 'search',
  1079. id: 'dynatable-query-search-' + obj.element.id,
  1080. 'data-dynatable-query': 'search',
  1081. value: settings.dataset.queries.search
  1082. }),
  1083. $searchSpan = $('<span></span>', {
  1084. id: 'dynatable-search-' + obj.element.id,
  1085. 'class': 'dynatable-search',
  1086. text: settings.inputs.searchText
  1087. }).append($search);
  1088. $search
  1089. .bind(settings.inputs.queryEvent, function() {
  1090. obj.queries.runSearch($(this).val());
  1091. })
  1092. .bind('keypress', function(e) {
  1093. if (e.which == 13) {
  1094. obj.queries.runSearch($(this).val());
  1095. e.preventDefault();
  1096. }
  1097. });
  1098. return $searchSpan;
  1099. };
  1100. this.attach = function() {
  1101. var $target = settings.inputs.searchTarget ? $(settings.inputs.searchTarget) : obj.$element;
  1102. $target[settings.inputs.searchPlacement](this.create());
  1103. };
  1104. };
  1105. // provide a public function for selecting page
  1106. function PaginationPage(obj, settings) {
  1107. this.initOnLoad = function() {
  1108. return settings.features.paginate;
  1109. };
  1110. this.init = function() {
  1111. var pageUrl = window.location.search.match(new RegExp(settings.params.page + '=([^&]*)'));
  1112. // If page is present in URL parameters and pushState is enabled
  1113. // (meaning that it'd be possible for dynatable to have put the
  1114. // page parameter in the URL)
  1115. if (pageUrl && settings.features.pushState) {
  1116. this.set(pageUrl[1]);
  1117. } else {
  1118. this.set(1);
  1119. }
  1120. };
  1121. this.set = function(page) {
  1122. var newPage = parseInt(page, 10);
  1123. settings.dataset.page = newPage;
  1124. obj.$element.trigger('dynatable:page:set', newPage);
  1125. }
  1126. };
  1127. function PaginationPerPage(obj, settings) {
  1128. var _this = this;
  1129. this.initOnLoad = function() {
  1130. return settings.features.paginate;
  1131. };
  1132. this.init = function() {
  1133. var perPageUrl = window.location.search.match(new RegExp(settings.params.perPage + '=([^&]*)'));
  1134. // If perPage is present in URL parameters and pushState is enabled
  1135. // (meaning that it'd be possible for dynatable to have put the
  1136. // perPage parameter in the URL)
  1137. if (perPageUrl && settings.features.pushState) {
  1138. // Don't reset page to 1 on init, since it might override page
  1139. // set on init from URL
  1140. this.set(perPageUrl[1], true);
  1141. } else {
  1142. this.set(settings.dataset.perPageDefault, true);
  1143. }
  1144. if (settings.features.perPageSelect) {
  1145. this.attach();
  1146. }
  1147. };
  1148. this.create = function() {
  1149. var $select = $('<select>', {
  1150. id: 'dynatable-per-page-' + obj.element.id,
  1151. 'class': 'dynatable-per-page-select'
  1152. });
  1153. for (var i = 0, len = settings.dataset.perPageOptions.length; i < len; i++) {
  1154. var number = settings.dataset.perPageOptions[i],
  1155. selected = settings.dataset.perPage == number ? 'selected="selected"' : '';
  1156. $select.append('<option value="' + number + '" ' + selected + '>' + number + '</option>');
  1157. }
  1158. $select.bind('change', function(e) {
  1159. _this.set($(this).val());
  1160. obj.process();
  1161. });
  1162. return $('<span />', {
  1163. 'class': 'dynatable-per-page'
  1164. }).append("<span class='dynatable-per-page-label'>" + settings.inputs.perPageText + "</span>").append($select);
  1165. };
  1166. this.attach = function() {
  1167. var $target = settings.inputs.perPageTarget ? $(settings.inputs.perPageTarget) : obj.$element;
  1168. $target[settings.inputs.perPagePlacement](this.create());
  1169. };
  1170. this.set = function(number, skipResetPage) {
  1171. var newPerPage = parseInt(number);
  1172. if (!skipResetPage) { obj.paginationPage.set(1); }
  1173. settings.dataset.perPage = newPerPage;
  1174. obj.$element.trigger('dynatable:perPage:set', newPerPage);
  1175. };
  1176. };
  1177. // pagination links which update dataset.page attribute
  1178. function PaginationLinks(obj, settings) {
  1179. var _this = this;
  1180. this.initOnLoad = function() {
  1181. return settings.features.paginate;
  1182. };
  1183. this.init = function() {
  1184. this.attach();
  1185. };
  1186. this.create = function() {
  1187. var pageLinks = '<ul id="' + 'dynatable-pagination-links-' + obj.element.id + '" class="' + settings.inputs.paginationClass + '">',
  1188. pageLinkClass = settings.inputs.paginationLinkClass,
  1189. activePageClass = settings.inputs.paginationActiveClass,
  1190. disabledPageClass = settings.inputs.paginationDisabledClass,
  1191. pages = Math.ceil(settings.dataset.queryRecordCount / settings.dataset.perPage),
  1192. page = settings.dataset.page,
  1193. breaks = [
  1194. settings.inputs.paginationGap[0],
  1195. settings.dataset.page - settings.inputs.paginationGap[1],
  1196. settings.dataset.page + settings.inputs.paginationGap[2],
  1197. (pages + 1) - settings.inputs.paginationGap[3]
  1198. ];
  1199. pageLinks += '<li><span>' + settings.inputs.pageText + '</span></li>';
  1200. for (var i = 1; i <= pages; i++) {
  1201. if ( (i > breaks[0] && i < breaks[1]) || (i > breaks[2] && i < breaks[3])) {
  1202. // skip to next iteration in loop
  1203. continue;
  1204. } else {
  1205. var li = obj.paginationLinks.buildLink(i, i, pageLinkClass, page == i, activePageClass),
  1206. breakIndex,
  1207. nextBreak;
  1208. // If i is not between one of the following
  1209. // (1 + (settings.paginationGap[0]))
  1210. // (page - settings.paginationGap[1])
  1211. // (page + settings.paginationGap[2])
  1212. // (pages - settings.paginationGap[3])
  1213. breakIndex = $.inArray(i, breaks);
  1214. nextBreak = breaks[breakIndex + 1];
  1215. if (breakIndex > 0 && i !== 1 && nextBreak && nextBreak > (i + 1)) {
  1216. var ellip = '<li><span class="dynatable-page-break">&hellip;</span></li>';
  1217. li = breakIndex < 2 ? ellip + li : li + ellip;
  1218. }
  1219. if (settings.inputs.paginationPrev && i === 1) {
  1220. var prevLi = obj.paginationLinks.buildLink(page - 1, settings.inputs.paginationPrev, pageLinkClass + ' ' + settings.inputs.paginationPrevClass, page === 1, disabledPageClass);
  1221. li = prevLi + li;
  1222. }
  1223. if (settings.inputs.paginationNext && i === pages) {
  1224. var nextLi = obj.paginationLinks.buildLink(page + 1, settings.inputs.paginationNext, pageLinkClass + ' ' + settings.inputs.paginationNextClass, page === pages, disabledPageClass);
  1225. li += nextLi;
  1226. }
  1227. pageLinks += li;
  1228. }
  1229. }
  1230. pageLinks += '</ul>';
  1231. // only bind page handler to non-active and non-disabled page links
  1232. var selector = '#dynatable-pagination-links-' + obj.element.id + ' a.' + pageLinkClass + ':not(.' + activePageClass + ',.' + disabledPageClass + ')';
  1233. // kill any existing delegated-bindings so they don't stack up
  1234. $(document).undelegate(selector, 'click.dynatable');
  1235. $(document).delegate(selector, 'click.dynatable', function(e) {
  1236. $this = $(this);
  1237. $this.closest(settings.inputs.paginationClass).find('.' + activePageClass).removeClass(activePageClass);
  1238. $this.addClass(activePageClass);
  1239. obj.paginationPage.set($this.data('dynatable-page'));
  1240. obj.process();
  1241. e.preventDefault();
  1242. });
  1243. return pageLinks;
  1244. };
  1245. this.buildLink = function(page, label, linkClass, conditional, conditionalClass) {
  1246. var link = '<a data-dynatable-page=' + page + ' class="' + linkClass,
  1247. li = '<li';
  1248. if (conditional) {
  1249. link += ' ' + conditionalClass;
  1250. li += ' class="' + conditionalClass + '"';
  1251. }
  1252. link += '">' + label + '</a>';
  1253. li += '>' + link + '</li>';
  1254. return li;
  1255. };
  1256. this.attach = function() {
  1257. // append page links *after* delegate-event-binding so it doesn't need to
  1258. // find and select all page links to bind event
  1259. var $target = settings.inputs.paginationLinkTarget ? $(settings.inputs.paginationLinkTarget) : obj.$element;
  1260. $target[settings.inputs.paginationLinkPlacement](obj.paginationLinks.create());
  1261. };
  1262. };
  1263. utility = dt.utility = {
  1264. normalizeText: function(text, style) {
  1265. text = this.textTransform[style](text);
  1266. return text;
  1267. },
  1268. textTransform: {
  1269. trimDash: function(text) {
  1270. return text.replace(/^\s+|\s+$/g, "").replace(/\s+/g, "-");
  1271. },
  1272. camelCase: function(text) {
  1273. text = this.trimDash(text);
  1274. return text
  1275. .replace(/(\-[a-zA-Z])/g, function($1){return $1.toUpperCase().replace('-','');})
  1276. .replace(/([A-Z])([A-Z]+)/g, function($1,$2,$3){return $2 + $3.toLowerCase();})
  1277. .replace(/^[A-Z]/, function($1){return $1.toLowerCase();});
  1278. },
  1279. dashed: function(text) {
  1280. text = this.trimDash(text);
  1281. return this.lowercase(text);
  1282. },
  1283. underscore: function(text) {
  1284. text = this.trimDash(text);
  1285. return this.lowercase(text.replace(/(-)/g, '_'));
  1286. },
  1287. lowercase: function(text) {
  1288. return text.replace(/([A-Z])/g, function($1){return $1.toLowerCase();});
  1289. }
  1290. },
  1291. // Deserialize params in URL to object
  1292. // see http://stackoverflow.com/questions/1131630/javascript-jquery-param-inverse-function/3401265#3401265
  1293. deserialize: function(query) {
  1294. if (!query) return {};
  1295. // modified to accept an array of partial URL strings
  1296. if (typeof(query) === "object") { query = query.join('&'); }
  1297. var hash = {},
  1298. vars = query.split("&");
  1299. for (var i = 0; i < vars.length; i++) {
  1300. var pair = vars[i].split("="),
  1301. k = decodeURIComponent(pair[0]),
  1302. v, m;
  1303. if (!pair[1]) { continue };
  1304. v = decodeURIComponent(pair[1].replace(/\+/g, ' '));
  1305. // modified to parse multi-level parameters (e.g. "hi[there][dude]=whatsup" => hi: {there: {dude: "whatsup"}})
  1306. while (m = k.match(/([^&=]+)\[([^&=]+)\]$/)) {
  1307. var origV = v;
  1308. k = m[1];
  1309. v = {};
  1310. // If nested param ends in '][', then the regex above erroneously included half of a trailing '[]',
  1311. // which indicates the end-value is part of an array
  1312. if (m[2].substr(m[2].length-2) == '][') { // must use substr for IE to understand it
  1313. v[m[2].substr(0,m[2].length-2)] = [origV];
  1314. } else {
  1315. v[m[2]] = origV;
  1316. }
  1317. }
  1318. // If it is the first entry with this name
  1319. if (typeof hash[k] === "undefined") {
  1320. if (k.substr(k.length-2) != '[]') { // not end with []. cannot use negative index as IE doesn't understand it
  1321. hash[k] = v;
  1322. } else {
  1323. hash[k] = [v];
  1324. }
  1325. // If subsequent entry with this name and not array
  1326. } else if (typeof hash[k] === "string") {
  1327. hash[k] = v; // replace it
  1328. // modified to add support for objects
  1329. } else if (typeof hash[k] === "object") {
  1330. hash[k] = $.extend({}, hash[k], v);
  1331. // If subsequent entry with this name and is array
  1332. } else {
  1333. hash[k].push(v);
  1334. }
  1335. }
  1336. return hash;
  1337. },
  1338. refreshQueryString: function(urlString, data, settings) {
  1339. var _this = this,
  1340. queryString = urlString.split('?'),
  1341. path = queryString.shift(),
  1342. urlOptions;
  1343. urlOptions = this.deserialize(urlString);
  1344. // Loop through each dynatable param and update the URL with it
  1345. for (attr in settings.params) {
  1346. if (settings.params.hasOwnProperty(attr)) {
  1347. var label = settings.params[attr];
  1348. // Skip over parameters matching attributes for disabled features (i.e. leave them untouched),
  1349. // because if the feature is turned off, then parameter name is a coincidence and it's unrelated to dynatable.
  1350. if (
  1351. (!settings.features.sort && attr == "sorts") ||
  1352. (!settings.features.paginate && _this.anyMatch(attr, ["page", "perPage", "offset"], function(attr, param) { return attr == param; }))
  1353. ) {
  1354. continue;
  1355. }
  1356. // Delete page and offset from url params if on page 1 (default)
  1357. if ((attr === "page" || attr === "offset") && data["page"] === 1) {
  1358. if (urlOptions[label]) {
  1359. delete urlOptions[label];
  1360. }
  1361. continue;
  1362. }
  1363. // Delete perPage from url params if default perPage value
  1364. if (attr === "perPage" && data[label] == settings.dataset.perPageDefault) {
  1365. if (urlOptions[label]) {
  1366. delete urlOptions[label];
  1367. }
  1368. continue;
  1369. }
  1370. // For queries, we're going to handle each possible query parameter individually here instead of
  1371. // handling the entire queries object below, since we need to make sure that this is a query controlled by dynatable.
  1372. if (attr == "queries" && data[label]) {
  1373. var queries = settings.inputs.queries || [],
  1374. inputQueries = $.makeArray(queries.map(function() { return $(this).attr('name') }));
  1375. if (settings.features.search) { inputQueries.push('search'); }
  1376. for (var i = 0, len = inputQueries.length; i < len; i++) {
  1377. var attr = inputQueries[i];
  1378. if (data[label][attr]) {
  1379. if (typeof urlOptions[label] === 'undefined') { urlOptions[label] = {}; }
  1380. urlOptions[label][attr] = data[label][attr];
  1381. } else {
  1382. delete urlOptions[label][attr];
  1383. }
  1384. }
  1385. continue;
  1386. }
  1387. // If we haven't returned true by now, then we actually want to update the parameter in the URL
  1388. if (data[label]) {
  1389. urlOptions[label] = data[label];
  1390. } else {
  1391. delete urlOptions[label];
  1392. }
  1393. }
  1394. }
  1395. return $.param(urlOptions);
  1396. },
  1397. // Get array of keys from object
  1398. // see http://stackoverflow.com/questions/208016/how-to-list-the-properties-of-a-javascript-object/208020#208020
  1399. keysFromObject: function(obj){
  1400. var keys = [];
  1401. for (var key in obj){
  1402. keys.push(key);
  1403. }
  1404. return keys;
  1405. },
  1406. // Find an object in an array of objects by attributes.
  1407. // E.g. find object with {id: 'hi', name: 'there'} in an array of objects
  1408. findObjectInArray: function(array, objectAttr) {
  1409. var _this = this,
  1410. foundObject;
  1411. for (var i = 0, len = array.length; i < len; i++) {
  1412. var item = array[i];
  1413. // For each object in array, test to make sure all attributes in objectAttr match
  1414. if (_this.allMatch(item, objectAttr, function(item, key, value) { return item[key] == value; })) {
  1415. foundObject = item;
  1416. break;
  1417. }
  1418. }
  1419. return foundObject;
  1420. },
  1421. // Return true if supplied test function passes for ALL items in an array
  1422. allMatch: function(item, arrayOrObject, test) {
  1423. // start off with true result by default
  1424. var match = true,
  1425. isArray = $.isArray(arrayOrObject);
  1426. // Loop through all items in array
  1427. $.each(arrayOrObject, function(key, value) {
  1428. var result = isArray ? test(item, value) : test(item, key, value);
  1429. // If a single item tests false, go ahead and break the array by returning false
  1430. // and return false as result,
  1431. // otherwise, continue with next iteration in loop
  1432. // (if we make it through all iterations without overriding match with false,
  1433. // then we can return the true result we started with by default)
  1434. if (!result) { return match = false; }
  1435. });
  1436. return match;
  1437. },
  1438. // Return true if supplied test function passes for ANY items in an array
  1439. anyMatch: function(item, arrayOrObject, test) {
  1440. var match = false,
  1441. isArray = $.isArray(arrayOrObject);
  1442. $.each(arrayOrObject, function(key, value) {
  1443. var result = isArray ? test(item, value) : test(item, key, value);
  1444. if (result) {
  1445. // As soon as a match is found, set match to true, and return false to stop the `$.each` loop
  1446. match = true;
  1447. return false;
  1448. }
  1449. });
  1450. return match;
  1451. },
  1452. // Return true if two objects are equal
  1453. // (i.e. have the same attributes and attribute values)
  1454. objectsEqual: function(a, b) {
  1455. for (attr in a) {
  1456. if (a.hasOwnProperty(attr)) {
  1457. if (!b.hasOwnProperty(attr) || a[attr] !== b[attr]) {
  1458. return false;
  1459. }
  1460. }
  1461. }
  1462. for (attr in b) {
  1463. if (b.hasOwnProperty(attr) && !a.hasOwnProperty(attr)) {
  1464. return false;
  1465. }
  1466. }
  1467. return true;
  1468. },
  1469. // Taken from http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/105074#105074
  1470. randomHash: function() {
  1471. return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
  1472. },
  1473. // Adapted from http://stackoverflow.com/questions/377961/efficient-javascript-string-replacement/378001#378001
  1474. template: function(str, data) {
  1475. return str.replace(/{(\w*)}/g, function(match, key) {
  1476. return data.hasOwnProperty(key) ? data[key] : "";
  1477. });
  1478. }
  1479. };
  1480. //-----------------------------------------------------------------
  1481. // Build the dynatable plugin
  1482. //-----------------------------------------------------------------
  1483. // Object.create support test, and fallback for browsers without it
  1484. if ( typeof Object.create !== "function" ) {
  1485. Object.create = function (o) {
  1486. function F() {}
  1487. F.prototype = o;
  1488. return new F();
  1489. };
  1490. }
  1491. //-----------------------------------------------------------------
  1492. // Global dynatable plugin setting defaults
  1493. //-----------------------------------------------------------------
  1494. $.dynatableSetup = function(options) {
  1495. defaults = mergeSettings(options);
  1496. };
  1497. // Create dynatable plugin based on a defined object
  1498. $.dynatable = function( object ) {
  1499. $.fn['dynatable'] = function( options ) {
  1500. return this.each(function() {
  1501. if ( ! $.data( this, 'dynatable' ) ) {
  1502. $.data( this, 'dynatable', Object.create(object).init(this, options) );
  1503. }
  1504. });
  1505. };
  1506. };
  1507. $.dynatable(dt);
  1508. })(jQuery);