Files
Trilium/apps/client/src/widgets/view_widgets/table_view/index.ts

259 lines
9.1 KiB
TypeScript
Raw Normal View History

import ViewMode, { type ViewModeArgs } from "../view_mode.js";
import attributes from "../../../services/attributes.js";
2025-06-27 17:58:25 +03:00
import SpacedUpdate from "../../../services/spaced_update.js";
import type { EventData } from "../../../components/app_context.js";
import {Tabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, ColumnDefinition, DataTreeModule, Options, RowComponent, ColumnComponent} from 'tabulator-tables';
import "tabulator-tables/dist/css/tabulator.css";
import "../../../../src/stylesheets/table.css";
import { canReorderRows, configureReorderingRows } from "./dragging.js";
import buildFooter from "./footer.js";
import getAttributeDefinitionInformation, { buildRowDefinitions } from "./rows.js";
import { AttributeDefinitionInformation, buildColumnDefinitions } from "./columns.js";
import { setupContextMenu } from "./context_menu.js";
import TableColumnEditing from "./col_editing.js";
import TableRowEditing from "./row_editing.js";
2025-06-25 10:31:41 +03:00
const TPL = /*html*/`
<div class="table-view">
2025-06-25 10:40:04 +03:00
<style>
.table-view {
overflow: hidden;
position: relative;
height: 100%;
user-select: none;
padding: 0 5px 0 10px;
2025-06-25 10:40:04 +03:00
}
2025-06-25 10:49:33 +03:00
.table-view-container {
height: 100%;
}
2025-06-27 17:18:52 +03:00
.search-result-widget-content .table-view {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.tabulator-cell .autocomplete {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: transparent;
outline: none !important;
}
2025-06-25 10:40:04 +03:00
2025-07-04 20:38:48 +03:00
.tabulator .tabulator-header {
border-top: unset;
border-bottom-width: 1px;
}
.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left,
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left {
border-right-width: 1px;
}
.tabulator .tabulator-footer {
background-color: unset;
2025-07-04 20:38:48 +03:00
padding: 5px 0;
}
.tabulator .tabulator-footer .tabulator-footer-contents {
justify-content: left;
gap: 0.5em;
}
.tabulator button.tree-expand,
.tabulator button.tree-collapse {
display: inline-block;
appearance: none;
border: 0;
background: transparent;
width: 1.5em;
position: relative;
vertical-align: middle;
}
.tabulator button.tree-expand span,
.tabulator button.tree-collapse span {
position: absolute;
top: 0;
left: 0;
font-size: 1.5em;
transform: translateY(-50%);
}
</style>
<div class="table-view-container"></div>
2025-06-25 10:31:41 +03:00
</div>
`;
export interface StateInfo {
tableData?: {
columns?: ColumnDefinition[];
};
}
2025-06-25 16:18:34 +03:00
export default class TableView extends ViewMode<StateInfo> {
2025-06-25 10:31:41 +03:00
private $root: JQuery<HTMLElement>;
2025-06-25 10:49:33 +03:00
private $container: JQuery<HTMLElement>;
2025-06-27 17:58:25 +03:00
private spacedUpdate: SpacedUpdate;
2025-06-28 12:24:40 +03:00
private api?: Tabulator;
private persistentData: StateInfo["tableData"];
private colEditing?: TableColumnEditing;
private rowEditing?: TableRowEditing;
private maxDepth: number = -1;
2025-06-25 10:31:41 +03:00
constructor(args: ViewModeArgs) {
2025-06-25 16:18:34 +03:00
super(args, "table");
2025-06-25 10:31:41 +03:00
this.$root = $(TPL);
2025-06-25 10:49:33 +03:00
this.$container = this.$root.find(".table-view-container");
2025-06-27 17:58:25 +03:00
this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000);
this.persistentData = {};
2025-06-25 10:31:41 +03:00
args.$parent.append(this.$root);
}
async renderList() {
this.$container.empty();
this.renderTable(this.$container[0]);
return this.$root;
}
private async renderTable(el: HTMLElement) {
const info = getAttributeDefinitionInformation(this.parentNote);
const modules = [ SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, DataTreeModule ];
2025-06-28 17:07:11 +03:00
for (const module of modules) {
Tabulator.registerModule(module);
}
this.initialize(el, info);
2025-06-28 12:24:40 +03:00
}
private async initialize(el: HTMLElement, info: AttributeDefinitionInformation[]) {
const viewStorage = await this.viewStorage.restore();
this.persistentData = viewStorage?.tableData || {};
this.maxDepth = parseInt(this.parentNote.getLabelValue("maxNestingDepth") ?? "-1", 10);
const { definitions: rowData, hasSubtree: hasChildren } = await buildRowDefinitions(this.parentNote, info, this.maxDepth);
const movableRows = canReorderRows(this.parentNote) && !hasChildren;
const columnDefs = buildColumnDefinitions(info, movableRows, this.persistentData.columns);
let opts: Options = {
layout: "fitDataFill",
index: "branchId",
columns: columnDefs,
data: rowData,
persistence: true,
movableColumns: true,
movableRows,
footerElement: buildFooter(this.parentNote),
persistenceWriterFunc: (_id, type: string, data: object) => {
(this.persistentData as Record<string, {}>)[type] = data;
this.spacedUpdate.scheduleUpdate();
},
persistenceReaderFunc: (_id, type: string) => this.persistentData?.[type],
};
if (hasChildren) {
opts = {
...opts,
dataTree: hasChildren,
dataTreeStartExpanded: true,
dataTreeBranchElement: false,
dataTreeElementColumn: "title",
dataTreeChildIndent: 20,
dataTreeExpandElement: `<button class="tree-expand"><span class="bx bx-chevron-right"></span></button>`,
dataTreeCollapseElement: `<button class="tree-collapse"><span class="bx bx-chevron-down"></span></button>`
}
}
this.api = new Tabulator(el, opts);
this.colEditing = new TableColumnEditing(this.args.$parent, this.args.parentNote, this.api);
this.rowEditing = new TableRowEditing(this.api, this.args.parentNotePath!);
if (movableRows) {
configureReorderingRows(this.api);
}
2025-07-13 13:06:53 +03:00
setupContextMenu(this.api, this.parentNote);
2025-06-27 17:58:25 +03:00
}
private onSave() {
this.viewStorage.store({
tableData: this.persistentData,
});
2025-06-25 10:31:41 +03:00
}
async onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) {
if (!this.api) {
return;
}
// Force a refresh if sorted is changed since we need to disable reordering.
if (loadResults.getAttributeRows().find(a => a.name === "sorted" && attributes.isAffecting(a, this.parentNote))) {
return true;
}
// Refresh if promoted attributes get changed.
if (loadResults.getAttributeRows().find(attr =>
attr.type === "label" &&
(attr.name?.startsWith("label:") || attr.name?.startsWith("relation:")) &&
attributes.isAffecting(attr, this.parentNote))) {
this.#manageColumnUpdate();
return await this.#manageRowsUpdate();
}
// Refresh max depth
if (loadResults.getAttributeRows().find(attr => attr.type === "label" && attr.name === "maxNestingDepth" && attributes.isAffecting(attr, this.parentNote))) {
this.maxDepth = parseInt(this.parentNote.getLabelValue("maxNestingDepth") ?? "-1", 10);
return await this.#manageRowsUpdate();
}
if (loadResults.getBranchRows().some(branch => branch.parentNoteId === this.parentNote.noteId || this.noteIds.includes(branch.parentNoteId ?? ""))
|| loadResults.getNoteIds().some(noteId => this.noteIds.includes(noteId)
|| loadResults.getAttributeRows().some(attr => this.noteIds.includes(attr.noteId!)))) {
return await this.#manageRowsUpdate();
}
return false;
}
#manageColumnUpdate() {
2025-07-01 12:09:13 +03:00
if (!this.api) {
return;
}
const info = getAttributeDefinitionInformation(this.parentNote);
const columnDefs = buildColumnDefinitions(info, !!this.api.options.movableRows, this.persistentData?.columns, this.colEditing?.getNewAttributePosition());
this.api.setColumns(columnDefs);
this.colEditing?.resetNewAttributePosition();
}
addNewRowCommand(e) { this.rowEditing?.addNewRowCommand(e); }
addNewTableColumnCommand(e) { this.colEditing?.addNewTableColumnCommand(e); }
deleteTableColumnCommand(e) { this.colEditing?.deleteTableColumnCommand(e); }
updateAttributeListCommand(e) { this.colEditing?.updateAttributeListCommand(e); }
saveAttributesCommand() { this.colEditing?.saveAttributesCommand(); }
async #manageRowsUpdate() {
2025-07-01 12:09:13 +03:00
if (!this.api) {
return;
}
const info = getAttributeDefinitionInformation(this.parentNote);
const { definitions, hasSubtree } = await buildRowDefinitions(this.parentNote, info, this.maxDepth);
// Force a refresh if the data tree needs enabling/disabling.
if (this.api.options.dataTree !== hasSubtree) {
return true;
}
await this.api.replaceData(definitions);
return false;
}
2025-06-27 19:53:40 +03:00
}