Private/Get-DhJsTable.ps1
|
function Get-DhJsTable { # Auto-split fragment of the dashboard runtime JS (see Get-DhJsContent). return @' /* ========================================================================= TABLE ENGINE ========================================================================= */ function TableEngine(cfg) { this.id = cfg.id; this.title = cfg.title||cfg.id; this.allData = cfg.data.slice(); this.columns = cfg.columns; this.pageSize = cfg.pageSize||15; this.multiSelect = cfg.multiSelect||false; this.filterable = cfg.filterable!==false; this.pageable = cfg.pageable!==false; this.outLinks = cfg.outLinks||[]; this.charts = cfg.charts||[]; this.currentPage = 1; this.sortFields = []; /* [{field, dir}] — multi-sort */ this.filterText = ''; this.linkedFilter = null; this.selected = []; this.colVisible = {}; this.columns.forEach(function(col,i){this.colVisible[i]=true;},this); this.exportFileName = cfg.exportFileName || cfg.id; this._initPageSizes(); this._bindFilter(); this._bindClearSel(); this._bindLinkClear(); this._bindExport(); this._buildColToggle(); this._renderCharts(); this._bindRightClick(); this.render(); } /* ── Card filter bridge ─────────────────────────────────────────── */ TableEngine.prototype._applyCardFilters = function () { this.currentPage = 1; this.render(); this._refreshLinkedBarCharts(); this._renderCharts(); }; /* Re-render any bar charts and Mode-A pie charts that reference this table */ TableEngine.prototype._refreshLinkedBarCharts = function () { var id = this.id; BLOCKS_CONFIG.forEach(function (block) { if (block.blockType === 'barchart' && block.tableId === id) { var container = document.getElementById('block-' + block.id); if (container) renderBarChart(container, block); } else if (block.blockType === 'piechart' && block.mode === 'fromtable' && block.tableId === id) { var container = document.getElementById('block-' + block.id); if (container) renderPieChart(container, block); } }); }; /* ── Visible columns ────────────────────────────────────────────── */ TableEngine.prototype._visibleCols = function () { var self=this; return this.columns.filter(function(col,i){return self.colVisible[i]!==false;}); }; /* ── Page size ──────────────────────────────────────────────────── */ TableEngine.prototype._initPageSizes = function () { var sel=document.getElementById('pagesize-'+this.id); if (!sel) return; var self=this; [5,10,15,25,50,100].forEach(function(n){ var o=document.createElement('option'); o.value=n;o.textContent=n; if(n===self.pageSize)o.selected=true; sel.appendChild(o); }); sel.addEventListener('change',function(){self.pageSize=parseInt(sel.value,10);self.currentPage=1;self.render();}); if (!this.filterable){var fw=document.getElementById('filter-wrap-'+this.id);if(fw)fw.style.display='none';} }; /* ── Filter ─────────────────────────────────────────────────────── */ TableEngine.prototype._bindFilter = function () { var inp=document.getElementById('filter-'+this.id); var clr=document.getElementById('filter-clear-'+this.id); if (!inp) return; var self=this; inp.addEventListener('input',function(){ self.filterText=inp.value;self.currentPage=1; clr.style.display=inp.value?'flex':'none'; self.render();self._refreshLinkedBarCharts();self._renderCharts();URLState.save(); }); clr.addEventListener('click',function(){ inp.value='';self.filterText='';clr.style.display='none'; self.currentPage=1;self.render();self._refreshLinkedBarCharts();self._renderCharts();URLState.save(); }); /* Restore from URL */ var saved=URLState.load(); if (saved.filters&&saved.filters[this.id]) { inp.value=saved.filters[this.id]; self.filterText=inp.value; clr.style.display='flex'; } }; /* ── Clear selection ────────────────────────────────────────────── */ TableEngine.prototype._bindClearSel = function () { var btn=document.getElementById('clear-sel-'+this.id); if (!btn) return; var self=this; btn.addEventListener('click',function(){self.selected=[];self._notifyLinks(null);self.render();}); }; /* ── Link clear ─────────────────────────────────────────────────── */ TableEngine.prototype._bindLinkClear = function () { var btn=document.getElementById('link-clear-'+this.id); if (!btn) return; var self=this; btn.addEventListener('click',function(){ self.linkedFilter=null;self.currentPage=1;self.selected=[]; var badge=document.getElementById('link-badge-'+self.id); if(badge)badge.style.display='none'; self.render(); }); }; /* ── Export bindings ────────────────────────────────────────────── */ TableEngine.prototype._bindExport = function () { var self=this; var csv=document.getElementById('exp-csv-'+this.id); var xlsx=document.getElementById('exp-xlsx-'+this.id); var pdf=document.getElementById('exp-pdf-'+this.id); if(csv) csv.addEventListener('click',function(){self.exportCsv();}); if(xlsx)xlsx.addEventListener('click',function(){self.exportExcel();}); if(pdf) pdf.addEventListener('click',function(){self.exportPdf();}); }; /* ── Right-click copy ───────────────────────────────────────────── */ TableEngine.prototype._bindRightClick = function () { var tbody = document.getElementById('tbody-' + this.id); if (!tbody) return; var self = this; /* Custom context menu element — created once and reused */ var ctxMenu = document.createElement('div'); ctxMenu.className = 'ctx-menu'; ctxMenu.style.display = 'none'; ctxMenu.innerHTML = '<div class="ctx-item" id="ctx-copy-cell">📋 Copy cell value</div>' + '<div class="ctx-item" id="ctx-copy-csv">💾 Copy table as CSV</div>'; document.body.appendChild(ctxMenu); var targetTd = null; function closeMenu() { ctxMenu.style.display = 'none'; targetTd = null; } document.addEventListener('click', closeMenu); document.addEventListener('keydown', function(e){ if(e.key==='Escape') closeMenu(); }); tbody.addEventListener('contextmenu', function(e) { var td = e.target.closest('td'); if (!td) return; targetTd = td; ctxMenu.style.cssText = 'display:block;position:fixed;top:'+e.clientY+'px;left:'+e.clientX+'px;z-index:9999;'; e.preventDefault(); }); /* Copy single cell */ ctxMenu.querySelector('#ctx-copy-cell').addEventListener('click', function() { if (!targetTd) return; var text = (targetTd.textContent || targetTd.innerText || '').trim(); if (text && navigator.clipboard) { navigator.clipboard.writeText(text).then(function() { targetTd.classList.add('cell-copied'); setTimeout(function(){ targetTd.classList.remove('cell-copied'); }, 600); }); } closeMenu(); }); /* Copy entire filtered table as CSV */ ctxMenu.querySelector('#ctx-copy-csv').addEventListener('click', function() { var r = self._exportRows(); var BOM = ''; var lines = [r.cols.map(function(c){ return '"'+c.label.replace(/"/g,'""')+'"'; }).join(',')]; r.rows.forEach(function(row){ lines.push(r.cols.map(function(c){ var v = FMT.forExport(c, row[c.field]); v = (v !== null && v !== undefined) ? String(v) : ''; return '"'+v.replace(/"/g,'""')+'"'; }).join(',')); }); var csv = lines.join('\r\n'); if (navigator.clipboard) { navigator.clipboard.writeText(csv).then(function(){ /* Brief visual feedback on the table */ var tbl = document.getElementById('tbl-'+self.id); if (tbl) { tbl.classList.add('table-copied'); setTimeout(function(){ tbl.classList.remove('table-copied'); }, 800); } }); } closeMenu(); }); }; /* ── Column visibility toggle ───────────────────────────────────── */ TableEngine.prototype._buildColToggle = function () { var btn=document.getElementById('btn-col-toggle-'+this.id); var dd=document.getElementById('col-toggle-dd-'+this.id); if(!btn||!dd) return; var self=this; this.columns.forEach(function(col,i){ var row=document.createElement('label'); row.className='col-toggle-item'; var chk=document.createElement('input'); chk.type='checkbox';chk.checked=true; chk.addEventListener('change',function(){self.colVisible[i]=chk.checked;self.render();}); row.appendChild(chk); row.appendChild(document.createTextNode(' '+col.label)); dd.appendChild(row); }); btn.addEventListener('click',function(e){e.stopPropagation();dd.style.display=dd.style.display==='none'?'block':'none';}); document.addEventListener('click',function(){dd.style.display='none';}); }; /* ── Linked-filter API ──────────────────────────────────────────── */ TableEngine.prototype.applyLinkedFilter = function (field,value,masterTitle) { this.linkedFilter=(value!==null)?{field:field,value:String(value),masterTitle:masterTitle}:null; this.currentPage=1;this.selected=[]; this._updateLinkBadge();this.render(); }; TableEngine.prototype._updateLinkBadge = function () { var badge=document.getElementById('link-badge-'+this.id); var text=document.getElementById('link-text-'+this.id); if(!badge)return; if(this.linkedFilter){ badge.style.display='flex'; text.textContent='Filtered by \u201C'+(this.linkedFilter.masterTitle||'master')+ '\u201D \u2192 '+this.linkedFilter.field+' = '+this.linkedFilter.value; } else {badge.style.display='none';} }; /* ── Data pipeline — multi-sort + card filters ──────────────────── */ TableEngine.prototype._getFiltered = function () { var data=this.allData;var self=this; if (self.linkedFilter) { var lf=self.linkedFilter; data=data.filter(function(row){return String(row[lf.field])===lf.value;}); } /* Card filters */ Object.keys(cardFilters).forEach(function(filterId) { var cf=cardFilters[filterId]; if (cf.values&&cf.values.length) { data=data.filter(function(row){ return cf.values.some(function(v){return String(row[cf.field])===v;}); }); } }); /* Global page-level filters (v1.9.0) — only rows matching every active facet / date-range whose field this table has. */ if (typeof GlobalFilter !== 'undefined' && GlobalFilter.active()) { data=data.filter(function(row){ return GlobalFilter.matchRow(row); }); } if (self.filterText) { var term=self.filterText.toLowerCase(); data=data.filter(function(row){ return self._visibleCols().some(function(col){ var v=row[col.field];if(v===null||v===undefined)return false; return String(v).toLowerCase().indexOf(term)!==-1|| FMT.apply(col,v).toLowerCase().indexOf(term)!==-1; }); }); } /* Multi-sort: sortFields is [{field,dir},...] */ if (self.sortFields&&self.sortFields.length) { /* Pre-build a field → column map so we can spot sparkline columns without scanning self.columns inside the hot comparator loop. */ var colMap = {}; (self.columns||[]).forEach(function(c){ colMap[c.field] = c; }); data=data.slice().sort(function(a,b){ for (var i=0;i<self.sortFields.length;i++) { var sf=self.sortFields[i].field,sd=self.sortFields[i].dir; var col=colMap[sf]; var av=(a[sf]!=null)?a[sf]:'',bv=(b[sf]!=null)?b[sf]:''; /* v1.4 F8 — sparkline columns reduce arrays to a comparable scalar */ if (col && col.cellType === 'sparkline') { av = sparkReduce(av, col.sortBy); bv = sparkReduce(bv, col.sortBy); } var an=parseFloat(av),bn=parseFloat(bv); var n=(!isNaN(an)&&!isNaN(bn))?(an-bn):String(av).localeCompare(String(bv),undefined,{sensitivity:'base'}); if(n!==0)return sd==='asc'?n:-n; } return 0; }); } return data; }; /* ── Cell rendering ─────────────────────────────────────────────── */ TableEngine.prototype._renderCell = function (col,rawVal) { var td=document.createElement('td'); var cellType=(col.cellType||'text').toLowerCase(); var isSparklineCell = (cellType === 'sparkline'); var strRaw=(rawVal!==null&&rawVal!==undefined)?String(rawVal):''; /* Skip FMT.apply + threshold matching for sparkline cells — the value is an array, not a scalar, and those code paths would mis-parse it. */ var dispVal = isSparklineCell ? '' : (FMT.apply(col,rawVal) || strRaw); var thClass = isSparklineCell ? '' : getThresholdClass(col,rawVal); var align=(col.align||'left').toLowerCase(); if(align==='right') td.classList.add('cell-right'); if(align==='center')td.classList.add('cell-center'); if(thClass)td.classList.add(thClass); if(col.bold) td.classList.add('cell-bold'); if(col.italic)td.classList.add('cell-italic'); if(col.font){var fm={mono:'cell-mono',ui:'cell-ui',display:'cell-display'};var fc=fm[col.font.toLowerCase()];if(fc)td.classList.add(fc);} if(cellType==='progressbar'){ var numVal=parseFloat(strRaw),pMax=col.progressMax||100; var pct=isNaN(numVal)?0:Math.min(100,Math.max(0,(numVal/pMax)*100)); var fillCls=thClass?thClass.replace('cell-','fill-'):'fill-default'; td.classList.add('td-progress'); td.innerHTML='<div class="progress-wrap"><span class="progress-label">'+dispVal+'</span>'+ '<div class="progress-track"><div class="progress-fill '+fillCls+'" style="width:'+pct.toFixed(1)+'%"></div></div></div>'; } else if(cellType==='badge'){ td.classList.add('td-badge'); var badge=document.createElement('span'); badge.className='cell-badge'+(thClass?' badge-'+thClass.replace('cell-',''):''); badge.textContent=dispVal;td.appendChild(badge); } else if(cellType==='sparkline'){ /* v1.4 F8 — inline mini-chart per row. Cell value must be a numeric array; null/non-array values render as an em dash. Reuses the SPARK helper shared with Add-DhSummary tile sparklines. */ td.classList.add('td-sparkline'); var arr = Array.isArray(rawVal) ? rawVal : null; if (arr && arr.length) { td.innerHTML = SPARK.build(arr, { style: col.sparklineStyle || 'line', width: 100, height: 22 }); } else { td.innerHTML = '<span class="td-sparkline-empty">—</span>'; } } else { td.textContent=dispVal; } /* Pin first column */ if(col.pinFirst){td.classList.add('col-pinned');} return td; }; /* ── Render ─────────────────────────────────────────────────────── */ TableEngine.prototype.render = function(){ this._renderHead();this._renderBody();this._renderFoot();this._renderPaging(); this._renderInfo();this._renderSelClearBtn(); }; /* Aggregate footer row — only rendered when any column has an 'aggregate' property */ TableEngine.prototype._renderFoot = function() { var self = this; var visCols = this._visibleCols(); var hasFoot = visCols.some(function(col){ return col.aggregate; }); var tbl = document.getElementById('tbl-' + this.id); if (!tbl) return; /* Remove existing tfoot */ var oldFoot = tbl.querySelector('tfoot'); if (oldFoot) oldFoot.remove(); if (!hasFoot) return; var filtered = this._getFiltered(); var tfoot = document.createElement('tfoot'); var tr = document.createElement('tr'); tr.className = 'tfoot-row'; if (this.multiSelect) { var tdBlank = document.createElement('td'); tdBlank.className = 'col-select'; tr.appendChild(tdBlank); } visCols.forEach(function(col) { var td = document.createElement('td'); td.className = 'tfoot-cell'; var align = (col.align || 'left').toLowerCase(); if (align === 'right') td.classList.add('cell-right'); if (align === 'center') td.classList.add('cell-center'); if (col.pinFirst) td.classList.add('col-pinned'); var agg = (col.aggregate || '').toLowerCase(); if (!agg) { tr.appendChild(td); return; } var nums = filtered.map(function(row){ var v = parseFloat(row[col.field]); return isNaN(v) ? null : v; }).filter(function(v){ return v !== null; }); var result = null; switch (agg) { case 'sum': result = nums.reduce(function(a,b){return a+b;}, 0); break; case 'avg': result = nums.length ? nums.reduce(function(a,b){return a+b;},0)/nums.length : null; break; case 'min': result = nums.length ? Math.min.apply(null, nums) : null; break; case 'max': result = nums.length ? Math.max.apply(null, nums) : null; break; case 'count': result = filtered.length; break; } if (result !== null) { var displayVal = FMT.apply(col, result) || String(result); td.textContent = displayVal; td.title = agg.charAt(0).toUpperCase()+agg.slice(1)+': '+displayVal; } tr.appendChild(td); }); tfoot.appendChild(tr); tbl.appendChild(tfoot); }; TableEngine.prototype._renderHead = function(){ var tr=document.getElementById('thead-'+this.id); if(!tr)return; var self=this;tr.innerHTML=''; var visCols=this._visibleCols(); if(this.multiSelect){ var th=document.createElement('th');th.className='col-select'; th.innerHTML='<input type="checkbox" id="chk-all-'+this.id+'">'; th.querySelector('input').addEventListener('change',function(e){ var checked=e.target.checked,filtered=self._getFiltered(); var start=(self.currentPage-1)*self.pageSize; filtered.slice(start,start+self.pageSize).forEach(function(row){ var idx=self.allData.indexOf(row); if(checked){if(self.selected.indexOf(idx)===-1)self.selected.push(idx);} else{self.selected=self.selected.filter(function(i){return i!==idx;});} });self.render(); }); tr.appendChild(th); } visCols.forEach(function(col,ci){ var th=document.createElement('th'); th.dataset.field=col.field; th.setAttribute('scope','col'); /* a11y: column-header association */ if(col.width)th.style.width=col.width; if(col.pinFirst)th.classList.add('col-pinned'); var align=(col.align||'left').toLowerCase(); if(align==='right') th.classList.add('th-right'); if(align==='center')th.classList.add('th-center'); var lbl=document.createElement('span');lbl.className='col-label';lbl.textContent=col.label; th.appendChild(lbl); if(col.sortable!==false){ th.classList.add('sortable'); /* a11y: keyboard-operable sort control + screen-reader sort state */ th.tabIndex=0; th.setAttribute('role','columnheader'); var icon=document.createElement('span');icon.className='sort-icon'; /* Show sort position for multi-sort */ var sortPos=self.sortFields.findIndex(function(sf){return sf.field===col.field;}); if(sortPos>-1){ icon.classList.add('sort-'+self.sortFields[sortPos].dir); th.classList.add('sorted'); th.setAttribute('aria-sort',self.sortFields[sortPos].dir==='asc'?'ascending':'descending'); if(self.sortFields.length>1){ var pos=document.createElement('span'); pos.className='sort-pos';pos.textContent=sortPos+1; th.appendChild(pos); } } else { th.setAttribute('aria-sort','none'); } th.appendChild(icon); var doSort=function(shift){ var existIdx=self.sortFields.findIndex(function(sf){return sf.field===col.field;}); if(shift&&self.sortFields.length>0){ /* Multi-sort: add or toggle */ if(existIdx>-1){ if(self.sortFields[existIdx].dir==='asc') self.sortFields[existIdx].dir='desc'; else self.sortFields.splice(existIdx,1); } else { self.sortFields.push({field:col.field,dir:'asc'}); } } else { /* Single sort */ if(existIdx>-1&&self.sortFields.length===1){ self.sortFields[0].dir=self.sortFields[0].dir==='asc'?'desc':'asc'; } else { self.sortFields=[{field:col.field,dir:'asc'}]; } } self.currentPage=1;self.render(); }; th.addEventListener('click',function(e){ doSort(e.shiftKey); }); th.addEventListener('keydown',function(e){ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); doSort(e.shiftKey); } }); } tr.appendChild(th); }); }; /* Build one <tr> for a data row. Extracted from _renderBody so both the paginated path and the virtualised (non-pageable) path produce identical rows. globalIdx is the row's index in allData (used for selection/links). */ TableEngine.prototype._buildRow = function(row, globalIdx){ var self=this; var visCols=this._visibleCols(); var tr=document.createElement('tr'); tr.dataset.idx=globalIdx; /* Row highlight */ this.columns.forEach(function(col){ if(col.rowHighlight){ var cls=getThresholdClass(col,row[col.field]); if(cls)tr.classList.add('row-hl-'+cls.replace('cell-','')); } }); if(this.selected.indexOf(globalIdx)!==-1)tr.classList.add('row-selected'); tr.addEventListener('click',function(e){ if(e.target.type==='checkbox')return; self._handleRowClick(globalIdx,row,tr); }); if(this.multiSelect){ var tdChk=document.createElement('td');tdChk.className='col-select'; var chk=document.createElement('input');chk.type='checkbox'; chk.checked=this.selected.indexOf(globalIdx)!==-1; chk.addEventListener('change',function(){ if(chk.checked){if(self.selected.indexOf(globalIdx)===-1)self.selected.push(globalIdx);} else{self.selected=self.selected.filter(function(i){return i!==globalIdx;});} self._syncRowClass(tr,globalIdx);self._renderInfo();self._renderSelClearBtn(); }); tdChk.appendChild(chk);tr.appendChild(tdChk); } visCols.forEach(function(col){tr.appendChild(self._renderCell(col,row[col.field]));}); return tr; }; /* v1.7.0 — row virtualisation threshold. Only the non-pageable path (which now shows ALL rows) windows the DOM; below this count it renders plainly. */ var VIRTUALISE_ABOVE = 150; TableEngine.prototype._renderBody = function(){ var tbody=document.getElementById('tbody-'+this.id); if(!tbody)return; var filtered=this._getFiltered(); var self=this;var visCols=this._visibleCols(); var colSpan=visCols.length+(this.multiSelect?1:0); /* Update nav badge with current filtered count */ var navBadge=document.querySelector('.nav-badge[data-table="'+this.id+'"]'); if(navBadge) navBadge.textContent=filtered.length; updateNavBadge(this.id); if(filtered.length===0){ this._teardownVirtual(); tbody.innerHTML=''; var tr=document.createElement('tr');var td=document.createElement('td'); td.colSpan=colSpan;td.className='no-data'; td.textContent='No matching records found.';tr.appendChild(td);tbody.appendChild(tr);return; } /* Paginated path (default) — unchanged: render only the current page. */ if(this.pageable){ this._teardownVirtual(); tbody.innerHTML=''; var start=(this.currentPage-1)*this.pageSize; filtered.slice(start,start+this.pageSize).forEach(function(row){ tbody.appendChild(self._buildRow(row, self.allData.indexOf(row))); }); return; } /* Non-pageable path — render ALL rows. Small tables render plainly; large ones are virtualised so the DOM only holds the visible window. (Before v1.7.0 this path incorrectly showed only pageSize rows.) */ if(filtered.length<=VIRTUALISE_ABOVE){ this._teardownVirtual(); tbody.innerHTML=''; filtered.forEach(function(row){ tbody.appendChild(self._buildRow(row, self.allData.indexOf(row))); }); return; } this._renderVirtual(filtered, colSpan); }; /* Virtualised body for a large non-pageable table. Renders only the rows near the scroll viewport, padded by top/bottom spacer <tr>s sized to preserve the scrollbar geometry. Row height is measured once from a real row. */ TableEngine.prototype._renderVirtual = function(filtered, colSpan){ var self=this; var tbody=document.getElementById('tbody-'+this.id); var wrapper=document.getElementById('tbl-'+this.id).closest('.table-wrapper'); if(!wrapper){ /* no scroll container — fall back to rendering all */ tbody.innerHTML=''; filtered.forEach(function(row){ tbody.appendChild(self._buildRow(row, self.allData.indexOf(row))); }); return; } wrapper.classList.add('table-virtual'); /* Measure a representative row height once. */ tbody.innerHTML=''; var probe=this._buildRow(filtered[0], this.allData.indexOf(filtered[0])); tbody.appendChild(probe); var rowH=probe.getBoundingClientRect().height||34; function spacer(){ var tr=document.createElement('tr');tr.className='virt-spacer'; var td=document.createElement('td');td.colSpan=colSpan; tr.appendChild(td);return tr; } var topSpacer=spacer(), botSpacer=spacer(); function renderWindow(){ var scrollTop=wrapper.scrollTop; var vpH=wrapper.clientHeight||400; var buffer=8; var first=Math.max(0, Math.floor(scrollTop/rowH)-buffer); var count=Math.ceil(vpH/rowH)+buffer*2; var last=Math.min(filtered.length, first+count); topSpacer.firstChild.style.height=(first*rowH)+'px'; botSpacer.firstChild.style.height=((filtered.length-last)*rowH)+'px'; tbody.innerHTML=''; tbody.appendChild(topSpacer); for(var i=first;i<last;i++){ tbody.appendChild(self._buildRow(filtered[i], self.allData.indexOf(filtered[i]))); } tbody.appendChild(botSpacer); } var ticking=false; this._teardownVirtual(); /* clear any previous handler before wiring a new one */ this._virtScrollHandler=function(){ if(ticking)return; ticking=true; requestAnimationFrame(function(){ renderWindow(); ticking=false; }); }; this._virtWrapper=wrapper; wrapper.addEventListener('scroll', this._virtScrollHandler); renderWindow(); }; TableEngine.prototype._teardownVirtual = function(){ if(this._virtWrapper && this._virtScrollHandler){ this._virtWrapper.removeEventListener('scroll', this._virtScrollHandler); this._virtWrapper.classList.remove('table-virtual'); } this._virtWrapper=null; this._virtScrollHandler=null; }; TableEngine.prototype._handleRowClick=function(globalIdx,row,tr){ if(this.multiSelect){ var pos=this.selected.indexOf(globalIdx); if(pos===-1)this.selected.push(globalIdx);else this.selected.splice(pos,1); this._syncRowClass(tr,globalIdx);this._renderInfo();this._renderSelClearBtn(); this._notifyLinks(this.selected.length>0?this.allData[this.selected[0]]:null); } else { if(this.selected.length===1&&this.selected[0]===globalIdx){this.selected=[];this._notifyLinks(null);} else{this.selected=[globalIdx];this._notifyLinks(row);} this.render(); } }; TableEngine.prototype._notifyLinks=function(masterRow){ var self=this; this.outLinks.forEach(function(link){ var det=engines[link.detailTableId];if(!det)return; det.applyLinkedFilter(link.detailField,masterRow!==null?masterRow[link.masterField]:null,self.title); }); }; /* After linked filter is applied, refresh bar charts referencing the detail table */ TableEngine.prototype.applyLinkedFilter = (function(_orig){ return function(field, value, masterTitle) { _orig.call(this, field, value, masterTitle); this._refreshLinkedBarCharts(); }; })(TableEngine.prototype.applyLinkedFilter); TableEngine.prototype._syncRowClass=function(tr,globalIdx){ if(this.selected.indexOf(globalIdx)!==-1)tr.classList.add('row-selected'); else tr.classList.remove('row-selected'); }; /* ── Pagination ─────────────────────────────────────────────────── */ TableEngine.prototype._renderPaging=function(){ var container=document.getElementById('paging-'+this.id); if(!container||!this.pageable)return; var filtered=this._getFiltered(); var totalPages=Math.max(1,Math.ceil(filtered.length/this.pageSize)); var self=this;container.innerHTML='';if(totalPages<=1)return; function mkBtn(label,page,disabled,active){ var b=document.createElement('button'); b.className='page-btn'+(active?' page-active':''); b.textContent=label;b.disabled=disabled; b.addEventListener('click',function(){if(!disabled){self.currentPage=page;self.render();}}); return b; } container.appendChild(mkBtn('\u00AB',1,self.currentPage===1,false)); container.appendChild(mkBtn('\u2039',self.currentPage-1,self.currentPage===1,false)); var start=Math.max(1,self.currentPage-2),end=Math.min(totalPages,start+4); start=Math.max(1,end-4); if(start>1){container.appendChild(mkBtn('1',1,false,false));if(start>2){var e1=document.createElement('span');e1.className='page-ellipsis';e1.textContent='\u2026';container.appendChild(e1);}} for(var p=start;p<=end;p++)container.appendChild(mkBtn(String(p),p,false,p===self.currentPage)); if(end<totalPages){if(end<totalPages-1){var e2=document.createElement('span');e2.className='page-ellipsis';e2.textContent='\u2026';container.appendChild(e2);}container.appendChild(mkBtn(String(totalPages),totalPages,false,false));} container.appendChild(mkBtn('\u203A',self.currentPage+1,self.currentPage===totalPages,false)); container.appendChild(mkBtn('\u00BB',totalPages,self.currentPage===totalPages,false)); }; TableEngine.prototype._renderInfo=function(){ var el=document.getElementById('info-'+this.id);if(!el)return; var filtered=this._getFiltered(); var total=this.allData.length,shown=filtered.length; var start=(this.currentPage-1)*this.pageSize+1; var end=Math.min(start+this.pageSize-1,shown); var text=shown<total?(start+'\u2013'+end+' of '+shown+' (filtered from '+total+')'):(start+'\u2013'+end+' of '+total); if(this.selected.length>0)text+=' \u2014 '+this.selected.length+' selected'; el.textContent=text; }; TableEngine.prototype._renderSelClearBtn=function(){ var btn=document.getElementById('clear-sel-'+this.id); if(btn)btn.style.display=this.selected.length>0?'inline-flex':'none'; }; /* ── Per-table Pie Charts (Add-DhTable -Charts) ───────────────────── Delegates SVG generation to the shared chartPalette() / buildPieSvg() helpers so the standalone Add-DhPieChart and the per-table chart stay visually consistent. */ TableEngine.prototype._chartPalette = function () { return chartPalette(); }; TableEngine.prototype._renderCharts = function () { if (!this.charts || !this.charts.length) return; var container = document.getElementById('charts-' + this.id); if (!container) return; container.innerHTML = ''; var self = this; this.charts.forEach(function (chart) { if ((chart.type || 'pie').toLowerCase() === 'pie') { container.appendChild(self._buildPieChart(chart)); } }); }; TableEngine.prototype._buildPieChart = function (chart) { var palette = chartPalette(); var counts = {}; this._getFiltered().forEach(function (row) { var v = (row[chart.field] != null) ? String(row[chart.field]) : '(empty)'; counts[v] = (counts[v] || 0) + 1; }); var entries = Object.keys(counts).map(function (k, i) { return { label: k, count: counts[k], color: palette[i % palette.length] }; }).sort(function (a, b) { return b.count - a.count; }); return buildPieSvg(entries, { style: 'donut', title: chart.title || chart.field, showTotal: true, showLegend: true }); }; /* ── Export ─────────────────────────────────────────────────────── */ TableEngine.prototype._triggerDownload=function(url,filename){ var a=document.createElement('a');a.href=url;a.download=filename; document.body.appendChild(a);a.click();document.body.removeChild(a); if(url.startsWith('blob:'))setTimeout(function(){URL.revokeObjectURL(url);},2000); }; TableEngine.prototype._exportRows=function(){return{cols:this._visibleCols(),rows:this._getFiltered()};}; TableEngine.prototype.exportCsv=function(){ var r=this._exportRows(),BOM='\uFEFF'; var lines=[r.cols.map(function(c){return'"'+c.label.replace(/"/g,'""')+'"';}).join(',')]; r.rows.forEach(function(row){lines.push(r.cols.map(function(c){var v=FMT.forExport(c,row[c.field]);v=(v!==null&&v!==undefined)?String(v):'';return'"'+v.replace(/"/g,'""')+'"';}).join(','));}); this._triggerDownload(URL.createObjectURL(new Blob([BOM+lines.join('\r\n')],{type:'text/csv;charset=utf-8;'})),this.id+'.csv'); }; TableEngine.prototype.exportExcel=function(){ if(typeof XLSX==='undefined'){alert('SheetJS did not load. Internet required for XLSX export.');return;} var r=this._exportRows(); var aoa=[r.cols.map(function(c){return c.label;})]; r.rows.forEach(function(row){aoa.push(r.cols.map(function(c){ var raw=row[c.field],fmt=(c.format||'').toLowerCase(); if(fmt==='number'||fmt==='currency'||fmt==='percent'){var n=parseFloat(raw);return isNaN(n)?FMT.apply(c,raw):n;} return FMT.forExport(c,raw); }));}); var ws=XLSX.utils.aoa_to_sheet(aoa); ws['!cols']=r.cols.map(function(c){var max=c.label.length;r.rows.forEach(function(row){var v=String(FMT.apply(c,row[c.field])||'');if(v.length>max)max=v.length;});return{wch:Math.min(max+2,60)};}); ws['!freeze']={xSplit:0,ySplit:1}; var wb=XLSX.utils.book_new();XLSX.utils.book_append_sheet(wb,ws,this.title.substring(0,31)); XLSX.writeFile(wb,this.exportFileName+'.xlsx'); }; TableEngine.prototype.exportPdf=function(){ var JsPDF=(typeof window.jsPDF!=='undefined')?window.jsPDF:(typeof jspdf!=='undefined'&&jspdf.jsPDF)?jspdf.jsPDF:null; if(!JsPDF){alert('jsPDF did not load.\nInternet access required (cdnjs.cloudflare.com).');return;} var _test=new JsPDF(); if(typeof _test.autoTable!=='function'){alert('jsPDF-AutoTable did not load.\nInternet access required (cdnjs.cloudflare.com).\nTry reloading the page.');return;} var r=this._exportRows(); var orient=(r.cols.length>6||r.rows.length>30)?'landscape':'portrait'; var doc=new JsPDF({orientation:orient,unit:'mm',format:'a4'}); doc.setFontSize(15);doc.setTextColor(0,119,204);doc.text(this.title,14,16); doc.setFontSize(8);doc.setTextColor(100,110,130);doc.text('Rows: '+r.rows.length+' | '+new Date().toLocaleString(),14,22); var colStyles={}; r.cols.forEach(function(col,i){var align=(col.align||'left').toLowerCase();if(align==='right'||align==='center')colStyles[i]={halign:align};}); doc.autoTable({head:[r.cols.map(function(c){return c.label;})], body:r.rows.map(function(row){return r.cols.map(function(c){var v=FMT.apply(c,row[c.field]);return(v!==null&&v!==undefined)?String(v):'';});}), startY:27,columnStyles:colStyles, styles:{fontSize:8,cellPadding:2,overflow:'ellipsize',font:'helvetica'}, headStyles:{fillColor:[0,77,153],textColor:255,fontStyle:'bold',halign:'left'}, alternateRowStyles:{fillColor:[240,245,252]},margin:{left:14,right:14}}); var pageCount=doc.internal.getNumberOfPages(); for(var i=1;i<=pageCount;i++){doc.setPage(i);doc.setFontSize(7);doc.setTextColor(160,160,180);doc.text('Page '+i+' of '+pageCount,doc.internal.pageSize.getWidth()-14,doc.internal.pageSize.getHeight()-6,{align:'right'});} doc.save(this.exportFileName+'.pdf'); }; '@ } |