File: C:/Users/fred/anaconda3/Lib/site-packages/panel/dist/lib/models/tabulator.js
import { undisplay } from "@bokehjs/core/dom";
import { sum } from "@bokehjs/core/util/arrayable";
import { isArray, isBoolean, isString, isNumber } from "@bokehjs/core/util/types";
import { ModelEvent } from "@bokehjs/core/bokeh_events";
import { div } from "@bokehjs/core/dom";
import { Enum } from "@bokehjs/core/kinds";
import { ColumnDataSource } from "@bokehjs/models/sources/column_data_source";
import { TableColumn } from "@bokehjs/models/widgets/tables";
import { debounce } from "debounce";
import { comm_settings } from "./comm_manager";
import { transform_cds_to_records } from "./data";
import { HTMLBox, HTMLBoxView } from "./layout";
import { schedule_when } from "./util";
export class TableEditEvent extends ModelEvent {
column;
row;
pre;
static __name__ = "TableEditEvent";
constructor(column, row, pre) {
super();
this.column = column;
this.row = row;
this.pre = pre;
}
get event_values() {
return { model: this.origin, column: this.column, row: this.row, pre: this.pre };
}
static {
this.prototype.event_name = "table-edit";
}
}
export class CellClickEvent extends ModelEvent {
column;
row;
static __name__ = "CellClickEvent";
constructor(column, row) {
super();
this.column = column;
this.row = row;
}
get event_values() {
return { model: this.origin, column: this.column, row: this.row };
}
static {
this.prototype.event_name = "cell-click";
}
}
export class SelectionEvent extends ModelEvent {
indices;
selected;
flush;
static __name__ = "SelectionEvent";
constructor(indices, selected, flush = false) {
super();
this.indices = indices;
this.selected = selected;
this.flush = flush;
}
get event_values() {
return { model: this.origin, indices: this.indices, selected: this.selected, flush: this.flush };
}
static {
this.prototype.event_name = "selection-change";
}
}
function find_group(key, value, records) {
for (const record of records) {
if (record[key] == value) {
return record;
}
}
return null;
}
function summarize(grouped, columns, aggregators, depth = 0) {
const summary = {};
if (grouped.length == 0) {
return summary;
}
const agg = aggregators[depth];
for (const group of grouped) {
const subsummary = summarize(group._children, columns, aggregators, depth + 1);
for (const col in subsummary) {
if (isArray(subsummary[col])) {
group[col] = sum(subsummary[col]) / subsummary[col].length;
}
else {
group[col] = subsummary[col];
}
}
for (const column of columns.slice(1)) {
const val = group[column.field];
if (column.field in summary) {
const old_val = summary[column.field];
if (agg === "min") {
summary[column.field] = Math.min(val, old_val);
}
else if (agg === "max") {
summary[column.field] = Math.max(val, old_val);
}
else if (agg === "sum") {
summary[column.field] = val + old_val;
}
else if (agg === "mean") {
if (isArray(summary[column.field])) {
summary[column.field].push(val);
}
else {
summary[column.field] = [old_val, val];
}
}
}
else {
summary[column.field] = val;
}
}
}
return summary;
}
function group_data(records, columns, indexes, aggregators) {
const grouped = [];
const index_field = columns[0].field;
for (const record of records) {
const value = record[indexes[0]];
let group = find_group(index_field, value, grouped);
if (group == null) {
group = { _children: [] };
group[index_field] = value;
grouped.push(group);
}
let subgroup = group;
const groups = {};
for (const index of indexes.slice(1)) {
subgroup = find_group(index_field, record[index], subgroup._children);
if (subgroup == null) {
subgroup = { _children: [] };
subgroup[index_field] = record[index];
group._children.push(subgroup);
}
groups[index] = group;
for (const column of columns.slice(1)) {
subgroup[column.field] = record[column];
}
group = subgroup;
}
for (const column of columns.slice(1)) {
subgroup[column.field] = record[column.field];
}
}
const aggs = [];
for (const index of indexes) {
aggs.push((index in aggregators) ? aggregators[index] : "sum");
}
summarize(grouped, columns, aggs);
return grouped;
}
const timestampSorter = function (a, b, _aRow, _bRow, _column, _dir, _params) {
// Bokeh/Panel serializes datetime objects as UNIX timestamps (in milliseconds).
//a, b - the two values being compared
//aRow, bRow - the row components for the values being compared (useful if you need to access additional fields in the row data for the sort)
//column - the column component for the column being sorted
//dir - the direction of the sort ("asc" or "desc")
//sorterParams - sorterParams object from column definition array
// Added an _ in front of some parameters as they're unused and the Typescript compiler was complaining about it.
// const alignEmptyValues = params.alignEmptyValues
let emptyAlign;
emptyAlign = 0;
const opts = { zone: new window.luxon.IANAZone("UTC") };
if (Number.isNaN(a)) {
a = window.luxon.DateTime.fromISO("invalid");
}
else {
a = window.luxon.DateTime.fromMillis(a, opts);
}
if (Number.isNaN(b)) {
b = window.luxon.DateTime.fromISO("invalid");
}
else {
b = window.luxon.DateTime.fromMillis(b, opts);
}
if (!a.isValid) {
emptyAlign = !b.isValid ? 0 : -1;
}
else if (!b.isValid) {
emptyAlign = 1;
}
else {
//compare valid values
return a - b;
}
// Invalid (e.g. NaN) always at the bottom
emptyAlign *= -1;
return emptyAlign;
};
const dateEditor = function (cell, onRendered, success, cancel) {
//cell - the cell component for the editable cell
//onRendered - function to call when the editor has been rendered
//success - function to call to pass the successfully updated value to Tabulator
//cancel - function to call to abort the edit and return to a normal cell
//create and style input
const rawValue = cell.getValue();
const opts = { zone: new window.luxon.IANAZone("UTC") };
let cellValue;
if (rawValue === "NaN" || rawValue === null) {
cellValue = null;
}
else {
cellValue = window.luxon.DateTime.fromMillis(rawValue, opts).toFormat("yyyy-MM-dd");
}
const input = document.createElement("input");
input.setAttribute("type", "date");
input.style.padding = "4px";
input.style.width = "100%";
input.style.boxSizing = "border-box";
input.value = cellValue;
onRendered(() => {
input.focus();
input.style.height = "100%";
});
function onChange() {
const new_val = window.luxon.DateTime.fromFormat(input.value, "yyyy-MM-dd", opts).toMillis();
if (new_val != cellValue) {
success(new_val);
}
else {
cancel();
}
}
//submit new value on blur or change
input.addEventListener("blur", onChange);
//submit new value on enter
input.addEventListener("keydown", (e) => {
if (e.key == "Enter") {
setTimeout(onChange, 100);
}
if (e.key == "Escape") {
setTimeout(cancel, 100);
}
});
return input;
};
const datetimeEditor = function (cell, onRendered, success, cancel) {
//cell - the cell component for the editable cell
//onRendered - function to call when the editor has been rendered
//success - function to call to pass the successfully updated value to Tabulator
//cancel - function to call to abort the edit and return to a normal cell
//create and style input
const rawValue = cell.getValue();
const opts = { zone: new window.luxon.IANAZone("UTC") };
let cellValue;
if (rawValue === "NaN" || rawValue === null) {
cellValue = null;
}
else {
cellValue = window.luxon.DateTime.fromMillis(rawValue, opts).toFormat("yyyy-MM-dd'T'T");
}
const input = document.createElement("input");
input.setAttribute("type", "datetime-local");
input.style.padding = "4px";
input.style.width = "100%";
input.style.boxSizing = "border-box";
input.value = cellValue;
onRendered(() => {
input.focus();
input.style.height = "100%";
});
function onChange() {
const new_val = window.luxon.DateTime.fromFormat(input.value, "yyyy-MM-dd'T'T", opts).toMillis();
if (new_val != cellValue) {
success(new_val);
}
else {
cancel();
}
}
//submit new value on blur or change
input.addEventListener("blur", onChange);
//submit new value on enter
input.addEventListener("keydown", (e) => {
if (e.key == "Enter") {
setTimeout(onChange, 100);
}
if (e.key == "Escape") {
setTimeout(cancel, 100);
}
});
return input;
};
const nestedEditor = function (cell, editorParams) {
//cell - the cell component for the editable cell
const row = cell.getRow().getData();
let values = editorParams.options;
for (const i of editorParams.lookup_order) {
values = row[i] in values ? values[row[i]] : [];
if (Array.isArray(values)) {
break;
}
}
return values ? values : [];
};
function find_column(group, field) {
if (group.columns != null) {
for (const col of group.columns) {
const found = find_column(col, field);
if (found) {
return found;
}
}
}
else {
return group.field === field ? group : null;
}
}
function clone_column(group) {
if (group.columns == null) {
return { ...group };
}
const group_columns = [];
for (const col of group.columns) {
group_columns.push(clone_column(col));
}
return { ...group, columns: group_columns };
}
export class DataTabulatorView extends HTMLBoxView {
static __name__ = "DataTabulatorView";
tabulator;
columns = new Map();
container = null;
_tabulator_cell_updating = false;
_updating_page = false;
_updating_sort = false;
_selection_updating = false;
_last_selected_row = null;
_initializing;
_lastVerticalScrollbarTopPosition = 0;
_lastHorizontalScrollbarLeftPosition = 0;
_applied_styles = false;
_building = false;
_redrawing = false;
_debounced_redraw = null;
_restore_scroll = false;
_updating_scroll = false;
_is_scrolling = false;
connect_signals() {
super.connect_signals();
this._debounced_redraw = debounce(() => this._resize_redraw(), 20, false);
const { configuration, layout, columns, groupby, visible, download, children, expanded, cell_styles, hidden_columns, page_size, page, max_page, frozen_rows, sorters, theme_classes, } = this.model.properties;
this.on_change([configuration, layout, groupby], debounce(() => {
this.invalidate_render();
}, 20, false));
this.on_change(visible, () => {
if (this.model.visible) {
this.tabulator.element.style.visibility = "visible";
}
});
this.on_change(columns, () => {
this.tabulator.setColumns(this.getColumns());
this.setHidden();
});
this.on_change(download, () => {
const ftype = this.model.filename.endsWith(".json") ? "json" : "csv";
this.tabulator.download(ftype, this.model.filename);
});
this.on_change(children, () => this.renderChildren(false));
this.on_change(expanded, () => {
// The first cell is the cell of the frozen _index column.
for (const row of this.tabulator.rowManager.getRows()) {
if (row.cells.length > 0) {
row.cells[0].layoutElement();
}
}
// Make sure the expand icon is changed when expanded is
// changed from Python.
for (const row of this.tabulator.rowManager.getRows()) {
if (row.cells.length > 0) {
const index = row.data._index;
const icon = this.model.expanded.includes(index) ? "▼" : "►";
row.cells[1].element.innerText = icon;
}
}
});
this.on_change(cell_styles, () => {
if (this._applied_styles) {
this.tabulator.redraw(true);
}
this.setStyles();
});
this.on_change(hidden_columns, () => {
this.setHidden();
this.tabulator.redraw(true);
});
this.on_change(page_size, () => this.setPageSize());
this.on_change(page, () => {
if (!this._updating_page) {
this.setPage();
}
});
this.on_change(visible, () => this.setVisibility());
this.on_change(max_page, () => this.setMaxPage());
this.on_change(frozen_rows, () => this.setFrozen());
this.on_change(sorters, () => this.setSorters());
this.on_change(theme_classes, () => this.setCSSClasses(this.tabulator.element));
this.on_change(this.model.source.properties.data, () => {
if (this.tabulator === undefined) {
return;
}
this._restore_scroll = "horizontal";
this._selection_updating = true;
this._updating_scroll = true;
this.setData();
this._updating_scroll = false;
this._selection_updating = false;
this.postUpdate();
});
this.connect(this.model.source.streaming, () => this.addData());
this.connect(this.model.source.patching, () => {
const inds = this.model.source.selected.indices;
this._updating_scroll = true;
this.updateOrAddData();
this._updating_scroll = false;
// Restore indices since updating data may have reset checkbox column
this.model.source.selected.indices = inds;
this.restore_scroll();
});
this.connect(this.model.source.selected.change, () => this.setSelection());
this.connect(this.model.source.selected.properties.indices.change, () => this.setSelection());
}
get groupBy() {
const groupby = (data) => {
const groups = [];
for (const g of this.model.groupby) {
const group = `${g}: ${data[g]}`;
groups.push(group);
}
return groups.join(", ");
};
return (this.model.groupby.length > 0) ? groupby : false;
}
get sorters() {
const sorters = [];
if (this.model.sorters.length > 0) {
sorters.push({ column: "_index", dir: "asc" });
}
for (const sort of this.model.sorters.reverse()) {
if (sort.column === undefined) {
sort.column = sort.field;
}
sorters.push(sort);
}
return sorters;
}
invalidate_render() {
this.tabulator.destroy();
this.tabulator = null;
this.render();
}
redraw(columns = true, rows = true) {
if (this._building || this.tabulator == null || this._redrawing) {
return;
}
this._redrawing = true;
if (columns && (this.tabulator.columnManager.element != null)) {
this.tabulator.columnManager.redraw(true);
}
if (rows && (this.tabulator.rowManager.renderer != null)) {
this.tabulator.rowManager.redraw(true);
this.setStyles();
}
this._redrawing = false;
this._restore_scroll = true;
}
get is_drawing() {
return this._building || this._redrawing || !this.root.has_finished();
}
after_layout() {
super.after_layout();
if (this.tabulator != null && this._initializing && !this.is_drawing) {
this._initializing = false;
this._resize_redraw();
}
}
after_resize() {
super.after_resize();
if (!this._is_scrolling && !this._initializing && !this.is_drawing) {
this._debounced_redraw();
}
}
_resize_redraw() {
if (this._initializing || !this.container || this._building) {
return;
}
const width = this.container.clientWidth;
const height = this.container.clientHeight;
if (!width || !height) {
return;
}
this.redraw(true, true);
this.restore_scroll();
}
setCSSClasses(el) {
el.className = "pnx-tabulator tabulator";
for (const cls of this.model.theme_classes) {
el.classList.add(cls);
}
}
render() {
if (this.tabulator != null) {
this.tabulator.destroy();
}
super.render();
this._initializing = true;
this._building = true;
const container = div({ style: { display: "contents" } });
const el = div({ style: { width: "100%", height: "100%", visibility: "hidden" } });
this.container = el;
this.setCSSClasses(el);
container.appendChild(el);
this.shadow_el.appendChild(container);
const configuration = this.getConfiguration();
this.tabulator = new Tabulator(el, configuration);
this.watch_stylesheets();
this.init_callbacks();
}
style_redraw() {
if (this.model.visible) {
this.tabulator.element.style.visibility = "visible";
}
if (!this._initializing && !this._building) {
this.redraw();
}
}
tableInit() {
this._building = true;
// Patch the ajax request and page data parsing methods
const ajax = this.tabulator.modules.ajax;
ajax.sendRequest = (_url, params, _config) => {
return this.requestPage(params.page, params.sort);
};
this.tabulator.modules.page._parseRemoteData = () => {
return false;
};
}
init_callbacks() {
// Initialization
this.tabulator.on("tableBuilding", () => this.tableInit());
this.tabulator.on("tableBuilt", () => this.tableBuilt());
// Rendering callbacks
this.tabulator.on("selectableRowsCheck", (row) => {
const selectable = this.model.selectable_rows;
return (selectable == null) || selectable.includes(row._row.data._index);
});
this.tabulator.on("tooltips", (cell) => {
return `${cell.getColumn().getField()}: ${cell.getValue()}`;
});
this.tabulator.on("scrollVertical", debounce(() => {
this.setStyles();
}, 50, false));
// Sync state with model
this.tabulator.on("rowSelectionChanged", (data, rows, selected, deselected) => {
this.rowSelectionChanged(data, rows, selected, deselected);
});
this.tabulator.on("rowClick", (e, row) => this.rowClicked(e, row));
this.tabulator.on("cellEdited", (cell) => this.cellEdited(cell));
this.tabulator.on("dataFiltering", (filters) => {
this.record_scroll();
this.model.filters = filters;
});
this.tabulator.on("dataFiltered", (_, rows) => {
if (this._building) {
return;
}
// Ensure that after filtering empty scroll renders
if (rows.length === 0) {
this.tabulator.rowManager.renderEmptyScroll();
}
if (this.model.pagination != null) {
// Ensure that after filtering the page is updated
this.updatePage(this.tabulator.getPage());
}
});
this.tabulator.on("pageLoaded", (pageno) => {
this.updatePage(pageno);
});
this.tabulator.on("renderComplete", () => {
if (this._building) {
return;
}
this.postUpdate();
});
this.tabulator.on("dataSorting", (sorters) => {
const sorts = [];
for (const s of sorters) {
if (s.field !== "_index") {
sorts.push({ field: s.field, dir: s.dir });
}
}
if (this.model.pagination !== "remote") {
this._updating_sort = true;
this.model.sorters = sorts.reverse();
this._updating_sort = false;
}
});
}
tableBuilt() {
this.setSelection();
this.renderChildren();
this.setStyles();
// Track scrolling position and active scroll
const holder = this.shadow_el.querySelector(".tabulator-tableholder");
let scroll_timeout;
if (holder) {
holder.addEventListener("scroll", () => {
this.record_scroll();
this._is_scrolling = true;
clearTimeout(scroll_timeout);
scroll_timeout = setTimeout(() => {
this._is_scrolling = false;
}, 200);
});
}
if (this.model.pagination) {
if (this.model.page_size == null) {
const table = this.shadow_el.querySelector(".tabulator-table");
if (table != null && holder != null) {
const table_height = holder.clientHeight;
let height = 0;
let page_size = null;
const heights = [];
for (let i = 0; i < table.children.length; i++) {
const row_height = table.children[i].clientHeight;
heights.push(row_height);
height += row_height;
if (height > table_height) {
page_size = i;
break;
}
}
if (height < table_height) {
page_size = table.children.length;
const remaining = table_height - height;
page_size += Math.floor(remaining / Math.min(...heights));
}
this.model.page_size = Math.max(page_size || 1, 1);
}
}
this.setMaxPage();
this.tabulator.setPage(this.model.page);
}
this._building = false;
schedule_when(() => {
const initializing = this._initializing;
this._initializing = false;
if (initializing) {
this._resize_redraw();
}
}, () => this.root.has_finished());
}
requestPage(page, sorters) {
return new Promise((resolve, reject) => {
try {
if (page != null && sorters != null) {
this._updating_sort = true;
const sorts = [];
for (const s of sorters) {
if (s.field !== "_index") {
sorts.push({ field: s.field, dir: s.dir });
}
}
this.model.sorters = sorts;
this._updating_sort = false;
this._updating_page = true;
try {
this.model.page = page || 1;
}
finally {
this._updating_page = false;
}
}
resolve([]);
}
catch (err) {
reject(err);
}
});
}
getLayout() {
const layout = this.model.layout;
switch (layout) {
case "fit_data":
return "fitData";
case "fit_data_fill":
return "fitDataFill";
case "fit_data_stretch":
return "fitDataStretch";
case "fit_data_table":
return "fitDataTable";
case "fit_columns":
return "fitColumns";
}
}
getConfiguration() {
// Only use selectable mode if explicitly requested otherwise manually handle selections
const selectableRows = this.model.select_mode === "toggle" ? true : NaN;
const configuration = {
...this.model.configuration,
index: "_index",
nestedFieldSeparator: false,
movableColumns: false,
selectableRows,
columns: this.getColumns(),
initialSort: this.sorters,
layout: this.getLayout(),
pagination: this.model.pagination != null,
paginationMode: this.model.pagination,
paginationSize: this.model.page_size || 20,
paginationInitialPage: 1,
groupBy: this.groupBy,
frozenRows: (row) => {
return (this.model.frozen_rows.length > 0) ? this.model.frozen_rows.includes(row._row.data._index) : false;
},
rowFormatter: (row) => this._render_row(row),
};
if (this.model.pagination === "remote") {
configuration.ajaxURL = "http://panel.pyviz.org";
configuration.sortMode = "remote";
}
const data = this.getData();
return {
...configuration,
data,
};
}
get_child(idx) {
if (this.model.children instanceof Map) {
return this.model.children.get(idx) || null;
}
return null;
}
get child_models() {
const children = [];
for (const idx of this.model.expanded) {
const child = this.get_child(idx);
if (child != null) {
children.push(child);
}
}
return children;
}
get row_index() {
const rows = this.tabulator.getRows();
const lookup = new Map();
for (const row of rows) {
const index = row._row?.data._index;
if (index != null) {
lookup.set(index, row);
}
}
return lookup;
}
renderChildren(all = true) {
new Promise(async (resolve) => {
let new_children = await this.build_child_views();
if (all) {
new_children = this.child_views;
}
resolve(new_children);
}).then((new_children) => {
const lookup = this.row_index;
for (const index of this.model.expanded) {
const model = this.get_child(index);
const row = lookup.get(index);
const view = model == null ? null : this._child_views.get(model);
if (view != null) {
const render = new_children.includes(view);
this._render_row(row, false, render);
}
}
this._update_children();
if (this.tabulator.rowManager.renderer != null) {
this.tabulator.rowManager.adjustTableSize();
}
this.invalidate_layout();
});
}
_render_row(row, resize = true, render = true) {
const index = row._row?.data._index;
if (!this.model.expanded.includes(index)) {
return;
}
const model = this.get_child(index);
const view = model == null ? null : this._child_views.get(model);
if (view == null) {
return;
}
const rowEl = row.getElement();
const style = getComputedStyle(this.tabulator.element.children[1].children[0]);
const bg = style.backgroundColor;
const neg_margin = rowEl.style.paddingLeft ? `-${rowEl.style.paddingLeft}` : "0";
const prev_child = rowEl.children[rowEl.children.length - 1];
let viewEl;
if (prev_child != null && prev_child.className == "row-content") {
viewEl = prev_child;
if (viewEl.children.length && viewEl.children[0] === view.el) {
return;
}
}
else {
viewEl = div({ class: "row-content", style: { background_color: bg, margin_left: neg_margin, max_width: "100%", overflow_x: "hidden" } });
rowEl.appendChild(viewEl);
}
viewEl.appendChild(view.el);
if (render) {
schedule_when(() => {
view.render();
view.after_render();
}, () => this.root.has_finished());
}
if (resize) {
this._update_children();
this.tabulator.rowManager.adjustTableSize();
this.invalidate_layout();
}
}
_expand_render(cell) {
const index = cell._cell.row.data._index;
const icon = this.model.expanded.indexOf(index) < 0 ? "►" : "▼";
return icon;
}
_update_expand(cell) {
const index = cell._cell.row.data._index;
const expanded = [...this.model.expanded];
if (!expanded.includes(index)) {
expanded.push(index);
}
else {
const exp_index = expanded.indexOf(index);
const removed = expanded.splice(exp_index, 1)[0];
const model = this.get_child(removed);
if (model != null) {
const view = this._child_views.get(model);
if (view !== undefined && view.el != null) {
undisplay(view.el);
}
}
}
this.model.expanded = expanded;
if (!expanded.includes(index)) {
return;
}
let ready = true;
for (const idx of this.model.expanded) {
if (this.get_child(idx) == null) {
ready = false;
break;
}
}
if (ready) {
this.renderChildren();
}
}
getData() {
const cds = this.model.source;
let data;
if (cds === null || (cds.columns().length === 0)) {
data = [];
}
else {
data = transform_cds_to_records(cds, true);
}
if (this.model.configuration.dataTree) {
data = group_data(data, this.model.columns, this.model.indexes, this.model.aggregators);
}
return data;
}
getColumns() {
this.columns = new Map();
const config_columns = this.model.configuration?.columns;
const columns = [];
columns.push({ field: "_index", frozen: true, visible: false });
if (config_columns != null) {
for (const column of config_columns) {
const new_column = clone_column(column);
if (column.formatter === "expand") {
const expand = {
hozAlign: "center",
cellClick: (_, cell) => {
this._update_expand(cell);
},
formatter: (cell) => {
return this._expand_render(cell);
},
width: 40,
frozen: true,
};
columns.push(expand);
}
else {
if (new_column.formatter === "rowSelection") {
new_column.cellClick = (_, cell) => {
cell.getRow().toggleSelect();
};
}
columns.push(new_column);
}
}
}
for (const column of this.model.columns) {
let tab_column = null;
if (config_columns != null) {
for (const col of columns) {
tab_column = find_column(col, column.field);
if (tab_column != null) {
break;
}
}
}
if (tab_column == null) {
tab_column = { field: column.field };
}
this.columns.set(column.field, tab_column);
if (tab_column.title == null) {
tab_column.title = column.title;
}
if (tab_column.width == null && column.width != null && column.width != 0) {
tab_column.width = column.width;
}
if (tab_column.formatter == null && column.formatter != null) {
const formatter = column.formatter;
const ftype = formatter.type;
if (ftype === "BooleanFormatter") {
tab_column.formatter = "tickCross";
}
else {
tab_column.formatter = (cell) => {
const row = cell.getRow();
const formatted = column.formatter.doFormat(cell.getRow(), cell, cell.getValue(), null, row.getData());
if (column.formatter.type === "HTMLTemplateFormatter") {
return formatted;
}
const node = div();
node.innerHTML = formatted;
const child = node.children[0];
if (child.innerHTML === "function(){return c.convert(arguments)}") { // If the formatter fails
return "";
}
return child;
};
}
}
if (tab_column.sorter == "timestamp") {
tab_column.sorter = timestampSorter;
}
if (tab_column.sorter === undefined) {
tab_column.sorter = "string";
}
const editor = column.editor;
const ctype = editor.type;
if (tab_column.editor != null) {
if (tab_column.editor === "date") {
tab_column.editor = dateEditor;
}
else if (tab_column.editor === "datetime") {
tab_column.editor = datetimeEditor;
}
else if (tab_column.editor === "nested") {
tab_column.editorParams.valuesLookup = (cell) => {
return nestedEditor(cell, tab_column.editorParams);
};
tab_column.editor = "list";
}
}
else if (ctype === "StringEditor") {
if (editor.completions.length > 0) {
tab_column.editor = "list";
tab_column.editorParams = { values: editor.completions, autocomplete: true, listOnEmpty: true };
}
else {
tab_column.editor = "input";
}
}
else if (ctype === "TextEditor") {
tab_column.editor = "textarea";
}
else if (ctype === "IntEditor" || ctype === "NumberEditor") {
tab_column.editor = "number";
tab_column.editorParams = { step: editor.step };
if (ctype === "IntEditor") {
tab_column.validator = "integer";
}
else {
tab_column.validator = "numeric";
}
}
else if (ctype === "CheckboxEditor") {
tab_column.editor = "tickCross";
}
else if (ctype === "DateEditor") {
tab_column.editor = dateEditor;
}
else if (ctype === "SelectEditor") {
tab_column.editor = "list";
tab_column.editorParams = { values: editor.options };
}
else if (editor != null && editor.default_view != null) {
tab_column.editor = (cell, onRendered, success, cancel) => {
this.renderEditor(column, cell, onRendered, success, cancel);
};
}
tab_column.visible = (tab_column.visible != false && !this.model.hidden_columns.includes(column.field));
tab_column.editable = () => (this.model.editable && (editor.default_view != null));
if (tab_column.headerFilter) {
if (isBoolean(tab_column.headerFilter) && isString(tab_column.editor)) {
tab_column.headerFilter = tab_column.editor;
tab_column.headerFilterParams = tab_column.editorParams;
}
}
for (const sort of this.model.sorters) {
if (tab_column.field === sort.field) {
tab_column.headerSortStartingDir = sort.dir;
}
}
tab_column.cellClick = (_, cell) => {
const index = cell.getData()._index;
const event = new CellClickEvent(column.field, index);
this.model.trigger_event(event);
};
if (config_columns == null) {
columns.push(tab_column);
}
}
for (const col in this.model.buttons) {
const button_formatter = () => {
return this.model.buttons[col];
};
const button_column = {
formatter: button_formatter,
hozAlign: "center",
cellClick: (_, cell) => {
const index = cell.getData()._index;
const event = new CellClickEvent(col, index);
this.model.trigger_event(event);
},
};
columns.push(button_column);
}
return columns;
}
renderEditor(column, cell, onRendered, success, cancel) {
const editor = column.editor;
const view = new editor.default_view({ column, model: editor, parent: this, container: cell._cell.element });
view.initialize();
view.connect_signals();
onRendered(() => {
view.setValue(cell.getValue());
});
view.inputEl.addEventListener("input", () => {
const value = view.serializeValue();
const old_value = cell.getValue();
const validation = view.validate();
if (!validation.valid) {
cancel(validation.msg);
}
if (old_value != null && typeof value != typeof old_value) {
cancel("Mismatching type");
}
else {
success(view.serializeValue());
}
});
return view.inputEl;
}
// Update table
setData() {
if (this._initializing || this._building || !this.tabulator.initialized) {
return Promise.resolve(undefined);
}
const data = this.getData();
if (this.model.pagination != null) {
return this.tabulator.rowManager.setData(data, true, false);
}
else {
return this.tabulator.setData(data);
}
}
addData() {
const rows = this.tabulator.rowManager.getRows();
const last_row = rows[rows.length - 1];
const start = ((last_row?.data._index) || 0);
this._updating_page = true;
const promise = this.setData();
if (this.model.follow) {
promise.then(() => {
if (this.model.pagination) {
this.tabulator.setPage(Math.ceil(this.tabulator.rowManager.getDataCount() / (this.model.page_size || 20)));
}
if (last_row) {
this.tabulator.scrollToRow(start, "top", false);
}
this._updating_page = false;
});
}
else {
this._updating_page = true;
}
}
postUpdate() {
this.setSelection();
this.setStyles();
if (this._restore_scroll) {
const vertical = this._restore_scroll === "horizontal" ? false : true;
const horizontal = this._restore_scroll === "vertical" ? false : true;
this.restore_scroll(horizontal, vertical);
this._restore_scroll = false;
}
}
updateOrAddData() {
// To avoid double updating the tabulator data
if (this._tabulator_cell_updating) {
return;
}
// Temporarily set minHeight to avoid "scroll-to-top" issues caused
// by Tabulator JS entirely destroying the table when .setData is called.
// Inspired by https://github.com/olifolkerd/tabulator/issues/4155
const prev_minheight = this.tabulator.element.style.minHeight;
this.tabulator.element.style.minHeight = `${this.tabulator.element.offsetHeight}px`;
const data = transform_cds_to_records(this.model.source, true);
this.tabulator.setData(data).then(() => {
this.tabulator.element.style.minHeight = prev_minheight;
});
}
setFrozen() {
for (const row of this.model.frozen_rows) {
this.tabulator.getRow(row).freeze();
}
}
setVisibility() {
if (this.tabulator == null) {
return;
}
this.tabulator.element.style.visibility = this.model.visible ? "visible" : "hidden";
}
updatePage(pageno) {
if (this.model.pagination === "local" && this.model.page !== pageno && !this._updating_page) {
this._updating_page = true;
this.model.page = pageno;
this._updating_page = false;
this.setStyles();
}
}
setGroupBy() {
this.tabulator.setGroupBy(this.groupBy);
}
setSorters() {
if (this._updating_sort) {
return;
}
this.tabulator.setSort(this.sorters);
}
setStyles() {
const style_data = this.model.cell_styles.data;
if (this.tabulator == null || this.tabulator.getDataCount() == 0 || style_data == null || !style_data.size) {
return;
}
this._applied_styles = false;
for (const r of style_data.keys()) {
const row_style = style_data.get(r);
const row = this.tabulator.getRow(r);
if (!row) {
continue;
}
const cells = row._row.cells;
for (const c of row_style.keys()) {
const style = row_style.get(c);
const cell = cells[c];
if (cell == null || !style.length) {
continue;
}
const element = cell.element;
for (const s of style) {
let prop, value;
if (isArray(s)) {
[prop, value] = s;
}
else if (!s.includes(":")) {
continue;
}
else {
[prop, value] = s.split(":");
}
element.style.setProperty(prop, value.trimLeft());
this._applied_styles = true;
}
}
}
}
setHidden() {
for (const column of this.tabulator.getColumns()) {
const col = column._column;
if ((col.field == "_index") || this.model.hidden_columns.includes(col.field)) {
column.hide();
}
else {
column.show();
}
}
}
setMaxPage() {
this.tabulator.setMaxPage(this.model.max_page);
if (this.tabulator.modules.page.pagesElement) {
this.tabulator.modules.page._setPageButtons();
}
}
setPage() {
this.tabulator.setPage(Math.min(this.model.max_page, this.model.page));
if (this.model.pagination === "local") {
this.setStyles();
}
}
setPageSize() {
this.tabulator.setPageSize(this.model.page_size);
if (this.model.pagination === "local") {
this.setStyles();
}
}
setSelection() {
if (this.tabulator == null || this._initializing || this._selection_updating || !this.tabulator.initialized) {
return;
}
const indices = this.model.source.selected.indices;
const current_indices = this.tabulator.getSelectedData().map((row) => row._index);
if (JSON.stringify(indices) == JSON.stringify(current_indices)) {
return;
}
this._selection_updating = true;
this.tabulator.deselectRow();
this.tabulator.selectRow(indices);
for (const index of indices) {
const row = this.tabulator.rowManager.findRow(index);
if (row) {
this.tabulator.scrollToRow(index, "center", false).catch(() => { });
}
}
this._selection_updating = false;
}
restore_scroll(horizontal = true, vertical = true) {
if (!(horizontal || vertical)) {
return;
}
const opts = { behavior: "instant" };
if (vertical) {
opts.top = this._lastVerticalScrollbarTopPosition;
}
if (horizontal) {
opts.left = this._lastHorizontalScrollbarLeftPosition;
}
setTimeout(() => {
this._updating_scroll = true;
this.tabulator.rowManager.element.scrollTo(opts);
this._updating_scroll = false;
}, 0);
}
// Update model
record_scroll() {
if (this._updating_scroll) {
return;
}
this._lastVerticalScrollbarTopPosition = this.tabulator.rowManager.element.scrollTop;
this._lastHorizontalScrollbarLeftPosition = this.tabulator.rowManager.element.scrollLeft;
}
rowClicked(e, row) {
if (this._selection_updating ||
this._initializing ||
isString(this.model.select_mode) ||
this.model.select_mode === false || // selection disabled
this.model.configuration.dataTree || // dataTree does not support selection
e.srcElement?.innerText === "►" // expand button
) {
return;
}
let indices = [];
const selected = this.model.source.selected;
const index = row._row.data._index;
if (e.ctrlKey || e.metaKey) {
indices = [...selected.indices];
}
else if (e.shiftKey && this._last_selected_row) {
const rows = row._row.parent.getDisplayRows();
const start_idx = rows.indexOf(this._last_selected_row);
if (start_idx !== -1) {
const end_idx = rows.indexOf(row._row);
const reverse = start_idx > end_idx;
const [start, end] = reverse ? [end_idx + 1, start_idx + 1] : [start_idx, end_idx];
indices = rows.slice(start, end).map((r) => r.data._index);
if (reverse) {
indices = indices.reverse();
}
}
}
const flush = !(e.ctrlKey || e.metaKey || e.shiftKey);
const includes = indices.includes(index);
const remote = this.model.pagination === "remote";
// Toggle the index on or off (if remote we let Python do the toggling)
if (!includes || remote) {
indices.push(index);
}
else {
indices.splice(indices.indexOf(index), 1);
}
// Remove the first selected indices when selectable is an int.
if (isNumber(this.model.select_mode)) {
while (indices.length > this.model.select_mode) {
indices.shift();
}
}
const filtered = this._filter_selected(indices);
if (!remote) {
this.tabulator.deselectRow();
this.tabulator.selectRow(filtered);
}
this._last_selected_row = row._row;
this._selection_updating = true;
if (!remote) {
selected.indices = filtered;
}
this.model.trigger_event(new SelectionEvent(indices, !includes, flush));
this._selection_updating = false;
}
_filter_selected(indices) {
const filtered = [];
for (const ind of indices) {
if (this.model.selectable_rows == null ||
this.model.selectable_rows.indexOf(ind) >= 0) {
filtered.push(ind);
}
}
return filtered;
}
rowSelectionChanged(data, _row, selected, deselected) {
if (this._selection_updating ||
this._initializing ||
isBoolean(this.model.select_mode) ||
isNumber(this.model.select_mode) ||
this.model.configuration.dataTree) {
return;
}
if (this.model.pagination === "remote") {
const selected_indices = selected.map((x) => x._row.data._index);
const deselected_indices = deselected.map((x) => x._row.data._index);
if (selected_indices.length > 0) {
this._selection_updating = true;
this.model.trigger_event(new SelectionEvent(selected_indices, true, false));
}
if (deselected_indices.length > 0) {
this._selection_updating = true;
this.model.trigger_event(new SelectionEvent(deselected_indices, false, false));
}
}
else {
const indices = data.map((row) => row._index);
const filtered = this._filter_selected(indices);
this._selection_updating = indices.length === filtered.length;
this.model.source.selected.indices = filtered;
}
this._selection_updating = false;
}
cellEdited(cell) {
const field = cell._cell.column.field;
const column_def = this.columns.get(field);
const index = cell.getData()._index;
const value = cell._cell.value;
if (column_def.validator === "numeric" && value === "") {
cell.setValue(NaN, true);
return;
}
this._tabulator_cell_updating = true;
comm_settings.debounce = false;
this.model.trigger_event(new TableEditEvent(field, index, true));
try {
this.model.source.patch({ [field]: [[index, value]] });
}
finally {
comm_settings.debounce = true;
this._tabulator_cell_updating = false;
}
this.model.trigger_event(new TableEditEvent(field, index, false));
this.tabulator.scrollToRow(index, "top", false);
}
}
export const TableLayout = Enum("fit_data", "fit_data_fill", "fit_data_stretch", "fit_data_table", "fit_columns");
// The Bokeh .ts model corresponding to the Bokeh .py model
export class DataTabulator extends HTMLBox {
static __name__ = "DataTabulator";
constructor(attrs) {
super(attrs);
}
static __module__ = "panel.models.tabulator";
static {
this.prototype.default_view = DataTabulatorView;
this.define(({ Any, List, Bool, Nullable, Float, Ref, Str }) => ({
aggregators: [Any, {}],
buttons: [Any, {}],
children: [Any, new Map()],
configuration: [Any, {}],
columns: [List(Ref(TableColumn)), []],
download: [Bool, false],
editable: [Bool, true],
expanded: [List(Float), []],
filename: [Str, "table.csv"],
filters: [List(Any), []],
follow: [Bool, true],
frozen_rows: [List(Float), []],
groupby: [List(Str), []],
hidden_columns: [List(Str), []],
indexes: [List(Str), []],
layout: [TableLayout, "fit_data"],
max_page: [Float, 0],
pagination: [Nullable(Str), null],
page: [Float, 0],
page_size: [Nullable(Float), null],
select_mode: [Any, true],
selectable_rows: [Nullable(List(Float)), null],
source: [Ref(ColumnDataSource)],
sorters: [List(Any), []],
cell_styles: [Any, {}],
theme_classes: [List(Str), []],
}));
}
}
//# sourceMappingURL=tabulator.js.map