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:
@@ -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
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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}
|
||||
|
@@ -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 []
|
||||
|
@@ -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}
|
||||
|
@@ -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 = [
|
||||
{
|
||||
|
@@ -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(
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
)
|
||||
|
@@ -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>
|
||||
|
@@ -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}>
|
||||
|
@@ -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
|
||||
|
@@ -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}
|
||||
>
|
||||
|
@@ -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} />, [])
|
||||
|
@@ -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)
|
||||
|
@@ -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 {
|
||||
|
@@ -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',
|
||||
|
@@ -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}
|
||||
|
@@ -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} />, [])
|
||||
|
@@ -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,
|
||||
|
@@ -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(
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
|
65
site/examples/ts/custom-types.d.ts
vendored
65
site/examples/ts/custom-types.d.ts
vendored
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
}}
|
||||
|
@@ -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)
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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}>
|
||||
|
@@ -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[] = [
|
||||
|
@@ -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)
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
|
@@ -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 = {
|
||||
|
@@ -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>
|
||||
}
|
||||
|
@@ -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',
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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) => {
|
||||
|
@@ -4,7 +4,7 @@
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
|
16
yarn.lock
16
yarn.lock
@@ -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"
|
||||
|
Reference in New Issue
Block a user