import { html, css, TemplateResult, CSSResultArray } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import NileElement from '../../internal/nile-element'; /** Smallest grid shown before the pointer starts expanding it. */ const MIN_ROWS = 5; const MIN_COLS = 5; /** * Toolbar item: a table button that opens a hover grid which auto-expands as * the pointer reaches the last row/column (up to `maxRows`×`maxCols`), plus a * "Custom" rows×cols input for larger tables. Picking a cell or confirming the * custom size emits `wysiwyg-command` with `insertTable { rows, cols }`. */ @customElement('nile-wysiwyg-table-picker') export class NileWysiwygTablePicker extends NileElement { @property({ type: Boolean, reflect: true }) disabled = false; /** Hard cap on the number of rows selectable via grid or custom input. */ @property({ type: Number, attribute: 'max-rows', reflect: true }) maxRows = 20; /** Hard cap on the number of columns selectable via grid or custom input. */ @property({ type: Number, attribute: 'max-cols', reflect: true }) maxCols = 20; @state() private open = false; @state() private hoverRows = 0; @state() private hoverCols = 0; @state() private customRows = ''; @state() private customCols = ''; public static get styles(): CSSResultArray { return [ css` :host { font-family: var( --nile-font-family-sans-serif, var(--ng-font-family-body, system-ui, -apple-system, sans-serif) ); position: relative; display: inline-flex; } button.trigger { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0 6px; box-sizing: border-box; border: none; border-radius: var(--nile-radius-radius-lg, var(--ng-radius-md, 8px)); background: transparent; cursor: pointer; color: var(--nile-colors-dark-900, var(--ng-colors-text-primary-900, #101828)); } button.trigger:hover:not(:disabled) { background: var(--nile-colors-neutral-100, #f2f4f7); } button.trigger:disabled { opacity: 0.4; cursor: default; } .panel { position: absolute; top: calc(100% + 4px); left: 0; z-index: 30; padding: 8px; background: var(--nile-wysiwyg-popover-bg, #ffffff); border: 1px solid var(--nile-colors-neutral-400, #d0d5dd); border-radius: 8px; box-shadow: var(--nile-wysiwyg-popover-shadow, 0 4px 12px rgba(16, 24, 40, 0.12)); } .grid { display: grid; gap: 2px; } .cell { width: 14px; height: 14px; border: 1px solid var(--nile-colors-neutral-300, #d0d5dd); border-radius: 2px; box-sizing: border-box; cursor: pointer; } .cell.hot { background: var(--nile-colors-primary-100, var(--ng-componentcolors-utility-blue-100, #dbeafe)); border-color: var(--nile-colors-primary-600, var(--ng-componentcolors-utility-blue-600, #2563eb)); } .label { margin-top: 6px; text-align: center; font-size: 12px; color: var(--nile-colors-neutral-600, #475467); } .custom { display: flex; align-items: center; gap: 6px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--nile-colors-neutral-300, #e4e7ec); } .custom input { width: 44px; height: 26px; padding: 0 6px; border: 1px solid var(--nile-colors-neutral-400, #d0d5dd); border-radius: 6px; font-size: 12px; box-sizing: border-box; text-align: center; color: var(--nile-colors-dark-900, var(--ng-colors-text-primary-900, #101828)); font-family: inherit; /* Hide native number spinners: they overlap the centered text in this narrow field and a stray click on the arrows mutates the value. */ -moz-appearance: textfield; appearance: textfield; } .custom input::-webkit-outer-spin-button, .custom input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .custom input:focus { outline: none; border-color: var(--nile-colors-primary-600, var(--ng-componentcolors-utility-blue-600, #2563eb)); } .custom .times { font-size: 12px; color: var(--nile-colors-neutral-600, #475467); } .custom button.insert { margin-left: auto; height: 26px; padding: 0 12px; border: none; border-radius: 6px; background: var(--nile-colors-primary-600, var(--ng-componentcolors-utility-blue-600, #2563eb)); color: #ffffff; font-size: 12px; font-weight: 600; font-family: inherit; cursor: pointer; } .custom button.insert:hover:not(:disabled) { background: var(--nile-colors-primary-700, #1d4ed8); } .custom button.insert:disabled { opacity: 0.4; cursor: default; } `, ]; } connectedCallback(): void { super.connectedCallback(); document.addEventListener('mousedown', this.handleOutside); } disconnectedCallback(): void { super.disconnectedCallback(); document.removeEventListener('mousedown', this.handleOutside); } private handleOutside = (event: MouseEvent): void => { if (this.open && !event.composedPath().includes(this)) this.open = false; }; private pick(rows: number, cols: number): void { this.open = false; this.customRows = ''; this.customCols = ''; this.emit('wysiwyg-command', { name: 'insertTable', attrs: { rows, cols, withHeaderRow: true }, }); } /** Clamp raw input to 1..max; returns 0 when empty/invalid. */ private clamp(raw: string, max: number): number { const n = parseInt(raw, 10); if (!Number.isFinite(n) || n < 1) return 0; return Math.min(n, max); } private insertCustom(): void { const rows = this.clamp(this.customRows, this.maxRows); const cols = this.clamp(this.customCols, this.maxCols); if (rows >= 1 && cols >= 1) this.pick(rows, cols); } public render(): TemplateResult { // Grow the grid by one row/col once the pointer reaches the edge, capped. const visibleRows = Math.min(Math.max(MIN_ROWS, this.hoverRows + 1), this.maxRows); const visibleCols = Math.min(Math.max(MIN_COLS, this.hoverCols + 1), this.maxCols); const cells: TemplateResult[] = []; for (let r = 1; r <= visibleRows; r += 1) { for (let c = 1; c <= visibleCols; c += 1) { cells.push(html` { this.hoverRows = r; this.hoverCols = c; }} @click=${() => this.pick(r, c)} > `); } } const customValid = this.clamp(this.customRows, this.maxRows) >= 1 && this.clamp(this.customCols, this.maxCols) >= 1; return html` ${this.open ? html`