Skip to content
This repository has been archived by the owner on Nov 7, 2022. It is now read-only.

Refactor react node view #463

Merged
merged 1 commit into from
Jul 26, 2022
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
7 changes: 7 additions & 0 deletions packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,12 @@
"lint:type": "run -T tsc",
"test:jest": "run -T jest",
"test:jest:fix": "run -T jest --silent -u"
},
"dependencies": {
"@mashcard/active-support": "workspace:^",
"prosemirror-model": "^1.18.1",
"prosemirror-state": "^1.4.1",
"prosemirror-transform": "^1.6.0",
"prosemirror-view": "^1.27.0"
}
}
25 changes: 25 additions & 0 deletions packages/editor/src/ReactNodeVew/NodeViewContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createContext, FC, PropsWithChildren, RefObject, useContext, useEffect, useMemo, useRef } from 'react'

export interface NodeViewContextProps {
setContentDOM?: (contentDOM: HTMLElement | null) => void
}

export const NodeViewContext = createContext<NodeViewContextProps>({})

export function useNodeContent<T extends HTMLElement>(): RefObject<T> {
const { setContentDOM } = useContext(NodeViewContext)
const ref = useRef<T>(null)

useEffect(() => {
setContentDOM?.(ref.current)
}, [setContentDOM])

return ref
}

export interface NodeViewContainerProps extends PropsWithChildren, NodeViewContextProps {}

export const NodeViewContainer: FC<NodeViewContainerProps> = ({ setContentDOM, children }) => {
const contextValue = useMemo<NodeViewContextProps>(() => ({ setContentDOM }), [setContentDOM])
return <NodeViewContext.Provider value={contextValue}>{children}</NodeViewContext.Provider>
}
116 changes: 116 additions & 0 deletions packages/editor/src/ReactNodeVew/ReactNodeView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { uuid } from '@mashcard/active-support'
import { EditorView, NodeView } from 'prosemirror-view'
import { ComponentType } from 'react'
import { NodePortal } from '../NodePortal'
import { flushSync } from 'react-dom'
import { NodeViewContainer } from './NodeViewContainer'

export { useNodeContent } from './NodeViewContainer'
export { NodeViewContainer }

/**
* The store for nodePortals mutation
*/
export interface NodePortalStore {
/**
* Adds or update node portal.
*/
set: (nodePortal: NodePortal) => void
/**
* Removes a node portal by id.
*/
remove: (id: string) => void
}

/**
* A Prosemirror NodeView rendering view by react component.
* @param component The react component for NodeView rendering.
* @param editorView The editor view of current Prosemirror editor state
* @param nodePortalStore The store for nodePortals mutation
* @param asContainer Define the tag of react portal container, optional argument.
*/
export class ReactNodeView<ComponentProps = {}> implements NodeView {
/**
* Unique id
*/
public readonly id: string
/**
* The store for nodePortals mutation
*/
public readonly nodePortalStore: NodePortalStore
/**
* The react component for NodeView rendering.
*/
public readonly component: ComponentType<ComponentProps>
/**
* The dom container for NodeView
*/
public readonly container: HTMLElement
/**
* The editor view of current Prosemirror editor state
*/
public readonly editorView: EditorView

public contentDOM: HTMLElement | null
public dom: Element

constructor({
component,
componentProps,
editorView,
nodePortalStore,
asContainer
}: {
component: ComponentType<ComponentProps>
componentProps: ComponentProps
editorView: EditorView
nodePortalStore: NodePortalStore
asContainer?: keyof HTMLElementTagNameMap
}) {
this.editorView = editorView
this.container = document.createElement(asContainer ?? 'div')
this.contentDOM = document.createElement('div')

this.nodePortalStore = nodePortalStore
this.id = uuid()
this.component = component

// renders react component synchronously when NodeView created.
flushSync(() => {
this.renderComponent(componentProps)
})

this.dom = this.container
}

public destroy(): void {
this.nodePortalStore.remove(this.id)
this.contentDOM = null
}

/**
* Used to set current contentDOM. Set it via useRef in react component for example.
* @param contentDOM The DOM node that should hold the node's content.
*/
protected readonly setContentDOM = (contentDOM: HTMLElement | null): void => {
if (contentDOM) {
contentDOM.append(...Array.from(this.contentDOM?.childNodes ?? []))
}
this.contentDOM = contentDOM
}

/**
* Renders the component for updating NodeView's view.
*/
protected renderComponent(props: ComponentProps): void {
this.nodePortalStore.set({
id: this.id,
container: this.container,
child: (
<NodeViewContainer setContentDOM={this.setContentDOM}>
<this.component {...props} />
</NodeViewContainer>
)
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { render } from '@testing-library/react'
import { FC } from 'react'
import { NodeViewContainer, useNodeContent } from '../NodeViewContainer'

describe('NodeViewContainer', () => {
it('sets contentDOM correctly', () => {
const setContentDOM = jest.fn()

const TestComponent: FC = () => {
const ref = useNodeContent<HTMLDivElement>()

return <div ref={ref} />
}

render(
<NodeViewContainer setContentDOM={setContentDOM}>
<TestComponent />
</NodeViewContainer>
)

expect(setContentDOM).toBeCalled()
})
})
86 changes: 86 additions & 0 deletions packages/editor/src/ReactNodeVew/__tests__/ReactNodeView.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { render, screen } from '@testing-library/react'
import { FC, ReactElement, ReactNode } from 'react'
import { NodePortalStore, ReactNodeView, useNodeContent } from '../ReactNodeView'

describe('ReactNodeView', () => {
it('renders NodeView component correctly', () => {
const componentProps = {
text: 'text'
}
const component: FC<typeof componentProps> = props => <div>{props.text}</div>

let child: ReactNode

const nodePortalStore: NodePortalStore = {
set(portal) {
child = portal.child
},
remove: jest.fn()
}

// eslint-disable-next-line no-new
new ReactNodeView({
component,
componentProps,
editorView: {} as any,
nodePortalStore
})

render(child as ReactElement)

expect(screen.getByText(componentProps.text)).toBeInTheDocument()
})

it('renders NodeView component with NodeContent correctly', () => {
const Component: FC = () => {
const ref = useNodeContent<HTMLDivElement>()
return <div ref={ref} />
}

const text = 'text'

let child: ReactNode

const nodePortalStore: NodePortalStore = {
set(portal) {
child = portal.child
},
remove: jest.fn()
}

const nodeView = new ReactNodeView({
component: Component,
componentProps: {},
editorView: {} as any,
nodePortalStore
})

const textElement = document.createElement('span')
textElement.innerHTML = text
nodeView.contentDOM?.append(textElement)

render(child as ReactElement)

expect(screen.getByText(text)).toBeInTheDocument()
})

it('destroys NodeView correctly', () => {
const component: FC = () => <div />

const nodePortalStore: NodePortalStore = {
set: jest.fn(),
remove: jest.fn()
}

const nodeView = new ReactNodeView({
component,
componentProps: {},
editorView: {} as any,
nodePortalStore
})

nodeView.destroy()

expect(nodePortalStore.remove).toBeCalled()
})
})
1 change: 1 addition & 0 deletions packages/editor/src/ReactNodeVew/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ReactNodeView'
1 change: 1 addition & 0 deletions packages/editor/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './prosemirror'
export * from './NodePortal'
export * from './ReactNodeVew'
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ exports[`BlockquoteView renders blockquote correctly 1`] = `
style="white-space: normal; pointer-events: unset;"
>
<blockquote
as="blockquote"
class="mc-c-fGUTRT"
data-node-view-content=""
data-placeholder=""
style="white-space: pre-wrap;"
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,8 @@ exports[`CalloutView renders CalloutView with emoji icon correctly 1`] = `
</button>
</div>
<span
as="span"
class="mc-c-hbPmqD"
data-node-view-content=""
data-placeholder=""
style="white-space: pre-wrap;"
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
exports[`CodeBlockView matches snapshot correctly 1`] = `
<div>
<div
class="mc-c-bxRahZ mc-c-eNvXfk"
class="mc-c-gSKecW mc-c-kMAtnc"
data-node-view-wrapper=""
style="white-space: normal; pointer-events: unset;"
>
Expand Down Expand Up @@ -70,14 +70,11 @@ exports[`CodeBlockView matches snapshot correctly 1`] = `
</div>
<pre
class="line-numbers language-javascript"
data-placeholder=""
spellcheck="false"
>
<code
as="code"
class="undefined language-javascript"
data-node-view-content=""
data-placeholder=""
style="white-space: pre-wrap;"
/>
</pre>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ exports[`HeadingView matches snapshot correctly 1`] = `
style="white-space: normal; position: relative; pointer-events: unset;"
>
<h1
as="h1"
class="mc-c-fHQyyx mc-c-fHQyyx-cOVgnz-level-1"
data-node-view-content=""
data-placeholder="placeholder.heading1"
style="white-space: pre-wrap;"
/>
</div>
</div>
Expand All @@ -24,11 +21,8 @@ exports[`HeadingView renders correspond heading according to level level 2 1`] =
style="white-space: normal; position: relative; pointer-events: unset;"
>
<h2
as="h2"
class="mc-c-fHQyyx mc-c-fHQyyx-gPLAez-level-2"
data-node-view-content=""
data-placeholder="placeholder.heading2"
style="white-space: pre-wrap;"
/>
</div>
</div>
Expand All @@ -41,11 +35,8 @@ exports[`HeadingView renders correspond heading according to level level 3 1`] =
style="white-space: normal; position: relative; pointer-events: unset;"
>
<h3
as="h3"
class="mc-c-fHQyyx mc-c-fHQyyx-llzMLw-level-3"
data-node-view-content=""
data-placeholder="placeholder.heading3"
style="white-space: pre-wrap;"
/>
</div>
</div>
Expand All @@ -58,11 +49,8 @@ exports[`HeadingView renders correspond heading according to level level 4 1`] =
style="white-space: normal; position: relative; pointer-events: unset;"
>
<h4
as="h4"
class="mc-c-fHQyyx mc-c-fHQyyx-dWBeGD-level-4"
data-node-view-content=""
data-placeholder="placeholder.heading4"
style="white-space: pre-wrap;"
/>
</div>
</div>
Expand All @@ -75,11 +63,8 @@ exports[`HeadingView renders correspond heading according to level level 5 1`] =
style="white-space: normal; position: relative; pointer-events: unset;"
>
<h5
as="h5"
class="mc-c-fHQyyx mc-c-fHQyyx-hCSGfy-level-5"
data-node-view-content=""
data-placeholder="placeholder.heading5"
style="white-space: pre-wrap;"
/>
</div>
</div>
Expand Down
Loading