602 lines
22 KiB
Go
602 lines
22 KiB
Go
// Port of web/kit/AutoTable.tsx.
|
||
//
|
||
// NOTE: AutoTable.tsx is ~4200 LOC and heavily coupled to the browser. This is a
|
||
// faithful, COMPILING *core* — the column model, the header/body/pagination
|
||
// render shell, and the Tailwind styling — not a behavioral clone. The following
|
||
// substantial features of the TSX are INTENTIONALLY OUT OF SCOPE here because
|
||
// they depend on DOM measurement, refs, timers, portals, or floating-ui, none of
|
||
// which exist in the neutral vdom runtime:
|
||
//
|
||
// - Virtual scrolling / windowed row rendering (rows are rendered eagerly).
|
||
// - Column drag-to-reorder and drag-to-resize (getBoundingClientRect, refs,
|
||
// mousemove tracking) — replaced by a static per-column WidthClass.
|
||
// - Computed/pinned table sizing (pinnedTableWidth measurement) — replaced by
|
||
// table-fixed + static WidthClass.
|
||
// - Runtime-editable calculated columns and footer summary rows, the whole
|
||
// Excel-style formula engine (tokenize/compile/highlight), and their popover
|
||
// editors (Popover/Menu/floating-ui).
|
||
// - CSV and PDF export (pdf-lib / pdfjs-dist), and the export/customize popover.
|
||
// - Remote data fetching (authFetch), remoteFiltering, refreshSignal.
|
||
// - Search/filter toolbar, quick-date presets, column-visibility toggles, the
|
||
// search-aside card, and localStorage persistence of order/width/columns.
|
||
// - Accordion expand/collapse rows and row-highlight auto-paging.
|
||
// - Client-side sorting/filtering/pagination logic (compareRowsGeneric,
|
||
// processDataLocally): sort state and pagination are surfaced here as plain
|
||
// value props + callbacks; the caller owns the actual sort/page computation.
|
||
//
|
||
// What IS ported: the AutoTableColumn model (header text, alignment, width class,
|
||
// sortable + a cell render func), the AutoTable render shell (sticky header,
|
||
// body rows, loading skeletons, error/empty states, display-only pagination),
|
||
// the verbatim Tailwind class constants/maps, and sort/pagination as props +
|
||
// callbacks. Reactive accessors collapse to plain values per the port guide.
|
||
package webui
|
||
|
||
import (
|
||
"strconv"
|
||
|
||
"kjol/vdom"
|
||
)
|
||
|
||
// ColumnPosition is a cell/header alignment (mirrors the TSX 0|1|2 union).
|
||
type ColumnPosition int
|
||
|
||
const (
|
||
COL_POS_LEFT ColumnPosition = 0
|
||
COL_POS_RIGHT ColumnPosition = 1
|
||
COL_POS_CENTER ColumnPosition = 2
|
||
)
|
||
|
||
// AutoTableHeaderColor selects the header/body color scheme.
|
||
type AutoTableHeaderColor int
|
||
|
||
const (
|
||
AUTOTABLE_HEADER_COLOR_DEFAULT AutoTableHeaderColor = 0
|
||
AUTOTABLE_HEADER_COLOR_BLUE AutoTableHeaderColor = 1
|
||
AUTOTABLE_HEADER_COLOR_GREEN AutoTableHeaderColor = 2
|
||
AUTOTABLE_HEADER_COLOR_GRAY AutoTableHeaderColor = 3
|
||
AUTOTABLE_HEADER_COLOR_DARK_BLUE AutoTableHeaderColor = 4
|
||
)
|
||
|
||
// AutoTableSize selects header/body/pagination density.
|
||
type AutoTableSize int
|
||
|
||
const (
|
||
AUTOTABLE_SIZE_DEFAULT AutoTableSize = 0
|
||
AUTOTABLE_SIZE_COMPACT AutoTableSize = 1
|
||
AUTOTABLE_SIZE_SUPERCOMPACT AutoTableSize = 2
|
||
)
|
||
|
||
// -- Tailwind class maps (copied verbatim from AutoTable.tsx) ---------------
|
||
|
||
// HEADER_COLOR_CLS is the background + text color per header color.
|
||
var HEADER_COLOR_CLS = map[AutoTableHeaderColor]string{
|
||
AUTOTABLE_HEADER_COLOR_DEFAULT: "bg-neutral-50",
|
||
AUTOTABLE_HEADER_COLOR_BLUE: "bg-sky-700 text-white",
|
||
AUTOTABLE_HEADER_COLOR_GREEN: "bg-green-700 text-white",
|
||
AUTOTABLE_HEADER_COLOR_GRAY: "bg-neutral-600 text-white",
|
||
AUTOTABLE_HEADER_COLOR_DARK_BLUE: "bg-sky-900 text-white",
|
||
}
|
||
|
||
// atHeaderSortHoverCls is the sortable-hover override per color.
|
||
var atHeaderSortHoverCls = map[AutoTableHeaderColor]string{
|
||
AUTOTABLE_HEADER_COLOR_DEFAULT: "hover:bg-neutral-300",
|
||
AUTOTABLE_HEADER_COLOR_BLUE: "hover:bg-sky-500",
|
||
AUTOTABLE_HEADER_COLOR_GREEN: "hover:bg-green-800",
|
||
AUTOTABLE_HEADER_COLOR_GRAY: "hover:bg-neutral-500",
|
||
AUTOTABLE_HEADER_COLOR_DARK_BLUE: "hover:bg-sky-800",
|
||
}
|
||
|
||
// HEADER_TEXT_CLS is the header text weight/case per color.
|
||
var HEADER_TEXT_CLS = map[AutoTableHeaderColor]string{
|
||
AUTOTABLE_HEADER_COLOR_DEFAULT: "font-bold uppercase tracking-wider",
|
||
AUTOTABLE_HEADER_COLOR_BLUE: "font-semibold",
|
||
AUTOTABLE_HEADER_COLOR_GREEN: "font-semibold",
|
||
AUTOTABLE_HEADER_COLOR_GRAY: "font-semibold",
|
||
AUTOTABLE_HEADER_COLOR_DARK_BLUE: "font-semibold",
|
||
}
|
||
|
||
// atHeaderSortIconCls is the sort-icon color (matches header text) per color.
|
||
var atHeaderSortIconCls = map[AutoTableHeaderColor]string{
|
||
AUTOTABLE_HEADER_COLOR_DEFAULT: "text-black",
|
||
AUTOTABLE_HEADER_COLOR_BLUE: "text-white",
|
||
AUTOTABLE_HEADER_COLOR_GREEN: "text-white",
|
||
AUTOTABLE_HEADER_COLOR_GRAY: "text-white",
|
||
AUTOTABLE_HEADER_COLOR_DARK_BLUE: "text-white",
|
||
}
|
||
|
||
// HEADER_PADDING_CLS is the th padding per table size.
|
||
var HEADER_PADDING_CLS = map[AutoTableSize]string{
|
||
AUTOTABLE_SIZE_DEFAULT: "p-4 text-sm",
|
||
AUTOTABLE_SIZE_COMPACT: "py-1.5 px-2 text-sm",
|
||
AUTOTABLE_SIZE_SUPERCOMPACT: "py-1 px-2 text-xs",
|
||
}
|
||
|
||
// BODY_PADDING_CLS is the body cell padding per table size (applied via [&_td]:).
|
||
var BODY_PADDING_CLS = map[AutoTableSize]string{
|
||
AUTOTABLE_SIZE_DEFAULT: "text-sm [&_td]:p-4",
|
||
AUTOTABLE_SIZE_COMPACT: "text-sm [&_td]:py-1 [&_td]:px-2",
|
||
AUTOTABLE_SIZE_SUPERCOMPACT: "text-xs [&_td]:py-0.5 [&_td]:px-2",
|
||
}
|
||
|
||
// atPaginationPaddingCls is the pagination bar padding per table size.
|
||
var atPaginationPaddingCls = map[AutoTableSize]string{
|
||
AUTOTABLE_SIZE_DEFAULT: "py-3 px-4",
|
||
AUTOTABLE_SIZE_COMPACT: "py-1 px-4",
|
||
AUTOTABLE_SIZE_SUPERCOMPACT: "py-1 px-4",
|
||
}
|
||
|
||
// atRowHoverCls is the body-row hover background per color (when hover is on).
|
||
var atRowHoverCls = map[AutoTableHeaderColor]string{
|
||
AUTOTABLE_HEADER_COLOR_DEFAULT: "hover:bg-neutral-200",
|
||
AUTOTABLE_HEADER_COLOR_BLUE: "hover:bg-sky-100",
|
||
AUTOTABLE_HEADER_COLOR_GREEN: "hover:bg-green-100",
|
||
AUTOTABLE_HEADER_COLOR_GRAY: "hover:bg-neutral-200",
|
||
AUTOTABLE_HEADER_COLOR_DARK_BLUE: "hover:bg-sky-100",
|
||
}
|
||
|
||
// POS_CLS is the text alignment per ColumnPosition.
|
||
var POS_CLS = map[ColumnPosition]string{
|
||
COL_POS_LEFT: "text-left",
|
||
COL_POS_RIGHT: "text-right",
|
||
COL_POS_CENTER: "text-center",
|
||
}
|
||
|
||
// HEADER_INNER_POS is the header inner flex direction per position.
|
||
var HEADER_INNER_POS = map[ColumnPosition]string{
|
||
COL_POS_LEFT: "",
|
||
COL_POS_RIGHT: "flex-row-reverse",
|
||
COL_POS_CENTER: "justify-center",
|
||
}
|
||
|
||
// -- Class string constants for parts that don't vary by config ------------
|
||
const (
|
||
TBL_CONTAINER = "relative flex flex-col w-full h-full bg-white rounded-default overflow-hidden"
|
||
TBL_WRAPPER = "overflow-x-auto w-full"
|
||
TBL_BASE = "min-w-full"
|
||
|
||
HEADER_CONTENT = "transition-transform duration-150 ease-in-out"
|
||
HEADER_INNER_BASE = "flex justify-between gap-2 items-center"
|
||
|
||
atSortIconWrap = "leading-none shrink-0 opacity-50"
|
||
|
||
atSkeleton = "h-4 bg-neutral-200 rounded-default animate-pulse"
|
||
atErrorCell = "text-center text-red-600"
|
||
atEmptyCell = "text-center text-neutral-500"
|
||
|
||
atPaginationBase = "flex justify-between items-center border-t border-neutral-300"
|
||
atPaginationInfo = "hidden sm:flex items-center text-sm text-neutral-500"
|
||
atPaginationControls = "flex items-center"
|
||
atPaginationLabel = "hidden sm:block text-sm text-neutral-500 mr-2"
|
||
atPaginationPage = "text-sm text-neutral-500 px-3"
|
||
atPaginationBtn = "p-1 min-h-9 text-sm font-normal leading-none bg-transparent border-0 cursor-pointer hover:bg-neutral-100 disabled:text-neutral-300 disabled:cursor-not-allowed disabled:hover:bg-transparent"
|
||
|
||
// thead sticky classes are the STATIC replacement for the TSX's JS-driven
|
||
// header pinning (transform tracking on scroll). See file-level NOTE.
|
||
atTheadCls = "sticky top-0 z-10 [&_th]:border-b [&_th]:border-neutral-300"
|
||
)
|
||
|
||
// AutoTableColumn describes one column: its header text, alignment, an optional
|
||
// static Tailwind width class (the static replacement for the TSX's measured/
|
||
// resizable widths), whether it is sortable (and under which identifier), and a
|
||
// cell render func returning the full <td> for a given row. If Cell is nil an
|
||
// empty aligned <td> is rendered.
|
||
//
|
||
// NOTE: The TSX AutoTableColumn also carries csv/csvValue, sortType/sortValue,
|
||
// toggleable/hiddenByDefault, and a `calculated` spec — all tied to features that
|
||
// are out of scope here (export, local sort, toggles, calculated columns).
|
||
type AutoTableColumn struct {
|
||
DisplayName string
|
||
DisplayPosition ColumnPosition
|
||
WidthClass string // static Tailwind width, e.g. "w-32" (replaces measured sizing)
|
||
HeaderClasses string
|
||
Sortable bool
|
||
SortIdentifier string
|
||
Cell func(row any) *vdom.VNode
|
||
}
|
||
|
||
// AutoTableOrderBy is the active sort (mirrors the TSX interface).
|
||
type AutoTableOrderBy struct {
|
||
Identifier string
|
||
Descending bool
|
||
}
|
||
|
||
// AutoTablePagination is the display-only pagination state (mirrors the TSX
|
||
// interface; only the display fields are used by this core).
|
||
type AutoTablePagination struct {
|
||
CurrentPage int
|
||
TotalPages int
|
||
TotalItems int
|
||
MaxItemsPerPage int
|
||
ViewRangeLower int
|
||
ViewRangeUpper int
|
||
}
|
||
|
||
// atConfig holds resolved AutoTable options. Defaults mirror the TSX opts memo.
|
||
type atConfig struct {
|
||
size AutoTableSize
|
||
color AutoTableHeaderColor
|
||
shadow bool
|
||
hover bool
|
||
alternate bool
|
||
headerBorderY bool
|
||
surroundingBorder bool
|
||
borderX bool
|
||
borderY bool
|
||
tableLayoutAuto bool
|
||
hidePagination bool
|
||
|
||
loading bool
|
||
errorMsg string
|
||
emptyMessage string
|
||
|
||
sortIdentifier string
|
||
sortDescending bool
|
||
onSort func(identifier string)
|
||
|
||
pagination *AutoTablePagination
|
||
onPageChange func(page int)
|
||
onItemsPerPage func(n int)
|
||
|
||
class string
|
||
}
|
||
|
||
// AutoTableOption configures AutoTable (functional-options for the variadic opts).
|
||
type AutoTableOption func(*atConfig)
|
||
|
||
// AutoTableWithSize sets the density (default / compact / supercompact).
|
||
func AutoTableWithSize(s AutoTableSize) AutoTableOption {
|
||
return func(c *atConfig) { c.size = s }
|
||
}
|
||
|
||
// AutoTableWithColor sets the header/body color scheme.
|
||
func AutoTableWithColor(color AutoTableHeaderColor) AutoTableOption {
|
||
return func(c *atConfig) { c.color = color }
|
||
}
|
||
|
||
// AutoTableWithHover enables per-row hover highlighting.
|
||
func AutoTableWithHover() AutoTableOption { return func(c *atConfig) { c.hover = true } }
|
||
|
||
// AutoTableWithAlternate enables zebra striping on odd rows.
|
||
func AutoTableWithAlternate() AutoTableOption { return func(c *atConfig) { c.alternate = true } }
|
||
|
||
// AutoTableWithShadow adds a drop shadow to the table container.
|
||
func AutoTableWithShadow() AutoTableOption { return func(c *atConfig) { c.shadow = true } }
|
||
|
||
// AutoTableWithSurroundingBorder draws a border around the table container.
|
||
func AutoTableWithSurroundingBorder() AutoTableOption {
|
||
return func(c *atConfig) { c.surroundingBorder = true }
|
||
}
|
||
|
||
// AutoTableWithHeaderBorderY adds vertical dividers between header cells.
|
||
func AutoTableWithHeaderBorderY() AutoTableOption {
|
||
return func(c *atConfig) { c.headerBorderY = true }
|
||
}
|
||
|
||
// AutoTableWithBorderX draws horizontal dividers between body rows.
|
||
func AutoTableWithBorderX() AutoTableOption { return func(c *atConfig) { c.borderX = true } }
|
||
|
||
// AutoTableWithBorderY draws vertical dividers between body cells.
|
||
func AutoTableWithBorderY() AutoTableOption { return func(c *atConfig) { c.borderY = true } }
|
||
|
||
// AutoTableWithTableLayoutAuto uses auto table layout instead of table-fixed.
|
||
func AutoTableWithTableLayoutAuto() AutoTableOption {
|
||
return func(c *atConfig) { c.tableLayoutAuto = true }
|
||
}
|
||
|
||
// AutoTableWithLoading renders skeleton placeholder rows instead of data.
|
||
func AutoTableWithLoading(loading bool) AutoTableOption {
|
||
return func(c *atConfig) { c.loading = loading }
|
||
}
|
||
|
||
// AutoTableWithError renders a single error row with the given message.
|
||
func AutoTableWithError(msg string) AutoTableOption {
|
||
return func(c *atConfig) { c.errorMsg = msg }
|
||
}
|
||
|
||
// AutoTableWithEmptyMessage overrides the "No entries found." empty-state text.
|
||
func AutoTableWithEmptyMessage(msg string) AutoTableOption {
|
||
return func(c *atConfig) { c.emptyMessage = msg }
|
||
}
|
||
|
||
// AutoTableWithSort surfaces the active sort as a plain value + callback. onSort
|
||
// is invoked with a sortable column's SortIdentifier when its header is clicked;
|
||
// the caller owns the actual re-sorting (the TSX's local sort is out of scope).
|
||
func AutoTableWithSort(identifier string, descending bool, onSort func(identifier string)) AutoTableOption {
|
||
return func(c *atConfig) {
|
||
c.sortIdentifier = identifier
|
||
c.sortDescending = descending
|
||
c.onSort = onSort
|
||
}
|
||
}
|
||
|
||
// AutoTableWithPagination surfaces display-only pagination state + callbacks. The
|
||
// caller owns the actual paging/query computation.
|
||
func AutoTableWithPagination(p *AutoTablePagination, onPageChange func(page int), onItemsPerPage func(n int)) AutoTableOption {
|
||
return func(c *atConfig) {
|
||
c.pagination = p
|
||
c.onPageChange = onPageChange
|
||
c.onItemsPerPage = onItemsPerPage
|
||
}
|
||
}
|
||
|
||
// AutoTableWithHidePagination hides the pagination bar.
|
||
func AutoTableWithHidePagination() AutoTableOption {
|
||
return func(c *atConfig) { c.hidePagination = true }
|
||
}
|
||
|
||
// AutoTableWithClass appends classes to the outermost wrapper.
|
||
func AutoTableWithClass(class string) AutoTableOption {
|
||
return func(c *atConfig) { c.class = class }
|
||
}
|
||
|
||
// AutoTable renders the table shell for the given columns and rows. Sort state,
|
||
// loading, and pagination are plain value props supplied via opts (reactive
|
||
// accessors collapse). See the file-level NOTE for out-of-scope features.
|
||
func AutoTable(cols []AutoTableColumn, rows []any, opts ...AutoTableOption) *vdom.VNode {
|
||
cfg := &atConfig{}
|
||
for _, o := range opts {
|
||
o(cfg)
|
||
}
|
||
|
||
tableCls := cx("border-collapse", TBL_BASE)
|
||
if !cfg.tableLayoutAuto {
|
||
tableCls = cx(tableCls, "table-fixed")
|
||
}
|
||
|
||
containerCls := TBL_CONTAINER
|
||
if cfg.surroundingBorder {
|
||
containerCls = cx(containerCls, "border border-neutral-300")
|
||
}
|
||
if cfg.shadow {
|
||
containerCls = cx(containerCls, "shadow-sm")
|
||
}
|
||
|
||
table := vdom.El("table",
|
||
vdom.Attr("class", tableCls),
|
||
atRenderHead(cols, cfg),
|
||
atRenderBody(cols, rows, cfg),
|
||
)
|
||
|
||
container := vdom.El("div", vdom.Attr("class", containerCls),
|
||
vdom.El("div", vdom.Attr("class", TBL_WRAPPER), table),
|
||
)
|
||
if footer := atRenderPagination(cfg); footer != nil {
|
||
container.Children = append(container.Children, footer)
|
||
}
|
||
|
||
return vdom.El("div",
|
||
vdom.Attr("class", cx("min-w-0 w-full max-w-full", cfg.class)),
|
||
container,
|
||
)
|
||
}
|
||
|
||
// atRenderHead builds the <thead> with one header <tr>.
|
||
func atRenderHead(cols []AutoTableColumn, cfg *atConfig) *vdom.VNode {
|
||
headerColor := HEADER_COLOR_CLS[cfg.color]
|
||
headerPadding := HEADER_PADDING_CLS[cfg.size]
|
||
|
||
tr := vdom.El("tr")
|
||
if len(cols) == 0 {
|
||
tr.Children = append(tr.Children,
|
||
vdom.El("th", vdom.Attr("class", cx(headerPadding, headerColor)), vdom.Text(" ")))
|
||
}
|
||
for i, col := range cols {
|
||
tr.Children = append(tr.Children, atRenderHeaderCell(col, i, cfg, headerColor, headerPadding))
|
||
}
|
||
return vdom.El("thead", vdom.Attr("class", atTheadCls), tr)
|
||
}
|
||
|
||
// atRenderHeaderCell builds one <th>. Drag/resize handles are out of scope.
|
||
func atRenderHeaderCell(col AutoTableColumn, displayIdx int, cfg *atConfig, headerColor, headerPadding string) *vdom.VNode {
|
||
pos := col.DisplayPosition
|
||
posCls := POS_CLS[pos]
|
||
|
||
thCls := cx(headerPadding, headerColor, col.WidthClass, posCls)
|
||
if cfg.headerBorderY && displayIdx > 0 {
|
||
thCls = cx(thCls, "border-l border-l-neutral-300")
|
||
}
|
||
if col.Sortable {
|
||
thCls = cx(thCls, "cursor-pointer", atHeaderSortHoverCls[cfg.color])
|
||
}
|
||
thCls = cx(thCls, col.HeaderClasses)
|
||
|
||
mods := []vdom.Mod{vdom.Attr("class", thCls)}
|
||
if col.Sortable {
|
||
sortID := col.SortIdentifier
|
||
if sortID != "" && cfg.onSort != nil {
|
||
mods = append(mods, vdom.On(vdom.EVENT_CLICK, func() { cfg.onSort(sortID) }))
|
||
}
|
||
}
|
||
|
||
// Inner content: label (grows) + sort caret.
|
||
inner := vdom.El("div", vdom.Attr("class", cx(HEADER_INNER_BASE, HEADER_INNER_POS[pos])),
|
||
vdom.El("div", vdom.Attr("class", cx("grow text-sm", HEADER_TEXT_CLS[cfg.color])), vdom.Text(col.DisplayName)),
|
||
)
|
||
if col.Sortable {
|
||
iconWrap := vdom.El("div", vdom.Attr("class", cx(atSortIconWrap, "w-4 text-center", atHeaderSortIconCls[cfg.color])))
|
||
if col.SortIdentifier != "" && cfg.sortIdentifier == col.SortIdentifier {
|
||
// caret-up/caret-down are not in the default icon registry, so they
|
||
// render as empty boxes until an app registers them (see Icons.go).
|
||
if cfg.sortDescending {
|
||
iconWrap.Children = append(iconWrap.Children, Icon("caret-down", 16, ""))
|
||
} else {
|
||
iconWrap.Children = append(iconWrap.Children, Icon("caret-up", 16, ""))
|
||
}
|
||
}
|
||
inner.Children = append(inner.Children, iconWrap)
|
||
}
|
||
|
||
content := vdom.El("div", vdom.Attr("class", HEADER_CONTENT), inner)
|
||
mods = append(mods, content)
|
||
return vdom.El("th", mods...)
|
||
}
|
||
|
||
// atRenderBody builds the <tbody> with its loading / error / empty / data states.
|
||
func atRenderBody(cols []AutoTableColumn, rows []any, cfg *atConfig) *vdom.VNode {
|
||
bodyCls := BODY_PADDING_CLS[cfg.size]
|
||
if cfg.borderY {
|
||
bodyCls = cx(bodyCls, "[&_td+td]:border-l [&_td+td]:border-neutral-300")
|
||
}
|
||
|
||
colspan := len(cols)
|
||
if colspan == 0 {
|
||
colspan = 1
|
||
}
|
||
|
||
tbody := vdom.El("tbody", vdom.Attr("class", bodyCls))
|
||
|
||
switch {
|
||
case cfg.loading:
|
||
// NOTE: the TSX randomizes each skeleton's width; a fixed width is used
|
||
// here (no measurement/randomness in the neutral runtime).
|
||
for r := 0; r < 5; r++ {
|
||
tr := vdom.El("tr")
|
||
if cfg.alternate && r%2 == 1 {
|
||
tr.Attrs["class"] = "bg-neutral-100"
|
||
}
|
||
for range cols {
|
||
tr.Children = append(tr.Children, vdom.El("td",
|
||
vdom.El("div", vdom.Attr("class", atSkeleton), vdom.Attr("style", "width: 70%")),
|
||
))
|
||
}
|
||
tbody.Children = append(tbody.Children, tr)
|
||
}
|
||
case cfg.errorMsg != "":
|
||
tbody.Children = append(tbody.Children, vdom.El("tr",
|
||
vdom.El("td", vdom.Attr("colspan", strconv.Itoa(colspan)), vdom.Attr("class", atErrorCell),
|
||
vdom.Text("Error: "+cfg.errorMsg)),
|
||
))
|
||
case len(cols) == 0:
|
||
tbody.Children = append(tbody.Children, vdom.El("tr",
|
||
vdom.El("td", vdom.Attr("colspan", strconv.Itoa(colspan)), vdom.Attr("class", atEmptyCell),
|
||
vdom.Text("No columns selected.")),
|
||
))
|
||
case len(rows) == 0:
|
||
tbody.Children = append(tbody.Children, vdom.El("tr",
|
||
vdom.El("td", vdom.Attr("colspan", strconv.Itoa(colspan)),
|
||
vdom.Text(pick(cfg.emptyMessage, "No entries found."))),
|
||
))
|
||
default:
|
||
hoverCls := ""
|
||
if cfg.hover {
|
||
hoverCls = atRowHoverCls[cfg.color]
|
||
}
|
||
for rowIdx, row := range rows {
|
||
isLast := rowIdx == len(rows)-1
|
||
rowCls := ""
|
||
if cfg.alternate && rowIdx%2 == 1 {
|
||
rowCls = cx(rowCls, "bg-neutral-100")
|
||
}
|
||
rowCls = cx(rowCls, hoverCls)
|
||
if cfg.borderX && !isLast {
|
||
rowCls = cx(rowCls, "border-b border-neutral-300")
|
||
}
|
||
|
||
tr := vdom.El("tr")
|
||
if rowCls != "" {
|
||
tr.Attrs["class"] = rowCls
|
||
}
|
||
for _, col := range cols {
|
||
tr.Children = append(tr.Children, atRenderCell(col, row))
|
||
}
|
||
tbody.Children = append(tbody.Children, tr)
|
||
}
|
||
}
|
||
return tbody
|
||
}
|
||
|
||
// atRenderCell renders a column's cell for a row. The cell func returns the full
|
||
// <td>; if nil, an empty aligned <td> is produced.
|
||
func atRenderCell(col AutoTableColumn, row any) *vdom.VNode {
|
||
if col.Cell != nil {
|
||
if td := col.Cell(row); td != nil {
|
||
return td
|
||
}
|
||
}
|
||
return vdom.El("td", vdom.Attr("class", POS_CLS[col.DisplayPosition]))
|
||
}
|
||
|
||
// atRenderPagination builds the display-only pagination bar, or nil when there is
|
||
// nothing to show. Page/items-per-page changes are surfaced via callbacks.
|
||
func atRenderPagination(cfg *atConfig) *vdom.VNode {
|
||
if cfg.pagination == nil || cfg.hidePagination {
|
||
return nil
|
||
}
|
||
p := cfg.pagination
|
||
|
||
info := vdom.El("div", vdom.Attr("class", atPaginationInfo),
|
||
vdom.El("b", vdom.Attr("class", "leading-none"), Icon("list-ol", 16, "")),
|
||
vdom.El("span", vdom.Attr("class", "ml-3"),
|
||
vdom.Text(strconv.Itoa(p.ViewRangeLower)+"-"+strconv.Itoa(p.ViewRangeUpper)+" of "+strconv.Itoa(p.TotalItems))),
|
||
)
|
||
|
||
// NOTE: the TSX uses the ported FormSelect; a bare <select> is used here.
|
||
sel := vdom.El("select", vdom.Attr("class", "mr-5"))
|
||
for _, n := range []int{5, 10, 25, 50, 100} {
|
||
optMods := []vdom.Mod{vdom.Attr("value", strconv.Itoa(n)), vdom.Text(strconv.Itoa(n))}
|
||
if n == p.MaxItemsPerPage {
|
||
optMods = append(optMods, vdom.Attr("selected", "selected"))
|
||
}
|
||
sel.Children = append(sel.Children, vdom.El("option", optMods...))
|
||
}
|
||
if cfg.onItemsPerPage != nil {
|
||
sel.Events[vdom.EVENT_CHANGE] = func(e vdom.Event) {
|
||
if n, err := strconv.Atoi(e.Value()); err == nil {
|
||
cfg.onItemsPerPage(n)
|
||
}
|
||
}
|
||
}
|
||
|
||
page := func(to int) func() {
|
||
return func() {
|
||
if cfg.onPageChange != nil {
|
||
cfg.onPageChange(to)
|
||
}
|
||
}
|
||
}
|
||
|
||
controls := vdom.El("div", vdom.Attr("class", atPaginationControls),
|
||
vdom.El("div", vdom.Attr("class", atPaginationLabel), vdom.Text("Items per page:")),
|
||
sel,
|
||
atPaginationButton(page(1), p.CurrentPage <= 1, Icon("angles-left", 16, "")),
|
||
atPaginationButton(page(p.CurrentPage-1), p.CurrentPage <= 1, Icon("chevron-left", 16, "")),
|
||
vdom.El("div", vdom.Attr("class", atPaginationPage),
|
||
vdom.Text("Page "+strconv.Itoa(p.CurrentPage)+" of "+strconv.Itoa(p.TotalPages))),
|
||
atPaginationButton(page(p.CurrentPage+1), p.CurrentPage >= p.TotalPages, Icon("chevron-right", 16, "")),
|
||
atPaginationButton(page(p.TotalPages), p.CurrentPage >= p.TotalPages, Icon("angles-right", 16, "")),
|
||
)
|
||
|
||
return vdom.El("div", vdom.Attr("class", cx(atPaginationBase, atPaginationPaddingCls[cfg.size])),
|
||
vdom.El("div", vdom.Attr("class", "flex items-center"), info),
|
||
controls,
|
||
)
|
||
}
|
||
|
||
// atPaginationButton is one pagination control button (mirrors TSX PaginationButton).
|
||
func atPaginationButton(onClick func(), disabled bool, child *vdom.VNode) *vdom.VNode {
|
||
mods := []vdom.Mod{vdom.Attr("class", atPaginationBtn)}
|
||
if disabled {
|
||
mods = append(mods, vdom.Attr("disabled", "disabled"))
|
||
} else if onClick != nil {
|
||
mods = append(mods, vdom.On(vdom.EVENT_CLICK, onClick))
|
||
}
|
||
if child != nil {
|
||
mods = append(mods, child)
|
||
}
|
||
return vdom.El("button", mods...)
|
||
}
|
||
|
||
// AutoTableTdLeft / AutoTableTdRight / AutoTableTdCenter build an aligned <td>,
|
||
// convenient for AutoTableColumn.Cell funcs (mirror the TSX TdLeft/Right/Center).
|
||
func AutoTableTdLeft(class string, children ...*vdom.VNode) *vdom.VNode {
|
||
return vdom.El("td", kids([]vdom.Mod{vdom.Attr("class", cx("text-left", class))}, children)...)
|
||
}
|
||
|
||
func AutoTableTdRight(class string, children ...*vdom.VNode) *vdom.VNode {
|
||
return vdom.El("td", kids([]vdom.Mod{vdom.Attr("class", cx("text-right", class))}, children)...)
|
||
}
|
||
|
||
func AutoTableTdCenter(class string, children ...*vdom.VNode) *vdom.VNode {
|
||
return vdom.El("td", kids([]vdom.Mod{vdom.Attr("class", cx("text-center", class))}, children)...)
|
||
}
|