1
0
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.

![image](https://user-images.githubusercontent.com/311752/43937175-ea9231e2-9c10-11e8-8e29-2cd9a24ca78d.png)

#### 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:
Ian Storm Taylor
2018-08-09 20:30:23 -07:00
committed by GitHub
parent f812816b7d
commit 1d53c5b1ef
5 changed files with 457 additions and 1 deletions

View File

@@ -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;

View 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

View 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!"
}
]
}
]
}
]
}
}

View File

@@ -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",

View File

@@ -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"