Skip to content

Commit

Permalink
Merge pull request #1244 from react-page/fix-slate-void
Browse files Browse the repository at this point in the history
fix: void elements may throw "Cannot resolve a DOM node from Slate no…
  • Loading branch information
macrozone committed Nov 1, 2022
2 parents 7553bd0 + 6cccacf commit 7d09101
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 92 deletions.
2 changes: 1 addition & 1 deletion examples/pages/examples/formFieldInText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const customSlate = slate((config) => ({
}));
const cellPlugins = [customSlate];
// prettier-ignore
const SAMPLE_VALUE: Value = createValue( { rows: [ [ { plugin: customSlate, data: { slate: [ { type: 'PARAGRAPH/PARAGRAPH', children: [ { text: 'Hello, my Name is ', }, { type: 'FormField', data: { fieldName: 'firstname', placeholder: 'fill Firstname', }, children: [ { text: '', }, ], }, { text: ' ', }, { type: 'FormField', data: { fieldName: 'lastname', placeholder: 'fill Lastname', }, children: [ { text: '', }, ], }, { text: ' and i work as a ', }, { type: 'FormField', data: { fieldName: 'jobDescription', placeholder: 'Job Description', }, children: [ { text: '', }, ], }, ], }, ], }, }, ], ], }, { cellPlugins, lang: 'default' } );
const SAMPLE_VALUE: Value = createValue( { rows: [ [ { plugin: customSlate, data: { slate: [ { type: 'PARAGRAPH/PARAGRAPH', children: [ { text: 'Hello, my Name is ', }, { type: 'FormField', data: { fieldName: 'firstname', placeholder: 'fill Firstname', }, children: [ { text: '', }, ], }, { text: ' ', }, { type: 'FormField', data: { fieldName: 'lastname', placeholder: 'fill Lastname', }, children: [ { text: '', }, ], }, { text: ' and i work as a ', }, { type: 'FormField', data: { fieldName: 'jobDescription', placeholder: 'Job Description', }, children: [ { text: '', }, ], },{ text: '.', } ], }, ], }, }, ], ], }, { cellPlugins, lang: 'default' } );

export default function SimpleExample() {
const [readOnly, setReadOnly] = useState(false);
Expand Down
11 changes: 8 additions & 3 deletions packages/plugins/content/slate/src/components/PluginButton.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useUiTranslator } from '@react-page/editor';
import React, { useCallback, useRef, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { Range } from 'slate';
import { useSlate } from 'slate-react';
import useAddPlugin from '../hooks/useAddPlugin';
import usePluginIsActive from '../hooks/usePluginIsActive';
import usePluginIsDisabled from '../hooks/usePluginIsDisabled';
import useRemovePlugin from '../hooks/useRemovePlugin';
import { useSlate } from 'slate-react';
import type {
PluginButtonProps,
SlatePluginDefinition,
Expand All @@ -28,11 +28,16 @@ function PluginButton(props: Props) {
const editor = useSlate();

const isActive = usePluginIsActive(plugin);
const isVoid =
plugin.pluginType === 'component' &&
(plugin.object === 'inline' || plugin.object === 'block') &&
plugin.isVoid;
const shouldInsertWithText =
plugin.pluginType === 'component' &&
(plugin.object === 'inline' || plugin.object === 'mark') &&
(!editor.selection || Range.isCollapsed(editor.selection)) &&
!isActive;
!isActive &&
!isVoid;

const add = useAddPlugin(plugin);
const remove = useRemovePlugin(plugin);
Expand Down
30 changes: 19 additions & 11 deletions packages/plugins/content/slate/src/components/PluginControls.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { DataTType, JsonSchema } from '@react-page/editor';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import type { BaseRange } from 'slate';
import { Range, Transforms } from 'slate';
import { useSlate } from 'slate-react';
Expand Down Expand Up @@ -71,16 +77,18 @@ function PluginControls(
}, [props.open, setIsVisible, _setOpen]);

const { controls } = plugin;
const Controls = controls
? controls.type === 'autoform'
? (props: SlatePluginControls<any>) => (
<UniformsControls
{...props}
schema={controls?.schema as JsonSchema<any>}
/>
)
: controls.Component
: UniformsControls;
const Controls = useMemo(() => {
return controls
? controls.type === 'autoform'
? (props: SlatePluginControls<any>) => (
<UniformsControls
{...props}
schema={controls?.schema as JsonSchema<any>}
/>
)
: controls.Component
: UniformsControls;
}, [controls]);

const add = useCallback(
(p: any) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FC, PropsWithChildren } from 'react';
import type { FC, MouseEvent } from 'react';
import React, { useCallback, useState } from 'react';

import type { Element } from 'slate';
Expand All @@ -9,18 +9,17 @@ import PluginControls from './PluginControls';

import { useSelected } from 'slate-react';

const VoidEditableElement: FC<
PropsWithChildren<{
plugin: SlatePluginDefinition;
element: Element;
component: React.ReactNode;
}>
> = ({ plugin, element, children, component }) => {
const VoidEditableElement: FC<{
plugin: SlatePluginDefinition;
element: Element;
component: React.ReactNode;
}> = ({ plugin, element, component }) => {
const [showVoidDialog, setShowVoidDialog] = useState(false);
const editor = useSlateStatic();
const onVoidClick = useCallback(
(e: any) => {
(e: MouseEvent) => {
e.stopPropagation();

const path = ReactEditor.findPath(editor, element);

setShowVoidDialog(true);
Expand All @@ -31,6 +30,7 @@ const VoidEditableElement: FC<
const closeVoidDialog = useCallback(() => setShowVoidDialog(false), []);
const Element = plugin.object === 'inline' ? 'span' : 'div';
const selected = useSelected();

return (
<>
{showVoidDialog ? (
Expand All @@ -49,10 +49,7 @@ const VoidEditableElement: FC<
outline: selected ? '1px dotted grey' : undefined,
}}
>
<Element style={{ pointerEvents: 'none' }}>
{children}
{component}
</Element>
<Element style={{ pointerEvents: 'none' }}>{component}</Element>
</Element>
</>
);
Expand Down
14 changes: 8 additions & 6 deletions packages/plugins/content/slate/src/components/renderHooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,16 @@ export const useRenderElement = (
// if block is void, we still need to render children due to some quirks of slate

if (isVoid && !injections.readOnly) {
const Element = matchingPlugin.object === 'inline' ? 'span' : 'div';
return (
<VoidEditableElement
component={component}
element={element}
plugin={matchingPlugin as SlatePluginDefinition}
>
<Element {...attributes} contentEditable={false}>
{children}
</VoidEditableElement>
<VoidEditableElement
component={component}
element={element}
plugin={matchingPlugin as SlatePluginDefinition}
/>
</Element>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import React, { useCallback, useRef, useState } from 'react';
import type { Data } from '../../types';

import type { SlatePluginControls } from '../../types/slatePluginDefinitions';
import { useEffect } from 'react';

function Controls<T extends Data>(
props: SlatePluginControls<T> & {
Expand All @@ -31,6 +32,7 @@ function Controls<T extends Data>(
};

const saveAndCloseWithData = useCallback(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(data: any) => {
props.close();
if (props.shouldInsertWithText) {
Expand Down Expand Up @@ -70,7 +72,7 @@ function Controls<T extends Data>(
>
<DialogContent>
{!props.shouldInsertWithText ? null : (
<div>
<div style={{ marginBottom: '1em' }}>
<TextField
autoFocus={true}
placeholder={'Text'}
Expand All @@ -83,6 +85,7 @@ function Controls<T extends Data>(
{hasSchema && uniformsSchema ? (
<AutoForm
ref={formRef}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
model={props.data as any}
schema={uniformsSchema}
onSubmit={saveAndCloseWithData}
Expand All @@ -91,28 +94,28 @@ function Controls<T extends Data>(
</AutoForm>
) : null}
</DialogContent>
{hasSchema ? (
<DialogActions>
<Button
variant="text"
onClick={onCancel}
style={{ marginRight: 'auto' }}
>
{props.cancelLabel || 'Cancel'}
</Button>
{props.isActive ? (
<Button variant="contained" color="secondary" onClick={onRemove}>
{props.removeLabel || 'Remove'}
<DeleteIcon style={{ marginLeft: 10 }} />
</Button>
) : null}

<DialogActions>
<Button
variant="text"
onClick={onCancel}
style={{ marginRight: 'auto' }}
>
{props.cancelLabel || 'Cancel'}
</Button>
{props.isActive ? (
<Button variant="contained" color="secondary" onClick={onRemove}>
{props.removeLabel || 'Remove'}
<DeleteIcon style={{ marginLeft: 10 }} />
</Button>
) : null}
{hasSchema ? (
<Button variant="contained" color="primary" onClick={onOkClick}>
{props.submitLabel || 'Ok'}
<DoneIcon style={{ marginLeft: 10 }} />
</Button>
</DialogActions>
) : null}
) : null}
</DialogActions>
</Dialog>
);
}
Expand Down
85 changes: 45 additions & 40 deletions packages/plugins/content/slate/src/types/slatePluginDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,49 @@ type MarkProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type NoInfer<T> = [T][T extends any ? 0 : never];

type SlateComponentPluginComponent<T extends Data> =
| keyof JSX.IntrinsicElements
| React.ComponentType<
NoInfer<
{
/**
* the attributes should be passed directly to the rendered html element
*/
attributes?: Record<string, unknown>;
/**
* the style that can be passed directly to the rendered html element
*/
style?: React.CSSProperties;
/**
* className to pass to the renderd html element
*/
className?: string;
/**
* raw child nodes. Usefull in certain niche cases
*/
childNodes: Node[];

/**
* hook that returns true if the current element is focused
*/
useFocused: () => boolean;
/**
* hook that returns true if the current element is selected
*/
useSelected: () => boolean;

/**
* @returns the current text content as an array. Usefull in some advanced use cases
*/
getTextContents: () => string[];

/**
* the childrens should be rendered in non-void plugins
*/
children: ReactNode;
} & T
>
>;
export type SlateComponentPluginDefinition<T extends Data> =
SlateNodeBasePluginDefinition<T> & {
/**
Expand All @@ -160,51 +203,13 @@ export type SlateComponentPluginDefinition<T extends Data> =
getData?: (el: HTMLElement) => T | void;
};

Component: SlateComponentPluginComponent<T>;

/**
* the Component that renders this element. Can be a primitiv component like "div", "p", etc.
* or a complex Component. If its a complex component, you should render the children passed in it
*
*/
Component:
| keyof JSX.IntrinsicElements
| React.ComponentType<
NoInfer<
{
/**
* the attributes should be passed directly to the rendered html element
*/
attributes?: Record<string, unknown>;
/**
* the style that can be passed directly to the rendered html element
*/
style?: React.CSSProperties;
/**
* className to pass to the renderd html element
*/
className?: string;
/**
* raw child nodes. Usefull in certain niche cases
*/
childNodes: Node[];

/**
* hook that returns true if the current element is focused
*/
useFocused: () => boolean;
/**
* hook that returns true if the current element is selected
*/
useSelected: () => boolean;

/**
* @returns the current text content as an array. Usefull in some advanced use cases
*/
getTextContents: () => string[];

children: ReactNode;
} & T
>
>;
} & (ObjectProps | InlineProps | MarkProps);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down

0 comments on commit 7d09101

Please sign in to comment.