1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-28 17:39:57 +02:00

remove marks, in favor of text properties (#3235)

* remove marks, in favor of text properties

* fix lint

* fix more examples

* update docs
This commit is contained in:
Ian Storm Taylor
2019-12-05 11:21:15 -05:00
committed by GitHub
parent 31df397930
commit 4c03b497d9
205 changed files with 792 additions and 4208 deletions

View File

@@ -6,11 +6,7 @@ import TextComponent from './text'
import { ReactEditor } from '..'
import { useEditor } from '../hooks/use-editor'
import { NODE_TO_INDEX, NODE_TO_PARENT } from '../utils/weak-maps'
import {
RenderDecorationProps,
RenderElementProps,
RenderMarkProps,
} from './editable'
import { RenderElementProps, RenderLeafProps } from './editable'
/**
* Children.
@@ -20,18 +16,16 @@ const Children = (props: {
decorate: (entry: NodeEntry) => Range[]
decorations: Range[]
node: Ancestor
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
renderElement?: (props: RenderElementProps) => JSX.Element
renderMark?: (props: RenderMarkProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
selection: Range | null
}) => {
const {
decorate,
decorations,
node,
renderDecoration,
renderElement,
renderMark,
renderLeaf,
selection,
} = props
const editor = useEditor()
@@ -65,9 +59,8 @@ const Children = (props: {
decorations={ds}
element={n}
key={key.id}
renderDecoration={renderDecoration}
renderElement={renderElement}
renderMark={renderMark}
renderLeaf={renderLeaf}
selection={sel}
/>
)
@@ -78,8 +71,7 @@ const Children = (props: {
key={key.id}
isLast={isLeafBlock && i === node.children.length}
parent={node}
renderDecoration={renderDecoration}
renderMark={renderMark}
renderLeaf={renderLeaf}
text={n}
/>
)

View File

@@ -5,7 +5,7 @@ import React, {
useMemo,
useCallback,
} from 'react'
import { Editor, Element, NodeEntry, Node, Range, Text, Mark } from 'slate'
import { Editor, Element, NodeEntry, Node, Range, Text } from 'slate'
import debounce from 'debounce'
import scrollIntoView from 'scroll-into-view-if-needed'
@@ -15,7 +15,6 @@ import { IS_FIREFOX, IS_SAFARI } from '../utils/environment'
import { ReactEditor } from '..'
import { ReadOnlyContext } from '../hooks/use-read-only'
import { useSlate } from '../hooks/use-slate'
import { Leaf } from '../utils/leaf'
import {
DOMElement,
DOMNode,
@@ -34,20 +33,6 @@ import {
PLACEHOLDER_SYMBOL,
} from '../utils/weak-maps'
/**
* `RenderDecorationProps` are passed to the `renderDecoration` handler.
*/
export interface RenderDecorationProps {
children: any
decoration: Range
leaf: Leaf
text: Text
attributes: {
'data-slate-decoration': true
}
}
/**
* `RenderElementProps` are passed to the `renderElement` handler.
*/
@@ -56,8 +41,8 @@ export interface RenderElementProps {
children: any
element: Element
attributes: {
'data-slate-inline'?: true
'data-slate-node': 'element'
'data-slate-inline'?: true
'data-slate-void'?: true
dir?: 'rtl'
ref: any
@@ -65,16 +50,15 @@ export interface RenderElementProps {
}
/**
* `RenderMarkProps` are passed to the `renderMark` handler.
* `RenderLeafProps` are passed to the `renderLeaf` handler.
*/
export interface RenderMarkProps {
export interface RenderLeafProps {
children: any
mark: Mark
leaf: Leaf
leaf: Text
text: Text
attributes: {
'data-slate-mark': true
'data-slate-leaf': true
}
}
@@ -90,21 +74,19 @@ export const Editable = (
readOnly?: boolean
role?: string
style?: React.CSSProperties
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
renderElement?: (props: RenderElementProps) => JSX.Element
renderMark?: (props: RenderMarkProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
} & React.TextareaHTMLAttributes<HTMLDivElement>
) => {
const {
autoFocus,
decorate = defaultDecorate,
onDOMBeforeInput: propsOnDOMBeforeInput,
placeholder,
readOnly = false,
renderDecoration,
renderElement,
renderMark,
autoFocus,
renderLeaf,
style = {},
onDOMBeforeInput: propsOnDOMBeforeInput,
...attributes
} = props
const editor = useSlate()
@@ -906,9 +888,8 @@ export const Editable = (
decorate={decorate}
decorations={decorations}
node={editor}
renderDecoration={renderDecoration}
renderElement={renderElement}
renderMark={renderMark}
renderLeaf={renderLeaf}
selection={editor.selection}
/>
</div>

View File

@@ -13,12 +13,7 @@ import {
NODE_TO_INDEX,
KEY_TO_ELEMENT,
} from '../utils/weak-maps'
import {
RenderDecorationProps,
RenderElementProps,
RenderMarkProps,
} from './editable'
import { isRangeListEqual } from '../utils/leaf'
import { RenderElementProps, RenderLeafProps } from './editable'
/**
* Element.
@@ -28,18 +23,16 @@ const Element = (props: {
decorate: (entry: NodeEntry) => Range[]
decorations: Range[]
element: SlateElement
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
renderElement?: (props: RenderElementProps) => JSX.Element
renderMark?: (props: RenderMarkProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
selection: Range | null
}) => {
const {
decorate,
decorations,
element,
renderDecoration,
renderElement = (p: RenderElementProps) => <DefaultElement {...p} />,
renderMark,
renderLeaf,
selection,
} = props
const ref = useRef<HTMLElement>(null)
@@ -53,9 +46,8 @@ const Element = (props: {
decorate={decorate}
decorations={decorations}
node={element}
renderDecoration={renderDecoration}
renderElement={renderElement}
renderMark={renderMark}
renderLeaf={renderLeaf}
selection={selection}
/>
)
@@ -141,9 +133,8 @@ const MemoizedElement = React.memo(Element, (prev, next) => {
return (
prev.decorate === next.decorate &&
prev.element === next.element &&
prev.renderDecoration === next.renderDecoration &&
prev.renderElement === next.renderElement &&
prev.renderMark === next.renderMark &&
prev.renderLeaf === next.renderLeaf &&
isRangeListEqual(prev.decorations, next.decorations) &&
(prev.selection === next.selection ||
(!!prev.selection &&
@@ -167,4 +158,29 @@ export const DefaultElement = (props: RenderElementProps) => {
)
}
/**
* Check if a list of ranges is equal to another.
*
* PERF: this requires the two lists to also have the ranges inside them in the
* same order, but this is an okay constraint for us since decorations are
* kept in order, and the odd case where they aren't is okay to re-render for.
*/
const isRangeListEqual = (list: Range[], another: Range[]): boolean => {
if (list.length !== another.length) {
return false
}
for (let i = 0; i < list.length; i++) {
const range = list[i]
const other = another[i]
if (!Range.equals(range, other)) {
return false
}
}
return true
}
export default MemoizedElement

View File

@@ -2,9 +2,8 @@ import React from 'react'
import { Text, Element } from 'slate'
import String from './string'
import { Leaf as SlateLeaf } from '../utils/leaf'
import { PLACEHOLDER_SYMBOL } from '../utils/weak-maps'
import { RenderDecorationProps, RenderMarkProps } from './editable'
import { RenderLeafProps } from './editable'
/**
* Individual leaves in a text node with unique formatting.
@@ -12,10 +11,9 @@ import { RenderDecorationProps, RenderMarkProps } from './editable'
const Leaf = (props: {
isLast: boolean
leaf: SlateLeaf
leaf: Text
parent: Element
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
renderMark?: (props: RenderMarkProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
text: Text
}) => {
const {
@@ -23,117 +21,64 @@ const Leaf = (props: {
isLast,
text,
parent,
renderDecoration = (props: RenderDecorationProps) => (
<DefaultDecoration {...props} />
),
renderMark = (props: RenderMarkProps) => <DefaultMark {...props} />,
renderLeaf = (props: RenderLeafProps) => <DefaultLeaf {...props} />,
} = props
let children = (
<String isLast={isLast} leaf={leaf} parent={parent} text={text} />
)
if (leaf[PLACEHOLDER_SYMBOL]) {
children = (
<React.Fragment>
<span
contentEditable={false}
style={{
pointerEvents: 'none',
display: 'inline-block',
verticalAlign: 'text-top',
width: '0',
maxWidth: '100%',
whiteSpace: 'nowrap',
opacity: '0.333',
}}
>
{leaf.placeholder}
</span>
{children}
</React.Fragment>
)
}
// COMPAT: Having the `data-` attributes on these leaf elements ensures that
// in certain misbehaving browsers they aren't weirdly cloned/destroyed by
// contenteditable behaviors. (2019/05/08)
for (const mark of leaf.marks) {
const ret = renderMark({
children,
leaf,
mark,
text,
attributes: {
'data-slate-mark': true,
},
})
if (ret) {
children = ret
}
const attributes: {
'data-slate-leaf': true
} = {
'data-slate-leaf': true,
}
for (const decoration of leaf.decorations) {
const p = {
children,
decoration,
leaf,
text,
attributes: {
'data-slate-decoration': true,
},
}
if (PLACEHOLDER_SYMBOL in decoration) {
// @ts-ignore
children = <PlaceholderDecoration {...p} />
} else {
// @ts-ignore
const ret = renderDecoration(p)
if (ret) {
children = ret
}
}
}
return <span data-slate-leaf>{children}</span>
return renderLeaf({ attributes, children, leaf, text })
}
const MemoizedLeaf = React.memo(Leaf, (prev, next) => {
return (
next.parent === prev.parent &&
next.isLast === prev.isLast &&
next.renderDecoration === prev.renderDecoration &&
next.renderMark === prev.renderMark &&
next.renderLeaf === prev.renderLeaf &&
next.text === prev.text &&
SlateLeaf.equals(next.leaf, prev.leaf)
Text.matches(next.leaf, prev.leaf)
)
})
/**
* The default custom decoration renderer.
* The default custom leaf renderer.
*/
export const DefaultDecoration = (props: RenderDecorationProps) => {
export const DefaultLeaf = (props: RenderLeafProps) => {
const { attributes, children } = props
return <span {...attributes}>{children}</span>
}
/**
* The default custom mark renderer.
*/
export const DefaultMark = (props: RenderMarkProps) => {
const { attributes, children } = props
return <span {...attributes}>{children}</span>
}
/**
* A custom decoration for the default placeholder behavior.
*/
const PlaceholderDecoration = (props: RenderDecorationProps) => {
const { decoration, attributes, children } = props
const { placeholder } = decoration
return (
<span {...attributes}>
<span
contentEditable={false}
style={{
pointerEvents: 'none',
display: 'inline-block',
verticalAlign: 'text-top',
width: '0',
maxWidth: '100%',
whiteSpace: 'nowrap',
opacity: '0.333',
}}
>
{placeholder}
</span>
{children}
</span>
)
}
export default MemoizedLeaf

View File

@@ -2,7 +2,6 @@ import React from 'react'
import { Editor, Text, Path, Element, Node } from 'slate'
import { ReactEditor, useEditor } from '..'
import { Leaf } from '../utils/leaf'
/**
* Leaf content strings.
@@ -10,7 +9,7 @@ import { Leaf } from '../utils/leaf'
const String = (props: {
isLast: boolean
leaf: Leaf
leaf: Text
parent: Element
text: Text
}) => {

View File

@@ -2,9 +2,8 @@ import React, { useLayoutEffect, useRef } from 'react'
import { Range, Element, Text as SlateText } from 'slate'
import Leaf from './leaf'
import { Leaf as SlateLeaf } from '../utils/leaf'
import { ReactEditor, useEditor } from '..'
import { RenderDecorationProps, RenderMarkProps } from './editable'
import { RenderLeafProps } from './editable'
import {
KEY_TO_ELEMENT,
NODE_TO_ELEMENT,
@@ -19,18 +18,10 @@ const Text = (props: {
decorations: Range[]
isLast: boolean
parent: Element
renderDecoration?: (props: RenderDecorationProps) => JSX.Element
renderMark?: (props: RenderMarkProps) => JSX.Element
renderLeaf?: (props: RenderLeafProps) => JSX.Element
text: SlateText
}) => {
const {
decorations,
isLast,
parent,
renderDecoration,
renderMark,
text,
} = props
const { decorations, isLast, parent, renderLeaf, text } = props
const editor = useEditor()
const ref = useRef<HTMLSpanElement>(null)
const leaves = getLeaves(text, decorations)
@@ -47,8 +38,7 @@ const Text = (props: {
leaf={leaf}
text={text}
parent={parent}
renderDecoration={renderDecoration}
renderMark={renderMark}
renderLeaf={renderLeaf}
/>
)
}
@@ -76,12 +66,12 @@ const Text = (props: {
* Get the leaves for a text node given decorations.
*/
const getLeaves = (node: SlateText, decorations: Range[]): SlateLeaf[] => {
const { text, marks } = node
let leaves: SlateLeaf[] = [{ text, marks, decorations: [] }]
const getLeaves = (node: SlateText, decorations: Range[]): SlateText[] => {
let leaves: SlateText[] = [{ ...node }]
const compile = (range: Range, key?: string) => {
const [start, end] = Range.edges(range)
for (const dec of decorations) {
const { anchor, focus, ...rest } = dec
const [start, end] = Range.edges(dec)
const next = []
let o = 0
@@ -92,7 +82,7 @@ const getLeaves = (node: SlateText, decorations: Range[]): SlateLeaf[] => {
// If the range encompases the entire leaf, add the range.
if (start.offset <= offset && end.offset >= offset + length) {
leaf.decorations.push(range)
Object.assign(leaf, rest)
next.push(leaf)
continue
}
@@ -115,14 +105,18 @@ const getLeaves = (node: SlateText, decorations: Range[]): SlateLeaf[] => {
let after
if (end.offset < offset + length) {
;[middle, after] = SlateLeaf.split(middle, end.offset - offset)
const off = end.offset - offset
after = { ...middle, text: middle.text.slice(off) }
middle = { ...middle, text: middle.text.slice(0, off) }
}
if (start.offset > offset) {
;[before, middle] = SlateLeaf.split(middle, start.offset - offset)
const off = start.offset - offset
before = { ...middle, text: middle.text.slice(0, off) }
middle = { ...middle, text: middle.text.slice(off) }
}
middle.decorations.push(range)
Object.assign(middle, rest)
if (before) {
next.push(before)
@@ -138,28 +132,16 @@ const getLeaves = (node: SlateText, decorations: Range[]): SlateLeaf[] => {
leaves = next
}
for (const range of decorations) {
compile(range)
}
return leaves
}
const MemoizedText = React.memo(Text, (prev, next) => {
if (
return (
next.parent === prev.parent &&
next.isLast === prev.isLast &&
next.renderDecoration === prev.renderDecoration &&
next.renderMark === prev.renderMark &&
next.renderLeaf === prev.renderLeaf &&
next.text === prev.text
) {
return SlateLeaf.equals(
{ ...next.text, decorations: next.decorations },
{ ...prev.text, decorations: prev.decorations }
)
}
return false
)
})
export default MemoizedText

View File

@@ -1,6 +1,6 @@
export * from './components/editable'
export { DefaultElement } from './components/element'
export { DefaultMark, DefaultDecoration } from './components/leaf'
export { DefaultLeaf } from './components/leaf'
export * from './hooks/use-editor'
export * from './hooks/use-focused'
export * from './hooks/use-read-only'

View File

@@ -1,113 +0,0 @@
import isPlainObject from 'is-plain-object'
import { Range, Mark } from 'slate'
/**
* The `Leaf` interface represents the individual leaves inside a text node,
* once decorations have been applied.
*/
interface Leaf {
decorations: Range[]
marks: Mark[]
text: string
}
namespace Leaf {
/**
* Check if two leaves are equal.
*/
export const equals = (leaf: Leaf, another: Leaf): boolean => {
return (
leaf.text === another.text &&
leaf.decorations.length === another.decorations.length &&
leaf.marks.length === another.marks.length &&
leaf.marks.every(m => Mark.exists(m, another.marks)) &&
another.marks.every(m => Mark.exists(m, leaf.marks)) &&
isRangeListEqual(leaf.decorations, another.decorations)
)
}
/**
* Check if a value is a `Leaf` object.
*/
export const isLeaf = (value: any): value is Leaf => {
return (
isPlainObject(value) &&
typeof value.text === 'string' &&
Mark.isMarkSet(value.marks) &&
Range.isRangeList(value.decorations)
)
}
/**
* Split a leaf into two at an offset.
*/
export const split = (leaf: Leaf, offset: number): [Leaf, Leaf] => {
return [
{
text: leaf.text.slice(0, offset),
marks: leaf.marks,
decorations: [...leaf.decorations],
},
{
text: leaf.text.slice(offset),
marks: leaf.marks,
decorations: [...leaf.decorations],
},
]
}
}
/**
* Check if a list of ranges is equal to another.
*
* PERF: this requires the two lists to also have the ranges inside them in the
* same order, but this is an okay constraint for us since decorations are
* kept in order, and the odd case where they aren't is okay to re-render for.
*/
const isRangeListEqual = (list: Range[], another: Range[]): boolean => {
if (list.length !== another.length) {
return false
}
for (let i = 0; i < list.length; i++) {
const range = list[i]
const other = another[i]
if (!Range.equals(range, other)) {
return false
}
}
return true
}
/**
* Check if a map of ranges is equal to another.
*/
const isRangeMapEqual = (
map: Record<string, Range>,
another: Record<string, Range>
): boolean => {
if (Object.keys(map).length !== Object.keys(another).length) {
return false
}
for (const key in map) {
const range = map[key]
const other = another[key]
if (!Range.equals(range, other)) {
return false
}
}
return true
}
export { Leaf, isRangeListEqual, isRangeMapEqual }

View File

@@ -1,4 +1,4 @@
import { Node, Ancestor, Editor } from 'slate'
import { Node, Ancestor, Editor, Text } from 'slate'
import { Key } from './key'
@@ -16,10 +16,11 @@ export const NODE_TO_PARENT: WeakMap<Node, Ancestor> = new WeakMap()
*/
export const EDITOR_TO_ELEMENT: WeakMap<Editor, HTMLElement> = new WeakMap()
export const NODE_TO_ELEMENT: WeakMap<Node, HTMLElement> = new WeakMap()
export const EDITOR_TO_PLACEHOLDER: WeakMap<Editor, string> = new WeakMap()
export const ELEMENT_TO_NODE: WeakMap<HTMLElement, Node> = new WeakMap()
export const NODE_TO_KEY: WeakMap<Node, Key> = new WeakMap()
export const KEY_TO_ELEMENT: WeakMap<Key, HTMLElement> = new WeakMap()
export const NODE_TO_ELEMENT: WeakMap<Node, HTMLElement> = new WeakMap()
export const NODE_TO_KEY: WeakMap<Node, Key> = new WeakMap()
/**
* Weak maps for storing editor-related state.
@@ -30,8 +31,4 @@ export const IS_FOCUSED: WeakMap<Editor, boolean> = new WeakMap()
export const IS_DRAGGING: WeakMap<Editor, boolean> = new WeakMap()
export const IS_CLICKING: WeakMap<Editor, boolean> = new WeakMap()
/**
* Symbols.
*/
export const PLACEHOLDER_SYMBOL = Symbol('placeholder')
export const PLACEHOLDER_SYMBOL = (Symbol('placeholder') as unknown) as string

View File

@@ -16,11 +16,8 @@ export const withReact = (editor: Editor): Editor => {
const matches: [Path, Key][] = []
switch (op.type) {
case 'add_mark':
case 'insert_text':
case 'remove_mark':
case 'remove_text':
case 'set_mark':
case 'set_node': {
for (const [node, path] of Editor.levels(editor, { at: op.path })) {
const key = ReactEditor.findKey(editor, node)