Skip to content

Commit

Permalink
Merge branch 'feat/plate-controller' of https://github.com/udecode/plate
Browse files Browse the repository at this point in the history
 into feat/plate-controller
  • Loading branch information
12joan committed Jan 8, 2024
2 parents 6d50854 + 25a9e5e commit 9141df5
Show file tree
Hide file tree
Showing 15 changed files with 203 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/nervous-nails-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@udecode/plate-serializer-md": minor
---

New option in markdown deserializer plugin: `indentList?: boolean`. Set it to true if you're using Indent List plugin instead of the List plugin.
5 changes: 5 additions & 0 deletions .changeset/violet-years-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@udecode/plate-core": patch
---

Ensure that beforeinput event is handled as a React.SyntheticEvent rather than a native DOM event
6 changes: 6 additions & 0 deletions apps/www/content/docs/components/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ Since Plate UI is not a component library, a changelog is maintained here.

Use the [CLI](https://platejs.org/docs/components/cli) to install the latest version of the components.

## January 2024 #7

### January 2 #7.1

- `dropdown-menu` - fix: do not exclude `className` in `DropdownMenuContent`

## December 2023 #6

### December 27 #6.3
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/registry/styles/default/dropdown-menu.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"files": [
{
"name": "dropdown-menu.tsx",
"content": "'use client';\n\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';\nimport {\n cn,\n createPrimitiveElement,\n withCn,\n withProps,\n withRef,\n withVariants,\n} from '@udecode/cn';\nimport { cva } from 'class-variance-authority';\n\nimport { Icons } from '@/components/icons';\n\nexport const DropdownMenu = DropdownMenuPrimitive.Root;\nexport const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\nexport const DropdownMenuGroup = DropdownMenuPrimitive.Group;\nexport const DropdownMenuPortal = DropdownMenuPrimitive.Portal;\nexport const DropdownMenuSub = DropdownMenuPrimitive.Sub;\nexport const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nexport const DropdownMenuSubTrigger = withRef<\n typeof DropdownMenuPrimitive.SubTrigger,\n {\n inset?: boolean;\n }\n>(({ className, inset, children, ...props }, ref) => (\n <DropdownMenuPrimitive.SubTrigger\n ref={ref}\n className={cn(\n 'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',\n 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n inset && 'pl-8',\n className\n )}\n {...props}\n >\n {children}\n <Icons.chevronRight className=\"ml-auto h-4 w-4\" />\n </DropdownMenuPrimitive.SubTrigger>\n));\n\nexport const DropdownMenuSubContent = withCn(\n DropdownMenuPrimitive.SubContent,\n 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2'\n);\n\nconst DropdownMenuContentVariants = withProps(DropdownMenuPrimitive.Content, {\n sideOffset: 4,\n className: cn(\n 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2'\n ),\n});\n\nexport const DropdownMenuContent = withRef<\n typeof DropdownMenuPrimitive.Content\n>(({ className, ...props }, ref) => (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuContentVariants ref={ref} {...props} />\n </DropdownMenuPrimitive.Portal>\n));\n\nconst menuItemVariants = cva(\n cn(\n 'relative flex h-9 cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors',\n 'focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'\n ),\n {\n variants: {\n inset: {\n true: 'pl-8',\n },\n },\n }\n);\n\nexport const DropdownMenuItem = withVariants(\n DropdownMenuPrimitive.Item,\n menuItemVariants,\n ['inset']\n);\n\nexport const DropdownMenuCheckboxItem = withRef<\n typeof DropdownMenuPrimitive.CheckboxItem\n>(({ className, children, ...props }, ref) => (\n <DropdownMenuPrimitive.CheckboxItem\n ref={ref}\n className={cn(\n 'relative flex select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n 'cursor-pointer',\n className\n )}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <Icons.check className=\"h-4 w-4\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.CheckboxItem>\n));\n\nexport const DropdownMenuRadioItem = withRef<\n typeof DropdownMenuPrimitive.RadioItem,\n {\n hideIcon?: boolean;\n }\n>(({ className, children, hideIcon, ...props }, ref) => (\n <DropdownMenuPrimitive.RadioItem\n ref={ref}\n className={cn(\n 'relative flex select-none items-center rounded-sm pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n 'h-9 cursor-pointer px-2 data-[state=checked]:bg-accent data-[state=checked]:text-accent-foreground',\n className\n )}\n {...props}\n >\n {!hideIcon && (\n <span className=\"absolute right-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <Icons.check className=\"h-4 w-4\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n )}\n {children}\n </DropdownMenuPrimitive.RadioItem>\n));\n\nconst dropdownMenuLabelVariants = cva(\n cn('select-none px-2 py-1.5 text-sm font-semibold'),\n {\n variants: {\n inset: {\n true: 'pl-8',\n },\n },\n }\n);\n\nexport const DropdownMenuLabel = withVariants(\n DropdownMenuPrimitive.Label,\n dropdownMenuLabelVariants,\n ['inset']\n);\n\nexport const DropdownMenuSeparator = withCn(\n DropdownMenuPrimitive.Separator,\n '-mx-1 my-1 h-px bg-muted'\n);\n\nexport const DropdownMenuShortcut = withCn(\n createPrimitiveElement('span'),\n 'ml-auto text-xs tracking-widest opacity-60'\n);\n\nexport const useOpenState = () => {\n const [open, setOpen] = useState(false);\n\n const onOpenChange = useCallback(\n (_value = !open) => {\n setOpen(_value);\n },\n [open]\n );\n\n return {\n open,\n onOpenChange,\n };\n};\n"
"content": "'use client';\n\nimport * as React from 'react';\nimport { useCallback, useState } from 'react';\nimport * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';\nimport {\n cn,\n createPrimitiveElement,\n withCn,\n withProps,\n withRef,\n withVariants,\n} from '@udecode/cn';\nimport { cva } from 'class-variance-authority';\n\nimport { Icons } from '@/components/icons';\n\nexport const DropdownMenu = DropdownMenuPrimitive.Root;\nexport const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\nexport const DropdownMenuGroup = DropdownMenuPrimitive.Group;\nexport const DropdownMenuPortal = DropdownMenuPrimitive.Portal;\nexport const DropdownMenuSub = DropdownMenuPrimitive.Sub;\nexport const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nexport const DropdownMenuSubTrigger = withRef<\n typeof DropdownMenuPrimitive.SubTrigger,\n {\n inset?: boolean;\n }\n>(({ className, inset, children, ...props }, ref) => (\n <DropdownMenuPrimitive.SubTrigger\n ref={ref}\n className={cn(\n 'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',\n 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n inset && 'pl-8',\n className\n )}\n {...props}\n >\n {children}\n <Icons.chevronRight className=\"ml-auto h-4 w-4\" />\n </DropdownMenuPrimitive.SubTrigger>\n));\n\nexport const DropdownMenuSubContent = withCn(\n DropdownMenuPrimitive.SubContent,\n 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2'\n);\n\nconst DropdownMenuContentVariants = withProps(DropdownMenuPrimitive.Content, {\n sideOffset: 4,\n className: cn(\n 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2'\n ),\n});\n\nexport const DropdownMenuContent = withRef<\n typeof DropdownMenuPrimitive.Content\n>(({ ...props }, ref) => (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuContentVariants ref={ref} {...props} />\n </DropdownMenuPrimitive.Portal>\n));\n\nconst menuItemVariants = cva(\n cn(\n 'relative flex h-9 cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors',\n 'focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'\n ),\n {\n variants: {\n inset: {\n true: 'pl-8',\n },\n },\n }\n);\n\nexport const DropdownMenuItem = withVariants(\n DropdownMenuPrimitive.Item,\n menuItemVariants,\n ['inset']\n);\n\nexport const DropdownMenuCheckboxItem = withRef<\n typeof DropdownMenuPrimitive.CheckboxItem\n>(({ className, children, ...props }, ref) => (\n <DropdownMenuPrimitive.CheckboxItem\n ref={ref}\n className={cn(\n 'relative flex select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n 'cursor-pointer',\n className\n )}\n {...props}\n >\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <Icons.check className=\"h-4 w-4\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.CheckboxItem>\n));\n\nexport const DropdownMenuRadioItem = withRef<\n typeof DropdownMenuPrimitive.RadioItem,\n {\n hideIcon?: boolean;\n }\n>(({ className, children, hideIcon, ...props }, ref) => (\n <DropdownMenuPrimitive.RadioItem\n ref={ref}\n className={cn(\n 'relative flex select-none items-center rounded-sm pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n 'h-9 cursor-pointer px-2 data-[state=checked]:bg-accent data-[state=checked]:text-accent-foreground',\n className\n )}\n {...props}\n >\n {!hideIcon && (\n <span className=\"absolute right-2 flex h-3.5 w-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <Icons.check className=\"h-4 w-4\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n )}\n {children}\n </DropdownMenuPrimitive.RadioItem>\n));\n\nconst dropdownMenuLabelVariants = cva(\n cn('select-none px-2 py-1.5 text-sm font-semibold'),\n {\n variants: {\n inset: {\n true: 'pl-8',\n },\n },\n }\n);\n\nexport const DropdownMenuLabel = withVariants(\n DropdownMenuPrimitive.Label,\n dropdownMenuLabelVariants,\n ['inset']\n);\n\nexport const DropdownMenuSeparator = withCn(\n DropdownMenuPrimitive.Separator,\n '-mx-1 my-1 h-px bg-muted'\n);\n\nexport const DropdownMenuShortcut = withCn(\n createPrimitiveElement('span'),\n 'ml-auto text-xs tracking-widest opacity-60'\n);\n\nexport const useOpenState = () => {\n const [open, setOpen] = useState(false);\n\n const onOpenChange = useCallback(\n (_value = !open) => {\n setOpen(_value);\n },\n [open]\n );\n\n return {\n open,\n onOpenChange,\n };\n};\n"
}
],
"type": "components:plate-ui"
Expand Down
2 changes: 1 addition & 1 deletion apps/www/src/config/customizer-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export const customizerPlugins = {
id: 'deserializemd',
label: 'Deserialize Markdown',
value: deserializeMdValue,
route: '/docs/serializing-markdown',
route: '/docs/serializing-md',
plugins: [KEY_DESERIALIZE_MD],
},
dnd: {
Expand Down
2 changes: 1 addition & 1 deletion apps/www/src/registry/default/plate-ui/dropdown-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const DropdownMenuContentVariants = withProps(DropdownMenuPrimitive.Content, {

export const DropdownMenuContent = withRef<
typeof DropdownMenuPrimitive.Content
>(({ className, ...props }, ref) => (
>(({ ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuContentVariants ref={ref} {...props} />
</DropdownMenuPrimitive.Portal>
Expand Down
41 changes: 39 additions & 2 deletions packages/core/src/utils/pipeHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,38 @@ import { PlateEditor } from '../types/PlateEditor';
import { DOMHandlers, HandlerReturnType } from '../types/plugin/DOMHandlers';
import { TEditableProps } from '../types/slate-react/TEditableProps';

export const convertDomEventToSyntheticEvent = (
domEvent: Event
): React.SyntheticEvent<unknown, unknown> => {
let propagationStopped = false;

return {
...domEvent,
nativeEvent: domEvent,
currentTarget: domEvent.currentTarget!,
target: domEvent.target!,
bubbles: domEvent.bubbles,
cancelable: domEvent.cancelable,
defaultPrevented: domEvent.defaultPrevented,
eventPhase: domEvent.eventPhase,
isTrusted: domEvent.isTrusted,
timeStamp: domEvent.timeStamp,
type: domEvent.type,
isDefaultPrevented: () => domEvent.defaultPrevented,
isPropagationStopped: () => propagationStopped,
persist: () => {
throw new Error(
'persist is not implemented for synthetic events created using convertDomEventToSyntheticEvent'
);
},
preventDefault: () => domEvent.preventDefault(),
stopPropagation: () => {
propagationStopped = true;
domEvent.stopPropagation();
},
};
};

/**
* Check if an event is overrided by a handler.
*/
Expand Down Expand Up @@ -54,11 +86,16 @@ export const pipeHandler = <V extends Value, K extends keyof DOMHandlers<V>>(
if (pluginsHandlers.length === 0 && !propsHandler) return;

return (event: any) => {
const isDomEvent = event instanceof Event;
const handledEvent = isDomEvent
? convertDomEventToSyntheticEvent(event)
: event;

const eventIsHandled = pluginsHandlers.some((handler) =>
isEventHandled(event, handler)
isEventHandled(handledEvent, handler)
);
if (eventIsHandled) return true;

return isEventHandled(event, propsHandler);
return isEventHandled(handledEvent, propsHandler);
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ export const createDeserializeMdPlugin =
options: {
elementRules: remarkDefaultElementRules,
textRules: remarkDefaultTextRules,
indentList: false,
},
});
1 change: 1 addition & 0 deletions packages/serializer-md/src/deserializer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import { RemarkElementRules, RemarkTextRules } from '../remark-slate/index';
export interface DeserializeMdPlugin<V extends Value = Value> {
elementRules?: RemarkElementRules<V>;
textRules?: RemarkTextRules<V>;
indentList?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,98 @@ describe('deserializeMd', () => {
expect(deserializeMd(editor, input)).toEqual(output);
});
});

describe('deserializeMdIndentList', () => {
const editor = createPlateEditor({
plugins: [
createDeserializeMdPlugin({ options: { indentList: true } }) as any,
],
});

it('should deserialize unordered lists', () => {
const input = '- List item 1\n- List item 2';

const output = [
{
type: 'p',
listStyleType: 'disc',
indent: 1,
children: [
{
text: 'List item 1',
},
],
},
{
type: 'p',
listStyleType: 'disc',
indent: 1,
children: [
{
text: 'List item 2',
},
],
},
];

expect(deserializeMd(editor, input)).toEqual(output);
});

it('should deserialize ordered lists', () => {
const input = '1. List item 1\n2. List item 2';

const output = [
{
type: 'p',
listStyleType: 'decimal',
indent: 1,
children: [
{
text: 'List item 1',
},
],
},
{
type: 'p',
listStyleType: 'decimal',
indent: 1,
children: [
{
text: 'List item 2',
},
],
},
];

expect(deserializeMd(editor, input)).toEqual(output);
});

it('should deserialize mixed nested lists', () => {
const input = '- List item 1\n 1. List item 1.1';

const output = [
{
type: 'p',
listStyleType: 'disc',
indent: 1,
children: [
{
text: 'List item 1',
},
],
},
{
type: 'p',
listStyleType: 'disc',
indent: 2,
children: [
{
text: 'List item 1.1',
},
],
},
];

expect(deserializeMd(editor, input)).toEqual(output);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@ export const deserializeMd = <V extends Value>(
editor: PlateEditor<V>,
data: string
) => {
const { elementRules, textRules } = getPluginOptions<DeserializeMdPlugin, V>(
editor,
KEY_DESERIALIZE_MD
);
const { elementRules, textRules, indentList } = getPluginOptions<
DeserializeMdPlugin,
V
>(editor, KEY_DESERIALIZE_MD);

const tree: any = unified()
.use(markdown)
.use(remarkPlugin, {
editor,
elementRules,
textRules,
indentList,
} as unknown as RemarkPluginOptions<V>)
.processSync(data);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { ELEMENT_IMAGE } from '@udecode/plate-media';
import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph';

import { remarkTransformElementChildren } from './remarkTransformElementChildren';
import { RemarkElementRules } from './types';
import { MdastNode, RemarkElementRules } from './types';

// FIXME: underline, subscript superscript not yet supported by remark-slate
export const remarkDefaultElementRules: RemarkElementRules<Value> = {
Expand All @@ -52,13 +52,44 @@ export const remarkDefaultElementRules: RemarkElementRules<Value> = {
},
},
list: {
transform: (node, options) => ({
type: getPluginType(
options.editor,
node.ordered ? ELEMENT_OL : ELEMENT_UL
),
children: remarkTransformElementChildren(node, options),
}),
transform: (node, options) => {
if (options.indentList) {
const listStyleType = node.ordered ? 'decimal' : 'disc';

const parseListItems = (
_node: MdastNode,
listItems: TElement[] = [],
indent = 1
) => {
_node.children!.forEach((listItem) => {
const [paragraph, ...subLists] = listItem.children!;

listItems.push({
type: getPluginType(options.editor, ELEMENT_PARAGRAPH),
listStyleType,
indent,
children: remarkTransformElementChildren(paragraph, options),
});

subLists.forEach((subList) => {
parseListItems(subList, listItems, indent + 1);
});
});

return listItems;
};

return parseListItems(node);
} else {
return {
type: getPluginType(
options.editor,
node.ordered ? ELEMENT_OL : ELEMENT_UL
),
children: remarkTransformElementChildren(node, options),
};
}
},
},
listItem: {
transform: (node, options) => ({
Expand Down
Loading

0 comments on commit 9141df5

Please sign in to comment.