diff --git a/examples/app.js b/examples/app.js
index d883ec3b0..5f8b40ddb 100644
--- a/examples/app.js
+++ b/examples/app.js
@@ -27,6 +27,7 @@ import RTL from './rtl'
import ReadOnly from './read-only'
import RichText from './rich-text'
import SearchHighlighting from './search-highlighting'
+import InputTester from './input-tester'
import SyncingOperations from './syncing-operations'
import Tables from './tables'
@@ -58,6 +59,7 @@ const EXAMPLES = [
['Forced Layout', ForcedLayout, '/forced-layout'],
['Huge Document', HugeDocument, '/huge-document'],
['History', History, '/history'],
+ ['Input Tester', InputTester, '/input-tester'],
]
/**
@@ -102,7 +104,9 @@ const TabList = styled('div')`
}
`
-const Tab = styled(({ active, ...props }) => )`
+const MaskedRouterLink = ({ active, ...props }) =>
+
+const Tab = styled(MaskedRouterLink)`
display: inline-block;
margin-bottom: 0.2em;
padding: 0.2em 0.5em;
diff --git a/examples/input-tester/index.js b/examples/input-tester/index.js
new file mode 100644
index 000000000..8a46d4b27
--- /dev/null
+++ b/examples/input-tester/index.js
@@ -0,0 +1,364 @@
+import { Editor, findRange } from 'slate-react'
+import { Value } from 'slate'
+
+import React from 'react'
+import styled from 'react-emotion'
+import initialValue from './value.json'
+import { Icon } from '../components'
+import { createArrayValue } from 'react-values'
+
+const EventsValue = createArrayValue()
+
+const Wrapper = styled('div')`
+ position: relative;
+`
+
+const EventsWrapper = styled('div')`
+ position: fixed;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ max-height: 40vh;
+ height: 500px;
+ overflow: auto;
+ border-top: 1px solid #ccc;
+ background: white;
+`
+
+const EventsTable = styled('table')`
+ font-family: monospace;
+ font-size: 0.9em;
+ border-collapse: collapse;
+ border: none;
+ min-width: 100%;
+
+ & > * + * {
+ margin-top: 1px;
+ }
+
+ tr,
+ th,
+ td {
+ border: none;
+ }
+
+ th,
+ td {
+ text-align: left;
+ padding: 0.333em;
+ }
+
+ th {
+ position: sticky;
+ top: 0;
+ background-color: #eee;
+ border-bottom: 1px solid #ccc;
+ }
+
+ td {
+ background-color: white;
+ border-top: 1px solid #eee;
+ border-bottom: 1px solid #eee;
+ }
+`
+
+const Pill = styled('span')`
+ display: inline-block;
+ padding: 0.25em 0.33em;
+ border-radius: 4px;
+ background-color: ${p => p.color};
+`
+
+const I = styled(Icon)`
+ font-size: 0.9em;
+ color: ${p => p.color};
+`
+
+const MissingCell = props => texture
+
+const TypeCell = ({ event }) => {
+ switch (event.constructor.name) {
+ case 'CompositionEvent':
+ return {event.type}
+ case 'InputEvent':
+ return {event.type}
+ case 'KeyboardEvent':
+ return {event.type}
+ case 'Event':
+ return {event.type}
+ default:
+ return {event.type}
+ }
+}
+
+const BooleanCell = ({ value }) =>
+ value === true ? (
+ check
+ ) : value === false ? (
+ clear
+ ) : (
+
+ )
+
+const StringCell = ({ value }) =>
+ value == null ? : JSON.stringify(value)
+
+const RangeCell = ({ value }) =>
+ value == null ? (
+
+ ) : (
+ `${value.anchor.path.toJSON()}.${
+ value.anchor.offset
+ }–${value.focus.path.toJSON()}.${value.focus.offset}`
+ )
+
+const EventsList = () => (
+
+ {({ value: events }) => (
+
+
+
+
+
+ {
+ e.preventDefault()
+ EventsValue.clear()
+ }}
+ >
+ block
+
+ |
+ type |
+ key |
+ code |
+ repeat |
+ inputType |
+ data |
+ dataTransfer |
+ targetRange |
+ isComposing |
+ findSelection |
+
+
+
+ {events.map((props, i) => )}
+
+
+
+ )}
+
+)
+
+const Event = ({ event, targetRange, selection }) => {
+ return (
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ )
+}
+
+class InputTester extends React.Component {
+ state = {
+ value: Value.fromJSON(initialValue),
+ }
+
+ componentDidMount() {
+ const editor = this.el.querySelector('[contenteditable="true"]')
+ editor.addEventListener('keydown', this.onEvent)
+ editor.addEventListener('keyup', this.onEvent)
+ editor.addEventListener('keypress', this.onEvent)
+ editor.addEventListener('input', this.onEvent)
+ editor.addEventListener('beforeinput', this.onEvent)
+ editor.addEventListener('compositionstart', this.onEvent)
+ editor.addEventListener('compositionupdate', this.onEvent)
+ editor.addEventListener('compositionend', this.onEvent)
+ window.document.addEventListener('selectionchange', this.onEvent)
+ }
+
+ render() {
+ return (
+
+ {
+ switch (node.type) {
+ case 'block-quote':
+ return {children}
+ case 'bulleted-list':
+ return
+ case 'heading-one':
+ return {children}
+ case 'heading-two':
+ return {children}
+ case 'list-item':
+ return {children}
+ case 'numbered-list':
+ return {children}
+ }
+ }}
+ renderMark={({ attributes, children, mark }) => {
+ switch (mark.type) {
+ case 'bold':
+ return {children}
+ case 'code':
+ return {children}
+ case 'italic':
+ return {children}
+ case 'underlined':
+ return {children}
+ }
+ }}
+ />
+
+
+ )
+ }
+
+ onRef = ref => {
+ this.el = ref
+ }
+
+ onChange = ({ value }) => {
+ this.setState({ value })
+ this.recordEvent({ type: 'change' })
+ this.logEvent({ type: 'change' })
+ }
+
+ onEvent = event => {
+ this.recordEvent(event)
+ this.logEvent(event)
+ }
+
+ recordEvent = event => {
+ const { value } = this.state
+ let targetRange
+
+ if (event.getTargetRanges) {
+ const [nativeTargetRange] = event.getTargetRanges()
+ targetRange = nativeTargetRange && findRange(nativeTargetRange, value)
+ }
+
+ const nativeSelection = window.getSelection()
+ const nativeRange = nativeSelection.rangeCount
+ ? nativeSelection.getRangeAt(0)
+ : undefined
+ const selection = nativeRange && findRange(nativeRange, value)
+
+ EventsValue.push({
+ event,
+ value,
+ targetRange,
+ selection,
+ })
+ }
+
+ logEvent = event => {
+ const { value } = this.state
+ const nativeSelection = window.getSelection()
+ const nativeRange = nativeSelection.rangeCount
+ ? nativeSelection.getRangeAt(0)
+ : undefined
+ const selection = nativeRange && findRange(nativeRange, value)
+
+ const {
+ type,
+ key,
+ code,
+ inputType,
+ data,
+ dataTransfer,
+ isComposing,
+ } = event
+
+ const prefix = `%c${type.padEnd(15)}`
+ let style = 'padding: 3px'
+ let details
+
+ switch (event.constructor.name) {
+ case 'CompositionEvent': {
+ style += '; background-color: thistle'
+ details = { data, selection, value }
+ break
+ }
+
+ case 'InputEvent': {
+ style += '; background-color: lightskyblue'
+ const [nativeTargetRange] = event.getTargetRanges()
+ const targetRange =
+ nativeTargetRange && findRange(nativeTargetRange, value)
+
+ details = {
+ inputType,
+ data,
+ dataTransfer,
+ targetRange,
+ isComposing,
+ selection,
+ value,
+ }
+
+ break
+ }
+
+ case 'KeyboardEvent': {
+ style += '; background-color: wheat'
+ details = { key, code, isComposing, selection, value }
+ break
+ }
+
+ case 'Event': {
+ style += '; background-color: #ddd'
+ details = { isComposing, selection, value }
+ break
+ }
+
+ default: {
+ style += '; background-color: palegreen'
+ details = { selection, value }
+ break
+ }
+ }
+
+ console.log(prefix, style, details) // eslint-disable-line no-console
+ }
+}
+
+export default InputTester
diff --git a/examples/input-tester/value.json b/examples/input-tester/value.json
new file mode 100644
index 000000000..a88082842
--- /dev/null
+++ b/examples/input-tester/value.json
@@ -0,0 +1,83 @@
+{
+ "document": {
+ "nodes": [
+ {
+ "object": "block",
+ "type": "paragraph",
+ "nodes": [
+ {
+ "object": "text",
+ "leaves": [
+ {
+ "text": "This Slate editor records all of the "
+ },
+ {
+ "text": "bold",
+ "marks": [
+ {
+ "type": "keyboard"
+ }
+ ]
+ },
+ {
+ "text": ", "
+ },
+ {
+ "text": "bold",
+ "marks": [
+ {
+ "type": "input"
+ }
+ ]
+ },
+ {
+ "text": " and "
+ },
+ {
+ "text": "bold",
+ "marks": [
+ {
+ "type": "selection"
+ }
+ ]
+ },
+ {
+ "text":
+ " event that occur while using it, so you can debug the exact combination of events that is firing for particular editing behaviors."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "object": "block",
+ "type": "block-quote",
+ "nodes": [
+ {
+ "object": "text",
+ "leaves": [
+ {
+ "text":
+ "And this is a quote in case you need to try testing across block types."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "object": "block",
+ "type": "paragraph",
+ "nodes": [
+ {
+ "object": "text",
+ "leaves": [
+ {
+ "text": "Try it out!"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/package.json b/package.json
index 6cc24931f..ce5063c37 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
"react-hot-loader": "^3.1.3",
"react-portal": "^4.1.5",
"react-router-dom": "^4.3.1",
+ "react-values": "^0.3.0",
"read-metadata": "^1.0.0",
"rollup": "^0.55.1",
"rollup-plugin-alias": "^1.4.0",
diff --git a/yarn.lock b/yarn.lock
index 6564e3eb3..402c32c91 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7072,6 +7072,10 @@ react-router@^4.3.1:
prop-types "^15.6.1"
warning "^4.0.1"
+react-values@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/react-values/-/react-values-0.3.0.tgz#ae592c368ea50bfa6063029e31430598026f5287"
+
react@^16.4.1:
version "16.4.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32"