1 /** 2 * @fileOverview Record List Helper Functions and Classes 3 */ 4 5 /** 6 * @namespace Record List Helper Functions and Classes 7 */ 8 BTM.Table = {}; 9 10 /** 11 * Convenience Function to create a Dynamic Table 12 * @param {HTMLElement|String} element element ID or element reference to existing HTML table 13 * @returns {BTM.Table.Dynamic} new Dynamic Table object 14 * @see BTM.Table.Dynamic 15 */ 16 BTM.Table.makeDynamic = function makeDynamic(element) { 17 return new BTM.Table.Dynamic(element); 18 }; 19 20 BTM.Mapping.add('table.btm-table:not(.custom)', BTM.Table.makeDynamic); 21 22 /** 23 * @class Dynamic Table to allow sorting, filtering, etc 24 * @param {HTMLElement|String} element element ID or element reference to existing HTML table 25 * @param {Object} [options] options for the Dynamic Table 26 * @param {Boolean} [options.rememberSorting=true] flag to remember the sort-order of the Dynamic Table across page loads (not yet implemented) 27 * @param {Array} [options.columnTypes] an Array to force columns to be handled as a specific type, overriding the automatic detection. 28 * Valid types are currently "currency", "numeric" & "checkbox" 29 * @param {Array} [options.filterTypes] an Array to force columns to have the specified filter type, overriding the automatic detection. 30 * Valid types are currently "select" 31 */ 32 BTM.Table.Dynamic = function Dynamic(table, options) { 33 this.table = BTM.$(table); 34 var head = this.table ? this.table.tHead.rows[0] : false; 35 var sortCol = false; 36 var sortReverse = false; 37 var selectCol = false; 38 options = Object.update({ 39 'columnTypes' : [], 40 'filterTypes' : [], 41 'rememberSorting' : true 42 }, options); 43 44 var columnTypeRegex = { 45 'currency' : /Price|Cost|Amount/gi, 46 'numeric' : /Number|Max|Min|Count/gi, 47 'checkbox' : function (element) { return BTM.$$('input[type=checkbox], input[type=checkbox]',element).length > 0;} 48 }; 49 50 var filterTypeClasses = { 51 'select' : 'select' 52 }; 53 54 /** 55 * Refreshes the HTML table with the relevant data, based on sorting, filtering & pageing (not yet implemented) 56 */ 57 this.display = function display() { 58 while (this.table.tBodies[0].rows.length > 0) { 59 this.table.tBodies[0].removeChild(this.table.tBodies[0].rows[0]); //this.table.tBodies[0].rows[this.table.tBodies[0].rows.length-1]); 60 } 61 data.forEach(displayRow, this); 62 BTM.$$('span.loading',this.table).forEach(function(el) {BTM.DOM.removeClass(el, 'loading');}); 63 if (this.table.tFoot) { 64 BTM.DOM.update(this.table.tFoot.rows[0].cells[0], 'Showing ' + data.length + ' of ' + originalData.length + ' records.'); 65 } 66 }; 67 68 function displayRow(row, rowIndex) { 69 var tRow = this.table.tBodies[0].insertRow(rowIndex); 70 71 for (var i = 0; i < row.length; i++) { 72 tRow.appendChild(row[i]); 73 } 74 /* 75 if (BTM.Browser.engine === 'Trident' && BTM.Browser.engineVersion <= 7) { 76 for (var i = 0; i < row.length; i++) { 77 this.table.tBodies[0].rows[rowIndex].appendChild(row.cells[i]); 78 } 79 } 80 */ 81 BTM.Table.initClickToSelectRow(this.table.tBodies[0].rows[rowIndex]); 82 BTM.Table.zebraStripes(this.table.tBodies[0].rows[rowIndex]); 83 } 84 85 /** 86 * Sort the table, on the specified column 87 * @param {Event} event the event that triggered the sort, or false (if called programatically) 88 * @param {Number} column the column number to sort on (starting at 0) 89 * @param {Boolean} [keepDirection] override the order detection and keep the current sort direction 90 */ 91 this.sort = function sort(event, column, keepDirection) { 92 type = options.columnTypes[column] || 'general'; 93 94 keepDirection = keepDirection || false; 95 96 if (event) { 97 BTM.Event.cancelEvent(event); 98 } 99 100 BTM.log('Sorting table on column "' + column + '" with type "' + type); 101 102 BTM.DOM.addClass(BTM.$$('span',head.cells[sortCol])[0],'loading'); 103 104 if (sortCol === column) { 105 sortReverse = keepDirection ? sortReverse : !sortReverse; 106 BTM.DOM.removeClass(head.cells[sortCol], 'sort-' + (sortReverse ? 'asc' : 'desc')); 107 } 108 else { 109 if (sortCol !== false) { 110 BTM.DOM.removeClass(head.cells[sortCol], 'sort sort-asc sort-desc'); 111 } 112 113 sortCol = column; 114 sortReverse = false; 115 } 116 BTM.DOM.addClass(head.cells[column], 'sort sort-' + (sortReverse ? 'desc' : 'asc')); 117 118 119 data.sort(function (a, b) { 120 return BTM.Sort[type](a[column].innerText, b[column].innerText); 121 }); 122 123 if (sortReverse) { 124 data.reverse(); 125 } 126 this.display(); 127 }; 128 129 /** 130 * Filter the data shown in the Table 131 * @todo 132 * Implement Number/Currency filtering using ">" "<" etc 133 * Implement smart Currency sorting (convert cents to Float) 134 */ 135 this.filter = function filter() { 136 BTM.log('Filtering table'); 137 data = originalData.filter(function (element) { 138 for (var i = 0; i < element.length; i++) { 139 if (filters[i] && (!filters[i].Placeholder || !filters[i].Placeholder.placeholderActive)) { 140 var filterVal = filters[i].value.escapeRegex(); 141 if(options.filterTypes[i] === 'select' && filterVal !== '') { 142 filterVal = "^" + filterVal + '$'; 143 } 144 if (!new RegExp(filterVal, 'gi').test(element[i].innerText)) { 145 return false; 146 } 147 } 148 } 149 return true; 150 }, this); 151 152 if (sortCol !== false) { 153 this.sort(false, sortCol, true); 154 } 155 else { 156 this.display(); 157 } 158 }; 159 160 /** 161 * Helper for the {@link BTM.Table.Dynamic#filter} method, to allow as-you-type filtering. 162 * @see BTM.Table.Dynamic#filter 163 */ 164 this.filterTimeout = function filterTimeout(event) { 165 if (this.timeout) { 166 window.clearTimeout(this.timeout); 167 } 168 this.timeout = window.setTimeout(this.filter.bind(this), 50); 169 }; 170 171 BTM.Table.zebraStripes(this.table); 172 173 var headers = initHeaders.call(this); 174 var originalData = initData.call(this); 175 var data = originalData.concat([]); 176 var filters = initFilters.call(this); 177 if (this.table.tFoot) { 178 BTM.DOM.update(this.table.tFoot.rows[0].cells[0], 'Showing ' + data.length + ' of ' + originalData.length + ' records.'); 179 } 180 181 function initHeaders() { 182 183 var headers = []; 184 if(BTM.DOM.hasClass(head, 'filter')) { 185 head = this.table.tHead.rows[1]; 186 } 187 188 for (var i = 0; i < head.cells.length; i++) { 189 190 if (selectCol === false) { 191 var selectAll = BTM.$$('input.select-all', head.cells[i]); 192 193 if (selectAll.length > 0) { 194 selectCol = i; 195 selectIdentifier = BTM.DOM.getAttribute(BTM.$$('input[type=' + selectAll[0].type + ']', this.table.tBodies[0].rows[0].cells[i])[0], 'name'); 196 BTM.Form.Checkbox.initSelectAll(selectAll[0], 'input[name=' + selectIdentifier + ']', this.table); 197 } 198 } 199 200 headers[i] = head.cells[i].innerText; 201 if (headers[i].trim() === "") { 202 headers[i] = head.cells[i].innerHTML; 203 } 204 205 for(var j in columnTypeRegex) { 206 if (columnTypeRegex.hasOwnProperty(j) && ((Object.isRegExp(columnTypeRegex[j]) && columnTypeRegex[j].test(headers[i])) || (Object.isFunction(columnTypeRegex[j]) && columnTypeRegex[j](head.cells[i]))) && !options.columnTypes.hasOwnProperty[i]) { 207 options.columnTypes[i] = j; 208 break; 209 } 210 } 211 212 for(var k in filterTypeClasses) { 213 if (filterTypeClasses.hasOwnProperty(k) && BTM.DOM.hasClass(head.cells[i], filterTypeClasses[k])) { 214 options.filterTypes[i] = k; 215 } 216 } 217 218 219 if (!BTM.DOM.hasClass(head.cells[i], 'no-sort')) { 220 var span = BTM.DOM.update(BTM.DOM.createElement('span'), head.cells[i].innerHTML); 221 BTM.DOM.update(head.cells[i], span); 222 BTM.observe(head.cells[i], 'click', this.sort.bindAsEventListener(this, i)); 223 BTM.DOM.makeUnselectable(head.cells[i]); 224 } 225 } 226 return headers; 227 } 228 229 function initData() { 230 var data = []; 231 for (var i = 0; i < this.table.tBodies[0].rows.length; i++) { 232 data[i] = Array.prototype.map.call(this.table.tBodies[0].rows[i].cells, BTM.self); 233 BTM.Table.initClickToSelectRow(this.table.tBodies[0].rows[i], selectCol); 234 } 235 return data; 236 } 237 238 function initFilters() { 239 var filters = []; 240 var caption = this.table.caption; 241 if (BTM.DOM.$$('span', caption).length === 0) { 242 var span = BTM.DOM.update(BTM.DOM.createElement('span', {'class':'caption'}), caption.innerHTML); 243 BTM.DOM.update(caption, span); 244 } 245 246 if (!BTM.DOM.hasClass(this.table, 'no-filter')) { 247 var filterButton = BTM.DOM.createElement('button', {'class':'small', 'type':'button'}); 248 BTM.DOM.update(filterButton, 'Filter'); 249 span.insertBefore(filterButton, span.firstChild); 250 251 /* var btn = BTM.Form.makeBrandedButton(filterButton); */ 252 BTM.Mapping.init(this.table.caption); 253 254 BTM.DOM.setStyle(filterButton.parentNode.parentNode, 'float', 'right'); 255 256 var filterSection = this.table.tHead.insertRow(0); 257 BTM.DOM.addClass(filterSection, 'filter'); 258 BTM.Effect.hide(filterSection); 259 260 for(var i = 0; i < head.cells.length; i++) { 261 var cell = filterSection.appendChild(BTM.DOM.createElement('th')); 262 263 if(options.filterTypes[i] === 'select') { 264 var select = BTM.DOM.createElement('select'); 265 filters[i] = select; 266 var opts = []; 267 data.forEach(function (row) { 268 if (opts.indexOf(row[i].innerText) === -1) { 269 opts.push(row[i].innerText); 270 } 271 }, this); 272 273 opts.sort(); 274 BTM.Form.Select.addOption(select, 'Filter', ''); 275 opts.forEach(function (txt) { 276 BTM.Form.Select.addOption(select, txt, txt); 277 }); 278 279 cell.appendChild(select); 280 BTM.observe(select, 'change', this.filterTimeout.bind(this)); 281 BTM.observe(select, 'keypress', this.filterTimeout.bind(this)); 282 } 283 else if(options.columnTypes[i] !== 'checkbox') { 284 var input = BTM.DOM.createElement('input', {'type':'text', 'title':'Filter'}); 285 filters[i] = input; 286 cell.appendChild(input); 287 BTM.Form.Text.makePlaceholder(input); 288 BTM.observe(input, 'keyup', this.filterTimeout.bind(this)); 289 } 290 291 } 292 293 BTM.observe(filterButton, 'click', function () { 294 BTM.Effect.toggle(filterSection); 295 BTM.DOM.swapClass(filterButton.parentNode.parentNode, 'btm-branded-button-on'); 296 }); 297 } 298 return filters; 299 } 300 }; 301 302 /** 303 * Setup a handler to select a checkbox or radio button when a table row is clicked 304 * @param {HTMLElement|String} element element ID or element reference to the table row 305 * @param {Number} [index=0] the index of the element to use in the array containing checkboxes & radio buttons for each row, (starting at 0) 306 * @param {String} [selector=input[type=checkbox], input[type=radio]] the CSS selector to find checkboxes & radio buttons in the row 307 * @see BTM.Form.Checkbox.toggle 308 */ 309 BTM.Table.initClickToSelectRow = function initClickToSelectRow(element, index, selector) { 310 element = BTM.$(element); 311 selector = selector || "input[type=checkbox], input[type=radio]"; 312 index = index || 0; 313 var input = BTM.$$(selector, element)[index]; 314 if(!input) { 315 return false; 316 } 317 BTM.observe(element, 'click', function(event){ 318 if (BTM.Event.getTarget(event) !== input) { 319 BTM.Form.Checkbox.toggle(input); 320 } 321 }); 322 }; 323 324 325 /** 326 * Add new rows to an HTML table from a multi-dimensional Array of data 327 * @param {HTMLElement|String} element element ID or element reference to the table or tbody to update. If a table is referenced, the first tbody will be updated 328 * @param {Array[]} array the multi-dimensional Array of data to add to the table 329 */ 330 BTM.Table.arrayToRows = function arrayToRows(element, array) { 331 element = BTM.$(element); 332 333 if (element.tagName.toLowerCase() === 'table') { 334 element = element.tBodies[0]; 335 } 336 for (var i = 0; i < array.length; i++) { 337 var row = element.insertRow(-1); 338 BTM.Table.zebraStripes(row); 339 for (var j = 0; j < array[i].length; j++) { 340 var cell = row.insertCell(-1); 341 BTM.DOM.update(cell, array[i][j]); 342 } 343 } 344 }; 345 346 /** 347 * Add "zebra stripes" to a table, tbody or table row. 348 * @param {HTMLElement|String} element element ID or element reference to the table, tbody or table row to apply "zebra stripes" to 349 * @param {String}[] [classes=['odd','even']] the class names to apply to the rows, in order 350 * @returns {HTMLElement} the original element 351 */ 352 BTM.Table.zebraStripes = function zebraStripes(element, classes) { 353 element = BTM.$(element); 354 355 classes = classes && Object.isArray(classes) ? classes : ['odd', 'even']; 356 357 switch(element.nodeName.toLowerCase()) { 358 case 'tr': 359 var r = element.sectionRowIndex % classes.length; 360 var className = classes[r]; 361 BTM.DOM.removeClass(element, classes.join(' ')); 362 BTM.DOM.addClass(element, className); 363 break; 364 365 case 'tbody': 366 Array.forEach(element.rows, BTM.Table.zebraStripes); 367 break; 368 369 case 'table': 370 Array.forEach(element.tBodies, BTM.Table.zebraStripes); 371 break; 372 } 373 374 return element; 375 }; 376