diff --git a/meerkat/interactive/app/src/lib/component/core/number/Number.svelte b/meerkat/interactive/app/src/lib/component/core/number/Number.svelte
index 5a059b13c..230046e42 100644
--- a/meerkat/interactive/app/src/lib/component/core/number/Number.svelte
+++ b/meerkat/interactive/app/src/lib/component/core/number/Number.svelte
@@ -6,12 +6,40 @@
export let precision: number = 3;
export let percentage: boolean = false;
export let editable: boolean = false;
+ export let focused: boolean = false;
export let classes: string = '';
const cellEdit: CallableFunction = getContext('cellEdit');
+ let editableCell: HTMLDivElement;
+
+ $: setFocus(focused);
+
+ function setFocus(focus: boolean) {
+ if (!editableCell) return;
+
+ if (!focus) {
+ editableCell.blur();
+ } else {
+ editableCell.focus();
+
+ // Set the cursor to the end of the div. From
+ // https://stackoverflow.com/a/3866442. Supported on Firefox,
+ // Chrome, Opera, Safari, IE 9+
+ let range = document.createRange();
+ range.selectNodeContents(editableCell);
+ range.collapse(false);
+
+ let selection = window.getSelection();
+ if (selection) {
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ }
+ }
+
let invalid = false;
- let showInvalid = false;
+ let invalidCount = 0;
if (dtype === 'auto') {
// The + operator converts a string or number to a number if possible
@@ -39,15 +67,17 @@
{#if editable}
- (invalid = data !== '' && data !== 'NaN' && isNaN(data))}
- on:change={() => {
+
0}
+ contenteditable="true"
+ bind:innerHTML={data}
+ bind:this={editableCell}
+ on:input={() => {
+ invalid = data !== '' && data !== 'NaN' && isNaN(data);
if (invalid) {
- showInvalid = true;
- setTimeout(() => (showInvalid = false), 1000);
+ invalidCount++;
+ setTimeout(() => invalidCount--, 400);
} else {
cellEdit(data === '' ? 0 : dtype === 'string' ? data : +data);
}
@@ -76,6 +106,6 @@
}
.invalid {
- animation: shake 0.2s ease-in-out 0s 2;
+ animation: shake 0.2s ease-in-out 0s infinite;
}
diff --git a/meerkat/interactive/app/src/lib/component/core/number/__init__.py b/meerkat/interactive/app/src/lib/component/core/number/__init__.py
index 5e0b2eaa5..00e301559 100644
--- a/meerkat/interactive/app/src/lib/component/core/number/__init__.py
+++ b/meerkat/interactive/app/src/lib/component/core/number/__init__.py
@@ -10,3 +10,4 @@ class Number(Component):
percentage: bool = False
classes: str = ""
editable: bool = False
+ focused: bool = False
diff --git a/meerkat/interactive/app/src/lib/component/core/table/Table.svelte b/meerkat/interactive/app/src/lib/component/core/table/Table.svelte
index 38787e04d..83b584218 100644
--- a/meerkat/interactive/app/src/lib/component/core/table/Table.svelte
+++ b/meerkat/interactive/app/src/lib/component/core/table/Table.svelte
@@ -3,8 +3,6 @@
import Pagination from '$lib/shared/pagination/Pagination.svelte';
import { fetchChunk, fetchSchema, dispatch } from '$lib/utils/api';
import { DataFrameChunk, type DataFrameRef, type DataFrameSchema } from '$lib/utils/dataframe';
- import { without } from 'underscore';
- import { onMount } from 'svelte';
import { openModal } from 'svelte-modals';
import type { Endpoint } from '$lib/utils/types';
import { zip } from 'underscore';
@@ -12,17 +10,37 @@
import Cell from '$lib/shared/cell/Cell.svelte';
import { Check, CheckAll, KeyFill } from 'svelte-bootstrap-icons';
+ type Cell = {
+ column: string;
+ keyidx: number; // TODO: decide if this should be number or string
+ posidx: number;
+ value: any;
+ };
+
export let df: DataFrameRef;
export let page: number = 0;
export let perPage: number = 50;
- export let selected: Array = [];
- export let singleSelect: boolean = false;
+ let editMode: boolean = false;
+ let editValue: string = '';
export let onEdit: Endpoint;
- export let onSelect: Endpoint;
+
+ let primarySelectedCell: Cell = { column: '', keyidx: -1, posidx: -1, value: '' };
+ let secondarySelectedCell: Cell = { column: '', keyidx: -1, posidx: -1, value: '' };
+ let activeCells: Array = []; // the cells currently being interacted with
+ let selectedCells: Array = [];
+ let selectedCols: Array = [];
+ let selectedRows: Array = [];
+ export let onSelectCells: Endpoint;
+ export let onSelectCols: Endpoint;
+ export let onSelectRows: Endpoint;
export let classes: string = 'h-fit';
+ let wrapping: string = 'clip';
+
+ let cutoutWidth: number = 0;
+ let cutoutHeight: number = 0;
// Setup row modal
const open_row_modal = (posidx: number) => {
@@ -33,6 +51,29 @@
});
};
+ let columnWidths: Array = [];
+ let columnUnit: string = 'px';
+ let rowHeights: Array = [];
+ let rowUnit: string = 'px';
+
+ function setColumnWidths(clamp: number | null = null) {
+ const headerCells = document.getElementsByClassName('header-cell');
+ // Use i + 1 because the first header-cell is the top-left cutout
+ columnWidths = columnWidths.map((_, i) =>
+ clamp
+ ? Math.min((headerCells[i + 1] as HTMLElement).offsetWidth, clamp)
+ : (headerCells[i + 1] as HTMLElement).offsetWidth
+ );
+ }
+
+ function setRowHeights() {
+ const headerCells = document.getElementsByClassName('header-cell');
+ // Use i + 1 because the first header-cell is the top-left cutout
+ rowHeights = rowHeights.map(
+ (_, i) => (headerCells[i + 1 + columnWidths.length] as HTMLElement).offsetHeight
+ );
+ }
+
// Create placeholder variables for table data.
let schema: Writable = writable({
columns: [],
@@ -42,42 +83,75 @@
});
let chunk: Writable = writable(new DataFrameChunk([], [], [], 0, 'pkey'));
+ // Run this once to set columnWidths
+ fetchSchema({
+ df: df,
+ formatter: 'icon'
+ }).then((newSchema) => {
+ columnWidths = Array(newSchema.columns.length).fill(-1);
+ // After giving 2 seconds for the table to load, clamp the column widths
+ setTimeout(() => setColumnWidths(300), 2000);
+ });
+
$: fetchSchema({
df: df,
formatter: 'icon'
}).then((newSchema) => {
schema.set(newSchema);
+ // After giving 2 seconds for the table to load, set the row heights
+ setTimeout(setRowHeights, 2000);
});
- $: fetchChunk({
+ // Run this once to set rowHeights (24 is same as text-sm + 4)
+ fetchChunk({
df: df,
start: page * perPage,
end: (page + 1) * perPage,
formatter: 'tiny'
- }).then((newChunk) => {
- console.log('here');
- chunk.set(newChunk);
- });
+ }).then((newChunk) => (rowHeights = Array(newChunk.keyidxs.length).fill(-1)));
- export let columnWidths = Array.apply(null, Array($schema.columns.length)).map((x, i) => 256);
- let columnUnit: string = 'px';
+ $: fetchChunk({
+ df: df,
+ start: page * perPage,
+ end: (page + 1) * perPage,
+ formatter: 'tiny'
+ }).then((newChunk) => chunk.set(newChunk));
let resizeProps = {
- colBeingResized: -1,
- x: 0,
- wLeft: 0,
- wRight: 0,
- dx: 0
+ direction: 'x',
+ idxBeingResized: -1,
+ mouseStart: 0,
+ sizeStart: 0,
+ offset: 0
+ };
+
+ /**
+ * Helper function to find the first ancestor of an element with a given
+ * class.
+ *
+ * @param et - The event target
+ * @param cls - The class to search for
+ */
+ const findAncestor = (et: EventTarget | null, cls: string) => {
+ let el: HTMLElement | null = et as HTMLElement;
+ while (el && (el = el.parentElement) && !el.classList.contains(cls));
+ return el;
};
- let resizeMethods = {
- mousedown(colIndex: number) {
+ const resizeMethods = {
+ mousedown(direction: string, idx: number) {
return (e: MouseEvent) => {
- // Update all the resize props
- resizeProps.colBeingResized = colIndex;
- resizeProps.x = e.clientX;
- resizeProps.wLeft = columnWidths[colIndex];
- resizeProps.wRight = columnWidths[colIndex + 1];
+ // Store the current state in the resize props
+ resizeProps.direction = direction;
+ resizeProps.idxBeingResized = idx;
+ resizeProps.mouseStart = direction === 'x' ? e.x : e.y;
+
+ const el = findAncestor(e.target, 'header-cell');
+ if (el) {
+ if (direction === 'x') columnWidths[idx] = el.offsetWidth;
+ else rowHeights[idx] = el.offsetHeight;
+ }
+ resizeProps.sizeStart = direction === 'x' ? columnWidths[idx] : rowHeights[idx];
// Attach listeners for events
window.addEventListener('mousemove', resizeMethods.mousemove);
@@ -86,15 +160,16 @@
},
mousemove(e: MouseEvent) {
- if (resizeProps.colBeingResized === -1) return;
+ if (resizeProps.idxBeingResized === -1) return;
// Determine how far the mouse has been moved
- resizeProps.dx = e.clientX - resizeProps.x;
+ resizeProps.offset = (resizeProps.direction === 'x' ? e.x : e.y) - resizeProps.mouseStart;
- // Update the width of column
- if (resizeProps.wLeft + resizeProps.dx > 164 && resizeProps.wRight - resizeProps.dx > 164) {
- columnWidths[resizeProps.colBeingResized] = resizeProps.wLeft + resizeProps.dx;
- columnWidths[resizeProps.colBeingResized + 1] = resizeProps.wRight - resizeProps.dx;
+ // Update the size
+ const newSize = resizeProps.sizeStart + resizeProps.offset;
+ if (newSize > 5) {
+ if (resizeProps.direction === 'x') columnWidths[resizeProps.idxBeingResized] = newSize;
+ else rowHeights[resizeProps.idxBeingResized] = newSize;
}
},
@@ -104,193 +179,930 @@
}
};
- let tableWidth: number;
- onMount(async () => {
- columnWidths = Array.apply(null, Array($schema.columns.length)).map(
- (x, i) => tableWidth / $schema.columns.length
+ /**
+ * Helper function to convert a column name to the index of that column in
+ * the schema.
+ * @param col - The name of the column
+ */
+ function col2idx(col: string) {
+ return $schema.columns.findIndex((c) => c.name === col);
+ }
+
+ /**
+ * Helper function to convert a keyidx to the index of that keyidx in the
+ * chunk. This also corresponds to the posidx.
+ * @param keyidx - The keyidx of the row
+ */
+ function keyidx2idx(keyidx: number) {
+ // TODO: figure out why we need to do k.toString(). It is a number when it should be a string
+ return $chunk.keyidxs.findIndex((k) => k.toString() === keyidx.toString());
+ }
+
+ /**
+ * Helper function to get the cell at a given column and posidx.
+ * @param col
+ * @param posidx
+ */
+ function getCell(column: string, posidx: number) {
+ return {
+ column,
+ keyidx: parseInt($chunk.keyidxs[posidx]),
+ posidx,
+ value: $chunk.getCell(posidx % perPage, column).data
+ };
+ }
+
+ /**
+ * Helper function to determine if two cells are equal.
+ * @param cell1
+ * @param cell2
+ */
+ function areEqual(cell1: Cell, cell2: Cell) {
+ return cell1.column === cell2.column && cell1.posidx === cell2.posidx;
+ }
+
+ /**
+ * Selects all cells between cell1 and cell2, inclusive.
+ * @param cell1
+ * @param cell2
+ * @param select - If true, will select the cells. If false, will mark as active
+ */
+ function selectRange(cell1: Cell, cell2: Cell, select = true) {
+ if (select) selectedCells = [];
+ else activeCells = [];
+
+ const [col1, row1] = [col2idx(cell1.column), cell1.posidx];
+ const [col2, row2] = [col2idx(cell2.column), cell2.posidx];
+
+ // If the user clicks on the same cell, none should be selected
+ if (col1 !== -1 && col1 === col2 && row1 !== -1 && row1 === row2) return;
+
+ const [colStart, colEnd] = col1 < col2 ? [col1, col2] : [col2, col1];
+ const [keyidxStart, keyidxEnd] = row1 < row2 ? [row1, row2] : [row2, row1];
+
+ for (let i = colStart; i <= colEnd; i++) {
+ for (let j = keyidxStart; j <= keyidxEnd; j++) {
+ const column = $schema.columns[i].name;
+ const keyidx = parseInt($chunk.keyidxs[j]);
+ const posidx = j;
+ const value = $chunk.getCell(posidx, column).data;
+ if (select) selectedCells.push({ column, keyidx, posidx, value });
+ else activeCells.push({ column, keyidx, posidx, value });
+ }
+ }
+
+ // trigger update
+ if (select) selectedCells = selectedCells.slice();
+ else activeCells = activeCells.slice();
+ }
+
+ const selectCellMethods = {
+ mousedown(cell: Cell) {
+ if (editMode) endEdit();
+ return (e: MouseEvent) => {
+ if (e.shiftKey) {
+ secondarySelectedCell = cell;
+ selectRange(primarySelectedCell, secondarySelectedCell);
+ } else if (e.metaKey) {
+ if (getSelectedBitmap(primarySelectedCell.column, primarySelectedCell.keyidx) === 0)
+ selectedCells.push(primarySelectedCell);
+ primarySelectedCell = secondarySelectedCell = cell;
+ activeCells.push(cell);
+ } else {
+ primarySelectedCell = secondarySelectedCell = cell;
+ selectedCells = []; // don't add to selectedCells
+ selectedCols = [];
+ selectedRows = [];
+ }
+ selectedCells = selectedCells.slice(); // trigger update
+
+ // Attach listeners for events
+ window.addEventListener('mousemove', selectCellMethods.mousemove);
+ window.addEventListener('mouseup', selectCellMethods.mouseup);
+ };
+ },
+
+ mousemove(e: MouseEvent) {
+ // Loop through elements beneath the mouse x and y to find the first
+ // element with class 'cell'
+ for (const element of document.elementsFromPoint(e.x, e.y)) {
+ if (!element.classList.contains('cell')) {
+ continue;
+ }
+ const column = element.getAttribute('column') || '';
+ const keyidx = parseInt(element.getAttribute('keyidx') || '-1');
+
+ if (
+ selectedCells.length > 0 &&
+ column === primarySelectedCell.column &&
+ keyidx === primarySelectedCell.keyidx
+ ) {
+ return;
+ }
+
+ const posidx = keyidx2idx(keyidx);
+ secondarySelectedCell = getCell(column, posidx);
+ selectRange(primarySelectedCell, secondarySelectedCell, false);
+
+ break;
+ }
+ },
+
+ mouseup(e: MouseEvent) {
+ // select all activeCells if at least one of them is not already selected
+ let foundUnselected = false;
+ for (const cell of activeCells) {
+ if (getSelectedBitmap(cell.column, cell.keyidx, false) === 0) {
+ foundUnselected = true;
+ break;
+ }
+ }
+
+ if (foundUnselected) {
+ // select all cells
+ selectedCells = selectedCells.concat(activeCells);
+ } else {
+ // unselect all cells
+ for (const cell of activeCells) {
+ const i = selectedCells.findIndex((c) => areEqual(c, cell));
+ if (i !== -1) {
+ if (selectedCells.length === 2) {
+ selectedCells.splice(i, 1);
+ primarySelectedCell = secondarySelectedCell = selectedCells[0];
+ selectedCells = [];
+ } else if (
+ selectedCells.length > 1 &&
+ (areEqual(selectedCells[i], primarySelectedCell) ||
+ areEqual(selectedCells[i], secondarySelectedCell))
+ ) {
+ selectedCells.splice(i, 1);
+ primarySelectedCell = secondarySelectedCell = selectedCells[0];
+ } else {
+ selectedCells.splice(i, 1);
+ }
+ } else if (selectedCols.includes(cell.column)) {
+ selectedCols.splice(selectedCols.indexOf(cell.column), 1);
+ // add all the cells in the column to selectedCells except the one we clicked on
+ for (let i = 0; i < $chunk.keyidxs.length; i++) {
+ if (i !== cell.posidx) {
+ selectedCells.push(getCell(cell.column, i));
+ }
+ }
+ if (selectedCells.length > 0)
+ primarySelectedCell = secondarySelectedCell = selectedCells[0];
+ else primarySelectedCell = secondarySelectedCell = getCell(cell.column, cell.posidx);
+ } else if (selectedRows.includes(cell.keyidx)) {
+ selectedRows.splice(selectedRows.indexOf(cell.keyidx), 1);
+ // add all the cells in the row to selectedCells except the one we clicked on
+ for (let i = 0; i < $schema.columns.length; i++) {
+ if ($schema.columns[i].name !== cell.column) {
+ selectedCells.push(getCell($schema.columns[i].name, cell.posidx));
+ }
+ }
+ if (selectedCells.length > 0)
+ primarySelectedCell = secondarySelectedCell = selectedCells[0];
+ else primarySelectedCell = secondarySelectedCell = getCell(cell.column, cell.posidx);
+ }
+ }
+ }
+
+ activeCells = [];
+
+ let s = [primarySelectedCell];
+ if (selectedCells.length > 0) {
+ // filter out duplicate cells
+ s = selectedCells.filter((cell, i, arr) => arr.findIndex((c) => areEqual(c, cell)) === i);
+ }
+ if (onSelectCells && onSelectCells.endpointId) {
+ dispatch(onSelectCells.endpointId, { detail: { selected: s } });
+ }
+
+ window.removeEventListener('mousemove', selectCellMethods.mousemove);
+ window.removeEventListener('mouseup', selectCellMethods.mouseup);
+ }
+ };
+
+ function onClickCol(e: MouseEvent, column: string) {
+ if (editMode) endEdit();
+ if (e.shiftKey) {
+ selectedCols = [];
+ // loop through all cols between primarySelectedCol and this col
+ const col1 = col2idx(primarySelectedCell.column);
+ const col2 = col2idx(column);
+ const [colStart, colEnd] = col1 < col2 ? [col1, col2] : [col2, col1];
+ for (let i = colStart; i <= colEnd; i++) {
+ selectedCols.push($schema.columns[i].name);
+ }
+ } else if (e.metaKey) {
+ const i = selectedCols.indexOf(column);
+ if (i !== -1) {
+ // remove cells from selectedCells in this col
+ selectedCells = selectedCells.filter((c) => c.column !== column);
+ selectedCols.splice(i, 1);
+
+ // set new primarySelectedCell
+ if (selectedCells.length > 0)
+ primarySelectedCell = secondarySelectedCell = selectedCells[0];
+ else if (selectedCols.length > 0)
+ primarySelectedCell = secondarySelectedCell = getCell(selectedCols[0], 0);
+ else if (selectedRows.length > 0)
+ primarySelectedCell = secondarySelectedCell = getCell(
+ $schema.columns[0].name,
+ selectedRows[0]
+ );
+ else primarySelectedCell = secondarySelectedCell = getCell($schema.columns[0].name, 0);
+ } else {
+ primarySelectedCell = secondarySelectedCell = getCell(column, 0);
+ selectedCols.push(column);
+ }
+ } else {
+ primarySelectedCell = secondarySelectedCell = getCell(column, 0);
+ selectedCells = [];
+ selectedCols = [column];
+ selectedRows = [];
+ }
+ selectedCols = selectedCols.sort((a, b) => col2idx(a) - col2idx(b));
+
+ if (onSelectCols && onSelectCols.endpointId) {
+ dispatch(onSelectCols.endpointId, { detail: { selected: selectedCols } });
+ }
+ }
+
+ function onClickRow(e: MouseEvent, keyidx: number) {
+ if (editMode) endEdit();
+ const posidx = keyidx2idx(keyidx);
+ if (e.shiftKey) {
+ selectedRows = [];
+ // loop through all rows between primarySelectedCol and this row
+ const row1 = primarySelectedCell.posidx;
+ const row2 = posidx;
+ const [rowStart, rowEnd] = row1 < row2 ? [row1, row2] : [row2, row1];
+ for (let i = rowStart; i <= rowEnd; i++) {
+ selectedRows.push(parseInt($chunk.keyidxs[i]));
+ }
+ } else if (e.metaKey) {
+ const i = selectedRows.indexOf(keyidx);
+ if (i !== -1) {
+ // remove cells from selectedCells in this row
+ selectedCells = selectedCells.filter((c) => c.keyidx !== keyidx);
+ selectedRows.splice(i, 1);
+
+ // set new primarySelectedCell
+ if (selectedCells.length > 0)
+ primarySelectedCell = secondarySelectedCell = selectedCells[0];
+ else if (selectedCols.length > 0)
+ primarySelectedCell = secondarySelectedCell = getCell(selectedCols[0], 0);
+ else if (selectedRows.length > 0)
+ primarySelectedCell = secondarySelectedCell = getCell(
+ $schema.columns[0].name,
+ selectedRows[0]
+ );
+ else primarySelectedCell = secondarySelectedCell = getCell($schema.columns[0].name, 0);
+ } else {
+ const column = $schema.columns[0].name;
+ primarySelectedCell = secondarySelectedCell = getCell(column, posidx);
+ selectedRows.push(keyidx);
+ }
+ } else {
+ const column = $schema.columns[0].name;
+ primarySelectedCell = secondarySelectedCell = getCell(column, posidx);
+ selectedCells = [];
+ selectedCols = [];
+ selectedRows = [keyidx];
+ }
+ selectedRows = selectedRows.sort((a, b) => keyidx2idx(a) - keyidx2idx(b));
+
+ if (onSelectRows && onSelectRows.endpointId) {
+ dispatch(onSelectRows.endpointId, { detail: { selected: selectedCols } });
+ }
+ }
+
+ function getColumnSelectClasses(
+ column: string,
+ primarySelectedCell: Cell,
+ activeCells: Array,
+ selectedCells: Array,
+ selectedCols: Array,
+ selectedRows: Array
+ ) {
+ if (selectedCols.includes(column)) return 'bg-violet-700 text-white font-bold ';
+ if (
+ primarySelectedCell.column === column ||
+ activeCells.some((c) => c.column === column) ||
+ selectedCells.some((c) => c.column === column) ||
+ selectedRows.length > 0
+ )
+ return 'bg-violet-200 font-bold ';
+ return '';
+ }
+
+ function getRowSelectClasses(
+ keyidx: number,
+ primarySelectedCell: Cell,
+ activeCells: Array,
+ selectedCells: Array,
+ selectedCols: Array,
+ selectedRows: Array
+ ) {
+ if (selectedRows.includes(keyidx)) return 'bg-violet-700 text-white font-bold ';
+ if (
+ primarySelectedCell.keyidx === keyidx ||
+ activeCells.some((c) => c.keyidx === keyidx) ||
+ selectedCells.some((c) => c.keyidx === keyidx) ||
+ selectedCols.length > 0
+ )
+ return 'bg-violet-200 font-bold ';
+ return '';
+ }
+
+ /**
+ * Helper function that returns a number representing the ways a cell has
+ * been selected or not.
+ *
+ * - ones's place: selectedCols (0 or 1)
+ * - ten's place: selectedRows (0 or 10)
+ * - hundred's place: activeCells (0 or 100)
+ * - thousand's place: number of occurences in selectedCells
+ *
+ * @param column
+ * @param keyidx
+ * @param countActive
+ */
+ function getSelectedBitmap(column: string, keyidx: number, countActive = true) {
+ return (
+ (selectedCols.includes(column) ? 1 : 0) +
+ (selectedRows.includes(keyidx) ? 10 : 0) +
+ (countActive && activeCells.some((c) => c.column === column && c.keyidx === keyidx)
+ ? 100
+ : 0) +
+ selectedCells.filter((c) => c.column === column && c.keyidx === keyidx).length * 1000
);
- columnUnit = 'px';
+ }
+
+ /**
+ * Helper function that returns the number of times a cell is selected.
+ * @param column
+ * @param keyidx
+ * @param posidx
+ * @param primarySelectedCell
+ * @param activeCells
+ * @param selectedCells
+ * @param selectedCols
+ * @param selectedRows
+ */
+ function getSelectedCount(column: string, keyidx: number) {
+ return (
+ (selectedCols.includes(column) ? 1 : 0) +
+ (selectedRows.includes(keyidx) ? 1 : 0) +
+ (activeCells.some((c) => c.column === column && c.keyidx === keyidx) ? 1 : 0) +
+ selectedCells.filter((c) => c.column === column && c.keyidx === keyidx).length
+ );
+ }
+
+ /**
+ * Helper function that returns a string of classes for a cell based on
+ * how it has been selected.
+ *
+ * NOTE: The params activeCells, selectedCells, selectedCols, and
+ * selectedRows are included so that this funciton is called reactively
+ * whenever any of those (or primarySelectedCell) changes.
+ *
+ * @param column
+ * @param keyidx
+ * @param posidx
+ * @param primarySelectedCell
+ * @param activeCells
+ * @param selectedCells
+ * @param selectedCols
+ * @param selectedRows
+ */
+ function getCellSelectClasses(
+ column: string,
+ keyidx: number,
+ posidx: number,
+ primarySelectedCell: Cell,
+ activeCells: Array,
+ selectedCells: Array,
+ selectedCols: Array,
+ selectedRows: Array,
+ editMode: boolean
+ ) {
+ let classes = '';
+ const bitmap = getSelectedBitmap(column, keyidx);
+
+ // Determine background color
+ if (bitmap > 0) {
+ // the max tailwind color is 900, so we cap the count at 9
+ classes += `bg-violet-${Math.min(getSelectedCount(column, keyidx), 9)}00 `;
+ }
+
+ // Determine borders
+ if (primarySelectedCell.column === column && primarySelectedCell.keyidx === keyidx) {
+ if (editMode) classes += 'overflow-visible -ml-px -mt-px ';
+ else classes += 'border-t-2 border-l-2 border-violet-600 ';
+ } else {
+ // border width of 1px, default color slate
+ classes += 'border-t border-l border-slate-300 ';
+
+ if (posidx > 0) {
+ if (areEqual(getCell(column, posidx - 1), primarySelectedCell)) {
+ if (!editMode) classes += 'border-t-2 border-t-violet-600 -mt-px ';
+ } else {
+ const bitmapAbove = getSelectedBitmap(column, parseInt($chunk.keyidxs[posidx - 1]));
+ if (bitmap !== bitmapAbove) classes += 'border-t-violet-600 ';
+ }
+ } else if (bitmap > 0 && posidx === 0) {
+ classes += 'border-t-violet-600 ';
+ }
+
+ if (bitmap > 0 && posidx === $chunk.keyidxs.length - 1)
+ classes += 'border-b border-b-violet-600 ';
+
+ const colidx = col2idx(column);
+ if (colidx > 0) {
+ if (areEqual(getCell($chunk.columns[colidx - 1], posidx), primarySelectedCell)) {
+ if (!editMode) classes += 'border-l-2 border-l-violet-600 -ml-px ';
+ } else {
+ const bitmapLeft = getSelectedBitmap($chunk.columns[colidx - 1], keyidx);
+ if (bitmap !== bitmapLeft) classes += 'border-l-violet-600 ';
+ }
+ } else if (bitmap > 0 && colidx === 0) {
+ classes += 'border-l-violet-600 ';
+ }
+
+ if (bitmap > 0 && colidx === $schema.columns.length - 1)
+ classes += 'border-r border-r-violet-600 ';
+ }
+
+ return classes;
+ }
+
+ /**
+ * Start editing the current cell.
+ */
+ function startEdit() {
+ editMode = true;
+ editValue = primarySelectedCell.value;
+ }
+
+ /**
+ * Finish editing. If callOnEdit is true, save the edit by calling the
+ * onEdit endpoint.
+ * @param callOnEdit Whether or not to call the onEdit endpoint.
+ */
+ function endEdit(callOnEdit: boolean = true) {
+ if (callOnEdit && onEdit && onEdit.endpointId) {
+ const { column, keyidx, posidx } = primarySelectedCell;
+ primarySelectedCell.value = editValue;
+ dispatch(onEdit.endpointId, {
+ detail: {
+ column,
+ keyidx,
+ posidx,
+ value: editValue
+ }
+ });
+ }
+ editMode = false;
+ }
+
+ // Define keyboard shortcuts
+ window.addEventListener('keydown', (e) => {
+ if (primarySelectedCell.column === '') {
+ primarySelectedCell = secondarySelectedCell = getCell($schema.columns[0].name, 0);
+ }
+
+ const colidx = col2idx(primarySelectedCell.column);
+ const posidx = primarySelectedCell.posidx;
+
+ if (e.key === 'a' && e.metaKey) {
+ e.preventDefault();
+ selectedCells = [];
+ selectedCols = $schema.columns.map((c) => c.name);
+ selectedRows = $chunk.keyidxs.map((k) => parseInt(k));
+ } else if (e.key === 'ArrowDown') {
+ if (editMode) return;
+ e.preventDefault();
+ if (e.metaKey) {
+ if (e.shiftKey) {
+ secondarySelectedCell = getCell(secondarySelectedCell.column, $chunk.keyidxs.length - 1);
+ selectRange(primarySelectedCell, secondarySelectedCell);
+ } else {
+ primarySelectedCell = secondarySelectedCell = getCell(
+ primarySelectedCell.column,
+ $chunk.keyidxs.length - 1
+ );
+ }
+ } else if (e.shiftKey) {
+ const posidx2 = secondarySelectedCell.posidx;
+ if (posidx2 + 1 < $chunk.keyidxs.length) {
+ secondarySelectedCell = getCell(secondarySelectedCell.column, posidx2 + 1);
+ selectRange(primarySelectedCell, secondarySelectedCell);
+ } else if (page * perPage + posidx2 < $schema.nrows - 1) {
+ // TODO: flesh out this case
+ page++;
+ secondarySelectedCell = getCell(secondarySelectedCell.column, 0);
+ }
+ } else {
+ if (posidx < $chunk.keyidxs.length - 1) {
+ primarySelectedCell = secondarySelectedCell = getCell(
+ primarySelectedCell.column,
+ posidx + 1
+ );
+ } else if (page * perPage + posidx < $schema.nrows - 1) {
+ page++;
+ primarySelectedCell = secondarySelectedCell = getCell(primarySelectedCell.column, 0);
+ }
+ selectedCells = [];
+ }
+ selectedCols = [];
+ selectedRows = [];
+ } else if (e.key === 'ArrowUp') {
+ if (editMode) return;
+ e.preventDefault();
+ if (e.metaKey) {
+ if (e.shiftKey) {
+ secondarySelectedCell = getCell(secondarySelectedCell.column, 0);
+ selectRange(primarySelectedCell, secondarySelectedCell);
+ } else {
+ primarySelectedCell = secondarySelectedCell = getCell(primarySelectedCell.column, 0);
+ }
+ } else if (e.shiftKey) {
+ const posidx2 = secondarySelectedCell.posidx;
+ if (posidx2 > 0) {
+ secondarySelectedCell = getCell(secondarySelectedCell.column, posidx2 - 1);
+ selectRange(primarySelectedCell, secondarySelectedCell);
+ } else if (page > 0) {
+ // TODO: flesh out this case
+ page--;
+ secondarySelectedCell = getCell(secondarySelectedCell.column, $chunk.keyidxs.length - 1);
+ }
+ } else {
+ if (posidx > 0) {
+ primarySelectedCell = secondarySelectedCell = getCell(
+ primarySelectedCell.column,
+ posidx - 1
+ );
+ } else if (page > 0) {
+ page--;
+ primarySelectedCell = secondarySelectedCell = getCell(
+ primarySelectedCell.column,
+ $chunk.keyidxs.length - 1
+ );
+ }
+ selectedCells = [];
+ }
+ selectedCols = [];
+ selectedRows = [];
+ } else if (e.key === 'ArrowLeft') {
+ if (editMode) return;
+ e.preventDefault();
+ if (e.metaKey) {
+ if (e.shiftKey) {
+ secondarySelectedCell = getCell($schema.columns[0].name, secondarySelectedCell.posidx);
+ selectRange(primarySelectedCell, secondarySelectedCell);
+ } else {
+ primarySelectedCell = secondarySelectedCell = getCell($schema.columns[0].name, posidx);
+ }
+ } else if (e.shiftKey) {
+ const colidx2 = col2idx(secondarySelectedCell.column);
+ const posidx2 = secondarySelectedCell.posidx;
+ if (colidx2 > 0) {
+ secondarySelectedCell = getCell($schema.columns[colidx2 - 1].name, posidx2);
+ selectRange(primarySelectedCell, secondarySelectedCell);
+ }
+ } else {
+ if (colidx > 0) {
+ primarySelectedCell = secondarySelectedCell = getCell(
+ $schema.columns[colidx - 1].name,
+ posidx
+ );
+ }
+ selectedCells = [];
+ }
+ selectedCols = [];
+ selectedRows = [];
+ } else if (e.key === 'ArrowRight') {
+ if (editMode) return;
+ e.preventDefault();
+ if (e.metaKey) {
+ if (e.shiftKey) {
+ secondarySelectedCell = getCell(
+ $schema.columns[$schema.columns.length - 1].name,
+ secondarySelectedCell.posidx
+ );
+ selectRange(primarySelectedCell, secondarySelectedCell);
+ } else {
+ primarySelectedCell = secondarySelectedCell = getCell(
+ $schema.columns[$schema.columns.length - 1].name,
+ posidx
+ );
+ }
+ } else if (e.shiftKey) {
+ const colidx2 = col2idx(secondarySelectedCell.column);
+ const posidx2 = secondarySelectedCell.posidx;
+ if (colidx2 < $schema.columns.length - 1) {
+ secondarySelectedCell = getCell($schema.columns[colidx2 + 1].name, posidx2);
+ selectRange(primarySelectedCell, secondarySelectedCell);
+ }
+ } else {
+ if (colidx < $schema.columns.length - 1) {
+ primarySelectedCell = secondarySelectedCell = getCell(
+ $schema.columns[colidx + 1].name,
+ posidx
+ );
+ }
+ selectedCells = [];
+ }
+ selectedCols = [];
+ selectedRows = [];
+ } else if (e.key === 'Tab') {
+ e.preventDefault();
+ if (editMode) endEdit();
+ // TODO: if there are selected cells, loop through them
+ if (e.shiftKey) {
+ if (colidx > 0) {
+ primarySelectedCell = secondarySelectedCell = getCell(
+ $schema.columns[colidx - 1].name,
+ posidx
+ );
+ }
+ } else {
+ if (colidx < $schema.columns.length - 1) {
+ primarySelectedCell = secondarySelectedCell = getCell(
+ $schema.columns[colidx + 1].name,
+ posidx
+ );
+ }
+ }
+ selectedCells = [];
+ selectedCols = [];
+ selectedRows = [];
+ } else if (e.key === 'Enter') {
+ if (editMode) {
+ if (e.ctrlKey || e.altKey || e.metaKey) {
+ return;
+ } else {
+ e.preventDefault();
+ endEdit();
+ }
+ } else {
+ e.preventDefault();
+ startEdit();
+ }
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ if (editMode) endEdit(false);
+ }
});
-
+
-
-
-
-
-
- {#each $schema.columns as column, col_index}
-