Skip to content

Commit

Permalink
feat: Add support for clickable links (#1088)
Browse files Browse the repository at this point in the history
Closes #712 

Installed LinkifyJs for link detection. Refer to original issue for
details.

```from deephaven import input_table
from deephaven import dtypes as dht, input_table

my_col_defs = {
    "Integers": dht.int32,
    "Doubles": dht.double,
    "Strings": dht.string,
    "Strings2": dht.string,
}

result = input_table(col_defs=my_col_defs)
```

Run this snippet to create an empty input table and enter links into the
cells.

Bundle size is 314.21 KB on the `main` branch and 322.4 KB on this
branch.

---------

Co-authored-by: Mike Bender <mofojed@users.noreply.github.com>
  • Loading branch information
emilyhuxng and mofojed authored Mar 9, 2023
1 parent 859bfa2 commit f7f918e
Show file tree
Hide file tree
Showing 29 changed files with 1,100 additions and 144 deletions.
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/grid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"classnames": "^2.3.1",
"color-convert": "^2.0.1",
"event-target-shim": "^6.0.2",
"linkifyjs": "^4.1.0",
"lodash.clamp": "^4.0.3",
"memoize-one": "^5.1.1",
"memoizee": "^0.4.15",
Expand Down
136 changes: 84 additions & 52 deletions packages/grid/src/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { CSSProperties, PureComponent, ReactNode } from 'react';
import classNames from 'classnames';
import memoize from 'memoize-one';
import clamp from 'lodash.clamp';
import { EMPTY_ARRAY } from '@deephaven/utils';
import { assertNotNull, EMPTY_ARRAY } from '@deephaven/utils';
import GridMetricCalculator, { GridMetricState } from './GridMetricCalculator';
import GridModel from './GridModel';
import GridMouseHandler, {
Expand All @@ -15,8 +15,9 @@ import GridRange, { GridRangeIndex, SELECTION_DIRECTION } from './GridRange';
import GridRenderer, {
EditingCell,
EditingCellTextSelectionRange,
GridRenderState,
} from './GridRenderer';
import GridUtils, { GridPoint } from './GridUtils';
import GridUtils, { GridPoint, isLinkToken, Token } from './GridUtils';
import {
GridSelectionMouseHandler,
GridColumnMoveMouseHandler,
Expand All @@ -29,6 +30,7 @@ import {
GridVerticalScrollBarMouseHandler,
EditMouseHandler,
GridSeparator,
GridTokenMouseHandler,
} from './mouse-handlers';
import './Grid.scss';
import KeyHandler, { GridKeyboardEvent } from './KeyHandler';
Expand Down Expand Up @@ -109,6 +111,10 @@ export type GridProps = typeof Grid.defaultProps & {
// Callback when the viewport has scrolled or changed
onViewChanged?: (metrics: GridMetrics) => void;

// Callback when a token is clicked
// eslint-disable-next-line react/no-unused-prop-types
onTokenClicked?: (token: Token) => void;

// Renderer for the grid canvas
renderer?: GridRenderer;

Expand Down Expand Up @@ -216,6 +222,11 @@ class Grid extends PureComponent<GridProps, GridState> {
onMovedRowsChanged: (): void => undefined,
onMoveRowComplete: (): void => undefined,
onViewChanged: (): void => undefined,
onTokenClicked: (token: Token) => {
if (isLinkToken(token)) {
window.open(token.href, '_blank', 'noopener,noreferrer');
}
},
stateOverride: {} as Record<string, unknown>,
theme: {
autoSelectColumn: false,
Expand Down Expand Up @@ -291,6 +302,8 @@ class Grid extends PureComponent<GridProps, GridState> {

metrics: GridMetrics | null;

renderState: GridRenderState;

// Track the cursor that is currently added to the document
// Add to document so that when dragging the cursor stays, even if mouse leaves the canvas
// Note: on document, not body so that cursor styling can be combined with
Expand Down Expand Up @@ -342,6 +355,8 @@ class Grid extends PureComponent<GridProps, GridState> {
this.prevMetrics = null;
this.metrics = null;

this.renderState = {} as GridRenderState;

// Track the cursor that is currently added to the document
// Add to document so that when dragging the cursor stays, even if mouse leaves the canvas
// Note: on document, not body so that cursor styling can be combined with
Expand All @@ -367,6 +382,7 @@ class Grid extends PureComponent<GridProps, GridState> {
new GridHorizontalScrollBarMouseHandler(600),
new GridScrollBarCornerMouseHandler(700),
new GridRowTreeMouseHandler(800),
new GridTokenMouseHandler(825),
new GridSelectionMouseHandler(900),
];

Expand Down Expand Up @@ -442,7 +458,6 @@ class Grid extends PureComponent<GridProps, GridState> {
});
window.addEventListener('resize', this.handleResize);

this.updateCanvasScale();
this.updateCanvas();

// apply on mount, so that it works with a static model
Expand Down Expand Up @@ -792,14 +807,17 @@ class Grid extends PureComponent<GridProps, GridState> {

updateCanvas(metrics = this.updateMetrics()): void {
this.updateCanvasScale();
this.updateRenderState();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.renderer.configureContext(this.canvasContext!, this.renderState);

const { onViewChanged } = this.props;
onViewChanged(metrics);

this.drawCanvas(metrics);
}

updateCanvasScale(): void {
private updateCanvasScale(): void {
const { canvas, canvasContext } = this;
if (!canvas) throw new Error('canvas not set');
if (!canvasContext) throw new Error('canvasContext not set');
Expand Down Expand Up @@ -1585,56 +1603,11 @@ class Grid extends PureComponent<GridProps, GridState> {
* must be very quick.
* @param metrics Metrics to use for rendering the grid
*/
drawCanvas(metrics = this.updateMetrics()): void {
private drawCanvas(metrics = this.updateMetrics()): void {
if (!this.canvas) throw new Error('canvas is not set');
if (!this.canvasContext) throw new Error('context not set');

const {
cursorColumn,
cursorRow,
draggingColumn,
draggingColumnSeparator,
draggingRow,
draggingRowOffset,
draggingRowSeparator,
editingCell,
isDraggingHorizontalScrollBar,
isDraggingVerticalScrollBar,
isDragging,
mouseX,
mouseY,
selectedRanges,
} = this.state;
const { model, stateOverride } = this.props;
const { renderer } = this;
const context = this.canvasContext;
const theme = this.getTheme();
const width = this.canvas.clientWidth;
const height = this.canvas.clientHeight;

const renderState = {
width,
height,
context,
theme,
model,
metrics,
mouseX,
mouseY,
selectedRanges,
draggingColumn,
draggingColumnSeparator,
draggingRow,
draggingRowOffset,
draggingRowSeparator,
editingCell,
isDraggingHorizontalScrollBar,
isDraggingVerticalScrollBar,
isDragging,
cursorColumn,
cursorRow,
...stateOverride,
};
const { renderer, canvasContext: context, renderState } = this;

context.save();

Expand Down Expand Up @@ -1816,7 +1789,6 @@ class Grid extends PureComponent<GridProps, GridState> {
* of doing outside of a full componentDidUpdate() call, so we force the update.
* Ideally, we could verify state/metrics without the forced update.
*/
this.updateCanvasScale();
this.updateCanvas();

if (!this.metrics) throw new Error('metrics not set');
Expand Down Expand Up @@ -2138,6 +2110,66 @@ class Grid extends PureComponent<GridProps, GridState> {
);
}

/**
* Gets the render state
* @returns The render state
*/
updateRenderState(): GridRenderState {
if (!this.canvas) throw new Error('canvas is not set');
if (!this.canvasContext) throw new Error('context not set');

const {
cursorColumn,
cursorRow,
draggingColumn,
draggingColumnSeparator,
draggingRow,
draggingRowOffset,
draggingRowSeparator,
editingCell,
isDraggingHorizontalScrollBar,
isDraggingVerticalScrollBar,
isDragging,
mouseX,
mouseY,
selectedRanges,
} = this.state;
const { model, stateOverride } = this.props;
const { metrics } = this;
const context = this.canvasContext;
const theme = this.getTheme();
const width = this.canvas.clientWidth;
const height = this.canvas.clientHeight;

assertNotNull(metrics);

this.renderState = {
width,
height,
context,
theme,
model,
metrics,
mouseX,
mouseY,
selectedRanges,
draggingColumn,
draggingColumnSeparator,
draggingRow,
draggingRowOffset,
draggingRowSeparator,
editingCell,
isDraggingHorizontalScrollBar,
isDraggingVerticalScrollBar,
isDragging,
cursorColumn,
cursorRow,
...stateOverride,
};

return this.renderState;
}

render(): ReactNode {
const { cursor } = this.state;

Expand Down
33 changes: 1 addition & 32 deletions packages/grid/src/GridMetricCalculator.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getOrThrow, trimMap } from './GridMetricCalculator';
import { trimMap } from './GridMetricCalculator';

describe('trimMap', () => {
function makeMap(low = 0, high = 10): Map<number, number> {
Expand Down Expand Up @@ -41,34 +41,3 @@ describe('trimMap', () => {
expectResult(makeMap(0, 100), makeMap(51, 100), 100, 50);
});
});

describe('getOrThrow', () => {
const MAP = new Map([
[5, 10],
[6, 16],
[10, 50],
[100, 250],
]);

it('gets the value if it exists', () => {
expect(getOrThrow(MAP, 5)).toBe(10);
expect(getOrThrow(MAP, 6)).toBe(16);
expect(getOrThrow(MAP, 10)).toBe(50);
expect(getOrThrow(MAP, 100)).toBe(250);
});

it('gets the value if it exists even if default provided', () => {
expect(getOrThrow(MAP, 5, 7)).toBe(10);
expect(getOrThrow(MAP, 6, 7)).toBe(16);
expect(getOrThrow(MAP, 10, 7)).toBe(50);
expect(getOrThrow(MAP, 100, 7)).toBe(250);
});

it('throws if no value set', () => {
expect(() => getOrThrow(MAP, 0)).toThrow();
});

it('returns default value if provided', () => {
expect(getOrThrow(MAP, 0, 7)).toBe(7);
});
});
23 changes: 2 additions & 21 deletions packages/grid/src/GridMetricCalculator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import clamp from 'lodash.clamp';
import { getOrThrow } from '@deephaven/utils';
import GridModel from './GridModel';
import type {
GridMetrics,
Expand All @@ -17,6 +18,7 @@ import { GridFont, GridTheme } from './GridTheme';
import { isExpandableGridModel } from './ExpandableGridModel';
import { DraggingColumn } from './mouse-handlers/GridColumnMoveMouseHandler';

export { getOrThrow } from '@deephaven/utils';
/* eslint class-methods-use-this: "off" */
/* eslint react/destructuring-assignment: "off" */

Expand Down Expand Up @@ -53,27 +55,6 @@ export interface GridMetricState {
draggingColumn: DraggingColumn | null;
}

/**
* Retrieve a value from a map. If the value is not found and no default value is provided, throw.
* Use when the value _must_ be present
* @param map The map to get the value from
* @param key The key to fetch the value for
* @param defaultValue A default value to set if the key is not present
* @returns The value set for that key
*/
export function getOrThrow<K, V>(
map: Map<K, V>,
key: K,
defaultValue: V | undefined = undefined
): V {
const value = map.get(key) ?? defaultValue;
if (value !== undefined) {
return value;
}

throw new Error(`Missing value for key ${key}`);
}

/**
* Trim the provided map in place. Trims oldest inserted items down to the target size if the cache size is exceeded.
* Instead of trimming one item on every tick, we trim half the items so there isn't a cache clear on every new item.
Expand Down
Loading

0 comments on commit f7f918e

Please sign in to comment.