Skip to content

Commit 281a045

Browse files
committed
first commit
0 parents  commit 281a045

36 files changed

+13657
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
lib
3+
storybook-static

.storybook/main.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = {
2+
"stories": [
3+
"../src/**/*.stories.mdx",
4+
"../src/**/*.stories.@(js|jsx|ts|tsx)",
5+
"../src/stories/component/*.stories.@(js|jsx|ts|tsx)",
6+
"../src/stories/*.stories.@(js|jsx|ts|tsx)"
7+
],
8+
"addons": [
9+
"@storybook/addon-links",
10+
"@storybook/addon-knobs",
11+
"@storybook/addon-essentials"
12+
]
13+
}

.storybook/preview.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
export const parameters = {
3+
actions: { argTypesRegex: "^on[A-Z].*" },
4+
}

LICENSE

+674
Large diffs are not rendered by default.

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Component library for open review tool
2+
The base storybook/jest/typescript component template was based from vijayt, see below.
3+
4+
# @vijayt/counter
5+
This is just a demo component, part of the boilerplate for putting together a project that publishes components to the NPM registry. Features of the boilerplate include: Compilation using Rollup and TypeScript, Unit / Functional testing using Jest and React Testing library, Visual testing using Storybook. There is a [tutorial](https://vijayt.com/post/boilerplate-for-publishing-components-with-a-storybook/) that explains how the project was put together.

babel.config.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
presets: [
3+
["@babel/preset-env", { targets: { node: "current" } }],
4+
"@babel/preset-typescript",
5+
"@babel/preset-react",
6+
],
7+
};

jest.config.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
roots: ["<rootDir>/src"],
3+
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
4+
testEnvironment: "jsdom",
5+
};

jest.setup.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import "@testing-library/jest-dom";

package.json

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "@vijayt/counter",
3+
"version": "1.0.2",
4+
"main": "lib/index.js",
5+
"repository": "[email protected]:vijayst/react-package-boilerplate.git",
6+
"author": "Vijay T <[email protected]>",
7+
"license": "MIT",
8+
"files": [
9+
"lib"
10+
],
11+
"devDependencies": {
12+
"@babel/core": "^7.14.8",
13+
"@babel/preset-env": "^7.14.9",
14+
"@babel/preset-react": "^7.14.5",
15+
"@babel/preset-typescript": "^7.14.5",
16+
"@storybook/addon-essentials": "^6.3.6",
17+
"@storybook/addon-knobs": "^6.3.1",
18+
"@storybook/react": "^6.3.6",
19+
"@testing-library/jest-dom": "^5.14.1",
20+
"@testing-library/react": "^12.0.0",
21+
"@types/fabric": "^4.5.3",
22+
"@types/jest": "^26.0.24",
23+
"@types/react": "^17.0.15",
24+
"@types/uuid": "^8.3.1",
25+
"babel-loader": "^8.2.2",
26+
"fabric": "^4.6.0",
27+
"jest": "^27.0.6",
28+
"react": "^17.0.2",
29+
"react-dom": "^17.0.2",
30+
"rollup": "^2.55.1",
31+
"rollup-plugin-typescript2": "^0.30.0",
32+
"typescript": "^4.3.5",
33+
"uuid": "^8.3.2"
34+
},
35+
"scripts": {
36+
"build": "rollup -c",
37+
"test": "jest --verbose --watch",
38+
"storybook": "start-storybook",
39+
"build-storybook": "build-storybook"
40+
}
41+
}

rollup.config.js

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Ts from "rollup-plugin-typescript2";
2+
3+
export default {
4+
input: ["src/index.ts"],
5+
output: {
6+
dir: "lib",
7+
format: "esm",
8+
sourcemap: true,
9+
},
10+
plugins: [Ts()],
11+
external: ["react"],
12+
};

src/Button.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from "react";
2+
3+
interface ButtonProps {
4+
onClick: () => void;
5+
}
6+
7+
const Button: React.FC<ButtonProps> = ({ onClick }) => {
8+
return <button onClick={onClick}>+1</button>;
9+
};
10+
11+
export default Button;

src/Count.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from "react";
2+
3+
interface CountProps {
4+
count: number;
5+
}
6+
7+
const Count: React.FC<CountProps> = ({ count }) => {
8+
return <span>{count}</span>;
9+
};
10+
11+
export default Count;

src/Counter.stories.tsx

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from "react";
2+
import Counter, { CounterProps } from "./Counter";
3+
4+
export default {
5+
title: "Counter",
6+
component: Counter,
7+
};
8+
9+
const Template = (args: CounterProps) => <Counter {...args} />;
10+
11+
export const Basic = Template.bind({});
12+
Basic.args = {};
13+
export const WithDefaultCounter = Template.bind({});
14+
WithDefaultCounter.args = { defaultCount: 1000 };

src/Counter.test.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from "react";
2+
import { render, screen } from "@testing-library/react";
3+
import Counter from "./Counter";
4+
5+
test("displays a button", () => {
6+
render(<Counter />);
7+
const button = screen.getByRole("button", { name: /\+1/i });
8+
expect(button).toBeInTheDocument();
9+
});

src/Counter.tsx

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React, { useState, useEffect } from "react";
2+
import Button from "./Button";
3+
import Count from "./Count";
4+
5+
export interface CounterProps {
6+
defaultCount?: number;
7+
}
8+
9+
const Counter: React.FC<CounterProps> = ({ defaultCount = 0 }) => {
10+
const [count, setCount] = useState<number>(defaultCount);
11+
useEffect(() => {
12+
setCount(defaultCount);
13+
}, [defaultCount]);
14+
function handleClick() {
15+
setCount((count) => count + 1);
16+
}
17+
return (
18+
<div>
19+
<Button onClick={handleClick} />
20+
<Count count={count} />
21+
</div>
22+
);
23+
};
24+
25+
export default Counter;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React, { useEffect } from 'react';
2+
import { fabric } from 'fabric';
3+
import { v4 as uuidv4 } from 'uuid';
4+
5+
const DEFAULT_CANVAS_ATTRS: Record<string, number | string | boolean> = {
6+
uniformScaling: false,
7+
preserveObjectStacking: true,
8+
targetFindTolerance: 10,
9+
// note: currently, canvas group selection only the selection rect with shape bounds.
10+
// ref: https://github.com/fabricjs/fabric.js/issues/3773
11+
// So next best thing is to require user's selection rect to contain the entire object.
12+
selectionFullyContained: true,
13+
};
14+
15+
interface FabricCanvasProps {
16+
fabricCanvasRef: React.MutableRefObject<fabric.Canvas>;
17+
width?: number;
18+
height?: number;
19+
backgroundColor?: string;
20+
attrs?: Record<string, number | string | boolean>;
21+
}
22+
23+
const FabricCanvas: React.FC<FabricCanvasProps> = ({
24+
width = 100,
25+
height = 100,
26+
backgroundColor = 'green',
27+
attrs = DEFAULT_CANVAS_ATTRS,
28+
...props
29+
}) => {
30+
const nativeCanvasRef = React.useRef<HTMLCanvasElement | null>(null);
31+
32+
// canvas resize
33+
React.useEffect(() => {
34+
props.fabricCanvasRef.current.setWidth(width);
35+
props.fabricCanvasRef.current.setHeight(height);
36+
}, [width, height]);
37+
38+
React.useEffect(() => {
39+
props.fabricCanvasRef.current.backgroundColor = backgroundColor;
40+
}, [backgroundColor]);
41+
42+
/**
43+
* use effect
44+
*/
45+
// mount object modify handle
46+
useEffect(() => {
47+
if (nativeCanvasRef.current !== null) {
48+
props.fabricCanvasRef.current = new fabric.Canvas(
49+
nativeCanvasRef.current.id,
50+
{
51+
width,
52+
height,
53+
backgroundColor,
54+
...attrs,
55+
},
56+
);
57+
}
58+
59+
return () => {
60+
props.fabricCanvasRef.current?.dispose();
61+
};
62+
}, []);
63+
64+
return <canvas ref={nativeCanvasRef} id={`canvas_${uuidv4()}`} />;
65+
};
66+
67+
export default FabricCanvas;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ElementsAction, AnnotateElement } from './types';
2+
3+
/**
4+
* The reducers captures all the element events, with the aim to sync to another user.
5+
* @param state All the elements on the canvas
6+
* @param action The action.
7+
* @returns Updated set of elements on the canvas
8+
*/
9+
export default function elementsActionReducer(
10+
state: AnnotateElement[],
11+
action: ElementsAction,
12+
): AnnotateElement[] {
13+
switch (action.type) {
14+
case 'addElement':
15+
if (!action.newElement.id) {
16+
throw Error(
17+
`Unexpected add element with no new id: ${action.newElement}`,
18+
);
19+
}
20+
return [...state, action.newElement];
21+
22+
case 'changeElement':
23+
return state.map((element) => {
24+
if (element.id === action.elementUpdates.id) {
25+
return { ...element, ...action.elementUpdates };
26+
} else {
27+
return element;
28+
}
29+
});
30+
31+
case 'removeElement':
32+
const removeIndex = state.reduce((a, c, index) => {
33+
if (action.removeIds.indexOf(c.id) !== -1) {
34+
a.push(index);
35+
}
36+
return a;
37+
}, [] as number[]);
38+
39+
const updatedElements = [...state];
40+
removeIndex.reverse().forEach((removeIndex) => {
41+
updatedElements.splice(removeIndex, 1);
42+
});
43+
44+
return updatedElements;
45+
}
46+
}
+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React from 'react';
2+
import { fabric } from 'fabric';
3+
4+
import {
5+
AnnotateElementType,
6+
AnnotateElement,
7+
UserControllerInputs,
8+
uiDefaults,
9+
} from './types';
10+
import { useCanvasDebugger } from './utils';
11+
import useDrawShapeHandler from './useDrawShapeHandler';
12+
import useCustomSelectCorners from './useCustomSelectCorners';
13+
import useModifyHandler from './useModifyHandler';
14+
import useSyncSelection from './useSyncSelection';
15+
import useApplyAttrsToSelection from './useApplyAttrsToSelection';
16+
import FabricCanvas from './FabricCanvas';
17+
import useRedrawElements from './useRedrawElements';
18+
19+
const REDRAW_ON_ELEMENT_MODIFY = true;
20+
21+
interface AnnotateCanvasProps {
22+
elements: AnnotateElement[];
23+
selection?: string[];
24+
uiState?: UserControllerInputs;
25+
26+
width?: number;
27+
height?: number;
28+
backgroundColor?: string;
29+
30+
onAddElement?: (etype: AnnotateElementType, element: fabric.Object) => void;
31+
onChangeElement?: (element: Partial<AnnotateElement>) => void;
32+
onSelection?: (
33+
selected: string[],
34+
added: string[],
35+
removed: string[],
36+
) => void;
37+
}
38+
39+
const AnnotateCanvas: React.FC<AnnotateCanvasProps> = ({
40+
elements = [],
41+
selection = [],
42+
43+
width = 100,
44+
height = 100,
45+
backgroundColor = '',
46+
47+
uiState = uiDefaults,
48+
49+
...props
50+
}) => {
51+
const fabricCanvasRef = React.useRef<fabric.Canvas>(new fabric.Canvas(''));
52+
53+
useRedrawElements(
54+
fabricCanvasRef,
55+
backgroundColor,
56+
elements,
57+
REDRAW_ON_ELEMENT_MODIFY,
58+
);
59+
useSyncSelection(fabricCanvasRef, props.onSelection);
60+
useCustomSelectCorners(fabricCanvasRef);
61+
useDrawShapeHandler(fabricCanvasRef, uiState, props.onAddElement);
62+
useModifyHandler(fabricCanvasRef, props.onChangeElement);
63+
useApplyAttrsToSelection(
64+
fabricCanvasRef,
65+
uiState,
66+
selection,
67+
elements,
68+
props.onChangeElement,
69+
);
70+
useCanvasDebugger(elements, selection);
71+
72+
return (
73+
<FabricCanvas
74+
fabricCanvasRef={fabricCanvasRef}
75+
width={width}
76+
height={height}
77+
backgroundColor={backgroundColor}
78+
/>
79+
);
80+
};
81+
82+
export default AnnotateCanvas;

0 commit comments

Comments
 (0)