jquery.dynatable.js 57 KB

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