import React, { ReactNode } from 'react'; import ReactDOM from 'react-dom'; import { RenderElementProps, RenderLeafProps, useSlate } from 'slate-react'; import { CustomEditor, CustomElement, CustomElementType, CustomElementWithAlign, } from '../custom-types'; import { Editor, Node, Transforms, Element as SlateElement } from 'slate'; import { ELEMENT_TAGS, TEXT_TAGS } from '../ELEMENT_TAGS'; // --- Icon Types --- export type SlateIconType = | 'format_bold' | 'format_italic' | 'format_underlined' | 'code' | 'looks_one' | 'looks_two' | 'format_quote' | 'format_list_numbered' | 'format_list_bulleted' | 'format_align_left' | 'format_align_center' | 'format_align_right' | 'format_align_justify'; // --- SVG Icons --- const FormatBoldIcon = () => ( ); const FormatItalicIcon = () => ( ); const FormatUnderlinedIcon = () => ( ); const CodeIcon = () => ( ); const LooksOneIcon = () => ( ); const LooksTwoIcon = () => ( ); const FormatQuoteIcon = () => ( ); const FormatListNumberedIcon = () => ( ); const FormatListBulletedIcon = () => ( ); const FormatAlignLeftIcon = () => ( ); const FormatAlignCenterIcon = () => ( ); const FormatAlignRightIcon = () => ( ); const FormatAlignJustifyIcon = () => ( ); // --- Icon Mapping --- const iconMap: Record = { format_bold: FormatBoldIcon, format_italic: FormatItalicIcon, format_underlined: FormatUnderlinedIcon, code: CodeIcon, looks_one: LooksOneIcon, looks_two: LooksTwoIcon, format_quote: FormatQuoteIcon, format_list_numbered: FormatListNumberedIcon, format_list_bulleted: FormatListBulletedIcon, format_align_left: FormatAlignLeftIcon, format_align_center: FormatAlignCenterIcon, format_align_right: FormatAlignRightIcon, format_align_justify: FormatAlignJustifyIcon, }; export const deserialize = (node: any): any[] => { if (node.nodeType === 3) { return [{ text: node.textContent }]; } else if (node.nodeType !== 1) { return []; } const el = node as any; const children = Array.from(node.childNodes).map(deserialize).flat(); if (el.nodeName === 'BODY') { return children; } if (ELEMENT_TAGS[el.nodeName]) { const attrs = ELEMENT_TAGS[el.nodeName](el); return [{ ...attrs, children }]; } if (TEXT_TAGS[el.nodeName]) { const attrs = TEXT_TAGS[el.nodeName](); return children.map((child) => ({ ...child, ...attrs })); } return children; }; const LIST_TYPES = ['numbered-list', 'bulleted-list'] as const; const TEXT_ALIGN_TYPES = ['left', 'center', 'right', 'justify'] as const; type AlignType = (typeof TEXT_ALIGN_TYPES)[number]; // --- Interfaces --- // Extend standard HTML attributes to include className, id, etc. automatically interface ButtonProps extends React.ButtonHTMLAttributes { active: boolean; reversed?: boolean; } type ListType = (typeof LIST_TYPES)[number]; type CustomElementFormat = CustomElementType | AlignType | ListType; const isAlignType = (format: CustomElementFormat): format is AlignType => { return TEXT_ALIGN_TYPES.includes(format as AlignType); }; const isListType = (format: CustomElementFormat): format is ListType => { return LIST_TYPES.includes(format as ListType); }; // --- Components --- export const Button = React.forwardRef( ({ className, active, ...props }, ref) => { // Determine color based on active state for dark theme const colorClass = active ? 'text-blue-400 bg-gray-700/50' : 'text-gray-400 hover:text-gray-200 hover:bg-gray-700/30'; return ( ); }; export const withHtml = (editor: CustomEditor) => { const { insertData, isInline, isVoid } = editor; editor.isInline = (element: any) => { return element.type === 'link' ? true : isInline(element); }; editor.isVoid = (element: any) => { return element.type === 'image' ? true : isVoid(element); }; editor.insertData = (data) => { const html = data.getData('text/html'); if (html) { const parsed = new DOMParser().parseFromString(html, 'text/html'); const fragment = deserialize(parsed.body); Transforms.insertFragment(editor, fragment); return; } insertData(data); }; return editor; };