From de9e9cdcddd367027cf32ec4c31343d5c6b07240 Mon Sep 17 00:00:00 2001 From: bishoy melek Date: Mon, 10 Jan 2022 14:59:59 +0200 Subject: [PATCH 1/2] fix --- .../core/src/DreifussWysiwygEditorDemo.tsx | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/core/src/DreifussWysiwygEditorDemo.tsx b/packages/core/src/DreifussWysiwygEditorDemo.tsx index 0da852a..8ef7843 100644 --- a/packages/core/src/DreifussWysiwygEditorDemo.tsx +++ b/packages/core/src/DreifussWysiwygEditorDemo.tsx @@ -4,27 +4,26 @@ import {DreifussWysiwygEditor} from './index' import {CustomImageToolbarProps} from '@dreifuss-wysiwyg-editor/image-ui' const value: any = [ - {type: 'action_item', checked: true, children: [{text: 'Slide to the right.'}], id: 10054}, - {type: 'action_item', children: [{text: 'Criss-cross.'}], id: 10055}, + {type: 'action_item', checked: true, children: [{text: 'Slide to the right.'}]}, + {type: 'action_item', children: [{text: 'Criss-cross.'}]}, + {type: 'action_item', children: [{text: 'Third item'}]}, { type: 'paragraph', - children: [{type: 'link', url: 'http://google.com', children: [{text: 'Links: Add links.'}]}], - id: 10052 - }, - { - type: 'paragraph', - children: [{text: 'Bold: Make the selected text bold.', code: true}], - id: 10055 + children: [ + {text: ''}, + {type: 'link', url: 'http://google.com', children: [{text: 'Links: Add linkss.'}]}, + {text: ''} + ] }, - - {type: 'paragraph', children: [{text: 'Italic: Make the selected text italic.', italic: true}]}, + {type: 'paragraph', children: [{text: 'Bold: Make the selected text bold. no?', code: true}]}, + {type: 'paragraph', children: [{text: 'Italic: Make the selected text italicc.', italic: true}]}, { type: 'paragraph', - children: [{text: 'Underline: Underline the selected text.', underline: true}] + children: [{text: 'Underline: Underline the selected textt.', underline: true}] }, { type: 'paragraph', - children: [{text: 'Strikethrough: Strikethrough the selected text.', strikethrough: true}] + children: [{text: 'Strikethrough: Strikethrough the selected textt.', strikethrough: true}] }, { type: 'paragraph', @@ -55,12 +54,12 @@ const value: any = [ type: 'table-cell', borderColor: '#000000', children: [{type: 'paragraph', children: [{text: ''}]}], - backgroundColor: '#f31212' + backgroundColor: 'lightgray' }, { type: 'table-cell', children: [{type: 'paragraph', children: [{text: ''}]}], - backgroundColor: '#f31212' + backgroundColor: 'lightgray' } ] }, @@ -71,12 +70,12 @@ const value: any = [ type: 'table-cell', borderColor: '#000000', children: [{type: 'paragraph', children: [{text: ''}]}], - backgroundColor: '#f31212' + backgroundColor: 'lightgray' }, { type: 'table-cell', children: [{type: 'paragraph', children: [{text: ''}]}], - backgroundColor: '#f31212' + backgroundColor: 'lightgray' } ] } @@ -90,7 +89,10 @@ const value: any = [ {text: 'Make', color: '#c21414'}, {text: ' the selected text bold.'} ] - } + }, + {type: 'paragraph', children: [{text: ''}]}, + {type: 'media_embed', url: 'https://youtu.be/6vcBKwnl_Y0', children: [{text: ''}]}, + {type: 'paragraph', children: [{text: ''}]} ] /** @@ -126,7 +128,7 @@ const toolbars = { const DreifussWysiwygEditorDemo = () => (
-

RichText Component Demo

+

WePublish Rich Text Editor

( dnd: false }} onChange={e => { - // console.log(e) + console.log(JSON.stringify(e)) }} id="two" value={value} From 8ac29c9a37a1b51272935b9d0c6b2001e4682dbd Mon Sep 17 00:00:00 2001 From: bishoy melek Date: Tue, 18 Jan 2022 03:11:28 +0200 Subject: [PATCH 2/2] initial working view --- .../src/components}/Divider.tsx | 0 packages/common/src/components/Icons.tsx | 12 ++ packages/common/src/components/index.ts | 4 + .../src/components/inline-dialog/index.tsx | 40 ++++ packages/core/package.json | 2 + packages/core/src/DreifussWysiwygEditor.tsx | 6 +- .../core/src/DreifussWysiwygEditorDemo.tsx | 33 +++ .../core/src/utils/createPlateComponents.tsx | 5 + .../core/src/utils/createPlateOptions.tsx | 4 + .../core/src/utils/createPlatePlugins.tsx | 4 +- packages/layout-ui/.npmignore | 3 + packages/layout-ui/README.md | 3 + packages/layout-ui/package.json | 65 ++++++ packages/layout-ui/src/Layouts.tsx | 138 +++++++++++++ packages/layout-ui/src/LinkMenu/LinkMenu.tsx | 188 ++++++++++++++++++ packages/layout-ui/src/LinkMenu/index.ts | 1 + .../layout-ui/src/LinkMenu/link-toolbar.css | 59 ++++++ packages/layout-ui/src/index.ts | 3 + packages/layout-ui/src/twin.d.ts | 29 +++ packages/layout-ui/tsconfig.json | 12 ++ packages/layout/.npmignore | 3 + packages/layout/README.md | 3 + packages/layout/package.json | 60 ++++++ packages/layout/src/createLayoutPlugin.ts | 16 ++ packages/layout/src/defaults.ts | 15 ++ packages/layout/src/getLayoutDeserialize.ts | 19 ++ packages/layout/src/index.ts | 7 + packages/layout/src/transforms/index.ts | 6 + .../src/transforms/upsertLinkAtSelection.ts | 59 ++++++ packages/layout/src/transforms/wrapLink.ts | 19 ++ packages/layout/src/types.ts | 17 ++ packages/layout/src/utils.ts | 14 ++ packages/layout/src/withLayout.ts | 154 ++++++++++++++ packages/layout/tsconfig.json | 11 + yarn.lock | 102 +++++++++- 35 files changed, 1103 insertions(+), 13 deletions(-) rename packages/{core/src/atoms => common/src/components}/Divider.tsx (100%) create mode 100644 packages/common/src/components/inline-dialog/index.tsx create mode 100644 packages/layout-ui/.npmignore create mode 100644 packages/layout-ui/README.md create mode 100644 packages/layout-ui/package.json create mode 100644 packages/layout-ui/src/Layouts.tsx create mode 100644 packages/layout-ui/src/LinkMenu/LinkMenu.tsx create mode 100644 packages/layout-ui/src/LinkMenu/index.ts create mode 100644 packages/layout-ui/src/LinkMenu/link-toolbar.css create mode 100644 packages/layout-ui/src/index.ts create mode 100644 packages/layout-ui/src/twin.d.ts create mode 100644 packages/layout-ui/tsconfig.json create mode 100644 packages/layout/.npmignore create mode 100644 packages/layout/README.md create mode 100644 packages/layout/package.json create mode 100644 packages/layout/src/createLayoutPlugin.ts create mode 100644 packages/layout/src/defaults.ts create mode 100644 packages/layout/src/getLayoutDeserialize.ts create mode 100644 packages/layout/src/index.ts create mode 100644 packages/layout/src/transforms/index.ts create mode 100644 packages/layout/src/transforms/upsertLinkAtSelection.ts create mode 100644 packages/layout/src/transforms/wrapLink.ts create mode 100644 packages/layout/src/types.ts create mode 100644 packages/layout/src/utils.ts create mode 100644 packages/layout/src/withLayout.ts create mode 100644 packages/layout/tsconfig.json diff --git a/packages/core/src/atoms/Divider.tsx b/packages/common/src/components/Divider.tsx similarity index 100% rename from packages/core/src/atoms/Divider.tsx rename to packages/common/src/components/Divider.tsx diff --git a/packages/common/src/components/Icons.tsx b/packages/common/src/components/Icons.tsx index 7c7bb44..7e378f1 100644 --- a/packages/common/src/components/Icons.tsx +++ b/packages/common/src/components/Icons.tsx @@ -119,6 +119,18 @@ export const FontColorIcon = () => ( ) +export const TrashIcon = () => ( + + + + +) + export const H1Icon = () => ( + +const InlineDialog = forwardRef(({isRelative, ...props}: any, ref) => { + const relativeStyles = isRelative + ? { + left: '50%', + margin: 10, + transform: 'translateX(-50%)' + } + : {} + + return ( +
+ ) +}) + +export default InlineDialog + +export {InlineDialog} diff --git a/packages/core/package.json b/packages/core/package.json index 180535a..d7e2ff1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -85,6 +85,8 @@ }, "dependencies": { "@babel/runtime": "^7.13.10", + "@dreifuss-wysiwyg-editor/layout": "0.0.0", + "@dreifuss-wysiwyg-editor/layout-ui": "0.0.0", "@dreifuss-wysiwyg-editor/common": "^1.0.2-alpha.1", "@dreifuss-wysiwyg-editor/link": "^1.0.2-alpha.1", "@dreifuss-wysiwyg-editor/link-ui": "^1.0.2-alpha.1", diff --git a/packages/core/src/DreifussWysiwygEditor.tsx b/packages/core/src/DreifussWysiwygEditor.tsx index 4887357..d181f4e 100644 --- a/packages/core/src/DreifussWysiwygEditor.tsx +++ b/packages/core/src/DreifussWysiwygEditor.tsx @@ -1,5 +1,4 @@ import React, {ReactNode} from 'react' -import Divider, {DividerType} from './atoms/Divider' import {HeadingToolbar} from '@udecode/plate-toolbar' import {ELEMENT_MEDIA_EMBED} from '@udecode/plate-media-embed' import {createPlateOptions} from './utils/createPlateOptions' @@ -11,7 +10,9 @@ import { MediaEmbedIcon, EmojiPicker, EmojiIcon, - EditorValue + EditorValue, + Divider, + DividerType } from '@dreifuss-wysiwyg-editor/common' import {createPlateComponents} from './utils/createPlateComponents' import {CharCountToolbar} from '@dreifuss-wysiwyg-editor/character-count-ui' @@ -56,6 +57,7 @@ export interface Toolbars { } export interface EnablePluginsProps { + layout?: boolean dnd?: boolean search?: boolean list?: boolean diff --git a/packages/core/src/DreifussWysiwygEditorDemo.tsx b/packages/core/src/DreifussWysiwygEditorDemo.tsx index 8ef7843..c6c64ae 100644 --- a/packages/core/src/DreifussWysiwygEditorDemo.tsx +++ b/packages/core/src/DreifussWysiwygEditorDemo.tsx @@ -4,6 +4,38 @@ import {DreifussWysiwygEditor} from './index' import {CustomImageToolbarProps} from '@dreifuss-wysiwyg-editor/image-ui' const value: any = [ + { + type: 'layout', + layout: [1, 1], + children: [ + { + type: 'layout-area', + children: [ + { + type: 'paragraph', + children: [ + { + text: 'hello' + } + ] + } + ] + }, + { + type: 'layout-area', + children: [ + { + type: 'paragraph', + children: [ + { + text: 'hii' + } + ] + } + ] + } + ] + }, {type: 'action_item', checked: true, children: [{text: 'Slide to the right.'}]}, {type: 'action_item', children: [{text: 'Criss-cross.'}]}, {type: 'action_item', children: [{text: 'Third item'}]}, @@ -131,6 +163,7 @@ const DreifussWysiwygEditorDemo = () => (

WePublish Rich Text Editor

`${r}fr`).join(' '), + gap: 2, + width: size, + height: size + }}> + {ratios.map((_, i) => { + return
+ })} +
+ ) +} + +const LayoutOptionsContext = createContext<[number, ...number[]][]>([]) + +export const LayoutOptionsProvider = LayoutOptionsContext.Provider + +export const LayoutContainer = ({ + attributes, + children, + element +}: RenderElementProps & {element: {type: 'layout'; layout: any}}) => { + const {spacing} = { + spacing: {medium: 10, small: 10} + } + const focused = useFocused() + const selected = useSelected() + const editor = useStoreEditorRef(useEventEditorId('focus')) + + const layout = element.layout + + // TODO: use this + // const layoutOptions = useContext(LayoutOptionsContext) + + const layoutOptions = [ + [1, 1], + [1, 1, 1], + [2, 1], + [1, 2], + [1, 2, 1], + [1, 1, 1, 1], + [1, 2, 2, 1] + ] + + return ( +
+
`${x}fr`).join(' ') + }}> + {children} +
+ {focused && selected && ( + +
+ {layoutOptions.map((layoutOption, i) => ( + <> + { + event.preventDefault() + const path = ReactEditor.findPath(editor, element) + Transforms.setNodes( + editor, + { + type: 'layout', + layout: layoutOption + }, + {at: path} + ) + }} + icon={makeLayoutIcon(layoutOption)} + active={true} + /> + + ))} + + } + onMouseDown={event => { + event.preventDefault() + + const path = ReactEditor.findPath(editor, element) + Transforms.removeNodes(editor, {at: path}) + }} + active={true} + /> +
+
+ )} +
+ ) +} + +export const LayoutArea = ({attributes, children}: RenderElementProps) => { + const {colors, radii, spacing} = { + spacing: {medium: 10}, + colors: {border: 'black'}, + radii: { + small: 10, + medium: 10 + } + } + + return ( +
+ {children} +
+ ) +} diff --git a/packages/layout-ui/src/LinkMenu/LinkMenu.tsx b/packages/layout-ui/src/LinkMenu/LinkMenu.tsx new file mode 100644 index 0000000..f35093f --- /dev/null +++ b/packages/layout-ui/src/LinkMenu/LinkMenu.tsx @@ -0,0 +1,188 @@ +import React, {useContext, useEffect, useRef, useState} from 'react' +import {Editor, Element, BaseEditor, BaseSelection, Transforms, Location} from 'slate' +import { + getPlatePluginType, + useEventEditorId, + useStoreEditorRef, + useStoreEditorSelection +} from '@udecode/plate-core' +import { + upsertLinkAtSelection, + validateUrl, + removeLink, + ELEMENT_LINK +} from '@dreifuss-wysiwyg-editor/link' +import {ModalContext} from '@dreifuss-wysiwyg-editor/common' +import {ReactEditor} from 'slate-react' +import {HistoryEditor} from 'slate-history' + +import './link-toolbar.css' + +type CustomElement = {type: 'link'; title: string; url?: string; children: CustomText[]} +type CustomText = {title: string; url?: string; text: string} + +declare module 'slate' { + interface CustomTypes { + Editor: BaseEditor & ReactEditor & HistoryEditor + Element: CustomElement + Text: CustomText + } +} + +export const ToolbarLink = () => { + const editor = useStoreEditorRef(useEventEditorId('focus')) + const selection = useStoreEditorSelection(useEventEditorId('focus')) + + const latestSelection = useRef() + + const [title, setTitle] = useState('') + const [url, setURL] = useState('') + + enum prefixType { + http = 'http://', + https = 'https://', + mailto = 'mailto://', + other = 'other' + } + + const [prefix, setPrefix] = useState(prefixType.http) + + const [isValidURL, setIsValidURL] = useState(false) + + const [isInsertBtnDisabled, setIsInsertBtnDisabled] = useState(false) + + const {toggleMenu} = useContext(ModalContext) + + useEffect(() => { + if (url) { + validateUrl(url).then((value: boolean) => setIsValidURL(value)) + + if (url.startsWith(prefixType.https)) { + setPrefix(prefixType.https) + setURL(url.replace(prefixType.https, '')) + } else if (url.startsWith(prefixType.http)) { + setPrefix(prefixType.http) + setURL(url.replace(prefixType.http, '')) + } else if (url.startsWith(prefixType.mailto)) { + setPrefix(prefixType.mailto) + setURL(url.replace(prefixType.mailto, '')) + } + } + }, [url]) + + useEffect(() => { + if (!url || !title || !isValidURL) { + setIsInsertBtnDisabled(true) + } else { + setIsInsertBtnDisabled(false) + } + }, [title, url, isValidURL]) + + useEffect(() => { + if (!editor) return + + const nodes = Array.from( + Editor.nodes(editor, { + at: editor.selection ?? undefined, + match: node => + Element.isElement(node) && node.type === getPlatePluginType(editor, ELEMENT_LINK) + }) + ) + + const tuple = nodes[0] + if (tuple) { + const [node] = tuple + if (Element.isElement(node)) { + setTitle((node.title || (node?.children[0]?.text as string)) ?? '') + + const nodeUrl = node.url as string + + if (!nodeUrl) return + + if (nodeUrl.startsWith(prefixType.https)) { + setPrefix(prefixType.https) + } else if (nodeUrl.startsWith(prefixType.http)) { + setPrefix(prefixType.http) + } else if (nodeUrl.startsWith(prefixType.mailto)) { + setPrefix(prefixType.mailto) + } else { + setPrefix(prefixType.other) + } + setURL(nodeUrl as string) + } + } else if (editor.selection) { + const text = Editor.string(editor, editor.selection) + setTitle(text ?? '') + } + }, [selection]) + + useEffect(() => { + if (selection) { + latestSelection.current = selection + } + }, [selection]) + + return ( +
+
+ +
+ + setURL(e.target.value)} /> +
+

{url && !isValidURL ? 'Invalid Link' : undefined}

+
+
+ +
+ setTitle(e.target.value)} /> +
+
+
+ + +
+
+ ) +} diff --git a/packages/layout-ui/src/LinkMenu/index.ts b/packages/layout-ui/src/LinkMenu/index.ts new file mode 100644 index 0000000..f7d0239 --- /dev/null +++ b/packages/layout-ui/src/LinkMenu/index.ts @@ -0,0 +1 @@ +export * from './LinkMenu' diff --git a/packages/layout-ui/src/LinkMenu/link-toolbar.css b/packages/layout-ui/src/LinkMenu/link-toolbar.css new file mode 100644 index 0000000..0f62b76 --- /dev/null +++ b/packages/layout-ui/src/LinkMenu/link-toolbar.css @@ -0,0 +1,59 @@ +.link-toolbar { + margin-top: 5px; +} + +.link-toolbar .input-group { + border: 1px solid #e5e5ea; + margin-top: 5px; + display: flex; +} + +.link-toolbar .input-group input { + border: 0px; + padding: 5px 5px; + flex: 1 1 auto; +} + +.invalid-value-tooltip { + color: red; +} + +.link-toolbar .form-group:not(:last-child) { + margin-bottom: 20px; +} + +.link-toolbar .toolbar button { + margin-bottom: 0; + margin: auto 5px; + font-weight: normal; + text-align: center; + vertical-align: middle; + cursor: pointer; + outline: 0 !important; + white-space: nowrap; + border: none; + user-select: none; + padding: 8px 12px; + font-size: 14px; + line-height: 1.42857143; + border-radius: 6px; +} + +.link-toolbar .toolbar button:first-child { + margin-left: 0px; +} + +.link-toolbar .toolbar button.insert { + color: white; + background-color: #0078d4; +} + +.link-toolbar .toolbar button.cancel { + color: red; + background-color: #f7f7fa; +} + +.link-toolbar .toolbar button.disabled { + color: #c5c6c7; + background-color: #f7f7fa; +} diff --git a/packages/layout-ui/src/index.ts b/packages/layout-ui/src/index.ts new file mode 100644 index 0000000..f4ff8ee --- /dev/null +++ b/packages/layout-ui/src/index.ts @@ -0,0 +1,3 @@ +export * from './Layouts' + +export * from './LinkMenu' diff --git a/packages/layout-ui/src/twin.d.ts b/packages/layout-ui/src/twin.d.ts new file mode 100644 index 0000000..e6d013a --- /dev/null +++ b/packages/layout-ui/src/twin.d.ts @@ -0,0 +1,29 @@ +// twin.d.ts +import 'twin.macro' +import styledImport, {CSSProp, css as cssImport} from 'styled-components' + +declare module 'twin.macro' { + // The styled and css imports + const styled: typeof styledImport + const css: typeof cssImport +} + +declare module 'react' { + // The css prop + interface HTMLAttributes extends DOMAttributes { + css?: CSSProp + } + // The inline svg css prop + interface SVGProps extends SVGProps { + css?: CSSProp + } +} + +// The 'as' prop on styled components +declare global { + namespace JSX { + interface IntrinsicAttributes extends DOMAttributes { + as?: string | Element + } + } +} diff --git a/packages/layout-ui/tsconfig.json b/packages/layout-ui/tsconfig.json new file mode 100644 index 0000000..3e5a034 --- /dev/null +++ b/packages/layout-ui/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "target": "ES2019", + "lib": ["dom", "ES2019"], + "jsx": "react-jsx" + }, + "exclude": ["node_modules", "dist"], + "files": ["src/twin.d.ts"] +} diff --git a/packages/layout/.npmignore b/packages/layout/.npmignore new file mode 100644 index 0000000..7d3b305 --- /dev/null +++ b/packages/layout/.npmignore @@ -0,0 +1,3 @@ +__tests__ +__test-utils__ +__mocks__ diff --git a/packages/layout/README.md b/packages/layout/README.md new file mode 100644 index 0000000..1f16bbd --- /dev/null +++ b/packages/layout/README.md @@ -0,0 +1,3 @@ +# @dreifuss-wysiwyg-editor/link + +This plugin is based on @udecode/plate-link diff --git a/packages/layout/package.json b/packages/layout/package.json new file mode 100644 index 0000000..f5f7f03 --- /dev/null +++ b/packages/layout/package.json @@ -0,0 +1,60 @@ +{ + "name": "@dreifuss-wysiwyg-editor/layout", + "version": "0.0.0", + "description": "Dreifuss wysiwyg Editor layout plugin", + "keywords": [], + "homepage": "https://github.com/wepublish/dreifuss-wysiwyg-editor/tree/main/packages/layout", + "bugs": { + "url": "https://github.com/wepublish/dreifuss-wysiwyg-editor/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/wepublish/dreifuss-wysiwyg-editor.git", + "directory": "packages/layout" + }, + "license": "MIT", + "main": "dist/index.js", + "module": "dist/index.es.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "clean": "rimraf dist tsconfig.tsbuildinfo", + "build": "tsc && yarn rollup", + "watch": "yarn rollup -w", + "rollup": "rollup -c=../../rollup.config.js", + "test": "jest" + }, + "peerDependencies": { + "react": "^17.0.2", + "react-dom": "^17.0.2", + "slate": "^0.60.0", + "slate-react": "^0.60.0" + }, + "dependencies": { + "@udecode/plate-core": "^1.0.0", + "@dreifuss-wysiwyg-editor/common": "1.0.2-alpha.1", + "@udecode/plate-common": "^2.0.0", + "@udecode/plate-normalizers": "^2.0.0" + }, + "devDependencies": { + "@types/react": "^17.0.14", + "@types/react-dom": "^17.0.9", + "@types/slate": "^0.47.8", + "@types/slate-react": "^0.50.1", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "slate": "^0.62.0", + "slate-react": "^0.62.0" + }, + "publishConfig": { + "access": "public" + }, + "eslintConfig": { + "extends": [ + "../../package.json" + ] + }, + "gitHead": "86b71bff4c9b87a901ef6ed530aa1b5882697797" +} diff --git a/packages/layout/src/createLayoutPlugin.ts b/packages/layout/src/createLayoutPlugin.ts new file mode 100644 index 0000000..554ed07 --- /dev/null +++ b/packages/layout/src/createLayoutPlugin.ts @@ -0,0 +1,16 @@ +import {getRenderElement, PlatePlugin} from '@udecode/plate-core' +import {KEYS_LAYOUT} from './defaults' +import {getLinkDeserialize} from './getLayoutDeserialize' +import {WithLinkOptions} from './types' +import {withLink} from './withLayout' + +/** + * Enables support for hyperlinks. + */ +export const createLayoutPlugin = (options?: WithLinkOptions): PlatePlugin => ({ + pluginKeys: KEYS_LAYOUT, + renderElement: getRenderElement(KEYS_LAYOUT), + deserialize: getLinkDeserialize(), + // inlineTypes: getPlatePluginTypes(ELEMENT_LAYOUT), + withOverrides: withLink() +}) diff --git a/packages/layout/src/defaults.ts b/packages/layout/src/defaults.ts new file mode 100644 index 0000000..2ea60b9 --- /dev/null +++ b/packages/layout/src/defaults.ts @@ -0,0 +1,15 @@ +import {PlatePluginOptions} from '@udecode/plate-core' + +export const ELEMENT_LAYOUT = 'layout' + +export const ELEMENT_LAYOUT_AREA = 'layout-area' + +export const KEYS_LAYOUT = [ELEMENT_LAYOUT, ELEMENT_LAYOUT_AREA] + +export const DEFAULTS_LAYOUT: Partial = { + getNodeProps: ({element}) => ({ + type: element?.type, + layout: element?.layout, + children: element?.children + }) +} diff --git a/packages/layout/src/getLayoutDeserialize.ts b/packages/layout/src/getLayoutDeserialize.ts new file mode 100644 index 0000000..1c38b51 --- /dev/null +++ b/packages/layout/src/getLayoutDeserialize.ts @@ -0,0 +1,19 @@ +import {getNodeDeserializer} from '@udecode/plate-common' +import {Deserialize, getPlatePluginOptions} from '@udecode/plate-core' +// import {ELEMENT_LINK} from './defaults' + +export const getLinkDeserialize = (): Deserialize => editor => { + // const options = getPlatePluginOptions(editor, ELEMENT_LINK) + + return { + // element: getNodeDeserializer({ + // type: options.type, + // getNode: el => ({ + // type: options.type, + // url: el.getAttribute('href') + // }), + // rules: [{nodeNames: 'A'}], + // ...options.deserialize + // }) + } +} diff --git a/packages/layout/src/index.ts b/packages/layout/src/index.ts new file mode 100644 index 0000000..5ea355a --- /dev/null +++ b/packages/layout/src/index.ts @@ -0,0 +1,7 @@ +export * from './createLayoutPlugin' +export * from './defaults' +export * from './getLayoutDeserialize' +export * from './types' +export * from './withLayout' +export * from './transforms/index' +export * from './utils' diff --git a/packages/layout/src/transforms/index.ts b/packages/layout/src/transforms/index.ts new file mode 100644 index 0000000..1c3ee3a --- /dev/null +++ b/packages/layout/src/transforms/index.ts @@ -0,0 +1,6 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from './upsertLinkAtSelection' +export * from './wrapLink' diff --git a/packages/layout/src/transforms/upsertLinkAtSelection.ts b/packages/layout/src/transforms/upsertLinkAtSelection.ts new file mode 100644 index 0000000..10d77b2 --- /dev/null +++ b/packages/layout/src/transforms/upsertLinkAtSelection.ts @@ -0,0 +1,59 @@ +import {insertNodes, isCollapsed, unwrapNodes} from '@udecode/plate-common' +import {getPlatePluginType, SPEditor, TElement} from '@udecode/plate-core' +import {Editor, Transforms} from 'slate' +// import {ELEMENT_LINK} from '../defaults' +import {wrapLink} from './wrapLink' + +/** + * Unwrap link at a location (default: selection). + * Then, the focus of the location is set to selection focus. + * Then, wrap the link at the location. + */ +export const upsertLinkAtSelection = ( + editor: SPEditor, + { + url, + wrap + }: { + url: string + /** + * If true, wrap the link at the location (default: selection) even if the selection is collapsed. + */ + wrap?: boolean + selection?: any + } +) => { + if (!editor.selection) return + + // const type = getPlatePluginType(editor, ELEMENT_LINK) + + // if (!wrap && isCollapsed(editor.selection)) { + // return insertNodes(editor, { + // type, + // url, + // children: [{text: url}] + // }) + // } + + // // if our cursor is inside an existing link, but don't have the text selected, select it now + // if (wrap && isCollapsed(editor.selection)) { + // const linkLeaf = Editor.leaf(editor, editor.selection) + // const [, inlinePath] = linkLeaf + // Transforms.select(editor, inlinePath) + // } + + // unwrapNodes(editor, {at: editor.selection, match: {type}}) + + // wrapLink(editor, {at: editor.selection, url}) + + // Transforms.collapse(editor, {edge: 'end'}) +} + +export function removeLink(editor: SPEditor) { + // if (!editor.selection) return null + // const type = getPlatePluginType(editor, ELEMENT_LINK) + // unwrapNodes(editor, { + // at: editor.selection, + // match: {type} + // }) +} diff --git a/packages/layout/src/transforms/wrapLink.ts b/packages/layout/src/transforms/wrapLink.ts new file mode 100644 index 0000000..92bd84c --- /dev/null +++ b/packages/layout/src/transforms/wrapLink.ts @@ -0,0 +1,19 @@ +import {wrapNodes} from '@udecode/plate-common' +import {getPlatePluginType, SPEditor} from '@udecode/plate-core' +import {Location} from 'slate' +// import {ELEMENT_LINK} from '../defaults' + +/** + * Wrap selected nodes with a link and collapse at the end. + */ +export const wrapLink = (editor: SPEditor, {at, url}: {url: string; at?: Location}) => { + // wrapNodes( + // editor, + // { + // type: getPlatePluginType(editor, ELEMENT_LINK), + // url, + // children: [] + // }, + // {at, split: true} + // ) +} diff --git a/packages/layout/src/types.ts b/packages/layout/src/types.ts new file mode 100644 index 0000000..10ba77b --- /dev/null +++ b/packages/layout/src/types.ts @@ -0,0 +1,17 @@ +import {RangeBeforeOptions} from '@udecode/plate-common' + +export interface LinkNodeData { + url: string +} + +export interface WithLinkOptions { + /** + * Allow custom config for rangeBeforeOptions. + */ + rangeBeforeOptions?: RangeBeforeOptions + + /** + * Callback to validate an url. + */ + isUrl?: (text: string) => boolean +} diff --git a/packages/layout/src/utils.ts b/packages/layout/src/utils.ts new file mode 100644 index 0000000..8c71598 --- /dev/null +++ b/packages/layout/src/utils.ts @@ -0,0 +1,14 @@ +export async function validateUrl(url: string) { + if (!url) return false + + const pattern = new RegExp( + '^(https?:\\/\\/)?' + // protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path + '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', + 'i' + ) + return pattern.test(url) +} diff --git a/packages/layout/src/withLayout.ts b/packages/layout/src/withLayout.ts new file mode 100644 index 0000000..daefc0d --- /dev/null +++ b/packages/layout/src/withLayout.ts @@ -0,0 +1,154 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import {createContext, useContext, useMemo} from 'react' +import { + getRangeFromBlockStart, + getText, + isUrl as isUrlProtocol, + unwrapNodes +} from '@udecode/plate-common' +import {getPlatePluginType, SPEditor, WithOverride} from '@udecode/plate-core' +import {withRemoveEmptyNodes} from '@udecode/plate-normalizers' +import {wrapLink} from './transforms/wrapLink' +import {ELEMENT_LAYOUT} from './defaults' +import {WithLinkOptions} from './types' +import {Editor, Element, Node, Transforms, Range, Point, NodeEntry, Path} from 'slate' +import {ReactEditor, RenderElementProps, useFocused, useSelected} from 'slate-react' + +export const paragraphElement = () => ({ + type: 'paragraph' as const, + children: [{text: ''}] +}) + +export function moveChildren( + editor: Editor, + parent: NodeEntry | Path, + to: Path, + shouldMoveNode: (node: Node) => boolean = () => true +) { + const parentPath = Path.isPath(parent) ? parent : parent[1] + const parentNode = Path.isPath(parent) ? Node.get(editor, parentPath) : parent[0] + if (!Editor.isBlock(editor, parentNode)) return + + for (let i = parentNode.children.length - 1; i >= 0; i--) { + if (shouldMoveNode(parentNode.children[i])) { + const childPath = [...parentPath, i] + Transforms.moveNodes(editor, {at: childPath, to}) + } + } +} + +/** + * Insert space after a url to wrap a link. + * Lookup from the block start to the cursor to check if there is an url. + * If not found, lookup before the cursor for a space character to check the url. + * + * On insert data: + * Paste a string inside a link element will edit its children text but not its url. + * + */ +export const withLink = (): WithOverride => editor => { + const {normalizeNode, deleteBackward} = editor + + // const type = getPlatePluginType(editor, ELEMENT_LINK) + + // + // + // + + editor.deleteBackward = unit => { + if ( + editor.selection && + Range.isCollapsed(editor.selection) && + // this is just an little optimisation + // we're only doing things if we're at the start of a layout area + // and the start of anything will always be offset 0 + // so we'll bailout if we're not at offset 0 + editor.selection.anchor.offset === 0 + ) { + const [aboveNode, abovePath] = Editor.above(editor, { + // @ts-ignore + match: node => node.type === 'layout-area' + }) || [editor, []] + if ( + // @ts-ignore + aboveNode.type === 'layout-area' && + Point.equals(Editor.start(editor, abovePath), editor.selection.anchor) + ) { + return + } + } + deleteBackward(unit) + } + + editor.normalizeNode = entry => { + const [node, path] = entry + + // @ts-ignore + if (Element.isElement(node) && node.type === 'layout') { + // @ts-ignore + if (node.layout === undefined) { + Transforms.unwrapNodes(editor, {at: path}) + return + } + // @ts-ignore + if (node.children.length < node.layout.length) { + Transforms.insertNodes( + editor, + Array.from({ + // @ts-ignore + length: node.layout.length - node.children.length + }).map(() => ({ + type: 'layout-area', + // @ts-ignore + children: [paragraphElement()] + })), + { + at: [...path, node.children.length] + } + ) + return + } + // @ts-ignore + if (node.children.length > node.layout.length) { + Array.from({ + // @ts-ignore + length: node.children.length - node.layout.length + }) + .map((_, i) => i) + .reverse() + .forEach(i => { + // @ts-ignore + const layoutAreaToRemovePath = [...path, i + node.layout.length] + // @ts-ignore + const child = node.children[i + node.layout.length] as Element + moveChildren( + editor, + layoutAreaToRemovePath, + [ + ...path, + // @ts-ignore + node.layout.length - 1, + // @ts-ignore + (node.children[node.layout.length - 1] as Element).children.length + ], + // @ts-ignore + node => node.type !== 'paragraph' || Node.string(child) !== '' + ) + + Transforms.removeNodes(editor, { + at: layoutAreaToRemovePath + }) + }) + return + } + } + normalizeNode(entry) + } + + // + // + // + // editor = withRemoveEmptyNodes({type})(editor) + + return editor +} diff --git a/packages/layout/tsconfig.json b/packages/layout/tsconfig.json new file mode 100644 index 0000000..8cb03b1 --- /dev/null +++ b/packages/layout/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "target": "ES2019", + "lib": ["dom", "ES2019"], + "jsx": "react-jsx" + }, + "exclude": ["node_modules", "dist"] +} diff --git a/yarn.lock b/yarn.lock index 1ad1f14..2a10d32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1538,6 +1538,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.9.2": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" + integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.12.13", "@babel/template@^7.3.3": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" @@ -2786,6 +2793,26 @@ prop-types "^15.6.1" react-lifecycles-compat "^3.0.4" +"@react-dnd/asap@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.0.tgz#b300eeed83e9801f51bd66b0337c9a6f04548651" + integrity sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ== + +"@react-dnd/invariant@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-2.0.0.tgz#09d2e81cd39e0e767d7da62df9325860f24e517e" + integrity sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw== + +"@react-dnd/shallowequal@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a" + integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg== + +"@react-hook/merged-ref@^1.3.0": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@react-hook/merged-ref/-/merged-ref-1.3.2.tgz#919b387a5f79ed67f2578f2015ab7b7d337787d2" + integrity sha512-cQ9Y8m4zlrw/qotReo33E+3Sy9FVqMZb5JwUlb3wj3IJJ1cNJtxcgfWF6rS2NZQrfBJ2nAnckUdPJjMyIJTNZg== + "@rollup/plugin-babel@^5.3.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879" @@ -4275,6 +4302,19 @@ lodash "^4.17.21" zustand "^3.4.2" +"@udecode/plate-dnd@^3.5.1": + version "3.5.1" + resolved "https://registry.yarnpkg.com/@udecode/plate-dnd/-/plate-dnd-3.5.1.tgz#e8e49b72dc954069580e772baa1312c748fff20a" + integrity sha512-qa7VKEoIzZN+Uz/0ISX8b1Hvi6RG+0h4GCS6vAt/PxKFHH0TSQ+jUBkj3i8BCAuvgm/9UApzogXBA0qg3HRJ9Q== + dependencies: + "@react-hook/merged-ref" "^1.3.0" + "@tippyjs/react" "^4.2.0" + "@udecode/plate-common" "3.4.0" + "@udecode/plate-core" "3.4.0" + "@udecode/plate-styled-components" "3.5.1" + react-dnd "^14.0.2" + react-dnd-html5-backend "^14.0.0" + "@udecode/plate-heading@2.0.0", "@udecode/plate-heading@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@udecode/plate-heading/-/plate-heading-2.0.0.tgz#b459c605d3a330fbca04a713d508d6a6fc1850b6" @@ -4355,6 +4395,14 @@ remark-slate "^1.4.0" unified "^9.2.0" +"@udecode/plate-media-embed@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@udecode/plate-media-embed/-/plate-media-embed-2.0.0.tgz#cf18ff653628c8e499a1ee0fb3f9e42062571450" + integrity sha512-6sQgYAU3qoY+wVrLYKIsswpjPFJv/gUKT9ihucZWLB+6YX509y0YdXp6GccaaYdPNVev86fUOhsJIOmH1cLUqQ== + dependencies: + "@udecode/plate-common" "2.0.0" + "@udecode/plate-core" "1.0.0" + "@udecode/plate-mention-ui@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@udecode/plate-mention-ui/-/plate-mention-ui-3.0.1.tgz#49f4c8aecfc1d73574eed857845754871eb0e506" @@ -4439,6 +4487,15 @@ "@udecode/plate-core" "1.0.0" clsx "^1.1.1" +"@udecode/plate-styled-components@3.5.1", "@udecode/plate-styled-components@^3.1.0": + version "3.5.1" + resolved "https://registry.yarnpkg.com/@udecode/plate-styled-components/-/plate-styled-components-3.5.1.tgz#879340ae8d482731e6ba037e1b54a40687d9516c" + integrity sha512-rsRrirF3B9nt9s8jpk4tEjNbAlpVeF2GHcI53jv0DnOLdEusPUO/QUzORQ9K+DzuUqB8mIIFL/jLH46ZJJic9A== + dependencies: + "@udecode/plate-common" "3.4.0" + "@udecode/plate-core" "3.4.0" + clsx "^1.1.1" + "@udecode/plate-styled-components@4.3.7": version "4.3.7" resolved "https://registry.yarnpkg.com/@udecode/plate-styled-components/-/plate-styled-components-4.3.7.tgz#32a455542e4669ed73fc3f4467bbd116c769c84a" @@ -4457,15 +4514,6 @@ "@udecode/plate-core" "4.3.7" clsx "^1.1.1" -"@udecode/plate-styled-components@^3.1.0": - version "3.5.1" - resolved "https://registry.yarnpkg.com/@udecode/plate-styled-components/-/plate-styled-components-3.5.1.tgz#879340ae8d482731e6ba037e1b54a40687d9516c" - integrity sha512-rsRrirF3B9nt9s8jpk4tEjNbAlpVeF2GHcI53jv0DnOLdEusPUO/QUzORQ9K+DzuUqB8mIIFL/jLH46ZJJic9A== - dependencies: - "@udecode/plate-common" "3.4.0" - "@udecode/plate-core" "3.4.0" - clsx "^1.1.1" - "@udecode/plate-toolbar@3.0.1", "@udecode/plate-toolbar@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@udecode/plate-toolbar/-/plate-toolbar-3.0.1.tgz#77f2be8a0b8989767e87c3f0199fac971149201f" @@ -7591,6 +7639,15 @@ dlv@^1.1.3: resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== +dnd-core@14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-14.0.1.tgz#76d000e41c494983210fb20a48b835f81a203c2e" + integrity sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A== + dependencies: + "@react-dnd/asap" "^4.0.0" + "@react-dnd/invariant" "^2.0.0" + redux "^4.1.1" + dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -9615,7 +9672,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -14169,6 +14226,24 @@ react-dev-utils@^11.0.3: strip-ansi "6.0.0" text-table "0.2.0" +react-dnd-html5-backend@^14.0.0, react-dnd-html5-backend@^14.0.1: + version "14.1.0" + resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz#b35a3a0c16dd3a2bfb5eb7ec62cf0c2cace8b62f" + integrity sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw== + dependencies: + dnd-core "14.0.1" + +react-dnd@^14.0.2, react-dnd@^14.0.3: + version "14.0.5" + resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-14.0.5.tgz#ecf264e220ae62e35634d9b941502f3fca0185ed" + integrity sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A== + dependencies: + "@react-dnd/invariant" "^2.0.0" + "@react-dnd/shallowequal" "^2.0.0" + dnd-core "14.0.1" + fast-deep-equal "^3.1.3" + hoist-non-react-statics "^3.3.2" + react-docgen-typescript@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.1.0.tgz#20db64a7fd62e63a8a9469fb4abd90600878cbb2" @@ -14574,6 +14649,13 @@ reduce-css-calc@^2.1.8: css-unit-converter "^1.1.1" postcss-value-parser "^3.3.0" +redux@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104" + integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw== + dependencies: + "@babel/runtime" "^7.9.2" + refractor@^3.1.0: version "3.4.0" resolved "https://registry.npmjs.org/refractor/-/refractor-3.4.0.tgz#62bd274b06c942041f390c371b676eb67cb0a678"