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
    {children}
+ 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"