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)}> -
- -
-
-
- )} -
- )} - - - -
+
+ )} {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 )} + )} + 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"> + + + + + + + + + + + + + + + + + ); +}; + +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 && ( +