mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-10 17:24:02 +02:00
Add input tester example (#2068)
#### Is this adding or improving a _feature_ or fixing a _bug_? Example. #### What's the new behavior? Adds a new example that is an input event logger, for more easily seeing which input/keyboard/selection events are firing when editing in a Slate editor.  #### Have you checked that...? * [x] The new code matches the existing patterns and styles. * [x] The tests pass with `yarn test`. * [x] The linter passes with `yarn lint`. (Fix errors with `yarn prettier`.) * [x] The relevant examples still work. (Run examples with `yarn watch`.)
This commit is contained in:
@@ -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 }) => <RouterLink {...props} />)`
|
||||
const MaskedRouterLink = ({ active, ...props }) => <RouterLink {...props} />
|
||||
|
||||
const Tab = styled(MaskedRouterLink)`
|
||||
display: inline-block;
|
||||
margin-bottom: 0.2em;
|
||||
padding: 0.2em 0.5em;
|
||||
|
364
examples/input-tester/index.js
Normal file
364
examples/input-tester/index.js
Normal file
@@ -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 => <I color="silver">texture</I>
|
||||
|
||||
const TypeCell = ({ event }) => {
|
||||
switch (event.constructor.name) {
|
||||
case 'CompositionEvent':
|
||||
return <Pill color="thistle">{event.type}</Pill>
|
||||
case 'InputEvent':
|
||||
return <Pill color="lightskyblue">{event.type}</Pill>
|
||||
case 'KeyboardEvent':
|
||||
return <Pill color="wheat">{event.type}</Pill>
|
||||
case 'Event':
|
||||
return <Pill color="#ddd">{event.type}</Pill>
|
||||
default:
|
||||
return <Pill color="palegreen">{event.type}</Pill>
|
||||
}
|
||||
}
|
||||
|
||||
const BooleanCell = ({ value }) =>
|
||||
value === true ? (
|
||||
<I color="mediumseagreen">check</I>
|
||||
) : value === false ? (
|
||||
<I color="tomato">clear</I>
|
||||
) : (
|
||||
<MissingCell />
|
||||
)
|
||||
|
||||
const StringCell = ({ value }) =>
|
||||
value == null ? <MissingCell /> : JSON.stringify(value)
|
||||
|
||||
const RangeCell = ({ value }) =>
|
||||
value == null ? (
|
||||
<MissingCell />
|
||||
) : (
|
||||
`${value.anchor.path.toJSON()}.${
|
||||
value.anchor.offset
|
||||
}–${value.focus.path.toJSON()}.${value.focus.offset}`
|
||||
)
|
||||
|
||||
const EventsList = () => (
|
||||
<EventsValue>
|
||||
{({ value: events }) => (
|
||||
<EventsWrapper>
|
||||
<EventsTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<I
|
||||
color="#666"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onMouseDown={e => {
|
||||
e.preventDefault()
|
||||
EventsValue.clear()
|
||||
}}
|
||||
>
|
||||
block
|
||||
</I>
|
||||
</th>
|
||||
<th>type</th>
|
||||
<th>key</th>
|
||||
<th>code</th>
|
||||
<th>repeat</th>
|
||||
<th>inputType</th>
|
||||
<th>data</th>
|
||||
<th>dataTransfer</th>
|
||||
<th>targetRange</th>
|
||||
<th>isComposing</th>
|
||||
<th>findSelection</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.map((props, i) => <Event key={i} {...props} />)}
|
||||
</tbody>
|
||||
</EventsTable>
|
||||
</EventsWrapper>
|
||||
)}
|
||||
</EventsValue>
|
||||
)
|
||||
|
||||
const Event = ({ event, targetRange, selection }) => {
|
||||
return (
|
||||
<tr>
|
||||
<td />
|
||||
<td>
|
||||
<TypeCell event={event} />
|
||||
</td>
|
||||
<td>
|
||||
<StringCell value={event.key} />
|
||||
</td>
|
||||
<td>
|
||||
<StringCell value={event.code} />
|
||||
</td>
|
||||
<td>
|
||||
<BooleanCell value={event.repeat} />
|
||||
</td>
|
||||
<td>
|
||||
<StringCell value={event.inputType} />
|
||||
</td>
|
||||
<td>
|
||||
<StringCell value={event.data} />
|
||||
</td>
|
||||
<td>
|
||||
<StringCell
|
||||
value={event.dataTransfer && event.dataTransfer.get('text/plain')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<RangeCell value={targetRange} />
|
||||
</td>
|
||||
<td>
|
||||
<BooleanCell value={event.isComposing} />
|
||||
</td>
|
||||
<td>
|
||||
<RangeCell value={selection} />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Wrapper innerRef={this.onRef}>
|
||||
<Editor
|
||||
spellCheck
|
||||
placeholder="Enter some text..."
|
||||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
renderNode={({ attributes, children, node }) => {
|
||||
switch (node.type) {
|
||||
case 'block-quote':
|
||||
return <blockquote {...attributes}>{children}</blockquote>
|
||||
case 'bulleted-list':
|
||||
return <ul {...attributes}>{children}</ul>
|
||||
case 'heading-one':
|
||||
return <h1 {...attributes}>{children}</h1>
|
||||
case 'heading-two':
|
||||
return <h2 {...attributes}>{children}</h2>
|
||||
case 'list-item':
|
||||
return <li {...attributes}>{children}</li>
|
||||
case 'numbered-list':
|
||||
return <ol {...attributes}>{children}</ol>
|
||||
}
|
||||
}}
|
||||
renderMark={({ attributes, children, mark }) => {
|
||||
switch (mark.type) {
|
||||
case 'bold':
|
||||
return <strong {...attributes}>{children}</strong>
|
||||
case 'code':
|
||||
return <code {...attributes}>{children}</code>
|
||||
case 'italic':
|
||||
return <em {...attributes}>{children}</em>
|
||||
case 'underlined':
|
||||
return <u {...attributes}>{children}</u>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<EventsList />
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
83
examples/input-tester/value.json
Normal file
83
examples/input-tester/value.json
Normal file
@@ -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!"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@@ -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",
|
||||
|
@@ -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"
|
||||
|
Reference in New Issue
Block a user