/*
 * codepunch-datatable
 *
 * Copyright (c) 2020-2021, CodePunch Solutions.
 * https://codepunch.com
 * Author: Anil Kumar
 * 
 */

class DataTableView {

	static _DATATABLES = {};
	static _tableInFocus = '';
	static _filterInFocus = '';

	static getTable(tableid) {
		if(DataTableView._DATATABLES.hasOwnProperty(tableid)) 
			return DataTableView._DATATABLES[tableid];
		return undefined;
	}

	static removeTable(tableid) {
		if(DataTableView._DATATABLES.hasOwnProperty(tableid)) {
			delete DataTableView._DATATABLES[tableid];
			return true;
		}
		return false;
	}

	constructor(elem, options) {
		this.options = { 
			table: elem,
			idcolumn: 'id',
			heading: false,
			dthleft: '',
			dthright: '',
			columnsort: false,
			autosize: true,
			parent: '',
			sortorder:'asc', 
			sort: null,
			data: null,
			totalrows: 0,
			start: 0,
			count: 500,
			page: 0,
			totalpages: 0,
			columns: null,
			savecolumn: null,
			footer: [],
			filters: {},
			source: null,
			handlers: {},
			outerparent: 'body'
		};
		this.debounce = this.debounce.bind(this);
		DataTableView._DATATABLES[elem] = this;
		this.last_checked_index = -1;
		this.last_highlighted_rowid = -1;
		this.widths = {};

		this.setOption('cid', 1);
		this.setOption('aqid', 0);
		for(var opt in options) {
			if(options.hasOwnProperty(opt)) {
				this.setOption(opt, options[opt]);
			}
		}
		if(options.hasOwnProperty('selection'))
			this.last_highlighted_rowid = options['selection'];
		
		if(this.options.parent != '' && this.options.outerparent == 'body')
			this.options.outerparent = '#'+this.options.parent;

		var dtv = this;
		$(this.options.outerparent).off('click', '#'+ elem + ' .th-sortable');
		$(this.options.outerparent).on('click', '#'+ elem + ' .th-sortable', function(event) {
			var column = $(this).attr("data-column");
			dtv.sort(column);
		});

		$(this.options.outerparent).off('click', '#'+ elem + ' .th-cb-select-all');
		$(this.options.outerparent).on('click', '#'+ elem + ' .th-cb-select-all', function(event) {
			$(".tcb-" + dtv.options.table).prop('checked', $(this).is(":checked"));
		});
		
		// Direct check-box clicks
		$(this.options.outerparent).off('click', '#'+ elem + ' .tcb-' + elem);
		$(this.options.outerparent).on('click', '#'+ elem + ' .tcb-' + elem, function(event) {
			if(!event.shiftKey)
				event.stopPropagation();
		});

		$(this.options.outerparent).off('click', '#'+ elem + ' .tr-rowid');
		$(this.options.outerparent).on('click', '#'+ elem + ' .tr-rowid', function(event) {
			event.stopPropagation();
			var rowid = $(this).attr("data-id");
			if(dtv.options.handlers && dtv.options.handlers.select)
				dtv.options.handlers.select(rowid);
			var table = dtv.options.table;
			$('#' + table + ' .tr-rowid').removeClass('tr-selected');
			$(this).addClass('tr-selected');
			var serialid = $(this).data("serial");
			if(!event.shiftKey && !event.ctrlKey) {
				$(".tcb-" + dtv.options.table).prop('checked', false);
				$("#tcb-" + dtv.options.table + "-did-" + rowid).prop('checked', true);
			}
			else if(event.shiftKey) {
				var start = (dtv.last_checked_index < serialid) ? dtv.last_checked_index : serialid;
				var end = (dtv.last_checked_index > serialid) ? dtv.last_checked_index : serialid;
				if(start >= 0 && end > start) {
					$(".tcb-" + dtv.options.table).prop('checked', false);
					for(var i = start; i <= end; i++) {
						var id = $("#"+dtv.options.table).find(`[data-serial='${i}']`).data('id');
						$("#tcb-" + dtv.options.table + "-did-" + id).prop('checked', true);
					}
				}
			}
			else {
				if(event.ctrlKey) {
					if ($("#tcb-" + dtv.options.table + "-did-" + rowid).is(':checked'))
						$("#tcb-" + dtv.options.table + "-did-" + rowid).prop('checked', false);
					else
						$("#tcb-" + dtv.options.table + "-did-" + rowid).prop('checked', true);
				}
			}
			if ($("#tcb-" + dtv.options.table + "-did-" + rowid).is(':checked'))
				dtv.last_checked_index = serialid;
			dtv.last_highlighted_rowid = rowid;
		});

		var wto;
		$(this.options.outerparent).off('keyup', '#'+ elem + ' .filter-input');
		$(this.options.outerparent).on('keyup', '#'+ elem + ' .filter-input', function(event) {
			var column = $(this).attr("data-column");
			var tableid = dtv.options.table;
			var uidelay = dtv.options.uidelay ? dtv.options.uidelay : 600;
			var word = $(this).val();
			clearTimeout(wto);
			wto = setTimeout(function() {
				var changed = false;
				if (dtv.options.filters.hasOwnProperty(column)) {
					if(word == "") {
						delete dtv.options.filters[column];
						changed = true;
					}
					else if(dtv.options.filters[column] != word) {
						dtv.options.filters[column] = word;
						changed = true;
					}
				}
				else if(word !== "") {
					dtv.options.filters[column] = word;
					changed = true;
				}
				DataTableView._tableInFocus = tableid;
				DataTableView._filterInFocus = column;
				if(changed) {
					dtv.reload();
				}
			}, uidelay);
		});

		$(this.options.outerparent).off('click', '.btn-datatable');
		$(this.options.outerparent).on('click keypress', '.btn-datatable', function(event) {
			let cs = DataTableView.a11yClick(event);
			if(cs > 0) {
				var id = $(this).attr('data-id');
				var parts = id.split(':');
				if(parts.length == 2) {
					var command = parts[0];
					if(command == 'popup') {
						new PopupPanel(parts[1]);
					}
					else if(command == "event") {
						var fnName = parts[1];
						try {
							if(typeof window[fnName] == "function")
								return window[fnName]();
						}
						catch(err) {
							console.log('Error: '+ fnName + ': ' + err);
						}
					}
					else {
						var tableid = parts[1];
						var thetable = DataTableView.getTable(tableid);
						if(thetable) {
							if(command == "reload" || command == 'refresh')
								thetable.reload();
							else if(command == "copy")
								thetable.copy();
							else if(command == "copy-domains")
								thetable.copyDomains();
							else if(command == "download")
								thetable.download();
							else if(command.indexOf('nav-') == 0)
								thetable.navigate(command.substr(4));
							else {
								if(dtv.options.handlers && dtv.options.handlers.role)
									return dtv.options.handlers.role(command);
							}
						}
					}
				}
			}
		});
		
		this._columnClickHandler();

		this._activeEdit = null;
		this._inlineEditHandler();

		if(this.options.handlers['columns'] && typeof this.options.handlers.columns == 'function')
			this.options.handlers.columns(this.options.columns, false);
		this.reload();
	}
	
	_columnClickHandler() {
		var dtv = this;
		$(this.options.outerparent).off('click', '#'+ this.options.table + ' td');
		$(this.options.outerparent).on('click', '#'+ this.options.table + ' td',  function(event) {
			var column_num = parseInt( $(this).index() ) + 1;
			var cd = dtv.options.columns[column_num-1];
			var rowid = $(this).parent().data('id');
			var value = $(this).text();
			var column_name = cd['name'];
			if(typeof dtv.options.handlers['columnclick'] != "undefined")
				dtv.options.handlers['columnclick']({id:rowid, column: column_name, value: value});
		});
	}

	///////////////////////////////////////////////////////////////////////////
	// Inline cell editing
	///////////////////////////////////////////////////////////////////////////

	_getColumnDefForTd(td) {
		const tdIndex = $(td).index();
		let visibleCount = 0;
		for (let i = 0; i < this.options.columns.length; i++) {
			const cd = this.options.columns[i];
			if (typeof cd.hidden !== 'undefined' && cd.hidden) continue;
			if (visibleCount === tdIndex) return { cd, colIndex: i };
			visibleCount++;
		}
		return null;
	}

	_inlineEditHandler() {
		const dtv = this;
		$(this.options.outerparent).off('dblclick', '#' + this.options.table + ' td');
		$(this.options.outerparent).on('dblclick', '#' + this.options.table + ' td', function(e) {
			const result = dtv._getColumnDefForTd(this);
			if (!result) return;
			const { cd } = result;
			if (!cd.editable || !cd.editField) return;

			const row = $(this).closest('tr[data-id]');
			if (!row.length) return;
			const rowId    = row.attr('data-id');
			const rowIndex = dtv.options.data
				? dtv.options.data.findIndex(r => String(r[dtv.options.idcolumn]) === String(rowId))
				: -1;
			if (rowIndex === -1) return;

			dtv._startCellEdit(this, cd, dtv.options.data[rowIndex], rowIndex);
			e.stopPropagation();
		});
	}

	_startCellEdit(td, cd, rowData, rowIndex) {
		if (this._activeEdit) this._cancelCellEdit();
		 $('.tooltip').remove();

		const rawValue     = rowData[cd.name] !== undefined ? String(rowData[cd.name] || '') : '';
		const originalHtml = $(td).find('.td-content').html();

		const input      = document.createElement('input');
		input.type       = 'text';
		input.className  = 'dt-inline-edit-input';
		input.value      = rawValue;

		this._activeEdit = { td, cd, rowData, rowIndex, originalHtml, input };

		$(td).find('.td-content').empty().append(input);
		$(td).addClass('dt-cell-editing');
		input.focus();
		input.select();

		const dtv = this;

		input.addEventListener('keydown', function(e) {
			if (e.key === 'Escape') {
				dtv._cancelCellEdit();
				e.preventDefault();
				e.stopPropagation();
			} else if (e.key === 'Enter') {
				dtv._commitCellEdit();
				e.preventDefault();
				e.stopPropagation();
			}
		});

		input.addEventListener('blur', function() {
			if (dtv._activeEdit && dtv._activeEdit.input === input) {
				dtv._cancelCellEdit();
			}
		});
	}

	async _commitCellEdit() {
		if (!this._activeEdit) return;
		const { td, cd, rowData, rowIndex, originalHtml, input } = this._activeEdit;

		const newValue = input.value;
		let oldValue = rowData[cd.name] !== undefined ? String(rowData[cd.name] || '') : '';

		if (newValue === oldValue) {
			this._cancelCellEdit();
			return;
		}
		
		if(cd.name === "d.visual_domain") {
			if(oldValue === "" && String(rowData['d.domain']))
				oldValue = String(rowData['d.domain']);
			if (String(newValue).toLowerCase() !== String(oldValue).toLowerCase()) {
				this._cancelCellEdit();
				return;
			}
		}

		// Clear active edit before async so blur handler cannot fire cancel after commit
		this._activeEdit = null;
		$(td).removeClass('dt-cell-editing');
		$(td).addClass('dt-cell-saving');   // dim while saving

		// Extract t= from source params data
		const tMatch = this.options.source.params.data.match(/(?:^|&)t=([^&]+)/);
		const t      = tMatch ? tMatch[1] : 'domain';

		const editParams = new URLSearchParams({
			c   : 'grid',
			t   : t,
			oper: 'edit',
			id  : rowData[this.options.idcolumn]
		});
		editParams.set(cd.editField, newValue);

		try {
			const res  = await fetch(this.options.source.params.url, {
				method     : 'POST',
				credentials: 'same-origin',
				headers    : { 'Content-Type': 'application/x-www-form-urlencoded' },
				body       : editParams.toString()
			});
			const json = await res.json();

			if (json.status !== 'ok') {
				console.error('[celledit] Save failed:', json.error);
				$(td).find('.td-content').html(originalHtml);
				this._showEditError(td, json.error || 'Could not save. Please try again.');
				return;
			}

			// Update in-memory row data so refreshTableRow renders the new value
			$(td).removeClass('dt-cell-saving');
			this.options.data[rowIndex][cd.name] = newValue;
			this.refreshTableRow(rowIndex);

			// Optional post-save handler
			if (this.options.handlers && this.options.handlers.celledit) {
				this.options.handlers.celledit({
					id       : rowData[this.options.idcolumn],
					column   : cd.name,
					field    : cd.editField,
					oldValue : oldValue,
					newValue : newValue,
					rowData  : this.options.data[rowIndex],
					columnDef: cd
				});
			}

		} catch (err) {
			console.error('[celledit] Save error:', err);
			$(td).removeClass('dt-cell-saving');
			$(td).find('.td-content').html(originalHtml);
			this._showEditError(td, 'Could not save. Please try again.');
		}
	}

	_cancelCellEdit() {
		if (!this._activeEdit) return;
		const { td, originalHtml } = this._activeEdit;
		this._activeEdit = null;
		$(td).find('.td-content').html(originalHtml);
		$(td).removeClass('dt-cell-editing');
	}

	_showEditError(td, msg) {
		const err       = document.createElement('div');
		err.className   = 'dt-edit-error';
		err.textContent = msg;
		$(td).append(err);
		setTimeout(() => $(err).remove(), 3000);
	}
	
	clearAllFilters(refresh=false) {
		if(this.hasFilter()) {
			$('.filter-input').val('');
			this.options.filters = {};
			if(refresh)
				this.reload();
		}
	}
	
	setFilter(column, value, refresh, setfocus=true) {
		this.options.filters[column] = value;
		if(refresh) {
			if(setfocus)
				DataTableView._tableInFocus = this.options.table;
			DataTableView._filterInFocus = column;
			this.reload();
		}
	}

	setOption(key, value) {
        this.options[key] = value;
    }
	
	getOption(key, value) {
        return this.options[key];
    }
	
	hasQuery() {
		return this.options.source.params.query ? true : false;
	}
	
	clearQuery() {
		this.options.source.params.query = '';
		this.options.source.params.params = '';
	}
	
	setQuery(query, params) {
		if(query != undefined && query.length) {
			this.options.source.params.query = query;
			if(params != undefined && params.length)
				this.options.source.params.params = params;
			else
				this.options.source.params.params = '';
		}
		else {
			this.options.source.params.query = '';
			this.options.source.params.params = '';
		}
	}
	
	setHeading(heading, iconleft='', iconright='') {
		this.setOption('heading', heading);
		this.setOption('dthleft', iconleft);
		this.setOption('dthright', iconright);
		$('#'+this.options.table + '-dth-text').html(heading);
		$('#'+this.options.table + '-dth-icon-left').removeClass(function (index, className) {
			return (className.match (/(^|\s)list-icon-\S+/g) || []).join(' ');
		});
		$('#'+this.options.table + '-dth-icon-right').removeClass(function (index, className) {
			return (className.match (/(^|\s)list-icon-\S+/g) || []).join(' ');
		});
		if(iconleft != '')
			$('#'+this.options.table + '-dth-icon-left').addClass(iconleft);
		if(iconright != '')
			$('#'+this.options.table + '-dth-icon-right').addClass(iconright);
	}

	setHandler(key, handler) {
		if(!this.options.handlers)
			this.options.handlers = {[key]:handler};
		else
			this.options.handlers[key] = handler;
	}

	navigate(command) {
		if(command == 'next')
			this.navToPage(1);
		else if(command == 'last')
			this.navToPage("last");
		else if(command == 'previous')
			this.navToPage(-1);
		else if(command == 'first')
			this.navToPage(0);
		else if(command == 'top')
			this.navToTop();
		else if(command == 'bottom')
			this.navToBottom();
	}

	navToPage(pi) {
		$('.tooltip').remove();
		var cpage = this.options.page;
		var total = this.options.totalrows;
		var current = this.options.start;
		var rowsperpage = this.options.count;
		if(rowsperpage) {
			var newpage = 0;
			if(pi == 'last')
				newpage = this.options.totalpages-1;
			else if(pi == 0)
				newpage = 0;
			else
				newpage = this.options.page+pi;
			if(newpage >= 0 && newpage < this.options.totalpages) {
				this.options.page = newpage;
				this.options.start = newpage*this.options.count;
			}
		}
		if(this.options.page != cpage)
			this.reload('',false);
	}
	
	navToBottom() {
		var parent = $('#'+this.options.table).parent();
		if(parent.hasClass('dtcard-body')) {
			var elem = parent.get(0);
			elem.scrollTop = elem.scrollHeight;
		}
	}
	
	navToTop() {
		var parent = $('#'+this.options.table).parent();
		if(parent.hasClass('dtcard-body')) {
			var elem = parent.get(0);
			elem.scrollTop = 0;
		}
	}
	
	scrollIntoView(rowid) {
		var element = $("#"+this.options.table).find(`[data-id='${rowid}']`);
		if(element.length) {
			var parent = $('#'+this.options.table).parent();
			var containerTop = parent.scrollTop(); 
			var containerBottom = containerTop + parent.height(); 
			var elemTop = element.offset().top;
			var elemBottom = elemTop + $(element).height(); 
			if (elemTop < containerTop) {
				parent.scrollTop(elemTop);
			} 
			else if (elemBottom > containerBottom) {
				parent.scrollTop(elemBottom - parent.height());
			}
		}
	}

	_createFooter() {
		var htmlbuttons = this._getToolbar();
		var parent = $('#'+this.options.table).parent();
		if(parent.hasClass('dtcard-body')) {
			parent = parent.parent();
			if(parent.hasClass('dtcard')) {
				var cf = parent.find('.dtcard-footer');
				if(cf) {
					var divs = '<div class"left">' + htmlbuttons + '</div>';
					divs += '<div class="mid"></div>';
					divs += '<div class="right"></div>';
					cf.html(divs);
					$('[title]').tooltip();
				}
			}
		}
		this.showStats();
	}
	
	_getToolbar() {
		var htmlbuttons = '';
		for(var i = 0; i < this.options.footer.length; i++) {
			var btn = this.options.footer[i];
			var action = btn['action'] ? btn.action : '';
			var icon = btn['icon'] ? btn.icon : '';
			var role = btn['role'] ? btn.role : '';
			if(action != '') {
				var btnclass = 'btn-dt-footer btn-datatable theme-bg';
				if(btn.class != undefined && btn.class.length)
					btnclass += ' ' + btn.class;
				var bg = 'background-image: url(' + this.iconPath(icon) + ');';
				var tooltip = btn['tooltip'] ? btn['tooltip'] : role;
				htmlbuttons += this.wrap('div', '&nbsp;', {'data-icon': icon, 'data-id': action, class: btnclass, style:bg, title: tooltip});
			}
			else if(role == "separator") {
				htmlbuttons += this.wrap('div', '', {class: 'btn-dt-separator'});
			}
			else {
				if(role == 'copy' && icon == '')
					icon = 'content-copy';
				else if(role == 'download' && icon == '')
					icon = 'file-download';
				else if(role == 'refresh' && icon == '')
					icon = 'refresh';
				else if(role == 'edit' && icon == '')
					icon = 'edit';
				else if(role == 'clearfilters' && icon == '')
					icon = 'clearfilter';
				else if(role == 'delete' && icon == '')
					icon = 'delete-forever';
				else if(role == 'safe-delete' && icon == '')
					icon = 'remove-circle-outline';
				else if(role == 'add' && icon == '')
					icon = 'add-circle-outline';
				var tooltip = btn['tooltip'] ? btn['tooltip'] : role;
				var btnclass = 'btn-dt-footer btn-datatable theme-bg';
				if(btn.class != undefined && btn.class.length)
					btnclass += ' ' + btn.class;
				var bg = 'background-image: url(' + this.iconPath(icon) + ');';
				htmlbuttons += this.wrap('div', '&nbsp;', {'data-icon': icon, 'data-id': role+':'+this.options.table, class: btnclass, style:bg, title: tooltip, tabindex: 0});
			}
		}
		return htmlbuttons;
	}
	
	_getNav() {
		var htmlnav = '';
		var nav = [
			{icon:'first-page', role: 'nav-first', tooltip: 'First Page'},
			{icon:'chevron-left', role: 'nav-previous', tooltip: 'Previous Page'},
			{icon:'chevron-right', role: 'nav-next', tooltip: 'Next Page'},
			{icon:'last-page', role: 'nav-last', tooltip: 'Last Page'},
			{icon:'expand-less', role: 'nav-top', tooltip: 'Scroll To Top'},
			{icon:'expand-more', role: 'nav-bottom', tooltip: 'Scroll To Bottom'}
		];
		var btnclass = 'btn-dt-footer btn-datatable theme-bg';
		for(var i = 0; i < nav.length; i++) {
			let nicon = nav[i]['icon'];
			let nrole = nav[i]['role'];
			let tooltip = nav[i].tooltip;
			let bg = 'background-image: url(' + this.iconPath(nicon) + ');';
			htmlnav += this.wrap('div', '&nbsp;', {'data-icon': nicon, 'data-id': nrole+':'+this.options.table, class: btnclass, style:bg, title: tooltip, tabindex: 0});
		}
		return htmlnav;
	}
	
	iconPath(nicon) {
		if(this.options.handlers && this.options.handlers.iconPath)
			return this.options.handlers.iconPath(nicon);
		else
			return 'lib/layouts/colors/' + ColorMode.getMode() + '/images/64/toolbar/' + nicon + '.png';
	}

	_showStats(msg, pos='right') {
		var parent = $('#'+this.options.table).parent();
		if(parent.hasClass('dtcard-body') && (pos == 'mid' || pos == 'right')) {
			parent = parent.parent();
			if(parent.hasClass('dtcard')) {
				var cf = parent.find('.dtcard-footer');
				if(cf) {
					cf.find('.'+pos).html(msg);
					$('[title]').tooltip();
				}
			}
		}
	}

	hideNav() {
		if(this.options.count >= this.options.totalrows || !this.options.totalrows)
			this._showStats('', 'mid');
	}
	
	showToolbarButtons() {
		this.showFooterButton('clearfilters', this.hasFilter() || this.hasQuery());
		this.showFooterButton('copy',         this.hasData());
		this.showFooterButton('copy-domains', this.hasData());
		this.showFooterButton('download',     this.hasData());
		this.showFooterButton('delete',       this.hasData());
		this.showFooterButton('edit',         this.hasData());
		this.showFooterButton('safe-delete',  this.hasData());
	}

	showStats() {
		var msg = '';
		if(this.options.totalrows) {
			let endcount = this.options.start+this.options.count;
			endcount = endcount > this.options.totalrows ? this.options.totalrows : endcount;
			msg = (this.options.start+1) + '-' + endcount + ' of ' + this.options.totalrows;
		}
		this._showStats(msg);
		var nav = this._getNav();
		this._showStats(nav, 'mid');
		this.hideNav();
		this.showToolbarButtons();
	}

	copy() {
		DataTableView._clipboardCopy(this.getSelectedCSV());
	}

	copyDomains() {
		const domains = this.getSelected('d.domain').filter(d => d && d.length > 0);
		DataTableView._clipboardCopy(domains.join('\n'));
	}

	download() {
		this._download(this.getSelectedCSV());
	}

	// Returns CSV string for selected rows only.
	getSelectedCSV() {
		const selectedIds = new Set(this.getSelectedIds());
		const data = selectedIds.size > 0
			? this.options.data.filter(row => selectedIds.has(String(row[this.options.idcolumn])))
			: [];
		return this._buildCSV(data);
	}

	// Returns CSV string for all rows (full table).
	getAllCSV() {
		return this._buildCSV(this.options.data);
	}

	// Shared CSV builder — used by getSelectedCSV() and getAllCSV().
	_buildCSV(data) {
		if (!data || data.length === 0) return '';

		var header = '';
		for (var i = 0; i < this.options.columns.length; i++) {
			var cd = this.options.columns[i];
			if (cd.name == "dt_ser_num" || cd.name == "cb") continue;
			if (cd.virtual === true) continue;
			if (cd.hidden  === true) continue;
			var label = this.csvQuote(cd.label);
			header += (header == '') ? label : ',' + label;
		}

		var csvrows = [header];
		for (var row = 0; row < data.length; row++) {
			var datarow = '';
			for (var i = 0; i < this.options.columns.length; i++) {
				var cd = this.options.columns[i];
				if (cd.name == "dt_ser_num" || cd.name == "cb") continue;
				if (cd.virtual === true) continue;
				if (cd.hidden  === true) continue;
				let value = this.csvQuote(data[row][cd.name]);
				datarow += (datarow == '') ? value : ',' + value;
			}
			csvrows.push(datarow);
		}
		return csvrows.join("\n");
	}
	
	static a11yClick(event) {
		if(event.type === 'click')
			return 3;
		else if(event.type === 'keypress') {
			var code = event.charCode || event.keyCode;
			if((code === 32)|| (code === 13))
				return code === 32 ? 2 : 1;
		}
		else
			return 0;
	}

	static _clipboardCopy(text) {
		var textArea = document.createElement("textarea");
		textArea.value = text;
		textArea.style.top = "0";
		textArea.style.left = "0";
		textArea.style.position = "fixed";
		document.body.appendChild(textArea);
		textArea.focus();
		textArea.select();
		try {
			document.execCommand('copy');
		} 
		catch (err) {
			console.error('Unable to copy', err);
		}
		document.body.removeChild(textArea);
	}

	_download(csvdata) {
		var posturl = this.options.source.params.url + "?c=download&t=post";
		posturl += "&filename=" + this.options.table;
		posturl += ".csv";
		$('body').append('<form id="exportform" action="' + posturl + '" method="post"><input type="hidden" id="' + "export" + '" name="' + "export" + '"></form>');
		$("#" + "export").val(csvdata);
		$("#exportform").submit().remove();
	}

	reload(extraparam='',redraw=true) {
		if(redraw) {
			this.show("wait");
			this._showStats('');
		}
		if(this.options.source.type == 'ajax') {
			var page = (this.options.start/this.options.count) + 1;
			var datastr = this.options.source.params.data;
			if(typeof this.options.source.params.post != "undefined") {
				for(var i = 0; i < this.options.source.params.post.length; i++)
					datastr += "&" + this.options.source.params.post[i] + "=" + this.options[this.options.source.params.post[i]];
			}
			var query = (this.options.source.params.query != undefined && this.options.source.params.query.length) ? encodeURIComponent(this.options.source.params.query) : '';
			var params = (this.options.source.params.params != undefined && this.options.source.params.params.length) ? encodeURIComponent(this.options.source.params.params) : '';
			datastr += "&query=" +  query;
			datastr += "&params=" + params;
			datastr += "&sord=" + this.options.sortorder;
			datastr += "&sidx=" + encodeURIComponent(this.options.sort);
			datastr += "&start=" + this.options.start;
			datastr += "&page=" + page;
			datastr += "&total=" + this.options.totalpages;
			datastr += "&count=" + this.options.totalrows;
			datastr += "&rows=" + this.options.count;
			datastr += "&columns=" + encodeURIComponent(this.getColumnNames());
			this.options.dthright = '';
			var searchfilter = {groupOp: 'AND', 'rules':[]};
			const sfmode = String(typeof PORTFOLIO_DEFAULT_SEARCH_FILTER_MODE !== "undefined" ? PORTFOLIO_DEFAULT_SEARCH_FILTER_MODE : "bw").toLowerCase();
			for(var field in this.options.filters) {
				if(this.options.filters.hasOwnProperty(field)) {
					var rule = {op: sfmode};
					var value = this.options.filters[field];
					var fl = value.substring(0,1);
					if(fl == '=') {
						rule['op'] = 'eq';
						value = value.substring(1);
					}
					else if(fl == '!') {
						rule['op'] = 'ne';
						value = value.substring(1);
					}
					rule['data'] = value;
					rule['field'] = field;
					searchfilter['rules'].push(rule);
				}
			}
			if(this.hasFilter())
				datastr += "&_search=1";

			datastr += "&filters=" + encodeURIComponent(JSON.stringify(searchfilter));
			if(this.options.source.params.extra)
				datastr += this.options.source.params.extra;
			if(extraparam)
				datastr += "&" + extraparam;
			var request = $.ajax({type: "POST", url: this.options.source.params.url, data: datastr});
			var dtv = this;
			request.done(function(msg) {  
				var x = SEDAPI.parse(msg);
				dtv.ajaxData(x, null, null, this.options);
			});
			request.fail(function(jqXHR, textStatus) {
				DataTableView.sessionTimedoutError(jqXHR);
				dtv.ajaxData(null, jqXHR, textStatus, this.options);
			});
		}
	}
	
	static sessionTimedoutError(jqXHR) {
		if(jqXHR.status == 408 || jqXHR.status == 400 || jqXHR.status == 403) {
			if(jqXHR.status == 408)
				SEDAPI.sessionRestart("auth_error_session_timeout");
			else if(jqXHR.status == 400)
				SEDAPI.sessionRestart("notloggedin");
			else if(jqXHR.status == 403)
				SEDAPI.sessionRestart("notloggedin");
			return true;
		}
		return false;
	}

	ajaxData(x, jqXHR, textStatus, params) {
		if(x != null) {
			if(x.total >= 0 || x.status == 'ok') {
				this.options.totalrows = parseInt(x.records);
				if(this.options.count <= 0)
					this.options.count = 500;
				this.options.totalpages = parseInt(this.options.totalrows/this.options.count);
				if(this.options.totalrows%this.options.count)
					this.options.totalpages++;
				this.options.start = this.options.start < this.options.totalrows ? this.options.start : 0;
				this.options.page = this.options.page < this.options.totalpages ? this.options.page : 0;
				this.show(x.rows);
			}
			else {
				this.show(x.error);
			}
		}
		else
			this.show(jqXHR.responseText);
		if(this.options.handlers && this.options.handlers.ajaxData)
			this.options.handlers.ajaxData(x);
	}
	
	getColumnNames() {
		var names = [];
		for(var i = 0; i < this.options.columns.length; i++) {
			var cd = this.options.columns[i];
			if(cd.name == "dt_ser_num" || cd.name == "cb")
				continue;
			if(cd.virtual === true)
				continue;
			names.push(cd.name);
		}
		return names.join(",");
	}

	sort(column) {
		// If column is virtual with a sortColumn, sort by the real column instead
		const cd = this.options.columns.find(c => c.name === column);
		if (cd && cd.virtual === true) {
			if (cd.sortColumn)
				column = cd.sortColumn;
			else
				return; // virtual with no sortColumn — not sortable
		}
		if(this.options.sort == column)
			this.options.sortorder = this.options.sortorder == "asc" ? "desc" : "asc";
		else
			this.options.sort = column;
    	this.reload();
	}

	setData(tabledata) {
		this.options.data = tabledata;
	}
	
	getOptions() {
		return this.options;
	}

	getTableHeadRow() {
		var table = "<tr>";
		this.options.min_width = 0;
		var stickyLeft = 0;
		for(var i = 0; i < this.options.columns.length; i++) {
			var cd = this.options.columns[i];
			let hiddencolumn = (typeof cd.hidden != 'undefined') ? cd.hidden : false;
			if(hiddencolumn)
				continue;
			var moveable = (typeof cd.move != 'undefined') ? cd.move : true;
			var sortorder = this.options.sortorder;
			var label = cd.label;

			// Virtual columns without a sortColumn are never sortable or searchable
			var isSortable = cd.sortable === true;
			var isSearchable = cd.search && cd.search.type;
			if (cd.virtual === true && !cd.sortColumn) {
				isSortable  = false;
				isSearchable = false;
			}
			var classname = 'th-' + cd.name;
			if(typeof cd.fixed !== 'undefined' && cd.fixed)
				classname += " noresize";
			if(this.options.columnsort && moveable !== false)
				classname += " csortable";
			var style = '';
			if(typeof cd.search != 'undefined' && typeof cd.search.type == 'undefined' && cd.search === true) {
				cd.search = {type:'text'};
			}
			if(typeof cd.width.default == 'undefined' && typeof cd.width != 'undefined' && cd.width != "") {
				cd.width = {default:parseInt(cd.width)};
			}
			if(typeof cd.minwidth != 'undefined' && typeof cd.width.min == "undefined") {
				cd.width['min'] = cd.minwidth;
			}
			if(typeof cd.width.default !== 'undefined') {
				style = 'width:' + cd.width.default + (cd.width.default == "auto" ? ';' : 'px' + ';');
				this.options.min_width += cd.width.default == "auto" ? (cd.width.min !== 'undefined' ? cd.width.min : 0) : parseInt(cd.width.default);
			}
			if (cd.sticky) {
				style += `left:${stickyLeft}px;`;
				classname += ' th-sticky';
				const cdw = typeof cd.width === 'object' ? (parseInt(cd.width.default) || 0) : (parseInt(cd.width) || 0);
				stickyLeft += cdw;
			}
			if(typeof cd.type !== 'undefined' && cd.type == "cb") {
				var cbcontent = this.wrap('input', '', {class: 'th-cb-select-all th-cb-' + this.options.table, id:'th-cb-'+ this.options.table, type:'checkbox', 'data-column': 'cb'});
			}
			else if(isSortable) {
				if(this.options.sort == null)
					this.options.sort = cd.name;
				var contentclassname = 'th-content th-sortable';
				var sortcontent = this.wrap('div', '', {class:'icon-16'});
			}
			else {
				var sortcontent = "";
				var contentclassname = "th-content";
			}
			if(typeof cbcontent != "undefined" && cbcontent) {
				var content = cbcontent;
				cbcontent = null;
			}
			else
				var content = this.wrap('div', label + sortcontent, {class: contentclassname, 'data-column': cd.name});
			if(this.options.columnsort && classname.includes('csortable'))
				content = "<div class=\"draghandle\"></div>" + content;
			
			if(!classname.includes('noresize') && i < (this.options.columns.length)) {
				content += '<div class="dtcol-divider"></div>';
			}
			
			if(isSearchable && cd.search.type == 'text') {
				var value = '';
				var filter = this.wrap('input', '', {type: 'text', 'data-id': 'th-filter-'+this.options.table+'-'+cd.name, class: 'filter-input', 'data-column': cd.name, placeholder: 'Filter', value: value});
				var inputelem = this.wrap('div', filter, {class:'th-column-filter'});
				table += this.wrap('th', content + inputelem, {class:classname, style:style});
			}
			else
				table += this.wrap('th', content, {class:classname, style:style});
		}
		table += "<th style=\"width:10px;\"></th>";
		table += '</tr>';
		return table;
	}

	getRandomMessage(msgs) {
		return msgs[Math.floor((Math.random() * msgs.length))];
	}

	showWait() {
		$('#'+this.options.parent + ' .dtcard-msg').html("<div class=\"lds-dual-ring\"></div>");
		return '';
	}

	showMessage(msg) {
		if(typeof msg == 'undefined') {
			msg = "Nothing found";
			if(this.hasFilter() || this.hasQuery()) {
				msg = "Nothing found (you have used filters)";
			}
			else if(this.options.cid <= 1 && this.options.aqid <= 1)
				msg = typeof this.options.nodatamsg == "undefined" ? msg : this.options.nodatamsg;
		}
		$('#'+this.options.parent + ' .dtcard-msg').html('<div>'+msg+'</div>');
		return '';
	}

	getBody(msg=null) {
		$('#'+this.options.parent + ' .dtcard-msg').html('');
		if(typeof msg == "string")
			return (msg == "wait") ? this.showWait() : this.showMessage(msg);
		else if(!Array.isArray(this.options.data))
			return this.showMessage(this.options.data);
		else if(!this.options.data.length) {
			if(this.hasFilter())
				return this.showMessage("No Results. Try clearing the filters on top.");
			else
				return this.showMessage("No Results");
		}
		
		var html = '';
		let visual_domain_exists = false;
		const stored = localStorage.getItem("link_out_domain_columns");
		const do_link_out = stored === null ? 1 : (stored === "1" ? 1 : 0);
		
		for (var row = 0; row < this.options.data.length; row++) {
			if (row === 0) {
				let vd0 = this.options.data[row].visual_domain;
				if (typeof vd0 === "undefined")
					vd0 = this.options.data[row]["d.visual_domain"];
				visual_domain_exists = typeof vd0 !== "undefined" && vd0 !== null;
			}
			html += this.getRowHtml(row, this.options.data[row], visual_domain_exists, do_link_out);
		}

		return html;
	}
	
	getRowHtml(row, rowData, visual_domain_exists, do_link_out) {
		var rowid = rowData[this.options.idcolumn];
		var serial = row + this.options.start + 1;
		var html = "<tr data-serial=\"" + row + "\" data-id=\"" + rowid + "\" class=\"tr-rowid\">";
		var stickyLeft = 0;

		for (var i = 0; i < this.options.columns.length; i++) {
			var cd = this.options.columns[i];
			
			if (typeof cd.hidden !== 'undefined' && cd.hidden) continue;

			// Compute sticky style once for all td branches
			var tdStickyStyle = '';
			var tdStickyClass = '';
			if (cd.sticky) {
				tdStickyStyle = ` style="left:${stickyLeft}px;"`;
				tdStickyClass = ' td-sticky';
				const cdw = typeof cd.width === 'object' ? (parseInt(cd.width.default) || 0) : (parseInt(cd.width) || 0);
				stickyLeft += cdw;
			}

			if (cd.name == "dt_ser_num") {
				html += `<td class="dt-serial${tdStickyClass}"${tdStickyStyle}><div class="td-content">${serial}</div></td>`;
			}
			else if (cd.name == "cb") {
				html += `<td class="cb-col${tdStickyClass}"${tdStickyStyle}><input class="tcb-${this.options.table}" id="tcb-${this.options.table}-did-${rowid}" type="checkbox"></td>`;
			}
			else {
				let value = rowData[cd.name];

				if (
					visual_domain_exists &&
					!cd.formatter &&
					(cd.name.toLowerCase() === "domain" || cd.name.toLowerCase() === "d.domain")
				) {
					let vd = rowData.visual_domain;
					if (typeof vd === "undefined")
						vd = rowData["d.visual_domain"];

					if (
						typeof vd !== "undefined" &&
						typeof value === "string" &&
						typeof vd === "string" &&
						vd.length === value.length
					) {
						value = vd;
					}
					if (do_link_out) {
						value = "<a class=\"icon-link-out\" target=\"_blank\" href=\"http://" + rowData["d.domain"] + "\"></a>&nbsp;" + value;
					}
				}

				var tdclass = 'td-content';
				if (cd.multiline)
					tdclass += ' multiline';
				if (cd.classes)
					tdclass += ' ' + cd.classes;
				if (cd.linkify)
					value = DataTableView.linkify(value);
				if (typeof cd.formatter != "undefined" && typeof cd.formatter == 'string') {
					if (typeof this[cd.formatter] == "function")
						value = this[cd.formatter](value, cd, rowData);
					else if (typeof window !== "undefined" && typeof window[cd.formatter] === "function") {
						value = window[cd.formatter](value, cd, rowData);
					}
					else {
						if (cd.formatter == "date")
							value = this.dateFormatter(value, cd);
					}
				}
				else if (String(cd.name).toLowerCase().endsWith("_price") || String(cd.name).toLowerCase().endsWith("minimum_offer")) {
					value = this.priceFormatter(value, cd);
				}
				const tdAttrs = (cd.editable && cd.editField) ? ' class="dt-cell-editable" title="Double-click to edit"' : '';
				let tdFinalClass = tdStickyClass.trim();
				if (cd.editable && cd.editField) tdFinalClass = (tdFinalClass ? tdFinalClass + ' ' : '') + 'dt-cell-editable';
				const titleAttr = (cd.editable && cd.editField) ? ' title="Double-click to edit"' : '';
				html += `<td${tdFinalClass ? ` class="${tdFinalClass}"` : ''}${tdStickyStyle}${titleAttr}><div class="${tdclass}">${value}</div></td>`;
			}
		}

		html += "<td></td>";
		html += "</tr>";
		return html;
	}
	
	refreshTableRow(index) {
		if (!this.options.data || index < 0 || index >= this.options.data.length) {
			console.warn("refreshTableRow: invalid index", index);
			return false;
		}

		const rowData = this.options.data[index];
		const rowId = rowData[this.options.idcolumn];

		let visual_domain_exists = false;
		if (this.options.data.length > 0) {
			let vd0 = this.options.data[0].visual_domain;
			if (typeof vd0 === "undefined")
				vd0 = this.options.data[0]["d.visual_domain"];
			visual_domain_exists = typeof vd0 !== "undefined" && vd0 !== null;
		}

		const stored = localStorage.getItem("link_out_domain_columns");
		const do_link_out = stored === null ? 1 : (stored === "1" ? 1 : 0);

		const existingRow = $("#" + this.options.table).find(`tr[data-id='${rowId}']`);
		if (!existingRow.length) {
			console.warn("refreshTableRow: row not found in DOM for id", rowId);
			return false;
		}

		const wasSelected = existingRow.hasClass("tr-selected");

		const newRowHtml = this.getRowHtml(index, rowData, visual_domain_exists, do_link_out);
		existingRow.replaceWith(newRowHtml);

		if (wasSelected) {
			$("#" + this.options.table).find(`tr[data-id='${rowId}']`).addClass("tr-selected");
		}

		if (this.last_highlighted_rowid == rowId) {
			$("#tcb-" + this.options.table + "-did-" + rowId).prop("checked", true);
		}

		$('[title]').tooltip();

		return true;
	}
	
	priceFormatter(cellvalue, cd, symbol = "$", locale = undefined) {
		if (cellvalue === null || cellvalue === undefined || cellvalue === '') {
			return cellvalue;
		}
		const num = Number(cellvalue);
		if (!Number.isFinite(num)) return cellvalue;
		const formatted = new Intl.NumberFormat(locale, {
			minimumFractionDigits: 2,
			maximumFractionDigits: 2
		}).format(num);
		return symbol + formatted;
	}

	dateFormatter(cellvalue, cd) {
		return SEDAPI.formatDate(cellvalue, cd.formatoptions.newformat, cd.name);
	}

	static linkify(inputText) {
		var replacedText, replacePattern1, replacePattern2, replacePattern3;
		replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
		replacedText = inputText.replace(replacePattern1, '<a href="$1" target="_blank">$1</a>');
		replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
		replacedText = replacedText.replace(replacePattern2, '$1<a href="http://$2" target="_blank">$2</a>');
		replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
		replacedText = replacedText.replace(replacePattern3, '<a href="mailto:$1">$1</a>');
		return replacedText;
	}

	getCSV() {
		var csvrows = [];
		var header = '';
		for(var row = 0; row < this.options.data.length; row++) {
			var datarow = '';
			for(var i = 0; i < this.options.columns.length; i++) {
				var cd = this.options.columns[i];
				if(cd.name == "dt_ser_num" || cd.name == "cb") continue;
				if(cd.virtual === true)  continue; // skip UI-only virtual columns
				if(cd.hidden  === true)  continue; // skip hidden backing columns
				else {
					if(!row) {
						var label = this.csvQuote(this.options.columns[i].label);
						header += (header == '') ? label : ','+ label;
					}
					let value = this.csvQuote(this.options.data[row][cd.name]);
					datarow += (datarow == '') ? value : ',' + value;
				}
			}
			if(!row)
				csvrows.push(header);
			csvrows.push(datarow);
		}
		return csvrows.join("\n");
	}

	csvQuote(term) {
		if(typeof term != "string")
			return term;
		var result = term.replace(/"/g, '""');
		if (result.search(/("|,|\n)/g) >= 0)
			result = '"' + result + '"';
		return result;
	}

	show(data) {
		let click_on_row = (this.options.data == null && data !== false && typeof data !== "string") ? true : false;
		
		if(data !== false && typeof data !== "string")
			this.setData(data);
		
		var tablehead = this.getTableHeadRow();
		if((typeof this.tablehead != 'undefined' && this.tablehead != tablehead) || typeof this.tablehead == 'undefined') {
			if(this.options.parent != '') {
				var divs = '' +
					'<div class="dtcard">' +
					'<div class="dtcard-header">';
				if(this.options.heading !== false) {
					divs += 
					'<div id="' + this.options.table + '-dth-icon-control" class="dtcard-header-control">' + '' + '</div>' +
					'<div id="' + this.options.table + '-dth-icon-left">' + '' + '</div>' +
					'<div id="' + this.options.table + '-dth-icon-right">' + '' + '</div>' +
					'<div id="' + this.options.table + '-dth-text" class="dtcard-header-text"></div>';
				}
				divs += 
					'</div>' +
					'<div class="dtcard-body">' +
					'<div class="dtcard-msg"></div>' +
					'<table id="' + this.options.table + '" class="datatable resizable"><thead></thead><tbody></tbody>' +
					'</table>' + 
					'</div>' +
					'<div class="dtcard-footer">' +
					'</div>' +
					'</div>';
				$('#' + this.options.parent + ' > .dtcard').remove();
				$('#' + this.options.parent).html(divs);
				if(!$('#'+this.options.parent).hasClass('datatable-wrapper'))
					$('#'+this.options.parent).addClass('datatable-wrapper');
			}
			$('#' + this.options.table + ' thead').html(tablehead);
			this.setFilterValues();
			this._createFooter();
			
			if(this.options.autosize === false) {
				document.getElementById(this.options.table).style.maxwidth = this.options.min_width + "px";
				document.getElementById(this.options.table).style.width = this.options.min_width + "px";
			}
			document.getElementById(this.options.table).style.minWidth = this.options.min_width + "px";
			
			this.enableColumnResizing();
			if(this.options.handlers && this.options.handlers.resize)
				this.options.handlers.resize();
			
			this.tablehead = tablehead;
		}
		this.setSortIcon();
		this.showHeading();
		this.showStats();
		
		var tablebody = this.getBody(typeof data == "string" ? data : null);
		$('#' + this.options.table + ' tbody').html(tablebody);
		this.selectRow(this.last_highlighted_rowid, click_on_row);
		if(this.last_highlighted_rowid > 0)
			$("#tcb-" + this.options.table + "-did-" + this.last_highlighted_rowid).prop('checked', true);
		$('[title]').tooltip();
	}
	
	showFooterButton(role, flag) {
		let cname = role + ":" + this.options.table;
		if(flag)
			$(this.options.outerparent + " .dtcard-footer").find(`[data-id='${cname}']`).show();
		else
			$(this.options.outerparent + " .dtcard-footer").find(`[data-id='${cname}']`).hide();
	}
	
	hasFilter() {
		return Object.keys(this.options.filters).length;
	}
	
	hasData() {
		return (this.options.data && Array.isArray(this.options.data) && this.options.data.length);
	}
	
	setFilterValues() {
		for(var i = 0; i < this.options.columns.length; i++) {
			var cd = this.options.columns[i];
			var value = this.options.filters[cd.name] ? this.options.filters[cd.name] : '';
			if(typeof cd.search !== 'undefined' && cd.search && cd.search.type && cd.search.type == 'text' && value != '') {
				var dataid = 'th-filter-'+this.options.table+'-'+cd.name;
				$("#"+this.options.table).find("[data-id='" + dataid + "']").val(value); 
			}
		}
		if(DataTableView._filterInFocus && DataTableView._tableInFocus == this.options.table) {
			var elem = $('#' + this.options.table + ' input[data-column="' + DataTableView._filterInFocus + '"]');
			if(elem && elem.length) {
				var strLength = elem.val().length * 2;
				elem.focus();
				elem[0].setSelectionRange(strLength, strLength);
			}
		}
	}
	
	setSortIcon() {
		$("#"+this.options.table + ' .th-sortable').removeClass('th-sorted-asc');
		$("#"+this.options.table + ' .th-sortable').removeClass('th-sorted-desc');
		var sortcolumn = this.options.sort.replace('.', '\\.');
		if(sortcolumn.length) {
			// Also check if any virtual column maps to the active sort column
			var thc = $("#"+this.options.table + ' > thead > tr > .th-' + sortcolumn + ' > .th-content');
			if(thc.length) {
				thc.addClass('th-sorted-' + this.options.sortorder);
			}
			// Highlight virtual column header if its sortColumn matches active sort
			this.options.columns.forEach(cd => {
				if (cd.virtual && cd.sortColumn) {
					const realCol = cd.sortColumn.replace('.', '\\.');
					if (realCol === sortcolumn) {
						const vthc = $("#"+this.options.table + ' > thead > tr > .th-' + cd.name.replace('.', '\\.') + ' > .th-content');
						if (vthc.length) vthc.addClass('th-sorted-' + this.options.sortorder);
					}
				}
			});
		}
	}
	
	showHeading() {
		$('#'+this.options.table + '-dth-icon-right').removeClass();
		$('#'+this.options.table + '-dth-icon-left').removeClass();
		$('#'+this.options.table + '-dth-icon-left').addClass(this.options.dthleft);
		if(this.hasFilter())
			$('#'+this.options.table + '-dth-icon-right').addClass('icon-filtered-data');
		$('#'+this.options.table + '-dth-text').html(this.options.heading);
	}
	
	selectRow(rowid, click=true) {
		if(this.hasData()) {
			if(rowid < 0 || !$("#"+this.options.table).find(`[data-id='${rowid}']`)) {
				rowid = this.options.data[0].id;
			}
			if(rowid > 0) {
				if(click)
					$("#"+this.options.table).find(`[data-id='${rowid}']`).click();
				else
					$("#"+this.options.table).find(`[data-id='${rowid}']`).addClass('tr-selected');
			}
		}
	}

	static normalizeColumns(columndata) {
		let init = [
			{name:'dt_ser_num', label:'#',width:{default:65},fixed:false,move:false},
			{name:'cb', label:'', width:{default:24}, fixed:true, type: 'cb', move:false}
		];
		if(columndata.length > 2) {
			if(columndata[0].hasOwnProperty('name') && columndata[0].name != 'dt_ser_num')
				columndata.unshift(init[0], init[1]);
		}
		return columndata;
	}
	
	setColumns(columndata) {
		columndata = DataTableView.normalizeColumns(columndata);
		this.setOption('columns', columndata);
		this.reload();
	}

	saveColumnWidth(column, width) {
		if(this.options.handlers['colwidth'])
			this.options.handlers.colwidth(column, width);
	}

	wrap(elem, content, props) {
		var inner = '';
		for(var propname in props) {
			if(props.hasOwnProperty(propname)) {
				inner += ' ' + propname + '="' + props[propname] + '"';
			}
		}
		return '<' + elem + inner + '>' + content + '</' + elem + '>';
	}

	getSelected(column) {
		var selected = [];
		var dtv = this;
		$('.tcb-' + dtv.options.table + ':checkbox:checked').each(function() {
			var rowid = $(this).attr('id').replace("tcb-" + dtv.options.table + "-did-", "");
			for(var i = 0; i < dtv.options.data.length; i++) {
				if(dtv.options.data[i].id == rowid)
					selected.push(dtv.options.data[i][column]);
			}
		});
		return selected;
	}

	getSelectedIds() {
		var selected = [];
		var dtv = this;
		$('.tcb-' + dtv.options.table + ':checkbox:checked').each(function() {
			var rowid = $(this).attr('id').replace("tcb-" + dtv.options.table + "-did-", "");
			selected.push(rowid);
		});
		return selected;
	}

	getHighlightedId() {
		return this.last_highlighted_rowid;
	}

	selectAll() {
		$(".tcb-" + this.options.table).prop('checked', true);
	}

	unSelectAll() {
		$(".tcb-" + this.options.table).prop('checked', false);
	}
	
	setHeight(height) {
		$('#'+this.options.table).parent().parent().css('height', height);
	}

	debounce(fn, delay) {
		let timer;
		return (...args) => {
			clearTimeout(timer);
			timer = setTimeout(() => fn.apply(this, args), delay);
		};
	}
	
	enableColumnResizing() {
		var table = document.getElementById(this.options.table);
		var row = table.getElementsByTagName('tr')[0],
			cols = row ? row.children : undefined;

		if (cols) {
			for (var i = 0; i < cols.length; i++) {
				if (!$(cols[i]).hasClass('noresize')) {
					var divider = cols[i].querySelector('.dtcol-divider');
					if (divider) {
						this.setColumnResizeListener(divider, cols[i]);
					}
				}
			}
		}
	}

	_updateStickyLeftPositions() {
		const tableEl = document.getElementById(this.options.table);
		if (!tableEl) return;
		let left   = 0;
		let visIdx = 0;
		for (let i = 0; i < this.options.columns.length; i++) {
			const cd = this.options.columns[i];
			if (typeof cd.hidden !== 'undefined' && cd.hidden) continue;
			if (!cd.sticky) break;
			const nth = visIdx + 1;
			$(`#${this.options.table} thead tr th:nth-child(${nth})`).css('left', left + 'px');
			$(`#${this.options.table} tbody tr td:nth-child(${nth})`).css('left', left + 'px');
			const cdw = typeof cd.width === 'object' ? (parseInt(cd.width.default) || 0) : (parseInt(cd.width) || 0);
			left += cdw;
			visIdx++;
		}
	}

	setColumnResizeListener(div, column) {
		var tablename = this.options.table;
		var columndata = this.options.columns;
		var startCursorX, originalWidth, newColumnWidth, columnName;
		var isResizing = false;

		$(div).on('mousedown', function(e) {
			startCursorX = e.pageX;
			originalWidth = $(column).outerWidth(true);
			columnName = $(column).attr('class').replace(" csortable", "").replace("th-", "").trim();
			isResizing = true;
			document.body.style.cursor = 'ew-resize';
			$('.datatable .th-sortable').css('cursor', 'ew-resize'); 
		});

		$(document).on('mouseup', function() {
			if (isResizing) {
				var colData = columndata.find(c => c.name === columnName);
				if (colData && colData.width.default !== "auto") {
					colData.width.default = newColumnWidth;
					DataTableView.getTable(tablename).saveColumnWidth(columnName, newColumnWidth);
					if (colData.sticky) {
						DataTableView.getTable(tablename)._updateStickyLeftPositions();
					}
				}
			}
			isResizing = false;
			document.body.style.cursor = '';
			$('.datatable .th-sortable').css('cursor', 'pointer'); 
		});

		$(document).on('mousemove', this.debounce((e) => {
			if (isResizing) {
				document.body.style.cursor = 'ew-resize';
				requestAnimationFrame(() => {
					let delta = (e.pageX - startCursorX);
					newColumnWidth = originalWidth + delta * 0.9;
					var colData = columndata.find(c => c.name === columnName);
					if (colData) {
						newColumnWidth = Math.max(newColumnWidth, colData.width.min || 50);
					}
					$(column).css("width", newColumnWidth + "px");
					
					var tableContainer = $(column).closest('.dtcard-body')[0]; 
					if ($(column).is(':last-child')) {
						let isAtRightEdge = tableContainer.scrollLeft + tableContainer.clientWidth >= tableContainer.scrollWidth;
						if (delta > 0 && isAtRightEdge) {
							tableContainer.scrollLeft += 5;
						}
					}
				});
			}
		}, 10));

		$(div).on('mouseover', function(e) {
			e.target.style.borderRight = '1px solid var(--datatable-row-vertical-border-color)';
		});

		$(div).on('mouseout', function(e) {
			e.target.style.borderRight = '';
		});
	}
}