Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export default tseslint.config(
...configs,
{
files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'],
settings: {
react: {
version: 'detect',
}
},
plugins: {
react,
},
Expand Down Expand Up @@ -44,7 +49,8 @@ export default tseslint.config(
],
'@typescript-eslint/no-deprecated': 'warn',
'react/no-deprecated': 'warn',
'@typescript-eslint/unbound-method': 'off'
'@typescript-eslint/unbound-method': 'off',
'no-undefined': 'off'
}
}
)
10,943 changes: 5,508 additions & 5,435 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"url": "https://github.com/retejs/react-plugin/issues"
},
"peerDependencies": {
"react": "^16.8.6 || ^17 || ^18",
"react-dom": "^16.8.6 || ^17 || ^18",
"react": "^16.8.6 || ^17 || ^18 || ^19 ",
"react-dom": "^16.8.6 || ^17 || ^18 || ^19",
"rete": "^2.0.1",
"rete-area-plugin": "^2.0.0",
"rete-render-utils": "^2.0.0",
Expand All @@ -34,19 +34,20 @@
"devDependencies": {
"@babel/preset-react": "^7.18.6",
"@rollup/plugin-commonjs": "^23.0.2",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@types/styled-components": "^5.1.26",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/styled-components": "^5.1.34",
"eslint-plugin-react": "^7.35.0",
"globals": "^15.9.0",
"react": "^18.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"rete": "^2.0.1",
"rete-area-plugin": "^2.0.0",
"rete-cli": "~2.0.1",
"rete-render-utils": "^2.0.0",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-sass": "^1.2.2",
"styled-components": "^5.3.6"
"styled-components": "^6.1.19"
},
"dependencies": {
"@babel/runtime": "^7.21.0",
Expand Down
9 changes: 9 additions & 0 deletions src/globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { JSX as Jsx } from 'react/jsx-runtime'

declare global {
namespace JSX {
type ElementClass = Jsx.ElementClass
type Element = Jsx.Element
type IntrinsicElements = Jsx.IntrinsicElements
}
}
10 changes: 6 additions & 4 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react'
import { BaseSchemes, CanAssignSignal, Scope } from 'rete'

import { RenderPreset } from './presets/types'
import { getRenderer, Renderer } from './renderer'
import { CreateRoot, getRenderer, HasLegacyRender, Renderer } from './renderer'
import { Position, RenderSignal } from './types'
import { Root } from './utils'

Expand All @@ -28,9 +28,11 @@ type Requires<Schemes extends BaseSchemes> =
/**
* Plugin props
*/
export type Props = {
export type Props = HasLegacyRender extends true ? {
/** root factory for React.js 18+ */
createRoot?: (container: Element | DocumentFragment) => any
createRoot?: CreateRoot
} : {
createRoot: CreateRoot
}

/**
Expand All @@ -44,7 +46,7 @@ export class ReactPlugin<Schemes extends BaseSchemes, T = Requires<Schemes>> ext
renderer: Renderer
presets: RenderPreset<Schemes, T>[] = []

constructor(props?: Props) {
constructor(...[props]: HasLegacyRender extends true ? [props?: Props] : [props: Props]) {
super('react-render')
this.renderer = getRenderer({ createRoot: props?.createRoot })

Expand Down
2 changes: 1 addition & 1 deletion src/presets/classic/components/Control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function Control<N extends 'text' | 'number'>(props: { data: ClassicPrese
type={props.data.type}
ref={ref}
readOnly={props.data.readonly}
onChange={e => {
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const val = (props.data.type === 'number'
? +e.target.value
: e.target.value) as typeof props.data['value']
Expand Down
8 changes: 4 additions & 4 deletions src/presets/classic/components/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ export const NodeStyles = styled.div<NodeExtraData & { selected: boolean, styles
cursor: pointer;
box-sizing: border-box;
width: ${props => Number.isFinite(props.width)
? `${props.width}px`
: `${$nodewidth}px`};
? `${props.width}px`
: `${$nodewidth}px`};
height: ${props => Number.isFinite(props.height)
? `${props.height}px`
: 'auto'};
? `${props.height}px`
: 'auto'};
padding-bottom: 6px;
position: relative;
user-select: none;
Expand Down
4 changes: 2 additions & 2 deletions src/presets/context-menu/components/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ export function ItemElement(props: Props) {
const Subitems = props.components?.subitems?.(props.data) || SubitemStyles

return <Component
onClick={e => {
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
props.data.handler()
props.hide()
}}
hasSubitems={Boolean(props.data.subitems)}
onPointerDown={e => {
onPointerDown={(e: React.PointerEvent) => {
e.stopPropagation()
}}
onPointerOver={() => {
Expand Down
6 changes: 4 additions & 2 deletions src/presets/context-menu/components/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ export function Menu(props: Props) {
onMouseOver={() => {
cancelHide()
}}
onMouseLeave={() => hide && hide()}
onWheel={e => {
onMouseLeave={() => {
hide?.()
}}
onWheel={(e: React.WheelEvent) => {
e.stopPropagation()
}}
data-testid="context-menu"
Expand Down
4 changes: 2 additions & 2 deletions src/presets/context-menu/components/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ export function Search(props: { value: string, onChange(value: string): void, co
return (
<Component
value={props.value}
onInput={e => {
onInput={(e: React.FormEvent<HTMLInputElement>) => {
props.onChange((e.target as HTMLInputElement).value)
}}
onPointerDown={e => {
onPointerDown={(e: React.PointerEvent) => {
e.stopPropagation()
}}
data-testid="context-menu-search-input"
Expand Down
2 changes: 1 addition & 1 deletion src/presets/context-menu/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useRef } from 'react'

export function useDebounce(cb: () => void, timeout: number): [null | (() => void), () => void] {
const ref = useRef<ReturnType<typeof setTimeout>>()
const ref = useRef<ReturnType<typeof setTimeout>>(undefined)

function cancel() {
if (ref.current) {
Expand Down
9 changes: 6 additions & 3 deletions src/presets/minimap/components/Minimap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ type Props = {
export function Minimap(props: Props) {
const ref = useRef<HTMLDivElement>(null)
const { width = 0 } = useResizeObserver({
ref
// https://github.com/juliencrn/usehooks-ts/issues/663
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
ref: ref
})
const containerWidth = ref.current?.clientWidth || width
const scale = useCallback((v: number) => v * containerWidth, [containerWidth])
Expand All @@ -44,11 +47,11 @@ export function Minimap(props: Props) {
width: px(props.size * props.ratio),
height: px(props.size)
}}
onPointerDown={e => {
onPointerDown={(e: React.PointerEvent) => {
e.stopPropagation()
e.preventDefault()
}}
onDoubleClick={e => {
onDoubleClick={(e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
if (!ref.current) return
Expand Down
4 changes: 2 additions & 2 deletions src/presets/reroute-pins/Pin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ export function Pin(props: Props) {

return (
<Styles
onPointerDown={e => {
onPointerDown={(e: React.PointerEvent) => {
e.stopPropagation()
e.preventDefault()
drag.start(e)
props.pointerdown()
}}
onContextMenu={e => {
onContextMenu={(e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
props.contextMenu()
Expand Down
12 changes: 9 additions & 3 deletions src/presets/reroute-pins/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@ export function setup<Schemes extends BaseSchemes, K extends PinsRender>(props?:
<Pin
{...pin}
key={pin.id}
contextMenu={() => props?.contextMenu && props.contextMenu(pin.id)}
translate={(dx, dy) => props?.translate && props.translate(pin.id, dx, dy)}
pointerdown={() => props?.pointerdown && props.pointerdown(pin.id)}
contextMenu={() => {
props?.contextMenu?.(pin.id)
}}
translate={(dx, dy) => {
props?.translate?.(pin.id, dx, dy)
}}
pointerdown={() => {
props?.pointerdown?.(pin.id)
}}
pointer={pointer}
/>
))}
Expand Down
78 changes: 56 additions & 22 deletions src/renderer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
/* eslint-disable react/no-render-return-value */
import * as React from 'react'
import * as ReactDOM from 'react-dom'

export type Renderer = { mount: ReactDOM.Renderer, unmount: (container: HTMLElement) => void }
// React 18+ root type
interface Root {
render(children: React.ReactNode): void
unmount(): void
}

export type HasLegacyRender = (typeof ReactDOM) extends { render(...args: any[]): any } ? true : false

export type CreateRoot = (container: Element | DocumentFragment) => Root

type ReactDOMRenderer = (
element: React.ReactElement,
container: HTMLElement
) => React.Component | Element

type CreateRoot = (container: Element | DocumentFragment) => any
export type Renderer = { mount: ReactDOMRenderer, unmount: (container: HTMLElement) => void }

export function getRenderer(props?: { createRoot?: CreateRoot }): Renderer {
const createRoot = props?.createRoot
Expand All @@ -17,51 +30,72 @@ export function getRenderer(props?: { createRoot?: CreateRoot }): Renderer {
const span = document.createElement('span')

container.appendChild(span)
return wrappers.set(container, span).get(container)!
wrappers.set(container, span)
return span
}
function removeWrapper(container: HTMLElement) {

function removeWrapper(container: HTMLElement): void {
const wrapper = wrappers.get(container)

if (wrapper) wrapper.remove()
wrappers.delete(container)
if (wrapper) {
wrapper.remove()
wrappers.delete(container)
}
}

// React 18+ path with createRoot
if (createRoot) {
const roots = new WeakMap<HTMLElement>()
const roots = new WeakMap<HTMLElement, Root>()

return {
mount: ((
element: React.DOMElement<React.DOMAttributes<any>, any>,
container: HTMLElement
): Element => {
mount: (element: React.ReactElement, container: HTMLElement) => {
const wrapper = getWrapper(container)

if (!roots.has(wrapper)) {
roots.set(wrapper, createRoot(wrapper))
let root = roots.get(wrapper)

if (!root) {
root = createRoot(wrapper)
roots.set(wrapper, root)
}
const root = roots.get(wrapper)

return root.render(element)
}) as ReactDOM.Renderer,
root.render(element)
return wrapper.firstElementChild ?? wrapper
},
unmount: (container: HTMLElement) => {
const wrapper = getWrapper(container)
const root = roots.get(wrapper)

if (root) {
root.unmount()
roots.delete(wrapper)
removeWrapper(container)
}
removeWrapper(container)
}
}
}

// React 16-17 legacy path with ReactDOM.render
return {
mount: ((element: React.DOMElement<React.DOMAttributes<any>, any>, container: HTMLElement): Element => {
return ReactDOM.render(element, getWrapper(container))
}) as ReactDOM.Renderer,
mount: (element: React.ReactElement, container: HTMLElement) => {
const wrapper = getWrapper(container)

if ('render' in ReactDOM && typeof ReactDOM.render === 'function') {
const result = ReactDOM.render(element, wrapper) as React.Component | Element

return result || wrapper
}

throw new Error('ReactDOM.render is not available')
},
unmount: (container: HTMLElement) => {
ReactDOM.unmountComponentAtNode(getWrapper(container))
const wrapper = getWrapper(container)

if ('unmountComponentAtNode' in ReactDOM && typeof ReactDOM.unmountComponentAtNode === 'function') {
ReactDOM.unmountComponentAtNode(wrapper)
} else {
throw new Error('ReactDOM.unmountComponentAtNode is not available')
}

removeWrapper(container)
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function copyEvent<T extends Event & Record<string, any>>(e: T) {
const rootPrefix = '__reactContainer$'

type Keys = `${typeof rootPrefix}${string}` | '_reactRootContainer'
type ReactNode = { [key in Keys]?: unknown } & HTMLElement
type ReactNode = Partial<Record<Keys, unknown>> & HTMLElement

export function findReactRoot(element: HTMLElement) {
let current: ReactNode | null = element as ReactNode
Expand Down
5 changes: 3 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ export function syncFlush() {

export function useRete<T extends { destroy(): void }>(create: (el: HTMLElement) => Promise<T>) {
const [container, setContainer] = useState<null | HTMLElement>(null)
const editorRef = useRef<T>()
const editorRef = useRef<T>(undefined)
const [editor, setEditor] = useState<T | null>(null)
const ref = useRef(null)
// compatible RefObject type for React 18 and earlier
const ref = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>

useEffect(() => {
if (container) {
Expand Down