@toolbox-web/grid 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all.js +1 -1
- package/all.js.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/lib/features/registry.js +1 -1
- package/lib/plugins/clipboard/index.js +1 -1
- package/lib/plugins/column-virtualization/index.js +1 -1
- package/lib/plugins/context-menu/index.js +1 -1
- package/lib/plugins/editing/index.js +1 -1
- package/lib/plugins/editing/index.js.map +1 -1
- package/lib/plugins/export/index.js +1 -1
- package/lib/plugins/filtering/index.js +1 -1
- package/lib/plugins/filtering/index.js.map +1 -1
- package/lib/plugins/grouping-columns/index.js +1 -1
- package/lib/plugins/grouping-rows/index.js +1 -1
- package/lib/plugins/master-detail/index.js +1 -1
- package/lib/plugins/multi-sort/index.js +1 -1
- package/lib/plugins/pinned-columns/index.js +1 -1
- package/lib/plugins/pinned-rows/index.js +1 -1
- package/lib/plugins/pivot/index.js +1 -1
- package/lib/plugins/print/index.js +1 -1
- package/lib/plugins/print/index.js.map +1 -1
- package/lib/plugins/reorder-columns/index.js +1 -1
- package/lib/plugins/responsive/index.js +1 -1
- package/lib/plugins/responsive/index.js.map +1 -1
- package/lib/plugins/row-drag-drop/index.js +1 -1
- package/lib/plugins/selection/index.js +1 -1
- package/lib/plugins/selection/index.js.map +1 -1
- package/lib/plugins/server-side/index.js +1 -1
- package/lib/plugins/shell/index.js +1 -1
- package/lib/plugins/sticky-rows/index.js +1 -1
- package/lib/plugins/tooltip/index.js +1 -1
- package/lib/plugins/tooltip/index.js.map +1 -1
- package/lib/plugins/tree/index.js +1 -1
- package/lib/plugins/undo-redo/index.js +1 -1
- package/lib/plugins/visibility/index.js +1 -1
- package/package.json +1 -1
- package/umd/grid.all.umd.js +1 -1
- package/umd/grid.all.umd.js.map +1 -1
- package/umd/grid.umd.js +1 -1
- package/umd/grid.umd.js.map +1 -1
- package/umd/plugins/editing.umd.js +1 -1
- package/umd/plugins/editing.umd.js.map +1 -1
- package/umd/plugins/filtering.umd.js +1 -1
- package/umd/plugins/filtering.umd.js.map +1 -1
- package/umd/plugins/print.umd.js +1 -1
- package/umd/plugins/print.umd.js.map +1 -1
- package/umd/plugins/responsive.umd.js +1 -1
- package/umd/plugins/responsive.umd.js.map +1 -1
- package/umd/plugins/selection.umd.js +1 -1
- package/umd/plugins/selection.umd.js.map +1 -1
- package/umd/plugins/tooltip.umd.js +1 -1
- package/umd/plugins/tooltip.umd.js.map +1 -1
package/umd/plugins/print.umd.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports,require("../../core/internal/diagnostics"),require("../../core/plugin/base-plugin")):"function"==typeof define&&define.amd?define(["exports","../../core/internal/diagnostics","../../core/plugin/base-plugin"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).TbwGridPlugin_print={},t.TbwGrid,t.TbwGrid)}(this,function(t,i,e){Object.defineProperty(t,Symbol.toStringTag,{value:"Module"});var n="tbw-print-isolation-style";async function r(t,e={}){const{orientation:r="landscape"}=e,o=t.id;document.querySelectorAll(`#${CSS.escape(o)}`).length>1&&(0,i.warnDiagnostic)(i.PRINT_DUPLICATE_ID,`Multiple elements found with id="${o}". Print isolation may not work correctly. Ensure each grid has a unique ID.`,o,"print"),document.getElementById(n)?.remove();const a=function(t,i){const e=document.createElement("style");return e.id=n,e.textContent=`\n /* Print isolation: hide everything except the target grid */\n @media print {\n /* Hide all body children by default */\n body > *:not(#${t}) {\n display: none !important;\n }\n\n /* But show the grid and ensure it's not hidden by ancestor rules */\n #${t} {\n display: block !important;\n position: static !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n width: 100% !important;\n max-height: none !important;\n margin: 0 !important;\n padding: 0 !important;\n transform: none !important;\n }\n\n /* If grid is nested, we need to show its ancestors too */\n #${t},\n #${t} * {\n visibility: visible !important;\n }\n\n /* Walk up the DOM and show all ancestors of the grid */\n body *:has(> #${t}),\n body *:has(#${t}) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n position: static !important;\n transform: none !important;\n background: transparent !important;\n border: none !important;\n padding: 0 !important;\n margin: 0 !important;\n }\n\n /* Hide siblings of ancestors (everything that's not in the path to the grid) */\n body *:has(#${t}) > *:not(:has(#${t})):not(#${t}) {\n display: none !important;\n }\n\n /* Page settings */\n @page {\n size: ${i};\n margin: 1cm;\n }\n\n /* Ensure proper print styling */\n body {\n margin: 0 !important;\n padding: 0 !important;\n background: white !important;\n color-scheme: light !important;\n }\n }\n\n /* Screen: also apply isolation for print preview */\n @media screen {\n /* When this stylesheet is active, we're about to print */\n /* No screen-specific rules needed - isolation only applies to print */\n }\n `,e}(o,r);return document.head.appendChild(a),new Promise(t=>{const i=()=>{window.removeEventListener("afterprint",i),document.getElementById(n)?.remove(),t()};window.addEventListener("afterprint",i),window.print(),setTimeout(()=>{window.removeEventListener("afterprint",i),document.getElementById(n)?.remove(),t()},5e3)})}var o={button:!1,orientation:"landscape",warnThreshold:500,maxRows:0,includeTitle:!0,includeTimestamp:!0,title:"",isolate:!1},a=class extends e.BaseGridPlugin{name="print";version="1.0.0";styles=".tbw-print-header,.tbw-print-footer{display:none}@media print{tbw-grid{
|
|
1
|
+
!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports,require("../../core/internal/diagnostics"),require("../../core/plugin/base-plugin")):"function"==typeof define&&define.amd?define(["exports","../../core/internal/diagnostics","../../core/plugin/base-plugin"],i):i((t="undefined"!=typeof globalThis?globalThis:t||self).TbwGridPlugin_print={},t.TbwGrid,t.TbwGrid)}(this,function(t,i,e){Object.defineProperty(t,Symbol.toStringTag,{value:"Module"});var n="tbw-print-isolation-style";async function r(t,e={}){const{orientation:r="landscape"}=e,o=t.id;document.querySelectorAll(`#${CSS.escape(o)}`).length>1&&(0,i.warnDiagnostic)(i.PRINT_DUPLICATE_ID,`Multiple elements found with id="${o}". Print isolation may not work correctly. Ensure each grid has a unique ID.`,o,"print"),document.getElementById(n)?.remove();const a=function(t,i){const e=document.createElement("style");return e.id=n,e.textContent=`\n /* Print isolation: hide everything except the target grid */\n @media print {\n /* Hide all body children by default */\n body > *:not(#${t}) {\n display: none !important;\n }\n\n /* But show the grid and ensure it's not hidden by ancestor rules */\n #${t} {\n display: block !important;\n position: static !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n width: 100% !important;\n max-height: none !important;\n margin: 0 !important;\n padding: 0 !important;\n transform: none !important;\n }\n\n /* If grid is nested, we need to show its ancestors too */\n #${t},\n #${t} * {\n visibility: visible !important;\n }\n\n /* Walk up the DOM and show all ancestors of the grid */\n body *:has(> #${t}),\n body *:has(#${t}) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n position: static !important;\n transform: none !important;\n background: transparent !important;\n border: none !important;\n padding: 0 !important;\n margin: 0 !important;\n }\n\n /* Hide siblings of ancestors (everything that's not in the path to the grid) */\n body *:has(#${t}) > *:not(:has(#${t})):not(#${t}) {\n display: none !important;\n }\n\n /* Page settings */\n @page {\n size: ${i};\n margin: 1cm;\n }\n\n /* Ensure proper print styling */\n body {\n margin: 0 !important;\n padding: 0 !important;\n background: white !important;\n color-scheme: light !important;\n }\n }\n\n /* Screen: also apply isolation for print preview */\n @media screen {\n /* When this stylesheet is active, we're about to print */\n /* No screen-specific rules needed - isolation only applies to print */\n }\n `,e}(o,r);return document.head.appendChild(a),new Promise(t=>{const i=()=>{window.removeEventListener("afterprint",i),document.getElementById(n)?.remove(),t()};window.addEventListener("afterprint",i),window.print(),setTimeout(()=>{window.removeEventListener("afterprint",i),document.getElementById(n)?.remove(),t()},5e3)})}var o={button:!1,orientation:"landscape",warnThreshold:500,maxRows:0,includeTitle:!0,includeTimestamp:!0,title:"",isolate:!1},a=class extends e.BaseGridPlugin{name="print";version="1.0.0";styles=".tbw-print-header,.tbw-print-footer{display:none}@media print{tbw-grid{color-scheme:light only;-webkit-print-color-adjust:exact;print-color-adjust:exact;border:none!important;border-radius:0!important;height:auto!important;overflow:visible!important}tbw-grid .tbw-grid-content,tbw-grid .tbw-scroll-area,tbw-grid .rows-body{height:auto!important;max-height:none!important;overflow:visible!important}tbw-grid .rows-container,tbw-grid .rows-viewport,tbw-grid .rows{height:auto!important;max-height:none!important;overflow:visible!important;transform:none!important}tbw-grid .rows-viewport .rows{position:static!important}tbw-grid .resize-handle,tbw-grid [part=sort-indicator],tbw-grid .tbw-filter-btn,tbw-grid .tool-panel,tbw-grid .tool-panel-content,tbw-grid .tbw-shell-header,tbw-grid .shell-toolbar,tbw-grid .tool-panel-toggle,tbw-grid [data-print-hide],tbw-grid .expander-cell,tbw-grid .tree-toggle,tbw-grid .context-menu,tbw-grid .faux-vscroll{display:none!important}tbw-grid .tbw-print-header{padding:var(--tbw-spacing-md,.5em) 0;margin-bottom:var(--tbw-spacing-md,.5em);border-bottom:2px solid var(--tbw-print-border,var(--tbw-color-border-strong));justify-content:space-between;align-items:baseline;font-family:inherit;display:flex}.tbw-print-header-title{font-size:1.25em;font-weight:700}.tbw-print-header-timestamp{font-size:var(--tbw-font-size-sm,.875em);color:var(--tbw-print-muted,var(--tbw-color-fg-muted))}tbw-grid .tbw-print-footer{margin-top:var(--tbw-spacing-md,.5em);padding-top:var(--tbw-spacing-md,.5em);border-top:1px solid var(--tbw-print-border,var(--tbw-color-border));font-size:var(--tbw-font-size-xs,.75em);color:var(--tbw-print-muted,var(--tbw-color-fg-muted));text-align:end;display:block}tbw-grid .data-grid-row{break-inside:avoid;page-break-inside:avoid}tbw-grid .cell{border:1px solid var(--tbw-print-cell-border,var(--tbw-color-border))!important}tbw-grid .header-row,tbw-grid .data-grid-row{padding-inline-end:1px}tbw-grid .data-grid-row:hover,tbw-grid .cell:hover{background:inherit!important}@page{margin:1cm}}";#t=!1;#i=null;#e=null;#n=null;#r=null;#o=null;#a=null;get#s(){return this.grid}isPrinting(){return this.#t}async print(t){if(this.#t)return void this.warn(i.PRINT_IN_PROGRESS,"Print already in progress");const e=this.gridElement;if(!e)return void this.warn(i.PRINT_NO_GRID,"Grid not available");const n={...o,...this.config,...t},r=this.rows.length;let a=r,s=!1;if(n.warnThreshold>0&&r>n.warnThreshold){const t=n.maxRows>0?`\n\nNote: Output will be limited to ${n.maxRows.toLocaleString()} rows.`:"";if(!confirm(`This grid has ${r.toLocaleString()} rows. Printing large datasets may cause performance issues or browser slowdowns.${t}\n\nClick OK to continue, or Cancel to abort.`))return}n.maxRows>0&&r>n.maxRows&&(a=n.maxRows,s=!0),this.#t=!0;const d=performance.now();this.emit("print-start",{rowCount:a,limitApplied:s,originalRowCount:r});try{const t=this.#s;this.#e={bypassThreshold:t._virtualization?.bypassThreshold??24},this.#d(),s&&(this.#n=this.sourceRows,this.grid.rows=this.sourceRows.slice(0,a),await new Promise(t=>setTimeout(t,50))),(n.includeTitle||n.includeTimestamp)&&this.#l(n),await this.#p(),await new Promise(t=>requestAnimationFrame(t)),await new Promise(t=>requestAnimationFrame(t)),e.classList.add(`print-${n.orientation}`),await new Promise(t=>requestAnimationFrame(t)),await new Promise(t=>requestAnimationFrame(t)),n.isolate?await this.#m(n):await this.#h(),this.emit("print-complete",{success:!0,rowCount:a,duration:Math.round(performance.now()-d)})}catch(l){(0,i.errorDiagnostic)(i.PRINT_FAILED,`Print failed: ${l}`,this.gridElement?.id,this.name),this.emit("print-complete",{success:!1,rowCount:0,duration:Math.round(performance.now()-d)})}finally{this.#c(),this.#t=!1}}#l(t){const i=this.gridElement;if(i){if(this.#r=document.createElement("div"),this.#r.className="tbw-print-header",t.includeTitle){const i=t.title||this.grid.effectiveConfig?.shell?.header?.title||"Grid Data",e=document.createElement("div");e.className="tbw-print-header-title",e.textContent=i,this.#r.appendChild(e)}if(t.includeTimestamp){const t=document.createElement("div");t.className="tbw-print-header-timestamp",t.textContent=`Printed: ${(new Date).toLocaleString()}`,this.#r.appendChild(t)}i.insertBefore(this.#r,i.firstChild),this.#o=document.createElement("div"),this.#o.className="tbw-print-footer",this.#o.textContent=`Page generated from ${window.location.hostname}`,i.appendChild(this.#o)}}async#p(){const t=this.#s;if(!t._virtualization)return;const i=this.rows.length;t._virtualization.bypassThreshold=i+100,t.refreshVirtualWindow(!0),await new Promise(t=>setTimeout(t,100))}async#h(){return new Promise(t=>{const i=()=>{window.removeEventListener("afterprint",i),t()};window.addEventListener("afterprint",i),window.print(),setTimeout(()=>{"undefined"!=typeof window&&window.removeEventListener("afterprint",i),t()},1e3)})}async#m(t){const i=this.gridElement;i&&await r(i,{orientation:t.orientation})}#d(){const t=this.columns;if(t){this.#i=new Map;for(const i of t)i.field&&(!0===i.printHidden||!0===i.utility&&!1!==i.printHidden)&&(this.#i.set(i.field,!i.hidden),this.grid.setColumnVisible(i.field,!1))}}#w(){if(this.#i){for(const[t,i]of this.#i)this.grid.setColumnVisible(t,i);this.#i=null}}#c(){const t=this.gridElement;if(!t)return;this.#w(),t.classList.remove("print-portrait","print-landscape"),null!==this.#a&&(t.style.transform="",t.style.transformOrigin="",t.style.width="",this.#a=null),this.#r&&(this.#r.remove(),this.#r=null),this.#o&&(this.#o.remove(),this.#o=null);const i=this.#s;this.#e&&i._virtualization&&(i._virtualization.bypassThreshold=this.#e.bypassThreshold,i.refreshVirtualWindow(!0),this.#e=null),null!==this.#n&&(this.grid.rows=this.#n,this.#n=null)}afterRender(){this.config?.button&&!this.#g&&(this.#u(),this.#g=!0)}#g=!1;#u(){this.#s.registerToolbarContent?.({id:"print-button",order:900,render:t=>{const i=document.createElement("button");i.className="tbw-toolbar-btn tbw-print-btn",i.title="Print grid",i.type="button",this.setIcon(i,"print"),i.addEventListener("click",()=>{this.print()},{signal:this.disconnectSignal}),t.appendChild(i)}})}};t.PrintPlugin=a,t.printGridIsolated=r});
|
|
2
2
|
//# sourceMappingURL=print.umd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"print.umd.js","names":["#internalGrid","#printing","#savedVirtualization","#hidePrintColumns","#savedRows","#addPrintHeader","#disableVirtualization","#printInIsolatedWindow","#triggerPrint","#cleanup","#printHeader","#printFooter","#savedHiddenColumns","#restorePrintColumns","#appliedScale","#toolbarRegistered","#registerToolbarButton"],"sources":["../../../../../libs/grid/src/lib/plugins/print/print-isolated.ts","../../../../../libs/grid/src/lib/plugins/print/print.css?inline","../../../../../libs/grid/src/lib/plugins/print/PrintPlugin.ts"],"sourcesContent":["/**\n * Utility for printing a grid in isolation by hiding all other page content.\n *\n * This approach keeps the grid in place (with virtualization disabled by PrintPlugin)\n * and uses CSS to hide everything else on the page during printing.\n */\n\nimport { PRINT_DUPLICATE_ID, warnDiagnostic } from '../../core/internal/diagnostics';\nimport type { PrintOrientation } from './types';\n\n/** @since 1.4.0 */\nexport interface PrintIsolatedOptions {\n /** Page orientation hint */\n orientation?: PrintOrientation;\n}\n\n/** ID for the isolation stylesheet */\nconst ISOLATION_STYLE_ID = 'tbw-print-isolation-style';\n\n/**\n * Create a stylesheet that hides everything except the target grid.\n * Uses the grid's ID to target it specifically.\n */\nfunction createIsolationStylesheet(gridId: string, orientation: PrintOrientation): HTMLStyleElement {\n const style = document.createElement('style');\n style.id = ISOLATION_STYLE_ID;\n style.textContent = `\n /* Print isolation: hide everything except the target grid */\n @media print {\n /* Hide all body children by default */\n body > *:not(#${gridId}) {\n display: none !important;\n }\n\n /* But show the grid and ensure it's not hidden by ancestor rules */\n #${gridId} {\n display: block !important;\n position: static !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n width: 100% !important;\n max-height: none !important;\n margin: 0 !important;\n padding: 0 !important;\n transform: none !important;\n }\n\n /* If grid is nested, we need to show its ancestors too */\n #${gridId},\n #${gridId} * {\n visibility: visible !important;\n }\n\n /* Walk up the DOM and show all ancestors of the grid */\n body *:has(> #${gridId}),\n body *:has(#${gridId}) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n position: static !important;\n transform: none !important;\n background: transparent !important;\n border: none !important;\n padding: 0 !important;\n margin: 0 !important;\n }\n\n /* Hide siblings of ancestors (everything that's not in the path to the grid) */\n body *:has(#${gridId}) > *:not(:has(#${gridId})):not(#${gridId}) {\n display: none !important;\n }\n\n /* Page settings */\n @page {\n size: ${orientation};\n margin: 1cm;\n }\n\n /* Ensure proper print styling */\n body {\n margin: 0 !important;\n padding: 0 !important;\n background: white !important;\n color-scheme: light !important;\n }\n }\n\n /* Screen: also apply isolation for print preview */\n @media screen {\n /* When this stylesheet is active, we're about to print */\n /* No screen-specific rules needed - isolation only applies to print */\n }\n `;\n return style;\n}\n\n/**\n * Print a grid in isolation by hiding all other page content.\n *\n * This function adds a temporary stylesheet that uses CSS to hide everything\n * on the page except the target grid during printing. The grid stays in place\n * with all its data (virtualization should be disabled separately).\n *\n * @param gridElement - The tbw-grid element to print (must have an ID)\n * @param options - Optional configuration\n * @returns Promise that resolves when the print dialog closes\n *\n * @example\n * ```typescript\n * import { queryGrid } from '@toolbox-web/grid';\n * import { printGridIsolated } from '@toolbox-web/grid/plugins/print';\n *\n * const grid = queryGrid('tbw-grid');\n * await printGridIsolated(grid, { orientation: 'landscape' });\n * ```\n * @since 1.4.0\n */\nexport async function printGridIsolated(gridElement: HTMLElement, options: PrintIsolatedOptions = {}): Promise<void> {\n const { orientation = 'landscape' } = options;\n\n const gridId = gridElement.id;\n\n // Warn if multiple elements share this ID (user-set IDs could collide)\n const elementsWithId = document.querySelectorAll(`#${CSS.escape(gridId)}`);\n if (elementsWithId.length > 1) {\n warnDiagnostic(\n PRINT_DUPLICATE_ID,\n `Multiple elements found with id=\"${gridId}\". ` +\n `Print isolation may not work correctly. Ensure each grid has a unique ID.`,\n gridId,\n 'print',\n );\n }\n\n // Remove any existing isolation stylesheet\n document.getElementById(ISOLATION_STYLE_ID)?.remove();\n\n // Add the isolation stylesheet\n const isolationStyle = createIsolationStylesheet(gridId, orientation);\n document.head.appendChild(isolationStyle);\n\n return new Promise((resolve) => {\n // Listen for afterprint event to cleanup\n const onAfterPrint = () => {\n window.removeEventListener('afterprint', onAfterPrint);\n // Remove isolation stylesheet\n document.getElementById(ISOLATION_STYLE_ID)?.remove();\n resolve();\n };\n window.addEventListener('afterprint', onAfterPrint);\n\n // Trigger print\n window.print();\n\n // Fallback timeout in case afterprint doesn't fire (some browsers)\n setTimeout(() => {\n window.removeEventListener('afterprint', onAfterPrint);\n document.getElementById(ISOLATION_STYLE_ID)?.remove();\n resolve();\n }, 5000);\n });\n}\n","/**\n * Print Plugin CSS\n *\n * Styles for print-optimized grid rendering.\n * Uses @media print for automatic print detection.\n */\n\n/* Print header/footer - hidden on screen, shown in print */\n.tbw-print-header,\n.tbw-print-footer {\n display: none;\n}\n\n/* ========================================\n @media print rules - all print styles\n ======================================== */\n\n@media print {\n tbw-grid {\n /* Force visible overflow to render all rows */\n overflow: visible !important;\n height: auto !important;\n\n /* Remove outer border - keep only cell borders */\n border: none !important;\n border-radius: 0 !important;\n\n /* Force light mode for printing */\n color-scheme: light only;\n\n /* Ensure colors print */\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n }\n\n tbw-grid .tbw-grid-content {\n overflow: visible !important;\n height: auto !important;\n max-height: none !important;\n }\n\n tbw-grid .tbw-scroll-area {\n overflow: visible !important;\n height: auto !important;\n max-height: none !important;\n }\n\n tbw-grid .rows-body {\n overflow: visible !important;\n height: auto !important;\n max-height: none !important;\n }\n\n /* Ensure virtualized rows container expands to show all rows */\n tbw-grid .rows-container,\n tbw-grid .rows-viewport,\n tbw-grid .rows {\n overflow: visible !important;\n height: auto !important;\n max-height: none !important;\n transform: none !important; /* Remove virtual scroll offset */\n }\n\n tbw-grid .rows-viewport .rows {\n position: static !important; /* Reset from absolute positioning */\n }\n\n /* Hide interactive elements, shell chrome, and header icons */\n tbw-grid .resize-handle,\n tbw-grid [part='sort-indicator'],\n tbw-grid .tbw-filter-btn,\n tbw-grid .tool-panel,\n tbw-grid .tool-panel-content,\n tbw-grid .tbw-shell-header,\n tbw-grid .shell-toolbar,\n tbw-grid .tool-panel-toggle,\n tbw-grid [data-print-hide],\n tbw-grid .expander-cell,\n tbw-grid .tree-toggle,\n tbw-grid .context-menu,\n tbw-grid .faux-vscroll {\n display: none !important;\n }\n\n /* Print header with title and timestamp */\n tbw-grid .tbw-print-header {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n padding: var(--tbw-spacing-md, 0.5em) 0;\n margin-bottom: var(--tbw-spacing-md, 0.5em);\n border-bottom: 2px solid var(--tbw-print-border, var(--tbw-color-border-strong));\n font-family: inherit;\n }\n\n .tbw-print-header-title {\n font-size: 1.25em;\n font-weight: bold;\n }\n\n .tbw-print-header-timestamp {\n font-size: var(--tbw-font-size-sm, 0.875em);\n color: var(--tbw-print-muted, var(--tbw-color-fg-muted));\n }\n\n /* Print footer */\n tbw-grid .tbw-print-footer {\n display: block;\n margin-top: var(--tbw-spacing-md, 0.5em);\n padding-top: var(--tbw-spacing-md, 0.5em);\n border-top: 1px solid var(--tbw-print-border, var(--tbw-color-border));\n font-size: var(--tbw-font-size-xs, 0.75em);\n color: var(--tbw-print-muted, var(--tbw-color-fg-muted));\n text-align: end;\n }\n\n /* Row styling */\n tbw-grid .data-grid-row {\n break-inside: avoid;\n page-break-inside: avoid;\n }\n\n /* Cell styling - use theme variables with print-safe fallbacks */\n tbw-grid .cell {\n border: 1px solid var(--tbw-print-cell-border, var(--tbw-color-border)) !important;\n }\n\n /* Ensure the last column's end border is visible */\n tbw-grid .header-row,\n tbw-grid .data-grid-row {\n padding-inline-end: 1px; /* Prevent end border from being clipped */\n }\n\n /* Remove hover effects */\n tbw-grid .data-grid-row:hover,\n tbw-grid .cell:hover {\n background: inherit !important;\n }\n\n /* Page setup */\n @page {\n margin: 1cm;\n }\n}\n","/**\n * Print Plugin (Class-based)\n *\n * Provides print layout functionality for tbw-grid.\n * Temporarily disables virtualization to render all rows and uses\n * @media print CSS for print-optimized styling.\n */\n\nimport { PRINT_FAILED, PRINT_IN_PROGRESS, PRINT_NO_GRID, errorDiagnostic } from '../../core/internal/diagnostics';\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport type { InternalGrid } from '../../core/types';\nimport type { ToolbarContentDefinition } from '../shell/types';\nimport { printGridIsolated } from './print-isolated';\nimport styles from './print.css?inline';\nimport type { PrintCompleteDetail, PrintConfig, PrintParams, PrintStartDetail } from './types';\n\n/**\n * Extended grid interface for PrintPlugin internal access.\n * Includes registerToolbarContent which is available on the grid class\n * but not exposed in the standard plugin API.\n */\ninterface PrintGridRef extends InternalGrid {\n registerToolbarContent?(content: ToolbarContentDefinition): void;\n unregisterToolbarContent?(contentId: string): void;\n}\n\n/** Default configuration */\nconst DEFAULT_CONFIG: Required<PrintConfig> = {\n button: false,\n orientation: 'landscape',\n warnThreshold: 500,\n maxRows: 0,\n includeTitle: true,\n includeTimestamp: true,\n title: '',\n isolate: false,\n};\n\n/**\n * Print Plugin for tbw-grid\n *\n * Enables printing the full grid content by temporarily disabling virtualization\n * and applying print-optimized styles. Handles large datasets gracefully with\n * configurable row limits.\n *\n * ## Installation\n *\n * ```ts\n * import { PrintPlugin } from '@toolbox-web/grid/plugins/print';\n * ```\n *\n * ## Events\n *\n * | Event | Detail | Description |\n * |-------|--------|-------------|\n * | `print-start` | {@link PrintStartDetail} | Fired when print begins |\n * | `print-complete` | {@link PrintCompleteDetail} | Fired when print completes |\n *\n * @example Basic Print\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { PrintPlugin } from '@toolbox-web/grid/plugins/print';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * plugins: [new PrintPlugin()],\n * };\n *\n * // Trigger print\n * const printPlugin = grid.getPluginByName('print');\n * await printPlugin.print();\n * ```\n *\n * @example With Toolbar Button\n * ```ts\n * grid.gridConfig = {\n * plugins: [new PrintPlugin({ button: true, orientation: 'landscape' })],\n * };\n * ```\n *\n * @see {@link PrintConfig} for all configuration options\n * @since 1.4.0\n */\nexport class PrintPlugin extends BaseGridPlugin<PrintConfig> {\n /** @internal */\n readonly name = 'print';\n\n /** @internal */\n override readonly version = '1.0.0';\n\n /** CSS styles for print mode */\n override readonly styles = styles;\n\n /** Current print state */\n #printing = false;\n\n /** Saved column visibility state */\n #savedHiddenColumns: Map<string, boolean> | null = null;\n\n /** Saved virtualization state */\n #savedVirtualization: { bypassThreshold: number } | null = null;\n\n /** Saved rows when maxRows limit is applied */\n #savedRows: unknown[] | null = null;\n\n /** Print header element */\n #printHeader: HTMLElement | null = null;\n\n /** Print footer element */\n #printFooter: HTMLElement | null = null;\n\n /** Applied scale factor (legacy, used for cleanup) */\n #appliedScale: number | null = null;\n\n /**\n * Get the grid typed as PrintGridRef for internal access.\n */\n get #internalGrid(): PrintGridRef {\n return this.grid as unknown as PrintGridRef;\n }\n\n /**\n * Check if print is currently in progress\n */\n isPrinting(): boolean {\n return this.#printing;\n }\n\n /**\n * Trigger the browser print dialog\n *\n * This method:\n * 1. Validates row count against maxRows limit\n * 2. Disables virtualization to render all rows\n * 3. Applies print-specific CSS classes\n * 4. Opens the browser print dialog (or isolated window if `isolate: true`)\n * 5. Restores normal state after printing\n *\n * @param params - Optional parameters to override config for this print\n * @param params.isolate - If true, prints in an isolated window containing only the grid\n * @returns Promise that resolves when print dialog closes\n */\n async print(params?: PrintParams): Promise<void> {\n if (this.#printing) {\n this.warn(PRINT_IN_PROGRESS, 'Print already in progress');\n return;\n }\n\n const grid = this.gridElement;\n if (!grid) {\n this.warn(PRINT_NO_GRID, 'Grid not available');\n return;\n }\n\n const config = { ...DEFAULT_CONFIG, ...this.config, ...params };\n const rows = this.rows;\n const originalRowCount = rows.length;\n let rowCount = originalRowCount;\n let limitApplied = false;\n\n // Check if we should warn about large datasets\n if (config.warnThreshold > 0 && originalRowCount > config.warnThreshold) {\n const limitInfo =\n config.maxRows > 0 ? `\\n\\nNote: Output will be limited to ${config.maxRows.toLocaleString()} rows.` : '';\n const proceed = confirm(\n `This grid has ${originalRowCount.toLocaleString()} rows. ` +\n `Printing large datasets may cause performance issues or browser slowdowns.${limitInfo}\\n\\n` +\n `Click OK to continue, or Cancel to abort.`,\n );\n if (!proceed) {\n return;\n }\n }\n\n // Apply hard row limit if configured\n if (config.maxRows > 0 && originalRowCount > config.maxRows) {\n rowCount = config.maxRows;\n limitApplied = true;\n }\n\n this.#printing = true;\n\n // Track timing for duration reporting\n const startTime = performance.now();\n\n // Emit print-start event\n this.emit<PrintStartDetail>('print-start', {\n rowCount,\n limitApplied,\n originalRowCount,\n });\n\n try {\n // Save current virtualization state\n const internalGrid = this.#internalGrid;\n this.#savedVirtualization = {\n bypassThreshold: internalGrid._virtualization?.bypassThreshold ?? 24,\n };\n\n // Hide columns marked with printHidden\n this.#hidePrintColumns();\n\n // Apply row limit if configured\n if (limitApplied) {\n this.#savedRows = this.sourceRows;\n // Set limited rows on the grid\n this.grid.rows = this.sourceRows.slice(0, rowCount);\n // Wait for grid to process new rows\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n\n // Add print header if configured\n if (config.includeTitle || config.includeTimestamp) {\n this.#addPrintHeader(config);\n }\n\n // Disable virtualization to render all rows\n // This forces the grid to render all rows in the DOM\n await this.#disableVirtualization();\n\n // Wait for next frame to ensure DOM is updated\n await new Promise((resolve) => requestAnimationFrame(resolve));\n await new Promise((resolve) => requestAnimationFrame(resolve));\n\n // Add orientation class for @page rules\n grid.classList.add(`print-${config.orientation}`);\n\n // Wait for next frame to ensure DOM is updated\n await new Promise((resolve) => requestAnimationFrame(resolve));\n await new Promise((resolve) => requestAnimationFrame(resolve));\n\n // Trigger browser print dialog (isolated or inline)\n if (config.isolate) {\n await this.#printInIsolatedWindow(config);\n } else {\n await this.#triggerPrint();\n }\n\n // Emit print-complete event\n this.emit<PrintCompleteDetail>('print-complete', {\n success: true,\n rowCount,\n duration: Math.round(performance.now() - startTime),\n });\n } catch (error) {\n errorDiagnostic(PRINT_FAILED, `Print failed: ${error}`, this.gridElement?.id, this.name);\n this.emit<PrintCompleteDetail>('print-complete', {\n success: false,\n rowCount: 0,\n duration: Math.round(performance.now() - startTime),\n });\n } finally {\n // Restore normal state\n this.#cleanup();\n this.#printing = false;\n }\n }\n\n /**\n * Add print header with title and timestamp\n */\n #addPrintHeader(config: Required<PrintConfig>): void {\n const grid = this.gridElement;\n if (!grid) return;\n\n // Create print header\n this.#printHeader = document.createElement('div');\n this.#printHeader.className = 'tbw-print-header';\n\n // Title\n if (config.includeTitle) {\n const title = config.title || this.grid.effectiveConfig?.shell?.header?.title || 'Grid Data';\n const titleEl = document.createElement('div');\n titleEl.className = 'tbw-print-header-title';\n titleEl.textContent = title;\n this.#printHeader.appendChild(titleEl);\n }\n\n // Timestamp\n if (config.includeTimestamp) {\n const timestampEl = document.createElement('div');\n timestampEl.className = 'tbw-print-header-timestamp';\n timestampEl.textContent = `Printed: ${new Date().toLocaleString()}`;\n this.#printHeader.appendChild(timestampEl);\n }\n\n // Insert at the beginning of the grid\n grid.insertBefore(this.#printHeader, grid.firstChild);\n\n // Create print footer\n this.#printFooter = document.createElement('div');\n this.#printFooter.className = 'tbw-print-footer';\n this.#printFooter.textContent = `Page generated from ${window.location.hostname}`;\n grid.appendChild(this.#printFooter);\n }\n\n /**\n * Disable virtualization to render all rows\n */\n async #disableVirtualization(): Promise<void> {\n const internalGrid = this.#internalGrid;\n if (!internalGrid._virtualization) return;\n\n // Set bypass threshold higher than total row count to disable virtualization\n // This makes the grid render all rows (up to maxRows) instead of just visible ones\n const totalRows = this.rows.length;\n internalGrid._virtualization.bypassThreshold = totalRows + 100;\n\n // Force a full refresh to re-render with virtualization disabled\n internalGrid.refreshVirtualWindow(true);\n\n // Wait for render to complete\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n\n /**\n * Trigger the browser print dialog\n */\n async #triggerPrint(): Promise<void> {\n return new Promise((resolve) => {\n // Listen for afterprint event\n const onAfterPrint = () => {\n window.removeEventListener('afterprint', onAfterPrint);\n resolve();\n };\n window.addEventListener('afterprint', onAfterPrint);\n\n // Trigger print\n window.print();\n\n // Fallback timeout in case afterprint doesn't fire (some browsers)\n setTimeout(() => {\n // Guard against test environment teardown where window may be undefined\n if (typeof window !== 'undefined') {\n window.removeEventListener('afterprint', onAfterPrint);\n }\n resolve();\n }, 1000);\n });\n }\n\n /**\n * Print in isolation by hiding all other page content.\n * This excludes navigation, sidebars, etc. while keeping the grid in place.\n */\n async #printInIsolatedWindow(config: Required<PrintConfig>): Promise<void> {\n const grid = this.gridElement;\n if (!grid) return;\n\n await printGridIsolated(grid, {\n orientation: config.orientation,\n });\n }\n\n /**\n * Hide columns marked with printHidden: true OR utility: true.\n *\n * Utility (system) columns — selection checkbox, expander, drag handle, custom\n * action menus, etc. — are excluded from print by default. Set `printHidden: false`\n * explicitly on a utility column if you need it to appear in the printout.\n */\n #hidePrintColumns(): void {\n const columns = this.columns;\n if (!columns) return;\n\n // Save current hidden state and hide print columns\n this.#savedHiddenColumns = new Map();\n\n for (const col of columns) {\n if (!col.field) continue;\n // Default: hide if printHidden === true OR utility === true.\n // Allow explicit opt-in for utility columns via `printHidden: false`.\n const shouldHide = col.printHidden === true || (col.utility === true && col.printHidden !== false);\n if (!shouldHide) continue;\n // Save current visibility state (true = visible, false = hidden)\n this.#savedHiddenColumns.set(col.field, !col.hidden);\n // Hide the column for printing\n this.grid.setColumnVisible(col.field, false);\n }\n }\n\n /**\n * Restore columns that were hidden for printing\n */\n #restorePrintColumns(): void {\n if (!this.#savedHiddenColumns) return;\n\n for (const [field, wasVisible] of this.#savedHiddenColumns) {\n // Restore original visibility\n this.grid.setColumnVisible(field, wasVisible);\n }\n\n this.#savedHiddenColumns = null;\n }\n\n /**\n * Cleanup after printing\n */\n #cleanup(): void {\n const grid = this.gridElement;\n if (!grid) return;\n\n // Restore columns that were hidden for printing\n this.#restorePrintColumns();\n\n // Remove orientation classes (both original and possibly switched)\n grid.classList.remove('print-portrait', 'print-landscape');\n\n // Remove scaling transform if applied (legacy)\n if (this.#appliedScale !== null) {\n grid.style.transform = '';\n grid.style.transformOrigin = '';\n grid.style.width = '';\n this.#appliedScale = null;\n }\n\n // Remove print header/footer\n if (this.#printHeader) {\n this.#printHeader.remove();\n this.#printHeader = null;\n }\n if (this.#printFooter) {\n this.#printFooter.remove();\n this.#printFooter = null;\n }\n\n // Restore virtualization\n const internalGrid = this.#internalGrid;\n if (this.#savedVirtualization && internalGrid._virtualization) {\n internalGrid._virtualization.bypassThreshold = this.#savedVirtualization.bypassThreshold;\n internalGrid.refreshVirtualWindow(true);\n this.#savedVirtualization = null;\n }\n\n // Restore original rows if they were limited\n if (this.#savedRows !== null) {\n this.grid.rows = this.#savedRows;\n this.#savedRows = null;\n }\n }\n\n /**\n * Register toolbar button if configured\n * @internal\n */\n override afterRender(): void {\n // Register toolbar on first render when button is enabled\n if (this.config?.button && !this.#toolbarRegistered) {\n this.#registerToolbarButton();\n this.#toolbarRegistered = true;\n }\n }\n\n /** Track if toolbar button is registered */\n #toolbarRegistered = false;\n\n /**\n * Register print button in toolbar\n */\n #registerToolbarButton(): void {\n const grid = this.#internalGrid;\n\n // Register toolbar content\n grid.registerToolbarContent?.({\n id: 'print-button',\n order: 900, // High order to appear at the end\n render: (container: HTMLElement) => {\n const button = document.createElement('button');\n button.className = 'tbw-toolbar-btn tbw-print-btn';\n button.title = 'Print grid';\n button.type = 'button';\n\n // Use print icon\n this.setIcon(button, 'print');\n\n button.addEventListener(\n 'click',\n () => {\n this.print();\n },\n { signal: this.disconnectSignal },\n );\n\n container.appendChild(button);\n },\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"print.umd.js","names":["#internalGrid","#printing","#savedVirtualization","#hidePrintColumns","#savedRows","#addPrintHeader","#disableVirtualization","#printInIsolatedWindow","#triggerPrint","#cleanup","#printHeader","#printFooter","#savedHiddenColumns","#restorePrintColumns","#appliedScale","#toolbarRegistered","#registerToolbarButton"],"sources":["../../../../../libs/grid/src/lib/plugins/print/print-isolated.ts","../../../../../libs/grid/src/lib/plugins/print/print.css?inline","../../../../../libs/grid/src/lib/plugins/print/PrintPlugin.ts"],"sourcesContent":["/**\n * Utility for printing a grid in isolation by hiding all other page content.\n *\n * This approach keeps the grid in place (with virtualization disabled by PrintPlugin)\n * and uses CSS to hide everything else on the page during printing.\n */\n\nimport { PRINT_DUPLICATE_ID, warnDiagnostic } from '../../core/internal/diagnostics';\nimport type { PrintOrientation } from './types';\n\n/** @since 1.4.0 */\nexport interface PrintIsolatedOptions {\n /** Page orientation hint */\n orientation?: PrintOrientation;\n}\n\n/** ID for the isolation stylesheet */\nconst ISOLATION_STYLE_ID = 'tbw-print-isolation-style';\n\n/**\n * Create a stylesheet that hides everything except the target grid.\n * Uses the grid's ID to target it specifically.\n */\nfunction createIsolationStylesheet(gridId: string, orientation: PrintOrientation): HTMLStyleElement {\n const style = document.createElement('style');\n style.id = ISOLATION_STYLE_ID;\n style.textContent = `\n /* Print isolation: hide everything except the target grid */\n @media print {\n /* Hide all body children by default */\n body > *:not(#${gridId}) {\n display: none !important;\n }\n\n /* But show the grid and ensure it's not hidden by ancestor rules */\n #${gridId} {\n display: block !important;\n position: static !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n width: 100% !important;\n max-height: none !important;\n margin: 0 !important;\n padding: 0 !important;\n transform: none !important;\n }\n\n /* If grid is nested, we need to show its ancestors too */\n #${gridId},\n #${gridId} * {\n visibility: visible !important;\n }\n\n /* Walk up the DOM and show all ancestors of the grid */\n body *:has(> #${gridId}),\n body *:has(#${gridId}) {\n display: block !important;\n visibility: visible !important;\n opacity: 1 !important;\n overflow: visible !important;\n height: auto !important;\n position: static !important;\n transform: none !important;\n background: transparent !important;\n border: none !important;\n padding: 0 !important;\n margin: 0 !important;\n }\n\n /* Hide siblings of ancestors (everything that's not in the path to the grid) */\n body *:has(#${gridId}) > *:not(:has(#${gridId})):not(#${gridId}) {\n display: none !important;\n }\n\n /* Page settings */\n @page {\n size: ${orientation};\n margin: 1cm;\n }\n\n /* Ensure proper print styling */\n body {\n margin: 0 !important;\n padding: 0 !important;\n background: white !important;\n color-scheme: light !important;\n }\n }\n\n /* Screen: also apply isolation for print preview */\n @media screen {\n /* When this stylesheet is active, we're about to print */\n /* No screen-specific rules needed - isolation only applies to print */\n }\n `;\n return style;\n}\n\n/**\n * Print a grid in isolation by hiding all other page content.\n *\n * This function adds a temporary stylesheet that uses CSS to hide everything\n * on the page except the target grid during printing. The grid stays in place\n * with all its data (virtualization should be disabled separately).\n *\n * @param gridElement - The tbw-grid element to print (must have an ID)\n * @param options - Optional configuration\n * @returns Promise that resolves when the print dialog closes\n *\n * @example\n * ```typescript\n * import { queryGrid } from '@toolbox-web/grid';\n * import { printGridIsolated } from '@toolbox-web/grid/plugins/print';\n *\n * const grid = queryGrid('tbw-grid');\n * await printGridIsolated(grid, { orientation: 'landscape' });\n * ```\n * @since 1.4.0\n */\nexport async function printGridIsolated(gridElement: HTMLElement, options: PrintIsolatedOptions = {}): Promise<void> {\n const { orientation = 'landscape' } = options;\n\n const gridId = gridElement.id;\n\n // Warn if multiple elements share this ID (user-set IDs could collide)\n const elementsWithId = document.querySelectorAll(`#${CSS.escape(gridId)}`);\n if (elementsWithId.length > 1) {\n warnDiagnostic(\n PRINT_DUPLICATE_ID,\n `Multiple elements found with id=\"${gridId}\". ` +\n `Print isolation may not work correctly. Ensure each grid has a unique ID.`,\n gridId,\n 'print',\n );\n }\n\n // Remove any existing isolation stylesheet\n document.getElementById(ISOLATION_STYLE_ID)?.remove();\n\n // Add the isolation stylesheet\n const isolationStyle = createIsolationStylesheet(gridId, orientation);\n document.head.appendChild(isolationStyle);\n\n return new Promise((resolve) => {\n // Listen for afterprint event to cleanup\n const onAfterPrint = () => {\n window.removeEventListener('afterprint', onAfterPrint);\n // Remove isolation stylesheet\n document.getElementById(ISOLATION_STYLE_ID)?.remove();\n resolve();\n };\n window.addEventListener('afterprint', onAfterPrint);\n\n // Trigger print\n window.print();\n\n // Fallback timeout in case afterprint doesn't fire (some browsers)\n setTimeout(() => {\n window.removeEventListener('afterprint', onAfterPrint);\n document.getElementById(ISOLATION_STYLE_ID)?.remove();\n resolve();\n }, 5000);\n });\n}\n","/**\n * Print Plugin CSS\n *\n * Styles for print-optimized grid rendering.\n * Uses @media print for automatic print detection.\n */\n\n/* Print header/footer - hidden on screen, shown in print */\n.tbw-print-header,\n.tbw-print-footer {\n display: none;\n}\n\n/* ========================================\n @media print rules - all print styles\n ======================================== */\n\n@media print {\n tbw-grid {\n /* Force visible overflow to render all rows */\n overflow: visible !important;\n height: auto !important;\n\n /* Remove outer border - keep only cell borders */\n border: none !important;\n border-radius: 0 !important;\n\n /* Force light mode for printing */\n color-scheme: light only;\n\n /* Ensure colors print */\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n }\n\n tbw-grid .tbw-grid-content {\n overflow: visible !important;\n height: auto !important;\n max-height: none !important;\n }\n\n tbw-grid .tbw-scroll-area {\n overflow: visible !important;\n height: auto !important;\n max-height: none !important;\n }\n\n tbw-grid .rows-body {\n overflow: visible !important;\n height: auto !important;\n max-height: none !important;\n }\n\n /* Ensure virtualized rows container expands to show all rows */\n tbw-grid .rows-container,\n tbw-grid .rows-viewport,\n tbw-grid .rows {\n overflow: visible !important;\n height: auto !important;\n max-height: none !important;\n transform: none !important; /* Remove virtual scroll offset */\n }\n\n tbw-grid .rows-viewport .rows {\n position: static !important; /* Reset from absolute positioning */\n }\n\n /* Hide interactive elements, shell chrome, and header icons */\n tbw-grid .resize-handle,\n tbw-grid [part='sort-indicator'],\n tbw-grid .tbw-filter-btn,\n tbw-grid .tool-panel,\n tbw-grid .tool-panel-content,\n tbw-grid .tbw-shell-header,\n tbw-grid .shell-toolbar,\n tbw-grid .tool-panel-toggle,\n tbw-grid [data-print-hide],\n tbw-grid .expander-cell,\n tbw-grid .tree-toggle,\n tbw-grid .context-menu,\n tbw-grid .faux-vscroll {\n display: none !important;\n }\n\n /* Print header with title and timestamp */\n tbw-grid .tbw-print-header {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n padding: var(--tbw-spacing-md, 0.5em) 0;\n margin-bottom: var(--tbw-spacing-md, 0.5em);\n border-bottom: 2px solid var(--tbw-print-border, var(--tbw-color-border-strong));\n font-family: inherit;\n }\n\n .tbw-print-header-title {\n font-size: 1.25em;\n font-weight: bold;\n }\n\n .tbw-print-header-timestamp {\n font-size: var(--tbw-font-size-sm, 0.875em);\n color: var(--tbw-print-muted, var(--tbw-color-fg-muted));\n }\n\n /* Print footer */\n tbw-grid .tbw-print-footer {\n display: block;\n margin-top: var(--tbw-spacing-md, 0.5em);\n padding-top: var(--tbw-spacing-md, 0.5em);\n border-top: 1px solid var(--tbw-print-border, var(--tbw-color-border));\n font-size: var(--tbw-font-size-xs, 0.75em);\n color: var(--tbw-print-muted, var(--tbw-color-fg-muted));\n text-align: end;\n }\n\n /* Row styling */\n tbw-grid .data-grid-row {\n break-inside: avoid;\n page-break-inside: avoid;\n }\n\n /* Cell styling - use theme variables with print-safe fallbacks */\n tbw-grid .cell {\n border: 1px solid var(--tbw-print-cell-border, var(--tbw-color-border)) !important;\n }\n\n /* Ensure the last column's end border is visible */\n tbw-grid .header-row,\n tbw-grid .data-grid-row {\n padding-inline-end: 1px; /* Prevent end border from being clipped */\n }\n\n /* Remove hover effects */\n tbw-grid .data-grid-row:hover,\n tbw-grid .cell:hover {\n background: inherit !important;\n }\n\n /* Page setup */\n @page {\n margin: 1cm;\n }\n}\n","/**\n * Print Plugin (Class-based)\n *\n * Provides print layout functionality for tbw-grid.\n * Temporarily disables virtualization to render all rows and uses\n * @media print CSS for print-optimized styling.\n */\n\nimport { PRINT_FAILED, PRINT_IN_PROGRESS, PRINT_NO_GRID, errorDiagnostic } from '../../core/internal/diagnostics';\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport type { InternalGrid } from '../../core/types';\nimport type { ToolbarContentDefinition } from '../shell/types';\nimport { printGridIsolated } from './print-isolated';\nimport styles from './print.css?inline';\nimport type { PrintCompleteDetail, PrintConfig, PrintParams, PrintStartDetail } from './types';\n\n/**\n * Extended grid interface for PrintPlugin internal access.\n * Includes registerToolbarContent which is available on the grid class\n * but not exposed in the standard plugin API.\n */\ninterface PrintGridRef extends InternalGrid {\n registerToolbarContent?(content: ToolbarContentDefinition): void;\n unregisterToolbarContent?(contentId: string): void;\n}\n\n/** Default configuration */\nconst DEFAULT_CONFIG: Required<PrintConfig> = {\n button: false,\n orientation: 'landscape',\n warnThreshold: 500,\n maxRows: 0,\n includeTitle: true,\n includeTimestamp: true,\n title: '',\n isolate: false,\n};\n\n/**\n * Print Plugin for tbw-grid\n *\n * Enables printing the full grid content by temporarily disabling virtualization\n * and applying print-optimized styles. Handles large datasets gracefully with\n * configurable row limits.\n *\n * ## Installation\n *\n * ```ts\n * import { PrintPlugin } from '@toolbox-web/grid/plugins/print';\n * ```\n *\n * ## Events\n *\n * | Event | Detail | Description |\n * |-------|--------|-------------|\n * | `print-start` | {@link PrintStartDetail} | Fired when print begins |\n * | `print-complete` | {@link PrintCompleteDetail} | Fired when print completes |\n *\n * @example Basic Print\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { PrintPlugin } from '@toolbox-web/grid/plugins/print';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * plugins: [new PrintPlugin()],\n * };\n *\n * // Trigger print\n * const printPlugin = grid.getPluginByName('print');\n * await printPlugin.print();\n * ```\n *\n * @example With Toolbar Button\n * ```ts\n * grid.gridConfig = {\n * plugins: [new PrintPlugin({ button: true, orientation: 'landscape' })],\n * };\n * ```\n *\n * @see {@link PrintConfig} for all configuration options\n * @since 1.4.0\n */\nexport class PrintPlugin extends BaseGridPlugin<PrintConfig> {\n /** @internal */\n readonly name = 'print';\n\n /** @internal */\n override readonly version = '1.0.0';\n\n /** CSS styles for print mode */\n override readonly styles = styles;\n\n /** Current print state */\n #printing = false;\n\n /** Saved column visibility state */\n #savedHiddenColumns: Map<string, boolean> | null = null;\n\n /** Saved virtualization state */\n #savedVirtualization: { bypassThreshold: number } | null = null;\n\n /** Saved rows when maxRows limit is applied */\n #savedRows: unknown[] | null = null;\n\n /** Print header element */\n #printHeader: HTMLElement | null = null;\n\n /** Print footer element */\n #printFooter: HTMLElement | null = null;\n\n /** Applied scale factor (legacy, used for cleanup) */\n #appliedScale: number | null = null;\n\n /**\n * Get the grid typed as PrintGridRef for internal access.\n */\n get #internalGrid(): PrintGridRef {\n return this.grid as unknown as PrintGridRef;\n }\n\n /**\n * Check if print is currently in progress\n */\n isPrinting(): boolean {\n return this.#printing;\n }\n\n /**\n * Trigger the browser print dialog\n *\n * This method:\n * 1. Validates row count against maxRows limit\n * 2. Disables virtualization to render all rows\n * 3. Applies print-specific CSS classes\n * 4. Opens the browser print dialog (or isolated window if `isolate: true`)\n * 5. Restores normal state after printing\n *\n * @param params - Optional parameters to override config for this print\n * @param params.isolate - If true, prints in an isolated window containing only the grid\n * @returns Promise that resolves when print dialog closes\n */\n async print(params?: PrintParams): Promise<void> {\n if (this.#printing) {\n this.warn(PRINT_IN_PROGRESS, 'Print already in progress');\n return;\n }\n\n const grid = this.gridElement;\n if (!grid) {\n this.warn(PRINT_NO_GRID, 'Grid not available');\n return;\n }\n\n const config = { ...DEFAULT_CONFIG, ...this.config, ...params };\n const rows = this.rows;\n const originalRowCount = rows.length;\n let rowCount = originalRowCount;\n let limitApplied = false;\n\n // Check if we should warn about large datasets\n if (config.warnThreshold > 0 && originalRowCount > config.warnThreshold) {\n const limitInfo =\n config.maxRows > 0 ? `\\n\\nNote: Output will be limited to ${config.maxRows.toLocaleString()} rows.` : '';\n const proceed = confirm(\n `This grid has ${originalRowCount.toLocaleString()} rows. ` +\n `Printing large datasets may cause performance issues or browser slowdowns.${limitInfo}\\n\\n` +\n `Click OK to continue, or Cancel to abort.`,\n );\n if (!proceed) {\n return;\n }\n }\n\n // Apply hard row limit if configured\n if (config.maxRows > 0 && originalRowCount > config.maxRows) {\n rowCount = config.maxRows;\n limitApplied = true;\n }\n\n this.#printing = true;\n\n // Track timing for duration reporting\n const startTime = performance.now();\n\n // Emit print-start event\n this.emit<PrintStartDetail>('print-start', {\n rowCount,\n limitApplied,\n originalRowCount,\n });\n\n try {\n // Save current virtualization state\n const internalGrid = this.#internalGrid;\n this.#savedVirtualization = {\n bypassThreshold: internalGrid._virtualization?.bypassThreshold ?? 24,\n };\n\n // Hide columns marked with printHidden\n this.#hidePrintColumns();\n\n // Apply row limit if configured\n if (limitApplied) {\n this.#savedRows = this.sourceRows;\n // Set limited rows on the grid\n this.grid.rows = this.sourceRows.slice(0, rowCount);\n // Wait for grid to process new rows\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n\n // Add print header if configured\n if (config.includeTitle || config.includeTimestamp) {\n this.#addPrintHeader(config);\n }\n\n // Disable virtualization to render all rows\n // This forces the grid to render all rows in the DOM\n await this.#disableVirtualization();\n\n // Wait for next frame to ensure DOM is updated\n await new Promise((resolve) => requestAnimationFrame(resolve));\n await new Promise((resolve) => requestAnimationFrame(resolve));\n\n // Add orientation class for @page rules\n grid.classList.add(`print-${config.orientation}`);\n\n // Wait for next frame to ensure DOM is updated\n await new Promise((resolve) => requestAnimationFrame(resolve));\n await new Promise((resolve) => requestAnimationFrame(resolve));\n\n // Trigger browser print dialog (isolated or inline)\n if (config.isolate) {\n await this.#printInIsolatedWindow(config);\n } else {\n await this.#triggerPrint();\n }\n\n // Emit print-complete event\n this.emit<PrintCompleteDetail>('print-complete', {\n success: true,\n rowCount,\n duration: Math.round(performance.now() - startTime),\n });\n } catch (error) {\n errorDiagnostic(PRINT_FAILED, `Print failed: ${error}`, this.gridElement?.id, this.name);\n this.emit<PrintCompleteDetail>('print-complete', {\n success: false,\n rowCount: 0,\n duration: Math.round(performance.now() - startTime),\n });\n } finally {\n // Restore normal state\n this.#cleanup();\n this.#printing = false;\n }\n }\n\n /**\n * Add print header with title and timestamp\n */\n #addPrintHeader(config: Required<PrintConfig>): void {\n const grid = this.gridElement;\n if (!grid) return;\n\n // Create print header\n this.#printHeader = document.createElement('div');\n this.#printHeader.className = 'tbw-print-header';\n\n // Title\n if (config.includeTitle) {\n const title = config.title || this.grid.effectiveConfig?.shell?.header?.title || 'Grid Data';\n const titleEl = document.createElement('div');\n titleEl.className = 'tbw-print-header-title';\n titleEl.textContent = title;\n this.#printHeader.appendChild(titleEl);\n }\n\n // Timestamp\n if (config.includeTimestamp) {\n const timestampEl = document.createElement('div');\n timestampEl.className = 'tbw-print-header-timestamp';\n timestampEl.textContent = `Printed: ${new Date().toLocaleString()}`;\n this.#printHeader.appendChild(timestampEl);\n }\n\n // Insert at the beginning of the grid\n grid.insertBefore(this.#printHeader, grid.firstChild);\n\n // Create print footer\n this.#printFooter = document.createElement('div');\n this.#printFooter.className = 'tbw-print-footer';\n this.#printFooter.textContent = `Page generated from ${window.location.hostname}`;\n grid.appendChild(this.#printFooter);\n }\n\n /**\n * Disable virtualization to render all rows\n */\n async #disableVirtualization(): Promise<void> {\n const internalGrid = this.#internalGrid;\n if (!internalGrid._virtualization) return;\n\n // Set bypass threshold higher than total row count to disable virtualization\n // This makes the grid render all rows (up to maxRows) instead of just visible ones\n const totalRows = this.rows.length;\n internalGrid._virtualization.bypassThreshold = totalRows + 100;\n\n // Force a full refresh to re-render with virtualization disabled\n internalGrid.refreshVirtualWindow(true);\n\n // Wait for render to complete\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n\n /**\n * Trigger the browser print dialog\n */\n async #triggerPrint(): Promise<void> {\n return new Promise((resolve) => {\n // Listen for afterprint event\n const onAfterPrint = () => {\n window.removeEventListener('afterprint', onAfterPrint);\n resolve();\n };\n window.addEventListener('afterprint', onAfterPrint);\n\n // Trigger print\n window.print();\n\n // Fallback timeout in case afterprint doesn't fire (some browsers)\n setTimeout(() => {\n // Guard against test environment teardown where window may be undefined\n if (typeof window !== 'undefined') {\n window.removeEventListener('afterprint', onAfterPrint);\n }\n resolve();\n }, 1000);\n });\n }\n\n /**\n * Print in isolation by hiding all other page content.\n * This excludes navigation, sidebars, etc. while keeping the grid in place.\n */\n async #printInIsolatedWindow(config: Required<PrintConfig>): Promise<void> {\n const grid = this.gridElement;\n if (!grid) return;\n\n await printGridIsolated(grid, {\n orientation: config.orientation,\n });\n }\n\n /**\n * Hide columns marked with printHidden: true OR utility: true.\n *\n * Utility (system) columns — selection checkbox, expander, drag handle, custom\n * action menus, etc. — are excluded from print by default. Set `printHidden: false`\n * explicitly on a utility column if you need it to appear in the printout.\n */\n #hidePrintColumns(): void {\n const columns = this.columns;\n if (!columns) return;\n\n // Save current hidden state and hide print columns\n this.#savedHiddenColumns = new Map();\n\n for (const col of columns) {\n if (!col.field) continue;\n // Default: hide if printHidden === true OR utility === true.\n // Allow explicit opt-in for utility columns via `printHidden: false`.\n const shouldHide = col.printHidden === true || (col.utility === true && col.printHidden !== false);\n if (!shouldHide) continue;\n // Save current visibility state (true = visible, false = hidden)\n this.#savedHiddenColumns.set(col.field, !col.hidden);\n // Hide the column for printing\n this.grid.setColumnVisible(col.field, false);\n }\n }\n\n /**\n * Restore columns that were hidden for printing\n */\n #restorePrintColumns(): void {\n if (!this.#savedHiddenColumns) return;\n\n for (const [field, wasVisible] of this.#savedHiddenColumns) {\n // Restore original visibility\n this.grid.setColumnVisible(field, wasVisible);\n }\n\n this.#savedHiddenColumns = null;\n }\n\n /**\n * Cleanup after printing\n */\n #cleanup(): void {\n const grid = this.gridElement;\n if (!grid) return;\n\n // Restore columns that were hidden for printing\n this.#restorePrintColumns();\n\n // Remove orientation classes (both original and possibly switched)\n grid.classList.remove('print-portrait', 'print-landscape');\n\n // Remove scaling transform if applied (legacy)\n if (this.#appliedScale !== null) {\n grid.style.transform = '';\n grid.style.transformOrigin = '';\n grid.style.width = '';\n this.#appliedScale = null;\n }\n\n // Remove print header/footer\n if (this.#printHeader) {\n this.#printHeader.remove();\n this.#printHeader = null;\n }\n if (this.#printFooter) {\n this.#printFooter.remove();\n this.#printFooter = null;\n }\n\n // Restore virtualization\n const internalGrid = this.#internalGrid;\n if (this.#savedVirtualization && internalGrid._virtualization) {\n internalGrid._virtualization.bypassThreshold = this.#savedVirtualization.bypassThreshold;\n internalGrid.refreshVirtualWindow(true);\n this.#savedVirtualization = null;\n }\n\n // Restore original rows if they were limited\n if (this.#savedRows !== null) {\n this.grid.rows = this.#savedRows;\n this.#savedRows = null;\n }\n }\n\n /**\n * Register toolbar button if configured\n * @internal\n */\n override afterRender(): void {\n // Register toolbar on first render when button is enabled\n if (this.config?.button && !this.#toolbarRegistered) {\n this.#registerToolbarButton();\n this.#toolbarRegistered = true;\n }\n }\n\n /** Track if toolbar button is registered */\n #toolbarRegistered = false;\n\n /**\n * Register print button in toolbar\n */\n #registerToolbarButton(): void {\n const grid = this.#internalGrid;\n\n // Register toolbar content\n grid.registerToolbarContent?.({\n id: 'print-button',\n order: 900, // High order to appear at the end\n render: (container: HTMLElement) => {\n const button = document.createElement('button');\n button.className = 'tbw-toolbar-btn tbw-print-btn';\n button.title = 'Print grid';\n button.type = 'button';\n\n // Use print icon\n this.setIcon(button, 'print');\n\n button.addEventListener(\n 'click',\n () => {\n this.print();\n },\n { signal: this.disconnectSignal },\n );\n\n container.appendChild(button);\n },\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("../../core/internal/diagnostics"),require("../../core/internal/keyboard"),require("../../core/internal/sanitize"),require("../../core/plugin/base-plugin")):"function"==typeof define&&define.amd?define(["exports","../../core/internal/diagnostics","../../core/internal/keyboard","../../core/internal/sanitize","../../core/plugin/base-plugin"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).TbwGridPlugin_responsive={},e.TbwGrid,e.TbwGrid,e.TbwGrid,e.TbwGrid)}(this,function(e,t,i,r,s){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});var o=class extends s.BaseGridPlugin{name="responsive";version="1.0.0";styles='tbw-grid[data-responsive-animate] .data-grid-row,tbw-grid[data-responsive-animate] .data-grid-row>.cell{transition:opacity var(--tbw-responsive-duration,.2s) ease-out, transform var(--tbw-responsive-duration,.2s) ease-out}tbw-grid[data-responsive][data-responsive-animate] .data-grid-row{animation:responsive-card-enter var(--tbw-responsive-duration,.2s) ease-out}@keyframes responsive-card-enter{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}tbw-grid[data-responsive] .header{display:none!important}tbw-grid[data-responsive] .footer-row{display:none}tbw-grid[data-responsive] .tbw-scroll-area{overflow-x:hidden;min-width:0!important}tbw-grid[data-responsive] .rows-body-wrapper{min-width:0!important}tbw-grid[data-responsive] .data-grid-row:not(.group-row){padding:var(--tbw-cell-padding);border-bottom:1px solid var(--tbw-color-border);background:var(--tbw-color-bg);padding-inline-start:var(--tbw-spacing-xl);position:relative;contain:none!important;content-visibility:visible!important;grid-template-columns:none!important;height:auto!important;min-height:auto!important;display:block!important}tbw-grid[data-responsive] .data-grid-row:not(.group-row):nth-child(2n){background:var(--tbw-color-row-alt)}tbw-grid[data-responsive] .data-grid-row:not(.group-row):hover{background:var(--tbw-color-row-hover)}tbw-grid[data-responsive] .data-grid-row:not(.group-row)[aria-selected=true]{background:var(--tbw-color-selection)}tbw-grid[data-responsive] .data-grid-row:not(.group-row)[aria-selected=true]:before{content:"";inset-inline-start:0;background:var(--tbw-color-accent);width:4px;position:absolute;top:0;bottom:0}tbw-grid[data-responsive] .data-grid-row:not(.group-row)>.cell{padding:var(--tbw-spacing-xs) var(--tbw-spacing-md);justify-content:space-between;align-items:center;border:none!important;white-space:normal!important;background:0 0!important;border-inline-end:none!important;width:100%!important;min-width:0!important;height:auto!important;min-height:auto!important;line-height:1.5!important;display:flex!important;position:static!important;left:auto!important;right:auto!important;overflow:visible!important}tbw-grid[data-responsive] .data-grid-row:not(.group-row)>.cell:before{content:attr(data-header) ": ";color:var(--tbw-color-header-fg);flex-shrink:0;min-width:100px;margin-inline-end:var(--tbw-spacing-md);font-weight:600}tbw-grid[data-responsive] .data-grid-row:not(.group-row)>.cell:after{content:none}tbw-grid[data-responsive] .cell[data-utility]{display:none!important}tbw-grid[data-responsive] .data-grid-row>.cell[data-responsive-hidden]{display:none!important}tbw-grid[data-responsive] .data-grid-row>.cell[data-responsive-value-only]:before{display:none!important}tbw-grid[data-responsive] .data-grid-row>.cell[data-responsive-value-only]{font-weight:500;justify-content:flex-start!important}tbw-grid[data-responsive][data-responsive-hide-header] .data-grid-row:not(.group-row)>.cell:before,tbw-grid:not([data-responsive]) .cell[data-responsive-hidden]{display:none!important}tbw-grid[data-responsive] .tbw-footer,tbw-grid[data-responsive] .tbw-header-pinned,tbw-grid[data-responsive] .tbw-pinned-rows,tbw-grid[data-responsive] .tbw-aggregation-rows{display:none!important}tbw-grid[data-responsive] .tbw-pinned-rows,tbw-grid[data-responsive] .tbw-aggregation-rows,tbw-grid[data-responsive] .tbw-aggregation-row{min-width:0!important}tbw-grid[data-responsive] .data-grid-row.responsive-card{padding:var(--tbw-cell-padding);border-bottom:1px solid var(--tbw-color-border);display:block!important}tbw-grid[data-responsive] .data-grid-row.responsive-card>*{width:100%}tbw-grid[data-responsive] .data-grid-row.responsive-card .cell:before{display:none}';static manifest={queries:[{type:"isCardMode",description:"Returns whether the grid is currently in responsive card mode"}]};#e;#t=!1;#i;#r=!1;#s=0;#o=new Set;#n=new Set;#a=null;#d=[];get#h(){return this.grid}isResponsive(){return this.#t}setResponsive(e){e!==this.#t&&(this.#t=e,this.#l(),this.emit("responsive-change",{isResponsive:e,width:this.#s,breakpoint:this.config.breakpoint??0}),this.requestRender())}setBreakpoint(e){this.config.breakpoint=e,this.#p(this.#s)}setCardRenderer(e){this.config.cardRenderer=e,this.#t&&this.requestRender()}getWidth(){return this.#s}getActiveBreakpoint(){return this.#a}attach(e){super.attach(e),this.#u(),this.#g(this.config.hiddenColumns),this.config.breakpoints?.length&&(this.#d=[...this.config.breakpoints].sort((e,t)=>t.maxWidth-e.maxWidth)),this.#e=new ResizeObserver(e=>{const t=e[0]?.contentRect.width??0;this.#s=t,clearTimeout(this.#i),this.#i=setTimeout(()=>{this.#p(t)},this.config.debounceMs??100)}),this.#e.observe(this.gridElement)}#u(){const e=this.gridElement;if(!e)return;const t=e.querySelector("tbw-grid-responsive-card");if(!t)return;const i=this.#h.__frameworkAdapter;if(i?.parseResponsiveCardElement){const e=i.parseResponsiveCardElement(t);e&&(this.config={...this.config,cardRenderer:e})}const s=t.getAttribute("breakpoint"),o=t.getAttribute("card-row-height"),n=t.getAttribute("hidden-columns"),a=t.getAttribute("hide-header"),d=t.getAttribute("debounce-ms"),h={};if(null!==s){const e=parseInt(s,10);isNaN(e)||(h.breakpoint=e)}if(null!==o&&(h.cardRowHeight="auto"===o?"auto":parseInt(o,10)),null!==n&&(h.hiddenColumns=n.split(",").map(e=>e.trim()).filter(e=>e.length>0)),null!==a&&(h.hideHeader="false"!==a),null!==d){const e=parseInt(d,10);isNaN(e)||(h.debounceMs=e)}const l=t.innerHTML.trim();!l||this.config.cardRenderer||i?.parseResponsiveCardElement||(h.cardRenderer=e=>{const t=(0,r.sanitizeHTML)((0,r.evalTemplateString)(l,{value:e,row:e})),i=document.createElement("div");return i.className="tbw-responsive-card-content",i.innerHTML=t,i}),Object.keys(h).length>0&&(this.config={...this.config,...h})}#g(e){if(this.#o.clear(),this.#n.clear(),e)for(const t of e)"string"==typeof t?this.#o.add(t):t.showValue?this.#n.add(t.field):this.#o.add(t.field)}detach(){this.#e?.disconnect(),this.#e=void 0,clearTimeout(this.#i),this.#i=void 0,this.gridElement&&this.gridElement.removeAttribute("data-responsive"),super.detach()}handleQuery(e){if("isCardMode"===e.type)return this.#t}afterRender(){if(this.#c(),!(this.#d.length>0?null!==this.#a:this.#t))return;const e=this.#o.size>0,t=this.#n.size>0;if(!e&&!t)return;const i=this.gridElement.querySelectorAll(".cell[data-field]");for(const r of i){const e=r.getAttribute("data-field");e&&(this.#o.has(e)?(r.setAttribute("data-responsive-hidden",""),r.removeAttribute("data-responsive-value-only")):this.#n.has(e)?(r.setAttribute("data-responsive-value-only",""),r.removeAttribute("data-responsive-hidden")):(r.removeAttribute("data-responsive-hidden"),r.removeAttribute("data-responsive-value-only")))}}#p(e){if(this.#d.length>0)return void this.#w(e);const i=this.config.breakpoint??0;0!==i||this.#r||(this.#r=!0,this.warn(t.MISSING_BREAKPOINT,"No breakpoint configured. Responsive mode is disabled. Set a breakpoint based on your grid's column count."));const r=i>0&&e<i;r!==this.#t&&(this.#t=r,this.#l(),this.emit("responsive-change",{isResponsive:r,width:e,breakpoint:i}),this.requestRender())}#w(e){let t=null;for(const i of this.#d)e<=i.maxWidth&&(t=i);if(t!==this.#a){this.#a=t,t?.hiddenColumns?this.#g(t.hiddenColumns):this.#g(this.config.hiddenColumns);const i=!0===t?.cardLayout;i!==this.#t&&(this.#t=i,this.#l()),this.emit("responsive-change",{isResponsive:this.#t,width:e,breakpoint:t?.maxWidth??0}),this.requestRender()}}#v;#l(){this.gridElement.toggleAttribute("data-responsive",this.#t);const e=!1!==this.config.animate;this.gridElement.toggleAttribute("data-responsive-animate",e),this.gridElement.toggleAttribute("data-responsive-hide-header",this.#t&&!0===this.config.hideHeader),this.config.animationDuration&&this.gridElement.style.setProperty("--tbw-responsive-duration",`${this.config.animationDuration}ms`);const t=this.#h;if(this.#t){t._virtualization&&(this.#v=t._virtualization.rowHeight);const e=this.gridElement.querySelector(".tbw-scroll-area");e&&(e.scrollLeft=0)}else{const e=this.gridElement.querySelectorAll(".data-grid-row");for(const t of e)t.style.height="",t.classList.remove("responsive-card");this.#v&&this.#v>0&&t._virtualization&&(t._virtualization.rowHeight=this.#v,this.#v=void 0),this.#b=void 0,this.#m=void 0,this.#f=void 0}}renderRow(e,t,i){if(!this.#t||!this.config.cardRenderer)return;if(e.__isGroupRow)return;t.replaceChildren();const r=this.config.cardRenderer(e,i);t.className="data-grid-row responsive-card";const s=this.config.cardRowHeight;return t.style.height="auto"===s||void 0===s?"auto":`${this.#R()}px`,t.appendChild(r),!0}onKeyDown(e){if(!this.#t)return!1;if(this.config.cardRenderer&&["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(e.key))return!1;const t=this.rows.length-1,r=this.visibleColumns.length-1;switch(e.key){case"ArrowDown":if(this.grid._focusCol<r)return this.grid._focusCol+=1,e.preventDefault(),(0,i.ensureCellVisible)(this.#h),!0;if(this.grid._focusRow<t)return this.grid._focusRow+=1,this.grid._focusCol=0,e.preventDefault(),(0,i.ensureCellVisible)(this.#h),!0;break;case"ArrowUp":if(this.grid._focusCol>0)return this.grid._focusCol-=1,e.preventDefault(),(0,i.ensureCellVisible)(this.#h),!0;if(this.grid._focusRow>0)return this.grid._focusRow-=1,this.grid._focusCol=r,e.preventDefault(),(0,i.ensureCellVisible)(this.#h),!0;break;case"ArrowRight":if(this.grid._focusRow<t)return this.grid._focusRow+=1,e.preventDefault(),(0,i.ensureCellVisible)(this.#h),!0;break;case"ArrowLeft":if(this.grid._focusRow>0)return this.grid._focusRow-=1,e.preventDefault(),(0,i.ensureCellVisible)(this.#h),!0}return!1}#b;#m;#f;#R(){if(this.#b&&this.#b>0)return this.#b;const e=this.config.cardRowHeight;return"number"==typeof e&&e>0?e:80}#C(){return this.#m&&this.#m>0?this.#m:this.#v??28}#y(){for(const e of this.rows)if(e.__isGroupRow)return!0;return!1}getRowHeight(e,t){if(this.#t&&this.config.cardRenderer)return e.__isGroupRow?this.#C():this.#R()}#k(){let e=0;for(const t of this.rows)t.__isGroupRow||e++;return e}#H=!1;#c(){if(!this.#t)return;let e=!1;const t=this.#h,i=this.#y(),r=this.#k();if(r!==this.#f&&(this.#f=r,e=!0),i){const t=this.gridElement.querySelector(".data-grid-row.group-row");if(t){const i=t.getBoundingClientRect().height;i>0&&i!==this.#m&&(this.#m=i,e=!0)}}const s=this.config.cardRenderer?".data-grid-row.responsive-card":".data-grid-row:not(.group-row)",o=this.gridElement.querySelector(s);if(o){const r=o.getBoundingClientRect().height;r>0&&r!==this.#b&&(this.#b=r,e=!0,!i&&t._virtualization&&(t._virtualization.rowHeight=r))}e&&!this.#H&&(this.#H=!0,queueMicrotask(()=>{this.#H=!1,this.grid&&this.#t&&this.#h.refreshVirtualWindow?.(!0,!0)}))}};e.ResponsivePlugin=o});
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("../../core/internal/diagnostics"),require("../../core/internal/keyboard"),require("../../core/internal/sanitize"),require("../../core/plugin/base-plugin")):"function"==typeof define&&define.amd?define(["exports","../../core/internal/diagnostics","../../core/internal/keyboard","../../core/internal/sanitize","../../core/plugin/base-plugin"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).TbwGridPlugin_responsive={},e.TbwGrid,e.TbwGrid,e.TbwGrid,e.TbwGrid)}(this,function(e,t,i,r,s){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});var o=class extends s.BaseGridPlugin{name="responsive";version="1.0.0";styles='tbw-grid[data-responsive-animate] .data-grid-row,tbw-grid[data-responsive-animate] .data-grid-row>.cell{transition:opacity var(--tbw-responsive-duration,.2s) ease-out, transform var(--tbw-responsive-duration,.2s) ease-out}tbw-grid[data-responsive][data-responsive-animate] .data-grid-row{animation:responsive-card-enter var(--tbw-responsive-duration,.2s) ease-out}@keyframes responsive-card-enter{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}tbw-grid[data-responsive] .header{display:none!important}tbw-grid[data-responsive] .footer-row{display:none}tbw-grid[data-responsive] .tbw-scroll-area{overflow-x:hidden;min-width:0!important}tbw-grid[data-responsive] .rows-body-wrapper{min-width:0!important}tbw-grid[data-responsive] .data-grid-row:not(.group-row){padding:var(--tbw-cell-padding);border-bottom:1px solid var(--tbw-color-border);background:var(--tbw-color-bg);padding-inline-start:var(--tbw-spacing-xl);position:relative;contain:none!important;content-visibility:visible!important;grid-template-columns:none!important;height:auto!important;min-height:auto!important;display:block!important}tbw-grid[data-responsive] .data-grid-row:not(.group-row):nth-child(2n){background:var(--tbw-color-row-alt)}tbw-grid[data-responsive] .data-grid-row:not(.group-row):hover{background:var(--tbw-color-row-hover)}tbw-grid[data-responsive] .data-grid-row:not(.group-row)[aria-selected=true]{background:var(--tbw-color-selection)}tbw-grid[data-responsive] .data-grid-row:not(.group-row)[aria-selected=true]:before{content:"";background:var(--tbw-color-accent);inset-inline-start:0;width:4px;position:absolute;top:0;bottom:0}tbw-grid[data-responsive] .data-grid-row:not(.group-row)>.cell{padding:var(--tbw-spacing-xs) var(--tbw-spacing-md);justify-content:space-between;align-items:center;white-space:normal!important;border:none!important;background:0 0!important;border-inline-end:none!important;width:100%!important;min-width:0!important;height:auto!important;min-height:auto!important;line-height:1.5!important;display:flex!important;position:static!important;left:auto!important;right:auto!important;overflow:visible!important}tbw-grid[data-responsive] .data-grid-row:not(.group-row)>.cell:before{content:attr(data-header) ": ";color:var(--tbw-color-header-fg);min-width:100px;flex-shrink:0;margin-inline-end:var(--tbw-spacing-md);font-weight:600}tbw-grid[data-responsive] .data-grid-row:not(.group-row)>.cell:after{content:none}tbw-grid[data-responsive] .cell[data-utility],tbw-grid[data-responsive] .data-grid-row>.cell[data-responsive-hidden],tbw-grid[data-responsive] .data-grid-row>.cell[data-responsive-value-only]:before{display:none!important}tbw-grid[data-responsive] .data-grid-row>.cell[data-responsive-value-only]{font-weight:500;justify-content:flex-start!important}tbw-grid[data-responsive][data-responsive-hide-header] .data-grid-row:not(.group-row)>.cell:before,tbw-grid:not([data-responsive]) .cell[data-responsive-hidden],tbw-grid[data-responsive] .tbw-footer,tbw-grid[data-responsive] .tbw-header-pinned,tbw-grid[data-responsive] .tbw-pinned-rows,tbw-grid[data-responsive] .tbw-aggregation-rows{display:none!important}tbw-grid[data-responsive] .tbw-pinned-rows,tbw-grid[data-responsive] .tbw-aggregation-rows,tbw-grid[data-responsive] .tbw-aggregation-row{min-width:0!important}tbw-grid[data-responsive] .data-grid-row.responsive-card{padding:var(--tbw-cell-padding);border-bottom:1px solid var(--tbw-color-border);display:block!important}tbw-grid[data-responsive] .data-grid-row.responsive-card>*{width:100%}tbw-grid[data-responsive] .data-grid-row.responsive-card .cell:before{display:none}';static manifest={queries:[{type:"isCardMode",description:"Returns whether the grid is currently in responsive card mode"}]};#e;#t=!1;#i;#r=!1;#s=0;#o=new Set;#n=new Set;#a=null;#d=[];get#h(){return this.grid}isResponsive(){return this.#t}setResponsive(e){e!==this.#t&&(this.#t=e,this.#l(),this.emit("responsive-change",{isResponsive:e,width:this.#s,breakpoint:this.config.breakpoint??0}),this.requestRender())}setBreakpoint(e){this.config.breakpoint=e,this.#p(this.#s)}setCardRenderer(e){this.config.cardRenderer=e,this.#t&&this.requestRender()}getWidth(){return this.#s}getActiveBreakpoint(){return this.#a}attach(e){super.attach(e),this.#u(),this.#g(this.config.hiddenColumns),this.config.breakpoints?.length&&(this.#d=[...this.config.breakpoints].sort((e,t)=>t.maxWidth-e.maxWidth)),this.#e=new ResizeObserver(e=>{const t=e[0]?.contentRect.width??0;this.#s=t,clearTimeout(this.#i),this.#i=setTimeout(()=>{this.#p(t)},this.config.debounceMs??100)}),this.#e.observe(this.gridElement)}#u(){const e=this.gridElement;if(!e)return;const t=e.querySelector("tbw-grid-responsive-card");if(!t)return;const i=this.#h.__frameworkAdapter;if(i?.parseResponsiveCardElement){const e=i.parseResponsiveCardElement(t);e&&(this.config={...this.config,cardRenderer:e})}const s=t.getAttribute("breakpoint"),o=t.getAttribute("card-row-height"),n=t.getAttribute("hidden-columns"),a=t.getAttribute("hide-header"),d=t.getAttribute("debounce-ms"),h={};if(null!==s){const e=parseInt(s,10);isNaN(e)||(h.breakpoint=e)}if(null!==o&&(h.cardRowHeight="auto"===o?"auto":parseInt(o,10)),null!==n&&(h.hiddenColumns=n.split(",").map(e=>e.trim()).filter(e=>e.length>0)),null!==a&&(h.hideHeader="false"!==a),null!==d){const e=parseInt(d,10);isNaN(e)||(h.debounceMs=e)}const l=t.innerHTML.trim();!l||this.config.cardRenderer||i?.parseResponsiveCardElement||(h.cardRenderer=e=>{const t=(0,r.sanitizeHTML)((0,r.evalTemplateString)(l,{value:e,row:e})),i=document.createElement("div");return i.className="tbw-responsive-card-content",i.innerHTML=t,i}),Object.keys(h).length>0&&(this.config={...this.config,...h})}#g(e){if(this.#o.clear(),this.#n.clear(),e)for(const t of e)"string"==typeof t?this.#o.add(t):t.showValue?this.#n.add(t.field):this.#o.add(t.field)}detach(){this.#e?.disconnect(),this.#e=void 0,clearTimeout(this.#i),this.#i=void 0,this.gridElement&&this.gridElement.removeAttribute("data-responsive"),super.detach()}handleQuery(e){if("isCardMode"===e.type)return this.#t}afterRender(){if(this.#c(),!(this.#d.length>0?null!==this.#a:this.#t))return;const e=this.#o.size>0,t=this.#n.size>0;if(!e&&!t)return;const i=this.gridElement.querySelectorAll(".cell[data-field]");for(const r of i){const e=r.getAttribute("data-field");e&&(this.#o.has(e)?(r.setAttribute("data-responsive-hidden",""),r.removeAttribute("data-responsive-value-only")):this.#n.has(e)?(r.setAttribute("data-responsive-value-only",""),r.removeAttribute("data-responsive-hidden")):(r.removeAttribute("data-responsive-hidden"),r.removeAttribute("data-responsive-value-only")))}}#p(e){if(this.#d.length>0)return void this.#w(e);const i=this.config.breakpoint??0;0!==i||this.#r||(this.#r=!0,this.warn(t.MISSING_BREAKPOINT,"No breakpoint configured. Responsive mode is disabled. Set a breakpoint based on your grid's column count."));const r=i>0&&e<i;r!==this.#t&&(this.#t=r,this.#l(),this.emit("responsive-change",{isResponsive:r,width:e,breakpoint:i}),this.requestRender())}#w(e){let t=null;for(const i of this.#d)e<=i.maxWidth&&(t=i);if(t!==this.#a){this.#a=t,t?.hiddenColumns?this.#g(t.hiddenColumns):this.#g(this.config.hiddenColumns);const i=!0===t?.cardLayout;i!==this.#t&&(this.#t=i,this.#l()),this.emit("responsive-change",{isResponsive:this.#t,width:e,breakpoint:t?.maxWidth??0}),this.requestRender()}}#v;#l(){this.gridElement.toggleAttribute("data-responsive",this.#t);const e=!1!==this.config.animate;this.gridElement.toggleAttribute("data-responsive-animate",e),this.gridElement.toggleAttribute("data-responsive-hide-header",this.#t&&!0===this.config.hideHeader),this.config.animationDuration&&this.gridElement.style.setProperty("--tbw-responsive-duration",`${this.config.animationDuration}ms`);const t=this.#h;if(this.#t){t._virtualization&&(this.#v=t._virtualization.rowHeight);const e=this.gridElement.querySelector(".tbw-scroll-area");e&&(e.scrollLeft=0)}else{const e=this.gridElement.querySelectorAll(".data-grid-row");for(const t of e)t.style.height="",t.classList.remove("responsive-card");this.#v&&this.#v>0&&t._virtualization&&(t._virtualization.rowHeight=this.#v,this.#v=void 0),this.#b=void 0,this.#m=void 0,this.#f=void 0}}renderRow(e,t,i){if(!this.#t||!this.config.cardRenderer)return;if(e.__isGroupRow)return;t.replaceChildren();const r=this.config.cardRenderer(e,i);t.className="data-grid-row responsive-card";const s=this.config.cardRowHeight;return t.style.height="auto"===s||void 0===s?"auto":`${this.#R()}px`,t.appendChild(r),!0}onKeyDown(e){if(!this.#t)return!1;if(this.config.cardRenderer&&["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(e.key))return!1;const t=this.rows.length-1,r=this.visibleColumns.length-1;switch(e.key){case"ArrowDown":if(this.grid._focusCol<r)return this.grid._focusCol+=1,e.preventDefault(),(0,i.ensureCellVisible)(this.#h),!0;if(this.grid._focusRow<t)return this.grid._focusRow+=1,this.grid._focusCol=0,e.preventDefault(),(0,i.ensureCellVisible)(this.#h),!0;break;case"ArrowUp":if(this.grid._focusCol>0)return this.grid._focusCol-=1,e.preventDefault(),(0,i.ensureCellVisible)(this.#h),!0;if(this.grid._focusRow>0)return this.grid._focusRow-=1,this.grid._focusCol=r,e.preventDefault(),(0,i.ensureCellVisible)(this.#h),!0;break;case"ArrowRight":if(this.grid._focusRow<t)return this.grid._focusRow+=1,e.preventDefault(),(0,i.ensureCellVisible)(this.#h),!0;break;case"ArrowLeft":if(this.grid._focusRow>0)return this.grid._focusRow-=1,e.preventDefault(),(0,i.ensureCellVisible)(this.#h),!0}return!1}#b;#m;#f;#R(){if(this.#b&&this.#b>0)return this.#b;const e=this.config.cardRowHeight;return"number"==typeof e&&e>0?e:80}#C(){return this.#m&&this.#m>0?this.#m:this.#v??28}#y(){for(const e of this.rows)if(e.__isGroupRow)return!0;return!1}getRowHeight(e,t){if(this.#t&&this.config.cardRenderer)return e.__isGroupRow?this.#C():this.#R()}#k(){let e=0;for(const t of this.rows)t.__isGroupRow||e++;return e}#H=!1;#c(){if(!this.#t)return;let e=!1;const t=this.#h,i=this.#y(),r=this.#k();if(r!==this.#f&&(this.#f=r,e=!0),i){const t=this.gridElement.querySelector(".data-grid-row.group-row");if(t){const i=t.getBoundingClientRect().height;i>0&&i!==this.#m&&(this.#m=i,e=!0)}}const s=this.config.cardRenderer?".data-grid-row.responsive-card":".data-grid-row:not(.group-row)",o=this.gridElement.querySelector(s);if(o){const r=o.getBoundingClientRect().height;r>0&&r!==this.#b&&(this.#b=r,e=!0,!i&&t._virtualization&&(t._virtualization.rowHeight=r))}e&&!this.#H&&(this.#H=!0,queueMicrotask(()=>{this.#H=!1,this.grid&&this.#t&&this.#h.refreshVirtualWindow?.(!0,!0)}))}};e.ResponsivePlugin=o});
|
|
2
2
|
//# sourceMappingURL=responsive.umd.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"responsive.umd.js","names":["#internalGrid","#isResponsive","#applyResponsiveState","#currentWidth","#checkBreakpoint","#activeBreakpoint","#parseLightDomCard","#buildHiddenColumnSets","#sortedBreakpoints","#resizeObserver","#debounceTimer","#hiddenColumnSet","#valueOnlyColumnSet","#measureCardHeightFromDOM","#checkMultiBreakpoint","#warnedAboutMissingBreakpoint","#originalRowHeight","#measuredCardHeight","#measuredGroupRowHeight","#lastCardRowCount","#getCardHeight","#getGroupRowHeight","#hasGroupRows","#countCardRows","#pendingRefresh"],"sources":["../../../../../libs/grid/src/lib/plugins/responsive/responsive.css?inline","../../../../../libs/grid/src/lib/plugins/responsive/ResponsivePlugin.ts"],"sourcesContent":["/**\n * Responsive Plugin CSS\n *\n * Card layout styles for narrow viewports.\n * Applied when [data-responsive] attribute is present on the grid.\n *\n * IMPORTANT: This CSS intentionally preserves the grid's virtualization system.\n * The row height observer will detect the new card height automatically and\n * recalculate the virtual window. Do NOT break virtualization by:\n * - Changing .rows-viewport overflow\n * - Changing .rows position\n * - Hiding .faux-vscroll\n */\n\n/* ============================================\n Animation / Transition Support\n ============================================ */\n\n/* Base transition setup for animated mode switching */\ntbw-grid[data-responsive-animate] {\n .data-grid-row,\n .data-grid-row > .cell {\n transition:\n opacity var(--tbw-responsive-duration, 200ms) ease-out,\n transform var(--tbw-responsive-duration, 200ms) ease-out;\n }\n}\n\n/* Fade-in when entering responsive mode */\ntbw-grid[data-responsive][data-responsive-animate] {\n .data-grid-row {\n animation: responsive-card-enter var(--tbw-responsive-duration, 200ms) ease-out;\n }\n}\n\n@keyframes responsive-card-enter {\n from {\n opacity: 0;\n transform: translateY(-10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* ============================================\n Responsive Card Layout\n ============================================ */\n\ntbw-grid[data-responsive] {\n /* Hide entire header section in card mode */\n .header {\n display: none !important;\n }\n\n /* Hide footer row in card mode (aggregations don't make sense in cards) */\n .footer-row {\n display: none;\n }\n\n /* Override horizontal scroll - single column flow */\n .tbw-scroll-area {\n overflow-x: hidden;\n min-width: 0 !important;\n }\n\n /* Override rows-body-wrapper to allow natural flow */\n .rows-body-wrapper {\n min-width: 0 !important;\n }\n\n /* Convert row from CSS Grid to block layout */\n .data-grid-row:not(.group-row) {\n display: block !important;\n grid-template-columns: none !important;\n padding: var(--tbw-cell-padding);\n padding-inline-start: var(--tbw-spacing-xl);\n border-bottom: 1px solid var(--tbw-color-border);\n min-height: auto !important;\n height: auto !important;\n /* Disable containment so row can grow to fit stacked cells */\n contain: none !important;\n content-visibility: visible !important;\n background: var(--tbw-color-bg);\n position: relative;\n\n &:nth-child(even) {\n background: var(--tbw-color-row-alt);\n }\n\n &:hover {\n background: var(--tbw-color-row-hover);\n }\n\n &[aria-selected='true'] {\n background: var(--tbw-color-selection);\n\n /* Selection indicator bar */\n &::before {\n content: '';\n position: absolute;\n inset-inline-start: 0;\n top: 0;\n bottom: 0;\n width: 4px;\n background: var(--tbw-color-accent);\n }\n }\n }\n\n /* Stack cells vertically as label: value pairs */\n .data-grid-row:not(.group-row) > .cell {\n display: flex !important;\n justify-content: space-between;\n align-items: center;\n padding: var(--tbw-spacing-xs) var(--tbw-spacing-md);\n width: 100% !important;\n min-width: 0 !important;\n min-height: auto !important;\n height: auto !important;\n line-height: 1.5 !important;\n position: static !important;\n left: auto !important;\n right: auto !important;\n border: none !important;\n border-bottom: none !important;\n border-inline-end: none !important;\n background: transparent !important;\n white-space: normal !important;\n overflow: visible !important;\n\n /* Header label from data-header attribute */\n &::before {\n content: attr(data-header) ': ';\n font-weight: 600;\n color: var(--tbw-color-header-fg);\n flex-shrink: 0;\n margin-inline-end: var(--tbw-spacing-md);\n min-width: 100px;\n }\n\n /* Value takes remaining space */\n &::after {\n content: none;\n }\n }\n\n /* Hide utility columns (row expander, selection checkbox, etc.) */\n .cell[data-utility] {\n display: none !important;\n }\n}\n\n/* Hidden columns — specificity must beat `.data-grid-row:not(.group-row) > .cell` above */\ntbw-grid[data-responsive] .data-grid-row > .cell[data-responsive-hidden] {\n display: none !important;\n}\n\n/* Value-only columns in responsive mode (hides label, shows value full-width) */\ntbw-grid[data-responsive] .data-grid-row > .cell[data-responsive-value-only] {\n /* Override the header label ::before to hide it */\n &::before {\n display: none !important;\n }\n\n /* Value takes full width, no justify-content needed */\n justify-content: flex-start !important;\n font-weight: 500;\n}\n\n/* Opt-in: hide per-card field labels (the \"Name:\" prefix) when\n `hideHeader: true` is configured. Toggled by ResponsivePlugin. */\ntbw-grid[data-responsive][data-responsive-hide-header] .data-grid-row:not(.group-row) > .cell::before {\n display: none !important;\n}\n\n/* Non-responsive mode: progressive column hiding still works */\ntbw-grid:not([data-responsive]) .cell[data-responsive-hidden] {\n display: none !important;\n}\n\ntbw-grid:not([data-responsive]) .cell[data-responsive-value-only] {\n /* In table mode, value-only just hides the cell like normal */\n /* Actually no - in table mode headers are in header row, so this has no effect */\n}\n\n/* Hide pinned/aggregation rows in responsive mode - they don't make sense in card layout */\ntbw-grid[data-responsive] {\n .tbw-footer,\n .tbw-header-pinned,\n .tbw-pinned-rows,\n .tbw-aggregation-rows {\n display: none !important;\n }\n\n /* Reset min-widths that hold horizontal scroll width */\n .tbw-pinned-rows,\n .tbw-aggregation-rows,\n .tbw-aggregation-row {\n min-width: 0 !important;\n }\n}\n\n/* Custom card renderer styling */\ntbw-grid[data-responsive] .data-grid-row.responsive-card {\n /* Override default cell layout - custom content takes full width */\n display: block !important;\n padding: var(--tbw-cell-padding);\n border-bottom: 1px solid var(--tbw-color-border);\n\n /* Card content should not have cell-based constraints */\n > * {\n width: 100%;\n }\n\n /* Remove default cell ::before headers - custom renderer handles layout */\n .cell::before {\n display: none;\n }\n}\n","/**\n * Responsive Plugin\n *\n * Transforms the grid from tabular layout to a card/list layout when the grid\n * width falls below a configurable breakpoint. This enables grids to work in\n * narrow containers (split-pane UIs, mobile viewports, dashboard widgets).\n *\n * ## Installation\n *\n * ```ts\n * import { ResponsivePlugin } from '@toolbox-web/grid/plugins/responsive';\n *\n * const config: GridConfig = {\n * plugins: [new ResponsivePlugin({ breakpoint: 500 })],\n * };\n * ```\n *\n * ## How It Works\n *\n * 1. ResizeObserver monitors the grid element's width\n * 2. When `width < breakpoint`, adds `data-responsive` attribute to grid\n * 3. CSS transforms cells from horizontal to vertical layout\n * 4. Each cell displays \"Header: Value\" using CSS `::before` pseudo-element\n *\n * @see [Responsive Demo](?path=/story/grid-plugins-responsive--default)\n */\n\nimport { MISSING_BREAKPOINT } from '../../core/internal/diagnostics';\nimport { ensureCellVisible } from '../../core/internal/keyboard';\nimport { evalTemplateString, sanitizeHTML } from '../../core/internal/sanitize';\nimport { BaseGridPlugin, type GridElement, type PluginManifest, type PluginQuery } from '../../core/plugin/base-plugin';\nimport type { GridHost } from '../../core/types';\nimport styles from './responsive.css?inline';\nimport type { BreakpointConfig, HiddenColumnConfig, ResponsiveChangeDetail, ResponsivePluginConfig } from './types';\n\n/**\n * Responsive Plugin for tbw-grid\n *\n * Adds automatic card layout mode when the grid width falls below a configurable\n * breakpoint. Perfect for responsive designs, split-pane UIs, and mobile viewports.\n *\n * @template T The row data type\n *\n * @example\n * ```ts\n * // Basic usage - switch to card layout below 500px\n * const config: GridConfig = {\n * plugins: [new ResponsivePlugin({ breakpoint: 500 })],\n * };\n * ```\n *\n * @example\n * ```ts\n * // Hide less important columns in card mode\n * const config: GridConfig = {\n * plugins: [\n * new ResponsivePlugin({\n * breakpoint: 600,\n * hiddenColumns: ['createdAt', 'updatedAt'],\n * }),\n * ],\n * };\n * ```\n *\n * @example\n * ```ts\n * // Custom card renderer for advanced layouts\n * const config: GridConfig = {\n * plugins: [\n * new ResponsivePlugin({\n * breakpoint: 400,\n * cardRenderer: (row) => {\n * const card = document.createElement('div');\n * card.className = 'custom-card';\n * card.innerHTML = `<strong>${row.name}</strong><br>${row.email}`;\n * return card;\n * },\n * }),\n * ],\n * };\n * ```\n * @since 1.1.0\n */\nexport class ResponsivePlugin<T = unknown> extends BaseGridPlugin<ResponsivePluginConfig<T>> {\n readonly name = 'responsive';\n override readonly version = '1.0.0';\n override readonly styles = styles;\n\n /**\n * Plugin manifest declaring queries this plugin handles.\n */\n static override readonly manifest: PluginManifest = {\n queries: [\n {\n type: 'isCardMode',\n description: 'Returns whether the grid is currently in responsive card mode',\n },\n ],\n };\n\n #resizeObserver?: ResizeObserver;\n #isResponsive = false;\n #debounceTimer?: ReturnType<typeof setTimeout>;\n #warnedAboutMissingBreakpoint = false;\n #currentWidth = 0;\n /** Set of column fields to completely hide */\n #hiddenColumnSet: Set<string> = new Set();\n /** Set of column fields to show value only (no header label) */\n #valueOnlyColumnSet: Set<string> = new Set();\n /** Currently active breakpoint, or null if none */\n #activeBreakpoint: BreakpointConfig | null = null;\n /** Sorted breakpoints from largest to smallest */\n #sortedBreakpoints: BreakpointConfig[] = [];\n\n /** Typed internal grid accessor — centralizes the single required cast. */\n get #internalGrid(): GridHost {\n return this.grid as unknown as GridHost;\n }\n\n /**\n * Check if currently in responsive mode.\n * @returns `true` if the grid is in card layout mode\n */\n isResponsive(): boolean {\n return this.#isResponsive;\n }\n\n /**\n * Force responsive mode regardless of width.\n * Useful for testing or manual control.\n * @param enabled - Whether to enable responsive mode\n */\n setResponsive(enabled: boolean): void {\n if (enabled !== this.#isResponsive) {\n this.#isResponsive = enabled;\n this.#applyResponsiveState();\n this.emit('responsive-change', {\n isResponsive: enabled,\n width: this.#currentWidth,\n breakpoint: this.config.breakpoint ?? 0,\n } satisfies ResponsiveChangeDetail);\n this.requestRender();\n }\n }\n\n /**\n * Update breakpoint dynamically.\n * @param width - New breakpoint width in pixels\n */\n setBreakpoint(width: number): void {\n this.config.breakpoint = width;\n this.#checkBreakpoint(this.#currentWidth);\n }\n\n /**\n * Set a custom card renderer.\n * This allows framework adapters to provide template-based renderers at runtime.\n * @param renderer - The card renderer function, or undefined to use default\n */\n setCardRenderer(renderer: ResponsivePluginConfig<T>['cardRenderer']): void {\n this.config.cardRenderer = renderer;\n // If already in responsive mode, trigger a re-render to apply the new renderer\n if (this.#isResponsive) {\n this.requestRender();\n }\n }\n\n /**\n * Get current grid width.\n * @returns Width of the grid element in pixels\n */\n getWidth(): number {\n return this.#currentWidth;\n }\n\n /**\n * Get the currently active breakpoint config (multi-breakpoint mode only).\n * @returns The active BreakpointConfig, or null if no breakpoint is active\n */\n getActiveBreakpoint(): BreakpointConfig | null {\n return this.#activeBreakpoint;\n }\n\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n // Parse light DOM configuration first (may update this.config)\n this.#parseLightDomCard();\n\n // Build hidden column sets from config\n this.#buildHiddenColumnSets(this.config.hiddenColumns);\n\n // Sort breakpoints from largest to smallest for evaluation\n if (this.config.breakpoints?.length) {\n this.#sortedBreakpoints = [...this.config.breakpoints].sort((a, b) => b.maxWidth - a.maxWidth);\n }\n\n // Observe the grid element itself (not internal viewport)\n // This captures the container width including when shell panels open/close\n this.#resizeObserver = new ResizeObserver((entries) => {\n const width = entries[0]?.contentRect.width ?? 0;\n this.#currentWidth = width;\n\n // Debounce to avoid thrashing during resize drag\n clearTimeout(this.#debounceTimer);\n this.#debounceTimer = setTimeout(() => {\n this.#checkBreakpoint(width);\n }, this.config.debounceMs ?? 100);\n });\n\n this.#resizeObserver.observe(this.gridElement);\n }\n\n // #region Light DOM Parsing\n\n /**\n * Parse `<tbw-grid-responsive-card>` elements from the grid's light DOM.\n *\n * Allows declarative configuration:\n * ```html\n * <tbw-grid [rows]=\"data\">\n * <tbw-grid-responsive-card breakpoint=\"500\" card-row-height=\"80\">\n * <div class=\"custom-card\">\n * <strong>{{ row.name }}</strong>\n * <span>{{ row.email }}</span>\n * </div>\n * </tbw-grid-responsive-card>\n * </tbw-grid>\n * ```\n *\n * Attributes:\n * - `breakpoint`: number - Width threshold for responsive mode\n * - `card-row-height`: number | 'auto' - Card height (default: 'auto')\n * - `hidden-columns`: string - Comma-separated fields to hide\n * - `hide-header`: 'true' | 'false' - Hide per-card field labels (default: 'false')\n * - `debounce-ms`: number - Resize debounce delay (default: 100)\n */\n #parseLightDomCard(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n const cardEl = gridEl.querySelector('tbw-grid-responsive-card');\n if (!cardEl) return;\n\n // Check if a framework adapter wants to handle this element\n // (e.g., React adapter intercepts for JSX rendering)\n const adapter = this.#internalGrid.__frameworkAdapter;\n if (adapter?.parseResponsiveCardElement) {\n const adapterRenderer = adapter.parseResponsiveCardElement(cardEl);\n if (adapterRenderer) {\n this.config = { ...this.config, cardRenderer: adapterRenderer };\n // Continue to parse attributes even if adapter provides renderer\n }\n }\n\n // Parse attributes for configuration\n const breakpointAttr = cardEl.getAttribute('breakpoint');\n const cardRowHeightAttr = cardEl.getAttribute('card-row-height');\n const hiddenColumnsAttr = cardEl.getAttribute('hidden-columns');\n const hideHeaderAttr = cardEl.getAttribute('hide-header');\n const debounceMsAttr = cardEl.getAttribute('debounce-ms');\n\n const configUpdates: Partial<ResponsivePluginConfig<T>> = {};\n\n if (breakpointAttr !== null) {\n const breakpoint = parseInt(breakpointAttr, 10);\n if (!isNaN(breakpoint)) {\n configUpdates.breakpoint = breakpoint;\n }\n }\n\n if (cardRowHeightAttr !== null) {\n configUpdates.cardRowHeight = cardRowHeightAttr === 'auto' ? 'auto' : parseInt(cardRowHeightAttr, 10);\n }\n\n if (hiddenColumnsAttr !== null) {\n // Parse comma-separated field names\n configUpdates.hiddenColumns = hiddenColumnsAttr\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n }\n\n if (hideHeaderAttr !== null) {\n configUpdates.hideHeader = hideHeaderAttr !== 'false';\n }\n\n if (debounceMsAttr !== null) {\n const debounceMs = parseInt(debounceMsAttr, 10);\n if (!isNaN(debounceMs)) {\n configUpdates.debounceMs = debounceMs;\n }\n }\n\n // Get template content from innerHTML (only if no renderer already set)\n const templateHTML = cardEl.innerHTML.trim();\n if (templateHTML && !this.config.cardRenderer && !adapter?.parseResponsiveCardElement) {\n // Create a template-based renderer using the inner HTML\n configUpdates.cardRenderer = (row: T): HTMLElement => {\n // Evaluate template expressions like {{ row.field }}\n const evaluated = evalTemplateString(templateHTML, { value: row, row: row as Record<string, unknown> });\n // Sanitize the result to prevent XSS\n const sanitized = sanitizeHTML(evaluated);\n const container = document.createElement('div');\n container.className = 'tbw-responsive-card-content';\n container.innerHTML = sanitized;\n return container;\n };\n }\n\n // Merge updates into config (light DOM values override constructor config)\n if (Object.keys(configUpdates).length > 0) {\n this.config = { ...this.config, ...configUpdates };\n }\n }\n\n // #endregion\n\n /**\n * Build the hidden and value-only column sets from config.\n */\n #buildHiddenColumnSets(hiddenColumns?: HiddenColumnConfig[]): void {\n this.#hiddenColumnSet.clear();\n this.#valueOnlyColumnSet.clear();\n\n if (!hiddenColumns) return;\n\n for (const col of hiddenColumns) {\n if (typeof col === 'string') {\n this.#hiddenColumnSet.add(col);\n } else if (col.showValue) {\n this.#valueOnlyColumnSet.add(col.field);\n } else {\n this.#hiddenColumnSet.add(col.field);\n }\n }\n }\n\n override detach(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = undefined;\n clearTimeout(this.#debounceTimer);\n this.#debounceTimer = undefined;\n\n // Clean up attribute\n if (this.gridElement) {\n this.gridElement.removeAttribute('data-responsive');\n }\n\n super.detach();\n }\n\n /**\n * Handle plugin queries.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'isCardMode') {\n return this.#isResponsive;\n }\n return undefined;\n }\n\n /**\n * Apply hidden and value-only columns.\n * In legacy mode (single breakpoint), only applies when in responsive mode.\n * In multi-breakpoint mode, applies whenever there's an active breakpoint.\n */\n override afterRender(): void {\n // Measure card height for virtualization calculations\n this.#measureCardHeightFromDOM();\n\n // In single breakpoint mode, only apply when responsive\n // In multi-breakpoint mode, apply when there's an active breakpoint\n const shouldApply = this.#sortedBreakpoints.length > 0 ? this.#activeBreakpoint !== null : this.#isResponsive;\n\n if (!shouldApply) {\n return;\n }\n\n const hasHiddenColumns = this.#hiddenColumnSet.size > 0;\n const hasValueOnlyColumns = this.#valueOnlyColumnSet.size > 0;\n\n if (!hasHiddenColumns && !hasValueOnlyColumns) {\n return;\n }\n\n // Mark cells for hidden columns and value-only columns\n const cells = this.gridElement.querySelectorAll('.cell[data-field]');\n for (const cell of cells) {\n const field = cell.getAttribute('data-field');\n if (!field) continue;\n\n // Apply hidden attribute\n if (this.#hiddenColumnSet.has(field)) {\n cell.setAttribute('data-responsive-hidden', '');\n cell.removeAttribute('data-responsive-value-only');\n }\n // Apply value-only attribute (shows value without header label)\n else if (this.#valueOnlyColumnSet.has(field)) {\n cell.setAttribute('data-responsive-value-only', '');\n cell.removeAttribute('data-responsive-hidden');\n }\n // Clear any previous responsive attributes\n else {\n cell.removeAttribute('data-responsive-hidden');\n cell.removeAttribute('data-responsive-value-only');\n }\n }\n }\n\n /**\n * Check if width has crossed any breakpoint threshold.\n * Handles both single breakpoint (legacy) and multi-breakpoint modes.\n */\n #checkBreakpoint(width: number): void {\n // Multi-breakpoint mode\n if (this.#sortedBreakpoints.length > 0) {\n this.#checkMultiBreakpoint(width);\n return;\n }\n\n // Legacy single breakpoint mode\n const breakpoint = this.config.breakpoint ?? 0;\n\n // Warn once if breakpoint not configured (0 means never responsive)\n if (breakpoint === 0 && !this.#warnedAboutMissingBreakpoint) {\n this.#warnedAboutMissingBreakpoint = true;\n this.warn(\n MISSING_BREAKPOINT,\n \"No breakpoint configured. Responsive mode is disabled. Set a breakpoint based on your grid's column count.\",\n );\n }\n\n const shouldBeResponsive = breakpoint > 0 && width < breakpoint;\n\n if (shouldBeResponsive !== this.#isResponsive) {\n this.#isResponsive = shouldBeResponsive;\n this.#applyResponsiveState();\n this.emit('responsive-change', {\n isResponsive: shouldBeResponsive,\n width,\n breakpoint,\n } satisfies ResponsiveChangeDetail);\n this.requestRender();\n }\n }\n\n /**\n * Check breakpoints in multi-breakpoint mode.\n * Evaluates breakpoints from largest to smallest, applying the first match.\n */\n #checkMultiBreakpoint(width: number): void {\n // Find the active breakpoint (first one where width <= maxWidth)\n // Since sorted largest to smallest, we find the largest matching breakpoint\n let newActiveBreakpoint: BreakpointConfig | null = null;\n\n for (const bp of this.#sortedBreakpoints) {\n if (width <= bp.maxWidth) {\n newActiveBreakpoint = bp;\n // Continue to find the most specific (smallest) matching breakpoint\n }\n }\n\n // Check if breakpoint changed\n const breakpointChanged = newActiveBreakpoint !== this.#activeBreakpoint;\n\n if (breakpointChanged) {\n this.#activeBreakpoint = newActiveBreakpoint;\n\n // Update hidden column sets from active breakpoint\n if (newActiveBreakpoint?.hiddenColumns) {\n this.#buildHiddenColumnSets(newActiveBreakpoint.hiddenColumns);\n } else {\n // Fall back to top-level hiddenColumns config\n this.#buildHiddenColumnSets(this.config.hiddenColumns);\n }\n\n // Determine if we should be in card layout\n const shouldBeResponsive = newActiveBreakpoint?.cardLayout === true;\n\n if (shouldBeResponsive !== this.#isResponsive) {\n this.#isResponsive = shouldBeResponsive;\n this.#applyResponsiveState();\n }\n\n // Emit event for any breakpoint change\n this.emit('responsive-change', {\n isResponsive: this.#isResponsive,\n width,\n breakpoint: newActiveBreakpoint?.maxWidth ?? 0,\n } satisfies ResponsiveChangeDetail);\n\n this.requestRender();\n }\n }\n\n /** Original row height before entering responsive mode, for restoration on exit */\n #originalRowHeight?: number;\n\n /**\n * Apply the responsive state to the grid element.\n * Handles scroll reset when entering responsive mode and row height restoration on exit.\n */\n #applyResponsiveState(): void {\n this.gridElement.toggleAttribute('data-responsive', this.#isResponsive);\n\n // Apply animation attribute if enabled (default: true)\n const animate = this.config.animate !== false;\n this.gridElement.toggleAttribute('data-responsive-animate', animate);\n\n // Toggle per-card label visibility based on hideHeader config.\n // Attribute only meaningful while in card mode; clear it otherwise so\n // the CSS rule cannot accidentally apply to non-responsive layouts.\n this.gridElement.toggleAttribute(\n 'data-responsive-hide-header',\n this.#isResponsive && this.config.hideHeader === true,\n );\n\n // Set custom animation duration if provided\n if (this.config.animationDuration) {\n this.gridElement.style.setProperty('--tbw-responsive-duration', `${this.config.animationDuration}ms`);\n }\n\n // Cast to internal type for virtualization access\n const internalGrid = this.#internalGrid;\n\n if (this.#isResponsive) {\n // Store original row height before responsive mode changes it\n if (internalGrid._virtualization) {\n this.#originalRowHeight = internalGrid._virtualization.rowHeight;\n }\n\n // Reset horizontal scroll position when entering responsive mode\n // The CSS hides overflow but doesn't reset the scroll position\n const scrollArea = this.gridElement.querySelector('.tbw-scroll-area') as HTMLElement | null;\n if (scrollArea) {\n scrollArea.scrollLeft = 0;\n }\n } else {\n // Exiting responsive mode - clean up inline styles set by renderRow\n // The rows are reused from the pool, so we need to remove the card-specific styles\n const rows = this.gridElement.querySelectorAll('.data-grid-row');\n for (const row of rows) {\n (row as HTMLElement).style.height = '';\n row.classList.remove('responsive-card');\n }\n\n // Restore original row height\n if (this.#originalRowHeight && this.#originalRowHeight > 0 && internalGrid._virtualization) {\n internalGrid._virtualization.rowHeight = this.#originalRowHeight;\n this.#originalRowHeight = undefined;\n }\n\n // Clear cached measurements so they're remeasured fresh when re-entering responsive mode\n // Without this, stale measurements cause incorrect height calculations after scrolling\n this.#measuredCardHeight = undefined;\n this.#measuredGroupRowHeight = undefined;\n this.#lastCardRowCount = undefined;\n }\n }\n\n /**\n * Custom row rendering when cardRenderer is provided and in responsive mode.\n *\n * When a cardRenderer is configured, this hook takes over row rendering to display\n * the custom card layout instead of the default cell structure.\n *\n * @param row - The row data object\n * @param rowEl - The row DOM element to render into\n * @param rowIndex - The index of the row in the data array\n * @returns `true` if rendered (prevents default), `void` for default rendering\n */\n override renderRow(row: unknown, rowEl: HTMLElement, rowIndex: number): boolean | void {\n // Only override when in responsive mode AND cardRenderer is provided\n if (!this.#isResponsive || !this.config.cardRenderer) {\n return; // Let default rendering proceed\n }\n\n // Skip group rows from GroupingRowsPlugin - they have special structure\n // and should use their own renderer\n if ((row as { __isGroupRow?: boolean }).__isGroupRow) {\n return; // Let GroupingRowsPlugin handle group row rendering\n }\n\n // Clear existing content\n rowEl.replaceChildren();\n\n // Call user's cardRenderer to get custom content\n const cardContent = this.config.cardRenderer(row as T, rowIndex);\n\n // Reset className - clears any stale classes from previous use (e.g., 'group-row' from recycled element)\n // This follows the same pattern as GroupingRowsPlugin which sets className explicitly\n rowEl.className = 'data-grid-row responsive-card';\n\n // Handle card row height — when a numeric height is configured, use the effective\n // height from #getCardHeight() which incorporates DOM measurement after first render.\n // This keeps inline height in sync with the position cache used for virtualization.\n const configuredHeight = this.config.cardRowHeight;\n if (configuredHeight === 'auto' || configuredHeight === undefined) {\n rowEl.style.height = 'auto';\n } else {\n rowEl.style.height = `${this.#getCardHeight()}px`;\n }\n\n // Append the custom card content\n rowEl.appendChild(cardContent);\n\n return true; // We handled rendering\n }\n\n /**\n * Handle keyboard navigation in responsive mode.\n *\n * In responsive mode, the visual layout is inverted:\n * - Cells are stacked vertically within each \"card\" (row)\n * - DOWN/UP visually moves within the card (between fields)\n * - Page Down/Page Up or Ctrl+Down/Up moves between cards\n *\n * For custom cardRenderers, keyboard navigation is disabled entirely\n * since the implementor controls the card content and should handle\n * navigation via their own event handlers.\n *\n * @returns `true` if the event was handled and default behavior should be prevented\n */\n override onKeyDown(e: KeyboardEvent): boolean {\n if (!this.#isResponsive) {\n return false;\n }\n\n // If custom cardRenderer is provided, disable grid's keyboard navigation\n // The implementor is responsible for their own navigation\n if (this.config.cardRenderer) {\n const navKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];\n if (navKeys.includes(e.key)) {\n // Let the event bubble - implementor can handle it\n return false;\n }\n }\n\n // Swap arrow key behavior for CSS-only responsive mode\n // In card layout, cells are stacked vertically:\n // Card 1: Card 2:\n // ID: 1 ID: 2\n // Name: Alice Name: Bob <- ArrowRight goes here\n // Dept: Eng Dept: Mkt\n // ↓ ArrowDown goes here\n //\n // ArrowDown/Up = move within card (change column/field)\n // ArrowRight/Left = move between cards (change row)\n const maxRow = this.rows.length - 1;\n const maxCol = this.visibleColumns.length - 1;\n\n switch (e.key) {\n case 'ArrowDown':\n // Move down WITHIN card (to next field/column)\n if (this.grid._focusCol < maxCol) {\n this.grid._focusCol += 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n // At bottom of card - optionally move to next card's first field\n if (this.grid._focusRow < maxRow) {\n this.grid._focusRow += 1;\n this.grid._focusCol = 0;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n\n case 'ArrowUp':\n // Move up WITHIN card (to previous field/column)\n if (this.grid._focusCol > 0) {\n this.grid._focusCol -= 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n // At top of card - optionally move to previous card's last field\n if (this.grid._focusRow > 0) {\n this.grid._focusRow -= 1;\n this.grid._focusCol = maxCol;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n\n case 'ArrowRight':\n // Move to NEXT card (same field)\n if (this.grid._focusRow < maxRow) {\n this.grid._focusRow += 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n\n case 'ArrowLeft':\n // Move to PREVIOUS card (same field)\n if (this.grid._focusRow > 0) {\n this.grid._focusRow -= 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n }\n\n return false;\n }\n\n // ============================================\n // Variable Height Support for Mixed Row Types\n // ============================================\n\n /** Measured card height from DOM for virtualization calculations */\n #measuredCardHeight?: number;\n\n /** Measured group row height from DOM for virtualization calculations */\n #measuredGroupRowHeight?: number;\n\n /** Last known card row count for detecting changes (e.g., group expand/collapse) */\n #lastCardRowCount?: number;\n\n /**\n * Get the effective card height for virtualization calculations.\n * Prioritizes DOM-measured height (actual rendered size) over config,\n * since content can overflow the configured height.\n */\n #getCardHeight(): number {\n // Prefer measured height - it reflects actual rendered size including overflow\n if (this.#measuredCardHeight && this.#measuredCardHeight > 0) {\n return this.#measuredCardHeight;\n }\n // Fall back to explicit config\n const configHeight = this.config.cardRowHeight;\n if (typeof configHeight === 'number' && configHeight > 0) {\n return configHeight;\n }\n // Default fallback\n return 80;\n }\n\n /**\n * Get the effective group row height for virtualization calculations.\n * Uses DOM-measured height, falling back to original row height.\n */\n #getGroupRowHeight(): number {\n if (this.#measuredGroupRowHeight && this.#measuredGroupRowHeight > 0) {\n return this.#measuredGroupRowHeight;\n }\n // Fall back to original row height (before responsive mode)\n return this.#originalRowHeight ?? 28;\n }\n\n /**\n * Check if there are any group rows in the current dataset.\n * Used to determine if we have mixed row heights.\n */\n #hasGroupRows(): boolean {\n for (const row of this.rows) {\n if ((row as { __isGroupRow?: boolean }).__isGroupRow) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Get the height of a specific row based on its type (group row vs card row).\n * Returns undefined if not in responsive mode.\n *\n * @param row - The row data\n * @param _index - The row index (unused, but part of the interface)\n * @returns The row height in pixels, or undefined if not in responsive mode\n */\n override getRowHeight(row: unknown, _index: number): number | undefined {\n // Only applies when in responsive mode with cardRenderer\n if (!this.#isResponsive || !this.config.cardRenderer) {\n return undefined;\n }\n\n // Check if this is a group row\n if ((row as { __isGroupRow?: boolean }).__isGroupRow) {\n return this.#getGroupRowHeight();\n }\n\n // Regular card row\n return this.#getCardHeight();\n }\n\n /**\n * Count the number of card rows (non-group rows) in the current dataset.\n */\n #countCardRows(): number {\n let count = 0;\n for (const row of this.rows) {\n if (!(row as { __isGroupRow?: boolean }).__isGroupRow) {\n count++;\n }\n }\n return count;\n }\n\n /** Pending refresh scheduled via microtask */\n #pendingRefresh = false;\n\n /**\n * Measure card height from DOM after render and detect row count changes.\n * Called in afterRender to ensure scroll calculations are accurate.\n *\n * This handles two scenarios:\n * 1. Card height changes (content overflow, dynamic sizing)\n * 2. Card row count changes (group expand/collapse)\n * 3. Group row height changes\n *\n * For uniform card layouts (no groups), we update the virtualization row height\n * directly to the card height. For mixed layouts (groups + cards), we use the\n * getExtraHeight mechanism to report height differences.\n *\n * The refresh is deferred via microtask to avoid nested render cycles.\n */\n #measureCardHeightFromDOM(): void {\n if (!this.#isResponsive) {\n return;\n }\n\n let needsRefresh = false;\n const internalGrid = this.#internalGrid;\n const hasGroups = this.#hasGroupRows();\n\n // Check if card row count changed (e.g., group expanded/collapsed)\n const currentCardRowCount = this.#countCardRows();\n if (currentCardRowCount !== this.#lastCardRowCount) {\n this.#lastCardRowCount = currentCardRowCount;\n needsRefresh = true;\n }\n\n // Measure actual group row height from DOM (for mixed layouts)\n if (hasGroups) {\n const groupRow = this.gridElement.querySelector('.data-grid-row.group-row') as HTMLElement | null;\n if (groupRow) {\n const height = groupRow.getBoundingClientRect().height;\n if (height > 0 && height !== this.#measuredGroupRowHeight) {\n this.#measuredGroupRowHeight = height;\n needsRefresh = true;\n }\n }\n }\n\n // Measure actual card height from DOM.\n // With cardRenderer, rows have the `.responsive-card` class.\n // Without cardRenderer (CSS-only card mode), rows are plain `.data-grid-row`\n // elements whose CSS height is `auto` — we need to measure their actual\n // rendered height so the faux scrollbar spacer is sized correctly.\n const cardSelector = this.config.cardRenderer ? '.data-grid-row.responsive-card' : '.data-grid-row:not(.group-row)';\n const cardRow = this.gridElement.querySelector(cardSelector) as HTMLElement | null;\n if (cardRow) {\n const height = cardRow.getBoundingClientRect().height;\n if (height > 0 && height !== this.#measuredCardHeight) {\n this.#measuredCardHeight = height;\n needsRefresh = true;\n\n // For uniform card layouts (no groups), update virtualization row height directly\n // This ensures proper row recycling and translateY calculations\n if (!hasGroups && internalGrid._virtualization) {\n internalGrid._virtualization.rowHeight = height;\n }\n }\n }\n\n // Defer virtualization refresh to avoid nested render cycles\n // This is called from afterRender, so we can't call refreshVirtualWindow synchronously\n // Use scheduler's VIRTUALIZATION phase to batch properly and avoid duplicate afterRender calls\n if (needsRefresh && !this.#pendingRefresh) {\n this.#pendingRefresh = true;\n queueMicrotask(() => {\n this.#pendingRefresh = false;\n // Only refresh if still attached and in responsive mode\n if (this.grid && this.#isResponsive) {\n // Request virtualization phase through grid's public API\n // This goes through the scheduler which batches and handles afterRender properly\n this.#internalGrid.refreshVirtualWindow?.(true, true);\n }\n });\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"responsive.umd.js","names":["#internalGrid","#isResponsive","#applyResponsiveState","#currentWidth","#checkBreakpoint","#activeBreakpoint","#parseLightDomCard","#buildHiddenColumnSets","#sortedBreakpoints","#resizeObserver","#debounceTimer","#hiddenColumnSet","#valueOnlyColumnSet","#measureCardHeightFromDOM","#checkMultiBreakpoint","#warnedAboutMissingBreakpoint","#originalRowHeight","#measuredCardHeight","#measuredGroupRowHeight","#lastCardRowCount","#getCardHeight","#getGroupRowHeight","#hasGroupRows","#countCardRows","#pendingRefresh"],"sources":["../../../../../libs/grid/src/lib/plugins/responsive/responsive.css?inline","../../../../../libs/grid/src/lib/plugins/responsive/ResponsivePlugin.ts"],"sourcesContent":["/**\n * Responsive Plugin CSS\n *\n * Card layout styles for narrow viewports.\n * Applied when [data-responsive] attribute is present on the grid.\n *\n * IMPORTANT: This CSS intentionally preserves the grid's virtualization system.\n * The row height observer will detect the new card height automatically and\n * recalculate the virtual window. Do NOT break virtualization by:\n * - Changing .rows-viewport overflow\n * - Changing .rows position\n * - Hiding .faux-vscroll\n */\n\n/* ============================================\n Animation / Transition Support\n ============================================ */\n\n/* Base transition setup for animated mode switching */\ntbw-grid[data-responsive-animate] {\n .data-grid-row,\n .data-grid-row > .cell {\n transition:\n opacity var(--tbw-responsive-duration, 200ms) ease-out,\n transform var(--tbw-responsive-duration, 200ms) ease-out;\n }\n}\n\n/* Fade-in when entering responsive mode */\ntbw-grid[data-responsive][data-responsive-animate] {\n .data-grid-row {\n animation: responsive-card-enter var(--tbw-responsive-duration, 200ms) ease-out;\n }\n}\n\n@keyframes responsive-card-enter {\n from {\n opacity: 0;\n transform: translateY(-10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* ============================================\n Responsive Card Layout\n ============================================ */\n\ntbw-grid[data-responsive] {\n /* Hide entire header section in card mode */\n .header {\n display: none !important;\n }\n\n /* Hide footer row in card mode (aggregations don't make sense in cards) */\n .footer-row {\n display: none;\n }\n\n /* Override horizontal scroll - single column flow */\n .tbw-scroll-area {\n overflow-x: hidden;\n min-width: 0 !important;\n }\n\n /* Override rows-body-wrapper to allow natural flow */\n .rows-body-wrapper {\n min-width: 0 !important;\n }\n\n /* Convert row from CSS Grid to block layout */\n .data-grid-row:not(.group-row) {\n display: block !important;\n grid-template-columns: none !important;\n padding: var(--tbw-cell-padding);\n padding-inline-start: var(--tbw-spacing-xl);\n border-bottom: 1px solid var(--tbw-color-border);\n min-height: auto !important;\n height: auto !important;\n /* Disable containment so row can grow to fit stacked cells */\n contain: none !important;\n content-visibility: visible !important;\n background: var(--tbw-color-bg);\n position: relative;\n\n &:nth-child(even) {\n background: var(--tbw-color-row-alt);\n }\n\n &:hover {\n background: var(--tbw-color-row-hover);\n }\n\n &[aria-selected='true'] {\n background: var(--tbw-color-selection);\n\n /* Selection indicator bar */\n &::before {\n content: '';\n position: absolute;\n inset-inline-start: 0;\n top: 0;\n bottom: 0;\n width: 4px;\n background: var(--tbw-color-accent);\n }\n }\n }\n\n /* Stack cells vertically as label: value pairs */\n .data-grid-row:not(.group-row) > .cell {\n display: flex !important;\n justify-content: space-between;\n align-items: center;\n padding: var(--tbw-spacing-xs) var(--tbw-spacing-md);\n width: 100% !important;\n min-width: 0 !important;\n min-height: auto !important;\n height: auto !important;\n line-height: 1.5 !important;\n position: static !important;\n left: auto !important;\n right: auto !important;\n border: none !important;\n border-bottom: none !important;\n border-inline-end: none !important;\n background: transparent !important;\n white-space: normal !important;\n overflow: visible !important;\n\n /* Header label from data-header attribute */\n &::before {\n content: attr(data-header) ': ';\n font-weight: 600;\n color: var(--tbw-color-header-fg);\n flex-shrink: 0;\n margin-inline-end: var(--tbw-spacing-md);\n min-width: 100px;\n }\n\n /* Value takes remaining space */\n &::after {\n content: none;\n }\n }\n\n /* Hide utility columns (row expander, selection checkbox, etc.) */\n .cell[data-utility] {\n display: none !important;\n }\n}\n\n/* Hidden columns — specificity must beat `.data-grid-row:not(.group-row) > .cell` above */\ntbw-grid[data-responsive] .data-grid-row > .cell[data-responsive-hidden] {\n display: none !important;\n}\n\n/* Value-only columns in responsive mode (hides label, shows value full-width) */\ntbw-grid[data-responsive] .data-grid-row > .cell[data-responsive-value-only] {\n /* Override the header label ::before to hide it */\n &::before {\n display: none !important;\n }\n\n /* Value takes full width, no justify-content needed */\n justify-content: flex-start !important;\n font-weight: 500;\n}\n\n/* Opt-in: hide per-card field labels (the \"Name:\" prefix) when\n `hideHeader: true` is configured. Toggled by ResponsivePlugin. */\ntbw-grid[data-responsive][data-responsive-hide-header] .data-grid-row:not(.group-row) > .cell::before {\n display: none !important;\n}\n\n/* Non-responsive mode: progressive column hiding still works */\ntbw-grid:not([data-responsive]) .cell[data-responsive-hidden] {\n display: none !important;\n}\n\ntbw-grid:not([data-responsive]) .cell[data-responsive-value-only] {\n /* In table mode, value-only just hides the cell like normal */\n /* Actually no - in table mode headers are in header row, so this has no effect */\n}\n\n/* Hide pinned/aggregation rows in responsive mode - they don't make sense in card layout */\ntbw-grid[data-responsive] {\n .tbw-footer,\n .tbw-header-pinned,\n .tbw-pinned-rows,\n .tbw-aggregation-rows {\n display: none !important;\n }\n\n /* Reset min-widths that hold horizontal scroll width */\n .tbw-pinned-rows,\n .tbw-aggregation-rows,\n .tbw-aggregation-row {\n min-width: 0 !important;\n }\n}\n\n/* Custom card renderer styling */\ntbw-grid[data-responsive] .data-grid-row.responsive-card {\n /* Override default cell layout - custom content takes full width */\n display: block !important;\n padding: var(--tbw-cell-padding);\n border-bottom: 1px solid var(--tbw-color-border);\n\n /* Card content should not have cell-based constraints */\n > * {\n width: 100%;\n }\n\n /* Remove default cell ::before headers - custom renderer handles layout */\n .cell::before {\n display: none;\n }\n}\n","/**\n * Responsive Plugin\n *\n * Transforms the grid from tabular layout to a card/list layout when the grid\n * width falls below a configurable breakpoint. This enables grids to work in\n * narrow containers (split-pane UIs, mobile viewports, dashboard widgets).\n *\n * ## Installation\n *\n * ```ts\n * import { ResponsivePlugin } from '@toolbox-web/grid/plugins/responsive';\n *\n * const config: GridConfig = {\n * plugins: [new ResponsivePlugin({ breakpoint: 500 })],\n * };\n * ```\n *\n * ## How It Works\n *\n * 1. ResizeObserver monitors the grid element's width\n * 2. When `width < breakpoint`, adds `data-responsive` attribute to grid\n * 3. CSS transforms cells from horizontal to vertical layout\n * 4. Each cell displays \"Header: Value\" using CSS `::before` pseudo-element\n *\n * @see [Responsive Demo](?path=/story/grid-plugins-responsive--default)\n */\n\nimport { MISSING_BREAKPOINT } from '../../core/internal/diagnostics';\nimport { ensureCellVisible } from '../../core/internal/keyboard';\nimport { evalTemplateString, sanitizeHTML } from '../../core/internal/sanitize';\nimport { BaseGridPlugin, type GridElement, type PluginManifest, type PluginQuery } from '../../core/plugin/base-plugin';\nimport type { GridHost } from '../../core/types';\nimport styles from './responsive.css?inline';\nimport type { BreakpointConfig, HiddenColumnConfig, ResponsiveChangeDetail, ResponsivePluginConfig } from './types';\n\n/**\n * Responsive Plugin for tbw-grid\n *\n * Adds automatic card layout mode when the grid width falls below a configurable\n * breakpoint. Perfect for responsive designs, split-pane UIs, and mobile viewports.\n *\n * @template T The row data type\n *\n * @example\n * ```ts\n * // Basic usage - switch to card layout below 500px\n * const config: GridConfig = {\n * plugins: [new ResponsivePlugin({ breakpoint: 500 })],\n * };\n * ```\n *\n * @example\n * ```ts\n * // Hide less important columns in card mode\n * const config: GridConfig = {\n * plugins: [\n * new ResponsivePlugin({\n * breakpoint: 600,\n * hiddenColumns: ['createdAt', 'updatedAt'],\n * }),\n * ],\n * };\n * ```\n *\n * @example\n * ```ts\n * // Custom card renderer for advanced layouts\n * const config: GridConfig = {\n * plugins: [\n * new ResponsivePlugin({\n * breakpoint: 400,\n * cardRenderer: (row) => {\n * const card = document.createElement('div');\n * card.className = 'custom-card';\n * card.innerHTML = `<strong>${row.name}</strong><br>${row.email}`;\n * return card;\n * },\n * }),\n * ],\n * };\n * ```\n * @since 1.1.0\n */\nexport class ResponsivePlugin<T = unknown> extends BaseGridPlugin<ResponsivePluginConfig<T>> {\n readonly name = 'responsive';\n override readonly version = '1.0.0';\n override readonly styles = styles;\n\n /**\n * Plugin manifest declaring queries this plugin handles.\n */\n static override readonly manifest: PluginManifest = {\n queries: [\n {\n type: 'isCardMode',\n description: 'Returns whether the grid is currently in responsive card mode',\n },\n ],\n };\n\n #resizeObserver?: ResizeObserver;\n #isResponsive = false;\n #debounceTimer?: ReturnType<typeof setTimeout>;\n #warnedAboutMissingBreakpoint = false;\n #currentWidth = 0;\n /** Set of column fields to completely hide */\n #hiddenColumnSet: Set<string> = new Set();\n /** Set of column fields to show value only (no header label) */\n #valueOnlyColumnSet: Set<string> = new Set();\n /** Currently active breakpoint, or null if none */\n #activeBreakpoint: BreakpointConfig | null = null;\n /** Sorted breakpoints from largest to smallest */\n #sortedBreakpoints: BreakpointConfig[] = [];\n\n /** Typed internal grid accessor — centralizes the single required cast. */\n get #internalGrid(): GridHost {\n return this.grid as unknown as GridHost;\n }\n\n /**\n * Check if currently in responsive mode.\n * @returns `true` if the grid is in card layout mode\n */\n isResponsive(): boolean {\n return this.#isResponsive;\n }\n\n /**\n * Force responsive mode regardless of width.\n * Useful for testing or manual control.\n * @param enabled - Whether to enable responsive mode\n */\n setResponsive(enabled: boolean): void {\n if (enabled !== this.#isResponsive) {\n this.#isResponsive = enabled;\n this.#applyResponsiveState();\n this.emit('responsive-change', {\n isResponsive: enabled,\n width: this.#currentWidth,\n breakpoint: this.config.breakpoint ?? 0,\n } satisfies ResponsiveChangeDetail);\n this.requestRender();\n }\n }\n\n /**\n * Update breakpoint dynamically.\n * @param width - New breakpoint width in pixels\n */\n setBreakpoint(width: number): void {\n this.config.breakpoint = width;\n this.#checkBreakpoint(this.#currentWidth);\n }\n\n /**\n * Set a custom card renderer.\n * This allows framework adapters to provide template-based renderers at runtime.\n * @param renderer - The card renderer function, or undefined to use default\n */\n setCardRenderer(renderer: ResponsivePluginConfig<T>['cardRenderer']): void {\n this.config.cardRenderer = renderer;\n // If already in responsive mode, trigger a re-render to apply the new renderer\n if (this.#isResponsive) {\n this.requestRender();\n }\n }\n\n /**\n * Get current grid width.\n * @returns Width of the grid element in pixels\n */\n getWidth(): number {\n return this.#currentWidth;\n }\n\n /**\n * Get the currently active breakpoint config (multi-breakpoint mode only).\n * @returns The active BreakpointConfig, or null if no breakpoint is active\n */\n getActiveBreakpoint(): BreakpointConfig | null {\n return this.#activeBreakpoint;\n }\n\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n // Parse light DOM configuration first (may update this.config)\n this.#parseLightDomCard();\n\n // Build hidden column sets from config\n this.#buildHiddenColumnSets(this.config.hiddenColumns);\n\n // Sort breakpoints from largest to smallest for evaluation\n if (this.config.breakpoints?.length) {\n this.#sortedBreakpoints = [...this.config.breakpoints].sort((a, b) => b.maxWidth - a.maxWidth);\n }\n\n // Observe the grid element itself (not internal viewport)\n // This captures the container width including when shell panels open/close\n this.#resizeObserver = new ResizeObserver((entries) => {\n const width = entries[0]?.contentRect.width ?? 0;\n this.#currentWidth = width;\n\n // Debounce to avoid thrashing during resize drag\n clearTimeout(this.#debounceTimer);\n this.#debounceTimer = setTimeout(() => {\n this.#checkBreakpoint(width);\n }, this.config.debounceMs ?? 100);\n });\n\n this.#resizeObserver.observe(this.gridElement);\n }\n\n // #region Light DOM Parsing\n\n /**\n * Parse `<tbw-grid-responsive-card>` elements from the grid's light DOM.\n *\n * Allows declarative configuration:\n * ```html\n * <tbw-grid [rows]=\"data\">\n * <tbw-grid-responsive-card breakpoint=\"500\" card-row-height=\"80\">\n * <div class=\"custom-card\">\n * <strong>{{ row.name }}</strong>\n * <span>{{ row.email }}</span>\n * </div>\n * </tbw-grid-responsive-card>\n * </tbw-grid>\n * ```\n *\n * Attributes:\n * - `breakpoint`: number - Width threshold for responsive mode\n * - `card-row-height`: number | 'auto' - Card height (default: 'auto')\n * - `hidden-columns`: string - Comma-separated fields to hide\n * - `hide-header`: 'true' | 'false' - Hide per-card field labels (default: 'false')\n * - `debounce-ms`: number - Resize debounce delay (default: 100)\n */\n #parseLightDomCard(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n const cardEl = gridEl.querySelector('tbw-grid-responsive-card');\n if (!cardEl) return;\n\n // Check if a framework adapter wants to handle this element\n // (e.g., React adapter intercepts for JSX rendering)\n const adapter = this.#internalGrid.__frameworkAdapter;\n if (adapter?.parseResponsiveCardElement) {\n const adapterRenderer = adapter.parseResponsiveCardElement(cardEl);\n if (adapterRenderer) {\n this.config = { ...this.config, cardRenderer: adapterRenderer };\n // Continue to parse attributes even if adapter provides renderer\n }\n }\n\n // Parse attributes for configuration\n const breakpointAttr = cardEl.getAttribute('breakpoint');\n const cardRowHeightAttr = cardEl.getAttribute('card-row-height');\n const hiddenColumnsAttr = cardEl.getAttribute('hidden-columns');\n const hideHeaderAttr = cardEl.getAttribute('hide-header');\n const debounceMsAttr = cardEl.getAttribute('debounce-ms');\n\n const configUpdates: Partial<ResponsivePluginConfig<T>> = {};\n\n if (breakpointAttr !== null) {\n const breakpoint = parseInt(breakpointAttr, 10);\n if (!isNaN(breakpoint)) {\n configUpdates.breakpoint = breakpoint;\n }\n }\n\n if (cardRowHeightAttr !== null) {\n configUpdates.cardRowHeight = cardRowHeightAttr === 'auto' ? 'auto' : parseInt(cardRowHeightAttr, 10);\n }\n\n if (hiddenColumnsAttr !== null) {\n // Parse comma-separated field names\n configUpdates.hiddenColumns = hiddenColumnsAttr\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n }\n\n if (hideHeaderAttr !== null) {\n configUpdates.hideHeader = hideHeaderAttr !== 'false';\n }\n\n if (debounceMsAttr !== null) {\n const debounceMs = parseInt(debounceMsAttr, 10);\n if (!isNaN(debounceMs)) {\n configUpdates.debounceMs = debounceMs;\n }\n }\n\n // Get template content from innerHTML (only if no renderer already set)\n const templateHTML = cardEl.innerHTML.trim();\n if (templateHTML && !this.config.cardRenderer && !adapter?.parseResponsiveCardElement) {\n // Create a template-based renderer using the inner HTML\n configUpdates.cardRenderer = (row: T): HTMLElement => {\n // Evaluate template expressions like {{ row.field }}\n const evaluated = evalTemplateString(templateHTML, { value: row, row: row as Record<string, unknown> });\n // Sanitize the result to prevent XSS\n const sanitized = sanitizeHTML(evaluated);\n const container = document.createElement('div');\n container.className = 'tbw-responsive-card-content';\n container.innerHTML = sanitized;\n return container;\n };\n }\n\n // Merge updates into config (light DOM values override constructor config)\n if (Object.keys(configUpdates).length > 0) {\n this.config = { ...this.config, ...configUpdates };\n }\n }\n\n // #endregion\n\n /**\n * Build the hidden and value-only column sets from config.\n */\n #buildHiddenColumnSets(hiddenColumns?: HiddenColumnConfig[]): void {\n this.#hiddenColumnSet.clear();\n this.#valueOnlyColumnSet.clear();\n\n if (!hiddenColumns) return;\n\n for (const col of hiddenColumns) {\n if (typeof col === 'string') {\n this.#hiddenColumnSet.add(col);\n } else if (col.showValue) {\n this.#valueOnlyColumnSet.add(col.field);\n } else {\n this.#hiddenColumnSet.add(col.field);\n }\n }\n }\n\n override detach(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = undefined;\n clearTimeout(this.#debounceTimer);\n this.#debounceTimer = undefined;\n\n // Clean up attribute\n if (this.gridElement) {\n this.gridElement.removeAttribute('data-responsive');\n }\n\n super.detach();\n }\n\n /**\n * Handle plugin queries.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'isCardMode') {\n return this.#isResponsive;\n }\n return undefined;\n }\n\n /**\n * Apply hidden and value-only columns.\n * In legacy mode (single breakpoint), only applies when in responsive mode.\n * In multi-breakpoint mode, applies whenever there's an active breakpoint.\n */\n override afterRender(): void {\n // Measure card height for virtualization calculations\n this.#measureCardHeightFromDOM();\n\n // In single breakpoint mode, only apply when responsive\n // In multi-breakpoint mode, apply when there's an active breakpoint\n const shouldApply = this.#sortedBreakpoints.length > 0 ? this.#activeBreakpoint !== null : this.#isResponsive;\n\n if (!shouldApply) {\n return;\n }\n\n const hasHiddenColumns = this.#hiddenColumnSet.size > 0;\n const hasValueOnlyColumns = this.#valueOnlyColumnSet.size > 0;\n\n if (!hasHiddenColumns && !hasValueOnlyColumns) {\n return;\n }\n\n // Mark cells for hidden columns and value-only columns\n const cells = this.gridElement.querySelectorAll('.cell[data-field]');\n for (const cell of cells) {\n const field = cell.getAttribute('data-field');\n if (!field) continue;\n\n // Apply hidden attribute\n if (this.#hiddenColumnSet.has(field)) {\n cell.setAttribute('data-responsive-hidden', '');\n cell.removeAttribute('data-responsive-value-only');\n }\n // Apply value-only attribute (shows value without header label)\n else if (this.#valueOnlyColumnSet.has(field)) {\n cell.setAttribute('data-responsive-value-only', '');\n cell.removeAttribute('data-responsive-hidden');\n }\n // Clear any previous responsive attributes\n else {\n cell.removeAttribute('data-responsive-hidden');\n cell.removeAttribute('data-responsive-value-only');\n }\n }\n }\n\n /**\n * Check if width has crossed any breakpoint threshold.\n * Handles both single breakpoint (legacy) and multi-breakpoint modes.\n */\n #checkBreakpoint(width: number): void {\n // Multi-breakpoint mode\n if (this.#sortedBreakpoints.length > 0) {\n this.#checkMultiBreakpoint(width);\n return;\n }\n\n // Legacy single breakpoint mode\n const breakpoint = this.config.breakpoint ?? 0;\n\n // Warn once if breakpoint not configured (0 means never responsive)\n if (breakpoint === 0 && !this.#warnedAboutMissingBreakpoint) {\n this.#warnedAboutMissingBreakpoint = true;\n this.warn(\n MISSING_BREAKPOINT,\n \"No breakpoint configured. Responsive mode is disabled. Set a breakpoint based on your grid's column count.\",\n );\n }\n\n const shouldBeResponsive = breakpoint > 0 && width < breakpoint;\n\n if (shouldBeResponsive !== this.#isResponsive) {\n this.#isResponsive = shouldBeResponsive;\n this.#applyResponsiveState();\n this.emit('responsive-change', {\n isResponsive: shouldBeResponsive,\n width,\n breakpoint,\n } satisfies ResponsiveChangeDetail);\n this.requestRender();\n }\n }\n\n /**\n * Check breakpoints in multi-breakpoint mode.\n * Evaluates breakpoints from largest to smallest, applying the first match.\n */\n #checkMultiBreakpoint(width: number): void {\n // Find the active breakpoint (first one where width <= maxWidth)\n // Since sorted largest to smallest, we find the largest matching breakpoint\n let newActiveBreakpoint: BreakpointConfig | null = null;\n\n for (const bp of this.#sortedBreakpoints) {\n if (width <= bp.maxWidth) {\n newActiveBreakpoint = bp;\n // Continue to find the most specific (smallest) matching breakpoint\n }\n }\n\n // Check if breakpoint changed\n const breakpointChanged = newActiveBreakpoint !== this.#activeBreakpoint;\n\n if (breakpointChanged) {\n this.#activeBreakpoint = newActiveBreakpoint;\n\n // Update hidden column sets from active breakpoint\n if (newActiveBreakpoint?.hiddenColumns) {\n this.#buildHiddenColumnSets(newActiveBreakpoint.hiddenColumns);\n } else {\n // Fall back to top-level hiddenColumns config\n this.#buildHiddenColumnSets(this.config.hiddenColumns);\n }\n\n // Determine if we should be in card layout\n const shouldBeResponsive = newActiveBreakpoint?.cardLayout === true;\n\n if (shouldBeResponsive !== this.#isResponsive) {\n this.#isResponsive = shouldBeResponsive;\n this.#applyResponsiveState();\n }\n\n // Emit event for any breakpoint change\n this.emit('responsive-change', {\n isResponsive: this.#isResponsive,\n width,\n breakpoint: newActiveBreakpoint?.maxWidth ?? 0,\n } satisfies ResponsiveChangeDetail);\n\n this.requestRender();\n }\n }\n\n /** Original row height before entering responsive mode, for restoration on exit */\n #originalRowHeight?: number;\n\n /**\n * Apply the responsive state to the grid element.\n * Handles scroll reset when entering responsive mode and row height restoration on exit.\n */\n #applyResponsiveState(): void {\n this.gridElement.toggleAttribute('data-responsive', this.#isResponsive);\n\n // Apply animation attribute if enabled (default: true)\n const animate = this.config.animate !== false;\n this.gridElement.toggleAttribute('data-responsive-animate', animate);\n\n // Toggle per-card label visibility based on hideHeader config.\n // Attribute only meaningful while in card mode; clear it otherwise so\n // the CSS rule cannot accidentally apply to non-responsive layouts.\n this.gridElement.toggleAttribute(\n 'data-responsive-hide-header',\n this.#isResponsive && this.config.hideHeader === true,\n );\n\n // Set custom animation duration if provided\n if (this.config.animationDuration) {\n this.gridElement.style.setProperty('--tbw-responsive-duration', `${this.config.animationDuration}ms`);\n }\n\n // Cast to internal type for virtualization access\n const internalGrid = this.#internalGrid;\n\n if (this.#isResponsive) {\n // Store original row height before responsive mode changes it\n if (internalGrid._virtualization) {\n this.#originalRowHeight = internalGrid._virtualization.rowHeight;\n }\n\n // Reset horizontal scroll position when entering responsive mode\n // The CSS hides overflow but doesn't reset the scroll position\n const scrollArea = this.gridElement.querySelector('.tbw-scroll-area') as HTMLElement | null;\n if (scrollArea) {\n scrollArea.scrollLeft = 0;\n }\n } else {\n // Exiting responsive mode - clean up inline styles set by renderRow\n // The rows are reused from the pool, so we need to remove the card-specific styles\n const rows = this.gridElement.querySelectorAll('.data-grid-row');\n for (const row of rows) {\n (row as HTMLElement).style.height = '';\n row.classList.remove('responsive-card');\n }\n\n // Restore original row height\n if (this.#originalRowHeight && this.#originalRowHeight > 0 && internalGrid._virtualization) {\n internalGrid._virtualization.rowHeight = this.#originalRowHeight;\n this.#originalRowHeight = undefined;\n }\n\n // Clear cached measurements so they're remeasured fresh when re-entering responsive mode\n // Without this, stale measurements cause incorrect height calculations after scrolling\n this.#measuredCardHeight = undefined;\n this.#measuredGroupRowHeight = undefined;\n this.#lastCardRowCount = undefined;\n }\n }\n\n /**\n * Custom row rendering when cardRenderer is provided and in responsive mode.\n *\n * When a cardRenderer is configured, this hook takes over row rendering to display\n * the custom card layout instead of the default cell structure.\n *\n * @param row - The row data object\n * @param rowEl - The row DOM element to render into\n * @param rowIndex - The index of the row in the data array\n * @returns `true` if rendered (prevents default), `void` for default rendering\n */\n override renderRow(row: unknown, rowEl: HTMLElement, rowIndex: number): boolean | void {\n // Only override when in responsive mode AND cardRenderer is provided\n if (!this.#isResponsive || !this.config.cardRenderer) {\n return; // Let default rendering proceed\n }\n\n // Skip group rows from GroupingRowsPlugin - they have special structure\n // and should use their own renderer\n if ((row as { __isGroupRow?: boolean }).__isGroupRow) {\n return; // Let GroupingRowsPlugin handle group row rendering\n }\n\n // Clear existing content\n rowEl.replaceChildren();\n\n // Call user's cardRenderer to get custom content\n const cardContent = this.config.cardRenderer(row as T, rowIndex);\n\n // Reset className - clears any stale classes from previous use (e.g., 'group-row' from recycled element)\n // This follows the same pattern as GroupingRowsPlugin which sets className explicitly\n rowEl.className = 'data-grid-row responsive-card';\n\n // Handle card row height — when a numeric height is configured, use the effective\n // height from #getCardHeight() which incorporates DOM measurement after first render.\n // This keeps inline height in sync with the position cache used for virtualization.\n const configuredHeight = this.config.cardRowHeight;\n if (configuredHeight === 'auto' || configuredHeight === undefined) {\n rowEl.style.height = 'auto';\n } else {\n rowEl.style.height = `${this.#getCardHeight()}px`;\n }\n\n // Append the custom card content\n rowEl.appendChild(cardContent);\n\n return true; // We handled rendering\n }\n\n /**\n * Handle keyboard navigation in responsive mode.\n *\n * In responsive mode, the visual layout is inverted:\n * - Cells are stacked vertically within each \"card\" (row)\n * - DOWN/UP visually moves within the card (between fields)\n * - Page Down/Page Up or Ctrl+Down/Up moves between cards\n *\n * For custom cardRenderers, keyboard navigation is disabled entirely\n * since the implementor controls the card content and should handle\n * navigation via their own event handlers.\n *\n * @returns `true` if the event was handled and default behavior should be prevented\n */\n override onKeyDown(e: KeyboardEvent): boolean {\n if (!this.#isResponsive) {\n return false;\n }\n\n // If custom cardRenderer is provided, disable grid's keyboard navigation\n // The implementor is responsible for their own navigation\n if (this.config.cardRenderer) {\n const navKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];\n if (navKeys.includes(e.key)) {\n // Let the event bubble - implementor can handle it\n return false;\n }\n }\n\n // Swap arrow key behavior for CSS-only responsive mode\n // In card layout, cells are stacked vertically:\n // Card 1: Card 2:\n // ID: 1 ID: 2\n // Name: Alice Name: Bob <- ArrowRight goes here\n // Dept: Eng Dept: Mkt\n // ↓ ArrowDown goes here\n //\n // ArrowDown/Up = move within card (change column/field)\n // ArrowRight/Left = move between cards (change row)\n const maxRow = this.rows.length - 1;\n const maxCol = this.visibleColumns.length - 1;\n\n switch (e.key) {\n case 'ArrowDown':\n // Move down WITHIN card (to next field/column)\n if (this.grid._focusCol < maxCol) {\n this.grid._focusCol += 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n // At bottom of card - optionally move to next card's first field\n if (this.grid._focusRow < maxRow) {\n this.grid._focusRow += 1;\n this.grid._focusCol = 0;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n\n case 'ArrowUp':\n // Move up WITHIN card (to previous field/column)\n if (this.grid._focusCol > 0) {\n this.grid._focusCol -= 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n // At top of card - optionally move to previous card's last field\n if (this.grid._focusRow > 0) {\n this.grid._focusRow -= 1;\n this.grid._focusCol = maxCol;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n\n case 'ArrowRight':\n // Move to NEXT card (same field)\n if (this.grid._focusRow < maxRow) {\n this.grid._focusRow += 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n\n case 'ArrowLeft':\n // Move to PREVIOUS card (same field)\n if (this.grid._focusRow > 0) {\n this.grid._focusRow -= 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n }\n\n return false;\n }\n\n // ============================================\n // Variable Height Support for Mixed Row Types\n // ============================================\n\n /** Measured card height from DOM for virtualization calculations */\n #measuredCardHeight?: number;\n\n /** Measured group row height from DOM for virtualization calculations */\n #measuredGroupRowHeight?: number;\n\n /** Last known card row count for detecting changes (e.g., group expand/collapse) */\n #lastCardRowCount?: number;\n\n /**\n * Get the effective card height for virtualization calculations.\n * Prioritizes DOM-measured height (actual rendered size) over config,\n * since content can overflow the configured height.\n */\n #getCardHeight(): number {\n // Prefer measured height - it reflects actual rendered size including overflow\n if (this.#measuredCardHeight && this.#measuredCardHeight > 0) {\n return this.#measuredCardHeight;\n }\n // Fall back to explicit config\n const configHeight = this.config.cardRowHeight;\n if (typeof configHeight === 'number' && configHeight > 0) {\n return configHeight;\n }\n // Default fallback\n return 80;\n }\n\n /**\n * Get the effective group row height for virtualization calculations.\n * Uses DOM-measured height, falling back to original row height.\n */\n #getGroupRowHeight(): number {\n if (this.#measuredGroupRowHeight && this.#measuredGroupRowHeight > 0) {\n return this.#measuredGroupRowHeight;\n }\n // Fall back to original row height (before responsive mode)\n return this.#originalRowHeight ?? 28;\n }\n\n /**\n * Check if there are any group rows in the current dataset.\n * Used to determine if we have mixed row heights.\n */\n #hasGroupRows(): boolean {\n for (const row of this.rows) {\n if ((row as { __isGroupRow?: boolean }).__isGroupRow) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Get the height of a specific row based on its type (group row vs card row).\n * Returns undefined if not in responsive mode.\n *\n * @param row - The row data\n * @param _index - The row index (unused, but part of the interface)\n * @returns The row height in pixels, or undefined if not in responsive mode\n */\n override getRowHeight(row: unknown, _index: number): number | undefined {\n // Only applies when in responsive mode with cardRenderer\n if (!this.#isResponsive || !this.config.cardRenderer) {\n return undefined;\n }\n\n // Check if this is a group row\n if ((row as { __isGroupRow?: boolean }).__isGroupRow) {\n return this.#getGroupRowHeight();\n }\n\n // Regular card row\n return this.#getCardHeight();\n }\n\n /**\n * Count the number of card rows (non-group rows) in the current dataset.\n */\n #countCardRows(): number {\n let count = 0;\n for (const row of this.rows) {\n if (!(row as { __isGroupRow?: boolean }).__isGroupRow) {\n count++;\n }\n }\n return count;\n }\n\n /** Pending refresh scheduled via microtask */\n #pendingRefresh = false;\n\n /**\n * Measure card height from DOM after render and detect row count changes.\n * Called in afterRender to ensure scroll calculations are accurate.\n *\n * This handles two scenarios:\n * 1. Card height changes (content overflow, dynamic sizing)\n * 2. Card row count changes (group expand/collapse)\n * 3. Group row height changes\n *\n * For uniform card layouts (no groups), we update the virtualization row height\n * directly to the card height. For mixed layouts (groups + cards), we use the\n * getExtraHeight mechanism to report height differences.\n *\n * The refresh is deferred via microtask to avoid nested render cycles.\n */\n #measureCardHeightFromDOM(): void {\n if (!this.#isResponsive) {\n return;\n }\n\n let needsRefresh = false;\n const internalGrid = this.#internalGrid;\n const hasGroups = this.#hasGroupRows();\n\n // Check if card row count changed (e.g., group expanded/collapsed)\n const currentCardRowCount = this.#countCardRows();\n if (currentCardRowCount !== this.#lastCardRowCount) {\n this.#lastCardRowCount = currentCardRowCount;\n needsRefresh = true;\n }\n\n // Measure actual group row height from DOM (for mixed layouts)\n if (hasGroups) {\n const groupRow = this.gridElement.querySelector('.data-grid-row.group-row') as HTMLElement | null;\n if (groupRow) {\n const height = groupRow.getBoundingClientRect().height;\n if (height > 0 && height !== this.#measuredGroupRowHeight) {\n this.#measuredGroupRowHeight = height;\n needsRefresh = true;\n }\n }\n }\n\n // Measure actual card height from DOM.\n // With cardRenderer, rows have the `.responsive-card` class.\n // Without cardRenderer (CSS-only card mode), rows are plain `.data-grid-row`\n // elements whose CSS height is `auto` — we need to measure their actual\n // rendered height so the faux scrollbar spacer is sized correctly.\n const cardSelector = this.config.cardRenderer ? '.data-grid-row.responsive-card' : '.data-grid-row:not(.group-row)';\n const cardRow = this.gridElement.querySelector(cardSelector) as HTMLElement | null;\n if (cardRow) {\n const height = cardRow.getBoundingClientRect().height;\n if (height > 0 && height !== this.#measuredCardHeight) {\n this.#measuredCardHeight = height;\n needsRefresh = true;\n\n // For uniform card layouts (no groups), update virtualization row height directly\n // This ensures proper row recycling and translateY calculations\n if (!hasGroups && internalGrid._virtualization) {\n internalGrid._virtualization.rowHeight = height;\n }\n }\n }\n\n // Defer virtualization refresh to avoid nested render cycles\n // This is called from afterRender, so we can't call refreshVirtualWindow synchronously\n // Use scheduler's VIRTUALIZATION phase to batch properly and avoid duplicate afterRender calls\n if (needsRefresh && !this.#pendingRefresh) {\n this.#pendingRefresh = true;\n queueMicrotask(() => {\n this.#pendingRefresh = false;\n // Only refresh if still attached and in responsive mode\n if (this.grid && this.#isResponsive) {\n // Request virtualization phase through grid's public API\n // This goes through the scheduler which batches and handles afterRender properly\n this.#internalGrid.refreshVirtualWindow?.(true, true);\n }\n });\n }\n }\n}\n"],"mappings":"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"}
|