const b = '<svg viewBox="0 0 16 16" width="12" height="12"><path fill="currentColor" d="M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z"/></svg>', x = {
  expand: "▶",
  collapse: "▼",
  sortAsc: "▲",
  sortDesc: "▼",
  sortNone: "⇅",
  submenuArrow: "▶",
  dragHandle: "⋮⋮",
  toolPanel: "☰",
  filter: b,
  filterActive: b,
  print: "🖨️"
};
class v {
  /**
   * Plugin dependencies - declare other plugins this one requires.
   *
   * Dependencies are validated when the plugin is attached.
   * Required dependencies throw an error if missing.
   * Optional dependencies log an info message if missing.
   *
   * @example
   * ```typescript
   * static readonly dependencies: PluginDependency[] = [
   *   { name: 'editing', required: true, reason: 'Tracks cell edits for undo/redo' },
   *   { name: 'selection', required: false, reason: 'Enables selection-based undo' },
   * ];
   * ```
   */
  static dependencies;
  /**
   * Plugin manifest - declares owned properties, config rules, and hook priorities.
   *
   * This is read by the configuration validator to:
   * - Validate that required plugins are loaded when their properties are used
   * - Execute configRules to detect invalid/conflicting settings
   * - Order hook execution based on priority
   *
   * @example
   * ```typescript
   * static override readonly manifest: PluginManifest<MyConfig> = {
   *   ownedProperties: [
   *     { property: 'myProp', level: 'column', description: 'the "myProp" column property' },
   *   ],
   *   configRules: [
   *     { id: 'myPlugin/conflict', severity: 'warn', message: '...', check: (c) => c.a && c.b },
   *   ],
   * };
   * ```
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  static manifest;
  /**
   * Plugin version - defaults to grid version for built-in plugins.
   * Third-party plugins can override with their own semver.
   */
  version = typeof __GRID_VERSION__ < "u" ? __GRID_VERSION__ : "dev";
  /** CSS styles to inject into the grid's shadow DOM */
  styles;
  /** Custom cell renderers keyed by type name */
  cellRenderers;
  /** Custom header renderers keyed by type name */
  headerRenderers;
  /** Custom cell editors keyed by type name */
  cellEditors;
  /** The grid instance this plugin is attached to */
  grid;
  /** Plugin configuration - merged with defaults in attach() */
  config;
  /** User-provided configuration from constructor */
  userConfig;
  /**
   * Plugin-level AbortController for event listener cleanup.
   * Created fresh in attach(), aborted in detach().
   * This ensures event listeners are properly cleaned up when plugins are re-attached.
   */
  #t;
  /**
   * Default configuration - subclasses should override this getter.
   * Note: This must be a getter (not property initializer) for proper inheritance
   * since property initializers run after parent constructor.
   */
  get defaultConfig() {
    return {};
  }
  constructor(t = {}) {
    this.userConfig = t;
  }
  /**
   * Called when the plugin is attached to a grid.
   * Override to set up event listeners, initialize state, etc.
   *
   * @example
   * ```ts
   * attach(grid: GridElement): void {
   *   super.attach(grid);
   *   // Set up document-level listeners with auto-cleanup
   *   document.addEventListener('keydown', this.handleEscape, {
   *     signal: this.disconnectSignal
   *   });
   * }
   * ```
   */
  attach(t) {
    this.#t?.abort(), this.#t = new AbortController(), this.grid = t, this.config = { ...this.defaultConfig, ...this.userConfig };
  }
  /**
   * Called when the plugin is detached from a grid.
   * Override to clean up event listeners, timers, etc.
   *
   * @example
   * ```ts
   * detach(): void {
   *   // Clean up any state not handled by disconnectSignal
   *   this.selectedRows.clear();
   *   this.cache = null;
   * }
   * ```
   */
  detach() {
    this.#t?.abort(), this.#t = void 0;
  }
  /**
   * Get another plugin instance from the same grid.
   * Use for inter-plugin communication.
   *
   * @example
   * ```ts
   * const selection = this.getPlugin(SelectionPlugin);
   * if (selection) {
   *   const selectedRows = selection.getSelectedRows();
   * }
   * ```
   */
  getPlugin(t) {
    return this.grid?.getPlugin(t);
  }
  /**
   * Emit a custom event from the grid.
   */
  emit(t, e) {
    this.grid?.dispatchEvent?.(new CustomEvent(t, { detail: e, bubbles: !0 }));
  }
  /**
   * Emit a cancelable custom event from the grid.
   * @returns `true` if the event was cancelled (preventDefault called), `false` otherwise
   */
  emitCancelable(t, e) {
    const o = new CustomEvent(t, { detail: e, bubbles: !0, cancelable: !0 });
    return this.grid?.dispatchEvent?.(o), o.defaultPrevented;
  }
  /**
   * Request a re-render of the grid.
   */
  requestRender() {
    this.grid?.requestRender?.();
  }
  /**
   * Request a re-render and restore focus styling afterward.
   * Use this when a plugin action (like expand/collapse) triggers a render
   * but needs to maintain keyboard navigation focus.
   */
  requestRenderWithFocus() {
    this.grid?.requestRenderWithFocus?.();
  }
  /**
   * Request a lightweight style update without rebuilding DOM.
   * Use this instead of requestRender() when only CSS classes need updating.
   */
  requestAfterRender() {
    this.grid?.requestAfterRender?.();
  }
  /**
   * Get the current rows from the grid.
   */
  get rows() {
    return this.grid?.rows ?? [];
  }
  /**
   * Get the original unfiltered/unprocessed rows from the grid.
   * Use this when you need all source data regardless of active filters.
   */
  get sourceRows() {
    return this.grid?.sourceRows ?? [];
  }
  /**
   * Get the current columns from the grid.
   */
  get columns() {
    return this.grid?.columns ?? [];
  }
  /**
   * Get only visible columns from the grid (excludes hidden).
   * Use this for rendering that needs to match the grid template.
   */
  get visibleColumns() {
    return this.grid?._visibleColumns ?? [];
  }
  /**
   * Get the grid as an HTMLElement for direct DOM operations.
   * Use sparingly - prefer the typed GridElementRef API when possible.
   *
   * @example
   * ```ts
   * const width = this.gridElement.clientWidth;
   * this.gridElement.classList.add('my-plugin-active');
   * ```
   */
  get gridElement() {
    return this.grid;
  }
  /**
   * Get the disconnect signal for event listener cleanup.
   * This signal is aborted when the grid disconnects from the DOM.
   * Use this when adding event listeners that should be cleaned up automatically.
   *
   * Best for:
   * - Document/window-level listeners added in attach()
   * - Listeners on the grid element itself
   * - Any listener that should persist across renders
   *
   * Not needed for:
   * - Listeners on elements created in afterRender() (removed with element)
   *
   * @example
   * element.addEventListener('click', handler, { signal: this.disconnectSignal });
   * document.addEventListener('keydown', handler, { signal: this.disconnectSignal });
   */
  get disconnectSignal() {
    return this.#t?.signal ?? this.grid?.disconnectSignal;
  }
  /**
   * Get the grid-level icons configuration.
   * Returns merged icons (user config + defaults).
   */
  get gridIcons() {
    const t = this.grid?.gridConfig?.icons ?? {};
    return { ...x, ...t };
  }
  // #region Animation Helpers
  /**
   * Check if animations are enabled at the grid level.
   * Respects gridConfig.animation.mode and the CSS variable set by the grid.
   *
   * Plugins should use this to skip animations when:
   * - Animation mode is 'off' or `false`
   * - User prefers reduced motion and mode is 'reduced-motion' (default)
   *
   * @example
   * ```ts
   * private get animationStyle(): 'slide' | 'fade' | false {
   *   if (!this.isAnimationEnabled) return false;
   *   return this.config.animation ?? 'slide';
   * }
   * ```
   */
  get isAnimationEnabled() {
    const t = this.grid?.effectiveConfig?.animation?.mode ?? "reduced-motion";
    if (t === !1 || t === "off") return !1;
    if (t === !0 || t === "on") return !0;
    const e = this.gridElement;
    return e ? getComputedStyle(e).getPropertyValue("--tbw-animation-enabled").trim() !== "0" : !0;
  }
  /**
   * Get the animation duration in milliseconds from CSS variable.
   * Falls back to 200ms if not set.
   *
   * Plugins can use this for their animation timing to stay consistent
   * with the grid-level animation.duration setting.
   *
   * @example
   * ```ts
   * element.animate(keyframes, { duration: this.animationDuration });
   * ```
   */
  get animationDuration() {
    const t = this.gridElement;
    if (t) {
      const e = getComputedStyle(t).getPropertyValue("--tbw-animation-duration").trim(), o = parseInt(e, 10);
      if (!isNaN(o)) return o;
    }
    return 200;
  }
  // #endregion
  /**
   * Resolve an icon value to string or HTMLElement.
   * Checks plugin config first, then grid-level icons, then defaults.
   *
   * @param iconKey - The icon key in GridIcons (e.g., 'expand', 'collapse')
   * @param pluginOverride - Optional plugin-level override
   * @returns The resolved icon value
   */
  resolveIcon(t, e) {
    return e !== void 0 ? e : this.gridIcons[t];
  }
  /**
   * Set an icon value on an element.
   * Handles both string (text/HTML) and HTMLElement values.
   *
   * @param element - The element to set the icon on
   * @param icon - The icon value (string or HTMLElement)
   */
  setIcon(t, e) {
    typeof e == "string" ? t.innerHTML = e : e instanceof HTMLElement && (t.innerHTML = "", t.appendChild(e.cloneNode(!0)));
  }
  /**
   * Log a warning message.
   */
  warn(t) {
    console.warn(`[tbw-grid:${this.name}] ${t}`);
  }
  // #endregion
}
function E(g) {
  return g.meta?.utility === !0;
}
async function R(g) {
  try {
    return await navigator.clipboard.writeText(g), !0;
  } catch {
    const t = document.createElement("textarea");
    t.value = g, t.style.position = "fixed", t.style.opacity = "0", t.style.pointerEvents = "none", document.body.appendChild(t), t.select();
    const e = document.execCommand("copy");
    return document.body.removeChild(t), e;
  }
}
function y(g, t) {
  const e = t.delimiter ?? "	", o = t.newline ?? `
`, a = g.replace(/\r\n/g, `
`).replace(/\r/g, `
`), s = [];
  let n = [], i = "", r = !1;
  for (let l = 0; l < a.length; l++) {
    const c = a[l];
    c === '"' && !r ? r = !0 : c === '"' && r ? a[l + 1] === '"' ? (i += '"', l++) : r = !1 : c === e && !r ? (n.push(i), i = "") : c === o && !r ? (n.push(i), i = "", (n.length > 1 || n.some((u) => u.trim() !== "")) && s.push(n), n = []) : i += c;
  }
  return n.push(i), (n.length > 1 || n.some((l) => l.trim() !== "")) && s.push(n), s;
}
async function I() {
  try {
    return await navigator.clipboard.readText();
  } catch {
    return "";
  }
}
function P(g, t) {
  const { rows: e, target: o, fields: a } = g;
  if (!o) return;
  const s = t.rows, n = t.effectiveConfig.columns ?? [], i = n.map((u) => u.field), r = /* @__PURE__ */ new Map();
  n.forEach((u) => {
    r.set(u.field, u.editable === !0);
  });
  const l = [...s], c = o.bounds ? o.bounds.endRow : 1 / 0;
  e.forEach((u, m) => {
    const h = o.row + m;
    if (!(h > c)) {
      if (o.bounds) {
        if (h >= l.length)
          return;
      } else for (; h >= l.length; ) {
        const d = {};
        i.forEach((f) => d[f] = ""), l.push(d);
      }
      l[h] = { ...l[h] }, u.forEach((d, f) => {
        const w = a[f];
        w && r.get(w) && (l[h][w] = d);
      });
    }
  }), t.rows = l;
}
class S extends v {
  /**
   * Plugin dependencies - ClipboardPlugin works best with SelectionPlugin.
   *
   * Without SelectionPlugin: copies entire grid, pastes at row 0 col 0.
   * With SelectionPlugin: copies/pastes based on selection.
   */
  /** @internal */
  static dependencies = [
    { name: "selection", required: !1, reason: "Enables copy/paste of selected cells instead of entire grid" }
  ];
  /** @internal */
  name = "clipboard";
  /** @internal */
  get defaultConfig() {
    return {
      includeHeaders: !1,
      delimiter: "	",
      newline: `
`,
      quoteStrings: !1
    };
  }
  // #region Internal State
  /** The last copied text (for reference/debugging) */
  lastCopied = null;
  // #endregion
  // #region Lifecycle
  /** @internal */
  attach(t) {
    super.attach(t), t.addEventListener(
      "paste",
      (e) => this.#o(e),
      { signal: this.disconnectSignal }
    );
  }
  /** @internal */
  detach() {
    this.lastCopied = null;
  }
  // #endregion
  // #region Event Handlers
  /** @internal */
  onKeyDown(t) {
    return (t.ctrlKey || t.metaKey) && t.key === "c" ? (this.#t(t.target), !0) : !1;
  }
  // #endregion
  // #region Private Methods
  /**
   * Handle copy operation.
   *
   * Everything is treated as a range:
   * - With selection: copies the selected range
   * - Row mode: range spanning all columns for selected rows
   * - No selection plugin: entire grid as a range
   * - No selection: try to get focused cell from DOM as 1x1 range
   */
  #t(t) {
    const e = this.#e(), o = e?.getSelection(), a = this.columns.length - 1, s = this.rows.length - 1;
    let n;
    if (o && o.ranges.length > 0) {
      const { mode: r, ranges: l } = o, c = l[l.length - 1];
      r === "row" ? n = {
        startRow: c.from.row,
        startCol: 0,
        endRow: c.to.row,
        endCol: a
      } : n = {
        startRow: c.from.row,
        startCol: c.from.col,
        endRow: c.to.row,
        endCol: c.to.col
      };
    } else if (!e)
      n = { startRow: 0, startCol: 0, endRow: s, endCol: a };
    else {
      const r = this.#r(t);
      if (!r) return;
      n = { startRow: r.row, startCol: r.col, endRow: r.row, endCol: r.col };
    }
    const i = this.#n(n);
    R(i.text).then(() => {
      this.lastCopied = { text: i.text, timestamp: Date.now() }, this.emit("copy", {
        text: i.text,
        rowCount: i.rowCount,
        columnCount: i.columnCount
      });
    });
  }
  /**
   * Handle native paste event (preferred method - works in iframes).
   * Uses synchronous clipboardData from the native paste event.
   *
   * Flow:
   * 1. Parse clipboard text
   * 2. Build target/fields info from selection
   * 3. Emit 'paste' event (for listeners)
   * 4. Call paste handler (if configured) to apply data to grid
   *
   * Selection behavior:
   * - Single cell: paste starts at cell, expands freely
   * - Range/row: paste is clipped to fit within selection bounds
   * - No selection: paste starts at row 0, col 0
   */
  #o(t) {
    const e = t.clipboardData?.getData("text/plain");
    if (!e) return;
    t.preventDefault();
    const o = y(e, this.config), s = this.#e()?.getSelection(), n = s?.ranges?.[0], i = n?.from.row ?? 0, r = n?.from.col ?? 0, c = n && (s?.mode === "range" || s?.mode === "row") && (n.from.row !== n.to.row || n.from.col !== n.to.col) ? { endRow: n.to.row, endCol: n.to.col } : null, u = c?.endCol ?? this.columns.length - 1, m = this.columns[r], h = m ? { row: i, col: r, field: m.field, bounds: c } : null, d = [], f = o[0]?.length ?? 0;
    for (let C = 0; C < f && r + C <= u; C++) {
      const p = this.columns[r + C];
      p && !p.hidden && d.push(p.field);
    }
    const w = { rows: o, text: e, target: h, fields: d };
    this.emit("paste", w), this.#s(w);
  }
  /**
   * Apply the paste handler to update grid data.
   *
   * Uses the configured `pasteHandler`, or the default handler if not specified.
   * Set `pasteHandler: null` in config to disable auto-paste.
   */
  #s(t) {
    if (!this.grid) return;
    const { pasteHandler: e } = this.config;
    if (e === null) return;
    (e ?? P)(t, this.grid);
  }
  /**
   * Get the selection plugin instance if available.
   */
  #e() {
    try {
      const t = this.grid?.getPluginByName("selection");
      if (t)
        return t;
    } catch {
    }
  }
  /**
   * Build text for a rectangular range of cells.
   * Utility columns (like expander columns) are automatically excluded.
   */
  #n(t) {
    const { startRow: e, startCol: o, endRow: a, endCol: s } = t, n = Math.min(e, a), i = Math.max(e, a), r = Math.min(o, s), l = Math.max(o, s), c = this.config.delimiter ?? "	", u = this.config.newline ?? `
`, m = [], h = this.columns.slice(r, l + 1).filter((d) => !E(d));
    if (this.config.includeHeaders) {
      const d = h.map((f) => f.header || f.field);
      m.push(d.join(c));
    }
    for (let d = n; d <= i; d++) {
      const f = this.rows[d];
      if (!f) continue;
      const w = h.map((C) => {
        const p = f[C.field];
        return p == null ? "" : p instanceof Date ? p.toISOString() : String(p);
      });
      m.push(w.join(c));
    }
    return {
      text: m.join(u),
      rowCount: i - n + 1,
      columnCount: l - r + 1
    };
  }
  /**
   * Get focused cell coordinates from DOM.
   * Used as fallback when SelectionPlugin has no selection.
   */
  #r(t) {
    const e = t.closest("[data-field-cache]");
    if (!e) return null;
    const o = e.dataset.fieldCache, a = e.dataset.row;
    if (!o || !a) return null;
    const s = parseInt(a, 10);
    if (isNaN(s)) return null;
    const n = this.columns.findIndex((i) => i.field === o);
    return n === -1 ? null : { row: s, col: n };
  }
  // #endregion
  // #region Public API
  /**
   * Copy currently selected rows to clipboard.
   * @returns The copied text
   */
  async copy() {
    const e = this.#e()?.getSelection(), o = this.columns.length - 1;
    let a = { startRow: 0, startCol: 0, endRow: this.rows.length - 1, endCol: o };
    if (e && e.ranges.length > 0) {
      const n = e.ranges[e.ranges.length - 1];
      e.mode === "row" ? a = { startRow: n.from.row, startCol: 0, endRow: n.to.row, endCol: o } : a = {
        startRow: n.from.row,
        startCol: n.from.col,
        endRow: n.to.row,
        endCol: n.to.col
      };
    }
    const s = this.#n(a);
    return await R(s.text), this.lastCopied = { text: s.text, timestamp: Date.now() }, s.text;
  }
  /**
   * Copy specific rows by index to clipboard.
   * @param indices - Array of row indices to copy
   * @returns The copied text
   */
  async copyRows(t) {
    if (t.length === 0) return "";
    const e = [...t].sort((n, i) => n - i), o = this.columns.length - 1, a = {
      startRow: e[0],
      startCol: 0,
      endRow: e[e.length - 1],
      endCol: o
    }, s = this.#n(a);
    return await R(s.text), this.lastCopied = { text: s.text, timestamp: Date.now() }, s.text;
  }
  /**
   * Read and parse clipboard content.
   * @returns Parsed 2D array of cell values, or null if clipboard is empty
   */
  async paste() {
    const t = await I();
    return t ? y(t, this.config) : null;
  }
  /**
   * Get the last copied text and timestamp.
   * @returns The last copied info or null
   */
  getLastCopied() {
    return this.lastCopied;
  }
  // #endregion
}
export {
  S as ClipboardPlugin,
  P as defaultPasteHandler
};
//# sourceMappingURL=index.js.map
