From b170900473f3689e354c4d9ab9f52d47d892f314 Mon Sep 17 00:00:00 2001 From: Darginec05 Date: Sun, 5 May 2024 17:12:35 +0300 Subject: [PATCH 01/24] create API methods for manipulating with block elements --- package.json | 2 +- packages/accordion/README.md | 11 ++ packages/accordion/package.json | 40 +++++ packages/accordion/rollup.config.js | 7 + packages/accordion/src/icons/checkmark.svg | 1 + packages/accordion/src/icons/default.svg | 1 + packages/accordion/src/icons/error.svg | 1 + packages/accordion/src/icons/info.svg | 1 + packages/accordion/src/icons/success.svg | 1 + packages/accordion/src/icons/warning.svg | 1 + packages/accordion/src/index.ts | 12 ++ packages/accordion/src/plugin/index.tsx | 52 ++++++ packages/accordion/src/react-svg.d.ts | 6 + .../accordion/src/renders/AccordionItem.tsx | 14 ++ .../src/renders/AccordionItemContent.tsx | 12 ++ .../src/renders/AccordionItemHeading.tsx | 49 ++++++ .../accordion/src/renders/AccordionList.tsx | 12 ++ packages/accordion/src/styles.css | 1 + packages/accordion/src/types.ts | 13 ++ packages/accordion/tsconfig.json | 14 ++ .../src/components/ActionMenuList.tsx | 9 +- .../core/src/editor/elements/createElement.ts | 81 +++++++++ .../core/src/editor/elements/deleteElement.ts | 32 ++++ .../core/src/editor/elements/getElement.ts | 29 ++++ .../updateElement.ts} | 2 - .../core/src/editor/transforms/createBlock.ts | 3 + .../src/editor/transforms/getBlockElement.ts | 1 - packages/core/src/editor/types.ts | 18 +- packages/core/src/utils/editorBuilders.ts | 19 ++- packages/development/package.json | 3 +- .../customPlugins/Accordion/Accordion.tsx | 66 ++++++++ packages/development/src/pages/dev/index.tsx | 14 +- yarn.lock | 158 +----------------- 33 files changed, 516 insertions(+), 170 deletions(-) create mode 100644 packages/accordion/README.md create mode 100644 packages/accordion/package.json create mode 100644 packages/accordion/rollup.config.js create mode 100644 packages/accordion/src/icons/checkmark.svg create mode 100644 packages/accordion/src/icons/default.svg create mode 100644 packages/accordion/src/icons/error.svg create mode 100644 packages/accordion/src/icons/info.svg create mode 100644 packages/accordion/src/icons/success.svg create mode 100644 packages/accordion/src/icons/warning.svg create mode 100644 packages/accordion/src/index.ts create mode 100644 packages/accordion/src/plugin/index.tsx create mode 100644 packages/accordion/src/react-svg.d.ts create mode 100644 packages/accordion/src/renders/AccordionItem.tsx create mode 100644 packages/accordion/src/renders/AccordionItemContent.tsx create mode 100644 packages/accordion/src/renders/AccordionItemHeading.tsx create mode 100644 packages/accordion/src/renders/AccordionList.tsx create mode 100644 packages/accordion/src/styles.css create mode 100644 packages/accordion/src/types.ts create mode 100644 packages/accordion/tsconfig.json create mode 100644 packages/core/src/editor/elements/createElement.ts create mode 100644 packages/core/src/editor/elements/deleteElement.ts create mode 100644 packages/core/src/editor/elements/getElement.ts rename packages/core/src/editor/{transforms/updateBlockElement.ts => elements/updateElement.ts} (97%) delete mode 100644 packages/core/src/editor/transforms/getBlockElement.ts create mode 100644 packages/development/src/components/customPlugins/Accordion/Accordion.tsx diff --git a/package.json b/package.json index 69d568117..2eb4d4847 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ ], "private": true, "scripts": { - "start": "yarn lerna run start --scope @yoopta/editor --scope @yoopta/embed --scope @yoopta/action-menu-list --scope @yoopta/toolbar --scope @yoopta/paragraph --scope @yoopta/blockquote --scope @yoopta/headings --parallel --ignore development", + "start": "yarn lerna run start --scope @yoopta/editor --scope @yoopta/lists --scope @yoopta/accordion --parallel --ignore development", "build": "yarn clean && yarn lerna run build --parallel --ignore development", "clean": "find ./packages -type d -name dist ! -path './packages/development/*' -exec rm -rf {} +", "serve": "cd ./packages/development && yarn dev", diff --git a/packages/accordion/README.md b/packages/accordion/README.md new file mode 100644 index 000000000..8b44db00b --- /dev/null +++ b/packages/accordion/README.md @@ -0,0 +1,11 @@ +# `yoopta-accordion` + +> TODO: description + +## Usage + +``` +const accordion = require('yoopta-accordion'); + +// TODO: DEMONSTRATE API +``` diff --git a/packages/accordion/package.json b/packages/accordion/package.json new file mode 100644 index 000000000..8c68e2d2c --- /dev/null +++ b/packages/accordion/package.json @@ -0,0 +1,40 @@ +{ + "name": "@yoopta/accordion", + "version": "4.0.0", + "description": "Accordion 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-rc.0", + "react": ">=17.0.2", + "react-dom": ">=17.0.2" + }, + "dependencies": { + "lucide-react": "^0.378.0" + }, + "publishConfig": { + "registry": "https://registry.yarnpkg.com" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Darginec05/Editor-Yoopta.git" + }, + "scripts": { + "test": "node ./__tests__/yoopta-accordion.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" + } +} diff --git a/packages/accordion/rollup.config.js b/packages/accordion/rollup.config.js new file mode 100644 index 000000000..0c2569789 --- /dev/null +++ b/packages/accordion/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}'], prefix: 'yoo-accordion-' }, +}); diff --git a/packages/accordion/src/icons/checkmark.svg b/packages/accordion/src/icons/checkmark.svg new file mode 100644 index 000000000..f156213cc --- /dev/null +++ b/packages/accordion/src/icons/checkmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/accordion/src/icons/default.svg b/packages/accordion/src/icons/default.svg new file mode 100644 index 000000000..57225f71b --- /dev/null +++ b/packages/accordion/src/icons/default.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/accordion/src/icons/error.svg b/packages/accordion/src/icons/error.svg new file mode 100644 index 000000000..73b084f3b --- /dev/null +++ b/packages/accordion/src/icons/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/accordion/src/icons/info.svg b/packages/accordion/src/icons/info.svg new file mode 100644 index 000000000..38a35652e --- /dev/null +++ b/packages/accordion/src/icons/info.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/accordion/src/icons/success.svg b/packages/accordion/src/icons/success.svg new file mode 100644 index 000000000..9052a4820 --- /dev/null +++ b/packages/accordion/src/icons/success.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/accordion/src/icons/warning.svg b/packages/accordion/src/icons/warning.svg new file mode 100644 index 000000000..387b63b4c --- /dev/null +++ b/packages/accordion/src/icons/warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/accordion/src/index.ts b/packages/accordion/src/index.ts new file mode 100644 index 000000000..fbb85b150 --- /dev/null +++ b/packages/accordion/src/index.ts @@ -0,0 +1,12 @@ +import { Accordion } from './plugin'; +import { AccordionItemElement } from './types'; +import './styles.css'; + +declare module 'slate' { + interface CustomTypes { + Element: AccordionItemElement; + } +} + +export default Accordion; +export { AccordionItemElement }; diff --git a/packages/accordion/src/plugin/index.tsx b/packages/accordion/src/plugin/index.tsx new file mode 100644 index 000000000..23630b740 --- /dev/null +++ b/packages/accordion/src/plugin/index.tsx @@ -0,0 +1,52 @@ +import { YooptaPlugin } from '@yoopta/editor'; +import { AccordionElementKeys, AccordionListItemProps } from '../types'; +import { AccordionList } from '../renders/AccordionList'; +import { AccordionItem } from '../renders/AccordionItem'; +import { AccordionItemHeading } from '../renders/AccordionItemHeading'; +import { AccordionItemContent } from '../renders/AccordionItemContent'; + +const Accordion = new YooptaPlugin({ + type: 'Accordion', + elements: { + 'accordion-list': { + asRoot: true, + render: AccordionList, + children: ['accordion-list-item'], + }, + 'accordion-list-item': { + render: AccordionItem, + children: ['accordion-list-item-heading', 'accordion-list-item-content'], + props: { isExpanded: false }, + }, + 'accordion-list-item-heading': { + render: AccordionItemHeading, + }, + 'accordion-list-item-content': { + render: AccordionItemContent, + }, + }, + events: { + onKeyDown(editor, slate, { hotkeys, currentBlock }) { + return (event) => { + if (hotkeys.isEnter(event)) { + event.preventDefault(); + + editor.blocks.Accordion.createElement( + currentBlock.id, + 'accordion-list-item', + { isExpanded: true }, + { at: 'next', focus: true }, + ); + } + }; + }, + }, + options: { + display: { + title: 'Accordion', + description: 'Create collapses', + }, + }, +}); + +export { Accordion }; diff --git a/packages/accordion/src/react-svg.d.ts b/packages/accordion/src/react-svg.d.ts new file mode 100644 index 000000000..3f79836c9 --- /dev/null +++ b/packages/accordion/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/accordion/src/renders/AccordionItem.tsx b/packages/accordion/src/renders/AccordionItem.tsx new file mode 100644 index 000000000..2f0eb65ea --- /dev/null +++ b/packages/accordion/src/renders/AccordionItem.tsx @@ -0,0 +1,14 @@ +import { PluginElementRenderProps } from '@yoopta/editor'; + +export const AccordionItem = (props: PluginElementRenderProps) => { + const { element, attributes, children } = props; + + return ( +
  • + {children} +
  • + ); +}; diff --git a/packages/accordion/src/renders/AccordionItemContent.tsx b/packages/accordion/src/renders/AccordionItemContent.tsx new file mode 100644 index 000000000..ee692d96e --- /dev/null +++ b/packages/accordion/src/renders/AccordionItemContent.tsx @@ -0,0 +1,12 @@ +import { PluginElementRenderProps, useYooptaEditor } from '@yoopta/editor'; +import { useEffect, useState } from 'react'; + +export const AccordionItemContent = (props: PluginElementRenderProps) => { + const { element, attributes, children, blockId } = props; + + return ( +

    + {children} +

    + ); +}; diff --git a/packages/accordion/src/renders/AccordionItemHeading.tsx b/packages/accordion/src/renders/AccordionItemHeading.tsx new file mode 100644 index 000000000..29f4833d5 --- /dev/null +++ b/packages/accordion/src/renders/AccordionItemHeading.tsx @@ -0,0 +1,49 @@ +import { PluginElementRenderProps, useYooptaEditor, useYooptaReadOnly } from '@yoopta/editor'; +import { ChevronUp, Plus } from 'lucide-react'; + +export const AccordionItemHeading = (props: PluginElementRenderProps) => { + const { element, attributes, children, blockId } = props; + const editor = useYooptaEditor(); + const isReadOnly = useYooptaReadOnly(); + + const onToggleExpand = () => { + const listItemEntry = editor.blocks.Accordion.getElement(blockId, 'accordion-list-item'); + + if (listItemEntry) { + const listItemElement = listItemEntry[0]; + + editor.blocks.Accordion.updateElement(blockId, 'accordion-list-item', { + isExpanded: !listItemElement.props?.isExpanded, + }); + } + }; + + const onAddAccordionItem = () => { + editor.blocks.Accordion.createElement( + blockId, + 'accordion-list-item', + { isExpanded: true }, + { at: 'next', focus: true }, + ); + }; + + return ( + + ); +}; diff --git a/packages/accordion/src/renders/AccordionList.tsx b/packages/accordion/src/renders/AccordionList.tsx new file mode 100644 index 000000000..629b68a31 --- /dev/null +++ b/packages/accordion/src/renders/AccordionList.tsx @@ -0,0 +1,12 @@ +import { PluginElementRenderProps, useYooptaEditor } from '@yoopta/editor'; +import { useEffect } from 'react'; + +export const AccordionList = (props: PluginElementRenderProps) => { + const { element, attributes, children, blockId } = props; + + return ( +
      + {children} +
    + ); +}; diff --git a/packages/accordion/src/styles.css b/packages/accordion/src/styles.css new file mode 100644 index 000000000..3db5b698c --- /dev/null +++ b/packages/accordion/src/styles.css @@ -0,0 +1 @@ +@tailwind utilities; \ No newline at end of file diff --git a/packages/accordion/src/types.ts b/packages/accordion/src/types.ts new file mode 100644 index 000000000..13cea2041 --- /dev/null +++ b/packages/accordion/src/types.ts @@ -0,0 +1,13 @@ +import { SlateElement } from '@yoopta/editor'; + +export type AccordionElementKeys = + | 'accordion-list' + | 'accordion-list-item' + | 'accordion-list-item-heading' + | 'accordion-list-item-content'; + +export type AccordionListItemProps = { + isExpanded: boolean; +}; + +export type AccordionItemElement = SlateElement<'accordion-list-item', AccordionListItemProps>; diff --git a/packages/accordion/tsconfig.json b/packages/accordion/tsconfig.json new file mode 100644 index 000000000..c26af02d3 --- /dev/null +++ b/packages/accordion/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../config/tsconfig.base.json", + "include": ["src/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" + } + ] +} diff --git a/packages/action-menu/src/components/ActionMenuList.tsx b/packages/action-menu/src/components/ActionMenuList.tsx index 61c910061..c55f4239c 100644 --- a/packages/action-menu/src/components/ActionMenuList.tsx +++ b/packages/action-menu/src/components/ActionMenuList.tsx @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { DefaultActionMenuRender } from './DefaultActionMenuRender'; import { useFloating, @@ -18,7 +18,6 @@ import { findSlateBySelectionPath, HOTKEYS, findPluginBlockBySelectionPath, - YooEditor, } from '@yoopta/editor'; import { ActionMenuRenderProps, ActionMenuToolItem, ActionMenuToolProps } from '../types'; import { buildActionMenuRenderProps, mapActionMenuItems } from './utils'; @@ -39,7 +38,7 @@ const filterActionMenuItems = (block: YooptaBlock, text: string) => { }; // [TODO] - add to props -const trigger = '/'; +const TRIGGER = '/'; const ActionMenuList = ({ items, render }: ActionMenuToolProps) => { const editor = useYooptaEditor(); @@ -71,9 +70,9 @@ const ActionMenuList = ({ items, render }: ActionMenuToolProps) => { const onClose = () => setIsMenuOpen(false); const onFilter = ({ text }) => { - const string = text.trim().replace(trigger, ''); + const string = text.trim().replace(TRIGGER, ''); - if (string.length === 0 || string === trigger) return setActions(blockTypes); + if (string.length === 0 || string === TRIGGER) return setActions(blockTypes); const filteredActions = actions.filter((action) => filterActionMenuItems(editor.blocks[action.type], string)); const isSelectedItemInsideFilteredActions = filteredActions.some((item) => item.type === selectedAction.type); if (filteredActions.length > 0 && !isSelectedItemInsideFilteredActions) setSelectedAction(filteredActions[0]); diff --git a/packages/core/src/editor/elements/createElement.ts b/packages/core/src/editor/elements/createElement.ts new file mode 100644 index 000000000..094e4b64b --- /dev/null +++ b/packages/core/src/editor/elements/createElement.ts @@ -0,0 +1,81 @@ +import { Editor, Path, Transforms } from 'slate'; +import { buildBlockElement } from '../../components/Editor/utils'; +import { findSlateBySelectionPath } from '../../utils/findSlateBySelectionPath'; +import { SlateElement, YooEditor } from '../types'; + +export type CreateBlockElementOptions = { + at?: 'next' | 'last' | 'first' | Path | 'prev'; + focus?: boolean; +}; + +export function createBlockElement( + editor: YooEditor, + blockId: string, + elementType: TElementKeys, + elementProps?: TElementProps, + options?: CreateBlockElementOptions, +) { + const blockData = editor.children[blockId]; + if (!blockData) { + throw new Error(`Block with id ${blockId} not found`); + } + + const slate = findSlateBySelectionPath(editor, { at: [blockData.meta.order] }); + if (!slate) { + console.warn('No slate found'); + return; + } + + Editor.withoutNormalizing(slate, () => { + const block = editor.blocks[blockData.type]; + const blockElement = block.elements[elementType]; + const nodeElement = buildBlockElement({ type: elementType, props: { ...blockElement.props, ...elementProps } }); + + const elementTypes = Object.keys(block.elements); + + let parentElement; + let childrenElements: SlateElement[] = []; + + elementTypes.forEach((blockElementType) => { + const blockElement = block.elements[blockElementType]; + const hasParentBlockElement = blockElement.children?.includes(elementType); + + if (blockElementType === elementType) { + if (Array.isArray(blockElement.children) && blockElement.children.length > 0) { + blockElement.children.forEach((childElementType) => { + const childElement = block.elements[childElementType]; + childrenElements.push(buildBlockElement({ type: childElementType, props: childElement.props })); + }); + } + } + + if (hasParentBlockElement) { + parentElement = buildBlockElement({ type: blockElementType, props: blockElement.props }); + } + }); + + if (childrenElements.length > 0) nodeElement.children = childrenElements; + + const { at, focus = true } = options || {}; + let atPath; + + const currentElementEntry = editor.blocks[block.type].getElement(blockId, elementType); + + if (currentElementEntry) { + const [, elementPath] = currentElementEntry; + + if (Path.isPath(at)) { + atPath = at; + } else if (at === 'prev') { + atPath = Path.previous(elementPath); + } else if (at === 'next') { + atPath = Path.next(elementPath); + } + } + + Transforms.insertNodes(slate, nodeElement, { at: atPath, select: focus }); + + editor.applyChanges(); + editor.emit('change', editor.children); + }); +} diff --git a/packages/core/src/editor/elements/deleteElement.ts b/packages/core/src/editor/elements/deleteElement.ts new file mode 100644 index 000000000..6437f82e3 --- /dev/null +++ b/packages/core/src/editor/elements/deleteElement.ts @@ -0,0 +1,32 @@ +import { Editor, Element, Transforms } from 'slate'; +import { findSlateBySelectionPath } from '../../utils/findSlateBySelectionPath'; +import { SlateElement, YooEditor } from '../types'; + +export function deleteBlockElement( + editor: YooEditor, + blockId: string, + elementType: TElementKeys, +) { + const block = editor.children[blockId]; + + if (!block) { + throw new Error(`Block with id ${blockId} not found`); + } + + const slate = findSlateBySelectionPath(editor, { at: [block.meta.order] }); + + if (!slate) { + console.warn('No slate found'); + return; + } + + Editor.withoutNormalizing(slate, () => { + const [elementEntry] = Editor.nodes(slate, { + at: [0], + match: (n) => Element.isElement(n) && n.type === elementType, + }); + + editor.applyChanges(); + editor.emit('change', editor.children); + }); +} diff --git a/packages/core/src/editor/elements/getElement.ts b/packages/core/src/editor/elements/getElement.ts new file mode 100644 index 000000000..ffb59eac4 --- /dev/null +++ b/packages/core/src/editor/elements/getElement.ts @@ -0,0 +1,29 @@ +import { Editor, Element, NodeEntry, Transforms } from 'slate'; +import { findSlateBySelectionPath } from '../../utils/findSlateBySelectionPath'; +import { SlateElement, YooEditor } from '../types'; + +export function getBlockElement( + editor: YooEditor, + blockId: string, + elementType: TElementKeys, +): NodeEntry> | undefined { + const block = editor.children[blockId]; + + if (!block) { + throw new Error(`Block with id ${blockId} not found`); + } + + const slate = findSlateBySelectionPath(editor, { at: [block.meta.order] }); + + if (!slate) { + console.warn('No slate found'); + return; + } + + const [elementEntry] = Editor.nodes(slate, { + at: slate.selection || [0], + match: (n) => Element.isElement(n) && n.type === elementType, + }); + + return elementEntry as NodeEntry>; +} diff --git a/packages/core/src/editor/transforms/updateBlockElement.ts b/packages/core/src/editor/elements/updateElement.ts similarity index 97% rename from packages/core/src/editor/transforms/updateBlockElement.ts rename to packages/core/src/editor/elements/updateElement.ts index 925ae65aa..f1ad47f10 100644 --- a/packages/core/src/editor/transforms/updateBlockElement.ts +++ b/packages/core/src/editor/elements/updateElement.ts @@ -37,8 +37,6 @@ export function updateBlockElement( mode: 'lowest', }); - // block.value = slate.children; - editor.applyChanges(); editor.emit('change', editor.children); }); diff --git a/packages/core/src/editor/transforms/createBlock.ts b/packages/core/src/editor/transforms/createBlock.ts index 0ac398088..88feb1ee6 100644 --- a/packages/core/src/editor/transforms/createBlock.ts +++ b/packages/core/src/editor/transforms/createBlock.ts @@ -27,6 +27,9 @@ export function createBlock(editor: YooEditor, type: string, options?: CreateBlo if (!rootBlockElement) return; const nodeProps = { nodeType: rootBlockElement.props?.nodeType || 'block', ...rootBlockElement.props }; + + console.log('nodeProps', nodeProps); + const elementNode = buildBlockElement({ id: generateId(), type: rootBlockElementType, diff --git a/packages/core/src/editor/transforms/getBlockElement.ts b/packages/core/src/editor/transforms/getBlockElement.ts deleted file mode 100644 index cb0ff5c3b..000000000 --- a/packages/core/src/editor/transforms/getBlockElement.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/core/src/editor/types.ts b/packages/core/src/editor/types.ts index 530cdc89e..c539f9e2b 100644 --- a/packages/core/src/editor/types.ts +++ b/packages/core/src/editor/types.ts @@ -1,5 +1,6 @@ -import { Descendant, Editor, Path, Point } from 'slate'; +import { Descendant, Editor, NodeEntry, Path, Point } from 'slate'; import { Plugin, PluginElementsMap, PluginOptions, PluginElementProps } from '../plugins/types'; +import { CreateBlockElementOptions } from './elements/createElement'; import { EditorBlurOptions } from './selection/blur'; import { BlockSelectedOptions } from './selection/setBlockSelected'; import { SetSelectionOptions } from './selection/setSelection'; @@ -59,12 +60,23 @@ export type YooptaBlock = { create: (options?: CreateBlockOptions) => void; toggle: (options?: ToggleBlockOptions) => void; update: (id: string, data: Partial) => void; + delete: (options: DeleteBlockOptions) => void; updateElement: ( blockId: string, elementType: TElementKeys, - elementProps: TElementProps, + elementProps?: TElementProps, ) => void; - delete: (options: DeleteBlockOptions) => void; + createElement: ( + blockId: string, + elementType: TElementKeys, + elementProps?: TElementProps, + options?: CreateBlockElementOptions, + ) => void; + getElement: ( + blockId: string, + elementType: TElementKeys, + ) => NodeEntry> | undefined; + deleteElement: (blockId: string, elementType: TElementKeys) => void; }; export type YooptaBlocks = Record; diff --git a/packages/core/src/utils/editorBuilders.ts b/packages/core/src/utils/editorBuilders.ts index e495e1e49..8f1a67b44 100644 --- a/packages/core/src/utils/editorBuilders.ts +++ b/packages/core/src/utils/editorBuilders.ts @@ -13,9 +13,12 @@ import { update } from '../editor/textFormats/update'; import { withShortcuts } from '../extensions/shortcuts'; import { getRootBlockElement } from './blockElements'; import { updateBlock } from '../editor/transforms/updateBlock'; -import { updateBlockElement } from '../editor/transforms/updateBlockElement'; import { toggleBlock, ToggleBlockOptions } from '../editor/transforms/toggleBlock'; import { deleteBlock, DeleteBlockOptions } from '../editor/transforms/deleteBlock'; +import { updateBlockElement } from '../editor/elements/updateElement'; +import { createBlockElement, CreateBlockElementOptions } from '../editor/elements/createElement'; +import { getBlockElement } from '../editor/elements/getElement'; +import { deleteBlockElement } from '../editor/elements/deleteElement'; export function buildMarks(editor, marks: YooptaMark[]) { const formats: YooEditor['formats'] = {}; @@ -79,6 +82,20 @@ export function buildBlocks(editor, plugins: Plugin(blockId: string, elementType: TKeys, props: TProps) => { updateBlockElement(editor, blockId, elementType, props); }, + createElement: ( + blockId: string, + elementType: TKeys, + props: TProps, + options?: CreateBlockElementOptions, + ) => { + createBlockElement(editor, blockId, elementType, props, options); + }, + getElement: (blockId: string, elementType: TKeys) => { + return getBlockElement(editor, blockId, elementType); + }, + deleteElement: (blockId: string, elementType: TKeys) => { + deleteBlockElement(editor, blockId, elementType); + }, delete: (options: DeleteBlockOptions) => { deleteBlock(editor, options); }, diff --git a/packages/development/package.json b/packages/development/package.json index 3ef9f855c..b8fa67c38 100644 --- a/packages/development/package.json +++ b/packages/development/package.json @@ -27,9 +27,10 @@ "@yoopta/table": "*", "@yoopta/toolbar": "*", "@yoopta/video": "*", + "@yoopta/accordion": "*", "classnames": "^2.5.1", "katex": "^0.16.10", - "lucide-react": "^0.365.0", + "lucide-react": "^0.378.0", "next": "14.1.0", "react": "^18", "react-dom": "^18", diff --git a/packages/development/src/components/customPlugins/Accordion/Accordion.tsx b/packages/development/src/components/customPlugins/Accordion/Accordion.tsx new file mode 100644 index 000000000..d6e880e9d --- /dev/null +++ b/packages/development/src/components/customPlugins/Accordion/Accordion.tsx @@ -0,0 +1,66 @@ +import { generateId, SlateElement, YooptaBlockData } from '@yoopta/editor'; + +// CREATE +const useBlockEventEmitter = () => {}; + +const ACCORDION_VALUE: SlateElement[] = [ + { + id: generateId(), + type: 'accordion-list', + children: [ + { + id: generateId(), + type: 'accordion-list-item', + props: { + isExpanded: false, + }, + children: [ + { id: generateId(), type: 'accordion-list-item-heading', children: [{ text: 'Title 1' }] }, + { + id: generateId(), + type: 'accordion-list-item-content', + children: [ + { + text: `This is the first item's accordion body. It is shown by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow.`, + }, + ], + }, + ], + }, + { + id: generateId(), + type: 'accordion-list-item', + props: { + isExpanded: false, + }, + children: [ + { id: generateId(), type: 'accordion-list-item-heading', children: [{ text: 'Title one' }] }, + { + id: generateId(), + type: 'accordion-list-item-content', + children: [ + { + text: `This is the first item's accordion body. It is shown by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the .accordion-body, though the transition does limit overflow.`, + }, + ], + }, + ], + }, + ], + }, +]; + +export const ACCORDION_BLOCK: YooptaBlockData = { + id: generateId(), + type: 'Accordion', + meta: { order: 0, depth: 0 }, + value: ACCORDION_VALUE, +}; + +/** +// WRITE about this +* - document + * - block + * - inline + * - text + */ diff --git a/packages/development/src/pages/dev/index.tsx b/packages/development/src/pages/dev/index.tsx index cf8f0d612..999ac61b3 100644 --- a/packages/development/src/pages/dev/index.tsx +++ b/packages/development/src/pages/dev/index.tsx @@ -1,4 +1,4 @@ -import YooptaEditor, { createYooptaEditor, Tools, YooEditor, YooptaBlockData } from '@yoopta/editor'; +import YooptaEditor, { createYooptaEditor, Tools, YooEditor, YooptaBlockData, YooptaPlugin } from '@yoopta/editor'; import Blockquote from '@yoopta/blockquote'; import Paragraph from '@yoopta/paragraph'; import Headings from '@yoopta/headings'; @@ -10,6 +10,7 @@ import Link from '@yoopta/link'; import Video from '@yoopta/video'; import File from '@yoopta/file'; import Embed from '@yoopta/embed'; +import AccordionPlugin from '@yoopta/accordion'; import ActionMenuList, { DefaultActionMenuRender } from '@yoopta/action-menu-list'; import LinkTool, { DefaultLinkToolRender } from '@yoopta/link-tool'; import Toolbar, { DefaultToolbarRender } from '@yoopta/toolbar'; @@ -17,13 +18,13 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { uploadToCloudinary } from '../../utils/cloudinary'; import Code from '@yoopta/code'; -import { BaseElement } from 'slate'; import { ActionNotionMenuExample } from '../../components/ActionMenuExamples/NotionExample/ActionNotionMenuExample'; -import { SlackChat } from '../../components/Chats/SlackChat/SlackChat'; import { NotionToolbar } from '../../components/Toolbars/NotionToolbar/NotionToolbar'; +import { ACCORDION_BLOCK } from '../../components/customPlugins/Accordion/Accordion'; // import Mention from '@yoopta/mention'; const plugins = [ + AccordionPlugin, Code, File.extend({ options: { @@ -585,6 +586,7 @@ const value = { }, }, }; + const BasicExample = () => { const editor: YooEditor = useMemo(() => createYooptaEditor(), []); const rectangleSelectionRef = useRef(null); @@ -600,18 +602,18 @@ const BasicExample = () => { }; return ( -
    +
    + +
    + )} + + ); +}; diff --git a/packages/development/src/components/customPlugins/Accordion/src/renders/AccordionList.tsx b/packages/development/src/components/customPlugins/Accordion/src/renders/AccordionList.tsx new file mode 100644 index 000000000..ad4c34fca --- /dev/null +++ b/packages/development/src/components/customPlugins/Accordion/src/renders/AccordionList.tsx @@ -0,0 +1,12 @@ +import { PluginElementRenderProps, useYooptaEditor } from '@yoopta/editor'; +import { useEffect } from 'react'; + +export const AccordionList = (props: PluginElementRenderProps) => { + const { element, attributes, children, blockId } = props; + + return ( +
      + {children} +
    + ); +}; diff --git a/packages/development/src/components/customPlugins/Accordion/src/renders/AccordionListItem.tsx b/packages/development/src/components/customPlugins/Accordion/src/renders/AccordionListItem.tsx new file mode 100644 index 000000000..593b60b67 --- /dev/null +++ b/packages/development/src/components/customPlugins/Accordion/src/renders/AccordionListItem.tsx @@ -0,0 +1,11 @@ +import { PluginElementRenderProps } from '@yoopta/editor'; + +export const AccordionListItem = (props: PluginElementRenderProps) => { + const { element, attributes, children } = props; + + return ( +
  • + {children} +
  • + ); +}; diff --git a/packages/development/src/components/customPlugins/Accordion/src/types.ts b/packages/development/src/components/customPlugins/Accordion/src/types.ts new file mode 100644 index 000000000..13cea2041 --- /dev/null +++ b/packages/development/src/components/customPlugins/Accordion/src/types.ts @@ -0,0 +1,13 @@ +import { SlateElement } from '@yoopta/editor'; + +export type AccordionElementKeys = + | 'accordion-list' + | 'accordion-list-item' + | 'accordion-list-item-heading' + | 'accordion-list-item-content'; + +export type AccordionListItemProps = { + isExpanded: boolean; +}; + +export type AccordionItemElement = SlateElement<'accordion-list-item', AccordionListItemProps>; diff --git a/packages/development/src/pages/dev/index.tsx b/packages/development/src/pages/dev/index.tsx index 400b72c79..42d2a3d26 100644 --- a/packages/development/src/pages/dev/index.tsx +++ b/packages/development/src/pages/dev/index.tsx @@ -10,7 +10,7 @@ import Link from '@yoopta/link'; import Video from '@yoopta/video'; import File from '@yoopta/file'; import Embed from '@yoopta/embed'; -import AccordionPlugin from '@yoopta/accordion'; +// import AccordionPlugin from '@yoopta/accordion'; import ActionMenuList, { DefaultActionMenuRender } from '@yoopta/action-menu-list'; import LinkTool, { DefaultLinkToolRender } from '@yoopta/link-tool'; import Toolbar, { DefaultToolbarRender } from '@yoopta/toolbar'; @@ -21,10 +21,11 @@ import Code from '@yoopta/code'; import { ActionNotionMenuExample } from '../../components/ActionMenuExamples/NotionExample/ActionNotionMenuExample'; import { NotionToolbar } from '../../components/Toolbars/NotionToolbar/NotionToolbar'; import { ACCORDION_BLOCK } from '../../components/customPlugins/Accordion/Accordion'; +import Accordion from '../../components/customPlugins/Accordion/src'; // import Mention from '@yoopta/mention'; const plugins = [ - AccordionPlugin, + Accordion, Code, File.extend({ options: { From 3bd8cc45ce4a7a6682728048ebd92b73a216724a Mon Sep 17 00:00:00 2001 From: Darginec05 Date: Fri, 17 May 2024 15:18:59 +0300 Subject: [PATCH 04/24] merge --- .../core/{ => editor}/src/editor/elements/createElement.ts | 0 .../core/{ => editor}/src/editor/elements/deleteElement.ts | 0 .../core/{ => editor}/src/editor/elements/getElement.ts | 0 .../{ => editor}/src/editor/elements/getElementEntry.ts | 0 .../core/{ => editor}/src/editor/elements/isElementEmpty.ts | 0 .../core/{ => editor}/src/editor/elements/splitElement.ts | 0 .../core/{ => editor}/src/editor/elements/updateElement.ts | 0 packages/{ => plugins}/accordion/README.md | 0 packages/{ => plugins}/accordion/package.json | 0 packages/{ => plugins}/accordion/rollup.config.js | 2 +- packages/{ => plugins}/accordion/src/icons/checkmark.svg | 0 packages/{ => plugins}/accordion/src/icons/default.svg | 0 packages/{ => plugins}/accordion/src/icons/error.svg | 0 packages/{ => plugins}/accordion/src/icons/info.svg | 0 packages/{ => plugins}/accordion/src/icons/success.svg | 0 packages/{ => plugins}/accordion/src/icons/warning.svg | 0 packages/{ => plugins}/accordion/src/index.ts | 0 packages/{ => plugins}/accordion/src/plugin/index.tsx | 0 packages/{ => plugins}/accordion/src/react-svg.d.ts | 0 .../accordion/src/renders/AccordionItemContent.tsx | 0 .../accordion/src/renders/AccordionItemHeading.tsx | 0 .../{ => plugins}/accordion/src/renders/AccordionList.tsx | 0 .../accordion/src/renders/AccordionListItem.tsx | 0 packages/{ => plugins}/accordion/src/styles.css | 0 packages/{ => plugins}/accordion/src/types.ts | 0 packages/{ => plugins}/accordion/tsconfig.json | 6 +++--- 26 files changed, 4 insertions(+), 4 deletions(-) rename packages/core/{ => editor}/src/editor/elements/createElement.ts (100%) rename packages/core/{ => editor}/src/editor/elements/deleteElement.ts (100%) rename packages/core/{ => editor}/src/editor/elements/getElement.ts (100%) rename packages/core/{ => editor}/src/editor/elements/getElementEntry.ts (100%) rename packages/core/{ => editor}/src/editor/elements/isElementEmpty.ts (100%) rename packages/core/{ => editor}/src/editor/elements/splitElement.ts (100%) rename packages/core/{ => editor}/src/editor/elements/updateElement.ts (100%) rename packages/{ => plugins}/accordion/README.md (100%) rename packages/{ => plugins}/accordion/package.json (100%) rename packages/{ => plugins}/accordion/rollup.config.js (74%) rename packages/{ => plugins}/accordion/src/icons/checkmark.svg (100%) rename packages/{ => plugins}/accordion/src/icons/default.svg (100%) rename packages/{ => plugins}/accordion/src/icons/error.svg (100%) rename packages/{ => plugins}/accordion/src/icons/info.svg (100%) rename packages/{ => plugins}/accordion/src/icons/success.svg (100%) rename packages/{ => plugins}/accordion/src/icons/warning.svg (100%) rename packages/{ => plugins}/accordion/src/index.ts (100%) rename packages/{ => plugins}/accordion/src/plugin/index.tsx (100%) rename packages/{ => plugins}/accordion/src/react-svg.d.ts (100%) rename packages/{ => plugins}/accordion/src/renders/AccordionItemContent.tsx (100%) rename packages/{ => plugins}/accordion/src/renders/AccordionItemHeading.tsx (100%) rename packages/{ => plugins}/accordion/src/renders/AccordionList.tsx (100%) rename packages/{ => plugins}/accordion/src/renders/AccordionListItem.tsx (100%) rename packages/{ => plugins}/accordion/src/styles.css (100%) rename packages/{ => plugins}/accordion/src/types.ts (100%) rename packages/{ => plugins}/accordion/tsconfig.json (55%) diff --git a/packages/core/src/editor/elements/createElement.ts b/packages/core/editor/src/editor/elements/createElement.ts similarity index 100% rename from packages/core/src/editor/elements/createElement.ts rename to packages/core/editor/src/editor/elements/createElement.ts diff --git a/packages/core/src/editor/elements/deleteElement.ts b/packages/core/editor/src/editor/elements/deleteElement.ts similarity index 100% rename from packages/core/src/editor/elements/deleteElement.ts rename to packages/core/editor/src/editor/elements/deleteElement.ts diff --git a/packages/core/src/editor/elements/getElement.ts b/packages/core/editor/src/editor/elements/getElement.ts similarity index 100% rename from packages/core/src/editor/elements/getElement.ts rename to packages/core/editor/src/editor/elements/getElement.ts diff --git a/packages/core/src/editor/elements/getElementEntry.ts b/packages/core/editor/src/editor/elements/getElementEntry.ts similarity index 100% rename from packages/core/src/editor/elements/getElementEntry.ts rename to packages/core/editor/src/editor/elements/getElementEntry.ts diff --git a/packages/core/src/editor/elements/isElementEmpty.ts b/packages/core/editor/src/editor/elements/isElementEmpty.ts similarity index 100% rename from packages/core/src/editor/elements/isElementEmpty.ts rename to packages/core/editor/src/editor/elements/isElementEmpty.ts diff --git a/packages/core/src/editor/elements/splitElement.ts b/packages/core/editor/src/editor/elements/splitElement.ts similarity index 100% rename from packages/core/src/editor/elements/splitElement.ts rename to packages/core/editor/src/editor/elements/splitElement.ts diff --git a/packages/core/src/editor/elements/updateElement.ts b/packages/core/editor/src/editor/elements/updateElement.ts similarity index 100% rename from packages/core/src/editor/elements/updateElement.ts rename to packages/core/editor/src/editor/elements/updateElement.ts diff --git a/packages/accordion/README.md b/packages/plugins/accordion/README.md similarity index 100% rename from packages/accordion/README.md rename to packages/plugins/accordion/README.md diff --git a/packages/accordion/package.json b/packages/plugins/accordion/package.json similarity index 100% rename from packages/accordion/package.json rename to packages/plugins/accordion/package.json diff --git a/packages/accordion/rollup.config.js b/packages/plugins/accordion/rollup.config.js similarity index 74% rename from packages/accordion/rollup.config.js rename to packages/plugins/accordion/rollup.config.js index 0c2569789..c2d7184b5 100644 --- a/packages/accordion/rollup.config.js +++ b/packages/plugins/accordion/rollup.config.js @@ -1,4 +1,4 @@ -import { createRollupConfig } from '../../config/rollup'; +import { createRollupConfig } from '../../../config/rollup'; const pkg = require('./package.json'); export default createRollupConfig({ diff --git a/packages/accordion/src/icons/checkmark.svg b/packages/plugins/accordion/src/icons/checkmark.svg similarity index 100% rename from packages/accordion/src/icons/checkmark.svg rename to packages/plugins/accordion/src/icons/checkmark.svg diff --git a/packages/accordion/src/icons/default.svg b/packages/plugins/accordion/src/icons/default.svg similarity index 100% rename from packages/accordion/src/icons/default.svg rename to packages/plugins/accordion/src/icons/default.svg diff --git a/packages/accordion/src/icons/error.svg b/packages/plugins/accordion/src/icons/error.svg similarity index 100% rename from packages/accordion/src/icons/error.svg rename to packages/plugins/accordion/src/icons/error.svg diff --git a/packages/accordion/src/icons/info.svg b/packages/plugins/accordion/src/icons/info.svg similarity index 100% rename from packages/accordion/src/icons/info.svg rename to packages/plugins/accordion/src/icons/info.svg diff --git a/packages/accordion/src/icons/success.svg b/packages/plugins/accordion/src/icons/success.svg similarity index 100% rename from packages/accordion/src/icons/success.svg rename to packages/plugins/accordion/src/icons/success.svg diff --git a/packages/accordion/src/icons/warning.svg b/packages/plugins/accordion/src/icons/warning.svg similarity index 100% rename from packages/accordion/src/icons/warning.svg rename to packages/plugins/accordion/src/icons/warning.svg diff --git a/packages/accordion/src/index.ts b/packages/plugins/accordion/src/index.ts similarity index 100% rename from packages/accordion/src/index.ts rename to packages/plugins/accordion/src/index.ts diff --git a/packages/accordion/src/plugin/index.tsx b/packages/plugins/accordion/src/plugin/index.tsx similarity index 100% rename from packages/accordion/src/plugin/index.tsx rename to packages/plugins/accordion/src/plugin/index.tsx diff --git a/packages/accordion/src/react-svg.d.ts b/packages/plugins/accordion/src/react-svg.d.ts similarity index 100% rename from packages/accordion/src/react-svg.d.ts rename to packages/plugins/accordion/src/react-svg.d.ts diff --git a/packages/accordion/src/renders/AccordionItemContent.tsx b/packages/plugins/accordion/src/renders/AccordionItemContent.tsx similarity index 100% rename from packages/accordion/src/renders/AccordionItemContent.tsx rename to packages/plugins/accordion/src/renders/AccordionItemContent.tsx diff --git a/packages/accordion/src/renders/AccordionItemHeading.tsx b/packages/plugins/accordion/src/renders/AccordionItemHeading.tsx similarity index 100% rename from packages/accordion/src/renders/AccordionItemHeading.tsx rename to packages/plugins/accordion/src/renders/AccordionItemHeading.tsx diff --git a/packages/accordion/src/renders/AccordionList.tsx b/packages/plugins/accordion/src/renders/AccordionList.tsx similarity index 100% rename from packages/accordion/src/renders/AccordionList.tsx rename to packages/plugins/accordion/src/renders/AccordionList.tsx diff --git a/packages/accordion/src/renders/AccordionListItem.tsx b/packages/plugins/accordion/src/renders/AccordionListItem.tsx similarity index 100% rename from packages/accordion/src/renders/AccordionListItem.tsx rename to packages/plugins/accordion/src/renders/AccordionListItem.tsx diff --git a/packages/accordion/src/styles.css b/packages/plugins/accordion/src/styles.css similarity index 100% rename from packages/accordion/src/styles.css rename to packages/plugins/accordion/src/styles.css diff --git a/packages/accordion/src/types.ts b/packages/plugins/accordion/src/types.ts similarity index 100% rename from packages/accordion/src/types.ts rename to packages/plugins/accordion/src/types.ts diff --git a/packages/accordion/tsconfig.json b/packages/plugins/accordion/tsconfig.json similarity index 55% rename from packages/accordion/tsconfig.json rename to packages/plugins/accordion/tsconfig.json index c26af02d3..ab104acf8 100644 --- a/packages/accordion/tsconfig.json +++ b/packages/plugins/accordion/tsconfig.json @@ -1,6 +1,6 @@ { - "extends": "../../config/tsconfig.base.json", - "include": ["src/react-svg.d.ts", "css-modules.d.ts", "src"], + "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", @@ -8,7 +8,7 @@ }, "references": [ { - "path": "../core" + "path": "../../core/editor" } ] } From a118f0a8a58cbfa09e779e73418c83c01889b5bc Mon Sep 17 00:00:00 2001 From: Darginec05 Date: Sat, 18 May 2024 15:59:42 +0300 Subject: [PATCH 05/24] pass element path to props --- package.json | 2 +- .../src/editor/elements/createElement.ts | 3 + .../src/plugins/SlateEditorComponent.tsx | 34 +++++-- packages/core/editor/src/plugins/types.ts | 3 +- packages/core/editor/src/utils/weakMaps.ts | 3 + packages/development/src/pages/dev/index.tsx | 7 +- .../accordion/src/elements/createElement.ts | 90 +++++++++++++++++++ .../accordion/src/elements/deleteElement.ts | 31 +++++++ .../accordion/src/elements/getElement.ts | 19 ++++ .../accordion/src/elements/getElementEntry.ts | 34 +++++++ .../accordion/src/elements/getElementPath.ts | 11 +++ .../accordion/src/elements/isElementEmpty.ts | 36 ++++++++ .../accordion/src/elements/splitElement.ts | 1 + .../accordion/src/elements/updateElement.ts | 47 ++++++++++ .../plugins/accordion/src/plugin/index.tsx | 24 +++-- .../src/renders/AccordionItemContent.tsx | 19 +++- .../src/renders/AccordionItemHeading.tsx | 44 ++++++--- .../accordion/src/renders/AccordionList.tsx | 3 +- .../src/renders/AccordionListItem.tsx | 5 +- packages/plugins/accordion/src/styles.css | 18 +++- .../plugins/blockquote/src/ui/Blockquote.tsx | 7 +- packages/plugins/callout/src/ui/Callout.tsx | 7 +- packages/plugins/code/src/ui/Code.tsx | 2 +- packages/plugins/embed/src/ui/Embed.tsx | 1 - packages/plugins/file/src/ui/File.tsx | 1 - .../headings/src/plugin/HeadingOne.tsx | 9 +- .../headings/src/plugin/HeadingThree.tsx | 1 - .../headings/src/plugin/HeadingTwo.tsx | 9 +- packages/plugins/image/src/ui/Image.tsx | 1 - packages/plugins/link/src/ui/LinkRender.tsx | 1 - .../lists/src/elements/BulletedList.tsx | 7 +- .../lists/src/elements/NumberedList.tsx | 7 +- .../plugins/lists/src/elements/TodoList.tsx | 8 +- .../plugins/mention/src/ui/MentionRender.tsx | 1 - .../plugins/paragraph/src/ui/Paragraph.tsx | 7 +- .../plugins/table/src/components/Table.tsx | 2 +- .../table/src/components/TableCell.tsx | 1 - .../plugins/table/src/components/TableRow.tsx | 2 +- packages/plugins/video/src/ui/Video.tsx | 1 - 39 files changed, 393 insertions(+), 116 deletions(-) create mode 100644 packages/core/editor/src/utils/weakMaps.ts create mode 100644 packages/plugins/accordion/src/elements/createElement.ts create mode 100644 packages/plugins/accordion/src/elements/deleteElement.ts create mode 100644 packages/plugins/accordion/src/elements/getElement.ts create mode 100644 packages/plugins/accordion/src/elements/getElementEntry.ts create mode 100644 packages/plugins/accordion/src/elements/getElementPath.ts create mode 100644 packages/plugins/accordion/src/elements/isElementEmpty.ts create mode 100644 packages/plugins/accordion/src/elements/splitElement.ts create mode 100644 packages/plugins/accordion/src/elements/updateElement.ts diff --git a/package.json b/package.json index 987c50057..856bf0d04 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ ], "private": true, "scripts": { - "start": "yarn lerna run start --parallel --ignore development", + "start": "yarn lerna run start --scope @yoopta/editor --scope @yoopta/accordion --parallel --ignore development", "build": "yarn clean && yarn lerna run build --parallel --ignore development", "clean": "find ./packages -type d -name dist ! -path './packages/development/*' -exec rm -rf {} +", "serve": "yarn lerna run dev --scope=development", diff --git a/packages/core/editor/src/editor/elements/createElement.ts b/packages/core/editor/src/editor/elements/createElement.ts index fd90a76b7..ccda646b2 100644 --- a/packages/core/editor/src/editor/elements/createElement.ts +++ b/packages/core/editor/src/editor/elements/createElement.ts @@ -77,6 +77,9 @@ export function createBlockElement( if (options?.split) { Transforms.splitNodes(slate, { at: atPath, always: true }); } else { + console.log('nodeElement', nodeElement); + console.log('atPath', atPath); + Transforms.insertNodes(slate, nodeElement, { at: atPath, select: focus }); } diff --git a/packages/core/editor/src/plugins/SlateEditorComponent.tsx b/packages/core/editor/src/plugins/SlateEditorComponent.tsx index be4760e22..eeec8da7c 100644 --- a/packages/core/editor/src/plugins/SlateEditorComponent.tsx +++ b/packages/core/editor/src/plugins/SlateEditorComponent.tsx @@ -1,5 +1,5 @@ -import React, { memo, useCallback, useMemo, useRef } from 'react'; -import { DefaultElement, Editable, RenderElementProps, Slate } from 'slate-react'; +import React, { memo, useCallback, useEffect, useMemo, useRef } from 'react'; +import { DefaultElement, Editable, ReactEditor, RenderElementProps, Slate } from 'slate-react'; import { useYooptaEditor, useBlockData } from '../contexts/YooptaContext/YooptaContext'; import { EVENT_HANDLERS } from '../handlers'; import { YooptaMark } from '../marks'; @@ -50,7 +50,6 @@ const SlateEditorComponent = ({ const editor = useYooptaEditor(); const block = useBlockData(id); const initialValue = useRef(block.value).current; - const type = block.type; const ELEMENTS_MAP = useMemo(() => getMappedElements(elements), [elements]); const MARKS_MAP = useMemo(() => getMappedMarks(marks), [marks]); @@ -84,7 +83,7 @@ const SlateEditorComponent = ({ }); return slateEditor; - }, []); + }, [elements]); const eventHandlers = useMemo(() => { if (!events || editor.readOnly) return {}; @@ -111,11 +110,30 @@ const SlateEditorComponent = ({ const onChange = useCallback((value) => editor.updateBlock(id, { value }), [id]); const renderElement = useCallback( - (props: RenderElementProps) => { - const ElementComponent = ELEMENTS_MAP[props.element.type]; + (elementProps: RenderElementProps) => { + const ElementComponent = ELEMENTS_MAP[elementProps.element.type]; + const { attributes, ...props } = elementProps; + attributes['data-element-type'] = props.element.type; + attributes['data-element-id'] = props.element.id; + + let path; + + try { + path = ReactEditor.findPath(slate, elementProps.element); + } catch (error) { + path = []; + } - if (!ElementComponent) return ; - return ; + if (!ElementComponent) return ; + return ( + + ); }, [elements], ); diff --git a/packages/core/editor/src/plugins/types.ts b/packages/core/editor/src/plugins/types.ts index bcfe05eb7..7fc7573c8 100644 --- a/packages/core/editor/src/plugins/types.ts +++ b/packages/core/editor/src/plugins/types.ts @@ -1,5 +1,5 @@ import { HTMLAttributes, ReactElement, ReactNode } from 'react'; -import { Descendant, Editor } from 'slate'; +import { Descendant, Editor, Path } from 'slate'; import { RenderElementProps as RenderSlateElementProps, RenderLeafProps } from 'slate-react'; import { SlateElement, YooEditor, YooptaBlockData } from '../editor/types'; import { YooptaMark } from '../marks'; @@ -29,6 +29,7 @@ export type PluginElementOptions = { export type PluginElementRenderProps = RenderSlateElementProps & { blockId: string; + path: Path; HTMLAttributes?: HTMLAttributes; }; diff --git a/packages/core/editor/src/utils/weakMaps.ts b/packages/core/editor/src/utils/weakMaps.ts new file mode 100644 index 000000000..40d92e576 --- /dev/null +++ b/packages/core/editor/src/utils/weakMaps.ts @@ -0,0 +1,3 @@ +import { SlateElement } from '../editor/types'; + +export const ELEMENT_INDEX: WeakMap = new WeakMap(); diff --git a/packages/development/src/pages/dev/index.tsx b/packages/development/src/pages/dev/index.tsx index e9ac4f435..00f980b0a 100644 --- a/packages/development/src/pages/dev/index.tsx +++ b/packages/development/src/pages/dev/index.tsx @@ -10,7 +10,7 @@ import Link from '@yoopta/link'; import Video from '@yoopta/video'; import File from '@yoopta/file'; import Embed from '@yoopta/embed'; -// import AccordionPlugin from '@yoopta/accordion'; +import AccordionPlugin from '@yoopta/accordion'; import ActionMenuList, { DefaultActionMenuRender } from '@yoopta/action-menu-list'; import LinkTool, { DefaultLinkToolRender } from '@yoopta/link-tool'; import Toolbar, { DefaultToolbarRender } from '@yoopta/toolbar'; @@ -21,11 +21,11 @@ import Code from '@yoopta/code'; import { ActionNotionMenuExample } from '../../components/ActionMenuExamples/NotionExample/ActionNotionMenuExample'; import { NotionToolbar } from '../../components/Toolbars/NotionToolbar/NotionToolbar'; import { ACCORDION_BLOCK } from '../../components/customPlugins/Accordion/Accordion'; -import Accordion from '../../components/customPlugins/Accordion/src'; +// import Accordion from '../../components/customPlugins/Accordion/src'; // import Mention from '@yoopta/mention'; const plugins = [ - Accordion, + AccordionPlugin, Code, File.extend({ options: { @@ -55,7 +55,6 @@ const plugins = [ //
    // diff --git a/packages/plugins/accordion/src/elements/createElement.ts b/packages/plugins/accordion/src/elements/createElement.ts new file mode 100644 index 000000000..9a0f5f801 --- /dev/null +++ b/packages/plugins/accordion/src/elements/createElement.ts @@ -0,0 +1,90 @@ +import { buildBlockElement, findSlateBySelectionPath, SlateElement, YooEditor } from '@yoopta/editor'; +import { Editor, Path, Transforms } from 'slate'; + +export type CreateBlockElementOptions = { + at?: 'next' | 'prev' | Path; + focus?: boolean; + split?: boolean; +}; + +export function createBlockElement( + editor: YooEditor, + blockId: string, + elementType: TElementKeys, + elementProps?: TElementProps, + options?: CreateBlockElementOptions, +) { + const blockData = editor.children[blockId]; + if (!blockData) { + throw new Error(`Block with id ${blockId} not found`); + } + + const slate = findSlateBySelectionPath(editor, { at: [blockData.meta.order] }); + if (!slate) { + console.warn('No slate found'); + return; + } + + Editor.withoutNormalizing(slate, () => { + const block = editor.blocks[blockData.type]; + const blockElement = block.elements[elementType]; + const nodeElement = buildBlockElement({ type: elementType, props: { ...blockElement.props, ...elementProps } }); + + const elementTypes = Object.keys(block.elements); + + let childrenElements: SlateElement[] = []; + + elementTypes.forEach((blockElementType) => { + const blockElement = block.elements[blockElementType]; + + if (blockElementType === elementType) { + if (Array.isArray(blockElement.children) && blockElement.children.length > 0) { + blockElement.children.forEach((childElementType) => { + const childElement = block.elements[childElementType]; + childrenElements.push(buildBlockElement({ type: childElementType, props: childElement.props })); + }); + } + } + }); + + if (childrenElements.length > 0) nodeElement.children = childrenElements; + + const { at, focus = true } = options || {}; + let atPath; + + const elementEntry = editor.blocks[block.type].getElementEntry(blockId, elementType); + + if (elementEntry) { + const [, elementPath] = elementEntry; + + if (Path.isPath(at)) { + atPath = at; + } else if (at === 'prev') { + atPath = Path.previous(elementPath); + } else if (at === 'next') { + atPath = Path.next(elementPath); + } + } + + Transforms.insertNodes(slate, nodeElement, { at: atPath, select: focus }); + + console.log('nodeElement', nodeElement); + + if (focus) { + if (childrenElements.length > 0) { + const firstChild = childrenElements[0]; + const firstElementEntry = editor.blocks[block.type].getElementEntry(blockId, firstChild.type, { + atPath: atPath, + }); + + if (firstElementEntry) { + const [, firstElementPath] = firstElementEntry; + Transforms.select(slate, firstElementPath); + } + } + } + + editor.applyChanges(); + editor.emit('change', editor.children); + }); +} diff --git a/packages/plugins/accordion/src/elements/deleteElement.ts b/packages/plugins/accordion/src/elements/deleteElement.ts new file mode 100644 index 000000000..4aa68fb77 --- /dev/null +++ b/packages/plugins/accordion/src/elements/deleteElement.ts @@ -0,0 +1,31 @@ +import { Editor, Element } from 'slate'; +import { findSlateBySelectionPath, SlateElement, YooEditor } from '@yoopta/editor'; + +export function deleteBlockElement( + editor: YooEditor, + blockId: string, + elementType: TElementKeys, +) { + const block = editor.children[blockId]; + + if (!block) { + throw new Error(`Block with id ${blockId} not found`); + } + + const slate = findSlateBySelectionPath(editor, { at: [block.meta.order] }); + + if (!slate) { + console.warn('No slate found'); + return; + } + + Editor.withoutNormalizing(slate, () => { + const [elementEntry] = Editor.nodes(slate, { + at: [0], + match: (n) => Element.isElement(n) && n.type === elementType, + }); + + editor.applyChanges(); + editor.emit('change', editor.children); + }); +} diff --git a/packages/plugins/accordion/src/elements/getElement.ts b/packages/plugins/accordion/src/elements/getElement.ts new file mode 100644 index 000000000..383c8e0a5 --- /dev/null +++ b/packages/plugins/accordion/src/elements/getElement.ts @@ -0,0 +1,19 @@ +import { SlateElement, YooEditor } from '@yoopta/editor'; +import { getBlockElementEntry, GetBlockElementEntryOptions } from './getElementEntry'; + +export type GetBlockElementOptions = GetBlockElementEntryOptions; + +export function getBlockElement( + editor: YooEditor, + blockId: string, + elementType: TElementKeys, + options?: GetBlockElementOptions, +): SlateElement | undefined { + const elementEntry = getBlockElementEntry(editor, blockId, elementType, options); + + if (elementEntry) { + return elementEntry[0] as SlateElement; + } + + return undefined; +} diff --git a/packages/plugins/accordion/src/elements/getElementEntry.ts b/packages/plugins/accordion/src/elements/getElementEntry.ts new file mode 100644 index 000000000..cc8d192c1 --- /dev/null +++ b/packages/plugins/accordion/src/elements/getElementEntry.ts @@ -0,0 +1,34 @@ +import { findSlateBySelectionPath, SlateElement, YooEditor } from '@yoopta/editor'; +import { Editor, Element, Location, NodeEntry, Span } from 'slate'; + +export type GetBlockElementEntryOptions = { + atPath?: Location | Span; +}; + +export function getBlockElementEntry( + editor: YooEditor, + blockId: string, + elementType: TElementKeys, + options?: GetBlockElementEntryOptions, +): NodeEntry> | undefined { + const block = editor.children[blockId]; + + if (!block) { + throw new Error(`Block with id ${blockId} not found`); + } + + const slate = findSlateBySelectionPath(editor, { at: [block.meta.order] }); + + if (!slate) { + console.warn('No slate found'); + return; + } + + const [elementEntry] = Editor.nodes(slate, { + at: options?.atPath || slate.selection || [0], + match: (n) => Element.isElement(n) && n.type === elementType, + mode: 'lowest', + }); + + return elementEntry as NodeEntry>; +} diff --git a/packages/plugins/accordion/src/elements/getElementPath.ts b/packages/plugins/accordion/src/elements/getElementPath.ts new file mode 100644 index 000000000..89ff9db13 --- /dev/null +++ b/packages/plugins/accordion/src/elements/getElementPath.ts @@ -0,0 +1,11 @@ +import { SlateElement, YooEditor } from '@yoopta/editor'; +import { Path } from 'slate'; + +export function getBlockElementPath( + editor: YooEditor, + blockId: string, + elementType: TElementKeys, + path: Path, +): Path { + return []; +} diff --git a/packages/plugins/accordion/src/elements/isElementEmpty.ts b/packages/plugins/accordion/src/elements/isElementEmpty.ts new file mode 100644 index 000000000..cfc2c1fcb --- /dev/null +++ b/packages/plugins/accordion/src/elements/isElementEmpty.ts @@ -0,0 +1,36 @@ +import { Editor, Element } from 'slate'; +import { findSlateBySelectionPath, SlateElement, YooEditor } from '@yoopta/editor'; + +export function isElementEmpty( + editor: YooEditor, + blockId: string, + elementType: TElementKeys, +): boolean | undefined { + const block = editor.children[blockId]; + + if (!block) { + throw new Error(`Block with id ${blockId} not found`); + } + + const slate = findSlateBySelectionPath(editor, { at: [block.meta.order] }); + + if (!slate) { + console.warn('No slate found'); + return; + } + + const [elementEntry] = Editor.nodes(slate, { + at: slate.selection || [0], + match: (n) => Element.isElement(n) && n.type === elementType, + }); + + if (elementEntry) { + const [node, nodePath] = elementEntry; + const string = Editor.string(slate, nodePath); + console.log({ node, nodePath, string }); + + return string.trim().length === 0; + } + + return false; +} diff --git a/packages/plugins/accordion/src/elements/splitElement.ts b/packages/plugins/accordion/src/elements/splitElement.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/plugins/accordion/src/elements/splitElement.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/plugins/accordion/src/elements/updateElement.ts b/packages/plugins/accordion/src/elements/updateElement.ts new file mode 100644 index 000000000..55487a57b --- /dev/null +++ b/packages/plugins/accordion/src/elements/updateElement.ts @@ -0,0 +1,47 @@ +import { Editor, Element, Path, Transforms } from 'slate'; +import { findSlateBySelectionPath, SlateElement, YooEditor } from '@yoopta/editor'; + +export type UpdateElementOptions = { + path?: Path; +}; + +export function updateElement( + editor: YooEditor, + blockId: string, + elementType: TElementKeys, + elementProps: TElementProps, + options?: UpdateElementOptions, +) { + const block = editor.children[blockId]; + + if (!block) { + throw new Error(`Block with id ${blockId} not found`); + } + + const slate = findSlateBySelectionPath(editor, { at: [block.meta.order] }); + + if (!slate) { + console.warn('No slate found'); + return; + } + Editor.withoutNormalizing(slate, () => { + const [elementEntry] = Editor.nodes(slate, { + at: options?.path || [0], + match: (n) => Element.isElement(n) && n.type === elementType, + }); + + const element = elementEntry?.[0]; + + const props = element?.props || {}; + const updatedNode = { props: { ...props, ...elementProps } }; + + Transforms.setNodes(slate, updatedNode, { + at: options?.path || [0], + match: (n) => Element.isElement(n) && n.type === elementType, + mode: 'lowest', + }); + + editor.applyChanges(); + editor.emit('change', editor.children); + }); +} diff --git a/packages/plugins/accordion/src/plugin/index.tsx b/packages/plugins/accordion/src/plugin/index.tsx index d76b29b40..336f65a8a 100644 --- a/packages/plugins/accordion/src/plugin/index.tsx +++ b/packages/plugins/accordion/src/plugin/index.tsx @@ -5,6 +5,9 @@ import { AccordionListItem } from '../renders/AccordionListItem'; import { AccordionItemHeading } from '../renders/AccordionItemHeading'; import { AccordionItemContent } from '../renders/AccordionItemContent'; import { Editor, Transforms } from 'slate'; +import { getBlockElementEntry } from '../elements/getElementEntry'; +import { createBlockElement } from '../elements/createElement'; +import { isElementEmpty } from '../elements/isElementEmpty'; const Accordion = new YooptaPlugin({ type: 'Accordion', @@ -31,19 +34,11 @@ const Accordion = new YooptaPlugin return (event) => { if (hotkeys.isBackspace(event)) { if (slate.selection) { - const headingElementEntry = editor.blocks.Accordion.getElementEntry( - currentBlock.id, - 'accordion-list-item-heading', - { atPath: slate.selection }, - ); + const headingElementEntry = getBlockElementEntry(editor, currentBlock.id, 'accordion-list-item-heading', { + atPath: slate.selection, + }); - console.log('headingElementEntry', headingElementEntry); - - const string = Editor.string(slate, slate.selection); - console.log('string', string); - if (string.trim().length === 0) { - event.preventDefault(); - console.log('slate.selection.anchor.path', slate.selection.anchor.path); + if (isElementEmpty(editor, currentBlock.id, 'accordion-list-item-heading')) { } } } @@ -61,11 +56,12 @@ const Accordion = new YooptaPlugin if (hotkeys.isEnter(event)) { event.preventDefault(); - editor.blocks.Accordion.createElement( + createBlockElement( + editor, currentBlock.id, 'accordion-list-item', { isExpanded: true }, - { at: 'next', focus: true, split: true }, + { at: 'next', focus: true, split: false }, ); } }; diff --git a/packages/plugins/accordion/src/renders/AccordionItemContent.tsx b/packages/plugins/accordion/src/renders/AccordionItemContent.tsx index 31ee01e7a..34398ae3a 100644 --- a/packages/plugins/accordion/src/renders/AccordionItemContent.tsx +++ b/packages/plugins/accordion/src/renders/AccordionItemContent.tsx @@ -1,12 +1,23 @@ -import { PluginElementRenderProps, useYooptaEditor } from '@yoopta/editor'; +import { findSlateBySelectionPath, PluginElementRenderProps, useBlockData, useYooptaEditor } from '@yoopta/editor'; import { useEffect, useState } from 'react'; -import { useSlate } from 'slate-react'; +import { Editor } from 'slate'; +import { ReactEditor, useSlate } from 'slate-react'; +import { getBlockElement } from '../elements/getElement'; +import { getBlockElementEntry } from '../elements/getElementEntry'; export const AccordionItemContent = (props: PluginElementRenderProps) => { - const { element, attributes, children, blockId } = props; + const { element, attributes, children, blockId, path } = props; + const editor = useYooptaEditor(); + + const nodeEl = getBlockElement(editor, blockId, 'accordion-list-item', { atPath: path.slice(0, 2) }); + const isExpanded = nodeEl?.props?.isExpanded; return ( -

    +

    {children}

    ); diff --git a/packages/plugins/accordion/src/renders/AccordionItemHeading.tsx b/packages/plugins/accordion/src/renders/AccordionItemHeading.tsx index 965654735..077baa37c 100644 --- a/packages/plugins/accordion/src/renders/AccordionItemHeading.tsx +++ b/packages/plugins/accordion/src/renders/AccordionItemHeading.tsx @@ -1,39 +1,49 @@ import { PluginElementRenderProps, useYooptaEditor, useYooptaReadOnly } from '@yoopta/editor'; import { ChevronUp, Plus } from 'lucide-react'; +import { Path } from 'slate'; +import { createBlockElement } from '../elements/createElement'; +import { updateElement } from '../elements/updateElement'; +import { getBlockElement } from '../elements/getElement'; export const AccordionItemHeading = (props: PluginElementRenderProps) => { - const { attributes, children, blockId } = props; + const { attributes, children, blockId, path } = props; const editor = useYooptaEditor(); const isReadOnly = useYooptaReadOnly(); const onToggleExpand = () => { - const listItemElement = editor.blocks.Accordion.getElement(blockId, 'accordion-list-item'); + const listItemElement = getBlockElement(editor, blockId, 'accordion-list-item', { atPath: path.slice(0, 2) }); if (listItemElement) { - editor.blocks.Accordion.updateElement(blockId, 'accordion-list-item', { - isExpanded: !listItemElement.props?.isExpanded, - }); + updateElement( + editor, + blockId, + 'accordion-list-item', + { + isExpanded: !listItemElement.props?.isExpanded, + }, + { path: path.slice(0, 2) }, + ); } }; const onAddAccordionItem = () => { - editor.blocks.Accordion.createElement( + createBlockElement( + editor, blockId, 'accordion-list-item', { isExpanded: true }, - { at: 'next', focus: true }, + { at: Path.next(path.slice(0, 2)), focus: true }, ); }; + const nodeEl = getBlockElement(editor, blockId, 'accordion-list-item', { atPath: path.slice(0, 2) }); + const isExpanded = nodeEl?.props?.isExpanded; + return ( -