UNPKG

@deep-foundation/deepcase

Version:

[![Gitpod](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/deep-foundation/deepcase) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label&color=purple)](https://discord.gg/deep-

473 lines 22.2 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { Box, Heading, useColorMode } from '@chakra-ui/react'; import { useDeep } from '@deep-foundation/deeplinks/imports/client'; import { useDebounceCallback } from '@react-hook/debounce'; import { motion, useAnimation } from 'framer-motion'; import isHotkey from 'is-hotkey'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { CiPenpot, CiTextAlignCenter, CiTextAlignJustify, CiTextAlignLeft, CiTextAlignRight, } from 'react-icons/ci'; import { FiBold, FiCode, FiItalic, FiUnderline, } from 'react-icons/fi'; import { TbList, TbListNumbers, TbNumber1, TbNumber2, TbQuote, TbBrandVscode, } from 'react-icons/tb'; import { Editor, Element as SlateElement, Transforms, createEditor, Point, Range } from 'slate'; import { Editable, Slate, useFocused, useSlate, withReact } from 'slate-react'; import { htmlToSlate, slateToHtml } from 'slate-serializers'; import { ClientHandlerSlateProxy } from './client-handler-slate-proxy'; import dynamic from 'next/dynamic'; const MonacoEditor = dynamic(() => import('@monaco-editor/react').then(m => m.default), { ssr: false }); const HOTKEYS = { 'mod+b': 'bold', 'mod+i': 'italic', 'mod+u': 'underline', 'mod+`': 'code', }; const SHORTCUTS = { '*': 'list-item', '-': 'list-item', '+': 'list-item', '>': 'block-quote', '#': 'heading-one', '##': 'heading-two', '###': 'heading-three', '####': 'heading-four', '#####': 'heading-five', '######': 'heading-six', '```': 'backtick-code-block', }; ; ; const LIST_TYPES = ['numbered-list', 'bulleted-list']; const TEXT_ALIGN_TYPES = ['left', 'center', 'right', 'justify']; const withShortcuts = editor => { const { deleteBackward, insertText } = editor; editor.insertText = text => { const { selection } = editor; if (text.endsWith(' ') && selection && Range.isCollapsed(selection)) { const { anchor } = selection; const block = Editor.above(editor, { match: n => SlateElement.isElement(n) && Editor.isBlock(editor, n), }); const path = block ? block[1] : []; const start = Editor.start(editor, path); const range = { anchor, focus: start }; const beforeText = Editor.string(editor, range) + text.slice(0, -1); const type = SHORTCUTS[beforeText]; if (type) { Transforms.select(editor, range); if (!Range.isCollapsed(range)) { Transforms.delete(editor); } const newProperties = { type, }; Transforms.setNodes(editor, newProperties, { match: n => SlateElement.isElement(n) && Editor.isBlock(editor, n), }); if (type === 'list-item') { const list = { type: 'bulleted-list', children: [], }; Transforms.wrapNodes(editor, list, { match: n => !Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'list-item', }); } else if (type === 'backtick-code-block') { return; } } insertText(text); } editor.deleteBackward = (...args) => { const { selection } = editor; if (selection && Range.isCollapsed(selection)) { const match = Editor.above(editor, { match: n => SlateElement.isElement(n) && Editor.isBlock(editor, n), }); if (match) { const [block, path] = match; const start = Editor.start(editor, path); if (!Editor.isEditor(block) && SlateElement.isElement(block) && block.type !== 'paragraph' && Point.equals(selection.anchor, start)) { const newProperties = { type: 'paragraph', }; Transforms.setNodes(editor, newProperties); if (block.type === 'list-item') { Transforms.unwrapNodes(editor, { match: n => !Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'bulleted-list', split: true, }); } return; } } deleteBackward(...args); } }; return editor; }; }; const topmenuVariants = { initial: { scale: 1, opacity: 1, }, hide: { scale: 0, opacity: 0, position: 'absolute', top: 0, }, show: { opacity: 1, scale: 1, } }; const boxVariants = { initial: { height: '100%', }, hide: { height: '50%', transition: { duration: 0.3, type: 'spring' } }, show: { height: '100%', transition: { duration: 0.3, type: 'spring' } } }; const Leaf = ({ attributes, children, leaf }) => { const { colorMode } = useColorMode(); const editorRef = useRef(null); const boxRef = useRef(null); const handleEditorDidMount = (editor, monaco) => { editorRef.current = editor; const container = boxRef.current; const updateHeight = () => { container.style.height = `${editor.getContentHeight()}px`; editor.layout(); }; editor.onDidContentSizeChange(updateHeight); }; if (leaf.bold) { children = _jsx("strong", { children: children }); } if (leaf.code) { children = (_jsx("div", { children: _jsx(Box, { minHeight: '5rem', width: '100%', resize: 'vertical', overflow: 'auto', ref: boxRef, children: _jsx(MonacoEditor, { options: { minimap: { enabled: false }, lineNumbers: 'off', wordWrap: 'on', scrollBeyondLastLine: false, wrappingStrategy: 'advanced', }, height: "100%", width: "100%", theme: colorMode === 'light' ? 'light' : "vs-dark", defaultLanguage: "json", defaultValue: leaf.text, onMount: handleEditorDidMount }) }) })); } if (leaf.italic) { children = _jsx("em", { children: children }); } if (leaf.underline) { children = _jsx("u", { children: children }); } return _jsx("span", Object.assign({}, attributes, { children: children })); }; const Element = ({ attributes, children, element, state }) => { const { colorMode } = useColorMode(); const style = { textAlign: element.align }; switch (element.type) { case 'block-quote': return (_jsx("blockquote", Object.assign({ style: { fontSize: '1.4em', width: '100%', margin: '0 0', fontFamily: 'Open Sans', fontStyle: 'italic', color: '#555555', padding: '1.2em 1.2rem 1.2em 1.7rem', borderLeft: `3px solid #${state}`, lineHeight: '1.1', position: 'relative', background: '#f3f3f3', style } }, attributes, { children: children }))); case 'bulleted-list': return (_jsx("ul", Object.assign({ style: style }, attributes, { children: children }))); case 'client-handler': return (_jsx(ClientHandlerSlateProxy, { children: children })); case 'heading-one': return (_jsx(Heading, Object.assign({ as: 'h1', size: 'xl', noOfLines: 1, sx: style }, attributes, { children: children }))); case 'heading-two': return (_jsx(Heading, Object.assign({ as: 'h2', size: 'lg', noOfLines: 1, sx: style }, attributes, { children: children }))); case 'list-item': return (_jsx("li", Object.assign({ style: style }, attributes, { children: children }))); case 'numbered-list': return (_jsx("ol", Object.assign({ style: style }, attributes, { children: children }))); default: return (_jsx("p", Object.assign({ style: style }, attributes, { children: children }))); } }; const FocusCatcher = ({ onFocusChanged }) => { const focused = useFocused(); useEffect(() => { onFocusChanged && onFocusChanged(focused); }, [focused]); return null; }; var level = (el) => { if (el && check(el)) mutate(el); else { if (typeof (el) === 'object') { if (Array.isArray(el)) { for (let i = 0; i < el.length; i++) { level(el[i]); } } else { level(el === null || el === void 0 ? void 0 : el.children); } } } }; var mutate = (el) => { el.type = 'client-handler'; }; var check = (el) => { var _a, _b, _c; return (!el.type && ((_c = (_b = (_a = el === null || el === void 0 ? void 0 : el.children) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.text) === null || _c === void 0 ? void 0 : _c.slice(0, 2)) === '##'); }; export const DeepWysiwyg = React.memo(function DeepWysiwyg(_a) { var { fillSize, disabled = false, topmenu = true, borderRadiusEditor = 0.5, borderWidthEditor = 'thin', paddingEditor = 1, handleKeyPress, onChange, value, width = '28.29rem', initialValue = [ { type: 'paragraph', children: [{ text: '' }], }, ], onFocusChanged, autoFocus = false, borderColorEditor, backgroundColorEditor, EditableProps = {}, SlateProps = {} } = _a, props = __rest(_a, ["fillSize", "disabled", "topmenu", "borderRadiusEditor", "borderWidthEditor", "paddingEditor", "handleKeyPress", "onChange", "value", "width", "initialValue", "onFocusChanged", "autoFocus", "borderColorEditor", "backgroundColorEditor", "EditableProps", "SlateProps"]); const _value = useMemo(() => { if (typeof (value) === 'string' && !!value) { const sl = htmlToSlate(value); level(sl); return sl; } else return initialValue; }, [value, initialValue]); const random = Math.floor(Math.random() * 16777215).toString(16); const [color, setColor] = useState(random); const renderLeaf = useCallback(props => _jsx(Leaf, Object.assign({}, props)), []); const renderElement = useCallback(props => _jsx(Element, Object.assign({}, props, { state: color })), []); const [editor] = useState(() => withReact(createEditor())); const { colorMode } = useColorMode(); const control = useAnimation(); const boxControl = useAnimation(); useEffect(() => { if (!topmenu) { control.start('hide'); boxControl.start('hide'); } else { control.start('show'); boxControl.start('show'); } }, [topmenu, control, boxControl]); return (_jsx(Box, Object.assign({ sx: { w: fillSize ? '100%' : width, '& > * > *:nth-of-type(2)': {} } }, props, { children: _jsxs(Slate, Object.assign({ editor: editor, value: _value, onChange: (value) => { const serializedToHtml = slateToHtml(value); onChange && onChange({ value: serializedToHtml, slateValue: value, }); } }, SlateProps, { children: [_jsx(FocusCatcher, { onFocusChanged: onFocusChanged }), !!topmenu && _jsx(_Fragment, { children: _jsxs(Box, { as: motion.div, animate: control, variants: topmenuVariants, initial: 'initial', display: 'flex', overflowX: 'scroll', gap: '0.5rem', sx: { backgroundColor: 'handlersInput', borderLeftWidth: 'thin', borderTopWidth: 'thin', borderRightWidth: 'thin', borderLeftColor: 'borderColor', borderTopColor: 'borderColor', borderRightColor: 'borderColor', borderRadius: '0.5rem', padding: '0.5rem', '& > *:hover': { transform: 'scale(1.15)' } }, children: [_jsx(MarkButton, { colorMode: colorMode, icon: _jsx(FiBold, { style: { padding: '0.2rem' } }), format: 'bold' }), _jsx(MarkButton, { colorMode: colorMode, icon: _jsx(FiItalic, { style: { padding: '0.2rem' } }), format: "italic" }), _jsx(MarkButton, { colorMode: colorMode, icon: _jsx(FiUnderline, { style: { padding: '0.2rem' } }), format: "underline" }), _jsx(MarkButton, { colorMode: colorMode, icon: _jsx(FiCode, { style: { padding: '0.2rem' } }), format: "code" }), _jsx(BlockButton, { colorMode: colorMode, format: "heading-one", icon: _jsx(TbNumber1, { style: { padding: '0.2rem' } }) }), _jsx(BlockButton, { colorMode: colorMode, format: "heading-two", icon: _jsx(TbNumber2, { style: { padding: '0.2rem' } }) }), _jsx(BlockButton, { colorMode: colorMode, format: "block-quote", icon: _jsx(TbQuote, { style: { padding: '0.2rem' } }), setColor: () => setColor(random) }), _jsx(BlockButton, { colorMode: colorMode, format: "numbered-list", icon: _jsx(TbListNumbers, { style: { padding: '0.2rem' } }) }), _jsx(BlockButton, { colorMode: colorMode, format: "bulleted-list", icon: _jsx(TbList, { style: { padding: '0.2rem' } }) }), _jsx(BlockButton, { colorMode: colorMode, format: "left", icon: _jsx(CiTextAlignLeft, { style: { padding: '0.2rem' } }) }), _jsx(BlockButton, { colorMode: colorMode, format: "center", icon: _jsx(CiTextAlignCenter, { style: { padding: '0.2rem' } }) }), _jsx(BlockButton, { colorMode: colorMode, format: "right", icon: _jsx(CiTextAlignRight, { style: { padding: '0.2rem' } }) }), _jsx(BlockButton, { colorMode: colorMode, format: "justify", icon: _jsx(CiTextAlignJustify, { style: { padding: '0.2rem' } }) }), _jsx(BlockButton, { colorMode: colorMode, format: "client-handler", icon: _jsx(CiPenpot, { style: { padding: '0.2rem' } }) }), _jsx(BlockButton, { colorMode: colorMode, format: "code-editor", icon: _jsx(TbBrandVscode, { style: { padding: '0.2rem' } }) })] }) }), _jsx(Editable, Object.assign({ style: { borderWidth: borderWidthEditor, borderColor: borderColorEditor, borderRadius: `${borderRadiusEditor}rem`, padding: `${paddingEditor}rem`, backgroundColor: backgroundColorEditor, }, spellCheck: true, readOnly: !!disabled, autoFocus: autoFocus, renderElement: renderElement, renderLeaf: renderLeaf, onKeyDown: event => { for (const hotkey in HOTKEYS) { if (isHotkey(hotkey, event)) { event.preventDefault(); const mark = HOTKEYS[hotkey]; toggleMark(editor, mark); } } handleKeyPress && handleKeyPress(event); } }, EditableProps))] })) }))); }); const toggleMark = (editor, format) => { const isActive = isMarkActive(editor, format); if (isActive) { Editor.removeMark(editor, format); } else { Editor.addMark(editor, format, true); } }; const isMarkActive = (editor, format) => { const marks = Editor.marks(editor); return marks ? marks[format] === true : false; }; const toggleBlock = (editor, format) => { const isActive = isBlockActive(editor, format, TEXT_ALIGN_TYPES.includes(format) ? 'align' : 'type'); const isList = LIST_TYPES.includes(format); Transforms.unwrapNodes(editor, { match: n => !Editor.isEditor(n) && SlateElement.isElement(n) && LIST_TYPES.includes(n.type) && !TEXT_ALIGN_TYPES.includes(format), split: true, }); let newProperties; if (TEXT_ALIGN_TYPES.includes(format)) { newProperties = { align: isActive ? undefined : format, }; } else { newProperties = { type: isActive ? 'paragraph' : isList ? 'list-item' : format, }; } Transforms.setNodes(editor, newProperties); if (!isActive && isList) { const block = { type: format, children: [] }; Transforms.wrapNodes(editor, block); } }; const isBlockActive = (editor, format, blockType = 'type') => { const { selection } = editor; if (!selection) return false; const [match] = Array.from(Editor.nodes(editor, { at: Editor.unhangRange(editor, selection), match: n => !Editor.isEditor(n) && SlateElement.isElement(n) && n[blockType] === format, })); return !!match; }; export const BlockButton = React.memo(({ format, icon, colorMode, setColor }) => { const editor = useSlate(); return (_jsx(Button, { colorMode: colorMode, active: isBlockActive(editor, format, TEXT_ALIGN_TYPES.includes(format) ? 'align' : 'type'), onMouseDown: event => { event.preventDefault(); toggleBlock(editor, format); if (format === 'block-quote') { setColor(); } console.log('format', format); }, children: _jsx(Icon, { children: icon }) })); }); export const MarkButton = ({ format, icon, colorMode }) => { const editor = useSlate(); return (_jsx(Button, { colorMode: colorMode, active: isMarkActive(editor, format), onMouseDown: event => { event.preventDefault(); toggleMark(editor, format); }, children: _jsx(Icon, { children: icon }) })); }; const Button = React.forwardRef((_a) => { var { colorMode, active, reversed, ref } = _a, props = __rest(_a, ["colorMode", "active", "reversed", "ref"]); return (_jsx("span", Object.assign({}, props, { ref: ref, style: { cursor: 'pointer', border: 'thin solid #c5c5c5', borderRadius: '0.2rem', backgroundColor: (colorMode === 'dark' && active) ? '#A8E0FF' : (colorMode === 'light' && active) ? 'white' : '#E8F1FC', color: reversed ? active ? 'white' : '#aaa' : active ? '#344055' : 'gray' } }))); }); const Icon = React.forwardRef((_a) => { var { className, ref } = _a, props = __rest(_a, ["className", "ref"]); return (_jsx("span", Object.assign({}, props, { ref: ref, style: { fontSize: 24, verticalAlign: 'text-bottom', } }))); }); export function useStringSaver(link) { var _a, _b; const deep = useDeep(); const [value, setValue] = useState(((_a = link === null || link === void 0 ? void 0 : link.value) === null || _a === void 0 ? void 0 : _a.value) || ''); const focusedRef = useRef(false); const save = (value) => __awaiter(this, void 0, void 0, function* () { try { if (!link.value) deep.insert({ link_id: link.id, value, }, { table: 'strings' }); deep.update({ link_id: link.id }, { value }, { table: 'strings' }); } catch (error) { } }); const saveDebounced = useDebounceCallback((value) => __awaiter(this, void 0, void 0, function* () { yield save(value); }), 500); useEffect(() => { if (focusedRef.current) saveDebounced(value); }, [value]); useEffect(() => { var _a, _b; console.log('on link value change', { focused: focusedRef.current, value: (_a = link === null || link === void 0 ? void 0 : link.value) === null || _a === void 0 ? void 0 : _a.value }); if (!focusedRef.current) setValue((_b = link === null || link === void 0 ? void 0 : link.value) === null || _b === void 0 ? void 0 : _b.value); }, [(_b = link === null || link === void 0 ? void 0 : link.value) === null || _b === void 0 ? void 0 : _b.value]); const onFocusChanged = useCallback((isFocused) => { focusedRef.current = isFocused; }, []); return { value, setValue, onFocusChanged, }; } //# sourceMappingURL=deep-wysiwyg.js.map