diff --git a/lib/clients/DataTable.ts b/lib/clients/DataTable.ts index ea3f750..c2b90a8 100644 --- a/lib/clients/DataTable.ts +++ b/lib/clients/DataTable.ts @@ -69,6 +69,8 @@ export class DataTable extends MosaicClient { #tbody: HTMLTableSectionElement = document.createElement("tbody"); /** The SQL order by */ #orderby: Array<{ field: string; order: "asc" | "desc" | "unset" }> = []; + /** The SQL select */ + #select: Record; /** template row for data */ #templateRow: HTMLTableRowElement | undefined = undefined; /** div containing the table */ @@ -93,12 +95,15 @@ export class DataTable extends MosaicClient { /** @type {AsyncBatchReader | null} */ #reader: AsyncBatchReader | null = null; - #sql = signal(undefined as string | undefined); + #sql = signal(undefined as Query | undefined); constructor(source: DataTableOptions) { super(Selection.crossfilter()); this.#format = formatof(source.schema); this.#meta = source; + this.#select = Object.fromEntries( + this.#columns.map((column) => [column, column]), + ); let maxHeight = `${(this.#rows + 1) * this.#rowHeight - 1}px`; // if maxHeight is set, calculate the number of rows to display @@ -132,7 +137,7 @@ export class DataTable extends MosaicClient { } get sql() { - return this.#sql.value; + return this.#sql.value?.toString(); } fields(): Array { @@ -157,20 +162,21 @@ export class DataTable extends MosaicClient { return this.#meta.schema.fields.map((field) => field.name); } - /** - * @param {Array} filter - */ query(filter: Array = []) { let query = Query.from(this.#meta.table) - .select(this.#columns) + .select(this.#select) .where(filter) .orderby( this.#orderby .filter((o) => o.order !== "unset") .map((o) => o.order === "asc" ? asc(o.field) : desc(o.field)), ); - this.#sql.value = query.clone().toString(); + if (this.#sql.value?.toString() !== query.toString()) { + // only update the sql signal if the query has changed + this.#sql.value = query.clone(); + } return query + .select(this.#columns) .limit(this.#limit) .offset(this.#offset); } @@ -282,6 +288,13 @@ export class DataTable extends MosaicClient { this.requestData(); }); + signals.effect(() => { + this.#select = Object.fromEntries( + cols.map((col, i) => [col.nameState.value, this.#columns[i]]), + ); + this.requestData(); + }); + // @deno-fmt-ignore this.#thead.appendChild( html` @@ -353,12 +366,6 @@ export class DataTable extends MosaicClient { } } -const TRUNCATE = /** @type {const} */ ({ - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", -}); - function thcol( field: arrow.Field, minWidth: number, @@ -369,6 +376,7 @@ function thcol( let sortState: signals.Signal<"unset" | "asc" | "desc"> = signals.signal( "unset", ); + let name = signals.signal(field.name); function nextSortState() { // simple state machine @@ -391,10 +399,60 @@ function thcol( html`
`; // @deno-fmt-ignore let sortButton = html`${svg}`; + + // @deno-fmt-ignore + let input = html` { + if (event.key === "Enter") { + event.preventDefault(); + // commit the change + name.value = input.value; + input.blur(); + resetButton.style.display = "none"; + } + }} + onfocus=${() => { + resetButton.style.display = "block"; + }} + onblur=${() => { + resetButton.style.display = "none"; + // if empty restore the original name + if (input.value === "") { + input.value = field.name; + } + }} + >`; + + let resetIcon = + html` + + + `; + + // @deno-fmt-ignore + let resetButton = html``; + // @deno-fmt-ignore let th: HTMLTableCellElement = html` -
- ${field.name} +
+ ${input} + ${resetButton} ${sortButton}
${verticalResizeHandle} @@ -467,7 +525,7 @@ function thcol( verticalResizeHandle.style.backgroundColor = "transparent"; }); - return Object.assign(th, { vis, sortState }); + return Object.assign(th, { vis, sortState, nameState: name }); } /** diff --git a/lib/clients/styles.css b/lib/clients/styles.css index 6832bc9..937f192 100644 --- a/lib/clients/styles.css +++ b/lib/clients/styles.css @@ -2,10 +2,11 @@ all: initial; --sans-serif: -apple-system, BlinkMacSystemFont, "avenir next", avenir, helvetica, "helvetica neue", ubuntu, roboto, noto, "segoe ui", arial, sans-serif; --light-silver: #efefef; + --light-gray: #e2e2e2; --spacing-none: 0; --white: #fff; --gray: #929292; - --dark-gray: #333; + --dark-gray: #454545; --moon-gray: #c4c4c4; --mid-gray: #6e6e6e; @@ -24,6 +25,25 @@ --secondary: var(--yellow-gold); } +button, input, optgroup, select, textarea { + font-family: var(--sans-serif); + font-size: 100%; + line-height: 1.15; + margin: 0; +} + +button { + all: unset; + display: inline-block; + cursor: pointer; +} + +.quak { + border-radius: 0.2rem; + border: 1px solid var(--light-silver); + overflow-y: auto; +} + .highlight { background-color: var(--light-silver); } @@ -32,93 +52,118 @@ border: 1px solid var(--moon-gray); } -.quak { - border-radius: 0.2rem; - border: 1px solid var(--light-silver); - overflow-y: auto; +th input[type="text"] { + color: var(--dark-gray); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-weight: bold; + border-radius: 4px; + padding-left: 0; + padding-right: 0; + margin-top: 4px; + margin-bottom: 4px; + background-color: transparent; + width: auto; + min-width: 0; + border: none; + outline: none; + accent-color: var(--primary); +} + +th input[type="text"]:focus { + -webkit-box-shadow: 0 0 0 1px var(--light-gray); + -moz-box-shadow: 0 0 0 1px var(--light-gray); + box-shadow: 0 0 0 1px var(--light-gray); +} + +th input[type="text"]:hover { + -webkit-box-shadow: 0 0 0 1px var(--light-gray); + -moz-box-shadow: 0 0 0 1px var(--light-gray); + box-shadow: 0 0 0 1px var(--light-gray); } table { - border-collapse: separate; - border-spacing: 0; - white-space: nowrap; - box-sizing: border-box; + border-collapse: separate; + border-spacing: 0; + white-space: nowrap; + box-sizing: border-box; - margin: var(--spacing-none); - color: var(--dark-gray); - font: 13px / 1.2 var(--sans-serif); + margin: var(--spacing-none); + color: var(--dark-gray); + font: 13px / 1.2 var(--sans-serif); - width: 100%; + width: 100%; } thead { - position: sticky; - vertical-align: top; - text-align: left; - top: 0; + position: sticky; + vertical-align: top; + text-align: left; + top: 0; } td { - border: 1px solid var(--light-silver); - border-bottom: solid 1px transparent; - border-right: solid 1px transparent; - overflow: hidden; - -o-text-overflow: ellipsis; - text-overflow: ellipsis; - padding: 4px 6px; + border: 1px solid var(--light-silver); + border-bottom: solid 1px transparent; + border-right: solid 1px transparent; + overflow: hidden; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + padding: 4px 6px; } tr:first-child td { - border-top: solid 1px transparent; + border-top: solid 1px transparent; } th { - display: table-cell; - vertical-align: inherit; - font-weight: bold; - text-align: -internal-center; - unicode-bidi: isolate; - - position: relative; - background: var(--white); - border-bottom: solid 1px var(--light-silver); - border-left: solid 1px var(--light-silver); - padding: 5px 6px; - user-select: none; + display: table-cell; + vertical-align: inherit; + font-weight: bold; + text-align: -internal-center; + unicode-bidi: isolate; + + position: relative; + background: var(--white); + border-bottom: solid 1px var(--light-silver); + border-left: solid 1px var(--light-silver); + padding: 5px 6px; + user-select: none; } .number, .date { - font-variant-numeric: tabular-nums; + font-variant-numeric: tabular-nums; } .gray { - color: var(--gray); + color: var(--gray); } .number { - text-align: right; + text-align: right; } td:nth-child(1), th:nth-child(1) { - font-variant-numeric: tabular-nums; - text-align: center; - color: var(--moon-gray); - padding: 0 4px; + font-variant-numeric: tabular-nums; + text-align: center; + color: var(--moon-gray); + padding: 0 4px; } td:first-child, th:first-child { - border-left: none; + border-left: none; } th:first-child { - border-left: none; - vertical-align: top; - width: 20px; - padding: 7px; + border-left: none; + vertical-align: top; + width: 20px; + padding: 7px; } td:nth-last-child(2), th:nth-last-child(2) { - border-right: 1px solid var(--light-silver); + border-right: 1px solid var(--light-silver); } tr:first-child td { @@ -136,10 +181,21 @@ tr:first-child td { z-index: 1; } -.quak .sort-button { - cursor: pointer; - background-color: var(--white); +.sort-button { + padding-left: 0.25rem; + background-color: transparent; user-select: none; + border: none; +} + +.reset-button { + color: var(--gray); + padding-left: 0.25rem; + padding-right: 0; +} + +.reset-button:hover { + color: var(--dark-gray); } .status-bar { diff --git a/lib/demo.ts b/lib/demo.ts index 810b210..42c9316 100644 --- a/lib/demo.ts +++ b/lib/demo.ts @@ -3,7 +3,7 @@ import * as mc from "@uwdata/mosaic-core"; import * as msql from "@uwdata/mosaic-sql"; import { assert } from "./utils/assert.ts"; -import { datatable } from "./clients/DataTable.ts"; +import { DataTable, datatable } from "./clients/DataTable.ts"; let dropzone = document.querySelector("input")!; let options = document.querySelector("#options")!; @@ -52,6 +52,7 @@ function handleLoading(source: string | null) { table.appendChild(loading); } +let dt: DataTable; async function main() { handleBanner(); let source = new URLSearchParams(location.search).get("source"); @@ -91,7 +92,7 @@ async function main() { exec = exec.replace("json_format", "format"); await coordinator.exec([exec]); - let dt = await datatable(tableName, { coordinator, height: 500 }); + dt = await datatable(tableName, { coordinator, height: 500 }); options.remove(); table.replaceChildren(); table.appendChild(dt.node()); @@ -117,4 +118,13 @@ async function main() { exportButton.classList.remove("hidden"); } +import.meta.hot?.accept("./clients/DataTable.ts", async (mod) => { + dt = await mod.datatable("df", { + coordinator: dt.coordinator, + height: 500, + }); + table.replaceChildren(); + table.appendChild(dt.node()); +}); + main();