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 { 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 @@
/**
* 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 @@ -40,13 +42,13 @@
* @listens render
* @listens unmount
*/
export class ReactPlugin<Schemes extends BaseSchemes, T = Requires<Schemes>> extends Scope<Produces<Schemes>, [Requires<Schemes> | T]> {

Check warning on line 45 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

This line has a length of 136. Maximum allowed is 120

Check warning on line 45 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

This line has a length of 136. Maximum allowed is 120
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 })

Check warning on line 51 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

Unnecessary optional chain on a non-nullish value

Check warning on line 51 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

Unnecessary optional chain on a non-nullish value

this.addPipe(context => {
if (!context || typeof context !== 'object' || !('type' in context)) return context
Expand All @@ -57,7 +59,7 @@
return context
}
if (this.mount(context.data.element, context)) {
return {

Check warning on line 62 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

The '{ ...context, data: { ...context.data, filled: true } } as typeof context' has unsafe 'as' type assertion

Check warning on line 62 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

The '{ ...context, data: { ...context.data, filled: true } } as typeof context' has unsafe 'as' type assertion
...context,
data: {
...context.data,
Expand All @@ -83,12 +85,12 @@
const parent = this.parentScope()

for (const preset of this.presets) {
const result = preset.render(context as any, this)

Check warning on line 88 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

Unexpected any. Specify a different type

Check warning on line 88 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

The 'context as any' has unsafe 'as' type assertion

Check warning on line 88 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

Unsafe argument of type `any` assigned to a parameter of type `Extract<T, { type: "render"; }>`

Check warning on line 88 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

Unexpected any. Specify a different type

Check warning on line 88 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

The 'context as any' has unsafe 'as' type assertion

Check warning on line 88 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

Unsafe argument of type `any` assigned to a parameter of type `Extract<T, { type: "render"; }>`

if (!result) continue

const reactElement = (
<Root rendered={() => void parent.emit({ type: 'rendered', data: context.data } as T)}>

Check warning on line 93 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

The '{ type: 'rendered', data: context.data } as T' has unsafe 'as' type assertion

Check warning on line 93 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

The '{ type: 'rendered', data: context.data } as T' has unsafe 'as' type assertion
{result}
</Root>
)
Expand All @@ -106,8 +108,8 @@
* Adds a preset to the plugin.
* @param preset Preset that can render nodes, connections and other elements.
*/
public addPreset<K>(preset: RenderPreset<Schemes, CanAssignSignal<T, K> extends true ? K : 'Cannot apply preset. Provided signals are not compatible'>) {

Check warning on line 111 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

This line has a length of 155. Maximum allowed is 120

Check warning on line 111 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

This line has a length of 155. Maximum allowed is 120
const local = preset as RenderPreset<Schemes, T>

Check warning on line 112 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

The 'preset as RenderPreset<Schemes, T>' has unsafe 'as' type assertion

Check warning on line 112 in src/index.tsx

View workflow job for this annotation

GitHub Actions / ci / ci

The 'preset as RenderPreset<Schemes, T>' has unsafe 'as' type assertion

if (local.attach) local.attach(this)
this.presets.push(local)
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
Loading