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