tablesaw.jquery.js 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076
  1. /*! Tablesaw - v3.0.9 - 2018-02-14
  2. * https://github.com/filamentgroup/tablesaw
  3. * Copyright (c) 2018 Filament Group; Licensed MIT */
  4. (function (root, factory) {
  5. if (typeof define === 'function' && define.amd) {
  6. define(["jquery"], function (jQuery) {
  7. return (root.Tablesaw = factory(jQuery, root));
  8. });
  9. } else if (typeof exports === 'object') {
  10. if( "document" in root ) {
  11. module.exports = factory(require('jquery'), root);
  12. } else {
  13. // special jQuery case for CommonJS (pass in a window)
  14. module.exports = factory(require('jquery')(root), root);
  15. }
  16. } else {
  17. root.Tablesaw = factory(jQuery, root);
  18. }
  19. }(typeof window !== "undefined" ? window : this, function ($, window) {
  20. "use strict";
  21. var document = window.document;
  22. var domContentLoadedTriggered = false;
  23. document.addEventListener("DOMContentLoaded", function() {
  24. domContentLoadedTriggered = true;
  25. });
  26. var Tablesaw = {
  27. i18n: {
  28. modeStack: "Stack",
  29. modeSwipe: "Swipe",
  30. modeToggle: "Toggle",
  31. modeSwitchColumnsAbbreviated: "Cols",
  32. modeSwitchColumns: "Columns",
  33. columnToggleButton: "Columns",
  34. columnToggleError: "No eligible columns.",
  35. sort: "Sort",
  36. swipePreviousColumn: "Previous column",
  37. swipeNextColumn: "Next column"
  38. },
  39. // cut the mustard
  40. mustard:
  41. "head" in document && // IE9+, Firefox 4+, Safari 5.1+, Mobile Safari 4.1+, Opera 11.5+, Android 2.3+
  42. (!window.blackberry || window.WebKitPoint) && // only WebKit Blackberry (OS 6+)
  43. !window.operamini,
  44. $: $,
  45. _init: function(element) {
  46. Tablesaw.$(element || document).trigger("enhance.tablesaw");
  47. },
  48. init: function(element) {
  49. if (!domContentLoadedTriggered) {
  50. if ("addEventListener" in document) {
  51. // Use raw DOMContentLoaded instead of shoestring (may have issues in Android 2.3, exhibited by stack table)
  52. document.addEventListener("DOMContentLoaded", function() {
  53. Tablesaw._init(element);
  54. });
  55. }
  56. } else {
  57. Tablesaw._init(element);
  58. }
  59. }
  60. };
  61. $(document).on("enhance.tablesaw", function() {
  62. // Extend i18n config, if one exists.
  63. if (typeof TablesawConfig !== "undefined" && TablesawConfig.i18n) {
  64. Tablesaw.i18n = $.extend(Tablesaw.i18n, TablesawConfig.i18n || {});
  65. }
  66. Tablesaw.i18n.modes = [
  67. Tablesaw.i18n.modeStack,
  68. Tablesaw.i18n.modeSwipe,
  69. Tablesaw.i18n.modeToggle
  70. ];
  71. });
  72. if (Tablesaw.mustard) {
  73. $(document.documentElement).addClass("tablesaw-enhanced");
  74. }
  75. (function() {
  76. var pluginName = "tablesaw";
  77. var classes = {
  78. toolbar: "tablesaw-bar"
  79. };
  80. var events = {
  81. create: "tablesawcreate",
  82. destroy: "tablesawdestroy",
  83. refresh: "tablesawrefresh",
  84. resize: "tablesawresize"
  85. };
  86. var defaultMode = "stack";
  87. var initSelector = "table";
  88. var initFilterSelector = "[data-tablesaw],[data-tablesaw-mode],[data-tablesaw-sortable]";
  89. var defaultConfig = {};
  90. Tablesaw.events = events;
  91. var Table = function(element) {
  92. if (!element) {
  93. throw new Error("Tablesaw requires an element.");
  94. }
  95. this.table = element;
  96. this.$table = $(element);
  97. // only one <thead> and <tfoot> are allowed, per the specification
  98. this.$thead = this.$table
  99. .children()
  100. .filter("thead")
  101. .eq(0);
  102. // multiple <tbody> are allowed, per the specification
  103. this.$tbody = this.$table.children().filter("tbody");
  104. this.mode = this.$table.attr("data-tablesaw-mode") || defaultMode;
  105. this.$toolbar = null;
  106. this.attributes = {
  107. subrow: "data-tablesaw-subrow",
  108. ignorerow: "data-tablesaw-ignorerow"
  109. };
  110. this.init();
  111. };
  112. Table.prototype.init = function() {
  113. if (!this.$thead.length) {
  114. throw new Error("tablesaw: a <thead> is required, but none was found.");
  115. }
  116. if (!this.$thead.find("th").length) {
  117. throw new Error("tablesaw: no header cells found. Are you using <th> inside of <thead>?");
  118. }
  119. // assign an id if there is none
  120. if (!this.$table.attr("id")) {
  121. this.$table.attr("id", pluginName + "-" + Math.round(Math.random() * 10000));
  122. }
  123. this.createToolbar();
  124. this._initCells();
  125. this.$table.data(pluginName, this);
  126. this.$table.trigger(events.create, [this]);
  127. };
  128. Table.prototype.getConfig = function(pluginSpecificConfig) {
  129. // shoestring extend doesn’t support arbitrary args
  130. var configs = $.extend(defaultConfig, pluginSpecificConfig || {});
  131. return $.extend(configs, typeof TablesawConfig !== "undefined" ? TablesawConfig : {});
  132. };
  133. Table.prototype._getPrimaryHeaderRow = function() {
  134. return this._getHeaderRows().eq(0);
  135. };
  136. Table.prototype._getHeaderRows = function() {
  137. return this.$thead
  138. .children()
  139. .filter("tr")
  140. .filter(function() {
  141. return !$(this).is("[data-tablesaw-ignorerow]");
  142. });
  143. };
  144. Table.prototype._getRowIndex = function($row) {
  145. return $row.prevAll().length;
  146. };
  147. Table.prototype._getHeaderRowIndeces = function() {
  148. var self = this;
  149. var indeces = [];
  150. this._getHeaderRows().each(function() {
  151. indeces.push(self._getRowIndex($(this)));
  152. });
  153. return indeces;
  154. };
  155. Table.prototype._getPrimaryHeaderCells = function($row) {
  156. return ($row || this._getPrimaryHeaderRow()).find("th");
  157. };
  158. Table.prototype._$getCells = function(th) {
  159. var self = this;
  160. return $(th)
  161. .add(th.cells)
  162. .filter(function() {
  163. var $t = $(this);
  164. var $row = $t.parent();
  165. var hasColspan = $t.is("[colspan]");
  166. // no subrows or ignored rows (keep cells in ignored rows that do not have a colspan)
  167. return (
  168. !$row.is("[" + self.attributes.subrow + "]") &&
  169. (!$row.is("[" + self.attributes.ignorerow + "]") || !hasColspan)
  170. );
  171. });
  172. };
  173. Table.prototype._getVisibleColspan = function() {
  174. var colspan = 0;
  175. this._getPrimaryHeaderCells().each(function() {
  176. var $t = $(this);
  177. if ($t.css("display") !== "none") {
  178. colspan += parseInt($t.attr("colspan"), 10) || 1;
  179. }
  180. });
  181. return colspan;
  182. };
  183. Table.prototype.getColspanForCell = function($cell) {
  184. var visibleColspan = this._getVisibleColspan();
  185. var visibleSiblingColumns = 0;
  186. if ($cell.closest("tr").data("tablesaw-rowspanned")) {
  187. visibleSiblingColumns++;
  188. }
  189. $cell.siblings().each(function() {
  190. var $t = $(this);
  191. var colColspan = parseInt($t.attr("colspan"), 10) || 1;
  192. if ($t.css("display") !== "none") {
  193. visibleSiblingColumns += colColspan;
  194. }
  195. });
  196. // console.log( $cell[ 0 ], visibleColspan, visibleSiblingColumns );
  197. return visibleColspan - visibleSiblingColumns;
  198. };
  199. Table.prototype.isCellInColumn = function(header, cell) {
  200. return $(header)
  201. .add(header.cells)
  202. .filter(function() {
  203. return this === cell;
  204. }).length;
  205. };
  206. Table.prototype.updateColspanCells = function(cls, header, userAction) {
  207. var self = this;
  208. var primaryHeaderRow = self._getPrimaryHeaderRow();
  209. // find persistent column rowspans
  210. this.$table.find("[rowspan][data-tablesaw-priority]").each(function() {
  211. var $t = $(this);
  212. if ($t.attr("data-tablesaw-priority") !== "persist") {
  213. return;
  214. }
  215. var $row = $t.closest("tr");
  216. var rowspan = parseInt($t.attr("rowspan"), 10);
  217. if (rowspan > 1) {
  218. $row = $row.next();
  219. $row.data("tablesaw-rowspanned", true);
  220. rowspan--;
  221. }
  222. });
  223. this.$table
  224. .find("[colspan],[data-tablesaw-maxcolspan]")
  225. .filter(function() {
  226. // is not in primary header row
  227. return $(this).closest("tr")[0] !== primaryHeaderRow[0];
  228. })
  229. .each(function() {
  230. var $cell = $(this);
  231. if (userAction === undefined || self.isCellInColumn(header, this)) {
  232. } else {
  233. // if is not a user action AND the cell is not in the updating column, kill it
  234. return;
  235. }
  236. var colspan = self.getColspanForCell($cell);
  237. if (cls && userAction !== undefined) {
  238. // console.log( colspan === 0 ? "addClass" : "removeClass", $cell );
  239. $cell[colspan === 0 ? "addClass" : "removeClass"](cls);
  240. }
  241. // cache original colspan
  242. var maxColspan = parseInt($cell.attr("data-tablesaw-maxcolspan"), 10);
  243. if (!maxColspan) {
  244. $cell.attr("data-tablesaw-maxcolspan", $cell.attr("colspan"));
  245. } else if (colspan > maxColspan) {
  246. colspan = maxColspan;
  247. }
  248. // console.log( this, "setting colspan to ", colspan );
  249. $cell.attr("colspan", colspan);
  250. });
  251. };
  252. Table.prototype._findPrimaryHeadersForCell = function(cell) {
  253. var $headerRow = this._getPrimaryHeaderRow();
  254. var $headers = this._getPrimaryHeaderCells($headerRow);
  255. var headerRowIndex = this._getRowIndex($headerRow);
  256. var results = [];
  257. for (var rowNumber = 0; rowNumber < this.headerMapping.length; rowNumber++) {
  258. if (rowNumber === headerRowIndex) {
  259. continue;
  260. }
  261. for (var colNumber = 0; colNumber < this.headerMapping[rowNumber].length; colNumber++) {
  262. if (this.headerMapping[rowNumber][colNumber] === cell) {
  263. results.push($headers[colNumber]);
  264. }
  265. }
  266. }
  267. return results;
  268. };
  269. // used by init cells
  270. Table.prototype.getRows = function() {
  271. var self = this;
  272. return this.$table.find("tr").filter(function() {
  273. return $(this)
  274. .closest("table")
  275. .is(self.$table);
  276. });
  277. };
  278. // used by sortable
  279. Table.prototype.getBodyRows = function(tbody) {
  280. return (tbody ? $(tbody) : this.$tbody).children().filter("tr");
  281. };
  282. Table.prototype.getHeaderCellIndex = function(cell) {
  283. var lookup = this.headerMapping[0];
  284. for (var colIndex = 0; colIndex < lookup.length; colIndex++) {
  285. if (lookup[colIndex] === cell) {
  286. return colIndex;
  287. }
  288. }
  289. return -1;
  290. };
  291. Table.prototype._initCells = function() {
  292. // re-establish original colspans
  293. this.$table.find("[data-tablesaw-maxcolspan]").each(function() {
  294. var $t = $(this);
  295. $t.attr("colspan", $t.attr("data-tablesaw-maxcolspan"));
  296. });
  297. var $rows = this.getRows();
  298. var columnLookup = [];
  299. $rows.each(function(rowNumber) {
  300. columnLookup[rowNumber] = [];
  301. });
  302. $rows.each(function(rowNumber) {
  303. var coltally = 0;
  304. var $t = $(this);
  305. var children = $t.children();
  306. children.each(function() {
  307. var colspan = parseInt(
  308. this.getAttribute("data-tablesaw-maxcolspan") || this.getAttribute("colspan"),
  309. 10
  310. );
  311. var rowspan = parseInt(this.getAttribute("rowspan"), 10);
  312. // set in a previous rowspan
  313. while (columnLookup[rowNumber][coltally]) {
  314. coltally++;
  315. }
  316. columnLookup[rowNumber][coltally] = this;
  317. // TODO? both colspan and rowspan
  318. if (colspan) {
  319. for (var k = 0; k < colspan - 1; k++) {
  320. coltally++;
  321. columnLookup[rowNumber][coltally] = this;
  322. }
  323. }
  324. if (rowspan) {
  325. for (var j = 1; j < rowspan; j++) {
  326. columnLookup[rowNumber + j][coltally] = this;
  327. }
  328. }
  329. coltally++;
  330. });
  331. });
  332. var headerRowIndeces = this._getHeaderRowIndeces();
  333. for (var colNumber = 0; colNumber < columnLookup[0].length; colNumber++) {
  334. for (var headerIndex = 0, k = headerRowIndeces.length; headerIndex < k; headerIndex++) {
  335. var headerCol = columnLookup[headerRowIndeces[headerIndex]][colNumber];
  336. var rowNumber = headerRowIndeces[headerIndex];
  337. var rowCell;
  338. if (!headerCol.cells) {
  339. headerCol.cells = [];
  340. }
  341. while (rowNumber < columnLookup.length) {
  342. rowCell = columnLookup[rowNumber][colNumber];
  343. if (headerCol !== rowCell) {
  344. headerCol.cells.push(rowCell);
  345. }
  346. rowNumber++;
  347. }
  348. }
  349. }
  350. this.headerMapping = columnLookup;
  351. };
  352. Table.prototype.refresh = function() {
  353. this._initCells();
  354. this.$table.trigger(events.refresh, [this]);
  355. };
  356. Table.prototype._getToolbarAnchor = function() {
  357. var $parent = this.$table.parent();
  358. if ($parent.is(".tablesaw-overflow")) {
  359. return $parent;
  360. }
  361. return this.$table;
  362. };
  363. Table.prototype._getToolbar = function($anchor) {
  364. if (!$anchor) {
  365. $anchor = this._getToolbarAnchor();
  366. }
  367. return $anchor.prev().filter("." + classes.toolbar);
  368. };
  369. Table.prototype.createToolbar = function() {
  370. // Insert the toolbar
  371. // TODO move this into a separate component
  372. var $anchor = this._getToolbarAnchor();
  373. var $toolbar = this._getToolbar($anchor);
  374. if (!$toolbar.length) {
  375. $toolbar = $("<div>")
  376. .addClass(classes.toolbar)
  377. .insertBefore($anchor);
  378. }
  379. this.$toolbar = $toolbar;
  380. if (this.mode) {
  381. this.$toolbar.addClass("tablesaw-mode-" + this.mode);
  382. }
  383. };
  384. Table.prototype.destroy = function() {
  385. // Don’t remove the toolbar, just erase the classes on it.
  386. // Some of the table features are not yet destroy-friendly.
  387. this._getToolbar().each(function() {
  388. this.className = this.className.replace(/\btablesaw-mode\-\w*\b/gi, "");
  389. });
  390. var tableId = this.$table.attr("id");
  391. $(document).off("." + tableId);
  392. $(window).off("." + tableId);
  393. // other plugins
  394. this.$table.trigger(events.destroy, [this]);
  395. this.$table.removeData(pluginName);
  396. };
  397. // Collection method.
  398. $.fn[pluginName] = function() {
  399. return this.each(function() {
  400. var $t = $(this);
  401. if ($t.data(pluginName)) {
  402. return;
  403. }
  404. new Table(this);
  405. });
  406. };
  407. var $doc = $(document);
  408. $doc.on("enhance.tablesaw", function(e) {
  409. // Cut the mustard
  410. if (Tablesaw.mustard) {
  411. $(e.target)
  412. .find(initSelector)
  413. .filter(initFilterSelector)
  414. [pluginName]();
  415. }
  416. });
  417. // Avoid a resize during scroll:
  418. // Some Mobile devices trigger a resize during scroll (sometimes when
  419. // doing elastic stretch at the end of the document or from the
  420. // location bar hide)
  421. var isScrolling = false;
  422. var scrollTimeout;
  423. $doc.on("scroll.tablesaw", function() {
  424. isScrolling = true;
  425. window.clearTimeout(scrollTimeout);
  426. scrollTimeout = window.setTimeout(function() {
  427. isScrolling = false;
  428. }, 300); // must be greater than the resize timeout below
  429. });
  430. var resizeTimeout;
  431. $(window).on("resize", function() {
  432. if (!isScrolling) {
  433. window.clearTimeout(resizeTimeout);
  434. resizeTimeout = window.setTimeout(function() {
  435. $doc.trigger(events.resize);
  436. }, 150); // must be less than the scrolling timeout above.
  437. }
  438. });
  439. Tablesaw.Table = Table;
  440. })();
  441. (function() {
  442. var classes = {
  443. stackTable: "tablesaw-stack",
  444. cellLabels: "tablesaw-cell-label",
  445. cellContentLabels: "tablesaw-cell-content"
  446. };
  447. var data = {
  448. key: "tablesaw-stack"
  449. };
  450. var attrs = {
  451. labelless: "data-tablesaw-no-labels",
  452. hideempty: "data-tablesaw-hide-empty"
  453. };
  454. var Stack = function(element, tablesaw) {
  455. this.tablesaw = tablesaw;
  456. this.$table = $(element);
  457. this.labelless = this.$table.is("[" + attrs.labelless + "]");
  458. this.hideempty = this.$table.is("[" + attrs.hideempty + "]");
  459. this.$table.data(data.key, this);
  460. };
  461. Stack.prototype.init = function() {
  462. this.$table.addClass(classes.stackTable);
  463. if (this.labelless) {
  464. return;
  465. }
  466. var self = this;
  467. this.$table
  468. .find("th, td")
  469. .filter(function() {
  470. return !$(this).closest("thead").length;
  471. })
  472. .filter(function() {
  473. return (
  474. !$(this)
  475. .closest("tr")
  476. .is("[" + attrs.labelless + "]") &&
  477. (!self.hideempty || !!$(this).html())
  478. );
  479. })
  480. .each(function() {
  481. var $newHeader = $(document.createElement("b")).addClass(classes.cellLabels);
  482. var $cell = $(this);
  483. $(self.tablesaw._findPrimaryHeadersForCell(this)).each(function(index) {
  484. var $header = $(this.cloneNode(true));
  485. // TODO decouple from sortable better
  486. // Changed from .text() in https://github.com/filamentgroup/tablesaw/commit/b9c12a8f893ec192830ec3ba2d75f062642f935b
  487. // to preserve structural html in headers, like <a>
  488. var $sortableButton = $header.find(".tablesaw-sortable-btn");
  489. $header.find(".tablesaw-sortable-arrow").remove();
  490. // TODO decouple from checkall better
  491. var $checkall = $header.find("[data-tablesaw-checkall]");
  492. $checkall.closest("label").remove();
  493. if ($checkall.length) {
  494. $newHeader = $([]);
  495. return;
  496. }
  497. if (index > 0) {
  498. $newHeader.append(document.createTextNode(", "));
  499. }
  500. var parentNode = $sortableButton.length ? $sortableButton[0] : $header[0];
  501. var el;
  502. while ((el = parentNode.firstChild)) {
  503. $newHeader[0].appendChild(el);
  504. }
  505. });
  506. if ($newHeader.length && !$cell.find("." + classes.cellContentLabels).length) {
  507. $cell.wrapInner("<span class='" + classes.cellContentLabels + "'></span>");
  508. }
  509. // Update if already exists.
  510. var $label = $cell.find("." + classes.cellLabels);
  511. if (!$label.length) {
  512. $cell.prepend($newHeader);
  513. } else {
  514. // only if changed
  515. $label.replaceWith($newHeader);
  516. }
  517. });
  518. };
  519. Stack.prototype.destroy = function() {
  520. this.$table.removeClass(classes.stackTable);
  521. this.$table.find("." + classes.cellLabels).remove();
  522. this.$table.find("." + classes.cellContentLabels).each(function() {
  523. $(this).replaceWith(this.childNodes);
  524. });
  525. };
  526. // on tablecreate, init
  527. $(document)
  528. .on(Tablesaw.events.create, function(e, tablesaw) {
  529. if (tablesaw.mode === "stack") {
  530. var table = new Stack(tablesaw.table, tablesaw);
  531. table.init();
  532. }
  533. })
  534. .on(Tablesaw.events.refresh, function(e, tablesaw) {
  535. if (tablesaw.mode === "stack") {
  536. $(tablesaw.table)
  537. .data(data.key)
  538. .init();
  539. }
  540. })
  541. .on(Tablesaw.events.destroy, function(e, tablesaw) {
  542. if (tablesaw.mode === "stack") {
  543. $(tablesaw.table)
  544. .data(data.key)
  545. .destroy();
  546. }
  547. });
  548. Tablesaw.Stack = Stack;
  549. })();
  550. (function() {
  551. var pluginName = "tablesawbtn",
  552. methods = {
  553. _create: function() {
  554. return $(this).each(function() {
  555. $(this)
  556. .trigger("beforecreate." + pluginName)
  557. [pluginName]("_init")
  558. .trigger("create." + pluginName);
  559. });
  560. },
  561. _init: function() {
  562. var oEl = $(this),
  563. sel = this.getElementsByTagName("select")[0];
  564. if (sel) {
  565. // TODO next major version: remove .btn-select
  566. $(this)
  567. .addClass("btn-select tablesaw-btn-select")
  568. [pluginName]("_select", sel);
  569. }
  570. return oEl;
  571. },
  572. _select: function(sel) {
  573. var update = function(oEl, sel) {
  574. var opts = $(sel).find("option");
  575. var label = document.createElement("span");
  576. var el;
  577. var children;
  578. var found = false;
  579. label.setAttribute("aria-hidden", "true");
  580. label.innerHTML = "&#160;";
  581. opts.each(function() {
  582. var opt = this;
  583. if (opt.selected) {
  584. label.innerHTML = opt.text;
  585. }
  586. });
  587. children = oEl.childNodes;
  588. if (opts.length > 0) {
  589. for (var i = 0, l = children.length; i < l; i++) {
  590. el = children[i];
  591. if (el && el.nodeName.toUpperCase() === "SPAN") {
  592. oEl.replaceChild(label, el);
  593. found = true;
  594. }
  595. }
  596. if (!found) {
  597. oEl.insertBefore(label, oEl.firstChild);
  598. }
  599. }
  600. };
  601. update(this, sel);
  602. // todo should this be tablesawrefresh?
  603. $(this).on("change refresh", function() {
  604. update(this, sel);
  605. });
  606. }
  607. };
  608. // Collection method.
  609. $.fn[pluginName] = function(arrg, a, b, c) {
  610. return this.each(function() {
  611. // if it's a method
  612. if (arrg && typeof arrg === "string") {
  613. return $.fn[pluginName].prototype[arrg].call(this, a, b, c);
  614. }
  615. // don't re-init
  616. if ($(this).data(pluginName + "active")) {
  617. return $(this);
  618. }
  619. $(this).data(pluginName + "active", true);
  620. $.fn[pluginName].prototype._create.call(this);
  621. });
  622. };
  623. // add methods
  624. $.extend($.fn[pluginName].prototype, methods);
  625. // TODO OOP this and add to Tablesaw object
  626. })();
  627. (function() {
  628. var data = {
  629. key: "tablesaw-coltoggle"
  630. };
  631. var ColumnToggle = function(element) {
  632. this.$table = $(element);
  633. if (!this.$table.length) {
  634. return;
  635. }
  636. this.tablesaw = this.$table.data("tablesaw");
  637. this.attributes = {
  638. btnTarget: "data-tablesaw-columntoggle-btn-target",
  639. set: "data-tablesaw-columntoggle-set"
  640. };
  641. this.classes = {
  642. columnToggleTable: "tablesaw-columntoggle",
  643. columnBtnContain: "tablesaw-columntoggle-btnwrap tablesaw-advance",
  644. columnBtn: "tablesaw-columntoggle-btn tablesaw-nav-btn down",
  645. popup: "tablesaw-columntoggle-popup",
  646. priorityPrefix: "tablesaw-priority-"
  647. };
  648. this.set = [];
  649. this.$headers = this.tablesaw._getPrimaryHeaderCells();
  650. this.$table.data(data.key, this);
  651. };
  652. // Column Toggle Sets (one column chooser can control multiple tables)
  653. ColumnToggle.prototype.initSet = function() {
  654. var set = this.$table.attr(this.attributes.set);
  655. if (set) {
  656. // Should not include the current table
  657. var table = this.$table[0];
  658. this.set = $("table[" + this.attributes.set + "='" + set + "']")
  659. .filter(function() {
  660. return this !== table;
  661. })
  662. .get();
  663. }
  664. };
  665. ColumnToggle.prototype.init = function() {
  666. if (!this.$table.length) {
  667. return;
  668. }
  669. var tableId,
  670. id,
  671. $menuButton,
  672. $popup,
  673. $menu,
  674. $btnContain,
  675. self = this;
  676. var cfg = this.tablesaw.getConfig({
  677. getColumnToggleLabelTemplate: function(text) {
  678. return "<label><input type='checkbox' checked>" + text + "</label>";
  679. }
  680. });
  681. this.$table.addClass(this.classes.columnToggleTable);
  682. tableId = this.$table.attr("id");
  683. id = tableId + "-popup";
  684. $btnContain = $("<div class='" + this.classes.columnBtnContain + "'></div>");
  685. // TODO next major version: remove .btn
  686. $menuButton = $(
  687. "<a href='#" +
  688. id +
  689. "' class='btn tablesaw-btn btn-micro " +
  690. this.classes.columnBtn +
  691. "' data-popup-link>" +
  692. "<span>" +
  693. Tablesaw.i18n.columnToggleButton +
  694. "</span></a>"
  695. );
  696. $popup = $("<div class='" + this.classes.popup + "' id='" + id + "'></div>");
  697. $menu = $("<div class='btn-group'></div>");
  698. this.$popup = $popup;
  699. var hasNonPersistentHeaders = false;
  700. this.$headers.each(function() {
  701. var $this = $(this),
  702. priority = $this.attr("data-tablesaw-priority"),
  703. $cells = self.tablesaw._$getCells(this);
  704. if (priority && priority !== "persist") {
  705. $cells.addClass(self.classes.priorityPrefix + priority);
  706. $(cfg.getColumnToggleLabelTemplate($this.text()))
  707. .appendTo($menu)
  708. .find('input[type="checkbox"]')
  709. .data("tablesaw-header", this);
  710. hasNonPersistentHeaders = true;
  711. }
  712. });
  713. if (!hasNonPersistentHeaders) {
  714. $menu.append("<label>" + Tablesaw.i18n.columnToggleError + "</label>");
  715. }
  716. $menu.appendTo($popup);
  717. function onToggleCheckboxChange(checkbox) {
  718. var checked = checkbox.checked;
  719. var header = self.getHeaderFromCheckbox(checkbox);
  720. var $cells = self.tablesaw._$getCells(header);
  721. $cells[!checked ? "addClass" : "removeClass"]("tablesaw-toggle-cellhidden");
  722. $cells[checked ? "addClass" : "removeClass"]("tablesaw-toggle-cellvisible");
  723. self.updateColspanCells(header, checked);
  724. self.$table.trigger("tablesawcolumns");
  725. }
  726. // bind change event listeners to inputs - TODO: move to a private method?
  727. $menu.find('input[type="checkbox"]').on("change", function(e) {
  728. onToggleCheckboxChange(e.target);
  729. if (self.set.length) {
  730. var index;
  731. $(self.$popup)
  732. .find("input[type='checkbox']")
  733. .each(function(j) {
  734. if (this === e.target) {
  735. index = j;
  736. return false;
  737. }
  738. });
  739. $(self.set).each(function() {
  740. var checkbox = $(this)
  741. .data(data.key)
  742. .$popup.find("input[type='checkbox']")
  743. .get(index);
  744. if (checkbox) {
  745. checkbox.checked = e.target.checked;
  746. onToggleCheckboxChange(checkbox);
  747. }
  748. });
  749. }
  750. });
  751. $menuButton.appendTo($btnContain);
  752. // Use a different target than the toolbar
  753. var $btnTarget = $(this.$table.attr(this.attributes.btnTarget));
  754. $btnContain.appendTo($btnTarget.length ? $btnTarget : this.tablesaw.$toolbar);
  755. function closePopup(event) {
  756. // Click came from inside the popup, ignore.
  757. if (event && $(event.target).closest("." + self.classes.popup).length) {
  758. return;
  759. }
  760. $(document).off("click." + tableId);
  761. $menuButton.removeClass("up").addClass("down");
  762. $btnContain.removeClass("visible");
  763. }
  764. var closeTimeout;
  765. function openPopup() {
  766. $btnContain.addClass("visible");
  767. $menuButton.removeClass("down").addClass("up");
  768. $(document).off("click." + tableId, closePopup);
  769. window.clearTimeout(closeTimeout);
  770. closeTimeout = window.setTimeout(function() {
  771. $(document).on("click." + tableId, closePopup);
  772. }, 15);
  773. }
  774. $menuButton.on("click.tablesaw", function(event) {
  775. event.preventDefault();
  776. if (!$btnContain.is(".visible")) {
  777. openPopup();
  778. } else {
  779. closePopup();
  780. }
  781. });
  782. $popup.appendTo($btnContain);
  783. this.$menu = $menu;
  784. // Fix for iOS not rendering shadows correctly when using `-webkit-overflow-scrolling`
  785. var $overflow = this.$table.closest(".tablesaw-overflow");
  786. if ($overflow.css("-webkit-overflow-scrolling")) {
  787. var timeout;
  788. $overflow.on("scroll", function() {
  789. var $div = $(this);
  790. window.clearTimeout(timeout);
  791. timeout = window.setTimeout(function() {
  792. $div.css("-webkit-overflow-scrolling", "auto");
  793. window.setTimeout(function() {
  794. $div.css("-webkit-overflow-scrolling", "touch");
  795. }, 0);
  796. }, 100);
  797. });
  798. }
  799. $(window).on(Tablesaw.events.resize + "." + tableId, function() {
  800. self.refreshToggle();
  801. });
  802. this.initSet();
  803. this.refreshToggle();
  804. };
  805. ColumnToggle.prototype.getHeaderFromCheckbox = function(checkbox) {
  806. return $(checkbox).data("tablesaw-header");
  807. };
  808. ColumnToggle.prototype.refreshToggle = function() {
  809. var self = this;
  810. var invisibleColumns = 0;
  811. this.$menu.find("input").each(function() {
  812. var header = self.getHeaderFromCheckbox(this);
  813. this.checked =
  814. self.tablesaw
  815. ._$getCells(header)
  816. .eq(0)
  817. .css("display") === "table-cell";
  818. });
  819. this.updateColspanCells();
  820. };
  821. ColumnToggle.prototype.updateColspanCells = function(header, userAction) {
  822. this.tablesaw.updateColspanCells("tablesaw-toggle-cellhidden", header, userAction);
  823. };
  824. ColumnToggle.prototype.destroy = function() {
  825. this.$table.removeClass(this.classes.columnToggleTable);
  826. this.$table.find("th, td").each(function() {
  827. var $cell = $(this);
  828. $cell.removeClass("tablesaw-toggle-cellhidden").removeClass("tablesaw-toggle-cellvisible");
  829. this.className = this.className.replace(/\bui\-table\-priority\-\d\b/g, "");
  830. });
  831. };
  832. // on tablecreate, init
  833. $(document).on(Tablesaw.events.create, function(e, tablesaw) {
  834. if (tablesaw.mode === "columntoggle") {
  835. var table = new ColumnToggle(tablesaw.table);
  836. table.init();
  837. }
  838. });
  839. $(document).on(Tablesaw.events.destroy, function(e, tablesaw) {
  840. if (tablesaw.mode === "columntoggle") {
  841. $(tablesaw.table)
  842. .data(data.key)
  843. .destroy();
  844. }
  845. });
  846. $(document).on(Tablesaw.events.refresh, function(e, tablesaw) {
  847. if (tablesaw.mode === "columntoggle") {
  848. $(tablesaw.table)
  849. .data(data.key)
  850. .refreshPriority();
  851. }
  852. });
  853. Tablesaw.ColumnToggle = ColumnToggle;
  854. })();
  855. (function() {
  856. function getSortValue(cell) {
  857. var text = [];
  858. $(cell.childNodes).each(function() {
  859. var $el = $(this);
  860. if ($el.is("input, select")) {
  861. text.push($el.val());
  862. } else if ($el.is(".tablesaw-cell-label")) {
  863. } else {
  864. text.push(($el.text() || "").replace(/^\s+|\s+$/g, ""));
  865. }
  866. });
  867. return text.join("");
  868. }
  869. var pluginName = "tablesaw-sortable",
  870. initSelector = "table[data-" + pluginName + "]",
  871. sortableSwitchSelector = "[data-" + pluginName + "-switch]",
  872. attrs = {
  873. sortCol: "data-tablesaw-sortable-col",
  874. defaultCol: "data-tablesaw-sortable-default-col",
  875. numericCol: "data-tablesaw-sortable-numeric",
  876. subRow: "data-tablesaw-subrow",
  877. ignoreRow: "data-tablesaw-ignorerow"
  878. },
  879. classes = {
  880. head: pluginName + "-head",
  881. ascend: pluginName + "-ascending",
  882. descend: pluginName + "-descending",
  883. switcher: pluginName + "-switch",
  884. tableToolbar: "tablesaw-bar-section",
  885. sortButton: pluginName + "-btn"
  886. },
  887. methods = {
  888. _create: function(o) {
  889. return $(this).each(function() {
  890. var init = $(this).data(pluginName + "-init");
  891. if (init) {
  892. return false;
  893. }
  894. $(this)
  895. .data(pluginName + "-init", true)
  896. .trigger("beforecreate." + pluginName)
  897. [pluginName]("_init", o)
  898. .trigger("create." + pluginName);
  899. });
  900. },
  901. _init: function() {
  902. var el = $(this);
  903. var tblsaw = el.data("tablesaw");
  904. var heads;
  905. var $switcher;
  906. function addClassToHeads(h) {
  907. $.each(h, function(i, v) {
  908. $(v).addClass(classes.head);
  909. });
  910. }
  911. function makeHeadsActionable(h, fn) {
  912. $.each(h, function(i, col) {
  913. var b = $("<button class='" + classes.sortButton + "'/>");
  914. b.on("click", { col: col }, fn);
  915. $(col)
  916. .wrapInner(b)
  917. .find("button")
  918. .append("<span class='tablesaw-sortable-arrow'>");
  919. });
  920. }
  921. function clearOthers(headcells) {
  922. $.each(headcells, function(i, v) {
  923. var col = $(v);
  924. col.removeAttr(attrs.defaultCol);
  925. col.removeClass(classes.ascend);
  926. col.removeClass(classes.descend);
  927. });
  928. }
  929. function headsOnAction(e) {
  930. if ($(e.target).is("a[href]")) {
  931. return;
  932. }
  933. e.stopPropagation();
  934. var headCell = $(e.target).closest("[" + attrs.sortCol + "]"),
  935. v = e.data.col,
  936. newSortValue = heads.index(headCell[0]);
  937. clearOthers(
  938. headCell
  939. .closest("thead")
  940. .find("th")
  941. .filter(function() {
  942. return this !== headCell[0];
  943. })
  944. );
  945. if (headCell.is("." + classes.descend) || !headCell.is("." + classes.ascend)) {
  946. el[pluginName]("sortBy", v, true);
  947. newSortValue += "_asc";
  948. } else {
  949. el[pluginName]("sortBy", v);
  950. newSortValue += "_desc";
  951. }
  952. if ($switcher) {
  953. $switcher
  954. .find("select")
  955. .val(newSortValue)
  956. .trigger("refresh");
  957. }
  958. e.preventDefault();
  959. }
  960. function handleDefault(heads) {
  961. $.each(heads, function(idx, el) {
  962. var $el = $(el);
  963. if ($el.is("[" + attrs.defaultCol + "]")) {
  964. if (!$el.is("." + classes.descend)) {
  965. $el.addClass(classes.ascend);
  966. }
  967. }
  968. });
  969. }
  970. function addSwitcher(heads) {
  971. $switcher = $("<div>")
  972. .addClass(classes.switcher)
  973. .addClass(classes.tableToolbar);
  974. var html = ["<label>" + Tablesaw.i18n.sort + ":"];
  975. // TODO next major version: remove .btn
  976. html.push('<span class="btn tablesaw-btn"><select>');
  977. heads.each(function(j) {
  978. var $t = $(this);
  979. var isDefaultCol = $t.is("[" + attrs.defaultCol + "]");
  980. var isDescending = $t.is("." + classes.descend);
  981. var hasNumericAttribute = $t.is("[" + attrs.numericCol + "]");
  982. var numericCount = 0;
  983. // Check only the first four rows to see if the column is numbers.
  984. var numericCountMax = 5;
  985. $(this.cells.slice(0, numericCountMax)).each(function() {
  986. if (!isNaN(parseInt(getSortValue(this), 10))) {
  987. numericCount++;
  988. }
  989. });
  990. var isNumeric = numericCount === numericCountMax;
  991. if (!hasNumericAttribute) {
  992. $t.attr(attrs.numericCol, isNumeric ? "" : "false");
  993. }
  994. html.push(
  995. "<option" +
  996. (isDefaultCol && !isDescending ? " selected" : "") +
  997. ' value="' +
  998. j +
  999. '_asc">' +
  1000. $t.text() +
  1001. " " +
  1002. (isNumeric ? "&#x2191;" : "(A-Z)") +
  1003. "</option>"
  1004. );
  1005. html.push(
  1006. "<option" +
  1007. (isDefaultCol && isDescending ? " selected" : "") +
  1008. ' value="' +
  1009. j +
  1010. '_desc">' +
  1011. $t.text() +
  1012. " " +
  1013. (isNumeric ? "&#x2193;" : "(Z-A)") +
  1014. "</option>"
  1015. );
  1016. });
  1017. html.push("</select></span></label>");
  1018. $switcher.html(html.join(""));
  1019. var $firstChild = tblsaw.$toolbar.children().eq(0);
  1020. if ($firstChild.length) {
  1021. $switcher.insertBefore($firstChild);
  1022. } else {
  1023. $switcher.appendTo(tblsaw.$toolbar);
  1024. }
  1025. $switcher.find(".tablesaw-btn").tablesawbtn();
  1026. $switcher.find("select").on("change", function() {
  1027. var val = $(this)
  1028. .val()
  1029. .split("_"),
  1030. head = heads.eq(val[0]);
  1031. clearOthers(head.siblings());
  1032. el[pluginName]("sortBy", head.get(0), val[1] === "asc");
  1033. });
  1034. }
  1035. el.addClass(pluginName);
  1036. heads = el
  1037. .children()
  1038. .filter("thead")
  1039. .find("th[" + attrs.sortCol + "]");
  1040. addClassToHeads(heads);
  1041. makeHeadsActionable(heads, headsOnAction);
  1042. handleDefault(heads);
  1043. if (el.is(sortableSwitchSelector)) {
  1044. addSwitcher(heads);
  1045. }
  1046. },
  1047. sortRows: function(rows, colNum, ascending, col, tbody) {
  1048. function convertCells(cellArr, belongingToTbody) {
  1049. var cells = [];
  1050. $.each(cellArr, function(i, cell) {
  1051. var row = cell.parentNode;
  1052. var $row = $(row);
  1053. // next row is a subrow
  1054. var subrows = [];
  1055. var $next = $row.next();
  1056. while ($next.is("[" + attrs.subRow + "]")) {
  1057. subrows.push($next[0]);
  1058. $next = $next.next();
  1059. }
  1060. var tbody = row.parentNode;
  1061. // current row is a subrow
  1062. if ($row.is("[" + attrs.subRow + "]")) {
  1063. } else if (tbody === belongingToTbody) {
  1064. cells.push({
  1065. element: cell,
  1066. cell: getSortValue(cell),
  1067. row: row,
  1068. subrows: subrows.length ? subrows : null,
  1069. ignored: $row.is("[" + attrs.ignoreRow + "]")
  1070. });
  1071. }
  1072. });
  1073. return cells;
  1074. }
  1075. function getSortFxn(ascending, forceNumeric) {
  1076. var fn,
  1077. regex = /[^\-\+\d\.]/g;
  1078. if (ascending) {
  1079. fn = function(a, b) {
  1080. if (a.ignored || b.ignored) {
  1081. return 0;
  1082. }
  1083. if (forceNumeric) {
  1084. return (
  1085. parseFloat(a.cell.replace(regex, "")) - parseFloat(b.cell.replace(regex, ""))
  1086. );
  1087. } else {
  1088. return a.cell.toLowerCase() > b.cell.toLowerCase() ? 1 : -1;
  1089. }
  1090. };
  1091. } else {
  1092. fn = function(a, b) {
  1093. if (a.ignored || b.ignored) {
  1094. return 0;
  1095. }
  1096. if (forceNumeric) {
  1097. return (
  1098. parseFloat(b.cell.replace(regex, "")) - parseFloat(a.cell.replace(regex, ""))
  1099. );
  1100. } else {
  1101. return a.cell.toLowerCase() < b.cell.toLowerCase() ? 1 : -1;
  1102. }
  1103. };
  1104. }
  1105. return fn;
  1106. }
  1107. function convertToRows(sorted) {
  1108. var newRows = [],
  1109. i,
  1110. l;
  1111. for (i = 0, l = sorted.length; i < l; i++) {
  1112. newRows.push(sorted[i].row);
  1113. if (sorted[i].subrows) {
  1114. newRows.push(sorted[i].subrows);
  1115. }
  1116. }
  1117. return newRows;
  1118. }
  1119. var fn;
  1120. var sorted;
  1121. var cells = convertCells(col.cells, tbody);
  1122. var customFn = $(col).data("tablesaw-sort");
  1123. fn =
  1124. (customFn && typeof customFn === "function" ? customFn(ascending) : false) ||
  1125. getSortFxn(
  1126. ascending,
  1127. $(col).is("[" + attrs.numericCol + "]") &&
  1128. !$(col).is("[" + attrs.numericCol + '="false"]')
  1129. );
  1130. sorted = cells.sort(fn);
  1131. rows = convertToRows(sorted);
  1132. return rows;
  1133. },
  1134. makeColDefault: function(col, a) {
  1135. var c = $(col);
  1136. c.attr(attrs.defaultCol, "true");
  1137. if (a) {
  1138. c.removeClass(classes.descend);
  1139. c.addClass(classes.ascend);
  1140. } else {
  1141. c.removeClass(classes.ascend);
  1142. c.addClass(classes.descend);
  1143. }
  1144. },
  1145. sortBy: function(col, ascending) {
  1146. var el = $(this);
  1147. var colNum;
  1148. var tbl = el.data("tablesaw");
  1149. tbl.$tbody.each(function() {
  1150. var tbody = this;
  1151. var $tbody = $(this);
  1152. var rows = tbl.getBodyRows(tbody);
  1153. var sortedRows;
  1154. var map = tbl.headerMapping[0];
  1155. var j, k;
  1156. // find the column number that we’re sorting
  1157. for (j = 0, k = map.length; j < k; j++) {
  1158. if (map[j] === col) {
  1159. colNum = j;
  1160. break;
  1161. }
  1162. }
  1163. sortedRows = el[pluginName]("sortRows", rows, colNum, ascending, col, tbody);
  1164. // replace Table rows
  1165. for (j = 0, k = sortedRows.length; j < k; j++) {
  1166. $tbody.append(sortedRows[j]);
  1167. }
  1168. });
  1169. el[pluginName]("makeColDefault", col, ascending);
  1170. el.trigger("tablesaw-sorted");
  1171. }
  1172. };
  1173. // Collection method.
  1174. $.fn[pluginName] = function(arrg) {
  1175. var args = Array.prototype.slice.call(arguments, 1),
  1176. returnVal;
  1177. // if it's a method
  1178. if (arrg && typeof arrg === "string") {
  1179. returnVal = $.fn[pluginName].prototype[arrg].apply(this[0], args);
  1180. return typeof returnVal !== "undefined" ? returnVal : $(this);
  1181. }
  1182. // check init
  1183. if (!$(this).data(pluginName + "-active")) {
  1184. $(this).data(pluginName + "-active", true);
  1185. $.fn[pluginName].prototype._create.call(this, arrg);
  1186. }
  1187. return $(this);
  1188. };
  1189. // add methods
  1190. $.extend($.fn[pluginName].prototype, methods);
  1191. $(document).on(Tablesaw.events.create, function(e, Tablesaw) {
  1192. if (Tablesaw.$table.is(initSelector)) {
  1193. Tablesaw.$table[pluginName]();
  1194. }
  1195. });
  1196. // TODO OOP this and add to Tablesaw object
  1197. })();
  1198. (function() {
  1199. var classes = {
  1200. hideBtn: "disabled",
  1201. persistWidths: "tablesaw-fix-persist",
  1202. hiddenCol: "tablesaw-swipe-cellhidden",
  1203. persistCol: "tablesaw-swipe-cellpersist",
  1204. allColumnsVisible: "tablesaw-all-cols-visible"
  1205. };
  1206. var attrs = {
  1207. disableTouchEvents: "data-tablesaw-no-touch",
  1208. ignorerow: "data-tablesaw-ignorerow",
  1209. subrow: "data-tablesaw-subrow"
  1210. };
  1211. function createSwipeTable(tbl, $table) {
  1212. var tblsaw = $table.data("tablesaw");
  1213. var $btns = $("<div class='tablesaw-advance'></div>");
  1214. // TODO next major version: remove .btn
  1215. var $prevBtn = $(
  1216. "<a href='#' class='btn tablesaw-nav-btn tablesaw-btn btn-micro left'>" +
  1217. Tablesaw.i18n.swipePreviousColumn +
  1218. "</a>"
  1219. ).appendTo($btns);
  1220. // TODO next major version: remove .btn
  1221. var $nextBtn = $(
  1222. "<a href='#' class='btn tablesaw-nav-btn tablesaw-btn btn-micro right'>" +
  1223. Tablesaw.i18n.swipeNextColumn +
  1224. "</a>"
  1225. ).appendTo($btns);
  1226. var $headerCells = tbl._getPrimaryHeaderCells();
  1227. var $headerCellsNoPersist = $headerCells.not('[data-tablesaw-priority="persist"]');
  1228. var headerWidths = [];
  1229. var $head = $(document.head || "head");
  1230. var tableId = $table.attr("id");
  1231. if (!$headerCells.length) {
  1232. throw new Error("tablesaw swipe: no header cells found.");
  1233. }
  1234. $table.addClass("tablesaw-swipe");
  1235. function initMinHeaderWidths() {
  1236. $table.css({
  1237. width: "1px"
  1238. });
  1239. // remove any hidden columns
  1240. $table.find("." + classes.hiddenCol).removeClass(classes.hiddenCol);
  1241. headerWidths = [];
  1242. // Calculate initial widths
  1243. $headerCells.each(function() {
  1244. headerWidths.push(this.offsetWidth);
  1245. });
  1246. // reset props
  1247. $table.css({
  1248. width: ""
  1249. });
  1250. }
  1251. initMinHeaderWidths();
  1252. $btns.appendTo(tblsaw.$toolbar);
  1253. if (!tableId) {
  1254. tableId = "tableswipe-" + Math.round(Math.random() * 10000);
  1255. $table.attr("id", tableId);
  1256. }
  1257. function showColumn(headerCell) {
  1258. tblsaw._$getCells(headerCell).removeClass(classes.hiddenCol);
  1259. }
  1260. function hideColumn(headerCell) {
  1261. tblsaw._$getCells(headerCell).addClass(classes.hiddenCol);
  1262. }
  1263. function persistColumn(headerCell) {
  1264. tblsaw._$getCells(headerCell).addClass(classes.persistCol);
  1265. }
  1266. function isPersistent(headerCell) {
  1267. return $(headerCell).is('[data-tablesaw-priority="persist"]');
  1268. }
  1269. function unmaintainWidths() {
  1270. $table.removeClass(classes.persistWidths);
  1271. $("#" + tableId + "-persist").remove();
  1272. }
  1273. function maintainWidths() {
  1274. var prefix = "#" + tableId + ".tablesaw-swipe ",
  1275. styles = [],
  1276. tableWidth = $table.width(),
  1277. hash = [],
  1278. newHash;
  1279. // save persistent column widths (as long as they take up less than 75% of table width)
  1280. $headerCells.each(function(index) {
  1281. var width;
  1282. if (isPersistent(this)) {
  1283. width = this.offsetWidth;
  1284. if (width < tableWidth * 0.75) {
  1285. hash.push(index + "-" + width);
  1286. styles.push(
  1287. prefix +
  1288. " ." +
  1289. classes.persistCol +
  1290. ":nth-child(" +
  1291. (index + 1) +
  1292. ") { width: " +
  1293. width +
  1294. "px; }"
  1295. );
  1296. }
  1297. }
  1298. });
  1299. newHash = hash.join("_");
  1300. if (styles.length) {
  1301. $table.addClass(classes.persistWidths);
  1302. var $style = $("#" + tableId + "-persist");
  1303. // If style element not yet added OR if the widths have changed
  1304. if (!$style.length || $style.data("tablesaw-hash") !== newHash) {
  1305. // Remove existing
  1306. $style.remove();
  1307. $("<style>" + styles.join("\n") + "</style>")
  1308. .attr("id", tableId + "-persist")
  1309. .data("tablesaw-hash", newHash)
  1310. .appendTo($head);
  1311. }
  1312. }
  1313. }
  1314. function getNext() {
  1315. var next = [],
  1316. checkFound;
  1317. $headerCellsNoPersist.each(function(i) {
  1318. var $t = $(this),
  1319. isHidden = $t.css("display") === "none" || $t.is("." + classes.hiddenCol);
  1320. if (!isHidden && !checkFound) {
  1321. checkFound = true;
  1322. next[0] = i;
  1323. } else if (isHidden && checkFound) {
  1324. next[1] = i;
  1325. return false;
  1326. }
  1327. });
  1328. return next;
  1329. }
  1330. function getPrev() {
  1331. var next = getNext();
  1332. return [next[1] - 1, next[0] - 1];
  1333. }
  1334. function nextpair(fwd) {
  1335. return fwd ? getNext() : getPrev();
  1336. }
  1337. function canAdvance(pair) {
  1338. return pair[1] > -1 && pair[1] < $headerCellsNoPersist.length;
  1339. }
  1340. function matchesMedia() {
  1341. var matchMedia = $table.attr("data-tablesaw-swipe-media");
  1342. return !matchMedia || ("matchMedia" in window && window.matchMedia(matchMedia).matches);
  1343. }
  1344. function fakeBreakpoints() {
  1345. if (!matchesMedia()) {
  1346. return;
  1347. }
  1348. var containerWidth = $table.parent().width(),
  1349. persist = [],
  1350. sum = 0,
  1351. sums = [],
  1352. visibleNonPersistantCount = $headerCells.length;
  1353. $headerCells.each(function(index) {
  1354. var $t = $(this),
  1355. isPersist = $t.is('[data-tablesaw-priority="persist"]');
  1356. persist.push(isPersist);
  1357. sum += headerWidths[index];
  1358. sums.push(sum);
  1359. // is persistent or is hidden
  1360. if (isPersist || sum > containerWidth) {
  1361. visibleNonPersistantCount--;
  1362. }
  1363. });
  1364. // We need at least one column to swipe.
  1365. var needsNonPersistentColumn = visibleNonPersistantCount === 0;
  1366. $headerCells.each(function(index) {
  1367. if (sums[index] > containerWidth) {
  1368. hideColumn(this);
  1369. }
  1370. });
  1371. $headerCells.each(function(index) {
  1372. if (persist[index]) {
  1373. // for visual box-shadow
  1374. persistColumn(this);
  1375. return;
  1376. }
  1377. if (sums[index] <= containerWidth || needsNonPersistentColumn) {
  1378. needsNonPersistentColumn = false;
  1379. showColumn(this);
  1380. tblsaw.updateColspanCells(classes.hiddenCol, this, true);
  1381. }
  1382. });
  1383. unmaintainWidths();
  1384. $table.trigger("tablesawcolumns");
  1385. }
  1386. function advance(fwd) {
  1387. var pair = nextpair(fwd);
  1388. if (canAdvance(pair)) {
  1389. if (isNaN(pair[0])) {
  1390. if (fwd) {
  1391. pair[0] = 0;
  1392. } else {
  1393. pair[0] = $headerCellsNoPersist.length - 1;
  1394. }
  1395. }
  1396. // TODO just blindly hiding the previous column and showing the next column can result in
  1397. // column content overflow
  1398. maintainWidths();
  1399. hideColumn($headerCellsNoPersist.get(pair[0]));
  1400. tblsaw.updateColspanCells(classes.hiddenCol, $headerCellsNoPersist.get(pair[0]), false);
  1401. showColumn($headerCellsNoPersist.get(pair[1]));
  1402. tblsaw.updateColspanCells(classes.hiddenCol, $headerCellsNoPersist.get(pair[1]), true);
  1403. $table.trigger("tablesawcolumns");
  1404. }
  1405. }
  1406. $prevBtn.add($nextBtn).on("click", function(e) {
  1407. advance(!!$(e.target).closest($nextBtn).length);
  1408. e.preventDefault();
  1409. });
  1410. function getCoord(event, key) {
  1411. return (event.touches || event.originalEvent.touches)[0][key];
  1412. }
  1413. if (!$table.is("[" + attrs.disableTouchEvents + "]")) {
  1414. $table.on("touchstart.swipetoggle", function(e) {
  1415. var originX = getCoord(e, "pageX");
  1416. var originY = getCoord(e, "pageY");
  1417. var x;
  1418. var y;
  1419. var scrollTop = window.pageYOffset;
  1420. $(window).off(Tablesaw.events.resize, fakeBreakpoints);
  1421. $(this)
  1422. .on("touchmove.swipetoggle", function(e) {
  1423. x = getCoord(e, "pageX");
  1424. y = getCoord(e, "pageY");
  1425. })
  1426. .on("touchend.swipetoggle", function() {
  1427. var cfg = tbl.getConfig({
  1428. swipeHorizontalThreshold: 30,
  1429. swipeVerticalThreshold: 30
  1430. });
  1431. // This config code is a little awkward because shoestring doesn’t support deep $.extend
  1432. // Trying to work around when devs only override one of (not both) horizontalThreshold or
  1433. // verticalThreshold in their TablesawConfig.
  1434. // @TODO major version bump: remove cfg.swipe, move to just use the swipePrefix keys
  1435. var verticalThreshold = cfg.swipe
  1436. ? cfg.swipe.verticalThreshold
  1437. : cfg.swipeVerticalThreshold;
  1438. var horizontalThreshold = cfg.swipe
  1439. ? cfg.swipe.horizontalThreshold
  1440. : cfg.swipeHorizontalThreshold;
  1441. var isPageScrolled = Math.abs(window.pageYOffset - scrollTop) >= verticalThreshold;
  1442. var isVerticalSwipe = Math.abs(y - originY) >= verticalThreshold;
  1443. if (!isVerticalSwipe && !isPageScrolled) {
  1444. if (x - originX < -1 * horizontalThreshold) {
  1445. advance(true);
  1446. }
  1447. if (x - originX > horizontalThreshold) {
  1448. advance(false);
  1449. }
  1450. }
  1451. window.setTimeout(function() {
  1452. $(window).on(Tablesaw.events.resize, fakeBreakpoints);
  1453. }, 300);
  1454. $(this).off("touchmove.swipetoggle touchend.swipetoggle");
  1455. });
  1456. });
  1457. }
  1458. $table
  1459. .on("tablesawcolumns.swipetoggle", function() {
  1460. var canGoPrev = canAdvance(getPrev());
  1461. var canGoNext = canAdvance(getNext());
  1462. $prevBtn[canGoPrev ? "removeClass" : "addClass"](classes.hideBtn);
  1463. $nextBtn[canGoNext ? "removeClass" : "addClass"](classes.hideBtn);
  1464. tblsaw.$toolbar[!canGoPrev && !canGoNext ? "addClass" : "removeClass"](
  1465. classes.allColumnsVisible
  1466. );
  1467. })
  1468. .on("tablesawnext.swipetoggle", function() {
  1469. advance(true);
  1470. })
  1471. .on("tablesawprev.swipetoggle", function() {
  1472. advance(false);
  1473. })
  1474. .on(Tablesaw.events.destroy + ".swipetoggle", function() {
  1475. var $t = $(this);
  1476. $t.removeClass("tablesaw-swipe");
  1477. tblsaw.$toolbar.find(".tablesaw-advance").remove();
  1478. $(window).off(Tablesaw.events.resize, fakeBreakpoints);
  1479. $t.off(".swipetoggle");
  1480. })
  1481. .on(Tablesaw.events.refresh, function() {
  1482. unmaintainWidths();
  1483. initMinHeaderWidths();
  1484. fakeBreakpoints();
  1485. });
  1486. fakeBreakpoints();
  1487. $(window).on(Tablesaw.events.resize, fakeBreakpoints);
  1488. }
  1489. // on tablecreate, init
  1490. $(document).on(Tablesaw.events.create, function(e, tablesaw) {
  1491. if (tablesaw.mode === "swipe") {
  1492. createSwipeTable(tablesaw, tablesaw.$table);
  1493. }
  1494. });
  1495. // TODO OOP this and add to Tablesaw object
  1496. })();
  1497. (function() {
  1498. var MiniMap = {
  1499. attr: {
  1500. init: "data-tablesaw-minimap"
  1501. },
  1502. show: function(table) {
  1503. var mq = table.getAttribute(MiniMap.attr.init);
  1504. if (mq === "") {
  1505. // value-less but exists
  1506. return true;
  1507. } else if (mq && "matchMedia" in window) {
  1508. // has a mq value
  1509. return window.matchMedia(mq).matches;
  1510. }
  1511. return false;
  1512. }
  1513. };
  1514. function createMiniMap($table) {
  1515. var tblsaw = $table.data("tablesaw");
  1516. var $btns = $('<div class="tablesaw-advance minimap">');
  1517. var $dotNav = $('<ul class="tablesaw-advance-dots">').appendTo($btns);
  1518. var hideDot = "tablesaw-advance-dots-hide";
  1519. var $headerCells = $table.data("tablesaw")._getPrimaryHeaderCells();
  1520. // populate dots
  1521. $headerCells.each(function() {
  1522. $dotNav.append("<li><i></i></li>");
  1523. });
  1524. $btns.appendTo(tblsaw.$toolbar);
  1525. function showHideNav() {
  1526. if (!MiniMap.show($table[0])) {
  1527. $btns.css("display", "none");
  1528. return;
  1529. }
  1530. $btns.css("display", "block");
  1531. // show/hide dots
  1532. var dots = $dotNav.find("li").removeClass(hideDot);
  1533. $table.find("thead th").each(function(i) {
  1534. if ($(this).css("display") === "none") {
  1535. dots.eq(i).addClass(hideDot);
  1536. }
  1537. });
  1538. }
  1539. // run on init and resize
  1540. showHideNav();
  1541. $(window).on(Tablesaw.events.resize, showHideNav);
  1542. $table
  1543. .on("tablesawcolumns.minimap", function() {
  1544. showHideNav();
  1545. })
  1546. .on(Tablesaw.events.destroy + ".minimap", function() {
  1547. var $t = $(this);
  1548. tblsaw.$toolbar.find(".tablesaw-advance").remove();
  1549. $(window).off(Tablesaw.events.resize, showHideNav);
  1550. $t.off(".minimap");
  1551. });
  1552. }
  1553. // on tablecreate, init
  1554. $(document).on(Tablesaw.events.create, function(e, tablesaw) {
  1555. if (
  1556. (tablesaw.mode === "swipe" || tablesaw.mode === "columntoggle") &&
  1557. tablesaw.$table.is("[ " + MiniMap.attr.init + "]")
  1558. ) {
  1559. createMiniMap(tablesaw.$table);
  1560. }
  1561. });
  1562. // TODO OOP this better
  1563. Tablesaw.MiniMap = MiniMap;
  1564. })();
  1565. (function() {
  1566. var S = {
  1567. selectors: {
  1568. init: "table[data-tablesaw-mode-switch]"
  1569. },
  1570. attributes: {
  1571. excludeMode: "data-tablesaw-mode-exclude"
  1572. },
  1573. classes: {
  1574. main: "tablesaw-modeswitch",
  1575. toolbar: "tablesaw-bar-section"
  1576. },
  1577. modes: ["stack", "swipe", "columntoggle"],
  1578. init: function(table) {
  1579. var $table = $(table);
  1580. var tblsaw = $table.data("tablesaw");
  1581. var ignoreMode = $table.attr(S.attributes.excludeMode);
  1582. var $toolbar = tblsaw.$toolbar;
  1583. var $switcher = $("<div>").addClass(S.classes.main + " " + S.classes.toolbar);
  1584. var html = [
  1585. '<label><span class="abbreviated">' +
  1586. Tablesaw.i18n.modeSwitchColumnsAbbreviated +
  1587. '</span><span class="longform">' +
  1588. Tablesaw.i18n.modeSwitchColumns +
  1589. "</span>:"
  1590. ],
  1591. dataMode = $table.attr("data-tablesaw-mode"),
  1592. isSelected;
  1593. // TODO next major version: remove .btn
  1594. html.push('<span class="btn tablesaw-btn"><select>');
  1595. for (var j = 0, k = S.modes.length; j < k; j++) {
  1596. if (ignoreMode && ignoreMode.toLowerCase() === S.modes[j]) {
  1597. continue;
  1598. }
  1599. isSelected = dataMode === S.modes[j];
  1600. html.push(
  1601. "<option" +
  1602. (isSelected ? " selected" : "") +
  1603. ' value="' +
  1604. S.modes[j] +
  1605. '">' +
  1606. Tablesaw.i18n.modes[j] +
  1607. "</option>"
  1608. );
  1609. }
  1610. html.push("</select></span></label>");
  1611. $switcher.html(html.join(""));
  1612. var $otherToolbarItems = $toolbar.find(".tablesaw-advance").eq(0);
  1613. if ($otherToolbarItems.length) {
  1614. $switcher.insertBefore($otherToolbarItems);
  1615. } else {
  1616. $switcher.appendTo($toolbar);
  1617. }
  1618. $switcher.find(".tablesaw-btn").tablesawbtn();
  1619. $switcher.find("select").on("change", function(event) {
  1620. return S.onModeChange.call(table, event, $(this).val());
  1621. });
  1622. },
  1623. onModeChange: function(event, val) {
  1624. var $table = $(this);
  1625. var tblsaw = $table.data("tablesaw");
  1626. var $switcher = tblsaw.$toolbar.find("." + S.classes.main);
  1627. $switcher.remove();
  1628. tblsaw.destroy();
  1629. $table.attr("data-tablesaw-mode", val);
  1630. $table.tablesaw();
  1631. }
  1632. };
  1633. $(document).on(Tablesaw.events.create, function(e, Tablesaw) {
  1634. if (Tablesaw.$table.is(S.selectors.init)) {
  1635. S.init(Tablesaw.table);
  1636. }
  1637. });
  1638. // TODO OOP this and add to Tablesaw object
  1639. })();
  1640. (function() {
  1641. var pluginName = "tablesawCheckAll";
  1642. function CheckAll(tablesaw) {
  1643. this.tablesaw = tablesaw;
  1644. this.$table = tablesaw.$table;
  1645. this.attr = "data-tablesaw-checkall";
  1646. this.checkAllSelector = "[" + this.attr + "]";
  1647. this.forceCheckedSelector = "[" + this.attr + "-checked]";
  1648. this.forceUncheckedSelector = "[" + this.attr + "-unchecked]";
  1649. this.checkboxSelector = 'input[type="checkbox"]';
  1650. this.$triggers = null;
  1651. this.$checkboxes = null;
  1652. if (this.$table.data(pluginName)) {
  1653. return;
  1654. }
  1655. this.$table.data(pluginName, this);
  1656. this.init();
  1657. }
  1658. CheckAll.prototype._filterCells = function($checkboxes) {
  1659. return $checkboxes
  1660. .filter(function() {
  1661. return !$(this)
  1662. .closest("tr")
  1663. .is("[data-tablesaw-subrow],[data-tablesaw-ignorerow]");
  1664. })
  1665. .find(this.checkboxSelector)
  1666. .not(this.checkAllSelector);
  1667. };
  1668. // With buttons you can use a scoping selector like: data-tablesaw-checkall="#my-scoped-id input[type='checkbox']"
  1669. CheckAll.prototype.getCheckboxesForButton = function(button) {
  1670. return this._filterCells($($(button).attr(this.attr)));
  1671. };
  1672. CheckAll.prototype.getCheckboxesForCheckbox = function(checkbox) {
  1673. return this._filterCells($($(checkbox).closest("th")[0].cells));
  1674. };
  1675. CheckAll.prototype.init = function() {
  1676. var self = this;
  1677. this.$table.find(this.checkAllSelector).each(function() {
  1678. var $trigger = $(this);
  1679. if ($trigger.is(self.checkboxSelector)) {
  1680. self.addCheckboxEvents(this);
  1681. } else {
  1682. self.addButtonEvents(this);
  1683. }
  1684. });
  1685. };
  1686. CheckAll.prototype.addButtonEvents = function(trigger) {
  1687. var self = this;
  1688. // Update body checkboxes when header checkbox is changed
  1689. $(trigger).on("click", function(event) {
  1690. event.preventDefault();
  1691. var $checkboxes = self.getCheckboxesForButton(this);
  1692. var allChecked = true;
  1693. $checkboxes.each(function() {
  1694. if (!this.checked) {
  1695. allChecked = false;
  1696. }
  1697. });
  1698. var setChecked;
  1699. if ($(this).is(self.forceCheckedSelector)) {
  1700. setChecked = true;
  1701. } else if ($(this).is(self.forceUncheckedSelector)) {
  1702. setChecked = false;
  1703. } else {
  1704. setChecked = allChecked ? false : true;
  1705. }
  1706. $checkboxes.each(function() {
  1707. this.checked = setChecked;
  1708. $(this).trigger("change." + pluginName);
  1709. });
  1710. });
  1711. };
  1712. CheckAll.prototype.addCheckboxEvents = function(trigger) {
  1713. var self = this;
  1714. // Update body checkboxes when header checkbox is changed
  1715. $(trigger).on("change", function() {
  1716. var setChecked = this.checked;
  1717. self.getCheckboxesForCheckbox(this).each(function() {
  1718. this.checked = setChecked;
  1719. });
  1720. });
  1721. var $checkboxes = self.getCheckboxesForCheckbox(trigger);
  1722. // Update header checkbox when body checkboxes are changed
  1723. $checkboxes.on("change." + pluginName, function() {
  1724. var checkedCount = 0;
  1725. $checkboxes.each(function() {
  1726. if (this.checked) {
  1727. checkedCount++;
  1728. }
  1729. });
  1730. var allSelected = checkedCount === $checkboxes.length;
  1731. trigger.checked = allSelected;
  1732. // only indeterminate if some are selected (not all and not none)
  1733. trigger.indeterminate = checkedCount !== 0 && !allSelected;
  1734. });
  1735. };
  1736. // on tablecreate, init
  1737. $(document).on(Tablesaw.events.create, function(e, tablesaw) {
  1738. new CheckAll(tablesaw);
  1739. });
  1740. Tablesaw.CheckAll = CheckAll;
  1741. })();
  1742. return Tablesaw;
  1743. }));