diff --git a/README.md b/README.md
index 64cc78925..0c6bae408 100644
--- a/README.md
+++ b/README.md
@@ -52,8 +52,10 @@ All of this is customizable, extensible, and easy to set up!
- Plugins
- [**@yoopta/paragraph**](https://github.com/Darginec05/Yoopta-Editor/blob/master/packages/plugins/paragraph/README.md)
- - [**@yoopta/accordion**](https://github.com/Darginec05/Yoopta-Editor/blob/master/packages/plugins/accordion/README.md)
- [**@yoopta/blockquote**](https://github.com/Darginec05/Yoopta-Editor/blob/master/packages/plugins/blockquote/README.md)
+ - [**@yoopta/accordion**](https://github.com/Darginec05/Yoopta-Editor/blob/master/packages/plugins/accordion/README.md)
+ - [**@yoopta/divider**](https://github.com/Darginec05/Yoopta-Editor/blob/master/packages/plugins/divider/README.md)
+ - [**@yoopta/table**](https://github.com/Darginec05/Yoopta-Editor/blob/master/packages/plugins/table/README.md)
- [**@yoopta/code**](https://github.com/Darginec05/Yoopta-Editor/blob/master/packages/plugins/code/README.md)
- [**@yoopta/embed**](https://github.com/Darginec05/Yoopta-Editor/blob/master/packages/plugins/embed/README.md)
- [**@yoopta/image**](https://github.com/Darginec05/Yoopta-Editor/blob/master/packages/plugins/image/README.md)
@@ -149,6 +151,8 @@ Here is list of available plugins
- @yoopta/paragraph
- @yoopta/blockquote
+- @yoopta/table
+- @yoopta/divider
- @yoopta/accordion
- @yoopta/code
- @yoopta/embed
diff --git a/packages/core/editor/package.json b/packages/core/editor/package.json
index 938446459..fd32932cc 100644
--- a/packages/core/editor/package.json
+++ b/packages/core/editor/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/editor",
- "version": "4.7.0",
+ "version": "4.8.0",
"license": "MIT",
"private": false,
"main": "dist/index.js",
@@ -67,5 +67,5 @@
"url": "https://github.com/Darginec05/Yoopta-Editor/issues"
},
"homepage": "https://github.com/Darginec05/Yoopta-Editor#readme",
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/core/editor/src/UI/BlockOptions/BlockOptions.tsx b/packages/core/editor/src/UI/BlockOptions/BlockOptions.tsx
index 5c3a9379a..dc98a3ca6 100644
--- a/packages/core/editor/src/UI/BlockOptions/BlockOptions.tsx
+++ b/packages/core/editor/src/UI/BlockOptions/BlockOptions.tsx
@@ -32,15 +32,18 @@ const BlockOptionsSeparator = ({ className = '' }: BlockOptionsSeparatorProps) =
);
-type BlockOptionsProps = {
+export type BlockOptionsProps = {
isOpen: boolean;
onClose: () => void;
refs: any;
style: CSSProperties;
children?: React.ReactNode;
+ actions?: ['delete', 'duplicate', 'turnInto', 'copy'] | null;
};
-const BlockOptions = ({ isOpen, onClose, refs, style, children }: BlockOptionsProps) => {
+const DEFAULT_ACTIONS: BlockOptionsProps['actions'] = ['delete', 'duplicate', 'turnInto', 'copy'];
+
+const BlockOptions = ({ isOpen, onClose, refs, style, actions = DEFAULT_ACTIONS, children }: BlockOptionsProps) => {
const editor = useYooptaEditor();
const tools = useYooptaTools();
const [isActionMenuOpen, setIsActionMenuOpen] = useState(false);
@@ -106,50 +109,52 @@ const BlockOptions = ({ isOpen, onClose, refs, style, children }: BlockOptionsPr
// [TODO] - take care about SSR
-
+
-
-
-
-
-
-
-
- {!!ActionMenu && !isVoidElement && !editor.blocks[currentBlock?.type || '']?.hasCustomEditor && (
+ {actions !== null && (
+
+
+
+
+
+
+
+ {!!ActionMenu && !isVoidElement && !editor.blocks[currentBlock?.type || '']?.hasCustomEditor && (
+
+ {isActionMenuMounted && (
+
+ setIsActionMenuOpen(false)}>
+
+
+
+ )}
+
+
+ )}
- {isActionMenuMounted && (
-
- setIsActionMenuOpen(false)}>
-
-
-
- )}
-
- )}
-
-
-
- Copy link to block
-
-
-
+
+ )}
{children}
diff --git a/packages/core/editor/src/UI/ExtendedBlockActions/ExtendedBlockActions.tsx b/packages/core/editor/src/UI/ExtendedBlockActions/ExtendedBlockActions.tsx
index b9ba21575..713eb1e90 100644
--- a/packages/core/editor/src/UI/ExtendedBlockActions/ExtendedBlockActions.tsx
+++ b/packages/core/editor/src/UI/ExtendedBlockActions/ExtendedBlockActions.tsx
@@ -54,6 +54,7 @@ const ExtendedBlockActions = ({ id, className, style, onClick, children }: Props
)}
, Record>[];
+ plugins: Readonly>[]>;
marks?: YooptaMark[];
value?: YooptaContentValue;
autoFocus?: boolean;
@@ -54,18 +54,6 @@ function validateInitialValue(value: any): boolean {
return true;
}
-const isLegacyVersionInUse = (value: any): boolean => {
- if (Array.isArray(value) && value.length > 0) {
- return value.some((node) => {
- if (node.id || node.nodeType || node.type || node.children) {
- return true;
- }
- });
- }
-
- return false;
-};
-
const YooptaEditor = ({
id,
editor,
@@ -92,10 +80,10 @@ const YooptaEditor = ({
}, [marksProps]);
const plugins = useMemo(() => {
- return pluginsProps.map((plugin) => plugin.getPlugin as Plugin);
+ return pluginsProps.map((plugin) => plugin.getPlugin as Plugin>);
}, [pluginsProps]);
- const [editorState, setEditorState] = useState<{ editor: YooEditor; version: number }>(() => {
+ const [editorState, setEditorState] = useState<{ editor: YooEditor; version: number }>(() => {
if (!editor.id) editor.id = id || generateId();
editor.applyChanges = applyChanges;
editor.readOnly = readOnly || false;
@@ -114,6 +102,7 @@ const YooptaEditor = ({
editor.blockEditorsMap = buildBlockSlateEditors(editor);
editor.shortcuts = buildBlockShortcuts(editor);
editor.plugins = buildPlugins(plugins);
+ editor.commands = buildCommands(editor, plugins);
editor.on = Events.on;
editor.once = Events.once;
@@ -123,50 +112,22 @@ const YooptaEditor = ({
return { editor, version: 0 };
});
- if (isLegacyVersionInUse(value)) {
- console.error('Legacy version of Yoopta-Editor in use');
-
- return (
-
-
Legacy version of the Yoopta-Editor is used
-
It looks like you are using a legacy version of the editor.
-
- The structure of value has changed in new @v4 version
-
-
- {/* [TODO] - add link to migration guide */}
- Please, check the migration guide to update your editor to the new @v4 version.
-
-
-
- If you have specific case please{' '}
-
- open the issue
- {' '}
- and we will solve your problem with migration
-
-
- );
- }
-
return (
-
-
-
-
- {children}
-
-
-
-
+
+
+
+ {children}
+
+
+
);
};
diff --git a/packages/core/editor/src/components/Block/Block.tsx b/packages/core/editor/src/components/Block/Block.tsx
index 04fa81523..241f34cb4 100644
--- a/packages/core/editor/src/components/Block/Block.tsx
+++ b/packages/core/editor/src/components/Block/Block.tsx
@@ -1,41 +1,41 @@
+import React, { useCallback, useMemo } from 'react';
import { useYooptaEditor } from '../../contexts/YooptaContext/YooptaContext';
import { useSortable } from '@dnd-kit/sortable';
-import { CSSProperties, useState } from 'react';
import { BlockActions } from './BlockActions';
+import { YooptaBlockData } from '../../editor/types';
+import { useBlockStyles } from './hooks';
-const Block = ({ children, block, blockId }) => {
+type BlockProps = {
+ children: React.ReactNode;
+ block: YooptaBlockData;
+ blockId: string;
+};
+
+const Block = ({ children, block, blockId }: BlockProps) => {
const editor = useYooptaEditor();
+ const [activeBlockId, setActiveBlockId] = React.useState(null);
- const [activeBlockId, setActiveBlockId] = useState(null);
const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isOver, isDragging } =
useSortable({ id: blockId, disabled: editor.readOnly });
+ const styles = useBlockStyles(block, transform, transition, isDragging, isOver);
const align = block.meta.align || 'left';
const className = `yoopta-block yoopta-align-${align}`;
- const style: CSSProperties = {
- // [TODO] = handle max depth
- marginLeft: `${block.meta.depth * 20}px`,
- transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : 'none',
- transition,
- opacity: isDragging ? 0.7 : 1,
- };
-
const isSelected = editor.selectedBlocks?.includes(block.meta.order);
const isHovered = activeBlockId === blockId;
- const onChangeActiveBlock = (id: string) => setActiveBlockId(id);
-
- const handleMouseEnter = () => {
+ const handleMouseEnter = useCallback(() => {
if (editor.readOnly) return;
setActiveBlockId(blockId);
- };
- const handleMouseLeave = () => {
+ }, [editor.readOnly, blockId]);
+
+ const handleMouseLeave = useCallback(() => {
if (editor.readOnly) return;
setActiveBlockId(null);
- };
+ }, [editor.readOnly]);
- const contentStyles = { borderBottom: isOver && !isDragging ? '2px solid #007aff' : 'none' };
+ const dragHandleProps = useMemo(() => ({ setActivatorNodeRef, attributes, listeners }), [block]);
return (
{
className={className}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
- style={style}
+ style={styles.container}
data-hovered-block={isHovered}
data-yoopta-block
data-yoopta-block-id={blockId}
@@ -53,17 +53,12 @@ const Block = ({ children, block, blockId }) => {
)}
-
- {children}
-
+
{children}
{isSelected && !editor.readOnly &&
}
);
diff --git a/packages/core/editor/src/components/Block/hooks.ts b/packages/core/editor/src/components/Block/hooks.ts
index 87c9db6a0..2f509fc5c 100644
--- a/packages/core/editor/src/components/Block/hooks.ts
+++ b/packages/core/editor/src/components/Block/hooks.ts
@@ -1,7 +1,9 @@
import { useYooptaTools } from '../../contexts/YooptaContext/ToolsContext';
import { useFloating, offset, flip, inline, shift, useTransitionStyles, autoUpdate } from '@floating-ui/react';
-import { useState } from 'react';
+import { useMemo, useState } from 'react';
import { buildActionMenuRenderProps } from '../../UI/BlockOptions/utils';
+import { YooptaBlockData } from '../../editor/types';
+import { Transform } from '@dnd-kit/utilities';
export const useActionMenuToolRefs = ({ editor }) => {
const tools = useYooptaTools();
@@ -47,3 +49,24 @@ export const useActionMenuToolRefs = ({ editor }) => {
ActionMenu,
};
};
+
+export const useBlockStyles = (
+ block: YooptaBlockData,
+ transform: Transform | null,
+ transition: string | undefined,
+ isDragging: boolean,
+ isOver: boolean,
+) => {
+ return useMemo(
+ () => ({
+ container: {
+ marginLeft: `${block.meta.depth * 20}px`,
+ transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : 'none',
+ transition,
+ opacity: isDragging ? 0.7 : 1,
+ },
+ content: isOver && !isDragging ? { borderBottom: '2px solid #007aff' } : undefined,
+ }),
+ [block.meta.depth, transform, transition, isDragging, isOver],
+ );
+};
diff --git a/packages/core/editor/src/components/Editor/Editor.tsx b/packages/core/editor/src/components/Editor/Editor.tsx
index a63996871..2e3a0eb4c 100644
--- a/packages/core/editor/src/components/Editor/Editor.tsx
+++ b/packages/core/editor/src/components/Editor/Editor.tsx
@@ -13,6 +13,7 @@ import { YooptaBlockPath, YooptaContentValue } from '../../editor/types';
import { useRectangeSelectionBox } from '../SelectionBox/hooks';
import { SelectionBox } from '../SelectionBox/SelectionBox';
import { Blocks } from '../../editor/blocks';
+import { useMultiSelection } from './selection';
type Props = {
marks?: YooptaMark[];
@@ -56,6 +57,7 @@ const Editor = ({
const editor = useYooptaEditor();
const isReadOnly = useYooptaReadOnly();
const selectionBox = useRectangeSelectionBox({ editor, root: selectionBoxRoot });
+ const multiSelection = useMultiSelection(editor);
let state = useRef(DEFAULT_STATE).current;
@@ -106,14 +108,6 @@ const Editor = ({
}
};
- const resetSelectedBlocks = () => {
- if (isReadOnly) return;
-
- if (Array.isArray(editor.selectedBlocks) && editor.selectedBlocks.length > 0) {
- editor.setBlockSelected(null);
- }
- };
-
const resetSelectionState = () => {
state.indexToSelect = null;
state.startedIndexToSelect = null;
@@ -123,30 +117,9 @@ const Editor = ({
const onMouseDown = (event: React.MouseEvent) => {
if (isReadOnly) return;
- // if (event.shiftKey) {
- // const currentSelectionIndex = editor.selection;
- // if (!currentSelectionIndex) return;
-
- // const targetBlock = (event.target as HTMLElement).closest('div[data-yoopta-block]');
- // const targetBlockId = targetBlock?.getAttribute('data-yoopta-block-id') || '';
- // const targetBlockIndex = editor.children[targetBlockId]?.meta.order;
- // if (typeof targetBlockIndex !== 'number') return;
-
- // const indexesBetween = Array.from({ length: Math.abs(targetBlockIndex - currentSelectionIndex[0]) }).map(
- // (_, index) =>
- // targetBlockIndex > currentSelectionIndex[0]
- // ? currentSelectionIndex[0] + index + 1
- // : currentSelectionIndex[0] - index - 1,
- // );
-
- // editor.blur();
- // editor.setBlockSelected([currentSelectionIndex[0], ...indexesBetween], { only: true });
- // return;
- // }
-
+ multiSelection.onMouseDown(event);
resetSelectionState();
handleEmptyZoneClick(event);
- resetSelectedBlocks();
};
const onBlur = (event: React.FocusEvent) => {
@@ -154,7 +127,10 @@ const Editor = ({
if (isInsideEditor || isReadOnly) return;
resetSelectionState();
- resetSelectedBlocks();
+
+ if (Array.isArray(editor.selectedBlocks) && editor.selectedBlocks.length > 0) {
+ editor.setBlockSelected(null);
+ }
};
const onKeyDown = (event) => {
diff --git a/packages/core/editor/src/components/Editor/RenderBlocks.tsx b/packages/core/editor/src/components/Editor/RenderBlocks.tsx
index 305e136b3..ef4fca1de 100644
--- a/packages/core/editor/src/components/Editor/RenderBlocks.tsx
+++ b/packages/core/editor/src/components/Editor/RenderBlocks.tsx
@@ -57,6 +57,7 @@ const RenderBlocks = ({ editor, marks, placeholder }: Props) => {
events={plugin.events}
elements={plugin.elements}
options={plugin.options}
+ extensions={plugin.extensions}
placeholder={placeholder}
/>
,
@@ -67,6 +68,7 @@ const RenderBlocks = ({ editor, marks, placeholder }: Props) => {
return (
(null);
+ const currentBlockPathRef = useRef(null);
+
+ const blurSlateSelection = (blockPath?: YooptaBlockPath) => {
+ const path = blockPath || editor.selection;
+
+ if (path) {
+ const slate = Blocks.getSlate(editor, { at: path });
+ if (!slate) return;
+
+ Editor.withoutNormalizing(slate, () => {
+ Transforms.select(slate, [0]);
+
+ if (slate.selection && Range.isExpanded(slate.selection)) {
+ ReactEditor.blur(slate);
+ ReactEditor.deselect(slate);
+ }
+ });
+ }
+ };
+
+ const onShiftKeyDown = (blockOrder: number) => {
+ blurSlateSelection();
+
+ const currentSelectionIndex = editor.selection![0];
+ const indexesBetween = Array.from({ length: Math.abs(blockOrder - currentSelectionIndex) }).map((_, index) =>
+ blockOrder > currentSelectionIndex ? currentSelectionIndex + index + 1 : currentSelectionIndex - index - 1,
+ );
+
+ editor.setBlockSelected([...indexesBetween, currentSelectionIndex], { only: true });
+ editor.setSelection([blockOrder]);
+ };
+
+ const onMouseDown = (e: React.MouseEvent) => {
+ if (editor.readOnly) return;
+
+ if (Array.isArray(editor.selectedBlocks) && editor.selectedBlocks.length > 0 && !e.shiftKey && !e.altKey) {
+ editor.setBlockSelected(null);
+ }
+
+ const target = e.target as HTMLElement;
+ const blockElement = target.closest('[data-yoopta-block]');
+
+ if (blockElement && e.button === 0) {
+ const blockId = blockElement.getAttribute('data-yoopta-block-id') || '';
+ const blockOrder = editor.children[blockId]?.meta.order;
+
+ if (typeof blockOrder === 'number') {
+ isMultiSelectingStarted.current = true;
+ startBlockPathRef.current = blockOrder;
+ currentBlockPathRef.current = blockOrder;
+
+ if (e.shiftKey && Array.isArray(editor.selection) && blockOrder !== editor.selection?.[0]) {
+ onShiftKeyDown(blockOrder);
+ return;
+ }
+
+ if (blockOrder !== editor.selection?.[0]) {
+ editor.setSelection([blockOrder]);
+ }
+
+ editor.refElement?.addEventListener('mousemove', onMouseMove);
+ editor.refElement?.addEventListener('mouseup', onMouseUp);
+ }
+ }
+ };
+
+ const onMouseMove = (e: MouseEvent) => {
+ if (!isMultiSelectingStarted.current || editor.readOnly) return;
+
+ const target = document.elementFromPoint(e.clientX, e.clientY);
+ const blockElement = target?.closest('[data-yoopta-block]');
+
+ if (blockElement) {
+ const blockId = blockElement.getAttribute('data-yoopta-block-id') || '';
+ const blockOrder = editor.children[blockId]?.meta.order;
+
+ // When multi-selecting is started and the mouse is moving over the start block
+ if (
+ isMultiSelectingInProgress.current &&
+ typeof blockOrder === 'number' &&
+ blockOrder === startBlockPathRef.current
+ ) {
+ currentBlockPathRef.current = blockOrder;
+ editor.setBlockSelected([blockOrder], { only: true });
+ return;
+ }
+
+ // Multi-selecting started between blocks
+ if (typeof blockOrder === 'number' && blockOrder !== currentBlockPathRef.current) {
+ currentBlockPathRef.current = blockOrder;
+ isMultiSelectingInProgress.current = true;
+
+ const start = Math.min(startBlockPathRef.current!, blockOrder);
+ const end = Math.max(startBlockPathRef.current!, blockOrder);
+
+ blurSlateSelection();
+
+ const selectedBlocks = Array.from({ length: end - start + 1 }, (_, i) => start + i);
+ editor.setBlockSelected(selectedBlocks);
+ }
+ }
+ };
+
+ const onMouseUp = () => {
+ isMultiSelectingStarted.current = false;
+ isMultiSelectingInProgress.current = false;
+ startBlockPathRef.current = null;
+ currentBlockPathRef.current = null;
+ editor.refElement?.removeEventListener('mousemove', onMouseMove);
+ editor.refElement?.removeEventListener('mouseup', onMouseUp);
+ };
+
+ return { onMouseDown };
+}
diff --git a/packages/core/editor/src/components/NoSsr/NoSsr.tsx b/packages/core/editor/src/components/NoSsr/NoSsr.tsx
deleted file mode 100644
index b55ac6721..000000000
--- a/packages/core/editor/src/components/NoSsr/NoSsr.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { ReactNode, useEffect, useState } from 'react';
-
-type Props = {
- children: ReactNode; // React.ReactNode
- fallback?: any; // JSX.Element
-};
-
-const NoSSR = ({ children, fallback = null }: Props) => {
- const [mounted, setMounted] = useState(false);
-
- useEffect(() => setMounted(true), []);
-
- if (!mounted) {
- return fallback;
- }
-
- return children;
-};
-
-export default NoSSR;
diff --git a/packages/core/editor/src/components/SelectionBox/hooks.ts b/packages/core/editor/src/components/SelectionBox/hooks.ts
index e3d403b8e..693943fe3 100644
--- a/packages/core/editor/src/components/SelectionBox/hooks.ts
+++ b/packages/core/editor/src/components/SelectionBox/hooks.ts
@@ -62,13 +62,6 @@ export const useRectangeSelectionBox = ({ editor, root }: RectangeSelectionProps
if (isInsideEditor) return;
- // const slate = findSlateBySelectionPath(editor);
- // if (slate) {
- // ReactEditor.blur(slate);
- // ReactEditor.deselect(slate);
- // Transforms.deselect(slate);
- // }
-
setState({
origin: [event.pageX, event.pageY - window.pageYOffset],
coords: [event.pageX, event.pageY - window.pageYOffset],
diff --git a/packages/core/editor/src/contexts/YooptaContext/YooptaContext.tsx b/packages/core/editor/src/contexts/YooptaContext/YooptaContext.tsx
index 1a448178e..2503fb550 100644
--- a/packages/core/editor/src/contexts/YooptaContext/YooptaContext.tsx
+++ b/packages/core/editor/src/contexts/YooptaContext/YooptaContext.tsx
@@ -1,5 +1,5 @@
import { createContext, useContext, useRef } from 'react';
-import { YooEditor, YooptaBlockPath } from '../../editor/types';
+import { YooEditor, YooptaBlockPath, YooptaContentValue } from '../../editor/types';
import { PluginOptions } from '../../plugins/types';
import { findPluginBlockBySelectionPath } from '../../utils/findPluginBlockBySelectionPath';
@@ -30,7 +30,7 @@ const DEFAULT_HANDLERS: YooptaEditorContext = {
setSelection: () => undefined,
applyChanges: () => undefined,
- getEditorValue: () => undefined,
+ getEditorValue: () => ({}),
setEditorValue: () => undefined,
blocks: {},
@@ -42,6 +42,7 @@ const DEFAULT_HANDLERS: YooptaEditorContext = {
isEmpty: () => false,
blockEditorsMap: {},
children: {},
+ commands: {},
emit: () => undefined,
on: () => undefined,
diff --git a/packages/core/editor/src/editor/blocks/createBlock.ts b/packages/core/editor/src/editor/blocks/createBlock.ts
index 51c9f2d46..858e36ad4 100644
--- a/packages/core/editor/src/editor/blocks/createBlock.ts
+++ b/packages/core/editor/src/editor/blocks/createBlock.ts
@@ -22,9 +22,6 @@ export function createBlock(editor: YooEditor, type: string, options?: CreateBlo
if (!slate || !slate.selection) return;
const selectedBlock = editor.blocks[type];
- const elements = buildBlockElementsStructure(editor, type);
-
- if (options?.deleteText) Transforms.delete(slate, { at: [0, 0] });
const blockData = buildBlockData({
id: generateId(),
@@ -36,6 +33,17 @@ export function createBlock(editor: YooEditor, type: string, options?: CreateBlo
},
});
+ const plugin = editor.plugins[type];
+ const pluginEvents = plugin.events || {};
+ const { onBeforeCreate, onCreate } = pluginEvents;
+
+ const elements =
+ typeof onBeforeCreate === 'function'
+ ? onBeforeCreate(editor, blockData.id)
+ : buildBlockElementsStructure(editor, type);
+
+ if (options?.deleteText) Transforms.delete(slate, { at: [0, 0] });
+
const newSlate = buildSlateEditor(editor);
newSlate.children = [elements];
@@ -52,6 +60,8 @@ export function createBlock(editor: YooEditor, type: string, options?: CreateBlo
editor.applyChanges();
editor.emit('change', editor.children);
+ onCreate?.(editor, blockData.id);
+
if (options?.focus) {
editor.focusBlock(blockId, { slate: newSlate, waitExecution: true });
}
diff --git a/packages/core/editor/src/editor/blocks/deleteBlock.ts b/packages/core/editor/src/editor/blocks/deleteBlock.ts
index 240b80622..eb54809c5 100644
--- a/packages/core/editor/src/editor/blocks/deleteBlock.ts
+++ b/packages/core/editor/src/editor/blocks/deleteBlock.ts
@@ -20,15 +20,21 @@ export function deleteBlock(editor: YooEditor, options: DeleteBlockOptions = {})
fromPaths.forEach((path) => {
const block = findPluginBlockBySelectionPath(editor, { at: [path] });
if (block) {
+ const plugin = editor.plugins[block.type];
+ const pluginEvents = plugin.events || {};
+ const { onDestroy } = pluginEvents;
+
+ onDestroy?.(editor, block.id);
+
delete editor.children[block.id];
delete editor.blockEditorsMap[block.id];
}
});
// Reorder blocks
- const pluginKeys = Object.keys(editor.children);
+ const blockDataKeys = Object.keys(editor.children);
- pluginKeys.forEach((id, index) => {
+ blockDataKeys.forEach((id, index) => {
editor.children[id].meta.order = index;
});
@@ -60,18 +66,26 @@ export function deleteBlock(editor: YooEditor, options: DeleteBlockOptions = {})
editor.children = createDraft(editor.children);
const [position] = at;
- const pluginKeys = Object.keys(editor.children);
+ const blockDataKeys = Object.keys(editor.children);
- const pluginToDeleteId = pluginKeys.find((id) => editor.children[id].meta.order === position);
+ const blockIdToDelete = blockDataKeys.find((id) => editor.children[id].meta.order === position);
- pluginKeys.forEach((blockId) => {
- const plugin = editor.children[blockId];
- if (plugin.meta.order > position) plugin.meta.order -= 1;
+ blockDataKeys.forEach((blockId) => {
+ const blockData = editor.children[blockId];
+ if (blockData.meta.order > position) blockData.meta.order -= 1;
});
- if (pluginToDeleteId) {
- delete editor.children[pluginToDeleteId];
- delete editor.blockEditorsMap[pluginToDeleteId];
+ if (blockIdToDelete) {
+ const block = editor.children[blockIdToDelete];
+ const plugin = editor.plugins[block.type];
+
+ const pluginEvents = plugin.events || {};
+ const { onDestroy } = pluginEvents;
+
+ onDestroy?.(editor, blockIdToDelete);
+
+ delete editor.children[blockIdToDelete];
+ delete editor.blockEditorsMap[blockIdToDelete];
}
editor.children = finishDraft(editor.children);
diff --git a/packages/core/editor/src/editor/blocks/duplicateBlock.tsx b/packages/core/editor/src/editor/blocks/duplicateBlock.ts
similarity index 100%
rename from packages/core/editor/src/editor/blocks/duplicateBlock.tsx
rename to packages/core/editor/src/editor/blocks/duplicateBlock.ts
diff --git a/packages/core/editor/src/editor/blocks/getSlate.ts b/packages/core/editor/src/editor/blocks/getSlate.ts
new file mode 100644
index 000000000..e4cd89d5b
--- /dev/null
+++ b/packages/core/editor/src/editor/blocks/getSlate.ts
@@ -0,0 +1,29 @@
+import { SlateEditor, YooEditor, YooptaBlockData, YooptaBlockPath } from '../types';
+
+export type GetSlateOptions = {
+ at?: YooptaBlockPath;
+ id?: string;
+};
+
+export function getSlate(editor: YooEditor, options: GetSlateOptions): SlateEditor {
+ if (!options?.id && !options?.at) {
+ throw new Error('getSlate requires either an id or at');
+ }
+
+ const blockId =
+ options?.id ||
+ Object.keys(editor.children).find((childrenId) => {
+ const plugin = editor.children[childrenId];
+ return plugin.meta.order === options?.at?.[0];
+ });
+
+ const slate = editor.blockEditorsMap[blockId || ''];
+ const blockData = editor.children[blockId || ''] as YooptaBlockData;
+ const blockEntity = editor.blocks[blockData?.type || ''];
+
+ if (!blockEntity?.hasCustomEditor && !slate) {
+ throw new Error(`Slate not found with params: ${JSON.stringify(options)}`);
+ }
+
+ return slate;
+}
diff --git a/packages/core/editor/src/editor/blocks/index.ts b/packages/core/editor/src/editor/blocks/index.ts
index 5f2536d78..4579cb02a 100644
--- a/packages/core/editor/src/editor/blocks/index.ts
+++ b/packages/core/editor/src/editor/blocks/index.ts
@@ -12,6 +12,7 @@ import { toggleBlock } from './toggleBlock';
import { insertBlocks } from './insertBlocks';
import { createBlock } from './createBlock';
import { getBlock } from './getBlock';
+import { getSlate } from './getSlate';
export const Blocks = {
insertBlock,
@@ -28,6 +29,7 @@ export const Blocks = {
createBlock,
deleteBlocks,
getBlock,
+ getSlate,
// [TODO]
// updateBlocks
};
diff --git a/packages/core/editor/src/editor/blocks/insertBlock.ts b/packages/core/editor/src/editor/blocks/insertBlock.ts
index 6098eaaca..ed212057d 100644
--- a/packages/core/editor/src/editor/blocks/insertBlock.ts
+++ b/packages/core/editor/src/editor/blocks/insertBlock.ts
@@ -6,11 +6,20 @@ import { generateId } from '../../utils/generateId';
import { YooEditor, YooptaEditorTransformOptions, YooptaBlockData } from '../types';
// make blockData optional
-export function insertBlock(editor: YooEditor, blockData: YooptaBlockData, options: YooptaEditorTransformOptions = {}) {
+export function insertBlock(
+ editor: YooEditor,
+ blockData: YooptaBlockData,
+ options: Partial = {},
+) {
editor.children = createDraft(editor.children);
const { at = null, focus = false, slate = null } = options;
const currentBlock = findPluginBlockBySelectionPath(editor);
+
+ const plugin = editor.plugins[blockData.type];
+ const pluginEvents = plugin.events || {};
+ const { onCreate } = pluginEvents;
+
const nextBlockPath = at;
const newPluginBlock = {
id: generateId(),
@@ -60,6 +69,8 @@ export function insertBlock(editor: YooEditor, blockData: YooptaBlockData, optio
editor.applyChanges();
editor.emit('change', editor.children);
+ onCreate?.(editor, newPluginBlock.id);
+
if (focus) {
editor.focusBlock(insertBefore && currentBlockId ? currentBlockId : newPluginBlock.id);
}
diff --git a/packages/core/editor/src/editor/blocks/insertBlocks.ts b/packages/core/editor/src/editor/blocks/insertBlocks.ts
index 783be09c3..d11dcb55a 100644
--- a/packages/core/editor/src/editor/blocks/insertBlocks.ts
+++ b/packages/core/editor/src/editor/blocks/insertBlocks.ts
@@ -4,7 +4,7 @@ import { ReactEditor } from 'slate-react';
import { buildSlateEditor } from '../../utils/buildSlate';
import { findPluginBlockBySelectionPath } from '../../utils/findPluginBlockBySelectionPath';
import { findSlateBySelectionPath } from '../../utils/findSlateBySelectionPath';
-import { YooEditor, YooptaEditorTransformOptions, YooptaBlockData, YooptaBlockPath } from '../types';
+import { YooEditor, YooptaEditorTransformOptions, YooptaBlockData } from '../types';
export function insertBlocks(editor: YooEditor, blocks: YooptaBlockData[], options: YooptaEditorTransformOptions = {}) {
editor.children = createDraft(editor.children);
diff --git a/packages/core/editor/src/editor/blocks/updateBlock.ts b/packages/core/editor/src/editor/blocks/updateBlock.ts
index e35515650..6ac4518e3 100644
--- a/packages/core/editor/src/editor/blocks/updateBlock.ts
+++ b/packages/core/editor/src/editor/blocks/updateBlock.ts
@@ -1,6 +1,7 @@
import { createDraft, finishDraft } from 'immer';
import { YooEditor, YooptaBlockData } from '../types';
+// [TODO] - optimize updateBlock
export function updateBlock(
editor: YooEditor,
blockId: string,
diff --git a/packages/core/editor/src/editor/elements/getElementEntry.ts b/packages/core/editor/src/editor/elements/getElementEntry.ts
index c8bab1848..c1f4d5afe 100644
--- a/packages/core/editor/src/editor/elements/getElementEntry.ts
+++ b/packages/core/editor/src/editor/elements/getElementEntry.ts
@@ -3,7 +3,7 @@ import { findSlateBySelectionPath } from '../../utils/findSlateBySelectionPath';
import { SlateElement, YooEditor } from '../types';
export type GetBlockElementEntryOptions = {
- path?: Selection | Location | Span;
+ path?: Location | Span | undefined;
type?: string;
};
@@ -32,6 +32,7 @@ export function getElementEntry(
}
try {
+ // to Editor.above
const [elementEntry] = Editor.nodes(slate, {
at: options?.path || slate.selection || [0],
match,
diff --git a/packages/core/editor/src/editor/index.tsx b/packages/core/editor/src/editor/index.tsx
index a4a5eacea..bd1db9b63 100644
--- a/packages/core/editor/src/editor/index.tsx
+++ b/packages/core/editor/src/editor/index.tsx
@@ -23,8 +23,9 @@ import { getHTML } from '../parsers/getHTML';
import { getMarkdown } from '../parsers/getMarkdown';
import { getPlainText } from '../parsers/getPlainText';
import { isEmpty } from './core/isEmpty';
+import { Plugin } from '../plugins/types';
-export const createYooptaEditor = (): YooEditor => {
+export function createYooptaEditor(): YooEditor {
const editor: YooEditor = {
id: '',
children: {},
@@ -55,6 +56,7 @@ export const createYooptaEditor = (): YooEditor => {
formats: {},
shortcuts: {},
plugins: {},
+ commands: {},
on: (event, callback) => {},
off: (event, callback) => {},
@@ -73,6 +75,6 @@ export const createYooptaEditor = (): YooEditor => {
};
return editor;
-};
+}
export const EDITOR_TO_ON_CHANGE = new WeakMap();
diff --git a/packages/core/editor/src/editor/types.ts b/packages/core/editor/src/editor/types.ts
index e08036513..5f0674d97 100644
--- a/packages/core/editor/src/editor/types.ts
+++ b/packages/core/editor/src/editor/types.ts
@@ -72,8 +72,10 @@ export type YooptaFormats = Record;
export type YooEditorEvents = 'change' | 'focus' | 'blur' | 'block:copy';
+export type BaseCommands = Record any>;
+
// [TODO] - Fix generic and default types
-export type YooEditor = {
+export type YooEditor = {
id: string;
readOnly: boolean;
isEmpty: () => boolean;
@@ -94,7 +96,7 @@ export type YooEditor = {
selection: YooptaBlockPath | null;
selectedBlocks: number[] | null;
children: YooptaContentValue;
- getEditorValue: () => TNodes;
+ getEditorValue: () => YooptaContentValue;
setEditorValue: (value: YooptaContentValue) => void;
setSelection: (path: YooptaBlockPath | null, options?: SetSelectionOptions) => void;
setBlockSelected: (path: number[] | null, options?: BlockSelectedOptions) => void;
@@ -102,7 +104,8 @@ export type YooEditor = {
blocks: YooptaBlocks;
formats: YooptaFormats;
shortcuts: Record;
- plugins: Record>;
+ plugins: Record, unknown>>;
+ commands: Record any>;
// events handlers
on: (event: YooEditorEvents, fn: (payload: any) => void) => void;
diff --git a/packages/core/editor/src/handlers/onKeyDown.ts b/packages/core/editor/src/handlers/onKeyDown.ts
index 052de2004..691de1b66 100644
--- a/packages/core/editor/src/handlers/onKeyDown.ts
+++ b/packages/core/editor/src/handlers/onKeyDown.ts
@@ -189,26 +189,22 @@ export function onKeyDown(editor: YooEditor) {
return;
}
+ // [TODO] - default behavior for complex plugins
if (HOTKEYS.isArrowUp(event)) {
if (event.isDefaultPrevented()) return;
-
// If element with any paths has all paths at 0
const isAllPathsInStart = new Set(slate.selection.anchor.path).size === 1;
-
if (isAllPathsInStart) {
const prevPath: YooptaBlockPath | null = editor.selection ? [editor.selection[0] - 1] : null;
const prevSlate = findSlateBySelectionPath(editor, { at: prevPath });
const prevBlock = findPluginBlockBySelectionPath(editor, { at: prevPath });
-
if (prevSlate && prevBlock) {
const [, prevLastPath] = Editor.last(prevSlate, [0]);
const prevLastNodeTextLength = Editor.string(prevSlate, prevLastPath).length;
-
const selection: Point = {
path: prevLastPath,
offset: prevLastNodeTextLength,
};
-
event.preventDefault();
editor.focusBlock(prevBlock.id, {
focusAt: selection,
@@ -220,21 +216,18 @@ export function onKeyDown(editor: YooEditor) {
}
}
+ // [TODO] - default behavior for complex plugins
if (HOTKEYS.isArrowDown(event)) {
if (event.isDefaultPrevented()) return;
-
const parentPath = Path.parent(slate.selection.anchor.path);
const isEnd = Editor.isEnd(slate, slate.selection.anchor, parentPath);
-
if (isEnd) {
const nextPath: YooptaBlockPath | null = editor.selection ? [editor.selection[0] + 1] : null;
const nextSlate = findSlateBySelectionPath(editor, { at: nextPath });
const nextBlock = findPluginBlockBySelectionPath(editor, { at: nextPath });
-
if (nextSlate && nextBlock) {
// [TODO] - should parent path, but for next slate
const selection: Point = getNextNodePoint(nextSlate, parentPath);
-
event.preventDefault();
editor.focusBlock(nextBlock.id, { focusAt: selection, waitExecution: false });
return;
diff --git a/packages/core/editor/src/index.ts b/packages/core/editor/src/index.ts
index 2e3bcc989..390dc9e13 100644
--- a/packages/core/editor/src/index.ts
+++ b/packages/core/editor/src/index.ts
@@ -20,10 +20,20 @@ export { getRootBlockElementType, getRootBlockElement } from './utils/blockEleme
export { findPluginBlockBySelectionPath } from './utils/findPluginBlockBySelectionPath';
export { findSlateBySelectionPath } from './utils/findSlateBySelectionPath';
export { findPluginBlockByType } from './utils/findPluginBlockByType';
+export { deserializeTextNodes } from './parsers/deserializeTextNodes';
+export { serializeTextNodes, serializeTextNodesIntoMarkdown } from './parsers/serializeTextNodes';
export { createYooptaEditor } from './editor';
export { createYooptaMark, YooptaMarkParams, YooptaMark } from './marks';
-export { YooEditor, SlateElement, YooptaBlockData, YooptaBlock, YooptaContentValue, SlateEditor } from './editor/types';
+export {
+ YooEditor,
+ SlateElement,
+ YooptaBlockData,
+ YooptaBlock,
+ YooptaContentValue,
+ SlateEditor,
+ YooptaBlockPath,
+} from './editor/types';
export { buildBlockData, buildBlockElement } from './components/Editor/utils';
export { buildBlockElementsStructure } from './utils/blockElements';
@@ -34,6 +44,7 @@ export {
PluginDeserializeParser,
PluginserializeParser,
YooptaMarkProps,
+ PluginOptions,
} from './plugins/types';
export { Elements } from './editor/elements';
diff --git a/packages/core/editor/src/parsers/deserializeHTML.ts b/packages/core/editor/src/parsers/deserializeHTML.ts
index 4fb2c804c..a5a4b5916 100644
--- a/packages/core/editor/src/parsers/deserializeHTML.ts
+++ b/packages/core/editor/src/parsers/deserializeHTML.ts
@@ -85,7 +85,7 @@ function buildBlock(editor: YooEditor, plugin: PluginsMapByNode, el: HTMLElement
let rootNode: SlateElement | YooptaBlockData[] = {
id: generateId(),
type: rootElementType,
- children: isVoid && !block.hasCustomEditor ? [{ text: '' }] : children.map(mapNodeChildren),
+ children: isVoid && !block.hasCustomEditor ? [{ text: '' }] : children.map(mapNodeChildren).flat(),
props: { nodeType: 'block', ...rootElement.props },
};
@@ -121,40 +121,45 @@ function buildBlock(editor: YooEditor, plugin: PluginsMapByNode, el: HTMLElement
return blockData;
}
-export function deserialize(editor: YooEditor, pluginsMap: PluginsMapByNodeNames, el: HTMLElement | ChildNode) {
+function deserialize(editor: YooEditor, pluginsMap: PluginsMapByNodeNames, el: HTMLElement | ChildNode) {
if (el.nodeType === 3) {
const text = el.textContent?.replace(/[\t\n\r\f\v]+/g, ' ');
- return text;
+ return { text };
} else if (el.nodeType !== 1) {
return null;
} else if (el.nodeName === 'BR') {
- return '\n';
+ return { text: '\n' };
}
- const parent = el;
-
+ const parent = el as HTMLElement;
let children = Array.from(parent.childNodes)
.map((node) => deserialize(editor, pluginsMap, node))
- .flat();
+ .flat()
+ .filter(Boolean);
- if (MARKS_NODE_NAME_MATCHERS_MAP[el.nodeName]) {
- const mark = MARKS_NODE_NAME_MATCHERS_MAP[el.nodeName];
+ if (MARKS_NODE_NAME_MATCHERS_MAP[parent.nodeName]) {
+ const mark = MARKS_NODE_NAME_MATCHERS_MAP[parent.nodeName];
const markType = mark.type;
- const text = el.textContent?.replace(/[\t\n\r\f\v]+/g, ' ');
- return { [markType]: true, text };
+
+ return children.map((child) => {
+ if (typeof child === 'string') {
+ return { [markType]: true, text: child };
+ } else if (child.text) {
+ return { ...child, [markType]: true };
+ }
+ return child;
+ });
}
- const plugin = pluginsMap[el.nodeName];
+ const plugin = pluginsMap[parent.nodeName];
if (plugin) {
if (Array.isArray(plugin)) {
- const blocks = plugin.map((p) => buildBlock(editor, p, el as HTMLElement, children)).filter(Boolean);
- console.log('blocks', blocks);
-
+ const blocks = plugin.map((p) => buildBlock(editor, p, parent, children)).filter(Boolean);
return blocks;
}
- return buildBlock(editor, plugin, el as HTMLElement, children);
+ return buildBlock(editor, plugin, parent, children);
}
return children;
@@ -170,7 +175,7 @@ function mapNodeChildren(child) {
}
if (Array.isArray(child)) {
- return { text: child[0] };
+ return child.map(mapNodeChildren).flat();
}
if (child?.text) {
@@ -179,13 +184,7 @@ function mapNodeChildren(child) {
if (isYooptaBlock(child)) {
const block = child as YooptaBlockData;
- let text = '';
-
- (block.value[0] as SlateElement).children.forEach((child: any) => {
- text += `${child.text}`;
- });
-
- return { text };
+ return (block.value[0] as SlateElement).children.map(mapNodeChildren).flat();
}
return { text: '' };
diff --git a/packages/core/editor/src/parsers/deserializeTextNodes.ts b/packages/core/editor/src/parsers/deserializeTextNodes.ts
new file mode 100644
index 000000000..89ee0f966
--- /dev/null
+++ b/packages/core/editor/src/parsers/deserializeTextNodes.ts
@@ -0,0 +1,88 @@
+import { Descendant } from 'slate';
+import { YooEditor } from '../editor/types';
+import { generateId } from '../utils/generateId';
+
+// [TODO] - Move to @yoopta/utils or @yoopta/editor/utils
+// helpers for deserializing text nodes when you use custom parsers in your plugins
+export function deserializeTextNodes(editor: YooEditor, nodes: NodeListOf): Descendant[] {
+ const deserializedNodes: Descendant[] = [];
+
+ nodes.forEach((node) => {
+ if (node.nodeType === Node.TEXT_NODE) {
+ deserializedNodes.push({
+ text: node.textContent || '',
+ });
+ }
+
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ const element = node as HTMLElement;
+
+ // [TODO] - Hmmmm
+ if (element.nodeName === 'P' || element.nodeName === 'SPAN' || element.nodeName === 'DIV') {
+ deserializedNodes.push({
+ ...deserializeTextNodes(editor, element.childNodes)[0],
+ });
+ }
+
+ if (element.nodeName === 'B' || element.nodeName === 'STRONG') {
+ deserializedNodes.push({
+ // @ts-ignore [FIXME] - Fix types
+ bold: true,
+ ...deserializeTextNodes(editor, element.childNodes)[0],
+ });
+ }
+
+ if (element.nodeName === 'I' || element.nodeName === 'EM') {
+ deserializedNodes.push({
+ // @ts-ignore [FIXME] - Fix types
+ italic: true,
+ ...deserializeTextNodes(editor, element.childNodes)[0],
+ });
+ }
+
+ if (element.nodeName === 'S' || element.nodeName === 'STRIKE') {
+ deserializedNodes.push({
+ // @ts-ignore [FIXME] - Fix types
+ strike: true,
+ ...deserializeTextNodes(editor, element.childNodes)[0],
+ });
+ }
+
+ if (element.nodeName === 'U' || element.nodeName === 'INS') {
+ deserializedNodes.push({
+ // @ts-ignore [FIXME] - Fix types
+ underline: true,
+ ...deserializeTextNodes(editor, element.childNodes)[0],
+ });
+ }
+
+ if (element.nodeName === 'CODE') {
+ deserializedNodes.push({
+ // @ts-ignore [FIXME] - Fix types
+ code: true,
+ ...deserializeTextNodes(editor, element.childNodes)[0],
+ });
+ }
+
+ if (element.nodeName === 'A') {
+ deserializedNodes.push({
+ id: generateId(),
+ type: 'link',
+ props: {
+ url: element.getAttribute('href') || '',
+ target: element.getAttribute('target') || '',
+ rel: element.getAttribute('rel') || '',
+ },
+ children: deserializeTextNodes(editor, element.childNodes),
+ });
+ }
+ }
+ });
+
+ // @ts-ignore [FIXME] - Fix types
+ if (deserializedNodes.length === 0 && !deserializedNodes[0]?.text) {
+ deserializedNodes.push({ text: '' });
+ }
+
+ return deserializedNodes;
+}
diff --git a/packages/core/editor/src/parsers/serializeTextNodes.ts b/packages/core/editor/src/parsers/serializeTextNodes.ts
new file mode 100644
index 000000000..daa038887
--- /dev/null
+++ b/packages/core/editor/src/parsers/serializeTextNodes.ts
@@ -0,0 +1,77 @@
+// [TODO] - Move to @yoopta/utils or @yoopta/editor/utils
+// helpers for serializing text nodes when you use custom parsers in your plugins
+export function serializeTextNodes(nodes: any[]): string {
+ return nodes
+ .map((node) => {
+ if ('text' in node) {
+ let text = node.text;
+
+ if (node.bold) {
+ text = `${text}`;
+ }
+ if (node.italic) {
+ text = `${text}`;
+ }
+ if (node.strike) {
+ text = `${text}`;
+ }
+ if (node.underline) {
+ text = `${text}`;
+ }
+ if (node.code) {
+ text = `${text}
`;
+ }
+
+ return text;
+ }
+
+ if (node.type === 'link') {
+ const { url, target, rel } = node.props;
+ const children = serializeTextNodes(node.children);
+
+ return `${children}`;
+ }
+
+ return '';
+ })
+ .join('');
+}
+
+// [TODO] - Move to @yoopta/utils or @yoopta/editor/utils
+// helpers for serializing text nodes into markdown style when you use custom parsers in your plugins
+export function serializeTextNodesIntoMarkdown(nodes: any[]): string {
+ return nodes
+ .map((node) => {
+ if ('text' in node) {
+ let text = node.text;
+
+ if (node.bold) {
+ text = `**${text}**`;
+ }
+ if (node.italic) {
+ text = `*${text}*`;
+ }
+ if (node.strike) {
+ text = `~~${text}~~`;
+ }
+ if (node.underline) {
+ text = `${text}`;
+ }
+ if (node.code) {
+ text = `\`${text}\``;
+ }
+
+ return text;
+ }
+
+ if (node.type === 'link') {
+ const { url, target, rel } = node.props;
+ const children = serializeTextNodesIntoMarkdown(node.children);
+
+ return `[${children}](${url})`;
+ }
+
+ return '';
+ })
+ .join('');
+}
diff --git a/packages/core/editor/src/plugins/SlateEditorComponent.tsx b/packages/core/editor/src/plugins/SlateEditorComponent.tsx
index 80c518ce7..e029e6ee5 100644
--- a/packages/core/editor/src/plugins/SlateEditorComponent.tsx
+++ b/packages/core/editor/src/plugins/SlateEditorComponent.tsx
@@ -1,30 +1,25 @@
-import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react';
-import { DefaultElement, Editable, ReactEditor, RenderElementProps, Slate } from 'slate-react';
+import React, { memo, useCallback, useMemo, useRef } from 'react';
+import { DefaultElement, Editable, RenderElementProps, Slate } from 'slate-react';
import { useYooptaEditor, useBlockData } from '../contexts/YooptaContext/YooptaContext';
import { EVENT_HANDLERS } from '../handlers';
import { YooptaMark } from '../marks';
-import { ExtendedLeafProps, PluginCustomEditorRenderProps, PluginEventHandlerOptions, Plugin } from './types';
+import { ExtendedLeafProps, PluginCustomEditorRenderProps, Plugin, PluginEvents } from './types';
import { EditorEventHandlers } from '../types/eventHandlers';
-import { HOTKEYS } from '../utils/hotkeys';
-import { Editor, Element, Node, NodeEntry, Path, Range, Transforms } from 'slate';
+import { Editor, NodeEntry, Range } from 'slate';
import { TextLeaf } from '../components/TextLeaf/TextLeaf';
-import { generateId } from '../utils/generateId';
-import { buildBlockData } from '../components/Editor/utils';
-
-// [TODO] - test
-import { withInlines } from './extenstions/withInlines';
import { IS_FOCUSED_EDITOR } from '../utils/weakMaps';
import { deserializeHTML } from '../parsers/deserializeHTML';
-import { getRootBlockElementType } from '../utils/blockElements';
-import { Elements } from '../editor/elements';
+import { useEventHandlers, useSlateEditor } from './hooks';
+import { SlateElement } from '../editor/types';
-type Props = Plugin & {
+type Props, TOptions> = Plugin & {
id: string;
marks?: YooptaMark[];
- options: Plugin['options'];
+ options: Plugin['options'];
placeholder?: string;
+ events?: PluginEvents;
};
const getMappedElements = (elements) => {
@@ -41,111 +36,31 @@ const getMappedMarks = (marks?: YooptaMark[]) => {
return mappedMarks;
};
-const SlateEditorComponent = ({
+const SlateEditorComponent = , TOptions>({
id,
customEditor,
elements,
marks,
events,
options,
+ extensions: withExtensions,
placeholder = `Type '/' for commands`,
-}: Props) => {
+}: Props) => {
const editor = useYooptaEditor();
const block = useBlockData(id);
- const initialValue = useRef(block.value).current;
-
+ let initialValue = useRef(block.value).current;
const ELEMENTS_MAP = useMemo(() => getMappedElements(elements), [elements]);
const MARKS_MAP = useMemo(() => getMappedMarks(marks), [marks]);
- const slate = useMemo(() => {
- let slateEditor = editor.blockEditorsMap[id];
- const { normalizeNode } = slateEditor;
- const elementTypes = Object.keys(elements);
-
- elementTypes.forEach((elementType) => {
- const nodeType = elements[elementType].props?.nodeType;
-
- const isInline = nodeType === 'inline';
- const isVoid = nodeType === 'void';
- const isInlineVoid = nodeType === 'inlineVoid';
-
- if (isInlineVoid) {
- slateEditor.markableVoid = (element) => element.type === elementType;
- }
-
- if (isVoid || isInlineVoid) {
- slateEditor.isVoid = (element) => element.type === elementType;
- }
-
- if (isInline || isInlineVoid) {
- slateEditor.isInline = (element) => element.type === elementType;
-
- // [TODO] - as test
- // [TODO] - should extend all slate editors for every block
- slateEditor = withInlines(editor, slateEditor);
- }
- });
-
- // This normalization is needed to validate the elements structure
- slateEditor.normalizeNode = (entry) => {
- const [node, path] = entry;
- const blockElements = editor.blocks[block.type].elements;
-
- // Normalize only `simple` block elements.
- // Simple elements are elements that have only one defined block element type.
- // [TODO] - handle validation for complex block elements
- if (Object.keys(blockElements).length > 1) {
- return normalizeNode(entry);
- }
-
- if (Element.isElement(node)) {
- const { type } = node;
- const rootElementType = getRootBlockElementType(blockElements);
-
- if (!elementTypes.includes(type)) {
- Transforms.setNodes(slateEditor, { type: rootElementType, props: { ...node.props } }, { at: path });
- return;
- }
-
- if (node.type === rootElementType) {
- for (const [child, childPath] of Node.children(slateEditor, path)) {
- if (Element.isElement(child) && !slateEditor.isInline(child)) {
- Transforms.unwrapNodes(slateEditor, { at: childPath });
- return;
- }
- }
- }
- }
+ const slate = useSlateEditor(id, editor, block, elements, withExtensions);
+ const eventHandlers = useEventHandlers(events, editor, block, slate);
- normalizeNode(entry);
- };
-
- return slateEditor;
- }, [elements, id]);
-
- const eventHandlers = useMemo(() => {
- if (!events || editor.readOnly) return {};
-
- const eventHandlersOptions: PluginEventHandlerOptions = {
- hotkeys: HOTKEYS,
- currentBlock: block,
- defaultBlock: buildBlockData({ id: generateId() }),
- };
- const eventHandlersMap = {};
-
- Object.keys(events).forEach((eventType) => {
- eventHandlersMap[eventType] = function handler(event) {
- if (events[eventType]) {
- const handler = events[eventType](editor, slate, eventHandlersOptions);
- handler(event);
- }
- };
- });
-
- return eventHandlersMap;
- }, [events, editor, block]);
-
- const onChange = useCallback((value) => editor.updateBlock(id, { value }), [id]);
+ const onChange = useCallback(
+ (value) => {
+ editor.updateBlock(id, { value });
+ },
+ [id, editor],
+ );
const renderElement = useCallback(
(elementProps: RenderElementProps) => {
@@ -153,24 +68,10 @@ const SlateEditorComponent = ({
const { attributes, ...props } = elementProps;
attributes['data-element-type'] = props.element.type;
- let path;
-
- try {
- path = ReactEditor.findPath(slate, elementProps.element);
- } catch (error) {
- path = [];
- }
-
if (!ElementComponent) return ;
return (
-
+
);
},
[elements, slate.children],
@@ -221,18 +122,6 @@ const SlateEditorComponent = ({
[eventHandlers.onKeyUp, editor.readOnly],
);
- const onMouseDown = useCallback(
- (event: React.MouseEvent) => {
- if (editor.readOnly) return;
-
- if (editor.selection?.[0] !== block.meta.order) {
- editor.setSelection([block.meta.order]);
- }
- eventHandlers?.onMouseDown?.(event);
- },
- [eventHandlers.onMouseDown, editor.readOnly, editor.selection?.[0], block.meta.order],
- );
-
const onBlur = useCallback(
(event: React.FocusEvent) => {
if (editor.readOnly) return;
@@ -322,7 +211,6 @@ const SlateEditorComponent = ({
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
onFocus={onFocus}
- onMouseDown={onMouseDown}
onBlur={onBlur}
customEditor={customEditor}
readOnly={editor.readOnly}
@@ -343,7 +231,6 @@ type SlateEditorInstanceProps = {
onKeyDown: (event: React.KeyboardEvent) => void;
onKeyUp: (event: React.KeyboardEvent) => void;
onFocus: (event: React.FocusEvent) => void;
- onMouseDown: (event: React.MouseEvent) => void;
onBlur: (event: React.FocusEvent) => void;
onPaste: (event: React.ClipboardEvent) => void;
customEditor?: (props: PluginCustomEditorRenderProps) => JSX.Element;
@@ -363,7 +250,6 @@ const SlateEditorInstance = memo(
onKeyDown,
onKeyUp,
onFocus,
- onMouseDown,
onBlur,
onPaste,
customEditor,
@@ -378,7 +264,6 @@ const SlateEditorInstance = memo(
(
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
onFocus={onFocus}
- onMouseDown={onMouseDown}
decorate={decorate}
// [TODO] - carefully check onBlur, e.x. transforms using functions, e.x. highlight update
onBlur={onBlur}
diff --git a/packages/core/editor/src/plugins/createYooptaPlugin.tsx b/packages/core/editor/src/plugins/createYooptaPlugin.tsx
index 593639d00..ed926514f 100644
--- a/packages/core/editor/src/plugins/createYooptaPlugin.tsx
+++ b/packages/core/editor/src/plugins/createYooptaPlugin.tsx
@@ -1,29 +1,40 @@
-import { Descendant } from 'slate';
-import { PluginElementRenderProps, Plugin, PluginOptions } from './types';
+import { SlateElement } from '../editor/types';
+import { PluginElementRenderProps, Plugin, PluginOptions, PluginEvents } from './types';
export type ExtendPluginRender = {
[x in TKeys]: (props: PluginElementRenderProps) => JSX.Element;
};
-export type ExtendPlugin = {
- renders?: Partial>;
+type ExtractProps = T extends SlateElement ? P : never;
+
+export type ExtendPlugin, TOptions> = {
+ renders?: {
+ [K in keyof TElementMap]?: (props: PluginElementRenderProps) => JSX.Element;
+ };
options?: Partial>;
- elementProps?: Partial TProps>>;
+ elementProps?: {
+ [K in keyof TElementMap]?: (props: ExtractProps) => ExtractProps;
+ };
+ events?: Partial;
};
-export class YooptaPlugin> {
- private readonly plugin: Plugin;
-
- constructor(plugin: Plugin) {
+export class YooptaPlugin, TOptions = Record> {
+ private readonly plugin: Plugin;
+ constructor(plugin: Plugin) {
this.plugin = plugin;
}
- get getPlugin() {
+ get getPlugin(): Plugin {
return this.plugin;
}
- extend(extendPlugin: ExtendPlugin): YooptaPlugin {
- const { renders, options, elementProps } = extendPlugin;
+ // [TODO] - add validation
+ // validatePlugin(): boolean {
+ // return true
+ // }
+
+ extend(extendPlugin: ExtendPlugin): YooptaPlugin {
+ const { renders, options, elementProps, events } = extendPlugin;
const extendedOptions = { ...this.plugin.options, ...options };
const elements = { ...this.plugin.elements };
@@ -51,13 +62,25 @@ export class YooptaPlugin {
+ const eventHandler = events[event];
- element.props = defaultPropsFn(updatedElementProps);
+ if (eventHandler) {
+ if (!this.plugin.events) this.plugin.events = {};
+ this.plugin.events[event] = eventHandler;
}
});
}
- return new YooptaPlugin({
+ return new YooptaPlugin({
...this.plugin,
elements: elements,
options: extendedOptions as PluginOptions,
diff --git a/packages/core/editor/src/plugins/hooks.ts b/packages/core/editor/src/plugins/hooks.ts
new file mode 100644
index 000000000..ce2e1ca00
--- /dev/null
+++ b/packages/core/editor/src/plugins/hooks.ts
@@ -0,0 +1,136 @@
+import { useMemo } from 'react';
+import { Element, Node, Operation, Range, Transforms } from 'slate';
+import { buildBlockData } from '../components/Editor/utils';
+import { SlateEditor, YooEditor, YooptaBlockData } from '../editor/types';
+import { EditorEventHandlers } from '../types/eventHandlers';
+import { getRootBlockElementType } from '../utils/blockElements';
+import { generateId } from '../utils/generateId';
+import { HOTKEYS } from '../utils/hotkeys';
+import { withInlines } from './extenstions/withInlines';
+import { PluginEventHandlerOptions, PluginEvents } from './types';
+
+export const useSlateEditor = (
+ id: string,
+ editor: YooEditor,
+ block: YooptaBlockData,
+ elements: any,
+ withExtensions: any,
+) => {
+ return useMemo(() => {
+ let slateEditor = editor.blockEditorsMap[id];
+
+ const { normalizeNode, insertText, apply } = slateEditor;
+ const elementTypes = Object.keys(elements);
+
+ elementTypes.forEach((elementType) => {
+ const nodeType = elements[elementType].props?.nodeType;
+
+ const isInline = nodeType === 'inline';
+ const isVoid = nodeType === 'void';
+ const isInlineVoid = nodeType === 'inlineVoid';
+
+ if (isInlineVoid) {
+ slateEditor.markableVoid = (element) => element.type === elementType;
+ }
+
+ if (isVoid || isInlineVoid) {
+ slateEditor.isVoid = (element) => element.type === elementType;
+ }
+
+ if (isInline || isInlineVoid) {
+ slateEditor.isInline = (element) => element.type === elementType;
+
+ // [TODO] - Move it to Link plugin extension
+ slateEditor = withInlines(editor, slateEditor);
+ }
+ });
+
+ slateEditor.insertText = (text) => {
+ if (Array.isArray(editor.selectedBlocks)) {
+ editor.setBlockSelected(null);
+ }
+
+ insertText(text);
+ };
+
+ slateEditor.apply = (op) => {
+ if (Operation.isSelectionOperation(op)) {
+ if (Array.isArray(editor.selectedBlocks) && slateEditor.selection && Range.isExpanded(slateEditor.selection)) {
+ editor.setBlockSelected(null);
+ }
+ }
+
+ apply(op);
+ };
+
+ // This normalization is needed to validate the elements structure
+ slateEditor.normalizeNode = (entry) => {
+ const [node, path] = entry;
+ const blockElements = editor.blocks[block.type].elements;
+
+ // Normalize only `simple` block elements.
+ // Simple elements are elements that have only one defined block element type.
+ // [TODO] - handle validation for complex block elements
+ if (Object.keys(blockElements).length > 1) {
+ return normalizeNode(entry);
+ }
+
+ if (Element.isElement(node)) {
+ const { type } = node;
+ const rootElementType = getRootBlockElementType(blockElements);
+
+ if (!elementTypes.includes(type)) {
+ Transforms.setNodes(slateEditor, { type: rootElementType, props: { ...node.props } }, { at: path });
+ return;
+ }
+
+ if (node.type === rootElementType) {
+ for (const [child, childPath] of Node.children(slateEditor, path)) {
+ if (Element.isElement(child) && !slateEditor.isInline(child)) {
+ Transforms.unwrapNodes(slateEditor, { at: childPath });
+ return;
+ }
+ }
+ }
+ }
+
+ normalizeNode(entry);
+ };
+
+ if (withExtensions) {
+ slateEditor = withExtensions(slateEditor, editor, id);
+ }
+
+ return slateEditor;
+ }, [elements, id, withExtensions]);
+};
+
+export const useEventHandlers = (
+ events: PluginEvents | undefined,
+ editor: YooEditor,
+ block: YooptaBlockData,
+ slate: SlateEditor,
+) => {
+ return useMemo(() => {
+ if (!events || editor.readOnly) return {};
+ const { onBeforeCreate, onDestroy, onCreate, ...eventHandlers } = events || {};
+
+ const eventHandlersOptions: PluginEventHandlerOptions = {
+ hotkeys: HOTKEYS,
+ currentBlock: block,
+ defaultBlock: buildBlockData({ id: generateId() }),
+ };
+ const eventHandlersMap = {};
+
+ Object.keys(eventHandlers).forEach((eventType) => {
+ eventHandlersMap[eventType] = function handler(event) {
+ if (eventHandlers[eventType]) {
+ const handler = eventHandlers[eventType](editor, slate, eventHandlersOptions);
+ handler(event);
+ }
+ };
+ });
+
+ return eventHandlersMap;
+ }, [events, editor, block]);
+};
diff --git a/packages/core/editor/src/plugins/types.ts b/packages/core/editor/src/plugins/types.ts
index 4b1f0b0da..44217e24f 100644
--- a/packages/core/editor/src/plugins/types.ts
+++ b/packages/core/editor/src/plugins/types.ts
@@ -1,26 +1,27 @@
import { HTMLAttributes, ReactElement, ReactNode } from 'react';
-import { Descendant, Editor, Path } from 'slate';
import { RenderElementProps as RenderSlateElementProps, RenderLeafProps } from 'slate-react';
import { SlateEditor, SlateElement, YooEditor, YooptaBlockBaseMeta, YooptaBlockData } from '../editor/types';
-import { YooptaMark } from '../marks';
import { EditorEventHandlers } from '../types/eventHandlers';
import { HOTKEYS_TYPE } from '../utils/hotkeys';
-export type RenderPluginProps = {
- id: string;
- elements: Plugin['elements'];
- marks?: YooptaMark[];
-};
-
-export type PluginOptions = {
- display?: {
- title?: string;
- description?: string;
- icon?: string | ReactNode | ReactElement;
- };
- shortcuts?: string[];
- HTMLAttributes?: HTMLAttributes;
-} & T;
+export enum NodeType {
+ Block = 'block',
+ Inline = 'inline',
+ Void = 'void',
+ InlineVoid = 'inlineVoid',
+}
+
+export type PluginOptions = Partial<
+ {
+ display?: {
+ title?: string;
+ description?: string;
+ icon?: string | ReactNode | ReactElement;
+ };
+ shortcuts?: string[];
+ HTMLAttributes?: HTMLAttributes;
+ } & T
+>;
export type PluginElementOptions = {
draggable?: boolean;
@@ -69,12 +70,24 @@ export type PluginEventHandlerOptions = {
currentBlock: YooptaBlockData;
};
-export type Plugin> = {
+export type ElementPropsMap = Record>;
+
+export type PluginEvents = {
+ onBeforeCreate?: (editor: YooEditor, blockId: string) => SlateElement;
+ onCreate?: (editor: YooEditor, blockId: string) => void;
+ onDestroy?: (editor: YooEditor, blockId: string) => void;
+} & EventHandlers;
+
+export type Plugin, TPluginOptions = Record> = {
type: string;
customEditor?: (props: PluginCustomEditorRenderProps) => JSX.Element;
- elements: PluginElementsMap;
- events?: EventHandlers;
- options?: PluginOptions;
+ extensions?: (slate: SlateEditor, editor: YooEditor, blockId: string) => SlateEditor;
+ commands?: Record any>;
+ elements: {
+ [K in keyof TElementMap]: PluginElement;
+ };
+ events?: PluginEvents;
+ options?: PluginOptions;
parsers?: Partial>;
};
diff --git a/packages/core/editor/src/styles.css b/packages/core/editor/src/styles.css
index 023ed4fe1..f72ed305c 100644
--- a/packages/core/editor/src/styles.css
+++ b/packages/core/editor/src/styles.css
@@ -3,6 +3,7 @@
.yoopta-editor * {
box-sizing: border-box;
border: 0 solid #e5e7eb;
+ scrollbar-color: #D3D1CB rgba(0, 0, 0, 0);
}
::-moz-selection {
diff --git a/packages/core/editor/src/utils/blockElements.ts b/packages/core/editor/src/utils/blockElements.ts
index abf58b10c..8b3a77c25 100644
--- a/packages/core/editor/src/utils/blockElements.ts
+++ b/packages/core/editor/src/utils/blockElements.ts
@@ -1,6 +1,6 @@
import { Editor, Element, NodeEntry, Path } from 'slate';
import { buildBlockElement } from '../components/Editor/utils';
-import { SlateEditor, SlateElement, YooEditor, YooptaBlock } from '../editor/types';
+import { SlateEditor, SlateElement, YooEditor, YooptaBlock, YooptaBlockData } from '../editor/types';
import { Plugin, PluginElement, PluginElementProps, PluginElementsMap } from '../plugins/types';
import { generateId } from './generateId';
@@ -66,7 +66,7 @@ export function buildSlateNodeElement(
type: string,
props: PluginElementProps = { nodeType: 'block' },
): SlateElement {
- return { id: generateId(), type, children: [{ text: '' }], props: props };
+ return { id: generateId(), type, children: [{ text: '' }], props };
}
function recursivelyCollectElementChildren(
@@ -105,8 +105,10 @@ export function buildBlockElementsStructure(
blockType: string,
elementsMapWithTextContent?: ElementsMapWithTextContent,
): SlateElement {
+ const plugin = editor.plugins[blockType];
const block: YooptaBlock = editor.blocks[blockType];
const blockElements = block.elements;
+
const rootBlockElementType = getRootBlockElementType(blockElements);
if (!rootBlockElementType) {
throw new Error(`Root element type not found for block type ${blockType}`);
@@ -129,7 +131,7 @@ export function buildBlockElementsStructure(
export function getPluginByInlineElement(
plugins: YooEditor['plugins'],
elementType: string,
-): Plugin | undefined {
+): Plugin, unknown> | undefined {
const plugin = Object.values(plugins).find((plugin) => {
return plugin.type === plugin.elements?.[elementType]?.rootPlugin;
});
diff --git a/packages/core/editor/src/utils/editorBuilders.ts b/packages/core/editor/src/utils/editorBuilders.ts
index 256b1650d..5f87a9987 100644
--- a/packages/core/editor/src/utils/editorBuilders.ts
+++ b/packages/core/editor/src/utils/editorBuilders.ts
@@ -1,5 +1,5 @@
-import { YooEditor, YooptaBlockData } from '../editor/types';
-import { Plugin, PluginElement, PluginElementsMap } from '../plugins/types';
+import { SlateElement, YooEditor, YooptaBlockData } from '../editor/types';
+import { Plugin, PluginElementsMap } from '../plugins/types';
import { YooptaMark } from '../marks';
import { findPluginBlockBySelectionPath } from '../utils/findPluginBlockBySelectionPath';
import { createBlock } from '../editor/blocks/createBlock';
@@ -31,7 +31,7 @@ export function buildMarks(editor, marks: YooptaMark[]) {
return formats;
}
-export function buildBlocks(editor, plugins: Plugin>[]) {
+export function buildBlocks(editor, plugins: Plugin>[]) {
const blocks: YooEditor['blocks'] = {};
plugins.forEach((plugin) => {
@@ -110,8 +110,8 @@ export function buildBlockShortcuts(editor: YooEditor) {
// const DEFAULT_PLUGIN_OPTIONS: PluginOptions = {};
export function buildPlugins(
- plugins: Plugin>[],
-): Record> {
+ plugins: Plugin>[],
+): Record>> {
const pluginsMap = {};
const inlineTopLevelPlugins: PluginElementsMap = {};
@@ -140,3 +140,22 @@ export function buildPlugins(
return pluginsMap;
}
+
+export function buildCommands(
+ editor: YooEditor,
+ plugins: Plugin>[],
+): Record any> {
+ const commands = {};
+
+ plugins.forEach((plugin) => {
+ if (plugin.commands) {
+ Object.keys(plugin.commands).forEach((command) => {
+ if (plugin.commands?.[command]) {
+ commands[command] = (...args) => plugin.commands?.[command](editor, ...args);
+ }
+ });
+ }
+ });
+
+ return commands;
+}
diff --git a/packages/core/editor/src/utils/hotkeys.ts b/packages/core/editor/src/utils/hotkeys.ts
index ef0de7dff..2aaa1c7b5 100644
--- a/packages/core/editor/src/utils/hotkeys.ts
+++ b/packages/core/editor/src/utils/hotkeys.ts
@@ -15,6 +15,7 @@ const HOTKEYS_MAP = {
backspace: 'backspace',
deleteForward: 'shift?+delete',
extendBackward: 'shift+left',
+ shiftDelete: 'shift+delete',
extendForward: 'shift+right',
shiftEnter: 'shift+enter',
enter: 'enter',
@@ -27,9 +28,15 @@ const HOTKEYS_MAP = {
tab: 'tab',
cmd: 'mod',
cmdEnter: 'mod+enter',
+ cmdShiftEnter: 'mod+shift+enter',
slashCommand: '/',
copy: 'mod+c',
cut: 'mod+x',
+ cmdShiftRight: 'mod+shift+right',
+ cmdShiftLeft: 'mod+shift+left',
+ cmdShiftDelete: 'mod+shift+backspace',
+ cmdShiftD: 'mod+shift+d',
+ cmdAltDelete: 'mod+alt+backspace',
};
const APPLE_HOTKEYS = {
@@ -111,6 +118,13 @@ export const HOTKEYS = {
isShiftArrowDown: create('shiftArrowDown'),
isCopy: create('copy'),
isCut: create('cut'),
+ isShiftDelete: create('shiftDelete'),
+ isCmdShiftEnter: create('cmdShiftEnter'),
+ isCmdShiftRight: create('cmdShiftRight'),
+ isCmdShiftLeft: create('cmdShiftLeft'),
+ isCmdShiftDelete: create('cmdShiftDelete'),
+ isCmdAltDelete: create('cmdAltDelete'),
+ isCmdShiftD: create('cmdShiftD'),
};
export type HOTKEYS_TYPE = {
diff --git a/packages/core/exports/package.json b/packages/core/exports/package.json
index 3144aa18a..d94afbeea 100644
--- a/packages/core/exports/package.json
+++ b/packages/core/exports/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/exports",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "Serialize/deserialize exports in different formats for Yoopta-Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -36,5 +36,5 @@
"dependencies": {
"marked": "^13.0.0"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/core/exports/src/html/deserialize.ts b/packages/core/exports/src/html/deserialize.ts
index 4126dee4e..58535645b 100644
--- a/packages/core/exports/src/html/deserialize.ts
+++ b/packages/core/exports/src/html/deserialize.ts
@@ -80,6 +80,7 @@ function buildBlock(editor: YooEditor, plugin: PluginsMapByNode, el: HTMLElement
if (plugin.parse) {
nodeElementOrBlocks = plugin.parse(el as HTMLElement, editor);
+ // @ts-ignore [FIXME] - fix types
const isInline = Element.isElement(nodeElementOrBlocks) && nodeElementOrBlocks.props?.nodeType === 'inline';
if (isInline) return nodeElementOrBlocks;
}
@@ -93,12 +94,13 @@ function buildBlock(editor: YooEditor, plugin: PluginsMapByNode, el: HTMLElement
let rootNode: SlateElement | YooptaBlockData[] = {
id: generateId(),
type: rootElementType,
- children: isVoid && !block.hasCustomEditor ? [{ text: '' }] : children.map(mapNodeChildren),
+ children: isVoid && !block.hasCustomEditor ? [{ text: '' }] : children.map(mapNodeChildren).flat(),
props: { nodeType: 'block', ...rootElement.props },
};
if (nodeElementOrBlocks) {
if (Element.isElement(nodeElementOrBlocks)) {
+ // @ts-ignore [FIXME] - fix types
rootNode = nodeElementOrBlocks;
} else if (Array.isArray(nodeElementOrBlocks)) {
const blocks = nodeElementOrBlocks;
@@ -106,8 +108,8 @@ function buildBlock(editor: YooEditor, plugin: PluginsMapByNode, el: HTMLElement
}
}
- if (rootNode.children.length === 0) {
- rootNode.children = [{ text: '' }];
+ if ((rootNode as SlateElement).children.length === 0) {
+ (rootNode as SlateElement).children = [{ text: '' }];
}
if (!nodeElementOrBlocks && plugin.parse) return;
@@ -118,7 +120,7 @@ function buildBlock(editor: YooEditor, plugin: PluginsMapByNode, el: HTMLElement
const blockData = buildBlockData({
id: generateId(),
type: plugin.type,
- value: [rootNode],
+ value: [rootNode as SlateElement],
meta: {
order: 0,
depth,
@@ -129,38 +131,45 @@ function buildBlock(editor: YooEditor, plugin: PluginsMapByNode, el: HTMLElement
return blockData;
}
-export function deserialize(editor: YooEditor, pluginsMap: PluginsMapByNodeNames, el: HTMLElement | ChildNode) {
+function deserialize(editor: YooEditor, pluginsMap: PluginsMapByNodeNames, el: HTMLElement | ChildNode) {
if (el.nodeType === 3) {
const text = el.textContent?.replace(/[\t\n\r\f\v]+/g, ' ');
- return text;
+ return { text };
} else if (el.nodeType !== 1) {
return null;
} else if (el.nodeName === 'BR') {
- return '\n';
+ return { text: '\n' };
}
- const parent = el;
-
+ const parent = el as HTMLElement;
let children = Array.from(parent.childNodes)
.map((node) => deserialize(editor, pluginsMap, node))
- .flat();
+ .flat()
+ .filter(Boolean);
- if (MARKS_NODE_NAME_MATCHERS_MAP[el.nodeName]) {
- const mark = MARKS_NODE_NAME_MATCHERS_MAP[el.nodeName];
+ if (MARKS_NODE_NAME_MATCHERS_MAP[parent.nodeName]) {
+ const mark = MARKS_NODE_NAME_MATCHERS_MAP[parent.nodeName];
const markType = mark.type;
- const text = el.textContent?.replace(/[\t\n\r\f\v]+/g, ' ');
- return { [markType]: true, text };
+
+ return children.map((child) => {
+ if (typeof child === 'string') {
+ return { [markType]: true, text: child };
+ } else if (child.text) {
+ return { ...child, [markType]: true };
+ }
+ return child;
+ });
}
- const plugin = pluginsMap[el.nodeName];
+ const plugin = pluginsMap[parent.nodeName];
if (plugin) {
if (Array.isArray(plugin)) {
- const blocks = plugin.map((p) => buildBlock(editor, p, el as HTMLElement, children)).filter(Boolean);
+ const blocks = plugin.map((p) => buildBlock(editor, p, parent, children)).filter(Boolean);
return blocks;
}
- return buildBlock(editor, plugin, el as HTMLElement, children);
+ return buildBlock(editor, plugin, parent, children);
}
return children;
@@ -176,7 +185,7 @@ function mapNodeChildren(child) {
}
if (Array.isArray(child)) {
- return { text: child[0] };
+ return child.map(mapNodeChildren).flat();
}
if (child?.text) {
@@ -185,13 +194,7 @@ function mapNodeChildren(child) {
if (isYooptaBlock(child)) {
const block = child as YooptaBlockData;
- let text = '';
-
- block.value[0].children.forEach((child) => {
- text += `${child.text}`;
- });
-
- return { text };
+ return (block.value[0] as SlateElement).children.map(mapNodeChildren).flat();
}
return { text: '' };
@@ -201,10 +204,12 @@ export function deserializeHTML(editor: YooEditor, htmlString: string): YooptaCo
const parsedHtml = new DOMParser().parseFromString(htmlString, 'text/html');
const value: YooptaContentValue = {};
+ console.log('parsedHtml.body', parsedHtml.body);
+
const PLUGINS_NODE_NAME_MATCHERS_MAP = getMappedPluginByNodeNames(editor);
- const blocks = deserialize(editor, PLUGINS_NODE_NAME_MATCHERS_MAP, parsedHtml.body).filter(
- isYooptaBlock,
- ) as YooptaBlockData[];
+ const blocks = deserialize(editor, PLUGINS_NODE_NAME_MATCHERS_MAP, parsedHtml.body)
+ .flat()
+ .filter(isYooptaBlock) as YooptaBlockData[];
blocks.forEach((block, i) => {
const blockData = block;
diff --git a/packages/core/starter-kit/package.json b/packages/core/starter-kit/package.json
index f621e48d3..e1865315c 100644
--- a/packages/core/starter-kit/package.json
+++ b/packages/core/starter-kit/package.json
@@ -1,11 +1,11 @@
{
"name": "@yoopta/starter-kit",
- "version": "4.7.0",
+ "version": "4.7.1-alpha.4",
"description": "StarterKit for Yoopta Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
"license": "MIT",
- "private": false,
+ "private": true,
"main": "dist/index.js",
"type": "module",
"module": "dist/index.js",
@@ -19,6 +19,7 @@
"@yoopta/blockquote": "latest",
"@yoopta/callout": "latest",
"@yoopta/code": "latest",
+ "@yoopta/divider": "latest",
"@yoopta/editor": "latest",
"@yoopta/embed": "latest",
"@yoopta/exports": "latest",
@@ -30,6 +31,7 @@
"@yoopta/lists": "latest",
"@yoopta/marks": "latest",
"@yoopta/paragraph": "latest",
+ "@yoopta/table": "latest",
"@yoopta/toolbar": "latest",
"@yoopta/video": "latest",
"slate": "^0.102.0",
@@ -55,5 +57,5 @@
"bugs": {
"url": "https://github.com/Darginec05/Editor-Yoopta/issues"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/core/starter-kit/src/utilts/plugins.ts b/packages/core/starter-kit/src/utilts/plugins.ts
index 1f7999caf..54bfdeca7 100644
--- a/packages/core/starter-kit/src/utilts/plugins.ts
+++ b/packages/core/starter-kit/src/utilts/plugins.ts
@@ -9,7 +9,9 @@ import File from '@yoopta/file';
import Accordion from '@yoopta/accordion';
import { NumberedList, BulletedList, TodoList } from '@yoopta/lists';
import { HeadingOne, HeadingThree, HeadingTwo } from '@yoopta/headings';
+import Table from '@yoopta/table';
import Code from '@yoopta/code';
+import Divider from '@yoopta/divider';
import { type MediaUploadsFn } from '../components/StarterKit/StarterKit';
type PluginParams = {
@@ -31,6 +33,8 @@ export const getPlugins = ({ media }: PluginParams) => {
Code,
Link,
Embed,
+ Table,
+ Divider,
Image.extend({
options: {
async onUpload(file) {
diff --git a/packages/development/package.json b/packages/development/package.json
index c8e12cba6..92f3613e1 100644
--- a/packages/development/package.json
+++ b/packages/development/package.json
@@ -27,11 +27,13 @@
"@yoopta/marks": "*",
"@yoopta/mention": "*",
"@yoopta/paragraph": "*",
+ "@yoopta/starter-kit": "*",
"@yoopta/table": "*",
"@yoopta/toolbar": "*",
"@yoopta/video": "*",
- "@yoopta/starter-kit": "*",
+ "@yoopta/divider": "*",
"classnames": "^2.5.1",
+ "js-beautify": "^1.15.1",
"katex": "^0.16.10",
"lucide-react": "^0.378.0",
"next": "14.1.0",
diff --git a/packages/development/src/components/Exports/html/HtmlPreview/HtmlPreview.module.scss b/packages/development/src/components/Exports/html/HtmlPreview/HtmlPreview.module.scss
new file mode 100644
index 000000000..39e72f579
--- /dev/null
+++ b/packages/development/src/components/Exports/html/HtmlPreview/HtmlPreview.module.scss
@@ -0,0 +1,125 @@
+.root {
+ width: 100%;
+ padding: 0;
+ margin: 0;
+ border: 0;
+}
+
+.label {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ word-wrap: normal;
+ border: 0;
+}
+
+.box {
+ --bgColor-default: #0d1117;
+ --bgColor-muted: #161b22;
+ --bgColor-inset: #010409;
+ --bgColor-emphasis: #6e7681;
+ --bgColor-inverse: #ffffff;
+
+ background-color: var(--bgColor-default);
+ border-color: var(--borderColor-default);
+ border-radius: 0.375rem;
+ border-style: solid;
+ border-width: var(--borderWidth-thin);
+}
+
+.tabNav {
+ display: flex;
+ background-color: var(--bgColor-muted, var(--color-canvas-subtle));
+ border-top-left-radius: 6px;
+ border-top-right-radius: 6px;
+ margin-bottom: 0;
+ border-bottom: var(--borderWidth-thin) solid var(--borderColor-default);
+ margin-bottom: var(--stack-gap-normal);
+ margin-top: 0;
+ width: 100%;
+}
+
+.tabs {
+ margin-top: -1px;
+ margin-left: -1px;
+ flex-shrink: 0;
+ display: flex;
+ margin-bottom: 0;
+ overflow: auto;
+}
+
+.tab {
+ -webkit-appearance: button;
+ background-color: initial;
+ border: 1px solid #0000;
+ border-bottom: 0;
+ color: #8d96a0;
+ display: inline-block;
+ flex-shrink: 0;
+ font-size: var(--text-body-size-medium);
+ line-height: 23px;
+ padding: 8px 16px;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ transition: color .2s cubic-bezier(0.3, 0, 0.5, 1);
+ cursor: pointer;
+
+ &[aria-selected=true] {
+ background-color: #0d1117;
+ border-color: #30363d;
+ border-radius: 0.375rem 0.375rem 0 0;
+ color: #e6edf3;
+ }
+}
+
+.toolbar {
+ display: flex;
+ min-width: 0;
+ margin-right: 4px;
+ flex-shrink: 1;
+ flex-grow: 1;
+}
+
+.commentBox {
+ border-color: transparent;
+ display: block;
+ width: auto;
+ height: 100%;
+ overflow: hidden;
+ border-right: 1px solid #30363d;
+
+ &>div {
+ height: 100%;
+ }
+}
+
+.textarea {
+ height: 300px;
+ width: 100%;
+ background-color: transparent;
+ display: block;
+ width: 100%;
+ min-height: 102px;
+ padding: 0.5rem;
+ line-height: 1.5;
+ resize: vertical;
+ background: none;
+ color: #e6edf3;
+ font-size: 14px;
+}
+
+.previewBox {
+ color: #fff;
+ min-height: 300px;
+ padding: 28px 16px;
+ height: 100%;
+}
+
+.preview {
+ word-wrap: break-word;
+ padding: 0;
+ width: 100%;
+}
\ No newline at end of file
diff --git a/packages/development/src/components/Exports/html/HtmlPreview/HtmlPreview.tsx b/packages/development/src/components/Exports/html/HtmlPreview/HtmlPreview.tsx
new file mode 100644
index 000000000..a4365a3c3
--- /dev/null
+++ b/packages/development/src/components/Exports/html/HtmlPreview/HtmlPreview.tsx
@@ -0,0 +1,166 @@
+import YooptaEditor, { createYooptaEditor, YooEditor, YooptaContentValue } from '@yoopta/editor';
+import parsers from '@yoopta/exports';
+import s from './HtmlPreview.module.scss';
+
+import CodeMirror, { BasicSetupOptions } from '@uiw/react-codemirror';
+
+import { useEffect, useMemo, useState } from 'react';
+
+import { html as codemirrorHTML } from '@codemirror/lang-html';
+import { vscodeDark } from '@uiw/codemirror-theme-vscode';
+import NextLink from 'next/link';
+import jsBeatify from 'js-beautify';
+import { YOOPTA_PLUGINS } from '../../../../utils/yoopta/plugins';
+import { MARKS } from '../../../../utils/yoopta/marks';
+
+const LANGUAGES_MAP = {
+ html: {
+ type: 'html',
+ name: 'HTML',
+ extension: codemirrorHTML(),
+ },
+};
+
+const codeMirrorSetup: BasicSetupOptions = {
+ lineNumbers: false,
+ autocompletion: false,
+ foldGutter: false,
+ highlightActiveLineGutter: false,
+ highlightActiveLine: false,
+ tabSize: 2,
+};
+
+type ViewProps = {
+ editor: YooEditor;
+ html: string;
+ onChange: (code: string) => void;
+ focusedEditor: FocusedView;
+ onChangeFocusedEditor: (type: FocusedView) => void;
+};
+
+const WriteHTML = ({ editor, html, onChange, onChangeFocusedEditor }: ViewProps) => {
+ return (
+
+
Type some html here and see the result on the right side (Deserializing 🎊)
+
+ {html.trim().length > 0 && (
+ {
+ onChange(
+ jsBeatify.html_beautify(html, {
+ indent_with_tabs: false,
+ indent_size: 2,
+ }),
+ );
+ }}
+ className="bg-blue-500 text-white p-2 rounded-md absolute top-0 right-0 z-10"
+ >
+ Beatify
+
+ )}
+ onChangeFocusedEditor('html')}
+ />
+
+
+ );
+};
+
+const ResultHTML = ({ editor, html, onChange, focusedEditor, onChangeFocusedEditor }: ViewProps) => {
+ useEffect(() => {
+ if (focusedEditor === 'yoopta') return;
+
+ if (html.length === 0) return;
+ const deserialized = parsers.html.deserialize(editor, html);
+ editor.setEditorValue(deserialized);
+ }, [html, focusedEditor]);
+
+ useEffect(() => {
+ const handleChange = (value: YooptaContentValue) => {
+ const string = parsers.html.serialize(editor, value);
+ onChange(string);
+ };
+
+ if (focusedEditor === 'yoopta') {
+ editor.on('change', handleChange);
+ return () => editor.off('change', handleChange);
+ }
+ }, [editor, focusedEditor]);
+
+ return (
+
+
Type some content here and see the html on the left side (Serializing 🎉)
+
+
onChangeFocusedEditor('yoopta')}>
+
+
+
+
+ );
+};
+
+type FocusedView = 'html' | 'yoopta';
+
+const HtmlPreview = () => {
+ const editor: YooEditor = useMemo(() => createYooptaEditor(), []);
+ const [html, setHTML] = useState('');
+ const [focusedEditor, setFocusedEditor] = useState('html');
+
+ const onChange = (code: string) => setHTML(code);
+
+ return (
+ <>
+
+
+ Back to examples
+
+
+ This example shows how html deserialize/serialize methods from @yoopta/exports work
+
+
+
+ setFocusedEditor(type)}
+ editor={editor}
+ />
+ setFocusedEditor(type)}
+ editor={editor}
+ />
+
+
+
+ >
+ );
+};
+
+export { HtmlPreview };
diff --git a/packages/development/src/components/Exports/markdown/MarkdownPreview/MarkdownPreview.module.scss b/packages/development/src/components/Exports/markdown/MarkdownPreview/MarkdownPreview.module.scss
new file mode 100644
index 000000000..39e72f579
--- /dev/null
+++ b/packages/development/src/components/Exports/markdown/MarkdownPreview/MarkdownPreview.module.scss
@@ -0,0 +1,125 @@
+.root {
+ width: 100%;
+ padding: 0;
+ margin: 0;
+ border: 0;
+}
+
+.label {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ word-wrap: normal;
+ border: 0;
+}
+
+.box {
+ --bgColor-default: #0d1117;
+ --bgColor-muted: #161b22;
+ --bgColor-inset: #010409;
+ --bgColor-emphasis: #6e7681;
+ --bgColor-inverse: #ffffff;
+
+ background-color: var(--bgColor-default);
+ border-color: var(--borderColor-default);
+ border-radius: 0.375rem;
+ border-style: solid;
+ border-width: var(--borderWidth-thin);
+}
+
+.tabNav {
+ display: flex;
+ background-color: var(--bgColor-muted, var(--color-canvas-subtle));
+ border-top-left-radius: 6px;
+ border-top-right-radius: 6px;
+ margin-bottom: 0;
+ border-bottom: var(--borderWidth-thin) solid var(--borderColor-default);
+ margin-bottom: var(--stack-gap-normal);
+ margin-top: 0;
+ width: 100%;
+}
+
+.tabs {
+ margin-top: -1px;
+ margin-left: -1px;
+ flex-shrink: 0;
+ display: flex;
+ margin-bottom: 0;
+ overflow: auto;
+}
+
+.tab {
+ -webkit-appearance: button;
+ background-color: initial;
+ border: 1px solid #0000;
+ border-bottom: 0;
+ color: #8d96a0;
+ display: inline-block;
+ flex-shrink: 0;
+ font-size: var(--text-body-size-medium);
+ line-height: 23px;
+ padding: 8px 16px;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ transition: color .2s cubic-bezier(0.3, 0, 0.5, 1);
+ cursor: pointer;
+
+ &[aria-selected=true] {
+ background-color: #0d1117;
+ border-color: #30363d;
+ border-radius: 0.375rem 0.375rem 0 0;
+ color: #e6edf3;
+ }
+}
+
+.toolbar {
+ display: flex;
+ min-width: 0;
+ margin-right: 4px;
+ flex-shrink: 1;
+ flex-grow: 1;
+}
+
+.commentBox {
+ border-color: transparent;
+ display: block;
+ width: auto;
+ height: 100%;
+ overflow: hidden;
+ border-right: 1px solid #30363d;
+
+ &>div {
+ height: 100%;
+ }
+}
+
+.textarea {
+ height: 300px;
+ width: 100%;
+ background-color: transparent;
+ display: block;
+ width: 100%;
+ min-height: 102px;
+ padding: 0.5rem;
+ line-height: 1.5;
+ resize: vertical;
+ background: none;
+ color: #e6edf3;
+ font-size: 14px;
+}
+
+.previewBox {
+ color: #fff;
+ min-height: 300px;
+ padding: 28px 16px;
+ height: 100%;
+}
+
+.preview {
+ word-wrap: break-word;
+ padding: 0;
+ width: 100%;
+}
\ No newline at end of file
diff --git a/packages/development/src/components/Exports/markdown/MarkdownPreview/MarkdownPreview.tsx b/packages/development/src/components/Exports/markdown/MarkdownPreview/MarkdownPreview.tsx
new file mode 100644
index 000000000..960b9c70c
--- /dev/null
+++ b/packages/development/src/components/Exports/markdown/MarkdownPreview/MarkdownPreview.tsx
@@ -0,0 +1,163 @@
+import YooptaEditor, { createYooptaEditor, YooEditor, YooptaContentValue } from '@yoopta/editor';
+import parsers from '@yoopta/exports';
+import s from './MarkdownPreview.module.scss';
+
+import CodeMirror, { BasicSetupOptions } from '@uiw/react-codemirror';
+
+import { useEffect, useMemo, useState } from 'react';
+
+import { markdown as codemirrorMD } from '@codemirror/lang-markdown';
+import { xml } from '@codemirror/lang-xml';
+import { html as codemirrorHTML } from '@codemirror/lang-html';
+import { vscodeDark } from '@uiw/codemirror-theme-vscode';
+
+import NextLink from 'next/link';
+import { YOOPTA_PLUGINS } from '../../../../utils/yoopta/plugins';
+import { MARKS } from '../../../../utils/yoopta/marks';
+
+const LANGUAGES_MAP = {
+ markdown: {
+ type: 'markdown',
+ name: 'Markdown',
+ extension: codemirrorMD(),
+ },
+ xml: {
+ type: 'xml',
+ name: 'XML',
+ extension: xml(),
+ },
+ html: {
+ type: 'html',
+ name: 'HTML',
+ extension: codemirrorHTML(),
+ },
+};
+
+const codeMirrorSetup: BasicSetupOptions = {
+ lineNumbers: false,
+ autocompletion: false,
+ foldGutter: false,
+ highlightActiveLineGutter: false,
+ highlightActiveLine: false,
+ tabSize: 2,
+};
+
+type ViewProps = {
+ editor: YooEditor;
+ markdown: string;
+ onChange: (code: string) => void;
+ focusedEditor: FocusedView;
+ onChangeFocusedEditor: (type: FocusedView) => void;
+};
+
+const WriteMarkdown = ({ editor, markdown, onChange, onChangeFocusedEditor }: ViewProps) => {
+ return (
+
+
Type some markdown here and see the result on the right side (Deserializing 🎊)
+
+ onChangeFocusedEditor('markdown')}
+ />
+
+
+ );
+};
+
+const ResultMarkdown = ({ editor, markdown, onChange, focusedEditor, onChangeFocusedEditor }: ViewProps) => {
+ useEffect(() => {
+ if (focusedEditor === 'yoopta') return;
+
+ if (markdown.length === 0) return;
+ const deserialized = parsers.markdown.deserialize(editor, markdown);
+ editor.setEditorValue(deserialized);
+ }, [markdown, focusedEditor]);
+
+ useEffect(() => {
+ const handleChange = (value: YooptaContentValue) => {
+ const string = parsers.markdown.serialize(editor, value);
+ onChange(string);
+ };
+
+ if (focusedEditor === 'yoopta') {
+ editor.on('change', handleChange);
+ return () => editor.off('change', handleChange);
+ }
+ }, [editor, focusedEditor]);
+
+ return (
+
+
Type some content here and see the markdown on the left side (Serializing 🎉)
+
+
onChangeFocusedEditor('yoopta')}>
+
+
+
+
+ );
+};
+
+type FocusedView = 'markdown' | 'yoopta';
+
+const MarkdownPreview = () => {
+ const editor: YooEditor = useMemo(() => createYooptaEditor(), []);
+ const [markdown, setMarkdown] = useState('');
+ const [focusedEditor, setFocusedEditor] = useState('markdown');
+
+ const onChange = (code: string) => setMarkdown(code);
+
+ return (
+ <>
+
+ Back to examples
+
+
+
+ This example shows how markdown deserialize/serialize methods from @yoopta/exports work
+
+
+
+ setFocusedEditor(type)}
+ editor={editor}
+ />
+ setFocusedEditor(type)}
+ editor={editor}
+ />
+
+
+
+ >
+ );
+};
+
+export { MarkdownPreview };
diff --git a/packages/development/src/pages/dev/index.tsx b/packages/development/src/pages/dev/index.tsx
index 55b93a40b..1164a03e1 100644
--- a/packages/development/src/pages/dev/index.tsx
+++ b/packages/development/src/pages/dev/index.tsx
@@ -1,38 +1,210 @@
-import YooptaEditor, { createYooptaEditor, YooEditor, YooptaBlockData, YooptaContentValue } from '@yoopta/editor';
+import YooptaEditor, {
+ Blocks,
+ createYooptaEditor,
+ YooEditor,
+ YooptaBlockData,
+ YooptaContentValue,
+} from '@yoopta/editor';
import { useEffect, useMemo, useRef, useState } from 'react';
+
import { MARKS } from '../../utils/yoopta/marks';
import { YOOPTA_PLUGINS } from '../../utils/yoopta/plugins';
import { TOOLS } from '../../utils/yoopta/tools';
+import { LinkCommands } from '@yoopta/link';
export type YooptaChildrenValue = Record;
+const EDITOR_STYLE = {
+ width: 750,
+};
+
const BasicExample = () => {
const editor: YooEditor = useMemo(() => createYooptaEditor(), []);
const selectionRef = useRef(null);
const [readOnly, setReadOnly] = useState(false);
- const [value, setValue] = useState();
+ const [value, setValue] = useState({
+ '208e71cc-aa15-4fe8-93e1-446fc9b1053f': {
+ id: '208e71cc-aa15-4fe8-93e1-446fc9b1053f',
+ value: [
+ {
+ id: 'c2971883-6dd0-4cb7-bb7e-f9a3d3454cbe',
+ type: 'table',
+ children: [
+ {
+ id: '705cf815-5d00-4013-b92b-8934fb93454f',
+ type: 'table-row',
+ children: [
+ {
+ id: 'c9021d7e-4336-4c68-a876-d0b76b83aee2',
+ type: 'table-data-cell',
+ children: [
+ {
+ text: 'First column',
+ },
+ ],
+ props: {
+ width: 200,
+ asHeader: false,
+ },
+ },
+ {
+ id: '76893065-3256-47a2-95c2-9731922b69c8',
+ type: 'table-data-cell',
+ children: [
+ {
+ text: '',
+ },
+ ],
+ props: {
+ width: 200,
+ asHeader: false,
+ },
+ },
+ {
+ id: 'e57acf66-0327-4247-b4ff-bfc38bb1ccad',
+ type: 'table-data-cell',
+ children: [
+ {
+ text: '',
+ },
+ ],
+ props: {
+ width: 200,
+ asHeader: false,
+ },
+ },
+ ],
+ },
+ {
+ id: '31ee10c5-7d17-4113-ae28-e81bbdbe0d70',
+ type: 'table-row',
+ children: [
+ {
+ id: '65d46e0d-35fd-4668-8784-83d7b6a8c0f3',
+ type: 'table-data-cell',
+ children: [
+ {
+ text: '',
+ },
+ ],
+ props: {
+ width: 200,
+ asHeader: false,
+ },
+ },
+ {
+ id: 'f75c1bd0-9302-4404-8f25-4854bdc554eb',
+ type: 'table-data-cell',
+ children: [
+ {
+ text: '',
+ },
+ ],
+ props: {
+ width: 200,
+ asHeader: false,
+ },
+ },
+ {
+ id: '2a90d671-024e-4b69-9b8c-647a4a52093e',
+ type: 'table-data-cell',
+ children: [
+ {
+ text: '',
+ },
+ ],
+ props: {
+ width: 200,
+ asHeader: false,
+ },
+ },
+ ],
+ },
+ {
+ id: 'f3916ae1-d479-48d8-bcb2-055d7c152093',
+ type: 'table-row',
+ children: [
+ {
+ id: '616fe6fa-0d21-4ab2-82ee-c00d2a5cfb8d',
+ type: 'table-data-cell',
+ children: [
+ {
+ text: '',
+ },
+ ],
+ props: {
+ width: 200,
+ asHeader: false,
+ },
+ },
+ {
+ id: 'bf01e43c-fa1e-44bc-b815-90b6a29a6624',
+ type: 'table-data-cell',
+ children: [
+ {
+ text: '',
+ },
+ ],
+ props: {
+ width: 200,
+ asHeader: false,
+ },
+ },
+ {
+ id: 'd6d969fb-6c43-4b9a-9770-7984d89a9824',
+ type: 'table-data-cell',
+ children: [
+ {
+ text: '',
+ },
+ ],
+ props: {
+ width: 200,
+ asHeader: false,
+ },
+ },
+ ],
+ },
+ ],
+ props: {
+ headerColumn: false,
+ headerRow: false,
+ },
+ },
+ ],
+ type: 'Table',
+ meta: {
+ order: 0,
+ depth: 0,
+ },
+ },
+ });
useEffect(() => {
- editor.on('change', (data) => {
- setValue(data);
+ editor.on('change', (value: YooptaChildrenValue) => {
+ setValue(value);
});
- }, []);
+ }, [editor]);
+
+ console.log(value);
return (
-
-
-
+ <>
+
+
+
+ >
);
};
diff --git a/packages/development/src/pages/exports/html.tsx b/packages/development/src/pages/exports/html.tsx
new file mode 100644
index 000000000..4904f5292
--- /dev/null
+++ b/packages/development/src/pages/exports/html.tsx
@@ -0,0 +1,5 @@
+import { HtmlPreview } from '../../components/Exports/html/HtmlPreview/HtmlPreview';
+
+export default function HtmlExports() {
+ return ;
+}
diff --git a/packages/development/src/pages/exports/markdown.tsx b/packages/development/src/pages/exports/markdown.tsx
new file mode 100644
index 000000000..9a0ed9d30
--- /dev/null
+++ b/packages/development/src/pages/exports/markdown.tsx
@@ -0,0 +1,5 @@
+import { MarkdownPreview } from '../../components/Exports/markdown/MarkdownPreview/MarkdownPreview';
+
+export default function MarkdownExports() {
+ return ;
+}
diff --git a/packages/development/src/utils/yoopta/plugins.tsx b/packages/development/src/utils/yoopta/plugins.tsx
index 44d9c9abe..cafdaa430 100644
--- a/packages/development/src/utils/yoopta/plugins.tsx
+++ b/packages/development/src/utils/yoopta/plugins.tsx
@@ -8,15 +8,30 @@ import Link from '@yoopta/link';
import Video, { VideoElementProps } from '@yoopta/video';
import File from '@yoopta/file';
import Embed from '@yoopta/embed';
-import AccordionPlugin from '@yoopta/accordion';
+import Accordion, { AccordionCommands } from '@yoopta/accordion';
import Code from '@yoopta/code';
+import Table from '@yoopta/table';
+import Divider from '@yoopta/divider';
-import NextLink from 'next/link';
import { uploadToCloudinary } from '../cloudinary';
-import { PluginElementRenderProps } from '@yoopta/editor';
+import { Elements } from '@yoopta/editor';
export const YOOPTA_PLUGINS = [
- AccordionPlugin.extend({
+ Table,
+ Divider.extend({
+ elementProps: {
+ divider: (props) => ({
+ ...props,
+ color: '#8383e0',
+ }),
+ },
+ }),
+ Accordion.extend({
+ events: {
+ onBeforeCreate: (editor) => {
+ return AccordionCommands.buildAccordionElements(editor, { items: 2, props: { isExpanded: true } });
+ },
+ },
elementProps: {
'accordion-list-item': (props) => {
return {
@@ -27,6 +42,11 @@ export const YOOPTA_PLUGINS = [
},
}),
File.extend({
+ events: {
+ onBeforeCreate: (editor) => {
+ return editor.commands.buildFileElements({ text: 'Hello world' });
+ },
+ },
options: {
onUpload: async (file: File) => {
const data = await uploadToCloudinary(file, 'auto');
@@ -48,6 +68,12 @@ export const YOOPTA_PLUGINS = [
},
}),
Image.extend({
+ events: {
+ onDestroy: (editor, id) => {
+ const imageElement = Elements.getElement(editor, id, { type: 'image' });
+ console.log('Image imageElement', imageElement);
+ },
+ },
elementProps: {
image: (props: ImageElementProps) => ({
...props,
@@ -79,6 +105,14 @@ export const YOOPTA_PLUGINS = [
},
}),
Headings.HeadingOne.extend({
+ events: {
+ onCreate: (editor, id) => {
+ console.log('HeadingOne onCreate', editor, id);
+ },
+ onDestroy: (editor, id) => {
+ console.log('HeadingOne onDestroy', editor, id);
+ },
+ },
options: {
HTMLAttributes: {
className: 'heading-one-element-extended',
diff --git a/packages/marks/package.json b/packages/marks/package.json
index 9b251dc46..f55422290 100644
--- a/packages/marks/package.json
+++ b/packages/marks/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/marks",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "Marks for Yoopta Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -34,5 +34,5 @@
"bugs": {
"url": "https://github.com/Darginec05/Editor-Yoopta/issues"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/plugins/accordion/package.json b/packages/plugins/accordion/package.json
index 9c47f7890..8e8507c00 100644
--- a/packages/plugins/accordion/package.json
+++ b/packages/plugins/accordion/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/accordion",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "Accordion plugin for Yoopta Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -37,5 +37,5 @@
"bugs": {
"url": "https://github.com/Darginec05/Editor-Yoopta/issues"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/plugins/accordion/src/commands/index.ts b/packages/plugins/accordion/src/commands/index.ts
new file mode 100644
index 000000000..195811182
--- /dev/null
+++ b/packages/plugins/accordion/src/commands/index.ts
@@ -0,0 +1,66 @@
+import { Blocks, buildBlockData, Elements, generateId, YooEditor, YooptaBlockPath } from '@yoopta/editor';
+import {
+ AccordionListElement,
+ AccordionItemElement,
+ AccordionListItemProps,
+ AccordionListItemHeadingElement,
+ AccordionListItemContentElement,
+} from '../types';
+
+type AccordionElementOptions = {
+ items?: number;
+ props: AccordionListItemProps;
+};
+
+type InsertAccordionOptions = AccordionElementOptions & {
+ at?: YooptaBlockPath;
+ focus?: boolean;
+};
+
+export type AccordionCommands = {
+ buildAccordionElements: (editor: YooEditor, options?: Partial) => AccordionListElement;
+ insertAccordion: (editor: YooEditor, options?: Partial) => void;
+ deleteAccordion: (editor: YooEditor, blockId: string) => void;
+};
+
+export const AccordionCommands: AccordionCommands = {
+ buildAccordionElements: (editor: YooEditor, options = {}) => {
+ // take props from block.elements
+ const { props = { isExpanded: false }, items = 1 } = options;
+
+ const accordionList: AccordionListElement = { id: generateId(), type: 'accordion-list', children: [] };
+
+ for (let i = 0; i < items; i++) {
+ const headingListItem: AccordionListItemHeadingElement = {
+ id: generateId(),
+ type: 'accordion-list-item-heading',
+ children: [{ text: `` }],
+ };
+
+ const contentListItem: AccordionListItemContentElement = {
+ id: generateId(),
+ type: 'accordion-list-item-content',
+ children: [{ text: `` }],
+ };
+
+ const accordionListItem: AccordionItemElement = {
+ id: generateId(),
+ type: 'accordion-list-item',
+ children: [headingListItem, contentListItem],
+ props,
+ };
+
+ accordionList.children.push(accordionListItem);
+ }
+
+ return accordionList;
+ },
+ insertAccordion: (editor: YooEditor, options = {}) => {
+ const { at, focus, props, items } = options;
+ const accordionList = AccordionCommands.buildAccordionElements(editor, { props, items });
+ Blocks.insertBlock(editor, buildBlockData({ value: [accordionList], type: 'Accordion' }), { focus, at });
+ },
+ deleteAccordion: (editor: YooEditor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+};
diff --git a/packages/plugins/accordion/src/index.ts b/packages/plugins/accordion/src/index.ts
index 5d4db985d..d836cb8fc 100644
--- a/packages/plugins/accordion/src/index.ts
+++ b/packages/plugins/accordion/src/index.ts
@@ -18,5 +18,7 @@ declare module 'slate' {
}
}
+export { AccordionCommands } from './commands';
+
export default Accordion;
export { AccordionItemElement, AccordionListItemProps };
diff --git a/packages/plugins/accordion/src/plugin/index.tsx b/packages/plugins/accordion/src/plugin/index.tsx
index 76f5d8d36..bff3dde99 100644
--- a/packages/plugins/accordion/src/plugin/index.tsx
+++ b/packages/plugins/accordion/src/plugin/index.tsx
@@ -1,19 +1,12 @@
-import {
- Blocks,
- Elements,
- YooptaPlugin,
- SlateEditor,
- generateId,
- buildBlockElementsStructure,
- SlateElement,
-} from '@yoopta/editor';
-import { AccordionElementKeys, AccordionListItemProps } from '../types';
+import { Blocks, Elements, YooptaPlugin, buildBlockElementsStructure } from '@yoopta/editor';
+import { AccordionElementMap } from '../types';
import { AccordionList } from '../renders/AccordionList';
import { AccordionListItem } from '../renders/AccordionListItem';
import { AccordionItemHeading } from '../renders/AccordionItemHeading';
import { AccordionItemContent } from '../renders/AccordionItemContent';
import { Transforms } from 'slate';
import { ListCollapse } from 'lucide-react';
+import { AccordionCommands } from '../commands';
const ACCORDION_ELEMENTS = {
AccordionList: 'accordion-list',
@@ -22,7 +15,7 @@ const ACCORDION_ELEMENTS = {
AccordionListItemContent: 'accordion-list-item-content',
};
-const Accordion = new YooptaPlugin({
+const Accordion = new YooptaPlugin({
type: 'Accordion',
elements: {
'accordion-list': {
@@ -42,6 +35,7 @@ const Accordion = new YooptaPlugin
render: AccordionItemContent,
},
},
+ commands: AccordionCommands,
events: {
onKeyDown(editor, slate, { hotkeys, currentBlock }) {
return (event) => {
@@ -116,7 +110,7 @@ const Accordion = new YooptaPlugin
{
type: ACCORDION_ELEMENTS.AccordionListItem,
props: {
- isExpanded: !listItem?.props.isExpanded,
+ isExpanded: !listItem?.props?.isExpanded,
},
},
{ path: listItemPath },
diff --git a/packages/plugins/accordion/src/renders/AccordionItemContent.tsx b/packages/plugins/accordion/src/renders/AccordionItemContent.tsx
index 811825ab7..098cb9704 100644
--- a/packages/plugins/accordion/src/renders/AccordionItemContent.tsx
+++ b/packages/plugins/accordion/src/renders/AccordionItemContent.tsx
@@ -1,5 +1,4 @@
import { Elements, PluginElementRenderProps, useYooptaEditor } from '@yoopta/editor';
-import { Path } from 'slate';
export const AccordionItemContent = ({ extendRender, ...props }: PluginElementRenderProps) => {
const { attributes, children, blockId, element } = props;
diff --git a/packages/plugins/accordion/src/types.ts b/packages/plugins/accordion/src/types.ts
index 8c291203f..531d3ac71 100644
--- a/packages/plugins/accordion/src/types.ts
+++ b/packages/plugins/accordion/src/types.ts
@@ -14,3 +14,10 @@ export type AccordionItemElement = SlateElement<'accordion-list-item', Accordion
export type AccordionListElement = SlateElement<'accordion-list'>;
export type AccordionListItemHeadingElement = SlateElement<'accordion-list-item-heading'>;
export type AccordionListItemContentElement = SlateElement<'accordion-list-item-content'>;
+
+export type AccordionElementMap = {
+ 'accordion-list': AccordionListElement;
+ 'accordion-list-item': AccordionItemElement;
+ 'accordion-list-item-heading': AccordionListItemHeadingElement;
+ 'accordion-list-item-content': AccordionListItemContentElement;
+};
diff --git a/packages/plugins/blockquote/package.json b/packages/plugins/blockquote/package.json
index 155e04699..4fd9bf894 100644
--- a/packages/plugins/blockquote/package.json
+++ b/packages/plugins/blockquote/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/blockquote",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "Blockquote plugin for Yoopta Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -34,5 +34,5 @@
"bugs": {
"url": "https://github.com/Darginec05/Editor-Yoopta/issues"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/plugins/blockquote/src/commands/index.ts b/packages/plugins/blockquote/src/commands/index.ts
new file mode 100644
index 000000000..4e5f09e24
--- /dev/null
+++ b/packages/plugins/blockquote/src/commands/index.ts
@@ -0,0 +1,33 @@
+import { Blocks, buildBlockData, generateId, YooEditor, YooptaBlockPath } from '@yoopta/editor';
+import { BlockquoteElement } from '../types';
+
+type BlockquoteElementOptions = {
+ text?: string;
+};
+
+export type InsertBlockquoteOptions = {
+ text?: string;
+ at?: YooptaBlockPath;
+ focus?: boolean;
+};
+
+export type BlockquoteCommands = {
+ buildBlockquoteElements: (editor: YooEditor, options?: Partial) => BlockquoteElement;
+ insertBlockquote: (editor: YooEditor, options?: Partial) => void;
+ deleteBlockquote: (editor: YooEditor, blockId: string) => void;
+};
+
+export const BlockquoteCommands: BlockquoteCommands = {
+ buildBlockquoteElements: (editor, options = {}) => {
+ return { id: generateId(), type: 'blockquote', children: [{ text: options?.text || '' }] };
+ },
+ insertBlockquote: (editor, options = {}) => {
+ const { at, focus, text } = options;
+
+ const blockquote = BlockquoteCommands.buildBlockquoteElements(editor, { text });
+ Blocks.insertBlock(editor, buildBlockData({ value: [blockquote], type: 'Blockquote' }), { at, focus });
+ },
+ deleteBlockquote: (editor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+};
diff --git a/packages/plugins/blockquote/src/index.ts b/packages/plugins/blockquote/src/index.ts
index 9a17008aa..e44f166f6 100644
--- a/packages/plugins/blockquote/src/index.ts
+++ b/packages/plugins/blockquote/src/index.ts
@@ -8,5 +8,7 @@ declare module 'slate' {
}
}
+export { BlockquoteCommands } from './commands';
+
export default Blockquote;
export { BlockquoteElement };
diff --git a/packages/plugins/blockquote/src/plugin/index.tsx b/packages/plugins/blockquote/src/plugin/index.tsx
index 36871fe5e..cf961aece 100644
--- a/packages/plugins/blockquote/src/plugin/index.tsx
+++ b/packages/plugins/blockquote/src/plugin/index.tsx
@@ -1,5 +1,6 @@
-import { Elements, generateId, YooptaPlugin } from '@yoopta/editor';
+import { Elements, generateId, serializeTextNodesIntoMarkdown, YooptaPlugin } from '@yoopta/editor';
import { Element, Transforms } from 'slate';
+import { BlockquoteCommands } from '../commands';
import { BlockquoteRender } from '../ui/Blockquote';
const Blockquote = new YooptaPlugin({
@@ -16,6 +17,7 @@ const Blockquote = new YooptaPlugin({
},
shortcuts: ['>'],
},
+ commands: BlockquoteCommands,
parsers: {
html: {
deserialize: {
@@ -28,7 +30,7 @@ const Blockquote = new YooptaPlugin({
},
markdown: {
serialize: (element, text) => {
- return `> ${text}`;
+ return `> ${serializeTextNodesIntoMarkdown(element.children)}`;
},
},
},
diff --git a/packages/plugins/callout/package.json b/packages/plugins/callout/package.json
index d2d96fc8c..6929554e0 100644
--- a/packages/plugins/callout/package.json
+++ b/packages/plugins/callout/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/callout",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "Callout plugin for Yoopta Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -34,5 +34,5 @@
"bugs": {
"url": "https://github.com/Darginec05/Editor-Yoopta/issues"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/plugins/callout/src/commands/index.ts b/packages/plugins/callout/src/commands/index.ts
new file mode 100644
index 000000000..16305f0ff
--- /dev/null
+++ b/packages/plugins/callout/src/commands/index.ts
@@ -0,0 +1,42 @@
+import { Blocks, buildBlockData, Elements, generateId, YooEditor, YooptaBlockPath } from '@yoopta/editor';
+import { CalloutElement, CalloutElementProps, CalloutPluginElementKeys, CalloutTheme } from '../types';
+
+type CalloutElementOptions = {
+ text?: string;
+};
+
+type InsertCalloutOptions = {
+ text?: string;
+ at?: YooptaBlockPath;
+ focus?: boolean;
+};
+
+export type CalloutCommands = {
+ buildCalloutElements: (editor: YooEditor, options?: Partial) => CalloutElement;
+ insertCallout: (editor: YooEditor, options?: Partial) => void;
+ deleteCallout: (editor: YooEditor, blockId: string) => void;
+ updateCalloutTheme: (editor: YooEditor, blockId: string, theme: CalloutTheme) => void;
+};
+
+export const CalloutCommands: CalloutCommands = {
+ buildCalloutElements: (editor, options = {}) => {
+ return { id: generateId(), type: 'callout', children: [{ text: options?.text || '' }] };
+ },
+ insertCallout: (editor, options = {}) => {
+ const { at, focus, text } = options;
+
+ const callout = CalloutCommands.buildCalloutElements(editor, { text });
+ Blocks.insertBlock(editor, buildBlockData({ value: [callout], type: 'Callout' }), { at, focus });
+ },
+ deleteCallout: (editor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+ updateCalloutTheme: (editor: YooEditor, blockId: string, theme: CalloutTheme) => {
+ Elements.updateElement(editor, blockId, {
+ type: 'callout',
+ props: {
+ theme,
+ },
+ });
+ },
+};
diff --git a/packages/plugins/callout/src/index.ts b/packages/plugins/callout/src/index.ts
index b1ffdfb40..1856b8ad7 100644
--- a/packages/plugins/callout/src/index.ts
+++ b/packages/plugins/callout/src/index.ts
@@ -8,5 +8,7 @@ declare module 'slate' {
}
}
+export { CalloutCommands } from './commands';
+
export default Callout;
export { CalloutElement, CalloutElementProps };
diff --git a/packages/plugins/callout/src/plugin/index.tsx b/packages/plugins/callout/src/plugin/index.tsx
index ec4781602..4f0155cc6 100644
--- a/packages/plugins/callout/src/plugin/index.tsx
+++ b/packages/plugins/callout/src/plugin/index.tsx
@@ -1,10 +1,17 @@
-import { generateId, YooptaPlugin } from '@yoopta/editor';
+import {
+ deserializeTextNodes,
+ generateId,
+ serializeTextNodes,
+ serializeTextNodesIntoMarkdown,
+ YooptaPlugin,
+} from '@yoopta/editor';
import { CSSProperties } from 'react';
-import { CalloutElementProps, CalloutPluginElementKeys, CalloutTheme } from '../types';
+import { CalloutCommands } from '../commands';
+import { CalloutElementMap, CalloutTheme } from '../types';
import { CalloutRender } from '../ui/Callout';
import { CALLOUT_THEME_STYLES } from '../utils';
-const Callout = new YooptaPlugin({
+const Callout = new YooptaPlugin({
type: 'Callout',
elements: {
callout: {
@@ -14,6 +21,7 @@ const Callout = new YooptaPlugin(
},
},
},
+ commands: CalloutCommands,
options: {
display: {
title: 'Callout',
@@ -25,14 +33,14 @@ const Callout = new YooptaPlugin(
html: {
deserialize: {
nodeNames: ['DL'],
- parse(el) {
+ parse(el, editor) {
if (el.nodeName === 'DL' || el.nodeName === 'DIV') {
const theme = el.getAttribute('data-theme') as CalloutTheme;
return {
id: generateId(),
type: 'callout',
- children: [{ text: el.textContent || '' }],
+ children: deserializeTextNodes(editor, el.childNodes),
props: {
theme,
},
@@ -48,12 +56,14 @@ const Callout = new YooptaPlugin(
element.props?.theme || 'default'
}" data-meta-align="${align}" data-meta-depth="${depth}" style="margin-left: ${depth}px; text-align: ${align}; padding: .5rem .5rem .5rem 1rem; margin-top: .5rem; border-radius: .375rem; color: ${
theme.color
- }; border-left: ${theme.borderLeft || 0}; background-color: ${theme.backgroundColor}">${text}`;
+ }; border-left: ${theme.borderLeft || 0}; background-color: ${theme.backgroundColor}">${serializeTextNodes(
+ element.children,
+ )}`;
},
},
markdown: {
serialize: (element, text) => {
- return `> ${text}`;
+ return `> ${serializeTextNodesIntoMarkdown(element.children)}`;
},
},
},
diff --git a/packages/plugins/callout/src/types.ts b/packages/plugins/callout/src/types.ts
index 2d5bb03f6..cf0b5fba8 100644
--- a/packages/plugins/callout/src/types.ts
+++ b/packages/plugins/callout/src/types.ts
@@ -5,3 +5,7 @@ export type CalloutPluginElementKeys = 'callout';
export type CalloutTheme = 'default' | 'success' | 'warning' | 'error' | 'info';
export type CalloutElementProps = { theme: CalloutTheme };
export type CalloutElement = SlateElement<'callout', CalloutElementProps>;
+
+export type CalloutElementMap = {
+ callout: CalloutElement;
+};
diff --git a/packages/plugins/code/package.json b/packages/plugins/code/package.json
index 178cf472c..3ded4a98b 100644
--- a/packages/plugins/code/package.json
+++ b/packages/plugins/code/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/code",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "Code plugin with syntax highlighting for Yoopta Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -50,6 +50,7 @@
"@codemirror/lang-vue": "^0.1.3",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.0.0",
+ "@codemirror/legacy-modes": "^6.4.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@radix-ui/react-select": "^2.0.0",
"@uiw/codemirror-extensions-basic-setup": "^4.21.24",
@@ -69,5 +70,5 @@
"devDependencies": {
"check-peer-dependencies": "^4.3.0"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/plugins/code/src/commands/index.ts b/packages/plugins/code/src/commands/index.ts
new file mode 100644
index 000000000..25dadf8bf
--- /dev/null
+++ b/packages/plugins/code/src/commands/index.ts
@@ -0,0 +1,44 @@
+import { Blocks, buildBlockData, generateId, YooEditor, YooptaBlockPath } from '@yoopta/editor';
+import { CodeElement, CodeElementProps } from '../types';
+
+type CodeElementOptions = {
+ text?: string;
+ props?: CodeElementProps;
+};
+
+type InsertCodeOptions = CodeElementOptions & {
+ at?: YooptaBlockPath;
+ focus?: boolean;
+};
+
+export type CodeCommands = {
+ buildCodeElements: (editor: YooEditor, options?: Partial) => CodeElement;
+ insertCode: (editor: YooEditor, options?: Partial) => void;
+ deleteCode: (editor: YooEditor, blockId: string) => void;
+ updateCodeTheme: (editor: YooEditor, blockId: string, theme: CodeElementProps['theme']) => void;
+ updateCodeLanguage: (editor: YooEditor, blockId: string, language: CodeElementProps['language']) => void;
+};
+
+export const CodeCommands: CodeCommands = {
+ buildCodeElements: (editor: YooEditor, options = {}) => {
+ return { id: generateId(), type: 'code', children: [{ text: options?.text || '', props: options?.props }] };
+ },
+ insertCode: (editor: YooEditor, options = {}) => {
+ const { at, focus, text, props } = options;
+ const code = CodeCommands.buildCodeElements(editor, { text, props });
+ Blocks.insertBlock(editor, buildBlockData({ value: [code], type: 'Code' }), { focus, at });
+ },
+ deleteCode: (editor: YooEditor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+ updateCodeTheme: (editor: YooEditor, blockId, theme) => {
+ const block = editor.children[blockId];
+ const element = block.value[0] as CodeElement;
+ Blocks.updateBlock(editor, blockId, { value: [{ ...element, props: { ...element.props, theme } }] });
+ },
+ updateCodeLanguage: (editor: YooEditor, blockId, language) => {
+ const block = editor.children[blockId];
+ const element = block.value[0] as CodeElement;
+ Blocks.updateBlock(editor, blockId, { value: [{ ...element, props: { ...element.props, language } }] });
+ },
+};
diff --git a/packages/plugins/code/src/index.ts b/packages/plugins/code/src/index.ts
index 2e0655ca8..a11e908d6 100644
--- a/packages/plugins/code/src/index.ts
+++ b/packages/plugins/code/src/index.ts
@@ -8,5 +8,7 @@ declare module 'slate' {
}
}
+export { CodeCommands } from './commands';
+
export default Code;
export { CodeElement, CodeElementProps };
diff --git a/packages/plugins/code/src/plugin/index.tsx b/packages/plugins/code/src/plugin/index.tsx
index 7e000dc46..15302b56f 100644
--- a/packages/plugins/code/src/plugin/index.tsx
+++ b/packages/plugins/code/src/plugin/index.tsx
@@ -1,5 +1,6 @@
import { generateId, YooptaPlugin } from '@yoopta/editor';
-import { CodeElementProps, CodePluginBlockOptions, CodePluginElements } from '../types';
+import { CodeCommands } from '../commands';
+import { CodeElementMap, CodeElementProps, CodePluginBlockOptions, CodePluginElements } from '../types';
import { CodeEditor } from '../ui/Code';
const ALIGNS_TO_JUSTIFY = {
@@ -8,7 +9,7 @@ const ALIGNS_TO_JUSTIFY = {
right: 'flex-end',
};
-const Code = new YooptaPlugin({
+const Code = new YooptaPlugin({
type: 'Code',
customEditor: CodeEditor,
elements: {
@@ -30,6 +31,7 @@ const Code = new YooptaPlugin;
+
+export type CodeElementMap = {
+ code: CodeElement;
+};
diff --git a/packages/plugins/code/src/ui/Code.tsx b/packages/plugins/code/src/ui/Code.tsx
index 0db97b541..161a168ca 100644
--- a/packages/plugins/code/src/ui/Code.tsx
+++ b/packages/plugins/code/src/ui/Code.tsx
@@ -69,7 +69,7 @@ const CodeEditor = ({ blockId }: PluginCustomEditorRenderProps) => {
{
+ return ;
+};
+```
+
+### Default classnames
+
+- .yoopta-divider
+
+### Default options
+
+```js
+const Divider = new YooptaPlugin({
+ options: {
+ display: {
+ title: 'Text',
+ description: 'Start writing plain text.',
+ },
+ shortcuts: ['p', 'text'],
+ },
+});
+```
+
+### How to extend
+
+```tsx
+const plugins = [
+ Divider.extend({
+ renders: {
+ divider: (props) =>
+ },
+ options: {
+ shortcuts: [``],
+ display: {
+ title: ``,
+ description: ``,
+ },
+ HTMLAttributes: {
+ className: '',
+ // ...other HTML attributes
+ },
+ },
+ });
+];
+```
diff --git a/packages/plugins/divider/package.json b/packages/plugins/divider/package.json
new file mode 100644
index 000000000..d6804eaef
--- /dev/null
+++ b/packages/plugins/divider/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@yoopta/divider",
+ "version": "4.8.0",
+ "description": "Divider plugin for Yoopta Editor",
+ "author": "Darginec05 ",
+ "homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
+ "license": "MIT",
+ "private": false,
+ "main": "dist/index.js",
+ "type": "module",
+ "module": "dist/index.js",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist/"
+ ],
+ "peerDependencies": {
+ "@yoopta/editor": ">=4.0.0",
+ "react": ">=17.0.2",
+ "react-dom": ">=17.0.2"
+ },
+ "publishConfig": {
+ "registry": "https://registry.yarnpkg.com"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Darginec05/Editor-Yoopta.git"
+ },
+ "scripts": {
+ "test": "node ./__tests__/@yoopta/divider.test.js",
+ "start": "rollup --config rollup.config.js --watch --bundleConfigAsCjs --environment NODE_ENV:development",
+ "prepublishOnly": "yarn build",
+ "build": "rollup --config rollup.config.js --bundleConfigAsCjs --environment NODE_ENV:production"
+ },
+ "bugs": {
+ "url": "https://github.com/Darginec05/Editor-Yoopta/issues"
+ },
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
+}
diff --git a/packages/plugins/divider/rollup.config.js b/packages/plugins/divider/rollup.config.js
new file mode 100644
index 000000000..d8f0b5d46
--- /dev/null
+++ b/packages/plugins/divider/rollup.config.js
@@ -0,0 +1,7 @@
+import { createRollupConfig } from '../../../config/rollup';
+
+const pkg = require('./package.json');
+export default createRollupConfig({
+ pkg,
+ tailwindConfig: { content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'] },
+});
diff --git a/packages/plugins/divider/src/commands/index.ts b/packages/plugins/divider/src/commands/index.ts
new file mode 100644
index 000000000..e74de4d88
--- /dev/null
+++ b/packages/plugins/divider/src/commands/index.ts
@@ -0,0 +1,42 @@
+import { Blocks, buildBlockData, Elements, generateId, YooEditor, YooptaBlockPath } from '@yoopta/editor';
+import { DividerElement, DividerElementProps } from '../types';
+
+type DividerInsertOptions = DividerElementProps & {
+ at?: YooptaBlockPath;
+ focus?: boolean;
+};
+
+export type DividerCommands = {
+ buildDividerElements: (editor: YooEditor, options?: Partial) => DividerElement;
+ insertDivider: (editor: YooEditor, options?: Partial) => void;
+ deleteDivider: (editor: YooEditor, blockId: string) => void;
+ updateDivider: (editor: YooEditor, blockId: string, props: Partial) => void;
+};
+
+export const DividerCommands: DividerCommands = {
+ buildDividerElements: (editor, options = {}) => {
+ return {
+ id: generateId(),
+ type: 'divider',
+ children: [{ text: '' }],
+ props: { color: options.color || '#EFEFEE', theme: options.theme || 'solid', nodeType: 'void' },
+ };
+ },
+ insertDivider: (editor, options = {}) => {
+ const { at, focus } = options;
+
+ const dividerElement = DividerCommands.buildDividerElements(editor);
+ Blocks.insertBlock(editor, buildBlockData({ value: [dividerElement], type: 'Divider' }), { at, focus });
+ },
+ deleteDivider: (editor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+ updateDivider: (editor: YooEditor, blockId: string, props) => {
+ Elements.updateElement(editor, blockId, {
+ type: 'divider',
+ props: {
+ ...props,
+ },
+ });
+ },
+};
diff --git a/packages/plugins/divider/src/components/DividerBlockOptions.tsx b/packages/plugins/divider/src/components/DividerBlockOptions.tsx
new file mode 100644
index 000000000..22b094cad
--- /dev/null
+++ b/packages/plugins/divider/src/components/DividerBlockOptions.tsx
@@ -0,0 +1,89 @@
+import { Elements, UI, YooEditor, YooptaBlockData } from '@yoopta/editor';
+import { DividerElementProps, DividerTheme } from '../types';
+import SolidIcon from '../icons/solid.svg';
+import DotsIcon from '../icons/dots.svg';
+import DashedIcon from '../icons/dashed.svg';
+import CheckmarkIcon from '../icons/checkmark.svg';
+
+const { ExtendedBlockActions, BlockOptionsMenuGroup, BlockOptionsMenuItem, BlockOptionsSeparator } = UI;
+
+type Props = {
+ editor: YooEditor;
+ block: YooptaBlockData;
+ props?: DividerElementProps;
+};
+
+const DividerBlockOptions = ({ editor, block, props: dividerProps }: Props) => {
+ const onChangeTheme = (theme: DividerTheme) => {
+ Elements.updateElement<'divider', DividerElementProps>(editor, block.id, {
+ type: 'divider',
+ props: {
+ theme,
+ },
+ });
+ };
+
+ const isActiveTheme = (theme: DividerTheme) => dividerProps?.theme === theme;
+
+ return (
+ editor.setSelection([block.meta.order])} className="yoopta-divider-options">
+
+
+
+ onChangeTheme('solid')}
+ >
+
+
+ Line
+
+ {isActiveTheme('solid') && }
+
+
+
+ onChangeTheme('dashed')}
+ >
+
+
+ Dashed
+
+ {isActiveTheme('dashed') && }
+
+
+
+ onChangeTheme('dotted')}
+ >
+
+
+ Dots
+
+ {isActiveTheme('dotted') && }
+
+
+
+ onChangeTheme('gradient')}
+ >
+
+
+ Gradient
+
+ {isActiveTheme('gradient') && }
+
+
+
+
+ );
+};
+
+export { DividerBlockOptions };
diff --git a/packages/plugins/divider/src/events/onKeyDown.ts b/packages/plugins/divider/src/events/onKeyDown.ts
new file mode 100644
index 000000000..b9b22b426
--- /dev/null
+++ b/packages/plugins/divider/src/events/onKeyDown.ts
@@ -0,0 +1,21 @@
+import { Elements, PluginEventHandlerOptions, SlateEditor, YooEditor } from '@yoopta/editor';
+import { DividerElement, DividerTheme } from '../types';
+
+const dividerTypes: DividerTheme[] = ['solid', 'dashed', 'dotted', 'gradient'];
+
+export function onKeyDown(editor: YooEditor, slate: SlateEditor, { hotkeys, currentBlock }: PluginEventHandlerOptions) {
+ return (event) => {
+ if (hotkeys.isCmdShiftD(event)) {
+ event.preventDefault();
+
+ const element = Elements.getElement(editor, currentBlock.id, { type: 'divider' }) as DividerElement;
+ const theme = dividerTypes[(dividerTypes.indexOf(element.props!.theme) + 1) % dividerTypes.length];
+ Elements.updateElement(editor, currentBlock.id, {
+ type: 'divider',
+ props: {
+ theme,
+ },
+ });
+ }
+ };
+}
diff --git a/packages/plugins/divider/src/icons/checkmark.svg b/packages/plugins/divider/src/icons/checkmark.svg
new file mode 100644
index 000000000..f156213cc
--- /dev/null
+++ b/packages/plugins/divider/src/icons/checkmark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/plugins/divider/src/icons/dashed.svg b/packages/plugins/divider/src/icons/dashed.svg
new file mode 100644
index 000000000..c5d73c4ca
--- /dev/null
+++ b/packages/plugins/divider/src/icons/dashed.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/plugins/divider/src/icons/dots.svg b/packages/plugins/divider/src/icons/dots.svg
new file mode 100644
index 000000000..a821ba044
--- /dev/null
+++ b/packages/plugins/divider/src/icons/dots.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/plugins/divider/src/icons/solid.svg b/packages/plugins/divider/src/icons/solid.svg
new file mode 100644
index 000000000..69db7f73c
--- /dev/null
+++ b/packages/plugins/divider/src/icons/solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/plugins/divider/src/index.ts b/packages/plugins/divider/src/index.ts
new file mode 100644
index 000000000..ef227d9ca
--- /dev/null
+++ b/packages/plugins/divider/src/index.ts
@@ -0,0 +1,14 @@
+import { DividerElement, DividerElementProps, DividerTheme } from './types';
+import { Divider } from './plugin';
+export { DividerCommands } from './commands';
+import './styles.css';
+
+declare module 'slate' {
+ interface CustomTypes {
+ Element: DividerElement;
+ }
+}
+
+export default Divider;
+
+export { DividerElement, DividerElementProps, DividerTheme };
diff --git a/packages/plugins/divider/src/plugin/index.tsx b/packages/plugins/divider/src/plugin/index.tsx
new file mode 100644
index 000000000..42a965eb7
--- /dev/null
+++ b/packages/plugins/divider/src/plugin/index.tsx
@@ -0,0 +1,63 @@
+import { generateId, YooptaPlugin } from '@yoopta/editor';
+import { DividerCommands } from '../commands';
+import { onKeyDown } from '../events/onKeyDown';
+import { DividerElementMap } from '../types';
+import { DividerRender } from '../ui/Divider';
+
+const Divider = new YooptaPlugin({
+ type: 'Divider',
+ elements: {
+ divider: {
+ render: DividerRender,
+ props: {
+ nodeType: 'void',
+ theme: 'solid',
+ color: '#EFEFEE',
+ },
+ },
+ },
+ options: {
+ display: {
+ title: 'Divider',
+ description: 'Divide your blocks',
+ },
+ shortcuts: ['---', 'divider', 'line'],
+ },
+ parsers: {
+ html: {
+ deserialize: {
+ nodeNames: ['HR'],
+ parse: (el) => {
+ const theme = el.getAttribute('data-meta-theme') || 'solid';
+ const color = el.getAttribute('data-meta-color') || '#EFEFEE';
+
+ return {
+ id: generateId(),
+ type: 'divider',
+ props: {
+ nodeType: 'void',
+ theme,
+ color,
+ },
+ children: [{ text: '' }],
+ };
+ },
+ },
+ serialize: (element, text, blockMeta) => {
+ const { theme = 'solid', color = '#EFEFEE' } = element.props || {};
+ return `
`;
+ },
+ },
+ markdown: {
+ serialize: (element, text) => {
+ return '---\n';
+ },
+ },
+ },
+ commands: DividerCommands,
+ events: {
+ onKeyDown,
+ },
+});
+
+export { Divider };
diff --git a/packages/plugins/divider/src/react-svg.d.ts b/packages/plugins/divider/src/react-svg.d.ts
new file mode 100644
index 000000000..3f79836c9
--- /dev/null
+++ b/packages/plugins/divider/src/react-svg.d.ts
@@ -0,0 +1,6 @@
+declare module '*.svg' {
+ import { ReactElement, SVGProps } from 'react';
+
+ const content: (props: SVGProps) => ReactElement;
+ export default content;
+}
diff --git a/packages/plugins/divider/src/styles.css b/packages/plugins/divider/src/styles.css
new file mode 100644
index 000000000..7a8db659d
--- /dev/null
+++ b/packages/plugins/divider/src/styles.css
@@ -0,0 +1,68 @@
+@tailwind utilities;
+
+.yoopta-divider .yoopta-divider-options {
+ opacity: 0;
+ transition: opacity 0.15s ease-in-out;
+ right: 3px;
+ top: 3px;
+ color: #000;
+}
+
+.yoopta-divider:hover .yoopta-divider-options {
+ opacity: 1;
+}
+
+.yoopta-divider {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: auto;
+ width: 100%;
+ height: 20px;
+ color: #e5e7eb;
+}
+
+.yoopta-divider-line {
+ width: 100%;
+ height: 1px;
+ visibility: visible;
+ border-bottom: 2px solid currentColor;
+}
+
+.yoopta-divider-solid {
+ @apply yoopta-divider-line;
+ border-bottom-style: solid;
+}
+
+.yoopta-divider-dashed {
+ @apply yoopta-divider-line;
+ border-bottom-style: dashed;
+}
+
+.yoopta-divider-dotted {
+ @apply yoopta-divider-line;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ user-select: none;
+ border-bottom: none;
+ height: 3px;
+}
+
+.yoopta-divider-dotted div {
+ min-width: 3px;
+ max-width: 3px;
+ min-height: 3px;
+ max-height: 3px;
+ margin-right: 10px;
+ border-radius: 50%;
+}
+
+.yoopta-divider-dotted div:last-child {
+ margin-right: 0;
+}
+
+.yoopta-divider-gradient {
+ width: 100%;
+ height: 2px;
+}
\ No newline at end of file
diff --git a/packages/plugins/divider/src/types.ts b/packages/plugins/divider/src/types.ts
new file mode 100644
index 000000000..dd38bd290
--- /dev/null
+++ b/packages/plugins/divider/src/types.ts
@@ -0,0 +1,13 @@
+import { SlateElement } from '@yoopta/editor';
+
+export type DividerTheme = 'solid' | 'dashed' | 'dotted' | 'gradient';
+
+export type DividerElementProps = {
+ theme: DividerTheme;
+ color?: string;
+};
+
+export type DividerElement = SlateElement<'divider', DividerElementProps>;
+export type DividerElementMap = {
+ divider: DividerElement;
+};
diff --git a/packages/plugins/divider/src/ui/Divider.tsx b/packages/plugins/divider/src/ui/Divider.tsx
new file mode 100644
index 000000000..65fcf6541
--- /dev/null
+++ b/packages/plugins/divider/src/ui/Divider.tsx
@@ -0,0 +1,58 @@
+import { PluginElementRenderProps, useBlockData, useYooptaEditor } from '@yoopta/editor';
+import { DividerBlockOptions } from '../components/DividerBlockOptions';
+
+const DividerRender = ({ extendRender, ...props }: PluginElementRenderProps) => {
+ const { className = '', ...htmlAttrs } = props.HTMLAttributes || {};
+ const editor = useYooptaEditor();
+ const blockData = useBlockData(props.blockId);
+
+ if (extendRender) return extendRender(props);
+
+ const color = props.element.props?.color || '#e5e7eb';
+ const theme = props.element.props?.theme || 'solid';
+
+ const getDividerContent = () => {
+ switch (theme) {
+ case 'dashed':
+ return
;
+ case 'dotted':
+ return (
+
+ );
+ case 'gradient':
+ return (
+
+ );
+ default:
+ return
;
+ }
+ };
+
+ const onClick = (event: React.MouseEvent) => {
+ editor.setSelection([blockData.meta.order]);
+ editor.setBlockSelected([blockData.meta.order]);
+ };
+
+ return (
+
+ {!editor.readOnly &&
}
+ {getDividerContent()}
+ {props.children}
+
+ );
+};
+
+export { DividerRender };
diff --git a/packages/plugins/divider/tsconfig.json b/packages/plugins/divider/tsconfig.json
new file mode 100644
index 000000000..ab104acf8
--- /dev/null
+++ b/packages/plugins/divider/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../../config/tsconfig.base.json",
+ "include": ["react-svg.d.ts", "css-modules.d.ts", "src"],
+ "exclude": ["dist", "src/**/*.test.tsx", "src/**/*.stories.tsx"],
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./dist"
+ },
+ "references": [
+ {
+ "path": "../../core/editor"
+ }
+ ]
+}
diff --git a/packages/plugins/embed/package.json b/packages/plugins/embed/package.json
index 463aa0c5f..a69752abc 100644
--- a/packages/plugins/embed/package.json
+++ b/packages/plugins/embed/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/embed",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "Embed plugin for Yoopta Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -39,5 +39,5 @@
"@radix-ui/react-icons": "^1.3.0",
"re-resizable": "^6.9.11"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/plugins/embed/src/commands/index.ts b/packages/plugins/embed/src/commands/index.ts
new file mode 100644
index 000000000..50a99877b
--- /dev/null
+++ b/packages/plugins/embed/src/commands/index.ts
@@ -0,0 +1,36 @@
+import { Blocks, buildBlockData, Elements, generateId, YooEditor, YooptaBlockPath } from '@yoopta/editor';
+import { EmbedElement, EmbedElementProps } from '../types';
+
+type EmbedElementOptions = {
+ props?: Omit;
+};
+
+type InsertEmbedOptions = EmbedElementOptions & {
+ at?: YooptaBlockPath;
+ focus?: boolean;
+};
+
+export type EmbedCommands = {
+ buildEmbedElements: (editor: YooEditor, options?: Partial) => EmbedElement;
+ insertEmbed: (editor: YooEditor, options?: Partial) => void;
+ deleteEmbed: (editor: YooEditor, blockId: string) => void;
+ updateEmbed: (editor: YooEditor, blockId: string, props: Partial) => void;
+};
+
+export const EmbedCommands: EmbedCommands = {
+ buildEmbedElements: (editor: YooEditor, options = {}) => {
+ const embedProps = options?.props ? { ...options.props, nodeType: 'void' } : { nodeType: 'void' };
+ return { id: generateId(), type: 'embed', children: [{ text: '', props: embedProps }] };
+ },
+ insertEmbed: (editor: YooEditor, options = {}) => {
+ const { at, focus, props } = options;
+ const embed = EmbedCommands.buildEmbedElements(editor, { props });
+ Blocks.insertBlock(editor, buildBlockData({ value: [embed], type: 'Embed' }), { focus, at });
+ },
+ deleteEmbed: (editor: YooEditor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+ updateEmbed: (editor: YooEditor, blockId, props) => {
+ Elements.updateElement(editor, blockId, { props });
+ },
+};
diff --git a/packages/plugins/embed/src/index.ts b/packages/plugins/embed/src/index.ts
index c061534c1..b7568f571 100644
--- a/packages/plugins/embed/src/index.ts
+++ b/packages/plugins/embed/src/index.ts
@@ -8,5 +8,7 @@ declare module 'slate' {
}
}
+export { EmbedCommands } from './commands';
+
export default Embed;
export { EmbedElement, EmbedElementProps };
diff --git a/packages/plugins/embed/src/plugin/index.tsx b/packages/plugins/embed/src/plugin/index.tsx
index ffec03745..997fab5d9 100644
--- a/packages/plugins/embed/src/plugin/index.tsx
+++ b/packages/plugins/embed/src/plugin/index.tsx
@@ -1,5 +1,12 @@
import { generateId, YooptaPlugin } from '@yoopta/editor';
-import { EmbedElementProps, EmbedPluginElements, EmbedPluginOptions, EmbedProviderTypes } from '../types';
+import { EmbedCommands } from '../commands';
+import {
+ EmbedElementMap,
+ EmbedElementProps,
+ EmbedPluginElements,
+ EmbedPluginOptions,
+ EmbedProviderTypes,
+} from '../types';
import { EmbedRender } from '../ui/Embed';
const ALIGNS_TO_JUSTIFY = {
@@ -8,13 +15,13 @@ const ALIGNS_TO_JUSTIFY = {
right: 'flex-end',
};
-const Embed = new YooptaPlugin({
+const Embed = new YooptaPlugin({
type: 'Embed',
elements: {
embed: {
render: EmbedRender,
props: {
- sizes: { width: 650, height: 400 },
+ sizes: { width: 650, height: 500 },
nodeType: 'void',
},
},
@@ -24,8 +31,9 @@ const Embed = new YooptaPlugin = ({ provider, width, height, attributes, children }) => {
+ const embedUrl = `https://www.instagram.com/p/${provider.id}/embed`;
+ const instagramRootRef = useRef(null);
+
+ const { isIntersecting: isInViewport } = useIntersectionObserver(instagramRootRef, {
+ freezeOnceVisible: true,
+ rootMargin: '50%',
+ });
+
+ return (
+
+
+ {isInViewport && (
+
+ )}
+ {children}
+
+
+ );
+};
+
+export default Instagram;
diff --git a/packages/plugins/embed/src/providers/Twitter.tsx b/packages/plugins/embed/src/providers/Twitter.tsx
index 89b9a6158..cd48438f2 100644
--- a/packages/plugins/embed/src/providers/Twitter.tsx
+++ b/packages/plugins/embed/src/providers/Twitter.tsx
@@ -1,10 +1,10 @@
+import { useEffect, useRef, useState } from 'react';
import { Elements, useYooptaEditor } from '@yoopta/editor';
-import { useEffect, useRef } from 'react';
import { useIntersectionObserver } from '../hooks/useIntersectionObserver';
import { EmbedElementProps, EmbedPluginElements, ProviderRenderProps } from '../types';
-function Twitter({ provider, blockId, attributes, children }: ProviderRenderProps) {
- const twitterRootRef = useRef(null);
+function Twitter({ provider, blockId, attributes, children, height, width }: ProviderRenderProps) {
+ const twitterRootRef = useRef(null);
const editor = useYooptaEditor();
const { isIntersecting: isInViewport } = useIntersectionObserver(twitterRootRef, {
@@ -19,40 +19,48 @@ function Twitter({ provider, blockId, attributes, children }: ProviderRenderProp
const script = document.createElement('script');
script.src = 'https://platform.twitter.com/widgets.js';
- (twitterRootRef.current as unknown as HTMLDivElement)?.appendChild(script);
+ script.async = true;
+ document.body.appendChild(script);
- script.onload = () => {
+ const renderTweet = () => {
if ((window as any).twttr) {
- (window as any).twttr.widgets.createTweet(provider.id, document.getElementById(elementId), {
- align: 'center',
- conversation: 'none',
- dnt: true,
- theme: 'dark',
- height: 500,
- width: 550,
- });
-
- Elements.updateElement(editor, blockId, {
- type: 'embed',
- props: {
- sizes: {
- width: 'auto',
- height: 'auto',
- },
- },
- });
+ (window as any).twttr.widgets
+ .createTweet(provider.id, document.getElementById(elementId), {
+ align: 'center',
+ conversation: 'none',
+ dnt: true,
+ theme: 'light',
+ })
+ .then((el) => {
+ if (el) {
+ Elements.updateElement(editor, blockId, {
+ type: 'embed',
+ props: {
+ sizes: {
+ height: el.offsetHeight + 16,
+ width: el.offsetWidth,
+ },
+ },
+ });
+ }
+ });
}
};
- }, [provider.id, isInViewport]);
- const onRef = (node) => {
- twitterRootRef.current = node;
- attributes.ref(node);
- };
+ if ((window as any).twttr) {
+ renderTweet();
+ } else {
+ script.onload = renderTweet;
+ }
+
+ return () => {
+ document.body.removeChild(script);
+ };
+ }, [provider.id, isInViewport, blockId, editor]);
return (
-
-
+
);
diff --git a/packages/plugins/embed/src/types.ts b/packages/plugins/embed/src/types.ts
index 30235f0f6..1fa2c852e 100644
--- a/packages/plugins/embed/src/types.ts
+++ b/packages/plugins/embed/src/types.ts
@@ -6,7 +6,15 @@ export type EmbedSizes = {
height: number | 'auto';
};
-export type EmbedProviderTypes = 'youtube' | 'vimeo' | 'dailymotion' | 'twitter' | 'figma' | string | null;
+export type EmbedProviderTypes =
+ | 'youtube'
+ | 'vimeo'
+ | 'dailymotion'
+ | 'twitter'
+ | 'figma'
+ | 'instagram'
+ | string
+ | null;
export type EmbedProvider = {
type: EmbedProviderTypes;
id: string;
@@ -23,8 +31,8 @@ export type EmbedElement = SlateElement<'embed', EmbedElementProps>;
export type EmbedPluginOptions = {
maxSizes?: {
- maxWidth?: number;
- maxHeight?: number;
+ maxWidth?: number | 'auto';
+ maxHeight?: number | 'auto';
};
};
@@ -34,3 +42,7 @@ export type ProviderRenderProps = {
width: number;
height: number;
} & Pick
;
+
+export type EmbedElementMap = {
+ embed: EmbedElement;
+};
diff --git a/packages/plugins/embed/src/ui/Embed.tsx b/packages/plugins/embed/src/ui/Embed.tsx
index 1c93cd1d6..5f33de094 100644
--- a/packages/plugins/embed/src/ui/Embed.tsx
+++ b/packages/plugins/embed/src/ui/Embed.tsx
@@ -44,7 +44,7 @@ const EmbedRender = ({ extendRender, ...props }: PluginElementRenderProps) => {
minWidth: 300,
size: { width: sizes.width, height: sizes.height },
maxWidth: pluginOptions?.maxSizes?.maxWidth || 800,
- maxHeight: pluginOptions?.maxSizes?.maxHeight || 720,
+ maxHeight: pluginOptions?.maxSizes?.maxHeight || 900,
lockAspectRatio: true,
resizeRatio: 2,
enable: {
diff --git a/packages/plugins/embed/src/ui/EmbedComponent.tsx b/packages/plugins/embed/src/ui/EmbedComponent.tsx
index 7acd585ad..3602783bf 100644
--- a/packages/plugins/embed/src/ui/EmbedComponent.tsx
+++ b/packages/plugins/embed/src/ui/EmbedComponent.tsx
@@ -1,6 +1,7 @@
import { RenderElementProps } from 'slate-react';
import { DailyMotion } from '../providers/DailyMotion';
import { Figma } from '../providers/Figma';
+import Instagram from '../providers/Instagram';
import Twitter from '../providers/Twitter';
import { Vimeo } from '../providers/Vimeo';
import { YouTube } from '../providers/Youtube';
@@ -18,6 +19,7 @@ const PROVIDERS = {
dailymotion: DailyMotion,
figma: Figma,
twitter: Twitter,
+ instagram: Instagram,
};
const EmbedComponent = ({ width, height, provider, blockId, attributes, children }: EmbedComponentProps) => {
diff --git a/packages/plugins/embed/src/ui/EmbedLinkUploader.tsx b/packages/plugins/embed/src/ui/EmbedLinkUploader.tsx
index 460aea8fb..f51ef47c3 100644
--- a/packages/plugins/embed/src/ui/EmbedLinkUploader.tsx
+++ b/packages/plugins/embed/src/ui/EmbedLinkUploader.tsx
@@ -47,7 +47,7 @@ const EmbedLinkUploader = ({ blockId, onClose }) => {
disabled={isEmpty}
onClick={embed}
>
- Embed embed
+ Embed link
);
diff --git a/packages/plugins/embed/src/utils/providers.ts b/packages/plugins/embed/src/utils/providers.ts
index 34f14d8d9..24ad06c0f 100644
--- a/packages/plugins/embed/src/utils/providers.ts
+++ b/packages/plugins/embed/src/utils/providers.ts
@@ -24,6 +24,12 @@ export const getDailymotionId = (url: string) => {
return null;
};
+export function getInstagramId(url: string) {
+ const regex = /(?:https?:\/\/)?(?:www\.)?instagram\.com(?:\/p\/|\/reel\/|\/tv\/)([^\/?#&]+).*$/;
+ const match = url.match(regex);
+ return match ? match[1] : null;
+}
+
export function getProvider(url: string): EmbedProviderTypes | null {
if (url.includes('youtube.com') || url.includes('youtu.be')) {
return 'youtube';
@@ -35,6 +41,8 @@ export function getProvider(url: string): EmbedProviderTypes | null {
return 'twitter';
} else if (url.includes('figma')) {
return 'figma';
+ } else if (url.includes('instagram.com')) {
+ return 'instagram';
} else {
return null;
}
@@ -64,4 +72,5 @@ export const ProviderGetters = {
dailymotion: getDailymotionId,
twitter: getTwitterEmbedId,
figma: getFigmaUrl,
+ instagram: getInstagramId,
};
diff --git a/packages/plugins/file/package.json b/packages/plugins/file/package.json
index 06f808dac..1309218bf 100644
--- a/packages/plugins/file/package.json
+++ b/packages/plugins/file/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/file",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "File plugin for Yoopta Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -38,5 +38,5 @@
"@floating-ui/react": "^0.26.9",
"@radix-ui/react-icons": "^1.3.0"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/plugins/file/src/commands/index.ts b/packages/plugins/file/src/commands/index.ts
new file mode 100644
index 000000000..e9e54d535
--- /dev/null
+++ b/packages/plugins/file/src/commands/index.ts
@@ -0,0 +1,36 @@
+import { Blocks, buildBlockData, Elements, generateId, YooEditor, YooptaBlockPath } from '@yoopta/editor';
+import { FileElement, FileElementProps } from '../types';
+
+type FileElementOptions = {
+ props?: Omit;
+};
+
+type InsertFileOptions = FileElementOptions & {
+ at?: YooptaBlockPath;
+ focus?: boolean;
+};
+
+export type FileCommands = {
+ buildFileElements: (editor: YooEditor, options?: Partial) => FileElement;
+ insertFile: (editor: YooEditor, options?: Partial) => void;
+ deleteFile: (editor: YooEditor, blockId: string) => void;
+ updateFile: (editor: YooEditor, blockId: string, props: Partial) => void;
+};
+
+export const FileCommands: FileCommands = {
+ buildFileElements: (editor: YooEditor, options = {}) => {
+ const fileProps = options?.props ? { ...options.props, nodeType: 'void' } : { nodeType: 'void' };
+ return { id: generateId(), type: 'file', children: [{ text: '', props: fileProps }] };
+ },
+ insertFile: (editor: YooEditor, options = {}) => {
+ const { at, focus, props } = options;
+ const file = FileCommands.buildFileElements(editor, { props });
+ Blocks.insertBlock(editor, buildBlockData({ value: [file], type: 'File' }), { focus, at });
+ },
+ deleteFile: (editor: YooEditor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+ updateFile: (editor: YooEditor, blockId, props) => {
+ Elements.updateElement(editor, blockId, { props });
+ },
+};
diff --git a/packages/plugins/file/src/index.ts b/packages/plugins/file/src/index.ts
index f8536eb46..0647082a8 100644
--- a/packages/plugins/file/src/index.ts
+++ b/packages/plugins/file/src/index.ts
@@ -8,5 +8,7 @@ declare module 'slate' {
}
}
+export { FileCommands } from './commands';
+
export default File;
export { FileElement, FileElementProps, FileUploadResponse };
diff --git a/packages/plugins/file/src/plugin/index.tsx b/packages/plugins/file/src/plugin/index.tsx
index bfbad2aba..f48352fd7 100644
--- a/packages/plugins/file/src/plugin/index.tsx
+++ b/packages/plugins/file/src/plugin/index.tsx
@@ -1,5 +1,6 @@
import { generateId, YooptaPlugin } from '@yoopta/editor';
-import { FileElementProps, FilePluginElements, FilePluginOptions } from '../types';
+import { FileCommands } from '../commands';
+import { FileElementMap, FilePluginOptions } from '../types';
import { FileRender } from '../ui/File';
const ALIGNS_TO_JUSTIFY = {
@@ -8,7 +9,7 @@ const ALIGNS_TO_JUSTIFY = {
right: 'flex-end',
};
-const File = new YooptaPlugin({
+const File = new YooptaPlugin({
type: 'File',
elements: {
file: {
@@ -22,6 +23,7 @@ const File = new YooptaPlugin Promise;
accept?: string;
};
+
+export type FileElementMap = {
+ file: FileElement;
+};
diff --git a/packages/plugins/headings/package.json b/packages/plugins/headings/package.json
index 9185a5b06..2e3950a15 100644
--- a/packages/plugins/headings/package.json
+++ b/packages/plugins/headings/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/headings",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "Headings plugin for Yoopta Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -39,5 +39,5 @@
"bugs": {
"url": "https://github.com/Darginec05/Editor-Yoopta/issues"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/plugins/headings/src/commands/index.ts b/packages/plugins/headings/src/commands/index.ts
new file mode 100644
index 000000000..b60fb0c54
--- /dev/null
+++ b/packages/plugins/headings/src/commands/index.ts
@@ -0,0 +1,65 @@
+import { Blocks, buildBlockData, generateId, YooEditor, YooptaBlockPath } from '@yoopta/editor';
+import { HeadingOneElement, HeadingThreeElement, HeadingTwoElement } from '../types';
+
+export type HeadingElementOptions = { text?: string };
+export type HeadingInsertOptions = HeadingElementOptions & { at: YooptaBlockPath; focus?: boolean };
+
+export type HeadingOneCommands = {
+ buildHeadingOneElements: (editor: YooEditor, options?: Partial) => HeadingOneElement;
+ insertHeadingOne: (editor: YooEditor, options?: Partial) => void;
+ deleteHeadingOne: (editor: YooEditor, blockId: string) => void;
+};
+
+export const HeadingOneCommands: HeadingOneCommands = {
+ buildHeadingOneElements: (editor, options) => {
+ return { id: generateId(), type: 'heading-one', children: [{ text: options?.text || '' }] };
+ },
+ insertHeadingOne: (editor, options = {}) => {
+ const { at, focus, text } = options;
+ const headingOne = HeadingOneCommands.buildHeadingOneElements(editor, { text });
+ Blocks.insertBlock(editor, buildBlockData({ value: [headingOne], type: 'HeadingOne' }), { at, focus });
+ },
+ deleteHeadingOne: (editor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+};
+
+export type HeadingTwoCommands = {
+ buildHeadingTwoElements: (editor: YooEditor, options?: Partial) => HeadingTwoElement;
+ insertHeadingTwo: (editor: YooEditor, options?: Partial) => void;
+ deleteHeadingTwo: (editor: YooEditor, blockId: string) => void;
+};
+
+export const HeadingTwoCommands: HeadingTwoCommands = {
+ buildHeadingTwoElements: (editor, options) => {
+ return { id: generateId(), type: 'heading-two', children: [{ text: options?.text || '' }] };
+ },
+ insertHeadingTwo: (editor, options = {}) => {
+ const { at, focus, text } = options;
+ const headingTwo = HeadingTwoCommands.buildHeadingTwoElements(editor, { text });
+ Blocks.insertBlock(editor, buildBlockData({ value: [headingTwo], type: 'HeadingTwo' }), { at, focus });
+ },
+ deleteHeadingTwo: (editor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+};
+
+export type HeadingThreeCommands = {
+ buildHeadingThreeElements: (editor: YooEditor, options?: Partial) => HeadingThreeElement;
+ insertHeadingThree: (editor: YooEditor, options?: Partial) => void;
+ deleteHeadingThree: (editor: YooEditor, blockId: string) => void;
+};
+
+export const HeadingThreeCommands: HeadingThreeCommands = {
+ buildHeadingThreeElements: (editor, options) => {
+ return { id: generateId(), type: 'heading-three', children: [{ text: options?.text || '' }] };
+ },
+ insertHeadingThree: (editor, options = {}) => {
+ const { at, focus, text } = options;
+ const headingThree = HeadingThreeCommands.buildHeadingThreeElements(editor, { text });
+ Blocks.insertBlock(editor, buildBlockData({ value: [headingThree], type: 'HeadingThree' }), { at, focus });
+ },
+ deleteHeadingThree: (editor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+};
diff --git a/packages/plugins/headings/src/index.ts b/packages/plugins/headings/src/index.ts
index 15d0b3516..55390076b 100644
--- a/packages/plugins/headings/src/index.ts
+++ b/packages/plugins/headings/src/index.ts
@@ -16,5 +16,7 @@ const Headings = {
HeadingThree,
};
+export { HeadingOneCommands, HeadingTwoCommands, HeadingThreeCommands } from './commands';
+
export default Headings;
export { HeadingOne, HeadingTwo, HeadingThree, HeadingOneElement, HeadingTwoElement, HeadingThreeElement };
diff --git a/packages/plugins/headings/src/plugin/HeadingOne.tsx b/packages/plugins/headings/src/plugin/HeadingOne.tsx
index 21c16ae18..97716dd2d 100644
--- a/packages/plugins/headings/src/plugin/HeadingOne.tsx
+++ b/packages/plugins/headings/src/plugin/HeadingOne.tsx
@@ -1,4 +1,5 @@
-import { YooptaPlugin, PluginElementRenderProps } from '@yoopta/editor';
+import { YooptaPlugin, PluginElementRenderProps, serializeTextNodesIntoMarkdown } from '@yoopta/editor';
+import { HeadingOneCommands } from '../commands';
const HeadingOneRender = ({ extendRender, ...props }: PluginElementRenderProps) => {
const { element, HTMLAttributes = {}, attributes, children } = props;
@@ -25,6 +26,7 @@ const HeadingOne = new YooptaPlugin({
},
},
},
+ commands: HeadingOneCommands,
options: {
display: {
title: 'Heading 1',
@@ -45,7 +47,7 @@ const HeadingOne = new YooptaPlugin({
},
markdown: {
serialize: (element, text) => {
- return `# ${text}\n`;
+ return `# ${serializeTextNodesIntoMarkdown(element.children)}\n`;
},
},
},
diff --git a/packages/plugins/headings/src/plugin/HeadingThree.tsx b/packages/plugins/headings/src/plugin/HeadingThree.tsx
index 41352d705..d693957d9 100644
--- a/packages/plugins/headings/src/plugin/HeadingThree.tsx
+++ b/packages/plugins/headings/src/plugin/HeadingThree.tsx
@@ -1,4 +1,5 @@
-import { PluginElementRenderProps, YooptaPlugin } from '@yoopta/editor';
+import { PluginElementRenderProps, serializeTextNodesIntoMarkdown, YooptaPlugin } from '@yoopta/editor';
+import { HeadingThreeCommands } from '../commands';
const HeadingThreeRender = ({ extendRender, ...props }: PluginElementRenderProps) => {
const { element, HTMLAttributes = {}, attributes, children } = props;
@@ -31,6 +32,7 @@ const HeadingThree = new YooptaPlugin({
},
},
},
+ commands: HeadingThreeCommands,
options: {
display: {
title: 'Heading 3',
@@ -51,7 +53,7 @@ const HeadingThree = new YooptaPlugin({
},
markdown: {
serialize: (element, text) => {
- return `### ${text}\n`;
+ return `### ${serializeTextNodesIntoMarkdown(element.children)}\n`;
},
},
},
diff --git a/packages/plugins/headings/src/plugin/HeadingTwo.tsx b/packages/plugins/headings/src/plugin/HeadingTwo.tsx
index 6427debe2..e9e41128b 100644
--- a/packages/plugins/headings/src/plugin/HeadingTwo.tsx
+++ b/packages/plugins/headings/src/plugin/HeadingTwo.tsx
@@ -1,4 +1,5 @@
-import { PluginElementRenderProps, YooptaPlugin } from '@yoopta/editor';
+import { PluginElementRenderProps, serializeTextNodesIntoMarkdown, YooptaPlugin } from '@yoopta/editor';
+import { HeadingTwoCommands } from '../commands';
const HeadingTwoRender = ({ extendRender, ...props }: PluginElementRenderProps) => {
const { element, HTMLAttributes = {}, attributes, children } = props;
@@ -25,6 +26,7 @@ const HeadingTwo = new YooptaPlugin({
},
},
},
+ commands: HeadingTwoCommands,
options: {
display: {
title: 'Heading 2',
@@ -45,7 +47,7 @@ const HeadingTwo = new YooptaPlugin({
},
markdown: {
serialize: (element, text) => {
- return `## ${text}\n`;
+ return `## ${serializeTextNodesIntoMarkdown(element.children)}\n`;
},
},
},
diff --git a/packages/plugins/image/package.json b/packages/plugins/image/package.json
index 9a1e68e7e..05d98d7da 100644
--- a/packages/plugins/image/package.json
+++ b/packages/plugins/image/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/image",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "Image plugin for Yoopta Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -39,5 +39,5 @@
"@radix-ui/react-icons": "^1.3.0",
"re-resizable": "^6.9.11"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/plugins/image/src/commands/index.ts b/packages/plugins/image/src/commands/index.ts
new file mode 100644
index 000000000..9651513a9
--- /dev/null
+++ b/packages/plugins/image/src/commands/index.ts
@@ -0,0 +1,36 @@
+import { Blocks, buildBlockData, Elements, generateId, YooEditor, YooptaBlockPath } from '@yoopta/editor';
+import { ImageElement, ImageElementProps } from '../types';
+
+type ImageElementOptions = {
+ props?: Omit;
+};
+
+type InsertImageOptions = ImageElementOptions & {
+ at?: YooptaBlockPath;
+ focus?: boolean;
+};
+
+export type ImageCommands = {
+ buildImageElements: (editor: YooEditor, options?: Partial) => ImageElement;
+ insertImage: (editor: YooEditor, options?: Partial) => void;
+ deleteImage: (editor: YooEditor, blockId: string) => void;
+ updateImage: (editor: YooEditor, blockId: string, props: Partial) => void;
+};
+
+export const ImageCommands: ImageCommands = {
+ buildImageElements: (editor: YooEditor, options = {}) => {
+ const imageProps = options?.props ? { ...options.props, nodeType: 'void' } : { nodeType: 'void' };
+ return { id: generateId(), type: 'image', children: [{ text: '', props: imageProps }] };
+ },
+ insertImage: (editor: YooEditor, options = {}) => {
+ const { at, focus, props } = options;
+ const image = ImageCommands.buildImageElements(editor, { props });
+ Blocks.insertBlock(editor, buildBlockData({ value: [image], type: 'Image' }), { focus, at });
+ },
+ deleteImage: (editor: YooEditor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+ updateImage: (editor: YooEditor, blockId, props) => {
+ Elements.updateElement(editor, blockId, { props });
+ },
+};
diff --git a/packages/plugins/image/src/index.ts b/packages/plugins/image/src/index.ts
index 3f06f6114..e4c5ea1d3 100644
--- a/packages/plugins/image/src/index.ts
+++ b/packages/plugins/image/src/index.ts
@@ -8,5 +8,7 @@ declare module 'slate' {
}
}
+export { ImageCommands } from './commands';
+
export default Image;
export { ImageElement, ImageElementProps, ImageUploadResponse };
diff --git a/packages/plugins/image/src/plugin/index.tsx b/packages/plugins/image/src/plugin/index.tsx
index 178c50237..3c0371652 100644
--- a/packages/plugins/image/src/plugin/index.tsx
+++ b/packages/plugins/image/src/plugin/index.tsx
@@ -1,5 +1,6 @@
import { generateId, SlateElement, YooptaPlugin } from '@yoopta/editor';
-import { ImageElementProps, ImagePluginElements, ImagePluginOptions } from '../types';
+import { ImageCommands } from '../commands';
+import { ImageElementMap, ImageElementProps, ImagePluginElements, ImagePluginOptions } from '../types';
import { ImageRender } from '../ui/Image';
const ALIGNS_TO_JUSTIFY = {
@@ -9,7 +10,7 @@ const ALIGNS_TO_JUSTIFY = {
};
// [TODO] - caption element??,
-const Image = new YooptaPlugin({
+const Image = new YooptaPlugin({
type: 'Image',
elements: {
image: {
@@ -25,6 +26,7 @@ const Image = new YooptaPlugin",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -37,5 +37,5 @@
"dependencies": {
"lucide-react": "^0.379.0"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/plugins/link/src/commands/index.ts b/packages/plugins/link/src/commands/index.ts
new file mode 100644
index 000000000..a450b0502
--- /dev/null
+++ b/packages/plugins/link/src/commands/index.ts
@@ -0,0 +1,104 @@
+import { generateId, SlateEditor, YooEditor } from '@yoopta/editor';
+import { Editor, Element, Location, Span, Transforms } from 'slate';
+import { LinkElement, LinkElementProps } from '../types';
+
+type LinkElementOptions = {
+ props: Omit;
+};
+
+type LinkInsertOptions = LinkElementOptions & {
+ selection: Location | undefined;
+ slate: SlateEditor;
+};
+
+type DeleteElementOptions = {
+ slate: SlateEditor;
+};
+
+export type LinkCommands = {
+ buildLinkElements: (editor: YooEditor, options: LinkElementOptions) => LinkElement;
+ insertLink: (editor: YooEditor, options: LinkInsertOptions) => void;
+ deleteLink: (editor: YooEditor, options: DeleteElementOptions) => void;
+};
+
+export const LinkCommands: LinkCommands = {
+ buildLinkElements: (editor, options) => {
+ const { props } = options || {};
+ const linkProps: LinkElementProps = { ...props, nodeType: 'inline' };
+ return {
+ id: generateId(),
+ type: 'link',
+ children: [{ text: props?.title || props?.url || '' }],
+ props: linkProps,
+ } as LinkElement;
+ },
+ insertLink: (editor, options) => {
+ let { props, slate } = options || {};
+
+ if (!slate || !slate.selection) return;
+
+ const textInSelection = Editor.string(slate, slate.selection);
+
+ const linkProps = {
+ ...props,
+ title: props.title || textInSelection || props.url || '',
+ nodeType: 'inline',
+ } as LinkElementProps;
+
+ const linkElement = LinkCommands.buildLinkElements(editor, { props });
+
+ const [linkNodeEntry] = Editor.nodes(slate, {
+ at: slate.selection,
+ match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === 'link',
+ });
+
+ if (linkNodeEntry) {
+ const [link, path] = linkNodeEntry;
+
+ Transforms.setNodes(
+ slate,
+ { props: { ...link?.props, ...linkProps, nodeType: 'inline' } },
+ {
+ match: (n) => Element.isElement(n) && n.type === 'link',
+ at: path,
+ },
+ );
+
+ Editor.insertText(slate, linkProps.title || linkProps.url || '', { at: slate.selection });
+ Transforms.collapse(slate, { edge: 'end' });
+ return;
+ }
+
+ Transforms.wrapNodes(slate, linkElement, { split: true, at: slate.selection });
+ Transforms.setNodes(
+ slate,
+ { text: props?.title || props?.url || '' },
+ {
+ at: slate.selection,
+ mode: 'lowest',
+ match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === 'link',
+ },
+ );
+
+ Editor.insertText(slate, props?.title || props?.url || '', { at: slate.selection });
+ Transforms.collapse(slate, { edge: 'end' });
+ },
+ deleteLink: (editor, options) => {
+ try {
+ const { slate } = options;
+ if (!slate || !slate.selection) return;
+
+ const [linkNodeEntry] = Editor.nodes(slate, {
+ at: slate.selection,
+ match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === 'link',
+ });
+
+ if (linkNodeEntry) {
+ Transforms.unwrapNodes(slate, {
+ match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === 'link',
+ at: slate.selection,
+ });
+ }
+ } catch (error) {}
+ },
+};
diff --git a/packages/plugins/link/src/index.ts b/packages/plugins/link/src/index.ts
index acfc857e0..e08b70356 100644
--- a/packages/plugins/link/src/index.ts
+++ b/packages/plugins/link/src/index.ts
@@ -8,6 +8,8 @@ declare module 'slate' {
}
}
+export { LinkCommands } from './commands';
+
export { LinkElement, LinkElementProps };
export default Link;
diff --git a/packages/plugins/link/src/plugin/index.tsx b/packages/plugins/link/src/plugin/index.tsx
index 98f9b3b1c..ea7664e00 100644
--- a/packages/plugins/link/src/plugin/index.tsx
+++ b/packages/plugins/link/src/plugin/index.tsx
@@ -1,8 +1,9 @@
-import { generateId, YooptaPlugin } from '@yoopta/editor';
-import { LinkElementProps, LinkPluginElementKeys } from '../types';
+import { deserializeTextNodes, generateId, serializeTextNodes, YooptaPlugin } from '@yoopta/editor';
+import { LinkCommands } from '../commands';
+import { LinkElementMap, LinkElementProps } from '../types';
import { LinkRender } from '../ui/LinkRender';
-const Link = new YooptaPlugin({
+const Link = new YooptaPlugin({
type: 'LinkPlugin',
elements: {
link: {
@@ -22,11 +23,12 @@ const Link = new YooptaPlugin({
description: 'Create link',
},
},
+ commands: LinkCommands,
parsers: {
html: {
serialize: (element, text) => {
const { url, target, rel, title } = element.props;
- return `${title || text}`;
+ return `${serializeTextNodes(element.children)}`;
},
deserialize: {
nodeNames: ['A'],
@@ -52,7 +54,7 @@ const Link = new YooptaPlugin({
id: generateId(),
type: 'link',
props,
- children: [{ text: title }],
+ children: deserializeTextNodes(editor, el.childNodes),
};
}
},
diff --git a/packages/plugins/link/src/types.ts b/packages/plugins/link/src/types.ts
index 1362a394a..35ccd9217 100644
--- a/packages/plugins/link/src/types.ts
+++ b/packages/plugins/link/src/types.ts
@@ -9,3 +9,7 @@ export type LinkElementProps = {
title?: string | null;
};
export type LinkElement = SlateElement<'link', LinkElementProps>;
+
+export type LinkElementMap = {
+ link: LinkElement;
+};
diff --git a/packages/plugins/lists/package.json b/packages/plugins/lists/package.json
index 3b57a3393..515800faa 100644
--- a/packages/plugins/lists/package.json
+++ b/packages/plugins/lists/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/lists",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "Lists plugin for Yoopta Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -34,5 +34,5 @@
"bugs": {
"url": "https://github.com/Darginec05/Editor-Yoopta/issues"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/plugins/lists/src/commands/index.ts b/packages/plugins/lists/src/commands/index.ts
new file mode 100644
index 000000000..94febc29a
--- /dev/null
+++ b/packages/plugins/lists/src/commands/index.ts
@@ -0,0 +1,77 @@
+import { Blocks, buildBlockData, Elements, generateId, YooEditor, YooptaBlockPath } from '@yoopta/editor';
+import { BulletedListElement, TodoListElement, NumberedListElement, TodoListElementProps } from '../types';
+
+export type ListElementOptions = { text?: string };
+export type ListInsertOptions = ListElementOptions & { at: YooptaBlockPath; focus?: boolean };
+
+export type TodoListElementOptions = ListElementOptions & { props?: TodoListElementProps };
+export type TodoListInsertOptions = TodoListElementOptions & { at: YooptaBlockPath; focus?: boolean };
+
+// BulletedList
+export type BulletedListCommands = {
+ buildBulletedListElements: (editor: YooEditor, options?: Partial) => BulletedListElement;
+ insertBulletedList: (editor: YooEditor, options?: Partial) => void;
+ deleteBulletedList: (editor: YooEditor, blockId: string) => void;
+};
+
+export const BulletedListCommands: BulletedListCommands = {
+ buildBulletedListElements: (editor, options) => {
+ return { id: generateId(), type: 'bulleted-list', children: [{ text: options?.text || '' }] };
+ },
+ insertBulletedList: (editor, options = {}) => {
+ const { at, focus, text } = options;
+ const bulletList = BulletedListCommands.buildBulletedListElements(editor, { text });
+ Blocks.insertBlock(editor, buildBlockData({ value: [bulletList], type: 'BulletedList' }), { at, focus });
+ },
+ deleteBulletedList: (editor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+};
+
+// NumberedList
+export type NumberedListCommands = {
+ buildNumberedListElements: (editor: YooEditor, options?: Partial) => NumberedListElement;
+ insertNumberedList: (editor: YooEditor, options?: Partial) => void;
+ deleteNumberedList: (editor: YooEditor, blockId: string) => void;
+};
+
+export const NumberedListCommands: NumberedListCommands = {
+ buildNumberedListElements: (editor, options) => {
+ return { id: generateId(), type: 'numbered-list', children: [{ text: options?.text || '' }] };
+ },
+ insertNumberedList: (editor, options = {}) => {
+ const { at, focus, text } = options;
+ const numberdedList = NumberedListCommands.buildNumberedListElements(editor, { text });
+ Blocks.insertBlock(editor, buildBlockData({ value: [numberdedList], type: 'NumberedList' }), { at, focus });
+ },
+ deleteNumberedList: (editor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+};
+
+// TodoList
+export type TodoListCommands = {
+ buildTodoListElements: (editor: YooEditor, options?: Partial) => TodoListElement;
+ insertTodoList: (editor: YooEditor, options?: Partial) => void;
+ deleteTodoList: (editor: YooEditor, blockId: string) => void;
+ updateTodoList: (editor: YooEditor, blockId: string, props: Partial) => void;
+};
+
+export const TodoListCommands: TodoListCommands = {
+ buildTodoListElements: (editor, options) => {
+ return { id: generateId(), type: 'todo-list', children: [{ text: options?.text || '' }] };
+ },
+ insertTodoList: (editor, options = {}) => {
+ const { at, focus, text, props } = options;
+ const todoList = TodoListCommands.buildTodoListElements(editor, { text, props });
+ Blocks.insertBlock(editor, buildBlockData({ value: [todoList], type: 'TodoList' }), { at, focus });
+ },
+ deleteTodoList: (editor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+ updateTodoList: (editor, blockId, props) => {
+ if (typeof props?.checked === 'boolean') {
+ Elements.updateElement(editor, blockId, { type: 'todo-list', props: { checked: props?.checked } });
+ }
+ },
+};
diff --git a/packages/plugins/lists/src/elements/NumberedList.tsx b/packages/plugins/lists/src/elements/NumberedList.tsx
index da3286613..b8972651d 100644
--- a/packages/plugins/lists/src/elements/NumberedList.tsx
+++ b/packages/plugins/lists/src/elements/NumberedList.tsx
@@ -45,6 +45,7 @@ const NumberedListRender = ({ extendRender, ...props }: PluginElementRenderProps
const alignClass = `yoopta-align-${currentAlign}`;
if (extendRender) {
+ // @ts-ignore [FIXME] - add generic type for extendRender props
return extendRender({ ...props, count });
}
diff --git a/packages/plugins/lists/src/index.ts b/packages/plugins/lists/src/index.ts
index 389353521..b7fbb56f7 100644
--- a/packages/plugins/lists/src/index.ts
+++ b/packages/plugins/lists/src/index.ts
@@ -14,6 +14,8 @@ const NumberedList = LISTS.NumberedList;
const BulletedList = LISTS.BulletedList;
const TodoList = LISTS.TodoList;
+export { TodoListCommands, BulletedListCommands, NumberedListCommands } from './commands';
+
export {
NumberedListElement,
BulletedListElement,
diff --git a/packages/plugins/lists/src/plugin/BulletedList.tsx b/packages/plugins/lists/src/plugin/BulletedList.tsx
index 5eb1e673b..170928bfb 100644
--- a/packages/plugins/lists/src/plugin/BulletedList.tsx
+++ b/packages/plugins/lists/src/plugin/BulletedList.tsx
@@ -1,10 +1,18 @@
-import { buildBlockData, generateId, YooptaBlockData, YooptaPlugin } from '@yoopta/editor';
-import { Element, Transforms } from 'slate';
+import {
+ buildBlockData,
+ deserializeTextNodes,
+ generateId,
+ serializeTextNodes,
+ serializeTextNodesIntoMarkdown,
+ YooptaBlockData,
+ YooptaPlugin,
+} from '@yoopta/editor';
+import { BulletedListCommands } from '../commands';
import { BulletedListRender } from '../elements/BulletedList';
import { onKeyDown } from '../events/onKeyDown';
-import { BulletedListElement, BulletedListPluginKeys } from '../types';
+import { ListElementMap } from '../types';
-const BulletedList = new YooptaPlugin({
+const BulletedList = new YooptaPlugin>({
type: 'BulletedList',
elements: {
'bulleted-list': {
@@ -21,11 +29,12 @@ const BulletedList = new YooptaPlugin {
- const textContent = listItem.textContent || '';
-
return buildBlockData({
id: generateId(),
type: 'BulletedList',
@@ -49,7 +56,7 @@ const BulletedList = new YooptaPlugin {
const { align = 'left', depth = 0 } = blockMeta || {};
- return ``;
+ return `- ${serializeTextNodes(
+ element.children,
+ )}
`;
},
},
markdown: {
serialize: (element, text) => {
- return `- ${text}`;
+ return `- ${serializeTextNodesIntoMarkdown(element.children)}`;
},
},
},
diff --git a/packages/plugins/lists/src/plugin/NumberedList.tsx b/packages/plugins/lists/src/plugin/NumberedList.tsx
index b870c5c96..f2822565b 100644
--- a/packages/plugins/lists/src/plugin/NumberedList.tsx
+++ b/packages/plugins/lists/src/plugin/NumberedList.tsx
@@ -1,9 +1,18 @@
-import { YooptaPlugin, buildBlockData, YooptaBlockData, generateId } from '@yoopta/editor';
+import {
+ YooptaPlugin,
+ buildBlockData,
+ YooptaBlockData,
+ generateId,
+ deserializeTextNodes,
+ serializeTextNodes,
+ serializeTextNodesIntoMarkdown,
+} from '@yoopta/editor';
+import { NumberedListCommands } from '../commands';
import { NumberedListRender } from '../elements/NumberedList';
import { onKeyDown } from '../events/onKeyDown';
-import { ListElementProps } from '../types';
+import { ListElementMap } from '../types';
-const NumberedList = new YooptaPlugin<'numbered-list', ListElementProps>({
+const NumberedList = new YooptaPlugin>({
type: 'NumberedList',
elements: {
'numbered-list': {
@@ -23,11 +32,12 @@ const NumberedList = new YooptaPlugin<'numbered-list', ListElementProps>({
events: {
onKeyDown,
},
+ commands: NumberedListCommands,
parsers: {
html: {
deserialize: {
nodeNames: ['OL'],
- parse(el) {
+ parse(el, editor) {
if (el.nodeName === 'OL') {
const listItems = el.querySelectorAll('li');
@@ -42,8 +52,6 @@ const NumberedList = new YooptaPlugin<'numbered-list', ListElementProps>({
return !isTodoListItem;
})
.map((listItem, i) => {
- const textContent = listItem.textContent || '';
-
return buildBlockData({
id: generateId(),
type: 'NumberedList',
@@ -51,13 +59,14 @@ const NumberedList = new YooptaPlugin<'numbered-list', ListElementProps>({
{
id: generateId(),
type: 'numbered-list',
- children: [{ text: textContent }],
+ children: deserializeTextNodes(editor, listItem.childNodes),
props: { nodeType: 'block' },
},
],
meta: { order: 0, depth, align },
});
});
+
if (numberedListBlocks.length > 0) return numberedListBlocks;
}
},
@@ -65,12 +74,14 @@ const NumberedList = new YooptaPlugin<'numbered-list', ListElementProps>({
serialize: (element, text, blockMeta) => {
const { align = 'left', depth = 0 } = blockMeta || {};
- return `- ${text}
`;
+ return `- ${serializeTextNodes(
+ element.children,
+ )}
`;
},
},
markdown: {
serialize: (element, text) => {
- return `- ${text}`;
+ return `- ${serializeTextNodesIntoMarkdown(element.children)}`;
},
},
},
diff --git a/packages/plugins/lists/src/plugin/TodoList.tsx b/packages/plugins/lists/src/plugin/TodoList.tsx
index c237496d5..bba314839 100644
--- a/packages/plugins/lists/src/plugin/TodoList.tsx
+++ b/packages/plugins/lists/src/plugin/TodoList.tsx
@@ -1,9 +1,18 @@
-import { buildBlockData, generateId, YooptaBlockData, YooptaPlugin } from '@yoopta/editor';
+import {
+ buildBlockData,
+ deserializeTextNodes,
+ generateId,
+ serializeTextNodes,
+ serializeTextNodesIntoMarkdown,
+ YooptaBlockData,
+ YooptaPlugin,
+} from '@yoopta/editor';
+import { TodoListCommands } from '../commands';
import { TodoListRender } from '../elements/TodoList';
import { onKeyDown } from '../events/onKeyDown';
-import { TodoListElementProps, TodoListPluginKeys } from '../types';
+import { ListElementMap } from '../types';
-const TodoList = new YooptaPlugin({
+const TodoList = new YooptaPlugin>({
type: 'TodoList',
elements: {
'todo-list': {
@@ -23,13 +32,12 @@ const TodoList = new YooptaPlugin({
events: {
onKeyDown,
},
+ commands: TodoListCommands,
parsers: {
html: {
deserialize: {
- // add ignore or continue statement
nodeNames: ['OL', 'UL'],
- // nodeNames: [],
- parse(el) {
+ parse(el, editor) {
if (el.nodeName === 'OL' || el.nodeName === 'UL') {
const listItems = el.querySelectorAll('li');
@@ -46,7 +54,6 @@ const TodoList = new YooptaPlugin({
.map((listItem) => {
const textContent = listItem.textContent || '';
const checked = /\[\s*x\s*\]/i.test(textContent);
- const clearedContent = textContent.replace(/\[\s*\S?\s*\]/, '').trim();
return buildBlockData({
id: generateId(),
@@ -55,7 +62,7 @@ const TodoList = new YooptaPlugin({
{
id: generateId(),
type: 'todo-list',
- children: [{ text: clearedContent }],
+ children: deserializeTextNodes(editor, listItem.childNodes),
props: { nodeType: 'block', checked: checked },
},
],
@@ -72,12 +79,12 @@ const TodoList = new YooptaPlugin({
return `- [${
element.props.checked ? 'x' : ' '
- }] ${text}
`;
+ }] ${serializeTextNodes(element.children)}`;
},
},
markdown: {
serialize: (element, text) => {
- return `- ${element.props.checked ? '[x]' : '[ ]'} ${text}`;
+ return `- ${element.props.checked ? '[x]' : '[ ]'} ${serializeTextNodesIntoMarkdown(element.children)}`;
},
},
},
diff --git a/packages/plugins/lists/src/types.ts b/packages/plugins/lists/src/types.ts
index 03a796291..f4f23727a 100644
--- a/packages/plugins/lists/src/types.ts
+++ b/packages/plugins/lists/src/types.ts
@@ -13,3 +13,9 @@ export type TodoListElement = SlateElement<'todo-list', TodoListElementProps>;
export type TodoListElementProps = {
checked: boolean;
};
+
+export type ListElementMap = {
+ 'bulleted-list': BulletedListElement;
+ 'numbered-list': NumberedListElement;
+ 'todo-list': TodoListElement;
+};
diff --git a/packages/plugins/paragraph/package.json b/packages/plugins/paragraph/package.json
index d43461b4d..4b706b27b 100644
--- a/packages/plugins/paragraph/package.json
+++ b/packages/plugins/paragraph/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/paragraph",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "Paragraph plugin for Yoopta Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -34,5 +34,5 @@
"bugs": {
"url": "https://github.com/Darginec05/Editor-Yoopta/issues"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/plugins/paragraph/src/commands/index.ts b/packages/plugins/paragraph/src/commands/index.ts
new file mode 100644
index 000000000..8c043e91b
--- /dev/null
+++ b/packages/plugins/paragraph/src/commands/index.ts
@@ -0,0 +1,32 @@
+import { Blocks, buildBlockData, generateId, YooEditor, YooptaBlockPath } from '@yoopta/editor';
+import { ParagraphElement } from '../types';
+
+type ParagraphElementOptions = {
+ text?: string;
+};
+
+type ParagraphInsertOptions = ParagraphElementOptions & {
+ at?: YooptaBlockPath;
+ focus?: boolean;
+};
+
+export type ParagraphCommands = {
+ buildParagraphElements: (editor: YooEditor, options?: Partial) => ParagraphElement;
+ insertParagraph: (editor: YooEditor, options?: Partial) => void;
+ deleteParagraph: (editor: YooEditor, blockId: string) => void;
+};
+
+export const ParagraphCommands: ParagraphCommands = {
+ buildParagraphElements: (editor, options = {}) => {
+ return { id: generateId(), type: 'paragraph', children: [{ text: options?.text || '' }] };
+ },
+ insertParagraph: (editor, options = {}) => {
+ const { at, focus, text } = options;
+
+ const paragraphElement = ParagraphCommands.buildParagraphElements(editor, { text });
+ Blocks.insertBlock(editor, buildBlockData({ value: [paragraphElement], type: 'Paragraph' }), { at, focus });
+ },
+ deleteParagraph: (editor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+};
diff --git a/packages/plugins/paragraph/src/index.ts b/packages/plugins/paragraph/src/index.ts
index 08c9bf83b..6a0924a8a 100644
--- a/packages/plugins/paragraph/src/index.ts
+++ b/packages/plugins/paragraph/src/index.ts
@@ -1,5 +1,6 @@
import { ParagraphElement } from './types';
import { Paragraph } from './plugin';
+export { ParagraphCommands } from './commands';
import './styles.css';
declare module 'slate' {
diff --git a/packages/plugins/paragraph/src/plugin/index.tsx b/packages/plugins/paragraph/src/plugin/index.tsx
index bda559b06..735aa8523 100644
--- a/packages/plugins/paragraph/src/plugin/index.tsx
+++ b/packages/plugins/paragraph/src/plugin/index.tsx
@@ -1,8 +1,10 @@
-import { YooptaPlugin } from '@yoopta/editor';
+import { serializeTextNodes, serializeTextNodesIntoMarkdown, YooptaPlugin } from '@yoopta/editor';
import { Element, Transforms } from 'slate';
+import { ParagraphCommands } from '../commands';
+import { ParagraphElement, ParagraphElementMap } from '../types';
import { ParagraphRender } from '../ui/Paragraph';
-const Paragraph = new YooptaPlugin({
+const Paragraph = new YooptaPlugin({
type: 'Paragraph',
elements: {
paragraph: {
@@ -23,15 +25,18 @@ const Paragraph = new YooptaPlugin({
},
serialize: (element, text, blockMeta) => {
const { align = 'left', depth = 0 } = blockMeta || {};
- return `${text}
`;
+ return `${serializeTextNodes(
+ element.children,
+ )}
`;
},
},
markdown: {
serialize: (element, text) => {
- return `${text}\n`;
+ return `${serializeTextNodesIntoMarkdown(element.children)}\n`;
},
},
},
+ commands: ParagraphCommands,
});
export { Paragraph };
diff --git a/packages/plugins/paragraph/src/types.ts b/packages/plugins/paragraph/src/types.ts
index c447c6742..82a646247 100644
--- a/packages/plugins/paragraph/src/types.ts
+++ b/packages/plugins/paragraph/src/types.ts
@@ -2,3 +2,6 @@ import { SlateElement } from '@yoopta/editor';
// [TODO] - Define the type of the paragraph element
export type ParagraphElement = SlateElement<'paragraph'>;
+export type ParagraphElementMap = {
+ paragraph: ParagraphElement;
+};
diff --git a/packages/plugins/table/package.json b/packages/plugins/table/package.json
index d3e6be4fe..a9d32c978 100644
--- a/packages/plugins/table/package.json
+++ b/packages/plugins/table/package.json
@@ -1,11 +1,11 @@
{
"name": "@yoopta/table",
- "version": "2.0.0",
+ "version": "4.8.0",
"description": "Table plugin for Yoopta Editor [IN PROGRESS]",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
"license": "MIT",
- "private": true,
+ "private": false,
"main": "dist/index.js",
"type": "module",
"module": "dist/index.js",
@@ -27,9 +27,16 @@
},
"scripts": {
"test": "node ./__tests__/yoopta-table.test.js",
- "start": "rollup --config rollup.config.js --watch --bundleConfigAsCjs --environment NODE_ENV:development"
+ "start": "rollup --config rollup.config.js --watch --bundleConfigAsCjs --environment NODE_ENV:development",
+ "prepublishOnly": "yarn build",
+ "build": "rollup --config rollup.config.js --bundleConfigAsCjs --environment NODE_ENV:production"
},
"bugs": {
"url": "https://github.com/Darginec05/Editor-Yoopta/issues"
- }
+ },
+ "dependencies": {
+ "@floating-ui/react": "^0.26.22",
+ "lucide-react": "^0.436.0"
+ },
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/plugins/table/src/commands/index.ts b/packages/plugins/table/src/commands/index.ts
new file mode 100644
index 000000000..51d1e1427
--- /dev/null
+++ b/packages/plugins/table/src/commands/index.ts
@@ -0,0 +1,366 @@
+import { Blocks, buildBlockData, Elements, generateId, SlateElement, YooEditor, YooptaBlockPath } from '@yoopta/editor';
+import { Editor, Element, Path, Span, Transforms } from 'slate';
+import { InsertTableOptions, TableCellElement, TableElement, TableRowElement } from '../types';
+
+type Options = {
+ path?: Location | Span;
+ select?: boolean;
+ insertMode?: 'before' | 'after';
+};
+
+type DeleteOptions = Omit;
+
+type MoveTableOptions = {
+ from: Path;
+ to: Path;
+};
+
+type InsertOptions = Partial<
+ InsertTableOptions & {
+ at: YooptaBlockPath;
+ }
+>;
+
+export type TableCommands = {
+ buildTableElements: (editor: YooEditor, options?: InsertOptions) => TableElement;
+ insertTable: (editor: YooEditor, options?: InsertOptions) => void;
+ deleteTable: (editor: YooEditor, blockId: string) => void;
+ insertTableRow: (editor: YooEditor, blockId: string, options?: Options) => void;
+ deleteTableRow: (editor: YooEditor, blockId: string, options?: DeleteOptions) => void;
+ moveTableRow: (editor: YooEditor, blockId: string, options: MoveTableOptions) => void;
+ moveTableColumn: (editor: YooEditor, blockId: string, options: MoveTableOptions) => void;
+ insertTableColumn: (editor: YooEditor, blockId: string, options?: Options) => void;
+ deleteTableColumn: (editor: YooEditor, blockId: string, options?: DeleteOptions) => void;
+ updateColumnWidth: (editor: YooEditor, blockId: string, columnIndex: number, width: number) => void;
+ toggleHeaderRow: (editor: YooEditor, blockId: string) => void;
+ toggleHeaderColumn: (editor: YooEditor, blockId: string) => void;
+};
+
+export const TableCommands: TableCommands = {
+ buildTableElements: (editor: YooEditor, options?: InsertOptions) => {
+ const { rows = 3, columns = 3, columnWidth = 200, headerColumn = false, headerRow = false } = options || {};
+
+ const table: TableElement = {
+ id: generateId(),
+ type: 'table',
+ children: [],
+ props: {
+ headerColumn: headerColumn,
+ headerRow: headerRow,
+ },
+ };
+
+ for (let i = 0; i < rows; i++) {
+ const row: TableRowElement = {
+ id: generateId(),
+ type: 'table-row',
+ children: [],
+ };
+
+ for (let j = 0; j < columns; j++) {
+ const cell: TableCellElement = {
+ id: generateId(),
+ type: 'table-data-cell',
+ children: [{ text: '' }],
+ props: {
+ width: columnWidth || 200,
+ asHeader: i === 0 ? !!headerRow : false,
+ },
+ };
+
+ row.children.push(cell);
+ }
+
+ table.children.push(row);
+ }
+
+ return table;
+ },
+ insertTable: (editor: YooEditor, options?: InsertOptions) => {
+ const table = TableCommands.buildTableElements(editor, options);
+ Blocks.insertBlock(editor, buildBlockData({ value: [table], type: 'Table' }), options);
+ },
+ deleteTable: (editor: YooEditor, blockId: string) => {
+ editor.deleteBlock({ blockId });
+ },
+ insertTableRow: (editor: YooEditor, blockId: string, options?: Options) => {
+ const slate = Blocks.getSlate(editor, { id: blockId });
+ if (!slate) return;
+
+ Editor.withoutNormalizing(slate, () => {
+ const { insertMode = 'after', path = slate.selection, select = true } = options || {};
+
+ const currentRowElementEntryByPath = Elements.getElementEntry(editor, blockId, {
+ // @ts-ignore [FIXME] - Fix this
+ path,
+ type: 'table-row',
+ });
+
+ if (!currentRowElementEntryByPath) return;
+
+ const [currentRowElement, currentRowPath] = currentRowElementEntryByPath;
+ const insertPath = insertMode === 'before' ? currentRowPath : Path.next(currentRowPath);
+
+ const newRow: SlateElement = {
+ id: generateId(),
+ type: 'table-row',
+ children: currentRowElement.children.map((cell) => {
+ return {
+ id: generateId(),
+ type: 'table-data-cell',
+ children: [{ text: '' }],
+ };
+ }),
+ props: {
+ nodeType: 'block',
+ },
+ };
+
+ Transforms.insertNodes(slate, newRow, { at: insertPath });
+ if (select) {
+ Transforms.select(slate, [...insertPath, 0]);
+ }
+ });
+ },
+ deleteTableRow: (editor: YooEditor, blockId: string, options?: DeleteOptions) => {
+ const slate = Blocks.getSlate(editor, { id: blockId });
+ if (!slate) return;
+
+ Editor.withoutNormalizing(slate, () => {
+ const { path = slate.selection } = options || {};
+
+ const currentRowElementEntryByPath = Elements.getElementEntry(editor, blockId, {
+ // @ts-ignore [FIXME] - Fix this
+ path,
+ type: 'table-row',
+ });
+
+ if (!currentRowElementEntryByPath) return;
+
+ const [_, currentRowPath] = currentRowElementEntryByPath;
+
+ const tableRowEntries = Editor.nodes(slate, {
+ at: [0],
+ match: (n) => Element.isElement(n) && n.type === 'table-row',
+ mode: 'highest',
+ });
+
+ const tableRows = Array.from(tableRowEntries);
+ if (tableRows.length === 1) return;
+
+ Transforms.removeNodes(slate, {
+ at: currentRowPath,
+ match: (n) => Element.isElement(n) && n.type === 'table-row',
+ });
+ });
+ },
+ moveTableRow: (editor: YooEditor, blockId: string, { from, to }: MoveTableOptions) => {
+ const slate = Blocks.getSlate(editor, { id: blockId });
+ if (!slate) return;
+
+ Editor.withoutNormalizing(slate, () => {
+ Transforms.moveNodes(slate, {
+ at: from,
+ to: to,
+ match: (n) => Element.isElement(n) && n.type === 'table-row',
+ });
+ });
+ },
+ moveTableColumn: (editor: YooEditor, blockId: string, { from, to }: MoveTableOptions) => {
+ const slate = Blocks.getSlate(editor, { id: blockId });
+ if (!slate) return;
+
+ Editor.withoutNormalizing(slate, () => {
+ const tableRowEntries = Editor.nodes(slate, {
+ at: [0],
+ match: (n) => Element.isElement(n) && n.type === 'table-row',
+ mode: 'all',
+ });
+
+ Array.from(tableRowEntries).forEach(([tableRowElement, tableRowPath]) => {
+ Transforms.moveNodes(slate, {
+ at: tableRowPath.concat(from[from.length - 1]),
+ to: [...tableRowPath, to[to.length - 1]],
+ match: (n) => Element.isElement(n),
+ });
+ });
+ });
+ },
+ insertTableColumn: (editor: YooEditor, blockId: string, options?: Options) => {
+ const slate = Blocks.getSlate(editor, { id: blockId });
+ if (!slate) return;
+
+ Editor.withoutNormalizing(slate, () => {
+ const { insertMode = 'after', path = slate.selection, select = true } = options || {};
+
+ const dataCellElementEntryByPath = Elements.getElementEntry(editor, blockId, {
+ // @ts-ignore [FIXME] - Fix this
+ path,
+ type: 'table-data-cell',
+ });
+
+ if (!dataCellElementEntryByPath) return;
+
+ const [_, dataCellPath] = dataCellElementEntryByPath;
+ const columnIndex = dataCellPath[dataCellPath.length - 1];
+ const columnInsertIndex =
+ insertMode === 'before' ? columnIndex : Path.next(dataCellPath)[dataCellPath.length - 1];
+
+ const elementEntries = Editor.nodes(slate, {
+ at: [0],
+ match: (n) => Element.isElement(n) && n.type === 'table-row',
+ mode: 'lowest',
+ });
+
+ for (const [, tableRowPath] of elementEntries) {
+ const newDataCell: TableCellElement = {
+ id: generateId(),
+ type: 'table-data-cell',
+ children: [{ text: '' }],
+ };
+
+ Transforms.insertNodes(slate, newDataCell, { at: [...tableRowPath, columnInsertIndex] });
+ }
+
+ if (select) {
+ Transforms.select(slate, [0, 0, columnInsertIndex, 0]);
+ }
+ });
+ },
+ deleteTableColumn: (editor: YooEditor, blockId: string, options?: DeleteOptions) => {
+ const slate = Blocks.getSlate(editor, { id: blockId });
+ if (!slate) return;
+
+ Editor.withoutNormalizing(slate, () => {
+ const { path = slate.selection } = options || {};
+
+ const tableRowEntries = Editor.nodes(slate, {
+ at: [0],
+ match: (n) => Element.isElement(n) && n.type === 'table-row',
+ mode: 'all',
+ });
+
+ const rows = Array.from(tableRowEntries);
+ if (rows[0][0].children.length <= 1) return;
+
+ const dataCellElementEntryByPath = Elements.getElementEntry(editor, blockId, {
+ // @ts-ignore [FIXME] - Fix this
+ path,
+ type: 'table-data-cell',
+ });
+
+ if (!dataCellElementEntryByPath) return;
+
+ const [_, dataCellPath] = dataCellElementEntryByPath;
+ const columnIndex = dataCellPath[dataCellPath.length - 1];
+
+ const dataCellPaths = rows.map(([row, path]) => {
+ return row.children[columnIndex] ? [...path, columnIndex] : null;
+ });
+
+ // [TODO] - Check if there are other columns
+ dataCellPaths.forEach((path) => {
+ if (path) {
+ Transforms.removeNodes(slate, { at: path });
+ }
+ });
+ });
+ },
+ updateColumnWidth: (editor: YooEditor, blockId: string, columnIndex: number, width: number) => {
+ const slate = Blocks.getSlate(editor, { id: blockId });
+ if (!slate) return;
+
+ Editor.withoutNormalizing(slate, () => {
+ const tableDataCellsPerColumn = Editor.nodes(slate, {
+ at: [0],
+ match: (n) => Element.isElement(n) && n.type === 'table-data-cell',
+ mode: 'all',
+ });
+
+ Array.from(tableDataCellsPerColumn).forEach(([cell, path]) => {
+ if (path[path.length - 1] === columnIndex) {
+ Transforms.setNodes(
+ slate,
+ { props: { ...cell.props, width } },
+ {
+ at: path,
+ match: (n) => Element.isElement(n) && n.type === 'table-data-cell',
+ },
+ );
+ }
+ });
+ });
+ },
+ toggleHeaderRow: (editor: YooEditor, blockId: string) => {
+ const slate = Blocks.getSlate(editor, { id: blockId });
+ if (!slate) return;
+
+ Editor.withoutNormalizing(slate, () => {
+ const table = Elements.getElement(editor, blockId, { type: 'table', path: [0] });
+ const headerRow = table?.props?.headerRow || false;
+
+ const firstTableRowChildren = Editor.nodes(slate, {
+ at: [0, 0],
+ match: (n) => Element.isElement(n) && n.type === 'table-data-cell',
+ mode: 'all',
+ });
+
+ Array.from(firstTableRowChildren).forEach(([cell, path]) => {
+ Transforms.setNodes(
+ slate,
+ { props: { ...cell.props, asHeader: !cell.props?.asHeader } },
+ {
+ at: path,
+ match: (n) => Element.isElement(n) && n.type === 'table-data-cell',
+ },
+ );
+ });
+
+ Transforms.setNodes(
+ slate,
+ { props: { ...table?.props, headerRow: !headerRow } },
+ {
+ at: [0],
+ match: (n) => Element.isElement(n) && n.type === 'table',
+ },
+ );
+ });
+ },
+ toggleHeaderColumn: (editor: YooEditor, blockId: string) => {
+ const slate = Blocks.getSlate(editor, { id: blockId });
+ if (!slate) return;
+
+ Editor.withoutNormalizing(slate, () => {
+ const table = Elements.getElement(editor, blockId, { type: 'table', path: [0] });
+ const headerColumn = table?.props?.headerColumn || false;
+
+ const tableRows = Editor.nodes(slate, {
+ at: [0],
+ match: (n) => Element.isElement(n) && n.type === 'table-row',
+ mode: 'all',
+ });
+
+ Array.from(tableRows).forEach(([row, path]) => {
+ const cell = row.children[0] as TableCellElement;
+
+ Transforms.setNodes(
+ slate,
+ { props: { ...cell.props, asHeader: !cell.props?.asHeader } },
+ {
+ at: path.concat(0),
+ match: (n) => Element.isElement(n) && n.type === 'table-data-cell',
+ },
+ );
+ });
+
+ Transforms.setNodes(
+ slate,
+ { props: { ...table?.props, headerColumn: !headerColumn } },
+ {
+ at: [0],
+ match: (n) => Element.isElement(n) && n.type === 'table',
+ },
+ );
+ });
+ },
+};
diff --git a/packages/plugins/table/src/components/ResizeHandle.tsx b/packages/plugins/table/src/components/ResizeHandle.tsx
new file mode 100644
index 000000000..481301ff2
--- /dev/null
+++ b/packages/plugins/table/src/components/ResizeHandle.tsx
@@ -0,0 +1,60 @@
+import { useEffect, useRef } from 'react';
+
+const ResizeHandle = ({ onResize, tdWidth, columnIndex }) => {
+ const resizeRef = useRef(null);
+ const startWidth = useRef(0);
+ const startX = useRef(0);
+
+ useEffect(() => {
+ const tableEl = document.querySelector('.yoopta-table') as HTMLElement;
+ if (!tableEl) return;
+
+ const tableHeight = tableEl?.offsetHeight;
+ if (!resizeRef.current) return;
+
+ resizeRef.current.style.height = `${tableHeight}px`;
+ }, []);
+
+ useEffect(() => {
+ const tableEl = document.querySelector('.yoopta-table') as HTMLElement;
+ if (!tableEl) return;
+
+ const handleMouseDown = (e) => {
+ const tableHeight = tableEl?.offsetHeight;
+ if (!resizeRef.current) return;
+ resizeRef.current.style.height = `${tableHeight}px`;
+
+ startX.current = e.clientX;
+ startWidth.current = tdWidth;
+
+ const handleMouseMove = (event) => {
+ const currentX = event.clientX;
+ const newWidth = startWidth.current + (currentX - startX.current);
+
+ onResize(newWidth);
+ };
+
+ const handleMouseUp = () => {
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ };
+
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ };
+
+ resizeRef.current?.addEventListener('mousedown', handleMouseDown);
+
+ return () => {
+ resizeRef.current?.removeEventListener('mousedown', handleMouseDown);
+ };
+ }, [columnIndex, tdWidth]);
+
+ return (
+
+ );
+};
+
+export { ResizeHandle };
diff --git a/packages/plugins/table/src/components/Table.tsx b/packages/plugins/table/src/components/Table.tsx
deleted file mode 100644
index c2b40cc73..000000000
--- a/packages/plugins/table/src/components/Table.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { PluginElementRenderProps } from '@yoopta/editor';
-
-const TableRender = ({ attributes, children, element }: PluginElementRenderProps) => {
- return (
-
- );
-};
-export { TableRender };
diff --git a/packages/plugins/table/src/components/TableBlockOptions.tsx b/packages/plugins/table/src/components/TableBlockOptions.tsx
new file mode 100644
index 000000000..82de5a94b
--- /dev/null
+++ b/packages/plugins/table/src/components/TableBlockOptions.tsx
@@ -0,0 +1,55 @@
+import { UI, YooEditor, YooptaBlockData } from '@yoopta/editor';
+import { Sheet, TableProperties, CheckIcon } from 'lucide-react';
+import { TableCommands } from '../commands';
+import { TableElement } from '../types';
+
+const { ExtendedBlockActions, BlockOptionsMenuGroup, BlockOptionsMenuItem, BlockOptionsSeparator } = UI;
+
+type Props = {
+ editor: YooEditor;
+ block: YooptaBlockData;
+ table: TableElement;
+};
+
+const TableBlockOptions = ({ editor, block, table }: Props) => {
+ const tableProps = table.props;
+
+ const isHeaderRowEnabled = tableProps?.headerRow;
+ const isHeaderColumnEnabled = tableProps?.headerColumn;
+
+ const onSwitchHeaderRow = () => {
+ TableCommands.toggleHeaderRow(editor, block.id);
+ };
+
+ const onSwitchHeaderColumn = () => {
+ TableCommands.toggleHeaderColumn(editor, block.id);
+ };
+
+ return (
+ editor.setSelection([block.meta.order])} className="yoopta-table-options">
+
+
+
+
+
+
+ Header row
+
+ {isHeaderRowEnabled && }
+
+
+
+
+
+
+ Header column
+
+ {isHeaderColumnEnabled && }
+
+
+
+
+ );
+};
+
+export { TableBlockOptions };
diff --git a/packages/plugins/table/src/components/TableCell.tsx b/packages/plugins/table/src/components/TableCell.tsx
deleted file mode 100644
index 9eddd963e..000000000
--- a/packages/plugins/table/src/components/TableCell.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-const TableCellRender = ({ attributes, children, element }) => {
- return (
-
- {children}
- |
- );
-};
-
-export { TableCellRender };
diff --git a/packages/plugins/table/src/components/TableColumnDragButton.tsx b/packages/plugins/table/src/components/TableColumnDragButton.tsx
new file mode 100644
index 000000000..729ec4ffd
--- /dev/null
+++ b/packages/plugins/table/src/components/TableColumnDragButton.tsx
@@ -0,0 +1,63 @@
+import { Elements, SlateElement, YooEditor } from '@yoopta/editor';
+import { useFloating, inline, flip, shift, offset } from '@floating-ui/react';
+import { useState } from 'react';
+import DragIcon from '../icons/drag.svg';
+import { TableColumnOptions } from './TableColumnOptions';
+import { Transforms } from 'slate';
+
+type TableRowProps = {
+ editor: YooEditor;
+ blockId: string;
+ tdElement: SlateElement;
+};
+
+const TableColumnDragButton = ({ editor, blockId, tdElement }: TableRowProps) => {
+ const [isTableColumnActionsOpen, setIsTableColumnActionsOpen] = useState(false);
+
+ const { refs, floatingStyles } = useFloating({
+ placement: 'bottom-start',
+ open: isTableColumnActionsOpen,
+ onOpenChange: setIsTableColumnActionsOpen,
+ middleware: [inline(), flip(), shift(), offset(10)],
+ });
+
+ const onClick = () => {
+ if (editor.readOnly) return;
+ const slate = editor.blockEditorsMap[blockId];
+ const tdElementPath = Elements.getElementPath(editor, blockId, tdElement);
+ if (!tdElementPath) return;
+
+ Transforms.select(slate, { path: tdElementPath.concat([0]), offset: 0 });
+ setIsTableColumnActionsOpen(true);
+ };
+
+ const onClose = () => {
+ setIsTableColumnActionsOpen(false);
+ };
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export { TableColumnDragButton };
diff --git a/packages/plugins/table/src/components/TableColumnOptions.tsx b/packages/plugins/table/src/components/TableColumnOptions.tsx
new file mode 100644
index 000000000..5b4e89c99
--- /dev/null
+++ b/packages/plugins/table/src/components/TableColumnOptions.tsx
@@ -0,0 +1,131 @@
+import { Elements, SlateElement, UI, YooEditor } from '@yoopta/editor';
+
+import { CSSProperties } from 'react';
+import { Editor, Element, Path, Transforms } from 'slate';
+import { TrashIcon, ArrowRightIcon, ArrowLeftIcon, MoveRightIcon, MoveLeftIcon } from 'lucide-react';
+import { TableCommands } from '../commands';
+
+const { BlockOptionsMenuGroup, BlockOptionsMenuItem, BlockOptions, BlockOptionsSeparator } = UI;
+
+export type Props = {
+ isOpen: boolean;
+ onClose: () => void;
+ refs: any;
+ style: CSSProperties;
+ children?: React.ReactNode;
+ actions?: ['delete', 'duplicate', 'turnInto', 'copy'] | null;
+} & {
+ editor: YooEditor;
+ blockId: string;
+ element: SlateElement;
+};
+
+const TableColumnOptions = ({ editor, blockId, element, onClose, ...props }: Props) => {
+ const insertColumnBefore = () => {
+ TableCommands.insertTableColumn(editor, blockId, { insertMode: 'before' });
+ onClose();
+ };
+
+ const insertColumnAfter = () => {
+ TableCommands.insertTableColumn(editor, blockId, { insertMode: 'after' });
+ onClose();
+ };
+
+ const deleteTableColumn = () => {
+ let path = Elements.getElementPath(editor, blockId, element);
+ if (!path) return;
+
+ // @ts-ignore [FIXME] - fix types
+ TableCommands.deleteTableColumn(editor, blockId, { path });
+ onClose();
+ };
+
+ const moveColumnRight = () => {
+ const slate = editor.blockEditorsMap[blockId];
+ const tdElementEntry = Elements.getElementEntry(editor, blockId, {
+ type: 'table-data-cell',
+ // @ts-ignore [FIXME] - fix types
+ path: slate.selection,
+ });
+
+ if (tdElementEntry) {
+ const [, tdPath] = tdElementEntry;
+
+ const nextTdEntry = Editor.next(slate, {
+ at: tdPath,
+ match: (n) => Element.isElement(n) && n.type === 'table-data-cell',
+ });
+
+ if (!nextTdEntry) return;
+
+ const [, nextTdPath] = nextTdEntry;
+ TableCommands.moveTableColumn(editor, blockId, { from: tdPath, to: nextTdPath });
+ }
+ };
+
+ const moveColumnLeft = () => {
+ const slate = editor.blockEditorsMap[blockId];
+
+ const tdElementEntry = Elements.getElementEntry(editor, blockId, {
+ type: 'table-data-cell',
+ // @ts-ignore [FIXME] - fix types
+ path: slate.selection,
+ });
+
+ if (tdElementEntry) {
+ const [, tdPath] = tdElementEntry;
+
+ const prevTdEntry = Editor.previous(slate, {
+ at: tdPath,
+ match: (n) => Element.isElement(n) && n.type === 'table-data-cell',
+ });
+
+ if (!prevTdEntry) return;
+
+ const [, prevTdPath] = prevTdEntry;
+ TableCommands.moveTableColumn(editor, blockId, { from: tdPath, to: prevTdPath });
+ }
+ };
+
+ return (
+
+
+
+
+
+ Insert left
+
+
+
+
+
+ Insert right
+
+
+
+
+
+
+ Move right
+
+
+
+
+
+ Move left
+
+
+
+
+
+
+
+ Delete
+
+
+
+
+ );
+};
+
+export { TableColumnOptions };
diff --git a/packages/plugins/table/src/components/TableRow.tsx b/packages/plugins/table/src/components/TableRow.tsx
deleted file mode 100644
index 6f61f3bb6..000000000
--- a/packages/plugins/table/src/components/TableRow.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-const TableRowRender = ({ attributes, children, element }) => {
- return (
-
- {children}
-
- );
-};
-
-export { TableRowRender };
diff --git a/packages/plugins/table/src/components/TableRowDragButton.tsx b/packages/plugins/table/src/components/TableRowDragButton.tsx
new file mode 100644
index 000000000..1868d757a
--- /dev/null
+++ b/packages/plugins/table/src/components/TableRowDragButton.tsx
@@ -0,0 +1,64 @@
+import { Elements, SlateElement, YooEditor } from '@yoopta/editor';
+import { useFloating, inline, flip, shift, offset } from '@floating-ui/react';
+import { useState } from 'react';
+import { TableRowOptions } from './TableRowOptions';
+import DragIcon from '../icons/drag.svg';
+import { Transforms } from 'slate';
+
+type TableRowProps = {
+ editor: YooEditor;
+ blockId: string;
+ tdElement: SlateElement;
+};
+
+const TableRowDragButton = ({ editor, blockId, tdElement }: TableRowProps) => {
+ const [isTableRowActionsOpen, setIsTableRowActionsOpen] = useState(false);
+
+ const { refs, floatingStyles } = useFloating({
+ placement: 'right-start',
+ open: isTableRowActionsOpen,
+ onOpenChange: setIsTableRowActionsOpen,
+ middleware: [inline(), flip(), shift(), offset(10)],
+ });
+
+ const onClick = () => {
+ if (editor.readOnly) return;
+
+ const slate = editor.blockEditorsMap[blockId];
+ const tdElementPath = Elements.getElementPath(editor, blockId, tdElement);
+ if (!tdElementPath) return;
+ Transforms.select(slate, { path: tdElementPath.concat([0]), offset: 0 });
+
+ setIsTableRowActionsOpen(true);
+ };
+
+ const onClose = () => {
+ setIsTableRowActionsOpen(false);
+ };
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export { TableRowDragButton };
diff --git a/packages/plugins/table/src/components/TableRowOptions.tsx b/packages/plugins/table/src/components/TableRowOptions.tsx
new file mode 100644
index 000000000..3fc4f163a
--- /dev/null
+++ b/packages/plugins/table/src/components/TableRowOptions.tsx
@@ -0,0 +1,119 @@
+import { Elements, SlateElement, UI, YooEditor } from '@yoopta/editor';
+
+import { CSSProperties } from 'react';
+import { Editor, Element, Path } from 'slate';
+import { TrashIcon, MoveDownIcon, MoveUpIcon, CornerUpRight, CornerDownRight } from 'lucide-react';
+import { TableCommands } from '../commands';
+
+const { BlockOptionsMenuGroup, BlockOptionsMenuItem, BlockOptions, BlockOptionsSeparator } = UI;
+
+export type Props = {
+ isOpen: boolean;
+ onClose: () => void;
+ refs: any;
+ style: CSSProperties;
+ children?: React.ReactNode;
+ actions?: ['delete', 'duplicate', 'turnInto', 'copy'] | null;
+} & {
+ editor: YooEditor;
+ blockId: string;
+ tdElement: SlateElement;
+};
+
+const TableRowOptions = ({ editor, blockId, onClose, tdElement, ...props }: Props) => {
+ const insertRowBefore = () => {
+ TableCommands.insertTableRow(editor, blockId, { insertMode: 'before', select: true });
+ onClose();
+ };
+
+ const insertRowAfter = () => {
+ TableCommands.insertTableRow(editor, blockId, { insertMode: 'after', select: true });
+ onClose();
+ };
+
+ const deleteTableRow = () => {
+ const tdPath = Elements.getElementPath(editor, blockId, tdElement);
+ const trElement = Elements.getElement(editor, blockId, { type: 'table-row', path: tdPath });
+ if (!trElement) return;
+
+ let path = Elements.getElementPath(editor, blockId, trElement);
+ // @ts-ignore [FIXME] - Fix this
+ TableCommands.deleteTableRow(editor, blockId, { path });
+ onClose();
+ };
+
+ const moveRowDown = () => {
+ const tdPath = Elements.getElementPath(editor, blockId, tdElement);
+ const trElement = Elements.getElement(editor, blockId, { type: 'table-row', path: tdPath });
+ if (!trElement) return;
+
+ let path = Elements.getElementPath(editor, blockId, trElement);
+
+ const slate = editor.blockEditorsMap[blockId];
+ const nextElementEntry = Editor.next(slate, {
+ at: path,
+ match: (n) => Element.isElement(n) && n.type === 'table-row',
+ });
+
+ if (!nextElementEntry) return onClose();
+ TableCommands.moveTableRow(editor, blockId, { from: path!, to: nextElementEntry[1] });
+ };
+
+ const moveRowUp = () => {
+ const tdPath = Elements.getElementPath(editor, blockId, tdElement);
+ const trElement = Elements.getElement(editor, blockId, { type: 'table-row', path: tdPath });
+ if (!trElement) return;
+
+ let path = Elements.getElementPath(editor, blockId, trElement);
+
+ const slate = editor.blockEditorsMap[blockId];
+ const prevElementEntry = Editor.previous(slate, {
+ at: path,
+ match: (n) => Element.isElement(n) && n.type === 'table-row',
+ });
+
+ if (!prevElementEntry) return onClose();
+ TableCommands.moveTableRow(editor, blockId, { from: path!, to: prevElementEntry[1] });
+ };
+
+ return (
+
+
+
+
+
+ Insert above
+
+
+
+
+
+ Insert below
+
+
+
+
+
+
+ Move up
+
+
+
+
+
+ Move down
+
+
+
+
+
+
+ Delete row
+
+
+
+
+ );
+};
+
+export { TableRowOptions };
diff --git a/packages/plugins/table/src/elements/Table.tsx b/packages/plugins/table/src/elements/Table.tsx
new file mode 100644
index 000000000..7eff2ddf8
--- /dev/null
+++ b/packages/plugins/table/src/elements/Table.tsx
@@ -0,0 +1,25 @@
+import { PluginElementRenderProps, useBlockData, useYooptaEditor } from '@yoopta/editor';
+import { useMemo } from 'react';
+import { TableBlockOptions } from '../components/TableBlockOptions';
+import { TableElement } from '../types';
+import { TABLE_SLATE_TO_SELECTION_SET } from '../utils/weakMaps';
+
+const Table = ({ attributes, children, blockId, element, HTMLAttributes }: PluginElementRenderProps) => {
+ const editor = useYooptaEditor();
+ const slate = editor.blockEditorsMap[blockId];
+ const blockData = useBlockData(blockId);
+ const isReadOnly = editor.readOnly;
+
+ const isSelecting = TABLE_SLATE_TO_SELECTION_SET.get(slate);
+
+ return (
+
+ );
+};
+
+export { Table };
diff --git a/packages/plugins/table/src/elements/TableDataCell.tsx b/packages/plugins/table/src/elements/TableDataCell.tsx
new file mode 100644
index 000000000..855a6d0e5
--- /dev/null
+++ b/packages/plugins/table/src/elements/TableDataCell.tsx
@@ -0,0 +1,94 @@
+import { Elements, PluginElementRenderProps, useYooptaEditor } from '@yoopta/editor';
+import { useMemo } from 'react';
+import { Editor, Element } from 'slate';
+import { ResizeHandle } from '../components/ResizeHandle';
+import { TableColumnDragButton } from '../components/TableColumnDragButton';
+import { TableRowDragButton } from '../components/TableRowDragButton';
+import { TableCommands } from '../commands';
+import { TableCellElement, TableElement, TableElementProps } from '../types';
+import { TABLE_SLATE_TO_SELECTION_SET } from '../utils/weakMaps';
+
+const TableDataCell = ({ attributes, children, element, blockId }: PluginElementRenderProps) => {
+ const editor = useYooptaEditor();
+ const slate = editor.blockEditorsMap[blockId];
+
+ const path = Elements.getElementPath(editor, blockId, element);
+ const selected = TABLE_SLATE_TO_SELECTION_SET.get(slate)?.has(element as TableCellElement);
+ const asHeader = element?.props?.asHeader || false;
+
+ const tableProps = useMemo(() => {
+ const tableElementEntry = Editor.above(slate, {
+ at: path,
+ match: (n) => Element.isElement(n) && n.type === 'table',
+ });
+
+ if (!tableElementEntry) return null;
+ const [tableElement] = tableElementEntry;
+
+ const headerRow = tableElement?.props?.headerRow || false;
+ const headerColumn = tableElement?.props?.headerColumn || false;
+
+ return {
+ headerColumn,
+ headerRow,
+ };
+ }, [asHeader]);
+
+ const { headerRow, headerColumn } = tableProps || {};
+
+ const columnIndex = path?.[path.length - 1] || 0;
+ const elementWidth = element?.props?.width || 200;
+
+ const isFirstDataCell = path?.[path.length - 1] === 0;
+ const isFirstRow = path?.[path.length - 2] === 0;
+
+ let isDataCellAsHeader = false;
+
+ if (isFirstRow && headerRow) {
+ isDataCellAsHeader = true;
+ }
+
+ if (isFirstDataCell && headerColumn) {
+ isDataCellAsHeader = true;
+ }
+
+ const onResize = (newWidth: number) => {
+ TableCommands.updateColumnWidth(editor, blockId, columnIndex, newWidth);
+ };
+
+ const Node = isDataCellAsHeader ? 'th' : 'td';
+ const style = {
+ maxWidth: elementWidth,
+ minWidth: elementWidth,
+ };
+
+ const className = isDataCellAsHeader
+ ? 'yoopta-table-data-cell yoopta-table-data-cell-head'
+ : 'yoopta-table-data-cell';
+
+ return (
+
+
+ {children}
+
+ {!editor.readOnly && isFirstRow && (
+
+ )}
+ {!editor.readOnly && isFirstRow && (
+
+ )}
+ {!editor.readOnly && isFirstDataCell && (
+
+ )}
+
+ );
+};
+
+export { TableDataCell };
diff --git a/packages/plugins/table/src/elements/TableRow.tsx b/packages/plugins/table/src/elements/TableRow.tsx
new file mode 100644
index 000000000..23d98ccbd
--- /dev/null
+++ b/packages/plugins/table/src/elements/TableRow.tsx
@@ -0,0 +1,11 @@
+import { PluginElementRenderProps, useYooptaEditor } from '@yoopta/editor';
+
+const TableRow = ({ attributes, children, element }: PluginElementRenderProps) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export { TableRow };
diff --git a/packages/plugins/table/src/events/onKeyDown.ts b/packages/plugins/table/src/events/onKeyDown.ts
new file mode 100644
index 000000000..ac23b43f7
--- /dev/null
+++ b/packages/plugins/table/src/events/onKeyDown.ts
@@ -0,0 +1,169 @@
+import { SlateEditor, YooEditor, PluginEventHandlerOptions } from '@yoopta/editor';
+import { Editor, Element, Node, Path, Range, Text, Transforms } from 'slate';
+import { TableCommands } from '../commands';
+import { EDITOR_TO_SELECTION } from '../utils/weakMaps';
+
+export function onKeyDown(editor: YooEditor, slate: SlateEditor, { hotkeys, currentBlock }: PluginEventHandlerOptions) {
+ return (event) => {
+ if (!slate.selection) return;
+
+ if (hotkeys.isBackspace(event)) {
+ const parentPath = Path.parent(slate.selection.anchor.path);
+ const isStart = Editor.isStart(slate, slate.selection.anchor, parentPath);
+
+ const elementEntries = EDITOR_TO_SELECTION.get(slate);
+ if (!!elementEntries) {
+ event.preventDefault();
+
+ Editor.withoutNormalizing(slate, () => {
+ // just remove text in selected nodes
+ for (const [, path] of elementEntries) {
+ for (const [childNode, childPath] of Node.children(slate, path)) {
+ if (Text.isText(childNode)) {
+ const textLength = Node.string(childNode).length;
+ if (textLength > 0) {
+ Transforms.delete(slate, {
+ at: {
+ anchor: { path: childPath, offset: 0 },
+ focus: { path: childPath, offset: textLength },
+ },
+ });
+ }
+ }
+ }
+ }
+
+ Transforms.select(slate, { path: elementEntries[0][1].concat(0), offset: 0 });
+ });
+ return;
+ }
+
+ if (isStart && Range.isCollapsed(slate.selection)) {
+ event.preventDefault();
+ return;
+ }
+ }
+
+ // add new row before current row
+ if (hotkeys.isCmdShiftEnter(event)) {
+ event.preventDefault();
+ TableCommands.insertTableRow(editor, currentBlock.id, { select: true, insertMode: 'before' });
+ return;
+ }
+
+ // add new row after current row
+ if (hotkeys.isShiftEnter(event)) {
+ event.preventDefault();
+ TableCommands.insertTableRow(editor, currentBlock.id, { select: true, insertMode: 'after' });
+ return;
+ }
+
+ if (hotkeys.isCmdShiftRight(event)) {
+ event.preventDefault();
+ TableCommands.insertTableColumn(editor, currentBlock.id, { select: true, insertMode: 'after' });
+ return;
+ }
+
+ if (hotkeys.isCmdShiftLeft(event)) {
+ event.preventDefault();
+ TableCommands.insertTableColumn(editor, currentBlock.id, { select: true, insertMode: 'before' });
+ return;
+ }
+
+ if (hotkeys.isCmdShiftDelete(event)) {
+ event.preventDefault();
+ TableCommands.deleteTableRow(editor, currentBlock.id);
+ return;
+ }
+
+ if (hotkeys.isCmdAltDelete(event)) {
+ event.preventDefault();
+ TableCommands.deleteTableColumn(editor, currentBlock.id);
+ return;
+ }
+
+ if (hotkeys.isArrowUp(event)) {
+ event.preventDefault();
+
+ const dataCellEntry = Editor.above(slate, {
+ at: slate.selection.anchor,
+ match: (n) => Element.isElement(n) && n.type === 'table-data-cell',
+ });
+
+ if (!dataCellEntry) return;
+ const [dataCellNode, dataCellpath] = dataCellEntry;
+
+ try {
+ const columnIndex = dataCellpath[dataCellpath.length - 1];
+ const prevRowPath = Path.previous(dataCellpath.slice(0, -1));
+ const prevDataCellPath = prevRowPath.concat(columnIndex);
+
+ // throws error if no node found in the path
+ Editor.node(slate, prevDataCellPath);
+ Transforms.select(slate, prevDataCellPath);
+ } catch (error) {}
+
+ return;
+ }
+
+ if (hotkeys.isArrowDown(event)) {
+ event.preventDefault();
+
+ const dataCellEntry = Editor.above(slate, {
+ at: slate.selection.anchor,
+ match: (n) => Element.isElement(n) && n.type === 'table-data-cell',
+ });
+
+ if (!dataCellEntry) return;
+ const [dataCellNode, dataCellpath] = dataCellEntry;
+
+ try {
+ const columnIndex = dataCellpath[dataCellpath.length - 1];
+ const nextRowPath = Path.next(dataCellpath.slice(0, -1));
+ const nextDataCellPath = nextRowPath.concat(columnIndex);
+
+ // throws error if no node found in the path
+ Editor.node(slate, nextDataCellPath);
+ Transforms.select(slate, nextDataCellPath);
+ } catch (error) {}
+
+ return;
+ }
+
+ if (hotkeys.isEnter(event)) {
+ event.preventDefault();
+ Transforms.insertText(slate, '\n');
+ }
+
+ // if first select then select the whole table
+ if (hotkeys.isSelect(event)) {
+ const tdElementEntry = Editor.above(slate, {
+ match: (n) => Element.isElement(n) && n.type === 'table-data-cell',
+ });
+
+ if (tdElementEntry) {
+ event.preventDefault();
+ const [tdElement, tdElementPath] = tdElementEntry;
+ const string = Editor.string(slate, tdElementPath);
+
+ if (Range.isExpanded(slate.selection) || string.length === 0) {
+ editor.blur();
+ editor.setBlockSelected([currentBlock.meta.order]);
+ return;
+ }
+
+ Transforms.select(slate, tdElementPath);
+ }
+ }
+
+ if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'h') {
+ event.preventDefault();
+ TableCommands.toggleHeaderRow(editor, currentBlock.id);
+ }
+
+ if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'v') {
+ event.preventDefault();
+ TableCommands.toggleHeaderColumn(editor, currentBlock.id);
+ }
+ };
+}
diff --git a/packages/plugins/table/src/extenstions/withDelete.ts b/packages/plugins/table/src/extenstions/withDelete.ts
new file mode 100644
index 000000000..47c3cd266
--- /dev/null
+++ b/packages/plugins/table/src/extenstions/withDelete.ts
@@ -0,0 +1,46 @@
+import { SlateEditor } from '@yoopta/editor';
+import { Editor, Element, Point, Range, Transforms } from 'slate';
+
+export function withDelete(slate: SlateEditor): SlateEditor {
+ const { deleteBackward } = slate;
+
+ slate.deleteBackward = (unit) => {
+ const { selection } = slate;
+
+ if (!selection || Range.isExpanded(selection)) {
+ return deleteBackward(unit);
+ }
+
+ const [td] = Editor.nodes(slate, {
+ match: (n) => Element.isElement(n) && n.type === 'table-data-cell',
+ at: selection,
+ });
+
+ const before = Editor.before(slate, selection, { unit });
+ const [tdBefore] = before
+ ? Editor.nodes(slate, {
+ match: (n) => Element.isElement(n) && n.type === 'table-data-cell',
+ at: before,
+ })
+ : [undefined];
+
+ if (!td && !tdBefore) {
+ return deleteBackward(unit);
+ }
+
+ if (!td && tdBefore && before) {
+ return Transforms.select(slate, before);
+ }
+
+ const [, tdPath] = td;
+ const start = Editor.start(slate, tdPath);
+
+ if (Point.equals(selection.anchor, start)) {
+ return;
+ }
+
+ deleteBackward(unit);
+ };
+
+ return slate;
+}
diff --git a/packages/plugins/table/src/extenstions/withNormalize.ts b/packages/plugins/table/src/extenstions/withNormalize.ts
new file mode 100644
index 000000000..402b7871f
--- /dev/null
+++ b/packages/plugins/table/src/extenstions/withNormalize.ts
@@ -0,0 +1,49 @@
+import { generateId, SlateEditor, YooEditor } from '@yoopta/editor';
+import { Element, Node, Transforms } from 'slate';
+
+export function withNormalize(slate: SlateEditor, editor: YooEditor): SlateEditor {
+ const { normalizeNode } = slate;
+
+ slate.normalizeNode = (entry, options) => {
+ const [node, path] = entry;
+
+ if (Element.isElement(node) && node.type === 'table-data-cell') {
+ for (const [child, childPath] of Node.children(slate, path)) {
+ if (Element.isElement(child) && child.type === 'table') {
+ Transforms.unwrapNodes(slate, { at: childPath });
+ return;
+ }
+
+ if (Element.isElement(child) && child.type === 'table-row') {
+ Transforms.unwrapNodes(slate, { at: childPath });
+ return;
+ }
+
+ if (Element.isElement(child) && child.type === 'table-data-cell') {
+ Transforms.unwrapNodes(slate, { at: childPath });
+ return;
+ }
+ }
+ }
+
+ if (Element.isElement(node) && node.type === 'table-row') {
+ for (const [child, childPath] of Node.children(slate, path)) {
+ if (!Element.isElement(child) || child.type !== 'table-data-cell') {
+ return Transforms.wrapNodes(
+ slate,
+ {
+ id: generateId(),
+ type: 'table-data-cell',
+ children: [child],
+ } as Element,
+ { at: childPath },
+ );
+ }
+ }
+ }
+
+ normalizeNode(entry, options);
+ };
+
+ return slate;
+}
diff --git a/packages/plugins/table/src/extenstions/withSelection.ts b/packages/plugins/table/src/extenstions/withSelection.ts
new file mode 100644
index 000000000..95348f4f7
--- /dev/null
+++ b/packages/plugins/table/src/extenstions/withSelection.ts
@@ -0,0 +1,81 @@
+import { SlateEditor, SlateElement } from '@yoopta/editor';
+import { Editor, Element, Operation, Path, Range } from 'slate';
+import { TableCellElement } from '../types';
+import { EDITOR_TO_SELECTION, TABLE_SLATE_TO_SELECTION_SET, SlateNodeEntry } from '../utils/weakMaps';
+
+export function withSelection(slate: SlateEditor): SlateEditor {
+ const { apply } = slate;
+
+ slate.apply = (op) => {
+ if (!Operation.isSelectionOperation(op) || !op.newProperties) {
+ TABLE_SLATE_TO_SELECTION_SET.delete(slate);
+ EDITOR_TO_SELECTION.delete(slate);
+ return apply(op);
+ }
+
+ const selection = {
+ ...slate.selection,
+ ...op.newProperties,
+ };
+
+ if (!Range.isRange(selection)) {
+ TABLE_SLATE_TO_SELECTION_SET.delete(slate);
+ EDITOR_TO_SELECTION.delete(slate);
+
+ return apply(op);
+ }
+
+ const [fromEntry] = Editor.nodes(slate, {
+ match: (n) => Element.isElement(n) && n.type === 'table-data-cell',
+ at: Range.start(selection),
+ });
+
+ const [toEntry] = Editor.nodes(slate, {
+ match: (n) => Element.isElement(n) && n.type === 'table-data-cell',
+ at: Range.end(selection),
+ });
+
+ if (!fromEntry || !toEntry) {
+ TABLE_SLATE_TO_SELECTION_SET.delete(slate);
+ EDITOR_TO_SELECTION.delete(slate);
+
+ return apply(op);
+ }
+
+ const [, fromPath] = fromEntry;
+ const [, toPath] = toEntry;
+
+ if (Path.equals(fromPath, toPath)) {
+ TABLE_SLATE_TO_SELECTION_SET.delete(slate);
+ EDITOR_TO_SELECTION.delete(slate);
+
+ return apply(op);
+ }
+
+ const selectedSet = new WeakSet();
+
+ const range = Editor.range(slate, fromPath, toPath);
+ const nodesInRange = Array.from(
+ Editor.nodes(slate, {
+ at: range,
+ match: (n) => Element.isElement(n) && n.type === 'table-data-cell',
+ }),
+ );
+
+ const selected: SlateNodeEntry[] = [];
+
+ for (const [element, path] of nodesInRange) {
+ if (Element.isElement(element) && element.type === 'table-data-cell') {
+ selectedSet.add(element);
+ selected.push([element as TableCellElement, path]);
+ }
+ }
+
+ EDITOR_TO_SELECTION.set(slate, selected);
+ TABLE_SLATE_TO_SELECTION_SET.set(slate, selectedSet);
+
+ apply(op);
+ };
+
+ return slate;
+}
diff --git a/packages/plugins/table/src/extenstions/withTable.ts b/packages/plugins/table/src/extenstions/withTable.ts
new file mode 100644
index 000000000..a96b42a83
--- /dev/null
+++ b/packages/plugins/table/src/extenstions/withTable.ts
@@ -0,0 +1,12 @@
+import { SlateEditor, YooEditor } from '@yoopta/editor';
+import { withDelete } from './withDelete';
+import { withNormalize } from './withNormalize';
+import { withSelection } from './withSelection';
+
+export function withTable(slate: SlateEditor, editor: YooEditor) {
+ slate = withSelection(slate);
+ slate = withNormalize(slate, editor);
+ slate = withDelete(slate);
+
+ return slate;
+}
diff --git a/packages/plugins/table/src/icons/drag.svg b/packages/plugins/table/src/icons/drag.svg
new file mode 100644
index 000000000..73ffb81a5
--- /dev/null
+++ b/packages/plugins/table/src/icons/drag.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/plugins/table/src/icons/plus.svg b/packages/plugins/table/src/icons/plus.svg
new file mode 100644
index 000000000..9a7656a1a
--- /dev/null
+++ b/packages/plugins/table/src/icons/plus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/plugins/table/src/index.ts b/packages/plugins/table/src/index.ts
index 3c9ea6c3c..b2846c4bc 100644
--- a/packages/plugins/table/src/index.ts
+++ b/packages/plugins/table/src/index.ts
@@ -8,4 +8,6 @@ declare module 'slate' {
}
}
+export { TableCommands } from './commands';
+
export default Table;
diff --git a/packages/plugins/table/src/parsers/html/deserialize.ts b/packages/plugins/table/src/parsers/html/deserialize.ts
new file mode 100644
index 000000000..caa4ea552
--- /dev/null
+++ b/packages/plugins/table/src/parsers/html/deserialize.ts
@@ -0,0 +1,105 @@
+import { deserializeTextNodes, generateId, YooEditor } from '@yoopta/editor';
+import { Descendant } from 'slate';
+import { TableElement, TableRowElement } from '../../types';
+
+export function deserializeTable(el: HTMLElement, editor: YooEditor) {
+ const tbody = el.querySelector('tbody');
+ const thead = el.querySelector('thead');
+
+ const tableElement: TableElement = {
+ id: generateId(),
+ type: 'table',
+ children: [],
+ props: {
+ headerRow: el.getAttribute('data-header-row') === 'true',
+ headerColumn: el.getAttribute('data-header-column') === 'true',
+ },
+ };
+
+ if (!tbody && !thead) return;
+
+ const theadRow = thead?.querySelector('tr');
+ if (theadRow) {
+ tableElement.props!.headerRow = true;
+ }
+
+ if (theadRow) {
+ const rowElement: TableRowElement = {
+ id: generateId(),
+ type: 'table-row',
+ children: [],
+ };
+
+ Array.from(theadRow.childNodes).forEach((th) => {
+ if (th.nodeName === 'TH') {
+ const cellElement = {
+ id: generateId(),
+ type: 'table-data-cell',
+ children: [{ text: '' }],
+ props: {
+ asHeader: true,
+ width: 200,
+ },
+ };
+
+ if (th instanceof HTMLElement && th?.hasAttribute('data-width')) {
+ cellElement.props.width = parseInt((th as HTMLElement).getAttribute('data-width') || '200', 10);
+ }
+
+ let textNodes = deserializeTextNodes(editor, th.childNodes);
+ // @ts-ignore [FIXME] - Fix this
+ cellElement.children = textNodes;
+ rowElement.children.push(cellElement);
+ }
+ });
+
+ tableElement.children.push(rowElement);
+ }
+
+ tbody?.childNodes.forEach((tr) => {
+ const trChildNodes = Array.from(tr.childNodes).filter((node) => node.nodeName === 'TD' || node.nodeName === 'TH');
+
+ if (trChildNodes.length > 0) {
+ const rowElement: TableRowElement = {
+ id: generateId(),
+ type: 'table-row',
+ children: [],
+ };
+
+ trChildNodes.forEach((td) => {
+ const cellElement = {
+ id: generateId(),
+ type: 'table-data-cell',
+ children: [{ text: '' }],
+ props: {
+ asHeader: false,
+ width: 200,
+ },
+ };
+
+ if (td.nodeName === 'TH') {
+ cellElement.props.asHeader = true;
+ }
+
+ if (td.nodeName === 'TD') {
+ cellElement.props.asHeader = false;
+ }
+
+ if (td.nodeName === 'TD' || td.nodeName === 'TH') {
+ if (td instanceof HTMLElement && td?.hasAttribute('data-width')) {
+ cellElement.props.width = parseInt((td as HTMLElement).getAttribute('data-width') || '200', 10);
+ }
+
+ let textNodes = deserializeTextNodes(editor, td.childNodes);
+ // @ts-ignore [FIXME] - Fix this
+ cellElement.children = textNodes;
+ rowElement.children.push(cellElement);
+ }
+ });
+
+ tableElement.children.push(rowElement);
+ }
+ });
+
+ return tableElement;
+}
diff --git a/packages/plugins/table/src/parsers/html/serialize.ts b/packages/plugins/table/src/parsers/html/serialize.ts
new file mode 100644
index 000000000..a57501b14
--- /dev/null
+++ b/packages/plugins/table/src/parsers/html/serialize.ts
@@ -0,0 +1,34 @@
+import { serializeTextNodes, SlateElement, YooEditor, YooptaBlockData } from '@yoopta/editor';
+import { TableCellElement } from '../../types';
+
+export function serializeTable(element: SlateElement, text: string, blockMeta?: YooptaBlockData['meta']) {
+ const columns = (element.children[0] as SlateElement).children as TableCellElement[];
+ const { align = 'left', depth = 0 } = blockMeta || {};
+
+ const serialized = `
+
+ ${columns
+ .map((td) => {
+ return ``;
+ })
+ .join('')}
+
+ ${element.children
+ .map((trElement) => {
+ return `${trElement.children
+ .map((td) => {
+ const text = serializeTextNodes(td.children);
+ if (td.props?.asHeader) {
+ return `${text} | `;
+ }
+
+ return `${text} | `;
+ })
+ .join('')}
`;
+ })
+ .join('')}
`;
+
+ return serialized;
+}
diff --git a/packages/plugins/table/src/parsers/markdown/serialize.ts b/packages/plugins/table/src/parsers/markdown/serialize.ts
new file mode 100644
index 000000000..89d6547ea
--- /dev/null
+++ b/packages/plugins/table/src/parsers/markdown/serialize.ts
@@ -0,0 +1,22 @@
+import { serializeTextNodesIntoMarkdown } from '@yoopta/editor';
+
+export function serializeMarkown(element, text) {
+ let markdownTable = '';
+
+ element.children.forEach((row, rowIndex) => {
+ const rowMarkdown = row.children
+ .map((cell) => {
+ return ` ${serializeTextNodesIntoMarkdown(cell.children)} `;
+ })
+ .join('|');
+
+ markdownTable += `|${rowMarkdown}|\n`;
+
+ if (rowIndex === 0) {
+ const separator = row.children.map(() => ' --- ').join('|');
+ markdownTable += `|${separator}|\n`;
+ }
+ });
+
+ return markdownTable;
+}
diff --git a/packages/plugins/table/src/plugin/Table.tsx b/packages/plugins/table/src/plugin/Table.tsx
index 3fb4c8d4b..c4235bd9b 100644
--- a/packages/plugins/table/src/plugin/Table.tsx
+++ b/packages/plugins/table/src/plugin/Table.tsx
@@ -1,22 +1,71 @@
import { YooptaPlugin } from '@yoopta/editor';
-import { TableRender } from '../components/Table';
-import { TableCellRender } from '../components/TableCell';
-import { TableRowRender } from '../components/TableRow';
+import { Table as TableRender } from '../elements/Table';
+import { TableDataCell } from '../elements/TableDataCell';
+import { TableRow } from '../elements/TableRow';
+import { TableElementMap } from '../types';
+import { TableCommands } from '../commands';
-export const Table = new YooptaPlugin({
+import { withTable } from '../extenstions/withTable';
+import { onKeyDown } from '../events/onKeyDown';
+import { TABLE_SLATE_TO_SELECTION_SET } from '../utils/weakMaps';
+import { deserializeTable } from '../parsers/html/deserialize';
+import { serializeTable } from '../parsers/html/serialize';
+import { serializeMarkown } from '../parsers/markdown/serialize';
+
+const Table = new YooptaPlugin({
type: 'Table',
elements: {
table: {
render: TableRender,
asRoot: true,
children: ['table-row'],
+ props: {
+ headerRow: false,
+ headerColumn: false,
+ },
},
'table-row': {
- render: TableRowRender,
- children: ['table-cell'],
+ render: TableRow,
+ children: ['table-data-cell'],
+ },
+ 'table-data-cell': {
+ render: TableDataCell,
+ props: {
+ asHeader: false,
+ width: 200,
+ },
+ },
+ },
+ events: {
+ onKeyDown,
+ onBlur: (editor, slate) => () => {
+ TABLE_SLATE_TO_SELECTION_SET.delete(slate);
+ },
+ onBeforeCreate(editor, blockId) {
+ return TableCommands.buildTableElements(editor, { rows: 3, columns: 3 });
},
- 'table-cell': {
- render: TableCellRender,
+ },
+ parsers: {
+ html: {
+ deserialize: {
+ nodeNames: ['TABLE'],
+ parse: deserializeTable,
+ },
+ serialize: serializeTable,
+ },
+ markdown: {
+ serialize: serializeMarkown,
},
},
+ extensions: withTable,
+ options: {
+ display: {
+ title: 'Table',
+ description: 'Add simple table',
+ },
+ shortcuts: ['table', '||', '3x3'],
+ },
+ commands: TableCommands,
});
+
+export { Table };
diff --git a/packages/plugins/table/src/react-svg.d.ts b/packages/plugins/table/src/react-svg.d.ts
new file mode 100644
index 000000000..3f79836c9
--- /dev/null
+++ b/packages/plugins/table/src/react-svg.d.ts
@@ -0,0 +1,6 @@
+declare module '*.svg' {
+ import { ReactElement, SVGProps } from 'react';
+
+ const content: (props: SVGProps) => ReactElement;
+ export default content;
+}
diff --git a/packages/plugins/table/src/styles.css b/packages/plugins/table/src/styles.css
index 3db5b698c..7afe9f56f 100644
--- a/packages/plugins/table/src/styles.css
+++ b/packages/plugins/table/src/styles.css
@@ -1 +1,92 @@
-@tailwind utilities;
\ No newline at end of file
+@tailwind utilities;
+
+.yoopta-table-block {
+ @apply w-full pt-2 pb-2 overflow-x-auto overflow-y-hidden relative;
+}
+
+.yoopta-table-block .yoopta-table-options {
+ opacity: 0;
+ transition: opacity 0.15s ease-in-out;
+ top: 12px;
+}
+
+.yoopta-table-block:hover .yoopta-table-options {
+ opacity: 1;
+}
+
+.yoopta-table {
+ @apply select-none border-collapse border-spacing-0 w-auto caption-bottom text-sm table-fixed;
+}
+
+.yoopta-table-selecting *::selection {
+ background: none;
+}
+
+.yoopta-table tbody {
+ @apply select-none;
+}
+
+.yoopta-table-row {
+ @apply transition-colors relative;
+}
+
+.yoopta-table-row-selected {
+ border-color: #73b6db;
+ border-width: 2px;
+}
+
+.yoopta-table-data-cell {
+ @apply transition-colors text-inherit fill-current border relative align-top min-h-[32px];
+ border: 1px solid #e9e9e7;
+}
+
+.yoopta-table-data-cell[data-cell-selected="true"] {
+ background-color: #37352f14;
+}
+
+.yoopta-table-data-cell-head {
+ background-color: #f7f6f3;
+}
+
+.yoopta-table-data-cell:hover .yoopta-table-column-button {
+ opacity: 1;
+}
+
+.yoopta-table-data-cell:hover .yoopta-table-row-button {
+ opacity: 1;
+}
+
+.yoopta-table-data-cell-content {
+ @apply max-w-full w-full whitespace-pre-wrap break-words p-[7px_9px] bg-transparent text-[14px] leading-[20px];
+ caret-color: rgb(55, 53, 47);
+}
+
+.yoopta-table-column-button {
+ @apply opacity-0 cursor-pointer select-none transition-opacity duration-[150ms] cursor-pointer absolute flex items-center justify-center rounded-[4px] bg-white z-[4] top-[-8px] left-[calc(50%-13px)] h-[16px] w-[26px] p-[2px_4px];
+ composes: yoopta-button;
+ fill: rgba(55, 53, 47, 0.35);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), hsla(0, 0%, 6%, .1) 0px 0px 0px 1px, hsla(0, 0%, 6%, .1) 0px 2px 4px;
+}
+
+.yoopta-table-row-button {
+ @apply opacity-0 cursor-pointer select-none transition-opacity duration-[150ms] cursor-pointer absolute flex items-center justify-center rounded-[4px] bg-white z-[4] top-[calc(50%-13px)] left-[-8px] w-[16px] h-[26px] p-[4px_2px];
+ composes: yoopta-button;
+ fill: rgba(55, 53, 47, 0.35);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), hsla(0, 0%, 6%, .1) 0px 0px 0px 1px, hsla(0, 0%, 6%, .1) 0px 2px 4px;
+}
+
+.yoopta-table-icons {
+ @apply w-4 h-4 mr-2;
+}
+
+.resize-handle {
+ @apply select-none absolute right-0 w-0 top-0 flex-grow-0 h-full z-[1]
+}
+
+.resize-handle-inner {
+ @apply select-none absolute w-[3px] -ml-[1px] -mt-[1px] h-[calc(100%+2px)] transition-[background] duration-[150ms] delay-[50ms] bg-[#2383e200] cursor-col-resize
+}
+
+.resize-handle-inner:hover {
+ background-color: #74b6db;
+}
\ No newline at end of file
diff --git a/packages/plugins/table/src/types.ts b/packages/plugins/table/src/types.ts
index e3fad3720..cc0addb90 100644
--- a/packages/plugins/table/src/types.ts
+++ b/packages/plugins/table/src/types.ts
@@ -1,5 +1,31 @@
-import { SlateElement } from '@yoopta/editor';
+import { PluginOptions, SlateElement } from '@yoopta/editor';
-export type TableElement = SlateElement;
-export type TableCellElement = SlateElement;
+export type TablePluginElementKeys = 'table' | 'table-row' | 'table-data-cell';
+
+export type TableDataCellElementProps = {
+ width: number;
+ asHeader: boolean;
+};
+
+export type TableElementProps = {
+ headerRow?: boolean;
+ headerColumn?: boolean;
+};
+
+export type TableElement = SlateElement<'table', TableElementProps>;
+export type TableCellElement = SlateElement<'table-data-cell', TableDataCellElementProps>;
export type TableRowElement = SlateElement;
+
+export type TableElementMap = {
+ table: TableElement;
+ 'table-data-cell': TableCellElement;
+ 'table-row': TableRowElement;
+};
+
+export type InsertTableOptions = {
+ rows: number;
+ columns: number;
+ columnWidth?: number;
+ headerColumn?: boolean;
+ headerRow?: boolean;
+};
diff --git a/packages/plugins/table/src/utils/weakMaps.ts b/packages/plugins/table/src/utils/weakMaps.ts
new file mode 100644
index 000000000..213a7182a
--- /dev/null
+++ b/packages/plugins/table/src/utils/weakMaps.ts
@@ -0,0 +1,8 @@
+import { SlateEditor, SlateElement } from '@yoopta/editor';
+import { NodeEntry } from 'slate';
+import { TableCellElement, TableRowElement } from '../types';
+
+export type SlateNodeEntry = NodeEntry;
+
+export const EDITOR_TO_SELECTION = new WeakMap();
+export const TABLE_SLATE_TO_SELECTION_SET = new WeakMap>();
diff --git a/packages/plugins/video/package.json b/packages/plugins/video/package.json
index fa8a6a7c2..96564007c 100644
--- a/packages/plugins/video/package.json
+++ b/packages/plugins/video/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/video",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "Video plugin for Yoopta Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -39,5 +39,5 @@
"@radix-ui/react-icons": "^1.3.0",
"re-resizable": "^6.9.11"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/plugins/video/src/commands/index.ts b/packages/plugins/video/src/commands/index.ts
new file mode 100644
index 000000000..c2549f0b3
--- /dev/null
+++ b/packages/plugins/video/src/commands/index.ts
@@ -0,0 +1,36 @@
+import { Blocks, buildBlockData, Elements, generateId, YooEditor, YooptaBlockPath } from '@yoopta/editor';
+import { VideoElement, VideoElementProps } from '../types';
+
+type VideoElementOptions = {
+ props?: Omit;
+};
+
+type InsertVideoOptions = VideoElementOptions & {
+ at?: YooptaBlockPath;
+ focus?: boolean;
+};
+
+export type VideoCommands = {
+ buildVideoElements: (editor: YooEditor, options?: Partial) => VideoElement;
+ insertVideo: (editor: YooEditor, options?: Partial) => void;
+ deleteVideo: (editor: YooEditor, blockId: string) => void;
+ updateVideo: (editor: YooEditor, blockId: string, props: Partial) => void;
+};
+
+export const VideoCommands: VideoCommands = {
+ buildVideoElements: (editor: YooEditor, options = {}) => {
+ const videoProps = options?.props ? { ...options.props, nodeType: 'void' } : { nodeType: 'void' };
+ return { id: generateId(), type: 'video', children: [{ text: '', props: videoProps }] };
+ },
+ insertVideo: (editor: YooEditor, options = {}) => {
+ const { at, focus, props } = options;
+ const video = VideoCommands.buildVideoElements(editor, { props });
+ Blocks.insertBlock(editor, buildBlockData({ value: [video], type: 'Video' }), { focus, at });
+ },
+ deleteVideo: (editor: YooEditor, blockId) => {
+ Blocks.deleteBlock(editor, { blockId });
+ },
+ updateVideo: (editor: YooEditor, blockId, props) => {
+ Elements.updateElement(editor, blockId, { props });
+ },
+};
diff --git a/packages/plugins/video/src/index.ts b/packages/plugins/video/src/index.ts
index ed1a89f8d..c6186fc7e 100644
--- a/packages/plugins/video/src/index.ts
+++ b/packages/plugins/video/src/index.ts
@@ -8,5 +8,7 @@ declare module 'slate' {
}
}
+export { VideoCommands } from './commands';
+
export default Video;
export { VideoElement, VideoElementProps, VideoUploadResponse };
diff --git a/packages/plugins/video/src/plugin/index.tsx b/packages/plugins/video/src/plugin/index.tsx
index abac2d890..79929a041 100644
--- a/packages/plugins/video/src/plugin/index.tsx
+++ b/packages/plugins/video/src/plugin/index.tsx
@@ -1,5 +1,6 @@
import { generateId, YooptaPlugin } from '@yoopta/editor';
-import { VideoElementProps, VideoPluginElements, VideoPluginOptions } from '../types';
+import { VideoCommands } from '../commands';
+import { VideoElementMap, VideoPluginOptions } from '../types';
import { VideoRender } from '../ui/Video';
const ALIGNS_TO_JUSTIFY = {
@@ -8,7 +9,7 @@ const ALIGNS_TO_JUSTIFY = {
right: 'flex-end',
};
-const Video = new YooptaPlugin({
+const Video = new YooptaPlugin({
type: 'Video',
elements: {
// [TODO] - caption element??,
@@ -43,6 +44,7 @@ const Video = new YooptaPlugin",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -38,5 +38,5 @@
"@floating-ui/react": "^0.26.9",
"@radix-ui/react-icons": "^1.3.0"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/tools/action-menu/src/components/ActionMenuList.tsx b/packages/tools/action-menu/src/components/ActionMenuList.tsx
index 107c4c419..e694fc6c9 100644
--- a/packages/tools/action-menu/src/components/ActionMenuList.tsx
+++ b/packages/tools/action-menu/src/components/ActionMenuList.tsx
@@ -235,7 +235,10 @@ const ActionMenuList = ({ items, render }: ActionMenuToolProps) => {
const block = findPluginBlockBySelectionPath(editor, { at: editor.selection });
if (!block) return;
- const slateEditorRef = editor.refElement?.querySelector(`#yoopta-slate-editor-${block.id}`) as HTMLElement;
+ const slateEditorRef = editor.refElement?.querySelector(
+ `[data-yoopta-block-id="${block.id}"] [data-slate-editor="true"]`,
+ ) as HTMLElement;
+
if (!slateEditorRef) return;
slateEditorRef.addEventListener('keydown', handleActionMenuKeyDown);
diff --git a/packages/tools/action-menu/src/components/icons.tsx b/packages/tools/action-menu/src/components/icons.tsx
index be6a30fab..02d793189 100644
--- a/packages/tools/action-menu/src/components/icons.tsx
+++ b/packages/tools/action-menu/src/components/icons.tsx
@@ -13,6 +13,7 @@ import NumberedListIcon from './icons/numbered-list.svg';
import TableIcon from './icons/table.svg';
import CalloutIcon from './icons/callout.svg';
import FileIcon from './icons/file.svg';
+import DividerIcon from './icons/divider.svg';
// LUICIDE icons
export const DEFAULT_ICONS_MAP: Record = {
@@ -31,4 +32,5 @@ export const DEFAULT_ICONS_MAP: Record = {
Table: TableIcon,
Callout: CalloutIcon,
File: FileIcon,
+ Divider: DividerIcon,
};
diff --git a/packages/tools/action-menu/src/components/icons/divider.svg b/packages/tools/action-menu/src/components/icons/divider.svg
new file mode 100644
index 000000000..2514d98a4
--- /dev/null
+++ b/packages/tools/action-menu/src/components/icons/divider.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/tools/link-tool/package.json b/packages/tools/link-tool/package.json
index b20178142..1fe6b247d 100644
--- a/packages/tools/link-tool/package.json
+++ b/packages/tools/link-tool/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/link-tool",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "Link tool for Yoopta Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -34,5 +34,5 @@
"bugs": {
"url": "https://github.com/Darginec05/Editor-Yoopta/issues"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/tools/link-tool/src/components/DefaultLinkToolRender.tsx b/packages/tools/link-tool/src/components/DefaultLinkToolRender.tsx
index 51fe943ea..05f674cf4 100644
--- a/packages/tools/link-tool/src/components/DefaultLinkToolRender.tsx
+++ b/packages/tools/link-tool/src/components/DefaultLinkToolRender.tsx
@@ -1,17 +1,14 @@
import { ChangeEvent, useEffect, useState } from 'react';
import { LinkToolRenderProps, Link } from '../types';
import ChevronUp from '../icons/chevronup.svg';
-
-const DEFAULT_LINK_VALUE: Link = {
- url: '',
- title: '',
- target: '_self',
- rel: 'noreferrer',
-};
+import { useYooptaEditor } from '@yoopta/editor';
const DefaultLinkToolRender = (props: LinkToolRenderProps) => {
const { withLink = true, withTitle = true } = props;
- const [link, setLink] = useState(props?.link || DEFAULT_LINK_VALUE);
+ const editor = useYooptaEditor();
+ const defaultLinkProps = editor.plugins?.LinkPlugin?.elements?.link?.props;
+
+ const [link, setLink] = useState(props?.link || defaultLinkProps);
const [isAdditionalPropsOpen, setAdditionPropsOpen] = useState(false);
const onChange = (e: ChangeEvent) => {
@@ -20,7 +17,15 @@ const DefaultLinkToolRender = (props: LinkToolRenderProps) => {
};
useEffect(() => {
- if (props.link) setLink(props.link);
+ const hasUrl = !!props.link.url;
+ if (hasUrl) setLink(props.link);
+ if (!hasUrl && defaultLinkProps) {
+ setLink({
+ ...props.link,
+ rel: defaultLinkProps.rel || props.link.rel || '',
+ target: defaultLinkProps.target || props.link.target || '_self',
+ });
+ }
}, [props.link]);
const onSave = () => {
@@ -44,7 +49,7 @@ const DefaultLinkToolRender = (props: LinkToolRenderProps) => {
type="text"
className="yoopta-link-tool-input"
name="title"
- value={link.title}
+ value={link.title || ''}
onChange={onChange}
placeholder="Edit link title"
autoComplete="off"
@@ -61,7 +66,7 @@ const DefaultLinkToolRender = (props: LinkToolRenderProps) => {
type="text"
className="yoopta-link-tool-input"
name="url"
- value={link.url}
+ value={link.url || ''}
onChange={onChange}
placeholder="Edit link URL"
autoComplete="off"
diff --git a/packages/tools/toolbar/package.json b/packages/tools/toolbar/package.json
index d16a67df4..65cb8a007 100644
--- a/packages/tools/toolbar/package.json
+++ b/packages/tools/toolbar/package.json
@@ -1,6 +1,6 @@
{
"name": "@yoopta/toolbar",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "Toolbar tool for Yoopta Editor",
"author": "Darginec05 ",
"homepage": "https://github.com/Darginec05/Editor-Yoopta#readme",
@@ -40,5 +40,5 @@
"@radix-ui/react-toolbar": "^1.0.4",
"lodash.throttle": "^4.1.1"
},
- "gitHead": "600e0cf267a0ce7df074ddc7db1114d67c4185d1"
+ "gitHead": "12d8460dfe0ead89ee1d6f3f2f1fc68239e93d4c"
}
diff --git a/packages/tools/toolbar/src/components/DefaultToolbarRender.tsx b/packages/tools/toolbar/src/components/DefaultToolbarRender.tsx
index 6d0da1b39..d7970c345 100644
--- a/packages/tools/toolbar/src/components/DefaultToolbarRender.tsx
+++ b/packages/tools/toolbar/src/components/DefaultToolbarRender.tsx
@@ -159,52 +159,25 @@ const DefaultToolbarRender = ({ activeBlock, editor, toggleHoldToolbar }: Toolba
}, [editor.selection, editor.children, modals.link]);
const onUpdateLink = (link: LinkValues) => {
- const slate = findSlateBySelectionPath(editor);
+ if (!editor.selection) return;
+
+ const slate = Blocks.getSlate(editor, { at: editor.selection });
if (!slate) return;
Editor.withoutNormalizing(slate, () => {
if (!slate.selection) return;
- const linkNodeEntry = getLinkEntry(slate);
-
- if (linkNodeEntry) {
- const [linkNode] = linkNodeEntry as NodeEntry;
- const updatedNode = { props: { ...linkNode?.props, ...link } };
-
- Transforms.setNodes(slate, updatedNode, {
- match: (n) => Element.isElement(n) && (n as SlateElement).type === 'link',
- });
-
- Editor.insertText(slate, link.title || link.url, { at: slate.selection });
- Transforms.collapse(slate, { edge: 'end' });
- } else {
- const defaultLinkProps: Record