diff --git a/package.json b/package.json index e7b29ed8b..4f468cc80 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ ], "private": true, "scripts": { - "start": "yarn lerna run start --parallel --ignore development", + "start": "yarn lerna run start --scope @yoopta/editor --scope @yoopta/toolbar --scope @yoopta/link --scope @yoopta/link-tool --parallel --ignore development", "build": "yarn clean && yarn lerna run build --parallel --ignore development", "clean": "find ./packages -type d -name dist ! -path './packages/development/*' -exec rm -rf {} +", "serve": "yarn lerna run dev --scope=development", diff --git a/packages/core/editor/src/UI/Overlay/Overlay.tsx b/packages/core/editor/src/UI/Overlay/Overlay.tsx index 5de6d0fa4..1c6f59dcd 100644 --- a/packages/core/editor/src/UI/Overlay/Overlay.tsx +++ b/packages/core/editor/src/UI/Overlay/Overlay.tsx @@ -1,19 +1,20 @@ import { FloatingOverlay } from '@floating-ui/react'; -import { MouseEvent, ReactNode } from 'react'; +import { MouseEvent, ReactNode, forwardRef } from 'react'; type Props = { children: ReactNode; lockScroll?: boolean; className?: string; onClick?: (e: MouseEvent) => void; + style?: React.CSSProperties; }; -const Overlay = ({ className, onClick, children, lockScroll = true }: Props) => { +const Overlay = forwardRef(({ className, onClick, children, lockScroll = true, ...rest }: Props, ref) => { return ( - + {children} ); -}; +}); export { Overlay }; diff --git a/packages/core/editor/src/components/Editor/Editor.tsx b/packages/core/editor/src/components/Editor/Editor.tsx index 8738e54b0..18eebd38a 100644 --- a/packages/core/editor/src/components/Editor/Editor.tsx +++ b/packages/core/editor/src/components/Editor/Editor.tsx @@ -60,6 +60,8 @@ const Editor = ({ let state = useRef(DEFAULT_STATE).current; + console.log('editor.children', editor.children); + useEffect(() => { if (!autoFocus || isReadOnly) return; editor.focus(); diff --git a/packages/core/editor/src/contexts/YooptaContext/YooptaContext.tsx b/packages/core/editor/src/contexts/YooptaContext/YooptaContext.tsx index cc7d64eae..d9d221bc4 100644 --- a/packages/core/editor/src/contexts/YooptaContext/YooptaContext.tsx +++ b/packages/core/editor/src/contexts/YooptaContext/YooptaContext.tsx @@ -11,7 +11,7 @@ const DEFAULT_HANDLERS: YooptaEditorContext = { editor: { id: '', - getBlock: () => undefined, + getBlock: () => null, insertBlock: () => undefined, insertBlocks: () => undefined, updateBlock: () => undefined, diff --git a/packages/core/editor/src/editor/elements/updateElement.ts b/packages/core/editor/src/editor/elements/updateElement.ts index dc0edc13c..e01385388 100644 --- a/packages/core/editor/src/editor/elements/updateElement.ts +++ b/packages/core/editor/src/editor/elements/updateElement.ts @@ -6,10 +6,9 @@ export type UpdateElementOptions = { path?: Path; }; -export type UpdateElement = { - type: TElementKeys; - props: TElementProps; -}; +export type UpdateElement = Partial< + Omit, 'id'> +>; export function updateElement( editor: YooEditor, @@ -37,12 +36,13 @@ export function updateElement( }); const elementToUpdate = elementEntry?.[0]; + const elementToUpdatePath = elementEntry?.[1]; const props = elementToUpdate?.props || {}; - const updatedNode = { props: { ...props, ...element.props } }; + const updatedElement = { props: { ...props, ...element.props } }; - Transforms.setNodes(slate, updatedNode, { - at: options?.path || [0], + Transforms.setNodes(slate, updatedElement, { + at: options?.path || elementToUpdatePath || [0], match: (n) => Element.isElement(n) && n.type === element.type, mode: 'lowest', }); diff --git a/packages/development/src/pages/dev/index.tsx b/packages/development/src/pages/dev/index.tsx index 9ab9d01ae..9d867f809 100644 --- a/packages/development/src/pages/dev/index.tsx +++ b/packages/development/src/pages/dev/index.tsx @@ -52,15 +52,15 @@ const BasicExample = () => { tools={TOOLS} readOnly={readOnly} value={{ - '36048c70-ce45-4fe6-ad92-70ef6810b88c': { - id: '36048c70-ce45-4fe6-ad92-70ef6810b88c', + 'b5709c7d-c79b-4786-8d72-8a4d727163d4': { + id: 'b5709c7d-c79b-4786-8d72-8a4d727163d4', value: [ { - id: '66260b1b-1efd-4d8f-b6cd-3342480deea7', - type: 'heading-two', + id: '03bbf41c-d9c0-44ed-90d9-1bd17c66f94f', + type: 'heading-one', children: [ { - text: 'Built-in Constraints', + text: 'With custom renders ', }, ], props: { @@ -68,158 +68,97 @@ const BasicExample = () => { }, }, ], - type: 'HeadingTwo', + type: 'HeadingOne', meta: { order: 0, depth: 0, }, }, - '02fba3b4-f90e-4fe0-9284-9dff8cf5fa3b': { - id: '02fba3b4-f90e-4fe0-9284-9dff8cf5fa3b', + '1c07a2a9-9dba-4626-86f9-7fa0dd8c9f83': { + id: '1c07a2a9-9dba-4626-86f9-7fa0dd8c9f83', value: [ { - id: '5ae35463-0542-4f9a-b3b2-8f86dc8a132f', - type: 'accordion-list', + id: 'a5db0166-46c1-4170-94f9-12d758cdc882', + type: 'callout', children: [ { - id: 'c584e4fa-b735-4cd8-9c5e-a9f2ce765fbd', - type: 'accordion-list-item', - children: [ - { - id: '310aa032-aa99-42e1-b9d4-664a68c0c0ef', - type: 'accordion-list-item-heading', - children: [ - { - text: 'Built-in Constraints', - }, - ], - props: { - nodeType: 'block', - }, - }, - { - id: 'ad3fb9a9-1533-4080-82e1-c102a1e725c9', - type: 'accordion-list-item-content', - children: [ - { - text: 'Slate editors come with a few built-in constraints out of the box. These constraints are there to make working with content much more predictable than standard contenteditable. All of the built-in logic in Slate depends on these constraints, so unfortunately you cannot omit them. They are...', - }, - ], - props: { - nodeType: 'block', - }, - }, - ], - props: { - nodeType: 'block', - isExpanded: true, - }, + text: 'This example will show you how to add custom renders to plugins', }, + ], + props: { + theme: 'info', + }, + }, + ], + type: 'Callout', + meta: { + order: 1, + depth: 0, + }, + }, + '18cb86e2-44df-4d76-94e0-cf71b78d8ee8': { + id: '18cb86e2-44df-4d76-94e0-cf71b78d8ee8', + value: [ + { + id: '83810c57-3b88-4f94-95ff-632f5e8d6c3f', + type: 'heading-three', + children: [ { - id: 'caffa035-bfe1-40be-b5a6-e41fcd74f189', - type: 'accordion-list-item', - children: [ - { - id: '4c5e2f14-4c1f-4a78-803a-27fae2d1fb5d', - type: 'accordion-list-item-heading', - children: [ - { - text: 'Adding Constraints', - }, - ], - props: { - nodeType: 'block', - }, - }, - { - id: 'a1dc9763-e1fa-4e70-88b0-dfe335ae4344', - type: 'accordion-list-item-content', - children: [ - { - text: "All Element nodes must contain at least one Text descendant — even Void Elements. If an element node does not contain any children, an empty text node will be added as its only child. This constraint exists to ensure that the selection's anchor and focus points (which rely on referencing text nodes) can always be placed inside any node. Without this, empty elements (or void elements) wouldn't be selectable.", - }, - ], - props: { - nodeType: 'block', - }, - }, - ], - props: { - nodeType: 'block', - isExpanded: true, - }, + text: 'Example with ', }, { - id: '7a327ed3-b8d3-41a5-8292-64a2d82b57b6', - type: 'accordion-list-item', + text: '', + }, + { + id: 'c59655ae-fae4-472a-8fb3-e776e35395ad', + type: 'link', + props: { + url: 'https://app.slack.com/client/T02V7P8BG/D02U1BMBYV6', + target: '_self', + rel: 'noreferrer', + title: 'next/image', + nodeType: 'inline', + }, children: [ { - id: 'e7b5774a-6bd7-47a1-b8d2-dddedcb28418', - type: 'accordion-list-item-heading', - children: [ - { - text: 'Multi-pass Normalizing', - }, - ], - props: { - nodeType: 'block', - }, - }, - { - id: '093afe90-0e9a-4602-81f6-66c55abb7ad7', - type: 'accordion-list-item-content', - children: [ - { - text: "To do this, you extend the normalizeNode function on the editor. The normalizeNode function gets called every time an operation is applied that inserts or updates a node (or its descendants), giving you the opportunity to ensure that the changes didn't leave it in an invalid state, and correcting the node if so.", - }, - ], - props: { - nodeType: 'block', - }, + text: 'next/image', }, ], - props: { - nodeType: 'block', - isExpanded: true, - }, + }, + { + text: ' ', }, ], + props: { + nodeType: 'block', + }, }, ], - type: 'Accordion', + type: 'HeadingThree', meta: { - order: 1, + order: 2, depth: 0, }, }, - '226f5963-b01c-4b14-a7e8-45f3cf6c6b95': { - id: '226f5963-b01c-4b14-a7e8-45f3cf6c6b95', + 'ae04c7a1-fb94-4f0b-b428-3757a8d21196': { + id: 'ae04c7a1-fb94-4f0b-b428-3757a8d21196', value: [ { - id: 'dcfc9ee2-db6e-4127-b40c-83bcd465c5dd', - type: 'embed', - props: { - sizes: { - width: 650, - height: 400, - }, - nodeType: 'void', - provider: { - type: 'youtube', - id: 'bItAw5xgI4I', - url: 'https://www.youtube.com/watch?v=bItAw5xgI4I&t=468s', - }, - }, + id: '08a13b95-2371-4eac-9c30-83f1f8b72fc5', + type: 'callout', children: [ { - text: '', + text: "By default, the @yoopta/image plugin provides its own image rendering. \nBut what if you want to change the default rendering with powerful components like next/image, which provide top-level optimization and rendering with different layout. So, it's easy-peasy. ", }, ], + props: { + theme: 'default', + }, }, ], - type: 'Embed', + type: 'Callout', meta: { - order: 2, + order: 3, depth: 0, }, }, diff --git a/packages/development/src/pages/index.tsx b/packages/development/src/pages/index.tsx index 70d732e3f..34fa5461f 100644 --- a/packages/development/src/pages/index.tsx +++ b/packages/development/src/pages/index.tsx @@ -4,11 +4,15 @@ import { useRouter } from 'next/router'; const Index = () => { const router = useRouter(); - useEffect(() => { - router.push('/dev'); - }); + // useEffect(() => { + // router.push('/dev'); + // }); - return null; + return ( +
+

Some markup

+
+ ); }; export default Index; diff --git a/packages/development/src/utils/yoopta/plugins.tsx b/packages/development/src/utils/yoopta/plugins.tsx index 9c4fb545d..1380b5b35 100644 --- a/packages/development/src/utils/yoopta/plugins.tsx +++ b/packages/development/src/utils/yoopta/plugins.tsx @@ -37,18 +37,18 @@ export const YOOPTA_PLUGINS = [ }, }), File.extend({ - renders: { - file: ({ attributes, children, element }) => { - return ( - - ); - }, - }, + // renders: { + // file: ({ attributes, children, element }) => { + // return ( + // + // ); + // }, + // }, options: { onUpload: async (file: File) => { const data = await uploadToCloudinary(file, 'auto'); @@ -70,9 +70,9 @@ export const YOOPTA_PLUGINS = [ }, }), Image.extend({ - renders: { - image: YooptaWithNextImage, - }, + // renders: { + // image: YooptaWithNextImage, + // }, options: { maxSizes: { maxHeight: 750, maxWidth: 750 }, HTMLAttributes: { @@ -95,15 +95,6 @@ export const YOOPTA_PLUGINS = [ }, }), Headings.HeadingOne.extend({ - renders: { - 'heading-one': ({ attributes, children, element, blockId }) => { - return ( -

- {children} -

- ); - }, - }, options: { HTMLAttributes: { className: 'heading-one-element-extended', @@ -122,15 +113,6 @@ export const YOOPTA_PLUGINS = [ }), Headings.HeadingThree, Blockquote.extend({ - renders: { - blockquote: ({ attributes, children }) => { - return ( -
- {children} -
- ); - }, - }, options: { HTMLAttributes: { className: 'blockquote-element-extended', @@ -138,15 +120,6 @@ export const YOOPTA_PLUGINS = [ }, }), Callout.extend({ - renders: { - callout: ({ attributes, children }) => { - return ( -
- {children} -
- ); - }, - }, options: { HTMLAttributes: { className: 'callout-element-extended', @@ -161,50 +134,9 @@ export const YOOPTA_PLUGINS = [ }, }), Lists.NumberedList, - Lists.TodoList.extend({ - renders: { - 'todo-list': ({ attributes, children, element, blockId }) => { - const editor = useYooptaEditor(); - const { checked = false } = element.props; - const style = { - textDecoration: checked ? 'line-through' : 'none', - }; - - const onCheckedChange = (isChecked: boolean) => { - console.log('onCheckedChange', isChecked); - - Elements.updateElement(editor, blockId, { type: 'todo-list', props: { checked: isChecked } }); - }; - - return ( - - {children} - - ); - }, - }, - }), + Lists.TodoList, Embed, Video.extend({ - renders: { - video: ({ attributes, children, element }) => { - return ( -
-
- ); - }, - }, options: { HTMLAttributes: { className: 'video-element-extended', @@ -226,14 +158,26 @@ export const YOOPTA_PLUGINS = [ Link.extend({ renders: { link: ({ attributes, children, element }) => { + if (element.props.target === '_blank') { + return ( + + {children} + + ); + } + return ( {children} diff --git a/packages/plugins/link/src/plugin/index.tsx b/packages/plugins/link/src/plugin/index.tsx index 3a75eefbe..de75e2af9 100644 --- a/packages/plugins/link/src/plugin/index.tsx +++ b/packages/plugins/link/src/plugin/index.tsx @@ -1,4 +1,4 @@ -import { YooptaPlugin } from '@yoopta/editor'; +import { generateId, YooptaPlugin } from '@yoopta/editor'; import { LinkElementProps, LinkPluginElementKeys } from '../types'; import { LinkRender } from '../ui/LinkRender'; diff --git a/packages/plugins/link/src/styles.css b/packages/plugins/link/src/styles.css index fb43ec92b..9619df4a5 100644 --- a/packages/plugins/link/src/styles.css +++ b/packages/plugins/link/src/styles.css @@ -5,9 +5,17 @@ } .yoopta-link-preview { - @apply yoo-link-bg-[#FFFFFF] yoo-link-flex yoo-link-items-center yoo-link-z-50 yoo-link-p-[5px] yoo-link-rounded-md yoo-link-shadow-md yoo-link-border-[1px] yoo-link-border-solid yoo-link-border-[#e3e3e3] + @apply yoo-link-bg-[#FFFFFF] yoo-link-flex yoo-link-items-center yoo-link-z-50 yoo-link-p-[6px] yoo-link-rounded-md yoo-link-shadow-md yoo-link-border-[1px] yoo-link-border-solid yoo-link-border-[#e3e3e3] } .yoopta-link-preview-text { - @apply yoo-link-mx-[4px] yoo-link-text-sm + @apply yoo-link-text-sm yoo-link-max-w-[200px] yoo-link-select-none yoo-link-text-ellipsis yoo-link-overflow-hidden yoo-link-whitespace-nowrap +} + +.yoopta-link-preview-separator { + @apply yoo-link-w-[1px] yoo-link-h-[20px] yoo-link-bg-[#e3e3e3] yoo-link-mx-[5px] +} + +.yoopta-link-edit-button { + @apply yoo-link-text-sm yoo-link-cursor-pointer } \ No newline at end of file diff --git a/packages/plugins/link/src/ui/LinkHoverPreview.tsx b/packages/plugins/link/src/ui/LinkHoverPreview.tsx index fc1c517c2..0bb34cd48 100644 --- a/packages/plugins/link/src/ui/LinkHoverPreview.tsx +++ b/packages/plugins/link/src/ui/LinkHoverPreview.tsx @@ -1,29 +1,107 @@ -import { UI, useYooptaTools } from '@yoopta/editor'; -import { Copy, Globe } from 'lucide-react'; +import { Elements, findSlateBySelectionPath, SlateElement, UI, useYooptaEditor, useYooptaTools } from '@yoopta/editor'; +import { Copy, SquareArrowOutUpRight } from 'lucide-react'; import { useState } from 'react'; +import { useFloating, offset, flip, shift, inline, autoUpdate, useTransitionStyles } from '@floating-ui/react'; +import { LinkElementProps } from '../types'; +import { Editor, Element, Transforms } from 'slate'; const { Overlay, Portal } = UI; -const LinkHoverPreview = ({ style, setFloating, element }) => { - console.log('element', element); +const LinkHoverPreview = ({ style, setFloating, element, setHoldLinkTool, blockId, onClose }) => { + const editor = useYooptaEditor(); const tools = useYooptaTools(); - const [isToolsOpen, setIsToolsOpen] = useState(false); + const [isEditLinkToolsOpen, setIsEditLinkToolsOpen] = useState(false); + + const { + refs: linkToolRefs, + floatingStyles: linkToolStyles, + context, + } = useFloating({ + placement: 'bottom', + open: isEditLinkToolsOpen, + onOpenChange: (open) => { + setIsEditLinkToolsOpen(open); + }, + middleware: [inline(), flip(), shift(), offset(10)], + whileElementsMounted: autoUpdate, + }); + + const { isMounted: isLinkToolMounted, styles: linkToolTransitionStyles } = useTransitionStyles(context, { + duration: { + open: 100, + close: 100, + }, + }); + + const linkToolEditStyles = { ...linkToolStyles, ...linkToolTransitionStyles, maxWidth: 400 }; const LinkTool = tools?.LinkTool; const hasLinkTool = !!LinkTool; + const onSave = (linkProps: LinkElementProps) => { + Elements.updateElement(editor, blockId, { + type: element.type, + props: { ...element.props, ...linkProps }, + }); + + setIsEditLinkToolsOpen(false); + setHoldLinkTool(false); + }; + + const onDelete = () => { + const slate = findSlateBySelectionPath(editor); + if (!slate || !slate.selection) return; + const linkNodeEntry = Elements.getElementEntry(editor, blockId); + + if (linkNodeEntry) { + Transforms.unwrapNodes(slate, { + match: (n) => !Editor.isEditor(n) && Element.isElement(n) && (n as SlateElement).type === element.type, + }); + } + }; + + const onOpenLink = () => { + window.open(element.props.url, element.props.target || '_blank'); + }; + return ( + {isLinkToolMounted && hasLinkTool && ( + + +
e.stopPropagation()}> + +
+
+
+ )}
- {isToolsOpen ? ( - - ) : ( - <> - - {element.props.url} - setIsToolsOpen(true)}>Edit - - )} + {element.props.url} + + + + + +
); diff --git a/packages/plugins/link/src/ui/LinkRender.tsx b/packages/plugins/link/src/ui/LinkRender.tsx index 265792074..fa9ce1c6b 100644 --- a/packages/plugins/link/src/ui/LinkRender.tsx +++ b/packages/plugins/link/src/ui/LinkRender.tsx @@ -1,15 +1,14 @@ -import { PluginElementRenderProps, useYooptaReadOnly, useYooptaTools, UI } from '@yoopta/editor'; +import { PluginElementRenderProps, useYooptaReadOnly } from '@yoopta/editor'; import { useState } from 'react'; import { LinkElementProps } from '../types'; import { LinkHoverPreview } from './LinkHoverPreview'; import { useFloating, offset, flip, shift, inline, autoUpdate, useTransitionStyles } from '@floating-ui/react'; -import { SquareArrowOutUpRight } from 'lucide-react'; const VALID_TARGET_VALUES = ['_blank', '_self', '_parent', '_top', 'framename']; const LinkRender = ({ extendRender, ...props }: PluginElementRenderProps) => { const [hovered, setHovered] = useState(false); - + const [holdLinkTool, setHoldLinkTool] = useState(false); const { className = '', ...htmlAttrs } = props.HTMLAttributes || {}; const { @@ -26,7 +25,7 @@ const LinkRender = ({ extendRender, ...props }: PluginElementRenderProps) => { const { isMounted: isActionMenuMounted, styles: linkPreviewTransitionStyles } = useTransitionStyles(context, { duration: { - open: 100, + open: 200, close: 100, }, }); @@ -49,47 +48,53 @@ const LinkRender = ({ extendRender, ...props }: PluginElementRenderProps) => { delete linkProps.rel; } - if (extendRender) { - return extendRender(props); - } + const onClose = () => { + setHoldLinkTool(false); + setHovered(false); + }; + const onMouseOver = () => { - console.log('onMouseOver', onMouseOver); setHovered(true); }; - const onMouseOut = () => { - console.log('onMouseOut', onMouseOut); - setHovered(false); - }; - const onRef = (ref) => { - props.attributes.ref = ref; - linkPreviewRefs.setReference(ref); + const onMouseOut = () => { + if (holdLinkTool) return; + onClose(); }; - const onClick = (e) => { - // if (isReadOnly) return; - // e.preventDefault(); + const onRef = (node) => { + props.attributes.ref(node); + // linkPreviewRefs.setReference(node); }; return ( - - {props.children} - + + {extendRender ? ( + extendRender(props) + ) : ( + + {props.children} + + )} {isActionMenuMounted && !isReadOnly && ( - + )} - + ); }; diff --git a/packages/tools/link-tool/src/components/DefaultLinkToolRender.tsx b/packages/tools/link-tool/src/components/DefaultLinkToolRender.tsx index 5a8bd27a2..24786a3db 100644 --- a/packages/tools/link-tool/src/components/DefaultLinkToolRender.tsx +++ b/packages/tools/link-tool/src/components/DefaultLinkToolRender.tsx @@ -9,18 +9,19 @@ const DEFAULT_LINK_VALUE: Link = { rel: 'noreferrer', }; -function isUrl(string: any): boolean { - try { - new URL(string); - return true; - } catch (_) { - return false; - } -} +// function isUrl(string: any): boolean { +// try { +// new URL(string); +// return true; +// } catch (_) { +// return false; +// } +// } const DefaultLinkToolRender = (props: LinkToolRenderProps) => { + const { withLink = true, withTitle = true } = props; const [link, setLink] = useState(props?.link || DEFAULT_LINK_VALUE); - const [additionalPropsOpen, setAdditionPropsOpen] = useState(false); + const [isAdditionalPropsOpen, setAdditionPropsOpen] = useState(false); const onChange = (e: ChangeEvent) => { const target = e.target as HTMLInputElement; @@ -42,45 +43,49 @@ const DefaultLinkToolRender = (props: LinkToolRenderProps) => { return (
-
- - -
-
- - -
+ {withTitle && ( +
+ + +
+ )} + {withLink && ( +
+ + +
+ )} - {additionalPropsOpen && ( + {isAdditionalPropsOpen && ( <>