diff --git a/README.md b/README.md index efb26b1..092b51c 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ npm install @hexx/editor yarn add @hexx/editor ``` -## Example +### Example ```jsx import { Editor } from '@hexx/editor'; import { - BlockMap, // default block mapping + PresetEditableScope, // default block mapping // preset PlusButton, TuneButton, @@ -21,10 +21,10 @@ import { // additional inline tool InlineCode, InlineMarker, - InlineLink + InlineLink, } from '@hexx/editor/components'; - + @@ -32,5 +32,5 @@ import { - +; ``` diff --git a/example/components/editor-example.tsx b/example/components/editor-example.tsx index 6b7d7ad..6c9ceb9 100644 --- a/example/components/editor-example.tsx +++ b/example/components/editor-example.tsx @@ -17,10 +17,11 @@ import { SelectionPlugin, Unstable_FileDropPlugin, Unstable_MarkdownShortcutPlugin, + PastHtmlPlugin, } from '@hexx/editor/plugins'; import { BasicImageBlock } from '@hexx/block-basic-image'; import { css } from '@hexx/theme'; -import { blockMap } from 'lib/block-map'; +import { mdastConfigs, scope } from 'lib/edit-scope'; import Linkify from 'linkify-it'; import { ElementRef, useCallback, useRef, useState } from 'react'; import tlds from 'tlds'; @@ -31,7 +32,7 @@ const linkify = Linkify(); linkify.tlds(tlds); -const EditorExample = (props: Omit) => { +const EditorExample = (props: Omit) => { const [showDataViewer, setShowDataViewer] = useState(false); const editorRef = useRef>(); const localSaverRef = @@ -71,21 +72,25 @@ const EditorExample = (props: Omit) => { onLoad={onLoadLocalStorage} {...editorStyles} {...props} - blockMap={blockMap} + scope={scope} > - - - - - + + {/* plugin */} + + + + + { if (files[0] && files[0].type.includes('image')) { diff --git a/example/lib/block-map.ts b/example/lib/block-map.ts deleted file mode 100644 index 79ac15a..0000000 --- a/example/lib/block-map.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BasicImageBlock } from '@hexx/block-basic-image'; -import { CodeBlock } from '@hexx/block-code'; -import { BlockMap } from '@hexx/editor/components'; - -export const blockMap = { - ...BlockMap, - code: CodeBlock, - 'basic-image': BasicImageBlock, -}; diff --git a/example/lib/edit-scope.ts b/example/lib/edit-scope.ts new file mode 100644 index 0000000..954b3c7 --- /dev/null +++ b/example/lib/edit-scope.ts @@ -0,0 +1,20 @@ +import { + BasicImageBlock, + basicImageMdast, +} from '@hexx/block-basic-image'; +import { CodeBlock, codeMdast } from '@hexx/block-code'; +import { presetEditableScope } from '@hexx/editor/components'; +import { presetMDASTConfig } from '@hexx/editor'; +import { MdastConfigs } from '../../packages/editor/src/parser/types'; + +export const scope = { + ...presetEditableScope, + code: CodeBlock, + 'basic-image': BasicImageBlock, +}; + +export const mdastConfigs: MdastConfigs = { + ...presetMDASTConfig, + image: basicImageMdast, + code: codeMdast, +}; diff --git a/example/pages/index.tsx b/example/pages/index.tsx index cb09c18..8cb0ccb 100644 --- a/example/pages/index.tsx +++ b/example/pages/index.tsx @@ -1,13 +1,13 @@ +import { BlockType, createHexxMarkdownParser } from '@hexx/editor'; +import { styled } from '@hexx/theme'; import fs from 'fs'; -import path from 'path'; +import { JSDOM } from 'jsdom'; +import { mdastConfigs } from 'lib/edit-scope'; +import { GetStaticProps } from 'next'; +import dynamic from 'next/dynamic'; import Head from 'next/head'; +import path from 'path'; import styles from '../styles/Home.module.css'; -import dynamic from 'next/dynamic'; -import { styled } from '@hexx/theme'; -import { GetStaticProps } from 'next'; -import { BlockType, createHexxMarkdownParser } from '@hexx/editor'; -import { blockMap } from 'lib/block-map'; -import { JSDOM } from 'jsdom'; const EditorExample = dynamic( () => import('../components/editor-example'), @@ -61,7 +61,7 @@ export default function Home(props: { json?: BlockType[] }) { } export const getStaticProps: GetStaticProps = async () => { - const markdownParser = createHexxMarkdownParser(blockMap, { + const markdownParser = createHexxMarkdownParser(mdastConfigs, { // to support ssr or ssg you have to use jsdom in markdown parser document: new JSDOM().window.document, autoGenerateId: true, diff --git a/example/pages/render.tsx b/example/pages/render.tsx index 5b170d6..4e9fa7f 100644 --- a/example/pages/render.tsx +++ b/example/pages/render.tsx @@ -1,20 +1,18 @@ -import fs from 'fs'; -import path from 'path'; -import Head from 'next/head'; -import styles from '../styles/Home.module.css'; -import dynamic from 'next/dynamic'; -import { styled } from '@hexx/theme'; -import { GetStaticProps } from 'next'; +import { CodeBlockRenderer } from '@hexx/block-code'; import { BlockType, createHexxMarkdownParser } from '@hexx/editor'; +import { EditorRenderer, PresetScope } from '@hexx/renderer'; +import { styled } from '@hexx/theme'; +import fs from 'fs'; import { JSDOM } from 'jsdom'; - -import { EditorRenderer, BlockMap } from '@hexx/renderer'; -import { blockMap } from 'lib/block-map'; -import { CodeBlockRenderer } from '@hexx/block-code'; import { editorStyles } from 'lib/common-style'; +import { mdastConfigs } from 'lib/edit-scope'; +import { GetStaticProps } from 'next'; +import Head from 'next/head'; +import path from 'path'; +import styles from '../styles/Home.module.css'; -const renderBlockMap = { - ...BlockMap, +const renderScope = { + ...PresetScope, code: CodeBlockRenderer, }; @@ -64,7 +62,7 @@ export default function Home(props: { json?: BlockType[] }) { css: editorStyles.blockCss, }} blocks={props.json} - blockMap={renderBlockMap} + scope={renderScope} /> @@ -72,7 +70,7 @@ export default function Home(props: { json?: BlockType[] }) { } export const getStaticProps: GetStaticProps = async () => { - const markdownParser = createHexxMarkdownParser(blockMap, { + const markdownParser = createHexxMarkdownParser(mdastConfigs, { // to support ssr or ssg you have to use jsdom in markdown parser document: new JSDOM().window.document, autoGenerateId: true, diff --git a/package.json b/package.json index b1ff022..cddde3a 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "devDependencies": { "@preconstruct/cli": "^2.1.0", "@svgr/cli": "^5.5.0", - "@types/node": "^15.12.4", + "@types/node": "^15.12.5", "@types/react": "^17.0.11", "@types/react-dom": "^17.0.8", "@types/uuid": "^8.3.0", @@ -32,6 +32,6 @@ }, "dependencies": { "@changesets/cli": "^2.16.0", - "prettier": "^2.3.1" + "prettier": "^2.3.2" } } diff --git a/packages/block-basic-image/src/basic-image.tsx b/packages/block-basic-image/src/basic-image.tsx index f1f0377..d8420bd 100644 --- a/packages/block-basic-image/src/basic-image.tsx +++ b/packages/block-basic-image/src/basic-image.tsx @@ -11,11 +11,10 @@ interface Config { const _BasicImageBlock = ({ id, - index, config, blockAtom, }: BlockProps) => { - const { update, block } = useBlock(blockAtom, index); + const { update, block } = useBlock(blockAtom); const handleImageUpdate = (url: string) => { update({ ...block, @@ -68,10 +67,6 @@ export const BasicImageBlock = applyBlock( _BasicImageBlock, { type: 'basic-image', - icon: { - svg: SvgImage, - text: 'Image', - }, config: { onInput: (files: File[] | FileList) => { return new Promise((res, rej) => { @@ -84,14 +79,21 @@ export const BasicImageBlock = applyBlock( }, }, isEmpty: (d) => !d.file?.url, - mdast: { - type: 'html.image', - in: (content: Image) => ({ - url: content.url, - }), - }, defaultValue: { url: '', }, }, ); + +export const ImageIcon = { + svg: SvgImage, + text: 'Image', +}; + +export const basicImageMdast = { + type: 'html.image', + blockType: 'basic-image', + in: (content: Image) => ({ + url: content.url, + }), +}; diff --git a/packages/block-code/src/code-block.tsx b/packages/block-code/src/code-block.tsx index 1307efd..b8cb7f3 100644 --- a/packages/block-code/src/code-block.tsx +++ b/packages/block-code/src/code-block.tsx @@ -17,13 +17,8 @@ type Config = { theme?: PrismTheme; }; -function _CodeBlock({ - id, - index, - config, - blockAtom, -}: BlockProps) { - const { update, block } = useBlock(blockAtom, index); +function _CodeBlock({ config, blockAtom }: BlockProps) { + const { update, block } = useBlock(blockAtom); const { padding, ...restCodeBlockStyle } = codeBlockStyle; @@ -73,8 +68,8 @@ function _CodeBlock({ value={block.data.value} onValueChange={onChange} highlight={highlightCode} - padding={padding} - style={restCodeBlockStyle} + padding={padding as string} + style={restCodeBlockStyle as any} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.stopPropagation(); @@ -96,10 +91,6 @@ export const CodeBlock = applyBlock( text: 'Code Block', svg: SvgCode, }, - mdast: { - type: 'code', - in: ({ lang, value }) => ({ value, lang }), - }, config: { placeholder: 'Code...', }, @@ -108,3 +99,9 @@ export const CodeBlock = applyBlock( }, }, ); + +export const codeMdast = { + type: 'code', + blockType: 'code', + in: ({ lang, value }) => ({ value, lang }), +}; diff --git a/packages/block-code/src/renderer.tsx b/packages/block-code/src/renderer.tsx index ac9f031..2e10c9a 100644 --- a/packages/block-code/src/renderer.tsx +++ b/packages/block-code/src/renderer.tsx @@ -1,4 +1,4 @@ -import { StitchesCssProp, StitchesProps, styled } from '@hexx/theme'; +import { styled, CSS } from '@hexx/theme'; import Highlight, { Language, Prism, @@ -13,7 +13,7 @@ export type TCodeBlock = { }; }; -export const codeBlockStyle: StitchesCssProp = { +export const codeBlockStyle: CSS = { fontFamily: 'monospace', padding: '24px', borderRadius: '4px', diff --git a/packages/editor/package.json b/packages/editor/package.json index 75d704e..1de1ea9 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -26,11 +26,11 @@ "@babel/preset-env": "^7.14.7", "@babel/preset-react": "^7.14.5", "@babel/preset-typescript": "^7.14.5", - "@size-limit/preset-big-lib": "^5.0.0", + "@size-limit/preset-big-lib": "^5.0.1", "@types/linkify-it": "^3.0.1", "@types/markdown-it": "^12.0.2", "@types/mdast": "^3.0.3", - "size-limit": "^5.0.0" + "size-limit": "^5.0.1" }, "dependencies": { "@babel/runtime": "^7.14.6", diff --git a/packages/editor/src/components/block-menu.tsx b/packages/editor/src/components/block-menu.tsx index c4234df..f404858 100644 --- a/packages/editor/src/components/block-menu.tsx +++ b/packages/editor/src/components/block-menu.tsx @@ -4,24 +4,27 @@ import { BlockAtom } from '../constants/atom'; import { useEditor } from '../hooks'; import { BlockType, isBlockEmpty } from '../utils/blocks'; -export type BlockMenuItem = { type: string; config?: any } | string; +export type BlockMenuItem = { + type: string; + defaultValue?: any; + icon: { text: string; svg: any }; +}; export function BlockMenu(props: { onAdd?: () => void; - menu?: BlockMenuItem[]; + menu: BlockMenuItem[]; blockAtom: BlockAtom; }) { - const { blockMap, insertBlockAfter } = useEditor(); + const { blockScope, insertBlockAfter } = useEditor(); const [block, setBlock] = useAtom(props.blockAtom); const handleAddBlock = (blockType: BlockType) => { if (!block) return; const blockToAdd = { type: blockType.type, - // @ts-ignore - data: blockType.defaultValue, + data: blockType.data, }; - if (isBlockEmpty(blockMap[block.type], block.data)) { + if (isBlockEmpty(blockScope[block.type], block.data)) { setBlock((s) => ({ ...s, ...blockToAdd })); } else { insertBlockAfter({ @@ -33,31 +36,30 @@ export function BlockMenu(props: { }; const menuList = useMemo(() => { - if (props.menu) { - return props.menu.map((item) => - typeof item === 'string' - ? ([item, blockMap[item]] as const) - : ([ - item.type, - { - ...blockMap[item.type], - block: { - ...blockMap[item.type].block, - ...item.config, - }, + return props.menu.map( + (item) => + [ + item, + { + ...blockScope[item.type], + block: { + ...blockScope[item.type].block, + data: { + ...blockScope[item.type].block.defaultValue, + ...item.defaultValue, }, - ] as const), - ); - } - return Object.entries(blockMap); - }, [props.menu, blockMap]); + }, + }, + ] as const, + ); + }, [props.menu, blockScope]); return ( <> - {menuList.map(([key, blockType], index) => ( - - {createElement(blockType.block.icon.svg, { - title: blockType.block.icon.text, + {menuList.map(([menuItem, blockType], index) => ( + + {createElement(menuItem.icon.svg, { + title: menuItem.icon.text, tabIndex: 0, onKeyPress: (e) => { if (e.key === 'Enter') { diff --git a/packages/editor/src/components/block/block.tsx b/packages/editor/src/components/block/block.tsx index dfb6869..091bbe3 100644 --- a/packages/editor/src/components/block/block.tsx +++ b/packages/editor/src/components/block/block.tsx @@ -1,4 +1,4 @@ -import { styled } from '@hexx/theme'; +import { CSS, styled } from '@hexx/theme'; import { PrimitiveAtom, useAtom } from 'jotai'; import { createElement, @@ -25,12 +25,10 @@ import { lastCursor, } from '../../utils/find-blocks'; import { - extractFragmentFromPosition, getSelectionRange, isEditableSelectAll, removeRanges, } from '../../utils/ranges'; -import { TextBlock } from './text'; const Wrapper = styled('div', { width: '100%', @@ -107,7 +105,7 @@ function useBlockWrapper({ blockAtom: PrimitiveAtom>; index: number; }) { - const { removeBlock, blockMap, insertBlock } = useEditor(); + const { removeBlock, blockScope, insertBlock } = useEditor(); const [editorId] = useAtom(editorIdAtom); const [isEditorSelectAll, setIsEditorSelectAll] = useAtom( isEditorSelectAllAtom, @@ -119,7 +117,7 @@ function useBlockWrapper({ const [hoverBlock, setHoverBlock] = useAtom($hoverAtom); const isHovering = hoverBlock === blockAtom; - const currentBlock = block && blockMap[block.type]; + const currentBlock = block && blockScope[block.type]; const isBlockSelect = blockSelect.has(blockAtom); const [drop] = useAtom(dropAtom); @@ -133,8 +131,9 @@ function useBlockWrapper({ if (!range) { return; } - if (range.startOffset === 0) { - focusContentEditable('up'); + if (!range.commonAncestorContainer.previousSibling) { + e.preventDefault(); + focusContentEditable('up', range.startOffset); } } if (e.key === 'ArrowDown') { @@ -142,42 +141,10 @@ function useBlockWrapper({ if (!range) { return; } - if ( - !(range.commonAncestorContainer as Text)?.length || - ((range.commonAncestorContainer as Text)?.length === - range.endOffset && - range.collapsed) - ) { - focusContentEditable('down'); - } - } - if (!e.shiftKey && e.key === 'Enter') { - const fragment = extractFragmentFromPosition(); - - if (!fragment) { - return; + if (!range.commonAncestorContainer.nextSibling) { + e.preventDefault(); + focusContentEditable('down', range.startOffset); } - - const { current, next } = fragment; - insertBlock({ - index: index + 1, - block: { - type: TextBlock.block.type, - data: { - ...TextBlock.block.defaultValue, - text: next, - }, - }, - }); - - setBlock((s) => ({ - ...s, - data: { - ...s.data, - text: current, - }, - })); - e.preventDefault(); } if (e[commandKey] && e.key === 'a') { if (isEditorSelectAll) { @@ -318,7 +285,7 @@ export function Block({ }: { index: number; blockAtom: PrimitiveAtom>; - css?: any; + css?: CSS; }) { const { block, diff --git a/packages/editor/src/components/block/divider.tsx b/packages/editor/src/components/block/divider.tsx index 5deb012..94b1751 100644 --- a/packages/editor/src/components/block/divider.tsx +++ b/packages/editor/src/components/block/divider.tsx @@ -7,12 +7,5 @@ const Render = () => ; export const Divider = applyBlock<{}, {}>(Render, { type: 'delimiter', - icon: { - text: 'Divider', - svg: DividerSvg, - }, - mdast: { - type: 'thematicBreak', - }, defaultValue: {}, }); diff --git a/packages/editor/src/components/block/header.tsx b/packages/editor/src/components/block/header.tsx index a9ec699..92f200c 100644 --- a/packages/editor/src/components/block/header.tsx +++ b/packages/editor/src/components/block/header.tsx @@ -2,12 +2,9 @@ import { Header, headerStyle } from '@hexx/renderer'; import { styled } from '@hexx/theme'; import * as mdast from 'mdast'; import * as React from 'react'; -import composeRefs from '../../hooks/use-compose-ref'; -import { useBlock } from '../../hooks/use-editor'; +import { useBlock, useEditor } from '../../hooks/use-editor'; import { applyBlock, BlockProps } from '../../utils/blocks'; -import { lastCursor } from '../../utils/find-blocks'; import { Editable } from '../editable'; -import { h1, h2, h3, header as HeaderSvg } from '../icons'; const Heading = styled(Editable, headerStyle); @@ -18,19 +15,13 @@ function _HeaderBlock({ css, blockAtom, }: BlockProps<{ placeholder: string }>) { - const { register, update, block } = useBlock(blockAtom, index); - - const ref = React.useRef(null); - React.useEffect(() => { - ref.current?.focus(); - lastCursor(); - }, []); + const { splitBlock } = useEditor(); + const { update, block } = useBlock(blockAtom); return ( update({ ...block, @@ -40,6 +31,15 @@ function _HeaderBlock({ }, }) } + onKeyDown={(e) => { + if (!e.shiftKey && e.key === 'Enter') { + splitBlock({ + atom: blockAtom, + updater: (s) => ({ text: s }), + }); + e.preventDefault(); + } + }} html={block.data.text} css={css} /> @@ -54,46 +54,9 @@ export const HeaderBlock = applyBlock< config: { placeholder: 'Heading', }, - mdast: { - type: 'heading', - in: (mdast: mdast.Heading, toHTML) => ({ - text: toHTML(mdast).innerHTML, - level: mdast.depth || 3, - }), - }, - icon: { - text: 'Header', - svg: HeaderSvg, - }, defaultValue: { text: '', level: 3, }, isEmpty: (d) => !d.text?.trim(), - tune: [ - { - icon: { - text: 'H1', - svg: h1, - isActive: (data) => data.level === 1, - }, - updater: (data) => ({ ...data, level: 1 }), - }, - { - icon: { - text: 'H2', - svg: h2, - isActive: (data) => data.level === 2, - }, - updater: (data) => ({ ...data, level: 2 }), - }, - { - icon: { - text: 'H3', - svg: h3, - isActive: (data) => data.level === 3, - }, - updater: (data) => ({ ...data, level: 3 }), - }, - ], }); diff --git a/packages/editor/src/components/block/list.tsx b/packages/editor/src/components/block/list.tsx index ee69491..e43d637 100644 --- a/packages/editor/src/components/block/list.tsx +++ b/packages/editor/src/components/block/list.tsx @@ -4,7 +4,6 @@ import * as mdast from 'mdast'; import * as React from 'react'; import { BlockAtom } from '../../constants/atom'; import { BackspaceKey } from '../../constants/key'; -import composeRefs from '../../hooks/use-compose-ref'; import { useBlock, useEditor } from '../../hooks/use-editor'; import { applyBlock, BlockProps } from '../../utils/blocks'; import { lastCursor } from '../../utils/find-blocks'; @@ -13,7 +12,6 @@ import { getSelectionRange, } from '../../utils/ranges'; import { Editable } from '../editable'; -import { IcNumList, list as ListSvg } from '../icons'; const Ul = styled('ul', listStyle.ul); const Ol = styled('ol', listStyle.ol); @@ -28,7 +26,7 @@ function _ListBlock({ const [activeListItemIndex, setActiveListItemIndex] = React.useState(0); - const { update, block } = useBlock(blockAtom, index); + const { update, block } = useBlock(blockAtom); const { defaultBlock, insertBlockAfter } = useEditor(); const handleEmptyListItem = (i: number) => { @@ -158,11 +156,6 @@ function ListItem(props: { blockIndex: number; blockAtom: BlockAtom; }) { - const { registerByIndex } = useBlock( - props.blockAtom, - props.blockIndex, - ); - const ref = React.useRef(null); React.useEffect(() => { @@ -172,7 +165,7 @@ function ListItem(props: { return (
  • { @@ -189,10 +182,6 @@ export const ListBlock = applyBlock< { placeholder: string } >(_ListBlock, { type: 'list', - icon: { - text: 'List', - svg: ListSvg, - }, config: { placeholder: 'list', }, @@ -200,31 +189,6 @@ export const ListBlock = applyBlock< items: [''], style: 'unordered', }, - mdast: { - type: 'list', - in: (content: mdast.List, toHTML) => ({ - style: content.ordered ? 'ordered' : 'unordered', - items: content.children.map((child) => toHTML(child).innerHTML), - }), - }, - tune: [ - { - icon: { - text: 'Bullet', - svg: ListSvg, - isActive: (data) => data.style === 'unordered', - }, - updater: (data) => ({ ...data, style: 'unordered' }), - }, - { - icon: { - text: 'Number', - svg: IcNumList, - isActive: (data) => data.style === 'ordered', - }, - updater: (data) => ({ ...data, style: 'ordered' }), - }, - ], isEmpty: (data) => data.items.length === 0, }); diff --git a/packages/editor/src/components/block/paragraph.tsx b/packages/editor/src/components/block/paragraph.tsx new file mode 100644 index 0000000..eab1ab9 --- /dev/null +++ b/packages/editor/src/components/block/paragraph.tsx @@ -0,0 +1,51 @@ +import type { Paragraph } from '@hexx/renderer'; +import { useBlock, useEditor } from '../../hooks/use-editor'; +import { applyBlock, BlockProps } from '../../utils/blocks'; +import { Editable } from '../editable'; + +function _ParagraphBlock(props: BlockProps) { + const { blockAtom, index } = props; + const { splitBlock } = useEditor(); + const { update, block } = useBlock(blockAtom); + + const editableProps = { + html: block.data.text || '', + style: { + textAlign: block.data.alignment || 'left', + }, + onKeyDown: (e) => { + if (!e.shiftKey && e.key === 'Enter') { + splitBlock({ + atom: blockAtom, + updater: (s) => ({ text: s }), + }); + e.preventDefault(); + } + }, + onChange: (evt) => { + update((s) => ({ + ...s, + data: { + ...s.data, + text: evt.target.value, + }, + })); + }, + }; + + return ; +} + +export const ParagraphBlock = applyBlock( + _ParagraphBlock, + { + type: 'paragraph', + defaultValue: { + text: '', + }, + isEmpty: (data) => + !data.text?.trim() || + // quick fix for safari + data.text === '
    ', + }, +); diff --git a/packages/editor/src/components/block/quote.tsx b/packages/editor/src/components/block/quote.tsx index 49ca723..8ae0727 100644 --- a/packages/editor/src/components/block/quote.tsx +++ b/packages/editor/src/components/block/quote.tsx @@ -2,24 +2,16 @@ import { Quote, quoteStyle } from '@hexx/renderer'; import { css } from '@hexx/theme'; import * as mdast from 'mdast'; import * as React from 'react'; -import composeRefs from '../..//hooks/use-compose-ref'; -import { lastCursor } from '../..//utils/find-blocks'; import { useBlock } from '../../hooks/use-editor'; import { applyBlock, BlockProps } from '../../utils/blocks'; import { Editable } from '../editable'; -import { quote as QuoteSvg } from '../icons'; function _QuoteBlock({ config, index, blockAtom, }: BlockProps<{ placeholder: string }>) { - const { update, register, block } = useBlock(blockAtom, index); - const ref = React.useRef(null); - React.useEffect(() => { - ref.current?.focus(); - lastCursor(); - }, []); + const { update, block } = useBlock(blockAtom); return (
    @@ -35,7 +27,6 @@ function _QuoteBlock({ }, }); }} - ref={composeRefs(ref, register)} html={block.data.text} />
    @@ -46,19 +37,9 @@ export const QuoteBlock = applyBlock< { placeholder: string } >(_QuoteBlock, { type: 'quote', - icon: { - text: 'Quote', - svg: QuoteSvg, - }, config: { placeholder: 'quote', }, - mdast: { - type: 'blockquote', - in: (content: mdast.Blockquote, toHTML) => ({ - text: toHTML(content).innerHTML, - }), - }, defaultValue: { text: '', alignment: 'left', diff --git a/packages/editor/src/components/block/text.tsx b/packages/editor/src/components/block/text.tsx deleted file mode 100644 index 70126b6..0000000 --- a/packages/editor/src/components/block/text.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import type { Paragraph } from '@hexx/renderer'; -import * as mdast from 'mdast'; -import { useEffect, useRef, useState } from 'react'; -import composeRefs from '../../hooks/use-compose-ref'; -import { useBlock } from '../../hooks/use-editor'; -import { applyBlock, BlockProps } from '../../utils/blocks'; -import { lastCursor } from '../../utils/find-blocks'; -import { Editable } from '../editable'; -import { - AlignCenter, - AlignLeft, - AlignRight, - text as TextIcon, -} from '../icons'; - -function _TextBlock(props: BlockProps) { - const { index, blockAtom } = props; - const ref = useRef(null); - const { update, register, block } = useBlock(blockAtom, index); - - useEffect(() => { - ref.current?.focus(); - if (!block.data.text) { - lastCursor(); - } - }, [block.data.text]); - - const editableProps = { - ref: composeRefs(ref, register), - html: block.data.text || '', - style: { - textAlign: block.data.alignment || 'left', - }, - onChange: (evt) => { - update((s) => ({ - ...s, - data: { - ...s.data, - text: evt.target.value, - }, - })); - }, - }; - - return ; -} - -export const TextBlock = applyBlock( - _TextBlock, - { - type: 'paragraph', - icon: { - text: 'Text', - svg: TextIcon, - }, - defaultValue: { - text: '', - }, - mdast: { - type: 'paragraph', - in: (content: mdast.Paragraph, toHTML) => ({ - text: toHTML(content).outerHTML, - }), - }, - tune: [ - { - icon: { - text: 'Left', - svg: AlignLeft, - isActive: (data) => data.alignment === 'left', - }, - updater: (data) => ({ ...data, alignment: 'left' }), - }, - { - icon: { - text: 'Center', - svg: AlignCenter, - isActive: (data) => data.alignment === 'center', - }, - updater: (data) => ({ ...data, alignment: 'center' }), - }, - { - icon: { - text: 'Right', - svg: AlignRight, - isActive: (data) => data.alignment === 'right', - }, - updater: (data) => ({ ...data, alignment: 'right' }), - }, - ], - isEmpty: (data) => - !data.text?.trim() || - // quick fix for safari - data.text === '
    ', - }, -); diff --git a/packages/editor/src/components/default-block-map.ts b/packages/editor/src/components/default-block-map.ts index f4b2f69..ca8b641 100644 --- a/packages/editor/src/components/default-block-map.ts +++ b/packages/editor/src/components/default-block-map.ts @@ -2,10 +2,10 @@ import { Divider } from './block/divider'; import { HeaderBlock } from './block/header'; import { ListBlock } from './block/list'; import { QuoteBlock } from './block/quote'; -import { TextBlock } from './block/text'; +import { ParagraphBlock } from './block/paragraph'; -export const BlockMap = { - paragraph: TextBlock, +export const presetEditableScope = { + paragraph: ParagraphBlock, header: HeaderBlock, list: ListBlock, quote: QuoteBlock, diff --git a/packages/editor/src/components/editable/editable.tsx b/packages/editor/src/components/editable/editable.tsx index 20c5860..1af3218 100644 --- a/packages/editor/src/components/editable/editable.tsx +++ b/packages/editor/src/components/editable/editable.tsx @@ -1,4 +1,4 @@ -import { css, StitchesCssProp } from '@hexx/theme'; +import { css, CSS } from '@hexx/theme'; import { forwardRef } from 'react'; import { ContentEditable, @@ -31,7 +31,7 @@ const styles = { export const Editable = forwardRef< any, ContentEditableProps & { - css?: StitchesCssProp; + css?: CSS; } >(({ html = '', css: overrideCss, ...props }, ref) => { const combineStyles = css({ ...styles, ...(overrideCss as any) }); diff --git a/packages/editor/src/components/editor/editor.tsx b/packages/editor/src/components/editor/editor.tsx index 8c0dc12..360aa8d 100644 --- a/packages/editor/src/components/editor/editor.tsx +++ b/packages/editor/src/components/editor/editor.tsx @@ -1,10 +1,9 @@ import { paragraphStyle } from '@hexx/renderer'; -import { css, StitchesCssProp, styled } from '@hexx/theme'; +import { css, CSS, styled } from '@hexx/theme'; import { Provider, useAtom } from 'jotai'; import { splitAtom, useAtomCallback } from 'jotai/utils'; import { forwardRef, - memo, MutableRefObject, ReactNode, useCallback, @@ -21,8 +20,8 @@ import { v4 } from 'uuid'; import { CLIPBOARD_DATA_FORMAT } from '../../constants'; import { BlockAtom, - blockMapAtom, blocksAtom, + blockScopeAtom, blocksDataAtom, blockSelectAtom, createAtom, @@ -41,10 +40,8 @@ import { BackspaceKey } from '../../constants/key'; import { useEventListener } from '../../hooks'; import { useActiveBlockId } from '../../hooks/use-active-element'; import { useEditor, UseEditorReturn } from '../../hooks/use-editor'; -import { useWhyDidYouUpdate } from '../../hooks/use-why-did-you-update'; import { usePlugin } from '../../plugins'; import { NewBlockOverlayPlugin } from '../../plugins/new-block-overlay'; -import { PastHtmlPlugin } from '../../plugins/paste'; import { BlockType } from '../../utils/blocks'; import { findBlockByIndex, @@ -53,19 +50,19 @@ import { lastCursor, } from '../../utils/find-blocks'; import { Block } from '../block/block'; -import { TextBlock } from '../block/text'; +import { ParagraphBlock } from '../block/paragraph'; export interface EditorProps extends HexxProps { data?: BlockType[]; - blockMap: Record; + scope: Record; } interface HexxProps { wrapperRef?: MutableRefObject; children?: ReactNode; defaultBlock?: Omit; - css?: StitchesCssProp; - blockCss?: StitchesCssProp; + css?: CSS; + blockCss?: CSS; onLoad?: () => void; autoFocus?: boolean; } @@ -321,7 +318,6 @@ const Hexx = forwardRef((props, ref) => { }} > - {props.children} { @@ -338,7 +334,7 @@ const Hexx = forwardRef((props, ref) => { blocks={blocksWithFilters} pressThreshold={300} /> - {!props.autoFocus && } + {/* {!props.autoFocus && } */} ); }); @@ -366,14 +362,15 @@ function AutoFocusInput() { ); } +const BlockListWrapper = styled('div', { + width: '100%', + position: 'relative', +}); + const SortableBlockList = SortableContainer( ({ blockCss, blocks }: { blockCss: any; blocks: BlockAtom[] }) => { return ( -
    + {blocks.map((blockAtom, i) => ( ))} -
    + ); }, ); @@ -390,8 +387,8 @@ const SortableBlockList = SortableContainer( export const Editor = forwardRef( (props, ref) => { const defaultBlock = props.defaultBlock || { - type: TextBlock.block.type, - data: TextBlock.block.defaultValue, + type: ParagraphBlock.block.type, + data: ParagraphBlock.block.defaultValue, }; const initDataAtom = useMemo(() => { @@ -408,7 +405,7 @@ export const Editor = forwardRef( [ [editorDefaultBlockAtom, defaultBlock], [editorIdAtom, v4()], - [blockMapAtom, props.blockMap], + [blockScopeAtom, props.scope], [_blocksAtom, initData], ] as const } diff --git a/packages/editor/src/components/inline-toolbar/code/index.tsx b/packages/editor/src/components/inline-toolbar/code/index.tsx index 5ff2ca2..7bc63ea 100644 --- a/packages/editor/src/components/inline-toolbar/code/index.tsx +++ b/packages/editor/src/components/inline-toolbar/code/index.tsx @@ -3,8 +3,10 @@ import SvgCode from '../../icons/code'; import { useEventChangeSelection, useInlineTool } from '../hooks'; import { surround } from '../../../utils/find-blocks'; import { getSelectionRange } from '../../../utils/ranges'; -import { StitchesProps } from '@hexx/theme'; -export function InlineCode(props: StitchesProps) { +import { ComponentProps } from 'react'; +export function InlineCode( + props: ComponentProps, +) { const { getProps, setIsActive } = useInlineTool({ shortcut: '⌘ + e', onToggle: (isActive) => { diff --git a/packages/editor/src/components/inline-toolbar/inline-toolbar.tsx b/packages/editor/src/components/inline-toolbar/inline-toolbar.tsx index 45ed387..b4d967c 100644 --- a/packages/editor/src/components/inline-toolbar/inline-toolbar.tsx +++ b/packages/editor/src/components/inline-toolbar/inline-toolbar.tsx @@ -1,5 +1,5 @@ -import { StitchesProps, styled } from '@hexx/theme'; -import { ReactNode, useCallback } from 'react'; +import { styled } from '@hexx/theme'; +import { ComponentProps, ReactNode, useCallback } from 'react'; import { SelectionChangePlugin } from '../../plugins'; import { generateGetBoundingClientRect } from '../../utils/virtual-element'; import Bold from '../icons/bold'; @@ -60,7 +60,7 @@ function DefaultInlineTool({ ...props }: UseInlineToolConfig & { children: ReactNode; -} & StitchesProps) { +} & ComponentProps) { const { getProps } = useDefaultInlineTool({ type, shortcut, @@ -76,11 +76,13 @@ function DefaultInlineTool({ export function InlineToolBar({ children, ...props -}: { children?: ReactNode } & StitchesProps) { +}: { children?: ReactNode } & ComponentProps) { return {children}; } -export function InlineBold(props: StitchesProps) { +export function InlineBold( + props: ComponentProps, +) { return ( ) { } export function InlineItalic( - props: StitchesProps, + props: ComponentProps, ) { return ( , + props: ComponentProps, ) { return ( ) { +}: { children?: ReactNode } & ComponentProps) { return ( @@ -145,7 +147,7 @@ export function InlineToolBarPreset({ ); } -export function InlineTool({ children }: { children?: ReactNode }) { +export function InlineTool({ children }: { children?: ReactNode }) { const popper = useReactPopper({ placement: 'bottom-start', modifiers: [ diff --git a/packages/editor/src/components/inline-toolbar/link/link.tsx b/packages/editor/src/components/inline-toolbar/link/link.tsx index 8f44200..7d9ef44 100644 --- a/packages/editor/src/components/inline-toolbar/link/link.tsx +++ b/packages/editor/src/components/inline-toolbar/link/link.tsx @@ -1,9 +1,9 @@ -import { StitchesProps, styled } from '@hexx/theme'; +import { styled } from '@hexx/theme'; import { useAtom } from 'jotai'; -import { useEffect, useRef, useState } from 'react'; +import { ComponentProps, useEffect, useRef, useState } from 'react'; import { ActiveBlock, - activeBlockAtom, + activeBlockAtom } from '../../../constants/atom'; import { getSelectionRange } from '../../../utils/ranges'; import Link from '../../icons/link'; @@ -12,7 +12,7 @@ import { useReactPopper } from '../../popper/use-react-popper'; import { isAnchorElement, useEventChangeSelection, - useInlineTool, + useInlineTool } from '../hooks'; import { IconWrapper } from '../inline-toolbar'; @@ -58,13 +58,13 @@ function highlight(r: Range | null) { return el; } -export function InlineLink(props: StitchesProps) { +export function InlineLink( + props: ComponentProps, +) { const [activeBlock] = useAtom(activeBlockAtom); const [initialValue, setInitialValue] = useState(''); - const [ - currentActiveBlock, - setCurrentActiveBlock, - ] = useState(null); + const [currentActiveBlock, setCurrentActiveBlock] = + useState(null); const snapHTML = useRef(); const editableSnap = useRef(); const [hasChanged, setHasChanged] = useState(false); @@ -131,9 +131,10 @@ export function InlineLink(props: StitchesProps) { return; } // @ts-ignore - const target = currentActiveBlock?.blockEl?.querySelector( - '.hexx-link-target', - ); + const target = + currentActiveBlock?.blockEl?.querySelector( + '.hexx-link-target', + ); if (!target) return; r.selectNodeContents(target); selection.removeAllRanges(); diff --git a/packages/editor/src/components/plus-button.tsx b/packages/editor/src/components/plus-button.tsx index d51a83f..b614494 100644 --- a/packages/editor/src/components/plus-button.tsx +++ b/packages/editor/src/components/plus-button.tsx @@ -1,12 +1,19 @@ -import { StitchesProps, styled } from '@hexx/theme'; +import { styled } from '@hexx/theme'; import { useAtom } from 'jotai'; -import { useEffect, useState } from 'react'; +import { ComponentProps, useEffect, useState } from 'react'; import { BlockAtom } from '../constants/atom'; import { useEditor, useEventListener } from '../hooks'; import { usePlugin } from '../plugins'; import { BlockType, isBlockEmpty } from '../utils/blocks'; import { findBlockById } from '../utils/find-blocks'; import { BlockMenu, BlockMenuItem } from './block-menu'; +import { + divider as DividerSvg, + header as HeaderSvg, + list as ListSvg, + quote as QuoteSvg, + text as TextIcon, +} from './icons'; import { PortalPopper } from './popper/portal-popper'; import { useReactPopper, @@ -68,8 +75,8 @@ const AddMenu = styled('div', { interface PlusButtonProps { popper?: UseReactPopperProps; menuPopper?: UseReactPopperProps; - iconProps?: StitchesProps; - menuProps?: StitchesProps; + iconProps?: ComponentProps; + menuProps?: ComponentProps; menu?: BlockMenuItem[]; } @@ -84,8 +91,7 @@ function useTabMenu( block: BlockType | null, ) { const { wrapperRef } = usePlugin(); - const { blockMap } = useEditor(); -; + const { blockScope } = useEditor(); useEventListener( 'keydown', (e) => { @@ -95,7 +101,7 @@ function useTabMenu( if (e.key === 'Tab' && !e.shiftKey) { if (block) { const isEmpty = isBlockEmpty( - blockMap[block.type], + blockScope[block.type], block.data, ); if (isEmpty) { @@ -148,9 +154,59 @@ function _PlusButton({ return null; } +export const presetPlusButtonMenu = [ + { + type: 'paragraph', + icon: { + text: 'text', + svg: TextIcon, + }, + }, + { + type: 'header', + icon: { + text: 'Header', + svg: HeaderSvg, + }, + }, + { + type: 'quote', + icon: { + text: 'Quote', + svg: QuoteSvg, + }, + }, + { + type: 'delimiter', + icon: { + text: 'Divider', + svg: DividerSvg, + }, + }, + { + type: 'list', + icon: { + text: 'Unordered List', + svg: ListSvg, + }, + }, + { + type: 'list', + icon: { + text: 'Ordered List', + svg: ListSvg, + }, + defaultValue: { + style: 'ordered', + }, + }, +]; + export function PlusButton(props: PlusButtonProps) { + const { menu = presetPlusButtonMenu } = props; const { hoverBlockAtom } = useEditor(); - const [activeAddingBlockAtom, setActiveAddingBlockAtom] = useState(); + const [activeAddingBlockAtom, setActiveAddingBlockAtom] = + useState(); const popper = useReactPopper({ defaultActive: false, @@ -206,7 +262,7 @@ export function PlusButton(props: PlusButtonProps) { menuPopper.setActive(false)} /> diff --git a/packages/editor/src/components/tune-button.tsx b/packages/editor/src/components/tune-button.tsx index 9079af1..aeecb68 100644 --- a/packages/editor/src/components/tune-button.tsx +++ b/packages/editor/src/components/tune-button.tsx @@ -1,14 +1,29 @@ -import { StitchesProps, styled } from '@hexx/theme'; +import { styled } from '@hexx/theme'; import { useAtom } from 'jotai'; -import { forwardRef, ReactNode, useEffect } from 'react'; +import { + ComponentProps, + forwardRef, + ReactNode, + useEffect +} from 'react'; import { $lastHoverAtom, BlockAtom } from '../constants/atom'; import { useEditor } from '../hooks'; import { findBlockById } from '../utils/find-blocks'; +import { + AlignCenter, + AlignLeft, + AlignRight, + h1, + h2, + h3, + IcNumList, + list as ListSvg +} from './icons'; import { PortalPopper } from './popper/portal-popper'; import { useReactPopper, UseReactPopperProps, - UseReactPopperReturn, + UseReactPopperReturn } from './popper/use-react-popper'; const Tune = styled('div', { @@ -51,8 +66,9 @@ const Icon = styled('svg', { interface TuneButtonProps { popper?: UseReactPopperProps; - buttonProps?: StitchesProps; + buttonProps?: ComponentProps; icon?: ReactNode; + config?: TuneConfig; } const Tunes = ({ @@ -60,20 +76,22 @@ const Tunes = ({ icon, popper, isSelecting, + config, }: { blockAtom: BlockAtom; icon?: ReactNode; popper: UseReactPopperReturn; isSelecting?: boolean; + config: TuneConfig; }) => { - const { blockMap, removeBlock, selectBlock } = useEditor(); + const { removeBlock, selectBlock } = useEditor(); const [currentBlockData, setBlockData] = useAtom(blockAtom); const tunes = currentBlockData && currentBlockData.type && typeof currentBlockData.type === 'string' && - blockMap[currentBlockData.type]?.block?.tune; + config[currentBlockData.type]; useEffect(() => { if (currentBlockData) { @@ -95,30 +113,31 @@ const Tunes = ({ <> {isSelecting ? ( <> - {tunes?.map((tune, i) => { - return ( - { - if (!currentBlockData) return; - setBlockData((s) => ({ - ...s, - data: tune.updater(currentBlockData?.data), - })); - selectBlock(); - popper.popperJs.update?.(); - e.stopPropagation(); - }} - /> - ); - })} + {Array.isArray(tunes) && + tunes.map((tune, i) => { + return ( + { + if (!currentBlockData) return; + setBlockData((s) => ({ + ...s, + data: tune.updater(currentBlockData?.data), + })); + selectBlock(); + popper.popperJs.update?.(); + e.stopPropagation(); + }} + /> + ); + })} { if (!currentBlockData) return; @@ -152,8 +171,94 @@ const Tunes = ({ ); }; +type TuneConfig = Record< + string, + { + icon: { + text: string; + svg: any; + isActive?: (data: any) => void; + }; + updater: (data: any) => any; + }[] +>; + +export const presetTuneConfig: TuneConfig = { + paragraph: [ + { + icon: { + text: 'Left', + svg: AlignLeft, + isActive: (data) => data.alignment === 'left', + }, + updater: (data) => ({ ...data, alignment: 'left' }), + }, + { + icon: { + text: 'Center', + svg: AlignCenter, + isActive: (data) => data.alignment === 'center', + }, + updater: (data) => ({ ...data, alignment: 'center' }), + }, + { + icon: { + text: 'Right', + svg: AlignRight, + isActive: (data) => data.alignment === 'right', + }, + updater: (data) => ({ ...data, alignment: 'right' }), + }, + ], + list: [ + { + icon: { + text: 'Bullet', + svg: ListSvg, + isActive: (data) => data.style === 'unordered', + }, + updater: (data) => ({ ...data, style: 'unordered' }), + }, + { + icon: { + text: 'Number', + svg: IcNumList, + isActive: (data) => data.style === 'ordered', + }, + updater: (data) => ({ ...data, style: 'ordered' }), + }, + ], + header: [ + { + icon: { + text: 'H1', + svg: h1, + isActive: (data) => data.level === 1, + }, + updater: (data) => ({ ...data, level: 1 }), + }, + { + icon: { + text: 'H2', + svg: h2, + isActive: (data) => data.level === 2, + }, + updater: (data) => ({ ...data, level: 2 }), + }, + { + icon: { + text: 'H3', + svg: h3, + isActive: (data) => data.level === 3, + }, + updater: (data) => ({ ...data, level: 3 }), + }, + ], +}; + export const TuneButton = forwardRef( (props: TuneButtonProps, ref) => { + const { config = presetTuneConfig } = props; const popper = useReactPopper({ defaultActive: false, placement: 'right-start', @@ -193,6 +298,7 @@ export const TuneButton = forwardRef( blockAtom={lastHoverBlock} popper={popper} icon={props.icon} + config={config} /> )} diff --git a/packages/editor/src/constants/atom.ts b/packages/editor/src/constants/atom.ts index d0c8e3e..033f760 100644 --- a/packages/editor/src/constants/atom.ts +++ b/packages/editor/src/constants/atom.ts @@ -1,8 +1,7 @@ import { atom, PrimitiveAtom } from 'jotai'; -import { atomFamily, atomWithReset } from 'jotai/utils'; +import { atomWithReset } from 'jotai/utils'; import { SetStateAction } from 'react'; -import { BlockType, BlockComponent } from '../utils/blocks'; -import { debounce } from '../utils/debounce'; +import { BlockComponent, BlockType } from '../utils/blocks'; export type BlockAtom = PrimitiveAtom>; @@ -155,10 +154,10 @@ export const blockSelectAtom = atom( blockSelectAtom.scope = _hexxScope; -export const blockMapAtom = atom< +export const blockScopeAtom = atom< Record> >({}); -blockMapAtom.scope = _hexxScope; +blockScopeAtom.scope = _hexxScope; export const _blockIdListAtom = atom([]); _blockIdListAtom.scope = _hexxScope; @@ -199,27 +198,3 @@ function updateHistory(data) { history.shift(); } } - -const debounceUpdateHistory = debounce(updateHistory, 200, false); - -export const blockIdListAtom = atom( - (get) => get(_blockIdListAtom), - (get, set, arg: SetStateAction) => { - const oldValue = get(_blockIdListAtom); - set(_blockIdListAtom, arg); - const newValue = get(_blockIdListAtom); - updateHistory({ - label: `${JSON.stringify(oldValue)} -> ${JSON.stringify( - newValue, - )}`, - undo: () => { - set(_blockIdListAtom, oldValue); - }, - redo: () => { - set(_blockIdListAtom, newValue); - }, - }); - }, -); - -blockIdListAtom.scope = _hexxScope; diff --git a/packages/editor/src/hooks/use-editor.ts b/packages/editor/src/hooks/use-editor.ts index d6ffe01..a168b9c 100644 --- a/packages/editor/src/hooks/use-editor.ts +++ b/packages/editor/src/hooks/use-editor.ts @@ -1,12 +1,13 @@ -import { atom, PrimitiveAtom, useAtom } from 'jotai'; +import { atom, PrimitiveAtom, SetStateAction, useAtom } from 'jotai'; import { useAtomCallback, useAtomValue } from 'jotai/utils'; -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback } from 'react'; import { v4 } from 'uuid'; +import { BlockData } from '../../../renderer/src/types'; import { $hoverAtom, BlockAtom, - blockMapAtom, blocksAtom, + blockScopeAtom, createAtom, editorDefaultBlockAtom, selectAtom, @@ -14,77 +15,37 @@ import { } from '../constants/atom'; import { insert, insertArray } from '../utils/array'; import { BlockType } from '../utils/blocks'; - -export const EditableWeakMap = new WeakMap< - HTMLDivElement | HTMLElement | Element, - { - blockIndex: number; - index: number; - id: string; - } ->(); +import { findBlockById } from '../utils/find-blocks'; +import { extractFragmentFromPosition } from '../utils/ranges'; export function useBlock( blockAtom: PrimitiveAtom>, - blockIndex: number, ) { const [block, setBlock] = useAtom(blockAtom); const [, setBlocks] = useAtom(blocksAtom); - const registeredRef = useRef>(); const remove = () => { setBlocks((s) => s.filter((item) => item !== blockAtom)); }; - const register = useCallback( - (ref, index: number = 0) => { - if (typeof blockIndex === 'undefined') { - throw new Error( - 'register editable block must provide blockIndex.', - ); - } - if (ref) { - EditableWeakMap.set(ref, { index, id: block.id, blockIndex }); - registeredRef.current?.push(ref); - } - }, - [blockIndex], - ); - - const registerByIndex = useCallback( - (index: number) => - useCallback((ref) => { - register(ref, index); - }, []), - [register], - ); - - useEffect(() => { - return () => { - const registeredList = registeredRef.current; - if (registeredList && registeredList.length > 0) { - for (const registered of registeredList) { - EditableWeakMap.delete(registered); - } - } - }; - }, []); - return { block, remove, update: setBlock, - registerByIndex, - register, }; } -export function useRemoveBlock() {} +function newBlockOnMount(id: string) { + requestAnimationFrame(() => { + const blockEl = findBlockById(id, true); + blockEl?.editable?.focus(); + }); +} export type UseEditorReturn = ReturnType; export function useEditor() { const [hoverBlockAtom] = useAtom($hoverAtom); - const blockMap = useAtomValue(blockMapAtom); + const blockScope = useAtomValue(blockScopeAtom); const [blockSelect, setBlockSelect] = useAtom(selectAtom); const defaultBlock = useAtomValue(editorDefaultBlockAtom); @@ -92,10 +53,51 @@ export function useEditor() { setBlockSelect(blockAtom ? new Set([blockAtom]) : new Set([])); }; + const splitBlock = useAtomCallback( + useCallback( + ( + get, + set, + { + atom, + updater, + }: { atom: BlockAtom; updater: SetStateAction }, + ) => { + const fragment = extractFragmentFromPosition(); + + if (!fragment) { + return; + } + + const { current, next } = fragment; + const currentBlock = get(atom); + set(atom, (s) => ({ + ...s, + data: { + ...s.data, + ...updater(current), + }, + })); + insertBlockAfter({ + atom, + block: { + type: currentBlock.type, + data: { + ...currentBlock.data, + ...updater(next), + }, + }, + }); + }, + [], + ), + _hexxScope, + ); + const insertBlockAfter = useAtomCallback( useCallback( - (get, set, arg: { atom: BlockAtom; block: any }) => { - let newBlock = { + (get, set, arg: { atom: BlockAtom; block: BlockData }) => { + let newBlock: BlockType = { ...arg.block, id: v4(), }; @@ -105,16 +107,17 @@ export function useEditor() { (d) => d === arg.atom, ); if (currentBockIndex > -1) { + const newBlockAtom = atom(newBlock); + newBlockAtom.scope = _hexxScope; + newBlockAtom.onMount = () => { + newBlockOnMount(newBlock.id); + }; set( blocksAtom, - insert( - blockData, - currentBockIndex + 1, - createAtom(newBlock), - ), + insert(blockData, currentBockIndex + 1, newBlockAtom), ); + return newBlockAtom; } - return newBlock; }, [blocksAtom], ), @@ -123,11 +126,16 @@ export function useEditor() { const insertBlock = useAtomCallback( useCallback( - (_, set, arg: { index?: number; block: any }) => { - let newBlock = createAtom({ + (_, set, arg: { index?: number; block: BlockData }) => { + const id = v4(); + let newBlock = atom({ ...arg.block, - id: v4(), + id, }); + newBlock.scope = _hexxScope; + newBlock.onMount = () => { + newBlockOnMount(id); + }; if (typeof arg.index === 'undefined') { set(blocksAtom, (s) => [...s, newBlock]); } else { @@ -209,12 +217,13 @@ export function useEditor() { replaceBlockById, batchRemoveBlocks, batchInsertBlocks, + splitBlock, clear, removeBlock, // data defaultBlock, blockSelect, - blockMap, + blockScope, hoverBlockAtom, // lastHoverBlock, selectBlock, diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 184e0d9..09b1519 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -3,4 +3,5 @@ export * from './utils/virtual-element'; export * from './hooks'; export * from './utils/find-blocks'; export * from './utils/blocks'; -export * from './parser/markdown/parser'; \ No newline at end of file +export * from './parser/markdown/parser'; +export * from './preset' \ No newline at end of file diff --git a/packages/editor/src/parser/html/parser.ts b/packages/editor/src/parser/html/parser.ts index b7e7341..d24add6 100644 --- a/packages/editor/src/parser/html/parser.ts +++ b/packages/editor/src/parser/html/parser.ts @@ -1,13 +1,21 @@ import { Parent, Content, BlockContent } from 'mdast'; import { processor } from './processor'; import toMdast from 'hast-util-to-mdast'; -import { AllMdastConfig } from '../types'; +import all from 'hast-util-to-mdast/lib/all'; +import { MdastConfigs } from '../types'; export const htmlToHast = (html: string) => processor.parse(html); -export const htmlToMdast = (html: string): Parent => - toMdast(htmlToHast(html)); +export const htmlToMdast = (html: string): Parent => { + return toMdast(htmlToHast(html), { + handlers: { + u: (h, node) => { + return h(node, 'underline', all(h, node)); + }, + }, + }); +}; export const isAvailableBlockContent = ( content: Content, - allMdastConfig: AllMdastConfig, + allMdastConfig: MdastConfigs, ): content is BlockContent => content.type in allMdastConfig; diff --git a/packages/editor/src/parser/markdown/parser.ts b/packages/editor/src/parser/markdown/parser.ts index 677f0a6..8d939ba 100644 --- a/packages/editor/src/parser/markdown/parser.ts +++ b/packages/editor/src/parser/markdown/parser.ts @@ -1,22 +1,20 @@ +import { BlockData } from '@hexx/renderer/src/types'; +import toDom from 'hast-util-to-dom'; import { Parent } from 'mdast'; -import toHast from 'mdast-util-to-hast'; -import toDOM from 'hast-util-to-dom'; import fromMarkdown from 'mdast-util-from-markdown'; -import { BlockComponent, BlockType } from '../../utils/blocks'; -import { isAvailableBlockContent } from '../html/parser'; -import { AllMdastConfig, MdastConfig } from '../types'; -import { BlockData } from '@hexx/renderer/src/types'; +import toHast from 'mdast-util-to-hast'; import { v4 } from 'uuid'; +import { BlockType } from '../../utils/blocks'; +import { isAvailableBlockContent } from '../html/parser'; +import { MdastConfigs } from '../types'; export function createHexxMarkdownParser( - blockMap: Record>, + mdastConfigs: MdastConfigs, config?: MDToDataConfig, ) { - const allMdastConfig = getAllMdastConfig(blockMap); return { toData: (markdown: string) => - markdownToData(allMdastConfig, markdown, config), - getAllMdastConfig, + markdownToData(mdastConfigs, markdown, config), }; } @@ -26,7 +24,7 @@ interface MDToDataConfig { } function markdownToData( - allMdastConfig: AllMdastConfig, + allMdastConfig: MdastConfigs, markdown: string, config?: MDToDataConfig, ) { @@ -34,58 +32,36 @@ function markdownToData( return mdastToData(allMdastConfig, mdast, config); } export function mdastToData( - allMdastConfig: AllMdastConfig, + allMdastConfig: MdastConfigs, mdast: Parent, config?: MDToDataConfig, ) { let results: BlockData[] = []; + console.log(allMdastConfig, 'allMdastConfig'); for (const children of mdast.children) { if (isAvailableBlockContent(children, allMdastConfig)) { const mdastConfig = allMdastConfig[children.type]; - let result: BlockData | BlockType = { - type: mdastConfig.blockType, - data: - typeof mdastConfig.in === 'function' - ? mdastConfig.in(children, (c) => { - const hast = toHast(c); - const dom = toDOM(hast, { - document: config?.document, - }); - return dom; - }) - : {}, - }; - if (config?.autoGenerateId) { - // @ts-ignore - result.id = v4(); + if (mdastConfig) { + let result: BlockData | BlockType = { + type: mdastConfig.type, + data: + typeof mdastConfig.in === 'function' + ? mdastConfig.in(children, (c) => { + const hast = toHast(c); + const dom = toDom(hast, { + document: config?.document, + }); + return dom; + }) + : {}, + }; + if (config?.autoGenerateId) { + // @ts-ignore + result.id = v4(); + } + results.push(result); } - results.push(result); } } return results; } - -export function getAllMdastConfig( - blockMap: Record>, -) { - let result = {} as AllMdastConfig; - const arrayTagsConfig = Object.values(blockMap) - .map((map) => { - if (map.block?.mdast) { - return { - blockType: map.block.type, - ...map.block.mdast, - }; - } - return null; - }) - .filter(Boolean) as Array<{ blockType: string } & MdastConfig>; - for (const config of arrayTagsConfig) { - result[config.type] = { - blockType: config.blockType, - type: config.type, - in: config.in, - }; - } - return result; -} diff --git a/packages/editor/src/parser/markdown/use-block-mdast.ts b/packages/editor/src/parser/markdown/use-block-mdast.ts deleted file mode 100644 index 8cc6a31..0000000 --- a/packages/editor/src/parser/markdown/use-block-mdast.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useAtom } from 'jotai'; -import { useMemo } from 'react'; -import { blockMapAtom } from '../../constants/atom'; -import { getAllMdastConfig } from './parser'; - -export function useBlockMdast() { - const [blockMap] = useAtom(blockMapAtom); - - const allMdastConfig = useMemo( - () => getAllMdastConfig(blockMap), - [], - ); - - return { - allMdastConfig, - }; -} diff --git a/packages/editor/src/parser/types.ts b/packages/editor/src/parser/types.ts index 9b65693..79e113d 100644 --- a/packages/editor/src/parser/types.ts +++ b/packages/editor/src/parser/types.ts @@ -1,12 +1,10 @@ import { Content } from 'mdast'; -import { BlockType } from '../utils/blocks'; -export type MdastConfig = { - type: K; - in: (content: Content, toHTML: Function) => BlockType['data']; +export type MdastConfig = { + in?: (content: any, toHTML: Function) => any; }; -export type AllMdastConfig = { - [key in Content['type']]: { - blockType: string; +export type MdastConfigs = { + [key in Content['type'] | 'root']?: { + type: string; } & MdastConfig; }; diff --git a/packages/editor/src/plugins/index.ts b/packages/editor/src/plugins/index.ts index d31e8f2..a2f47f7 100644 --- a/packages/editor/src/plugins/index.ts +++ b/packages/editor/src/plugins/index.ts @@ -8,4 +8,5 @@ export * from './file-drop'; export * from './local-storage'; export * from './editor-width'; export * from './linkify-it'; -export * from './markdown-shortcut'; \ No newline at end of file +export * from './markdown-shortcut'; +export * from './paste'; diff --git a/packages/editor/src/plugins/markdown-shortcut.ts b/packages/editor/src/plugins/markdown-shortcut.ts index 4f6d253..58fe364 100644 --- a/packages/editor/src/plugins/markdown-shortcut.ts +++ b/packages/editor/src/plugins/markdown-shortcut.ts @@ -1,7 +1,6 @@ -import toDOM from 'hast-util-to-dom'; +import toDom from 'hast-util-to-dom'; import toHast from 'mdast-util-to-hast'; import { useEditor, useEventListener } from '../hooks'; -import { useBlockMdast } from '../parser/markdown/use-block-mdast'; import { isContentEditableDiv } from '../utils/is'; import { usePlugin } from './plugin'; import fromMarkdown from 'mdast-util-from-markdown'; @@ -15,6 +14,7 @@ import { Content, } from 'mdast'; import { v4 } from 'uuid'; +import { MdastConfigs } from '../parser/types'; type WholeBlock = Heading | ThematicBreak | Blockquote | List | Code; @@ -54,10 +54,14 @@ const replaceInlineMarkdown = ( replace: string, ) => str.replace(match, replace); -// it's a very rough version of markdown parser for editor -export function Unstable_MarkdownShortcutPlugin() { +type MarkdownShortcutPluginProps = { + mdastConfigs: MdastConfigs; +}; + +export function Unstable_MarkdownShortcutPlugin({ + mdastConfigs, +}: MarkdownShortcutPluginProps) { const { wrapperRef } = usePlugin(); - const { allMdastConfig } = useBlockMdast(); const { replaceBlockById } = useEditor(); useEventListener( @@ -76,17 +80,17 @@ export function Unstable_MarkdownShortcutPlugin() { const content = mdast.children[0]; if (content && isWholeBlock(content)) { const firstMdast = mdast.children[0]; - const mdastConfig = allMdastConfig[firstMdast.type]; + const mdastConfig = mdastConfigs[firstMdast.type]; if (mdastConfig) { replaceBlockById({ id: currentBlockId, block: { - type: mdastConfig.blockType, + type: mdastConfig.type, id: v4(), data: typeof mdastConfig.in === 'function' ? mdastConfig.in(firstMdast, (c) => - toDOM(toHast(c)), + toDom(toHast(c)), ) : {}, }, @@ -181,7 +185,7 @@ function getTextNodeAtPosition(root, index) { let treeWalker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, - function next(elem) { + function (elem) { if (index >= elem.textContent.length) { index -= elem.textContent.length; lastNode = elem; diff --git a/packages/editor/src/plugins/new-block-overlay.tsx b/packages/editor/src/plugins/new-block-overlay.tsx index c4f45e1..b2f1bb3 100644 --- a/packages/editor/src/plugins/new-block-overlay.tsx +++ b/packages/editor/src/plugins/new-block-overlay.tsx @@ -27,7 +27,8 @@ export function NewBlockOverlayPlugin(props: { const [lastBlock] = useAtom($lastBlockAtom); - const { blockSelect, selectBlock, insertBlock, blockMap } = editor; + const { blockSelect, selectBlock, insertBlock, blockScope } = + editor; const handleClick = useAtomCallback( useCallback( @@ -52,7 +53,7 @@ export function NewBlockOverlayPlugin(props: { if (!lastBlockType) { return; } - const blockType = blockMap[lastBlockType.type]; + const blockType = blockScope[lastBlockType.type]; if ( blockType && diff --git a/packages/editor/src/plugins/paste.ts b/packages/editor/src/plugins/paste.ts index ede18e0..98962a1 100644 --- a/packages/editor/src/plugins/paste.ts +++ b/packages/editor/src/plugins/paste.ts @@ -7,14 +7,19 @@ import { blocksIdsAtom } from '../constants/atom'; import { useEventListener } from '../hooks'; import { htmlToMdast } from '../parser/html/parser'; import { mdastToData } from '../parser/markdown/parser'; -import { useBlockMdast } from '../parser/markdown/use-block-mdast'; +import { MdastConfigs } from '../parser/types'; import { usePlugin } from './plugin'; -export function PastHtmlPlugin() { +type PastHtmlPluginProps = { + mdastConfigs: MdastConfigs; +}; + +export function PastHtmlPlugin({ + mdastConfigs, +}: PastHtmlPluginProps) { const { wrapperRef, activeBlock, editor } = usePlugin(); const { batchInsertBlocks } = editor; const [idList] = useAtom(blocksIdsAtom); - const { allMdastConfig } = useBlockMdast(); useEventListener( 'paste', @@ -23,6 +28,7 @@ export function PastHtmlPlugin() { const text = e.clipboardData?.getData('text/plain'); const hexx = e.clipboardData?.getData(CLIPBOARD_DATA_FORMAT); const index = idList.findIndex((id) => id === activeBlock?.id); + console.log(html, text); if (hexx) { batchInsertBlocks({ index, @@ -35,14 +41,14 @@ export function PastHtmlPlugin() { } else if (html) { const mdastParent = htmlToMdast(html); try { - const results = mdastToData(allMdastConfig, mdastParent); + const results = mdastToData(mdastConfigs, mdastParent); if (results.length > 0) { batchInsertBlocks({ blocks: results, index }); - e.preventDefault(); } } catch (error) { console.error('[hexx] error when pasting html', error); } + e.preventDefault(); } else if (text) { const mdast = fromMarkdown(text) as Parent; if ( @@ -52,14 +58,14 @@ export function PastHtmlPlugin() { return; } try { - const results = mdastToData(allMdastConfig, mdast); + const results = mdastToData(mdastConfigs, mdast); if (results.length > 0) { batchInsertBlocks({ blocks: results, index }); - e.preventDefault(); } } catch (error) { console.error('[hexx] error when pasting markdown', error); } + e.preventDefault(); } }, wrapperRef, diff --git a/packages/editor/src/preset.ts b/packages/editor/src/preset.ts new file mode 100644 index 0000000..3a4a22e --- /dev/null +++ b/packages/editor/src/preset.ts @@ -0,0 +1,40 @@ +import * as mdast from 'mdast'; +import { MdastConfigs } from './parser/types'; + +export const presetMDASTConfig: MdastConfigs = { + root: { + type: 'paragraph', + in: (content: mdast.Paragraph, toHTML) => ({ + text: toHTML(content).outerHTML, + }), + }, + paragraph: { + type: 'paragraph', + in: (content: mdast.Paragraph, toHTML) => ({ + text: toHTML(content).outerHTML, + }), + }, + blockquote: { + type: 'quote', + in: (content: mdast.Blockquote, toHTML) => ({ + text: toHTML(content).innerHTML, + }), + }, + list: { + type: 'list', + in: (content: mdast.List, toHTML) => ({ + style: content.ordered ? 'ordered' : 'unordered', + items: content.children.map((child) => toHTML(child).innerHTML), + }), + }, + heading: { + type: 'header', + in: (mdast: mdast.Heading, toHTML) => ({ + text: toHTML(mdast).innerHTML, + level: mdast.depth || 3, + }), + }, + thematicBreak: { + type: 'delimiter', + }, +}; diff --git a/packages/editor/src/utils/blocks.ts b/packages/editor/src/utils/blocks.ts index 5a9dbf4..e4d22ab 100644 --- a/packages/editor/src/utils/blocks.ts +++ b/packages/editor/src/utils/blocks.ts @@ -1,4 +1,4 @@ -import { StitchesCssProp } from '@hexx/theme'; +import { CSS } from '@hexx/theme'; import { PrimitiveAtom } from 'jotai'; import { BlockContent, PhrasingContent } from 'mdast'; import { FunctionComponent, ReactNode } from 'react'; @@ -12,10 +12,6 @@ export type BlockType = { interface BlockConfig { type: string; config?: Config; - icon?: { - text: string; - svg: any; - }; defaultValue: Partial; isEmpty?: (d: Data) => boolean; mdast?: { @@ -27,15 +23,7 @@ interface BlockConfig { toHTML: (child: BlockContent['children']) => HTMLElement, ) => Data; }; - tune?: Array<{ - icon: { - text: string; - svg: any; - isActive?: (data: Data) => boolean; - }; - updater: (data: Data) => Data; - }>; - css?: StitchesCssProp; + css?: CSS; [x: string]: any; } @@ -45,7 +33,7 @@ export interface BlockProps { config?: C; children?: ReactNode; blockAtom: PrimitiveAtom>; - css?: StitchesCssProp; + css?: CSS; } interface BlockComponentBefore diff --git a/packages/editor/src/utils/find-blocks.ts b/packages/editor/src/utils/find-blocks.ts index 12ee5a3..46df313 100644 --- a/packages/editor/src/utils/find-blocks.ts +++ b/packages/editor/src/utils/find-blocks.ts @@ -1,56 +1,89 @@ -import { EditableWeakMap } from '../hooks/use-editor'; import { getSelectionRange } from './ranges'; function isBrowser() { return typeof window !== 'undefined'; } +function focusWithCaretIndex( + el: HTMLDivElement, + direction: 'up' | 'down', + caretIndex: number, +) { + const range = document.createRange(); + const sel = window.getSelection(); + const treeWalker = document.createTreeWalker( + el, + NodeFilter.SHOW_TEXT, + ); + + try { + if (direction === 'down') { + const nextTextNode = treeWalker.nextNode(); + let textLength = nextTextNode?.textContent?.length ?? 0; + if (nextTextNode) { + range.setStart( + nextTextNode, + textLength >= caretIndex ? caretIndex : textLength, + ); + el.scrollIntoView({ block: 'end' }); + } + } else { + const nextTextNode = treeWalker.nextNode(); + let textLength = nextTextNode?.textContent?.length ?? 0; + if (nextTextNode) { + range.setStart( + nextTextNode, + textLength >= caretIndex ? caretIndex : textLength, + ); + el.scrollIntoView({ block: 'start' }); + } + } + range.collapse(true); + + sel?.removeAllRanges(); + sel?.addRange(range); + } catch (error) {} +} + function focusContentEditableWithOffset( - currentActiveEl?: HTMLDivElement | HTMLElement | Element | null, - offset: number = 0, + currentActiveEl: HTMLDivElement | HTMLElement | Element | null, + direction: 'up' | 'down', + caretOffset: number, ) { if (!currentActiveEl) { return; } - const editableWeakData = EditableWeakMap.get(currentActiveEl); - if (editableWeakData && editableWeakData.blockIndex + offset >= 0) { - // first contenteditable - if (editableWeakData.index === 0) { - // find next contenteditable if existed - const offsetBlock = findBlockByIndex( - editableWeakData.blockIndex + offset, + if (direction === 'down') { + if (!currentActiveEl.nextElementSibling) return; + const nextEditable = + currentActiveEl.nextElementSibling.querySelector( + '[contenteditable]', + ) as HTMLDivElement; + + if (!nextEditable) { + focusContentEditableWithOffset( + currentActiveEl.nextElementSibling, + direction, + caretOffset, ); - if (offsetBlock?.editable) { - focusWithLastCursor(offsetBlock.editable, offset <= 0); - } else { - focusContentEditableWithOffset( - findPreviousContentEditable( - editableWeakData.blockIndex, - offset, - ), - 0, - ); - } } else { - const currentBlockDiv = findBlockById(editableWeakData.id)?.el; - const nodeList = currentBlockDiv?.querySelectorAll( + focusWithCaretIndex(nextEditable, direction, caretOffset); + } + } + if (direction === 'up') { + if (!currentActiveEl.previousElementSibling) return; + const prevEditable = + currentActiveEl.previousElementSibling?.querySelector( '[contenteditable]', + ) as HTMLDivElement; + if (!prevEditable) { + focusContentEditableWithOffset( + currentActiveEl.previousElementSibling, + direction, + caretOffset, ); - if ( - nodeList && - nodeList?.length > 0 && - nodeList?.[editableWeakData.index + offset] - ) { - focusWithLastCursor( - nodeList[editableWeakData.index + offset] as HTMLDivElement, - offset > 0, - ); - } else { - focusContentEditableWithOffset( - currentBlockDiv, - offset + offset, - ); - } + } else { + focusWithCaretIndex(prevEditable, direction, caretOffset); } } } @@ -72,10 +105,18 @@ function findPreviousContentEditable( export function focusContentEditable( query: 'up' | 'down' | 'current', + caretOffset: number, ) { const isActiveElementEditable = document.activeElement?.getAttribute('contenteditable') === 'true'; + let activeBlockElement; + + if (isActiveElementEditable) { + activeBlockElement = document.activeElement?.closest( + '.hexx-block-wrapper', + ); + } switch (query) { case 'current': if (isActiveElementEditable) { @@ -90,10 +131,22 @@ export function focusContentEditable( } break; case 'up': - focusContentEditableWithOffset(document.activeElement, -1); + if (activeBlockElement) { + focusContentEditableWithOffset( + activeBlockElement, + 'up', + caretOffset, + ); + } break; case 'down': - focusContentEditableWithOffset(document.activeElement, 1); + if (activeBlockElement) { + focusContentEditableWithOffset( + activeBlockElement, + 'down', + caretOffset, + ); + } break; default: break; diff --git a/packages/renderer/README.md b/packages/renderer/README.md new file mode 100644 index 0000000..9335076 --- /dev/null +++ b/packages/renderer/README.md @@ -0,0 +1,24 @@ +## Hexx Renderer + +install package + +```bash +npm install @hexx/editor +# or +yarn add @hexx/editor +``` + +### Example + +```tsx +import { EditorRenderer, PresetScope } from '@hexx/renderer'; + +const blocks = [{ type: 'paragraph', data: { text: 'render' }}] + + +``` diff --git a/packages/renderer/src/block/delimiter.tsx b/packages/renderer/src/block/delimiter.tsx index a1ceaaf..fed2be5 100644 --- a/packages/renderer/src/block/delimiter.tsx +++ b/packages/renderer/src/block/delimiter.tsx @@ -1,4 +1,4 @@ -import { css, StitchesStyleObject } from '@hexx/theme'; +import { css, CSS } from '@hexx/theme'; import * as React from 'react'; export type Delimiter = { @@ -6,7 +6,7 @@ export type Delimiter = { data: {}; }; -export const dividerStyles: StitchesStyleObject = { +export const dividerStyles: CSS = { lineHeight: '1.6em', width: '100%', height: 'auto', diff --git a/packages/renderer/src/block/list.tsx b/packages/renderer/src/block/list.tsx index 03331d7..e08523f 100644 --- a/packages/renderer/src/block/list.tsx +++ b/packages/renderer/src/block/list.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import ReactHtmlParser from 'react-html-parser'; -import { css, StitchesStyleObject } from '@hexx/theme'; +import { css, CSS } from '@hexx/theme'; export type List = { type: 'list'; data: { @@ -16,19 +16,19 @@ const commonListStyle = { left: 18, } as const; -export const listStyle: StitchesStyleObject = { +export const listStyle = { ul: { listStyle: 'disc', ...commonListStyle, - }, + } as CSS, ol: { listStyle: 'decimal', ...commonListStyle, - }, + } as CSS, item: { padding: '5.5px 0 5.5px 3px', lineHeight: '1.6em', - }, + } as CSS, }; export const ListRenderer = ({ data }: { data: List['data'] }) => { diff --git a/packages/renderer/src/block/paragraph.tsx b/packages/renderer/src/block/paragraph.tsx index abd9542..bdaf208 100644 --- a/packages/renderer/src/block/paragraph.tsx +++ b/packages/renderer/src/block/paragraph.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import ReactHtmlParser from 'react-html-parser'; -import { css, StitchesStyleObject } from '@hexx/theme'; +import { css, CSS } from '@hexx/theme'; export type Paragraph = { type: 'paragraph'; data: { @@ -9,7 +9,7 @@ export type Paragraph = { }; }; -export const paragraphStyle: StitchesStyleObject = { +export const paragraphStyle: CSS = { p: { lineHeight: '24px', fontSize: '16px', diff --git a/packages/renderer/src/block/quote.tsx b/packages/renderer/src/block/quote.tsx index bbeb158..df083bc 100644 --- a/packages/renderer/src/block/quote.tsx +++ b/packages/renderer/src/block/quote.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import ReactHtmlParser from 'react-html-parser'; -import { css, StitchesStyleObject } from '@hexx/theme'; +import { css, CSS } from '@hexx/theme'; export type Quote = { type: 'quote'; @@ -11,13 +11,13 @@ export type Quote = { }; }; -export const quoteStyle: StitchesStyleObject = { +export const quoteStyle = { wrapper: { paddingLeft: '24px', paddingRight: '24px', paddingTop: 3, paddingBottom: 3, - }, + } as CSS, text: { borderTop: 'none', borderRight: 'none', @@ -37,7 +37,7 @@ export const quoteStyle: StitchesStyleObject = { fontStyle: 'italic', color: 'rgb(36, 37, 38)', minHeight: 'unset !important', - }, + } as CSS, }; export const QuoteRenderer = ({ data }: { data: Quote['data'] }) => { diff --git a/packages/renderer/src/block/text.tsx b/packages/renderer/src/block/text.tsx new file mode 100644 index 0000000..dd5bc07 --- /dev/null +++ b/packages/renderer/src/block/text.tsx @@ -0,0 +1,16 @@ +import { Text, InlineCode, Emphasis, Strong, AlignType } from 'mdast'; + +export type Leaf = Text | InlineCode; + +// export const LeafRenderer = ({ data, children }: Leaf) => { +// return ( +// <> +// {data?.children?.map(child => { +// child. +// return null +// })} +// +// ) +// } + + diff --git a/packages/renderer/src/index.ts b/packages/renderer/src/index.ts index 9c6f575..f853e79 100644 --- a/packages/renderer/src/index.ts +++ b/packages/renderer/src/index.ts @@ -1,3 +1,3 @@ export { EditorRenderer } from './renderer'; -export { BlockMap } from './block-map'; +export { PresetScope } from './preset-scope'; export * from './block'; \ No newline at end of file diff --git a/packages/renderer/src/block-map.ts b/packages/renderer/src/preset-scope.ts similarity index 92% rename from packages/renderer/src/block-map.ts rename to packages/renderer/src/preset-scope.ts index 354d51e..f17400c 100644 --- a/packages/renderer/src/block-map.ts +++ b/packages/renderer/src/preset-scope.ts @@ -1,7 +1,7 @@ import { Block } from './types'; import * as DefaultBlocks from './block'; -export const BlockMap: { +export const PresetScope: { [key in Block['type'] | string]: any; } = { paragraph: DefaultBlocks.ParagraphRenderer, diff --git a/packages/renderer/src/renderer.tsx b/packages/renderer/src/renderer.tsx index 08cf803..b8d2f11 100644 --- a/packages/renderer/src/renderer.tsx +++ b/packages/renderer/src/renderer.tsx @@ -1,15 +1,15 @@ -import { StitchesProps, styled } from '@hexx/theme'; +import { styled } from '@hexx/theme'; import * as React from 'react'; import { BlockData } from './types'; interface EditorRendererProps { blocks: BlockData[]; - blockMap: { + scope: { [x: string]: any; }; maxWidth?: string; - wrapper?: StitchesProps; - blockWrapper?: StitchesProps; + wrapper?: React.ComponentProps; + blockWrapper?: React.ComponentProps; } const Wrapper = styled('div', { @@ -30,7 +30,7 @@ const BlockWrapper = styled('div', { export function EditorRenderer({ blocks, - blockMap, + scope, maxWidth = '720px', blockWrapper, wrapper, @@ -40,13 +40,10 @@ export function EditorRenderer({ } const content = React.Children.toArray( blocks.map((block) => { - let Renderer = blockMap[block?.type]; + let Renderer = scope[block?.type]; if (!Renderer) { return null; } - if (typeof block.data !== 'object') { - return null; - } const hasStretched = 'stretched' in block.data && !!(block.data as any).stretched; diff --git a/packages/theme/src/stitches.config.ts b/packages/theme/src/stitches.config.ts index a1d3e64..674d5e1 100644 --- a/packages/theme/src/stitches.config.ts +++ b/packages/theme/src/stitches.config.ts @@ -1,9 +1,4 @@ -import { - createCss, - TCssProp, - StitchesProps, - TCssWithBreakpoints, -} from '@stitches/react'; +import { createCss, StitchesCss } from '@stitches/react'; const config = { prefix: 'hexx', @@ -28,15 +23,9 @@ const config = { }, }; -export const { css, styled, global, getCssString } = createCss( - config, -); +export const stitchesCss = createCss(config); -export type { StitchesProps }; +export const { css, styled, global, getCssString } = stitchesCss; -export type StitchesStyleObject = Record< - string, - TCssProp ->; +export type CSS = StitchesCss; -export type StitchesCssProp = TCssWithBreakpoints; diff --git a/yarn.lock b/yarn.lock index 2dbacc3..1991680 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1761,34 +1761,34 @@ dependencies: debug "^4.1.1" -"@size-limit/file@5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@size-limit/file/-/file-5.0.0.tgz#29986ad7bac6208e289a38d5ae8007e60135f371" - integrity sha512-699vC9SlQWwJKtAurF9oE20gsfGhZnIiaw5YOZHkM6v6jb+qfns2Kdh63r/WPGJMcyc8J74pZvRt+CK8vLwlJg== +"@size-limit/file@5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@size-limit/file/-/file-5.0.1.tgz#8cf5a550a88fe5972db31e65cfd0b040485fd141" + integrity sha512-BgbGFdFwijuR6tZRlmUoN3UzOIN+dt+28hzsYKjTl1LOn4zUJjTWgTCye11Qygj5L6+MkDZOA1ppEm4ZfjPgmQ== dependencies: semver "7.3.5" -"@size-limit/preset-big-lib@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@size-limit/preset-big-lib/-/preset-big-lib-5.0.0.tgz#a022591a924c360033ea256f7ec87446a76030ca" - integrity sha512-gBEjzcl0VTMeI1cBvHKgjte4PvUZd7ZeYV+tw2/Qn+s+UKtqrVVvM67Raf5k7GqWEbjcpqvDYW63vJBVwBBspQ== +"@size-limit/preset-big-lib@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@size-limit/preset-big-lib/-/preset-big-lib-5.0.1.tgz#ca4e7e37b0a12a24378256f1b0955d7811a88996" + integrity sha512-D9UTwN7kzK7qFGUY8rOd0nenRp2BkgCmr1widKalvI8q0Q2CjRBmLiSSBa4cSgjkBTT0/jTdMGWbnLXPKoxhHA== dependencies: - "@size-limit/file" "5.0.0" - "@size-limit/time" "5.0.0" - "@size-limit/webpack" "5.0.0" + "@size-limit/file" "5.0.1" + "@size-limit/time" "5.0.1" + "@size-limit/webpack" "5.0.1" -"@size-limit/time@5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@size-limit/time/-/time-5.0.0.tgz#9865258e0ce124d3d95e3a8d3b2594f0219bb5ac" - integrity sha512-LwjxLm81qbkjb3bcMpq52Jz1+M1p1MhQ4Fjh3ntSELdzA8Jaoze9L1ff7VSqR1zh0XhqSSfDH7t8JYPssv4rmA== +"@size-limit/time@5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@size-limit/time/-/time-5.0.1.tgz#0566ba1f7a139b4b593352f1d34f7d33dc9f572f" + integrity sha512-VcEQF3ejYPf97CjcJ6jnRTn8ypORma2uoeq2lPr+KEBTNoaUAnZJ1ByrXmhriz0iOSJ0WYKMnsVbaqnabC7RJQ== dependencies: estimo "^2.2.8" react "^17.0.2" -"@size-limit/webpack@5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@size-limit/webpack/-/webpack-5.0.0.tgz#bf62fecff81e008a8390c64bae667ba55f932785" - integrity sha512-lwlMxX2XZ/2DNLsb7bpxBRVl/1WgowzZ8bi9WIj1ywVEJEtG1f7q2WLVYMzMtIG312HHJ80pCZfZAwd+Hq32aQ== +"@size-limit/webpack@5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@size-limit/webpack/-/webpack-5.0.1.tgz#029495f5227efec22ffe3c36e3b7f23d2d2a0c22" + integrity sha512-dG/H0GRc4P5Dc3KRjaQBq7EfGiOqGDw/eTBdkzvd2VnELl7DFcgoC1hULu9Y0XL50Ha+dGQ1AtNKWsZ3dacH/Q== dependencies: css-loader "^5.2.6" escape-string-regexp "^4.0.0" @@ -2010,10 +2010,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.15.tgz#0de7e978fb43db62da369db18ea088a63673c182" integrity sha512-lowukE3GUI+VSYSu6VcBXl14d61Rp5hA1D+61r16qnwC0lYNSqdxcvRh0pswejorHfS+HgwBasM8jLXz0/aOsw== -"@types/node@^15.12.4": - version "15.12.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.4.tgz#e1cf817d70a1e118e81922c4ff6683ce9d422e26" - integrity sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA== +"@types/node@^15.12.5": + version "15.12.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.5.tgz#9a78318a45d75c9523d2396131bd3cca54b2d185" + integrity sha512-se3yX7UHv5Bscf8f1ERKvQOD6sTyycH3hdaoozvaLxgUiY5lIGEeH37AD0G0Qi9kPqihPn0HOfd2yaIEN9VwEg== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -3150,11 +3150,6 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-support@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - colord@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/colord/-/colord-2.0.1.tgz#1e7fb1f9fa1cf74f42c58cb9c20320bab8435aa0" @@ -5334,9 +5329,9 @@ jest-worker@^26.3.0: supports-color "^7.0.0" jotai@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.1.0.tgz#a0f1afd15a187ab5578250d54a42bac999bfbaf9" - integrity sha512-+c/bMxS/c1LgsOYVfevOYLAu+WEgn85TJhmBoNSNHjMV+mTKZY0ACSyCrX1yz8KialK4+Ggm6d7Ek3sTXy8UgA== + version "1.1.1" + resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.1.1.tgz#e79a267753f0a0d1173dddc4a034effb4d48d840" + integrity sha512-Sse+QgFbF8wfv3Vo1JcWQoB+utg2syajSLNi6B/xi+A5iCvobm7fqY2HHfGTvzkNf6t4IIB1dQlVkUqHPEB4Tg== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -5800,13 +5795,12 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -mico-spinner@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/mico-spinner/-/mico-spinner-1.0.0.tgz#3ec7a3a5343019db1d97812bfdfc802ee8c277f8" - integrity sha512-tQ1ros/rb3N3nC5kRgk+AMcBMJAEpBRKASSOGQeq9WyIfSoe6eq464OuNTA/NjYUpz4fca4qqMEpR7nqVXLHVQ== +mico-spinner@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/mico-spinner/-/mico-spinner-1.1.1.tgz#b904cf4dc1ae247d9bff24b969d8bcbb08371f5d" + integrity sha512-b9F3Qx9l6fO141+FbR9hqRUJ15p4bGVsqgXiO/noxxXZmN+hYZVCuWd4LEubadrVkw0eIYAJDXWo0ZYGoLrmfg== dependencies: - ansi-colors "^4.1.1" - color-support "^1.1.3" + colorette "^1.2.2" micromark@~2.10.0: version "2.10.1" @@ -6868,10 +6862,10 @@ prettier@^2.1.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5" integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q== -prettier@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6" - integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA== +prettier@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d" + integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== prism-react-renderer@^1.2.1: version "1.2.1" @@ -7574,10 +7568,10 @@ sirv@^1.0.7: mime "^2.3.1" totalist "^1.0.0" -size-limit@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-5.0.0.tgz#eea703ccde7b37199880e8dcf17b2f313c2da17a" - integrity sha512-4d3tjcMF2OVWbM4JMxZGgQJXC7Z7veZrMWIog8N6DPhSDOhqRoiXDcPM5zY9uM9EvIgHqrdf9xgNKb0eFS0D6g== +size-limit@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-5.0.1.tgz#9aedea30bfb7cea17e78d86bc19c61d3df53b91e" + integrity sha512-FZwVay5fCdatHoZU1lGyvVK3KrI4puMKbU1UoLLhEaMjQBxPuovuF+eJIDzmJN+xZRk3thuG7yS77DhuWF4fEQ== dependencies: bytes-iec "^3.1.1" chokidar "^3.5.2" @@ -7585,7 +7579,7 @@ size-limit@^5.0.0: colorette "^1.2.2" globby "^11.0.4" lilconfig "^2.0.3" - mico-spinner "1.0.0" + mico-spinner "^1.1.1" slash@^2.0.0: version "2.0.0"