1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-03-06 05:49:47 +01:00

Remove data (#1244)

* remove data from event handler signatures

* standardize known transfer types

* add setEventTransfer to docs

* update examples, fix drag/drop

* fix tests and draggable attribute setting
This commit is contained in:
Ian Storm Taylor 2017-10-16 21:04:16 -07:00 committed by GitHub
parent ba5263e0f6
commit 11b2003f53
28 changed files with 473 additions and 577 deletions

View File

@ -9,6 +9,7 @@ import {
findRange, findRange,
getEventRange, getEventRange,
getEventTransfer, getEventTransfer,
setEventTransfer,
} from 'slate-react' } from 'slate-react'
``` ```
@ -41,9 +42,33 @@ Find the Slate range from a DOM `range` or `selection` and a Slate `state`.
### `getEventRange` ### `getEventRange`
`getEventRange(event: DOMEvent, state: State) => Range` `getEventRange(event: DOMEvent, state: State) => Range`
Find the affected Slate range from a DOM `event` and Slate `state`. Get the affected Slate range from a DOM `event` and Slate `state`.
### `getEventTransfer` ### `getEventTransfer`
`getEventTransfer(event: DOMEvent) => Object` `getEventTransfer(event: DOMEvent) => Object`
Find the Slate-related data from a DOM `event` and Slate `state`. Get the Slate-related data from a DOM `event` and Slate `state`.
```js
function onDrop(event, change, editor) {
const transfer = getEventTransfer(event)
const { type, node } = transfer
if (type == 'node') {
// Do something with `node`...
}
}
```
### `setEventTransfer`
`setEventTransfer(event: DOMEvent, type: String, data: Any)`
Sets the Slate-related `data` with `type` on an `event`. The `type` must be one of the types Slate recognizes: `'fragment'`, `'html'`, `'node'`, `'rich'`, or `'text'`.
```js
function onDragStart(event, change, editor) {
const { state } = change
const { startNode } = state
setEventTransfer(event, 'node', startNode)
}
```

View File

@ -16,11 +16,11 @@ class CheckListItem extends React.Component {
/** /**
* On change, set the new checked value on the block. * On change, set the new checked value on the block.
* *
* @param {Event} e * @param {Event} event
*/ */
onChange = (e) => { onChange = (event) => {
const checked = e.target.checked const checked = event.target.checked
const { editor, node } = this.props const { editor, node } = this.props
editor.change(c => c.setNodeByKey(node.key, { data: { checked }})) editor.change(c => c.setNodeByKey(node.key, { data: { checked }}))
} }
@ -106,32 +106,30 @@ class CheckLists extends React.Component {
* If backspace is pressed when collapsed at the start of a check list item, * If backspace is pressed when collapsed at the start of a check list item,
* then turn it back into a paragraph. * then turn it back into a paragraph.
* *
* @param {Event} e * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @return {State|Void} * @return {State|Void}
*/ */
onKeyDown = (e, data, change) => { onKeyDown = (event, change) => {
const { state } = change const { state } = change
if ( if (
e.key == 'Enter' && event.key == 'Enter' &&
state.startBlock.type == 'check-list-item' state.startBlock.type == 'check-list-item'
) { ) {
return change change.splitBlock().setBlock({ data: { checked: false }})
.splitBlock() return true
.setBlock({ data: { checked: false }})
} }
if ( if (
e.key == 'Backspace' && event.key == 'Backspace' &&
state.isCollapsed && state.isCollapsed &&
state.startBlock.type == 'check-list-item' && state.startBlock.type == 'check-list-item' &&
state.selection.startOffset == 0 state.selection.startOffset == 0
) { ) {
return change change.setBlock('paragraph')
.setBlock('paragraph') return true
} }
} }

View File

@ -17,8 +17,8 @@ function CodeBlock(props) {
const { editor, node } = props const { editor, node } = props
const language = node.data.get('language') const language = node.data.get('language')
function onChange(e) { function onChange(event) {
editor.change(c => c.setNodeByKey(node.key, { data: { language: e.target.value }})) editor.change(c => c.setNodeByKey(node.key, { data: { language: event.target.value }}))
} }
return ( return (
@ -165,19 +165,19 @@ class CodeHighlighting extends React.Component {
/** /**
* On key down inside code blocks, insert soft new lines. * On key down inside code blocks, insert soft new lines.
* *
* @param {Event} e * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @return {Change} * @return {Change}
*/ */
onKeyDown = (e, data, change) => { onKeyDown = (event, change) => {
const { state } = change const { state } = change
const { startBlock } = state const { startBlock } = state
if (e.key != 'Enter') return if (event.key != 'Enter') return
if (startBlock.type != 'code') return if (startBlock.type != 'code') return
if (state.isExpanded) change.delete() if (state.isExpanded) change.delete()
return change.insertText('\n') change.insertText('\n')
return true
} }
/** /**

View File

@ -23,51 +23,59 @@ class ForcedLayout extends React.Component {
state: State.fromJSON(initialState), state: State.fromJSON(initialState),
schema: { schema: {
nodes: { nodes: {
'title': props => <h2 {...props.attrs}>{props.children}</h2>, title: props => <h2 {...props.attrs}>{props.children}</h2>,
'paragraph': props => <p {...props.attrs}>{props.children}</p> paragraph: props => <p {...props.attrs}>{props.children}</p>,
}, },
rules: [ rules: [
/* Rule that always makes the first block a title, normalizes by inserting one if no children, or setting the top to be a title */ /* Rule that always makes the first block a title, normalizes by inserting one if no children, or setting the top to be a title */
{ {
match: node => node.kind === 'document', match: (object) => {
validate: document => !document.nodes.size || document.nodes.first().type !== 'title' ? document.nodes : null, return object.kind == 'document'
},
validate: (document) => {
return !document.nodes.size || document.nodes.first().type != 'title' ? document.nodes : null
},
normalize: (change, document, nodes) => { normalize: (change, document, nodes) => {
if (!nodes.size) { if (!nodes.size) {
const title = Block.create({ type: 'title', data: {}}) const title = Block.create({ type: 'title', data: {}})
return change.insertNodeByKey(document.key, 0, title) change.insertNodeByKey(document.key, 0, title)
return
} }
return change.setNodeByKey(nodes.first().key, 'title') change.setNodeByKey(nodes.first().key, 'title')
} }
}, },
/* Rule that only allows for one title, normalizes by making titles paragraphs */ /* Rule that only allows for one title, normalizes by making titles paragraphs */
{ {
match: node => node.kind === 'document', match: (object) => {
return object.kind == 'document'
},
validate: (document) => { validate: (document) => {
const invalidChildren = document.nodes.filter((child, index) => child.type === 'title' && index !== 0) const invalidChildren = document.nodes.filter((child, index) => child.type == 'title' && index != 0)
return invalidChildren.size ? invalidChildren : null return invalidChildren.size ? invalidChildren : null
}, },
normalize: (change, document, invalidChildren) => { normalize: (change, document, invalidChildren) => {
let updatedTransform = change
invalidChildren.forEach((child) => { invalidChildren.forEach((child) => {
updatedTransform = change.setNodeByKey(child.key, 'paragraph') change.setNodeByKey(child.key, 'paragraph')
}) })
return updatedTransform
} }
}, },
/* Rule that forces at least one paragraph, normalizes by inserting an empty paragraph */ /* Rule that forces at least one paragraph, normalizes by inserting an empty paragraph */
{ {
match: node => node.kind === 'document', match: (object) => {
validate: document => document.nodes.size < 2 ? true : null, return object.kind == 'document'
},
validate: (document) => {
return document.nodes.size < 2 ? true : null
},
normalize: (change, document) => { normalize: (change, document) => {
const paragraph = Block.create({ type: 'paragraph', data: {}}) const paragraph = Block.create({ type: 'paragraph', data: {}})
return change.insertNodeByKey(document.key, 1, paragraph) change.insertNodeByKey(document.key, 1, paragraph)
} }
} }
] ]

View File

@ -23,7 +23,14 @@ const schema = {
} }
} }
function Menu({ menuRef, onChange, state }) { /**
* The menu.
*
* @type {Component}
*/
class Menu extends React.Component {
/** /**
* Check if the current selection has a mark with `type` in it. * Check if the current selection has a mark with `type` in it.
* *
@ -31,22 +38,22 @@ function Menu({ menuRef, onChange, state }) {
* @return {Boolean} * @return {Boolean}
*/ */
function hasMark(type) { hasMark(type) {
const { state } = this.props
return state.activeMarks.some(mark => mark.type == type) return state.activeMarks.some(mark => mark.type == type)
} }
/** /**
* When a mark button is clicked, toggle the current mark. * When a mark button is clicked, toggle the current mark.
* *
* @param {Event} e * @param {Event} event
* @param {String} type * @param {String} type
*/ */
function onClickMark(e, type) { onClickMark(event, type) {
e.preventDefault() const { state, onChange } = this.props
const change = state event.preventDefault()
.change() const change = state.change().toggleMark(type)
.toggleMark(type)
onChange(change) onChange(change)
} }
@ -58,11 +65,9 @@ function Menu({ menuRef, onChange, state }) {
* @return {Element} * @return {Element}
*/ */
function renderMarkButton(type, icon) { renderMarkButton(type, icon) {
const isActive = hasMark(type) const isActive = this.hasMark(type)
function onMouseDown(e) { const onMouseDown = event => this.onClickMark(event, type)
onClickMark(e, type)
}
return ( return (
<span className="button" onMouseDown={onMouseDown} data-active={isActive}> <span className="button" onMouseDown={onMouseDown} data-active={isActive}>
@ -71,16 +76,26 @@ function Menu({ menuRef, onChange, state }) {
) )
} }
return ( /**
ReactDOM.createPortal( * Render.
<div className="menu hover-menu" ref={menuRef}> *
{renderMarkButton('bold', 'format_bold')} * @return {Element}
{renderMarkButton('italic', 'format_italic')} */
{renderMarkButton('underlined', 'format_underlined')}
{renderMarkButton('code', 'code')} render() {
</div>, root return (
ReactDOM.createPortal(
<div className="menu hover-menu" ref={this.props.menuRef}>
{this.renderMarkButton('bold', 'format_bold')}
{this.renderMarkButton('italic', 'format_italic')}
{this.renderMarkButton('underlined', 'format_underlined')}
{this.renderMarkButton('code', 'code')}
</div>,
root
)
) )
) }
} }
@ -125,11 +140,14 @@ class HoveringMenu extends React.Component {
} }
/** /**
* Set menu ref * Save the `menu` ref.
* *
* @param {Menu} menu
*/ */
menuRef = el => this.menu = el menuRef = (menu) => {
this.menu = menu
}
/** /**
* Render. * Render.

View File

@ -23,14 +23,14 @@ for (let h = 0; h < HEADINGS; h++) {
nodes.push({ nodes.push({
kind: 'block', kind: 'block',
type: 'heading', type: 'heading',
nodes: [{ kind: 'text', ranges: [{ text: faker.lorem.sentence() }] }] nodes: [{ kind: 'text', leaves: [{ text: faker.lorem.sentence() }] }]
}) })
for (let p = 0; p < PARAGRAPHS; p++) { for (let p = 0; p < PARAGRAPHS; p++) {
nodes.push({ nodes.push({
kind: 'block', kind: 'block',
type: 'paragraph', type: 'paragraph',
nodes: [{ kind: 'text', ranges: [{ text: faker.lorem.paragraph() }] }] nodes: [{ kind: 'text', leaves: [{ text: faker.lorem.paragraph() }] }]
}) })
} }
} }

View File

@ -175,11 +175,11 @@ class Images extends React.Component {
/** /**
* On clicking the image button, prompt for an image and insert it. * On clicking the image button, prompt for an image and insert it.
* *
* @param {Event} e * @param {Event} event
*/ */
onClickImage = (e) => { onClickImage = (event) => {
e.preventDefault() event.preventDefault()
const src = window.prompt('Enter the URL of the image:') const src = window.prompt('Enter the URL of the image:')
if (!src) return if (!src) return
@ -193,17 +193,16 @@ class Images extends React.Component {
/** /**
* On drop, insert the image wherever it is dropped. * On drop, insert the image wherever it is dropped.
* *
* @param {Event} e * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
onDropOrPaste = (e, data, change, editor) => { onDropOrPaste = (event, change, editor) => {
const target = getEventRange(e) const target = getEventRange(event)
if (!target) return if (!target) return
const transfer = getEventTransfer(e) const transfer = getEventTransfer(event)
const { type, text, files } = transfer const { type, text, files } = transfer
if (type == 'files') { if (type == 'files') {

View File

@ -92,11 +92,11 @@ class Links extends React.Component {
* When clicking a link, if the selection has a link in it, remove the link. * When clicking a link, if the selection has a link in it, remove the link.
* Otherwise, add a new link with an href and text. * Otherwise, add a new link with an href and text.
* *
* @param {Event} e * @param {Event} event
*/ */
onClickLink = (e) => { onClickLink = (event) => {
e.preventDefault() event.preventDefault()
const { state } = this.state const { state } = this.state
const hasLinks = this.hasLinks() const hasLinks = this.hasLinks()
const change = state.change() const change = state.change()
@ -125,15 +125,14 @@ class Links extends React.Component {
/** /**
* On paste, if the text is a link, wrap the selection in a link. * On paste, if the text is a link, wrap the selection in a link.
* *
* @param {Event} e * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
*/ */
onPaste = (e, data, change) => { onPaste = (event, change) => {
if (change.state.isCollapsed) return if (change.state.isCollapsed) return
const transfer = getEventTransfer(e) const transfer = getEventTransfer(event)
const { type, text } = transfer const { type, text } = transfer
if (type != 'text' && type != 'html') return if (type != 'text' && type != 'html') return
if (!isUrl(text)) return if (!isUrl(text)) return

View File

@ -99,16 +99,15 @@ class MarkdownShortcuts extends React.Component {
/** /**
* On key down, check for our specific key shortcuts. * On key down, check for our specific key shortcuts.
* *
* @param {Event} e * @param {Event} event
* @param {Data} data
* @param {Change} change * @param {Change} change
*/ */
onKeyDown = (e, data, change) => { onKeyDown = (event, change) => {
switch (e.key) { switch (event.key) {
case ' ': return this.onSpace(e, change) case ' ': return this.onSpace(event, change)
case 'Backspace': return this.onBackspace(e, change) case 'Backspace': return this.onBackspace(event, change)
case 'Enter': return this.onEnter(e, change) case 'Enter': return this.onEnter(event, change)
} }
} }
@ -116,12 +115,12 @@ class MarkdownShortcuts extends React.Component {
* On space, if it was after an auto-markdown shortcut, convert the current * On space, if it was after an auto-markdown shortcut, convert the current
* node into the shortcut's corresponding type. * node into the shortcut's corresponding type.
* *
* @param {Event} e * @param {Event} event
* @param {State} change * @param {State} change
* @return {State or Null} state * @return {State or Null} state
*/ */
onSpace = (e, change) => { onSpace = (event, change) => {
const { state } = change const { state } = change
if (state.isExpanded) return if (state.isExpanded) return
@ -131,7 +130,7 @@ class MarkdownShortcuts extends React.Component {
if (!type) return if (!type) return
if (type == 'list-item' && startBlock.type == 'list-item') return if (type == 'list-item' && startBlock.type == 'list-item') return
e.preventDefault() event.preventDefault()
change.setBlock(type) change.setBlock(type)
@ -139,10 +138,7 @@ class MarkdownShortcuts extends React.Component {
change.wrapBlock('bulleted-list') change.wrapBlock('bulleted-list')
} }
change change.extendToStartOf(startBlock).delete()
.extendToStartOf(startBlock)
.delete()
return true return true
} }
@ -150,12 +146,12 @@ class MarkdownShortcuts extends React.Component {
* On backspace, if at the start of a non-paragraph, convert it back into a * On backspace, if at the start of a non-paragraph, convert it back into a
* paragraph node. * paragraph node.
* *
* @param {Event} e * @param {Event} event
* @param {State} change * @param {State} change
* @return {State or Null} state * @return {State or Null} state
*/ */
onBackspace = (e, change) => { onBackspace = (event, change) => {
const { state } = change const { state } = change
if (state.isExpanded) return if (state.isExpanded) return
if (state.startOffset != 0) return if (state.startOffset != 0) return
@ -163,7 +159,7 @@ class MarkdownShortcuts extends React.Component {
const { startBlock } = state const { startBlock } = state
if (startBlock.type == 'paragraph') return if (startBlock.type == 'paragraph') return
e.preventDefault() event.preventDefault()
change.setBlock('paragraph') change.setBlock('paragraph')
if (startBlock.type == 'list-item') { if (startBlock.type == 'list-item') {
@ -177,17 +173,17 @@ class MarkdownShortcuts extends React.Component {
* On return, if at the end of a node type that should not be extended, * On return, if at the end of a node type that should not be extended,
* create a new paragraph below it. * create a new paragraph below it.
* *
* @param {Event} e * @param {Event} event
* @param {State} change * @param {State} change
* @return {State or Null} state * @return {State or Null} state
*/ */
onEnter = (e, change) => { onEnter = (event, change) => {
const { state } = change const { state } = change
if (state.isExpanded) return if (state.isExpanded) return
const { startBlock, startOffset, endOffset } = state const { startBlock, startOffset, endOffset } = state
if (startOffset == 0 && startBlock.text.length == 0) return this.onBackspace(e, change) if (startOffset == 0 && startBlock.text.length == 0) return this.onBackspace(event, change)
if (endOffset != startBlock.text.length) return if (endOffset != startBlock.text.length) return
if ( if (
@ -202,12 +198,8 @@ class MarkdownShortcuts extends React.Component {
return return
} }
e.preventDefault() event.preventDefault()
change.splitBlock().setBlock('paragraph')
change
.splitBlock()
.setBlock('paragraph')
return true return true
} }

View File

@ -174,13 +174,12 @@ class PasteHtml extends React.Component {
/** /**
* On paste, deserialize the HTML and then insert the fragment. * On paste, deserialize the HTML and then insert the fragment.
* *
* @param {Event} e * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
*/ */
onPaste = (e, data, change) => { onPaste = (event, change) => {
const transfer = getEventTransfer(e) const transfer = getEventTransfer(event)
if (transfer.type != 'html') return if (transfer.type != 'html') return
const { document } = serializer.deserialize(transfer.html) const { document } = serializer.deserialize(transfer.html)
change.insertFragment(document) change.insertFragment(document)

View File

@ -114,28 +114,27 @@ class RichTextExample extends React.Component {
/** /**
* On key down, if it's a formatting command toggle a mark. * On key down, if it's a formatting command toggle a mark.
* *
* @param {Event} e * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @return {Change} * @return {Change}
*/ */
onKeyDown = (e, data, change) => { onKeyDown = (event, change) => {
let mark let mark
if (isBoldHotkey(e)) { if (isBoldHotkey(event)) {
mark = 'bold' mark = 'bold'
} else if (isItalicHotkey(e)) { } else if (isItalicHotkey(event)) {
mark = 'italic' mark = 'italic'
} else if (isUnderlinedHotkey(e)) { } else if (isUnderlinedHotkey(event)) {
mark = 'underlined' mark = 'underlined'
} else if (isCodeHotkey(e)) { } else if (isCodeHotkey(event)) {
mark = 'code' mark = 'code'
} else { } else {
return return
} }
e.preventDefault() event.preventDefault()
change.toggleMark(mark) change.toggleMark(mark)
return true return true
} }
@ -143,12 +142,12 @@ class RichTextExample extends React.Component {
/** /**
* When a mark button is clicked, toggle the current mark. * When a mark button is clicked, toggle the current mark.
* *
* @param {Event} e * @param {Event} event
* @param {String} type * @param {String} type
*/ */
onClickMark = (e, type) => { onClickMark = (event, type) => {
e.preventDefault() event.preventDefault()
const { state } = this.state const { state } = this.state
const change = state.change().toggleMark(type) const change = state.change().toggleMark(type)
this.onChange(change) this.onChange(change)
@ -157,12 +156,12 @@ class RichTextExample extends React.Component {
/** /**
* When a block button is clicked, toggle the block type. * When a block button is clicked, toggle the block type.
* *
* @param {Event} e * @param {Event} event
* @param {String} type * @param {String} type
*/ */
onClickBlock = (e, type) => { onClickBlock = (event, type) => {
e.preventDefault() event.preventDefault()
const { state } = this.state const { state } = this.state
const change = state.change() const change = state.change()
const { document } = state const { document } = state
@ -258,7 +257,7 @@ class RichTextExample extends React.Component {
renderMarkButton = (type, icon) => { renderMarkButton = (type, icon) => {
const isActive = this.hasMark(type) const isActive = this.hasMark(type)
const onMouseDown = e => this.onClickMark(e, type) const onMouseDown = event => this.onClickMark(event, type)
return ( return (
<span className="button" onMouseDown={onMouseDown} data-active={isActive}> <span className="button" onMouseDown={onMouseDown} data-active={isActive}>
@ -277,7 +276,7 @@ class RichTextExample extends React.Component {
renderBlockButton = (type, icon) => { renderBlockButton = (type, icon) => {
const isActive = this.hasBlock(type) const isActive = this.hasBlock(type)
const onMouseDown = e => this.onClickBlock(e, type) const onMouseDown = event => this.onClickBlock(event, type)
return ( return (
<span className="button" onMouseDown={onMouseDown} data-active={isActive}> <span className="button" onMouseDown={onMouseDown} data-active={isActive}>

View File

@ -48,14 +48,13 @@ class PlainText extends React.Component {
/** /**
* On key down, if it's <shift-enter> add a soft break. * On key down, if it's <shift-enter> add a soft break.
* *
* @param {Event} e * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
*/ */
onKeyDown = (e, data, change) => { onKeyDown = (event, change) => {
if (e.key == 'Enter' && e.shiftKey) { if (event.key == 'Enter' && event.shiftKey) {
e.preventDefault() event.preventDefault()
change.insertText('\n') change.insertText('\n')
return true return true
} }

View File

@ -50,12 +50,12 @@ class SearchHighlighting extends React.Component {
/** /**
* On input change, update the decorations. * On input change, update the decorations.
* *
* @param {Event} e * @param {Event} event
*/ */
onInputChange = (e) => { onInputChange = (event) => {
const { state } = this.state const { state } = this.state
const string = e.target.value const string = event.target.value
const texts = state.document.getTexts() const texts = state.document.getTexts()
const decorations = [] const decorations = []

View File

@ -105,28 +105,27 @@ class SyncingEditor extends React.Component {
/** /**
* On key down, if it's a formatting command toggle a mark. * On key down, if it's a formatting command toggle a mark.
* *
* @param {Event} e * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @return {Change} * @return {Change}
*/ */
onKeyDown = (e, data, change) => { onKeyDown = (event, change) => {
let mark let mark
if (isBoldHotkey(e)) { if (isBoldHotkey(event)) {
mark = 'bold' mark = 'bold'
} else if (isItalicHotkey(e)) { } else if (isItalicHotkey(event)) {
mark = 'italic' mark = 'italic'
} else if (isUnderlinedHotkey(e)) { } else if (isUnderlinedHotkey(event)) {
mark = 'underlined' mark = 'underlined'
} else if (isCodeHotkey(e)) { } else if (isCodeHotkey(event)) {
mark = 'code' mark = 'code'
} else { } else {
return return
} }
e.preventDefault() event.preventDefault()
change.toggleMark(mark) change.toggleMark(mark)
return true return true
} }
@ -134,12 +133,12 @@ class SyncingEditor extends React.Component {
/** /**
* When a mark button is clicked, toggle the current mark. * When a mark button is clicked, toggle the current mark.
* *
* @param {Event} e * @param {Event} event
* @param {String} type * @param {String} type
*/ */
onClickMark = (e, type) => { onClickMark = (event, type) => {
e.preventDefault() event.preventDefault()
const { state } = this.state const { state } = this.state
const change = state.change().toggleMark(type) const change = state.change().toggleMark(type)
this.onChange(change) this.onChange(change)
@ -187,7 +186,7 @@ class SyncingEditor extends React.Component {
renderButton = (type, icon) => { renderButton = (type, icon) => {
const isActive = this.hasMark(type) const isActive = this.hasMark(type)
const onMouseDown = e => this.onClickMark(e, type) const onMouseDown = event => this.onClickMark(event, type)
return ( return (
<span className="button" onMouseDown={onMouseDown} data-active={isActive}> <span className="button" onMouseDown={onMouseDown} data-active={isActive}>

View File

@ -43,14 +43,14 @@ class Tables extends React.Component {
/** /**
* On backspace, do nothing if at the start of a table cell. * On backspace, do nothing if at the start of a table cell.
* *
* @param {Event} e * @param {Event} event
* @param {Change} change * @param {Change} change
*/ */
onBackspace = (e, change) => { onBackspace = (event, change) => {
const { state } = change const { state } = change
if (state.startOffset != 0) return if (state.startOffset != 0) return
e.preventDefault() event.preventDefault()
return true return true
} }
@ -67,38 +67,37 @@ class Tables extends React.Component {
/** /**
* On delete, do nothing if at the end of a table cell. * On delete, do nothing if at the end of a table cell.
* *
* @param {Event} e * @param {Event} event
* @param {Change} change * @param {Change} change
*/ */
onDelete = (e, change) => { onDelete = (event, change) => {
const { state } = change const { state } = change
if (state.endOffset != state.startText.text.length) return if (state.endOffset != state.startText.text.length) return
e.preventDefault() event.preventDefault()
return true return true
} }
/** /**
* On return, do nothing if inside a table cell. * On return, do nothing if inside a table cell.
* *
* @param {Event} e * @param {Event} event
* @param {Change} change * @param {Change} change
*/ */
onEnter = (e, change) => { onEnter = (event, change) => {
e.preventDefault() event.preventDefault()
return true return true
} }
/** /**
* On key down, check for our specific key shortcuts. * On key down, check for our specific key shortcuts.
* *
* @param {Event} e * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
*/ */
onKeyDown = (e, data, change) => { onKeyDown = (event, change) => {
const { state } = change const { state } = change
const { document, selection } = state const { document, selection } = state
const { startKey } = selection const { startKey } = selection
@ -109,7 +108,7 @@ class Tables extends React.Component {
const prevBlock = document.getClosestBlock(previous.key) const prevBlock = document.getClosestBlock(previous.key)
if (prevBlock.type == 'table-cell') { if (prevBlock.type == 'table-cell') {
e.preventDefault() event.preventDefault()
return true return true
} }
} }
@ -118,10 +117,10 @@ class Tables extends React.Component {
return return
} }
switch (e.key) { switch (event.key) {
case 'Backspace': return this.onBackspace(e, state) case 'Backspace': return this.onBackspace(event, state)
case 'Delete': return this.onDelete(e, state) case 'Delete': return this.onDelete(event, state)
case 'Enter': return this.onEnter(e, state) case 'Enter': return this.onEnter(event, state)
} }
} }

View File

@ -11,9 +11,8 @@ import Node from './node'
import findClosestNode from '../utils/find-closest-node' import findClosestNode from '../utils/find-closest-node'
import findDOMRange from '../utils/find-dom-range' import findDOMRange from '../utils/find-dom-range'
import findRange from '../utils/find-range' import findRange from '../utils/find-range'
import getHtmlFromNativePaste from '../utils/get-html-from-native-paste'
import scrollToSelection from '../utils/scroll-to-selection' import scrollToSelection from '../utils/scroll-to-selection'
import { IS_FIREFOX, IS_IE, SUPPORTED_EVENTS } from '../constants/environment' import { IS_FIREFOX, SUPPORTED_EVENTS } from '../constants/environment'
/** /**
* Debug. * Debug.
@ -229,18 +228,6 @@ class Content extends React.Component {
this.tmp.key++ this.tmp.key++
} }
// COMPAT: In IE 11, only plain text can be retrieved from the event's
// `clipboardData`. To get HTML, use the browser's native paste action which
// can only be handled synchronously. (2017/06/23)
if (handler == 'onPaste' && IS_IE) {
getHtmlFromNativePaste(event.target, (html) => {
const data = html ? { html, type: 'html' } : {}
this.props.onPaste(event, data)
})
return
}
// If the `onSelect` handler fires while the `isUpdatingSelection` flag is // If the `onSelect` handler fires while the `isUpdatingSelection` flag is
// set it's a result of updating the selection manually, so skip it. // set it's a result of updating the selection manually, so skip it.
if (handler == 'onSelect' && this.tmp.isUpdatingSelection) { if (handler == 'onSelect' && this.tmp.isUpdatingSelection) {
@ -274,7 +261,6 @@ class Content extends React.Component {
handler == 'onCompositionStart' || handler == 'onCompositionStart' ||
handler == 'onCopy' || handler == 'onCopy' ||
handler == 'onCut' || handler == 'onCut' ||
handler == 'onDragStart' ||
handler == 'onFocus' || handler == 'onFocus' ||
handler == 'onInput' || handler == 'onInput' ||
handler == 'onKeyDown' || handler == 'onKeyDown' ||
@ -285,7 +271,7 @@ class Content extends React.Component {
if (!this.isInEditor(event.target)) return if (!this.isInEditor(event.target)) return
} }
this.props[handler](event, {}) this.props[handler](event)
} }
/** /**
@ -315,7 +301,6 @@ class Content extends React.Component {
if (text == null) return if (text == null) return
debug('onNativeBeforeInput', { event, text })
event.preventDefault() event.preventDefault()
const { editor, state } = this.props const { editor, state } = this.props

View File

@ -1,13 +1,10 @@
import Base64 from 'slate-base64-serializer'
import Debug from 'debug' import Debug from 'debug'
import React from 'react' import React from 'react'
import SlateTypes from 'slate-prop-types' import SlateTypes from 'slate-prop-types'
import Types from 'prop-types' import Types from 'prop-types'
import setTransferData from '../utils/set-transfer-data'
import Text from './text' import Text from './text'
import TRANSFER_TYPES from '../constants/transfer-types'
/** /**
* Debug. * Debug.
@ -56,68 +53,6 @@ class Void extends React.Component {
debug(message, `${id}`, ...args) debug(message, `${id}`, ...args)
} }
/**
* When one of the wrapper elements it clicked, select the void node.
*
* @param {Event} event
*/
onClick = (event) => {
if (this.props.readOnly) return
this.debug('onClick', { event })
const { node, editor } = this.props
editor.change((change) => {
change
// COMPAT: In Chrome & Safari, selections that are at the zero offset of
// an inline node will be automatically replaced to be at the last
// offset of a previous inline node, which screws us up, so we always
// want to set it to the end of the node. (2016/11/29)
.collapseToEndOf(node)
.focus()
})
}
/**
* On drag enter, prevent default to allow drops.
*
* @type {Event} event
*/
onDragEnter = (event) => {
if (this.props.readOnly) return
event.preventDefault()
}
/**
* On drag over, prevent default to allow drops.
*
* @type {Event} event
*/
onDragOver = (event) => {
if (this.props.readOnly) return
event.preventDefault()
}
/**
* On drag start, add a serialized representation of the node to the data.
*
* @param {Event} event
*/
onDragStart = (event) => {
const { node } = this.props
const encoded = Base64.serializeNode(node, { preserveKeys: true })
const { dataTransfer } = event.nativeEvent
setTransferData(dataTransfer, TRANSFER_TYPES.NODE, encoded)
this.debug('onDragStart', event)
}
/** /**
* Render. * Render.
* *
@ -136,14 +71,7 @@ class Void extends React.Component {
this.debug('render', { props }) this.debug('render', { props })
return ( return (
<Tag <Tag data-slate-void data-key={node.key} draggable={readOnly ? null : true}>
data-slate-void
data-key={node.key}
onClick={this.onClick}
onDragOver={this.onDragOver}
onDragEnter={this.onDragEnter}
onDragStart={this.onDragStart}
>
{!readOnly && <Tag style={style}> {!readOnly && <Tag style={style}>
{this.renderText()} {this.renderText()}
</Tag>} </Tag>}

View File

@ -8,11 +8,15 @@
const EVENT_HANDLERS = [ const EVENT_HANDLERS = [
'onBeforeInput', 'onBeforeInput',
'onBlur', 'onBlur',
'onClick',
'onCompositionEnd', 'onCompositionEnd',
'onCompositionStart', 'onCompositionStart',
'onCopy', 'onCopy',
'onCut', 'onCut',
'onDragEnd', 'onDragEnd',
'onDragEnter',
'onDragExit',
'onDragLeave',
'onDragOver', 'onDragOver',
'onDragStart', 'onDragStart',
'onDrop', 'onDrop',

View File

@ -38,6 +38,13 @@ const CONTENTEDITABLE = e => (
UNDO(e) UNDO(e)
) )
const COMPOSING = e => (
e.key == 'ArrowDown' ||
e.key == 'ArrowLeft' ||
e.key == 'ArrowRight' ||
e.key == 'ArrowUp'
)
/** /**
* Export. * Export.
* *
@ -46,6 +53,7 @@ const CONTENTEDITABLE = e => (
export default { export default {
BOLD, BOLD,
COMPOSING,
CONTENTEDITABLE, CONTENTEDITABLE,
DELETE_CHAR_BACKWARD, DELETE_CHAR_BACKWARD,
DELETE_CHAR_FORWARD, DELETE_CHAR_FORWARD,

View File

@ -1,12 +1,16 @@
/** /**
* Slate-specific data transfer types. * The transfer types that Slate recognizes.
* *
* @type {Object} * @type {Object}
*/ */
const TYPES = { const TRANSFER_TYPES = {
FRAGMENT: 'application/x-slate-fragment', FRAGMENT: 'application/x-slate-fragment',
HTML: 'text/html',
NODE: 'application/x-slate-node', NODE: 'application/x-slate-node',
RICH: 'text/rtf',
TEXT: 'text/plain',
} }
/** /**
@ -15,4 +19,4 @@ const TYPES = {
* @type {Object} * @type {Object}
*/ */
export default TYPES export default TRANSFER_TYPES

View File

@ -1,12 +1,13 @@
import Editor from './components/editor' import Editor from './components/editor'
import Placeholder from './components/placeholder' import Placeholder from './components/placeholder'
import getEventRange from './utils/get-event-range'
import getEventTransfer from './utils/get-event-transfer'
import findDOMNode from './utils/find-dom-node' import findDOMNode from './utils/find-dom-node'
import findDOMRange from './utils/find-dom-range' import findDOMRange from './utils/find-dom-range'
import findNode from './utils/find-node' import findNode from './utils/find-node'
import findRange from './utils/find-range' 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'
/** /**
* Export. * Export.
@ -17,21 +18,23 @@ import findRange from './utils/find-range'
export { export {
Editor, Editor,
Placeholder, Placeholder,
getEventRange,
getEventTransfer,
findDOMNode, findDOMNode,
findDOMRange, findDOMRange,
findNode, findNode,
findRange, findRange,
getEventRange,
getEventTransfer,
setEventTransfer,
} }
export default { export default {
Editor, Editor,
Placeholder, Placeholder,
getEventRange,
getEventTransfer,
findDOMNode, findDOMNode,
findDOMRange, findDOMRange,
findNode, findNode,
findRange, findRange,
getEventRange,
getEventTransfer,
setEventTransfer,
} }

View File

@ -11,10 +11,12 @@ import HOTKEYS from '../constants/hotkeys'
import Content from '../components/content' import Content from '../components/content'
import Placeholder from '../components/placeholder' import Placeholder from '../components/placeholder'
import findDOMNode from '../utils/find-dom-node' import findDOMNode from '../utils/find-dom-node'
import findNode from '../utils/find-node'
import findPoint from '../utils/find-point' import findPoint from '../utils/find-point'
import findRange from '../utils/find-range' import findRange from '../utils/find-range'
import getEventRange from '../utils/get-event-range' import getEventRange from '../utils/get-event-range'
import getEventTransfer from '../utils/get-event-transfer' import getEventTransfer from '../utils/get-event-transfer'
import setEventTransfer from '../utils/set-event-transfer'
import { IS_CHROME, IS_MAC, IS_SAFARI } from '../constants/environment' import { IS_CHROME, IS_MAC, IS_SAFARI } from '../constants/environment'
/** /**
@ -60,21 +62,23 @@ function AfterPlugin(options = {}) {
// normalize changes to the document, not selection. // normalize changes to the document, not selection.
if (prevState && state.document == prevState.document) return if (prevState && state.document == prevState.document) return
debug('onBeforeChange')
change.normalize(coreSchema) change.normalize(coreSchema)
change.normalize(schema) change.normalize(schema)
debug('onBeforeChange')
} }
/** /**
* On before input, correct any browser inconsistencies. * On before input, correct any browser inconsistencies.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onBeforeInput(event, data, change) { function onBeforeInput(event, change, editor) {
debug('onBeforeInput', { data }) debug('onBeforeInput', { event })
event.preventDefault() event.preventDefault()
change.insertText(event.data) change.insertText(event.data)
} }
@ -83,40 +87,69 @@ function AfterPlugin(options = {}) {
* On blur. * On blur.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onBlur(event, data, change) { function onBlur(event, change, editor) {
debug('onBlur', { data }) debug('onBlur', { event })
change.blur() change.blur()
} }
/**
* On click.
*
* @param {Event} event
* @param {Change} change
* @param {Editor} editor
*/
function onClick(event, change, editor) {
if (editor.props.readOnly) return true
const { state } = change
const { document } = state
const node = findNode(event.target, state)
const isVoid = node && (node.isVoid || document.hasVoidParent(node.key))
if (isVoid) {
// COMPAT: In Chrome & Safari, selections that are at the zero offset of
// an inline node will be automatically replaced to be at the last offset
// of a previous inline node, which screws us up, so we always want to set
// it to the end of the node. (2016/11/29)
change.focus().collapseToEndOf(node)
}
debug('onClick', { event })
}
/** /**
* On copy. * On copy.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onCopy(event, data, change) { function onCopy(event, change, editor) {
debug('onCopy', data) debug('onCopy', { event })
onCutOrCopy(event, data, change)
onCutOrCopy(event, change)
} }
/** /**
* On cut. * On cut.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onCut(event, data, change, editor) { function onCut(event, change, editor) {
debug('onCut', data) debug('onCut', { event })
onCutOrCopy(event, data, change)
onCutOrCopy(event, change)
const window = getWindow(event.target) const window = getWindow(event.target)
// Once the fake cut content has successfully been added to the clipboard, // Once the fake cut content has successfully been added to the clipboard,
@ -130,11 +163,11 @@ function AfterPlugin(options = {}) {
* On cut or copy. * On cut or copy.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onCutOrCopy(event, data, change) { function onCutOrCopy(event, change, editor) {
const window = getWindow(event.target) const window = getWindow(event.target)
const native = window.getSelection() const native = window.getSelection()
const { state } = change const { state } = change
@ -242,12 +275,11 @@ function AfterPlugin(options = {}) {
* On drag end. * On drag end.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onDragEnd(event, data, change, editor) { function onDragEnd(event, change, editor) {
debug('onDragEnd', { event }) debug('onDragEnd', { event })
isDraggingInternally = null isDraggingInternally = null
@ -257,12 +289,11 @@ function AfterPlugin(options = {}) {
* On drag over. * On drag over.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onDragOver(event, data, change, editor) { function onDragOver(event, change, editor) {
debug('onDragOver', { event }) debug('onDragOver', { event })
isDraggingInternally = false isDraggingInternally = false
@ -272,26 +303,39 @@ function AfterPlugin(options = {}) {
* On drag start. * On drag start.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onDragStart(event, data, change, editor) { function onDragStart(event, change, editor) {
debug('onDragStart', { event }) debug('onDragStart', { event })
isDraggingInternally = true isDraggingInternally = true
const { state } = change
const { document } = state
const node = findNode(event.target, state)
const isVoid = node && (node.isVoid || document.hasVoidParent(node.key))
if (isVoid) {
const encoded = Base64.serializeNode(node, { preserveKeys: true })
setEventTransfer(event, 'node', encoded)
} else {
const { fragment } = state
const encoded = Base64.serializeNode(fragment)
setEventTransfer(event, 'fragment', encoded)
}
} }
/** /**
* On drop. * On drop.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onDrop(event, data, change, editor) { function onDrop(event, change, editor) {
debug('onDrop', { event }) debug('onDrop', { event })
const { state } = change const { state } = change
@ -363,12 +407,12 @@ function AfterPlugin(options = {}) {
* On input. * On input.
* *
* @param {Event} eventvent * @param {Event} eventvent
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onInput(event, data, change, editor) { function onInput(event, change, editor) {
debug('onInput', { event })
const window = getWindow(event.target) const window = getWindow(event.target)
const { state } = change const { state } = change
@ -428,21 +472,21 @@ function AfterPlugin(options = {}) {
* On key down. * On key down.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onKeyDown(event, data, change) { function onKeyDown(event, change, editor) {
debug('onKeyDown', { data }) debug('onKeyDown', { event })
switch (event.key) { switch (event.key) {
case 'Enter': return onKeyDownEnter(event, data, change) case 'Enter': return onKeyDownEnter(event, change)
case 'Backspace': return onKeyDownBackspace(event, data, change) case 'Backspace': return onKeyDownBackspace(event, change)
case 'Delete': return onKeyDownDelete(event, data, change) case 'Delete': return onKeyDownDelete(event, change)
case 'ArrowLeft': return onKeyDownLeft(event, data, change) case 'ArrowLeft': return onKeyDownLeft(event, change)
case 'ArrowRight': return onKeyDownRight(event, data, change) case 'ArrowRight': return onKeyDownRight(event, change)
case 'ArrowUp': return onKeyDownUp(event, data, change) case 'ArrowUp': return onKeyDownUp(event, change)
case 'ArrowDown': return onKeyDownDown(event, data, change) case 'ArrowDown': return onKeyDownDown(event, change)
} }
if (HOTKEYS.DELETE_CHAR_BACKWARD(event)) { if (HOTKEYS.DELETE_CHAR_BACKWARD(event)) {
@ -470,11 +514,11 @@ function AfterPlugin(options = {}) {
* On `enter` key down, split the current block in half. * On `enter` key down, split the current block in half.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onKeyDownEnter(event, data, change) { function onKeyDownEnter(event, change, editor) {
const { state } = change const { state } = change
const { document, startKey } = state const { document, startKey } = state
const hasVoidParent = document.hasVoidParent(startKey) const hasVoidParent = document.hasVoidParent(startKey)
@ -495,11 +539,11 @@ function AfterPlugin(options = {}) {
* On `backspace` key down, delete backwards. * On `backspace` key down, delete backwards.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onKeyDownBackspace(event, data, change) { function onKeyDownBackspace(event, change, editor) {
const isWord = IS_MAC ? event.altKey : event.ctrlKey const isWord = IS_MAC ? event.altKey : event.ctrlKey
const isLine = IS_MAC ? event.metaKey : false const isLine = IS_MAC ? event.metaKey : false
@ -514,11 +558,11 @@ function AfterPlugin(options = {}) {
* On `delete` key down, delete forwards. * On `delete` key down, delete forwards.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onKeyDownDelete(event, data, change) { function onKeyDownDelete(event, change, editor) {
const isWord = IS_MAC ? event.altKey : event.ctrlKey const isWord = IS_MAC ? event.altKey : event.ctrlKey
const isLine = IS_MAC ? event.metaKey : false const isLine = IS_MAC ? event.metaKey : false
@ -540,11 +584,11 @@ function AfterPlugin(options = {}) {
* the zero-width spaces will cause two arrow keys to jump to the next text. * the zero-width spaces will cause two arrow keys to jump to the next text.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onKeyDownLeft(event, data, change) { function onKeyDownLeft(event, change, editor) {
const { state } = change const { state } = change
if (event.ctrlKey) return if (event.ctrlKey) return
@ -596,11 +640,11 @@ function AfterPlugin(options = {}) {
* selection to the very start of an inline node here. (2016/11/29) * selection to the very start of an inline node here. (2016/11/29)
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onKeyDownRight(event, data, change) { function onKeyDownRight(event, change, editor) {
const { state } = change const { state } = change
if (event.ctrlKey) return if (event.ctrlKey) return
@ -650,11 +694,11 @@ function AfterPlugin(options = {}) {
* Firefox, option-up doesn't properly move the selection. * Firefox, option-up doesn't properly move the selection.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onKeyDownUp(event, data, change) { function onKeyDownUp(event, change, editor) {
if (!IS_MAC || event.ctrlKey || !event.altKey) return if (!IS_MAC || event.ctrlKey || !event.altKey) return
const { state } = change const { state } = change
@ -679,11 +723,11 @@ function AfterPlugin(options = {}) {
* Firefox, option-down doesn't properly move the selection. * Firefox, option-down doesn't properly move the selection.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onKeyDownDown(event, data, change) { function onKeyDownDown(event, change, editor) {
if (!IS_MAC || event.ctrlKey || !event.altKey) return if (!IS_MAC || event.ctrlKey || !event.altKey) return
const { state } = change const { state } = change
@ -704,12 +748,12 @@ function AfterPlugin(options = {}) {
* On paste. * On paste.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onPaste(event, data, change) { function onPaste(event, change, editor) {
debug('onPaste', { data }) debug('onPaste', { event })
const transfer = getEventTransfer(event) const transfer = getEventTransfer(event)
const { type, fragment, text } = transfer const { type, fragment, text } = transfer
@ -734,12 +778,12 @@ function AfterPlugin(options = {}) {
* On select. * On select.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor
*/ */
function onSelect(event, data, change) { function onSelect(event, change, editor) {
debug('onSelect', { data }) debug('onSelect', { event })
const window = getWindow(event.target) const window = getWindow(event.target)
const { state } = change const { state } = change
@ -916,6 +960,7 @@ function AfterPlugin(options = {}) {
onBeforeChange, onBeforeChange,
onBeforeInput, onBeforeInput,
onBlur, onBlur,
onClick,
onCopy, onCopy,
onCut, onCut,
onDragEnd, onDragEnd,

View File

@ -1,18 +1,10 @@
import Base64 from 'slate-base64-serializer'
import Debug from 'debug' import Debug from 'debug'
import getWindow from 'get-window' import getWindow from 'get-window'
import keycode from 'keycode'
import logger from 'slate-dev-logger'
import { findDOMNode } from 'react-dom' import { findDOMNode } from 'react-dom'
import HOTKEYS from '../constants/hotkeys' import HOTKEYS from '../constants/hotkeys'
import TRANSFER_TYPES from '../constants/transfer-types' import { IS_FIREFOX, SUPPORTED_EVENTS } from '../constants/environment'
import findRange from '../utils/find-range'
import getEventRange from '../utils/get-event-range'
import getEventTransfer from '../utils/get-event-transfer'
import setTransferData from '../utils/set-transfer-data'
import { IS_FIREFOX, IS_MAC, SUPPORTED_EVENTS } from '../constants/environment'
/** /**
* Debug. * Debug.
@ -33,19 +25,16 @@ function BeforePlugin() {
let isComposing = false let isComposing = false
let isCopying = false let isCopying = false
let isDragging = false let isDragging = false
let isShifting = false
let isInternalDrag = null
/** /**
* On before input. * On before input.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onBeforeInput(event, data, change, editor) { function onBeforeInput(event, change, editor) {
if (editor.props.readOnly) return true if (editor.props.readOnly) return true
// COMPAT: React's `onBeforeInput` synthetic event is based on the native // COMPAT: React's `onBeforeInput` synthetic event is based on the native
@ -63,12 +52,11 @@ function BeforePlugin() {
* On blur. * On blur.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onBlur(event, data, change, editor) { function onBlur(event, change, editor) {
if (isCopying) return true if (isCopying) return true
if (editor.props.readOnly) return true if (editor.props.readOnly) return true
@ -86,12 +74,11 @@ function BeforePlugin() {
* On composition end. * On composition end.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onCompositionEnd(event, data, change, editor) { function onCompositionEnd(event, change, editor) {
const n = compositionCount const n = compositionCount
// The `count` check here ensures that if another composition starts // The `count` check here ensures that if another composition starts
@ -109,12 +96,11 @@ function BeforePlugin() {
* On composition start. * On composition start.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onCompositionStart(event, data, change, editor) { function onCompositionStart(event, change, editor) {
isComposing = true isComposing = true
compositionCount++ compositionCount++
@ -125,20 +111,15 @@ function BeforePlugin() {
* On copy. * On copy.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onCopy(event, data, change, editor) { function onCopy(event, change, editor) {
const window = getWindow(event.target) const window = getWindow(event.target)
isCopying = true isCopying = true
window.requestAnimationFrame(() => isCopying = false) window.requestAnimationFrame(() => isCopying = false)
const { state } = change
defineDeprecatedData(data, 'type', 'fragment')
defineDeprecatedData(data, 'fragment', state.fragment)
debug('onCopy', { event }) debug('onCopy', { event })
} }
@ -146,22 +127,17 @@ function BeforePlugin() {
* On cut. * On cut.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onCut(event, data, change, editor) { function onCut(event, change, editor) {
if (editor.props.readOnly) return true if (editor.props.readOnly) return true
const window = getWindow(event.target) const window = getWindow(event.target)
isCopying = true isCopying = true
window.requestAnimationFrame(() => isCopying = false) window.requestAnimationFrame(() => isCopying = false)
const { state } = change
defineDeprecatedData(data, 'type', 'fragment')
defineDeprecatedData(data, 'fragment', state.fragment)
debug('onCut', { event }) debug('onCut', { event })
} }
@ -169,33 +145,84 @@ function BeforePlugin() {
* On drag end. * On drag end.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onDragEnd(event, data, change, editor) { function onDragEnd(event, change, editor) {
// Stop propagation so the event isn't visible to parent editors.
event.stopPropagation() event.stopPropagation()
isDragging = false isDragging = false
isInternalDrag = null
debug('onDragEnd', { event }) debug('onDragEnd', { event })
} }
/**
* On drag enter.
*
* @param {Event} event
* @param {Change} change
* @param {Editor} editor
*/
function onDragEnter(event, change, editor) {
// Stop propagation so the event isn't visible to parent editors.
event.stopPropagation()
debug('onDragEnter', { event })
}
/**
* On drag exit.
*
* @param {Event} event
* @param {Change} change
* @param {Editor} editor
*/
function onDragExit(event, change, editor) {
// Stop propagation so the event isn't visible to parent editors.
event.stopPropagation()
debug('onDragExit', { event })
}
/**
* On drag leave.
*
* @param {Event} event
* @param {Change} change
* @param {Editor} editor
*/
function onDragLeave(event, change, editor) {
// Stop propagation so the event isn't visible to parent editors.
event.stopPropagation()
debug('onDragLeave', { event })
}
/** /**
* On drag over. * On drag over.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onDragOver(event, data, change, editor) { function onDragOver(event, change, editor) {
if (isDragging) return true // Stop propagation so the event isn't visible to parent editors.
event.stopPropagation() event.stopPropagation()
// If a drag is already in progress, don't do this again.
if (!isDragging) return true
isDragging = true isDragging = true
isInternalDrag = false event.nativeEvent.dataTransfer.dropEffect = 'move'
// You must call `preventDefault` to signal that drops are allowed.
event.preventDefault()
debug('onDragOver', { event }) debug('onDragOver', { event })
} }
@ -204,29 +231,15 @@ function BeforePlugin() {
* On drag start. * On drag start.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onDragStart(event, data, change, editor) { function onDragStart(event, change, editor) {
// Stop propagation so the event isn't visible to parent editors.
event.stopPropagation()
isDragging = true isDragging = true
isInternalDrag = true
const d = getEventTransfer(event)
const { nativeEvent } = event
const { dataTransfer } = nativeEvent
Object.keys(d).forEach((key) => {
defineDeprecatedData(data, key, d[key])
})
if (d.type != 'node') {
const { state } = change
const { fragment } = state
const encoded = Base64.serializeNode(fragment)
setTransferData(dataTransfer, TRANSFER_TYPES.FRAGMENT, encoded)
}
debug('onDragStart', { event }) debug('onDragStart', { event })
} }
@ -235,43 +248,19 @@ function BeforePlugin() {
* On drop. * On drop.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onDrop(event, data, change, editor) { function onDrop(event, change, editor) {
// Stop propagation so the event isn't visible to parent editors.
event.stopPropagation() event.stopPropagation()
event.preventDefault()
// Nothing happens in read-only mode.
if (editor.props.readOnly) return true if (editor.props.readOnly) return true
const { state } = change // Prevent default so the DOM's state isn't corrupted.
const { nativeEvent } = event event.preventDefault()
const { dataTransfer } = nativeEvent
const d = getEventTransfer(event)
Object.keys(d).forEach((key) => {
defineDeprecatedData(data, key, d[key])
})
const range = getEventRange(event, state)
if (!range) return true
// Add drop-specific information to the data.
defineDeprecatedData(data, 'target', range)
// COMPAT: Edge throws "Permission denied" errors when
// accessing `dropEffect` or `effectAllowed` (2017/7/12)
try {
defineDeprecatedData(data, 'effect', dataTransfer.dropEffect)
} catch (err) {
defineDeprecatedData(data, 'effect', null)
}
if (d.type == 'fragment' || d.type == 'node') {
defineDeprecatedData(data, 'isInternal', isInternalDrag)
}
debug('onDrop', { event }) debug('onDrop', { event })
} }
@ -280,12 +269,11 @@ function BeforePlugin() {
* On focus. * On focus.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onFocus(event, data, change, editor) { function onFocus(event, change, editor) {
if (isCopying) return true if (isCopying) return true
if (editor.props.readOnly) return true if (editor.props.readOnly) return true
@ -306,12 +294,11 @@ function BeforePlugin() {
* On input. * On input.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onInput(event, data, change, editor) { function onInput(event, change, editor) {
if (isComposing) return true if (isComposing) return true
if (change.state.isBlurred) return true if (change.state.isBlurred) return true
@ -322,23 +309,17 @@ function BeforePlugin() {
* On key down. * On key down.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onKeyDown(event, data, change, editor) { function onKeyDown(event, change, editor) {
if (editor.props.readOnly) return true if (editor.props.readOnly) return true
const { key } = event
// When composing, these characters commit the composition but also move the // When composing, these characters commit the composition but also move the
// selection before we're able to handle it, so prevent their default, // selection before we're able to handle it, so prevent their default,
// selection-moving behavior. // selection-moving behavior.
if ( if (isComposing && HOTKEYS.COMPOSING(event)) {
isComposing &&
(key == 'ArrowLeft' || key == 'ArrowRight' || key == 'ArrowUp' || key == 'ArrowDown')
) {
event.preventDefault() event.preventDefault()
return true return true
} }
@ -349,59 +330,22 @@ function BeforePlugin() {
event.preventDefault() event.preventDefault()
} }
// Keep track of an `isShifting` flag, because it's often used to trigger
// "Paste and Match Style" commands, but isn't available on the event in a
// normal paste event.
if (key == 'Shift') {
isShifting = true
}
// COMPAT: add the deprecated keyboard event properties.
addDeprecatedKeyProperties(data, event)
debug('onKeyDown', { event }) debug('onKeyDown', { event })
} }
/**
* On key up.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onKeyUp(event, data, change, editor) {
// COMPAT: add the deprecated keyboard event properties.
addDeprecatedKeyProperties(data, event)
if (event.key == 'Shift') {
isShifting = false
}
debug('onKeyUp', { event })
}
/** /**
* On paste. * On paste.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onPaste(event, data, change, editor) { function onPaste(event, change, editor) {
if (editor.props.readOnly) return true if (editor.props.readOnly) return true
// Prevent defaults so the DOM state isn't corrupted.
event.preventDefault() event.preventDefault()
const d = getEventTransfer(event)
Object.keys(d).forEach((key) => {
defineDeprecatedData(data, key, d[key])
})
defineDeprecatedData(data, 'isShift', isShifting)
debug('onPaste', { event }) debug('onPaste', { event })
} }
@ -410,84 +354,15 @@ function BeforePlugin() {
* On select. * On select.
* *
* @param {Event} event * @param {Event} event
* @param {Object} data
* @param {Change} change * @param {Change} change
* @param {Editor} editor * @param {Editor} editor
*/ */
function onSelect(event, data, change, editor) { function onSelect(event, change, editor) {
if (isCopying) return true if (isCopying) return true
if (isComposing) return true if (isComposing) return true
if (editor.props.readOnly) return true if (editor.props.readOnly) return true
const window = getWindow(event.target)
const { state } = change
const { document, selection } = state
const native = window.getSelection()
// If there are no ranges, the editor was blurred natively.
if (!native.rangeCount) {
defineDeprecatedData(data, 'selection', selection.blur())
}
// Otherwise, determine the Slate selection from the native one.
else {
let range = findRange(native, state)
if (!range) return true
const { anchorKey, anchorOffset, focusKey, focusOffset } = range
const anchorText = document.getNode(anchorKey)
const focusText = document.getNode(focusKey)
const anchorInline = document.getClosestInline(anchorKey)
const focusInline = document.getClosestInline(focusKey)
const focusBlock = document.getClosestBlock(focusKey)
const anchorBlock = document.getClosestBlock(anchorKey)
// 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 &&
!anchorBlock.isVoid &&
anchorOffset == 0 &&
focusBlock &&
focusBlock.isVoid &&
focusOffset != 0
) {
range = range.set('focusOffset', 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 &&
!anchorInline.isVoid &&
anchorOffset == anchorText.text.length
) {
const block = document.getClosestBlock(anchorKey)
const next = block.getNextText(anchorKey)
if (next) range = range.moveAnchorTo(next.key, 0)
}
if (
focusInline &&
!focusInline.isVoid &&
focusOffset == focusText.text.length
) {
const block = document.getClosestBlock(focusKey)
const next = block.getNextText(focusKey)
if (next) range = range.moveFocusTo(next.key, 0)
}
range = range.normalize(document)
defineDeprecatedData(data, 'selection', range)
}
debug('onSelect', { event }) debug('onSelect', { event })
} }
@ -505,48 +380,20 @@ function BeforePlugin() {
onCopy, onCopy,
onCut, onCut,
onDragEnd, onDragEnd,
onDragEnter,
onDragExit,
onDragLeave,
onDragOver, onDragOver,
onDragStart, onDragStart,
onDrop, onDrop,
onFocus, onFocus,
onInput, onInput,
onKeyDown, onKeyDown,
onKeyUp,
onPaste, onPaste,
onSelect, onSelect,
} }
} }
/**
* Deprecated.
*/
function defineDeprecatedData(data, key, value) {
Object.defineProperty(data, key, {
enumerable: true,
get() {
logger.deprecate('slate-react@0.5.0', `Accessing the \`data.${key}\` property is deprecated, please use the native \`event\` properties instead, or one of the newly exposed helper utilities.`)
return value
}
})
}
function addDeprecatedKeyProperties(data, event) {
const { altKey, ctrlKey, metaKey, shiftKey, which } = event
const name = keycode(which)
defineDeprecatedData(data, 'code', which)
defineDeprecatedData(data, 'key', name)
defineDeprecatedData(data, 'isAlt', altKey)
defineDeprecatedData(data, 'isCmd', IS_MAC ? metaKey && !altKey : false)
defineDeprecatedData(data, 'isCtrl', ctrlKey && !altKey)
defineDeprecatedData(data, 'isLine', IS_MAC ? metaKey : false)
defineDeprecatedData(data, 'isMeta', metaKey)
defineDeprecatedData(data, 'isMod', IS_MAC ? metaKey && !altKey : ctrlKey && !altKey)
defineDeprecatedData(data, 'isModAlt', IS_MAC ? metaKey && altKey : ctrlKey && altKey)
defineDeprecatedData(data, 'isShift', shiftKey)
defineDeprecatedData(data, 'isWord', IS_MAC ? altKey : ctrlKey)
}
/** /**
* Export. * Export.
* *

View File

@ -15,6 +15,8 @@ import findPoint from './find-point'
function findRange(native, state) { function findRange(native, state) {
const el = native.anchorNode || native.startContainer const el = native.anchorNode || native.startContainer
if (!el) return null
const window = getWindow(el) const window = getWindow(el)
// If the `native` object is a DOM `Range` or `StaticRange` object, change it // If the `native` object is a DOM `Range` or `StaticRange` object, change it

View File

@ -3,6 +3,20 @@ import Base64 from 'slate-base64-serializer'
import TRANSFER_TYPES from '../constants/transfer-types' import TRANSFER_TYPES from '../constants/transfer-types'
/**
* Trasnfer types.
*
* @type {String}
*/
const {
FRAGMENT,
HTML,
NODE,
RICH,
TEXT
} = TRANSFER_TYPES
/** /**
* Fragment matching regexp for HTML nodes. * Fragment matching regexp for HTML nodes.
* *
@ -24,11 +38,11 @@ function getEventTransfer(event) {
} }
const transfer = event.dataTransfer || event.clipboardData const transfer = event.dataTransfer || event.clipboardData
let fragment = getType(transfer, TRANSFER_TYPES.FRAGMENT) let fragment = getType(transfer, FRAGMENT)
let node = getType(transfer, TRANSFER_TYPES.NODE) let node = getType(transfer, NODE)
const html = getType(transfer, 'text/html') const html = getType(transfer, HTML)
const rich = getType(transfer, 'text/rtf') const rich = getType(transfer, RICH)
let text = getType(transfer, 'text/plain') let text = getType(transfer, TEXT)
let files let files
// If there isn't a fragment, but there is HTML, check to see if the HTML is // If there isn't a fragment, but there is HTML, check to see if the HTML is
@ -48,9 +62,9 @@ function getEventTransfer(event) {
if (text) { if (text) {
const embeddedTypes = getEmbeddedTypes(text) const embeddedTypes = getEmbeddedTypes(text)
if (embeddedTypes[TRANSFER_TYPES.FRAGMENT]) fragment = embeddedTypes[TRANSFER_TYPES.FRAGMENT] if (embeddedTypes[FRAGMENT]) fragment = embeddedTypes[FRAGMENT]
if (embeddedTypes[TRANSFER_TYPES.NODE]) node = embeddedTypes[TRANSFER_TYPES.NODE] if (embeddedTypes[NODE]) node = embeddedTypes[NODE]
if (embeddedTypes['text/plain']) text = embeddedTypes['text/plain'] if (embeddedTypes[TEXT]) text = embeddedTypes[TEXT]
} }
// Decode a fragment or node if they exist. // Decode a fragment or node if they exist.
@ -91,8 +105,8 @@ function getEventTransfer(event) {
function getEmbeddedTypes(text) { function getEmbeddedTypes(text) {
const prefix = 'SLATE-DATA-EMBED::' const prefix = 'SLATE-DATA-EMBED::'
if (text.substring(0, prefix.length) !== prefix) { if (text.substring(0, prefix.length) != prefix) {
return { 'text/plain': text } return { TEXT: text }
} }
// Attempt to parse, if fails then just standard text/plain // Attempt to parse, if fails then just standard text/plain
@ -141,7 +155,7 @@ function getType(transfer, type) {
if (!transfer.types || !transfer.types.length) { if (!transfer.types || !transfer.types.length) {
// COMPAT: In IE 11, there is no `types` field but `getData('Text')` // COMPAT: In IE 11, there is no `types` field but `getData('Text')`
// is supported`. (2017/06/23) // is supported`. (2017/06/23)
return type === 'text/plain' ? transfer.getData('Text') || null : null return type == TEXT ? transfer.getData('Text') || null : null
} }
return transfer.types.indexOf(type) !== -1 ? transfer.getData(type) || null : null return transfer.types.indexOf(type) !== -1 ? transfer.getData(type) || null : null

View File

@ -1,21 +1,43 @@
import TRANSFER_TYPES from '../constants/transfer-types'
/** /**
* Set data with `type` and `content` on a `dataTransfer` object. * The default plain text transfer type.
*
* @type {String}
*/
const { TEXT } = TRANSFER_TYPES
/**
* Set data with `type` and `content` on an `event`.
* *
* COMPAT: In Edge, custom types throw errors, so embed all non-standard * COMPAT: In Edge, custom types throw errors, so embed all non-standard
* types in text/plain compound object. (2017/7/12) * types in text/plain compound object. (2017/7/12)
* *
* @param {DataTransfer} dataTransfer * @param {Event} event
* @param {String} type * @param {String} type
* @param {String} content * @param {String} content
*/ */
function setTransferData(dataTransfer, type, content) { function setEventTransfer(event, type, content) {
const mime = TRANSFER_TYPES[type.toUpperCase()]
if (!mime) {
throw new Error(`Cannot set unknown transfer type "${mime}"`)
}
if (event.nativeEvent) {
event = event.nativeEvent
}
const transfer = event.dataTransfer || event.clipboardData
try { try {
dataTransfer.setData(type, content) transfer.setData(mime, content)
} catch (err) { } catch (err) {
const prefix = 'SLATE-DATA-EMBED::' const prefix = 'SLATE-DATA-EMBED::'
const text = dataTransfer.getData('text/plain') const text = transfer.getData(TEXT)
let obj = {} let obj = {}
// If the existing plain text data is prefixed, it's Slate JSON data. // If the existing plain text data is prefixed, it's Slate JSON data.
@ -29,12 +51,12 @@ function setTransferData(dataTransfer, type, content) {
// Otherwise, it's just set it as is. // Otherwise, it's just set it as is.
else { else {
obj['text/plain'] = text obj[TEXT] = text
} }
obj[type] = content obj[mime] = content
const string = `${prefix}${JSON.stringify(obj)}` const string = `${prefix}${JSON.stringify(obj)}`
dataTransfer.setData('text/plain', string) transfer.setData(TEXT, string)
} }
} }
@ -44,4 +66,4 @@ function setTransferData(dataTransfer, type, content) {
* @type {Function} * @type {Function}
*/ */
export default setTransferData export default setEventTransfer

View File

@ -23,7 +23,7 @@ export const state = (
export const output = ` export const output = `
<div data-slate-editor="true" contenteditable="true" role="textbox"> <div data-slate-editor="true" contenteditable="true" role="textbox">
<div data-slate-void="true"> <div data-slate-void="true" draggable="true">
<div style="height:0;color:transparent"> <div style="height:0;color:transparent">
<span> <span>
<span></span> <span></span>

View File

@ -31,7 +31,7 @@ export const output = `
<span data-slate-zero-width="true">&#x200A;</span> <span data-slate-zero-width="true">&#x200A;</span>
</span> </span>
</span> </span>
<span data-slate-void="true"> <span data-slate-void="true" draggable="true">
<span style="height:0;color:transparent"> <span style="height:0;color:transparent">
<span> <span>
<span></span> <span></span>