const R = "data-animating", A = {
  change: "--tbw-row-change-duration",
  insert: "--tbw-row-insert-duration",
  remove: "--tbw-row-remove-duration"
}, T = {
  change: 500,
  insert: 300,
  remove: 200
};
function P(s) {
  const e = s.trim().toLowerCase();
  return e.endsWith("ms") ? parseFloat(e) : e.endsWith("s") ? parseFloat(e) * 1e3 : parseFloat(e);
}
function I(s, e) {
  const i = A[e], t = getComputedStyle(s).getPropertyValue(i);
  if (t) {
    const r = P(t);
    if (!isNaN(r) && r > 0)
      return r;
  }
  return T[e];
}
function D(s, e, i) {
  s.removeAttribute(R), s.offsetWidth, s.setAttribute(R, e);
  const t = I(s, e);
  setTimeout(() => {
    s.removeAttribute(R);
  }, t);
}
const L = '<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>', q = {
  expand: "▶",
  collapse: "▼",
  sortAsc: "▲",
  sortDesc: "▼",
  sortNone: "⇅",
  submenuArrow: "▶",
  dragHandle: "⋮⋮",
  toolPanel: "☰",
  filter: L,
  filterActive: L,
  print: "🖨️"
};
class O {
  /**
   * 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.
   */
  #e;
  /**
   * 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(e = {}) {
    this.userConfig = e;
  }
  /**
   * 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(e) {
    this.#e?.abort(), this.#e = new AbortController(), this.grid = e, 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.#e?.abort(), this.#e = 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(e) {
    return this.grid?.getPlugin(e);
  }
  /**
   * Emit a custom event from the grid.
   */
  emit(e, i) {
    this.grid?.dispatchEvent?.(new CustomEvent(e, { detail: i, bubbles: !0 }));
  }
  /**
   * Emit a cancelable custom event from the grid.
   * @returns `true` if the event was cancelled (preventDefault called), `false` otherwise
   */
  emitCancelable(e, i) {
    const t = new CustomEvent(e, { detail: i, bubbles: !0, cancelable: !0 });
    return this.grid?.dispatchEvent?.(t), t.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.#e?.signal ?? this.grid?.disconnectSignal;
  }
  /**
   * Get the grid-level icons configuration.
   * Returns merged icons (user config + defaults).
   */
  get gridIcons() {
    const e = this.grid?.gridConfig?.icons ?? {};
    return { ...q, ...e };
  }
  // #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 e = this.grid?.effectiveConfig?.animation?.mode ?? "reduced-motion";
    if (e === !1 || e === "off") return !1;
    if (e === !0 || e === "on") return !0;
    const i = this.gridElement;
    return i ? getComputedStyle(i).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 e = this.gridElement;
    if (e) {
      const i = getComputedStyle(e).getPropertyValue("--tbw-animation-duration").trim(), t = parseInt(i, 10);
      if (!isNaN(t)) return t;
    }
    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(e, i) {
    return i !== void 0 ? i : this.gridIcons[e];
  }
  /**
   * 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(e, i) {
    typeof i == "string" ? e.innerHTML = i : i instanceof HTMLElement && (e.innerHTML = "", e.appendChild(i.cloneNode(!0)));
  }
  /**
   * Log a warning message.
   */
  warn(e) {
    console.warn(`[tbw-grid:${this.name}] ${e}`);
  }
  // #endregion
}
const N = "@layer tbw-plugins{tbw-grid{--tbw-editing-bg: var(--tbw-color-selection);--tbw-editing-row-bg: var(--tbw-editing-bg);--tbw-editing-border: var(--tbw-border-input, 1px solid var(--tbw-color-border-strong));--tbw-padding-editing-input: var(--tbw-cell-padding-input, 2px 6px);--tbw-font-size-editor: inherit;--tbw-editing-row-outline-color: var(--tbw-color-accent);--tbw-editing-row-outline-width: 1px}tbw-grid .data-grid-row:has(.editing){background:var(--tbw-editing-row-bg);outline:var(--tbw-editing-row-outline-width) solid var(--tbw-editing-row-outline-color);outline-offset:calc(-1 * var(--tbw-editing-row-outline-width))}tbw-grid .data-grid-row>.cell.editing{overflow:hidden;padding:0;display:flex;min-height:calc(var(--tbw-row-height) + 2px);align-items:center;justify-content:center}tbw-grid .data-grid-row>.cell.editing input:not([type=checkbox]),tbw-grid .data-grid-row>.cell.editing select,tbw-grid .data-grid-row>.cell.editing textarea{width:100%;height:100%;flex:1 1 auto;min-width:0;border:var(--tbw-editing-border);padding:var(--tbw-padding-editing-input);font-size:var(--tbw-font-size-editor)}tbw-grid .tbw-editor-host{display:contents}}";
function M(s) {
  const e = s.options;
  return e ? typeof e == "function" ? e() : e : [];
}
function H(s) {
  return (e) => {
    const i = s.editorParams, t = document.createElement("input");
    t.type = "number", t.value = e.value != null ? String(e.value) : "", i?.min !== void 0 && (t.min = String(i.min)), i?.max !== void 0 && (t.max = String(i.max)), i?.step !== void 0 && (t.step = String(i.step)), i?.placeholder && (t.placeholder = i.placeholder);
    const r = () => e.commit(t.value === "" ? null : Number(t.value));
    return t.addEventListener("blur", r), t.addEventListener("keydown", (n) => {
      n.key === "Enter" && r(), n.key === "Escape" && e.cancel();
    }), t;
  };
}
function x() {
  return (s) => {
    const e = document.createElement("input");
    return e.type = "checkbox", e.checked = !!s.value, e.addEventListener("change", () => s.commit(e.checked)), e;
  };
}
function G(s) {
  return (e) => {
    const i = s.editorParams, t = document.createElement("input");
    return t.type = "date", e.value instanceof Date && (t.valueAsDate = e.value), i?.min && (t.min = i.min), i?.max && (t.max = i.max), i?.placeholder && (t.placeholder = i.placeholder), t.addEventListener("change", () => e.commit(t.valueAsDate)), t.addEventListener("keydown", (r) => {
      r.key === "Escape" && e.cancel();
    }), t;
  };
}
function $(s) {
  return (e) => {
    const i = s.editorParams, t = document.createElement("select");
    if (s.multi && (t.multiple = !0), i?.includeEmpty) {
      const o = document.createElement("option");
      o.value = "", o.textContent = i.emptyLabel ?? "", t.appendChild(o);
    }
    M(s).forEach((o) => {
      const a = document.createElement("option");
      a.value = String(o.value), a.textContent = o.label, (s.multi && Array.isArray(e.value) && e.value.includes(o.value) || !s.multi && e.value === o.value) && (a.selected = !0), t.appendChild(a);
    });
    const n = () => {
      if (s.multi) {
        const o = Array.from(t.selectedOptions).map((a) => a.value);
        e.commit(o);
      } else
        e.commit(t.value);
    };
    return t.addEventListener("change", n), t.addEventListener("blur", n), t.addEventListener("keydown", (o) => {
      o.key === "Escape" && e.cancel();
    }), t;
  };
}
function F(s) {
  return (e) => {
    const i = s.editorParams, t = document.createElement("input");
    return t.type = "text", t.value = e.value != null ? String(e.value) : "", i?.maxLength !== void 0 && (t.maxLength = i.maxLength), i?.pattern && (t.pattern = i.pattern), i?.placeholder && (t.placeholder = i.placeholder), t.addEventListener("blur", () => e.commit(t.value)), t.addEventListener("keydown", (r) => {
      r.key === "Enter" && e.commit(t.value), r.key === "Escape" && e.cancel();
    }), t;
  };
}
function z(s) {
  switch (s.type) {
    case "number":
      return H(s);
    case "boolean":
      return x();
    case "date":
      return G(s);
    case "select":
      return $(s);
    default:
      return F(s);
  }
}
const _ = 'input,select,textarea,[contenteditable="true"],[contenteditable=""],[tabindex]:not([tabindex="-1"])';
function B(s, e) {
  if (e.editor) return e.editor;
  if (e.__editorTemplate) return "template";
  if (!e.type) return;
  const t = s.effectiveConfig?.typeDefaults;
  if (t?.[e.type]?.editor)
    return t[e.type].editor;
  const r = s.__frameworkAdapter;
  if (r?.getTypeDefault) {
    const n = r.getTypeDefault(e.type);
    if (n?.editor)
      return n.editor;
  }
}
function v(s) {
  return !(typeof s != "string" || s === "__proto__" || s === "constructor" || s === "prototype");
}
function U(s) {
  const e = (s.__editingCellCount ?? 0) + 1;
  s.__editingCellCount = e, s.setAttribute("data-has-editing", "");
}
function j(s) {
  s.__editingCellCount = 0, s.removeAttribute("data-has-editing");
}
function E(s, e) {
  return s instanceof HTMLInputElement ? s.type === "checkbox" ? s.checked : s.type === "number" ? s.value === "" ? null : Number(s.value) : s.type === "date" ? s.valueAsDate : s.value : e?.type === "number" && s.value !== "" ? Number(s.value) : s.value;
}
function k(s) {
}
function V(s, e, i) {
  const t = s.querySelector("input,textarea,select");
  t && (t.addEventListener("blur", () => {
    i(E(t, e));
  }), t instanceof HTMLInputElement && t.type === "checkbox" ? t.addEventListener("change", () => i(t.checked)) : t instanceof HTMLSelectElement && t.addEventListener("change", () => i(E(t, e))));
}
class W extends O {
  /**
   * Plugin manifest - declares owned properties for configuration validation.
   * @internal
   */
  static manifest = {
    ownedProperties: [
      {
        property: "editable",
        level: "column",
        description: 'the "editable" column property',
        isUsed: (e) => e === !0
      },
      {
        property: "editor",
        level: "column",
        description: 'the "editor" column property'
      },
      {
        property: "editorParams",
        level: "column",
        description: 'the "editorParams" column property'
      }
    ]
  };
  /** @internal */
  name = "editing";
  /** @internal */
  styles = N;
  /** @internal */
  get defaultConfig() {
    return {
      editOn: "click"
    };
  }
  // #region Editing State (fully owned by plugin)
  /** Currently active edit row index, or -1 if not editing */
  #e = -1;
  /** Currently active edit column index, or -1 if not editing */
  #s = -1;
  /** Snapshots of row data before editing started */
  #r = /* @__PURE__ */ new Map();
  /** Set of row IDs that have been modified (ID-based for stability) */
  #t = /* @__PURE__ */ new Set();
  /** Set of cells currently in edit mode: "rowIndex:colIndex" */
  #n = /* @__PURE__ */ new Set();
  /** Flag to restore focus after next render (used when exiting edit mode) */
  #o = !1;
  // #endregion
  // #region Lifecycle
  /** @internal */
  attach(e) {
    super.attach(e);
    const i = this.disconnectSignal, t = e;
    t._activeEditRows = -1, t._rowEditSnapshots = /* @__PURE__ */ new Map(), Object.defineProperty(e, "changedRows", {
      get: () => this.changedRows,
      configurable: !0
    }), Object.defineProperty(e, "changedRowIds", {
      get: () => this.changedRowIds,
      configurable: !0
    }), e.resetChangedRows = (r) => this.resetChangedRows(r), e.beginBulkEdit = (r, n) => {
      n && this.beginCellEdit(r, n);
    }, document.addEventListener(
      "keydown",
      (r) => {
        r.key === "Escape" && this.#e !== -1 && this.#i(this.#e, !0);
      },
      { capture: !0, signal: i }
    ), document.addEventListener(
      "mousedown",
      (r) => {
        if (this.#e === -1) return;
        const n = t.findRenderedRowElement?.(this.#e);
        !n || (r.composedPath && r.composedPath() || []).includes(n) || queueMicrotask(() => {
          this.#e !== -1 && this.#i(this.#e, !1);
        });
      },
      { signal: i }
    );
  }
  /** @internal */
  detach() {
    this.#e = -1, this.#s = -1, this.#r.clear(), this.#t.clear(), this.#n.clear(), super.detach();
  }
  // #endregion
  // #region Event Handlers (event distribution)
  /**
   * Handle cell clicks - start editing if configured for click mode.
   * Both click and dblclick events come through this handler.
   * Starts row-based editing (all editable cells in the row get editors).
   * @internal
   */
  onCellClick(e) {
    const i = this.grid, t = this.config.editOn ?? i.effectiveConfig?.editOn;
    if (t === !1 || t === "manual" || t !== "click" && t !== "dblclick") return !1;
    const r = e.originalEvent.type === "dblclick";
    if (t === "click" && r || t === "dblclick" && !r) return !1;
    const { rowIndex: n } = e;
    return i._columns?.some((a) => a.editable) ? (e.originalEvent.stopPropagation(), this.beginBulkEdit(n), !0) : !1;
  }
  /**
   * Handle keyboard events for edit lifecycle.
   * @internal
   */
  onKeyDown(e) {
    const i = this.grid;
    if (e.key === "Escape" && this.#e !== -1)
      return this.#i(this.#e, !0), !0;
    if (e.key === " " || e.key === "Spacebar") {
      const t = i._focusRow, r = i._focusCol;
      if (t >= 0 && r >= 0) {
        const n = i._visibleColumns[r], o = i._rows[t];
        if (n?.editable && n.type === "boolean" && o) {
          const a = n.field;
          if (v(a)) {
            const c = !o[a];
            return this.#l(t, n, c, o), e.preventDefault(), this.requestRender(), !0;
          }
        }
      }
      return !1;
    }
    if (e.key === "Enter" && !e.shiftKey) {
      if (this.#e !== -1)
        return !1;
      const t = this.config.editOn ?? i.effectiveConfig?.editOn;
      if (t === !1 || t === "manual") return !1;
      const r = i._focusRow, n = i._focusCol;
      if (r >= 0 && i._columns?.some((a) => a.editable)) {
        const a = i._visibleColumns[n], l = i._rows[r], c = a?.field ?? "", f = c && l ? l[c] : void 0, g = this.gridElement.querySelector(`[data-row="${r}"][data-col="${n}"]`), d = new CustomEvent("cell-activate", {
          cancelable: !0,
          bubbles: !0,
          detail: {
            rowIndex: r,
            colIndex: n,
            field: c,
            value: f,
            row: l,
            cellEl: g,
            trigger: "keyboard",
            originalEvent: e
          }
        });
        this.gridElement.dispatchEvent(d);
        const h = new CustomEvent("activate-cell", {
          cancelable: !0,
          bubbles: !0,
          detail: { row: r, col: n }
        });
        return this.gridElement.dispatchEvent(h), d.defaultPrevented || h.defaultPrevented ? (e.preventDefault(), !0) : (this.beginBulkEdit(r), !0);
      }
      return !1;
    }
    return !1;
  }
  // #endregion
  // #region Render Hooks
  /**
   * Process columns to merge type-level editorParams with column-level.
   * Column-level params take precedence.
   * @internal
   */
  processColumns(e) {
    const i = this.grid, t = i.effectiveConfig?.typeDefaults, r = i.__frameworkAdapter;
    return !t && !r?.getTypeDefault ? e : e.map((n) => {
      if (!n.type) return n;
      let o;
      if (t?.[n.type]?.editorParams && (o = t[n.type].editorParams), !o && r?.getTypeDefault) {
        const a = r.getTypeDefault(n.type);
        a?.editorParams && (o = a.editorParams);
      }
      return o ? {
        ...n,
        editorParams: { ...o, ...n.editorParams }
      } : n;
    });
  }
  /**
   * After render, reapply editors to cells in edit mode.
   * This handles virtualization - when a row scrolls back into view,
   * we need to re-inject the editor.
   * @internal
   */
  afterRender() {
    const e = this.grid;
    if (this.#o && (this.#o = !1, this.#u(e)), this.#n.size !== 0)
      for (const i of this.#n) {
        const [t, r] = i.split(":"), n = parseInt(t, 10), o = parseInt(r, 10), a = e.findRenderedRowElement?.(n);
        if (!a) continue;
        const l = a.querySelector(`.cell[data-col="${o}"]`);
        if (!l || l.classList.contains("editing")) continue;
        const c = e._rows[n], f = e._visibleColumns[o];
        c && f && this.#d(c, n, f, o, l, !0);
      }
  }
  /**
   * On scroll render, reapply editors to recycled cells.
   * @internal
   */
  onScrollRender() {
    this.afterRender();
  }
  // #endregion
  // #region Public API
  /**
   * Get all rows that have been modified.
   * Uses ID-based lookup for stability when rows are reordered.
   */
  get changedRows() {
    const e = [];
    for (const i of this.#t) {
      const t = this.grid.getRow(i);
      t && e.push(t);
    }
    return e;
  }
  /**
   * Get IDs of all modified rows.
   */
  get changedRowIds() {
    return Array.from(this.#t);
  }
  /**
   * Get the currently active edit row index, or -1 if not editing.
   */
  get activeEditRow() {
    return this.#e;
  }
  /**
   * Get the currently active edit column index, or -1 if not editing.
   */
  get activeEditCol() {
    return this.#s;
  }
  /**
   * Check if a specific row is currently being edited.
   */
  isRowEditing(e) {
    return this.#e === e;
  }
  /**
   * Check if a specific cell is currently being edited.
   */
  isCellEditing(e, i) {
    return this.#n.has(`${e}:${i}`);
  }
  /**
   * Check if a specific row has been modified.
   * @param rowIndex - Row index to check (will be converted to ID internally)
   */
  isRowChanged(e) {
    const i = this.grid, t = i._rows[e];
    if (!t) return !1;
    try {
      const r = i.getRowId?.(t);
      return r ? this.#t.has(r) : !1;
    } catch {
      return !1;
    }
  }
  /**
   * Check if a row with the given ID has been modified.
   * @param rowId - Row ID to check
   */
  isRowChangedById(e) {
    return this.#t.has(e);
  }
  /**
   * Reset all change tracking.
   * @param silent - If true, suppresses the `changed-rows-reset` event
   * @fires changed-rows-reset - Emitted when tracking is reset (unless silent)
   */
  resetChangedRows(e) {
    const i = this.changedRows, t = this.changedRowIds;
    this.#t.clear(), this.#a(), e || this.emit("changed-rows-reset", { rows: i, ids: t }), this.grid._rowPool?.forEach((n) => n.classList.remove("changed"));
  }
  /**
   * Programmatically begin editing a cell.
   * @param rowIndex - Index of the row to edit
   * @param field - Field name of the column to edit
   * @fires cell-commit - Emitted when the cell value is committed (on blur or Enter)
   */
  beginCellEdit(e, i) {
    const t = this.grid, r = t._visibleColumns.findIndex((l) => l.field === i);
    if (r === -1 || !t._visibleColumns[r]?.editable) return;
    const a = t.findRenderedRowElement?.(e)?.querySelector(`.cell[data-col="${r}"]`);
    a && this.#f(e, r, a);
  }
  /**
   * Programmatically begin editing all editable cells in a row.
   * @param rowIndex - Index of the row to edit
   * @fires cell-commit - Emitted for each cell value that is committed
   * @fires row-commit - Emitted when focus leaves the row
   */
  beginBulkEdit(e) {
    const i = this.grid;
    if ((this.config.editOn ?? i.effectiveConfig?.editOn) === !1 || !i._columns?.some((a) => a.editable)) return;
    const n = i.findRenderedRowElement?.(e);
    if (!n) return;
    const o = i._rows[e];
    this.#c(e, o), Array.from(n.children).forEach((a, l) => {
      const c = i._visibleColumns[l];
      if (c?.editable) {
        const f = a;
        f.classList.contains("editing") || this.#d(o, e, c, l, f, !0);
      }
    }), setTimeout(() => {
      let a = n.querySelector(`.cell[data-col="${i._focusCol}"]`);
      if (a?.classList.contains("editing") || (a = n.querySelector(".cell.editing")), a?.classList.contains("editing")) {
        const l = a.querySelector(_);
        try {
          l?.focus({ preventScroll: !0 });
        } catch {
        }
      }
    }, 0);
  }
  /**
   * Commit the currently active row edit.
   * @fires row-commit - Emitted after the row edit is committed
   */
  commitActiveRowEdit() {
    this.#e !== -1 && this.#i(this.#e, !1);
  }
  /**
   * Cancel the currently active row edit.
   */
  cancelActiveRowEdit() {
    this.#e !== -1 && this.#i(this.#e, !0);
  }
  // #endregion
  // #region Internal Methods
  /**
   * Begin editing a single cell.
   */
  #f(e, i, t) {
    const r = this.grid, n = r._rows[e], o = r._visibleColumns[i];
    !n || !o?.editable || t.classList.contains("editing") || (this.#e !== e && this.#c(e, n), this.#s = i, this.#d(n, e, o, i, t, !1));
  }
  /**
   * Sync the internal grid state with the plugin's editing state.
   */
  #a() {
    const e = this.grid;
    e._activeEditRows = this.#e, e._rowEditSnapshots = this.#r;
  }
  /**
   * Snapshot original row data and mark as editing.
   */
  #c(e, i) {
    this.#e !== e && (this.#r.set(e, { ...i }), this.#e = e, this.#a());
  }
  /**
   * Exit editing for a row.
   */
  #i(e, i) {
    if (this.#e !== e) return;
    const t = this.grid, r = this.#r.get(e), n = t._rows[e], o = t.findRenderedRowElement?.(e);
    let a;
    if (n)
      try {
        a = t.getRowId?.(n);
      } catch {
      }
    if (!i && o && n && o.querySelectorAll(".cell.editing").forEach((c) => {
      const f = Number(c.getAttribute("data-col"));
      if (isNaN(f)) return;
      const g = t._visibleColumns[f];
      if (!g) return;
      const d = c.querySelector("input,textarea,select");
      if (d) {
        const h = E(d, g);
        n[g.field] !== h && this.#l(e, g, h, n);
      }
    }), i && r && n)
      Object.keys(r).forEach((l) => {
        n[l] = r[l];
      }), a && this.#t.delete(a);
    else if (!i && n) {
      const l = a ? this.#t.has(a) : !1;
      this.emit("row-commit", {
        rowIndex: e,
        rowId: a ?? "",
        row: n,
        changed: l,
        changedRows: this.changedRows,
        changedRowIds: this.changedRowIds
      }), l && this.isAnimationEnabled && t.animateRow?.(e, "change");
    }
    this.#r.delete(e), this.#e = -1, this.#s = -1, this.#a();
    for (const l of this.#n)
      l.startsWith(`${e}:`) && this.#n.delete(l);
    o && (o.querySelectorAll(".cell.editing").forEach((l) => {
      l.classList.remove("editing"), j(l.parentElement);
    }), this.requestRender()), this.#o = !0, o || (this.#u(t), this.#o = !1);
  }
  /**
   * Commit a single cell value change.
   * Uses ID-based change tracking for stability when rows are reordered.
   */
  #l(e, i, t, r) {
    const n = i.field;
    if (!v(n)) return;
    const o = r[n];
    if (o === t) return;
    const a = this.grid;
    let l;
    try {
      l = this.grid.getRowId(r);
    } catch {
    }
    const c = l ? !this.#t.has(l) : !0, f = l ? (h) => this.grid.updateRow(l, h, "cascade") : k;
    if (this.emitCancelable("cell-commit", {
      row: r,
      rowId: l ?? "",
      field: n,
      oldValue: o,
      value: t,
      rowIndex: e,
      changedRows: this.changedRows,
      changedRowIds: this.changedRowIds,
      firstTimeForRow: c,
      updateRow: f
    })) return;
    r[n] = t, l && this.#t.add(l), this.#a();
    const d = a.findRenderedRowElement?.(e);
    d && (d.classList.add("changed"), D(d, "change"));
  }
  /**
   * Inject an editor into a cell.
   */
  #d(e, i, t, r, n, o) {
    if (!t.editable || n.classList.contains("editing")) return;
    let a;
    try {
      a = this.grid.getRowId(e);
    } catch {
    }
    const l = a ? (u) => this.grid.updateRow(a, u, "cascade") : k, c = v(t.field) ? e[t.field] : void 0;
    n.classList.add("editing"), this.#n.add(`${i}:${r}`);
    const f = n.parentElement;
    f && U(f);
    let g = !1;
    const d = (u) => {
      g || this.#e === -1 || this.#l(i, t, u, e);
    }, h = () => {
      g = !0, v(t.field) && (e[t.field] = c);
    }, p = document.createElement("div");
    p.className = "tbw-editor-host", n.innerHTML = "", n.appendChild(p), p.addEventListener("keydown", (u) => {
      u.key === "Enter" && (u.stopPropagation(), u.preventDefault(), g = !0, this.#i(i, !1)), u.key === "Escape" && (u.stopPropagation(), u.preventDefault(), h(), this.#i(i, !0));
    });
    const m = t, y = m.__editorTemplate, b = B(this.grid, m) ?? z(t), C = c;
    if (b === "template" && y)
      this.#h(p, m, e, c, d, h, o, i);
    else if (typeof b == "string") {
      const u = document.createElement(b);
      u.value = C, u.addEventListener("change", () => d(u.value)), p.appendChild(u), o || queueMicrotask(() => {
        p.querySelector(_)?.focus({ preventScroll: !0 });
      });
    } else if (typeof b == "function") {
      const u = {
        row: e,
        rowId: a ?? "",
        value: C,
        field: t.field,
        column: t,
        commit: d,
        cancel: h,
        updateRow: l
      }, w = b(u);
      typeof w == "string" ? (p.innerHTML = w, V(p, t, d)) : w instanceof Node && p.appendChild(w), o || queueMicrotask(() => {
        p.querySelector(_)?.focus({ preventScroll: !0 });
      });
    } else if (b && typeof b == "object") {
      const u = document.createElement("div");
      u.setAttribute("data-external-editor", ""), u.setAttribute("data-field", t.field), p.appendChild(u);
      const w = {
        row: e,
        rowId: a ?? "",
        value: C,
        field: t.field,
        column: t,
        commit: d,
        cancel: h,
        updateRow: l
      };
      if (b.mount)
        try {
          b.mount({ placeholder: u, context: w, spec: b });
        } catch (S) {
          console.warn(`[tbw-grid] External editor mount error for column '${t.field}':`, S);
        }
      else
        this.grid.dispatchEvent(
          new CustomEvent("mount-external-editor", { detail: { placeholder: u, spec: b, context: w } })
        );
    }
  }
  /**
   * Render a template-based editor.
   */
  #h(e, i, t, r, n, o, a, l) {
    const c = i.__editorTemplate;
    if (!c) return;
    const f = c.cloneNode(!0), g = i.__compiledEditor;
    g ? f.innerHTML = g({
      row: t,
      value: r,
      field: i.field,
      column: i,
      commit: n,
      cancel: o
    }) : f.querySelectorAll("*").forEach((h) => {
      h.childNodes.length === 1 && h.firstChild?.nodeType === Node.TEXT_NODE && (h.textContent = h.textContent?.replace(/{{\s*value\s*}}/g, r == null ? "" : String(r)).replace(/{{\s*row\.([a-zA-Z0-9_]+)\s*}}/g, (p, m) => {
        if (!v(m)) return "";
        const y = t[m];
        return y == null ? "" : String(y);
      }) || "");
    });
    const d = f.querySelector(
      "input,textarea,select"
    );
    if (d) {
      d instanceof HTMLInputElement && d.type === "checkbox" ? d.checked = !!r : d.value = String(r ?? "");
      let h = !1;
      d.addEventListener("blur", () => {
        h || n(E(d, i));
      }), d.addEventListener("keydown", (p) => {
        const m = p;
        m.key === "Enter" && (m.stopPropagation(), m.preventDefault(), h = !0, n(E(d, i)), this.#i(l, !1)), m.key === "Escape" && (m.stopPropagation(), m.preventDefault(), o(), this.#i(l, !0));
      }), d instanceof HTMLInputElement && d.type === "checkbox" && d.addEventListener("change", () => n(d.checked)), a || setTimeout(() => d.focus({ preventScroll: !0 }), 0);
    }
    e.appendChild(f);
  }
  /**
   * Restore focus to cell after exiting edit mode.
   */
  #u(e) {
    queueMicrotask(() => {
      try {
        const i = e._focusRow, t = e._focusCol, r = e.findRenderedRowElement?.(i);
        if (r) {
          Array.from(e._bodyEl.querySelectorAll(".cell-focus")).forEach(
            (o) => o.classList.remove("cell-focus")
          );
          const n = r.querySelector(`.cell[data-row="${i}"][data-col="${t}"]`);
          n && (n.classList.add("cell-focus"), n.setAttribute("aria-selected", "true"), n.hasAttribute("tabindex") || n.setAttribute("tabindex", "-1"), n.focus({ preventScroll: !0 }));
        }
      } catch {
      }
    });
  }
  // #endregion
}
export {
  W as EditingPlugin,
  z as defaultEditorFor
};
//# sourceMappingURL=index.js.map
