@toolbox-web/grid 3.9.2 → 3.10.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.
Files changed (123) hide show
  1. package/all.js +1 -1
  2. package/all.js.map +1 -1
  3. package/index.js +1 -1
  4. package/index.js.map +1 -1
  5. package/lib/core/types.d.ts +6 -3
  6. package/lib/features/clipboard.js.map +1 -1
  7. package/lib/features/column-virtualization.js.map +1 -1
  8. package/lib/features/context-menu.js.map +1 -1
  9. package/lib/features/editing.js.map +1 -1
  10. package/lib/features/export.js.map +1 -1
  11. package/lib/features/filtering.js.map +1 -1
  12. package/lib/features/grouping-columns.js.map +1 -1
  13. package/lib/features/grouping-rows.js.map +1 -1
  14. package/lib/features/master-detail.js.map +1 -1
  15. package/lib/features/multi-sort.js.map +1 -1
  16. package/lib/features/pinned-columns.js.map +1 -1
  17. package/lib/features/pinned-rows.js.map +1 -1
  18. package/lib/features/pivot.js.map +1 -1
  19. package/lib/features/print.js.map +1 -1
  20. package/lib/features/registry.js +1 -1
  21. package/lib/features/registry.js.map +1 -1
  22. package/lib/features/reorder-columns.js.map +1 -1
  23. package/lib/features/responsive.js.map +1 -1
  24. package/lib/features/row-drag-drop.js.map +1 -1
  25. package/lib/features/selection.js.map +1 -1
  26. package/lib/features/server-side.js.map +1 -1
  27. package/lib/features/shell.js.map +1 -1
  28. package/lib/features/sticky-rows.js.map +1 -1
  29. package/lib/features/tooltip.js.map +1 -1
  30. package/lib/features/tree.js.map +1 -1
  31. package/lib/features/undo-redo.js.map +1 -1
  32. package/lib/features/visibility.js.map +1 -1
  33. package/lib/plugins/clipboard/index.js.map +1 -1
  34. package/lib/plugins/column-virtualization/index.js.map +1 -1
  35. package/lib/plugins/context-menu/index.js.map +1 -1
  36. package/lib/plugins/editing/editors.d.ts +15 -0
  37. package/lib/plugins/editing/index.js +1 -1
  38. package/lib/plugins/editing/index.js.map +1 -1
  39. package/lib/plugins/editing/internal/helpers.d.ts +23 -0
  40. package/lib/plugins/editing/types.d.ts +16 -1
  41. package/lib/plugins/export/index.js +1 -1
  42. package/lib/plugins/export/index.js.map +1 -1
  43. package/lib/plugins/filtering/filter-model.d.ts +20 -1
  44. package/lib/plugins/filtering/filter-panel-default.d.ts +15 -1
  45. package/lib/plugins/filtering/index.d.ts +1 -1
  46. package/lib/plugins/filtering/index.js +1 -1
  47. package/lib/plugins/filtering/index.js.map +1 -1
  48. package/lib/plugins/filtering/types.d.ts +58 -3
  49. package/lib/plugins/grouping-columns/index.js.map +1 -1
  50. package/lib/plugins/grouping-rows/index.js +1 -1
  51. package/lib/plugins/grouping-rows/index.js.map +1 -1
  52. package/lib/plugins/hierarchy/index.js.map +1 -1
  53. package/lib/plugins/master-detail/index.js +1 -1
  54. package/lib/plugins/master-detail/index.js.map +1 -1
  55. package/lib/plugins/master-detail/master-detail-plugin.d.ts +6 -1
  56. package/lib/plugins/multi-sort/index.js.map +1 -1
  57. package/lib/plugins/pinned-columns/index.js.map +1 -1
  58. package/lib/plugins/pinned-rows/index.js.map +1 -1
  59. package/lib/plugins/pivot/index.js +1 -1
  60. package/lib/plugins/pivot/index.js.map +1 -1
  61. package/lib/plugins/pivot/pivot-plugin.d.ts +2 -0
  62. package/lib/plugins/pivot/types.d.ts +2 -0
  63. package/lib/plugins/print/index.js.map +1 -1
  64. package/lib/plugins/reorder-columns/index.js.map +1 -1
  65. package/lib/plugins/responsive/index.js.map +1 -1
  66. package/lib/plugins/row-drag-drop/index.js.map +1 -1
  67. package/lib/plugins/selection/index.js +1 -1
  68. package/lib/plugins/selection/index.js.map +1 -1
  69. package/lib/plugins/server-side/index.js.map +1 -1
  70. package/lib/plugins/shared/form-field-target.d.ts +10 -0
  71. package/lib/plugins/shared/format-value.d.ts +12 -0
  72. package/lib/plugins/shared/structural-toggle.d.ts +14 -0
  73. package/lib/plugins/shell/index.js.map +1 -1
  74. package/lib/plugins/sticky-rows/index.js.map +1 -1
  75. package/lib/plugins/tooltip/index.js.map +1 -1
  76. package/lib/plugins/tree/index.js +1 -1
  77. package/lib/plugins/tree/index.js.map +1 -1
  78. package/lib/plugins/undo-redo/index.d.ts +1 -1
  79. package/lib/plugins/undo-redo/index.js +1 -1
  80. package/lib/plugins/undo-redo/index.js.map +1 -1
  81. package/lib/plugins/undo-redo/types.d.ts +20 -2
  82. package/lib/plugins/undo-redo/undo-redo-plugin.d.ts +8 -0
  83. package/lib/plugins/visibility/index.js.map +1 -1
  84. package/package.json +1 -1
  85. package/umd/grid.all.umd.js +1 -1
  86. package/umd/grid.all.umd.js.map +1 -1
  87. package/umd/grid.umd.js +1 -1
  88. package/umd/grid.umd.js.map +1 -1
  89. package/umd/plugins/clipboard.umd.js.map +1 -1
  90. package/umd/plugins/column-virtualization.umd.js.map +1 -1
  91. package/umd/plugins/context-menu.umd.js.map +1 -1
  92. package/umd/plugins/editing.umd.js +1 -1
  93. package/umd/plugins/editing.umd.js.map +1 -1
  94. package/umd/plugins/export.umd.js +1 -1
  95. package/umd/plugins/export.umd.js.map +1 -1
  96. package/umd/plugins/filtering.umd.js +1 -1
  97. package/umd/plugins/filtering.umd.js.map +1 -1
  98. package/umd/plugins/grouping-columns.umd.js.map +1 -1
  99. package/umd/plugins/grouping-rows.umd.js +1 -1
  100. package/umd/plugins/grouping-rows.umd.js.map +1 -1
  101. package/umd/plugins/hierarchy.umd.js.map +1 -1
  102. package/umd/plugins/master-detail.umd.js +1 -1
  103. package/umd/plugins/master-detail.umd.js.map +1 -1
  104. package/umd/plugins/multi-sort.umd.js.map +1 -1
  105. package/umd/plugins/pinned-columns.umd.js.map +1 -1
  106. package/umd/plugins/pinned-rows.umd.js.map +1 -1
  107. package/umd/plugins/pivot.umd.js +1 -1
  108. package/umd/plugins/pivot.umd.js.map +1 -1
  109. package/umd/plugins/print.umd.js.map +1 -1
  110. package/umd/plugins/reorder-columns.umd.js.map +1 -1
  111. package/umd/plugins/responsive.umd.js.map +1 -1
  112. package/umd/plugins/row-drag-drop.umd.js.map +1 -1
  113. package/umd/plugins/selection.umd.js +1 -1
  114. package/umd/plugins/selection.umd.js.map +1 -1
  115. package/umd/plugins/server-side.umd.js.map +1 -1
  116. package/umd/plugins/shell.umd.js.map +1 -1
  117. package/umd/plugins/sticky-rows.umd.js.map +1 -1
  118. package/umd/plugins/tooltip.umd.js.map +1 -1
  119. package/umd/plugins/tree.umd.js +1 -1
  120. package/umd/plugins/tree.umd.js.map +1 -1
  121. package/umd/plugins/undo-redo.umd.js +1 -1
  122. package/umd/plugins/undo-redo.umd.js.map +1 -1
  123. package/umd/plugins/visibility.umd.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../../../libs/grid/src/lib/plugins/shared/cell-text.ts","../../../../../../libs/grid/src/lib/plugins/shared/data-collection.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/plugins/clipboard/clipboard-payload.ts","../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/plugins/clipboard/copy.ts","../../../../../../libs/grid/src/lib/plugins/clipboard/paste.ts","../../../../../../libs/grid/src/lib/plugins/clipboard/types.ts","../../../../../../libs/grid/src/lib/plugins/clipboard/clipboard-plugin.ts"],"sourcesContent":["/**\n * Displayed text of a body cell, excluding a custom `loadingRenderer` overlay that lives inside\n * the cell (`aria-hidden` does not remove it from `textContent`). Only text and element children\n * are read, matching `textContent`, which skips comment nodes (e.g. Angular's anchors).\n * @internal\n */\nexport function readCellDisplayText(cell: HTMLElement): string {\n const loader = cell.querySelector(':scope > .tbw-cell-loading-overlay');\n if (!loader) return cell.textContent?.trim() ?? '';\n let text = '';\n for (const node of cell.childNodes) {\n if (node === loader) continue;\n if (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE) text += node.textContent ?? '';\n }\n return text.trim();\n}\n","/**\n * Shared Data Collection Utilities\n *\n * Pure functions for resolving columns and formatting values, shared between\n * the Clipboard and Export plugins. Each plugin bundles its own copy of this\n * module (no chunk splitting) since plugin builds inline sibling imports.\n *\n * @internal\n */\n\nimport type { ColumnConfig } from '../../core/types';\n\n/**\n * Resolve which columns to include in a data export or copy operation.\n *\n * Filters out hidden columns, utility columns (`meta.utility`), and\n * internal columns (`__`-prefixed fields). Optionally restricts to an\n * explicit set of field names.\n *\n * @param columns - All column configurations\n * @param fields - If provided, only include columns whose field is in this list\n * @param onlyVisible - When `true` (default), exclude hidden and internal columns\n * @returns Filtered column array preserving original order\n */\nexport function resolveColumns(\n columns: readonly ColumnConfig[],\n fields?: string[],\n onlyVisible = true,\n): ColumnConfig[] {\n let result = columns as ColumnConfig[];\n\n if (onlyVisible) {\n result = result.filter((c) => !c.hidden && !c.field.startsWith('__') && c.utility !== true);\n }\n\n if (fields?.length) {\n const fieldSet = new Set(fields);\n result = result.filter((c) => fieldSet.has(c.field));\n }\n\n return result;\n}\n\n/**\n * Resolve which rows to include, optionally filtered to specific indices.\n *\n * @param rows - All row data\n * @param indices - If provided, only include rows at these indices (sorted ascending)\n * @returns Filtered row array\n */\nexport function resolveRows<T>(rows: readonly T[], indices?: number[]): T[] {\n if (!indices?.length) return rows as T[];\n\n return [...indices]\n .sort((a, b) => a - b)\n .map((i) => rows[i])\n .filter((r): r is T => r != null);\n}\n\n/**\n * Format a raw cell value as a text string.\n *\n * Provides the common null / Date / object → string conversion shared by\n * both clipboard and export output builders.\n *\n * @param value - The cell value to format\n * @returns A plain-text representation of the value\n */\nexport function formatValueAsText(value: unknown): string {\n if (value == null) return '';\n if (value instanceof Date) return value.toISOString();\n if (typeof value === 'object') return JSON.stringify(value);\n return String(value);\n}\n\n/**\n * Leading characters that make Excel / LibreOffice / Google Sheets treat a cell\n * as a formula rather than text.\n */\nconst FORMULA_TRIGGER_RE = /^[=+\\-@\\t\\r]/;\n\n/**\n * Options for {@link formatDelimitedValue}.\n *\n * @since 3.5.0\n */\nexport interface DelimitedFormatOptions {\n /** Field separator (`,` for CSV, `\\t` for clipboard). */\n delimiter: string;\n /** Row separator. */\n newline: string;\n /**\n * `'auto'` (default) quotes only strings containing a separator or quote,\n * `'always'` quotes every string, `'never'` disables quoting entirely.\n */\n quoting?: 'auto' | 'always' | 'never';\n /**\n * Prefix formula-triggering strings with `'` so spreadsheets treat them as\n * text. Defaults to `true`.\n */\n escapeFormulas?: boolean;\n}\n\n/**\n * Format a cell value for a delimiter-separated text format (CSV export or\n * clipboard copy).\n *\n * Only string values are formula-escaped: numbers such as `-5` stringify to a\n * leading `-` but are unambiguously numeric to a spreadsheet, and escaping them\n * would turn every negative number into text.\n *\n * Dispatch order is `typeof`-first because string + number cover the vast\n * majority of cell values; the cheap typeof check skips an `instanceof Date`\n * probe (which V8 cannot fold into a fast path) for every plain string cell.\n *\n * @param value - The cell value to format\n * @param opts - Delimiter, newline, quoting and formula-escaping behaviour\n * @returns The value as a delimiter-safe string\n * @since 3.5.0\n */\nexport function formatDelimitedValue(value: unknown, opts: DelimitedFormatOptions): string {\n // Hot path: strings (most cells).\n if (typeof value === 'string') {\n const text = opts.escapeFormulas !== false && FORMULA_TRIGGER_RE.test(value) ? `'${value}` : value;\n const quoting = opts.quoting ?? 'auto';\n if (\n quoting === 'always' ||\n (quoting === 'auto' &&\n (text.includes(opts.delimiter) ||\n text.includes(opts.newline) ||\n text.includes('\"') ||\n text.includes('\\n') ||\n text.includes('\\r')))\n ) {\n return `\"${text.replace(/\"/g, '\"\"')}\"`;\n }\n return text;\n }\n if (typeof value === 'number') return String(value);\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n if (value == null) return '';\n if (value instanceof Date) return value.toISOString();\n if (typeof value === 'object') return JSON.stringify(value);\n // Symbols, bigints, etc.\n return String(value);\n}\n","// Centralized template expression evaluation & sanitization utilities.\n// Responsible for safely interpolating {{ }} expressions while blocking\n// access to dangerous globals / reflective capabilities.\nimport type { CompiledViewFunction, EvalContext } from '../types';\n\n// #region Constants\nconst EXPR_RE = /{{\\s*([^}]+)\\s*}}/g;\nconst EMPTY_SENTINEL = '__DG_EMPTY__';\nconst SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/;\n\n// Forbidden identifiers built at runtime to avoid static analysis scanners\n// from flagging string literals (e.g. socket.dev \"network access\" / \"uses eval\").\n// Stored as reversed, pipe-delimited tokens — reversed back when constructing the regex.\nconst _F =\n '__otorp__|__retteGenifed__|__retteSenifed__|rotcurtsnoc|wodniw|sihTlabolg|labolg|ssecorp|noitcnuF|tropmi|lave|tcelfeR|yxorP|rorrE|stnemugra|tnemucod|noitacol|eikooc|egarotSlacol|egarotSnoisses|BDdexedni|hctef|tseuqeRpttHLMX|tekcoSbeW|rekroW|rekroWderahS|rekroWecivreS|renepo|tnerap|pot|semarf|fles'\n .split('|')\n .map((s) => s.split('').reverse().join(''));\nconst FORBIDDEN = new RegExp(`__(proto|defineGetter|defineSetter)|${_F.slice(3).join('|')}|this\\\\b`);\n// #endregion\n\n// #region HTML Sanitization\n\n/**\n * Escape a plain text string for safe insertion into HTML.\n * Converts special HTML characters to their entity equivalents.\n *\n * @param text - Plain text string to escape\n * @returns HTML-safe string\n */\nexport function escapeHtml(text: string): string {\n if (!text || typeof text !== 'string') return '';\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\n/**\n * Tags that are considered dangerous and will be completely removed.\n * These can execute scripts, load external resources, or manipulate the page.\n */\nconst DANGEROUS_TAGS = new Set(\n 'script|iframe|object|embed|form|input|button|textarea|select|link|meta|base|style|template|slot|portal|frame|frameset|applet|noscript|noembed|plaintext|xmp|listing|math'.split(\n '|',\n ),\n);\n\n/**\n * Attributes that are considered dangerous - event handlers and data loading.\n */\nconst DANGEROUS_ATTR_PATTERN = /^on\\w+$/i;\n\n/**\n * Attributes that can contain URLs which might be javascript: or data: URIs.\n */\nconst URL_ATTRS = new Set('href|src|action|formaction|data|srcdoc|xlink:href|poster|srcset'.split('|'));\n\n/**\n * Protocol patterns that are dangerous in URLs.\n */\nconst DANGEROUS_URL_PROTOCOL = /^\\s*(javascript|vbscript|data|blob):/i;\n\n/**\n * Sanitize an HTML string by removing dangerous tags and attributes.\n *\n * Prefer {@link setSanitizedHTML} wherever the result is destined for an\n * element: this function has to re-serialize the sanitized tree back to a\n * string, and the caller's `innerHTML =` then re-parses it. That\n * parse→serialize→parse round trip is the classic mutation-XSS vector, and it\n * costs a second parse. Use this overload only when a string is genuinely\n * required (e.g. the value is stored, compared, or handed to another API).\n *\n * @param html - Raw HTML string to sanitize\n * @returns Sanitized HTML string safe for innerHTML\n */\nexport function sanitizeHTML(html: string): string {\n if (!html || typeof html !== 'string') return '';\n\n // Fast path: if no HTML tags at all, return as-is (already safe)\n if (html.indexOf('<') === -1) return html;\n\n const template = document.createElement('template');\n template.innerHTML = html;\n\n sanitizeNode(template.content);\n\n return template.innerHTML;\n}\n\n/**\n * Parse and sanitize an HTML string into a detached {@link DocumentFragment}.\n *\n * This is the primitive {@link setSanitizedHTML} builds on, exposed for callers\n * that need to inspect or relocate the nodes before insertion.\n *\n * @param html - Raw HTML string to sanitize\n * @returns Fragment containing the sanitized nodes (empty for falsy input)\n */\nexport function sanitizeToFragment(html: string): DocumentFragment {\n const template = document.createElement('template');\n if (html && typeof html === 'string') {\n template.innerHTML = html;\n sanitizeNode(template.content);\n }\n return template.content;\n}\n\n/**\n * Replace an element's children with the sanitized parse of `html`.\n *\n * **This is the canonical sink for user-supplied markup** — cell/header/group\n * renderer string returns, `gridConfig.icons.*`, light-DOM tool panel fallback\n * content, and any new extension point that turns a user string into markup.\n * `textContent` is wrong here (it renders SVG icon strings as literal text) and\n * raw `innerHTML` is wrong (it is an XSS sink).\n *\n * Nodes are moved from the parse fragment rather than re-serialized, so the\n * sanitized tree is exactly what lands in the document.\n *\n * @param el - Element whose children are replaced\n * @param html - Raw HTML string to sanitize and insert\n */\nexport function setSanitizedHTML(el: Element, html: string): void {\n if (!html || typeof html !== 'string') {\n el.replaceChildren();\n return;\n }\n // Nothing for the parser to do: no markup and no entities to decode.\n if (html.indexOf('<') === -1 && html.indexOf('&') === -1) {\n el.textContent = html;\n return;\n }\n el.replaceChildren(sanitizeToFragment(html));\n}\n\n/**\n * Recursively sanitize a DOM node tree.\n */\nfunction sanitizeNode(root: DocumentFragment | Element): void {\n const toRemove: Element[] = [];\n\n // Use querySelectorAll to find all elements, then filter\n const elements = root.querySelectorAll('*');\n\n for (const el of elements) {\n const tagName = el.tagName.toLowerCase();\n\n // Check if tag is dangerous\n if (DANGEROUS_TAGS.has(tagName)) {\n toRemove.push(el);\n continue;\n }\n\n // SVG elements need special handling - they can contain script-like behavior\n if (tagName === 'svg' || el.namespaceURI === 'http://www.w3.org/2000/svg') {\n // Remove entire SVG if it has any suspicious attributes\n const hasDangerousContent = Array.from(el.attributes).some(\n (attr) => DANGEROUS_ATTR_PATTERN.test(attr.name) || attr.name === 'href' || attr.name === 'xlink:href',\n );\n if (hasDangerousContent) {\n toRemove.push(el);\n continue;\n }\n }\n\n // Check and remove dangerous attributes\n const attrsToRemove: string[] = [];\n for (const attr of el.attributes) {\n const attrName = attr.name.toLowerCase();\n\n // Event handlers (onclick, onerror, onload, etc.)\n if (DANGEROUS_ATTR_PATTERN.test(attrName)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // `is=` upgrades a plain tag into a registered customized built-in, so it\n // can smuggle behaviour past the tag-name check above.\n if (attrName === 'is') {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // URL attributes with dangerous protocols\n if (URL_ATTRS.has(attrName) && DANGEROUS_URL_PROTOCOL.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // style attribute can contain expressions (IE) or url() with javascript:\n if (attrName === 'style' && /expression\\s*\\(|javascript:|behavior\\s*:/i.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n }\n\n attrsToRemove.forEach((name) => el.removeAttribute(name));\n }\n\n // Remove dangerous elements (do this after iteration to avoid modifying during traversal)\n toRemove.forEach((el) => el.remove());\n}\n\n// #endregion\n\n// #region Template Evaluation\n\n// #region Safe Expression Evaluator\n// A minimal recursive-descent evaluator for the token set allowed by SAFE_EXPR.\n// Replaces `new Function()` to avoid dynamic code execution warnings from\n// static analysis tools (socket.dev, npm audit) while maintaining identical\n// functionality for expressions that pass the allowlist guards.\n//\n// Supported grammar (matching SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/):\n// ternary → logicalOr ('?' ternary ':' ternary)?\n// logicalOr → logicalAnd ('||' logicalAnd)*\n// logicalAnd → equality ('&&' equality)*\n// equality → comparison (('==' | '!=' | '===' | '!==') comparison)*\n// comparison → additive (('<' | '>' | '<=' | '>=') additive)*\n// additive → multiplicative (('+' | '-') multiplicative)*\n// multiplicative → unary (('*' | '/' | '%') unary)*\n// unary → '!' unary | primary\n// primary → NUMBER | STRING | IDENT ('.' IDENT)? | '(' ternary ')'\n\ntype Token =\n { type: 'num'; v: number } | { type: 'str'; v: string } | { type: 'id'; v: string } | { type: 'op'; v: string };\n\nfunction tokenize(expr: string): Token[] | null {\n const tokens: Token[] = [];\n let i = 0;\n while (i < expr.length) {\n const ch = expr[i];\n if (ch === ' ') {\n i++;\n continue;\n }\n\n // Numbers\n if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < expr.length && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {\n const start = i;\n while (i < expr.length && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) i++;\n tokens.push({ type: 'num', v: Number(expr.slice(start, i)) });\n continue;\n }\n\n // String literals (single-quoted only, per SAFE_EXPR)\n if (ch === \"'\") {\n let s = '';\n i++; // skip opening quote\n while (i < expr.length && expr[i] !== \"'\") s += expr[i++];\n if (i >= expr.length) return null; // unterminated\n i++; // skip closing quote\n tokens.push({ type: 'str', v: s });\n continue;\n }\n\n // Identifiers (value, row, row.field)\n if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {\n const start = i;\n while (i < expr.length && /[\\w$.]/.test(expr[i])) i++;\n tokens.push({ type: 'id', v: expr.slice(start, i) });\n continue;\n }\n\n // Multi-char operators\n if (i + 2 < expr.length) {\n const tri = expr.slice(i, i + 3);\n if (tri === '===' || tri === '!==') {\n tokens.push({ type: 'op', v: tri });\n i += 3;\n continue;\n }\n }\n if (i + 1 < expr.length) {\n const bi = expr.slice(i, i + 2);\n if (bi === '==' || bi === '!=' || bi === '<=' || bi === '>=' || bi === '&&' || bi === '||') {\n tokens.push({ type: 'op', v: bi });\n i += 2;\n continue;\n }\n }\n\n // Single-char operators\n if ('+-*/%<>!?:(),'.includes(ch)) {\n tokens.push({ type: 'op', v: ch });\n i++;\n continue;\n }\n\n return null; // unexpected character\n }\n return tokens;\n}\n\nfunction safeEval(expr: string, ctx: EvalContext): unknown {\n const toks = tokenize(expr);\n if (!toks) return undefined;\n const tokens: Token[] = toks;\n let pos = 0;\n\n function peek(): Token | undefined {\n return tokens[pos];\n }\n function advance(): Token {\n return tokens[pos++];\n }\n function match(type: string, val?: string): boolean {\n const t = peek();\n if (!t) return false;\n if (t.type !== type) return false;\n if (val !== undefined && t.v !== val) return false;\n return true;\n }\n\n function resolveId(name: string): unknown {\n if (name === 'value') return ctx.value;\n if (name.startsWith('row.')) {\n const key = name.slice(4);\n return ctx.row ? ctx.row[key] : undefined;\n }\n if (name.startsWith('typeDefault.')) {\n const key = name.slice('typeDefault.'.length);\n return ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n }\n // bare 'row' reference\n if (name === 'row') return ctx.row;\n if (name === 'typeDefault') return ctx.typeDefault;\n return undefined;\n }\n\n function parseTernary(): unknown {\n const result = parseLogicalOr();\n if (match('op', '?')) {\n advance(); // consume '?'\n const consequent = parseTernary();\n if (!match('op', ':')) return undefined;\n advance(); // consume ':'\n const alternate = parseTernary();\n return result ? consequent : alternate;\n }\n return result;\n }\n\n function parseLogicalOr(): unknown {\n let left = parseLogicalAnd();\n while (match('op', '||')) {\n advance();\n left = left || parseLogicalAnd();\n }\n return left;\n }\n\n function parseLogicalAnd(): unknown {\n let left = parseEquality();\n while (match('op', '&&')) {\n advance();\n left = left && parseEquality();\n }\n return left;\n }\n\n function parseEquality(): unknown {\n let left = parseComparison();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '===') {\n advance();\n left = left === parseComparison();\n } else if (t.v === '!==') {\n advance();\n left = left !== parseComparison();\n } else if (t.v === '==') {\n advance();\n left = left == parseComparison();\n } else if (t.v === '!=') {\n advance();\n left = left != parseComparison();\n } else break;\n }\n return left;\n }\n\n function parseComparison(): unknown {\n let left = parseAdditive();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '<') {\n advance();\n left = (left as number) < (parseAdditive() as number);\n } else if (t.v === '>') {\n advance();\n left = (left as number) > (parseAdditive() as number);\n } else if (t.v === '<=') {\n advance();\n left = (left as number) <= (parseAdditive() as number);\n } else if (t.v === '>=') {\n advance();\n left = (left as number) >= (parseAdditive() as number);\n } else break;\n }\n return left;\n }\n\n function parseAdditive(): unknown {\n let left = parseMultiplicative();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '+') {\n advance();\n left = (left as number) + (parseMultiplicative() as number);\n } else if (t.v === '-') {\n advance();\n left = (left as number) - (parseMultiplicative() as number);\n } else break;\n }\n return left;\n }\n\n function parseMultiplicative(): unknown {\n let left = parseUnary();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '*') {\n advance();\n left = (left as number) * (parseUnary() as number);\n } else if (t.v === '/') {\n advance();\n left = (left as number) / (parseUnary() as number);\n } else if (t.v === '%') {\n advance();\n left = (left as number) % (parseUnary() as number);\n } else break;\n }\n return left;\n }\n\n function parseUnary(): unknown {\n if (match('op', '!')) {\n advance();\n return !parseUnary();\n }\n return parsePrimary();\n }\n\n function parsePrimary(): unknown {\n const t = peek();\n if (!t) return undefined;\n if (t.type === 'num') {\n advance();\n return t.v;\n }\n if (t.type === 'str') {\n advance();\n return t.v;\n }\n if (t.type === 'id') {\n advance();\n return resolveId(t.v as string);\n }\n if (t.type === 'op' && t.v === '(') {\n advance(); // consume '('\n const result = parseTernary();\n if (match('op', ')')) advance(); // consume ')'\n return result;\n }\n // Unary minus for negative numbers\n if (t.type === 'op' && t.v === '-') {\n advance();\n return -(parsePrimary() as number);\n }\n return undefined;\n }\n\n const result = parseTernary();\n // Ensure all tokens were consumed\n if (pos < tokens.length) return undefined;\n return result;\n}\n// #endregion\n\nexport function evalTemplateString(raw: string, ctx: EvalContext): string {\n if (!raw || raw.indexOf('{{') === -1) return raw; // fast path (no expressions)\n const parts: { expr: string; result: string }[] = [];\n const evaluated = raw.replace(EXPR_RE, (_m, expr) => {\n const res = evalSingle(expr, ctx);\n parts.push({ expr: expr.trim(), result: res });\n // The template markup is author-controlled; the interpolated data is not.\n // Escaping here stops row data from injecting elements or breaking out of\n // an attribute (sanitizeHTML downstream only strips *dangerous* markup).\n return res === EMPTY_SENTINEL ? res : escapeHtml(res);\n });\n const finalStr = postProcess(evaluated);\n // If every part evaluated to EMPTY_SENTINEL we treat this as intentionally blank.\n // If any expression was blocked due to forbidden token (EMPTY_SENTINEL) we *still* only output ''\n // but do not escalate to BLOCKED_SENTINEL unless the original contained explicit forbidden tokens.\n const allEmpty = parts.length && parts.every((p) => p.result === '' || p.result === EMPTY_SENTINEL);\n const hadForbidden = REFLECTIVE_RE.test(raw);\n if (hadForbidden || allEmpty) return '';\n return finalStr;\n}\n\nfunction evalSingle(expr: string, ctx: EvalContext): string {\n expr = (expr || '').trim();\n if (!expr) return EMPTY_SENTINEL;\n if (REFLECTIVE_RE.test(expr)) return EMPTY_SENTINEL;\n if (expr === 'value') return ctx.value == null ? EMPTY_SENTINEL : String(ctx.value);\n if (expr.startsWith('row.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice(4);\n const v = ctx.row ? ctx.row[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.startsWith('typeDefault.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice('typeDefault.'.length);\n const v = ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.length > 80) return EMPTY_SENTINEL;\n if (!SAFE_EXPR.test(expr) || FORBIDDEN.test(expr)) return EMPTY_SENTINEL;\n const dotChain = expr.match(/\\./g);\n if (dotChain && dotChain.length > 1) return EMPTY_SENTINEL;\n try {\n const out = safeEval(expr, ctx);\n const str = out == null ? '' : String(out);\n if (REFLECTIVE_RE.test(str)) return EMPTY_SENTINEL;\n return str || EMPTY_SENTINEL;\n } catch {\n return EMPTY_SENTINEL;\n }\n}\n// #endregion\n\n// #region Cell Scrubbing\n/** Pattern matching reflective/introspective APIs that must be stripped from output. */\nconst REFLECTIVE_RE = /Reflect|Proxy|ownKeys/;\n\nfunction postProcess(s: string): string {\n if (!s) return s;\n return s.replace(new RegExp(EMPTY_SENTINEL, 'g'), '').replace(/Reflect\\.[^<>{}\\s]+|\\bProxy\\b|ownKeys\\([^)]*\\)/g, '');\n}\n\nexport function finalCellScrub(cell: HTMLElement): void {\n if (!REFLECTIVE_RE.test(cell.textContent || '')) return;\n // First pass: clear only text nodes containing forbidden tokens\n for (const n of cell.childNodes) {\n if (n.nodeType === Node.TEXT_NODE && REFLECTIVE_RE.test(n.textContent || '')) n.textContent = '';\n }\n // If forbidden tokens persist in element nodes, nuke everything\n if (REFLECTIVE_RE.test(cell.textContent || '')) {\n cell.textContent = '';\n }\n}\n// #endregion\n\n// #region Template Compilation\nexport function compileTemplate(raw: string) {\n if (REFLECTIVE_RE.test(raw)) {\n const fn = (() => '') as CompiledViewFunction;\n fn.__blocked = true;\n return fn;\n }\n const fn = ((ctx: EvalContext) => evalTemplateString(raw, ctx)) as CompiledViewFunction;\n fn.__blocked = false;\n return fn;\n}\n// #endregion\n","/**\n * Structured clipboard payload codec.\n *\n * Copy writes both `text/plain` (WYSIWYG for external targets) and `text/html`.\n * The HTML is a real `<table>` of the display text whose root element also\n * carries a base64-encoded structured payload in a `data-tbw-clip` attribute.\n * A same-app paste — even **cross-grid or cross-window** — can then recover the\n * raw cell values losslessly, while external targets just see the table/text.\n *\n * Kept in its own module so `copy.ts` and `paste.ts` share the format without\n * importing each other.\n */\nimport { escapeHtml } from '../../core/internal/sanitize';\n\n/**\n * Structured clipboard payload carried in `text/html`. `rows` are the RAW cell\n * values (data rows only), `fields` the source column fields in the same order.\n * @internal\n */\nexport interface ClipboardPayload {\n /** Payload schema version. */\n v: 1;\n /** Source column fields, in column order. */\n fields: string[];\n /** Raw cell values, one inner array per data row (no header). */\n rows: unknown[][];\n}\n\n/** Attribute (on the payload table) that marks our structured HTML clipboard. */\nexport const CLIPBOARD_PAYLOAD_ATTR = 'data-tbw-clip';\n\n/** Unicode-safe base64 encode of a JSON payload for an HTML attribute. */\nfunction encodePayload(payload: ClipboardPayload): string {\n return btoa(encodeURIComponent(JSON.stringify(payload)));\n}\n\n/**\n * Build the `text/html` clipboard representation: a real `<table>` of the\n * DISPLAY text (so external targets like Excel get a proper table) whose root\n * element carries the base64-encoded structured {@link ClipboardPayload} in a\n * `data-tbw-clip` attribute for lossless same-app paste.\n */\nexport function buildClipboardHtml(\n header: string[] | null,\n displayRows: string[][],\n payload: ClipboardPayload,\n): string {\n const thead = header ? `<thead><tr>${header.map((h) => `<th>${escapeHtml(h)}</th>`).join('')}</tr></thead>` : '';\n const tbody = `<tbody>${displayRows\n .map((r) => `<tr>${r.map((c) => `<td>${escapeHtml(c)}</td>`).join('')}</tr>`)\n .join('')}</tbody>`;\n return `<table ${CLIPBOARD_PAYLOAD_ATTR}=\"${encodePayload(payload)}\">${thead}${tbody}</table>`;\n}\n\n/**\n * Extract the structured {@link ClipboardPayload} embedded in a `text/html`\n * clipboard representation written by {@link buildClipboardHtml}. Enables a\n * lossless **cross-grid / cross-window** paste of object-valued cells (the\n * in-memory buffer only covers the same grid instance).\n *\n * Returns `null` when the HTML has no `data-tbw-clip` marker (e.g. an external\n * paste from Excel) or the payload can't be decoded. Parsing is defensive —\n * malformed input never throws.\n *\n * @param html - Raw `text/html` from `clipboardData.getData('text/html')`\n */\nexport function parseClipboardHtmlPayload(html: string | undefined | null): ClipboardPayload | null {\n if (!html || html.indexOf(CLIPBOARD_PAYLOAD_ATTR) === -1) return null;\n try {\n // Extract only our own base64 payload attribute with a targeted match rather\n // than parsing the whole (untrusted) clipboard HTML through a DOM sink\n // (DOMParser) — the latter feeds attacker-controlled markup to the parser and\n // trips CodeQL js/xss. The attribute value is base64 (btoa output), whose\n // alphabet excludes quotes and tag characters, so a simple attribute match is\n // both safe and sufficient.\n const match = html.match(new RegExp(`${CLIPBOARD_PAYLOAD_ATTR}=\"([A-Za-z0-9+/=]*)\"`));\n const encoded = match?.[1];\n if (!encoded) return null;\n const payload = JSON.parse(decodeURIComponent(atob(encoded))) as ClipboardPayload;\n if (payload && Array.isArray(payload.rows) && Array.isArray(payload.fields)) {\n return payload;\n }\n return null;\n } catch {\n return null;\n }\n}\n","/**\n * Centralized diagnostic messages for @toolbox-web/grid.\n *\n * Every user-facing warning, error, or info message in the grid has a unique\n * diagnostic code (e.g. `TBW001`). Each code maps to a section on the online\n * troubleshooting page, giving developers a direct link to resolution steps.\n *\n * ## Usage\n *\n * ```ts\n * import { MISSING_BREAKPOINT, warnDiagnostic, throwDiagnostic } from './diagnostics';\n *\n * // Warn with a code\n * warnDiagnostic(MISSING_BREAKPOINT, 'Set a breakpoint...', gridId);\n *\n * // Throw with a code\n * throwDiagnostic(MISSING_ROW_ID, 'Configure getRowId...', gridId);\n * ```\n *\n * Plugins should prefer `this.warn(MISSING_BREAKPOINT, message)` via BaseGridPlugin\n * instead of importing this module directly.\n *\n * @internal\n */\n\n// #region Grid Prefix\n\n/**\n * Build the `[tbw-grid]` or `[tbw-grid#my-id]` log prefix.\n * Pass `pluginName` for a scoped prefix like `[tbw-grid:SelectionPlugin]`.\n */\nexport function gridPrefix(gridId?: string, pluginName?: string): string {\n const id = gridId ? `#${gridId}` : '';\n const plugin = pluginName ? `:${pluginName}` : '';\n return `[tbw-grid${id}${plugin}]`;\n}\n\n// #endregion\n\n// #region Diagnostic Codes\n\n/**\n * Diagnostic codes used across the grid library.\n *\n * Each code is an individual export so that unused codes are tree-shaken\n * from bundles that don't reference them (esbuild can't tree-shake\n * properties from a single object).\n *\n * Naming: TBW + 3-digit number.\n * Ranges:\n * 001–019 Configuration validation (missing plugins, bad config)\n * 020–029 Plugin lifecycle (dependencies, incompatibilities, alias collapse)\n * 030–039 Feature registry\n * 040–049 Row operations (row ID, row mutations)\n * 050–059 Column operations (width, template)\n * 060–069 Rendering (callbacks, formatters, external views)\n * 070–079 Shell (tool panels, header/toolbar content)\n * 080–089 Editing & editors\n * 090–099 Print\n * 100–109 Clipboard\n * 110–119 Plugin-specific (responsive, undo-redo, grouping-columns)\n * 120–129 Style injection\n * 130–139 Attribute parsing\n * 140–149 DataSource / ServerSide\n */\n\n// --- Config validation (001–019) ---\n/** Column uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN = 'TBW001' as const;\n/** Grid config uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN_CONFIG = 'TBW002' as const;\n/** Plugin config rule violation (error severity). */\nexport const CONFIG_RULE_ERROR = 'TBW003' as const;\n/** Plugin config rule violation (warning severity). */\nexport const CONFIG_RULE_WARN = 'TBW004' as const;\n\n// --- Plugin lifecycle (020–029) ---\n/** Required plugin dependency is missing. */\nexport const MISSING_DEPENDENCY = 'TBW020' as const;\n/** Optional plugin dependency is missing. */\nexport const OPTIONAL_DEPENDENCY = 'TBW021' as const;\n/** Two loaded plugins are incompatible. */\nexport const INCOMPATIBLE_PLUGINS = 'TBW022' as const;\n/** Two plugin instances resolve to the same canonical name (alias collapse). */\nexport const PLUGIN_ALIAS_COLLAPSE = 'TBW023' as const;\n/** Error thrown inside a plugin event handler. */\nexport const PLUGIN_EVENT_ERROR = 'TBW024' as const;\n/** Conflicting config values found while merging alias-collapsed plugin instances. */\nexport const PLUGIN_ALIAS_CONFIG_CONFLICT = 'TBW025' as const;\n\n// --- Feature registry (030–039) ---\n/** Feature was re-registered (overwritten). */\nexport const FEATURE_REREGISTERED = 'TBW030' as const;\n/** Feature configured but not imported. */\nexport const FEATURE_NOT_IMPORTED = 'TBW031' as const;\n/** Feature depends on another feature that is not enabled. */\nexport const FEATURE_MISSING_DEP = 'TBW032' as const;\n\n// --- Row operations (040–049) ---\n/** Cannot determine row ID (no getRowId and no id property). */\nexport const MISSING_ROW_ID = 'TBW040' as const;\n/** Row with given ID not found. */\nexport const ROW_NOT_FOUND = 'TBW041' as const;\n\n// --- Column operations (050–059) ---\n/** Column has an invalid CSS width value. */\nexport const INVALID_COLUMN_WIDTH = 'TBW050' as const;\n\n// --- Rendering callbacks (060–069) ---\n/** rowClass callback threw an error. */\nexport const ROW_CLASS_ERROR = 'TBW060' as const;\n/** cellClass callback threw an error. */\nexport const CELL_CLASS_ERROR = 'TBW061' as const;\n/** Column format function threw an error. */\nexport const FORMAT_ERROR = 'TBW062' as const;\n/** External view mount() threw an error. */\nexport const VIEW_MOUNT_ERROR = 'TBW063' as const;\n/** External view event dispatch error. */\nexport const VIEW_DISPATCH_ERROR = 'TBW064' as const;\n\n// --- Shell (070–079) ---\n/** Tool panel missing required id. */\nexport const TOOL_PANEL_MISSING_ATTR = 'TBW070' as const;\n/** No tool panels registered. */\nexport const NO_TOOL_PANELS = 'TBW071' as const;\n/** Tool panel section not found. */\nexport const TOOL_PANEL_NOT_FOUND = 'TBW072' as const;\n/** Tool panel already registered. */\nexport const TOOL_PANEL_DUPLICATE = 'TBW073' as const;\n/** Header content already registered. */\nexport const HEADER_CONTENT_DUPLICATE = 'TBW074' as const;\n/** Toolbar content already registered. */\nexport const TOOLBAR_CONTENT_DUPLICATE = 'TBW075' as const;\n\n// --- Editing & editors (080–089) ---\n/** External editor mount() threw an error. */\nexport const EDITOR_MOUNT_ERROR = 'TBW080' as const;\n\n// --- Print (090–099) ---\n/** Print already in progress. */\nexport const PRINT_IN_PROGRESS = 'TBW090' as const;\n/** Grid not available for printing. */\nexport const PRINT_NO_GRID = 'TBW091' as const;\n/** Print operation failed. */\nexport const PRINT_FAILED = 'TBW092' as const;\n/** Multiple elements share the same grid ID (print isolation issue). */\nexport const PRINT_DUPLICATE_ID = 'TBW093' as const;\n\n// --- Clipboard (100–109) ---\n/** Clipboard API write failed. */\nexport const CLIPBOARD_FAILED = 'TBW100' as const;\n\n// --- Plugin-specific (110–119) ---\n/** ResponsivePlugin: no breakpoint configured. */\nexport const MISSING_BREAKPOINT = 'TBW110' as const;\n/** UndoRedoPlugin: transaction already in progress. */\nexport const TRANSACTION_IN_PROGRESS = 'TBW111' as const;\n/** UndoRedoPlugin: no transaction in progress. */\nexport const NO_TRANSACTION = 'TBW112' as const;\n/** GroupingColumnsPlugin: missing id or header on column group definition. */\nexport const COLUMN_GROUP_NO_ID = 'TBW113' as const;\n/** GroupingColumnsPlugin: conflicting columnGroups sources. */\nexport const COLUMN_GROUPS_CONFLICT = 'TBW114' as const;\n\n// --- Style injection (120–129) ---\n/** Failed to extract grid.css from document stylesheets. */\nexport const STYLE_EXTRACT_FAILED = 'TBW120' as const;\n/** Could not find grid.css in document.styleSheets. */\nexport const STYLE_NOT_FOUND = 'TBW121' as const;\n\n// --- Attribute parsing (130–139) ---\n/** Invalid JSON in an HTML attribute. */\nexport const INVALID_ATTRIBUTE_JSON = 'TBW130' as const;\n\n// --- DataSource / ServerSide (140–149) ---\n/** ServerSidePlugin: getRows() rejected. */\nexport const DATASOURCE_FETCH_ERROR = 'TBW140' as const;\n/** ServerSidePlugin: getChildRows() rejected. */\nexport const DATASOURCE_CHILD_FETCH_ERROR = 'TBW141' as const;\n/** ServerSidePlugin: getChildRows() not implemented but a plugin queried for children. */\nexport const DATASOURCE_NO_CHILD_HANDLER = 'TBW142' as const;\n/** ServerSidePlugin: concurrent request limit reached, request deferred. */\nexport const DATASOURCE_THROTTLED = 'TBW143' as const;\n\nexport type DiagnosticCode =\n | typeof MISSING_PLUGIN\n | typeof MISSING_PLUGIN_CONFIG\n | typeof CONFIG_RULE_ERROR\n | typeof CONFIG_RULE_WARN\n | typeof MISSING_DEPENDENCY\n | typeof OPTIONAL_DEPENDENCY\n | typeof INCOMPATIBLE_PLUGINS\n | typeof PLUGIN_ALIAS_COLLAPSE\n | typeof PLUGIN_EVENT_ERROR\n | typeof PLUGIN_ALIAS_CONFIG_CONFLICT\n | typeof FEATURE_REREGISTERED\n | typeof FEATURE_NOT_IMPORTED\n | typeof FEATURE_MISSING_DEP\n | typeof MISSING_ROW_ID\n | typeof ROW_NOT_FOUND\n | typeof INVALID_COLUMN_WIDTH\n | typeof ROW_CLASS_ERROR\n | typeof CELL_CLASS_ERROR\n | typeof FORMAT_ERROR\n | typeof VIEW_MOUNT_ERROR\n | typeof VIEW_DISPATCH_ERROR\n | typeof TOOL_PANEL_MISSING_ATTR\n | typeof NO_TOOL_PANELS\n | typeof TOOL_PANEL_NOT_FOUND\n | typeof TOOL_PANEL_DUPLICATE\n | typeof HEADER_CONTENT_DUPLICATE\n | typeof TOOLBAR_CONTENT_DUPLICATE\n | typeof EDITOR_MOUNT_ERROR\n | typeof PRINT_IN_PROGRESS\n | typeof PRINT_NO_GRID\n | typeof PRINT_FAILED\n | typeof PRINT_DUPLICATE_ID\n | typeof CLIPBOARD_FAILED\n | typeof MISSING_BREAKPOINT\n | typeof TRANSACTION_IN_PROGRESS\n | typeof NO_TRANSACTION\n | typeof COLUMN_GROUP_NO_ID\n | typeof COLUMN_GROUPS_CONFLICT\n | typeof STYLE_EXTRACT_FAILED\n | typeof STYLE_NOT_FOUND\n | typeof INVALID_ATTRIBUTE_JSON\n | typeof DATASOURCE_FETCH_ERROR\n | typeof DATASOURCE_CHILD_FETCH_ERROR\n | typeof DATASOURCE_NO_CHILD_HANDLER\n | typeof DATASOURCE_THROTTLED;\n\n// #endregion\n\n// #region Docs URL\n\nconst DOCS_BASE = 'https://toolboxjs.com/grid/errors';\n\n/** Build a direct link to the troubleshooting section for a code. */\nfunction docsUrl(code: DiagnosticCode): string {\n return `${DOCS_BASE}#${code.toLowerCase()}`;\n}\n\n// #endregion\n\n// #region Formatting\n\n/**\n * Format a diagnostic message with prefix, code, and docs link.\n *\n * Output format:\n * ```\n * [tbw-grid#my-id] TBW001: Your message here.\n *\n * → More info: https://toolboxjs.com/grid/errors#tbw001\n * ```\n */\nexport function formatDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): string {\n const prefix = gridPrefix(gridId, pluginName);\n return `${prefix} ${code}: ${message}\\n\\n → More info: ${docsUrl(code)}`;\n}\n\n// #endregion\n\n// #region Public API\n\n/**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\nexport function throwDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): never {\n throw new Error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a warning with a diagnostic code and docs link.\n * Use for recoverable issues the developer should fix.\n */\nexport function warnDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.warn(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a debug message with a diagnostic code and docs link.\n * Use for optional/soft dependency notifications — visible only when\n * the browser DevTools \"Verbose\" log level is enabled.\n */\nexport function debugDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.debug(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log an error with a diagnostic code and docs link.\n * Use for non-throwing errors (e.g., failed async operations).\n */\nexport function errorDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n// #endregion\n","/**\n * Clipboard Copy Logic\n *\n * Pure functions for copying grid data to clipboard.\n */\n\nimport { CLIPBOARD_FAILED, warnDiagnostic } from '../../core/internal/diagnostics';\nimport { resolveCellValue } from '../../core/internal/value-accessor';\nimport type { ColumnConfig } from '../../core/types';\nimport { formatDelimitedValue, type DelimitedFormatOptions } from '../shared/data-collection';\nimport type { ClipboardConfig } from './types';\n\n/** Parameters for building clipboard text */\nexport interface CopyParams {\n /** All grid rows */\n rows: unknown[];\n /** Column configurations */\n columns: ColumnConfig[];\n /** Selected row indices */\n selectedIndices: Set<number> | number[];\n /** Clipboard configuration */\n config: ClipboardConfig;\n}\n\n/**\n * Format a cell value for clipboard output.\n *\n * Uses custom processCell if provided, otherwise applies default formatting:\n * - null/undefined → empty string\n * - Date → ISO string\n * - Object → JSON string\n * - Other → String conversion with optional quoting\n *\n * @param value - The cell value to format\n * @param field - The field name\n * @param row - The full row object\n * @param config - Clipboard configuration\n * @returns Formatted string value\n */\nexport function formatCellValue(value: unknown, field: string, row: unknown, config: ClipboardConfig): string {\n if (config.processCell) {\n return config.processCell(value, field, row);\n }\n return formatDelimitedValue(value, toFormatOptions(config));\n}\n\n/** Map the plugin config onto the shared delimited-format options. */\nfunction toFormatOptions(config: ClipboardConfig): DelimitedFormatOptions {\n return {\n delimiter: config.delimiter ?? '\\t',\n newline: config.newline ?? '\\n',\n quoting: config.quoteStrings ? 'always' : 'auto',\n escapeFormulas: config.escapeFormulas,\n };\n}\n\n/**\n * Build clipboard text from selected rows and columns.\n *\n * @param params - Copy parameters including rows, columns, selection, and config\n * @returns Tab-separated (or custom delimiter) text ready for clipboard\n */\nexport function buildClipboardText(params: CopyParams): string {\n const { rows, columns, selectedIndices, config } = params;\n const delimiter = config.delimiter ?? '\\t';\n const newline = config.newline ?? '\\n';\n // Built once — this runs per cell across the whole selection.\n const format = toFormatOptions(config);\n\n // Filter to visible columns (not hidden, not internal __ prefixed)\n const visibleColumns = columns.filter((c) => !c.hidden && !c.field.startsWith('__'));\n\n const lines: string[] = [];\n\n // Add header row if configured\n if (config.includeHeaders) {\n const headerCells = visibleColumns.map((c) => {\n const header = c.header || c.field;\n // Quote headers if they contain special characters\n if (header.includes(delimiter) || header.includes(newline) || header.includes('\"')) {\n return `\"${header.replace(/\"/g, '\"\"')}\"`;\n }\n return header;\n });\n lines.push(headerCells.join(delimiter));\n }\n\n // Convert indices to sorted array\n const indices = selectedIndices instanceof Set ? [...selectedIndices] : selectedIndices;\n const sortedIndices = [...indices].sort((a, b) => a - b);\n\n // Build data rows\n for (const idx of sortedIndices) {\n const row = rows[idx];\n if (!row) continue;\n\n const cells = visibleColumns.map((col) => {\n const value = resolveCellValue(row, col);\n return config.processCell\n ? config.processCell(value, col.field, row)\n : formatDelimitedValue(value, format);\n });\n lines.push(cells.join(delimiter));\n }\n\n return lines.join(newline);\n}\n\n/**\n * Copy text to the system clipboard.\n *\n * Uses the modern Clipboard API when available, with fallback\n * to execCommand for older browsers.\n *\n * When `html` is supplied, writes BOTH `text/plain` and `text/html` via\n * `ClipboardItem` so a same-app paste (even cross-grid or cross-window) can\n * recover a structured payload embedded in the HTML, while external targets\n * (Excel, editors) still get the plain text. Silently falls back to text-only\n * when `ClipboardItem` / `clipboard.write` is unavailable or rejects.\n *\n * @param text - The text to copy (`text/plain`)\n * @param html - Optional rich `text/html` representation (may embed a payload)\n * @returns Promise resolving to true if successful, false otherwise\n */\nexport async function copyToClipboard(text: string, html?: string): Promise<boolean> {\n if (html && typeof ClipboardItem !== 'undefined' && navigator.clipboard?.write) {\n try {\n await navigator.clipboard.write([\n new ClipboardItem({\n 'text/plain': new Blob([text], { type: 'text/plain' }),\n 'text/html': new Blob([html], { type: 'text/html' }),\n }),\n ]);\n return true;\n } catch {\n // Fall through to text-only write below.\n }\n }\n try {\n await navigator.clipboard.writeText(text);\n return true;\n } catch (err) {\n warnDiagnostic(CLIPBOARD_FAILED, `Clipboard API failed: ${err}`);\n // Fallback for older browsers or when Clipboard API is not available\n const textarea = document.createElement('textarea');\n textarea.value = text;\n textarea.style.position = 'fixed';\n textarea.style.opacity = '0';\n textarea.style.pointerEvents = 'none';\n document.body.appendChild(textarea);\n textarea.select();\n const success = document.execCommand('copy');\n document.body.removeChild(textarea);\n return success;\n }\n}\n","/**\n * Clipboard Paste Logic\n *\n * Pure functions for reading and parsing clipboard data.\n */\n\nimport type { ClipboardConfig } from './types';\n\n/**\n * Parse clipboard text into a 2D array of cell values.\n *\n * Handles:\n * - Quoted values (with escaped double quotes \"\")\n * - Multi-line quoted values (newlines within quotes are preserved)\n * - Custom delimiters and newlines\n * - Empty lines (filtered out only if they contain no data)\n *\n * Implementation: char-by-char state machine for delimiter/newline/quote\n * detection, but cell values are extracted via `slice(start, end)` rather\n * than `+= char` accumulation. The vast majority of cells have no quotes,\n * so the hot path is a single substring slice per cell. Cells that did\n * contain quotes fall back to {@link unquoteCell} for the strip + escape\n * pass.\n *\n * @param text - Raw clipboard text\n * @param config - Clipboard configuration\n * @returns 2D array where each sub-array is a row of cell values\n */\nexport function parseClipboardText(text: string, config: ClipboardConfig): string[][] {\n const delimiter = config.delimiter ?? '\\t';\n const newline = config.newline ?? '\\n';\n\n // Normalize Windows / old-Mac line endings.\n const normalized = text.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n const len = normalized.length;\n\n const rows: string[][] = [];\n let currentRow: string[] = [];\n let cellStart = 0;\n let inQuotes = false;\n let cellHasQuotes = false;\n\n const finalizeCell = (endIdx: number): string => {\n const raw = normalized.slice(cellStart, endIdx);\n return cellHasQuotes ? unquoteCell(raw) : raw;\n };\n\n const pushRow = () => {\n // Filter purely-empty rows (single empty cell or all-whitespace cells),\n // matching the original char-by-char implementation's contract.\n if (currentRow.length > 1 || currentRow.some((c) => c.trim() !== '')) {\n rows.push(currentRow);\n }\n currentRow = [];\n };\n\n for (let i = 0; i < len; i++) {\n const char = normalized[i];\n\n if (char === '\"') {\n cellHasQuotes = true;\n if (!inQuotes) {\n inQuotes = true;\n } else if (normalized[i + 1] === '\"') {\n // Escaped quote (\"\") — skip the second quote, stay inside the quoted run.\n i++;\n } else {\n inQuotes = false;\n }\n } else if (char === delimiter && !inQuotes) {\n currentRow.push(finalizeCell(i));\n cellStart = i + 1;\n cellHasQuotes = false;\n } else if (char === newline && !inQuotes) {\n currentRow.push(finalizeCell(i));\n cellStart = i + 1;\n cellHasQuotes = false;\n pushRow();\n }\n }\n\n // Final cell + row.\n currentRow.push(finalizeCell(len));\n pushRow();\n\n return rows;\n}\n\n/**\n * Strip surrounding quotes and unescape `\"\"` → `\"` from a cell value that\n * contained at least one quote character. Only invoked from the slow path\n * of {@link parseClipboardText}.\n */\nfunction unquoteCell(raw: string): string {\n let out = '';\n let inQuotes = false;\n for (let i = 0; i < raw.length; i++) {\n const c = raw[i];\n if (c === '\"') {\n if (!inQuotes) {\n inQuotes = true;\n } else if (raw[i + 1] === '\"') {\n out += '\"';\n i++;\n } else {\n inQuotes = false;\n }\n } else {\n out += c;\n }\n }\n return out;\n}\n\n/**\n * Read text from the system clipboard.\n *\n * Uses the modern Clipboard API. Returns empty string if\n * the API is not available or permission is denied.\n *\n * @returns Promise resolving to clipboard text or empty string\n */\nexport async function readFromClipboard(): Promise<string> {\n try {\n return await navigator.clipboard.readText();\n } catch {\n // Permission denied or API not available\n return '';\n }\n}\n","/**\n * Clipboard Plugin Types\n *\n * Type definitions for clipboard copy/paste functionality.\n */\n\nimport { invalidateAccessorCache, readCellField, writeCellField } from '../../core/internal/value-accessor';\nimport type { GridElement } from '../../core/plugin/base-plugin';\nimport type { CellEditablePredicate } from '../../core/plugin/types';\n\n/**\n * Custom paste handler function.\n *\n * @param detail - The parsed paste data with target and field info\n * @param grid - The grid element to update\n * @returns `false` to prevent the default paste behavior, or `void`/`true` to allow it\n *\n * @example\n * ```ts\n * // Custom handler that validates before pasting\n * new ClipboardPlugin({\n * pasteHandler: (detail, grid) => {\n * if (!detail.target) return false;\n * // Apply custom validation/transformation...\n * applyPasteData(detail, grid);\n * return false; // We handled it, skip default\n * }\n * })\n * ```\n * @since 0.4.2\n */\nexport type PasteHandler = (detail: PasteDetail, grid: GridElement) => boolean | void;\n\n/**\n * Options for programmatic copy operations.\n *\n * Allows callers to control exactly which columns and rows are included\n * in a copy, independently of the current selection state.\n *\n * @example Copy specific columns from selected rows\n * ```ts\n * const clipboard = grid.getPluginByName('clipboard');\n * // User selected rows 0, 2, 4 via a dialog, chose columns to include\n * const text = await clipboard.copy({\n * rowIndices: [0, 2, 4],\n * columns: ['name', 'email'],\n * includeHeaders: true,\n * });\n * ```\n * @since 1.14.0\n */\nexport interface CopyOptions {\n /** Specific column fields to include. If omitted, uses current selection or all visible columns. */\n columns?: string[];\n /** Specific row indices to copy. If omitted, uses current selection or all rows. */\n rowIndices?: number[];\n /** Include column headers in copied text. Defaults to the plugin config value. */\n includeHeaders?: boolean;\n /** Column delimiter override. Defaults to the plugin config value. */\n delimiter?: string;\n /** Row delimiter override. Defaults to the plugin config value. */\n newline?: string;\n /** Custom cell value processor for this operation. Overrides the plugin config's `processCell`. */\n processCell?: (value: unknown, field: string, row: unknown) => string;\n}\n\n/**\n * Configuration options for the clipboard plugin\n *\n * @since 0.1.1\n */\nexport interface ClipboardConfig {\n /** Include column headers in copied text (default: false) */\n includeHeaders?: boolean;\n /** Column delimiter character (default: '\\t' for tab) */\n delimiter?: string;\n /** Row delimiter/newline character (default: '\\n') */\n newline?: string;\n /** Wrap string values with quotes (default: false) */\n quoteStrings?: boolean;\n /**\n * Neutralize spreadsheet formula injection (CWE-1236) by prefixing string\n * values that start with `=`, `+`, `-`, `@`, TAB or CR with a single quote,\n * so a paste into Excel / LibreOffice / Sheets renders them as text instead\n * of evaluating them (default: `true`).\n *\n * Grid-to-grid paste is unaffected — that path uses the structured\n * `text/html` payload, not the plain-text representation.\n *\n * @since 3.5.0\n */\n escapeFormulas?: boolean;\n /** Custom cell value processor for copy operations */\n processCell?: (value: unknown, field: string, row: unknown) => string;\n /**\n * When `true`, pasting into a multi-cell selection larger than the clipboard\n * source tiles (repeats) the source to fill the whole selection: a single\n * copied cell fills every selected cell, a 1×2 source fills as\n * `val1, val2, val1, val2`, and a 2×2 block tiles across the selection.\n *\n * Only applies when a bounded (multi-cell) selection is active — it never\n * grows the grid. Defaults to `false` (paste writes only the source extent).\n *\n * @default false\n * @since 3.0.0\n */\n fillSelection?: boolean;\n /**\n * Custom paste handler that **fully replaces** {@link defaultPasteHandler}.\n *\n * ⚠️ **Prefer not to override this.** The default handler is no longer a thin\n * \"write text into cells\" routine — it now implements substantial behavior\n * that is easy to get subtly wrong when reimplemented:\n *\n * - editability resolution via the editing plugin (`getCellEditableResolver`,\n * including row-conditional `editable`);\n * - per-column {@link BaseColumnConfig.onPaste} reject / transform, plus the\n * `paste-rejected` event;\n * - structured (`rawRows`) same-grid / cross-window values so object cells\n * round-trip losslessly (not degraded to display text);\n * - fill-selection tiling of a smaller source across a bounded selection;\n * - routing edits through `grid.updateRows(…, 'paste')` so paste participates\n * in dirty tracking, undo/redo history, cancelable validation, and the\n * `'paste'` commit source — with a direct-write fallback for id-less rows;\n * - growing the grid for unbounded single-cell-expansion pastes.\n *\n * **Use an extension point instead of overriding, wherever possible:**\n * - reject / transform per cell → column {@link BaseColumnConfig.onPaste};\n * - coerce / veto committed values globally → the cancelable `cell-commit`\n * event (`detail.source === 'paste'` distinguishes a paste);\n * - observe the raw paste → the `paste` event;\n * - react to rejected cells → the `paste-rejected` event.\n *\n * Other values:\n * - `null` — disable auto-paste entirely (the `paste` event still fires).\n * - a function returning `false` — you handled it; skip the default.\n *\n * **If you must override** (e.g. bespoke target placement or side effects),\n * you are responsible for re-implementing the behaviors above. At minimum,\n * honor per-column `onPaste` with the exported {@link resolveColumnPaste} and\n * fire {@link emitPasteRejected} so `onPaste` and `paste-rejected` keep working:\n *\n * ```ts\n * import { resolveColumnPaste, emitPasteRejected } from '@toolbox-web/grid/plugins/clipboard';\n *\n * pasteHandler: (detail, grid) => {\n * const rejected = [];\n * for (const cell of myCells(detail)) {\n * const res = resolveColumnPaste(cell.column.onPaste, {\n * value: cell.value, field: cell.field, row: cell.row,\n * rowIndex: cell.rowIndex, oldValue: cell.oldValue, sourceField: cell.sourceField,\n * });\n * if (res.accepted) writeCell(cell, res.value);\n * else rejected.push({ field: cell.field, rowIndex: cell.rowIndex, row: cell.row, value: cell.value, reason: res.reason });\n * }\n * emitPasteRejected(grid, rejected);\n * return false; // handled\n * }\n * ```\n *\n * @default defaultPasteHandler (auto-applies paste data)\n */\n pasteHandler?: PasteHandler | null;\n}\n\n/** Internal state managed by the clipboard plugin */\nexport interface ClipboardState {\n /** The last copied text (for reference/debugging) */\n lastCopied: string | null;\n}\n\n/**\n * Event detail emitted after a successful copy operation\n *\n * @since 0.1.1\n */\nexport interface CopyDetail {\n /** The text that was copied to clipboard */\n text: string;\n /** Number of rows copied */\n rowCount: number;\n /** Number of columns copied */\n columnCount: number;\n}\n\n/**\n * Target cell coordinates and bounds for paste operations\n *\n * @since 0.4.2\n */\nexport interface PasteTarget {\n /** Target row index (top-left of paste area) */\n row: number;\n /** Target column index (top-left of paste area) */\n col: number;\n /** Target column field name (for easy data mapping) */\n field: string;\n /**\n * Selection bounds that constrain the paste area.\n * If set, paste data will be clipped to fit within these bounds.\n * If null, paste expands freely from the target cell.\n */\n bounds: {\n /** End row index (inclusive) */\n endRow: number;\n /** End column index (inclusive) */\n endCol: number;\n } | null;\n}\n\n/**\n * Event detail emitted after a paste operation\n *\n * @since 0.1.1\n */\nexport interface PasteDetail {\n /** Parsed rows from clipboard (2D array of cell values) */\n rows: string[][];\n /** Raw text that was pasted */\n text: string;\n /** The target cell where paste starts (top-left of paste area). Null if no cell is selected. */\n target: PasteTarget | null;\n /**\n * Column fields for each column in the paste range, starting from target.col.\n * Useful for mapping parsed cell values to data fields.\n * Length matches the width of the pasted data (or available columns, whichever is smaller).\n * When `fillSelection` is active, this spans the full selection width so the source can be tiled.\n */\n fields: string[];\n /**\n * When `true`, the paste handler tiles the clipboard source across the full\n * selection bounds (see {@link ClipboardConfig.fillSelection}). Set by the\n * plugin from config; only ever `true` when `target.bounds` is set.\n * @since 3.0.0\n */\n fillSelection?: boolean;\n /**\n * Raw (structured) cell values for a **same-grid** paste, aligned 1:1 with\n * {@link rows} (same dimensions, data rows only). Present only when the paste\n * text exactly matches what this grid last copied — so an object-valued cell\n * (e.g. `{id,name}`, arrays) round-trips losslessly instead of degrading to\n * the copied display text. `undefined` for external/cross-grid pastes, where\n * only the text is available. The default handler prefers `rawRows` over\n * `rows` for values (cloning each so targets don't share references).\n * @since 3.0.0\n */\n rawRows?: unknown[][];\n /**\n * Source column fields the pasted data was copied FROM, in the copied column\n * order (aligned with the columns of {@link rows} / {@link rawRows}). Lets a\n * column's {@link BaseColumnConfig.onPaste} know the origin column so it can\n * reject cross-column / cross-type pastes. Present for a **same-app** paste\n * (same grid via the in-memory buffer, or cross-grid / cross-window via the\n * structured `text/html` payload); `undefined` for external pastes (e.g. from\n * Excel), where only the text is available.\n * @since 3.0.0\n */\n sourceFields?: string[];\n}\n\n/**\n * Context passed to a column's {@link BaseColumnConfig.onPaste} guard/transform\n * for each cell a paste would write into.\n *\n * @since 3.0.0\n */\nexport interface PasteCellContext<TRow = unknown, TValue = unknown> {\n /** The incoming pasted value (the raw structured value for a same-grid paste). */\n value: TValue;\n /** The column field receiving the value. */\n field: string;\n /** The target row object (pre-paste state). */\n row: TRow;\n /** Index of the target row in the current data set. */\n rowIndex: number;\n /** The cell's current value before the paste. */\n oldValue: TValue;\n /**\n * The column field the value was copied FROM (the origin column), for a\n * same-app paste. `undefined` for an external paste (e.g. from Excel) or when\n * the source column is unknown. Use it to reject cross-column / cross-type\n * pastes (e.g. refuse a value copied from a \"car\" column into a \"fruit\"\n * column). To compare types, look up your own column config by this field.\n * @since 3.0.0\n */\n sourceField?: string;\n}\n\n/**\n * A column's paste guard / transform (`onPaste`). See {@link BaseColumnConfig.onPaste}.\n * @since 3.0.0\n */\nexport type ColumnPasteGuard<TRow = unknown, TValue = unknown> =\n boolean | ((ctx: PasteCellContext<TRow, TValue>) => boolean | { value: TValue });\n\n/**\n * Why a cell's paste was rejected: `'column'` = the column set `onPaste: false`;\n * `'cell'` = an `onPaste` callback returned `false`.\n * @since 3.0.0\n */\nexport type PasteRejectionReason = 'column' | 'cell';\n\n/** One cell whose paste a column's `onPaste` guard rejected. @since 3.0.0 */\nexport interface PasteRejectedCell {\n /** The column field. */\n field: string;\n /** Index of the target row in the current data set. */\n rowIndex: number;\n /** The target row object. */\n row: unknown;\n /** The incoming value that was rejected (before any transform). */\n value: unknown;\n /** Why it was rejected. */\n reason: PasteRejectionReason;\n}\n\n/**\n * Detail for the `paste-rejected` event: the cells a column's `onPaste` guard\n * rejected during a paste. Fires once per paste, only when at least one cell was\n * rejected, so a consumer can surface a message. Non-editable cells are skipped\n * silently and are NOT reported here.\n * @since 3.0.0\n */\nexport interface PasteRejectedDetail {\n /** Cells rejected by a column `onPaste` guard. */\n rejected: PasteRejectedCell[];\n}\n\n/** Result of {@link resolveColumnPaste}. @since 3.0.0 */\nexport type PasteResolution = { accepted: true; value: unknown } | { accepted: false; reason: PasteRejectionReason };\n\n/**\n * Resolve a column's {@link ColumnPasteGuard} (`onPaste`) for a single cell.\n *\n * Pure — never mutates or emits. Exposed so a **custom `pasteHandler`** can honor\n * per-column `onPaste` identically to {@link defaultPasteHandler}: call it per\n * cell, write `resolution.value` when accepted, and collect the rejected cells to\n * pass to {@link emitPasteRejected}.\n *\n * @param onPaste - The column's `onPaste` config (`column.onPaste`), if any.\n * @param ctx - The cell context (value, field, row, rowIndex, oldValue, sourceField).\n * @returns `{ accepted: true, value }` (value possibly transformed) or\n * `{ accepted: false, reason }`.\n *\n * @example\n * ```ts\n * // Inside a custom pasteHandler:\n * const rejected: PasteRejectedCell[] = [];\n * const res = resolveColumnPaste(column.onPaste, { value, field, row, rowIndex, oldValue });\n * if (res.accepted) row[field] = res.value;\n * else rejected.push({ field, rowIndex, row, value, reason: res.reason });\n * // …after writing all cells:\n * emitPasteRejected(grid, rejected);\n * ```\n * @since 3.0.0\n */\nexport function resolveColumnPaste(onPaste: ColumnPasteGuard | undefined, ctx: PasteCellContext): PasteResolution {\n if (onPaste === false) return { accepted: false, reason: 'column' };\n if (typeof onPaste === 'function') {\n const result = onPaste(ctx);\n if (result === false) return { accepted: false, reason: 'cell' };\n if (result && typeof result === 'object' && 'value' in result) {\n return { accepted: true, value: (result as { value: unknown }).value };\n }\n }\n return { accepted: true, value: ctx.value };\n}\n\n/**\n * Dispatch the `paste-rejected` event on the grid. No-op when `rejected` is empty.\n * Exposed so a custom `pasteHandler` can emit the same event the default handler\n * does, after resolving cells via {@link resolveColumnPaste}.\n * @since 3.0.0\n */\nexport function emitPasteRejected(grid: GridElement, rejected: PasteRejectedCell[]): void {\n if (rejected.length === 0) return;\n grid.dispatchEvent?.(new CustomEvent<PasteRejectedDetail>('paste-rejected', { detail: { rejected }, bubbles: true }));\n}\n\n/**\n * Default paste handler that applies pasted data to the grid.\n *\n * This is the built-in handler used when no custom `pasteHandler` is configured.\n *\n * Edits are routed through `grid.updateRows(…, 'paste')` so paste participates in\n * the full edit pipeline — dirty tracking, cancelable validation, undo/redo\n * history, and abortion — exactly like an interactive edit, and so a\n * `cell-commit` listener can tell a paste apart via `detail.source === 'paste'`.\n * Rows without a resolvable ID (no `getRowId`) fall back to a direct in-place\n * write, which still updates values but does not track them.\n *\n * Behavior:\n * - Single cell selection: paste expands freely, adds new rows if needed\n * - Range/row selection: paste is clipped to fit within selection bounds\n * - Fill-selection: when `detail.fillSelection` is set, the source is tiled\n * (repeated via modulo indexing) to fill the whole selection bounds\n * - Non-editable cells: values are skipped (column alignment preserved).\n * Editability is resolved by the editing plugin via the\n * `getCellEditableResolver` query — including row-conditional `editable`.\n * Without the editing plugin, no cell is editable and paste is a no-op.\n * - Per-column `onPaste`: after the editability check, a column's\n * {@link BaseColumnConfig.onPaste} may reject the cell (skipped, alignment\n * preserved) or transform the pasted value. Rejected cells are reported once\n * via the `paste-rejected` event ({@link PasteRejectedDetail}).\n *\n * @param detail - The parsed paste data from clipboard\n * @param grid - The grid element to update\n * @since 0.4.2\n */\nexport function defaultPasteHandler(detail: PasteDetail, grid: GridElement): void {\n const { rows: pastedRows, rawRows, target, fields, fillSelection, sourceFields } = detail;\n\n // No target = nothing to do\n if (!target || pastedRows.length === 0) return;\n\n // Prefer the raw structured values for a same-grid paste (lossless object\n // round-trip); fall back to the parsed text for external/cross-grid pastes.\n const valueRows: unknown[][] = rawRows ?? pastedRows;\n\n const currentRows = grid.rows as Record<string, unknown>[];\n const columns = grid.effectiveConfig.columns ?? [];\n const allFields = columns.map((col) => col.field);\n const columnByField = new Map(columns.map((col) => [col.field, col]));\n\n // Editability is owned by the editing plugin (row-conditional included). With\n // no editing plugin loaded, nothing is editable and paste is a no-op.\n const [canEditCell = () => false] = grid.query<CellEditablePredicate>('getCellEditableResolver');\n\n // Accumulate the intended edits per target row (honoring editability, tiling,\n // and selection bounds). Rows beyond current data are only created for an\n // unbounded (single-cell expansion) paste.\n const editsByRow = new Map<number, Record<string, unknown>>();\n let rowCountAfter = currentRows.length;\n // Cells a column's `onPaste` guard rejected — reported via `paste-rejected`.\n const rejected: PasteRejectedCell[] = [];\n\n const addEdit = (\n rowIndex: number,\n field: string | undefined,\n value: unknown,\n evalRow: Record<string, unknown>,\n sourceField?: string,\n ) => {\n if (!field || !canEditCell(field, evalRow)) return;\n // Per-column paste guard/transform (clipboard-augmented `onPaste`). Runs\n // after the editability check so it only sees cells paste could write.\n const resolution = resolveColumnPaste(columnByField.get(field)?.onPaste, {\n value,\n field,\n row: evalRow,\n rowIndex,\n oldValue: readCellField(evalRow, field),\n sourceField,\n });\n if (!resolution.accepted) {\n rejected.push({ field, rowIndex, row: evalRow, value, reason: resolution.reason });\n return; // reject this cell (alignment preserved, like a non-editable cell)\n }\n let changes = editsByRow.get(rowIndex);\n if (!changes) {\n changes = {};\n editsByRow.set(rowIndex, changes);\n }\n // Clone structured values so tiled/multi-target pastes never share a\n // reference (mutating one row's object would otherwise mutate the source\n // and every other target). No-op for primitives (the text path).\n changes[field] = cloneStructured(resolution.value);\n };\n\n if (fillSelection && target.bounds) {\n // Fill-selection (tile) mode: repeat the source across the selection bounds\n // using modulo indexing. Never grows the grid. Editability is resolved\n // against the pre-paste row (bounded mode never grows).\n const srcRows = valueRows.length;\n for (let rowIndex = target.row; rowIndex <= target.bounds.endRow; rowIndex++) {\n if (rowIndex >= currentRows.length) break;\n const sourceRow = valueRows[(rowIndex - target.row) % srcRows];\n const srcCols = sourceRow.length;\n if (srcCols === 0) continue;\n const evalRow = currentRows[rowIndex];\n fields.forEach((field, colOffset) =>\n addEdit(rowIndex, field, sourceRow[colOffset % srcCols], evalRow, sourceFields?.[colOffset % srcCols]),\n );\n }\n } else {\n const maxPasteRow = target.bounds ? target.bounds.endRow : Infinity;\n valueRows.forEach((rowData, rowOffset) => {\n const targetRowIndex = target.row + rowOffset;\n if (targetRowIndex > maxPasteRow) return;\n if (target.bounds && targetRowIndex >= currentRows.length) return; // bounded: don't grow\n if (!target.bounds && targetRowIndex >= rowCountAfter) rowCountAfter = targetRowIndex + 1;\n // Editability resolved against the pre-paste row (empty for grown rows).\n const evalRow = currentRows[targetRowIndex] ?? {};\n rowData.forEach((cellValue, colOffset) =>\n addEdit(targetRowIndex, fields[colOffset], cellValue, evalRow, sourceFields?.[colOffset]),\n );\n });\n }\n\n if (editsByRow.size === 0 && rowCountAfter === currentRows.length) {\n // Nothing accepted (e.g. every cell rejected) — still report the rejections.\n emitPasteRejected(grid, rejected);\n return;\n }\n\n // Grow the grid first for unbounded pastes that extend past existing data.\n // New rows start empty; their pasted values are applied in the write phase.\n if (rowCountAfter > currentRows.length) {\n const grown = [...currentRows];\n while (grown.length < rowCountAfter) {\n const emptyRow: Record<string, unknown> = {};\n allFields.forEach((f) => (emptyRow[f] = ''));\n grown.push(emptyRow);\n }\n grid.rows = grown;\n }\n\n const rowsNow = grid.rows as Record<string, unknown>[];\n\n // Route edits through updateRows so paste flows through the edit pipeline\n // (dirty tracking, validation, history, abortion). Rows without a resolvable\n // ID fall back to a direct in-place write.\n const updates: Array<{ id: string; changes: Record<string, unknown> }> = [];\n let directWrote = false;\n for (const [rowIndex, changes] of editsByRow) {\n const row = rowsNow[rowIndex];\n if (!row) continue;\n let id: string | undefined;\n try {\n id = grid.getRowId(row);\n } catch {\n id = undefined;\n }\n if (id == null) {\n // No stable ID → can't route through updateRow; write directly. Invalidate\n // the value-accessor cache per field (row identity is unchanged), matching\n // RowManager.updateRow — otherwise a `column.valueAccessor` display can go\n // stale after paste.\n for (const [field, value] of Object.entries(changes)) {\n writeCellField(row, field, value);\n invalidateAccessorCache(row as object, field);\n }\n directWrote = true;\n } else {\n updates.push({ id, changes });\n }\n }\n\n if (updates.length > 0) {\n grid.updateRows(updates, 'paste');\n } else if (directWrote && rowCountAfter === currentRows.length) {\n // Direct writes only, no structural grow above — trigger a render.\n grid.rows = [...rowsNow];\n }\n\n // Notify consumers of any cells a column's `onPaste` guard rejected.\n emitPasteRejected(grid, rejected);\n}\n\n/**\n * Deep-clone a structured cell value so tiled/multi-target pastes don't share a\n * reference. Primitives (and `null`) are returned as-is. Falls back to the\n * original value if it isn't structured-cloneable.\n * @internal\n */\nexport function cloneStructured(value: unknown): unknown {\n if (value === null || typeof value !== 'object') return value;\n try {\n return structuredClone(value);\n } catch {\n return value;\n }\n}\n\n// Module Augmentation - Register plugin name for type-safe getPluginByName()\ndeclare module '../../core/types' {\n interface DataGridEventMap {\n /** Fired after a successful copy operation. Provides the copied text, row count, and column count. @group Clipboard Events */\n copy: CopyDetail;\n /** Fired after a paste operation. Provides parsed rows, target cell, and column fields. @group Clipboard Events */\n paste: PasteDetail;\n /** Fired after a paste when one or more cells were rejected by a column's `onPaste` guard. @group Clipboard Events */\n 'paste-rejected': PasteRejectedDetail;\n }\n\n interface PluginNameMap {\n clipboard: import('./clipboard-plugin').ClipboardPlugin;\n }\n\n /** Clipboard contributes its own edit-origin tag so a `cell-commit` listener\n * can tell a paste from a user edit via `detail.source === 'paste'`. */\n interface UpdateSourceMap {\n /** A value written by a clipboard paste. @since 3.0.0 */\n paste: true;\n }\n\n interface BaseColumnConfig<TRow, TValue> {\n /**\n * Per-column paste guard / transform. Requires the ClipboardPlugin.\n *\n * Runs for each cell a paste would write into (after the editing plugin's\n * editability check), letting a consumer reject or rewrite pasted values\n * **per column** — finer-grained than the whole-operation\n * {@link ClipboardConfig.pasteHandler}.\n *\n * - `true` / omitted — accept pastes (default behavior).\n * - `false` — this column never accepts pastes; matching cells are skipped\n * (column alignment is preserved, like a non-editable cell).\n * - `(ctx) => …` — called per cell. Return `false` to reject just that cell,\n * `true` (or nothing) to accept as-is, or `{ value }` to write a\n * transformed value instead. Returning `{ value }` disambiguates a\n * transform from a boolean verdict, so boolean-valued columns stay safe.\n *\n * Synchronous only — the paste pipeline does not await. Rejected cells\n * (either form) are reported once per paste via the `paste-rejected` event\n * ({@link PasteRejectedDetail}), so a consumer can show a message.\n *\n * The callback context also carries `sourceField` — the column the value was\n * copied FROM (for a same-app paste; `undefined` for external pastes) — so a\n * column can reject cross-column / cross-type pastes (e.g. refuse a value\n * copied from a \"car\" column into a \"fruit\" column).\n *\n * @example\n * ```typescript\n * // Reject pastes into a locked column\n * { field: 'id', onPaste: false }\n *\n * // Reject invalid values, coerce the rest\n * {\n * field: 'price',\n * onPaste: ({ value }) => {\n * const n = Number(String(value).replace(/[^0-9.]/g, ''));\n * return Number.isFinite(n) ? { value: n } : false;\n * },\n * }\n * ```\n * @since 3.0.0\n */\n onPaste?: ColumnPasteGuard<TRow, TValue>;\n }\n}\n","/**\n * Clipboard Plugin (Class-based)\n *\n * Provides copy/paste functionality for tbw-grid.\n * Supports Ctrl+C/Cmd+C for copying and Ctrl+V/Cmd+V for pasting.\n *\n * **With Selection plugin:** Copies selected cells/rows/range\n * **Without Selection plugin:** Copies entire grid\n */\n\nimport { resolveCellValue } from '../../core/internal/value-accessor';\nimport {\n BaseGridPlugin,\n type GridElement,\n type PluginDependency,\n type PluginManifest,\n type PluginQuery,\n} from '../../core/plugin/base-plugin';\nimport type { ColumnConfig } from '../../core/types';\nimport { readCellDisplayText } from '../shared/cell-text';\nimport { formatValueAsText, resolveColumns, resolveRows } from '../shared/data-collection';\nimport { buildClipboardHtml, parseClipboardHtmlPayload } from './clipboard-payload';\nimport { copyToClipboard } from './copy';\nimport { parseClipboardText, readFromClipboard } from './paste';\nimport {\n cloneStructured,\n defaultPasteHandler,\n type ClipboardConfig,\n type CopyDetail,\n type CopyOptions,\n type PasteDetail,\n type PasteTarget,\n} from './types';\n\n/**\n * Clipboard Plugin for tbw-grid\n *\n * Brings familiar copy/paste functionality with full keyboard shortcut support\n * (Ctrl/Cmd+C, Ctrl/Cmd+V). Handles single cells, multi-cell selections, and integrates\n * seamlessly with Excel and other spreadsheet applications via tab-delimited output.\n *\n * > **Optional Dependency:** Works best with SelectionPlugin for copying/pasting selected\n * > cells. Without SelectionPlugin, copies the entire grid and pastes at row 0, column 0.\n *\n * ## Installation\n *\n * ```ts\n * import { ClipboardPlugin } from '@toolbox-web/grid/plugins/clipboard';\n * ```\n *\n * ## Keyboard Shortcuts\n *\n * | Shortcut | Action |\n * |----------|--------|\n * | `Ctrl+C` / `Cmd+C` | Copy selected cells |\n * | `Ctrl+V` / `Cmd+V` | Paste into selected cells |\n *\n * ## Paste Behavior by Selection Type\n *\n * | Selection Type | Paste Behavior |\n * |----------------|----------------|\n * | Single cell | Paste expands freely from that cell |\n * | Range selection | Paste is clipped to fit within the selected range |\n * | Row selection | Paste is clipped to the selected rows |\n * | No selection | Paste starts at row 0, column 0 |\n *\n * With `fillSelection: true`, pasting a smaller source into a larger bounded\n * selection tiles the source to fill the whole selection (e.g. one cell fills\n * all selected cells; a 1×2 source fills as `val1, val2, val1, val2`).\n *\n * @example Basic Usage with Excel Compatibility\n * ```ts\n * import '@toolbox-web/grid';\n * import { ClipboardPlugin } from '@toolbox-web/grid/plugins/clipboard';\n * import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection';\n *\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name' },\n * { field: 'email', header: 'Email' },\n * ],\n * plugins: [\n * new SelectionPlugin({ mode: 'range' }),\n * new ClipboardPlugin({\n * includeHeaders: true,\n * delimiter: '\\t', // Tab for Excel\n * }),\n * ],\n * };\n * ```\n *\n * @example Custom Paste Handler\n * ```ts\n * new ClipboardPlugin({\n * pasteHandler: (grid, target, data) => {\n * // Validate or transform data before applying\n * console.log('Pasting', data.length, 'rows');\n * return defaultPasteHandler(grid, target, data);\n * },\n * })\n * ```\n *\n * @see {@link ClipboardConfig} for all configuration options\n * @see SelectionPlugin for enhanced copy/paste with selection\n *\n * @internal Extends BaseGridPlugin\n * @since 0.1.1\n */\nexport class ClipboardPlugin extends BaseGridPlugin<ClipboardConfig> {\n /**\n * Plugin manifest — declares queries for inter-plugin communication.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n queries: [{ type: 'clipboard:copy', description: 'Triggers a copy operation and returns the copied text' }],\n };\n\n /**\n * Plugin dependencies - ClipboardPlugin works best with SelectionPlugin.\n *\n * Without SelectionPlugin: copies entire grid, pastes at row 0 col 0.\n * With SelectionPlugin: copies/pastes based on selection.\n */\n /** @internal */\n static override readonly dependencies: PluginDependency[] = [\n { name: 'selection', required: false, reason: 'Enables copy/paste of selected cells instead of entire grid' },\n ];\n\n /** @internal */\n readonly name = 'clipboard';\n\n /** @internal */\n protected override get defaultConfig(): Partial<ClipboardConfig> {\n return {\n includeHeaders: false,\n delimiter: '\\t',\n newline: '\\n',\n quoteStrings: false,\n };\n }\n\n // #region Internal State\n /** The last copied text (for reference/debugging) */\n private lastCopied: { text: string; timestamp: number } | null = null;\n /**\n * Raw (structured) values for the last copy, so a same-grid paste round-trips\n * object cells losslessly instead of degrading to the copied display text.\n * `text` is the exact clipboard text; a paste whose text matches restores\n * `rawRows` (data rows only, aligned with the copied columns). Cleared on\n * detach; a non-matching external paste simply ignores it.\n */\n #internalClipboard: { text: string; rawRows: unknown[][]; fields: string[] } | null = null;\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n // Listen for native paste events to get clipboard data synchronously\n // This is more reliable than the async Clipboard API in iframe contexts\n grid.addEventListener('paste', (e) => this.#handleNativePaste(e), {\n signal: this.disconnectSignal,\n });\n }\n\n /** @internal */\n override detach(): void {\n this.lastCopied = null;\n this.#internalClipboard = null;\n }\n\n /** @internal */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'clipboard:copy') {\n this.copy();\n return true;\n }\n return undefined;\n }\n // #endregion\n\n // #region Event Handlers\n\n /** @internal */\n override onKeyDown(event: KeyboardEvent): boolean {\n const isCopy = (event.ctrlKey || event.metaKey) && event.key === 'c';\n\n if (isCopy) {\n // Prevent the browser's default copy action so it doesn't overwrite\n // our clipboard write with whatever text is selected in the DOM.\n event.preventDefault();\n this.#handleCopy(event.target as HTMLElement);\n return true;\n }\n\n // For paste, we do NOT return true - let the native paste event fire\n // so we can access clipboardData synchronously in #handleNativePaste\n return false;\n }\n // #endregion\n\n // #region Private Methods\n\n /**\n * Handle copy operation from keyboard shortcut.\n *\n * For keyboard-triggered copies, respects the current selection or\n * falls back to the focused cell from the DOM.\n */\n #handleCopy(target: HTMLElement): void {\n const selection = this.#getSelection();\n\n // Selection plugin exists but has no ranges → resolve the single active cell.\n // Range mode CLEARS `ranges` on plain keyboard navigation (arrow/Tab without\n // Shift), keeping the focused cell only as the selection `anchor`, and DOM\n // focus is NOT on a cell element after keyboard nav — so prefer the anchor\n // (a visible-column index) and only fall back to the focused DOM cell for\n // the mouse-focus / no-anchor edge. Without the anchor path, keyboard-nav\n // copy silently no-ops.\n if (selection && selection.ranges.length === 0) {\n let row: number | undefined;\n let field: string | undefined;\n if (selection.anchor) {\n row = selection.anchor.row;\n field = this.visibleColumns[selection.anchor.col]?.field;\n } else {\n const focused = this.#getFocusedCellFromDOM(target);\n if (focused) {\n row = focused.row;\n field = this.columns[focused.col]?.field;\n }\n }\n if (row == null || field == null) return;\n this.copy({ rowIndices: [row], columns: [field] });\n return;\n }\n\n // Delegate to the public copy() method (selection or full grid)\n this.copy();\n }\n\n /**\n * Handle native paste event (preferred method - works in iframes).\n * Uses synchronous clipboardData from the native paste event.\n *\n * Flow:\n * 1. Parse clipboard text\n * 2. Build target/fields info from selection\n * 3. Emit 'paste' event (for listeners)\n * 4. Call paste handler (if configured) to apply data to grid\n *\n * Selection behavior:\n * - Single cell: paste starts at cell, expands freely\n * - Range/row: paste is clipped to fit within selection bounds\n * - No selection: paste starts at row 0, col 0\n */\n #handleNativePaste(event: ClipboardEvent): void {\n // If the paste target is an editable form control or contenteditable element\n // (e.g. an active cell editor injected by EditingPlugin, a filter input, a\n // header search box), let the browser deliver the paste to that control\n // natively. Without this guard `event.preventDefault()` below would swallow\n // the user's keystroke and the editor input would never receive the text.\n if (this.#isEditablePasteTarget(event.target)) return;\n\n const text = event.clipboardData?.getData('text/plain');\n if (!text) return;\n\n // Prevent default to avoid pasting into contenteditable elements\n event.preventDefault();\n\n const parsed = parseClipboardText(text, this.config);\n\n // Get target cell from selection via query\n const selection = this.#getSelection();\n const ranges = selection?.ranges ?? [];\n const firstRange = ranges[0];\n\n // Aggregate the bounding box across ALL selected ranges. A multi-cell\n // selection is frequently expressed as several single-cell ranges (clicking\n // or dragging cell-by-cell), so inspecting `ranges[0]` alone would treat it\n // as a single cell and skip fill/clip — pasting into only the first cell.\n // min/max also normalizes reversed (bottom-up / right-to-left) drags.\n let selMinRow = Infinity;\n let selMinCol = Infinity;\n let selMaxRow = -Infinity;\n let selMaxCol = -Infinity;\n for (const r of ranges) {\n selMinRow = Math.min(selMinRow, r.from.row, r.to.row);\n selMaxRow = Math.max(selMaxRow, r.from.row, r.to.row);\n selMinCol = Math.min(selMinCol, r.from.col, r.to.col);\n selMaxCol = Math.max(selMaxCol, r.from.col, r.to.col);\n }\n\n // Determine target cell (top-left of the whole selection). Range mode\n // CLEARS `ranges` on plain keyboard navigation (arrow/Tab without Shift),\n // keeping the focused cell only as the selection `anchor`. So when there are\n // no ranges, fall back to the anchor before defaulting to (0,0) — otherwise\n // \"copy, arrow to another cell, paste\" always lands on cell (0,0) instead of\n // the active cell. Mirrors the focused-cell fallback in #handleCopy.\n const anchor = selection?.anchor ?? null;\n const targetRow = firstRange ? selMinRow : (anchor?.row ?? 0);\n const targetCol = firstRange ? selMinCol : (anchor?.col ?? 0);\n\n // Multi-cell when the selection spans more than one cell in total, whether\n // it's one large range or many single-cell ranges.\n const axis = selectionAxis(selection);\n const isMultiCell =\n !!firstRange && (axis === 'range' || axis === 'row') && (selMinRow !== selMaxRow || selMinCol !== selMaxCol);\n\n const bounds = isMultiCell ? { endRow: selMaxRow, endCol: selMaxCol } : null;\n // Selection range indices are visible-column indices (from data-col)\n const maxCol = bounds?.endCol ?? this.visibleColumns.length - 1;\n\n // Drop a copied header row so a round-trip paste writes only values into\n // cells (never the header label). Strips when this grid is configured to\n // include headers AND every cell of the first parsed row is one of this\n // grid's own column labels — independent of which column is pasted INTO, so\n // cross-column pastes (e.g. \"Source terminal\" → \"Terminal route\") strip too.\n // External pastes (first row ≠ labels) are left intact.\n this.#dropCopiedHeaderRow(parsed);\n\n // Same-grid round-trip: when the pasted text is exactly what THIS grid last\n // copied, restore the raw structured values (lossless object cells) aligned\n // with the header-stripped text rows — this path preserves exact value types\n // (e.g. Date) since it never serializes. For a cross-grid / cross-window\n // paste the in-memory buffer misses, so fall back to the structured payload\n // embedded in the clipboard's text/html. External pastes have neither and\n // use the parsed text.\n let rawRows: unknown[][] | undefined;\n let sourceFields: string[] | undefined;\n if (\n this.#internalClipboard &&\n this.#internalClipboard.text === text &&\n this.#internalClipboard.rawRows.length === parsed.length\n ) {\n rawRows = this.#internalClipboard.rawRows;\n sourceFields = this.#internalClipboard.fields;\n } else {\n const payload = parseClipboardHtmlPayload(event.clipboardData?.getData('text/html'));\n if (payload && payload.rows.length === parsed.length) {\n rawRows = payload.rows;\n sourceFields = payload.fields;\n }\n }\n\n // Build target info\n const column = this.visibleColumns[targetCol];\n const target: PasteTarget | null = column ? { row: targetRow, col: targetCol, field: column.field, bounds } : null;\n\n // Fill-selection tiling only applies to a bounded (multi-cell) selection.\n const fillSelection = this.config.fillSelection === true && bounds !== null;\n\n // Build field list for paste width (constrained by bounds if set). When\n // filling the selection, extend fields across the full selection width so\n // the source can be tiled to fill every selected column; otherwise cap at\n // the clipboard's own width.\n const fields: string[] = [];\n const pasteWidth = parsed[0]?.length ?? 0;\n const fieldWidth = fillSelection ? maxCol - targetCol + 1 : pasteWidth;\n for (let i = 0; i < fieldWidth && targetCol + i <= maxCol; i++) {\n const col = this.visibleColumns[targetCol + i];\n if (col) {\n fields.push(col.field);\n }\n }\n\n const detail: PasteDetail = { rows: parsed, text, target, fields, fillSelection, rawRows, sourceFields };\n\n // Emit the event for any listeners\n this.emit<PasteDetail>('paste', detail);\n\n // Apply paste data using the configured handler (or default)\n this.#applyPasteHandler(detail);\n }\n\n /**\n * Apply the paste handler to update grid data.\n *\n * Uses the configured `pasteHandler`, or the default handler if not specified.\n * Set `pasteHandler: null` in config to disable auto-paste.\n */\n #applyPasteHandler(detail: PasteDetail): void {\n if (!this.grid) return;\n\n const { pasteHandler } = this.config;\n\n // pasteHandler: null means explicitly disabled\n if (pasteHandler === null) return;\n\n // Use custom handler or default\n const handler = pasteHandler ?? defaultPasteHandler;\n handler(detail, this.grid);\n }\n\n /**\n * Drop a header row that this grid itself copied, so a round-trip paste\n * never lands a header label in a data cell.\n *\n * A row copied with `includeHeaders` consists entirely of this grid's own\n * column labels (`header || field`). Detect that by matching every cell of the\n * first parsed row against the full set of column labels — independent of\n * which column the user pastes INTO. This is essential for **cross-column**\n * paste (e.g. copying \"Source terminal\" and pasting into \"Terminal route\"):\n * the copied header carries the SOURCE column's label, which does not equal\n * the target column's label but is still one of this grid's labels. Matching\n * the label set fixes cross-column paste while leaving external pastes (whose\n * first row is data, not labels) untouched.\n *\n * Guarded to avoid eating a legitimate first data row:\n * 1. The grid is configured with `includeHeaders` (it copies headers), and\n * 2. there is more than one row (never strip the only row), and\n * 3. every cell of the first row equals some column's label.\n */\n #dropCopiedHeaderRow(parsed: string[][]): void {\n if (!this.config.includeHeaders) return;\n // Never strip away the only row — that would make paste a no-op.\n if (parsed.length <= 1) return;\n\n const headerRow = parsed[0];\n if (headerRow.length === 0) return;\n\n const labels = this.#columnLabels();\n for (const cell of headerRow) {\n // A cell that isn't a known column label → this is real data, keep the row.\n if (!labels.has(cell)) return;\n }\n\n parsed.shift();\n }\n\n /**\n * Set of this grid's column labels (`header || field`), matching how\n * {@link buildClipboardText} serializes the copied header row. Used to detect\n * a header row this grid copied, regardless of paste position.\n */\n #columnLabels(): Set<string> {\n const labels = new Set<string>();\n for (const col of this.columns) {\n const label = col.header || col.field;\n if (label) labels.add(label);\n }\n return labels;\n }\n\n /**\n * True when the paste event originated from a focusable form control or\n * contenteditable element — i.e. somewhere the user expects the browser's\n * native paste behavior (typing into an editor input). Lets EditingPlugin\n * editors, filter inputs, and other text fields receive paste normally\n * instead of having ClipboardPlugin swallow the event.\n */\n #isEditablePasteTarget(target: EventTarget | null): boolean {\n if (!(target instanceof Element)) return false;\n if (target.closest('input, textarea, select, [contenteditable=\"\"], [contenteditable=\"true\"]')) {\n return true;\n }\n return false;\n }\n\n /**\n * Get the current selection via Query System.\n * Returns undefined if no selection plugin is loaded or nothing is selected.\n */\n #getSelection(): SelectionQueryResult | undefined {\n const responses = this.grid?.query<SelectionQueryResult>('getSelection');\n return responses?.[0];\n }\n\n /**\n * Resolve columns and rows to include based on options and/or current selection.\n *\n * Priority for columns:\n * 1. `options.columns` (explicit field list)\n * 2. Selection union column bounds across ALL ranges (range/cell mode only)\n * 3. All visible non-utility columns\n *\n * Priority for rows:\n * 1. `options.rowIndices` (explicit indices)\n * 2. Selection union row bounds across ALL ranges\n * 3. All rows\n *\n * `contains(rowOffset, colOffset)` (when non-null) reports whether a cell of\n * the resolved grid lies inside at least one selected range — callers use it\n * to blank the gaps between disjoint ranges, matching Excel's rectangular\n * multi-range copy.\n */\n #resolveData(options?: CopyOptions): {\n columns: ColumnConfig[];\n rows: Record<string, unknown>[];\n contains: ((rowOffset: number, colOffset: number) => boolean) | null;\n } {\n const selection = this.#getSelection();\n const ranges = selection?.ranges ?? [];\n const isRowAxis = selectionAxis(selection) === 'row';\n\n // Union bounding box across ALL ranges. A multi-range selection (CTRL+drag\n // repeatedly) must copy every range, not just the last one drawn; min/max\n // also normalizes reversed (bottom-up / right-to-left) drags.\n let minRow = Infinity;\n let maxRow = -Infinity;\n let minCol = Infinity;\n let maxCol = -Infinity;\n for (const r of ranges) {\n minRow = Math.min(minRow, r.from.row, r.to.row);\n maxRow = Math.max(maxRow, r.from.row, r.to.row);\n minCol = Math.min(minCol, r.from.col, r.to.col);\n maxCol = Math.max(maxCol, r.from.col, r.to.col);\n }\n\n // --- Columns ---\n let columns: ColumnConfig[];\n // Absolute visible-column index per resolved column (null when the columns\n // did not come from the selection). `resolveColumns` may drop utility /\n // hidden columns from the slice, so offsets can't be derived arithmetically.\n let colIndices: number[] | null = null;\n if (options?.columns) {\n // Caller specified exact fields\n columns = resolveColumns(this.columns, options.columns);\n } else if (ranges.length && !isRowAxis) {\n // Range/cell selection: restrict to selection column bounds\n // Selection indices are visible-column indices (from data-col)\n columns = resolveColumns(this.visibleColumns.slice(minCol, maxCol + 1));\n colIndices = columns.map((col) => this.visibleColumns.indexOf(col));\n } else {\n // Row selection or no selection: all visible columns\n columns = resolveColumns(this.columns);\n }\n\n // --- Rows ---\n let rows: Record<string, unknown>[];\n let rowIndices: number[] | null = null;\n if (options?.rowIndices) {\n // Caller specified exact row indices\n rows = resolveRows(this.rows as Record<string, unknown>[], options.rowIndices);\n } else if (ranges.length) {\n // Selection range: extract the union row span. Row mode selects WHOLE\n // rows and gets no column mask, so gaps between disjoint row ranges\n // (Ctrl+click rows 0, 5, 9) must be dropped outright rather than copied.\n const dropUnselectedRows = isRowAxis;\n const rowIsSelected = (r: number) =>\n ranges.some((g) => r >= Math.min(g.from.row, g.to.row) && r <= Math.max(g.from.row, g.to.row));\n rows = [];\n rowIndices = [];\n for (let r = minRow; r <= maxRow; r++) {\n const row = this.rows[r] as Record<string, unknown> | undefined;\n if (!row) continue;\n if (dropUnselectedRows && !rowIsSelected(r)) continue;\n rows.push(row);\n rowIndices.push(r);\n }\n } else {\n // No selection: all rows\n rows = this.rows as Record<string, unknown>[];\n }\n\n // Masking is only meaningful when BOTH axes came from the selection —\n // explicit `options.columns` / `options.rowIndices` (e.g. `copyRows()`)\n // would otherwise blank everything.\n const rowAbs = rowIndices;\n const colAbs = colIndices;\n const contains =\n rowAbs && colAbs\n ? (rowOffset: number, colOffset: number): boolean => {\n const row = rowAbs[rowOffset];\n const col = colAbs[colOffset];\n return ranges.some(\n (r) =>\n row >= Math.min(r.from.row, r.to.row) &&\n row <= Math.max(r.from.row, r.to.row) &&\n col >= Math.min(r.from.col, r.to.col) &&\n col <= Math.max(r.from.col, r.to.col),\n );\n }\n : null;\n\n return { columns, rows, contains };\n }\n\n /**\n * Build delimited text from resolved columns and rows.\n *\n * When no `processCell` callback is configured, uses \"copy what you see\" logic:\n * 1. Column `format` function (includes typeDefaults merged at config time)\n * 2. DOM cell textContent for columns with custom renderers (visible rows only)\n * 3. Raw value via `formatValueAsText` as final fallback\n */\n #buildText(\n columns: ColumnConfig[],\n rows: Record<string, unknown>[],\n options?: CopyOptions,\n contains?: ((rowOffset: number, colOffset: number) => boolean) | null,\n ): string {\n return this.#joinDisplayGrid(this.#buildDisplayGrid(columns, rows, options, contains), options);\n }\n\n /**\n * Build the 2D grid of DISPLAY strings for the given columns/rows (the same\n * WYSIWYG text used for `text/plain` and the `text/html` table). Header is\n * included only when configured.\n *\n * `contains` blanks cells that sit inside the selection's union bounding box\n * but outside every individual range, so a multi-range copy stays rectangular.\n */\n #buildDisplayGrid(\n columns: ColumnConfig[],\n rows: Record<string, unknown>[],\n options?: CopyOptions,\n contains?: ((rowOffset: number, colOffset: number) => boolean) | null,\n ): { header: string[] | null; rows: string[][] } {\n const includeHeaders = options?.includeHeaders ?? this.config.includeHeaders ?? false;\n const processCell = options?.processCell ?? this.config.processCell;\n const header = includeHeaders ? columns.map((c) => c.header || c.field) : null;\n const dataRows = rows.map((row, rowOffset) =>\n columns.map((col, colOffset) => {\n if (contains && !contains(rowOffset, colOffset)) return '';\n const value = resolveCellValue(row, col);\n if (processCell) return processCell(value, col.field, row);\n return this.#formatCellAsDisplayed(col, value, row);\n }),\n );\n return { header, rows: dataRows };\n }\n\n /** Join a display grid into delimited text (header row first when present). */\n #joinDisplayGrid(display: { header: string[] | null; rows: string[][] }, options?: CopyOptions): string {\n const delimiter = options?.delimiter ?? this.config.delimiter ?? '\\t';\n const newline = options?.newline ?? this.config.newline ?? '\\n';\n const lines: string[] = [];\n if (display.header) lines.push(display.header.join(delimiter));\n for (const cells of display.rows) lines.push(cells.join(delimiter));\n return lines.join(newline);\n }\n\n /**\n * Format a cell value the way the grid displays it.\n *\n * Priority:\n * 1. Column `format` function (includes typeDefaults applied at config merge time)\n * 2. DOM textContent for columns with a custom renderer (virtualized rows only)\n * 3. Raw value via `formatValueAsText`\n */\n #formatCellAsDisplayed(col: ColumnConfig, value: unknown, row: Record<string, unknown>): string {\n // 1. Column format function (covers col.format and typeDefaults)\n if (col.format) {\n try {\n const formatted = col.format(value, row);\n return formatted == null ? '' : String(formatted);\n } catch {\n // Format failed — fall through to next strategy\n }\n }\n\n // 2. DOM textContent for columns with custom renderers\n if (col.renderer || col.viewRenderer) {\n const text = this.#readCellTextFromDOM(col.field, row);\n if (text != null) return text;\n }\n\n // 3. Fallback to raw value formatting\n return formatValueAsText(value);\n }\n\n /**\n * Try to read a cell's displayed text from the DOM.\n *\n * Only works for rows currently rendered in the virtualized viewport.\n * Returns `null` if the row is not in the DOM (caller should fall back).\n */\n #readCellTextFromDOM(field: string, row: Record<string, unknown>): string | null {\n const host = this.gridElement;\n if (!host) return null;\n\n // Find the row index in the grid's current row array\n const rowIndex = this.rows.indexOf(row);\n if (rowIndex === -1) return null;\n\n // Query the rendered cell by row index and field name\n const cell = host.querySelector<HTMLElement>(`.cell[data-row=\"${rowIndex}\"][data-field=\"${field}\"]`);\n if (!cell) return null;\n\n return readCellDisplayText(cell);\n }\n\n /**\n * Get focused cell coordinates from DOM.\n * Used as fallback when SelectionPlugin has no selection.\n */\n #getFocusedCellFromDOM(target: HTMLElement): { row: number; col: number } | null {\n // Cells carry `data-field` + `data-row` (see `data-field-cache` was never a\n // real attribute). Body cells have `data-row`; header cells don't, so the\n // `data-row` guard below excludes headers.\n const cell = target.closest('.cell[data-field]') as HTMLElement | null;\n if (!cell) return null;\n\n const field = cell.dataset.field;\n const rowIndexStr = cell.dataset.row;\n if (!field || !rowIndexStr) return null;\n\n const row = parseInt(rowIndexStr, 10);\n if (isNaN(row)) return null;\n\n const col = this.columns.findIndex((c) => c.field === field);\n if (col === -1) return null;\n\n return { row, col };\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Get the text representation of the current selection (or specified data)\n * without writing to the system clipboard.\n *\n * Useful for previewing what would be copied, or for feeding the text into\n * a custom UI (e.g., a \"copy with column picker\" dialog).\n *\n * @param options - Control which columns/rows to include\n * @returns Delimited text, or empty string if nothing to copy\n *\n * @example Get text for specific columns\n * ```ts\n * const clipboard = grid.getPluginByName('clipboard');\n * const text = clipboard.getSelectionAsText({\n * columns: ['name', 'email'],\n * includeHeaders: true,\n * });\n * console.log(text);\n * // \"Name\\tEmail\\nAlice\\talice@example.com\\n...\"\n * ```\n */\n getSelectionAsText(options?: CopyOptions): string {\n const { columns, rows, contains } = this.#resolveData(options);\n if (columns.length === 0 || rows.length === 0) return '';\n return this.#buildText(columns, rows, options, contains);\n }\n\n /**\n * Copy data to the system clipboard.\n *\n * Without options, copies the current selection (or entire grid if no selection).\n * With options, copies exactly the specified columns and/or rows — ideal for\n * \"copy with column picker\" workflows where the user selects rows first,\n * then chooses which columns to include via a dialog.\n *\n * @param options - Control which columns/rows to include\n * @returns The copied text\n *\n * @example Copy current selection (default)\n * ```ts\n * const clipboard = grid.getPluginByName('clipboard');\n * await clipboard.copy();\n * ```\n *\n * @example Copy specific columns from specific rows\n * ```ts\n * // User selected rows in the grid, then picked columns in a dialog\n * const selectedRowIndices = [0, 3, 7];\n * const chosenColumns = ['name', 'department', 'salary'];\n * await clipboard.copy({\n * rowIndices: selectedRowIndices,\n * columns: chosenColumns,\n * includeHeaders: true,\n * });\n * ```\n */\n async copy(options?: CopyOptions): Promise<string> {\n const { columns, rows, contains } = this.#resolveData(options);\n if (columns.length === 0 || rows.length === 0) {\n return '';\n }\n\n const display = this.#buildDisplayGrid(columns, rows, options, contains);\n const text = this.#joinDisplayGrid(display, options);\n // Snapshot the RAW cell values (same column order as the text) for a\n // structured round-trip. Cloned so later row mutations don't change it.\n // Masked gaps between disjoint ranges stay blank here too, so a same-app\n // paste can't resurrect values the user never selected.\n const rawRows = rows.map((row, rowOffset) =>\n columns.map((col, colOffset) =>\n contains && !contains(rowOffset, colOffset) ? '' : cloneStructured(resolveCellValue(row, col)),\n ),\n );\n const fields = columns.map((col) => col.field);\n // Write BOTH text/plain (WYSIWYG for external targets) and text/html (a\n // table that also embeds the structured payload) so a same-app paste — even\n // cross-grid or cross-window — restores object cells losslessly.\n await copyToClipboard(text, buildClipboardHtml(display.header, display.rows, { v: 1, fields, rows: rawRows }));\n this.lastCopied = { text, timestamp: Date.now() };\n this.#internalClipboard = { text, rawRows, fields };\n this.emit<CopyDetail>('copy', {\n text,\n rowCount: rows.length,\n columnCount: columns.length,\n });\n return text;\n }\n\n /**\n * Copy specific rows by index to clipboard.\n *\n * Convenience wrapper around {@link copy} for row-based workflows.\n * Supports non-contiguous row indices (e.g., `[0, 3, 7]`).\n *\n * @param indices - Array of row indices to copy\n * @param options - Additional copy options (columns, headers, etc.)\n * @returns The copied text\n *\n * @example\n * ```ts\n * const clipboard = grid.getPluginByName('clipboard');\n * // Copy only rows 0 and 5, including just name and email columns\n * await clipboard.copyRows([0, 5], { columns: ['name', 'email'] });\n * ```\n */\n async copyRows(indices: number[], options?: Omit<CopyOptions, 'rowIndices'>): Promise<string> {\n if (indices.length === 0) return '';\n return this.copy({ ...options, rowIndices: indices });\n }\n\n /**\n * Read and parse clipboard content.\n * @returns Parsed 2D array of cell values, or null if clipboard is empty\n */\n async paste(): Promise<string[][] | null> {\n const text = await readFromClipboard();\n if (!text) return null;\n return parseClipboardText(text, this.config);\n }\n\n /**\n * Get the last copied text and timestamp.\n * @returns The last copied info or null\n */\n getLastCopied(): { text: string; timestamp: number } | null {\n return this.lastCopied;\n }\n // #endregion\n}\n\n// #region Internal Types\n\n/**\n * Range representation for clipboard operations.\n */\ninterface CellRange {\n from: { row: number; col: number };\n to: { row: number; col: number };\n}\n\n/**\n * Selection result returned by the Query System.\n * Matches the SelectionResult type from SelectionPlugin.\n */\ninterface SelectionQueryResult {\n mode: string | string[];\n activeAxis?: string;\n ranges: CellRange[];\n anchor: { row: number; col: number } | null;\n}\n\n/**\n * Axis that owns the current selection. `mode` may be an array (e.g.\n * `['range', 'column']`), so prefer `activeAxis` and fall back to a string `mode`.\n */\nfunction selectionAxis(sel: SelectionQueryResult | undefined): string | undefined {\n return sel?.activeAxis ?? (typeof sel?.mode === 'string' ? sel.mode : undefined);\n}\n// #endregion\n\n// Re-export types\nexport type { ClipboardConfig, CopyDetail, CopyOptions, PasteDetail } from './types';\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["BaseGridPlugin","invalidateAccessorCache","readCellField","resolveCellValue","writeCellField","resolveColumns","columns","fields","onlyVisible","result","filter","c","hidden","field","startsWith","utility","length","fieldSet","Set","has","_F","split","map","s","reverse","join","RegExp","slice","escapeHtml","text","replace","CLIPBOARD_PAYLOAD_ATTR","buildClipboardHtml","header","displayRows","payload","thead","h","tbody","r","btoa","encodeURIComponent","JSON","stringify","encodePayload","async","copyToClipboard","html","ClipboardItem","navigator","clipboard","write","Blob","type","writeText","err","code","message","console","warn","gridId","pluginName","gridPrefix","toLowerCase","docsUrl","formatDiagnostic","textarea","document","createElement","value","style","position","opacity","pointerEvents","body","appendChild","select","success","execCommand","removeChild","parseClipboardText","config","delimiter","newline","normalized","len","rows","currentRow","cellStart","inQuotes","cellHasQuotes","finalizeCell","endIdx","raw","out","i","unquoteCell","pushRow","some","trim","push","char","resolveColumnPaste","onPaste","ctx","accepted","reason","emitPasteRejected","grid","rejected","dispatchEvent","CustomEvent","detail","bubbles","defaultPasteHandler","pastedRows","rawRows","target","fillSelection","sourceFields","valueRows","currentRows","effectiveConfig","allFields","col","columnByField","Map","canEditCell","query","editsByRow","rowCountAfter","addEdit","rowIndex","evalRow","sourceField","resolution","get","row","oldValue","changes","set","cloneStructured","bounds","srcRows","endRow","sourceRow","srcCols","forEach","colOffset","maxPasteRow","Infinity","rowData","rowOffset","targetRowIndex","cellValue","size","grown","emptyRow","f","rowsNow","updates","directWrote","id","getRowId","Object","entries","updateRows","structuredClone","ClipboardPlugin","static","queries","description","name","required","defaultConfig","includeHeaders","quoteStrings","lastCopied","internalClipboard","attach","super","addEventListener","e","this","handleNativePaste","signal","disconnectSignal","detach","handleQuery","copy","onKeyDown","event","ctrlKey","metaKey","key","preventDefault","handleCopy","selection","getSelection","ranges","anchor","visibleColumns","focused","getFocusedCellFromDOM","rowIndices","isEditablePasteTarget","clipboardData","getData","parsed","firstRange","selMinRow","selMinCol","selMaxRow","selMaxCol","Math","min","from","to","max","targetRow","targetCol","axis","selectionAxis","endCol","maxCol","dropCopiedHeaderRow","indexOf","encoded","match","parse","decodeURIComponent","atob","Array","isArray","parseClipboardHtmlPayload","column","pasteWidth","fieldWidth","emit","applyPasteHandler","pasteHandler","headerRow","labels","columnLabels","cell","shift","label","add","Element","closest","resolveData","options","isRowAxis","minRow","maxRow","minCol","colIndices","indices","sort","a","b","resolveRows","dropUnselectedRows","rowIsSelected","g","rowAbs","colAbs","contains","buildText","joinDisplayGrid","buildDisplayGrid","processCell","formatCellAsDisplayed","display","lines","cells","format","formatted","String","renderer","viewRenderer","readCellTextFromDOM","Date","toISOString","formatValueAsText","host","gridElement","querySelector","loader","textContent","node","childNodes","nodeType","Node","TEXT_NODE","ELEMENT_NODE","readCellDisplayText","dataset","rowIndexStr","parseInt","isNaN","findIndex","getSelectionAsText","v","timestamp","now","rowCount","columnCount","copyRows","paste","readText","readFromClipboard","getLastCopied","sel","activeAxis","mode"],"sources":["../../../../../../libs/grid/src/lib/plugins/shared/cell-text.ts","../../../../../../libs/grid/src/lib/plugins/shared/data-collection.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/plugins/clipboard/clipboard-payload.ts","../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/plugins/clipboard/copy.ts","../../../../../../libs/grid/src/lib/plugins/clipboard/paste.ts","../../../../../../libs/grid/src/lib/plugins/clipboard/types.ts","../../../../../../libs/grid/src/lib/plugins/clipboard/clipboard-plugin.ts"],"sourcesContent":["/**\n * Displayed text of a body cell, excluding a custom `loadingRenderer` overlay that lives inside\n * the cell (`aria-hidden` does not remove it from `textContent`). Only text and element children\n * are read, matching `textContent`, which skips comment nodes (e.g. Angular's anchors).\n * @internal\n */\nexport function readCellDisplayText(cell: HTMLElement): string {\n const loader = cell.querySelector(':scope > .tbw-cell-loading-overlay');\n if (!loader) return cell.textContent?.trim() ?? '';\n let text = '';\n for (const node of cell.childNodes) {\n if (node === loader) continue;\n if (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE) text += node.textContent ?? '';\n }\n return text.trim();\n}\n","/**\n * Shared Data Collection Utilities\n *\n * Pure functions for resolving columns and formatting values, shared between\n * the Clipboard and Export plugins. Each plugin bundles its own copy of this\n * module (no chunk splitting) since plugin builds inline sibling imports.\n *\n * @internal\n */\n\nimport type { ColumnConfig } from '../../core/types';\n\n/**\n * Resolve which columns to include in a data export or copy operation.\n *\n * Filters out hidden columns, utility columns (`meta.utility`), and\n * internal columns (`__`-prefixed fields). Optionally restricts to an\n * explicit set of field names.\n *\n * @param columns - All column configurations\n * @param fields - If provided, only include columns whose field is in this list\n * @param onlyVisible - When `true` (default), exclude hidden and internal columns\n * @returns Filtered column array preserving original order\n */\nexport function resolveColumns(\n columns: readonly ColumnConfig[],\n fields?: string[],\n onlyVisible = true,\n): ColumnConfig[] {\n let result = columns as ColumnConfig[];\n\n if (onlyVisible) {\n result = result.filter((c) => !c.hidden && !c.field.startsWith('__') && c.utility !== true);\n }\n\n if (fields?.length) {\n const fieldSet = new Set(fields);\n result = result.filter((c) => fieldSet.has(c.field));\n }\n\n return result;\n}\n\n/**\n * Resolve which rows to include, optionally filtered to specific indices.\n *\n * @param rows - All row data\n * @param indices - If provided, only include rows at these indices (sorted ascending)\n * @returns Filtered row array\n */\nexport function resolveRows<T>(rows: readonly T[], indices?: number[]): T[] {\n if (!indices?.length) return rows as T[];\n\n return [...indices]\n .sort((a, b) => a - b)\n .map((i) => rows[i])\n .filter((r): r is T => r != null);\n}\n\n/**\n * Format a raw cell value as a text string.\n *\n * Provides the common null / Date / object → string conversion shared by\n * both clipboard and export output builders.\n *\n * @param value - The cell value to format\n * @returns A plain-text representation of the value\n */\nexport function formatValueAsText(value: unknown): string {\n if (value == null) return '';\n if (value instanceof Date) return value.toISOString();\n if (typeof value === 'object') return JSON.stringify(value);\n return String(value);\n}\n\n/**\n * Leading characters that make Excel / LibreOffice / Google Sheets treat a cell\n * as a formula rather than text.\n */\nconst FORMULA_TRIGGER_RE = /^[=+\\-@\\t\\r]/;\n\n/**\n * Options for {@link formatDelimitedValue}.\n *\n * @since 3.5.0\n */\nexport interface DelimitedFormatOptions {\n /** Field separator (`,` for CSV, `\\t` for clipboard). */\n delimiter: string;\n /** Row separator. */\n newline: string;\n /**\n * `'auto'` (default) quotes only strings containing a separator or quote,\n * `'always'` quotes every string, `'never'` disables quoting entirely.\n */\n quoting?: 'auto' | 'always' | 'never';\n /**\n * Prefix formula-triggering strings with `'` so spreadsheets treat them as\n * text. Defaults to `true`.\n */\n escapeFormulas?: boolean;\n}\n\n/**\n * Format a cell value for a delimiter-separated text format (CSV export or\n * clipboard copy).\n *\n * Only string values are formula-escaped: numbers such as `-5` stringify to a\n * leading `-` but are unambiguously numeric to a spreadsheet, and escaping them\n * would turn every negative number into text.\n *\n * Dispatch order is `typeof`-first because string + number cover the vast\n * majority of cell values; the cheap typeof check skips an `instanceof Date`\n * probe (which V8 cannot fold into a fast path) for every plain string cell.\n *\n * @param value - The cell value to format\n * @param opts - Delimiter, newline, quoting and formula-escaping behaviour\n * @returns The value as a delimiter-safe string\n * @since 3.5.0\n */\nexport function formatDelimitedValue(value: unknown, opts: DelimitedFormatOptions): string {\n // Hot path: strings (most cells).\n if (typeof value === 'string') {\n const text = opts.escapeFormulas !== false && FORMULA_TRIGGER_RE.test(value) ? `'${value}` : value;\n const quoting = opts.quoting ?? 'auto';\n if (\n quoting === 'always' ||\n (quoting === 'auto' &&\n (text.includes(opts.delimiter) ||\n text.includes(opts.newline) ||\n text.includes('\"') ||\n text.includes('\\n') ||\n text.includes('\\r')))\n ) {\n return `\"${text.replace(/\"/g, '\"\"')}\"`;\n }\n return text;\n }\n if (typeof value === 'number') return String(value);\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n if (value == null) return '';\n if (value instanceof Date) return value.toISOString();\n if (typeof value === 'object') return JSON.stringify(value);\n // Symbols, bigints, etc.\n return String(value);\n}\n","// Centralized template expression evaluation & sanitization utilities.\n// Responsible for safely interpolating {{ }} expressions while blocking\n// access to dangerous globals / reflective capabilities.\nimport type { CompiledViewFunction, EvalContext } from '../types';\n\n// #region Constants\nconst EXPR_RE = /{{\\s*([^}]+)\\s*}}/g;\nconst EMPTY_SENTINEL = '__DG_EMPTY__';\nconst SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/;\n\n// Forbidden identifiers built at runtime to avoid static analysis scanners\n// from flagging string literals (e.g. socket.dev \"network access\" / \"uses eval\").\n// Stored as reversed, pipe-delimited tokens — reversed back when constructing the regex.\nconst _F =\n '__otorp__|__retteGenifed__|__retteSenifed__|rotcurtsnoc|wodniw|sihTlabolg|labolg|ssecorp|noitcnuF|tropmi|lave|tcelfeR|yxorP|rorrE|stnemugra|tnemucod|noitacol|eikooc|egarotSlacol|egarotSnoisses|BDdexedni|hctef|tseuqeRpttHLMX|tekcoSbeW|rekroW|rekroWderahS|rekroWecivreS|renepo|tnerap|pot|semarf|fles'\n .split('|')\n .map((s) => s.split('').reverse().join(''));\nconst FORBIDDEN = new RegExp(`__(proto|defineGetter|defineSetter)|${_F.slice(3).join('|')}|this\\\\b`);\n// #endregion\n\n// #region HTML Sanitization\n\n/**\n * Escape a plain text string for safe insertion into HTML.\n * Converts special HTML characters to their entity equivalents.\n *\n * @param text - Plain text string to escape\n * @returns HTML-safe string\n */\nexport function escapeHtml(text: string): string {\n if (!text || typeof text !== 'string') return '';\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\n/**\n * Tags that are considered dangerous and will be completely removed.\n * These can execute scripts, load external resources, or manipulate the page.\n */\nconst DANGEROUS_TAGS = new Set(\n 'script|iframe|object|embed|form|input|button|textarea|select|link|meta|base|style|template|slot|portal|frame|frameset|applet|noscript|noembed|plaintext|xmp|listing|math'.split(\n '|',\n ),\n);\n\n/**\n * Attributes that are considered dangerous - event handlers and data loading.\n */\nconst DANGEROUS_ATTR_PATTERN = /^on\\w+$/i;\n\n/**\n * Attributes that can contain URLs which might be javascript: or data: URIs.\n */\nconst URL_ATTRS = new Set('href|src|action|formaction|data|srcdoc|xlink:href|poster|srcset'.split('|'));\n\n/**\n * Protocol patterns that are dangerous in URLs.\n */\nconst DANGEROUS_URL_PROTOCOL = /^\\s*(javascript|vbscript|data|blob):/i;\n\n/**\n * Sanitize an HTML string by removing dangerous tags and attributes.\n *\n * Prefer {@link setSanitizedHTML} wherever the result is destined for an\n * element: this function has to re-serialize the sanitized tree back to a\n * string, and the caller's `innerHTML =` then re-parses it. That\n * parse→serialize→parse round trip is the classic mutation-XSS vector, and it\n * costs a second parse. Use this overload only when a string is genuinely\n * required (e.g. the value is stored, compared, or handed to another API).\n *\n * @param html - Raw HTML string to sanitize\n * @returns Sanitized HTML string safe for innerHTML\n */\nexport function sanitizeHTML(html: string): string {\n if (!html || typeof html !== 'string') return '';\n\n // Fast path: if no HTML tags at all, return as-is (already safe)\n if (html.indexOf('<') === -1) return html;\n\n const template = document.createElement('template');\n template.innerHTML = html;\n\n sanitizeNode(template.content);\n\n return template.innerHTML;\n}\n\n/**\n * Parse and sanitize an HTML string into a detached {@link DocumentFragment}.\n *\n * This is the primitive {@link setSanitizedHTML} builds on, exposed for callers\n * that need to inspect or relocate the nodes before insertion.\n *\n * @param html - Raw HTML string to sanitize\n * @returns Fragment containing the sanitized nodes (empty for falsy input)\n */\nexport function sanitizeToFragment(html: string): DocumentFragment {\n const template = document.createElement('template');\n if (html && typeof html === 'string') {\n template.innerHTML = html;\n sanitizeNode(template.content);\n }\n return template.content;\n}\n\n/**\n * Replace an element's children with the sanitized parse of `html`.\n *\n * **This is the canonical sink for user-supplied markup** — cell/header/group\n * renderer string returns, `gridConfig.icons.*`, light-DOM tool panel fallback\n * content, and any new extension point that turns a user string into markup.\n * `textContent` is wrong here (it renders SVG icon strings as literal text) and\n * raw `innerHTML` is wrong (it is an XSS sink).\n *\n * Nodes are moved from the parse fragment rather than re-serialized, so the\n * sanitized tree is exactly what lands in the document.\n *\n * @param el - Element whose children are replaced\n * @param html - Raw HTML string to sanitize and insert\n */\nexport function setSanitizedHTML(el: Element, html: string): void {\n if (!html || typeof html !== 'string') {\n el.replaceChildren();\n return;\n }\n // Nothing for the parser to do: no markup and no entities to decode.\n if (html.indexOf('<') === -1 && html.indexOf('&') === -1) {\n el.textContent = html;\n return;\n }\n el.replaceChildren(sanitizeToFragment(html));\n}\n\n/**\n * Recursively sanitize a DOM node tree.\n */\nfunction sanitizeNode(root: DocumentFragment | Element): void {\n const toRemove: Element[] = [];\n\n // Use querySelectorAll to find all elements, then filter\n const elements = root.querySelectorAll('*');\n\n for (const el of elements) {\n const tagName = el.tagName.toLowerCase();\n\n // Check if tag is dangerous\n if (DANGEROUS_TAGS.has(tagName)) {\n toRemove.push(el);\n continue;\n }\n\n // SVG elements need special handling - they can contain script-like behavior\n if (tagName === 'svg' || el.namespaceURI === 'http://www.w3.org/2000/svg') {\n // Remove entire SVG if it has any suspicious attributes\n const hasDangerousContent = Array.from(el.attributes).some(\n (attr) => DANGEROUS_ATTR_PATTERN.test(attr.name) || attr.name === 'href' || attr.name === 'xlink:href',\n );\n if (hasDangerousContent) {\n toRemove.push(el);\n continue;\n }\n }\n\n // Check and remove dangerous attributes\n const attrsToRemove: string[] = [];\n for (const attr of el.attributes) {\n const attrName = attr.name.toLowerCase();\n\n // Event handlers (onclick, onerror, onload, etc.)\n if (DANGEROUS_ATTR_PATTERN.test(attrName)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // `is=` upgrades a plain tag into a registered customized built-in, so it\n // can smuggle behaviour past the tag-name check above.\n if (attrName === 'is') {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // URL attributes with dangerous protocols\n if (URL_ATTRS.has(attrName) && DANGEROUS_URL_PROTOCOL.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // style attribute can contain expressions (IE) or url() with javascript:\n if (attrName === 'style' && /expression\\s*\\(|javascript:|behavior\\s*:/i.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n }\n\n attrsToRemove.forEach((name) => el.removeAttribute(name));\n }\n\n // Remove dangerous elements (do this after iteration to avoid modifying during traversal)\n toRemove.forEach((el) => el.remove());\n}\n\n// #endregion\n\n// #region Template Evaluation\n\n// #region Safe Expression Evaluator\n// A minimal recursive-descent evaluator for the token set allowed by SAFE_EXPR.\n// Replaces `new Function()` to avoid dynamic code execution warnings from\n// static analysis tools (socket.dev, npm audit) while maintaining identical\n// functionality for expressions that pass the allowlist guards.\n//\n// Supported grammar (matching SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/):\n// ternary → logicalOr ('?' ternary ':' ternary)?\n// logicalOr → logicalAnd ('||' logicalAnd)*\n// logicalAnd → equality ('&&' equality)*\n// equality → comparison (('==' | '!=' | '===' | '!==') comparison)*\n// comparison → additive (('<' | '>' | '<=' | '>=') additive)*\n// additive → multiplicative (('+' | '-') multiplicative)*\n// multiplicative → unary (('*' | '/' | '%') unary)*\n// unary → '!' unary | primary\n// primary → NUMBER | STRING | IDENT ('.' IDENT)? | '(' ternary ')'\n\ntype Token =\n { type: 'num'; v: number } | { type: 'str'; v: string } | { type: 'id'; v: string } | { type: 'op'; v: string };\n\nfunction tokenize(expr: string): Token[] | null {\n const tokens: Token[] = [];\n let i = 0;\n while (i < expr.length) {\n const ch = expr[i];\n if (ch === ' ') {\n i++;\n continue;\n }\n\n // Numbers\n if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < expr.length && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {\n const start = i;\n while (i < expr.length && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) i++;\n tokens.push({ type: 'num', v: Number(expr.slice(start, i)) });\n continue;\n }\n\n // String literals (single-quoted only, per SAFE_EXPR)\n if (ch === \"'\") {\n let s = '';\n i++; // skip opening quote\n while (i < expr.length && expr[i] !== \"'\") s += expr[i++];\n if (i >= expr.length) return null; // unterminated\n i++; // skip closing quote\n tokens.push({ type: 'str', v: s });\n continue;\n }\n\n // Identifiers (value, row, row.field)\n if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {\n const start = i;\n while (i < expr.length && /[\\w$.]/.test(expr[i])) i++;\n tokens.push({ type: 'id', v: expr.slice(start, i) });\n continue;\n }\n\n // Multi-char operators\n if (i + 2 < expr.length) {\n const tri = expr.slice(i, i + 3);\n if (tri === '===' || tri === '!==') {\n tokens.push({ type: 'op', v: tri });\n i += 3;\n continue;\n }\n }\n if (i + 1 < expr.length) {\n const bi = expr.slice(i, i + 2);\n if (bi === '==' || bi === '!=' || bi === '<=' || bi === '>=' || bi === '&&' || bi === '||') {\n tokens.push({ type: 'op', v: bi });\n i += 2;\n continue;\n }\n }\n\n // Single-char operators\n if ('+-*/%<>!?:(),'.includes(ch)) {\n tokens.push({ type: 'op', v: ch });\n i++;\n continue;\n }\n\n return null; // unexpected character\n }\n return tokens;\n}\n\nfunction safeEval(expr: string, ctx: EvalContext): unknown {\n const toks = tokenize(expr);\n if (!toks) return undefined;\n const tokens: Token[] = toks;\n let pos = 0;\n\n function peek(): Token | undefined {\n return tokens[pos];\n }\n function advance(): Token {\n return tokens[pos++];\n }\n function match(type: string, val?: string): boolean {\n const t = peek();\n if (!t) return false;\n if (t.type !== type) return false;\n if (val !== undefined && t.v !== val) return false;\n return true;\n }\n\n function resolveId(name: string): unknown {\n if (name === 'value') return ctx.value;\n if (name.startsWith('row.')) {\n const key = name.slice(4);\n return ctx.row ? ctx.row[key] : undefined;\n }\n if (name.startsWith('typeDefault.')) {\n const key = name.slice('typeDefault.'.length);\n return ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n }\n // bare 'row' reference\n if (name === 'row') return ctx.row;\n if (name === 'typeDefault') return ctx.typeDefault;\n return undefined;\n }\n\n function parseTernary(): unknown {\n const result = parseLogicalOr();\n if (match('op', '?')) {\n advance(); // consume '?'\n const consequent = parseTernary();\n if (!match('op', ':')) return undefined;\n advance(); // consume ':'\n const alternate = parseTernary();\n return result ? consequent : alternate;\n }\n return result;\n }\n\n function parseLogicalOr(): unknown {\n let left = parseLogicalAnd();\n while (match('op', '||')) {\n advance();\n left = left || parseLogicalAnd();\n }\n return left;\n }\n\n function parseLogicalAnd(): unknown {\n let left = parseEquality();\n while (match('op', '&&')) {\n advance();\n left = left && parseEquality();\n }\n return left;\n }\n\n function parseEquality(): unknown {\n let left = parseComparison();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '===') {\n advance();\n left = left === parseComparison();\n } else if (t.v === '!==') {\n advance();\n left = left !== parseComparison();\n } else if (t.v === '==') {\n advance();\n left = left == parseComparison();\n } else if (t.v === '!=') {\n advance();\n left = left != parseComparison();\n } else break;\n }\n return left;\n }\n\n function parseComparison(): unknown {\n let left = parseAdditive();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '<') {\n advance();\n left = (left as number) < (parseAdditive() as number);\n } else if (t.v === '>') {\n advance();\n left = (left as number) > (parseAdditive() as number);\n } else if (t.v === '<=') {\n advance();\n left = (left as number) <= (parseAdditive() as number);\n } else if (t.v === '>=') {\n advance();\n left = (left as number) >= (parseAdditive() as number);\n } else break;\n }\n return left;\n }\n\n function parseAdditive(): unknown {\n let left = parseMultiplicative();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '+') {\n advance();\n left = (left as number) + (parseMultiplicative() as number);\n } else if (t.v === '-') {\n advance();\n left = (left as number) - (parseMultiplicative() as number);\n } else break;\n }\n return left;\n }\n\n function parseMultiplicative(): unknown {\n let left = parseUnary();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '*') {\n advance();\n left = (left as number) * (parseUnary() as number);\n } else if (t.v === '/') {\n advance();\n left = (left as number) / (parseUnary() as number);\n } else if (t.v === '%') {\n advance();\n left = (left as number) % (parseUnary() as number);\n } else break;\n }\n return left;\n }\n\n function parseUnary(): unknown {\n if (match('op', '!')) {\n advance();\n return !parseUnary();\n }\n return parsePrimary();\n }\n\n function parsePrimary(): unknown {\n const t = peek();\n if (!t) return undefined;\n if (t.type === 'num') {\n advance();\n return t.v;\n }\n if (t.type === 'str') {\n advance();\n return t.v;\n }\n if (t.type === 'id') {\n advance();\n return resolveId(t.v as string);\n }\n if (t.type === 'op' && t.v === '(') {\n advance(); // consume '('\n const result = parseTernary();\n if (match('op', ')')) advance(); // consume ')'\n return result;\n }\n // Unary minus for negative numbers\n if (t.type === 'op' && t.v === '-') {\n advance();\n return -(parsePrimary() as number);\n }\n return undefined;\n }\n\n const result = parseTernary();\n // Ensure all tokens were consumed\n if (pos < tokens.length) return undefined;\n return result;\n}\n// #endregion\n\nexport function evalTemplateString(raw: string, ctx: EvalContext): string {\n if (!raw || raw.indexOf('{{') === -1) return raw; // fast path (no expressions)\n const parts: { expr: string; result: string }[] = [];\n const evaluated = raw.replace(EXPR_RE, (_m, expr) => {\n const res = evalSingle(expr, ctx);\n parts.push({ expr: expr.trim(), result: res });\n // The template markup is author-controlled; the interpolated data is not.\n // Escaping here stops row data from injecting elements or breaking out of\n // an attribute (sanitizeHTML downstream only strips *dangerous* markup).\n return res === EMPTY_SENTINEL ? res : escapeHtml(res);\n });\n const finalStr = postProcess(evaluated);\n // If every part evaluated to EMPTY_SENTINEL we treat this as intentionally blank.\n // If any expression was blocked due to forbidden token (EMPTY_SENTINEL) we *still* only output ''\n // but do not escalate to BLOCKED_SENTINEL unless the original contained explicit forbidden tokens.\n const allEmpty = parts.length && parts.every((p) => p.result === '' || p.result === EMPTY_SENTINEL);\n const hadForbidden = REFLECTIVE_RE.test(raw);\n if (hadForbidden || allEmpty) return '';\n return finalStr;\n}\n\nfunction evalSingle(expr: string, ctx: EvalContext): string {\n expr = (expr || '').trim();\n if (!expr) return EMPTY_SENTINEL;\n if (REFLECTIVE_RE.test(expr)) return EMPTY_SENTINEL;\n if (expr === 'value') return ctx.value == null ? EMPTY_SENTINEL : String(ctx.value);\n if (expr.startsWith('row.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice(4);\n const v = ctx.row ? ctx.row[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.startsWith('typeDefault.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice('typeDefault.'.length);\n const v = ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.length > 80) return EMPTY_SENTINEL;\n if (!SAFE_EXPR.test(expr) || FORBIDDEN.test(expr)) return EMPTY_SENTINEL;\n const dotChain = expr.match(/\\./g);\n if (dotChain && dotChain.length > 1) return EMPTY_SENTINEL;\n try {\n const out = safeEval(expr, ctx);\n const str = out == null ? '' : String(out);\n if (REFLECTIVE_RE.test(str)) return EMPTY_SENTINEL;\n return str || EMPTY_SENTINEL;\n } catch {\n return EMPTY_SENTINEL;\n }\n}\n// #endregion\n\n// #region Cell Scrubbing\n/** Pattern matching reflective/introspective APIs that must be stripped from output. */\nconst REFLECTIVE_RE = /Reflect|Proxy|ownKeys/;\n\nfunction postProcess(s: string): string {\n if (!s) return s;\n return s.replace(new RegExp(EMPTY_SENTINEL, 'g'), '').replace(/Reflect\\.[^<>{}\\s]+|\\bProxy\\b|ownKeys\\([^)]*\\)/g, '');\n}\n\nexport function finalCellScrub(cell: HTMLElement): void {\n if (!REFLECTIVE_RE.test(cell.textContent || '')) return;\n // First pass: clear only text nodes containing forbidden tokens\n for (const n of cell.childNodes) {\n if (n.nodeType === Node.TEXT_NODE && REFLECTIVE_RE.test(n.textContent || '')) n.textContent = '';\n }\n // If forbidden tokens persist in element nodes, nuke everything\n if (REFLECTIVE_RE.test(cell.textContent || '')) {\n cell.textContent = '';\n }\n}\n// #endregion\n\n// #region Template Compilation\nexport function compileTemplate(raw: string) {\n if (REFLECTIVE_RE.test(raw)) {\n const fn = (() => '') as CompiledViewFunction;\n fn.__blocked = true;\n return fn;\n }\n const fn = ((ctx: EvalContext) => evalTemplateString(raw, ctx)) as CompiledViewFunction;\n fn.__blocked = false;\n return fn;\n}\n// #endregion\n","/**\n * Structured clipboard payload codec.\n *\n * Copy writes both `text/plain` (WYSIWYG for external targets) and `text/html`.\n * The HTML is a real `<table>` of the display text whose root element also\n * carries a base64-encoded structured payload in a `data-tbw-clip` attribute.\n * A same-app paste — even **cross-grid or cross-window** — can then recover the\n * raw cell values losslessly, while external targets just see the table/text.\n *\n * Kept in its own module so `copy.ts` and `paste.ts` share the format without\n * importing each other.\n */\nimport { escapeHtml } from '../../core/internal/sanitize';\n\n/**\n * Structured clipboard payload carried in `text/html`. `rows` are the RAW cell\n * values (data rows only), `fields` the source column fields in the same order.\n * @internal\n */\nexport interface ClipboardPayload {\n /** Payload schema version. */\n v: 1;\n /** Source column fields, in column order. */\n fields: string[];\n /** Raw cell values, one inner array per data row (no header). */\n rows: unknown[][];\n}\n\n/** Attribute (on the payload table) that marks our structured HTML clipboard. */\nexport const CLIPBOARD_PAYLOAD_ATTR = 'data-tbw-clip';\n\n/** Unicode-safe base64 encode of a JSON payload for an HTML attribute. */\nfunction encodePayload(payload: ClipboardPayload): string {\n return btoa(encodeURIComponent(JSON.stringify(payload)));\n}\n\n/**\n * Build the `text/html` clipboard representation: a real `<table>` of the\n * DISPLAY text (so external targets like Excel get a proper table) whose root\n * element carries the base64-encoded structured {@link ClipboardPayload} in a\n * `data-tbw-clip` attribute for lossless same-app paste.\n */\nexport function buildClipboardHtml(\n header: string[] | null,\n displayRows: string[][],\n payload: ClipboardPayload,\n): string {\n const thead = header ? `<thead><tr>${header.map((h) => `<th>${escapeHtml(h)}</th>`).join('')}</tr></thead>` : '';\n const tbody = `<tbody>${displayRows\n .map((r) => `<tr>${r.map((c) => `<td>${escapeHtml(c)}</td>`).join('')}</tr>`)\n .join('')}</tbody>`;\n return `<table ${CLIPBOARD_PAYLOAD_ATTR}=\"${encodePayload(payload)}\">${thead}${tbody}</table>`;\n}\n\n/**\n * Extract the structured {@link ClipboardPayload} embedded in a `text/html`\n * clipboard representation written by {@link buildClipboardHtml}. Enables a\n * lossless **cross-grid / cross-window** paste of object-valued cells (the\n * in-memory buffer only covers the same grid instance).\n *\n * Returns `null` when the HTML has no `data-tbw-clip` marker (e.g. an external\n * paste from Excel) or the payload can't be decoded. Parsing is defensive —\n * malformed input never throws.\n *\n * @param html - Raw `text/html` from `clipboardData.getData('text/html')`\n */\nexport function parseClipboardHtmlPayload(html: string | undefined | null): ClipboardPayload | null {\n if (!html || html.indexOf(CLIPBOARD_PAYLOAD_ATTR) === -1) return null;\n try {\n // Extract only our own base64 payload attribute with a targeted match rather\n // than parsing the whole (untrusted) clipboard HTML through a DOM sink\n // (DOMParser) — the latter feeds attacker-controlled markup to the parser and\n // trips CodeQL js/xss. The attribute value is base64 (btoa output), whose\n // alphabet excludes quotes and tag characters, so a simple attribute match is\n // both safe and sufficient.\n const match = html.match(new RegExp(`${CLIPBOARD_PAYLOAD_ATTR}=\"([A-Za-z0-9+/=]*)\"`));\n const encoded = match?.[1];\n if (!encoded) return null;\n const payload = JSON.parse(decodeURIComponent(atob(encoded))) as ClipboardPayload;\n if (payload && Array.isArray(payload.rows) && Array.isArray(payload.fields)) {\n return payload;\n }\n return null;\n } catch {\n return null;\n }\n}\n","/**\n * Centralized diagnostic messages for @toolbox-web/grid.\n *\n * Every user-facing warning, error, or info message in the grid has a unique\n * diagnostic code (e.g. `TBW001`). Each code maps to a section on the online\n * troubleshooting page, giving developers a direct link to resolution steps.\n *\n * ## Usage\n *\n * ```ts\n * import { MISSING_BREAKPOINT, warnDiagnostic, throwDiagnostic } from './diagnostics';\n *\n * // Warn with a code\n * warnDiagnostic(MISSING_BREAKPOINT, 'Set a breakpoint...', gridId);\n *\n * // Throw with a code\n * throwDiagnostic(MISSING_ROW_ID, 'Configure getRowId...', gridId);\n * ```\n *\n * Plugins should prefer `this.warn(MISSING_BREAKPOINT, message)` via BaseGridPlugin\n * instead of importing this module directly.\n *\n * @internal\n */\n\n// #region Grid Prefix\n\n/**\n * Build the `[tbw-grid]` or `[tbw-grid#my-id]` log prefix.\n * Pass `pluginName` for a scoped prefix like `[tbw-grid:SelectionPlugin]`.\n */\nexport function gridPrefix(gridId?: string, pluginName?: string): string {\n const id = gridId ? `#${gridId}` : '';\n const plugin = pluginName ? `:${pluginName}` : '';\n return `[tbw-grid${id}${plugin}]`;\n}\n\n// #endregion\n\n// #region Diagnostic Codes\n\n/**\n * Diagnostic codes used across the grid library.\n *\n * Each code is an individual export so that unused codes are tree-shaken\n * from bundles that don't reference them (esbuild can't tree-shake\n * properties from a single object).\n *\n * Naming: TBW + 3-digit number.\n * Ranges:\n * 001–019 Configuration validation (missing plugins, bad config)\n * 020–029 Plugin lifecycle (dependencies, incompatibilities, alias collapse)\n * 030–039 Feature registry\n * 040–049 Row operations (row ID, row mutations)\n * 050–059 Column operations (width, template)\n * 060–069 Rendering (callbacks, formatters, external views)\n * 070–079 Shell (tool panels, header/toolbar content)\n * 080–089 Editing & editors\n * 090–099 Print\n * 100–109 Clipboard\n * 110–119 Plugin-specific (responsive, undo-redo, grouping-columns)\n * 120–129 Style injection\n * 130–139 Attribute parsing\n * 140–149 DataSource / ServerSide\n */\n\n// --- Config validation (001–019) ---\n/** Column uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN = 'TBW001' as const;\n/** Grid config uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN_CONFIG = 'TBW002' as const;\n/** Plugin config rule violation (error severity). */\nexport const CONFIG_RULE_ERROR = 'TBW003' as const;\n/** Plugin config rule violation (warning severity). */\nexport const CONFIG_RULE_WARN = 'TBW004' as const;\n\n// --- Plugin lifecycle (020–029) ---\n/** Required plugin dependency is missing. */\nexport const MISSING_DEPENDENCY = 'TBW020' as const;\n/** Optional plugin dependency is missing. */\nexport const OPTIONAL_DEPENDENCY = 'TBW021' as const;\n/** Two loaded plugins are incompatible. */\nexport const INCOMPATIBLE_PLUGINS = 'TBW022' as const;\n/** Two plugin instances resolve to the same canonical name (alias collapse). */\nexport const PLUGIN_ALIAS_COLLAPSE = 'TBW023' as const;\n/** Error thrown inside a plugin event handler. */\nexport const PLUGIN_EVENT_ERROR = 'TBW024' as const;\n/** Conflicting config values found while merging alias-collapsed plugin instances. */\nexport const PLUGIN_ALIAS_CONFIG_CONFLICT = 'TBW025' as const;\n\n// --- Feature registry (030–039) ---\n/** Feature was re-registered (overwritten). */\nexport const FEATURE_REREGISTERED = 'TBW030' as const;\n/** Feature configured but not imported. */\nexport const FEATURE_NOT_IMPORTED = 'TBW031' as const;\n/** Feature depends on another feature that is not enabled. */\nexport const FEATURE_MISSING_DEP = 'TBW032' as const;\n\n// --- Row operations (040–049) ---\n/** Cannot determine row ID (no getRowId and no id property). */\nexport const MISSING_ROW_ID = 'TBW040' as const;\n/** Row with given ID not found. */\nexport const ROW_NOT_FOUND = 'TBW041' as const;\n\n// --- Column operations (050–059) ---\n/** Column has an invalid CSS width value. */\nexport const INVALID_COLUMN_WIDTH = 'TBW050' as const;\n\n// --- Rendering callbacks (060–069) ---\n/** rowClass callback threw an error. */\nexport const ROW_CLASS_ERROR = 'TBW060' as const;\n/** cellClass callback threw an error. */\nexport const CELL_CLASS_ERROR = 'TBW061' as const;\n/** Column format function threw an error. */\nexport const FORMAT_ERROR = 'TBW062' as const;\n/** External view mount() threw an error. */\nexport const VIEW_MOUNT_ERROR = 'TBW063' as const;\n/** External view event dispatch error. */\nexport const VIEW_DISPATCH_ERROR = 'TBW064' as const;\n\n// --- Shell (070–079) ---\n/** Tool panel missing required id. */\nexport const TOOL_PANEL_MISSING_ATTR = 'TBW070' as const;\n/** No tool panels registered. */\nexport const NO_TOOL_PANELS = 'TBW071' as const;\n/** Tool panel section not found. */\nexport const TOOL_PANEL_NOT_FOUND = 'TBW072' as const;\n/** Tool panel already registered. */\nexport const TOOL_PANEL_DUPLICATE = 'TBW073' as const;\n/** Header content already registered. */\nexport const HEADER_CONTENT_DUPLICATE = 'TBW074' as const;\n/** Toolbar content already registered. */\nexport const TOOLBAR_CONTENT_DUPLICATE = 'TBW075' as const;\n\n// --- Editing & editors (080–089) ---\n/** External editor mount() threw an error. */\nexport const EDITOR_MOUNT_ERROR = 'TBW080' as const;\n\n// --- Print (090–099) ---\n/** Print already in progress. */\nexport const PRINT_IN_PROGRESS = 'TBW090' as const;\n/** Grid not available for printing. */\nexport const PRINT_NO_GRID = 'TBW091' as const;\n/** Print operation failed. */\nexport const PRINT_FAILED = 'TBW092' as const;\n/** Multiple elements share the same grid ID (print isolation issue). */\nexport const PRINT_DUPLICATE_ID = 'TBW093' as const;\n\n// --- Clipboard (100–109) ---\n/** Clipboard API write failed. */\nexport const CLIPBOARD_FAILED = 'TBW100' as const;\n\n// --- Plugin-specific (110–119) ---\n/** ResponsivePlugin: no breakpoint configured. */\nexport const MISSING_BREAKPOINT = 'TBW110' as const;\n/** UndoRedoPlugin: transaction already in progress. */\nexport const TRANSACTION_IN_PROGRESS = 'TBW111' as const;\n/** UndoRedoPlugin: no transaction in progress. */\nexport const NO_TRANSACTION = 'TBW112' as const;\n/** GroupingColumnsPlugin: missing id or header on column group definition. */\nexport const COLUMN_GROUP_NO_ID = 'TBW113' as const;\n/** GroupingColumnsPlugin: conflicting columnGroups sources. */\nexport const COLUMN_GROUPS_CONFLICT = 'TBW114' as const;\n\n// --- Style injection (120–129) ---\n/** Failed to extract grid.css from document stylesheets. */\nexport const STYLE_EXTRACT_FAILED = 'TBW120' as const;\n/** Could not find grid.css in document.styleSheets. */\nexport const STYLE_NOT_FOUND = 'TBW121' as const;\n\n// --- Attribute parsing (130–139) ---\n/** Invalid JSON in an HTML attribute. */\nexport const INVALID_ATTRIBUTE_JSON = 'TBW130' as const;\n\n// --- DataSource / ServerSide (140–149) ---\n/** ServerSidePlugin: getRows() rejected. */\nexport const DATASOURCE_FETCH_ERROR = 'TBW140' as const;\n/** ServerSidePlugin: getChildRows() rejected. */\nexport const DATASOURCE_CHILD_FETCH_ERROR = 'TBW141' as const;\n/** ServerSidePlugin: getChildRows() not implemented but a plugin queried for children. */\nexport const DATASOURCE_NO_CHILD_HANDLER = 'TBW142' as const;\n/** ServerSidePlugin: concurrent request limit reached, request deferred. */\nexport const DATASOURCE_THROTTLED = 'TBW143' as const;\n\nexport type DiagnosticCode =\n | typeof MISSING_PLUGIN\n | typeof MISSING_PLUGIN_CONFIG\n | typeof CONFIG_RULE_ERROR\n | typeof CONFIG_RULE_WARN\n | typeof MISSING_DEPENDENCY\n | typeof OPTIONAL_DEPENDENCY\n | typeof INCOMPATIBLE_PLUGINS\n | typeof PLUGIN_ALIAS_COLLAPSE\n | typeof PLUGIN_EVENT_ERROR\n | typeof PLUGIN_ALIAS_CONFIG_CONFLICT\n | typeof FEATURE_REREGISTERED\n | typeof FEATURE_NOT_IMPORTED\n | typeof FEATURE_MISSING_DEP\n | typeof MISSING_ROW_ID\n | typeof ROW_NOT_FOUND\n | typeof INVALID_COLUMN_WIDTH\n | typeof ROW_CLASS_ERROR\n | typeof CELL_CLASS_ERROR\n | typeof FORMAT_ERROR\n | typeof VIEW_MOUNT_ERROR\n | typeof VIEW_DISPATCH_ERROR\n | typeof TOOL_PANEL_MISSING_ATTR\n | typeof NO_TOOL_PANELS\n | typeof TOOL_PANEL_NOT_FOUND\n | typeof TOOL_PANEL_DUPLICATE\n | typeof HEADER_CONTENT_DUPLICATE\n | typeof TOOLBAR_CONTENT_DUPLICATE\n | typeof EDITOR_MOUNT_ERROR\n | typeof PRINT_IN_PROGRESS\n | typeof PRINT_NO_GRID\n | typeof PRINT_FAILED\n | typeof PRINT_DUPLICATE_ID\n | typeof CLIPBOARD_FAILED\n | typeof MISSING_BREAKPOINT\n | typeof TRANSACTION_IN_PROGRESS\n | typeof NO_TRANSACTION\n | typeof COLUMN_GROUP_NO_ID\n | typeof COLUMN_GROUPS_CONFLICT\n | typeof STYLE_EXTRACT_FAILED\n | typeof STYLE_NOT_FOUND\n | typeof INVALID_ATTRIBUTE_JSON\n | typeof DATASOURCE_FETCH_ERROR\n | typeof DATASOURCE_CHILD_FETCH_ERROR\n | typeof DATASOURCE_NO_CHILD_HANDLER\n | typeof DATASOURCE_THROTTLED;\n\n// #endregion\n\n// #region Docs URL\n\nconst DOCS_BASE = 'https://toolboxjs.com/grid/errors';\n\n/** Build a direct link to the troubleshooting section for a code. */\nfunction docsUrl(code: DiagnosticCode): string {\n return `${DOCS_BASE}#${code.toLowerCase()}`;\n}\n\n// #endregion\n\n// #region Formatting\n\n/**\n * Format a diagnostic message with prefix, code, and docs link.\n *\n * Output format:\n * ```\n * [tbw-grid#my-id] TBW001: Your message here.\n *\n * → More info: https://toolboxjs.com/grid/errors#tbw001\n * ```\n */\nexport function formatDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): string {\n const prefix = gridPrefix(gridId, pluginName);\n return `${prefix} ${code}: ${message}\\n\\n → More info: ${docsUrl(code)}`;\n}\n\n// #endregion\n\n// #region Public API\n\n/**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\nexport function throwDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): never {\n throw new Error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a warning with a diagnostic code and docs link.\n * Use for recoverable issues the developer should fix.\n */\nexport function warnDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.warn(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a debug message with a diagnostic code and docs link.\n * Use for optional/soft dependency notifications — visible only when\n * the browser DevTools \"Verbose\" log level is enabled.\n */\nexport function debugDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.debug(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log an error with a diagnostic code and docs link.\n * Use for non-throwing errors (e.g., failed async operations).\n */\nexport function errorDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n// #endregion\n","/**\n * Clipboard Copy Logic\n *\n * Pure functions for copying grid data to clipboard.\n */\n\nimport { CLIPBOARD_FAILED, warnDiagnostic } from '../../core/internal/diagnostics';\nimport { resolveCellValue } from '../../core/internal/value-accessor';\nimport type { ColumnConfig } from '../../core/types';\nimport { formatDelimitedValue, type DelimitedFormatOptions } from '../shared/data-collection';\nimport type { ClipboardConfig } from './types';\n\n/** Parameters for building clipboard text */\nexport interface CopyParams {\n /** All grid rows */\n rows: unknown[];\n /** Column configurations */\n columns: ColumnConfig[];\n /** Selected row indices */\n selectedIndices: Set<number> | number[];\n /** Clipboard configuration */\n config: ClipboardConfig;\n}\n\n/**\n * Format a cell value for clipboard output.\n *\n * Uses custom processCell if provided, otherwise applies default formatting:\n * - null/undefined → empty string\n * - Date → ISO string\n * - Object → JSON string\n * - Other → String conversion with optional quoting\n *\n * @param value - The cell value to format\n * @param field - The field name\n * @param row - The full row object\n * @param config - Clipboard configuration\n * @returns Formatted string value\n */\nexport function formatCellValue(value: unknown, field: string, row: unknown, config: ClipboardConfig): string {\n if (config.processCell) {\n return config.processCell(value, field, row);\n }\n return formatDelimitedValue(value, toFormatOptions(config));\n}\n\n/** Map the plugin config onto the shared delimited-format options. */\nfunction toFormatOptions(config: ClipboardConfig): DelimitedFormatOptions {\n return {\n delimiter: config.delimiter ?? '\\t',\n newline: config.newline ?? '\\n',\n quoting: config.quoteStrings ? 'always' : 'auto',\n escapeFormulas: config.escapeFormulas,\n };\n}\n\n/**\n * Build clipboard text from selected rows and columns.\n *\n * @param params - Copy parameters including rows, columns, selection, and config\n * @returns Tab-separated (or custom delimiter) text ready for clipboard\n */\nexport function buildClipboardText(params: CopyParams): string {\n const { rows, columns, selectedIndices, config } = params;\n const delimiter = config.delimiter ?? '\\t';\n const newline = config.newline ?? '\\n';\n // Built once — this runs per cell across the whole selection.\n const format = toFormatOptions(config);\n\n // Filter to visible columns (not hidden, not internal __ prefixed)\n const visibleColumns = columns.filter((c) => !c.hidden && !c.field.startsWith('__'));\n\n const lines: string[] = [];\n\n // Add header row if configured\n if (config.includeHeaders) {\n const headerCells = visibleColumns.map((c) => {\n const header = c.header || c.field;\n // Quote headers if they contain special characters\n if (header.includes(delimiter) || header.includes(newline) || header.includes('\"')) {\n return `\"${header.replace(/\"/g, '\"\"')}\"`;\n }\n return header;\n });\n lines.push(headerCells.join(delimiter));\n }\n\n // Convert indices to sorted array\n const indices = selectedIndices instanceof Set ? [...selectedIndices] : selectedIndices;\n const sortedIndices = [...indices].sort((a, b) => a - b);\n\n // Build data rows\n for (const idx of sortedIndices) {\n const row = rows[idx];\n if (!row) continue;\n\n const cells = visibleColumns.map((col) => {\n const value = resolveCellValue(row, col);\n return config.processCell\n ? config.processCell(value, col.field, row)\n : formatDelimitedValue(value, format);\n });\n lines.push(cells.join(delimiter));\n }\n\n return lines.join(newline);\n}\n\n/**\n * Copy text to the system clipboard.\n *\n * Uses the modern Clipboard API when available, with fallback\n * to execCommand for older browsers.\n *\n * When `html` is supplied, writes BOTH `text/plain` and `text/html` via\n * `ClipboardItem` so a same-app paste (even cross-grid or cross-window) can\n * recover a structured payload embedded in the HTML, while external targets\n * (Excel, editors) still get the plain text. Silently falls back to text-only\n * when `ClipboardItem` / `clipboard.write` is unavailable or rejects.\n *\n * @param text - The text to copy (`text/plain`)\n * @param html - Optional rich `text/html` representation (may embed a payload)\n * @returns Promise resolving to true if successful, false otherwise\n */\nexport async function copyToClipboard(text: string, html?: string): Promise<boolean> {\n if (html && typeof ClipboardItem !== 'undefined' && navigator.clipboard?.write) {\n try {\n await navigator.clipboard.write([\n new ClipboardItem({\n 'text/plain': new Blob([text], { type: 'text/plain' }),\n 'text/html': new Blob([html], { type: 'text/html' }),\n }),\n ]);\n return true;\n } catch {\n // Fall through to text-only write below.\n }\n }\n try {\n await navigator.clipboard.writeText(text);\n return true;\n } catch (err) {\n warnDiagnostic(CLIPBOARD_FAILED, `Clipboard API failed: ${err}`);\n // Fallback for older browsers or when Clipboard API is not available\n const textarea = document.createElement('textarea');\n textarea.value = text;\n textarea.style.position = 'fixed';\n textarea.style.opacity = '0';\n textarea.style.pointerEvents = 'none';\n document.body.appendChild(textarea);\n textarea.select();\n const success = document.execCommand('copy');\n document.body.removeChild(textarea);\n return success;\n }\n}\n","/**\n * Clipboard Paste Logic\n *\n * Pure functions for reading and parsing clipboard data.\n */\n\nimport type { ClipboardConfig } from './types';\n\n/**\n * Parse clipboard text into a 2D array of cell values.\n *\n * Handles:\n * - Quoted values (with escaped double quotes \"\")\n * - Multi-line quoted values (newlines within quotes are preserved)\n * - Custom delimiters and newlines\n * - Empty lines (filtered out only if they contain no data)\n *\n * Implementation: char-by-char state machine for delimiter/newline/quote\n * detection, but cell values are extracted via `slice(start, end)` rather\n * than `+= char` accumulation. The vast majority of cells have no quotes,\n * so the hot path is a single substring slice per cell. Cells that did\n * contain quotes fall back to {@link unquoteCell} for the strip + escape\n * pass.\n *\n * @param text - Raw clipboard text\n * @param config - Clipboard configuration\n * @returns 2D array where each sub-array is a row of cell values\n */\nexport function parseClipboardText(text: string, config: ClipboardConfig): string[][] {\n const delimiter = config.delimiter ?? '\\t';\n const newline = config.newline ?? '\\n';\n\n // Normalize Windows / old-Mac line endings.\n const normalized = text.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n const len = normalized.length;\n\n const rows: string[][] = [];\n let currentRow: string[] = [];\n let cellStart = 0;\n let inQuotes = false;\n let cellHasQuotes = false;\n\n const finalizeCell = (endIdx: number): string => {\n const raw = normalized.slice(cellStart, endIdx);\n return cellHasQuotes ? unquoteCell(raw) : raw;\n };\n\n const pushRow = () => {\n // Filter purely-empty rows (single empty cell or all-whitespace cells),\n // matching the original char-by-char implementation's contract.\n if (currentRow.length > 1 || currentRow.some((c) => c.trim() !== '')) {\n rows.push(currentRow);\n }\n currentRow = [];\n };\n\n for (let i = 0; i < len; i++) {\n const char = normalized[i];\n\n if (char === '\"') {\n cellHasQuotes = true;\n if (!inQuotes) {\n inQuotes = true;\n } else if (normalized[i + 1] === '\"') {\n // Escaped quote (\"\") — skip the second quote, stay inside the quoted run.\n i++;\n } else {\n inQuotes = false;\n }\n } else if (char === delimiter && !inQuotes) {\n currentRow.push(finalizeCell(i));\n cellStart = i + 1;\n cellHasQuotes = false;\n } else if (char === newline && !inQuotes) {\n currentRow.push(finalizeCell(i));\n cellStart = i + 1;\n cellHasQuotes = false;\n pushRow();\n }\n }\n\n // Final cell + row.\n currentRow.push(finalizeCell(len));\n pushRow();\n\n return rows;\n}\n\n/**\n * Strip surrounding quotes and unescape `\"\"` → `\"` from a cell value that\n * contained at least one quote character. Only invoked from the slow path\n * of {@link parseClipboardText}.\n */\nfunction unquoteCell(raw: string): string {\n let out = '';\n let inQuotes = false;\n for (let i = 0; i < raw.length; i++) {\n const c = raw[i];\n if (c === '\"') {\n if (!inQuotes) {\n inQuotes = true;\n } else if (raw[i + 1] === '\"') {\n out += '\"';\n i++;\n } else {\n inQuotes = false;\n }\n } else {\n out += c;\n }\n }\n return out;\n}\n\n/**\n * Read text from the system clipboard.\n *\n * Uses the modern Clipboard API. Returns empty string if\n * the API is not available or permission is denied.\n *\n * @returns Promise resolving to clipboard text or empty string\n */\nexport async function readFromClipboard(): Promise<string> {\n try {\n return await navigator.clipboard.readText();\n } catch {\n // Permission denied or API not available\n return '';\n }\n}\n","/**\n * Clipboard Plugin Types\n *\n * Type definitions for clipboard copy/paste functionality.\n */\n\nimport { invalidateAccessorCache, readCellField, writeCellField } from '../../core/internal/value-accessor';\nimport type { GridElement } from '../../core/plugin/base-plugin';\nimport type { CellEditablePredicate } from '../../core/plugin/types';\n\n/**\n * Custom paste handler function.\n *\n * @param detail - The parsed paste data with target and field info\n * @param grid - The grid element to update\n * @returns `false` to prevent the default paste behavior, or `void`/`true` to allow it\n *\n * @example\n * ```ts\n * // Custom handler that validates before pasting\n * new ClipboardPlugin({\n * pasteHandler: (detail, grid) => {\n * if (!detail.target) return false;\n * // Apply custom validation/transformation...\n * applyPasteData(detail, grid);\n * return false; // We handled it, skip default\n * }\n * })\n * ```\n * @since 0.4.2\n */\nexport type PasteHandler = (detail: PasteDetail, grid: GridElement) => boolean | void;\n\n/**\n * Options for programmatic copy operations.\n *\n * Allows callers to control exactly which columns and rows are included\n * in a copy, independently of the current selection state.\n *\n * @example Copy specific columns from selected rows\n * ```ts\n * const clipboard = grid.getPluginByName('clipboard');\n * // User selected rows 0, 2, 4 via a dialog, chose columns to include\n * const text = await clipboard.copy({\n * rowIndices: [0, 2, 4],\n * columns: ['name', 'email'],\n * includeHeaders: true,\n * });\n * ```\n * @since 1.14.0\n */\nexport interface CopyOptions {\n /** Specific column fields to include. If omitted, uses current selection or all visible columns. */\n columns?: string[];\n /** Specific row indices to copy. If omitted, uses current selection or all rows. */\n rowIndices?: number[];\n /** Include column headers in copied text. Defaults to the plugin config value. */\n includeHeaders?: boolean;\n /** Column delimiter override. Defaults to the plugin config value. */\n delimiter?: string;\n /** Row delimiter override. Defaults to the plugin config value. */\n newline?: string;\n /** Custom cell value processor for this operation. Overrides the plugin config's `processCell`. */\n processCell?: (value: unknown, field: string, row: unknown) => string;\n}\n\n/**\n * Configuration options for the clipboard plugin\n *\n * @since 0.1.1\n */\nexport interface ClipboardConfig {\n /** Include column headers in copied text (default: false) */\n includeHeaders?: boolean;\n /** Column delimiter character (default: '\\t' for tab) */\n delimiter?: string;\n /** Row delimiter/newline character (default: '\\n') */\n newline?: string;\n /** Wrap string values with quotes (default: false) */\n quoteStrings?: boolean;\n /**\n * Neutralize spreadsheet formula injection (CWE-1236) by prefixing string\n * values that start with `=`, `+`, `-`, `@`, TAB or CR with a single quote,\n * so a paste into Excel / LibreOffice / Sheets renders them as text instead\n * of evaluating them (default: `true`).\n *\n * Grid-to-grid paste is unaffected — that path uses the structured\n * `text/html` payload, not the plain-text representation.\n *\n * @since 3.5.0\n */\n escapeFormulas?: boolean;\n /** Custom cell value processor for copy operations */\n processCell?: (value: unknown, field: string, row: unknown) => string;\n /**\n * When `true`, pasting into a multi-cell selection larger than the clipboard\n * source tiles (repeats) the source to fill the whole selection: a single\n * copied cell fills every selected cell, a 1×2 source fills as\n * `val1, val2, val1, val2`, and a 2×2 block tiles across the selection.\n *\n * Only applies when a bounded (multi-cell) selection is active — it never\n * grows the grid. Defaults to `false` (paste writes only the source extent).\n *\n * @default false\n * @since 3.0.0\n */\n fillSelection?: boolean;\n /**\n * Custom paste handler that **fully replaces** {@link defaultPasteHandler}.\n *\n * ⚠️ **Prefer not to override this.** The default handler is no longer a thin\n * \"write text into cells\" routine — it now implements substantial behavior\n * that is easy to get subtly wrong when reimplemented:\n *\n * - editability resolution via the editing plugin (`getCellEditableResolver`,\n * including row-conditional `editable`);\n * - per-column {@link BaseColumnConfig.onPaste} reject / transform, plus the\n * `paste-rejected` event;\n * - structured (`rawRows`) same-grid / cross-window values so object cells\n * round-trip losslessly (not degraded to display text);\n * - fill-selection tiling of a smaller source across a bounded selection;\n * - routing edits through `grid.updateRows(…, 'paste')` so paste participates\n * in dirty tracking, undo/redo history, cancelable validation, and the\n * `'paste'` commit source — with a direct-write fallback for id-less rows;\n * - growing the grid for unbounded single-cell-expansion pastes.\n *\n * **Use an extension point instead of overriding, wherever possible:**\n * - reject / transform per cell → column {@link BaseColumnConfig.onPaste};\n * - coerce / veto committed values globally → the cancelable `cell-commit`\n * event (`detail.source === 'paste'` distinguishes a paste);\n * - observe the raw paste → the `paste` event;\n * - react to rejected cells → the `paste-rejected` event.\n *\n * Other values:\n * - `null` — disable auto-paste entirely (the `paste` event still fires).\n * - a function returning `false` — you handled it; skip the default.\n *\n * **If you must override** (e.g. bespoke target placement or side effects),\n * you are responsible for re-implementing the behaviors above. At minimum,\n * honor per-column `onPaste` with the exported {@link resolveColumnPaste} and\n * fire {@link emitPasteRejected} so `onPaste` and `paste-rejected` keep working:\n *\n * ```ts\n * import { resolveColumnPaste, emitPasteRejected } from '@toolbox-web/grid/plugins/clipboard';\n *\n * pasteHandler: (detail, grid) => {\n * const rejected = [];\n * for (const cell of myCells(detail)) {\n * const res = resolveColumnPaste(cell.column.onPaste, {\n * value: cell.value, field: cell.field, row: cell.row,\n * rowIndex: cell.rowIndex, oldValue: cell.oldValue, sourceField: cell.sourceField,\n * });\n * if (res.accepted) writeCell(cell, res.value);\n * else rejected.push({ field: cell.field, rowIndex: cell.rowIndex, row: cell.row, value: cell.value, reason: res.reason });\n * }\n * emitPasteRejected(grid, rejected);\n * return false; // handled\n * }\n * ```\n *\n * @default defaultPasteHandler (auto-applies paste data)\n */\n pasteHandler?: PasteHandler | null;\n}\n\n/** Internal state managed by the clipboard plugin */\nexport interface ClipboardState {\n /** The last copied text (for reference/debugging) */\n lastCopied: string | null;\n}\n\n/**\n * Event detail emitted after a successful copy operation\n *\n * @since 0.1.1\n */\nexport interface CopyDetail {\n /** The text that was copied to clipboard */\n text: string;\n /** Number of rows copied */\n rowCount: number;\n /** Number of columns copied */\n columnCount: number;\n}\n\n/**\n * Target cell coordinates and bounds for paste operations\n *\n * @since 0.4.2\n */\nexport interface PasteTarget {\n /** Target row index (top-left of paste area) */\n row: number;\n /** Target column index (top-left of paste area) */\n col: number;\n /** Target column field name (for easy data mapping) */\n field: string;\n /**\n * Selection bounds that constrain the paste area.\n * If set, paste data will be clipped to fit within these bounds.\n * If null, paste expands freely from the target cell.\n */\n bounds: {\n /** End row index (inclusive) */\n endRow: number;\n /** End column index (inclusive) */\n endCol: number;\n } | null;\n}\n\n/**\n * Event detail emitted after a paste operation\n *\n * @since 0.1.1\n */\nexport interface PasteDetail {\n /** Parsed rows from clipboard (2D array of cell values) */\n rows: string[][];\n /** Raw text that was pasted */\n text: string;\n /** The target cell where paste starts (top-left of paste area). Null if no cell is selected. */\n target: PasteTarget | null;\n /**\n * Column fields for each column in the paste range, starting from target.col.\n * Useful for mapping parsed cell values to data fields.\n * Length matches the width of the pasted data (or available columns, whichever is smaller).\n * When `fillSelection` is active, this spans the full selection width so the source can be tiled.\n */\n fields: string[];\n /**\n * When `true`, the paste handler tiles the clipboard source across the full\n * selection bounds (see {@link ClipboardConfig.fillSelection}). Set by the\n * plugin from config; only ever `true` when `target.bounds` is set.\n * @since 3.0.0\n */\n fillSelection?: boolean;\n /**\n * Raw (structured) cell values for a **same-grid** paste, aligned 1:1 with\n * {@link rows} (same dimensions, data rows only). Present only when the paste\n * text exactly matches what this grid last copied — so an object-valued cell\n * (e.g. `{id,name}`, arrays) round-trips losslessly instead of degrading to\n * the copied display text. `undefined` for external/cross-grid pastes, where\n * only the text is available. The default handler prefers `rawRows` over\n * `rows` for values (cloning each so targets don't share references).\n * @since 3.0.0\n */\n rawRows?: unknown[][];\n /**\n * Source column fields the pasted data was copied FROM, in the copied column\n * order (aligned with the columns of {@link rows} / {@link rawRows}). Lets a\n * column's {@link BaseColumnConfig.onPaste} know the origin column so it can\n * reject cross-column / cross-type pastes. Present for a **same-app** paste\n * (same grid via the in-memory buffer, or cross-grid / cross-window via the\n * structured `text/html` payload); `undefined` for external pastes (e.g. from\n * Excel), where only the text is available.\n * @since 3.0.0\n */\n sourceFields?: string[];\n}\n\n/**\n * Context passed to a column's {@link BaseColumnConfig.onPaste} guard/transform\n * for each cell a paste would write into.\n *\n * @since 3.0.0\n */\nexport interface PasteCellContext<TRow = unknown, TValue = unknown> {\n /** The incoming pasted value (the raw structured value for a same-grid paste). */\n value: TValue;\n /** The column field receiving the value. */\n field: string;\n /** The target row object (pre-paste state). */\n row: TRow;\n /** Index of the target row in the current data set. */\n rowIndex: number;\n /** The cell's current value before the paste. */\n oldValue: TValue;\n /**\n * The column field the value was copied FROM (the origin column), for a\n * same-app paste. `undefined` for an external paste (e.g. from Excel) or when\n * the source column is unknown. Use it to reject cross-column / cross-type\n * pastes (e.g. refuse a value copied from a \"car\" column into a \"fruit\"\n * column). To compare types, look up your own column config by this field.\n * @since 3.0.0\n */\n sourceField?: string;\n}\n\n/**\n * A column's paste guard / transform (`onPaste`). See {@link BaseColumnConfig.onPaste}.\n * @since 3.0.0\n */\nexport type ColumnPasteGuard<TRow = unknown, TValue = unknown> =\n boolean | ((ctx: PasteCellContext<TRow, TValue>) => boolean | { value: TValue });\n\n/**\n * Why a cell's paste was rejected: `'column'` = the column set `onPaste: false`;\n * `'cell'` = an `onPaste` callback returned `false`.\n * @since 3.0.0\n */\nexport type PasteRejectionReason = 'column' | 'cell';\n\n/** One cell whose paste a column's `onPaste` guard rejected. @since 3.0.0 */\nexport interface PasteRejectedCell {\n /** The column field. */\n field: string;\n /** Index of the target row in the current data set. */\n rowIndex: number;\n /** The target row object. */\n row: unknown;\n /** The incoming value that was rejected (before any transform). */\n value: unknown;\n /** Why it was rejected. */\n reason: PasteRejectionReason;\n}\n\n/**\n * Detail for the `paste-rejected` event: the cells a column's `onPaste` guard\n * rejected during a paste. Fires once per paste, only when at least one cell was\n * rejected, so a consumer can surface a message. Non-editable cells are skipped\n * silently and are NOT reported here.\n * @since 3.0.0\n */\nexport interface PasteRejectedDetail {\n /** Cells rejected by a column `onPaste` guard. */\n rejected: PasteRejectedCell[];\n}\n\n/** Result of {@link resolveColumnPaste}. @since 3.0.0 */\nexport type PasteResolution = { accepted: true; value: unknown } | { accepted: false; reason: PasteRejectionReason };\n\n/**\n * Resolve a column's {@link ColumnPasteGuard} (`onPaste`) for a single cell.\n *\n * Pure — never mutates or emits. Exposed so a **custom `pasteHandler`** can honor\n * per-column `onPaste` identically to {@link defaultPasteHandler}: call it per\n * cell, write `resolution.value` when accepted, and collect the rejected cells to\n * pass to {@link emitPasteRejected}.\n *\n * @param onPaste - The column's `onPaste` config (`column.onPaste`), if any.\n * @param ctx - The cell context (value, field, row, rowIndex, oldValue, sourceField).\n * @returns `{ accepted: true, value }` (value possibly transformed) or\n * `{ accepted: false, reason }`.\n *\n * @example\n * ```ts\n * // Inside a custom pasteHandler:\n * const rejected: PasteRejectedCell[] = [];\n * const res = resolveColumnPaste(column.onPaste, { value, field, row, rowIndex, oldValue });\n * if (res.accepted) row[field] = res.value;\n * else rejected.push({ field, rowIndex, row, value, reason: res.reason });\n * // …after writing all cells:\n * emitPasteRejected(grid, rejected);\n * ```\n * @since 3.0.0\n */\nexport function resolveColumnPaste(onPaste: ColumnPasteGuard | undefined, ctx: PasteCellContext): PasteResolution {\n if (onPaste === false) return { accepted: false, reason: 'column' };\n if (typeof onPaste === 'function') {\n const result = onPaste(ctx);\n if (result === false) return { accepted: false, reason: 'cell' };\n if (result && typeof result === 'object' && 'value' in result) {\n return { accepted: true, value: (result as { value: unknown }).value };\n }\n }\n return { accepted: true, value: ctx.value };\n}\n\n/**\n * Dispatch the `paste-rejected` event on the grid. No-op when `rejected` is empty.\n * Exposed so a custom `pasteHandler` can emit the same event the default handler\n * does, after resolving cells via {@link resolveColumnPaste}.\n * @since 3.0.0\n */\nexport function emitPasteRejected(grid: GridElement, rejected: PasteRejectedCell[]): void {\n if (rejected.length === 0) return;\n grid.dispatchEvent?.(new CustomEvent<PasteRejectedDetail>('paste-rejected', { detail: { rejected }, bubbles: true }));\n}\n\n/**\n * Default paste handler that applies pasted data to the grid.\n *\n * This is the built-in handler used when no custom `pasteHandler` is configured.\n *\n * Edits are routed through `grid.updateRows(…, 'paste')` so paste participates in\n * the full edit pipeline — dirty tracking, cancelable validation, undo/redo\n * history, and abortion — exactly like an interactive edit, and so a\n * `cell-commit` listener can tell a paste apart via `detail.source === 'paste'`.\n * Rows without a resolvable ID (no `getRowId`) fall back to a direct in-place\n * write, which still updates values but does not track them.\n *\n * Behavior:\n * - Single cell selection: paste expands freely, adds new rows if needed\n * - Range/row selection: paste is clipped to fit within selection bounds\n * - Fill-selection: when `detail.fillSelection` is set, the source is tiled\n * (repeated via modulo indexing) to fill the whole selection bounds\n * - Non-editable cells: values are skipped (column alignment preserved).\n * Editability is resolved by the editing plugin via the\n * `getCellEditableResolver` query — including row-conditional `editable`.\n * Without the editing plugin, no cell is editable and paste is a no-op.\n * - Per-column `onPaste`: after the editability check, a column's\n * {@link BaseColumnConfig.onPaste} may reject the cell (skipped, alignment\n * preserved) or transform the pasted value. Rejected cells are reported once\n * via the `paste-rejected` event ({@link PasteRejectedDetail}).\n *\n * @param detail - The parsed paste data from clipboard\n * @param grid - The grid element to update\n * @since 0.4.2\n */\nexport function defaultPasteHandler(detail: PasteDetail, grid: GridElement): void {\n const { rows: pastedRows, rawRows, target, fields, fillSelection, sourceFields } = detail;\n\n // No target = nothing to do\n if (!target || pastedRows.length === 0) return;\n\n // Prefer the raw structured values for a same-grid paste (lossless object\n // round-trip); fall back to the parsed text for external/cross-grid pastes.\n const valueRows: unknown[][] = rawRows ?? pastedRows;\n\n const currentRows = grid.rows as Record<string, unknown>[];\n const columns = grid.effectiveConfig.columns ?? [];\n const allFields = columns.map((col) => col.field);\n const columnByField = new Map(columns.map((col) => [col.field, col]));\n\n // Editability is owned by the editing plugin (row-conditional included). With\n // no editing plugin loaded, nothing is editable and paste is a no-op.\n const [canEditCell = () => false] = grid.query<CellEditablePredicate>('getCellEditableResolver');\n\n // Accumulate the intended edits per target row (honoring editability, tiling,\n // and selection bounds). Rows beyond current data are only created for an\n // unbounded (single-cell expansion) paste.\n const editsByRow = new Map<number, Record<string, unknown>>();\n let rowCountAfter = currentRows.length;\n // Cells a column's `onPaste` guard rejected — reported via `paste-rejected`.\n const rejected: PasteRejectedCell[] = [];\n\n const addEdit = (\n rowIndex: number,\n field: string | undefined,\n value: unknown,\n evalRow: Record<string, unknown>,\n sourceField?: string,\n ) => {\n if (!field || !canEditCell(field, evalRow)) return;\n // Per-column paste guard/transform (clipboard-augmented `onPaste`). Runs\n // after the editability check so it only sees cells paste could write.\n const resolution = resolveColumnPaste(columnByField.get(field)?.onPaste, {\n value,\n field,\n row: evalRow,\n rowIndex,\n oldValue: readCellField(evalRow, field),\n sourceField,\n });\n if (!resolution.accepted) {\n rejected.push({ field, rowIndex, row: evalRow, value, reason: resolution.reason });\n return; // reject this cell (alignment preserved, like a non-editable cell)\n }\n let changes = editsByRow.get(rowIndex);\n if (!changes) {\n changes = {};\n editsByRow.set(rowIndex, changes);\n }\n // Clone structured values so tiled/multi-target pastes never share a\n // reference (mutating one row's object would otherwise mutate the source\n // and every other target). No-op for primitives (the text path).\n changes[field] = cloneStructured(resolution.value);\n };\n\n if (fillSelection && target.bounds) {\n // Fill-selection (tile) mode: repeat the source across the selection bounds\n // using modulo indexing. Never grows the grid. Editability is resolved\n // against the pre-paste row (bounded mode never grows).\n const srcRows = valueRows.length;\n for (let rowIndex = target.row; rowIndex <= target.bounds.endRow; rowIndex++) {\n if (rowIndex >= currentRows.length) break;\n const sourceRow = valueRows[(rowIndex - target.row) % srcRows];\n const srcCols = sourceRow.length;\n if (srcCols === 0) continue;\n const evalRow = currentRows[rowIndex];\n fields.forEach((field, colOffset) =>\n addEdit(rowIndex, field, sourceRow[colOffset % srcCols], evalRow, sourceFields?.[colOffset % srcCols]),\n );\n }\n } else {\n const maxPasteRow = target.bounds ? target.bounds.endRow : Infinity;\n valueRows.forEach((rowData, rowOffset) => {\n const targetRowIndex = target.row + rowOffset;\n if (targetRowIndex > maxPasteRow) return;\n if (target.bounds && targetRowIndex >= currentRows.length) return; // bounded: don't grow\n if (!target.bounds && targetRowIndex >= rowCountAfter) rowCountAfter = targetRowIndex + 1;\n // Editability resolved against the pre-paste row (empty for grown rows).\n const evalRow = currentRows[targetRowIndex] ?? {};\n rowData.forEach((cellValue, colOffset) =>\n addEdit(targetRowIndex, fields[colOffset], cellValue, evalRow, sourceFields?.[colOffset]),\n );\n });\n }\n\n if (editsByRow.size === 0 && rowCountAfter === currentRows.length) {\n // Nothing accepted (e.g. every cell rejected) — still report the rejections.\n emitPasteRejected(grid, rejected);\n return;\n }\n\n // Grow the grid first for unbounded pastes that extend past existing data.\n // New rows start empty; their pasted values are applied in the write phase.\n if (rowCountAfter > currentRows.length) {\n const grown = [...currentRows];\n while (grown.length < rowCountAfter) {\n const emptyRow: Record<string, unknown> = {};\n allFields.forEach((f) => (emptyRow[f] = ''));\n grown.push(emptyRow);\n }\n grid.rows = grown;\n }\n\n const rowsNow = grid.rows as Record<string, unknown>[];\n\n // Route edits through updateRows so paste flows through the edit pipeline\n // (dirty tracking, validation, history, abortion). Rows without a resolvable\n // ID fall back to a direct in-place write.\n const updates: Array<{ id: string; changes: Record<string, unknown> }> = [];\n let directWrote = false;\n for (const [rowIndex, changes] of editsByRow) {\n const row = rowsNow[rowIndex];\n if (!row) continue;\n let id: string | undefined;\n try {\n id = grid.getRowId(row);\n } catch {\n id = undefined;\n }\n if (id == null) {\n // No stable ID → can't route through updateRow; write directly. Invalidate\n // the value-accessor cache per field (row identity is unchanged), matching\n // RowManager.updateRow — otherwise a `column.valueAccessor` display can go\n // stale after paste.\n for (const [field, value] of Object.entries(changes)) {\n writeCellField(row, field, value);\n invalidateAccessorCache(row as object, field);\n }\n directWrote = true;\n } else {\n updates.push({ id, changes });\n }\n }\n\n if (updates.length > 0) {\n grid.updateRows(updates, 'paste');\n } else if (directWrote && rowCountAfter === currentRows.length) {\n // Direct writes only, no structural grow above — trigger a render.\n grid.rows = [...rowsNow];\n }\n\n // Notify consumers of any cells a column's `onPaste` guard rejected.\n emitPasteRejected(grid, rejected);\n}\n\n/**\n * Deep-clone a structured cell value so tiled/multi-target pastes don't share a\n * reference. Primitives (and `null`) are returned as-is. Falls back to the\n * original value if it isn't structured-cloneable.\n * @internal\n */\nexport function cloneStructured(value: unknown): unknown {\n if (value === null || typeof value !== 'object') return value;\n try {\n return structuredClone(value);\n } catch {\n return value;\n }\n}\n\n// Module Augmentation - Register plugin name for type-safe getPluginByName()\ndeclare module '../../core/types' {\n interface DataGridEventMap {\n /** Fired after a successful copy operation. Provides the copied text, row count, and column count. @group Clipboard Events */\n copy: CopyDetail;\n /** Fired after a paste operation. Provides parsed rows, target cell, and column fields. @group Clipboard Events */\n paste: PasteDetail;\n /** Fired after a paste when one or more cells were rejected by a column's `onPaste` guard. @group Clipboard Events */\n 'paste-rejected': PasteRejectedDetail;\n }\n\n interface PluginNameMap {\n clipboard: import('./clipboard-plugin').ClipboardPlugin;\n }\n\n /** Clipboard contributes its own edit-origin tag so a `cell-commit` listener\n * can tell a paste from a user edit via `detail.source === 'paste'`. */\n interface UpdateSourceMap {\n /** A value written by a clipboard paste. @since 3.0.0 */\n paste: true;\n }\n\n interface BaseColumnConfig<TRow, TValue> {\n /**\n * Per-column paste guard / transform. Requires the ClipboardPlugin.\n *\n * Runs for each cell a paste would write into (after the editing plugin's\n * editability check), letting a consumer reject or rewrite pasted values\n * **per column** — finer-grained than the whole-operation\n * {@link ClipboardConfig.pasteHandler}.\n *\n * - `true` / omitted — accept pastes (default behavior).\n * - `false` — this column never accepts pastes; matching cells are skipped\n * (column alignment is preserved, like a non-editable cell).\n * - `(ctx) => …` — called per cell. Return `false` to reject just that cell,\n * `true` (or nothing) to accept as-is, or `{ value }` to write a\n * transformed value instead. Returning `{ value }` disambiguates a\n * transform from a boolean verdict, so boolean-valued columns stay safe.\n *\n * Synchronous only — the paste pipeline does not await. Rejected cells\n * (either form) are reported once per paste via the `paste-rejected` event\n * ({@link PasteRejectedDetail}), so a consumer can show a message.\n *\n * The callback context also carries `sourceField` — the column the value was\n * copied FROM (for a same-app paste; `undefined` for external pastes) — so a\n * column can reject cross-column / cross-type pastes (e.g. refuse a value\n * copied from a \"car\" column into a \"fruit\" column).\n *\n * @example\n * ```typescript\n * // Reject pastes into a locked column\n * { field: 'id', onPaste: false }\n *\n * // Reject invalid values, coerce the rest\n * {\n * field: 'price',\n * onPaste: ({ value }) => {\n * const n = Number(String(value).replace(/[^0-9.]/g, ''));\n * return Number.isFinite(n) ? { value: n } : false;\n * },\n * }\n * ```\n * @since 3.0.0\n */\n onPaste?: ColumnPasteGuard<TRow, TValue>;\n }\n}\n","/**\n * Clipboard Plugin (Class-based)\n *\n * Provides copy/paste functionality for tbw-grid.\n * Supports Ctrl+C/Cmd+C for copying and Ctrl+V/Cmd+V for pasting.\n *\n * **With Selection plugin:** Copies selected cells/rows/range\n * **Without Selection plugin:** Copies entire grid\n */\n\nimport { resolveCellValue } from '../../core/internal/value-accessor';\nimport {\n BaseGridPlugin,\n type GridElement,\n type PluginDependency,\n type PluginManifest,\n type PluginQuery,\n} from '../../core/plugin/base-plugin';\nimport type { ColumnConfig } from '../../core/types';\nimport { readCellDisplayText } from '../shared/cell-text';\nimport { formatValueAsText, resolveColumns, resolveRows } from '../shared/data-collection';\nimport { buildClipboardHtml, parseClipboardHtmlPayload } from './clipboard-payload';\nimport { copyToClipboard } from './copy';\nimport { parseClipboardText, readFromClipboard } from './paste';\nimport {\n cloneStructured,\n defaultPasteHandler,\n type ClipboardConfig,\n type CopyDetail,\n type CopyOptions,\n type PasteDetail,\n type PasteTarget,\n} from './types';\n\n/**\n * Clipboard Plugin for tbw-grid\n *\n * Brings familiar copy/paste functionality with full keyboard shortcut support\n * (Ctrl/Cmd+C, Ctrl/Cmd+V). Handles single cells, multi-cell selections, and integrates\n * seamlessly with Excel and other spreadsheet applications via tab-delimited output.\n *\n * > **Optional Dependency:** Works best with SelectionPlugin for copying/pasting selected\n * > cells. Without SelectionPlugin, copies the entire grid and pastes at row 0, column 0.\n *\n * ## Installation\n *\n * ```ts\n * import { ClipboardPlugin } from '@toolbox-web/grid/plugins/clipboard';\n * ```\n *\n * ## Keyboard Shortcuts\n *\n * | Shortcut | Action |\n * |----------|--------|\n * | `Ctrl+C` / `Cmd+C` | Copy selected cells |\n * | `Ctrl+V` / `Cmd+V` | Paste into selected cells |\n *\n * ## Paste Behavior by Selection Type\n *\n * | Selection Type | Paste Behavior |\n * |----------------|----------------|\n * | Single cell | Paste expands freely from that cell |\n * | Range selection | Paste is clipped to fit within the selected range |\n * | Row selection | Paste is clipped to the selected rows |\n * | No selection | Paste starts at row 0, column 0 |\n *\n * With `fillSelection: true`, pasting a smaller source into a larger bounded\n * selection tiles the source to fill the whole selection (e.g. one cell fills\n * all selected cells; a 1×2 source fills as `val1, val2, val1, val2`).\n *\n * @example Basic Usage with Excel Compatibility\n * ```ts\n * import '@toolbox-web/grid';\n * import { ClipboardPlugin } from '@toolbox-web/grid/plugins/clipboard';\n * import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection';\n *\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name' },\n * { field: 'email', header: 'Email' },\n * ],\n * plugins: [\n * new SelectionPlugin({ mode: 'range' }),\n * new ClipboardPlugin({\n * includeHeaders: true,\n * delimiter: '\\t', // Tab for Excel\n * }),\n * ],\n * };\n * ```\n *\n * @example Custom Paste Handler\n * ```ts\n * new ClipboardPlugin({\n * pasteHandler: (grid, target, data) => {\n * // Validate or transform data before applying\n * console.log('Pasting', data.length, 'rows');\n * return defaultPasteHandler(grid, target, data);\n * },\n * })\n * ```\n *\n * @see {@link ClipboardConfig} for all configuration options\n * @see SelectionPlugin for enhanced copy/paste with selection\n *\n * @internal Extends BaseGridPlugin\n * @since 0.1.1\n */\nexport class ClipboardPlugin extends BaseGridPlugin<ClipboardConfig> {\n /**\n * Plugin manifest — declares queries for inter-plugin communication.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n queries: [{ type: 'clipboard:copy', description: 'Triggers a copy operation and returns the copied text' }],\n };\n\n /**\n * Plugin dependencies - ClipboardPlugin works best with SelectionPlugin.\n *\n * Without SelectionPlugin: copies entire grid, pastes at row 0 col 0.\n * With SelectionPlugin: copies/pastes based on selection.\n */\n /** @internal */\n static override readonly dependencies: PluginDependency[] = [\n { name: 'selection', required: false, reason: 'Enables copy/paste of selected cells instead of entire grid' },\n ];\n\n /** @internal */\n readonly name = 'clipboard';\n\n /** @internal */\n protected override get defaultConfig(): Partial<ClipboardConfig> {\n return {\n includeHeaders: false,\n delimiter: '\\t',\n newline: '\\n',\n quoteStrings: false,\n };\n }\n\n // #region Internal State\n /** The last copied text (for reference/debugging) */\n private lastCopied: { text: string; timestamp: number } | null = null;\n /**\n * Raw (structured) values for the last copy, so a same-grid paste round-trips\n * object cells losslessly instead of degrading to the copied display text.\n * `text` is the exact clipboard text; a paste whose text matches restores\n * `rawRows` (data rows only, aligned with the copied columns). Cleared on\n * detach; a non-matching external paste simply ignores it.\n */\n #internalClipboard: { text: string; rawRows: unknown[][]; fields: string[] } | null = null;\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n // Listen for native paste events to get clipboard data synchronously\n // This is more reliable than the async Clipboard API in iframe contexts\n grid.addEventListener('paste', (e) => this.#handleNativePaste(e), {\n signal: this.disconnectSignal,\n });\n }\n\n /** @internal */\n override detach(): void {\n this.lastCopied = null;\n this.#internalClipboard = null;\n }\n\n /** @internal */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'clipboard:copy') {\n this.copy();\n return true;\n }\n return undefined;\n }\n // #endregion\n\n // #region Event Handlers\n\n /** @internal */\n override onKeyDown(event: KeyboardEvent): boolean {\n const isCopy = (event.ctrlKey || event.metaKey) && event.key === 'c';\n\n if (isCopy) {\n // Prevent the browser's default copy action so it doesn't overwrite\n // our clipboard write with whatever text is selected in the DOM.\n event.preventDefault();\n this.#handleCopy(event.target as HTMLElement);\n return true;\n }\n\n // For paste, we do NOT return true - let the native paste event fire\n // so we can access clipboardData synchronously in #handleNativePaste\n return false;\n }\n // #endregion\n\n // #region Private Methods\n\n /**\n * Handle copy operation from keyboard shortcut.\n *\n * For keyboard-triggered copies, respects the current selection or\n * falls back to the focused cell from the DOM.\n */\n #handleCopy(target: HTMLElement): void {\n const selection = this.#getSelection();\n\n // Selection plugin exists but has no ranges → resolve the single active cell.\n // Range mode CLEARS `ranges` on plain keyboard navigation (arrow/Tab without\n // Shift), keeping the focused cell only as the selection `anchor`, and DOM\n // focus is NOT on a cell element after keyboard nav — so prefer the anchor\n // (a visible-column index) and only fall back to the focused DOM cell for\n // the mouse-focus / no-anchor edge. Without the anchor path, keyboard-nav\n // copy silently no-ops.\n if (selection && selection.ranges.length === 0) {\n let row: number | undefined;\n let field: string | undefined;\n if (selection.anchor) {\n row = selection.anchor.row;\n field = this.visibleColumns[selection.anchor.col]?.field;\n } else {\n const focused = this.#getFocusedCellFromDOM(target);\n if (focused) {\n row = focused.row;\n field = this.columns[focused.col]?.field;\n }\n }\n if (row == null || field == null) return;\n this.copy({ rowIndices: [row], columns: [field] });\n return;\n }\n\n // Delegate to the public copy() method (selection or full grid)\n this.copy();\n }\n\n /**\n * Handle native paste event (preferred method - works in iframes).\n * Uses synchronous clipboardData from the native paste event.\n *\n * Flow:\n * 1. Parse clipboard text\n * 2. Build target/fields info from selection\n * 3. Emit 'paste' event (for listeners)\n * 4. Call paste handler (if configured) to apply data to grid\n *\n * Selection behavior:\n * - Single cell: paste starts at cell, expands freely\n * - Range/row: paste is clipped to fit within selection bounds\n * - No selection: paste starts at row 0, col 0\n */\n #handleNativePaste(event: ClipboardEvent): void {\n // If the paste target is an editable form control or contenteditable element\n // (e.g. an active cell editor injected by EditingPlugin, a filter input, a\n // header search box), let the browser deliver the paste to that control\n // natively. Without this guard `event.preventDefault()` below would swallow\n // the user's keystroke and the editor input would never receive the text.\n if (this.#isEditablePasteTarget(event.target)) return;\n\n const text = event.clipboardData?.getData('text/plain');\n if (!text) return;\n\n // Prevent default to avoid pasting into contenteditable elements\n event.preventDefault();\n\n const parsed = parseClipboardText(text, this.config);\n\n // Get target cell from selection via query\n const selection = this.#getSelection();\n const ranges = selection?.ranges ?? [];\n const firstRange = ranges[0];\n\n // Aggregate the bounding box across ALL selected ranges. A multi-cell\n // selection is frequently expressed as several single-cell ranges (clicking\n // or dragging cell-by-cell), so inspecting `ranges[0]` alone would treat it\n // as a single cell and skip fill/clip — pasting into only the first cell.\n // min/max also normalizes reversed (bottom-up / right-to-left) drags.\n let selMinRow = Infinity;\n let selMinCol = Infinity;\n let selMaxRow = -Infinity;\n let selMaxCol = -Infinity;\n for (const r of ranges) {\n selMinRow = Math.min(selMinRow, r.from.row, r.to.row);\n selMaxRow = Math.max(selMaxRow, r.from.row, r.to.row);\n selMinCol = Math.min(selMinCol, r.from.col, r.to.col);\n selMaxCol = Math.max(selMaxCol, r.from.col, r.to.col);\n }\n\n // Determine target cell (top-left of the whole selection). Range mode\n // CLEARS `ranges` on plain keyboard navigation (arrow/Tab without Shift),\n // keeping the focused cell only as the selection `anchor`. So when there are\n // no ranges, fall back to the anchor before defaulting to (0,0) — otherwise\n // \"copy, arrow to another cell, paste\" always lands on cell (0,0) instead of\n // the active cell. Mirrors the focused-cell fallback in #handleCopy.\n const anchor = selection?.anchor ?? null;\n const targetRow = firstRange ? selMinRow : (anchor?.row ?? 0);\n const targetCol = firstRange ? selMinCol : (anchor?.col ?? 0);\n\n // Multi-cell when the selection spans more than one cell in total, whether\n // it's one large range or many single-cell ranges.\n const axis = selectionAxis(selection);\n const isMultiCell =\n !!firstRange && (axis === 'range' || axis === 'row') && (selMinRow !== selMaxRow || selMinCol !== selMaxCol);\n\n const bounds = isMultiCell ? { endRow: selMaxRow, endCol: selMaxCol } : null;\n // Selection range indices are visible-column indices (from data-col)\n const maxCol = bounds?.endCol ?? this.visibleColumns.length - 1;\n\n // Drop a copied header row so a round-trip paste writes only values into\n // cells (never the header label). Strips when this grid is configured to\n // include headers AND every cell of the first parsed row is one of this\n // grid's own column labels — independent of which column is pasted INTO, so\n // cross-column pastes (e.g. \"Source terminal\" → \"Terminal route\") strip too.\n // External pastes (first row ≠ labels) are left intact.\n this.#dropCopiedHeaderRow(parsed);\n\n // Same-grid round-trip: when the pasted text is exactly what THIS grid last\n // copied, restore the raw structured values (lossless object cells) aligned\n // with the header-stripped text rows — this path preserves exact value types\n // (e.g. Date) since it never serializes. For a cross-grid / cross-window\n // paste the in-memory buffer misses, so fall back to the structured payload\n // embedded in the clipboard's text/html. External pastes have neither and\n // use the parsed text.\n let rawRows: unknown[][] | undefined;\n let sourceFields: string[] | undefined;\n if (\n this.#internalClipboard &&\n this.#internalClipboard.text === text &&\n this.#internalClipboard.rawRows.length === parsed.length\n ) {\n rawRows = this.#internalClipboard.rawRows;\n sourceFields = this.#internalClipboard.fields;\n } else {\n const payload = parseClipboardHtmlPayload(event.clipboardData?.getData('text/html'));\n if (payload && payload.rows.length === parsed.length) {\n rawRows = payload.rows;\n sourceFields = payload.fields;\n }\n }\n\n // Build target info\n const column = this.visibleColumns[targetCol];\n const target: PasteTarget | null = column ? { row: targetRow, col: targetCol, field: column.field, bounds } : null;\n\n // Fill-selection tiling only applies to a bounded (multi-cell) selection.\n const fillSelection = this.config.fillSelection === true && bounds !== null;\n\n // Build field list for paste width (constrained by bounds if set). When\n // filling the selection, extend fields across the full selection width so\n // the source can be tiled to fill every selected column; otherwise cap at\n // the clipboard's own width.\n const fields: string[] = [];\n const pasteWidth = parsed[0]?.length ?? 0;\n const fieldWidth = fillSelection ? maxCol - targetCol + 1 : pasteWidth;\n for (let i = 0; i < fieldWidth && targetCol + i <= maxCol; i++) {\n const col = this.visibleColumns[targetCol + i];\n if (col) {\n fields.push(col.field);\n }\n }\n\n const detail: PasteDetail = { rows: parsed, text, target, fields, fillSelection, rawRows, sourceFields };\n\n // Emit the event for any listeners\n this.emit<PasteDetail>('paste', detail);\n\n // Apply paste data using the configured handler (or default)\n this.#applyPasteHandler(detail);\n }\n\n /**\n * Apply the paste handler to update grid data.\n *\n * Uses the configured `pasteHandler`, or the default handler if not specified.\n * Set `pasteHandler: null` in config to disable auto-paste.\n */\n #applyPasteHandler(detail: PasteDetail): void {\n if (!this.grid) return;\n\n const { pasteHandler } = this.config;\n\n // pasteHandler: null means explicitly disabled\n if (pasteHandler === null) return;\n\n // Use custom handler or default\n const handler = pasteHandler ?? defaultPasteHandler;\n handler(detail, this.grid);\n }\n\n /**\n * Drop a header row that this grid itself copied, so a round-trip paste\n * never lands a header label in a data cell.\n *\n * A row copied with `includeHeaders` consists entirely of this grid's own\n * column labels (`header || field`). Detect that by matching every cell of the\n * first parsed row against the full set of column labels — independent of\n * which column the user pastes INTO. This is essential for **cross-column**\n * paste (e.g. copying \"Source terminal\" and pasting into \"Terminal route\"):\n * the copied header carries the SOURCE column's label, which does not equal\n * the target column's label but is still one of this grid's labels. Matching\n * the label set fixes cross-column paste while leaving external pastes (whose\n * first row is data, not labels) untouched.\n *\n * Guarded to avoid eating a legitimate first data row:\n * 1. The grid is configured with `includeHeaders` (it copies headers), and\n * 2. there is more than one row (never strip the only row), and\n * 3. every cell of the first row equals some column's label.\n */\n #dropCopiedHeaderRow(parsed: string[][]): void {\n if (!this.config.includeHeaders) return;\n // Never strip away the only row — that would make paste a no-op.\n if (parsed.length <= 1) return;\n\n const headerRow = parsed[0];\n if (headerRow.length === 0) return;\n\n const labels = this.#columnLabels();\n for (const cell of headerRow) {\n // A cell that isn't a known column label → this is real data, keep the row.\n if (!labels.has(cell)) return;\n }\n\n parsed.shift();\n }\n\n /**\n * Set of this grid's column labels (`header || field`), matching how\n * {@link buildClipboardText} serializes the copied header row. Used to detect\n * a header row this grid copied, regardless of paste position.\n */\n #columnLabels(): Set<string> {\n const labels = new Set<string>();\n for (const col of this.columns) {\n const label = col.header || col.field;\n if (label) labels.add(label);\n }\n return labels;\n }\n\n /**\n * True when the paste event originated from a focusable form control or\n * contenteditable element — i.e. somewhere the user expects the browser's\n * native paste behavior (typing into an editor input). Lets EditingPlugin\n * editors, filter inputs, and other text fields receive paste normally\n * instead of having ClipboardPlugin swallow the event.\n */\n #isEditablePasteTarget(target: EventTarget | null): boolean {\n if (!(target instanceof Element)) return false;\n if (target.closest('input, textarea, select, [contenteditable=\"\"], [contenteditable=\"true\"]')) {\n return true;\n }\n return false;\n }\n\n /**\n * Get the current selection via Query System.\n * Returns undefined if no selection plugin is loaded or nothing is selected.\n */\n #getSelection(): SelectionQueryResult | undefined {\n const responses = this.grid?.query<SelectionQueryResult>('getSelection');\n return responses?.[0];\n }\n\n /**\n * Resolve columns and rows to include based on options and/or current selection.\n *\n * Priority for columns:\n * 1. `options.columns` (explicit field list)\n * 2. Selection union column bounds across ALL ranges (range/cell mode only)\n * 3. All visible non-utility columns\n *\n * Priority for rows:\n * 1. `options.rowIndices` (explicit indices)\n * 2. Selection union row bounds across ALL ranges\n * 3. All rows\n *\n * `contains(rowOffset, colOffset)` (when non-null) reports whether a cell of\n * the resolved grid lies inside at least one selected range — callers use it\n * to blank the gaps between disjoint ranges, matching Excel's rectangular\n * multi-range copy.\n */\n #resolveData(options?: CopyOptions): {\n columns: ColumnConfig[];\n rows: Record<string, unknown>[];\n contains: ((rowOffset: number, colOffset: number) => boolean) | null;\n } {\n const selection = this.#getSelection();\n const ranges = selection?.ranges ?? [];\n const isRowAxis = selectionAxis(selection) === 'row';\n\n // Union bounding box across ALL ranges. A multi-range selection (CTRL+drag\n // repeatedly) must copy every range, not just the last one drawn; min/max\n // also normalizes reversed (bottom-up / right-to-left) drags.\n let minRow = Infinity;\n let maxRow = -Infinity;\n let minCol = Infinity;\n let maxCol = -Infinity;\n for (const r of ranges) {\n minRow = Math.min(minRow, r.from.row, r.to.row);\n maxRow = Math.max(maxRow, r.from.row, r.to.row);\n minCol = Math.min(minCol, r.from.col, r.to.col);\n maxCol = Math.max(maxCol, r.from.col, r.to.col);\n }\n\n // --- Columns ---\n let columns: ColumnConfig[];\n // Absolute visible-column index per resolved column (null when the columns\n // did not come from the selection). `resolveColumns` may drop utility /\n // hidden columns from the slice, so offsets can't be derived arithmetically.\n let colIndices: number[] | null = null;\n if (options?.columns) {\n // Caller specified exact fields\n columns = resolveColumns(this.columns, options.columns);\n } else if (ranges.length && !isRowAxis) {\n // Range/cell selection: restrict to selection column bounds\n // Selection indices are visible-column indices (from data-col)\n columns = resolveColumns(this.visibleColumns.slice(minCol, maxCol + 1));\n colIndices = columns.map((col) => this.visibleColumns.indexOf(col));\n } else {\n // Row selection or no selection: all visible columns\n columns = resolveColumns(this.columns);\n }\n\n // --- Rows ---\n let rows: Record<string, unknown>[];\n let rowIndices: number[] | null = null;\n if (options?.rowIndices) {\n // Caller specified exact row indices\n rows = resolveRows(this.rows as Record<string, unknown>[], options.rowIndices);\n } else if (ranges.length) {\n // Selection range: extract the union row span. Row mode selects WHOLE\n // rows and gets no column mask, so gaps between disjoint row ranges\n // (Ctrl+click rows 0, 5, 9) must be dropped outright rather than copied.\n const dropUnselectedRows = isRowAxis;\n const rowIsSelected = (r: number) =>\n ranges.some((g) => r >= Math.min(g.from.row, g.to.row) && r <= Math.max(g.from.row, g.to.row));\n rows = [];\n rowIndices = [];\n for (let r = minRow; r <= maxRow; r++) {\n const row = this.rows[r] as Record<string, unknown> | undefined;\n if (!row) continue;\n if (dropUnselectedRows && !rowIsSelected(r)) continue;\n rows.push(row);\n rowIndices.push(r);\n }\n } else {\n // No selection: all rows\n rows = this.rows as Record<string, unknown>[];\n }\n\n // Masking is only meaningful when BOTH axes came from the selection —\n // explicit `options.columns` / `options.rowIndices` (e.g. `copyRows()`)\n // would otherwise blank everything.\n const rowAbs = rowIndices;\n const colAbs = colIndices;\n const contains =\n rowAbs && colAbs\n ? (rowOffset: number, colOffset: number): boolean => {\n const row = rowAbs[rowOffset];\n const col = colAbs[colOffset];\n return ranges.some(\n (r) =>\n row >= Math.min(r.from.row, r.to.row) &&\n row <= Math.max(r.from.row, r.to.row) &&\n col >= Math.min(r.from.col, r.to.col) &&\n col <= Math.max(r.from.col, r.to.col),\n );\n }\n : null;\n\n return { columns, rows, contains };\n }\n\n /**\n * Build delimited text from resolved columns and rows.\n *\n * When no `processCell` callback is configured, uses \"copy what you see\" logic:\n * 1. Column `format` function (includes typeDefaults merged at config time)\n * 2. DOM cell textContent for columns with custom renderers (visible rows only)\n * 3. Raw value via `formatValueAsText` as final fallback\n */\n #buildText(\n columns: ColumnConfig[],\n rows: Record<string, unknown>[],\n options?: CopyOptions,\n contains?: ((rowOffset: number, colOffset: number) => boolean) | null,\n ): string {\n return this.#joinDisplayGrid(this.#buildDisplayGrid(columns, rows, options, contains), options);\n }\n\n /**\n * Build the 2D grid of DISPLAY strings for the given columns/rows (the same\n * WYSIWYG text used for `text/plain` and the `text/html` table). Header is\n * included only when configured.\n *\n * `contains` blanks cells that sit inside the selection's union bounding box\n * but outside every individual range, so a multi-range copy stays rectangular.\n */\n #buildDisplayGrid(\n columns: ColumnConfig[],\n rows: Record<string, unknown>[],\n options?: CopyOptions,\n contains?: ((rowOffset: number, colOffset: number) => boolean) | null,\n ): { header: string[] | null; rows: string[][] } {\n const includeHeaders = options?.includeHeaders ?? this.config.includeHeaders ?? false;\n const processCell = options?.processCell ?? this.config.processCell;\n const header = includeHeaders ? columns.map((c) => c.header || c.field) : null;\n const dataRows = rows.map((row, rowOffset) =>\n columns.map((col, colOffset) => {\n if (contains && !contains(rowOffset, colOffset)) return '';\n const value = resolveCellValue(row, col);\n if (processCell) return processCell(value, col.field, row);\n return this.#formatCellAsDisplayed(col, value, row);\n }),\n );\n return { header, rows: dataRows };\n }\n\n /** Join a display grid into delimited text (header row first when present). */\n #joinDisplayGrid(display: { header: string[] | null; rows: string[][] }, options?: CopyOptions): string {\n const delimiter = options?.delimiter ?? this.config.delimiter ?? '\\t';\n const newline = options?.newline ?? this.config.newline ?? '\\n';\n const lines: string[] = [];\n if (display.header) lines.push(display.header.join(delimiter));\n for (const cells of display.rows) lines.push(cells.join(delimiter));\n return lines.join(newline);\n }\n\n /**\n * Format a cell value the way the grid displays it.\n *\n * Priority:\n * 1. Column `format` function (includes typeDefaults applied at config merge time)\n * 2. DOM textContent for columns with a custom renderer (virtualized rows only)\n * 3. Raw value via `formatValueAsText`\n */\n #formatCellAsDisplayed(col: ColumnConfig, value: unknown, row: Record<string, unknown>): string {\n // 1. Column format function (covers col.format and typeDefaults)\n if (col.format) {\n try {\n const formatted = col.format(value, row);\n return formatted == null ? '' : String(formatted);\n } catch {\n // Format failed — fall through to next strategy\n }\n }\n\n // 2. DOM textContent for columns with custom renderers\n if (col.renderer || col.viewRenderer) {\n const text = this.#readCellTextFromDOM(col.field, row);\n if (text != null) return text;\n }\n\n // 3. Fallback to raw value formatting\n return formatValueAsText(value);\n }\n\n /**\n * Try to read a cell's displayed text from the DOM.\n *\n * Only works for rows currently rendered in the virtualized viewport.\n * Returns `null` if the row is not in the DOM (caller should fall back).\n */\n #readCellTextFromDOM(field: string, row: Record<string, unknown>): string | null {\n const host = this.gridElement;\n if (!host) return null;\n\n // Find the row index in the grid's current row array\n const rowIndex = this.rows.indexOf(row);\n if (rowIndex === -1) return null;\n\n // Query the rendered cell by row index and field name\n const cell = host.querySelector<HTMLElement>(`.cell[data-row=\"${rowIndex}\"][data-field=\"${field}\"]`);\n if (!cell) return null;\n\n return readCellDisplayText(cell);\n }\n\n /**\n * Get focused cell coordinates from DOM.\n * Used as fallback when SelectionPlugin has no selection.\n */\n #getFocusedCellFromDOM(target: HTMLElement): { row: number; col: number } | null {\n // Cells carry `data-field` + `data-row` (see `data-field-cache` was never a\n // real attribute). Body cells have `data-row`; header cells don't, so the\n // `data-row` guard below excludes headers.\n const cell = target.closest('.cell[data-field]') as HTMLElement | null;\n if (!cell) return null;\n\n const field = cell.dataset.field;\n const rowIndexStr = cell.dataset.row;\n if (!field || !rowIndexStr) return null;\n\n const row = parseInt(rowIndexStr, 10);\n if (isNaN(row)) return null;\n\n const col = this.columns.findIndex((c) => c.field === field);\n if (col === -1) return null;\n\n return { row, col };\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Get the text representation of the current selection (or specified data)\n * without writing to the system clipboard.\n *\n * Useful for previewing what would be copied, or for feeding the text into\n * a custom UI (e.g., a \"copy with column picker\" dialog).\n *\n * @param options - Control which columns/rows to include\n * @returns Delimited text, or empty string if nothing to copy\n *\n * @example Get text for specific columns\n * ```ts\n * const clipboard = grid.getPluginByName('clipboard');\n * const text = clipboard.getSelectionAsText({\n * columns: ['name', 'email'],\n * includeHeaders: true,\n * });\n * console.log(text);\n * // \"Name\\tEmail\\nAlice\\talice@example.com\\n...\"\n * ```\n */\n getSelectionAsText(options?: CopyOptions): string {\n const { columns, rows, contains } = this.#resolveData(options);\n if (columns.length === 0 || rows.length === 0) return '';\n return this.#buildText(columns, rows, options, contains);\n }\n\n /**\n * Copy data to the system clipboard.\n *\n * Without options, copies the current selection (or entire grid if no selection).\n * With options, copies exactly the specified columns and/or rows — ideal for\n * \"copy with column picker\" workflows where the user selects rows first,\n * then chooses which columns to include via a dialog.\n *\n * @param options - Control which columns/rows to include\n * @returns The copied text\n *\n * @example Copy current selection (default)\n * ```ts\n * const clipboard = grid.getPluginByName('clipboard');\n * await clipboard.copy();\n * ```\n *\n * @example Copy specific columns from specific rows\n * ```ts\n * // User selected rows in the grid, then picked columns in a dialog\n * const selectedRowIndices = [0, 3, 7];\n * const chosenColumns = ['name', 'department', 'salary'];\n * await clipboard.copy({\n * rowIndices: selectedRowIndices,\n * columns: chosenColumns,\n * includeHeaders: true,\n * });\n * ```\n */\n async copy(options?: CopyOptions): Promise<string> {\n const { columns, rows, contains } = this.#resolveData(options);\n if (columns.length === 0 || rows.length === 0) {\n return '';\n }\n\n const display = this.#buildDisplayGrid(columns, rows, options, contains);\n const text = this.#joinDisplayGrid(display, options);\n // Snapshot the RAW cell values (same column order as the text) for a\n // structured round-trip. Cloned so later row mutations don't change it.\n // Masked gaps between disjoint ranges stay blank here too, so a same-app\n // paste can't resurrect values the user never selected.\n const rawRows = rows.map((row, rowOffset) =>\n columns.map((col, colOffset) =>\n contains && !contains(rowOffset, colOffset) ? '' : cloneStructured(resolveCellValue(row, col)),\n ),\n );\n const fields = columns.map((col) => col.field);\n // Write BOTH text/plain (WYSIWYG for external targets) and text/html (a\n // table that also embeds the structured payload) so a same-app paste — even\n // cross-grid or cross-window — restores object cells losslessly.\n await copyToClipboard(text, buildClipboardHtml(display.header, display.rows, { v: 1, fields, rows: rawRows }));\n this.lastCopied = { text, timestamp: Date.now() };\n this.#internalClipboard = { text, rawRows, fields };\n this.emit<CopyDetail>('copy', {\n text,\n rowCount: rows.length,\n columnCount: columns.length,\n });\n return text;\n }\n\n /**\n * Copy specific rows by index to clipboard.\n *\n * Convenience wrapper around {@link copy} for row-based workflows.\n * Supports non-contiguous row indices (e.g., `[0, 3, 7]`).\n *\n * @param indices - Array of row indices to copy\n * @param options - Additional copy options (columns, headers, etc.)\n * @returns The copied text\n *\n * @example\n * ```ts\n * const clipboard = grid.getPluginByName('clipboard');\n * // Copy only rows 0 and 5, including just name and email columns\n * await clipboard.copyRows([0, 5], { columns: ['name', 'email'] });\n * ```\n */\n async copyRows(indices: number[], options?: Omit<CopyOptions, 'rowIndices'>): Promise<string> {\n if (indices.length === 0) return '';\n return this.copy({ ...options, rowIndices: indices });\n }\n\n /**\n * Read and parse clipboard content.\n * @returns Parsed 2D array of cell values, or null if clipboard is empty\n */\n async paste(): Promise<string[][] | null> {\n const text = await readFromClipboard();\n if (!text) return null;\n return parseClipboardText(text, this.config);\n }\n\n /**\n * Get the last copied text and timestamp.\n * @returns The last copied info or null\n */\n getLastCopied(): { text: string; timestamp: number } | null {\n return this.lastCopied;\n }\n // #endregion\n}\n\n// #region Internal Types\n\n/**\n * Range representation for clipboard operations.\n */\ninterface CellRange {\n from: { row: number; col: number };\n to: { row: number; col: number };\n}\n\n/**\n * Selection result returned by the Query System.\n * Matches the SelectionResult type from SelectionPlugin.\n */\ninterface SelectionQueryResult {\n mode: string | string[];\n activeAxis?: string;\n ranges: CellRange[];\n anchor: { row: number; col: number } | null;\n}\n\n/**\n * Axis that owns the current selection. `mode` may be an array (e.g.\n * `['range', 'column']`), so prefer `activeAxis` and fall back to a string `mode`.\n */\nfunction selectionAxis(sel: SelectionQueryResult | undefined): string | undefined {\n return sel?.activeAxis ?? (typeof sel?.mode === 'string' ? sel.mode : undefined);\n}\n// #endregion\n\n// Re-export types\nexport type { ClipboardConfig, CopyDetail, CopyOptions, PasteDetail } from './types';\n"],"mappings":"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"}