diff --git a/docs/reference/slate-react/utils.md b/docs/reference/slate-react/utils.md index 43f864139..e6625d4a3 100644 --- a/docs/reference/slate-react/utils.md +++ b/docs/reference/slate-react/utils.md @@ -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) +} +``` diff --git a/examples/check-lists/index.js b/examples/check-lists/index.js index c1e1008ad..df81d579e 100644 --- a/examples/check-lists/index.js +++ b/examples/check-lists/index.js @@ -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 } } diff --git a/examples/code-highlighting/index.js b/examples/code-highlighting/index.js index aff2ce2fb..f196d2abb 100644 --- a/examples/code-highlighting/index.js +++ b/examples/code-highlighting/index.js @@ -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 } /** diff --git a/examples/forced-layout/index.js b/examples/forced-layout/index.js index 57f98f858..043e3f255 100644 --- a/examples/forced-layout/index.js +++ b/examples/forced-layout/index.js @@ -23,51 +23,59 @@ class ForcedLayout extends React.Component { state: State.fromJSON(initialState), schema: { nodes: { - 'title': props =>

{props.children}

, - 'paragraph': props =>

{props.children}

+ title: props =>

{props.children}

, + paragraph: props =>

{props.children}

, }, 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) } } ] diff --git a/examples/hovering-menu/index.js b/examples/hovering-menu/index.js index 267b320e6..f8bfc991c 100644 --- a/examples/hovering-menu/index.js +++ b/examples/hovering-menu/index.js @@ -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 ( @@ -71,16 +76,26 @@ function Menu({ menuRef, onChange, state }) { ) } - return ( - ReactDOM.createPortal( -
- {renderMarkButton('bold', 'format_bold')} - {renderMarkButton('italic', 'format_italic')} - {renderMarkButton('underlined', 'format_underlined')} - {renderMarkButton('code', 'code')} -
, root + /** + * Render. + * + * @return {Element} + */ + + render() { + return ( + ReactDOM.createPortal( +
+ {this.renderMarkButton('bold', 'format_bold')} + {this.renderMarkButton('italic', 'format_italic')} + {this.renderMarkButton('underlined', 'format_underlined')} + {this.renderMarkButton('code', 'code')} +
, + 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. diff --git a/examples/huge-document/index.js b/examples/huge-document/index.js index ec5b29437..d4cbd1ab7 100644 --- a/examples/huge-document/index.js +++ b/examples/huge-document/index.js @@ -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() }] }] }) } } diff --git a/examples/images/index.js b/examples/images/index.js index 2b9850be1..e397d258a 100644 --- a/examples/images/index.js +++ b/examples/images/index.js @@ -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') { diff --git a/examples/links/index.js b/examples/links/index.js index 6de53252c..9f1018a5e 100644 --- a/examples/links/index.js +++ b/examples/links/index.js @@ -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 diff --git a/examples/markdown-shortcuts/index.js b/examples/markdown-shortcuts/index.js index fd51a0272..a2b842b69 100644 --- a/examples/markdown-shortcuts/index.js +++ b/examples/markdown-shortcuts/index.js @@ -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 } diff --git a/examples/paste-html/index.js b/examples/paste-html/index.js index e190593f6..5186b9a71 100644 --- a/examples/paste-html/index.js +++ b/examples/paste-html/index.js @@ -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) diff --git a/examples/rich-text/index.js b/examples/rich-text/index.js index c28cd9d6b..102e7af4b 100644 --- a/examples/rich-text/index.js +++ b/examples/rich-text/index.js @@ -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 ( @@ -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 ( diff --git a/examples/rtl/index.js b/examples/rtl/index.js index a0e43d4a7..c5df00c71 100644 --- a/examples/rtl/index.js +++ b/examples/rtl/index.js @@ -48,14 +48,13 @@ class PlainText extends React.Component { /** * On key down, if it's 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 } diff --git a/examples/search-highlighting/index.js b/examples/search-highlighting/index.js index 50c0850ca..bd6756aca 100644 --- a/examples/search-highlighting/index.js +++ b/examples/search-highlighting/index.js @@ -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 = [] diff --git a/examples/syncing-operations/index.js b/examples/syncing-operations/index.js index 93d4d5f06..aa3f2faf3 100644 --- a/examples/syncing-operations/index.js +++ b/examples/syncing-operations/index.js @@ -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 ( diff --git a/examples/tables/index.js b/examples/tables/index.js index 9d934c8a4..47be1fc65 100644 --- a/examples/tables/index.js +++ b/examples/tables/index.js @@ -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) } } diff --git a/packages/slate-react/src/components/content.js b/packages/slate-react/src/components/content.js index ab16a4b37..b4b89b5d2 100644 --- a/packages/slate-react/src/components/content.js +++ b/packages/slate-react/src/components/content.js @@ -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 diff --git a/packages/slate-react/src/components/void.js b/packages/slate-react/src/components/void.js index cc3011ee4..69700a1f1 100644 --- a/packages/slate-react/src/components/void.js +++ b/packages/slate-react/src/components/void.js @@ -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 ( - + {!readOnly && {this.renderText()} } diff --git a/packages/slate-react/src/constants/event-handlers.js b/packages/slate-react/src/constants/event-handlers.js index b6473ece0..d15cfe809 100644 --- a/packages/slate-react/src/constants/event-handlers.js +++ b/packages/slate-react/src/constants/event-handlers.js @@ -8,11 +8,15 @@ const EVENT_HANDLERS = [ 'onBeforeInput', 'onBlur', + 'onClick', 'onCompositionEnd', 'onCompositionStart', 'onCopy', 'onCut', 'onDragEnd', + 'onDragEnter', + 'onDragExit', + 'onDragLeave', 'onDragOver', 'onDragStart', 'onDrop', diff --git a/packages/slate-react/src/constants/hotkeys.js b/packages/slate-react/src/constants/hotkeys.js index 026f9cd77..675c0706c 100644 --- a/packages/slate-react/src/constants/hotkeys.js +++ b/packages/slate-react/src/constants/hotkeys.js @@ -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, diff --git a/packages/slate-react/src/constants/transfer-types.js b/packages/slate-react/src/constants/transfer-types.js index bf5ae1595..3fa4e6bcb 100644 --- a/packages/slate-react/src/constants/transfer-types.js +++ b/packages/slate-react/src/constants/transfer-types.js @@ -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 diff --git a/packages/slate-react/src/index.js b/packages/slate-react/src/index.js index 8b92fa3cd..9461742b3 100644 --- a/packages/slate-react/src/index.js +++ b/packages/slate-react/src/index.js @@ -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, } diff --git a/packages/slate-react/src/plugins/after.js b/packages/slate-react/src/plugins/after.js index bd6d47f4a..c2d7b52f4 100644 --- a/packages/slate-react/src/plugins/after.js +++ b/packages/slate-react/src/plugins/after.js @@ -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, diff --git a/packages/slate-react/src/plugins/before.js b/packages/slate-react/src/plugins/before.js index df2ba13b3..25f4a80d9 100644 --- a/packages/slate-react/src/plugins/before.js +++ b/packages/slate-react/src/plugins/before.js @@ -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 '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. * diff --git a/packages/slate-react/src/utils/find-range.js b/packages/slate-react/src/utils/find-range.js index 5647f3df5..6ec544e14 100644 --- a/packages/slate-react/src/utils/find-range.js +++ b/packages/slate-react/src/utils/find-range.js @@ -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 diff --git a/packages/slate-react/src/utils/get-event-transfer.js b/packages/slate-react/src/utils/get-event-transfer.js index a3eed8a1f..6e953fb57 100644 --- a/packages/slate-react/src/utils/get-event-transfer.js +++ b/packages/slate-react/src/utils/get-event-transfer.js @@ -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 diff --git a/packages/slate-react/src/utils/set-transfer-data.js b/packages/slate-react/src/utils/set-event-transfer.js similarity index 50% rename from packages/slate-react/src/utils/set-transfer-data.js rename to packages/slate-react/src/utils/set-event-transfer.js index 95eecc580..8bb4b6408 100644 --- a/packages/slate-react/src/utils/set-transfer-data.js +++ b/packages/slate-react/src/utils/set-event-transfer.js @@ -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 diff --git a/packages/slate-react/test/rendering/fixtures/custom-block-void.js b/packages/slate-react/test/rendering/fixtures/custom-block-void.js index 3c684f90e..f1a8bedf5 100644 --- a/packages/slate-react/test/rendering/fixtures/custom-block-void.js +++ b/packages/slate-react/test/rendering/fixtures/custom-block-void.js @@ -23,7 +23,7 @@ export const state = ( export const output = `
-
+
diff --git a/packages/slate-react/test/rendering/fixtures/custom-inline-void.js b/packages/slate-react/test/rendering/fixtures/custom-inline-void.js index d1627c4a2..b7da3c6f4 100644 --- a/packages/slate-react/test/rendering/fixtures/custom-inline-void.js +++ b/packages/slate-react/test/rendering/fixtures/custom-inline-void.js @@ -31,7 +31,7 @@ export const output = ` - +