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,
getEventRange,
getEventTransfer,
setEventTransfer,
} from 'slate-react'
```
@ -41,9 +42,33 @@ Find the Slate range from a DOM `range` or `selection` and a Slate `state`.
### `getEventRange`
`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(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.
*
* @param {Event} e
* @param {Event} event
*/
onChange = (e) => {
const checked = e.target.checked
onChange = (event) => {
const checked = event.target.checked
const { editor, node } = this.props
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,
* then turn it back into a paragraph.
*
* @param {Event} e
* @param {Object} data
* @param {Event} event
* @param {Change} change
* @return {State|Void}
*/
onKeyDown = (e, data, change) => {
onKeyDown = (event, change) => {
const { state } = change
if (
e.key == 'Enter' &&
event.key == 'Enter' &&
state.startBlock.type == 'check-list-item'
) {
return change
.splitBlock()
.setBlock({ data: { checked: false }})
change.splitBlock().setBlock({ data: { checked: false }})
return true
}
if (
e.key == 'Backspace' &&
event.key == 'Backspace' &&
state.isCollapsed &&
state.startBlock.type == 'check-list-item' &&
state.selection.startOffset == 0
) {
return change
.setBlock('paragraph')
change.setBlock('paragraph')
return true
}
}

View File

@ -17,8 +17,8 @@ function CodeBlock(props) {
const { editor, node } = props
const language = node.data.get('language')
function onChange(e) {
editor.change(c => c.setNodeByKey(node.key, { data: { language: e.target.value }}))
function onChange(event) {
editor.change(c => c.setNodeByKey(node.key, { data: { language: event.target.value }}))
}
return (
@ -165,19 +165,19 @@ class CodeHighlighting extends React.Component {
/**
* On key down inside code blocks, insert soft new lines.
*
* @param {Event} e
* @param {Object} data
* @param {Event} event
* @param {Change} change
* @return {Change}
*/
onKeyDown = (e, data, change) => {
onKeyDown = (event, change) => {
const { state } = change
const { startBlock } = state
if (e.key != 'Enter') return
if (event.key != 'Enter') return
if (startBlock.type != 'code') return
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),
schema: {
nodes: {
'title': props => <h2 {...props.attrs}>{props.children}</h2>,
'paragraph': props => <p {...props.attrs}>{props.children}</p>
title: props => <h2 {...props.attrs}>{props.children}</h2>,
paragraph: props => <p {...props.attrs}>{props.children}</p>,
},
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 */
{
match: node => node.kind === 'document',
validate: document => !document.nodes.size || document.nodes.first().type !== 'title' ? document.nodes : null,
match: (object) => {
return object.kind == 'document'
},
validate: (document) => {
return !document.nodes.size || document.nodes.first().type != 'title' ? document.nodes : null
},
normalize: (change, document, nodes) => {
if (!nodes.size) {
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 */
{
match: node => node.kind === 'document',
match: (object) => {
return object.kind == '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
},
normalize: (change, document, invalidChildren) => {
let updatedTransform = change
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 */
{
match: node => node.kind === 'document',
validate: document => document.nodes.size < 2 ? true : null,
match: (object) => {
return object.kind == 'document'
},
validate: (document) => {
return document.nodes.size < 2 ? true : null
},
normalize: (change, document) => {
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.
*
@ -31,22 +38,22 @@ function Menu({ menuRef, onChange, state }) {
* @return {Boolean}
*/
function hasMark(type) {
hasMark(type) {
const { state } = this.props
return state.activeMarks.some(mark => mark.type == type)
}
/**
* When a mark button is clicked, toggle the current mark.
*
* @param {Event} e
* @param {Event} event
* @param {String} type
*/
function onClickMark(e, type) {
e.preventDefault()
const change = state
.change()
.toggleMark(type)
onClickMark(event, type) {
const { state, onChange } = this.props
event.preventDefault()
const change = state.change().toggleMark(type)
onChange(change)
}
@ -58,11 +65,9 @@ function Menu({ menuRef, onChange, state }) {
* @return {Element}
*/
function renderMarkButton(type, icon) {
const isActive = hasMark(type)
function onMouseDown(e) {
onClickMark(e, type)
}
renderMarkButton(type, icon) {
const isActive = this.hasMark(type)
const onMouseDown = event => this.onClickMark(event, type)
return (
<span className="button" onMouseDown={onMouseDown} data-active={isActive}>
@ -71,16 +76,26 @@ function Menu({ menuRef, onChange, state }) {
)
}
return (
ReactDOM.createPortal(
<div className="menu hover-menu" ref={menuRef}>
{renderMarkButton('bold', 'format_bold')}
{renderMarkButton('italic', 'format_italic')}
{renderMarkButton('underlined', 'format_underlined')}
{renderMarkButton('code', 'code')}
</div>, root
/**
* Render.
*
* @return {Element}
*/
render() {
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.

View File

@ -23,14 +23,14 @@ for (let h = 0; h < HEADINGS; h++) {
nodes.push({
kind: 'block',
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++) {
nodes.push({
kind: 'block',
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.
*
* @param {Event} e
* @param {Event} event
*/
onClickImage = (e) => {
e.preventDefault()
onClickImage = (event) => {
event.preventDefault()
const src = window.prompt('Enter the URL of the image:')
if (!src) return
@ -193,17 +193,16 @@ class Images extends React.Component {
/**
* On drop, insert the image wherever it is dropped.
*
* @param {Event} e
* @param {Object} data
* @param {Event} event
* @param {Change} change
* @param {Editor} editor
*/
onDropOrPaste = (e, data, change, editor) => {
const target = getEventRange(e)
onDropOrPaste = (event, change, editor) => {
const target = getEventRange(event)
if (!target) return
const transfer = getEventTransfer(e)
const transfer = getEventTransfer(event)
const { type, text, files } = transfer
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.
* Otherwise, add a new link with an href and text.
*
* @param {Event} e
* @param {Event} event
*/
onClickLink = (e) => {
e.preventDefault()
onClickLink = (event) => {
event.preventDefault()
const { state } = this.state
const hasLinks = this.hasLinks()
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.
*
* @param {Event} e
* @param {Object} data
* @param {Event} event
* @param {Change} change
*/
onPaste = (e, data, change) => {
onPaste = (event, change) => {
if (change.state.isCollapsed) return
const transfer = getEventTransfer(e)
const transfer = getEventTransfer(event)
const { type, text } = transfer
if (type != 'text' && type != 'html') 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.
*
* @param {Event} e
* @param {Data} data
* @param {Event} event
* @param {Change} change
*/
onKeyDown = (e, data, change) => {
switch (e.key) {
case ' ': return this.onSpace(e, change)
case 'Backspace': return this.onBackspace(e, change)
case 'Enter': return this.onEnter(e, change)
onKeyDown = (event, change) => {
switch (event.key) {
case ' ': return this.onSpace(event, change)
case 'Backspace': return this.onBackspace(event, 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
* node into the shortcut's corresponding type.
*
* @param {Event} e
* @param {Event} event
* @param {State} change
* @return {State or Null} state
*/
onSpace = (e, change) => {
onSpace = (event, change) => {
const { state } = change
if (state.isExpanded) return
@ -131,7 +130,7 @@ class MarkdownShortcuts extends React.Component {
if (!type) return
if (type == 'list-item' && startBlock.type == 'list-item') return
e.preventDefault()
event.preventDefault()
change.setBlock(type)
@ -139,10 +138,7 @@ class MarkdownShortcuts extends React.Component {
change.wrapBlock('bulleted-list')
}
change
.extendToStartOf(startBlock)
.delete()
change.extendToStartOf(startBlock).delete()
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
* paragraph node.
*
* @param {Event} e
* @param {Event} event
* @param {State} change
* @return {State or Null} state
*/
onBackspace = (e, change) => {
onBackspace = (event, change) => {
const { state } = change
if (state.isExpanded) return
if (state.startOffset != 0) return
@ -163,7 +159,7 @@ class MarkdownShortcuts extends React.Component {
const { startBlock } = state
if (startBlock.type == 'paragraph') return
e.preventDefault()
event.preventDefault()
change.setBlock('paragraph')
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,
* create a new paragraph below it.
*
* @param {Event} e
* @param {Event} event
* @param {State} change
* @return {State or Null} state
*/
onEnter = (e, change) => {
onEnter = (event, change) => {
const { state } = change
if (state.isExpanded) return
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 (
@ -202,12 +198,8 @@ class MarkdownShortcuts extends React.Component {
return
}
e.preventDefault()
change
.splitBlock()
.setBlock('paragraph')
event.preventDefault()
change.splitBlock().setBlock('paragraph')
return true
}

View File

@ -174,13 +174,12 @@ class PasteHtml extends React.Component {
/**
* On paste, deserialize the HTML and then insert the fragment.
*
* @param {Event} e
* @param {Object} data
* @param {Event} event
* @param {Change} change
*/
onPaste = (e, data, change) => {
const transfer = getEventTransfer(e)
onPaste = (event, change) => {
const transfer = getEventTransfer(event)
if (transfer.type != 'html') return
const { document } = serializer.deserialize(transfer.html)
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.
*
* @param {Event} e
* @param {Object} data
* @param {Event} event
* @param {Change} change
* @return {Change}
*/
onKeyDown = (e, data, change) => {
onKeyDown = (event, change) => {
let mark
if (isBoldHotkey(e)) {
if (isBoldHotkey(event)) {
mark = 'bold'
} else if (isItalicHotkey(e)) {
} else if (isItalicHotkey(event)) {
mark = 'italic'
} else if (isUnderlinedHotkey(e)) {
} else if (isUnderlinedHotkey(event)) {
mark = 'underlined'
} else if (isCodeHotkey(e)) {
} else if (isCodeHotkey(event)) {
mark = 'code'
} else {
return
}
e.preventDefault()
event.preventDefault()
change.toggleMark(mark)
return true
}
@ -143,12 +142,12 @@ class RichTextExample extends React.Component {
/**
* When a mark button is clicked, toggle the current mark.
*
* @param {Event} e
* @param {Event} event
* @param {String} type
*/
onClickMark = (e, type) => {
e.preventDefault()
onClickMark = (event, type) => {
event.preventDefault()
const { state } = this.state
const change = state.change().toggleMark(type)
this.onChange(change)
@ -157,12 +156,12 @@ class RichTextExample extends React.Component {
/**
* When a block button is clicked, toggle the block type.
*
* @param {Event} e
* @param {Event} event
* @param {String} type
*/
onClickBlock = (e, type) => {
e.preventDefault()
onClickBlock = (event, type) => {
event.preventDefault()
const { state } = this.state
const change = state.change()
const { document } = state
@ -258,7 +257,7 @@ class RichTextExample extends React.Component {
renderMarkButton = (type, icon) => {
const isActive = this.hasMark(type)
const onMouseDown = e => this.onClickMark(e, type)
const onMouseDown = event => this.onClickMark(event, type)
return (
<span className="button" onMouseDown={onMouseDown} data-active={isActive}>
@ -277,7 +276,7 @@ class RichTextExample extends React.Component {
renderBlockButton = (type, icon) => {
const isActive = this.hasBlock(type)
const onMouseDown = e => this.onClickBlock(e, type)
const onMouseDown = event => this.onClickBlock(event, type)
return (
<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.
*
* @param {Event} e
* @param {Object} data
* @param {Event} event
* @param {Change} change
*/
onKeyDown = (e, data, change) => {
if (e.key == 'Enter' && e.shiftKey) {
e.preventDefault()
onKeyDown = (event, change) => {
if (event.key == 'Enter' && event.shiftKey) {
event.preventDefault()
change.insertText('\n')
return true
}

View File

@ -50,12 +50,12 @@ class SearchHighlighting extends React.Component {
/**
* On input change, update the decorations.
*
* @param {Event} e
* @param {Event} event
*/
onInputChange = (e) => {
onInputChange = (event) => {
const { state } = this.state
const string = e.target.value
const string = event.target.value
const texts = state.document.getTexts()
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.
*
* @param {Event} e
* @param {Object} data
* @param {Event} event
* @param {Change} change
* @return {Change}
*/
onKeyDown = (e, data, change) => {
onKeyDown = (event, change) => {
let mark
if (isBoldHotkey(e)) {
if (isBoldHotkey(event)) {
mark = 'bold'
} else if (isItalicHotkey(e)) {
} else if (isItalicHotkey(event)) {
mark = 'italic'
} else if (isUnderlinedHotkey(e)) {
} else if (isUnderlinedHotkey(event)) {
mark = 'underlined'
} else if (isCodeHotkey(e)) {
} else if (isCodeHotkey(event)) {
mark = 'code'
} else {
return
}
e.preventDefault()
event.preventDefault()
change.toggleMark(mark)
return true
}
@ -134,12 +133,12 @@ class SyncingEditor extends React.Component {
/**
* When a mark button is clicked, toggle the current mark.
*
* @param {Event} e
* @param {Event} event
* @param {String} type
*/
onClickMark = (e, type) => {
e.preventDefault()
onClickMark = (event, type) => {
event.preventDefault()
const { state } = this.state
const change = state.change().toggleMark(type)
this.onChange(change)
@ -187,7 +186,7 @@ class SyncingEditor extends React.Component {
renderButton = (type, icon) => {
const isActive = this.hasMark(type)
const onMouseDown = e => this.onClickMark(e, type)
const onMouseDown = event => this.onClickMark(event, type)
return (
<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.
*
* @param {Event} e
* @param {Event} event
* @param {Change} change
*/
onBackspace = (e, change) => {
onBackspace = (event, change) => {
const { state } = change
if (state.startOffset != 0) return
e.preventDefault()
event.preventDefault()
return true
}
@ -67,38 +67,37 @@ class Tables extends React.Component {
/**
* On delete, do nothing if at the end of a table cell.
*
* @param {Event} e
* @param {Event} event
* @param {Change} change
*/
onDelete = (e, change) => {
onDelete = (event, change) => {
const { state } = change
if (state.endOffset != state.startText.text.length) return
e.preventDefault()
event.preventDefault()
return true
}
/**
* On return, do nothing if inside a table cell.
*
* @param {Event} e
* @param {Event} event
* @param {Change} change
*/
onEnter = (e, change) => {
e.preventDefault()
onEnter = (event, change) => {
event.preventDefault()
return true
}
/**
* On key down, check for our specific key shortcuts.
*
* @param {Event} e
* @param {Object} data
* @param {Event} event
* @param {Change} change
*/
onKeyDown = (e, data, change) => {
onKeyDown = (event, change) => {
const { state } = change
const { document, selection } = state
const { startKey } = selection
@ -109,7 +108,7 @@ class Tables extends React.Component {
const prevBlock = document.getClosestBlock(previous.key)
if (prevBlock.type == 'table-cell') {
e.preventDefault()
event.preventDefault()
return true
}
}
@ -118,10 +117,10 @@ class Tables extends React.Component {
return
}
switch (e.key) {
case 'Backspace': return this.onBackspace(e, state)
case 'Delete': return this.onDelete(e, state)
case 'Enter': return this.onEnter(e, state)
switch (event.key) {
case 'Backspace': return this.onBackspace(event, state)
case 'Delete': return this.onDelete(event, 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 findDOMRange from '../utils/find-dom-range'
import findRange from '../utils/find-range'
import getHtmlFromNativePaste from '../utils/get-html-from-native-paste'
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.
@ -229,18 +228,6 @@ class Content extends React.Component {
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
// set it's a result of updating the selection manually, so skip it.
if (handler == 'onSelect' && this.tmp.isUpdatingSelection) {
@ -274,7 +261,6 @@ class Content extends React.Component {
handler == 'onCompositionStart' ||
handler == 'onCopy' ||
handler == 'onCut' ||
handler == 'onDragStart' ||
handler == 'onFocus' ||
handler == 'onInput' ||
handler == 'onKeyDown' ||
@ -285,7 +271,7 @@ class Content extends React.Component {
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
debug('onNativeBeforeInput', { event, text })
event.preventDefault()
const { editor, state } = this.props

View File

@ -1,13 +1,10 @@
import Base64 from 'slate-base64-serializer'
import Debug from 'debug'
import React from 'react'
import SlateTypes from 'slate-prop-types'
import Types from 'prop-types'
import setTransferData from '../utils/set-transfer-data'
import Text from './text'
import TRANSFER_TYPES from '../constants/transfer-types'
/**
* Debug.
@ -56,68 +53,6 @@ class Void extends React.Component {
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.
*
@ -136,14 +71,7 @@ class Void extends React.Component {
this.debug('render', { props })
return (
<Tag
data-slate-void
data-key={node.key}
onClick={this.onClick}
onDragOver={this.onDragOver}
onDragEnter={this.onDragEnter}
onDragStart={this.onDragStart}
>
<Tag data-slate-void data-key={node.key} draggable={readOnly ? null : true}>
{!readOnly && <Tag style={style}>
{this.renderText()}
</Tag>}

View File

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

View File

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

View File

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

View File

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

View File

@ -11,10 +11,12 @@ import HOTKEYS from '../constants/hotkeys'
import Content from '../components/content'
import Placeholder from '../components/placeholder'
import findDOMNode from '../utils/find-dom-node'
import findNode from '../utils/find-node'
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'
import setEventTransfer from '../utils/set-event-transfer'
import { IS_CHROME, IS_MAC, IS_SAFARI } from '../constants/environment'
/**
@ -60,21 +62,23 @@ function AfterPlugin(options = {}) {
// normalize changes to the document, not selection.
if (prevState && state.document == prevState.document) return
debug('onBeforeChange')
change.normalize(coreSchema)
change.normalize(schema)
debug('onBeforeChange')
}
/**
* On before input, correct any browser inconsistencies.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onBeforeInput(event, data, change) {
debug('onBeforeInput', { data })
function onBeforeInput(event, change, editor) {
debug('onBeforeInput', { event })
event.preventDefault()
change.insertText(event.data)
}
@ -83,40 +87,69 @@ function AfterPlugin(options = {}) {
* On blur.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onBlur(event, data, change) {
debug('onBlur', { data })
function onBlur(event, change, editor) {
debug('onBlur', { event })
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.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onCopy(event, data, change) {
debug('onCopy', data)
onCutOrCopy(event, data, change)
function onCopy(event, change, editor) {
debug('onCopy', { event })
onCutOrCopy(event, change)
}
/**
* On cut.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onCut(event, data, change, editor) {
debug('onCut', data)
onCutOrCopy(event, data, change)
function onCut(event, change, editor) {
debug('onCut', { event })
onCutOrCopy(event, change)
const window = getWindow(event.target)
// Once the fake cut content has successfully been added to the clipboard,
@ -130,11 +163,11 @@ function AfterPlugin(options = {}) {
* On cut or copy.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onCutOrCopy(event, data, change) {
function onCutOrCopy(event, change, editor) {
const window = getWindow(event.target)
const native = window.getSelection()
const { state } = change
@ -242,12 +275,11 @@ function AfterPlugin(options = {}) {
* On drag end.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onDragEnd(event, data, change, editor) {
function onDragEnd(event, change, editor) {
debug('onDragEnd', { event })
isDraggingInternally = null
@ -257,12 +289,11 @@ function AfterPlugin(options = {}) {
* On drag over.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onDragOver(event, data, change, editor) {
function onDragOver(event, change, editor) {
debug('onDragOver', { event })
isDraggingInternally = false
@ -272,26 +303,39 @@ function AfterPlugin(options = {}) {
* On drag start.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onDragStart(event, data, change, editor) {
function onDragStart(event, change, editor) {
debug('onDragStart', { event })
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.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onDrop(event, data, change, editor) {
function onDrop(event, change, editor) {
debug('onDrop', { event })
const { state } = change
@ -363,12 +407,12 @@ function AfterPlugin(options = {}) {
* On input.
*
* @param {Event} eventvent
* @param {Object} data
* @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 { state } = change
@ -428,21 +472,21 @@ function AfterPlugin(options = {}) {
* On key down.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onKeyDown(event, data, change) {
debug('onKeyDown', { data })
function onKeyDown(event, change, editor) {
debug('onKeyDown', { event })
switch (event.key) {
case 'Enter': return onKeyDownEnter(event, data, change)
case 'Backspace': return onKeyDownBackspace(event, data, change)
case 'Delete': return onKeyDownDelete(event, data, change)
case 'ArrowLeft': return onKeyDownLeft(event, data, change)
case 'ArrowRight': return onKeyDownRight(event, data, change)
case 'ArrowUp': return onKeyDownUp(event, data, change)
case 'ArrowDown': return onKeyDownDown(event, data, change)
case 'Enter': return onKeyDownEnter(event, change)
case 'Backspace': return onKeyDownBackspace(event, change)
case 'Delete': return onKeyDownDelete(event, change)
case 'ArrowLeft': return onKeyDownLeft(event, change)
case 'ArrowRight': return onKeyDownRight(event, change)
case 'ArrowUp': return onKeyDownUp(event, change)
case 'ArrowDown': return onKeyDownDown(event, change)
}
if (HOTKEYS.DELETE_CHAR_BACKWARD(event)) {
@ -470,11 +514,11 @@ function AfterPlugin(options = {}) {
* On `enter` key down, split the current block in half.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onKeyDownEnter(event, data, change) {
function onKeyDownEnter(event, change, editor) {
const { state } = change
const { document, startKey } = state
const hasVoidParent = document.hasVoidParent(startKey)
@ -495,11 +539,11 @@ function AfterPlugin(options = {}) {
* On `backspace` key down, delete backwards.
*
* @param {Event} event
* @param {Object} data
* @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 isLine = IS_MAC ? event.metaKey : false
@ -514,11 +558,11 @@ function AfterPlugin(options = {}) {
* On `delete` key down, delete forwards.
*
* @param {Event} event
* @param {Object} data
* @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 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.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onKeyDownLeft(event, data, change) {
function onKeyDownLeft(event, change, editor) {
const { state } = change
if (event.ctrlKey) return
@ -596,11 +640,11 @@ function AfterPlugin(options = {}) {
* selection to the very start of an inline node here. (2016/11/29)
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onKeyDownRight(event, data, change) {
function onKeyDownRight(event, change, editor) {
const { state } = change
if (event.ctrlKey) return
@ -650,11 +694,11 @@ function AfterPlugin(options = {}) {
* Firefox, option-up doesn't properly move the selection.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onKeyDownUp(event, data, change) {
function onKeyDownUp(event, change, editor) {
if (!IS_MAC || event.ctrlKey || !event.altKey) return
const { state } = change
@ -679,11 +723,11 @@ function AfterPlugin(options = {}) {
* Firefox, option-down doesn't properly move the selection.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onKeyDownDown(event, data, change) {
function onKeyDownDown(event, change, editor) {
if (!IS_MAC || event.ctrlKey || !event.altKey) return
const { state } = change
@ -704,12 +748,12 @@ function AfterPlugin(options = {}) {
* On paste.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onPaste(event, data, change) {
debug('onPaste', { data })
function onPaste(event, change, editor) {
debug('onPaste', { event })
const transfer = getEventTransfer(event)
const { type, fragment, text } = transfer
@ -734,12 +778,12 @@ function AfterPlugin(options = {}) {
* On select.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onSelect(event, data, change) {
debug('onSelect', { data })
function onSelect(event, change, editor) {
debug('onSelect', { event })
const window = getWindow(event.target)
const { state } = change
@ -916,6 +960,7 @@ function AfterPlugin(options = {}) {
onBeforeChange,
onBeforeInput,
onBlur,
onClick,
onCopy,
onCut,
onDragEnd,

View File

@ -1,18 +1,10 @@
import Base64 from 'slate-base64-serializer'
import Debug from 'debug'
import getWindow from 'get-window'
import keycode from 'keycode'
import logger from 'slate-dev-logger'
import { findDOMNode } from 'react-dom'
import HOTKEYS from '../constants/hotkeys'
import TRANSFER_TYPES from '../constants/transfer-types'
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'
import { IS_FIREFOX, SUPPORTED_EVENTS } from '../constants/environment'
/**
* Debug.
@ -33,19 +25,16 @@ function BeforePlugin() {
let isComposing = false
let isCopying = false
let isDragging = false
let isShifting = false
let isInternalDrag = null
/**
* On before input.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onBeforeInput(event, data, change, editor) {
function onBeforeInput(event, change, editor) {
if (editor.props.readOnly) return true
// COMPAT: React's `onBeforeInput` synthetic event is based on the native
@ -63,12 +52,11 @@ function BeforePlugin() {
* On blur.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onBlur(event, data, change, editor) {
function onBlur(event, change, editor) {
if (isCopying) return true
if (editor.props.readOnly) return true
@ -86,12 +74,11 @@ function BeforePlugin() {
* On composition end.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onCompositionEnd(event, data, change, editor) {
function onCompositionEnd(event, change, editor) {
const n = compositionCount
// The `count` check here ensures that if another composition starts
@ -109,12 +96,11 @@ function BeforePlugin() {
* On composition start.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onCompositionStart(event, data, change, editor) {
function onCompositionStart(event, change, editor) {
isComposing = true
compositionCount++
@ -125,20 +111,15 @@ function BeforePlugin() {
* On copy.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onCopy(event, data, change, editor) {
function onCopy(event, change, editor) {
const window = getWindow(event.target)
isCopying = true
window.requestAnimationFrame(() => isCopying = false)
const { state } = change
defineDeprecatedData(data, 'type', 'fragment')
defineDeprecatedData(data, 'fragment', state.fragment)
debug('onCopy', { event })
}
@ -146,22 +127,17 @@ function BeforePlugin() {
* On cut.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onCut(event, data, change, editor) {
function onCut(event, change, editor) {
if (editor.props.readOnly) return true
const window = getWindow(event.target)
isCopying = true
window.requestAnimationFrame(() => isCopying = false)
const { state } = change
defineDeprecatedData(data, 'type', 'fragment')
defineDeprecatedData(data, 'fragment', state.fragment)
debug('onCut', { event })
}
@ -169,33 +145,84 @@ function BeforePlugin() {
* On drag end.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @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()
isDragging = false
isInternalDrag = null
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.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onDragOver(event, data, change, editor) {
if (isDragging) return true
function onDragOver(event, change, editor) {
// Stop propagation so the event isn't visible to parent editors.
event.stopPropagation()
// If a drag is already in progress, don't do this again.
if (!isDragging) return 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 })
}
@ -204,29 +231,15 @@ function BeforePlugin() {
* On drag start.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @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
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 })
}
@ -235,43 +248,19 @@ function BeforePlugin() {
* On drop.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @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.preventDefault()
// Nothing happens in read-only mode.
if (editor.props.readOnly) return true
const { state } = change
const { nativeEvent } = event
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)
}
// Prevent default so the DOM's state isn't corrupted.
event.preventDefault()
debug('onDrop', { event })
}
@ -280,12 +269,11 @@ function BeforePlugin() {
* On focus.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onFocus(event, data, change, editor) {
function onFocus(event, change, editor) {
if (isCopying) return true
if (editor.props.readOnly) return true
@ -306,12 +294,11 @@ function BeforePlugin() {
* On input.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onInput(event, data, change, editor) {
function onInput(event, change, editor) {
if (isComposing) return true
if (change.state.isBlurred) return true
@ -322,23 +309,17 @@ function BeforePlugin() {
* On key down.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onKeyDown(event, data, change, editor) {
function onKeyDown(event, change, editor) {
if (editor.props.readOnly) return true
const { key } = event
// When composing, these characters commit the composition but also move the
// selection before we're able to handle it, so prevent their default,
// selection-moving behavior.
if (
isComposing &&
(key == 'ArrowLeft' || key == 'ArrowRight' || key == 'ArrowUp' || key == 'ArrowDown')
) {
if (isComposing && HOTKEYS.COMPOSING(event)) {
event.preventDefault()
return true
}
@ -349,59 +330,22 @@ function BeforePlugin() {
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 })
}
/**
* 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.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onPaste(event, data, change, editor) {
function onPaste(event, change, editor) {
if (editor.props.readOnly) return true
// Prevent defaults so the DOM state isn't corrupted.
event.preventDefault()
const d = getEventTransfer(event)
Object.keys(d).forEach((key) => {
defineDeprecatedData(data, key, d[key])
})
defineDeprecatedData(data, 'isShift', isShifting)
debug('onPaste', { event })
}
@ -410,84 +354,15 @@ function BeforePlugin() {
* On select.
*
* @param {Event} event
* @param {Object} data
* @param {Change} change
* @param {Editor} editor
*/
function onSelect(event, data, change, editor) {
function onSelect(event, change, editor) {
if (isCopying) return true
if (isComposing) 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 })
}
@ -505,48 +380,20 @@ function BeforePlugin() {
onCopy,
onCut,
onDragEnd,
onDragEnter,
onDragExit,
onDragLeave,
onDragOver,
onDragStart,
onDrop,
onFocus,
onInput,
onKeyDown,
onKeyUp,
onPaste,
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.
*

View File

@ -15,6 +15,8 @@ import findPoint from './find-point'
function findRange(native, state) {
const el = native.anchorNode || native.startContainer
if (!el) return null
const window = getWindow(el)
// 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'
/**
* Trasnfer types.
*
* @type {String}
*/
const {
FRAGMENT,
HTML,
NODE,
RICH,
TEXT
} = TRANSFER_TYPES
/**
* Fragment matching regexp for HTML nodes.
*
@ -24,11 +38,11 @@ function getEventTransfer(event) {
}
const transfer = event.dataTransfer || event.clipboardData
let fragment = getType(transfer, TRANSFER_TYPES.FRAGMENT)
let node = getType(transfer, TRANSFER_TYPES.NODE)
const html = getType(transfer, 'text/html')
const rich = getType(transfer, 'text/rtf')
let text = getType(transfer, 'text/plain')
let fragment = getType(transfer, FRAGMENT)
let node = getType(transfer, NODE)
const html = getType(transfer, HTML)
const rich = getType(transfer, RICH)
let text = getType(transfer, TEXT)
let files
// 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) {
const embeddedTypes = getEmbeddedTypes(text)
if (embeddedTypes[TRANSFER_TYPES.FRAGMENT]) fragment = embeddedTypes[TRANSFER_TYPES.FRAGMENT]
if (embeddedTypes[TRANSFER_TYPES.NODE]) node = embeddedTypes[TRANSFER_TYPES.NODE]
if (embeddedTypes['text/plain']) text = embeddedTypes['text/plain']
if (embeddedTypes[FRAGMENT]) fragment = embeddedTypes[FRAGMENT]
if (embeddedTypes[NODE]) node = embeddedTypes[NODE]
if (embeddedTypes[TEXT]) text = embeddedTypes[TEXT]
}
// Decode a fragment or node if they exist.
@ -91,8 +105,8 @@ function getEventTransfer(event) {
function getEmbeddedTypes(text) {
const prefix = 'SLATE-DATA-EMBED::'
if (text.substring(0, prefix.length) !== prefix) {
return { 'text/plain': text }
if (text.substring(0, prefix.length) != prefix) {
return { TEXT: text }
}
// Attempt to parse, if fails then just standard text/plain
@ -141,7 +155,7 @@ function getType(transfer, type) {
if (!transfer.types || !transfer.types.length) {
// COMPAT: In IE 11, there is no `types` field but `getData('Text')`
// 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

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
* types in text/plain compound object. (2017/7/12)
*
* @param {DataTransfer} dataTransfer
* @param {Event} event
* @param {String} type
* @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 {
dataTransfer.setData(type, content)
transfer.setData(mime, content)
} catch (err) {
const prefix = 'SLATE-DATA-EMBED::'
const text = dataTransfer.getData('text/plain')
const text = transfer.getData(TEXT)
let obj = {}
// 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.
else {
obj['text/plain'] = text
obj[TEXT] = text
}
obj[type] = content
obj[mime] = content
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}
*/
export default setTransferData
export default setEventTransfer

View File

@ -23,7 +23,7 @@ export const state = (
export const output = `
<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">
<span>
<span></span>

View File

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