// ## File Browser Example // // A simple file browser example built with // [`regular-table`](https://github.com/finos/regular-table). Also a great // introduction to `row_headers`, and how to use them to achieve group-like and // tree-like behavior. For this example, we'll want the latter. // // ## Tree-like `row_headers` // // `regular-table` will merge consecutive `` defined in `row_headers` with the // same content, but it will prefer `rowspan` to `colspan`, inserting empty `` // when necessary to fill-in gaps, since `table-cell` elements cannot overlap. // Knowing this, it is easy to fine-tune header structure and behavior with empty // cells. In this case, we want to modify the basic _group-like_ `row_headers` // layout to support _tree-like_ asymmetric groups. Typically, when representing // groups of rows via `row_headers`, for example a file structure like so: // // - Dir_1 // - Dir_2 // - File_1 // - File_2 // // ... one may think to implement a `regular-table` Virtual Data Model using a // `row_headers` parameter like this: // // ```json // [ // ["Dir_1"], // ["Dir_1", "Dir_2"], // ["Dir_1", "Dir_2", "File_1"], // ["Dir_1", "File_2"] // ] // ``` // // This will render _group-like_ row headers, with the consecutive `"Dir_1"` and // `"Dir_2"` elements merged via `rowspan`. The resulting headers visually indicate // all content on the right-hand side belong to the directory. This is exactly what // column headers do, but it is not very like a file-tree; each directory "level" // will determine its respective column's minimum width, and deeply assymmetric // trees will yield wide row headers. // // Group-like row headers are nice for always keeping the entire directory path in // view regardless of scroll position, but for a more tree-like like experience, we // can instead replace the consecutive duplicates with `""`. // // ```json // [["Dir_1"], ["", "Dir_2"], ["", "", "File_1"], ["", "File_2"]] // ``` // // The new consecutive `""` will still merge via `rowspan`, excluding the first // row, but `regular-table` will detect that a `` lacks a `rowspan`, and // instead merge trailing `undefined`/empty values via `colspan` to produce one // long `` for each row header group, as in the HTML below. In this tree-like // layout, no content will exclusively occupy any but the last column of // `row_headers`, and these empty columns can then be sized via CSS to create trees // of any geometry, where e.g. "directory" group rows overlap the columns of their // children as-in a conventional file tree. // // Despite this long-winded explanation, the implementation in Javascript is fairly // straightforward, and for our purposes, we only need create one such path for // `row_headers` at a time. import "/dist/esm/regular-table.js"; function new_path(n, name) { return Array(n).fill("").concat([name]); } // ## File System // // We can use a regular 2D Array, row oriented, for the file system listing state // itself, including file metadata like `size` and the open/closed state of // directory rows. const COLUMNS = [["size"], ["kind"], ["modified"], ["writable"]]; const DATA = Array.from(generateDirContents()); // These file-metadata rows are fake, but for the purposes of an example, they are // worth putting "B Movie"-level effort into making look like a "real" file system. function new_row(type) { const scale = Math.random() > 0.5 ? "kb" : "mb"; const size = numberFormat(Math.pow(Math.random(), 2) * 1000); const date = dateFormat(new Date()); return [`${size} ${scale}`, type, date, true]; } // For the fake file system contents themselves, we will generate directory // contents on the fly as directories are opened and closed by the user. function* generateDirContents(n = 0) { for (let i = 0; i < 5; i++) { yield { path: new_path(n, `Dir_${i}`), row: new_row("directory"), is_open: false, }; } for (let i = 0; i < 5; i++) { yield { path: new_path(n, `File_${i}`), row: new_row("file"), }; } } // Open and close directory operations are applied via `DATA.splice()`, mutating // the `Array` reference directly and inserting or stripping elements as needed. function closeDir(y) { const path = DATA[y].path; while (y + 2 < DATA.length && DATA[y + 1].path.length > path.length) { DATA.splice(y + 1, 1); } } function openDir(y) { const new_contents = generateDirContents(DATA[y].path.length); DATA.splice(y + 1, 0, ...Array.from(new_contents)); } function toggleDir(y) { const { is_open } = DATA[y]; if (is_open) { closeDir(y); } else { openDir(y); } DATA[y].is_open = !is_open; } // ## Virtual Data Model // // `DATA` needs to be _transposed_ before we can return slices of it from our // `dataListener()` function, because it is row-oriented and `regular-table` // expects column-oriented data. function transpose(m) { return m.length === 0 ? [] : m[0].map((x, i) => m.map((x) => x[i])); } // Otherwise, this `dataListener()` is very similar to `2d_array.md`. function dataListener(x0, y0, x1, y1) { return { num_rows: DATA.length, num_columns: DATA[0].row.length, row_headers: DATA.slice(y0, y1).map((z) => z.path.slice()), column_headers: COLUMNS.slice(x0, x1), data: transpose(DATA.slice(y0, y1).map(({ row }) => row.slice(x0, x1))), }; } // ## Custom Style // // Directory and file icon styles applied as classes, using `getMeta()`, every `td` // is mapped back to it's row in `DATA`. function styleListener() { for (const td of window.regularTable.querySelectorAll("tbody th")) { const { y, value } = window.regularTable.getMeta(td); const { row, is_open } = DATA[y]; const [, type] = row; td.classList.toggle("fb-directory", !!value && type === "directory"); td.classList.toggle("fb-file", !!value && type === "file"); td.classList.toggle("fb-open", !!value && is_open); } } // ## UI // // When directory rows are clicked, generate new directory contents at the `td` // metadata's `y` coordinate in `DATA` and redraw. // // TODO `resetAutoSize()` is not documented - this is currently required to // prevent the column size scroll memoize functionality from pinning the sizes // of the 'blank' cells, as these columns may be re-purposed as the user expands // or collapses the tree. But auto-sizing is not well formalized feature yet // and this API is just a stand-in. function mousedownListener() { if (event.target.tagName === "TH") { const meta = regularTable.getMeta(event.target); if (DATA[meta.y].row[1] === "directory") { toggleDir(meta.y); regularTable.resetAutoSize(); regularTable.draw(); } } } // ## Main regularTable.setDataListener(dataListener); regularTable.addStyleListener(styleListener); regularTable.addEventListener("mousedown", mousedownListener); regularTable.addEventListener("scroll", () => { regularTable.resetAutoSize(); }); regularTable.draw(); // ## Appendix (Utilities) function numberFormat(x) { const formatter = new Intl.NumberFormat("en-us", { style: "decimal", minimumFractionDigits: 2, maximumFractionDigits: 2, }); return formatter.format(x); } function dateFormat(x) { const formatter = new Intl.DateTimeFormat("en-us", { week: "numeric", year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric", }); return formatter.format(x); }