1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-30 10:29:48 +02:00

Fix example types (#5812)

* fix: types for richtext.tsx

* fix: types for check-lists, code-highlighting and custom placeholder

* fix: types for editable-voids, embeds, forced-layout, hovering-toolbar

* fix: types for remaining examples

* fix: typescript error for files in image element

* fix: types for examples and some minor fixes

* fix: normalize-tokens.ts type

* fix: types for [example].tsx
This commit is contained in:
Ravi Lamkoti
2025-03-10 21:50:10 +05:30
committed by GitHub
parent 7a8ab18c52
commit 4f992cff5c
43 changed files with 935 additions and 592 deletions

View File

@@ -90,7 +90,9 @@ const App = () => {
Transforms.setNodes(
editor,
{ type: match ? 'paragraph' : 'code' },
{ match: n => Element.isElement(n) && Editor.isBlock(editor, n) }
{
match: n => Element.isElement(n) && Editor.isBlock(editor, n),
}
)
break
}
@@ -178,7 +180,9 @@ const App = () => {
Transforms.setNodes(
editor,
{ type: match ? null : 'code' },
{ match: n => Element.isElement(n) && Editor.isBlock(editor, n) }
{
match: n => Element.isElement(n) && Editor.isBlock(editor, n),
}
)
break
}

View File

@@ -58,6 +58,8 @@
"@emotion/css": "^11.11.2",
"@faker-js/faker": "^8.2.0",
"@playwright/test": "^1.39.0",
"@types/is-hotkey": "^0.1.10",
"@types/is-url": "^1.2.32",
"@types/jest": "29.5.6",
"@types/lodash": "^4.14.200",
"@types/mocha": "^10.0.3",

View File

@@ -121,9 +121,9 @@ const Element = props => {
}
}
const CheckListItemElement = ({ attributes, children, element }) => {
const { checked } = element
const editor = useSlateStatic()
const readOnly = useReadOnly()
const { checked } = element
return (
<div
{...attributes}

View File

@@ -1,28 +1,28 @@
import { css } from '@emotion/css'
import isHotkey from 'is-hotkey'
import Prism from 'prismjs'
import 'prismjs/components/prism-java'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-jsx'
import 'prismjs/components/prism-typescript'
import 'prismjs/components/prism-tsx'
import 'prismjs/components/prism-markdown'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-php'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-sql'
import 'prismjs/components/prism-java'
import 'prismjs/components/prism-tsx'
import 'prismjs/components/prism-typescript'
import React, { useCallback, useState } from 'react'
import { createEditor, Node, Editor, Element, Transforms } from 'slate'
import {
withReact,
Slate,
Editable,
useSlate,
ReactEditor,
useSlateStatic,
} from 'slate-react'
import { Editor, Element, Node, Transforms, createEditor } from 'slate'
import { withHistory } from 'slate-history'
import isHotkey from 'is-hotkey'
import { css } from '@emotion/css'
import { normalizeTokens } from './utils/normalize-tokens'
import {
Editable,
ReactEditor,
Slate,
useSlate,
useSlateStatic,
withReact,
} from 'slate-react'
import { Button, Icon, Toolbar } from './components'
import { normalizeTokens } from './utils/normalize-tokens'
const ParagraphType = 'paragraph'
const CodeBlockType = 'code-block'
@@ -139,7 +139,7 @@ const useDecorate = editor => {
return useCallback(
([node, path]) => {
if (Element.isElement(node) && node.type === CodeLineType) {
const ranges = editor.nodeToDecorations.get(node) || []
const ranges = editor.nodeToDecorations?.get(node) || []
return ranges
}
return []

View File

@@ -1,6 +1,6 @@
import { css, cx } from '@emotion/css'
import React from 'react'
import ReactDOM from 'react-dom'
import { cx, css } from '@emotion/css'
export const Button = React.forwardRef(
({ className, active, reversed, ...props }, ref) => (
@@ -23,51 +23,6 @@ export const Button = React.forwardRef(
/>
)
)
export const EditorValue = React.forwardRef(
({ className, value, ...props }, ref) => {
const textLines = value.document.nodes
.map(node => node.text)
.toArray()
.join('\n')
return (
<div
ref={ref}
{...props}
className={cx(
className,
css`
margin: 30px -20px 0;
`
)}
>
<div
className={css`
font-size: 14px;
padding: 5px 20px;
color: #404040;
border-top: 2px solid #eeeeee;
background: #f8f8f8;
`}
>
Slate's value as text
</div>
<div
className={css`
color: #404040;
font: 12px monospace;
white-space: pre-wrap;
padding: 10px 20px;
div {
margin: 0 0 0.5em;
}
`}
>
{textLines}
</div>
</div>
)
}
)
export const Icon = React.forwardRef(({ className, ...props }, ref) => (
<span
{...props}

View File

@@ -1,7 +1,7 @@
import React, { useMemo } from 'react'
import { createEditor } from 'slate'
import { Slate, Editable, withReact } from 'slate-react'
import { withHistory } from 'slate-history'
import { Editable, Slate, withReact } from 'slate-react'
const initialValue = [
{

View File

@@ -1,10 +1,10 @@
import React, { useState, useMemo } from 'react'
import { Transforms, createEditor } from 'slate'
import { Slate, Editable, useSlateStatic, withReact } from 'slate-react'
import { withHistory } from 'slate-history'
import { css } from '@emotion/css'
import RichTextEditor from './richtext'
import React, { useMemo, useState } from 'react'
import { createEditor, Transforms } from 'slate'
import { withHistory } from 'slate-history'
import { Editable, Slate, useSlateStatic, withReact } from 'slate-react'
import { Button, Icon, Toolbar } from './components'
import RichTextEditor from './richtext'
const EditableVoidsExample = () => {
const editor = useMemo(

View File

@@ -1,13 +1,13 @@
import React, { useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import {
Transforms,
createEditor,
Editor,
Node,
Element as SlateElement,
Editor,
Transforms,
createEditor,
} from 'slate'
import { withHistory } from 'slate-history'
import { Editable, Slate, withReact } from 'slate-react'
const withLayout = editor => {
const { normalizeNode } = editor

View File

@@ -1,8 +1,8 @@
import React, { useMemo, useRef, useEffect } from 'react'
import { Slate, Editable, withReact, useSlate, useFocused } from 'slate-react'
import { Editor, createEditor, Range } from 'slate'
import { css } from '@emotion/css'
import React, { useEffect, useMemo, useRef } from 'react'
import { Editor, Range, createEditor } from 'slate'
import { withHistory } from 'slate-history'
import { Editable, Slate, useFocused, useSlate, withReact } from 'slate-react'
import { Button, Icon, Menu, Portal } from './components'
const HoveringMenuExample = () => {
@@ -23,7 +23,7 @@ const HoveringMenuExample = () => {
return toggleMark(editor, 'italic')
case 'formatUnderline':
event.preventDefault()
return toggleMark(editor, 'underlined')
return toggleMark(editor, 'underline')
}
}}
/>
@@ -49,13 +49,13 @@ const Leaf = ({ attributes, children, leaf }) => {
if (leaf.italic) {
children = <em>{children}</em>
}
if (leaf.underlined) {
if (leaf.underline) {
children = <u>{children}</u>
}
return <span {...attributes}>{children}</span>
}
const HoveringToolbar = () => {
const ref = useRef()
const ref = useRef(null)
const editor = useSlate()
const inFocus = useFocused()
useEffect(() => {
@@ -105,7 +105,7 @@ const HoveringToolbar = () => {
>
<FormatButton format="bold" icon="format_bold" />
<FormatButton format="italic" icon="format_italic" />
<FormatButton format="underlined" icon="format_underlined" />
<FormatButton format="underline" icon="format_underlined" />
</Menu>
</Portal>
)

View File

@@ -1,21 +1,23 @@
import React, { useMemo, useCallback } from 'react'
import { faker } from '@faker-js/faker'
import React, { useCallback, useMemo } from 'react'
import { createEditor } from 'slate'
import { Slate, Editable, withReact } from 'slate-react'
import { Editable, Slate, withReact } from 'slate-react'
const HEADINGS = 100
const PARAGRAPHS = 7
const initialValue = []
for (let h = 0; h < HEADINGS; h++) {
initialValue.push({
type: 'heading',
const heading = {
type: 'heading-one',
children: [{ text: faker.lorem.sentence() }],
})
}
initialValue.push(heading)
for (let p = 0; p < PARAGRAPHS; p++) {
initialValue.push({
const paragraph = {
type: 'paragraph',
children: [{ text: faker.lorem.paragraph() }],
})
}
initialValue.push(paragraph)
}
}
const HugeDocumentExample = () => {
@@ -29,7 +31,7 @@ const HugeDocumentExample = () => {
}
const Element = ({ attributes, children, element }) => {
switch (element.type) {
case 'heading':
case 'heading-one':
return <h1 {...attributes}>{children}</h1>
default:
return <p {...attributes}>{children}</p>

View File

@@ -1,9 +1,9 @@
import isHotkey from 'is-hotkey'
import React, { useCallback, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import isHotkey from 'is-hotkey'
import { Editable, withReact, useSlate, Slate, ReactEditor } from 'slate-react'
import { Editor, createEditor } from 'slate'
import { withHistory } from 'slate-history'
import { Editable, ReactEditor, Slate, useSlate, withReact } from 'slate-react'
import { Button, Icon, Toolbar } from './components'
const HOTKEYS = {
@@ -93,7 +93,9 @@ const MarkButton = ({ format, icon }) => {
const IFrame = ({ children, ...props }) => {
const [iframeBody, setIframeBody] = useState(null)
const handleLoad = e => {
setIframeBody(e.target.contentDocument.body)
const iframe = e.target
if (!iframe.contentDocument) return
setIframeBody(iframe.contentDocument.body)
}
return (
<iframe srcDoc={`<!DOCTYPE html>`} {...props} onLoad={handleLoad}>

View File

@@ -1,19 +1,19 @@
import React, { useMemo } from 'react'
import imageExtensions from 'image-extensions'
import isUrl from 'is-url'
import isHotkey from 'is-hotkey'
import { Transforms, createEditor } from 'slate'
import {
Slate,
Editable,
useSlateStatic,
useSelected,
useFocused,
withReact,
ReactEditor,
} from 'slate-react'
import { withHistory } from 'slate-history'
import { css } from '@emotion/css'
import imageExtensions from 'image-extensions'
import isHotkey from 'is-hotkey'
import isUrl from 'is-url'
import React, { useMemo } from 'react'
import { Transforms, createEditor } from 'slate'
import { withHistory } from 'slate-history'
import {
Editable,
ReactEditor,
Slate,
useFocused,
useSelected,
useSlateStatic,
withReact,
} from 'slate-react'
import { Button, Icon, Toolbar } from './components'
const ImagesExample = () => {
@@ -48,7 +48,7 @@ const withImages = editor => {
const text = data.getData('text/plain')
const { files } = data
if (files && files.length > 0) {
for (const file of files) {
Array.from(files).forEach(file => {
const reader = new FileReader()
const [mime] = file.type.split('/')
if (mime === 'image') {
@@ -58,7 +58,7 @@ const withImages = editor => {
})
reader.readAsDataURL(file)
}
}
})
} else if (isImageUrl(text)) {
insertImage(editor, text)
} else {
@@ -71,10 +71,11 @@ const insertImage = (editor, url) => {
const text = { text: '' }
const image = { type: 'image', url, children: [text] }
Transforms.insertNodes(editor, image)
Transforms.insertNodes(editor, {
const paragraph = {
type: 'paragraph',
children: [{ text: '' }],
})
}
Transforms.insertNodes(editor, paragraph)
}
const Element = props => {
const { attributes, children, element } = props

View File

@@ -1,17 +1,17 @@
import React, { useMemo } from 'react'
import isUrl from 'is-url'
import { isKeyHotkey } from 'is-hotkey'
import { css } from '@emotion/css'
import { Editable, withReact, useSlate, useSelected } from 'slate-react'
import * as SlateReact from 'slate-react'
import { isKeyHotkey } from 'is-hotkey'
import isUrl from 'is-url'
import React, { useMemo } from 'react'
import {
Transforms,
Editor,
Range,
createEditor,
Editor,
Element as SlateElement,
Range,
Transforms,
} from 'slate'
import { withHistory } from 'slate-history'
import { Editable, useSelected, useSlate, withReact } from 'slate-react'
import * as SlateReact from 'slate-react'
import { Button, Icon, Toolbar } from './components'
const initialValue = [
@@ -326,7 +326,7 @@ const Text = props => {
? css`
padding-left: 0.1px;
`
: null
: undefined
}
{...attributes}
>

View File

@@ -1,10 +1,10 @@
import { css } from '@emotion/css'
import Prism from 'prismjs'
import 'prismjs/components/prism-markdown'
import React, { useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import { Text, createEditor } from 'slate'
import { withHistory } from 'slate-history'
import { css } from '@emotion/css'
import { Editable, Slate, withReact } from 'slate-react'
const MarkdownPreviewExample = () => {
const renderLeaf = useCallback(props => <Leaf {...props} />, [])

View File

@@ -1,27 +1,27 @@
import React, {
useMemo,
useCallback,
useRef,
useEffect,
useState,
Fragment,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { Editor, Transforms, Range, createEditor } from 'slate'
import { withHistory } from 'slate-history'
import {
Slate,
Editable,
ReactEditor,
withReact,
useSelected,
Slate,
useFocused,
useSelected,
withReact,
} from 'slate-react'
import { Portal } from './components'
import { IS_MAC } from './utils/environment'
const MentionExample = () => {
const ref = useRef()
const [target, setTarget] = useState()
const ref = useRef(null)
const [target, setTarget] = useState(null)
const [index, setIndex] = useState(0)
const [search, setSearch] = useState('')
const renderElement = useCallback(props => <Element {...props} />, [])
@@ -64,7 +64,7 @@ const MentionExample = () => {
[chars, editor, index, target]
)
useEffect(() => {
if (target && chars.length > 0) {
if (target && chars.length > 0 && ref.current) {
const el = ref.current
const domRange = ReactEditor.toDOMRange(editor, target)
const rect = domRange.getBoundingClientRect()
@@ -124,7 +124,7 @@ const MentionExample = () => {
{chars.map((char, i) => (
<div
key={char}
onClick={() => {
onClick={e => {
Transforms.select(editor, target)
insertMention(editor, char)
setTarget(null)

View File

@@ -1,19 +1,19 @@
import { css } from '@emotion/css'
import React, { useCallback, useMemo } from 'react'
import { jsx } from 'slate-hyperscript'
import { Transforms, createEditor } from 'slate'
import { withHistory } from 'slate-history'
import { css } from '@emotion/css'
import { jsx } from 'slate-hyperscript'
import {
Slate,
Editable,
withReact,
useSelected,
Slate,
useFocused,
useSelected,
withReact,
} from 'slate-react'
const ELEMENT_TAGS = {
A: el => ({ type: 'link', url: el.getAttribute('href') }),
BLOCKQUOTE: () => ({ type: 'quote' }),
BLOCKQUOTE: () => ({ type: 'block-quote' }),
H1: () => ({ type: 'heading-one' }),
H2: () => ({ type: 'heading-two' }),
H3: () => ({ type: 'heading-three' }),
@@ -24,10 +24,9 @@ const ELEMENT_TAGS = {
LI: () => ({ type: 'list-item' }),
OL: () => ({ type: 'numbered-list' }),
P: () => ({ type: 'paragraph' }),
PRE: () => ({ type: 'code' }),
PRE: () => ({ type: 'code-block' }),
UL: () => ({ type: 'bulleted-list' }),
}
// COMPAT: `B` is omitted here because Google Docs uses `<b>` in weird ways.
const TEXT_TAGS = {
CODE: () => ({ code: true }),
DEL: () => ({ strikethrough: true }),
@@ -66,7 +65,7 @@ export const deserialize = el => {
return jsx('element', attrs, children)
}
if (TEXT_TAGS[nodeName]) {
const attrs = TEXT_TAGS[nodeName](el)
const attrs = TEXT_TAGS[nodeName]()
return children.map(child => jsx('text', attrs, child))
}
return children
@@ -113,9 +112,9 @@ const Element = props => {
switch (element.type) {
default:
return <p {...attributes}>{children}</p>
case 'quote':
case 'block-quote':
return <blockquote {...attributes}>{children}</blockquote>
case 'code':
case 'code-block':
return (
<pre>
<code {...attributes}>{children}</code>
@@ -141,7 +140,7 @@ const Element = props => {
return <ol {...attributes}>{children}</ol>
case 'link':
return (
<SafeLink href={element.url} {...attributes}>
<SafeLink href={element.url} attributes={attributes}>
{children}
</SafeLink>
)
@@ -150,7 +149,7 @@ const Element = props => {
}
}
const allowedSchemes = ['http:', 'https:', 'mailto:', 'tel:']
const SafeLink = ({ attributes, children, href }) => {
const SafeLink = ({ children, href, attributes }) => {
const safeHref = useMemo(() => {
let parsedUrl = null
try {

View File

@@ -1,13 +1,13 @@
import React, { useCallback, useMemo } from 'react'
import isHotkey from 'is-hotkey'
import { Editable, withReact, useSlate, Slate } from 'slate-react'
import React, { useCallback, useMemo } from 'react'
import {
Editor,
Element as SlateElement,
Transforms,
createEditor,
Element as SlateElement,
} from 'slate'
import { withHistory } from 'slate-history'
import { Editable, Slate, useSlate, withReact } from 'slate-react'
import { Button, Icon, Toolbar } from './components'
const HOTKEYS = {
@@ -62,19 +62,19 @@ const toggleBlock = (editor, format) => {
const isActive = isBlockActive(
editor,
format,
TEXT_ALIGN_TYPES.includes(format) ? 'align' : 'type'
isAlignType(format) ? 'align' : 'type'
)
const isList = LIST_TYPES.includes(format)
const isList = isListType(format)
Transforms.unwrapNodes(editor, {
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
LIST_TYPES.includes(n.type) &&
!TEXT_ALIGN_TYPES.includes(format),
isListType(n.type) &&
!isAlignType(format),
split: true,
})
let newProperties
if (TEXT_ALIGN_TYPES.includes(format)) {
if (isAlignType(format)) {
newProperties = {
align: isActive ? undefined : format,
}
@@ -103,10 +103,15 @@ const isBlockActive = (editor, format, blockType = 'type') => {
const [match] = Array.from(
Editor.nodes(editor, {
at: Editor.unhangRange(editor, selection),
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
n[blockType] === format,
match: n => {
if (!Editor.isEditor(n) && SlateElement.isElement(n)) {
if (blockType === 'align' && isAlignElement(n)) {
return n.align === format
}
return n.type === format
}
return false
},
})
)
return !!match
@@ -116,7 +121,10 @@ const isMarkActive = (editor, format) => {
return marks ? marks[format] === true : false
}
const Element = ({ attributes, children, element }) => {
const style = { textAlign: element.align }
const style = {}
if (isAlignElement(element)) {
style.textAlign = element.align
}
switch (element.type) {
case 'block-quote':
return (
@@ -184,7 +192,7 @@ const BlockButton = ({ format, icon }) => {
active={isBlockActive(
editor,
format,
TEXT_ALIGN_TYPES.includes(format) ? 'align' : 'type'
isAlignType(format) ? 'align' : 'type'
)}
onMouseDown={event => {
event.preventDefault()
@@ -209,6 +217,15 @@ const MarkButton = ({ format, icon }) => {
</Button>
)
}
const isAlignType = format => {
return TEXT_ALIGN_TYPES.includes(format)
}
const isListType = format => {
return LIST_TYPES.includes(format)
}
const isAlignElement = element => {
return 'align' in element
}
const initialValue = [
{
type: 'paragraph',

View File

@@ -1,18 +1,19 @@
import React, { useState, useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import { Text, createEditor } from 'slate'
import { css } from '@emotion/css'
import React, { useCallback, useMemo, useState } from 'react'
import { Element, Text, createEditor } from 'slate'
import { withHistory } from 'slate-history'
import { Editable, Slate, withReact } from 'slate-react'
import { Icon, Toolbar } from './components'
const SearchHighlightingExample = () => {
const [search, setSearch] = useState()
const [search, setSearch] = useState('')
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
const decorate = useCallback(
([node, path]) => {
const ranges = []
if (
search &&
Element.isElement(node) &&
Array.isArray(node.children) &&
node.children.every(Text.isText)
) {
@@ -93,13 +94,14 @@ const SearchHighlightingExample = () => {
)
}
const Leaf = ({ attributes, children, leaf }) => {
const highlightLeaf = leaf
return (
<span
{...attributes}
{...(leaf.highlight && { 'data-cy': 'search-highlighted' })}
{...(highlightLeaf.highlight && { 'data-cy': 'search-highlighted' })}
className={css`
font-weight: ${leaf.bold && 'bold'};
background-color: ${leaf.highlight && '#ffeeba'};
font-weight: ${highlightLeaf.bold && 'bold'};
background-color: ${highlightLeaf.highlight && '#ffeeba'};
`}
>
{children}

View File

@@ -1,13 +1,13 @@
import React, { useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import {
Editor,
Range,
Point,
createEditor,
Range,
Element as SlateElement,
createEditor,
} from 'slate'
import { withHistory } from 'slate-history'
import { Editable, Slate, withReact } from 'slate-react'
const TablesExample = () => {
const renderElement = useCallback(props => <Element {...props} />, [])

View File

@@ -1,23 +1,29 @@
import React, { useMemo, useCallback } from 'react'
import { css } from '@emotion/css'
import React, { ChangeEvent, useCallback, useMemo } from 'react'
import {
Descendant,
Editor,
Point,
Range,
Element as SlateElement,
Transforms,
createEditor,
} from 'slate'
import { withHistory } from 'slate-history'
import {
Slate,
Editable,
withReact,
useSlateStatic,
useReadOnly,
ReactEditor,
RenderElementProps,
Slate,
useReadOnly,
useSlateStatic,
withReact,
} from 'slate-react'
import {
Editor,
Transforms,
Range,
Point,
createEditor,
Descendant,
Element as SlateElement,
} from 'slate'
import { css } from '@emotion/css'
import { withHistory } from 'slate-history'
CheckListItemElement as CheckListItemType,
CustomEditor,
RenderElementPropsFor,
} from './custom-types.d'
const initialValue: Descendant[] = [
{
@@ -65,7 +71,10 @@ const initialValue: Descendant[] = [
]
const CheckListsExample = () => {
const renderElement = useCallback(props => <Element {...props} />, [])
const renderElement = useCallback(
(props: RenderElementProps) => <Element {...props} />,
[]
)
const editor = useMemo(
() => withChecklists(withHistory(withReact(createEditor()))),
[]
@@ -83,7 +92,7 @@ const CheckListsExample = () => {
)
}
const withChecklists = editor => {
const withChecklists = (editor: CustomEditor) => {
const { deleteBackward } = editor
editor.deleteBackward = (...args) => {
@@ -122,7 +131,7 @@ const withChecklists = editor => {
return editor
}
const Element = props => {
const Element = (props: RenderElementProps) => {
const { attributes, children, element } = props
switch (element.type) {
@@ -133,10 +142,14 @@ const Element = props => {
}
}
const CheckListItemElement = ({ attributes, children, element }) => {
const CheckListItemElement = ({
attributes,
children,
element,
}: RenderElementPropsFor<CheckListItemType>) => {
const { checked } = element
const editor = useSlateStatic()
const readOnly = useReadOnly()
const { checked } = element
return (
<div
{...attributes}
@@ -159,7 +172,7 @@ const CheckListItemElement = ({ attributes, children, element }) => {
<input
type="checkbox"
checked={checked}
onChange={event => {
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const path = ReactEditor.findPath(editor, element)
const newProperties: Partial<SlateElement> = {
checked: event.target.checked,

View File

@@ -1,39 +1,45 @@
import { css } from '@emotion/css'
import isHotkey from 'is-hotkey'
import Prism from 'prismjs'
import 'prismjs/components/prism-java'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-jsx'
import 'prismjs/components/prism-typescript'
import 'prismjs/components/prism-tsx'
import 'prismjs/components/prism-markdown'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-php'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-sql'
import 'prismjs/components/prism-java'
import React, { useCallback, useState } from 'react'
import 'prismjs/components/prism-tsx'
import 'prismjs/components/prism-typescript'
import React, { ChangeEvent, MouseEvent, useCallback, useState } from 'react'
import {
createEditor,
Node,
Editor,
Range,
Element,
Transforms,
Node,
NodeEntry,
Range,
Transforms,
createEditor,
} from 'slate'
import { withHistory } from 'slate-history'
import {
withReact,
Slate,
Editable,
ReactEditor,
RenderElementProps,
RenderLeafProps,
Slate,
useSlate,
ReactEditor,
useSlateStatic,
withReact,
} from 'slate-react'
import { withHistory } from 'slate-history'
import isHotkey from 'is-hotkey'
import { css } from '@emotion/css'
import { CodeBlockElement } from './custom-types.d'
import { normalizeTokens } from './utils/normalize-tokens'
import { Button, Icon, Toolbar } from './components'
import {
CodeBlockElement,
CodeLineElement,
CustomEditor,
CustomElement,
CustomText,
} from './custom-types.d'
import { normalizeTokens } from './utils/normalize-tokens'
const ParagraphType = 'paragraph'
const CodeBlockType = 'code-block'
@@ -139,7 +145,7 @@ const CodeBlockButton = () => {
<Button
data-test-id="code-block-button"
active
onMouseDown={event => {
onMouseDown={(event: MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
handleClick()
}}
@@ -160,11 +166,11 @@ const renderLeaf = (props: RenderLeafProps) => {
)
}
const useDecorate = (editor: Editor) => {
const useDecorate = (editor: CustomEditor) => {
return useCallback(
([node, path]) => {
([node, path]: NodeEntry) => {
if (Element.isElement(node) && node.type === CodeLineType) {
const ranges = editor.nodeToDecorations.get(node) || []
const ranges = editor.nodeToDecorations?.get(node) || []
return ranges
}
@@ -174,11 +180,20 @@ const useDecorate = (editor: Editor) => {
)
}
interface TokenRange extends Range {
token: boolean
[key: string]: unknown
}
type EditorWithDecorations = CustomEditor & {
nodeToDecorations: Map<Element, TokenRange[]>
}
const getChildNodeToDecorations = ([
block,
blockPath,
]: NodeEntry<CodeBlockElement>) => {
const nodeToDecorations = new Map<Element, Range[]>()
]: NodeEntry<CodeBlockElement>): Map<Element, TokenRange[]> => {
const nodeToDecorations = new Map<Element, TokenRange[]>()
const text = block.children.map(line => Node.string(line)).join('\n')
const language = block.language
@@ -222,10 +237,10 @@ const getChildNodeToDecorations = ([
// precalculate editor.nodeToDecorations map to use it inside decorate function then
const SetNodeToDecorations = () => {
const editor = useSlate()
const editor = useSlate() as EditorWithDecorations
const blockEntries = Array.from(
Editor.nodes(editor, {
Editor.nodes<CodeBlockElement>(editor, {
at: [],
mode: 'highest',
match: n => Element.isElement(n) && n.type === CodeBlockType,
@@ -241,8 +256,8 @@ const SetNodeToDecorations = () => {
return null
}
const useOnKeydown = (editor: Editor) => {
const onKeyDown: React.KeyboardEventHandler = useCallback(
const useOnKeydown = (editor: CustomEditor) => {
const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = useCallback(
e => {
if (isHotkey('tab', e)) {
// handle tab key, insert spaces
@@ -257,7 +272,13 @@ const useOnKeydown = (editor: Editor) => {
return onKeyDown
}
const LanguageSelect = (props: JSX.IntrinsicElements['select']) => {
interface LanguageSelectProps
extends React.SelectHTMLAttributes<HTMLSelectElement> {
value?: string
onChange: (event: ChangeEvent<HTMLSelectElement>) => void
}
const LanguageSelect = (props: LanguageSelectProps) => {
return (
<select
data-test-id="language-select"
@@ -297,13 +318,13 @@ const mergeMaps = <K, V>(...maps: Map<K, V>[]) => {
return map
}
const toChildren = (content: string) => [{ text: content }]
const toCodeLines = (content: string): Element[] =>
const toChildren = (content: string): CustomText[] => [{ text: content }]
const toCodeLines = (content: string): CodeLineElement[] =>
content
.split('\n')
.map(line => ({ type: CodeLineType, children: toChildren(line) }))
const initialValue: Element[] = [
const initialValue: CustomElement[] = [
{
type: ParagraphType,
children: toChildren(

View File

@@ -1,12 +1,11 @@
import React, { ReactNode, Ref, PropsWithChildren } from 'react'
import { css, cx } from '@emotion/css'
import React, { PropsWithChildren, ReactNode, Ref } from 'react'
import ReactDOM from 'react-dom'
import { cx, css } from '@emotion/css'
interface BaseProps {
className: string
[key: string]: unknown
}
type OrNull<T> = T | null
export const Button = React.forwardRef(
(
@@ -21,7 +20,7 @@ export const Button = React.forwardRef(
reversed: boolean
} & BaseProps
>,
ref: Ref<OrNull<HTMLSpanElement>>
ref: Ref<HTMLSpanElement>
) => (
<span
{...props}
@@ -43,67 +42,10 @@ export const Button = React.forwardRef(
)
)
export const EditorValue = React.forwardRef(
(
{
className,
value,
...props
}: PropsWithChildren<
{
value: any
} & BaseProps
>,
ref: Ref<OrNull<null>>
) => {
const textLines = value.document.nodes
.map(node => node.text)
.toArray()
.join('\n')
return (
<div
ref={ref}
{...props}
className={cx(
className,
css`
margin: 30px -20px 0;
`
)}
>
<div
className={css`
font-size: 14px;
padding: 5px 20px;
color: #404040;
border-top: 2px solid #eeeeee;
background: #f8f8f8;
`}
>
Slate's value as text
</div>
<div
className={css`
color: #404040;
font: 12px monospace;
white-space: pre-wrap;
padding: 10px 20px;
div {
margin: 0 0 0.5em;
}
`}
>
{textLines}
</div>
</div>
)
}
)
export const Icon = React.forwardRef(
(
{ className, ...props }: PropsWithChildren<BaseProps>,
ref: Ref<OrNull<HTMLSpanElement>>
ref: Ref<HTMLSpanElement>
) => (
<span
{...props}
@@ -123,7 +65,7 @@ export const Icon = React.forwardRef(
export const Instruction = React.forwardRef(
(
{ className, ...props }: PropsWithChildren<BaseProps>,
ref: Ref<OrNull<HTMLDivElement>>
ref: Ref<HTMLDivElement>
) => (
<div
{...props}
@@ -145,7 +87,7 @@ export const Instruction = React.forwardRef(
export const Menu = React.forwardRef(
(
{ className, ...props }: PropsWithChildren<BaseProps>,
ref: Ref<OrNull<HTMLDivElement>>
ref: Ref<HTMLDivElement>
) => (
<div
{...props}
@@ -176,7 +118,7 @@ export const Portal = ({ children }: { children?: ReactNode }) => {
export const Toolbar = React.forwardRef(
(
{ className, ...props }: PropsWithChildren<BaseProps>,
ref: Ref<OrNull<HTMLDivElement>>
ref: Ref<HTMLDivElement>
) => (
<Menu
{...props}

View File

@@ -1,7 +1,7 @@
import React, { useMemo } from 'react'
import { createEditor, Descendant } from 'slate'
import { Slate, Editable, withReact } from 'slate-react'
import { Descendant, createEditor } from 'slate'
import { withHistory } from 'slate-history'
import { Editable, RenderPlaceholderProps, Slate, withReact } from 'slate-react'
const initialValue: Descendant[] = [
{
@@ -16,7 +16,10 @@ const PlainTextExample = () => {
<Slate editor={editor} initialValue={initialValue}>
<Editable
placeholder="Type something"
renderPlaceholder={({ children, attributes }) => (
renderPlaceholder={({
children,
attributes,
}: RenderPlaceholderProps) => (
<div {...attributes}>
<p>{children}</p>
<pre>

View File

@@ -26,7 +26,7 @@ export type EditableVoidElement = {
}
export type HeadingElement = {
type: 'heading'
type: 'heading-one'
align?: string
children: Descendant[]
}
@@ -37,6 +37,30 @@ export type HeadingTwoElement = {
children: Descendant[]
}
export type HeadingThreeElement = {
type: 'heading-three'
align?: string
children: Descendant[]
}
export type HeadingFourElement = {
type: 'heading-four'
align?: string
children: Descendant[]
}
export type HeadingFiveElement = {
type: 'heading-five'
align?: string
children: Descendant[]
}
export type HeadingSixElement = {
type: 'heading-six'
align?: string
children: Descendant[]
}
export type ImageElement = {
type: 'image'
url: string
@@ -51,6 +75,11 @@ export type BadgeElement = { type: 'badge'; children: Descendant[] }
export type ListItemElement = { type: 'list-item'; children: Descendant[] }
export type NumberedListItemElement = {
type: 'numbered-list'
children: Descendant[]
}
export type MentionElement = {
type: 'mention'
character: string
@@ -84,6 +113,17 @@ export type CodeLineElement = {
children: Descendant[]
}
export type CustomElementWithAlign =
| ParagraphElement
| HeadingElement
| HeadingTwoElement
| HeadingThreeElement
| HeadingFourElement
| HeadingFiveElement
| HeadingSixElement
| BlockQuoteElement
| BulletedListElement
type CustomElement =
| BlockQuoteElement
| BulletedListElement
@@ -91,11 +131,16 @@ type CustomElement =
| EditableVoidElement
| HeadingElement
| HeadingTwoElement
| HeadingThreeElement
| HeadingFourElement
| HeadingFiveElement
| HeadingSixElement
| ImageElement
| LinkElement
| ButtonElement
| BadgeElement
| ListItemElement
| NumberedListItemElement
| MentionElement
| ParagraphElement
| TableElement
@@ -106,17 +151,33 @@ type CustomElement =
| CodeBlockElement
| CodeLineElement
export type CustomElementType = CustomElement['type']
export type CustomText = {
bold?: boolean
italic?: boolean
code?: boolean
underline?: boolean
strikethrough?: boolean
// MARKDOWN PREVIEW SPECIFIC LEAF
underlined?: boolean
title?: boolean
list?: boolean
hr?: boolean
blockquote?: boolean
text: string
}
export type CustomTextKey = keyof Omit<CustomText, 'text'>
export type EmptyText = {
text: string
}
export type RenderElementPropsFor<T> = RenderElementProps & {
element: T
}
export type CustomEditor = BaseEditor &
ReactEditor &
HistoryEditor & {
@@ -127,7 +188,7 @@ declare module 'slate' {
interface CustomTypes {
Editor: CustomEditor
Element: CustomElement
Text: CustomText | EmptyText
Text: CustomText
Range: BaseRange & {
[key: string]: unknown
}

View File

@@ -1,12 +1,18 @@
import React, { useState, useMemo } from 'react'
import { Transforms, createEditor, Descendant } from 'slate'
import { Slate, Editable, useSlateStatic, withReact } from 'slate-react'
import { withHistory } from 'slate-history'
import { css } from '@emotion/css'
import React, { MouseEvent, useMemo, useState } from 'react'
import { createEditor, Descendant, Transforms } from 'slate'
import { withHistory } from 'slate-history'
import {
Editable,
RenderElementProps,
Slate,
useSlateStatic,
withReact,
} from 'slate-react'
import RichTextEditor from './richtext'
import { Button, Icon, Toolbar } from './components'
import { EditableVoidElement } from './custom-types.d'
import { CustomEditor, EditableVoidElement } from './custom-types.d'
import RichTextEditor from './richtext'
const EditableVoidsExample = () => {
const editor = useMemo(
@@ -28,7 +34,7 @@ const EditableVoidsExample = () => {
)
}
const withEditableVoids = editor => {
const withEditableVoids = (editor: CustomEditor) => {
const { isVoid } = editor
editor.isVoid = element => {
@@ -38,7 +44,7 @@ const withEditableVoids = editor => {
return editor
}
const insertEditableVoid = editor => {
const insertEditableVoid = (editor: CustomEditor) => {
const text = { text: '' }
const voidNode: EditableVoidElement = {
type: 'editable-void',
@@ -47,7 +53,7 @@ const insertEditableVoid = editor => {
Transforms.insertNodes(editor, voidNode)
}
const Element = props => {
const Element = (props: RenderElementProps) => {
const { attributes, children, element } = props
switch (element.type) {
@@ -62,7 +68,11 @@ const unsetWidthStyle = css`
width: unset;
`
const EditableVoid = ({ attributes, children, element }) => {
const EditableVoid = ({
attributes,
children,
element,
}: RenderElementProps) => {
const [inputValue, setInputValue] = useState('')
return (
@@ -81,7 +91,7 @@ const EditableVoid = ({ attributes, children, element }) => {
`}
type="text"
value={inputValue}
onChange={e => {
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value)
}}
/>
@@ -120,7 +130,7 @@ const InsertEditableVoidButton = () => {
const editor = useSlateStatic()
return (
<Button
onMouseDown={event => {
onMouseDown={(event: MouseEvent<HTMLSpanElement>) => {
event.preventDefault()
insertEditableVoid(editor)
}}

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from 'react'
import React, { ChangeEvent, useMemo } from 'react'
import {
Transforms,
createEditor,
@@ -11,7 +11,13 @@ import {
withReact,
useSlateStatic,
ReactEditor,
RenderElementProps,
} from 'slate-react'
import {
CustomEditor,
RenderElementPropsFor,
VideoElement as VideoElementType,
} from './custom-types.d'
const EmbedsExample = () => {
const editor = useMemo(() => withEmbeds(withReact(createEditor())), [])
@@ -25,13 +31,13 @@ const EmbedsExample = () => {
)
}
const withEmbeds = editor => {
const withEmbeds = (editor: CustomEditor) => {
const { isVoid } = editor
editor.isVoid = element => (element.type === 'video' ? true : isVoid(element))
return editor
}
const Element = props => {
const Element = (props: RenderElementProps) => {
const { attributes, children, element } = props
switch (element.type) {
case 'video':
@@ -43,12 +49,16 @@ const Element = props => {
const allowedSchemes = ['http:', 'https:']
const VideoElement = ({ attributes, children, element }) => {
const VideoElement = ({
attributes,
children,
element,
}: RenderElementPropsFor<VideoElementType>) => {
const editor = useSlateStatic()
const { url } = element
const safeUrl = useMemo(() => {
let parsedUrl: URL = null
let parsedUrl: URL | null = null
try {
parsedUrl = new URL(url)
// eslint-disable-next-line no-empty
@@ -98,17 +108,22 @@ const VideoElement = ({ attributes, children, element }) => {
)
}
const UrlInput = ({ url, onChange }) => {
interface UrlInputProps {
url: string
onChange: (url: string) => void
}
const UrlInput = ({ url, onChange }: UrlInputProps) => {
const [value, setValue] = React.useState(url)
return (
<input
value={value}
onClick={e => e.stopPropagation()}
onClick={(e: React.MouseEvent) => e.stopPropagation()}
style={{
marginTop: '5px',
boxSizing: 'border-box',
}}
onChange={e => {
onChange={(e: ChangeEvent<HTMLInputElement>) => {
const newUrl = e.target.value
setValue(newUrl)
onChange(newUrl)

View File

@@ -1,20 +1,26 @@
import React, { useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import {
Transforms,
createEditor,
Node,
Element as SlateElement,
Descendant,
Editor,
Node,
NodeEntry,
Element as SlateElement,
Transforms,
createEditor,
} from 'slate'
import { withHistory } from 'slate-history'
import { ParagraphElement, TitleElement } from './custom-types.d'
import { Editable, RenderElementProps, Slate, withReact } from 'slate-react'
import {
CustomEditor,
CustomElementType,
ParagraphElement,
TitleElement,
} from './custom-types.d'
const withLayout = editor => {
const withLayout = (editor: CustomEditor) => {
const { normalizeNode } = editor
editor.normalizeNode = ([node, path]) => {
editor.normalizeNode = ([node, path]: NodeEntry) => {
if (path.length === 0) {
if (editor.children.length <= 1 && Editor.string(editor, [0, 0]) === '') {
const title: TitleElement = {
@@ -36,9 +42,9 @@ const withLayout = editor => {
}
for (const [child, childPath] of Node.children(editor, path)) {
let type: string
let type: CustomElementType
const slateIndex = childPath[0]
const enforceType = type => {
const enforceType = (type: CustomElementType) => {
if (SlateElement.isElement(child) && child.type !== type) {
const newProperties: Partial<SlateElement> = { type }
Transforms.setNodes<SlateElement>(editor, newProperties, {
@@ -68,7 +74,10 @@ const withLayout = editor => {
}
const ForcedLayoutExample = () => {
const renderElement = useCallback(props => <Element {...props} />, [])
const renderElement = useCallback(
(props: RenderElementProps) => <Element {...props} />,
[]
)
const editor = useMemo(
() => withLayout(withHistory(withReact(createEditor()))),
[]
@@ -85,7 +94,7 @@ const ForcedLayoutExample = () => {
)
}
const Element = ({ attributes, children, element }) => {
const Element = ({ attributes, children, element }: RenderElementProps) => {
switch (element.type) {
case 'title':
return <h2 {...attributes}>{children}</h2>

View File

@@ -1,17 +1,18 @@
import React, { useMemo, useRef, useEffect } from 'react'
import { Slate, Editable, withReact, useSlate, useFocused } from 'slate-react'
import {
Editor,
Transforms,
Text,
createEditor,
Descendant,
Range,
} from 'slate'
import { css } from '@emotion/css'
import React, { MouseEvent, useEffect, useMemo, useRef } from 'react'
import { Descendant, Editor, Range, createEditor } from 'slate'
import { withHistory } from 'slate-history'
import {
Editable,
RenderLeafProps,
Slate,
useFocused,
useSlate,
withReact,
} from 'slate-react'
import { Button, Icon, Menu, Portal } from './components'
import { CustomEditor, CustomTextKey } from './custom-types.d'
const HoveringMenuExample = () => {
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
@@ -32,7 +33,7 @@ const HoveringMenuExample = () => {
return toggleMark(editor, 'italic')
case 'formatUnderline':
event.preventDefault()
return toggleMark(editor, 'underlined')
return toggleMark(editor, 'underline')
}
}}
/>
@@ -40,7 +41,7 @@ const HoveringMenuExample = () => {
)
}
const toggleMark = (editor, format) => {
const toggleMark = (editor: CustomEditor, format: CustomTextKey) => {
const isActive = isMarkActive(editor, format)
if (isActive) {
@@ -50,12 +51,12 @@ const toggleMark = (editor, format) => {
}
}
const isMarkActive = (editor, format) => {
const isMarkActive = (editor: CustomEditor, format: CustomTextKey) => {
const marks = Editor.marks(editor)
return marks ? marks[format] === true : false
}
const Leaf = ({ attributes, children, leaf }) => {
const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {
if (leaf.bold) {
children = <strong>{children}</strong>
}
@@ -64,7 +65,7 @@ const Leaf = ({ attributes, children, leaf }) => {
children = <em>{children}</em>
}
if (leaf.underlined) {
if (leaf.underline) {
children = <u>{children}</u>
}
@@ -72,7 +73,7 @@ const Leaf = ({ attributes, children, leaf }) => {
}
const HoveringToolbar = () => {
const ref = useRef<HTMLDivElement | null>()
const ref = useRef<HTMLDivElement | null>(null)
const editor = useSlate()
const inFocus = useFocused()
@@ -95,7 +96,7 @@ const HoveringToolbar = () => {
}
const domSelection = window.getSelection()
const domRange = domSelection.getRangeAt(0)
const domRange = domSelection!.getRangeAt(0)
const rect = domRange.getBoundingClientRect()
el.style.opacity = '1'
el.style.top = `${rect.top + window.pageYOffset - el.offsetHeight}px`
@@ -120,20 +121,25 @@ const HoveringToolbar = () => {
border-radius: 4px;
transition: opacity 0.75s;
`}
onMouseDown={e => {
onMouseDown={(e: MouseEvent) => {
// prevent toolbar from taking focus away from editor
e.preventDefault()
}}
>
<FormatButton format="bold" icon="format_bold" />
<FormatButton format="italic" icon="format_italic" />
<FormatButton format="underlined" icon="format_underlined" />
<FormatButton format="underline" icon="format_underlined" />
</Menu>
</Portal>
)
}
const FormatButton = ({ format, icon }) => {
interface FormatButtonProps {
format: CustomTextKey
icon: string
}
const FormatButton = ({ format, icon }: FormatButtonProps) => {
const editor = useSlate()
return (
<Button

View File

@@ -1,29 +1,40 @@
import React, { useMemo, useCallback } from 'react'
import { faker } from '@faker-js/faker'
import React, { useCallback, useMemo } from 'react'
import { createEditor, Descendant } from 'slate'
import { Slate, Editable, withReact } from 'slate-react'
import { Editable, RenderElementProps, Slate, withReact } from 'slate-react'
import {
CustomEditor,
HeadingElement,
ParagraphElement,
} from './custom-types.d'
const HEADINGS = 100
const PARAGRAPHS = 7
const initialValue: Descendant[] = []
for (let h = 0; h < HEADINGS; h++) {
initialValue.push({
type: 'heading',
const heading: HeadingElement = {
type: 'heading-one',
children: [{ text: faker.lorem.sentence() }],
})
}
initialValue.push(heading)
for (let p = 0; p < PARAGRAPHS; p++) {
initialValue.push({
const paragraph: ParagraphElement = {
type: 'paragraph',
children: [{ text: faker.lorem.paragraph() }],
})
}
initialValue.push(paragraph)
}
}
const HugeDocumentExample = () => {
const renderElement = useCallback(props => <Element {...props} />, [])
const editor = useMemo(() => withReact(createEditor()), [])
const renderElement = useCallback(
(props: RenderElementProps) => <Element {...props} />,
[]
)
const editor = useMemo(() => withReact(createEditor()) as CustomEditor, [])
return (
<Slate editor={editor} initialValue={initialValue}>
<Editable renderElement={renderElement} spellCheck autoFocus />
@@ -31,9 +42,9 @@ const HugeDocumentExample = () => {
)
}
const Element = ({ attributes, children, element }) => {
const Element = ({ attributes, children, element }: RenderElementProps) => {
switch (element.type) {
case 'heading':
case 'heading-one':
return <h1 {...attributes}>{children}</h1>
default:
return <p {...attributes}>{children}</p>

View File

@@ -1,13 +1,22 @@
import React, { useCallback, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import isHotkey from 'is-hotkey'
import { Editable, withReact, useSlate, Slate, ReactEditor } from 'slate-react'
import React, { MouseEvent, useCallback, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import { Editor, createEditor, Descendant } from 'slate'
import { withHistory } from 'slate-history'
import {
Editable,
ReactEditor,
RenderElementProps,
RenderLeafProps,
Slate,
useSlate,
withReact,
} from 'slate-react'
import { Button, Icon, Toolbar } from './components'
import { CustomEditor, CustomTextKey } from './custom-types.d'
const HOTKEYS = {
const HOTKEYS: Record<string, CustomTextKey> = {
'mod+b': 'bold',
'mod+i': 'italic',
'mod+u': 'underline',
@@ -16,11 +25,19 @@ const HOTKEYS = {
const IFrameExample = () => {
const renderElement = useCallback(
({ attributes, children }) => <p {...attributes}>{children}</p>,
({ attributes, children }: RenderElementProps) => (
<p {...attributes}>{children}</p>
),
[]
)
const renderLeaf = useCallback(
(props: RenderLeafProps) => <Leaf {...props} />,
[]
)
const editor = useMemo(
() => withHistory(withReact(createEditor())) as CustomEditor,
[]
)
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
const handleBlur = useCallback(() => ReactEditor.deselect(editor), [editor])
@@ -54,7 +71,7 @@ const IFrameExample = () => {
)
}
const toggleMark = (editor, format) => {
const toggleMark = (editor: CustomEditor, format: CustomTextKey) => {
const isActive = isMarkActive(editor, format)
if (isActive) {
Editor.removeMark(editor, format)
@@ -63,12 +80,12 @@ const toggleMark = (editor, format) => {
}
}
const isMarkActive = (editor, format) => {
const isMarkActive = (editor: CustomEditor, format: CustomTextKey) => {
const marks = Editor.marks(editor)
return marks ? marks[format] === true : false
}
const Leaf = ({ attributes, children, leaf }) => {
const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {
if (leaf.bold) {
children = <strong>{children}</strong>
}
@@ -88,12 +105,17 @@ const Leaf = ({ attributes, children, leaf }) => {
return <span {...attributes}>{children}</span>
}
const MarkButton = ({ format, icon }) => {
interface MarkButtonProps {
format: CustomTextKey
icon: string
}
const MarkButton = ({ format, icon }: MarkButtonProps) => {
const editor = useSlate()
return (
<Button
active={isMarkActive(editor, format)}
onMouseDown={event => {
onMouseDown={(event: MouseEvent) => {
event.preventDefault()
toggleMark(editor, format)
}}
@@ -103,10 +125,16 @@ const MarkButton = ({ format, icon }) => {
)
}
const IFrame = ({ children, ...props }) => {
const [iframeBody, setIframeBody] = useState(null)
const handleLoad = e => {
setIframeBody(e.target.contentDocument.body)
interface IFrameProps extends React.IframeHTMLAttributes<HTMLIFrameElement> {
children: React.ReactNode
}
const IFrame = ({ children, ...props }: IFrameProps) => {
const [iframeBody, setIframeBody] = useState<HTMLElement | null>(null)
const handleLoad = (e: React.SyntheticEvent<HTMLIFrameElement>) => {
const iframe = e.target as HTMLIFrameElement
if (!iframe.contentDocument) return
setIframeBody(iframe.contentDocument.body)
}
return (
<iframe srcDoc={`<!DOCTYPE html>`} {...props} onLoad={handleLoad}>

View File

@@ -1,26 +1,32 @@
import React, { useMemo } from 'react'
import imageExtensions from 'image-extensions'
import isUrl from 'is-url'
import isHotkey from 'is-hotkey'
import { Transforms, createEditor, Descendant } from 'slate'
import {
Slate,
Editable,
useSlateStatic,
useSelected,
useFocused,
withReact,
ReactEditor,
} from 'slate-react'
import { withHistory } from 'slate-history'
import { css } from '@emotion/css'
import imageExtensions from 'image-extensions'
import isHotkey from 'is-hotkey'
import isUrl from 'is-url'
import React, { MouseEvent, useMemo } from 'react'
import { Descendant, Transforms, createEditor } from 'slate'
import { withHistory } from 'slate-history'
import {
Editable,
ReactEditor,
RenderElementProps,
Slate,
useFocused,
useSelected,
useSlateStatic,
withReact,
} from 'slate-react'
import { Button, Icon, Toolbar } from './components'
import { ImageElement } from './custom-types.d'
import {
CustomEditor,
ImageElement,
ParagraphElement,
RenderElementPropsFor,
} from './custom-types.d'
const ImagesExample = () => {
const editor = useMemo(
() => withImages(withHistory(withReact(createEditor()))),
() => withImages(withHistory(withReact(createEditor()))) as CustomEditor,
[]
)
@@ -36,14 +42,14 @@ const ImagesExample = () => {
Transforms.select(editor, [])
}
}}
renderElement={props => <Element {...props} />}
renderElement={(props: RenderElementProps) => <Element {...props} />}
placeholder="Enter some text..."
/>
</Slate>
)
}
const withImages = editor => {
const withImages = (editor: CustomEditor) => {
const { insertData, isVoid } = editor
editor.isVoid = element => {
@@ -55,19 +61,19 @@ const withImages = editor => {
const { files } = data
if (files && files.length > 0) {
for (const file of files) {
Array.from(files).forEach(file => {
const reader = new FileReader()
const [mime] = file.type.split('/')
if (mime === 'image') {
reader.addEventListener('load', () => {
const url = reader.result
insertImage(editor, url)
insertImage(editor, url as string)
})
reader.readAsDataURL(file)
}
}
})
} else if (isImageUrl(text)) {
insertImage(editor, text)
} else {
@@ -78,17 +84,18 @@ const withImages = editor => {
return editor
}
const insertImage = (editor, url) => {
const insertImage = (editor: CustomEditor, url: string) => {
const text = { text: '' }
const image: ImageElement = { type: 'image', url, children: [text] }
Transforms.insertNodes(editor, image)
Transforms.insertNodes(editor, {
const paragraph: ParagraphElement = {
type: 'paragraph',
children: [{ text: '' }],
})
}
Transforms.insertNodes(editor, paragraph)
}
const Element = props => {
const Element = (props: RenderElementProps) => {
const { attributes, children, element } = props
switch (element.type) {
@@ -99,10 +106,13 @@ const Element = props => {
}
}
const Image = ({ attributes, children, element }) => {
const Image = ({
attributes,
children,
element,
}: RenderElementPropsFor<ImageElement>) => {
const editor = useSlateStatic()
const path = ReactEditor.findPath(editor, element)
const selected = useSelected()
const focused = useFocused()
return (
@@ -145,7 +155,7 @@ const InsertImageButton = () => {
const editor = useSlateStatic()
return (
<Button
onMouseDown={event => {
onMouseDown={(event: MouseEvent) => {
event.preventDefault()
const url = window.prompt('Enter the URL of the image:')
if (url && !isImageUrl(url)) {
@@ -160,11 +170,11 @@ const InsertImageButton = () => {
)
}
const isImageUrl = url => {
const isImageUrl = (url: string): boolean => {
if (!url) return false
if (!isUrl(url)) return false
const ext = new URL(url).pathname.split('.').pop()
return imageExtensions.includes(ext)
return imageExtensions.includes(ext!)
}
const initialValue: Descendant[] = [

View File

@@ -1,21 +1,35 @@
import React, { useMemo } from 'react'
import isUrl from 'is-url'
import { isKeyHotkey } from 'is-hotkey'
import { css } from '@emotion/css'
import { Editable, withReact, useSlate, useSelected } from 'slate-react'
import * as SlateReact from 'slate-react'
import { isKeyHotkey } from 'is-hotkey'
import isUrl from 'is-url'
import React, { MouseEvent, useMemo } from 'react'
import {
Transforms,
Editor,
Range,
createEditor,
Element as SlateElement,
Descendant,
Editor,
Element as SlateElement,
Range,
Transforms,
} from 'slate'
import { withHistory } from 'slate-history'
import { LinkElement, ButtonElement } from './custom-types.d'
import {
Editable,
RenderElementProps,
RenderLeafProps,
useSelected,
useSlate,
withReact,
} from 'slate-react'
import * as SlateReact from 'slate-react'
import { Button, Icon, Toolbar } from './components'
import {
BadgeElement,
ButtonElement,
CustomEditor,
CustomElement,
LinkElement,
RenderElementPropsFor,
} from './custom-types.d'
const initialValue: Descendant[] = [
{
@@ -67,7 +81,7 @@ const initialValue: Descendant[] = [
]
const InlinesExample = () => {
const editor = useMemo(
() => withInlines(withHistory(withReact(createEditor()))),
() => withInlines(withHistory(withReact(createEditor()))) as CustomEditor,
[]
)
@@ -112,17 +126,17 @@ const InlinesExample = () => {
)
}
const withInlines = editor => {
const withInlines = (editor: CustomEditor) => {
const { insertData, insertText, isInline, isElementReadOnly, isSelectable } =
editor
editor.isInline = element =>
editor.isInline = (element: CustomElement) =>
['link', 'button', 'badge'].includes(element.type) || isInline(element)
editor.isElementReadOnly = element =>
editor.isElementReadOnly = (element: CustomElement) =>
element.type === 'badge' || isElementReadOnly(element)
editor.isSelectable = element =>
editor.isSelectable = (element: CustomElement) =>
element.type !== 'badge' && isSelectable(element)
editor.insertText = text => {
@@ -146,19 +160,19 @@ const withInlines = editor => {
return editor
}
const insertLink = (editor, url) => {
const insertLink = (editor: CustomEditor, url: string) => {
if (editor.selection) {
wrapLink(editor, url)
}
}
const insertButton = editor => {
const insertButton = (editor: CustomEditor) => {
if (editor.selection) {
wrapButton(editor)
}
}
const isLinkActive = editor => {
const isLinkActive = (editor: CustomEditor): boolean => {
const [link] = Editor.nodes(editor, {
match: n =>
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'link',
@@ -166,7 +180,7 @@ const isLinkActive = editor => {
return !!link
}
const isButtonActive = editor => {
const isButtonActive = (editor: CustomEditor): boolean => {
const [button] = Editor.nodes(editor, {
match: n =>
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'button',
@@ -174,21 +188,21 @@ const isButtonActive = editor => {
return !!button
}
const unwrapLink = editor => {
const unwrapLink = (editor: CustomEditor) => {
Transforms.unwrapNodes(editor, {
match: n =>
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'link',
})
}
const unwrapButton = editor => {
const unwrapButton = (editor: CustomEditor) => {
Transforms.unwrapNodes(editor, {
match: n =>
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'button',
})
}
const wrapLink = (editor, url: string) => {
const wrapLink = (editor: CustomEditor, url: string) => {
if (isLinkActive(editor)) {
unwrapLink(editor)
}
@@ -209,7 +223,7 @@ const wrapLink = (editor, url: string) => {
}
}
const wrapButton = editor => {
const wrapButton = (editor: CustomEditor) => {
if (isButtonActive(editor)) {
unwrapButton(editor)
}
@@ -244,11 +258,14 @@ const InlineChromiumBugfix = () => (
const allowedSchemes = ['http:', 'https:', 'mailto:', 'tel:']
const LinkComponent = ({ attributes, children, element }) => {
const LinkComponent = ({
attributes,
children,
element,
}: RenderElementPropsFor<LinkElement>) => {
const selected = useSelected()
const safeUrl = useMemo(() => {
let parsedUrl: URL = null
let parsedUrl: URL | null = null
try {
parsedUrl = new URL(element.url)
// eslint-disable-next-line no-empty
@@ -278,7 +295,10 @@ const LinkComponent = ({ attributes, children, element }) => {
)
}
const EditableButtonComponent = ({ attributes, children }) => {
const EditableButtonComponent = ({
attributes,
children,
}: RenderElementProps) => {
return (
/*
Note that this is not a true button, but a span with button-like CSS.
@@ -310,7 +330,11 @@ const EditableButtonComponent = ({ attributes, children }) => {
)
}
const BadgeComponent = ({ attributes, children, element }) => {
const BadgeComponent = ({
attributes,
children,
element,
}: RenderElementProps) => {
const selected = useSelected()
return (
@@ -334,7 +358,7 @@ const BadgeComponent = ({ attributes, children, element }) => {
)
}
const Element = props => {
const Element = (props: RenderElementProps) => {
const { attributes, children, element } = props
switch (element.type) {
case 'link':
@@ -348,7 +372,7 @@ const Element = props => {
}
}
const Text = props => {
const Text = (props: RenderLeafProps) => {
const { attributes, children, leaf } = props
return (
<span
@@ -362,7 +386,7 @@ const Text = props => {
? css`
padding-left: 0.1px;
`
: null
: undefined
}
{...attributes}
>
@@ -376,7 +400,7 @@ const AddLinkButton = () => {
return (
<Button
active={isLinkActive(editor)}
onMouseDown={event => {
onMouseDown={(event: MouseEvent) => {
event.preventDefault()
const url = window.prompt('Enter the URL of the link:')
if (!url) return
@@ -394,7 +418,7 @@ const RemoveLinkButton = () => {
return (
<Button
active={isLinkActive(editor)}
onMouseDown={event => {
onMouseDown={(event: MouseEvent) => {
if (isLinkActive(editor)) {
unwrapLink(editor)
}
@@ -410,7 +434,7 @@ const ToggleEditableButtonButton = () => {
return (
<Button
active
onMouseDown={event => {
onMouseDown={(event: MouseEvent) => {
event.preventDefault()
if (isButtonActive(editor)) {
unwrapButton(editor)

View File

@@ -1,28 +1,38 @@
import { css } from '@emotion/css'
import Prism from 'prismjs'
import 'prismjs/components/prism-markdown'
import React, { useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import { Text, createEditor, Descendant } from 'slate'
import { Descendant, NodeEntry, Range, Text, createEditor } from 'slate'
import { withHistory } from 'slate-history'
import { css } from '@emotion/css'
import { Editable, RenderLeafProps, Slate, withReact } from 'slate-react'
import { CustomEditor } from './custom-types.d'
const MarkdownPreviewExample = () => {
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
const decorate = useCallback(([node, path]) => {
const ranges = []
const renderLeaf = useCallback(
(props: RenderLeafProps) => <Leaf {...props} />,
[]
)
const editor = useMemo(
() => withHistory(withReact(createEditor())) as CustomEditor,
[]
)
const decorate = useCallback(([node, path]: NodeEntry) => {
const ranges: Range[] = []
if (!Text.isText(node)) {
return ranges
}
const getLength = token => {
const getLength = (token: string | Prism.Token): number => {
if (typeof token === 'string') {
return token.length
} else if (typeof token.content === 'string') {
return token.content.length
} else {
return token.content.reduce((l, t) => l + getLength(t), 0)
return (token.content as Prism.Token[]).reduce(
(l, t) => l + getLength(t),
0
)
}
}
@@ -58,7 +68,7 @@ const MarkdownPreviewExample = () => {
)
}
const Leaf = ({ attributes, children, leaf }) => {
const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {
return (
<span
{...attributes}

View File

@@ -10,10 +10,21 @@ import {
Transforms,
} from 'slate'
import { withHistory } from 'slate-history'
import { Editable, ReactEditor, Slate, withReact } from 'slate-react'
import { BulletedListElement } from './custom-types.d'
import {
Editable,
ReactEditor,
RenderElementProps,
Slate,
withReact,
} from 'slate-react'
const SHORTCUTS = {
import {
BulletedListElement,
CustomEditor,
CustomElementType,
} from './custom-types.d'
const SHORTCUTS: Record<string, CustomElementType> = {
'*': 'list-item',
'-': 'list-item',
'+': 'list-item',
@@ -24,12 +35,15 @@ const SHORTCUTS = {
'####': 'heading-four',
'#####': 'heading-five',
'######': 'heading-six',
}
} as const
const MarkdownShortcutsExample = () => {
const renderElement = useCallback(props => <Element {...props} />, [])
const renderElement = useCallback(
(props: RenderElementProps) => <Element {...props} />,
[]
)
const editor = useMemo(
() => withShortcuts(withReact(withHistory(createEditor()))),
() => withShortcuts(withReact(withHistory(createEditor()))) as CustomEditor,
[]
)
@@ -82,7 +96,7 @@ const MarkdownShortcutsExample = () => {
)
}
const withShortcuts = editor => {
const withShortcuts = (editor: CustomEditor) => {
const { deleteBackward, insertText } = editor
editor.insertText = text => {
@@ -177,7 +191,7 @@ const withShortcuts = editor => {
return editor
}
const Element = ({ attributes, children, element }) => {
const Element = ({ attributes, children, element }: RenderElementProps) => {
switch (element.type) {
case 'block-quote':
return <blockquote {...attributes}>{children}</blockquote>

View File

@@ -1,35 +1,56 @@
import React, {
useMemo,
useCallback,
useRef,
useEffect,
useState,
Fragment,
KeyboardEvent,
MouseEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { Editor, Transforms, Range, createEditor, Descendant } from 'slate'
import {
Editor,
Transforms,
Range,
createEditor,
Descendant,
Element as SlateElement,
} from 'slate'
import { withHistory } from 'slate-history'
import {
Slate,
Editable,
ReactEditor,
withReact,
useSelected,
RenderElementProps,
RenderLeafProps,
Slate,
useFocused,
useSelected,
withReact,
} from 'slate-react'
import { Portal } from './components'
import { MentionElement } from './custom-types.d'
import {
CustomEditor,
MentionElement,
RenderElementPropsFor,
} from './custom-types.d'
import { IS_MAC } from './utils/environment'
const MentionExample = () => {
const ref = useRef<HTMLDivElement | null>()
const [target, setTarget] = useState<Range | undefined>()
const ref = useRef<HTMLDivElement | null>(null)
const [target, setTarget] = useState<Range | null>(null)
const [index, setIndex] = useState(0)
const [search, setSearch] = useState('')
const renderElement = useCallback(props => <Element {...props} />, [])
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
const renderElement = useCallback(
(props: RenderElementProps) => <Element {...props} />,
[]
)
const renderLeaf = useCallback(
(props: RenderLeafProps) => <Leaf {...props} />,
[]
)
const editor = useMemo(
() => withMentions(withReact(withHistory(createEditor()))),
() => withMentions(withReact(withHistory(createEditor()))) as CustomEditor,
[]
)
@@ -38,7 +59,7 @@ const MentionExample = () => {
).slice(0, 10)
const onKeyDown = useCallback(
event => {
(event: KeyboardEvent<HTMLDivElement>) => {
if (target && chars.length > 0) {
switch (event.key) {
case 'ArrowDown':
@@ -69,7 +90,7 @@ const MentionExample = () => {
)
useEffect(() => {
if (target && chars.length > 0) {
if (target && chars.length > 0 && ref.current) {
const el = ref.current
const domRange = ReactEditor.toDOMRange(editor, target)
const rect = domRange.getBoundingClientRect()
@@ -133,7 +154,7 @@ const MentionExample = () => {
{chars.map((char, i) => (
<div
key={char}
onClick={() => {
onClick={(e: MouseEvent) => {
Transforms.select(editor, target)
insertMention(editor, char)
setTarget(null)
@@ -155,25 +176,25 @@ const MentionExample = () => {
)
}
const withMentions = editor => {
const withMentions = (editor: CustomEditor) => {
const { isInline, isVoid, markableVoid } = editor
editor.isInline = element => {
editor.isInline = (element: SlateElement) => {
return element.type === 'mention' ? true : isInline(element)
}
editor.isVoid = element => {
editor.isVoid = (element: SlateElement) => {
return element.type === 'mention' ? true : isVoid(element)
}
editor.markableVoid = element => {
editor.markableVoid = (element: SlateElement) => {
return element.type === 'mention' || markableVoid(element)
}
return editor
}
const insertMention = (editor, character) => {
const insertMention = (editor: CustomEditor, character: string) => {
const mention: MentionElement = {
type: 'mention',
character,
@@ -185,7 +206,7 @@ const insertMention = (editor, character) => {
// Borrow Leaf renderer from the Rich Text example.
// In a real project you would get this via `withRichText(editor)` or similar.
const Leaf = ({ attributes, children, leaf }) => {
const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {
if (leaf.bold) {
children = <strong>{children}</strong>
}
@@ -205,7 +226,7 @@ const Leaf = ({ attributes, children, leaf }) => {
return <span {...attributes}>{children}</span>
}
const Element = props => {
const Element = (props: RenderElementProps) => {
const { attributes, children, element } = props
switch (element.type) {
case 'mention':
@@ -215,7 +236,11 @@ const Element = props => {
}
}
const Mention = ({ attributes, children, element }) => {
const Mention = ({
attributes,
children,
element,
}: RenderElementPropsFor<MentionElement>) => {
const selected = useSelected()
const focused = useFocused()
const style: React.CSSProperties = {

View File

@@ -1,35 +1,58 @@
import React, { useCallback, useMemo } from 'react'
import { jsx } from 'slate-hyperscript'
import { Transforms, createEditor, Descendant } from 'slate'
import { withHistory } from 'slate-history'
import { css } from '@emotion/css'
import React, { useCallback, useMemo } from 'react'
import { Descendant, Transforms, createEditor } from 'slate'
import { withHistory } from 'slate-history'
import { jsx } from 'slate-hyperscript'
import {
Slate,
Editable,
withReact,
useSelected,
RenderElementProps,
RenderLeafProps,
Slate,
useFocused,
useSelected,
withReact,
} from 'slate-react'
const ELEMENT_TAGS = {
A: el => ({ type: 'link', url: el.getAttribute('href') }),
BLOCKQUOTE: () => ({ type: 'quote' }),
import {
CustomEditor,
CustomElement,
CustomElementType,
ImageElement as ImageElementType,
RenderElementPropsFor,
} from './custom-types.d'
interface ElementAttributes {
type: CustomElementType
url?: string
}
const ELEMENT_TAGS: Record<string, (el: HTMLElement) => ElementAttributes> = {
A: el => ({ type: 'link', url: el.getAttribute('href')! }),
BLOCKQUOTE: () => ({ type: 'block-quote' }),
H1: () => ({ type: 'heading-one' }),
H2: () => ({ type: 'heading-two' }),
H3: () => ({ type: 'heading-three' }),
H4: () => ({ type: 'heading-four' }),
H5: () => ({ type: 'heading-five' }),
H6: () => ({ type: 'heading-six' }),
IMG: el => ({ type: 'image', url: el.getAttribute('src') }),
IMG: el => ({ type: 'image', url: el.getAttribute('src')! }),
LI: () => ({ type: 'list-item' }),
OL: () => ({ type: 'numbered-list' }),
P: () => ({ type: 'paragraph' }),
PRE: () => ({ type: 'code' }),
PRE: () => ({ type: 'code-block' }),
UL: () => ({ type: 'bulleted-list' }),
}
// COMPAT: `B` is omitted here because Google Docs uses `<b>` in weird ways.
const TEXT_TAGS = {
interface TextAttributes {
code?: boolean
strikethrough?: boolean
italic?: boolean
bold?: boolean
underline?: boolean
}
const TEXT_TAGS: Record<string, () => TextAttributes> = {
CODE: () => ({ code: true }),
DEL: () => ({ strikethrough: true }),
EM: () => ({ italic: true }),
@@ -39,7 +62,7 @@ const TEXT_TAGS = {
U: () => ({ underline: true }),
}
export const deserialize = el => {
export const deserialize = (el: HTMLElement | ChildNode): any => {
if (el.nodeType === 3) {
return el.textContent
} else if (el.nodeType !== 1) {
@@ -69,12 +92,12 @@ export const deserialize = el => {
}
if (ELEMENT_TAGS[nodeName]) {
const attrs = ELEMENT_TAGS[nodeName](el)
const attrs = ELEMENT_TAGS[nodeName](el as HTMLElement)
return jsx('element', attrs, children)
}
if (TEXT_TAGS[nodeName]) {
const attrs = TEXT_TAGS[nodeName](el)
const attrs = TEXT_TAGS[nodeName]()
return children.map(child => jsx('text', attrs, child))
}
@@ -82,10 +105,16 @@ export const deserialize = el => {
}
const PasteHtmlExample = () => {
const renderElement = useCallback(props => <Element {...props} />, [])
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
const renderElement = useCallback(
(props: RenderElementProps) => <Element {...props} />,
[]
)
const renderLeaf = useCallback(
(props: RenderLeafProps) => <Leaf {...props} />,
[]
)
const editor = useMemo(
() => withHtml(withReact(withHistory(createEditor()))),
() => withHtml(withReact(withHistory(createEditor()))) as CustomEditor,
[]
)
return (
@@ -99,14 +128,14 @@ const PasteHtmlExample = () => {
)
}
const withHtml = editor => {
const withHtml = (editor: CustomEditor) => {
const { insertData, isInline, isVoid } = editor
editor.isInline = element => {
editor.isInline = (element: CustomElement) => {
return element.type === 'link' ? true : isInline(element)
}
editor.isVoid = element => {
editor.isVoid = (element: CustomElement) => {
return element.type === 'image' ? true : isVoid(element)
}
@@ -126,15 +155,15 @@ const withHtml = editor => {
return editor
}
const Element = props => {
const Element = (props: RenderElementProps) => {
const { attributes, children, element } = props
switch (element.type) {
default:
return <p {...attributes}>{children}</p>
case 'quote':
case 'block-quote':
return <blockquote {...attributes}>{children}</blockquote>
case 'code':
case 'code-block':
return (
<pre>
<code {...attributes}>{children}</code>
@@ -160,7 +189,7 @@ const Element = props => {
return <ol {...attributes}>{children}</ol>
case 'link':
return (
<SafeLink href={element.url} {...attributes}>
<SafeLink href={element.url} attributes={attributes}>
{children}
</SafeLink>
)
@@ -171,9 +200,15 @@ const Element = props => {
const allowedSchemes = ['http:', 'https:', 'mailto:', 'tel:']
const SafeLink = ({ attributes, children, href }) => {
interface SafeLinkProps {
attributes: Record<string, unknown>
children: React.ReactNode
href: string
}
const SafeLink = ({ children, href, attributes }: SafeLinkProps) => {
const safeHref = useMemo(() => {
let parsedUrl: URL = null
let parsedUrl: URL | null = null
try {
parsedUrl = new URL(href)
// eslint-disable-next-line no-empty
@@ -191,7 +226,11 @@ const SafeLink = ({ attributes, children, href }) => {
)
}
const ImageElement = ({ attributes, children, element }) => {
const ImageElement = ({
attributes,
children,
element,
}: RenderElementPropsFor<ImageElementType>) => {
const selected = useSelected()
const focused = useFocused()
return (
@@ -210,7 +249,7 @@ const ImageElement = ({ attributes, children, element }) => {
)
}
const Leaf = ({ attributes, children, leaf }) => {
const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {
if (leaf.bold) {
children = <strong>{children}</strong>
}

View File

@@ -1,30 +1,53 @@
import React, { useCallback, useMemo } from 'react'
import isHotkey from 'is-hotkey'
import { Editable, withReact, useSlate, Slate } from 'slate-react'
import React, { KeyboardEvent, MouseEvent, useCallback, useMemo } from 'react'
import {
Descendant,
Editor,
Element as SlateElement,
Transforms,
createEditor,
Descendant,
Element as SlateElement,
} from 'slate'
import { withHistory } from 'slate-history'
import {
Editable,
RenderElementProps,
RenderLeafProps,
Slate,
useSlate,
withReact,
} from 'slate-react'
import { Button, Icon, Toolbar } from './components'
import {
CustomEditor,
CustomElement,
CustomElementType,
CustomElementWithAlign,
CustomTextKey,
} from './custom-types.d'
const HOTKEYS = {
const HOTKEYS: Record<string, CustomTextKey> = {
'mod+b': 'bold',
'mod+i': 'italic',
'mod+u': 'underline',
'mod+`': 'code',
}
const LIST_TYPES = ['numbered-list', 'bulleted-list']
const TEXT_ALIGN_TYPES = ['left', 'center', 'right', 'justify']
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]
type ListType = (typeof LIST_TYPES)[number]
type CustomElementFormat = CustomElementType | AlignType | ListType
const RichTextExample = () => {
const renderElement = useCallback(props => <Element {...props} />, [])
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
const renderElement = useCallback(
(props: RenderElementProps) => <Element {...props} />,
[]
)
const renderLeaf = useCallback(
(props: RenderLeafProps) => <Leaf {...props} />,
[]
)
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
return (
@@ -50,7 +73,7 @@ const RichTextExample = () => {
placeholder="Enter some rich text…"
spellCheck
autoFocus
onKeyDown={event => {
onKeyDown={(event: KeyboardEvent<HTMLDivElement>) => {
for (const hotkey in HOTKEYS) {
if (isHotkey(hotkey, event as any)) {
event.preventDefault()
@@ -64,24 +87,24 @@ const RichTextExample = () => {
)
}
const toggleBlock = (editor, format) => {
const toggleBlock = (editor: CustomEditor, format: CustomElementFormat) => {
const isActive = isBlockActive(
editor,
format,
TEXT_ALIGN_TYPES.includes(format) ? 'align' : 'type'
isAlignType(format) ? 'align' : 'type'
)
const isList = LIST_TYPES.includes(format)
const isList = isListType(format)
Transforms.unwrapNodes(editor, {
match: n =>
!Editor.isEditor(n) &&
SlateElement.isElement(n) &&
LIST_TYPES.includes(n.type) &&
!TEXT_ALIGN_TYPES.includes(format),
isListType(n.type) &&
!isAlignType(format),
split: true,
})
let newProperties: Partial<SlateElement>
if (TEXT_ALIGN_TYPES.includes(format)) {
if (isAlignType(format)) {
newProperties = {
align: isActive ? undefined : format,
}
@@ -98,7 +121,7 @@ const toggleBlock = (editor, format) => {
}
}
const toggleMark = (editor, format) => {
const toggleMark = (editor: CustomEditor, format: CustomTextKey) => {
const isActive = isMarkActive(editor, format)
if (isActive) {
@@ -108,30 +131,42 @@ const toggleMark = (editor, format) => {
}
}
const isBlockActive = (editor, format, blockType = 'type') => {
const isBlockActive = (
editor: CustomEditor,
format: CustomElementFormat,
blockType: 'type' | 'align' = '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,
match: n => {
if (!Editor.isEditor(n) && SlateElement.isElement(n)) {
if (blockType === 'align' && isAlignElement(n)) {
return n.align === format
}
return n.type === format
}
return false
},
})
)
return !!match
}
const isMarkActive = (editor, format) => {
const isMarkActive = (editor: CustomEditor, format: CustomTextKey) => {
const marks = Editor.marks(editor)
return marks ? marks[format] === true : false
}
const Element = ({ attributes, children, element }) => {
const style = { textAlign: element.align }
const Element = ({ attributes, children, element }: RenderElementProps) => {
const style: React.CSSProperties = {}
if (isAlignElement(element)) {
style.textAlign = element.align as AlignType
}
switch (element.type) {
case 'block-quote':
return (
@@ -178,7 +213,7 @@ const Element = ({ attributes, children, element }) => {
}
}
const Leaf = ({ attributes, children, leaf }) => {
const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {
if (leaf.bold) {
children = <strong>{children}</strong>
}
@@ -198,16 +233,21 @@ const Leaf = ({ attributes, children, leaf }) => {
return <span {...attributes}>{children}</span>
}
const BlockButton = ({ format, icon }) => {
interface BlockButtonProps {
format: CustomElementFormat
icon: string
}
const BlockButton = ({ format, icon }: BlockButtonProps) => {
const editor = useSlate()
return (
<Button
active={isBlockActive(
editor,
format,
TEXT_ALIGN_TYPES.includes(format) ? 'align' : 'type'
isAlignType(format) ? 'align' : 'type'
)}
onMouseDown={event => {
onMouseDown={(event: MouseEvent<HTMLSpanElement>) => {
event.preventDefault()
toggleBlock(editor, format)
}}
@@ -217,12 +257,17 @@ const BlockButton = ({ format, icon }) => {
)
}
const MarkButton = ({ format, icon }) => {
interface MarkButtonProps {
format: CustomTextKey
icon: string
}
const MarkButton = ({ format, icon }: MarkButtonProps) => {
const editor = useSlate()
return (
<Button
active={isMarkActive(editor, format)}
onMouseDown={event => {
onMouseDown={(event: MouseEvent<HTMLSpanElement>) => {
event.preventDefault()
toggleMark(editor, format)
}}
@@ -232,6 +277,20 @@ const MarkButton = ({ format, icon }) => {
)
}
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)
}
const isAlignElement = (
element: CustomElement
): element is CustomElementWithAlign => {
return 'align' in element
}
const initialValue: Descendant[] = [
{
type: 'paragraph',

View File

@@ -1,20 +1,31 @@
import React, { useState, useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import { Text, Descendant, createEditor } from 'slate'
import { css } from '@emotion/css'
import React, { useCallback, useMemo, useState } from 'react'
import {
Descendant,
Element,
NodeEntry,
Range,
Text,
createEditor,
} from 'slate'
import { withHistory } from 'slate-history'
import { Editable, RenderLeafProps, Slate, withReact } from 'slate-react'
import { Icon, Toolbar } from './components'
import { CustomEditor, CustomText } from './custom-types.d'
const SearchHighlightingExample = () => {
const [search, setSearch] = useState<string | undefined>()
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
const [search, setSearch] = useState<string>('')
const editor = useMemo(
() => withHistory(withReact(createEditor())) as CustomEditor,
[]
)
const decorate = useCallback(
([node, path]) => {
const ranges = []
([node, path]: NodeEntry) => {
const ranges: Range[] = []
if (
search &&
Element.isElement(node) &&
Array.isArray(node.children) &&
node.children.every(Text.isText)
) {
@@ -92,19 +103,27 @@ const SearchHighlightingExample = () => {
/>
</div>
</Toolbar>
<Editable decorate={decorate} renderLeaf={props => <Leaf {...props} />} />
<Editable
decorate={decorate}
renderLeaf={(props: RenderLeafProps) => <Leaf {...props} />}
/>
</Slate>
)
}
const Leaf = ({ attributes, children, leaf }) => {
interface HighlightLeaf extends CustomText {
highlight?: boolean
}
const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {
const highlightLeaf = leaf as HighlightLeaf
return (
<span
{...attributes}
{...(leaf.highlight && { 'data-cy': 'search-highlighted' })}
{...(highlightLeaf.highlight && { 'data-cy': 'search-highlighted' })}
className={css`
font-weight: ${leaf.bold && 'bold'};
background-color: ${leaf.highlight && '#ffeeba'};
font-weight: ${highlightLeaf.bold && 'bold'};
background-color: ${highlightLeaf.highlight && '#ffeeba'};
`}
>
{children}

View File

@@ -1,20 +1,33 @@
import React, { useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import {
Editor,
Range,
Point,
Descendant,
createEditor,
Editor,
Point,
Range,
Element as SlateElement,
createEditor,
} from 'slate'
import { withHistory } from 'slate-history'
import {
Editable,
RenderElementProps,
RenderLeafProps,
Slate,
withReact,
} from 'slate-react'
import { CustomEditor } from './custom-types.d'
const TablesExample = () => {
const renderElement = useCallback(props => <Element {...props} />, [])
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
const renderElement = useCallback(
(props: RenderElementProps) => <Element {...props} />,
[]
)
const renderLeaf = useCallback(
(props: RenderLeafProps) => <Leaf {...props} />,
[]
)
const editor = useMemo(
() => withTables(withHistory(withReact(createEditor()))),
() => withTables(withHistory(withReact(createEditor()))) as CustomEditor,
[]
)
return (
@@ -24,10 +37,10 @@ const TablesExample = () => {
)
}
const withTables = editor => {
const withTables = (editor: CustomEditor) => {
const { deleteBackward, deleteForward, insertBreak } = editor
editor.deleteBackward = unit => {
editor.deleteBackward = (unit: 'character' | 'word' | 'line' | 'block') => {
const { selection } = editor
if (selection && Range.isCollapsed(selection)) {
@@ -97,7 +110,7 @@ const withTables = editor => {
return editor
}
const Element = ({ attributes, children, element }) => {
const Element = ({ attributes, children, element }: RenderElementProps) => {
switch (element.type) {
case 'table':
return (
@@ -114,7 +127,7 @@ const Element = ({ attributes, children, element }) => {
}
}
const Leaf = ({ attributes, children, leaf }) => {
const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => {
if (leaf.bold) {
children = <strong>{children}</strong>
}

View File

@@ -53,7 +53,7 @@ export const normalizeTokens = (
let i = 0
let stackIndex = 0
let currentLine = []
let currentLine: Token[] = []
const acc = [currentLine]
@@ -84,7 +84,7 @@ export const normalizeTokens = (
if (typeof content !== 'string') {
stackIndex++
typeArrStack.push(types)
tokenArrStack.push(content)
tokenArrStack.push(content as PrismToken[])
tokenArrIndexStack.push(0)
tokenArrSizeStack.push(content.length)
continue

View File

@@ -33,7 +33,9 @@ import CustomPlaceholder from '../../examples/ts/custom-placeholder'
// node
import { getAllExamples } from '../api'
const EXAMPLES = [
type ExampleTuple = [string, React.ComponentType, string]
const EXAMPLES: ExampleTuple[] = [
['Checklists', CheckLists, 'check-lists'],
['Editable Voids', EditableVoids, 'editable-voids'],
['Embeds', Embeds, 'embeds'],
@@ -58,7 +60,7 @@ const EXAMPLES = [
['Custom placeholder', CustomPlaceholder, 'custom-placeholder'],
]
const Header = props => (
const Header = (props: React.HTMLAttributes<HTMLDivElement>) => (
<div
{...props}
className={css`
@@ -73,7 +75,7 @@ const Header = props => (
/>
)
const Title = props => (
const Title = (props: React.HTMLAttributes<HTMLSpanElement>) => (
<span
{...props}
className={css`
@@ -82,7 +84,7 @@ const Title = props => (
/>
)
const LinkList = props => (
const LinkList = (props: React.HTMLAttributes<HTMLDivElement>) => (
<div
{...props}
className={css`
@@ -92,7 +94,7 @@ const LinkList = props => (
/>
)
const A = props => (
const A = (props: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
<a
{...props}
className={css`
@@ -108,7 +110,7 @@ const A = props => (
/>
)
const Pill = props => (
const Pill = (props: React.HTMLAttributes<HTMLSpanElement>) => (
<span
{...props}
className={css`
@@ -120,7 +122,10 @@ const Pill = props => (
/>
)
const TabList = ({ isVisible, ...props }) => (
const TabList = ({
isVisible,
...props
}: React.HTMLAttributes<HTMLDivElement> & { isVisible?: boolean }) => (
<div
{...props}
className={css`
@@ -139,7 +144,10 @@ const TabList = ({ isVisible, ...props }) => (
/>
)
const TabListUnderlay = ({ isVisible, ...props }) => (
const TabListUnderlay = ({
isVisible,
...props
}: React.HTMLAttributes<HTMLDivElement> & { isVisible?: boolean }) => (
<div
{...props}
className={css`
@@ -153,7 +161,7 @@ const TabListUnderlay = ({ isVisible, ...props }) => (
/>
)
const TabButton = props => (
const TabButton = (props: React.HTMLAttributes<HTMLSpanElement>) => (
<span
{...props}
className={css`
@@ -182,7 +190,7 @@ const Tab = React.forwardRef(
href: string
[key: string]: unknown
}>,
ref: Ref<HTMLAnchorElement | null>
ref: Ref<HTMLAnchorElement>
) => (
<a
ref={ref}
@@ -205,7 +213,10 @@ const Tab = React.forwardRef(
)
)
const Wrapper = ({ className, ...props }) => (
const Wrapper = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
{...props}
className={cx(
@@ -219,7 +230,7 @@ const Wrapper = ({ className, ...props }) => (
/>
)
const ExampleHeader = props => (
const ExampleHeader = (props: React.HTMLAttributes<HTMLDivElement>) => (
<div
{...props}
className={css`
@@ -234,7 +245,7 @@ const ExampleHeader = props => (
/>
)
const ExampleTitle = props => (
const ExampleTitle = (props: React.HTMLAttributes<HTMLSpanElement>) => (
<span
{...props}
className={css`
@@ -243,7 +254,7 @@ const ExampleTitle = props => (
/>
)
const ExampleContent = props => (
const ExampleContent = (props: React.HTMLAttributes<HTMLDivElement>) => (
<Wrapper
{...props}
className={css`
@@ -252,7 +263,7 @@ const ExampleContent = props => (
/>
)
const Warning = props => (
const Warning = (props: React.HTMLAttributes<HTMLDivElement>) => (
<Wrapper
{...props}
className={css`
@@ -269,11 +280,11 @@ const Warning = props => (
)
const ExamplePage = ({ example }: { example: string }) => {
const [error, setError] = useState<Error | undefined>()
const [stacktrace, setStacktrace] = useState<ErrorInfo | undefined>()
const [showTabs, setShowTabs] = useState<boolean>()
const [error, setError] = useState<Error | undefined>(undefined)
const [stacktrace, setStacktrace] = useState<ErrorInfo | undefined>(undefined)
const [showTabs, setShowTabs] = useState<boolean>(false)
const EXAMPLE = EXAMPLES.find(e => e[2] === example)
const [name, Component, path] = EXAMPLE
const [name, Component, path] = EXAMPLE!
return (
<ErrorBoundary
onError={(error, stacktrace) => {

View File

@@ -4,7 +4,7 @@
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"strict": true,
"downlevelIteration": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,

View File

@@ -3357,6 +3357,13 @@ __metadata:
languageName: node
linkType: hard
"@types/is-hotkey@npm:^0.1.10":
version: 0.1.10
resolution: "@types/is-hotkey@npm:0.1.10"
checksum: 9ecc49fb3822b3cfa8335132d54c6e577d0b14bb52d0bf1f817cdd19c442555b7523945e2ae72f6098e3c7f64b4777390f38afec3e4660343cfb471377e7fd82
languageName: node
linkType: hard
"@types/is-hotkey@npm:^0.1.8":
version: 0.1.8
resolution: "@types/is-hotkey@npm:0.1.8"
@@ -3364,6 +3371,13 @@ __metadata:
languageName: node
linkType: hard
"@types/is-url@npm:^1.2.32":
version: 1.2.32
resolution: "@types/is-url@npm:1.2.32"
checksum: f76697c868680b3be88d7f18f9724a334c62a8dc1b0f40fad8dc725b2072ad74f38d50b4ce902c07a65bb081ae2782baf06f5b3334c64bd2679c35e0a12042c5
languageName: node
linkType: hard
"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1":
version: 2.0.3
resolution: "@types/istanbul-lib-coverage@npm:2.0.3"
@@ -13333,6 +13347,8 @@ __metadata:
"@emotion/css": "npm:^11.11.2"
"@faker-js/faker": "npm:^8.2.0"
"@playwright/test": "npm:^1.39.0"
"@types/is-hotkey": "npm:^0.1.10"
"@types/is-url": "npm:^1.2.32"
"@types/jest": "npm:29.5.6"
"@types/lodash": "npm:^4.14.200"
"@types/mocha": "npm:^10.0.3"