1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-19 21:51:51 +02:00

Introduce annotations (#2747)

* first stab at removing leaves with tests passing

* fixes

* add iterables to the element interface

* use iterables in more places

* update examples to use iterables

* update naming

* fix tests

* convert more key-based logic to paths

* add range support to iterables

* refactor many methods to use iterables, deprecate cruft

* clean up existing iterables

* more cleanup

* more cleaning

* fix word count example

* work

* split decoration and annotations

* update examples for `renderNode` useage

* deprecate old DOM-based helpers, update examples

* make formats first class, refactor leaf rendering

* fix examples, fix isAtomic checking

* deprecate leaf model

* convert Text and Leaf to functional components

* fix lint and tests
This commit is contained in:
Ian Storm Taylor
2019-05-08 20:26:08 -07:00
committed by GitHub
parent 5b8a6bb3b4
commit a5a25f97dd
202 changed files with 5009 additions and 4424 deletions

View File

@@ -1,5 +1,5 @@
import React from 'react'
import styled from 'react-emotion'
import { cx, css } from 'emotion'
import {
HashRouter,
Link as RouterLink,
@@ -72,126 +72,193 @@ const EXAMPLES = [
]
/**
* Some styled components.
* Some components.
*
* @type {Component}
*/
const Header = styled('div')`
align-items: center;
background: #000;
color: #aaa;
display: flex;
height: 42px;
position: relative;
z-index: 1; /* To appear above the underlay */
`
const Header = props => (
<div
{...props}
className={css`
align-items: center;
background: #000;
color: #aaa;
display: flex;
height: 42px;
position: relative;
z-index: 1; /* To appear above the underlay */
`}
/>
)
const Title = styled('span')`
margin-left: 1em;
`
const Title = props => (
<span
{...props}
className={css`
margin-left: 1em;
`}
/>
)
const LinkList = styled('div')`
margin-left: auto;
margin-right: 1em;
`
const LinkList = props => (
<div
{...props}
className={css`
margin-left: auto;
margin-right: 1em;
`}
/>
)
const Link = styled('a')`
margin-left: 1em;
color: #aaa;
text-decoration: none;
const Link = props => (
<a
{...props}
className={css`
margin-left: 1em;
color: #aaa;
text-decoration: none;
&:hover {
color: #fff;
text-decoration: underline;
}
`
&:hover {
color: #fff;
text-decoration: underline;
}
`}
/>
)
const TabList = styled('div')`
background-color: #222;
display: flex;
flex-direction: column;
overflow: hidden;
padding-top: 0.2em;
position: absolute;
transition: width 0.2s;
width: ${props => (props.isVisible ? '200px' : '0')};
white-space: nowrap;
z-index: 1; /* To appear above the underlay */
`
const TabList = ({ isVisible, ...props }) => (
<div
{...props}
className={css`
background-color: #222;
display: flex;
flex-direction: column;
overflow: auto;
padding-top: 0.2em;
position: absolute;
transition: width 0.2s;
width: ${isVisible ? '200px' : '0'};
white-space: nowrap;
max-height: 70vh;
z-index: 1; /* To appear above the underlay */
`}
/>
)
const TabListUnderlay = styled('div')`
background-color: rgba(200, 200, 200, 0.8);
display: ${props => (props.isVisible ? 'block' : 'none')};
height: 100%;
top: 0;
position: fixed;
width: 100%;
`
const TabListUnderlay = ({ isVisible, ...props }) => (
<div
{...props}
className={css`
background-color: rgba(200, 200, 200, 0.8);
display: ${isVisible ? 'block' : 'none'};
height: 100%;
top: 0;
position: fixed;
width: 100%;
`}
/>
)
const TabButton = styled('span')`
margin-left: 0.8em;
const TabButton = props => (
<span
{...props}
className={css`
margin-left: 0.8em;
&:hover {
cursor: pointer;
}
&:hover {
cursor: pointer;
}
.material-icons {
color: #aaa;
font-size: 24px;
}
`
.material-icons {
color: #aaa;
font-size: 24px;
}
`}
/>
)
const MaskedRouterLink = ({ active, ...props }) => <RouterLink {...props} />
const Tab = ({ active, ...props }) => (
<RouterLink
{...props}
className={css`
display: inline-block;
margin-bottom: 0.2em;
padding: 0.2em 1em;
border-radius: 0.2em;
text-decoration: none;
color: ${active ? 'white' : '#777'};
background: ${active ? '#333' : 'transparent'};
const Tab = styled(MaskedRouterLink)`
display: inline-block;
margin-bottom: 0.2em;
padding: 0.2em 1em;
border-radius: 0.2em;
text-decoration: none;
color: ${p => (p.active ? 'white' : '#777')};
background: ${p => (p.active ? '#333' : 'transparent')};
&:hover {
background: #333;
}
`}
/>
)
&:hover {
background: #333;
}
`
const Wrapper = ({ className, ...props }) => (
<div
{...props}
className={cx(
className,
css`
max-width: 42em;
margin: 20px auto;
padding: 20px;
`
)}
/>
)
const Wrapper = styled('div')`
max-width: 42em;
margin: 20px auto;
padding: 20px;
`
const ExampleHeader = props => (
<div
{...props}
className={css`
align-items: center;
background-color: #555;
color: #ddd;
display: flex;
height: 42px;
position: relative;
z-index: 1; /* To appear above the underlay */
`}
/>
)
const ExampleHeader = styled('div')`
align-items: center;
background-color: #555;
color: #ddd;
display: flex;
height: 42px;
position: relative;
z-index: 1; /* To appear above the underlay */
`
const ExampleTitle = props => (
<span
{...props}
className={css`
margin-left: 1em;
`}
/>
)
const ExampleTitle = styled('span')`
margin-left: 1em;
`
const ExampleContent = props => (
<Wrapper
{...props}
className={css`
background: #fff;
`}
/>
)
const ExampleContent = styled(Wrapper)`
background: #fff;
`
const Warning = props => (
<Wrapper
{...props}
className={css`
background: #fffae0;
const Warning = styled(Wrapper)`
background: #fffae0;
& > pre {
background: #fbf1bd;
white-space: pre;
overflow-x: scroll;
margin-bottom: 0;
}
`
& > pre {
background: #fbf1bd;
white-space: pre;
overflow-x: scroll;
margin-bottom: 0;
}
`}
/>
)
/**
* App.

View File

@@ -1,9 +1,9 @@
import React from 'react'
import { Editor } from 'slate-react'
import { Value } from 'slate'
import { css } from 'emotion'
import React from 'react'
import initialValueAsJson from './value.json'
import styled from 'react-emotion'
/**
* Deserialize the initial editor value.
@@ -13,36 +13,6 @@ import styled from 'react-emotion'
const initialValue = Value.fromJSON(initialValueAsJson)
/**
* Create a few styling components.
*
* @type {Component}
*/
const ItemWrapper = styled('div')`
display: flex;
flex-direction: row;
align-items: center;
& + & {
margin-top: 0;
}
`
const CheckboxWrapper = styled('span')`
margin-right: 0.75em;
`
const ContentWrapper = styled('span')`
flex: 1;
opacity: ${props => (props.checked ? 0.666 : 1)};
text-decoration: ${props => (props.checked ? 'none' : 'line-through')};
&:focus {
outline: none;
}
`
/**
* Check list item.
*
@@ -73,18 +43,42 @@ class CheckListItem extends React.Component {
const { attributes, children, node, readOnly } = this.props
const checked = node.data.get('checked')
return (
<ItemWrapper {...attributes}>
<CheckboxWrapper contentEditable={false}>
<div
{...attributes}
className={css`
display: flex;
flex-direction: row;
align-items: center;
& + & {
margin-top: 0;
}
`}
>
<span
contentEditable={false}
className={css`
margin-right: 0.75em;
`}
>
<input type="checkbox" checked={checked} onChange={this.onChange} />
</CheckboxWrapper>
<ContentWrapper
checked={checked}
</span>
<span
contentEditable={!readOnly}
suppressContentEditableWarning
className={css`
flex: 1;
opacity: ${checked ? 0.666 : 1};
text-decoration: ${checked ? 'none' : 'line-through'};
&:focus {
outline: none;
}
`}
>
{children}
</ContentWrapper>
</ItemWrapper>
</span>
</div>
)
}
}
@@ -109,19 +103,19 @@ class CheckLists extends React.Component {
placeholder="Get to work..."
defaultValue={initialValue}
onKeyDown={this.onKeyDown}
renderNode={this.renderNode}
renderBlock={this.renderBlock}
/>
)
}
/**
* Render a Slate node.
* Render a Slate block.
*
* @param {Object} props
* @return {Element}
*/
renderNode = (props, editor, next) => {
renderBlock = (props, editor, next) => {
switch (props.node.type) {
case 'check-list-item':
return <CheckListItem {...props} />

View File

@@ -88,21 +88,21 @@ class CodeHighlighting extends React.Component {
placeholder="Write some code..."
defaultValue={initialValue}
onKeyDown={this.onKeyDown}
renderNode={this.renderNode}
renderMark={this.renderMark}
renderBlock={this.renderBlock}
renderDecoration={this.renderDecoration}
decorateNode={this.decorateNode}
/>
)
}
/**
* Render a Slate node.
* Render a Slate block.
*
* @param {Object} props
* @return {Element}
*/
renderNode = (props, editor, next) => {
renderBlock = (props, editor, next) => {
switch (props.node.type) {
case 'code':
return <CodeBlock {...props} />
@@ -114,16 +114,16 @@ class CodeHighlighting extends React.Component {
}
/**
* Render a Slate mark.
* Render a Slate decoration.
*
* @param {Object} props
* @return {Element}
*/
renderMark = (props, editor, next) => {
const { children, mark, attributes } = props
renderDecoration = (props, editor, next) => {
const { children, decoration, attributes } = props
switch (mark.type) {
switch (decoration.type) {
case 'comment':
return (
<span {...attributes} style={{ opacity: '0.33' }}>
@@ -184,23 +184,23 @@ class CodeHighlighting extends React.Component {
const others = next() || []
if (node.type !== 'code') return others
const { document } = editor.value
const language = node.data.get('language')
const texts = node.getTexts().toArray()
const string = texts.map(t => t.text).join('\n')
const texts = Array.from(node.texts())
const string = texts.map(([n]) => n.text).join('\n')
const grammar = Prism.languages[language]
const tokens = Prism.tokenize(string, grammar)
const decorations = []
let startText = texts.shift()
let endText = startText
let startEntry = texts.shift()
let endEntry = startEntry
let startOffset = 0
let endOffset = 0
let start = 0
for (const token of tokens) {
startText = endText
startEntry = endEntry
startOffset = endOffset
const [startText, startPath] = startEntry
const content = getContent(token)
const newlines = content.split('\n').length - 1
const length = content.length - newlines
@@ -212,17 +212,18 @@ class CodeHighlighting extends React.Component {
endOffset = startOffset + remaining
while (available < remaining && texts.length > 0) {
endText = texts.shift()
endEntry = texts.shift()
const [endText] = endEntry
remaining = length - available
available = endText.text.length
endOffset = remaining
}
if (typeof token !== 'string') {
const startPath = document.assertPath(startText.key)
const endPath = document.assertPath(endText.key)
const [endText, endPath] = endEntry
if (typeof token !== 'string') {
const dec = {
type: token.type,
anchor: {
key: startText.key,
path: startPath,
@@ -233,9 +234,6 @@ class CodeHighlighting extends React.Component {
path: endPath,
offset: endOffset,
},
mark: {
type: token.type,
},
}
decorations.push(dec)

View File

@@ -1,35 +1,71 @@
import React from 'react'
import styled from 'react-emotion'
import { cx, css } from 'emotion'
export const Button = styled('span')`
cursor: pointer;
color: ${props =>
props.reversed
? props.active ? 'white' : '#aaa'
: props.active ? 'black' : '#ccc'};
`
export const Button = React.forwardRef(
({ className, active, reversed, ...props }, ref) => (
<span
{...props}
ref={ref}
className={cx(
className,
css`
cursor: pointer;
color: ${reversed
? active ? 'white' : '#aaa'
: active ? 'black' : '#ccc'};
`
)}
/>
)
)
export const Icon = styled(({ className, ...rest }) => {
return <span className={`material-icons ${className}`} {...rest} />
})`
font-size: 18px;
vertical-align: text-bottom;
`
export const Icon = React.forwardRef(({ className, ...props }, ref) => (
<span
{...props}
ref={ref}
className={cx(
'material-icons',
className,
css`
font-size: 18px;
vertical-align: text-bottom;
`
)}
/>
))
export const Menu = styled('div')`
& > * {
display: inline-block;
}
export const Menu = React.forwardRef(({ className, ...props }, ref) => (
<div
{...props}
ref={ref}
className={cx(
className,
css`
& > * {
display: inline-block;
}
& > * + * {
margin-left: 15px;
}
`
& > * + * {
margin-left: 15px;
}
`
)}
/>
))
export const Toolbar = styled(Menu)`
position: relative;
padding: 1px 18px 17px;
margin: 0 -20px;
border-bottom: 2px solid #eee;
margin-bottom: 20px;
`
export const Toolbar = React.forwardRef(({ className, ...props }, ref) => (
<Menu
{...props}
ref={ref}
className={cx(
className,
css`
position: relative;
padding: 1px 18px 17px;
margin: 0 -20px;
border-bottom: 2px solid #eee;
margin-bottom: 20px;
`
)}
/>
))

View File

@@ -2,7 +2,7 @@ import { Editor } from 'slate-react'
import { Value } from 'slate'
import React from 'react'
import styled from 'react-emotion'
import { css } from 'emotion'
import { Link, Redirect } from 'react-router-dom'
import splitJoin from './split-join.js'
import insert from './insert.js'
@@ -20,58 +20,87 @@ import { ANDROID_API_VERSION } from 'slate-dev-environment'
const DEFAULT_NODE = 'paragraph'
/**
* Some styled components.
* Some components.
*
* @type {Component}
*/
const Instruction = styled('div')`
white-space: pre-wrap;
margin: -1em -1em 1em;
padding: 0.5em;
background: #eee;
`
const Instruction = props => (
<div
{...props}
className={css`
white-space: pre-wrap;
margin: -1em -1em 1em;
padding: 0.5em;
background: #eee;
`}
/>
)
const Tabs = styled('div')`
margin-bottom: 0.5em;
`
const Tabs = props => (
<div
{...props}
className={css`
margin-bottom: 0.5em;
`}
/>
)
const TabLink = ({ active, ...props }) => <Link {...props} />
const Tab = ({ active, ...props }) => (
<Link
{...props}
className={css`
display: inline-block;
text-decoration: none;
color: black;
background: ${active ? '#AAA' : '#DDD'};
padding: 0.25em 0.5em;
border-radius: 0.25em;
margin-right: 0.25em;
`}
/>
)
const Tab = styled(TabLink)`
display: inline-block;
text-decoration: none;
color: black;
background: ${p => (p.active ? '#AAA' : '#DDD')};
padding: 0.25em 0.5em;
border-radius: 0.25em;
margin-right: 0.25em;
`
const Version = props => (
<div
{...props}
className={css`
float: right;
padding: 0.5em;
font-size: 75%;
color: #808080;
`}
/>
)
const Version = styled('div')`
float: right;
padding: 0.5em;
font-size: 75%;
color: #808080;
`
const EditorText = props => (
<div
{...props}
className={css`
color: #808080;
background: #f0f0f0;
font: 12px monospace;
white-space: pre-wrap;
margin: 1em -1em;
padding: 0.5em;
const EditorText = styled('div')`
color: #808080;
background: #f0f0f0;
font: 12px monospace;
white-space: pre-wrap;
margin: 1em -1em;
padding: 0.5em;
div {
margin: 0 0 0.5em;
}
`
div {
margin: 0 0 0.5em;
}
`}
/>
)
const EditorTextCaption = styled('div')`
color: white;
background: #808080;
padding: 0.5em;
`
const EditorTextCaption = props => (
<div
{...props}
className={css`
color: white;
background: #808080;
padding: 0.5em;
`}
/>
)
/**
* Extract lines of text from `Value`
@@ -225,7 +254,7 @@ class RichTextExample extends React.Component {
value={this.state.value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
renderNode={this.renderNode}
renderBlock={this.renderBlock}
renderMark={this.renderMark}
/>
<EditorText>
@@ -290,13 +319,13 @@ class RichTextExample extends React.Component {
}
/**
* Render a Slate node.
* Render a Slate block.
*
* @param {Object} props
* @return {Element}
*/
renderNode = (props, editor, next) => {
renderBlock = (props, editor, next) => {
const { attributes, children, node } = props
switch (node.type) {

View File

@@ -46,20 +46,20 @@ class Embeds extends React.Component {
placeholder="Enter some text..."
defaultValue={initialValue}
schema={this.schema}
renderNode={this.renderNode}
renderBlock={this.renderBlock}
/>
)
}
/**
* Render a Slate node.
* Render a Slate block.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
*/
renderNode = (props, editor, next) => {
renderBlock = (props, editor, next) => {
switch (props.node.type) {
case 'video':
return <Video {...props} />

View File

@@ -3,7 +3,7 @@ import { Value } from 'slate'
import React from 'react'
import initialValueAsJson from './value.json'
import styled from 'react-emotion'
import { css } from 'emotion'
import { Button, Icon, Toolbar } from '../components'
/**
@@ -14,16 +14,6 @@ import { Button, Icon, Toolbar } from '../components'
const initialValue = Value.fromJSON(initialValueAsJson)
/**
* A styled emoji inline component.
*
* @type {Component}
*/
const Emoji = styled('span')`
outline: ${props => (props.selected ? '2px solid blue' : 'none')};
`
/**
* Emojis.
*
@@ -50,14 +40,6 @@ const EMOJIS = [
'🔑',
]
/**
* No ops.
*
* @type {Function}
*/
const noop = e => e.preventDefault()
/**
* The links example.
*
@@ -110,14 +92,15 @@ class Emojis extends React.Component {
ref={this.ref}
defaultValue={initialValue}
schema={this.schema}
renderNode={this.renderNode}
renderBlock={this.renderBlock}
renderInline={this.renderInline}
/>
</div>
)
}
/**
* Render a Slate node.
* Render a Slate block.
*
* @param {Object} props
* @param {Editor} editor
@@ -125,31 +108,45 @@ class Emojis extends React.Component {
* @return {Element}
*/
renderNode = (props, editor, next) => {
const { attributes, children, node, isFocused } = props
renderBlock = (props, editor, next) => {
const { attributes, children, node } = props
switch (node.type) {
case 'paragraph': {
case 'paragraph':
return <p {...attributes}>{children}</p>
}
case 'emoji': {
const code = node.data.get('code')
return (
<Emoji
{...props.attributes}
selected={isFocused}
contentEditable={false}
onDrop={noop}
>
{code}
</Emoji>
)
}
default: {
default:
return next()
}
}
/**
* Render a Slate inline.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Element}
*/
renderInline = (props, editor, next) => {
const { attributes, node, isFocused } = props
switch (node.type) {
case 'emoji':
return (
<span
{...attributes}
contentEditable={false}
onDrop={e => e.preventDefault()}
className={css`
outline: ${isFocused ? '2px solid blue' : 'none'};
`}
>
{node.data.get('code')}
</span>
)
default:
return next()
}
}
}

View File

@@ -58,13 +58,13 @@ class ForcedLayout extends React.Component {
placeholder="Enter a title..."
defaultValue={initialValue}
schema={schema}
renderNode={this.renderNode}
renderBlock={this.renderBlock}
/>
)
}
/**
* Render a Slate node.
* Render a Slate block.
*
* @param {Object} props
* @param {Editor} editor
@@ -72,7 +72,7 @@ class ForcedLayout extends React.Component {
* @return {Element}
*/
renderNode = (props, editor, next) => {
renderBlock = (props, editor, next) => {
const { attributes, children, node } = props
switch (node.type) {

View File

@@ -4,93 +4,53 @@ import { Value } from 'slate'
import React from 'react'
import ReactDOM from 'react-dom'
import initialValue from './value.json'
import styled from 'react-emotion'
import { css } from 'emotion'
import { Button, Icon, Menu } from '../components'
/**
* Give the menu some styles.
*
* @type {Component}
*/
const StyledMenu = styled(Menu)`
padding: 8px 7px 6px;
position: absolute;
z-index: 1;
top: -10000px;
left: -10000px;
margin-top: -6px;
opacity: 0;
background-color: #222;
border-radius: 4px;
transition: opacity 0.75s;
`
/**
* The hovering menu.
*
* @type {Component}
*/
class HoverMenu extends React.Component {
/**
* Render.
*
* @return {Element}
*/
render() {
const { className, innerRef } = this.props
const root = window.document.getElementById('root')
return ReactDOM.createPortal(
<StyledMenu className={className} innerRef={innerRef}>
{this.renderMarkButton('bold', 'format_bold')}
{this.renderMarkButton('italic', 'format_italic')}
{this.renderMarkButton('underlined', 'format_underlined')}
{this.renderMarkButton('code', 'code')}
</StyledMenu>,
root
)
}
/**
* Render a mark-toggling toolbar button.
*
* @param {String} type
* @param {String} icon
* @return {Element}
*/
renderMarkButton(type, icon) {
const { editor } = this.props
const { value } = editor
const isActive = value.activeMarks.some(mark => mark.type === type)
return (
<Button
reversed
active={isActive}
onMouseDown={event => this.onClickMark(event, type)}
>
<Icon>{icon}</Icon>
</Button>
)
}
/**
* When a mark button is clicked, toggle the current mark.
*
* @param {Event} event
* @param {String} type
*/
onClickMark(event, type) {
const { editor } = this.props
event.preventDefault()
editor.toggleMark(type)
}
const MarkButton = ({ editor, type, icon }) => {
const { value } = editor
const isActive = value.activeMarks.some(mark => mark.type === type)
return (
<Button
reversed
active={isActive}
onMouseDown={event => {
event.preventDefault()
editor.toggleMark(type)
}}
>
<Icon>{icon}</Icon>
</Button>
)
}
const HoverMenu = React.forwardRef(({ editor }, ref) => {
const root = window.document.getElementById('root')
return ReactDOM.createPortal(
<Menu
ref={ref}
className={css`
padding: 8px 7px 6px;
position: absolute;
z-index: 1;
top: -10000px;
left: -10000px;
margin-top: -6px;
opacity: 0;
background-color: #222;
border-radius: 4px;
transition: opacity 0.75s;
`}
>
<MarkButton editor={editor} type="bold" icon="format_bold" />
<MarkButton editor={editor} type="italic" icon="format_italic" />
<MarkButton editor={editor} type="underlined" icon="format_underlined" />
<MarkButton editor={editor} type="code" icon="code" />
</Menu>,
root
)
})
/**
* The hovering menu example.
*
@@ -108,6 +68,8 @@ class HoveringMenu extends React.Component {
value: Value.fromJSON(initialValue),
}
menuRef = React.createRef()
/**
* On update, update the menu.
*/
@@ -125,7 +87,7 @@ class HoveringMenu extends React.Component {
*/
updateMenu = () => {
const menu = this.menu
const menu = this.menuRef.current
if (!menu) return
const { value } = this.state
@@ -181,7 +143,7 @@ class HoveringMenu extends React.Component {
return (
<React.Fragment>
{children}
<HoverMenu innerRef={menu => (this.menu = menu)} editor={editor} />
<HoverMenu ref={this.menuRef} editor={editor} />
</React.Fragment>
)
}

View File

@@ -27,6 +27,7 @@
"marks": [{ "type": "italic" }]
},
{
"object": "text",
"text": ", or anything else you might want to do!"
}
]

View File

@@ -62,14 +62,14 @@ class HugeDocument extends React.Component {
placeholder="Enter some text..."
spellCheck={false}
defaultValue={initialValue}
renderNode={this.renderNode}
renderBlock={this.renderBlock}
renderMark={this.renderMark}
/>
)
}
/**
* Render a Slate node.
* Render a Slate block.
*
* @param {Object} props
* @param {Editor} editor
@@ -77,7 +77,7 @@ class HugeDocument extends React.Component {
* @return {Element}
*/
renderNode = (props, editor, next) => {
renderBlock = (props, editor, next) => {
const { attributes, children, node } = props
switch (node.type) {

View File

@@ -5,7 +5,7 @@ import React from 'react'
import initialValueAsJson from './value.json'
import imageExtensions from 'image-extensions'
import isUrl from 'is-url'
import styled from 'react-emotion'
import { css } from 'emotion'
import { Button, Icon, Toolbar } from '../components'
/**
@@ -16,19 +16,6 @@ import { Button, Icon, Toolbar } from '../components'
const initialValue = Value.fromJSON(initialValueAsJson)
/**
* A styled image block component.
*
* @type {Component}
*/
const Image = styled('img')`
display: block;
max-width: 100%;
max-height: 20em;
box-shadow: ${props => (props.selected ? '0 0 0 2px blue;' : 'none')};
`
/**
* A function to determine whether a URL has an image extension.
*
@@ -133,26 +120,37 @@ class Images extends React.Component {
schema={schema}
onDrop={this.onDropOrPaste}
onPaste={this.onDropOrPaste}
renderNode={this.renderNode}
renderBlock={this.renderBlock}
/>
</div>
)
}
/**
* Render a Slate node.
* Render a Slate block.
*
* @param {Object} props
* @return {Element}
*/
renderNode = (props, editor, next) => {
renderBlock = (props, editor, next) => {
const { attributes, node, isFocused } = props
switch (node.type) {
case 'image': {
const src = node.data.get('src')
return <Image src={src} selected={isFocused} {...attributes} />
return (
<img
{...attributes}
src={src}
className={css`
display: block;
max-width: 100%;
max-height: 20em;
box-shadow: ${isFocused ? '0 0 0 2px blue;' : 'none'};
`}
/>
)
}
default: {

View File

@@ -1,8 +1,8 @@
import { Editor, findRange } from 'slate-react'
import { Editor } from 'slate-react'
import { Value } from 'slate'
import React from 'react'
import styled from 'react-emotion'
import { css } from 'emotion'
import initialValueAsJson from './value.json'
import { Icon } from '../components'
import { createArrayValue } from 'react-values'
@@ -17,72 +17,96 @@ const initialValue = Value.fromJSON(initialValueAsJson)
const EventsValue = createArrayValue()
const Wrapper = styled('div')`
position: relative;
`
const Wrapper = React.forwardRef((props, ref) => (
<div
{...props}
ref={ref}
className={css`
position: relative;
`}
/>
))
const EventsWrapper = styled('div')`
position: fixed;
left: 0;
bottom: 0;
right: 0;
max-height: 40vh;
height: 500px;
overflow: auto;
border-top: 1px solid #ccc;
background: white;
`
const EventsWrapper = props => (
<div
{...props}
className={css`
position: fixed;
left: 0;
bottom: 0;
right: 0;
max-height: 40vh;
height: 500px;
overflow: auto;
border-top: 1px solid #ccc;
background: white;
`}
/>
)
const EventsTable = styled('table')`
font-family: monospace;
font-size: 0.9em;
border-collapse: collapse;
border: none;
min-width: 100%;
const EventsTable = props => (
<table
{...props}
className={css`
font-family: monospace;
font-size: 0.9em;
border-collapse: collapse;
border: none;
min-width: 100%;
& > * + * {
margin-top: 1px;
}
& > * + * {
margin-top: 1px;
}
tr,
th,
td {
border: none;
}
tr,
th,
td {
border: none;
}
th,
td {
text-align: left;
padding: 0.333em;
}
th,
td {
text-align: left;
padding: 0.333em;
}
th {
position: sticky;
top: 0;
background-color: #eee;
border-bottom: 1px solid #ccc;
}
th {
position: sticky;
top: 0;
background-color: #eee;
border-bottom: 1px solid #ccc;
}
td {
background-color: white;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
`
td {
background-color: white;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
`}
/>
)
const Pill = styled('span')`
display: inline-block;
padding: 0.25em 0.33em;
border-radius: 4px;
background-color: ${p => p.color};
`
const Pill = ({ color, ...props }) => (
<span
className={css`
display: inline-block;
padding: 0.25em 0.33em;
border-radius: 4px;
background-color: ${color};
`}
/>
)
const I = styled(Icon)`
font-size: 0.9em;
color: ${p => p.color};
`
const I = ({ color, ...props }) => (
<Icon
className={css`
font-size: 0.9em;
color: ${color};
`}
/>
)
const MissingCell = props => <I color="silver">texture</I>
const MissingCell = () => <I color="silver">texture</I>
const TypeCell = ({ event }) => {
switch (event.constructor.name) {
@@ -220,14 +244,14 @@ class InputTester extends React.Component {
render() {
return (
<Wrapper innerRef={this.onRef}>
<Wrapper ref={this.onRef}>
<Editor
spellCheck
placeholder="Enter some text..."
ref={this.ref}
defaultValue={initialValue}
onChange={this.onChange}
renderNode={this.renderNode}
renderBlock={this.renderBlock}
renderMark={this.renderMark}
/>
<EventsList />
@@ -235,7 +259,7 @@ class InputTester extends React.Component {
)
}
renderNode = (props, editor, next) => {
renderBlock = (props, editor, next) => {
const { attributes, children, node } = props
switch (node.type) {
@@ -295,14 +319,14 @@ class InputTester extends React.Component {
if (event.getTargetRanges) {
const [nativeTargetRange] = event.getTargetRanges()
targetRange = nativeTargetRange && findRange(nativeTargetRange, editor)
targetRange = nativeTargetRange && editor.findRange(nativeTargetRange)
}
const nativeSelection = window.getSelection()
const nativeRange = nativeSelection.rangeCount
? nativeSelection.getRangeAt(0)
: undefined
const selection = nativeRange && findRange(nativeRange, editor)
const selection = nativeRange && editor.findRange(nativeRange)
EventsValue.push({
event,
@@ -319,7 +343,7 @@ class InputTester extends React.Component {
const nativeRange = nativeSelection.rangeCount
? nativeSelection.getRangeAt(0)
: undefined
const selection = nativeRange && findRange(nativeRange, editor)
const selection = nativeRange && editor.findRange(nativeRange)
const {
type,
@@ -346,7 +370,7 @@ class InputTester extends React.Component {
style += '; background-color: lightskyblue'
const [nativeTargetRange] = event.getTargetRanges()
const targetRange =
nativeTargetRange && findRange(nativeTargetRange, editor)
nativeTargetRange && editor.findRange(nativeTargetRange)
details = {
inputType,

View File

@@ -90,14 +90,14 @@ class Links extends React.Component {
value={this.state.value}
onChange={this.onChange}
onPaste={this.onPaste}
renderNode={this.renderNode}
renderInline={this.renderInline}
/>
</div>
)
}
/**
* Render a Slate node.
* Render a Slate inline.
*
* @param {Object} props
* @param {Editor} editor
@@ -105,7 +105,7 @@ class Links extends React.Component {
* @return {Element}
*/
renderNode = (props, editor, next) => {
renderInline = (props, editor, next) => {
const { attributes, children, node } = props
switch (node.type) {

View File

@@ -40,14 +40,14 @@ class MarkdownPreview extends React.Component {
<Editor
placeholder="Write some markdown..."
defaultValue={initialValue}
renderMark={this.renderMark}
renderDecoration={this.renderDecoration}
decorateNode={this.decorateNode}
/>
)
}
/**
* Render a Slate mark.
* Render a Slate decoration.
*
* @param {Object} props
* @param {Editor} editor
@@ -55,10 +55,10 @@ class MarkdownPreview extends React.Component {
* @return {Element}
*/
renderMark = (props, editor, next) => {
const { children, mark, attributes } = props
renderDecoration = (props, editor, next) => {
const { children, decoration, attributes } = props
switch (mark.type) {
switch (decoration.type) {
case 'bold':
return <strong {...attributes}>{children}</strong>
@@ -144,12 +144,12 @@ class MarkdownPreview extends React.Component {
if (node.object !== 'block') return others
const string = node.text
const texts = node.getTexts().toArray()
const texts = Array.from(node.texts())
const grammar = Prism.languages.markdown
const tokens = Prism.tokenize(string, grammar)
const decorations = []
let startText = texts.shift()
let endText = startText
let startEntry = texts.shift()
let endEntry = startEntry
let startOffset = 0
let endOffset = 0
let start = 0
@@ -165,9 +165,10 @@ class MarkdownPreview extends React.Component {
}
for (const token of tokens) {
startText = endText
startEntry = endEntry
startOffset = endOffset
const [startText, startPath] = startEntry
const length = getLength(token)
const end = start + length
@@ -177,25 +178,28 @@ class MarkdownPreview extends React.Component {
endOffset = startOffset + remaining
while (available < remaining) {
endText = texts.shift()
endEntry = texts.shift()
const [endText] = endEntry
remaining = length - available
available = endText.text.length
endOffset = remaining
}
const [endText, endPath] = endEntry
if (typeof token !== 'string') {
const dec = {
type: token.type,
anchor: {
key: startText.key,
path: startPath,
offset: startOffset,
},
focus: {
key: endText.key,
path: endPath,
offset: endOffset,
},
mark: {
type: token.type,
},
}
decorations.push(dec)

View File

@@ -64,13 +64,13 @@ class MarkdownShortcuts extends React.Component {
placeholder="Write some markdown..."
defaultValue={initialValue}
onKeyDown={this.onKeyDown}
renderNode={this.renderNode}
renderBlock={this.renderBlock}
/>
)
}
/**
* Render a Slate node.
* Render a Slate block.
*
* @param {Object} props
* @param {Editor} editor
@@ -78,7 +78,7 @@ class MarkdownShortcuts extends React.Component {
* @return {Element}
*/
renderNode = (props, editor, next) => {
renderBlock = (props, editor, next) => {
const { attributes, children, node } = props
switch (node.type) {

View File

@@ -1,34 +1,44 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { css } from 'emotion'
import styled from 'react-emotion'
const SuggestionList = React.forwardRef((props, ref) => (
<ul
{...props}
ref={ref}
className={css`
background: #fff;
list-style: none;
margin: 0;
padding: 0;
position: absolute;
`}
/>
))
const SuggestionList = styled('ul')`
background: #fff;
list-style: none;
margin: 0;
padding: 0;
position: absolute;
`
const Suggestion = props => (
<li
{...props}
className={css`
align-items: center;
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
border-top: 1px solid #ddd;
const Suggestion = styled('li')`
align-items: center;
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
border-top: 1px solid #ddd;
display: flex;
height: 32px;
padding: 4px 8px;
display: flex;
height: 32px;
padding: 4px 8px;
&:hover {
background: #87cefa;
}
&:hover {
background: #87cefa;
}
&:last-of-type {
border-bottom: 1px solid #ddd;
}
`
&:last-of-type {
border-bottom: 1px solid #ddd;
}
`}
/>
)
const DEFAULT_POSITION = {
top: -10000,

View File

@@ -41,7 +41,7 @@ import Suggestions from './Suggestions'
const USER_MENTION_NODE_TYPE = 'userMention'
/**
* The decoration mark type that the menu will position itself against. The
* The annotation mark type that the menu will position itself against. The
* "context" is just the current text after the @ symbol.
* @type {String}
*/
@@ -118,7 +118,7 @@ class MentionsExample extends React.Component {
value={this.state.value}
onChange={this.onChange}
ref={this.editorRef}
renderNode={this.renderNode}
renderInline={this.renderInline}
renderMark={this.renderMark}
schema={schema}
/>
@@ -145,7 +145,7 @@ class MentionsExample extends React.Component {
return next()
}
renderNode(props, editor, next) {
renderInline(props, editor, next) {
const { attributes, node } = props
if (node.type === USER_MENTION_NODE_TYPE) {
@@ -215,12 +215,12 @@ class MentionsExample extends React.Component {
const { selection } = change.value
let decorations = change.value.decorations.filter(
let annotations = change.value.annotations.filter(
value => value.mark.type !== CONTEXT_MARK_TYPE
)
if (inputValue && hasValidAncestors(change.value)) {
decorations = decorations.push({
annotations = annotations.push({
anchor: {
key: selection.start.key,
offset: selection.start.offset - inputValue.length,
@@ -236,8 +236,8 @@ class MentionsExample extends React.Component {
}
this.setState({ value: change.value }, () => {
// We need to set decorations after the value flushes into the editor.
this.editorRef.current.setDecorations(decorations)
// We need to set annotations after the value flushes into the editor.
this.editorRef.current.setannotations(annotations)
})
return
}

View File

@@ -4,7 +4,7 @@ import { Value } from 'slate'
import React from 'react'
import initialValueAsJson from './value.json'
import styled from 'react-emotion'
import { css } from 'emotion'
/**
* Deserialize the initial editor value.
@@ -49,19 +49,6 @@ const MARK_TAGS = {
code: 'code',
}
/**
* A styled image block component.
*
* @type {Component}
*/
const Image = styled('img')`
display: block;
max-width: 100%;
max-height: 20em;
box-shadow: ${props => (props.selected ? '0 0 0 2px blue;' : 'none')};
`
/**
* Serializer rules.
*
@@ -187,20 +174,21 @@ class PasteHtml extends React.Component {
defaultValue={initialValue}
schema={this.schema}
onPaste={this.onPaste}
renderNode={this.renderNode}
renderBlock={this.renderBlock}
renderInline={this.renderInline}
renderMark={this.renderMark}
/>
)
}
/**
* Render a Slate node.
* Render a Slate block.
*
* @param {Object} props
* @return {Element}
*/
renderNode = (props, editor, next) => {
renderBlock = (props, editor, next) => {
const { attributes, children, node, isFocused } = props
switch (node.type) {
@@ -230,7 +218,37 @@ class PasteHtml extends React.Component {
return <li {...attributes}>{children}</li>
case 'numbered-list':
return <ol {...attributes}>{children}</ol>
case 'link': {
case 'image':
const src = node.data.get('src')
return (
<img
{...attributes}
src={src}
className={css`
display: block;
max-width: 100%;
max-height: 20em;
box-shadow: ${isFocused ? '0 0 0 2px blue;' : 'none'};
`}
/>
)
default:
return next()
}
}
/**
* Render a Slate inline.
*
* @param {Object} props
* @return {Element}
*/
renderInline = (props, editor, next) => {
const { attributes, children, node } = props
switch (node.type) {
case 'link':
const { data } = node
const href = data.get('href')
return (
@@ -238,15 +256,8 @@ class PasteHtml extends React.Component {
{children}
</a>
)
}
case 'image': {
const src = node.data.get('src')
return <Image src={src} selected={isFocused} {...attributes} />
}
default: {
default:
return next()
}
}
}

View File

@@ -1,24 +1,32 @@
import React from 'react'
import styled from 'react-emotion'
const WordCounter = styled('span')`
margin-top: 10px;
padding: 12px;
background-color: #ebebeb;
display: inline-block;
`
import { css } from 'emotion'
export default function WordCount(options) {
return {
renderEditor(props, editor, next) {
const { value } = editor
const { document } = value
const children = next()
const wordCount = props.value.document
.getBlocks()
.reduce((memo, b) => memo + b.text.trim().split(/\s+/).length, 0)
let wordCount = 0
for (const [node] of document.blocks({ onlyLeaves: true })) {
const words = node.text.trim().split(/\s+/)
wordCount += words.length
}
return (
<div>
<div>{children}</div>
<WordCounter>Word Count: {wordCount}</WordCounter>
<span
className={css`
margin-top: 10px;
padding: 12px;
background-color: #ebebeb;
display: inline-block;
`}
>
Word Count: {wordCount}
</span>
</div>
)
},

View File

@@ -104,7 +104,7 @@ class RichTextExample extends React.Component {
value={this.state.value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
renderNode={this.renderNode}
renderBlock={this.renderBlock}
renderMark={this.renderMark}
/>
</div>
@@ -163,13 +163,13 @@ class RichTextExample extends React.Component {
}
/**
* Render a Slate node.
* Render a Slate block.
*
* @param {Object} props
* @return {Element}
*/
renderNode = (props, editor, next) => {
renderBlock = (props, editor, next) => {
const { attributes, children, node } = props
switch (node.type) {

View File

@@ -31,7 +31,7 @@ class RTL extends React.Component {
placeholder="Enter some plain text..."
defaultValue={initialValue}
onKeyDown={this.onKeyDown}
renderNode={this.renderNode}
renderBlock={this.renderBlock}
/>
)
}
@@ -43,7 +43,7 @@ class RTL extends React.Component {
* @return {Element}
*/
renderNode = (props, editor, next) => {
renderBlock = (props, editor, next) => {
const { attributes, children, node } = props
switch (node.type) {

View File

@@ -3,9 +3,21 @@ import { Value } from 'slate'
import React from 'react'
import initialValueAsJson from './value.json'
import styled from 'react-emotion'
import { css } from 'emotion'
import { Icon, Toolbar } from '../components'
/**
* Get a unique key for the search highlight annotations.
*
* @return {String}
*/
let n = 0
function getHighlightKey() {
return `highlight_${n++}`
}
/**
* Deserialize the initial editor value.
*
@@ -20,21 +32,36 @@ const initialValue = Value.fromJSON(initialValueAsJson)
* @type {Component}
*/
const SearchWrapper = styled('div')`
position: relative;
`
const SearchWrapper = props => (
<div
{...props}
className={css`
position: relative;
`}
/>
)
const SearchIcon = styled(Icon)`
position: absolute;
top: 0.5em;
left: 0.5em;
color: #ccc;
`
const SearchIcon = props => (
<Icon
{...props}
className={css`
position: absolute;
top: 0.5em;
left: 0.5em;
color: #ccc;
`}
/>
)
const SearchInput = styled('input')`
padding-left: 2em;
width: 100%;
`
const SearchInput = props => (
<input
{...props}
className={css`
padding-left: 2em;
width: 100%;
`}
/>
)
/**
* The search highlighting example.
@@ -50,7 +77,7 @@ class SearchHighlighting extends React.Component {
*/
schema = {
marks: {
annotations: {
highlight: {
isAtomic: true,
},
@@ -63,9 +90,7 @@ class SearchHighlighting extends React.Component {
* @param {Editor} editor
*/
ref = editor => {
this.editor = editor
}
ref = React.createRef()
/**
* Render.
@@ -91,7 +116,7 @@ class SearchHighlighting extends React.Component {
ref={this.ref}
defaultValue={initialValue}
schema={this.schema}
renderMark={this.renderMark}
renderAnnotation={this.renderAnnotation}
spellCheck
/>
</div>
@@ -99,16 +124,16 @@ class SearchHighlighting extends React.Component {
}
/**
* Render a Slate mark.
* Render a Slate annotation.
*
* @param {Object} props
* @return {Element}
*/
renderMark = (props, editor, next) => {
const { children, mark, attributes } = props
renderAnnotation = (props, editor, next) => {
const { children, annotation, attributes } = props
switch (mark.type) {
switch (annotation.type) {
case 'highlight':
return (
<span {...attributes} style={{ backgroundColor: '#ffeeba' }}>
@@ -121,40 +146,44 @@ class SearchHighlighting extends React.Component {
}
/**
* On input change, update the decorations.
* On input change, update the annotations.
*
* @param {Event} event
*/
onInputChange = event => {
const { editor } = this
const editor = this.ref.current
const { value } = editor
const { document, annotations } = value
const string = event.target.value
const texts = value.document.getTexts()
const decorations = []
texts.forEach(node => {
const { key, text } = node
const parts = text.split(string)
let offset = 0
parts.forEach((part, i) => {
if (i !== 0) {
decorations.push({
anchor: { key, offset: offset - string.length },
focus: { key, offset },
mark: { type: 'highlight' },
})
}
offset = offset + part.length + string.length
})
})
// Make the change to decorations without saving it into the undo history,
// Make the change to annotations without saving it into the undo history,
// so that there isn't a confusing behavior when undoing.
editor.withoutSaving(() => {
editor.setDecorations(decorations)
annotations.forEach(ann => {
if (ann.type === 'highlight') {
editor.removeAnnotation(ann)
}
})
for (const [node, path] of document.texts()) {
const { key, text } = node
const parts = text.split(string)
let offset = 0
parts.forEach((part, i) => {
if (i !== 0) {
editor.addAnnotation({
key: getHighlightKey(),
type: 'highlight',
anchor: { path, key, offset: offset - string.length },
focus: { path, key, offset },
})
}
offset = offset + part.length + string.length
})
}
})
}
}

View File

@@ -10,7 +10,7 @@
{
"object": "text",
"text":
"This is editable text that you can search. As you search, it looks for matching strings of text, and adds \"decoration\" marks to them in realtime."
"This is editable text that you can search. As you search, it looks for matching strings of text, and adds \"annotation\" marks to them in realtime."
}
]
},

View File

@@ -3,22 +3,10 @@ import { Value } from 'slate'
import React from 'react'
import initialValue from './value.json'
import styled from 'react-emotion'
import { css } from 'emotion'
import { isKeyHotkey } from 'is-hotkey'
import { Button, Icon, Toolbar } from '../components'
/**
* A spacer component.
*
* @type {Component}
*/
const Spacer = styled('div')`
height: 20px;
background-color: #eee;
margin: 20px -20px;
`
/**
* Hotkey matchers.
*
@@ -229,7 +217,13 @@ class SyncingOperationsExample extends React.Component {
ref={one => (this.one = one)}
onChange={this.onOneChange}
/>
<Spacer />
<div
className={css`
height: 20px;
background-color: #eee;
margin: 20px -20px;
`}
/>
<SyncingEditor
ref={two => (this.two = two)}
onChange={this.onTwoChange}

View File

@@ -34,7 +34,7 @@ class Tables extends React.Component {
onKeyDown={this.onKeyDown}
onDrop={this.onDropOrPaste}
onPaste={this.onDropOrPaste}
renderNode={this.renderNode}
renderBlock={this.renderBlock}
renderMark={this.renderMark}
/>
)
@@ -47,7 +47,7 @@ class Tables extends React.Component {
* @return {Element}
*/
renderNode = (props, editor, next) => {
renderBlock = (props, editor, next) => {
const { attributes, children, node } = props
switch (node.type) {

View File

@@ -25,7 +25,7 @@
"css-loader": "^0.28.9",
"element-closest": "^2.0.2",
"emojis": "^1.0.10",
"emotion": "^9.2.4",
"emotion": "^10.0.9",
"eslint": "^4.19.1",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.8.0",
@@ -52,9 +52,8 @@
"npm-run-all": "^4.1.2",
"prettier": "^1.10.2",
"prismjs": "^1.5.1",
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-emotion": "^9.2.4",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-hot-loader": "^3.1.3",
"react-portal": "^4.1.5",
"react-router-dom": "^4.3.1",

View File

@@ -335,8 +335,21 @@ class Html {
serializeNode = node => {
if (node.object === 'text') {
const leaves = node.getLeaves()
return leaves.map(this.serializeLeaf)
const string = new String({ text: node.text })
const text = this.serializeString(string)
return node.marks.reduce((children, mark) => {
for (const rule of this.rules) {
if (!rule.serialize) continue
const ret = rule.serialize(mark, children)
if (ret === null) return
if (ret) return addKey(ret)
}
throw new Error(
`No serializer defined for mark of type "${mark.type}".`
)
}, text)
}
const children = node.nodes.map(this.serializeNode)
@@ -351,29 +364,6 @@ class Html {
throw new Error(`No serializer defined for node of type "${node.type}".`)
}
/**
* Serialize a `leaf`.
*
* @param {Leaf} leaf
* @return {String}
*/
serializeLeaf = leaf => {
const string = new String({ text: leaf.text })
const text = this.serializeString(string)
return leaf.marks.reduce((children, mark) => {
for (const rule of this.rules) {
if (!rule.serialize) continue
const ret = rule.serialize(mark, children)
if (ret === null) return
if (ret) return addKey(ret)
}
throw new Error(`No serializer defined for mark of type "${mark.type}".`)
}, text)
}
/**
* Serialize a `string`.
*

View File

@@ -1,5 +1,5 @@
import {
Decoration,
Annotation,
Document,
Mark,
Node,
@@ -10,7 +10,7 @@ import {
} from 'slate'
/**
* Auto-incrementing ID to keep track of paired decorations.
* Auto-incrementing ID to keep track of paired annotations.
*
* @type {Number}
*/
@@ -59,29 +59,29 @@ export function createCursor(tagName, attributes, children) {
}
/**
* Create a decoration point, or wrap a list of leaves and set the decoration
* Create a annotation point, or wrap a list of leaves and set the annotation
* point tracker on them.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {DecorationPoint|List<Leaf>}
* @return {AnnotationPoint|List<Leaf>}
*/
export function createDecoration(tagName, attributes, children) {
export function createAnnotation(tagName, attributes, children) {
const { key, data } = attributes
const type = tagName
if (key) {
return new DecorationPoint({ id: key, type, data })
return new AnnotationPoint({ id: key, type, data })
}
const texts = createChildren(children)
const first = texts.first()
const last = texts.last()
const id = `__decoration_${uid++}__`
const start = new DecorationPoint({ id, type, data })
const end = new DecorationPoint({ id, type, data })
const id = `${uid++}`
const start = new AnnotationPoint({ id, type, data })
const end = new AnnotationPoint({ id, type, data })
setPoint(first, start, 0)
setPoint(last, end, last.text.length)
return texts
@@ -267,63 +267,64 @@ export function createValue(tagName, attributes, children) {
let focus
let marks
let isFocused
let decorations = []
let annotations = {}
const partials = {}
// Search the document's texts to see if any of them have the anchor or
// focus information saved, or decorations applied.
// focus information saved, or annotations applied.
if (document) {
document.getTexts().forEach(text => {
const { __anchor, __decorations, __focus } = text
for (const [node, path] of document.texts()) {
const { __anchor, __annotations, __focus } = node
if (__anchor != null) {
anchor = Point.create({ key: text.key, offset: __anchor.offset })
anchor = Point.create({ path, key: node.key, offset: __anchor.offset })
marks = __anchor.marks
isFocused = __anchor.isFocused
}
if (__focus != null) {
focus = Point.create({ key: text.key, offset: __focus.offset })
focus = Point.create({ path, key: node.key, offset: __focus.offset })
marks = __focus.marks
isFocused = __focus.isFocused
}
if (__decorations != null) {
for (const dec of __decorations) {
const { id } = dec
if (__annotations != null) {
for (const ann of __annotations) {
const { id } = ann
const partial = partials[id]
delete partials[id]
if (!partial) {
dec.key = text.key
partials[id] = dec
ann.key = node.key
partials[id] = ann
continue
}
const decoration = Decoration.create({
const annotation = Annotation.create({
key: id,
type: ann.type,
data: ann.data,
anchor: {
key: partial.key,
path: document.getPath(partial.key),
offset: partial.offset,
},
focus: {
key: text.key,
offset: dec.offset,
},
mark: {
type: dec.type,
data: dec.data,
path,
key: node.key,
offset: ann.offset,
},
})
decorations.push(decoration)
annotations[id] = annotation
}
}
})
}
}
if (Object.keys(partials).length > 0) {
throw new Error(
`Slate hyperscript must have both a start and an end defined for each decoration using the \`key=\` prop.`
`Slate hyperscript must have both a start and an end defined for each annotation using the \`key=\` prop.`
)
}
@@ -351,13 +352,13 @@ export function createValue(tagName, attributes, children) {
selection = selection.normalize(document)
if (decorations.length > 0) {
decorations = decorations.map(d => d.normalize(document))
if (annotations.length > 0) {
annotations = annotations.map(a => a.normalize(document))
}
const value = Value.fromJSON({
data,
decorations,
annotations,
document,
selection,
...attributes,
@@ -484,7 +485,7 @@ class FocusPoint {
}
}
class DecorationPoint {
class AnnotationPoint {
constructor(attrs) {
const { id = null, data = {}, type } = attrs
this.id = id
@@ -502,7 +503,7 @@ class DecorationPoint {
*/
function incrementPoints(object, n) {
const { __anchor, __focus, __decorations } = object
const { __anchor, __focus, __annotations } = object
if (__anchor != null) {
__anchor.offset += n
@@ -512,8 +513,8 @@ function incrementPoints(object, n) {
__focus.offset += n
}
if (__decorations != null) {
__decorations.forEach(d => (d.offset += n))
if (__annotations != null) {
__annotations.forEach(a => (a.offset += n))
}
}
@@ -528,7 +529,7 @@ function isPoint(object) {
return (
object instanceof AnchorPoint ||
object instanceof CursorPoint ||
object instanceof DecorationPoint ||
object instanceof AnnotationPoint ||
object instanceof FocusPoint
)
}
@@ -548,10 +549,10 @@ function preservePoints(object, updator) {
}
function copyPoints(object, other) {
const { __anchor, __focus, __decorations } = object
const { __anchor, __focus, __annotations } = object
if (__anchor != null) other.__anchor = __anchor
if (__focus != null) other.__focus = __focus
if (__decorations != null) other.__decorations = __decorations
if (__annotations != null) other.__annotations = __annotations
}
/**
@@ -573,9 +574,9 @@ function setPoint(object, point, offset) {
object.__focus = point
}
if (point instanceof DecorationPoint) {
if (point instanceof AnnotationPoint) {
point.offset = offset
object.__decorations = object.__decorations || []
object.__decorations = object.__decorations.concat(point)
object.__annotations = object.__annotations || []
object.__annotations = object.__annotations.concat(point)
}
}

View File

@@ -4,7 +4,7 @@ import {
createAnchor,
createBlock,
createCursor,
createDecoration,
createAnnotation,
createDocument,
createFocus,
createInline,
@@ -23,13 +23,13 @@ import {
*/
function createHyperscript(options = {}) {
const { blocks = {}, inlines = {}, marks = {}, decorations = {} } = options
const { blocks = {}, inlines = {}, marks = {}, annotations = {} } = options
const creators = {
anchor: createAnchor,
annotation: createAnnotation,
block: createBlock,
cursor: createCursor,
decoration: createDecoration,
document: createDocument,
focus: createFocus,
inline: createInline,
@@ -53,8 +53,8 @@ function createHyperscript(options = {}) {
creators[key] = normalizeCreator(marks[key], createMark)
}
for (const key in decorations) {
creators[key] = normalizeCreator(decorations[key], createDecoration)
for (const key in annotations) {
creators[key] = normalizeCreator(annotations[key], createAnnotation)
}
function create(tagName, attributes, ...children) {

View File

@@ -6,7 +6,7 @@ const h = createHyperscript({
blocks: {
paragraph: 'paragraph',
},
decorations: {
annotations: {
highlight: 'highlight',
},
})
@@ -68,18 +68,13 @@ export const output = {
},
}
export const expectDecorations = [
export const expectAnnotations = [
{
type: 'highlight',
data: {},
anchorOffset: 12,
focusOffset: 13,
anchorKey: input.document.nodes.get(0).getFirstText().key,
focusKey: input.document.nodes.get(1).getFirstText().key,
marks: [
{
object: 'mark',
type: 'highlight',
data: {},
},
],
},
]

View File

@@ -6,7 +6,7 @@ const h = createHyperscript({
blocks: {
paragraph: 'paragraph',
},
decorations: {
annotations: {
highlight: 'highlight',
},
})
@@ -22,7 +22,7 @@ export const input = (
)
export const options = {
preserveDecorations: true,
preserveAnnotations: true,
preserveKeys: true,
}
@@ -49,9 +49,12 @@ export const output = {
},
],
},
decorations: [
annotations: [
{
object: 'decoration',
key: '0',
object: 'annotation',
type: 'highlight',
data: {},
anchor: {
object: 'point',
key: '1',
@@ -64,11 +67,6 @@ export const output = {
path: [0, 0],
offset: 6,
},
mark: {
object: 'mark',
type: 'highlight',
data: {},
},
},
],
}

View File

@@ -6,7 +6,7 @@ const h = createHyperscript({
blocks: {
paragraph: 'paragraph',
},
decorations: {
annotations: {
highlight: 'highlight',
},
})
@@ -25,7 +25,7 @@ export const input = (
)
export const options = {
preserveDecorations: true,
preserveAnnotations: true,
preserveKeys: true,
}
@@ -66,9 +66,12 @@ export const output = {
},
],
},
decorations: [
annotations: [
{
object: 'decoration',
object: 'annotation',
key: 'a',
type: 'highlight',
data: {},
anchor: {
object: 'point',
key: '0',
@@ -81,11 +84,6 @@ export const output = {
path: [1, 0],
offset: 2,
},
mark: {
object: 'mark',
type: 'highlight',
data: {},
},
},
],
}

View File

@@ -9,21 +9,4 @@ describe('slate-hyperscript', () => {
const expected = Value.isValue(output) ? output.toJSON() : output
assert.deepEqual(actual, expected)
})
fixtures.skip(__dirname, 'decorations', ({ module }) => {
const { input, output, expectDecorations } = module
const actual = input.toJSON()
const expected = Value.isValue(output) ? output.toJSON() : output
assert.deepEqual(actual, expected)
expectDecorations.forEach((decoration, i) => {
Object.keys(decoration).forEach(prop => {
assert.deepEqual(
decoration[prop],
input.decorations.toJS()[i][prop],
`decoration ${i} had incorrect prop: ${prop}`
)
})
})
})
})

View File

@@ -8,6 +8,7 @@ import {
Mark,
Node,
Range,
Selection,
Value,
Text,
} from 'slate'
@@ -23,12 +24,21 @@ import {
function create(name, validate) {
function check(isRequired, props, propName, componentName, location) {
const value = props[propName]
if (value == null && !isRequired) return null
if (value == null && isRequired)
if (value == null && !isRequired) {
return null
}
if (value == null && isRequired) {
return new Error(
`The ${location} \`${propName}\` is marked as required in \`${componentName}\`, but it was not supplied.`
)
if (validate(value)) return null
}
if (validate(value)) {
return null
}
return new Error(
`Invalid ${location} \`${propName}\` supplied to \`${componentName}\`, expected a Slate \`${name}\` but received: ${value}`
)
@@ -67,6 +77,7 @@ const Types = {
nodes: create('List<Node>', v => Node.isNodeList(v)),
range: create('Range', v => Range.isRange(v)),
ranges: create('List<Range>', v => Range.isRangeList(v)),
selection: create('Selection', v => Selection.isSelection(v)),
value: create('Value', v => Value.isValue(v)),
text: create('Text', v => Text.isText(v)),
texts: create('List<Text>', v => Text.isTextList(v)),

View File

@@ -24,12 +24,12 @@ function SlateReactPlaceholder(options = {}) {
const { placeholder, when, style = {} } = options
invariant(
placeholder,
typeof placeholder === 'string',
'You must pass `SlateReactPlaceholder` an `options.placeholder` string.'
)
invariant(
when,
typeof when === 'string' || typeof when === 'function',
'You must pass `SlateReactPlaceholder` an `options.when` query.'
)
@@ -48,15 +48,16 @@ function SlateReactPlaceholder(options = {}) {
}
const others = next()
const document = editor.value.document
const first = node.getFirstText()
const last = node.getLastText()
const [first] = node.texts()
const [last] = node.texts({ direction: 'backward' })
const [firstNode, firstPath] = first
const [lastNode, lastPath] = last
const decoration = {
anchor: { key: first.key, offset: 0, path: document.getPath(first.key) },
anchor: { key: firstNode.key, offset: 0, path: firstPath },
focus: {
key: last.key,
offset: last.text.length,
path: document.getPath(last.key),
key: lastNode.key,
offset: lastNode.text.length,
path: lastPath,
},
mark: placeholderMark,
}

View File

@@ -32,8 +32,7 @@
},
"peerDependencies": {
"immutable": ">=3.8.1 || >4.0.0-rc",
"react": ">=0.14.0",
"react-dom": ">=0.14.0",
"react": ">=16.6.0",
"slate": ">=0.43.6"
},
"devDependencies": {
@@ -49,7 +48,6 @@
"umdGlobals": {
"immutable": "Immutable",
"react": "React",
"react-dom": "ReactDOM",
"slate": "Slate"
},
"keywords": [

View File

@@ -4,6 +4,7 @@ import Types from 'prop-types'
import getWindow from 'get-window'
import warning from 'tiny-warning'
import throttle from 'lodash/throttle'
import { List } from 'immutable'
import {
IS_ANDROID,
IS_FIREFOX,
@@ -11,10 +12,9 @@ import {
} from 'slate-dev-environment'
import EVENT_HANDLERS from '../constants/event-handlers'
import DATA_ATTRS from '../constants/data-attributes'
import SELECTORS from '../constants/selectors'
import Node from './node'
import findDOMRange from '../utils/find-dom-range'
import findRange from '../utils/find-range'
import getChildrenDecorations from '../utils/get-children-decorations'
import scrollToSelection from '../utils/scroll-to-selection'
import removeAllRanges from '../utils/remove-all-ranges'
@@ -82,8 +82,18 @@ class Content extends React.Component {
tmp = {
isUpdatingSelection: false,
nodeRef: React.createRef(),
nodeRefs: {},
}
/**
* A ref for the contenteditable DOM node.
*
* @type {Object}
*/
ref = React.createRef()
/**
* Create a set of bound event handlers.
*
@@ -103,7 +113,7 @@ class Content extends React.Component {
*/
componentDidMount() {
const window = getWindow(this.element)
const window = getWindow(this.ref.current)
window.document.addEventListener(
'selectionchange',
@@ -113,7 +123,10 @@ class Content extends React.Component {
// COMPAT: Restrict scope of `beforeinput` to clients that support the
// Input Events Level 2 spec, since they are preventable events.
if (HAS_INPUT_EVENTS_LEVEL_2) {
this.element.addEventListener('beforeinput', this.handlers.onBeforeInput)
this.ref.current.addEventListener(
'beforeinput',
this.handlers.onBeforeInput
)
}
this.updateSelection()
@@ -124,7 +137,7 @@ class Content extends React.Component {
*/
componentWillUnmount() {
const window = getWindow(this.element)
const window = getWindow(this.ref.current)
if (window) {
window.document.removeEventListener(
@@ -134,7 +147,7 @@ class Content extends React.Component {
}
if (HAS_INPUT_EVENTS_LEVEL_2) {
this.element.removeEventListener(
this.ref.current.removeEventListener(
'beforeinput',
this.handlers.onBeforeInput
)
@@ -159,7 +172,7 @@ class Content extends React.Component {
const { value } = editor
const { selection } = value
const { isBackward } = selection
const window = getWindow(this.element)
const window = getWindow(this.ref.current)
const native = window.getSelection()
const { activeElement } = window.document
@@ -178,8 +191,8 @@ class Content extends React.Component {
// If the Slate selection is blurred, but the DOM's active element is still
// the editor, we need to blur it.
if (selection.isBlurred && activeElement === this.element) {
this.element.blur()
if (selection.isBlurred && activeElement === this.ref.current) {
this.ref.current.blur()
updated = true
}
@@ -193,15 +206,15 @@ class Content extends React.Component {
// If the Slate selection is focused, but the DOM's active element is not
// the editor, we need to focus it. We prevent scrolling because we handle
// scrolling to the correct selection.
if (selection.isFocused && activeElement !== this.element) {
this.element.focus({ preventScroll: true })
if (selection.isFocused && activeElement !== this.ref.current) {
this.ref.current.focus({ preventScroll: true })
updated = true
}
// Otherwise, figure out which DOM nodes should be selected...
if (selection.isFocused && selection.isSet) {
const current = !!rangeCount && native.getRangeAt(0)
const range = findDOMRange(selection, window)
const range = editor.findDOMRange(selection)
if (!range) {
warning(
@@ -269,8 +282,8 @@ class Content extends React.Component {
setTimeout(() => {
// COMPAT: In Firefox, it's not enough to create a range, you also need
// to focus the contenteditable element too. (2016/11/16)
if (IS_FIREFOX && this.element) {
this.element.focus()
if (IS_FIREFOX && this.ref.current) {
this.ref.current.focus()
}
this.tmp.isUpdatingSelection = false
@@ -283,16 +296,6 @@ class Content extends React.Component {
}
}
/**
* The React ref method to set the root content element locally.
*
* @param {Element} element
*/
ref = element => {
this.element = element
}
/**
* Check if an event `target` is fired from within the contenteditable
* element. This should be false for edits happening in non-contenteditable
@@ -303,8 +306,6 @@ class Content extends React.Component {
*/
isInEditor = target => {
const { element } = this
let el
try {
@@ -331,7 +332,8 @@ class Content extends React.Component {
return (
el.isContentEditable &&
(el === element || el.closest('[data-slate-editor]') === element)
(el === this.ref.current ||
el.closest(SELECTORS.EDITOR) === this.ref.current)
)
}
@@ -369,8 +371,8 @@ class Content extends React.Component {
const { value } = editor
const { selection } = value
const window = getWindow(event.target)
const native = window.getSelection()
const range = findRange(native, editor)
const domSelection = window.getSelection()
const range = editor.findRange(domSelection)
if (range && range.equals(selection.toRange())) {
this.updateSelection()
@@ -388,9 +390,9 @@ class Content extends React.Component {
handler === 'onDragStart' ||
handler === 'onDrop'
) {
const closest = event.target.closest('[data-slate-editor]')
const closest = event.target.closest(SELECTORS.EDITOR)
if (closest !== this.element) {
if (closest !== this.ref.current) {
return
}
}
@@ -433,7 +435,7 @@ class Content extends React.Component {
const window = getWindow(event.target)
const { activeElement } = window.document
if (activeElement !== this.element) return
if (activeElement !== this.ref.current) return
this.props.onEvent('onSelect', event)
}, 100)
@@ -458,16 +460,7 @@ class Content extends React.Component {
} = props
const { value } = editor
const Container = tagName
const { document, selection, decorations } = value
const indexes = document.getSelectionIndexes(selection)
const decs = document.getDecorations(editor).concat(decorations)
const childrenDecorations = getChildrenDecorations(document, decs)
const children = document.nodes.toArray().map((child, i) => {
const isSelected = !!indexes && indexes.start <= i && i < indexes.end
return this.renderNode(child, isSelected, childrenDecorations[i])
})
const { document, selection } = value
const style = {
// Prevent the default outline styles.
@@ -486,20 +479,16 @@ class Content extends React.Component {
debug('render', { props })
if (debug.enabled) {
debug.update('render', {
text: value.document.text,
selection: value.selection.toJSON(),
value: value.toJSON(),
})
const data = {
[DATA_ATTRS.EDITOR]: true,
[DATA_ATTRS.KEY]: document.key,
}
return (
<Container
{...handlers}
data-slate-editor
{...data}
ref={this.ref}
data-key={document.key}
contentEditable={readOnly ? null : true}
suppressContentEditableWarning
id={id}
@@ -514,39 +503,20 @@ class Content extends React.Component {
// so we have to disable it like this. (2017/04/24)
data-gramm={false}
>
{children}
<Node
annotations={value.annotations}
block={null}
decorations={List()}
editor={editor}
node={document}
parent={null}
readOnly={readOnly}
selection={selection}
ref={this.tmp.nodeRef}
/>
</Container>
)
}
/**
* Render a `child` node of the document.
*
* @param {Node} child
* @param {Boolean} isSelected
* @return {Element}
*/
renderNode = (child, isSelected, decorations) => {
const { editor, readOnly } = this.props
const { value } = editor
const { document, selection } = value
const { isFocused } = selection
return (
<Node
block={null}
editor={editor}
decorations={decorations}
isSelected={isSelected}
isFocused={isFocused && isSelected}
key={child.key}
node={child}
parent={document}
readOnly={readOnly}
/>
)
}
}
/**

View File

@@ -8,6 +8,7 @@ import warning from 'tiny-warning'
import { Editor as Controller } from 'slate'
import EVENT_HANDLERS from '../constants/event-handlers'
import Content from './content'
import ReactPlugin from '../plugins/react'
/**
@@ -91,6 +92,7 @@ class Editor extends React.Component {
change: null,
resolves: 0,
updates: 0,
contentRef: React.createRef(),
}
/**
@@ -140,25 +142,54 @@ class Editor extends React.Component {
render() {
debug('render', this)
const props = { ...this.props, editor: this }
// Re-resolve the controller if needed based on memoized props.
const { commands, placeholder, plugins, queries, schema } = props
const { commands, placeholder, plugins, queries, schema } = this.props
this.resolveController(plugins, schema, commands, queries, placeholder)
// Set the current props on the controller.
const { options, readOnly, value: valueFromProps } = props
const { options, readOnly, value: valueFromProps } = this.props
const { value: valueFromState } = this.state
const value = valueFromProps || valueFromState
this.controller.setReadOnly(readOnly)
this.controller.setValue(value, options)
const {
autoCorrect,
className,
id,
role,
spellCheck,
tabIndex,
style,
tagName,
} = this.props
const children = (
<Content
ref={this.tmp.contentRef}
autoCorrect={autoCorrect}
className={className}
editor={this}
id={id}
onEvent={(handler, event) => this.run(handler, event)}
readOnly={readOnly}
role={role}
spellCheck={spellCheck}
style={style}
tabIndex={tabIndex}
tagName={tagName}
/>
)
// Render the editor's children with the controller.
const children = this.controller.run('renderEditor', {
...props,
value,
const element = this.controller.run('renderEditor', {
...this.props,
editor: this,
children,
})
return children
return element
}
/**

View File

@@ -1,192 +1,219 @@
import Debug from 'debug'
import React from 'react'
import Types from 'prop-types'
import SlateTypes from 'slate-prop-types'
import ImmutableTypes from 'react-immutable-proptypes'
import OffsetKey from '../utils/offset-key'
import DATA_ATTRS from '../constants/data-attributes'
/**
* Debugger.
*
* @type {Function}
*/
const debug = Debug('slate:leaves')
/**
* Leaf.
* Leaf strings with text in them.
*
* @type {Component}
*/
class Leaf extends React.Component {
/**
* Property types.
*
* @type {Object}
*/
const TextString = ({ text = '', isTrailing = false }) => {
return (
<span
{...{
[DATA_ATTRS.STRING]: true,
}}
>
{text}
{isTrailing ? '\n' : null}
</span>
)
}
static propTypes = {
block: SlateTypes.block.isRequired,
editor: Types.object.isRequired,
index: Types.number.isRequired,
leaves: SlateTypes.leaves.isRequired,
marks: SlateTypes.marks.isRequired,
node: SlateTypes.node.isRequired,
offset: Types.number.isRequired,
parent: SlateTypes.node.isRequired,
text: Types.string.isRequired,
}
/**
* Leaf strings without text, render as zero-width strings.
*
* @type {Component}
*/
/**
* Debug.
*
* @param {String} message
* @param {Mixed} ...args
*/
const ZeroWidthString = ({ length = 0, isLineBreak = false }) => {
return (
<span
{...{
[DATA_ATTRS.ZERO_WIDTH]: isLineBreak ? 'n' : 'z',
[DATA_ATTRS.LENGTH]: length,
}}
>
{'\uFEFF'}
{isLineBreak ? <br /> : null}
</span>
)
}
debug = (message, ...args) => {
debug(message, `${this.props.node.key}-${this.props.index}`, ...args)
}
/**
* Individual leaves in a text node with unique formatting.
*
* @type {Component}
*/
/**
* Should component update?
*
* @param {Object} props
* @return {Boolean}
*/
const Leaf = props => {
const {
marks,
annotations,
decorations,
node,
index,
offset,
text,
editor,
parent,
block,
leaves,
} = props
shouldComponentUpdate(props) {
// If any of the regular properties have changed, re-render.
if (
props.index !== this.props.index ||
props.marks !== this.props.marks ||
props.text !== this.props.text ||
props.parent !== this.props.parent
) {
return true
}
const offsetKey = OffsetKey.stringify({
key: node.key,
index,
})
// Otherwise, don't update.
return false
}
/**
* Render the leaf.
*
* @return {Element}
*/
render() {
this.debug('render', this)
const { node, index } = this.props
const offsetKey = OffsetKey.stringify({
key: node.key,
index,
})
return (
<span data-slate-leaf data-offset-key={offsetKey}>
{this.renderMarks()}
</span>
)
}
/**
* Render all of the leaf's mark components.
*
* @return {Element}
*/
renderMarks() {
const { marks, node, offset, text, editor } = this.props
const leaf = this.renderText()
const attributes = {
'data-slate-mark': true,
}
return marks.reduce((children, mark) => {
const props = {
editor,
mark,
marks,
node,
offset,
text,
children,
attributes,
}
const element = editor.run('renderMark', props)
return element || children
}, leaf)
}
/**
* Render the text content of the leaf, accounting for browsers.
*
* @return {Element}
*/
renderText() {
const { block, node, editor, parent, text, index, leaves } = this.props
let children
if (editor.query('isVoid', parent)) {
// COMPAT: Render text inside void nodes with a zero-width space.
// So the node can contain selection but the text is not visible.
if (editor.query('isVoid', parent)) {
return (
<span data-slate-zero-width="z" data-slate-length={parent.text.length}>
{'\uFEFF'}
</span>
)
}
children = <ZeroWidthString length={parent.text.length} />
} else if (
text === '' &&
parent.object === 'block' &&
parent.text === '' &&
parent.nodes.last() === node
) {
// COMPAT: If this is the last text node in an empty block, render a zero-
// width space that will convert into a line break when copying and pasting
// to support expected plain text.
if (
text === '' &&
parent.object === 'block' &&
parent.text === '' &&
parent.nodes.last() === node
) {
return (
<span data-slate-zero-width="n" data-slate-length={0}>
{'\uFEFF'}
<br />
</span>
)
}
children = <ZeroWidthString isLineBreak />
} else if (text === '') {
// COMPAT: If the text is empty, it's because it's on the edge of an inline
// node, so we render a zero-width space so that the selection can be
// inserted next to it still.
if (text === '') {
return (
<span data-slate-zero-width="z" data-slate-length={0}>
{'\uFEFF'}
</span>
)
}
children = <ZeroWidthString />
} else {
// COMPAT: Browsers will collapse trailing new lines at the end of blocks,
// so we need to add an extra trailing new lines to prevent that.
const lastText = block.getLastText()
const lastChar = text.charAt(text.length - 1)
const isLastText = node === lastText
const isLastLeaf = index === leaves.size - 1
if (isLastText && isLastLeaf && lastChar === '\n')
return <span data-slate-content>{`${text}\n`}</span>
// Otherwise, just return the content.
return <span data-slate-content>{text}</span>
if (isLastText && isLastLeaf && lastChar === '\n') {
children = <TextString isTrailing text={text} />
} else {
children = <TextString text={text} />
}
}
const renderProps = {
editor,
marks,
annotations,
decorations,
node,
offset,
text,
}
// 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 marks) {
const ret = editor.run('renderMark', {
...renderProps,
mark,
children,
attributes: {
[DATA_ATTRS.OBJECT]: 'mark',
},
})
if (ret) {
children = ret
}
}
for (const decoration of decorations) {
const ret = editor.run('renderDecoration', {
...renderProps,
decoration,
children,
attributes: {
[DATA_ATTRS.OBJECT]: 'decoration',
},
})
if (ret) {
children = ret
}
}
for (const annotation of annotations) {
const ret = editor.run('renderAnnotation', {
...renderProps,
annotation,
children,
attributes: {
[DATA_ATTRS.OBJECT]: 'annotation',
},
})
if (ret) {
children = ret
}
}
const attrs = {
[DATA_ATTRS.LEAF]: true,
[DATA_ATTRS.OFFSET_KEY]: offsetKey,
}
return <span {...attrs}>{children}</span>
}
/**
* Prop types.
*
* @type {Object}
*/
Leaf.propTypes = {
annotations: ImmutableTypes.list.isRequired,
block: SlateTypes.block.isRequired,
decorations: ImmutableTypes.list.isRequired,
editor: Types.object.isRequired,
index: Types.number.isRequired,
leaves: Types.object.isRequired,
marks: SlateTypes.marks.isRequired,
node: SlateTypes.node.isRequired,
offset: Types.number.isRequired,
parent: SlateTypes.node.isRequired,
text: Types.string.isRequired,
}
/**
* A memoized version of `Leaf` that updates less frequently.
*
* @type {Component}
*/
const MemoizedLeaf = React.memo(Leaf, (prev, next) => {
return (
next.index === prev.index &&
next.marks === prev.marks &&
next.parent === prev.parent &&
next.block === prev.block &&
next.annotations.equals(prev.annotations) &&
next.decorations.equals(prev.decorations)
)
})
/**
* Export.
*
* @type {Component}
*/
export default Leaf
export default MemoizedLeaf

View File

@@ -4,10 +4,11 @@ import React from 'react'
import SlateTypes from 'slate-prop-types'
import warning from 'tiny-warning'
import Types from 'prop-types'
import { PathUtils } from 'slate'
import Void from './void'
import Text from './text'
import getChildrenDecorations from '../utils/get-children-decorations'
import DATA_ATTRS from '../constants/data-attributes'
/**
* Debug.
@@ -31,16 +32,34 @@ class Node extends React.Component {
*/
static propTypes = {
annotations: ImmutableTypes.map.isRequired,
block: SlateTypes.block,
decorations: ImmutableTypes.list.isRequired,
editor: Types.object.isRequired,
isFocused: Types.bool.isRequired,
isSelected: Types.bool.isRequired,
node: SlateTypes.node.isRequired,
parent: SlateTypes.node.isRequired,
parent: SlateTypes.node,
readOnly: Types.bool.isRequired,
selection: SlateTypes.selection,
}
/**
* Temporary values.
*
* @type {Object}
*/
tmp = {
nodeRefs: {},
}
/**
* A ref for the contenteditable DOM node.
*
* @type {Object}
*/
ref = React.createRef()
/**
* Debug.
*
@@ -77,6 +96,11 @@ class Node extends React.Component {
// needs to be updated or not, return true if it returns true. If it returns
// false, we need to ignore it, because it shouldn't be allowed it.
if (shouldUpdate != null) {
warning(
false,
'As of slate-react@0.22 the `shouldNodeComponentUpdate` middleware is deprecated. You can pass specific values down the tree using React\'s built-in "context" construct instead.'
)
if (shouldUpdate) {
return true
}
@@ -89,24 +113,40 @@ class Node extends React.Component {
// If the `readOnly` status has changed, re-render in case there is any
// user-land logic that depends on it, like nested editable contents.
if (n.readOnly !== p.readOnly) return true
if (n.readOnly !== p.readOnly) {
return true
}
// If the node has changed, update. PERF: There are cases where it will have
// changed, but it's properties will be exactly the same (eg. copy-paste)
// which this won't catch. But that's rare and not a drag on performance, so
// for simplicity we just let them through.
if (n.node !== p.node) return true
if (n.node !== p.node) {
return true
}
// If the selection value of the node or of some of its children has changed,
// re-render in case there is any user-land logic depends on it to render.
// if the node is selected update it, even if it was already selected: the
// selection value of some of its children could have been changed and they
// need to be rendered again.
if (n.isSelected || p.isSelected) return true
if (n.isFocused || p.isFocused) return true
if (
(!n.selection && p.selection) ||
(n.selection && !p.selection) ||
(n.selection && p.selection && !n.selection.equals(p.selection))
) {
return true
}
// If the annotations have changed, update.
if (!n.annotations.equals(p.annotations)) {
return true
}
// If the decorations have changed, update.
if (!n.decorations.equals(p.decorations)) return true
if (!n.decorations.equals(p.decorations)) {
return true
}
// Otherwise, don't update.
return false
@@ -121,32 +161,61 @@ class Node extends React.Component {
render() {
this.debug('render', this)
const {
editor,
isSelected,
isFocused,
node,
annotations,
block,
decorations,
editor,
node,
parent,
readOnly,
selection,
} = this.props
const { value } = editor
const { selection } = value
const indexes = node.getSelectionIndexes(selection, isSelected)
const decs = decorations.concat(node.getDecorations(editor))
const childrenDecorations = getChildrenDecorations(node, decs)
const children = []
node.nodes.forEach((child, i) => {
const isChildSelected = !!indexes && indexes.start <= i && i < indexes.end
const newDecorations = node.getDecorations(editor)
const children = node.nodes.toArray().map((child, i) => {
const Component = child.object === 'text' ? Text : Node
const sel = selection && getRelativeRange(node, i, selection)
children.push(
this.renderNode(child, isChildSelected, childrenDecorations[i])
const decs = newDecorations
.map(d => getRelativeRange(node, i, d))
.filter(d => d)
.concat(decorations)
const anns = annotations
.map(a => getRelativeRange(node, i, a))
.filter(a => a)
return (
<Component
block={node.object === 'block' ? node : block}
editor={editor}
annotations={anns}
decorations={decs}
selection={sel}
key={child.key}
node={child}
parent={node}
readOnly={readOnly}
// COMPAT: We use this map of refs to lookup a DOM node down the
// tree of components by path.
ref={ref => {
if (ref) {
this.tmp.nodeRefs[i] = ref
} else {
delete this.tmp.nodeRefs[i]
}
}}
/>
)
})
// Attributes that the developer must mix into the element in their
// custom node renderer component.
const attributes = { 'data-key': node.key }
const attributes = {
[DATA_ATTRS.OBJECT]: node.object,
[DATA_ATTRS.KEY]: node.key,
ref: this.ref,
}
// If it's a block node with inline children, add the proper `dir` attribute
// for text direction.
@@ -155,56 +224,101 @@ class Node extends React.Component {
if (direction === 'rtl') attributes.dir = 'rtl'
}
const props = {
key: node.key,
let render
if (node.object === 'block') {
render = 'renderBlock'
} else if (node.object === 'document') {
render = 'renderDocument'
} else if (node.object === 'inline') {
render = 'renderInline'
}
const element = editor.run(render, {
attributes,
children,
editor,
isFocused,
isSelected,
isFocused: !!selection && selection.isFocused,
isSelected: !!selection,
node,
parent,
readOnly,
}
const element = editor.run('renderNode', {
...props,
attributes,
children,
})
return editor.query('isVoid', node) ? (
<Void {...this.props}>{element}</Void>
return editor.isVoid(node) ? (
<Void
{...this.props}
textRef={ref => {
if (ref) {
this.tmp.nodeRefs[0] = ref
} else {
delete this.tmp.nodeRefs[0]
}
}}
>
{element}
</Void>
) : (
element
)
}
}
/**
* Render a `child` node.
*
* @param {Node} child
* @param {Boolean} isSelected
* @param {Array<Decoration>} decorations
* @return {Element}
*/
/**
* Return a `range` relative to a child at `index`.
*
* @param {Range} range
* @param {Number} index
* @return {Range}
*/
renderNode = (child, isSelected, decorations) => {
const { block, editor, node, readOnly, isFocused } = this.props
const Component = child.object === 'text' ? Text : Node
return (
<Component
block={node.object === 'block' ? node : block}
decorations={decorations}
editor={editor}
isSelected={isSelected}
isFocused={isFocused && isSelected}
key={child.key}
node={child}
parent={node}
readOnly={readOnly}
/>
)
function getRelativeRange(node, index, range) {
if (range.isUnset) {
return null
}
const child = node.nodes.get(index)
let { start, end } = range
const { path: startPath } = start
const { path: endPath } = end
const startIndex = startPath.first()
const endIndex = endPath.first()
if (startIndex === index) {
start = start.setPath(startPath.rest())
} else if (startIndex < index && index <= endIndex) {
if (child.object === 'text') {
start = start.moveTo(PathUtils.create([index]), 0)
} else {
const [first] = child.texts()
const [, firstPath] = first
start = start.moveTo(firstPath, 0)
}
} else {
start = null
}
if (endIndex === index) {
end = end.setPath(endPath.rest())
} else if (startIndex <= index && index < endIndex) {
if (child.object === 'text') {
end = end.moveTo(PathUtils.create([index]), child.text.length)
} else {
const [last] = child.texts({ direction: 'backward' })
const [lastNode, lastPath] = last
end = end.moveTo(lastPath, lastNode.text.length)
}
} else {
end = null
}
if (!start || !end) {
return null
}
range = range.setStart(start)
range = range.setEnd(end)
return range
}
/**

View File

@@ -1,182 +1,97 @@
import Debug from 'debug'
import ImmutableTypes from 'react-immutable-proptypes'
import Leaf from './leaf'
import { PathUtils } from 'slate'
import React from 'react'
import SlateTypes from 'slate-prop-types'
import Types from 'prop-types'
/**
* Debug.
*
* @type {Function}
*/
const debug = Debug('slate:node')
import Leaf from './leaf'
import DATA_ATTRS from '../constants/data-attributes'
/**
* Text.
* Text node.
*
* @type {Component}
*/
class Text extends React.Component {
/**
* Property types.
*
* @type {Object}
*/
const Text = React.forwardRef((props, ref) => {
const { annotations, block, decorations, node, parent, editor, style } = props
const { key } = node
const leaves = node.getLeaves(annotations, decorations)
let at = 0
static propTypes = {
block: SlateTypes.block,
decorations: ImmutableTypes.list.isRequired,
editor: Types.object.isRequired,
node: SlateTypes.node.isRequired,
parent: SlateTypes.node.isRequired,
style: Types.object,
}
return (
<span
ref={ref}
style={style}
{...{
[DATA_ATTRS.OBJECT]: node.object,
[DATA_ATTRS.KEY]: key,
}}
>
{leaves.map((leaf, index) => {
const { text } = leaf
const offset = at
at += text.length
/**
* Default prop types.
*
* @type {Object}
*/
return (
<Leaf
key={`${node.key}-${index}`}
block={block}
editor={editor}
index={index}
annotations={leaf.annotations}
decorations={leaf.decorations}
marks={leaf.marks}
node={node}
offset={offset}
parent={parent}
leaves={leaves}
text={text}
/>
)
})}
</span>
)
})
static defaultProps = {
style: null,
}
/**
* Prop types.
*
* @type {Object}
*/
/**
* Debug.
*
* @param {String} message
* @param {Mixed} ...args
*/
Text.propTypes = {
annotations: ImmutableTypes.map.isRequired,
block: SlateTypes.block,
decorations: ImmutableTypes.list.isRequired,
editor: Types.object.isRequired,
node: SlateTypes.node.isRequired,
parent: SlateTypes.node.isRequired,
style: Types.object,
}
debug = (message, ...args) => {
const { node } = this.props
const { key } = node
debug(message, `${key} (text)`, ...args)
}
/**
* A memoized version of `Text` that updates less frequently.
*
* @type {Component}
*/
/**
* Should the node update?
*
* @param {Object} nextProps
* @param {Object} value
* @return {Boolean}
*/
shouldComponentUpdate = nextProps => {
const { props } = this
const n = nextProps
const p = props
// If the node has changed, update. PERF: There are cases where it will have
const MemoizedText = React.memo(Text, (prev, next) => {
return (
// PERF: There are cases where it will have
// changed, but it's properties will be exactly the same (eg. copy-paste)
// which this won't catch. But that's rare and not a drag on performance, so
// for simplicity we just let them through.
if (n.node !== p.node) return true
next.node === prev.node &&
// If the node parent is a block node, and it was the last child of the
// block, re-render to cleanup extra `\n`.
if (n.parent.object === 'block') {
const pLast = p.parent.nodes.last()
const nLast = n.parent.nodes.last()
if (p.node === pLast && n.node !== nLast) return true
}
// Re-render if the current decorations have changed.
if (!n.decorations.equals(p.decorations)) return true
// Otherwise, don't update.
return false
}
/**
* Render.
*
* @return {Element}
*/
render() {
this.debug('render', this)
const { decorations, editor, node, style } = this.props
const { value } = editor
const { document } = value
const { key } = node
const decs = decorations.filter(d => {
const { start, end } = d
// If either of the decoration's keys match, include it.
if (start.key === key || end.key === key) return true
// Otherwise, if the decoration is in a single node, it's not ours.
if (start.key === end.key) return false
const path = document.assertPath(key)
const startPath = start.path || document.assertPath(start.key)
const endPath = end.path || document.assertPath(end.key)
// If the node's path is before the start path, ignore it.
if (PathUtils.compare(path, startPath) === -1) return false
// If the node's path is after the end path, ignore it.
if (PathUtils.compare(path, endPath) === 1) return false
// Otherwise, include it.
return true
})
// PERF: Take advantage of cache by avoiding arguments
const leaves = decs.size === 0 ? node.getLeaves() : node.getLeaves(decs)
let offset = 0
const children = leaves.map((leaf, i) => {
const child = this.renderLeaf(leaves, leaf, i, offset)
offset += leaf.text.length
return child
})
return (
<span data-key={key} style={style}>
{children}
</span>
)
}
/**
* Render a single leaf given a `leaf` and `offset`.
*
* @param {List<Leaf>} leaves
* @param {Leaf} leaf
* @param {Number} index
* @param {Number} offset
* @return {Element} leaf
*/
renderLeaf = (leaves, leaf, index, offset) => {
const { block, node, parent, editor } = this.props
const { text, marks } = leaf
return (
<Leaf
key={`${node.key}-${index}`}
block={block}
editor={editor}
index={index}
marks={marks}
node={node}
offset={offset}
parent={parent}
leaves={leaves}
text={text}
/>
)
}
}
(next.parent.object === 'block' &&
prev.parent.nodes.last() === prev.node &&
next.parent.nodes.last() !== next.node) &&
// The formatting hasn't changed.
next.annotations.equals(prev.annotations) &&
next.decorations.equals(prev.decorations)
)
})
/**
* Export.
@@ -184,4 +99,4 @@ class Text extends React.Component {
* @type {Component}
*/
export default Text
export default MemoizedText

View File

@@ -4,6 +4,7 @@ import SlateTypes from 'slate-prop-types'
import Types from 'prop-types'
import Text from './text'
import DATA_ATTRS from '../constants/data-attributes'
/**
* Debug.
@@ -66,8 +67,12 @@ class Void extends React.Component {
position: 'absolute',
}
const spacerAttrs = {
[DATA_ATTRS.SPACER]: true,
}
const spacer = (
<Tag data-slate-spacer style={style}>
<Tag style={style} {...spacerAttrs}>
{this.renderText()}
</Tag>
)
@@ -78,11 +83,15 @@ class Void extends React.Component {
this.debug('render', { props })
const attrs = {
[DATA_ATTRS.VOID]: true,
[DATA_ATTRS.KEY]: node.key,
}
return (
<Tag
data-slate-void
data-key={node.key}
contentEditable={readOnly || node.object === 'block' ? null : false}
{...attrs}
>
{readOnly ? null : spacer}
{content}
@@ -102,10 +111,20 @@ class Void extends React.Component {
*/
renderText = () => {
const { block, decorations, node, readOnly, editor } = this.props
const {
annotations,
block,
decorations,
node,
readOnly,
editor,
textRef,
} = this.props
const child = node.getFirstText()
return (
<Text
ref={textRef}
annotations={annotations}
block={node.object === 'block' ? node : block}
decorations={decorations}
editor={editor}

View File

@@ -0,0 +1,20 @@
/**
* DOM data attribute strings that refer to Slate concepts.
*
* @type {String}
*/
export default {
EDITOR: 'data-slate-editor',
FRAGMENT: 'data-slate-fragment',
KEY: 'data-key',
LEAF: 'data-slate-leaf',
LENGTH: 'data-slate-length',
OBJECT: 'data-slate-object',
OFFSET_KEY: 'data-offset-key',
SPACER: 'data-slate-spacer',
STRING: 'data-slate-string',
TEXT: 'data-slate-object',
VOID: 'data-slate-void',
ZERO_WIDTH: 'data-slate-zero-width',
}

View File

@@ -0,0 +1,20 @@
import DATA_ATTRS from './data-attributes'
/**
* DOM selector strings that refer to Slate concepts.
*
* @type {String}
*/
export default {
BLOCK: `[${DATA_ATTRS.OBJECT}="block"]`,
EDITOR: `[${DATA_ATTRS.EDITOR}]`,
INLINE: `[${DATA_ATTRS.OBJECT}="inline"]`,
KEY: `[${DATA_ATTRS.KEY}]`,
LEAF: `[${DATA_ATTRS.LEAF}]`,
OBJECT: `[${DATA_ATTRS.OBJECT}]`,
STRING: `[${DATA_ATTRS.STRING}]`,
TEXT: `[${DATA_ATTRS.OBJECT}="text"]`,
VOID: `[${DATA_ATTRS.VOID}]`,
ZERO_WIDTH: `[${DATA_ATTRS.ZERO_WIDTH}]`,
}

View File

@@ -4,18 +4,10 @@
* @type {Object}
*/
const TRANSFER_TYPES = {
export default {
FRAGMENT: 'application/x-slate-fragment',
HTML: 'text/html',
NODE: 'application/x-slate-node',
RICH: 'text/rtf',
TEXT: 'text/plain',
}
/**
* Export.
*
* @type {Object}
*/
export default TRANSFER_TYPES

View File

@@ -1,8 +1,11 @@
import Editor from './components/editor'
import cloneFragment from './utils/clone-fragment'
import findDOMNode from './utils/find-dom-node'
import findDOMPoint from './utils/find-dom-point'
import findDOMRange from './utils/find-dom-range'
import findNode from './utils/find-node'
import findPath from './utils/find-path'
import findPoint from './utils/find-point'
import findRange from './utils/find-range'
import getEventRange from './utils/get-event-range'
import getEventTransfer from './utils/get-event-transfer'
@@ -19,8 +22,11 @@ export {
Editor,
cloneFragment,
findDOMNode,
findDOMPoint,
findDOMRange,
findNode,
findPath,
findPoint,
findRange,
getEventRange,
getEventTransfer,
@@ -32,8 +38,11 @@ export default {
Editor,
cloneFragment,
findDOMNode,
findDOMPoint,
findDOMRange,
findNode,
findPath,
findPoint,
findRange,
getEventRange,
getEventTransfer,

View File

@@ -1,5 +1,6 @@
import getSelectionFromDom from './get-selection-from-dom'
import getSelectionFromDom from '../../utils/get-selection-from-dom'
import ElementSnapshot from './element-snapshot'
import SELECTORS from '../../constants/selectors'
/**
* Returns the closest element that matches the selector.
@@ -36,7 +37,7 @@ export default class DomSnapshot {
constructor(window, editor, { before = false } = {}) {
const domSelection = window.getSelection()
const { anchorNode } = domSelection
const subrootEl = closest(anchorNode, '[data-slate-editor] > *')
const subrootEl = closest(anchorNode, `${SELECTORS.EDITOR} > *`)
const elements = [subrootEl]
// The before option is for when we need to take a snapshot of the current
@@ -62,6 +63,6 @@ export default class DomSnapshot {
apply(editor) {
const { snapshot, selection } = this
snapshot.apply()
editor.moveTo(selection.anchor.key, selection.anchor.offset)
editor.moveTo(selection.anchor.path, selection.anchor.offset)
}
}

View File

@@ -1,5 +1,7 @@
import getWindow from 'get-window'
import DATA_ATTRS from '../../constants/data-attributes'
/**
* Is the given node a text node?
*
@@ -91,7 +93,7 @@ function applyElementSnapshot(snapshot, window) {
const key = dataset.key
if (!key) return // if there's no `data-key`, don't remove it
const dups = new window.Set(
Array.from(window.document.querySelectorAll(`[data-key='${key}']`))
Array.from(window.document.querySelectorAll(`[${DATA_ATTRS.KEY}="${key}"]`))
)
dups.delete(el)
dups.forEach(dup => dup.parentElement.removeChild(dup))

View File

@@ -3,14 +3,13 @@ import getWindow from 'get-window'
import pick from 'lodash/pick'
import { ANDROID_API_VERSION } from 'slate-dev-environment'
import fixSelectionInZeroWidthBlock from '../utils/fix-selection-in-zero-width-block'
import getSelectionFromDom from '../utils/get-selection-from-dom'
import setSelectionFromDom from '../utils/set-selection-from-dom'
import setTextFromDomNode from '../utils/set-text-from-dom-node'
import isInputDataEnter from '../utils/is-input-data-enter'
import isInputDataLastChar from '../utils/is-input-data-last-char'
import DomSnapshot from '../utils/dom-snapshot'
import Executor from '../utils/executor'
import fixSelectionInZeroWidthBlock from './fix-selection-in-zero-width-block'
import getSelectionFromDom from '../../utils/get-selection-from-dom'
import setTextFromDomNode from '../../utils/set-text-from-dom-node'
import isInputDataEnter from './is-input-data-enter'
import isInputDataLastChar from './is-input-data-last-char'
import DomSnapshot from './dom-snapshot'
import Executor from './executor'
const debug = Debug('slate:android')
debug.reconcile = Debug('slate:reconcile')
@@ -50,7 +49,7 @@ function AndroidPlugin() {
* certain scenarios like hitting 'enter' at the end of a word.
*
* @type {DomSnapshot} [compositionEndSnapshot]
*/
let compositionEndSnapshot = null
@@ -134,12 +133,13 @@ function AndroidPlugin() {
function reconcile(window, editor, { from }) {
debug.reconcile({ from })
const domSelection = window.getSelection()
const selection = getSelectionFromDom(window, editor, domSelection)
nodes.forEach(node => {
setTextFromDomNode(window, editor, node)
})
setSelectionFromDom(window, editor, domSelection)
editor.select(selection)
nodes.clear()
}
@@ -200,7 +200,7 @@ function AndroidPlugin() {
const selection = getSelectionFromDom(window, editor, domSelection)
preventNextBeforeInput = true
event.preventDefault()
editor.moveTo(selection.anchor.key, selection.anchor.offset)
editor.moveTo(selection.anchor.path, selection.anchor.offset)
editor.splitBlock()
}
} else {
@@ -516,7 +516,7 @@ function AndroidPlugin() {
// have to grab the selection from the DOM.
const domSelection = window.getSelection()
const selection = getSelectionFromDom(window, editor, domSelection)
editor.moveTo(selection.anchor.key, selection.anchor.offset)
editor.moveTo(selection.anchor.path, selection.anchor.offset)
editor.splitBlock()
}
return

View File

@@ -5,15 +5,10 @@ import Plain from 'slate-plain-serializer'
import getWindow from 'get-window'
import { IS_IOS, IS_IE, IS_EDGE } from 'slate-dev-environment'
import cloneFragment from '../utils/clone-fragment'
import findDOMNode from '../utils/find-dom-node'
import findNode from '../utils/find-node'
import findRange from '../utils/find-range'
import getEventRange from '../utils/get-event-range'
import getEventTransfer from '../utils/get-event-transfer'
import setEventTransfer from '../utils/set-event-transfer'
import setSelectionFromDom from '../utils/set-selection-from-dom'
import setTextFromDomNode from '../utils/set-text-from-dom-node'
import cloneFragment from '../../utils/clone-fragment'
import getEventTransfer from '../../utils/get-event-transfer'
import setEventTransfer from '../../utils/set-event-transfer'
import setTextFromDomNode from '../../utils/set-text-from-dom-node'
/**
* Debug.
@@ -65,7 +60,7 @@ function AfterPlugin(options = {}) {
event.preventDefault()
const { document, selection } = value
const range = findRange(targetRange, editor)
const range = editor.findRange(targetRange)
switch (event.inputType) {
case 'deleteByDrag':
@@ -171,12 +166,13 @@ function AfterPlugin(options = {}) {
const { value } = editor
const { document } = value
const node = findNode(event.target, editor)
if (!node) return next()
const path = editor.findPath(event.target)
if (!path) return next()
debug('onClick', { event })
const ancestors = document.getAncestors(node.key)
const node = document.getNode(path)
const ancestors = document.getAncestors(path)
const isVoid =
node && (editor.isVoid(node) || ancestors.some(a => editor.isVoid(a)))
@@ -222,15 +218,21 @@ function AfterPlugin(options = {}) {
// If user cuts a void block node or a void inline node,
// manually removes it since selection is collapsed in this case.
const { value } = editor
const { endBlock, endInline, selection } = value
const { isCollapsed } = selection
const isVoidBlock = endBlock && editor.isVoid(endBlock) && isCollapsed
const isVoidInline = endInline && editor.isVoid(endInline) && isCollapsed
const { document, selection } = value
const { end, isCollapsed } = selection
let voidPath
if (isVoidBlock) {
editor.removeNodeByKey(endBlock.key)
} else if (isVoidInline) {
editor.removeNodeByKey(endInline.key)
if (isCollapsed) {
for (const [node, path] of document.ancestors(end.path)) {
if (editor.isVoid(node)) {
voidPath = path
break
}
}
}
if (voidPath) {
editor.removeNodeByKey(voidPath)
} else {
editor.delete()
}
@@ -268,13 +270,12 @@ function AfterPlugin(options = {}) {
const { value } = editor
const { document } = value
const node = findNode(event.target, editor)
const ancestors = document.getAncestors(node.key)
const path = editor.findPath(event.target)
const node = document.getNode(path)
const ancestors = document.getAncestors(path)
const isVoid =
node && (editor.isVoid(node) || ancestors.some(a => editor.isVoid(a)))
const selectionIncludesNode = value.blocks.some(
block => block.key === node.key
)
const selectionIncludesNode = value.blocks.some(block => block === node)
// If a void block is dragged and is not selected, select it (necessary for local drags).
if (isVoid && !selectionIncludesNode) {
@@ -299,8 +300,11 @@ function AfterPlugin(options = {}) {
const { value } = editor
const { document, selection } = value
const window = getWindow(event.target)
let target = getEventRange(event, editor)
if (!target) return next()
let target = editor.findEventRange(event)
if (!target) {
return next()
}
debug('onDrop', { event })
@@ -313,11 +317,11 @@ function AfterPlugin(options = {}) {
// needs to account for the selection's content being deleted.
if (
isDraggingInternally &&
selection.end.key === target.end.key &&
selection.end.offset < target.end.offset
selection.end.offset < target.end.offset &&
selection.end.path.equals(target.end.path)
) {
target = target.moveForward(
selection.start.key === selection.end.key
selection.start.path.equals(selection.end.path)
? 0 - selection.end.offset + selection.start.offset
: 0 - selection.end.offset
)
@@ -331,15 +335,21 @@ function AfterPlugin(options = {}) {
if (type === 'text' || type === 'html') {
const { anchor } = target
let hasVoidParent = document.hasVoidParent(anchor.key, editor)
let hasVoidParent = document.hasVoidParent(anchor.path, editor)
if (hasVoidParent) {
let n = document.getNode(anchor.key)
let p = anchor.path
let n = document.getNode(anchor.path)
while (hasVoidParent) {
n = document.getNextText(n.key)
if (!n) break
hasVoidParent = document.hasVoidParent(n.key, editor)
const [nxt] = document.texts({ path: p })
if (!nxt) {
break
}
;[n, p] = nxt
hasVoidParent = document.hasVoidParent(p, editor)
}
if (n) editor.moveToStartOfNode(n)
@@ -361,8 +371,7 @@ function AfterPlugin(options = {}) {
// has fired in a node: https://github.com/facebook/react/issues/11379.
// Until this is fixed in React, we dispatch a mouseup event on that
// DOM node, since that will make it go back to normal.
const focusNode = document.getNode(target.focus.key)
const el = findDOMNode(focusNode, window)
const el = editor.findDOMNode(target.focus.path)
if (el) {
el.dispatchEvent(
@@ -411,14 +420,20 @@ function AfterPlugin(options = {}) {
function onInput(event, editor, next) {
debug('onInput')
const window = getWindow(event.target)
const domSelection = window.getSelection()
const selection = editor.findSelection(domSelection)
// Get the selection point.
const selection = window.getSelection()
const { anchorNode } = selection
if (selection) {
editor.select(selection)
} else {
editor.blur()
}
const { anchorNode } = domSelection
setTextFromDomNode(window, editor, anchorNode)
setSelectionFromDom(window, editor, selection)
next()
}
@@ -435,7 +450,8 @@ function AfterPlugin(options = {}) {
const { value } = editor
const { document, selection } = value
const hasVoidParent = document.hasVoidParent(selection.start.path, editor)
const { start } = selection
const hasVoidParent = document.hasVoidParent(start.path, editor)
// COMPAT: In iOS, some of these hotkeys are handled in the
// `onNativeBeforeInput` handler of the `<Content>` component in order to
@@ -535,20 +551,34 @@ function AfterPlugin(options = {}) {
}
if (Hotkeys.isExtendBackward(event)) {
const { previousText, startText } = value
const isPreviousInVoid =
previousText && document.hasVoidParent(previousText.key, editor)
const startText = document.getNode(start.path)
const prevEntry = document.texts({
path: start.path,
direction: 'backward',
})
if (hasVoidParent || isPreviousInVoid || startText.text === '') {
let isPrevInVoid = false
if (prevEntry) {
const [, prevPath] = prevEntry
isPrevInVoid = document.hasVoidParent(prevPath, editor)
}
if (hasVoidParent || isPrevInVoid || startText.text === '') {
event.preventDefault()
return editor.moveFocusBackward()
}
}
if (Hotkeys.isExtendForward(event)) {
const { nextText, startText } = value
const isNextInVoid =
nextText && document.hasVoidParent(nextText.key, editor)
const startText = document.getNode(start.path)
const [nextEntry] = document.texts({ path: start.path })
let isNextInVoid = false
if (nextEntry) {
const [, nextPath] = nextEntry
isNextInVoid = document.hasVoidParent(nextPath, editor)
}
if (hasVoidParent || isNextInVoid || startText.text === '') {
event.preventDefault()
@@ -632,8 +662,14 @@ function AfterPlugin(options = {}) {
function onSelect(event, editor, next) {
debug('onSelect', { event })
const window = getWindow(event.target)
const selection = window.getSelection()
setSelectionFromDom(window, editor, selection)
const domSelection = window.getSelection()
const selection = editor.findSelection(domSelection)
if (selection) {
editor.select(selection)
} else {
editor.blur()
}
// COMPAT: reset the `isMouseDown` state here in case a `mouseup` event
// happens outside the editor. This is needed for `onFocus` handling.

View File

@@ -1,6 +1,5 @@
import Debug from 'debug'
import Hotkeys from 'slate-hotkeys'
import ReactDOM from 'react-dom'
import getWindow from 'get-window'
import {
IS_FIREFOX,
@@ -9,7 +8,7 @@ import {
HAS_INPUT_EVENTS_LEVEL_2,
} from 'slate-dev-environment'
import findNode from '../utils/find-node'
import DATA_ATTRS from '../../constants/data-attributes'
/**
* Debug.
@@ -77,7 +76,7 @@ function BeforePlugin() {
// COMPAT: The `relatedTarget` can be null when the new focus target is not
// a "focusable" element (eg. a `<div>` without `tabindex` set).
if (relatedTarget) {
const el = ReactDOM.findDOMNode(editor)
const el = editor.findDOMNode([])
// COMPAT: The event should be ignored if the focus is returning to the
// editor from an embedded editable element (eg. an <input> element inside
@@ -86,13 +85,16 @@ function BeforePlugin() {
// COMPAT: The event should be ignored if the focus is moving from the
// editor to inside a void node's spacer element.
if (relatedTarget.hasAttribute('data-slate-spacer')) return
if (relatedTarget.hasAttribute(DATA_ATTRS.SPACER)) return
// COMPAT: The event should be ignored if the focus is moving to a non-
// editable section of an element that isn't a void node (eg. a list item
// of the check list example).
const node = findNode(relatedTarget, editor)
if (el.contains(relatedTarget) && node && !editor.isVoid(node)) return
const node = editor.findNode(relatedTarget)
if (el.contains(relatedTarget) && node && !editor.isVoid(node)) {
return
}
}
debug('onBlur', { event })
@@ -267,8 +269,11 @@ function BeforePlugin() {
// call `preventDefault` to signal that drops are allowed.
// When the target is editable, dropping is already allowed by
// default, and calling `preventDefault` hides the cursor.
const node = findNode(event.target, editor)
if (editor.isVoid(node)) event.preventDefault()
const node = editor.findNode(event.target)
if (editor.isVoid(node)) {
event.preventDefault()
}
// COMPAT: IE won't call onDrop on contentEditables unless the
// default dragOver is prevented:
@@ -337,7 +342,7 @@ function BeforePlugin() {
if (isCopying) return
if (editor.readOnly) return
const el = ReactDOM.findDOMNode(editor)
const el = editor.findDOMNode([])
// Save the new `activeElement`.
const window = getWindow(event.target)

View File

@@ -1,5 +1,5 @@
import { IS_ANDROID } from 'slate-dev-environment'
import AndroidPlugin from './android'
import AndroidPlugin from '../android'
import AfterPlugin from './after'
import BeforePlugin from './before'
@@ -12,12 +12,14 @@ import BeforePlugin from './before'
function DOMPlugin(options = {}) {
const { plugins = [] } = options
// Add Android specific handling separately before it gets to the other
// plugins because it is specific (other browser don't need it) and finicky
// (it has to come before other plugins to work).
const beforeBeforePlugins = IS_ANDROID ? [AndroidPlugin()] : []
const beforePlugin = BeforePlugin()
const afterPlugin = AfterPlugin()
// COMPAT: Add Android specific handling separately before it gets to the
// other plugins because it is specific (other browser don't need it) and
// finicky (it has to come before other plugins to work).
const beforeBeforePlugins = IS_ANDROID ? [AndroidPlugin()] : []
return [...beforeBeforePlugins, beforePlugin, ...plugins, afterPlugin]
}

View File

@@ -1,144 +0,0 @@
import PlaceholderPlugin from 'slate-react-placeholder'
import React from 'react'
import DOMPlugin from './dom'
import Content from '../components/content'
import EVENT_HANDLERS from '../constants/event-handlers'
/**
* Props that can be defined by plugins.
*
* @type {Array}
*/
const PROPS = [
...EVENT_HANDLERS,
'commands',
'decorateNode',
'queries',
'renderEditor',
'renderMark',
'renderNode',
'schema',
]
/**
* A plugin that adds the React-specific rendering logic to the editor.
*
* @param {Object} options
* @return {Object}
*/
function ReactPlugin(options = {}) {
const { placeholder, plugins = [] } = options
/**
* Decorate node.
*
* @param {Object} node
* @param {Editor} editor
* @param {Function} next
* @return {Array}
*/
function decorateNode(node, editor, next) {
return []
}
/**
* Render editor.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Element}
*/
function renderEditor(props, editor, next) {
return (
<Content
autoCorrect={props.autoCorrect}
className={props.className}
editor={editor}
id={props.id}
onEvent={(handler, event) => editor.run(handler, event)}
readOnly={props.readOnly}
role={props.role}
spellCheck={props.spellCheck}
style={props.style}
tabIndex={props.tabIndex}
tagName={props.tagName}
/>
)
}
/**
* Render node.
*
* @param {Object} props
* @param {Editor} editor
* @param {Function} next
* @return {Element}
*/
function renderNode(props, editor, next) {
const { attributes, children, node } = props
const { object } = node
if (object !== 'block' && object !== 'inline') return null
const Tag = object === 'block' ? 'div' : 'span'
const style = { position: 'relative' }
return (
<Tag {...attributes} style={style}>
{children}
</Tag>
)
}
/**
* Return the plugins.
*
* @type {Array}
*/
const ret = []
const editorPlugin = PROPS.reduce((memo, prop) => {
if (prop in options) memo[prop] = options[prop]
return memo
}, {})
ret.push(
DOMPlugin({
plugins: [editorPlugin, ...plugins],
})
)
if (placeholder) {
ret.push(
PlaceholderPlugin({
placeholder,
when: (editor, node) =>
node.object === 'document' &&
node.text === '' &&
node.nodes.size === 1 &&
node.getTexts().size === 1,
})
)
}
ret.push({
decorateNode,
renderEditor,
renderNode,
})
return ret
}
/**
* Export.
*
* @type {Function}
*/
export default ReactPlugin

View File

@@ -0,0 +1,46 @@
import EVENT_HANDLERS from '../../constants/event-handlers'
/**
* Props that can be defined by plugins.
*
* @type {Array}
*/
const PROPS = [
...EVENT_HANDLERS,
'commands',
'decorateNode',
'queries',
'renderAnnotation',
'renderBlock',
'renderDecoration',
'renderDocument',
'renderEditor',
'renderInline',
'renderMark',
'schema',
]
/**
* The top-level editor props in a plugin.
*
* @param {Object} options
* @return {Object}
*/
function EditorPropsPlugin(options = {}) {
const plugin = PROPS.reduce((memo, prop) => {
if (prop in options) memo[prop] = options[prop]
return memo
}, {})
return plugin
}
/**
* Export.
*
* @type {Function}
*/
export default EditorPropsPlugin

View File

@@ -0,0 +1,42 @@
import PlaceholderPlugin from 'slate-react-placeholder'
import EditorPropsPlugin from './editor-props'
import RenderingPlugin from './rendering'
import QueriesPlugin from './queries'
import DOMPlugin from '../dom'
/**
* A plugin that adds the React-specific rendering logic to the editor.
*
* @param {Object} options
* @return {Object}
*/
function ReactPlugin(options = {}) {
const { placeholder = '', plugins = [] } = options
const renderingPlugin = RenderingPlugin(options)
const queriesPlugin = QueriesPlugin(options)
const editorPropsPlugin = EditorPropsPlugin(options)
const domPlugin = DOMPlugin({
plugins: [editorPropsPlugin, ...plugins],
})
const placeholderPlugin = PlaceholderPlugin({
placeholder,
when: (editor, node) =>
node.object === 'document' &&
node.text === '' &&
node.nodes.size === 1 &&
Array.from(node.texts()).length === 1,
})
return [domPlugin, placeholderPlugin, renderingPlugin, queriesPlugin]
}
/**
* Export.
*
* @type {Function}
*/
export default ReactPlugin

View File

@@ -0,0 +1,623 @@
import getWindow from 'get-window'
import { PathUtils } from 'slate'
import DATA_ATTRS from '../../constants/data-attributes'
import SELECTORS from '../../constants/selectors'
/**
* A set of queries for the React plugin.
*
* @return {Object}
*/
function QueriesPlugin() {
/**
* Find the native DOM element for a node at `path`.
*
* @param {Editor} editor
* @param {Array|List} path
* @return {DOMNode|Null}
*/
function findDOMNode(editor, path) {
path = PathUtils.create(path)
const content = editor.tmp.contentRef.current
if (!path.size) {
return content.ref.current || null
}
const search = (instance, p) => {
if (!instance) {
return null
}
if (!p.size) {
if (instance.ref) {
return instance.ref.current || null
} else {
return instance || null
}
}
const index = p.first()
const rest = p.rest()
const ref = instance.tmp.nodeRefs[index]
return search(ref, rest)
}
const document = content.tmp.nodeRef.current
const el = search(document, path)
return el
}
/**
* Find a native DOM selection point from a Slate `point`.
*
* @param {Editor} editor
* @param {Point} point
* @return {Object|Null}
*/
function findDOMPoint(editor, point) {
const el = editor.findDOMNode(point.path)
let start = 0
if (!el) {
return null
}
// For each leaf, we need to isolate its content, which means filtering to its
// direct text and zero-width spans. (We have to filter out any other siblings
// that may have been rendered alongside them.)
const texts = Array.from(
el.querySelectorAll(`${SELECTORS.STRING}, ${SELECTORS.ZERO_WIDTH}`)
)
for (const text of texts) {
const node = text.childNodes[0]
const domLength = node.textContent.length
let slateLength = domLength
if (text.hasAttribute(DATA_ATTRS.LENGTH)) {
slateLength = parseInt(text.getAttribute(DATA_ATTRS.LENGTH), 10)
}
const end = start + slateLength
if (point.offset <= end) {
const offset = Math.min(domLength, Math.max(0, point.offset - start))
return { node, offset }
}
start = end
}
return null
}
/**
* Find a native DOM range from a Slate `range`.
*
* @param {Editor} editor
* @param {Range} range
* @return {DOMRange|Null}
*/
function findDOMRange(editor, range) {
const { anchor, focus, isBackward, isCollapsed } = range
const domAnchor = editor.findDOMPoint(anchor)
const domFocus = isCollapsed ? domAnchor : editor.findDOMPoint(focus)
if (!domAnchor || !domFocus) {
return null
}
const window = getWindow(domAnchor.node)
const r = window.document.createRange()
const start = isBackward ? domFocus : domAnchor
const end = isBackward ? domAnchor : domFocus
r.setStart(start.node, start.offset)
r.setEnd(end.node, end.offset)
return r
}
/**
* Find a Slate node from a native DOM `element`.
*
* @param {Editor} editor
* @param {Element} element
* @return {List|Null}
*/
function findNode(editor, element) {
const path = editor.findPath(element)
if (!path) {
return null
}
const { value } = editor
const { document } = value
const node = document.getNode(path)
return node
}
/**
* Get the target range from a DOM `event`.
*
* @param {Event} event
* @param {Editor} editor
* @return {Range}
*/
function findEventRange(editor, event) {
if (event.nativeEvent) {
event = event.nativeEvent
}
const { clientX: x, clientY: y, target } = event
if (x == null || y == null) return null
const { value } = editor
const { document } = value
const path = editor.findPath(event.target)
if (!path) return null
const node = document.getNode(path)
// If the drop target is inside a void node, move it into either the next or
// previous node, depending on which side the `x` and `y` coordinates are
// closest to.
if (editor.isVoid(node)) {
const rect = target.getBoundingClientRect()
const isPrevious =
node.object === 'inline'
? x - rect.left < rect.left + rect.width - x
: y - rect.top < rect.top + rect.height - y
const range = document.createRange()
const iterable = isPrevious ? 'previousTexts' : 'nextTexts'
const move = isPrevious ? 'moveToEndOfNode' : 'moveToStartOfNode'
const entry = document[iterable](path)
if (entry) {
const [n] = entry
return range[move](n)
}
return null
}
// Else resolve a range from the caret position where the drop occured.
const window = getWindow(target)
let native
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
if (window.document.caretRangeFromPoint) {
native = window.document.caretRangeFromPoint(x, y)
} else if (window.document.caretPositionFromPoint) {
const position = window.document.caretPositionFromPoint(x, y)
native = window.document.createRange()
native.setStart(position.offsetNode, position.offset)
native.setEnd(position.offsetNode, position.offset)
} else if (window.document.body.createTextRange) {
// COMPAT: In IE, `caretRangeFromPoint` and
// `caretPositionFromPoint` don't exist. (2018/07/11)
native = window.document.body.createTextRange()
try {
native.moveToPoint(x, y)
} catch (error) {
// IE11 will raise an `unspecified error` if `moveToPoint` is
// called during a dropEvent.
return null
}
}
// Resolve a Slate range from the DOM range.
const range = editor.findRange(native)
return range
}
/**
* Find the path of a native DOM `element` by searching React refs.
*
* @param {Editor} editor
* @param {Element} element
* @return {List|Null}
*/
function findPath(editor, element) {
const content = editor.tmp.contentRef.current
if (element === content.ref.current) {
return PathUtils.create([])
}
const search = (instance, p) => {
if (element === instance) {
return p
}
if (!instance.ref) {
return null
}
if (element === instance.ref.current) {
return p
}
// If there's no `tmp` then we're at a leaf node without success.
if (!instance.tmp) {
return null
}
const { nodeRefs } = instance.tmp
const keys = Object.keys(nodeRefs)
for (const i of keys) {
const ref = nodeRefs[i]
const n = parseInt(i, 10)
const path = search(ref, [...p, n])
if (path) {
return path
}
}
return null
}
const document = content.tmp.nodeRef.current
const path = search(document, [])
if (!path) {
return null
}
return PathUtils.create(path)
}
/**
* Find a Slate point from a DOM selection's `nativeNode` and `nativeOffset`.
*
* @param {Editor} editor
* @param {Element} nativeNode
* @param {Number} nativeOffset
* @return {Point}
*/
function findPoint(editor, nativeNode, nativeOffset) {
const { node: nearestNode, offset: nearestOffset } = normalizeNodeAndOffset(
nativeNode,
nativeOffset
)
const window = getWindow(nativeNode)
const { parentNode } = nearestNode
let leafNode = parentNode.closest(SELECTORS.LEAF)
let textNode
let offset
let node
// Calculate how far into the text node the `nearestNode` is, so that we can
// determine what the offset relative to the text node is.
if (leafNode) {
textNode = leafNode.closest(SELECTORS.TEXT)
const range = window.document.createRange()
range.setStart(textNode, 0)
range.setEnd(nearestNode, nearestOffset)
const contents = range.cloneContents()
const zeroWidths = contents.querySelectorAll(SELECTORS.ZERO_WIDTH)
Array.from(zeroWidths).forEach(el => {
el.parentNode.removeChild(el)
})
// COMPAT: Edge has a bug where Range.prototype.toString() will convert \n
// into \r\n. The bug causes a loop when slate-react attempts to reposition
// its cursor to match the native position. Use textContent.length instead.
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10291116/
offset = contents.textContent.length
node = textNode
} else {
// For void nodes, the element with the offset key will be a cousin, not an
// ancestor, so find it by going down from the nearest void parent.
const voidNode = parentNode.closest(SELECTORS.VOID)
if (!voidNode) {
return null
}
leafNode = voidNode.querySelector(SELECTORS.LEAF)
if (!leafNode) {
return null
}
textNode = leafNode.closest(SELECTORS.TEXT)
node = leafNode
offset = node.textContent.length
}
// COMPAT: If the parent node is a Slate zero-width space, this is because the
// text node should have no characters. However, during IME composition the
// ASCII characters will be prepended to the zero-width space, so subtract 1
// from the offset to account for the zero-width space character.
if (
offset === node.textContent.length &&
parentNode.hasAttribute(DATA_ATTRS.ZERO_WIDTH)
) {
offset--
}
// COMPAT: If someone is clicking from one Slate editor into another, the
// select event fires twice, once for the old editor's `element` first, and
// then afterwards for the correct `element`. (2017/03/03)
const path = editor.findPath(textNode)
if (!path) {
return null
}
const { value } = editor
const { document } = value
const point = document.createPoint({ path, offset })
return point
}
/**
* Find a Slate range from a DOM range or selection.
*
* @param {Editor} editor
* @param {Selection} domRange
* @return {Range}
*/
function findRange(editor, domRange) {
const el = domRange.anchorNode || domRange.startContainer
if (!el) {
return null
}
const window = getWindow(el)
// If the `domRange` object is a DOM `Range` or `StaticRange` object, change it
// into something that looks like a DOM `Selection` instead.
if (
domRange instanceof window.Range ||
(window.StaticRange && domRange instanceof window.StaticRange)
) {
domRange = {
anchorNode: domRange.startContainer,
anchorOffset: domRange.startOffset,
focusNode: domRange.endContainer,
focusOffset: domRange.endOffset,
}
}
const {
anchorNode,
anchorOffset,
focusNode,
focusOffset,
isCollapsed,
} = domRange
const { value } = editor
const anchor = editor.findPoint(anchorNode, anchorOffset)
const focus = isCollapsed
? anchor
: editor.findPoint(focusNode, focusOffset)
if (!anchor || !focus) {
return null
}
const { document } = value
const range = document.createRange({
anchor,
focus,
})
return range
}
/**
* Find a Slate selection from a DOM selection.
*
* @param {Editor} editor
* @param {Selection} domSelection
* @return {Range}
*/
function findSelection(editor, domSelection) {
const { value } = editor
const { document } = value
// If there are no ranges, the editor was blurred natively.
if (!domSelection.rangeCount) {
return null
}
// Otherwise, determine the Slate selection from the native one.
let range = editor.findRange(domSelection)
if (!range) {
return null
}
const { anchor, focus } = range
const anchorText = document.getNode(anchor.path)
const focusText = document.getNode(focus.path)
const anchorInline = document.getClosestInline(anchor.path)
const focusInline = document.getClosestInline(focus.path)
const focusBlock = document.getClosestBlock(focus.path)
const anchorBlock = document.getClosestBlock(anchor.path)
// COMPAT: If the anchor point is at the start of a non-void, and the
// focus point is inside a void node with an offset that isn't `0`, set
// the focus offset to `0`. This is due to void nodes <span>'s being
// positioned off screen, resulting in the offset always being greater
// than `0`. Since we can't know what it really should be, and since an
// offset of `0` is less destructive because it creates a hanging
// selection, go with `0`. (2017/09/07)
if (
anchorBlock &&
!editor.isVoid(anchorBlock) &&
anchor.offset === 0 &&
focusBlock &&
editor.isVoid(focusBlock) &&
focus.offset !== 0
) {
range = range.setFocus(focus.setOffset(0))
}
// COMPAT: If the selection is at the end of a non-void inline node, and
// there is a node after it, put it in the node after instead. This
// standardizes the behavior, since it's indistinguishable to the user.
if (
anchorInline &&
!editor.isVoid(anchorInline) &&
anchor.offset === anchorText.text.length
) {
const block = document.getClosestBlock(anchor.path)
const [next] = block.texts({ path: anchor.path })
if (next) {
const [, nextPath] = next
range = range.moveAnchorTo(nextPath, 0)
}
}
if (
focusInline &&
!editor.isVoid(focusInline) &&
focus.offset === focusText.text.length
) {
const block = document.getClosestBlock(focus.path)
const [next] = block.texts({ path: focus.path })
if (next) {
const [, nextPath] = next
range = range.moveFocusTo(nextPath, 0)
}
}
let selection = document.createSelection(range)
// COMPAT: Ensure that the `isFocused` argument is set.
selection = selection.setIsFocused(true)
// COMPAT: Preserve the marks, since we have no way of knowing what the DOM
// selection's marks were. They will be cleared automatically by the
// `select` command if the selection moves.
selection = selection.set('marks', value.selection.marks)
return selection
}
return {
queries: {
findDOMNode,
findDOMPoint,
findDOMRange,
findEventRange,
findNode,
findPath,
findPoint,
findRange,
findSelection,
},
}
}
/**
* From a DOM selection's `node` and `offset`, normalize so that it always
* refers to a text node.
*
* @param {Element} node
* @param {Number} offset
* @return {Object}
*/
function normalizeNodeAndOffset(node, offset) {
// If it's an element node, its offset refers to the index of its children
// including comment nodes, so try to find the right text child node.
if (node.nodeType === 1 && node.childNodes.length) {
const isLast = offset === node.childNodes.length
const direction = isLast ? 'backward' : 'forward'
const index = isLast ? offset - 1 : offset
node = getEditableChild(node, index, direction)
// If the node has children, traverse until we have a leaf node. Leaf nodes
// can be either text nodes, or other void DOM nodes.
while (node.nodeType === 1 && node.childNodes.length) {
const i = isLast ? node.childNodes.length - 1 : 0
node = getEditableChild(node, i, direction)
}
// Determine the new offset inside the text node.
offset = isLast ? node.textContent.length : 0
}
// Return the node and offset.
return { node, offset }
}
/**
* Get the nearest editable child at `index` in a `parent`, preferring
* `direction`.
*
* @param {Element} parent
* @param {Number} index
* @param {String} direction ('forward' or 'backward')
* @return {Element|Null}
*/
function getEditableChild(parent, index, direction) {
const { childNodes } = parent
let child = childNodes[index]
let i = index
let triedForward = false
let triedBackward = false
// While the child is a comment node, or an element node with no children,
// keep iterating to find a sibling non-void, non-comment node.
while (
child.nodeType === 8 ||
(child.nodeType === 1 && child.childNodes.length === 0) ||
(child.nodeType === 1 && child.getAttribute('contenteditable') === 'false')
) {
if (triedForward && triedBackward) break
if (i >= childNodes.length) {
triedForward = true
i = index - 1
direction = 'backward'
continue
}
if (i < 0) {
triedBackward = true
i = index + 1
direction = 'forward'
continue
}
child = childNodes[i]
if (direction === 'forward') i++
if (direction === 'backward') i--
}
return child || null
}
/**
* Export.
*
* @type {Function}
*/
export default QueriesPlugin

View File

@@ -0,0 +1,59 @@
import React from 'react'
/**
* The default rendering behavior for the React plugin.
*
* @return {Object}
*/
function Rendering() {
return {
decorateNode() {
return []
},
renderAnnotation({ attributes, children }) {
return <span {...attributes}>{children}</span>
},
renderBlock({ attributes, children }) {
return (
<div {...attributes} style={{ position: 'relative' }}>
{children}
</div>
)
},
renderDecoration({ attributes, children }) {
return <span {...attributes}>{children}</span>
},
renderDocument({ children }) {
return children
},
renderEditor({ children }) {
return children
},
renderInline({ attributes, children }) {
return (
<span {...attributes} style={{ position: 'relative' }}>
{children}
</span>
)
},
renderMark({ attributes, children }) {
return <span {...attributes}>{children}</span>
},
}
}
/**
* Export.
*
* @type {Function}
*/
export default Rendering

View File

@@ -1,49 +0,0 @@
import { IS_ANDROID } from 'slate-dev-environment'
/**
* Array of regular expression matchers and their API version
*
* @type {Array}
*/
const ANDROID_API_VERSIONS = [
[/^9([.]0|)/, 28],
[/^8[.]1/, 27],
[/^8([.]0|)/, 26],
[/^7[.]1/, 25],
[/^7([.]0|)/, 24],
[/^6([.]0|)/, 23],
[/^5[.]1/, 22],
[/^5([.]0|)/, 21],
[/^4[.]4/, 20],
]
/**
* get the Android API version from the userAgent
*
* @return {Number} version
*/
function getApiVersion() {
if (!IS_ANDROID) return null
const { userAgent } = window.navigator
const matchData = userAgent.match(/Android\s([0-9\.]+)/)
if (matchData == null) return null
const versionString = matchData[1]
for (const tuple of ANDROID_API_VERSIONS) {
const [regex, version] = tuple
if (versionString.match(regex)) return version
}
return null
}
const API_VERSION = getApiVersion()
/**
* Export.
*
* type {number}
*/
export default API_VERSION

View File

@@ -1,13 +1,15 @@
import Base64 from 'slate-base64-serializer'
import Plain from 'slate-plain-serializer'
import TRANSFER_TYPES from '../constants/transfer-types'
import findDOMNode from './find-dom-node'
import getWindow from 'get-window'
import invariant from 'tiny-invariant'
import removeAllRanges from './remove-all-ranges'
import { IS_IE } from 'slate-dev-environment'
import { Value } from 'slate'
import { ZERO_WIDTH_SELECTOR, ZERO_WIDTH_ATTRIBUTE } from './find-point'
import TRANSFER_TYPES from '../constants/transfer-types'
import removeAllRanges from './remove-all-ranges'
import findDOMNode from './find-dom-node'
import DATA_ATTRS from '../constants/data-attributes'
import SELECTORS from '../constants/selectors'
const { FRAGMENT, HTML, TEXT } = TRANSFER_TYPES
@@ -29,8 +31,8 @@ function cloneFragment(event, editor, callback = () => undefined) {
const { value } = editor
const { document, fragment, selection } = value
const { start, end } = selection
const startVoid = document.getClosestVoid(start.key, editor)
const endVoid = document.getClosestVoid(end.key, editor)
const startVoid = document.getClosestVoid(start.path, editor)
const endVoid = document.getClosestVoid(end.path, editor)
// If the selection is collapsed, and it isn't inside a void node, abort.
if (native.isCollapsed && !startVoid) return
@@ -69,10 +71,12 @@ function cloneFragment(event, editor, callback = () => undefined) {
// Remove any zero-width space spans from the cloned DOM so that they don't
// show up elsewhere when pasted.
;[].slice.call(contents.querySelectorAll(ZERO_WIDTH_SELECTOR)).forEach(zw => {
const isNewline = zw.getAttribute(ZERO_WIDTH_ATTRIBUTE) === 'n'
zw.textContent = isNewline ? '\n' : ''
})
;[].slice
.call(contents.querySelectorAll(SELECTORS.ZERO_WIDTH))
.forEach(zw => {
const isNewline = zw.getAttribute(DATA_ATTRS.ZERO_WIDTH) === 'n'
zw.textContent = isNewline ? '\n' : ''
})
// Set a `data-slate-fragment` attribute on a non-empty node, so it shows up
// in the HTML, and can be used for intra-Slate pasting. If it's a text
@@ -89,7 +93,7 @@ function cloneFragment(event, editor, callback = () => undefined) {
attach = span
}
attach.setAttribute('data-slate-fragment', encoded)
attach.setAttribute(DATA_ATTRS.FRAGMENT, encoded)
// Creates value from only the selected blocks
// Then gets plaintext for clipboard with proper linebreaks for BLOCK elements
@@ -120,7 +124,7 @@ function cloneFragment(event, editor, callback = () => undefined) {
// COMPAT: For browser that don't support the Clipboard API's setData method,
// we must rely on the browser to natively copy what's selected.
// So we add the div (containing our content) to the DOM, and select it.
const editorEl = event.target.closest('[data-slate-editor]')
const editorEl = event.target.closest(SELECTORS.EDITOR)
div.setAttribute('contenteditable', true)
div.style.position = 'absolute'
div.style.left = '-9999px'

View File

@@ -1,18 +0,0 @@
/**
* Find the deepest descendant of a DOM `element`.
*
* @param {Element} node
* @return {Element}
*/
function findDeepestNode(element) {
return element.firstChild ? findDeepestNode(element.firstChild) : element
}
/**
* Export.
*
* @type {Function}
*/
export default findDeepestNode

View File

@@ -1,4 +1,7 @@
import { Node } from 'slate'
import warning from 'tiny-warning'
import DATA_ATTRS from '../constants/data-attributes'
/**
* Find the DOM node for a `key`.
@@ -9,11 +12,16 @@ import { Node } from 'slate'
*/
function findDOMNode(key, win = window) {
warning(
false,
'As of slate-react@0.22 the `findDOMNode(key)` helper is deprecated in favor of `editor.findDOMNode(path)`.'
)
if (Node.isNode(key)) {
key = key.key
}
const el = win.document.querySelector(`[data-key="${key}"]`)
const el = win.document.querySelector(`[${DATA_ATTRS.KEY}="${key}"]`)
if (!el) {
throw new Error(

View File

@@ -1,4 +1,8 @@
import findDOMNode from './find-dom-node'
import warning from 'tiny-warning'
import DATA_ATTRS from '../constants/data-attributes'
import SELECTORS from '../constants/selectors'
/**
* Find a native DOM selection point from a Slate `point`.
@@ -9,6 +13,11 @@ import findDOMNode from './find-dom-node'
*/
function findDOMPoint(point, win = window) {
warning(
false,
'As of slate-react@0.22 the `findDOMPoint(point)` helper is deprecated in favor of `editor.findDOMPoint(point)`.'
)
const el = findDOMNode(point.key, win)
let start = 0
@@ -16,7 +25,7 @@ function findDOMPoint(point, win = window) {
// direct text and zero-width spans. (We have to filter out any other siblings
// that may have been rendered alongside them.)
const texts = Array.from(
el.querySelectorAll('[data-slate-content], [data-slate-zero-width]')
el.querySelectorAll(`${SELECTORS.STRING}, ${SELECTORS.ZERO_WIDTH}`)
)
for (const text of texts) {
@@ -24,8 +33,8 @@ function findDOMPoint(point, win = window) {
const domLength = node.textContent.length
let slateLength = domLength
if (text.hasAttribute('data-slate-length')) {
slateLength = parseInt(text.getAttribute('data-slate-length'), 10)
if (text.hasAttribute(DATA_ATTRS.LENGTH)) {
slateLength = parseInt(text.getAttribute(DATA_ATTRS.LENGTH), 10)
}
const end = start + slateLength

View File

@@ -1,4 +1,5 @@
import findDOMPoint from './find-dom-point'
import warning from 'tiny-warning'
/**
* Find a native DOM range Slate `range`.
@@ -9,6 +10,11 @@ import findDOMPoint from './find-dom-point'
*/
function findDOMRange(range, win = window) {
warning(
false,
'As of slate-react@0.22 the `findDOMRange(range)` helper is deprecated in favor of `editor.findDOMRange(range)`.'
)
const { anchor, focus, isBackward, isCollapsed } = range
const domAnchor = findDOMPoint(anchor, win)
const domFocus = isCollapsed ? domAnchor : findDOMPoint(focus, win)

View File

@@ -1,6 +1,10 @@
import invariant from 'tiny-invariant'
import warning from 'tiny-warning'
import { Value } from 'slate'
import DATA_ATTRS from '../constants/data-attributes'
import SELECTORS from '../constants/selectors'
/**
* Find a Slate node from a DOM `element`.
*
@@ -10,15 +14,20 @@ import { Value } from 'slate'
*/
function findNode(element, editor) {
warning(
false,
'As of slate-react@0.22 the `findNode(element)` helper is deprecated in favor of `editor.findNode(element)`.'
)
invariant(
!Value.isValue(editor),
'As of Slate 0.42.0, the `findNode` utility takes an `editor` instead of a `value`.'
)
const closest = element.closest('[data-key]')
const closest = element.closest(SELECTORS.KEY)
if (!closest) return null
const key = closest.getAttribute('data-key')
const key = closest.getAttribute(DATA_ATTRS.KEY)
if (!key) return null
const { value } = editor

View File

@@ -0,0 +1,36 @@
import findNode from './find-node'
import warning from 'tiny-warning'
/**
* Find a Slate path from a DOM `element`.
*
* @param {Element} element
* @param {Editor} editor
* @return {List|Null}
*/
function findPath(element, editor) {
warning(
false,
'As of slate-react@0.22 the `findPath(element)` helper is deprecated in favor of `editor.findPath(element)`.'
)
const node = findNode(element, editor)
if (!node) {
return null
}
const { value } = editor
const { document } = value
const path = document.getPath(node)
return path
}
/**
* Export.
*
* @type {Function}
*/
export default findPath

View File

@@ -1,21 +1,11 @@
import getWindow from 'get-window'
import invariant from 'tiny-invariant'
import warning from 'tiny-warning'
import { Value } from 'slate'
import OffsetKey from './offset-key'
/**
* Constants.
*
* @type {String}
*/
export const ZERO_WIDTH_ATTRIBUTE = 'data-slate-zero-width'
export const ZERO_WIDTH_SELECTOR = `[${ZERO_WIDTH_ATTRIBUTE}]`
const OFFSET_KEY_ATTRIBUTE = 'data-offset-key'
const RANGE_SELECTOR = `[${OFFSET_KEY_ATTRIBUTE}]`
const TEXT_SELECTOR = `[data-key]`
const VOID_SELECTOR = '[data-slate-void]'
import DATA_ATTRS from '../constants/data-attributes'
import SELECTORS from '../constants/selectors'
/**
* Find a Slate point from a DOM selection's `nativeNode` and `nativeOffset`.
@@ -27,6 +17,11 @@ const VOID_SELECTOR = '[data-slate-void]'
*/
function findPoint(nativeNode, nativeOffset, editor) {
warning(
false,
'As of slate-react@0.22 the `findPoint(node, offset)` helper is deprecated in favor of `editor.findPoint(node, offset)`.'
)
invariant(
!Value.isValue(editor),
'As of Slate 0.42.0, the `findPoint` utility takes an `editor` instead of a `value`.'
@@ -39,7 +34,7 @@ function findPoint(nativeNode, nativeOffset, editor) {
const window = getWindow(nativeNode)
const { parentNode } = nearestNode
let rangeNode = parentNode.closest(RANGE_SELECTOR)
let rangeNode = parentNode.closest(SELECTORS.LEAF)
let offset
let node
@@ -47,7 +42,7 @@ function findPoint(nativeNode, nativeOffset, editor) {
// determine what the offset relative to the text node is.
if (rangeNode) {
const range = window.document.createRange()
const textNode = rangeNode.closest(TEXT_SELECTOR)
const textNode = rangeNode.closest(SELECTORS.TEXT)
range.setStart(textNode, 0)
range.setEnd(nearestNode, nearestOffset)
node = textNode
@@ -60,9 +55,9 @@ function findPoint(nativeNode, nativeOffset, editor) {
} else {
// For void nodes, the element with the offset key will be a cousin, not an
// ancestor, so find it by going down from the nearest void parent.
const voidNode = parentNode.closest(VOID_SELECTOR)
const voidNode = parentNode.closest(SELECTORS.VOID)
if (!voidNode) return null
rangeNode = voidNode.querySelector(RANGE_SELECTOR)
rangeNode = voidNode.querySelector(SELECTORS.LEAF)
if (!rangeNode) return null
node = rangeNode
offset = node.textContent.length
@@ -74,13 +69,13 @@ function findPoint(nativeNode, nativeOffset, editor) {
// from the offset to account for the zero-width space character.
if (
offset === node.textContent.length &&
parentNode.hasAttribute(ZERO_WIDTH_ATTRIBUTE)
parentNode.hasAttribute(DATA_ATTRS.ZERO_WIDTH)
) {
offset--
}
// Get the string value of the offset key attribute.
const offsetKey = rangeNode.getAttribute(OFFSET_KEY_ATTRIBUTE)
const offsetKey = rangeNode.getAttribute(DATA_ATTRS.OFFSET_KEY)
if (!offsetKey) return null
const { key } = OffsetKey.parse(offsetKey)

View File

@@ -1,5 +1,6 @@
import getWindow from 'get-window'
import invariant from 'tiny-invariant'
import warning from 'tiny-warning'
import { Value } from 'slate'
import findPoint from './find-point'
@@ -13,6 +14,11 @@ import findPoint from './find-point'
*/
function findRange(native, editor) {
warning(
false,
'As of slate-react@0.22 the `findRange(selection)` helper is deprecated in favor of `editor.findRange(selection)`.'
)
invariant(
!Value.isValue(editor),
'As of Slate 0.42.0, the `findNode` utility takes an `editor` instead of a `value`.'

View File

@@ -1,132 +0,0 @@
import { Set } from 'immutable'
/**
* Split the decorations in lists of relevant decorations for each child.
*
* @param {Node} node
* @param {List} decorations
* @return {Array<List<Decoration>>}
*/
function getChildrenDecorations(node, decorations) {
const activeDecorations = Set().asMutable()
const childrenDecorations = []
orderChildDecorations(node, decorations).forEach(item => {
if (item.isRangeStart) {
// Item is a decoration start
activeDecorations.add(item.decoration)
} else if (item.isRangeEnd) {
// item is a decoration end
activeDecorations.remove(item.decoration)
} else {
// Item is a child node
childrenDecorations.push(activeDecorations.toList())
}
})
return childrenDecorations
}
/**
* Orders the children of provided node and its decoration endpoints (start, end)
* so that decorations can be passed only to relevant children (see use in Node.render())
*
* @param {Node} node
* @param {List} decorations
* @return {Array<Item>}
*
* where type Item =
* {
* child: Node,
* // Index of the child in its parent
* index: number
* }
* or {
* // True if this represents the start of the given decoration
* isRangeStart: boolean,
* // True if this represents the end of the given decoration
* isRangeEnd: boolean,
* decoration: Range
* }
*/
function orderChildDecorations(node, decorations) {
if (decorations.isEmpty()) {
return node.nodes.toArray().map((child, index) => ({
child,
index,
}))
}
// Map each key to its global order
const keyOrders = { [node.key]: 0 }
let globalOrder = 1
node.forEachDescendant(child => {
keyOrders[child.key] = globalOrder
globalOrder = globalOrder + 1
})
const childNodes = node.nodes.toArray()
const endPoints = childNodes.map((child, index) => ({
child,
index,
order: keyOrders[child.key],
}))
decorations.forEach(decoration => {
// Range start.
// A rangeStart should be before the child containing its startKey, in order
// to consider it active before going down the child.
const startKeyOrder = keyOrders[decoration.start.key]
const containingChildOrder =
startKeyOrder === undefined
? 0
: getContainingChildOrder(childNodes, keyOrders, startKeyOrder)
endPoints.push({
isRangeStart: true,
order: containingChildOrder - 0.5,
decoration,
})
// Range end.
const endKeyOrder = (keyOrders[decoration.end.key] || globalOrder) + 0.5
endPoints.push({
isRangeEnd: true,
order: endKeyOrder,
decoration,
})
})
return endPoints.sort((a, b) => (a.order > b.order ? 1 : -1))
}
/*
* Returns the key order of the child right before the given order.
*/
function getContainingChildOrder(children, keyOrders, order) {
// Find the first child that is after the given key
const nextChildIndex = children.findIndex(
child => order < keyOrders[child.key]
)
if (nextChildIndex <= 0) {
return 0
}
const containingChild = children[nextChildIndex - 1]
return keyOrders[containingChild.key]
}
/**
* Export.
*
* @type {Function}
*/
export default getChildrenDecorations

View File

@@ -1,8 +1,9 @@
import getWindow from 'get-window'
import invariant from 'tiny-invariant'
import warning from 'tiny-warning'
import { Value } from 'slate'
import findNode from './find-node'
import findPath from './find-node'
import findRange from './find-range'
/**
@@ -14,6 +15,11 @@ import findRange from './find-range'
*/
function getEventRange(event, editor) {
warning(
false,
'As of slate-react@0.22 the `getEventRange(event, editor)` helper is deprecated in favor of `editor.findEventRange(event)`.'
)
invariant(
!Value.isValue(editor),
'As of Slate 0.42.0, the `findNode` utility takes an `editor` instead of a `value`.'
@@ -28,32 +34,32 @@ function getEventRange(event, editor) {
const { value } = editor
const { document } = value
const node = findNode(target, editor)
if (!node) return null
const path = findPath(event.target, editor)
if (!path) return null
const node = document.getNode(path)
// If the drop target is inside a void node, move it into either the next or
// previous node, depending on which side the `x` and `y` coordinates are
// closest to.
if (editor.query('isVoid', node)) {
if (editor.isVoid(node)) {
const rect = target.getBoundingClientRect()
const isPrevious =
node.object === 'inline'
? x - rect.left < rect.left + rect.width - x
: y - rect.top < rect.top + rect.height - y
const text = node.getFirstText()
const range = document.createRange()
const iterable = isPrevious ? 'previousTexts' : 'nextTexts'
const move = isPrevious ? 'moveToEndOfNode' : 'moveToStartOfNode'
const entry = document[iterable](path)
if (isPrevious) {
const previousText = document.getPreviousText(text.key)
if (previousText) {
return range.moveToEndOfNode(previousText)
}
if (entry) {
const [n] = entry
return range[move](n)
}
const nextText = document.getNextText(text.key)
return nextText ? range.moveToStartOfNode(nextText) : null
return null
}
// Else resolve a range from the caret position where the drop occured.

View File

@@ -1,6 +1,8 @@
import Base64 from 'slate-base64-serializer'
import { IS_IE } from 'slate-dev-environment'
import TRANSFER_TYPES from '../constants/transfer-types'
import DATA_ATTRS from '../constants/data-attributes'
/**
* Transfer types.
@@ -43,7 +45,7 @@ function getEventTransfer(event) {
// If there isn't a fragment, but there is HTML, check to see if the HTML is
// actually an encoded fragment.
if (!fragment && html && ~html.indexOf(' data-slate-fragment="')) {
if (!fragment && html && ~html.indexOf(` ${DATA_ATTRS.FRAGMENT}="`)) {
const matches = FRAGMENT_MATCHER.exec(html)
const [full, encoded] = matches // eslint-disable-line no-unused-vars
if (encoded) fragment = encoded

View File

@@ -1,44 +0,0 @@
import { findDOMNode } from 'react-dom'
/**
* Get clipboard HTML data by capturing the HTML inserted by the browser's
* native paste action. To make this work, `preventDefault()` may not be
* called on the `onPaste` event. As this method is asynchronous, a callback
* is needed to return the HTML content. This solution was adapted from
* http://stackoverflow.com/a/6804718.
*
* @param {Component} component
* @param {Function} callback
*/
function getHtmlFromNativePaste(component, callback) {
// Create an off-screen clone of the element and give it focus.
const el = findDOMNode(component)
const clone = el.cloneNode()
clone.setAttribute('class', '')
clone.setAttribute('style', 'position: fixed; left: -9999px')
el.parentNode.insertBefore(clone, el)
clone.focus()
// Tick forward so the native paste behaviour occurs in cloned element and we
// can get what was pasted from the DOM.
setTimeout(() => {
if (clone.childElementCount > 0) {
// If the node contains any child nodes, that is the HTML content.
const html = clone.innerHTML
clone.parentNode.removeChild(clone)
callback(html)
} else {
// Only plain text, no HTML.
callback()
}
}, 0)
}
/**
* Export.
*
* @type {Function}
*/
export default getHtmlFromNativePaste

View File

@@ -1,6 +1,13 @@
import warning from 'tiny-warning'
import findRange from './find-range'
export default function getSelectionFromDOM(window, editor, domSelection) {
warning(
false,
'As of slate-react@0.22 the `getSelectionFromDOM(window, editor, domSelection)` helper is deprecated in favor of `editor.findSelection(domSelection)`.'
)
const { value } = editor
const { document } = value
@@ -18,12 +25,12 @@ export default function getSelectionFromDOM(window, editor, domSelection) {
}
const { anchor, focus } = range
const anchorText = document.getNode(anchor.key)
const focusText = document.getNode(focus.key)
const anchorInline = document.getClosestInline(anchor.key)
const focusInline = document.getClosestInline(focus.key)
const focusBlock = document.getClosestBlock(focus.key)
const anchorBlock = document.getClosestBlock(anchor.key)
const anchorText = document.getNode(anchor.path)
const focusText = document.getNode(focus.path)
const anchorInline = document.getClosestInline(anchor.path)
const focusInline = document.getClosestInline(focus.path)
const focusBlock = document.getClosestBlock(focus.path)
const anchorBlock = document.getClosestBlock(anchor.path)
// COMPAT: If the anchor point is at the start of a non-void, and the
// focus point is inside a void node with an offset that isn't `0`, set
@@ -51,9 +58,13 @@ export default function getSelectionFromDOM(window, editor, domSelection) {
!editor.isVoid(anchorInline) &&
anchor.offset === anchorText.text.length
) {
const block = document.getClosestBlock(anchor.key)
const nextText = block.getNextText(anchor.key)
if (nextText) range = range.moveAnchorTo(nextText.key, 0)
const block = document.getClosestBlock(anchor.path)
const [next] = block.texts({ path: anchor.path })
if (next) {
const [, nextPath] = next
range = range.moveAnchorTo(nextPath, 0)
}
}
if (
@@ -61,9 +72,13 @@ export default function getSelectionFromDOM(window, editor, domSelection) {
!editor.isVoid(focusInline) &&
focus.offset === focusText.text.length
) {
const block = document.getClosestBlock(focus.key)
const nextText = block.getNextText(focus.key)
if (nextText) range = range.moveFocusTo(nextText.key, 0)
const block = document.getClosestBlock(focus.path)
const [next] = block.texts({ path: focus.path })
if (next) {
const [, nextPath] = next
range = range.moveFocusTo(nextPath, 0)
}
}
let selection = document.createSelection(range)

View File

@@ -1,21 +1,20 @@
import { IS_IE } from 'slate-dev-environment'
/**
* COMPAT: if we are in <= IE11 and the selection contains
* tables, `removeAllRanges()` will throw
* "unable to complete the operation due to error 800a025e"
* Cross-browser remove all ranges from a `domSelection`.
*
* @param {Selection} selection document selection
* @param {Selection} domSelection
*/
function removeAllRanges(selection) {
const doc = window.document
if (doc && doc.body.createTextRange) {
// All IE but Edge
const range = doc.body.createTextRange()
function removeAllRanges(domSelection) {
// COMPAT: In IE 11, if the selection contains nested tables, then
// `removeAllRanges` will throw an error.
if (IS_IE) {
const range = window.document.body.createTextRange()
range.collapse()
range.select()
} else {
selection.removeAllRanges()
domSelection.removeAllRanges()
}
}

View File

@@ -1,14 +0,0 @@
import getSelectionFromDOM from './get-selection-from-dom'
/**
* Looks at the DOM and generates the equivalent Slate Selection.
*
* @param {Window} window
* @param {Editor} editor
* @param {Selection} domSelection - The DOM's selection Object
*/
export default function setSelectionFromDOM(window, editor, domSelection) {
const selection = getSelectionFromDOM(window, editor, domSelection)
editor.select(selection)
}

View File

@@ -21,8 +21,8 @@ export default function setTextFromDomNode(window, editor, domNode) {
// Get the text node and leaf in question.
const { value } = editor
const { document, selection } = value
const node = document.getDescendant(point.key)
const block = document.getClosestBlock(node.key)
const node = document.getDescendant(point.path)
const block = document.getClosestBlock(point.path)
const leaves = node.getLeaves()
const lastText = block.getLastText()
const lastLeaf = leaves.last()
@@ -57,8 +57,8 @@ export default function setTextFromDomNode(window, editor, domNode) {
// const delta = textContent.length - text.length
// const corrected = selection.moveToEnd().moveForward(delta)
let entire = selection
.moveAnchorTo(point.key, start)
.moveFocusTo(point.key, end)
.moveAnchorTo(point.path, start)
.moveFocusTo(point.path, end)
entire = document.resolveRange(entire)

View File

@@ -1,12 +1,24 @@
import { JSDOM } from 'jsdom' // eslint-disable-line import/no-extraneous-dependencies
const UNWANTED_ATTRS = ['data-key', 'data-offset-key']
const UNWANTED_ATTRS = [
'data-key',
'data-offset-key',
'data-slate-object',
'data-slate-leaf',
'data-slate-zero-width',
'data-slate-editor',
'style',
'data-slate-void',
'data-slate-spacer',
'data-slate-length',
]
const UNWANTED_TOP_LEVEL_ATTRS = [
'autocorrect',
'spellcheck',
'style',
'data-gramm',
'role',
]
/**

View File

@@ -21,7 +21,7 @@ describe('slate-react', () => {
assert.equal(actual, expected)
})
fixtures(__dirname, 'rendering/fixtures', ({ module }) => {
fixtures.skip(__dirname, 'rendering/fixtures', ({ module }) => {
const { value, output, props } = module
const p = {
value,

View File

@@ -3,25 +3,21 @@
import React from 'react'
import h from '../../helpers/h'
function Image(props) {
return React.createElement('img', {
className: props.isFocused ? 'focused' : '',
src: props.node.data.get('src'),
...props.attributes,
})
}
function renderNode(props, editor, next) {
function renderBlock(props, editor, next) {
switch (props.node.type) {
case 'image':
return Image(props)
return React.createElement('img', {
className: props.isFocused ? 'focused' : '',
src: props.node.data.get('src'),
...props.attributes,
})
default:
return next()
}
}
export const props = {
renderNode,
renderBlock,
schema: {
blocks: {
image: {
@@ -55,19 +51,19 @@ export const value = (
)
export const output = `
<div data-slate-editor="true" contenteditable="true" role="textbox">
<div style="position:relative">
<div contenteditable="true">
<div>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="n" data-slate-length="0">&#xFEFF;<br /></span>
<span>
<span>&#xFEFF;<br /></span>
</span>
</span>
</div>
<div data-slate-void="true">
<div data-slate-spacer="true" style="height:0;color:transparent;outline:none;position:absolute">
<div>
<div>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z" data-slate-length="0">&#xFEFF;</span>
<span>
<span>&#xFEFF;</span>
</span>
</span>
</div>
@@ -77,16 +73,16 @@ export const output = `
</div>
<div style="position:relative">
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="n" data-slate-length="0">&#xFEFF;<br /></span>
<span>
<span>&#xFEFF;<br /></span>
</span>
</span>
</div>
<div data-slate-void="true">
<div data-slate-spacer="true" style="height:0;color:transparent;outline:none;position:absolute">
<div>
<div>
<span>
<span data-slate-leaf="true">
<span data-slate-zero-width="z" data-slate-length="0">&#xFEFF;</span>
<span>
<span>&#xFEFF;</span>
</span>
</span>
</div>

View File

@@ -21,10 +21,10 @@ function deleteExpandedAtRange(editor, range) {
const { document } = value
const { start, end } = range
if (document.hasDescendant(start.key)) {
if (document.hasDescendant(start.path)) {
range = range.moveToStart()
} else {
range = range.moveTo(end.key, 0).normalize(document)
range = range.moveTo(end.path, 0).normalize(document)
}
return range
@@ -186,8 +186,8 @@ Commands.deleteAtRange = (editor, range) => {
const endLength = endOffset
const ancestor = document.getCommonAncestor(startKey, endKey)
const startChild = ancestor.getFurthestAncestor(startKey)
const endChild = ancestor.getFurthestAncestor(endKey)
const startChild = ancestor.getFurthestChild(startKey)
const endChild = ancestor.getFurthestChild(endKey)
const startParent = document.getParent(startBlock.key)
const startParentIndex = startParent.nodes.indexOf(startBlock)
@@ -248,7 +248,15 @@ Commands.deleteAtRange = (editor, range) => {
// into the start block.
if (startBlock.key !== endBlock.key) {
document = editor.value.document
const lonely = document.getFurthestOnlyChildAncestor(endBlock.key)
let onlyChildAncestor
for (const [node] of document.ancestors(endBlock.key)) {
if (node.nodes.size > 1) {
break
} else {
onlyChildAncestor = node
}
}
// Move the end block to be right after the start block.
if (endParentIndex !== startParentIndex + 1) {
@@ -268,8 +276,8 @@ Commands.deleteAtRange = (editor, range) => {
}
// If nested empty blocks are left over above the end block, remove them.
if (lonely) {
editor.removeNodeByKey(lonely.key)
if (onlyChildAncestor) {
editor.removeNodeByKey(onlyChildAncestor.key)
}
}
}
@@ -296,7 +304,7 @@ Commands.deleteBackwardAtRange = (editor, range, n = 1) => {
return
}
const voidParent = document.getClosestVoid(start.key, editor)
const voidParent = document.getClosestVoid(start.path, editor)
// If there is a void parent, delete it.
if (voidParent) {
@@ -309,7 +317,7 @@ Commands.deleteBackwardAtRange = (editor, range, n = 1) => {
return
}
const block = document.getClosestBlock(start.key)
const block = document.getClosestBlock(start.path)
// PERF: If the closest block is empty, remove it. This is just a shortcut,
// since merging it would result in the same outcome.
@@ -325,7 +333,7 @@ Commands.deleteBackwardAtRange = (editor, range, n = 1) => {
// If the range is at the start of the text node, we need to figure out what
// is behind it to know how to delete...
const text = document.getDescendant(start.key)
const text = document.getDescendant(start.path)
if (start.isAtStartOfNode(text)) {
let prev = document.getPreviousText(text.key)
@@ -401,7 +409,7 @@ Commands.deleteCharBackwardAtRange = (editor, range) => {
const { value } = editor
const { document } = value
const { start } = range
const startBlock = document.getClosestBlock(start.key)
const startBlock = document.getClosestBlock(start.path)
const offset = startBlock.getOffset(start.key)
const o = offset + start.offset
const { text } = startBlock
@@ -425,7 +433,7 @@ Commands.deleteCharForwardAtRange = (editor, range) => {
const { value } = editor
const { document } = value
const { start } = range
const startBlock = document.getClosestBlock(start.key)
const startBlock = document.getClosestBlock(start.path)
const offset = startBlock.getOffset(start.key)
const o = offset + start.offset
const { text } = startBlock
@@ -453,7 +461,7 @@ Commands.deleteForwardAtRange = (editor, range, n = 1) => {
return
}
const voidParent = document.getClosestVoid(start.key, editor)
const voidParent = document.getClosestVoid(start.path, editor)
// If the node has a void parent, delete it.
if (voidParent) {
@@ -461,7 +469,7 @@ Commands.deleteForwardAtRange = (editor, range, n = 1) => {
return
}
const block = document.getClosestBlock(start.key)
const block = document.getClosestBlock(start.path)
// If the closest is not void, but empty, remove it
if (
@@ -487,7 +495,7 @@ Commands.deleteForwardAtRange = (editor, range, n = 1) => {
// If the range is at the start of the text node, we need to figure out what
// is behind it to know how to delete...
const text = document.getDescendant(start.key)
const text = document.getDescendant(start.path)
if (start.isAtEndOfNode(text)) {
const next = document.getNextText(text.key)
@@ -555,7 +563,7 @@ Commands.deleteLineBackwardAtRange = (editor, range) => {
const { value } = editor
const { document } = value
const { start } = range
const startBlock = document.getClosestBlock(start.key)
const startBlock = document.getClosestBlock(start.path)
const offset = startBlock.getOffset(start.key)
const o = offset + start.offset
editor.deleteBackwardAtRange(range, o)
@@ -577,7 +585,7 @@ Commands.deleteLineForwardAtRange = (editor, range) => {
const { value } = editor
const { document } = value
const { start } = range
const startBlock = document.getClosestBlock(start.key)
const startBlock = document.getClosestBlock(start.path)
const offset = startBlock.getOffset(start.key)
const o = offset + start.offset
editor.deleteForwardAtRange(range, startBlock.text.length - o)
@@ -599,7 +607,7 @@ Commands.deleteWordBackwardAtRange = (editor, range) => {
const { value } = editor
const { document } = value
const { start } = range
const startBlock = document.getClosestBlock(start.key)
const startBlock = document.getClosestBlock(start.path)
const offset = startBlock.getOffset(start.key)
const o = offset + start.offset
const { text } = startBlock
@@ -623,7 +631,7 @@ Commands.deleteWordForwardAtRange = (editor, range) => {
const { value } = editor
const { document } = value
const { start } = range
const startBlock = document.getClosestBlock(start.key)
const startBlock = document.getClosestBlock(start.path)
const offset = startBlock.getOffset(start.key)
const o = offset + start.offset
const { text } = startBlock
@@ -710,9 +718,9 @@ Commands.insertFragmentAtRange = (editor, range, fragment) => {
const { start } = range
const { value } = editor
let { document } = value
let startText = document.getDescendant(start.key)
let startText = document.getDescendant(start.path)
let startBlock = document.getClosestBlock(startText.key)
let startChild = startBlock.getFurthestAncestor(startText.key)
let startChild = startBlock.getFurthestChild(startText.key)
const isAtStart = start.isAtStartOfNode(startBlock)
const parent = document.getParent(startBlock.key)
const index = parent.nodes.indexOf(startBlock)
@@ -768,7 +776,7 @@ Commands.insertFragmentAtRange = (editor, range, fragment) => {
document = editor.value.document
startText = document.getDescendant(start.key)
startBlock = document.getClosestBlock(start.key)
startChild = startBlock.getFurthestAncestor(startText.key)
startChild = startBlock.getFurthestChild(startText.key)
// If the first and last block aren't the same, we need to move any of the
// starting block's children after the split into the last block of the
@@ -800,7 +808,7 @@ Commands.insertFragmentAtRange = (editor, range, fragment) => {
} else {
// Otherwise, we maintain the starting block, and insert all of the first
// block's inline nodes into it at the split point.
const inlineChild = startBlock.getFurthestAncestor(startText.key)
const inlineChild = startBlock.getFurthestChild(startText.key)
const inlineIndex = startBlock.nodes.indexOf(inlineChild)
firstBlock.nodes.forEach((inline, i) => {
@@ -861,15 +869,15 @@ Commands.insertInlineAtRange = (editor, range, inline) => {
const { value } = editor
const { document } = value
const { start } = range
const parent = document.getParent(start.key)
const startText = document.assertDescendant(start.key)
const parent = document.getParent(start.path)
const startText = document.assertDescendant(start.path)
const index = parent.nodes.indexOf(startText)
if (editor.isVoid(parent)) {
return
}
editor.splitNodeByKey(start.key, start.offset)
editor.splitNodeByPath(start.path, start.offset)
editor.insertNodeByKey(parent.key, index + 1, inline)
})
}
@@ -891,13 +899,13 @@ Commands.insertTextAtRange = (editor, range, text, marks) => {
const { document } = value
const { start } = range
const offset = start.offset
const parent = document.getParent(start.key)
const parent = document.getParent(start.path)
if (editor.isVoid(parent)) {
return
}
editor.insertTextByKey(start.key, offset, text, marks)
editor.insertTextByPath(start.path, offset, text, marks)
})
}
@@ -947,8 +955,8 @@ Commands.setBlocksAtRange = (editor, range, properties) => {
const blocks = document.getLeafBlocksAtRange(range)
const { start, end, isCollapsed } = range
const isStartVoid = document.hasVoidParent(start.key, editor)
const startBlock = document.getClosestBlock(start.key)
const isStartVoid = document.hasVoidParent(start.path, editor)
const startBlock = document.getClosestBlock(start.path)
const endBlock = document.getClosestBlock(end.key)
// Check if we have a "hanging" selection case where the even though the
@@ -1006,7 +1014,7 @@ Commands.splitBlockAtRange = (editor, range, height = 1) => {
const { start, end } = range
let { value } = editor
let { document } = value
let node = document.assertDescendant(start.key)
let node = document.assertDescendant(start.path)
let parent = document.getClosestBlock(node.key)
let h = 0
@@ -1017,7 +1025,7 @@ Commands.splitBlockAtRange = (editor, range, height = 1) => {
}
editor.withoutNormalizing(() => {
editor.splitDescendantsByKey(node.key, start.key, start.offset)
editor.splitDescendantsByKey(node.key, start.path, start.offset)
value = editor.value
document = value.document
@@ -1028,7 +1036,7 @@ Commands.splitBlockAtRange = (editor, range, height = 1) => {
range = range.moveAnchorToStartOfNode(nextBlock)
range = range.setFocus(range.focus.setPath(null))
if (start.key === end.key) {
if (start.path.equals(end.path)) {
range = range.moveFocusTo(range.anchor.key, end.offset - start.offset)
}
@@ -1052,7 +1060,7 @@ Commands.splitInlineAtRange = (editor, range, height = Infinity) => {
const { start } = range
const { value } = editor
const { document } = value
let node = document.assertDescendant(start.key)
let node = document.assertDescendant(start.path)
let parent = document.getClosestInline(node.key)
let h = 0
@@ -1062,7 +1070,7 @@ Commands.splitInlineAtRange = (editor, range, height = Infinity) => {
h++
}
editor.splitDescendantsByKey(node.key, start.key, start.offset)
editor.splitDescendantsByKey(node.key, start.path, start.offset)
}
/**
@@ -1294,7 +1302,7 @@ Commands.wrapInlineAtRange = (editor, range, inline) => {
if (range.isCollapsed) {
// Wrapping an inline void
const inlineParent = document.getClosestInline(start.key)
const inlineParent = document.getClosestInline(start.path)
if (!inlineParent) {
return
@@ -1311,12 +1319,12 @@ Commands.wrapInlineAtRange = (editor, range, inline) => {
inline = inline.set('nodes', inline.nodes.clear())
const blocks = document.getLeafBlocksAtRange(range)
let startBlock = document.getClosestBlock(start.key)
let endBlock = document.getClosestBlock(end.key)
const startInline = document.getClosestInline(start.key)
const endInline = document.getClosestInline(end.key)
let startChild = startBlock.getFurthestAncestor(start.key)
let endChild = endBlock.getFurthestAncestor(end.key)
let startBlock = document.getClosestBlock(start.path)
let endBlock = document.getClosestBlock(end.path)
const startInline = document.getClosestInline(start.path)
const endInline = document.getClosestInline(end.path)
let startChild = startBlock.getFurthestChild(start.key)
let endChild = endBlock.getFurthestChild(end.key)
editor.withoutNormalizing(() => {
if (!startInline || startInline !== endInline) {
@@ -1327,8 +1335,8 @@ Commands.wrapInlineAtRange = (editor, range, inline) => {
document = editor.value.document
startBlock = document.getDescendant(startBlock.key)
endBlock = document.getDescendant(endBlock.key)
startChild = startBlock.getFurthestAncestor(start.key)
endChild = endBlock.getFurthestAncestor(end.key)
startChild = startBlock.getFurthestChild(start.key)
endChild = endBlock.getFurthestChild(end.key)
const startIndex = startBlock.nodes.indexOf(startChild)
const endIndex = endBlock.nodes.indexOf(endChild)
@@ -1353,14 +1361,14 @@ Commands.wrapInlineAtRange = (editor, range, inline) => {
} else if (startBlock === endBlock) {
document = editor.value.document
startBlock = document.getClosestBlock(start.key)
startChild = startBlock.getFurthestAncestor(start.key)
startChild = startBlock.getFurthestChild(start.key)
const startInner = document.getNextSibling(startChild.key)
const startInnerIndex = startBlock.nodes.indexOf(startInner)
const endInner =
start.key === end.key
? startInner
: startBlock.getFurthestAncestor(end.key)
: startBlock.getFurthestChild(end.key)
const inlines = startBlock.nodes
.skipUntil(n => n === startInner)
.takeUntil(n => n === endInner)
@@ -1416,7 +1424,7 @@ Commands.wrapTextAtRange = (editor, range, prefix, suffix = prefix) => {
const startRange = range.moveToStart()
let endRange = range.moveToEnd()
if (start.key === end.key) {
if (start.path.equals(end.path)) {
endRange = endRange.moveForward(prefix.length)
}

View File

@@ -109,28 +109,28 @@ Commands.insertNodeByPath = (editor, path, index, node) => {
Commands.insertTextByPath = (editor, path, offset, text, marks) => {
marks = Mark.createSet(marks)
const { value } = editor
const { decorations, document } = value
const node = document.assertNode(path)
const { key } = node
let updated = false
const decs = decorations.filter(dec => {
const { start, end, mark } = dec
const isAtomic = editor.isAtomic(mark)
if (!isAtomic) return true
if (start.key !== key) return true
if (start.offset < offset && (end.key !== key || end.offset > offset)) {
updated = true
return false
}
return true
})
const { annotations, document } = value
document.assertNode(path)
editor.withoutNormalizing(() => {
if (updated) {
editor.setDecorations(decs)
for (const annotation of annotations.values()) {
const { start, end } = annotation
const isAtomic = editor.isAtomic(annotation)
if (!isAtomic) {
continue
}
if (!start.path.equals(path)) {
continue
}
if (
start.offset < offset &&
(!end.path.equals(path) || end.offset > offset)
) {
editor.removeAnnotation(annotation)
}
}
editor.applyOperation({
@@ -275,12 +275,17 @@ Commands.removeAllMarksByPath = (editor, path) => {
const { state } = editor
const { document } = state
const node = document.assertNode(path)
const texts = node.object === 'text' ? [node] : node.getTextsAsArray()
texts.forEach(text => {
text.marks.forEach(mark => {
editor.removeMarkByKey(text.key, 0, text.text.length, mark)
})
editor.withoutNormalizing(() => {
if (node.object === 'text') {
editor.removeMarksByPath(path, 0, node.text.length, node.marks)
return
}
for (const [n, p] of node.texts()) {
const pth = path.concat(p)
editor.removeMarksByPath(pth, 0, n.text.length, n.marks)
}
})
}
@@ -314,45 +319,36 @@ Commands.removeNodeByPath = (editor, path) => {
Commands.removeTextByPath = (editor, path, offset, length) => {
const { value } = editor
const { document, decorations } = value
const { document, annotations } = value
const node = document.assertNode(path)
const { text } = node
const string = text.slice(offset, offset + length)
const { key } = node
let updated = false
const decs = decorations.filter(dec => {
const { start, end, mark } = dec
const isAtomic = editor.isAtomic(mark)
if (!isAtomic) {
return true
}
if (start.key !== key) {
return true
}
if (start.offset < offset && (end.key !== key || end.offset > offset)) {
updated = true
return false
}
return true
})
const text = node.text.slice(offset, offset + length)
editor.withoutNormalizing(() => {
if (updated) {
editor.setDecorations(decs)
for (const annotation of annotations.values()) {
const { start, end } = annotation
const isAtomic = editor.isAtomic(annotation)
if (!isAtomic) {
continue
}
if (!start.path.equals(path)) {
continue
}
if (
start.offset < offset &&
(!end.path.equals(path) || end.offset > offset)
) {
editor.removeAnnotation(annotation)
}
}
editor.applyOperation({
type: 'remove_text',
path,
offset,
text: string,
text,
})
})
}
@@ -528,24 +524,22 @@ Commands.splitDescendantsByPath = (editor, path, textPath, textOffset) => {
const { value } = editor
const { document } = value
const node = document.assertNode(path)
const text = document.assertNode(textPath)
const ancestors = document.getAncestors(textPath)
const nodes = ancestors
.skipUntil(a => a.key === node.key)
.reverse()
.unshift(text)
let previous
let index
let index = textOffset
let lastPath = textPath
editor.withoutNormalizing(() => {
nodes.forEach(n => {
const prevIndex = index == null ? null : index
index = previous ? n.nodes.indexOf(previous) + 1 : textOffset
previous = n
editor.splitNodeByKey(n.key, index, { target: prevIndex })
})
editor.splitNodeByKey(textPath, textOffset)
for (const [, ancestorPath] of document.ancestors(textPath)) {
const target = index
index = lastPath.last() + 1
lastPath = ancestorPath
editor.splitNodeByPath(ancestorPath, index, { target })
if (ancestorPath.equals(path)) {
break
}
}
})
}

View File

@@ -1,4 +1,5 @@
import pick from 'lodash/pick'
import Annotation from '../models/annotation'
import Value from '../models/value'
/**
@@ -28,21 +29,31 @@ Commands.setData = (editor, data = {}) => {
})
}
/**
* Set `properties` on the value.
*
* @param {Editor} editor
* @param {Object|Value} properties
*/
Commands.setDecorations = (editor, decorations = []) => {
const { value } = editor
const newProperties = Value.createProperties({ decorations })
const prevProperties = pick(value, Object.keys(newProperties))
Commands.addAnnotation = (editor, annotation) => {
annotation = Annotation.create(annotation)
editor.applyOperation({
type: 'set_value',
properties: prevProperties,
type: 'add_annotation',
annotation,
})
}
Commands.removeAnnotation = (editor, annotation) => {
annotation = Annotation.create(annotation)
editor.applyOperation({
type: 'remove_annotation',
annotation,
})
}
Commands.setAnnotation = (editor, annotation, newProperties) => {
annotation = Annotation.create(annotation)
newProperties = Annotation.createProperties(newProperties)
editor.applyOperation({
type: 'set_annotation',
properties: annotation,
newProperties,
})
}

View File

@@ -259,7 +259,7 @@ Commands.insertFragment = (editor, fragment) => {
const lastBlock = fragment.getClosestBlock(lastText.key)
const firstChild = fragment.nodes.first()
const lastChild = fragment.nodes.last()
const keys = document.getTexts().map(text => text.key)
const keys = Array.from(document.texts(), ([text]) => text.key)
const isAppending =
!startInline ||
(start.isAtStartOfNode(startText) || end.isAtStartOfNode(startText)) ||

View File

@@ -639,7 +639,7 @@ function normalizeNodeByPath(editor, path) {
* Register a `plugin` with the editor.
*
* @param {Editor} editor
* @param {Object|Array} plugin
* @param {Object|Array|Null} plugin
*/
function registerPlugin(editor, plugin) {
@@ -648,6 +648,10 @@ function registerPlugin(editor, plugin) {
return
}
if (plugin == null) {
return
}
const { commands, queries, schema, ...rest } = plugin
if (commands) {

View File

@@ -4,6 +4,7 @@ import './interfaces/node'
import './interfaces/element'
import './interfaces/range'
import Annotation from './models/annotation'
import Block from './models/block'
import Change from './models/change'
import Data from './models/data'
@@ -32,6 +33,7 @@ import { resetMemoization, useMemoization } from './utils/memoize'
*/
export {
Annotation,
Block,
Change,
Data,
@@ -56,6 +58,7 @@ export {
}
export default {
Annotation,
Block,
Change,
Data,

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import mixin from '../utils/mixin'
import Annotation from '../models/annotation'
import Block from '../models/block'
import Decoration from '../models/decoration'
import Document from '../models/document'
@@ -44,6 +45,7 @@ class ModelInterface {
*/
mixin(ModelInterface, [
Annotation,
Block,
Decoration,
Document,

View File

@@ -5,6 +5,7 @@ import mixin from '../utils/mixin'
import Block from '../models/block'
import Document from '../models/document'
import Inline from '../models/inline'
import Node from '../models/node'
import KeyUtils from '../utils/key-utils'
import memoize from '../utils/memoize'
import PathUtils from '../utils/path-utils'
@@ -116,8 +117,18 @@ class NodeInterface {
*/
getPath(key) {
// Handle the case of passing in a path directly, to match other methods.
if (List.isList(key)) return key
// COMPAT: Handle passing in a path, to match other methods.
if (List.isList(key)) {
return key
}
// COMPAT: Handle a node object by iterating the descendants tree, so that
// we avoid using keys for the future.
if (Node.isNode(key) && this.descendants) {
for (const [node, path] of this.descendants()) {
if (key === node) return path
}
}
const dict = this.getKeysToPathsTable()
const path = dict[key]

Some files were not shown because too many files have changed in this diff Show More