glideapps / glideapps/glide-data-grid

Add Plugin/Extension Mechanism Inspired by Lexical for Common Interactions

Open
#1,044 3 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
5.3k
Forks
428
PR merge metrics
No merged PRs in 30d

Description

This project seems to be active again! First, thank you to the GDG team for open-sourcing such an excellent table component. I have been using it extensively in my own projects and have developed some solutions that might be useful to other developers as well.

Storkbook provides an API for explanations, but it is not very convenient for [sharing code](https://github.com/glideapps/glide-data-grid/issues/1028).

I understand that the GDG team is focused on implementing core canvas features, with most extensions centered around cells. However, there are actually some common interactive extensions that could be reused, providing capabilities beyond just the canvas.

Since I also use [lexical](https://github.com/facebook/lexical), I've noticed that its extension ecosystem is much richer for highly interactive document projects. I believe adding a lexical-style extension mechanism would make GDG more popular and easier to use. I have already implemented this in a forked branch. By exposing the GDG ref API in a context-style, we can achieve this effect.

**Example:**

```javascript
// add-row-plugin.tsx
import * as React from "react";
import { useGlideDataGridContext } from "@glideapps/glide-data-grid";

export function AddRowPlugin() {
const [editor] = useGlideDataGridContext();
return (
editor?.appendRow(0)} style={{ position: "absolute", top: 8, right: 8, zIndex: 10 }}>
add row

);
}
```

Usage:

```javascript
// my-grid.tsx
import * as React from "react";
import { DataEditorAll as DataEditor } from "@glideapps/glide-data-grid";
import { AddRowPlugin } from "./add-row-plugin";

export const PluginDemo = () => {
// ...
return (






);
};
```

A real-world example is a "freeze line" plugin

https://github.com/user-attachments/assets/85632034-0bd9-40f3-94ae-82099fcdd1db

We don't need to actually publish every plugin as a package—other developers can simply copy and paste the plugin code into their own projects. This is very friendly in the AI era.

Here is the code. Simply copy and paste it into your project to use it.

```tsx
import type { GridColumn } from "@glideapps/glide-data-grid";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useGlideDataGridContext } from "@glideapps/glide-data-grid";

const DEFAULT_ROW_NUMBER_COL_WIDTH = 0; // Default width for row numbers

interface FreezeLinePluginProps {
columns: readonly GridColumn[];
freezeColumns: number;
onFreezeColumnsChange: (count: number) => void;
hoverTargetWidth?: number;
freezeLineWidth?: number;
rowNumberColWidth?: number;
}

export const FreezeLinePlugin: React.FC = ({
columns,
freezeColumns,
onFreezeColumnsChange,
hoverTargetWidth = 4,
freezeLineWidth = 2,
rowNumberColWidth = DEFAULT_ROW_NUMBER_COL_WIDTH,
}) => {
const [editor] = useGlideDataGridContext();
const freezeHandleRef = useRef(null);
const [isDragging, setIsDragging] = useState(false);
const [isHovering, setIsHovering] = useState(false);
const dragStartX = useRef(0);
const currentDragX = useRef(null);
const [previewFreezeColumns, setPreviewFreezeColumns] = useState(null);

// Calculate column end positions (right edge relative to start)
const columnEndPositions: number[] = useMemo(() => {
if (!columns) return [];
const positions = [rowNumberColWidth];
let currentLeft = rowNumberColWidth;
for (let i = 0; i < columns.length; i++) {
const column = columns[i];
const colWidth =
typeof (column as any)?.width === "number" && (column as any).width > 0 ? (column as any).width : 300;
currentLeft += colWidth;
positions.push(currentLeft);
}
return positions;
}, [columns, rowNumberColWidth]);

// Calculate the exact position where the *actual* freeze line should be visually
const freezeLinePosition = useMemo(() => {
if (freezeColumns <= 0 || freezeColumns > columns.length) {
return rowNumberColWidth + (hoverTargetWidth / 2 - 1);
}
return (columnEndPositions[freezeColumns] ?? 0) + (hoverTargetWidth / 2 - 1);
}, [columnEndPositions, freezeColumns, hoverTargetWidth, rowNumberColWidth]);

// Calculate the exact position where the *preview* freeze line should snap to
const previewLinePosition = useMemo(() => {
if (
previewFreezeColumns === null ||
previewFreezeColumns < 0 ||
previewFreezeColumns > columns.length ||
columnEndPositions.length === 0
) {
return null;
}
return columnEndPositions[previewFreezeColumns] ?? 0;
}, [columnEndPositions, previewFreezeColumns]);

// Adjust the visual left position of the handle element to center the hover/drag target
const freezeHandleVisualLeft = useMemo(() => {
return Math.max(0, freezeLinePosition - hoverTargetWidth / 2);
}, [freezeLinePosition, hoverTargetWidth]);

const handleMouseDown = useCallback(
(event: React.MouseEvent) => {
const gridRect = editor?.getBounds ? editor.getBounds(0, 0) : undefined;
const rect = gridRect ?? document.body.getBoundingClientRect();
const relativeX = event.clientX - rect.x;

setIsDragging(true);
setPreviewFreezeColumns(freezeColumns);
dragStartX.current = relativeX;
currentDragX.current = relativeX;

document.body.style.userSelect = "none";
document.body.style.cursor = "col-resize";
event.preventDefault();
},
[freezeColumns, editor]
);

const handleMouseMove = useCallback(
(event: MouseEvent) => {
if (!isDragging || !columns || !columnEndPositions || columnEndPositions.length <= 1) return;
const gridRect = editor?.getBounds ? editor.getBounds(0, 0) : undefined;
const rect = gridRect ?? document.body.getBoundingClientRect();
const currentRelativeX = event.clientX - rect.x;
currentDragX.current = currentRelativeX;

const targetLinePosition = currentRelativeX;

let newPreviewFreezeColumns = 0;
let found = false;
for (let k = 0; k < columns.length; k++) {
const colStartPos = columnEndPositions[k];
const colEndPos = columnEndPositions[k + 1];
const midPoint = colStartPos + Math.max(0, colEndPos - colStartPos) / 2;
if (targetLinePosition < midPoint) {
newPreviewFreezeColumns = k;
found = true;
break;
}
}
if (!found) {
newPreviewFreezeColumns = columns.length;
}
newPreviewFreezeColumns = Math.max(0, Math.min(newPreviewFreezeColumns, columns.length));
if (newPreviewFreezeColumns !== previewFreezeColumns) {
setPreviewFreezeColumns(newPreviewFreezeColumns);
}
},
[isDragging, columns, columnEndPositions, previewFreezeColumns, editor]
);

const handleMouseUp = useCallback(() => {
if (previewFreezeColumns !== null && previewFreezeColumns !== freezeColumns) {
onFreezeColumnsChange(previewFreezeColumns);
}
setIsDragging(false);
setPreviewFreezeColumns(null);
dragStartX.current = 0;
currentDragX.current = null;
document.body.style.userSelect = "";
document.body.style.cursor = "";
}, [previewFreezeColumns, freezeColumns, onFreezeColumnsChange]);

useEffect(() => {
if (isDragging) {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
} else {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
}
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
if (isDragging) {
if (document.body.style.userSelect === "none") {
document.body.style.userSelect = "";
}
if (document.body.style.cursor === "col-resize") {
document.body.style.cursor = "";
}
}
};
}, [isDragging, handleMouseMove, handleMouseUp]);

const handleMouseEnter = useCallback(() => {
setIsHovering(true);
}, []);
const handleMouseLeave = useCallback(() => {
if (!isDragging) {
setIsHovering(false);
}
}, [isDragging]);

return (
<>
{columns && columns.length > 0 && (


{(isHovering || isDragging) && (

)}

)}
{isDragging && previewLinePosition !== null && (

)}

);
};

```

These extensions could also be registered as shadcn blocks, allowing other developers to add them with a single CLI command. This would make it easier to distribute both interaction and cell extensions, lowering the barrier for developers.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.