mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-01 04:50:27 +02:00
This reverts commit 17cdeae858
.
This commit is contained in:
@@ -29,7 +29,6 @@ import RTL from './rtl'
|
||||
import ReadOnly from './read-only'
|
||||
import RichText from './rich-text'
|
||||
import SearchHighlighting from './search-highlighting'
|
||||
import Composition from './composition'
|
||||
import InputTester from './input-tester'
|
||||
import SyncingOperations from './syncing-operations'
|
||||
import Tables from './tables'
|
||||
@@ -44,7 +43,6 @@ import Mentions from './mentions'
|
||||
const EXAMPLES = [
|
||||
['Check Lists', CheckLists, '/check-lists'],
|
||||
['Code Highlighting', CodeHighlighting, '/code-highlighting'],
|
||||
['Composition', Composition, '/composition/:subpage?'],
|
||||
['Embeds', Embeds, '/embeds'],
|
||||
['Emojis', Emojis, '/emojis'],
|
||||
['Forced Layout', ForcedLayout, '/forced-layout'],
|
||||
@@ -264,13 +262,11 @@ export default class App extends React.Component {
|
||||
<Switch>
|
||||
{EXAMPLES.map(([name, Component, path]) => (
|
||||
<Route key={path} path={path}>
|
||||
{({ match }) => (
|
||||
<div>
|
||||
<ExampleContent>
|
||||
<Component params={match.params} />
|
||||
</ExampleContent>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<ExampleContent>
|
||||
<Component />
|
||||
</ExampleContent>
|
||||
</div>
|
||||
</Route>
|
||||
))}
|
||||
<Redirect from="/" to="/rich-text" />
|
||||
@@ -294,7 +290,7 @@ export default class App extends React.Component {
|
||||
</TabButton>
|
||||
<Switch>
|
||||
{EXAMPLES.map(([name, Component, path]) => (
|
||||
<Route key={path} path={path}>
|
||||
<Route key={path} exact path={path}>
|
||||
<ExampleTitle>{name}</ExampleTitle>
|
||||
</Route>
|
||||
))}
|
||||
|
@@ -1,401 +0,0 @@
|
||||
import { Editor } from 'slate-react'
|
||||
import { Value } from 'slate'
|
||||
|
||||
import React from 'react'
|
||||
import styled from 'react-emotion'
|
||||
import { Link, Redirect } from 'react-router-dom'
|
||||
import splitJoin from './split-join.js'
|
||||
import insert from './insert.js'
|
||||
import special from './special.js'
|
||||
import { isKeyHotkey } from 'is-hotkey'
|
||||
import { Button, Icon, Toolbar } from '../components'
|
||||
|
||||
/**
|
||||
* Define the default node type.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
|
||||
const DEFAULT_NODE = 'paragraph'
|
||||
|
||||
/**
|
||||
* Some styled components.
|
||||
*
|
||||
* @type {Component}
|
||||
*/
|
||||
|
||||
const Instruction = styled('div')`
|
||||
white-space: pre-wrap;
|
||||
margin: -1em -1em 1em;
|
||||
padding: 0.5em;
|
||||
background: #eee;
|
||||
`
|
||||
|
||||
const Tabs = styled('div')`
|
||||
margin-bottom: 0.5em;
|
||||
`
|
||||
|
||||
const TabLink = ({ active, ...props }) => <Link {...props} />
|
||||
|
||||
const Tab = styled(TabLink)`
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
background: ${p => (p.active ? '#AAA' : '#DDD')};
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: 0.25em;
|
||||
margin-right: 0.25em;
|
||||
`
|
||||
|
||||
/**
|
||||
* Subpages which are each a smoke test.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const SUBPAGES = [
|
||||
['Split/Join', splitJoin, 'split-join'],
|
||||
['Insertion', insert, 'insert'],
|
||||
['Special', special, 'special'],
|
||||
]
|
||||
|
||||
/**
|
||||
* Define hotkey matchers.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
const isBoldHotkey = isKeyHotkey('mod+b')
|
||||
const isItalicHotkey = isKeyHotkey('mod+i')
|
||||
const isUnderlinedHotkey = isKeyHotkey('mod+u')
|
||||
const isCodeHotkey = isKeyHotkey('mod+`')
|
||||
|
||||
/**
|
||||
* The rich text example.
|
||||
*
|
||||
* @type {Component}
|
||||
*/
|
||||
|
||||
class RichTextExample extends React.Component {
|
||||
state = {}
|
||||
|
||||
/**
|
||||
* Select and deserialize the initial editor value.
|
||||
*
|
||||
* @param {Object} nextProps
|
||||
* @param {Object} prevState
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
const { subpage } = nextProps.params
|
||||
if (subpage === prevState.subpage) return null
|
||||
const found = SUBPAGES.find(
|
||||
([name, value, iSubpage]) => iSubpage === subpage
|
||||
)
|
||||
if (found == null) return {}
|
||||
const { text, document } = found[1]
|
||||
return {
|
||||
subpage,
|
||||
text,
|
||||
value: Value.fromJSON({ document }),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current selection has a mark with `type` in it.
|
||||
*
|
||||
* @param {String} type
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
hasMark = type => {
|
||||
const { value } = this.state
|
||||
return value.activeMarks.some(mark => mark.type == type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the any of the currently selected blocks are of `type`.
|
||||
*
|
||||
* @param {String} type
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
hasBlock = type => {
|
||||
const { value } = this.state
|
||||
return value.blocks.some(node => node.type == type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a reference to the `editor`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
ref = editor => {
|
||||
this.editor = editor
|
||||
}
|
||||
|
||||
/**
|
||||
* Render.
|
||||
*
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
render() {
|
||||
const { text } = this.state
|
||||
if (text == null) return <Redirect to="/composition/split-join" />
|
||||
return (
|
||||
<div>
|
||||
<Instruction>
|
||||
<Tabs>
|
||||
{SUBPAGES.map(([name, Component, subpage]) => {
|
||||
const active = subpage === this.props.params.subpage
|
||||
return (
|
||||
<Tab
|
||||
key={subpage}
|
||||
to={`/composition/${subpage}`}
|
||||
active={active}
|
||||
>
|
||||
{name}
|
||||
</Tab>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
<div>{this.state.text}</div>
|
||||
</Instruction>
|
||||
<Toolbar>
|
||||
{this.renderMarkButton('bold', 'format_bold')}
|
||||
{this.renderMarkButton('italic', 'format_italic')}
|
||||
{this.renderMarkButton('underlined', 'format_underlined')}
|
||||
{this.renderMarkButton('code', 'code')}
|
||||
{this.renderBlockButton('heading-one', 'looks_one')}
|
||||
{this.renderBlockButton('heading-two', 'looks_two')}
|
||||
{this.renderBlockButton('block-quote', 'format_quote')}
|
||||
{this.renderBlockButton('numbered-list', 'format_list_numbered')}
|
||||
{this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
|
||||
</Toolbar>
|
||||
<Editor
|
||||
spellCheck
|
||||
autoFocus
|
||||
placeholder="Enter some rich text..."
|
||||
ref={this.ref}
|
||||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
renderNode={this.renderNode}
|
||||
renderMark={this.renderMark}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a mark-toggling toolbar button.
|
||||
*
|
||||
* @param {String} type
|
||||
* @param {String} icon
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
renderMarkButton = (type, icon) => {
|
||||
const isActive = this.hasMark(type)
|
||||
|
||||
return (
|
||||
<Button
|
||||
active={isActive}
|
||||
onMouseDown={event => this.onClickMark(event, type)}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a block-toggling toolbar button.
|
||||
*
|
||||
* @param {String} type
|
||||
* @param {String} icon
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
renderBlockButton = (type, icon) => {
|
||||
let isActive = this.hasBlock(type)
|
||||
|
||||
if (['numbered-list', 'bulleted-list'].includes(type)) {
|
||||
const { value: { document, blocks } } = this.state
|
||||
|
||||
if (blocks.size > 0) {
|
||||
const parent = document.getParent(blocks.first().key)
|
||||
isActive = this.hasBlock('list-item') && parent && parent.type === type
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
active={isActive}
|
||||
onMouseDown={event => this.onClickBlock(event, type)}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Slate node.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
renderNode = (props, editor, next) => {
|
||||
const { attributes, children, node } = props
|
||||
|
||||
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>
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Slate mark.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @return {Element}
|
||||
*/
|
||||
|
||||
renderMark = (props, editor, next) => {
|
||||
const { children, mark, attributes } = props
|
||||
|
||||
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>
|
||||
default:
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On change, save the new `value`.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
onChange = ({ value }) => {
|
||||
this.setState({ value })
|
||||
}
|
||||
|
||||
/**
|
||||
* On key down, if it's a formatting command toggle a mark.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Editor} editor
|
||||
* @return {Change}
|
||||
*/
|
||||
|
||||
onKeyDown = (event, editor, next) => {
|
||||
let mark
|
||||
|
||||
if (isBoldHotkey(event)) {
|
||||
mark = 'bold'
|
||||
} else if (isItalicHotkey(event)) {
|
||||
mark = 'italic'
|
||||
} else if (isUnderlinedHotkey(event)) {
|
||||
mark = 'underlined'
|
||||
} else if (isCodeHotkey(event)) {
|
||||
mark = 'code'
|
||||
} else {
|
||||
return next()
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
editor.toggleMark(mark)
|
||||
}
|
||||
|
||||
/**
|
||||
* When a mark button is clicked, toggle the current mark.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {String} type
|
||||
*/
|
||||
|
||||
onClickMark = (event, type) => {
|
||||
event.preventDefault()
|
||||
this.editor.toggleMark(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* When a block button is clicked, toggle the block type.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {String} type
|
||||
*/
|
||||
|
||||
onClickBlock = (event, type) => {
|
||||
event.preventDefault()
|
||||
|
||||
const { editor } = this
|
||||
const { value } = editor
|
||||
const { document } = value
|
||||
|
||||
// Handle everything but list buttons.
|
||||
if (type != 'bulleted-list' && type != 'numbered-list') {
|
||||
const isActive = this.hasBlock(type)
|
||||
const isList = this.hasBlock('list-item')
|
||||
|
||||
if (isList) {
|
||||
editor
|
||||
.setBlocks(isActive ? DEFAULT_NODE : type)
|
||||
.unwrapBlock('bulleted-list')
|
||||
.unwrapBlock('numbered-list')
|
||||
} else {
|
||||
editor.setBlocks(isActive ? DEFAULT_NODE : type)
|
||||
}
|
||||
} else {
|
||||
// Handle the extra wrapping required for list buttons.
|
||||
const isList = this.hasBlock('list-item')
|
||||
const isType = value.blocks.some(block => {
|
||||
return !!document.getClosest(block.key, parent => parent.type == type)
|
||||
})
|
||||
|
||||
if (isList && isType) {
|
||||
editor
|
||||
.setBlocks(DEFAULT_NODE)
|
||||
.unwrapBlock('bulleted-list')
|
||||
.unwrapBlock('numbered-list')
|
||||
} else if (isList) {
|
||||
editor
|
||||
.unwrapBlock(
|
||||
type == 'bulleted-list' ? 'numbered-list' : 'bulleted-list'
|
||||
)
|
||||
.wrapBlock(type)
|
||||
} else {
|
||||
editor.setBlocks('list-item').wrapBlock(type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*/
|
||||
|
||||
export default RichTextExample
|
@@ -1,15 +0,0 @@
|
||||
import { p, text, bold } from './util'
|
||||
|
||||
export default {
|
||||
text: `Enter text below each line of instruction exactly including mis-spelling wasnt`,
|
||||
document: {
|
||||
nodes: [
|
||||
p(bold('Tap on virtual keyboard: '), text('It wasnt me. No.')),
|
||||
p(),
|
||||
p(bold('Gesture write: '), text('Yes Sam, I am.')),
|
||||
p(),
|
||||
p(bold('If you have IME, write any two words with it')),
|
||||
p(),
|
||||
],
|
||||
},
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
import { p, bold } from './util'
|
||||
|
||||
export default {
|
||||
text: `Follow the instructions on each line exactly`,
|
||||
document: {
|
||||
nodes: [
|
||||
p(bold('Type "it is". cursor to "i|t" then hit enter.')),
|
||||
p(''),
|
||||
p(
|
||||
bold(
|
||||
'Cursor to "mid|dle" then press space, backspace, space, backspace. Should say "middle".'
|
||||
)
|
||||
),
|
||||
p('The middle word.'),
|
||||
p(
|
||||
bold(
|
||||
'Cursor in line below. Wait for caps on keyboard to show up. If not try again. Type "It me. No." and it should not mangle on the last period.'
|
||||
)
|
||||
),
|
||||
p(''),
|
||||
],
|
||||
},
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
import { p, text, bold } from './util'
|
||||
|
||||
export default {
|
||||
text: `Hit enter x2 then backspace x2 before word "before", after "after", and in "middle" between two "d"s`,
|
||||
document: {
|
||||
nodes: [
|
||||
p(
|
||||
text('Before it before it '),
|
||||
bold('before'),
|
||||
text(' it middle it '),
|
||||
bold('middle'),
|
||||
text(' it after it '),
|
||||
bold('after'),
|
||||
text(' it after')
|
||||
),
|
||||
],
|
||||
},
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
export function p(...leaves) {
|
||||
return {
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
nodes: [{ object: 'text', leaves }],
|
||||
}
|
||||
}
|
||||
|
||||
export function text(textContent) {
|
||||
return { text: textContent }
|
||||
}
|
||||
|
||||
export function bold(textContent) {
|
||||
return { text: textContent, marks: [{ type: 'bold' }] }
|
||||
}
|
@@ -1,119 +0,0 @@
|
||||
{
|
||||
"document": {
|
||||
"nodes": [
|
||||
{
|
||||
"object": "block",
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"object": "text",
|
||||
"leaves": [
|
||||
{ "text": "Insert Text: ", "marks": [{ "type": "bold" }] },
|
||||
{
|
||||
"text":
|
||||
"Type 'cat' before every word 'before' and after every word 'after' and in the middle of the word 'pion' so that it says 'pi cat on' using the virtual keyboard",
|
||||
"marks": [{ "type": "italic" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "block",
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"object": "text",
|
||||
"leaves": [
|
||||
{
|
||||
"text": "Before there before is pion at after for after"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "block",
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"object": "text",
|
||||
"leaves": [
|
||||
{ "text": "Handle Enter: ", "marks": [{ "type": "bold" }] },
|
||||
{
|
||||
"text":
|
||||
"Hit Enter twice before every word 'before' and after every word 'after' and in the middle of the word 'split'",
|
||||
"marks": [{ "type": "italic" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "block",
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"object": "text",
|
||||
"leaves": [
|
||||
{
|
||||
"text": "Before there before is split at after for after"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "block",
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"object": "text",
|
||||
"leaves": [
|
||||
{
|
||||
"text":
|
||||
"Since it's rich text, you can do things like turn a selection of text "
|
||||
},
|
||||
{
|
||||
"text": "bold",
|
||||
"marks": [{ "type": "bold" }]
|
||||
},
|
||||
{
|
||||
"text":
|
||||
", or add a semantically rendered block quote in the middle of the page, like this:"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "block",
|
||||
"type": "block-quote",
|
||||
"nodes": [
|
||||
{
|
||||
"object": "text",
|
||||
"leaves": [
|
||||
{
|
||||
"text": "A wise quote."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"object": "block",
|
||||
"type": "paragraph",
|
||||
"nodes": [
|
||||
{
|
||||
"object": "text",
|
||||
"leaves": [
|
||||
{
|
||||
"text": "Try it out for yourself!"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@@ -93,10 +93,8 @@
|
||||
"bootstrap": "lerna bootstrap && yarn build",
|
||||
"build": "rollup --config ./support/rollup/config.js",
|
||||
"build:production": "cross-env NODE_ENV=production rollup --config ./support/rollup/config.js && cross-env NODE_ENV=production webpack --config support/webpack/config.js",
|
||||
"build:clean-fork": "rm ./build/CNAME",
|
||||
"clean": "lerna run clean && rm -rf ./node_modules ./dist ./build",
|
||||
"gh-pages": "gh-pages --dist ./build",
|
||||
"gh-pages:fork": "npm-run-all build:production build:clean-fork gh-pages",
|
||||
"lint": "yarn lint:eslint && yarn lint:prettier",
|
||||
"lint:eslint": "eslint benchmark packages/*/src packages/*/test examples/*/*.js examples/dev/*/*.js",
|
||||
"lint:prettier": "prettier --list-different '**/*.{md,json,css}'",
|
||||
|
@@ -4,12 +4,7 @@ import Types from 'prop-types'
|
||||
import getWindow from 'get-window'
|
||||
import warning from 'tiny-warning'
|
||||
import throttle from 'lodash/throttle'
|
||||
import {
|
||||
IS_ANDROID,
|
||||
IS_FIREFOX,
|
||||
HAS_INPUT_EVENTS_LEVEL_2,
|
||||
} from 'slate-dev-environment'
|
||||
import ANDROID_API_VERSION from '../utils/android-api-version'
|
||||
import { IS_FIREFOX, HAS_INPUT_EVENTS_LEVEL_2 } from 'slate-dev-environment'
|
||||
|
||||
import EVENT_HANDLERS from '../constants/event-handlers'
|
||||
import Node from './node'
|
||||
@@ -29,15 +24,6 @@ const FIREFOX_NODE_TYPE_ACCESS_ERROR = /Permission denied to access property "no
|
||||
|
||||
const debug = Debug('slate:content')
|
||||
|
||||
/**
|
||||
* Separate debug to easily see when the DOM has updated either by render or
|
||||
* changing selection.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
debug.update = Debug('slate:update')
|
||||
|
||||
/**
|
||||
* Content.
|
||||
*
|
||||
@@ -113,7 +99,7 @@ class Content extends React.Component {
|
||||
|
||||
// COMPAT: Restrict scope of `beforeinput` to clients that support the
|
||||
// Input Events Level 2 spec, since they are preventable events.
|
||||
if (HAS_INPUT_EVENTS_LEVEL_2 || ANDROID_API_VERSION === 28) {
|
||||
if (HAS_INPUT_EVENTS_LEVEL_2) {
|
||||
this.element.addEventListener('beforeinput', this.handlers.onBeforeInput)
|
||||
}
|
||||
|
||||
@@ -134,7 +120,7 @@ class Content extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
if (HAS_INPUT_EVENTS_LEVEL_2 || ANDROID_API_VERSION === 28) {
|
||||
if (HAS_INPUT_EVENTS_LEVEL_2) {
|
||||
this.element.removeEventListener(
|
||||
'beforeinput',
|
||||
this.handlers.onBeforeInput
|
||||
@@ -147,14 +133,6 @@ class Content extends React.Component {
|
||||
*/
|
||||
|
||||
componentDidUpdate() {
|
||||
debug.update('componentDidUpdate')
|
||||
// NOTE:
|
||||
// Don't disable `updateSelection` on Android. Clicking a word and a
|
||||
// suggestion breaks on API27. It does fix the crazy jumping cursor loop
|
||||
// when doing an auto-suggest on a fully enclosed text with bold though.
|
||||
// Most likely it still needs other fix issues though.
|
||||
//
|
||||
// if (IS_ANDROID) return
|
||||
this.updateSelection()
|
||||
}
|
||||
|
||||
@@ -170,7 +148,6 @@ class Content extends React.Component {
|
||||
const window = getWindow(this.element)
|
||||
const native = window.getSelection()
|
||||
const { activeElement } = window.document
|
||||
debug.update('updateSelection', { selection: selection.toJSON() })
|
||||
|
||||
// COMPAT: In Firefox, there's a but where `getSelection` can return `null`.
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=827585 (2018/11/07)
|
||||
@@ -283,7 +260,6 @@ class Content extends React.Component {
|
||||
|
||||
if (updated) {
|
||||
debug('updateSelection', { selection, native, activeElement })
|
||||
debug.update('updateSelection-applied', { selection })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,12 +339,7 @@ class Content extends React.Component {
|
||||
// cases we don't need to trigger any changes, since our internal model is
|
||||
// already up to date, but we do want to update the native selection again
|
||||
// to make sure it is in sync. (2017/10/16)
|
||||
//
|
||||
// ANDROID: The updateSelection causes issues in Android when you are
|
||||
// at the end of a black. The selection ends up to the left of the inserted
|
||||
// character instead of to the right. This behavior continues even if
|
||||
// you enter more than one character. (2019/01/03)
|
||||
if (!IS_ANDROID && handler == 'onSelect') {
|
||||
if (handler == 'onSelect') {
|
||||
const { editor } = this.props
|
||||
const { value } = editor
|
||||
const { selection } = value
|
||||
@@ -490,12 +461,6 @@ class Content extends React.Component {
|
||||
|
||||
debug('render', { props })
|
||||
|
||||
debug.update('render', {
|
||||
text: value.document.text,
|
||||
selection: value.selection.toJSON(),
|
||||
value: value.toJSON(),
|
||||
})
|
||||
|
||||
return (
|
||||
<Container
|
||||
{...handlers}
|
||||
|
@@ -1,219 +0,0 @@
|
||||
# Android
|
||||
|
||||
The following is a list of unexpected behaviors in Android
|
||||
|
||||
# Debugging
|
||||
|
||||
```
|
||||
slate:android,slate:before,slate:after,slate:update,slate:reconcile
|
||||
```
|
||||
|
||||
# API 25
|
||||
|
||||
### Backspace Handling
|
||||
|
||||
There appears to be no way to discern that backspace was pressed by looking at the events. There are two options (1) check the DOM and (2) look for a signature in the mutation.
|
||||
|
||||
For (1) we may be able to look at the current block and see if it disappears in the `input` event. If it no longer appears there, we know a `backspace` is likely what happened.
|
||||
|
||||
* Join previous paragraph
|
||||
* keydown:Unidentified
|
||||
* DOM change
|
||||
* input:native
|
||||
* mutation
|
||||
* input:react
|
||||
* keyup
|
||||
|
||||
# API 28
|
||||
|
||||
### Backspace Handling
|
||||
|
||||
* join previous paragraph
|
||||
* compositionEnd
|
||||
* keydown:Unidentified
|
||||
* beforeinput:deleteContentBackward
|
||||
* DOM change
|
||||
* input:deleteContentBackward
|
||||
|
||||
## In the middle of a word, space then backspace
|
||||
|
||||
When you select in `edit|able` then press space and backspace we end up with `editble`.
|
||||
|
||||
When you hit `space` the composition hasn't ended.
|
||||
|
||||
## Two Words. One.
|
||||
|
||||
Type two words followed by a period. Then one word followed by a period.
|
||||
|
||||
The space after the second period is deleted. It does not happen if there is only one word followed by a period.
|
||||
|
||||
This text exhibits that issue when typed in a blank paragraph:
|
||||
|
||||
```
|
||||
It me. No.
|
||||
```
|
||||
|
||||
When we hit the period, here are the events:
|
||||
|
||||
* onCompositionEnd
|
||||
* onKeyDown:Unidentified
|
||||
* onBeforeInput:native:insertText "."
|
||||
* onBeforeInput:synthetic:TextEvent data:"."
|
||||
* onInput:insertText data:"."
|
||||
* onSelect
|
||||
* onKeyDown:Unidentified
|
||||
* onBeforeInput:deleteContentBackward
|
||||
* onInput:deleteContentBackward
|
||||
|
||||
# API 26/27
|
||||
|
||||
Although there are minor differences, API 26/27 behave similarly.
|
||||
|
||||
### It me. No. Failure with uppercase I.
|
||||
|
||||
Touch away from original selection position. Touch into a blank line. Wait for the keyboard to display uppercase letters. If it doesn't, this bug won't present itself.
|
||||
|
||||
Type `It me. No.` and upon hitting the final `.` you will end up with unexpected value which is usually removing the first `.` and putting the cursor behind it.
|
||||
|
||||
### Data in Input
|
||||
|
||||
In API 27, in certain situations, the `input` event returns data which lets us identify, for example, that a '.' was the last character typed. Other times, it does not provide this data.
|
||||
|
||||
If you start typing a line and the first character capitalizes itself, then you will not receive the data. If you start typing a line and the first character stays lower case, you will receive the data.
|
||||
|
||||
Because of this, `data` is not a reliable source of information even when it is available because in other scenarios it may not be.
|
||||
|
||||
### Backspace Handling
|
||||
|
||||
* Save the state using a snapshot during a `keydown` event as it may end up being a delete. The DOM is in a good before state at this time.
|
||||
* Look at the `input` event to see if `event.nativeEvent.inputType` is equal to `deleteContentBackward`. If it is, then we are going to have to handle a delete but the DOM is already damaged.
|
||||
* If we are handling a delete then:
|
||||
* stop the `reconciler` which was started at `compositionEnd` because we don't need to reconcile anything as we will be reverting dom then deleting.
|
||||
* start the `deleter` which will revert to the snapshot then execute the delete command within Slate after a `requestAnimationFrame` time.
|
||||
* HOWEVER! if an `onBeforeInput` is called before the `deleter` handler is executed, we now know that it wasn't a delete after all and instead it was responding to a text change from a suggestion. In this case:
|
||||
* cancel the `deleter`
|
||||
* resume the `reconciler`
|
||||
|
||||
### Enter Handling
|
||||
|
||||
* Save the state using a snapshot during a `compositionEnd` event as it may end up being an `enter`. The DOM is in a good before state at this time.
|
||||
* Look at the native version of the `beforeInput` event (two will fire a native and a React). Note: We manually forced Android to handle the native `beforeInput` by adding it to the `content` code.
|
||||
* If the `event.nativeEvent.inputType` is equal to `insertParagraph` or `insertLineBreak` then we have determined that the user pressed enter at the end of a block (and only at the end of a block).
|
||||
* If `enter is detected then:
|
||||
* `preventDefault`
|
||||
* set Slate's selection using the DOM
|
||||
* call `splitBlock`
|
||||
* Put some code in to make sure React's version of `beforeInput` doesn't fire by setting a variable. React's version will fire as it can't be cancelled from the native version even though we told it to stop.
|
||||
* During React's version of `beforeInput`, if the `data` property which is a string ends in a linefeed (character code 10) then we know that it was an enter anywhere other than the end of block. At this point the DOM is already damaged.
|
||||
* If we are handling an enter then:
|
||||
* cancel the reconciler which was started from the `compositionEnd` event because we don't want reconciliation from the DOM to happen.
|
||||
* wait until next animation frame
|
||||
* revert to the last good state
|
||||
* splitBlock using Slate
|
||||
|
||||
Events for different cases
|
||||
|
||||
* Start of word & Start of line
|
||||
* compositionEnd
|
||||
* keydown:Unidentified
|
||||
* input:deleteContentBackward START DELETER
|
||||
* keydown:Enter \*
|
||||
* beforeInput:insertParagraph \*
|
||||
* TOO LATE TO CANCEL
|
||||
* Middle of word
|
||||
* compositionEnd
|
||||
* keydown:Unidentified
|
||||
* input:deleteContentBackward START DELETER => SELF
|
||||
* keydown:Unidentified
|
||||
* beforeInput:CHR(10) at end \*
|
||||
* TOO LATE TO CANCEL
|
||||
* End of word
|
||||
* compositionEnd
|
||||
* keydown:Enter \*
|
||||
* beforeInput:insertParagraph
|
||||
* CANCELLABLE
|
||||
* End of line
|
||||
* keydown:Enter \*
|
||||
* beforeInput:insertParagraph
|
||||
* CANCELLABLE
|
||||
|
||||
Based on the previous cases:
|
||||
|
||||
* Use a snapshot if `input:deleteContentBackward` is detected before an Enter which is detected either by a `keydown:Enter` or a `beforeInput:insertParagraph` and we don't know which.
|
||||
* Cancel the event if we detect a `keydown:Enter` without an immediately preceding `input:deleteContentBackward`.
|
||||
|
||||
### Enter at Start of Line
|
||||
|
||||
**TODO:**
|
||||
|
||||
* Go through all the steps in the Backspace handler. An enter at the beginning of a block looks exactly like a `delete` action at the beginning. The `reconciler` will be cancelled in the course of these events.
|
||||
* A `keydown` event will fire with `event.key` === `Enter`. We need to set a variable `ENTER_START_OF_LINE` to `true`. Cancel the delete event and remove the reference.
|
||||
* NOTE!!! Looks like splitting at other positions (not end of line) also provides an `Enter` and might be preferable to using the native `beforeInput` which we had to hack in!!! Try this!!!
|
||||
* A `beforeinput` event will be called like in the `delete` code which usually cancels the `deleter` and resumes the `reconciler`. But since we removed the reference to the `deleter` neither of these methods are called.
|
||||
|
||||
# API 28
|
||||
|
||||
## DOM breaks when suggestion selected on text entirely within a `strong` tag
|
||||
|
||||
Appears similar to the bug in API 27.
|
||||
|
||||
## Can't hit Enter at begining of word API27 (probably 26 too)
|
||||
|
||||
WORKING ON THIS
|
||||
|
||||
## Can't split word with Enter (PARTIAL FIXED)
|
||||
|
||||
Move the cursor to `edit|able` where | is the cursor.
|
||||
|
||||
Hit `enter` on the virtual keyboard.
|
||||
|
||||
The `keydown` event does not indicate what key is being pressed so we don't know that we should be handling an enter. There are two opportunities:
|
||||
|
||||
1. The onBeforeInput event has a `data` property that contains the text immediately before the cursor and it includes `edit|` where the pipe indicates an enter.
|
||||
2. We can look through the text at the end of a composition and simulate hitting enter maybe.
|
||||
|
||||
### Fixed for API 28
|
||||
|
||||
Allow enter to go through to the before plugin even during a compositiong and it works in API 28.
|
||||
|
||||
### Broken in API 27
|
||||
|
||||
# API 27
|
||||
|
||||
## Typing at end of line yields wrong cursor position (FIXED)
|
||||
|
||||
When you enter any text at the end of a block, the text gets entered in the wrong position.
|
||||
|
||||
### Fix
|
||||
|
||||
Fixed by ignoring the `updateSelection` code in `content.js` on the `onEvent` method if we are in Android. This doesn't ignore `updateSelection` altogether, only in that one place.
|
||||
|
||||
## Missing `onCompositionStart` (FIXED)
|
||||
|
||||
### Desciption
|
||||
|
||||
Insert a word using the virtual keyboard. Click outside the editor. Touch after the last letter in the word. This will display some suggestions. Click one. Selecting a suggestion will fire the `onCompositionEnd` but will not fire the corresponding `onCompositionStart` before it.
|
||||
|
||||
### Fix
|
||||
|
||||
Fixed by setting `isComposing` from the `onCompositionEnd` event until the `requestAnimationFrame` callback is executed.
|
||||
|
||||
## DOM breaks when suggestion selected on text entirely within a `strong` tag
|
||||
|
||||
Touch anywhere in the bold word "rich" in the example. Select an alternative recommendation and we get a failure.
|
||||
|
||||
Android is destroying the `strong` tag and replacing it with a `b` tag.
|
||||
|
||||
The problem does not present itself if the word is surrounding by spaces before the `strong` tag.
|
||||
|
||||
A possible fix may be to surround the word with a `ZERO WIDTH NO-BREAK SPACE` represented as `` in HTML. It appears in React for empty paragraphs.#
|
||||
|
||||
## Other stuff
|
||||
|
||||
In API 28 and possibly other versions of Android, when you select inside an empty block, the block is not actually empty. It contains a `ZERO WIDTH NO-BREAK SPACE` which is `𐃁` or `\uFEFF`.
|
||||
|
||||
When the editor first starts, if you click immediately into an empty block, you will end up to the right of the zero-width space. Because of this, we don't get the all caps because I presume the editor only capitalizes the first characters and since the no break space is the first character it doesn't do this.
|
||||
|
||||
But also, as a side effect, you end up in a different editing mode which fires events differently. This breaks a bunch of things.
|
||||
|
||||
The fix (which I will be attempting) is to move the offset to `0` if we find ourselves in a block with the property `data-slate-zero-width="n"`.
|
@@ -1,624 +0,0 @@
|
||||
import Debug from 'debug'
|
||||
import getWindow from 'get-window'
|
||||
import pick from 'lodash/pick'
|
||||
|
||||
import API_VERSION from '../utils/android-api-version'
|
||||
import fixSelectionInZeroWidthBlock from '../utils/fix-selection-in-zero-width-block'
|
||||
import getSelectionFromDom from '../utils/get-selection-from-dom'
|
||||
import setSelectionFromDom from '../utils/set-selection-from-dom'
|
||||
import setTextFromDomNode from '../utils/set-text-from-dom-node'
|
||||
import isInputDataEnter from '../utils/is-input-data-enter'
|
||||
import isInputDataLastChar from '../utils/is-input-data-last-char'
|
||||
import SlateSnapshot from '../utils/slate-snapshot'
|
||||
import Executor from '../utils/executor'
|
||||
|
||||
const debug = Debug('slate:android')
|
||||
debug.reconcile = Debug('slate:reconcile')
|
||||
|
||||
debug('API_VERSION', { API_VERSION })
|
||||
|
||||
/**
|
||||
* Define variables related to composition state.
|
||||
*/
|
||||
|
||||
const NONE = 0
|
||||
const COMPOSING = 1
|
||||
|
||||
function AndroidPlugin() {
|
||||
/**
|
||||
* The current state of composition.
|
||||
*
|
||||
* @type {NONE|COMPOSING|WAITING}
|
||||
*/
|
||||
|
||||
let status = NONE
|
||||
|
||||
/**
|
||||
* The set of nodes that we need to process when we next reconcile.
|
||||
* Usually this is soon after the `onCompositionEnd` event.
|
||||
*
|
||||
* @type {Set} set containing Node objects
|
||||
*/
|
||||
|
||||
const nodes = new window.Set()
|
||||
|
||||
/**
|
||||
* Keep a snapshot after a composition end for API 26/27. If a `beforeInput`
|
||||
* gets called with data that ends in an ENTER then we need to use this
|
||||
* snapshot to revert the DOM so that React doesn't get out of sync with the
|
||||
* DOM. We also need to cancel the `reconcile` operation as it interferes in
|
||||
* certain scenarios like hitting 'enter' at the end of a word.
|
||||
*
|
||||
* @type {SlateSnapshot} [compositionEndSnapshot]
|
||||
|
||||
*/
|
||||
|
||||
let compositionEndSnapshot = null
|
||||
|
||||
/**
|
||||
* When there is a `compositionEnd` we ened to reconcile Slate's Document
|
||||
* with the DOM. The `reconciler` is an instance of `Executor` that does
|
||||
* this for us. It is created on every `compositionEnd` and executes on the
|
||||
* next `requestAnimationFrame`. The `Executor` can be cancelled and resumed
|
||||
* which some methods do.
|
||||
*
|
||||
* @type {Executor}
|
||||
*/
|
||||
|
||||
let reconciler = null
|
||||
|
||||
/**
|
||||
* A snapshot that gets taken when there is a `keydown` event in API26/27.
|
||||
* If an `input` gets called with `inputType` of `deleteContentBackward`
|
||||
* we need to undo the delete that Android does to keep React in sync with
|
||||
* the DOM.
|
||||
*
|
||||
* @type {SlateSnapshot}
|
||||
*/
|
||||
|
||||
let keyDownSnapshot = null
|
||||
|
||||
/**
|
||||
* The deleter is an instace of `Executor` that will execute a delete
|
||||
* operation on the next `requestAnimationFrame`. It has to wait because
|
||||
* we need Android to finish all of its DOM operations to do with deletion
|
||||
* before we revert them to a Snapshot. After reverting, we then execute
|
||||
* Slate's version of delete.
|
||||
*
|
||||
* @type {Executor}
|
||||
*/
|
||||
|
||||
let deleter = null
|
||||
|
||||
/**
|
||||
* Because Slate implements its own event handler for `beforeInput` in
|
||||
* addition to React's version, we actually get two. If we cancel the
|
||||
* first native version, the React one will still fire. We set this to
|
||||
* `true` if we don't want that to happen. Remember that when we prevent it,
|
||||
* we need to tell React to `preventDefault` so the event doesn't continue
|
||||
* through React's event system.
|
||||
*
|
||||
* type {Boolean}
|
||||
*/
|
||||
|
||||
let preventNextBeforeInput = false
|
||||
|
||||
/**
|
||||
* When a composition ends, in some API versions we may need to know what we
|
||||
* have learned so far about the composition and what we want to do because of
|
||||
* some actions that may come later.
|
||||
*
|
||||
* For example in API 26/27, if we get a `beforeInput` that tells us that the
|
||||
* input was a `.`, then we want the reconcile to happen even if there are
|
||||
* `onInput:delete` events that follow. In this case, we would set
|
||||
* `compositionEndAction` to `period`. During the `onInput` we would check if
|
||||
* the `compositionEndAction` says `period` and if so we would not start the
|
||||
* `delete` action.
|
||||
*
|
||||
* @type {(String|null)}
|
||||
*/
|
||||
|
||||
let compositionEndAction = null
|
||||
|
||||
/**
|
||||
* Looks at the `nodes` we have collected, usually the things we have edited
|
||||
* during the course of a composition, and then updates Slate's internal
|
||||
* Document based on the text values in these DOM nodes and also updates
|
||||
* Slate's Selection based on the current cursor position in the Editor.
|
||||
*
|
||||
* @param {Window} window
|
||||
* @param {Editor} editor
|
||||
* @param {String} options.from - where reconcile was called from for debug
|
||||
*/
|
||||
|
||||
function reconcile(window, editor, { from }) {
|
||||
debug.reconcile({ from })
|
||||
const domSelection = window.getSelection()
|
||||
|
||||
nodes.forEach(node => {
|
||||
setTextFromDomNode(window, editor, node)
|
||||
})
|
||||
|
||||
setSelectionFromDom(window, editor, domSelection)
|
||||
nodes.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* On before input.
|
||||
*
|
||||
* Check `components/content` because some versions of Android attach a
|
||||
* native `beforeinput` event on the Editor. In this case, you might need
|
||||
* to distinguish whether the event coming through is the native or React
|
||||
* version of the event. Also, if you cancel the native version that does
|
||||
* not necessarily mean that the React version is cancelled.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Editor} editor
|
||||
* @param {Function} next
|
||||
*/
|
||||
|
||||
function onBeforeInput(event, editor, next) {
|
||||
const isNative = !event.nativeEvent
|
||||
|
||||
debug('onBeforeInput', {
|
||||
isNative,
|
||||
event,
|
||||
status,
|
||||
e: pick(event, ['data', 'inputType', 'isComposing', 'nativeEvent']),
|
||||
})
|
||||
|
||||
const window = getWindow(event.target)
|
||||
|
||||
if (preventNextBeforeInput) {
|
||||
event.preventDefault()
|
||||
preventNextBeforeInput = false
|
||||
return
|
||||
}
|
||||
|
||||
switch (API_VERSION) {
|
||||
case 25:
|
||||
// prevent onBeforeInput to allow selecting an alternate suggest to
|
||||
// work.
|
||||
break
|
||||
case 26:
|
||||
case 27:
|
||||
if (deleter) {
|
||||
deleter.cancel()
|
||||
reconciler.resume()
|
||||
}
|
||||
|
||||
// This analyses Android's native `beforeInput` which Slate adds
|
||||
// on in the `Content` component. It only fires if the cursor is at
|
||||
// the end of a block. Otherwise, the code below checks.
|
||||
if (isNative) {
|
||||
if (
|
||||
event.inputType === 'insertParagraph' ||
|
||||
event.inputType === 'insertLineBreak'
|
||||
) {
|
||||
debug('onBeforeInput:enter:native', {})
|
||||
const domSelection = window.getSelection()
|
||||
const selection = getSelectionFromDom(window, editor, domSelection)
|
||||
preventNextBeforeInput = true
|
||||
event.preventDefault()
|
||||
editor.moveTo(selection.anchor.key, selection.anchor.offset)
|
||||
editor.splitBlock()
|
||||
}
|
||||
} else {
|
||||
if (isInputDataLastChar(event.data, ['.'])) {
|
||||
debug('onBeforeInput:period')
|
||||
reconciler.cancel()
|
||||
compositionEndAction = 'period'
|
||||
return
|
||||
}
|
||||
|
||||
// This looks at the beforeInput event's data property and sees if it
|
||||
// ends in a linefeed which is character code 10. This appears to be
|
||||
// the only way to detect that enter has been pressed except at end
|
||||
// of line where it doesn't work.
|
||||
const isEnter = isInputDataEnter(event.data)
|
||||
|
||||
if (isEnter) {
|
||||
if (reconciler) reconciler.cancel()
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
debug('onBeforeInput:enter:react', {})
|
||||
compositionEndSnapshot.apply(editor)
|
||||
editor.splitBlock()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
case 28:
|
||||
// If a `beforeInput` event fires after an `input:deleteContentBackward`
|
||||
// event, it appears to be a good indicator that it is some sort of
|
||||
// special combined Android event. If this is the case, then we don't
|
||||
// want to have a deletion to happen, we just want to wait until Android
|
||||
// has done its thing and then at the end we just want to reconcile.
|
||||
if (deleter) {
|
||||
deleter.cancel()
|
||||
reconciler.resume()
|
||||
}
|
||||
|
||||
break
|
||||
default:
|
||||
if (status !== COMPOSING) next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On Composition end. By default, when a `compositionEnd` event happens,
|
||||
* we start a reconciler. The reconciler will update Slate's Document using
|
||||
* the DOM as the source of truth. In some cases, the reconciler needs to
|
||||
* be cancelled and can also be resumed. For example, when a delete
|
||||
* immediately followed a `compositionEnd`, we don't want to reconcile.
|
||||
* Instead, we want the `delete` to take precedence.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Editor} editor
|
||||
* @param {Function} next
|
||||
*/
|
||||
|
||||
function onCompositionEnd(event, editor, next) {
|
||||
debug('onCompositionEnd', { event })
|
||||
const window = getWindow(event.target)
|
||||
const domSelection = window.getSelection()
|
||||
const { anchorNode } = domSelection
|
||||
|
||||
switch (API_VERSION) {
|
||||
case 26:
|
||||
case 27:
|
||||
compositionEndSnapshot = new SlateSnapshot(window, editor)
|
||||
// fixes a bug in Android API 26 & 27 where a `compositionEnd` is triggered
|
||||
// without the corresponding `compositionStart` event when clicking a
|
||||
// suggestion.
|
||||
//
|
||||
// If we don't add this, the `onBeforeInput` is triggered and passes
|
||||
// through to the `before` plugin.
|
||||
status = COMPOSING
|
||||
break
|
||||
}
|
||||
|
||||
compositionEndAction = 'reconcile'
|
||||
nodes.add(anchorNode)
|
||||
|
||||
reconciler = new Executor(window, () => {
|
||||
status = NONE
|
||||
reconcile(window, editor, { from: 'onCompositionEnd:reconciler' })
|
||||
compositionEndAction = null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* On composition start.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Editor} editor
|
||||
* @param {Function} next
|
||||
*/
|
||||
|
||||
function onCompositionStart(event, editor, next) {
|
||||
debug('onCompositionStart', { event })
|
||||
status = COMPOSING
|
||||
nodes.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* On composition update.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Editor} editor
|
||||
* @param {Function} next
|
||||
*/
|
||||
|
||||
function onCompositionUpdate(event, editor, next) {
|
||||
debug('onCompositionUpdate', { event })
|
||||
}
|
||||
|
||||
/**
|
||||
* On input.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Editor} editor
|
||||
* @param {Function} next
|
||||
*/
|
||||
|
||||
function onInput(event, editor, next) {
|
||||
debug('onInput', {
|
||||
event,
|
||||
status,
|
||||
e: pick(event, ['data', 'nativeEvent', 'inputType', 'isComposing']),
|
||||
})
|
||||
|
||||
switch (API_VERSION) {
|
||||
case 24:
|
||||
case 25:
|
||||
break
|
||||
case 26:
|
||||
case 27:
|
||||
case 28:
|
||||
const { nativeEvent } = event
|
||||
|
||||
if (API_VERSION === 28) {
|
||||
// NOTE API 28:
|
||||
// When a user hits space and then backspace in `middle` we end up
|
||||
// with `midle`.
|
||||
//
|
||||
// This is because when the user hits space, the composition is not
|
||||
// ended because `compositionEnd` is not called yet. When backspace is
|
||||
// hit, the `compositionEnd` is called. We need to revert the DOM but
|
||||
// the reconciler has not had a chance to run from the
|
||||
// `compositionEnd` because it is set to run on the next
|
||||
// `requestAnimationFrame`. When the backspace is carried out on the
|
||||
// Slate Value, Slate doesn't know about the space yet so the
|
||||
// backspace is carried out without the space cuasing us to lose a
|
||||
// character.
|
||||
//
|
||||
// This fix forces Android to reconcile immediately after hitting
|
||||
// the space.
|
||||
//
|
||||
// NOTE API 27:
|
||||
// It is confirmed that this bug does not present itself on API27.
|
||||
// A space fires a `compositionEnd` (as well as other events including
|
||||
// an input that is a delete) so the reconciliation happens.
|
||||
//
|
||||
if (
|
||||
nativeEvent.inputType === 'insertText' &&
|
||||
nativeEvent.data === ' '
|
||||
) {
|
||||
if (reconciler) reconciler.cancel()
|
||||
if (deleter) deleter.cancel()
|
||||
reconcile(window, editor, { from: 'onInput:space' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (API_VERSION === 26 || API_VERSION === 27) {
|
||||
if (compositionEndAction === 'period') {
|
||||
debug('onInput:period:abort')
|
||||
// This means that there was a `beforeInput` that indicated the
|
||||
// period was pressed. When a period is pressed, you get a bunch
|
||||
// of delete actions mixed in. We want to ignore those. At this
|
||||
// point, we add the current node to the list of things we need to
|
||||
// resolve at the next compositionEnd. We know that a new
|
||||
// composition will start right after this event so it is safe to
|
||||
// do this.
|
||||
const { anchorNode } = window.getSelection()
|
||||
nodes.add(anchorNode)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (nativeEvent.inputType === 'deleteContentBackward') {
|
||||
debug('onInput:delete', { keyDownSnapshot })
|
||||
const window = getWindow(event.target)
|
||||
if (reconciler) reconciler.cancel()
|
||||
if (deleter) deleter.cancel()
|
||||
|
||||
deleter = new Executor(
|
||||
window,
|
||||
() => {
|
||||
debug('onInput:delete:callback', { keyDownSnapshot })
|
||||
keyDownSnapshot.apply(editor)
|
||||
editor.deleteBackward()
|
||||
deleter = null
|
||||
},
|
||||
{
|
||||
onCancel() {
|
||||
deleter = null
|
||||
},
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (status === COMPOSING) {
|
||||
const { anchorNode } = window.getSelection()
|
||||
nodes.add(anchorNode)
|
||||
return
|
||||
}
|
||||
|
||||
// Some keys like '.' are input without compositions. This happens
|
||||
// in API28. It might be happening in API 27 as well. Check by typing
|
||||
// `It me. No.` On a blank line.
|
||||
if (API_VERSION === 28) {
|
||||
debug('onInput:fallback')
|
||||
const { anchorNode } = window.getSelection()
|
||||
nodes.add(anchorNode)
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
debug('onInput:fallback:callback')
|
||||
reconcile(window, editor, { from: 'onInput:fallback' })
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
default:
|
||||
if (status === COMPOSING) return
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On key down.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Editor} editor
|
||||
* @param {Function} next
|
||||
*/
|
||||
|
||||
function onKeyDown(event, editor, next) {
|
||||
debug('onKeyDown', {
|
||||
event,
|
||||
status,
|
||||
e: pick(event, [
|
||||
'char',
|
||||
'charCode',
|
||||
'code',
|
||||
'key',
|
||||
'keyCode',
|
||||
'keyIdentifier',
|
||||
'keyLocation',
|
||||
'location',
|
||||
'nativeEvent',
|
||||
'which',
|
||||
]),
|
||||
})
|
||||
|
||||
const window = getWindow(event.target)
|
||||
|
||||
switch (API_VERSION) {
|
||||
// 1. We want to allow enter keydown to allows go through
|
||||
// 2. We want to deny keydown, I think, when it fires before the composition
|
||||
// or something. Need to remember what it was.
|
||||
|
||||
case 25:
|
||||
// in API25 prevent other keys to fix clicking a word and then
|
||||
// selecting an alternate suggestion.
|
||||
//
|
||||
// NOTE:
|
||||
// The `setSelectionFromDom` is to allow hitting `Enter` to work
|
||||
// because the selection needs to be in the right place; however,
|
||||
// for now we've removed the cancelling of `onSelect` and everything
|
||||
// appears to be working. Not sure why we removed `onSelect` though
|
||||
// in API25.
|
||||
if (event.key === 'Enter') {
|
||||
// const window = getWindow(event.target)
|
||||
// const selection = window.getSelection()
|
||||
// setSelectionFromDom(window, editor, selection)
|
||||
next()
|
||||
}
|
||||
|
||||
break
|
||||
case 26:
|
||||
case 27:
|
||||
if (event.key === 'Enter') {
|
||||
debug('onKeyDown:enter', {})
|
||||
|
||||
if (deleter) {
|
||||
// If a `deleter` exists which means there was an onInput with
|
||||
// `deleteContentBackward` that hasn't fired yet, then we know
|
||||
// this is one of the cases where we have to revert to before
|
||||
// the split.
|
||||
deleter.cancel()
|
||||
event.preventDefault()
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
debug('onKeyDown:enter:callback')
|
||||
compositionEndSnapshot.apply(editor)
|
||||
editor.splitBlock()
|
||||
})
|
||||
} else {
|
||||
event.preventDefault()
|
||||
// If there is no deleter, all we have to do is prevent the
|
||||
// action before applying or splitBlock. In this scenario, we
|
||||
// have to grab the selection from the DOM.
|
||||
const domSelection = window.getSelection()
|
||||
const selection = getSelectionFromDom(window, editor, domSelection)
|
||||
editor.moveTo(selection.anchor.key, selection.anchor.offset)
|
||||
editor.splitBlock()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// We need to take a snapshot of the current selection and the
|
||||
// element before when the user hits the backspace key. This is because
|
||||
// we only know if the user hit backspace if the `onInput` event that
|
||||
// follows has an `inputType` of `deleteContentBackward`. At that time
|
||||
// it's too late to stop the event.
|
||||
keyDownSnapshot = new SlateSnapshot(window, editor, {
|
||||
before: true,
|
||||
})
|
||||
|
||||
// If we let 'Enter' through it breaks handling of hitting
|
||||
// enter at the beginning of a word so we need to stop it.
|
||||
break
|
||||
case 28:
|
||||
{
|
||||
if (event.key === 'Enter') {
|
||||
debug('onKeyDown:enter')
|
||||
event.preventDefault()
|
||||
if (reconciler) reconciler.cancel()
|
||||
if (deleter) deleter.cancel()
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
reconcile(window, editor, { from: 'onKeyDown:enter' })
|
||||
editor.splitBlock()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// We need to take a snapshot of the current selection and the
|
||||
// element before when the user hits the backspace key. This is because
|
||||
// we only know if the user hit backspace if the `onInput` event that
|
||||
// follows has an `inputType` of `deleteContentBackward`. At that time
|
||||
// it's too late to stop the event.
|
||||
keyDownSnapshot = new SlateSnapshot(window, editor, {
|
||||
before: true,
|
||||
})
|
||||
|
||||
debug('onKeyDown:snapshot', { keyDownSnapshot })
|
||||
}
|
||||
|
||||
// If we let 'Enter' through it breaks handling of hitting
|
||||
// enter at the beginning of a word so we need to stop it.
|
||||
break
|
||||
|
||||
default:
|
||||
if (status !== COMPOSING) {
|
||||
next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On select.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {Editor} editor
|
||||
* @param {Function} next
|
||||
*/
|
||||
|
||||
function onSelect(event, editor, next) {
|
||||
debug('onSelect', { event, status })
|
||||
|
||||
switch (API_VERSION) {
|
||||
// We don't want to have the selection move around in an onSelect because
|
||||
// it happens after we press `enter` in the same transaction. This
|
||||
// causes the cursor position to not be properly placed.
|
||||
case 26:
|
||||
case 27:
|
||||
case 28:
|
||||
const window = getWindow(event.target)
|
||||
fixSelectionInZeroWidthBlock(window)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the plugin.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
return {
|
||||
onBeforeInput,
|
||||
onCompositionEnd,
|
||||
onCompositionStart,
|
||||
onCompositionUpdate,
|
||||
onInput,
|
||||
onKeyDown,
|
||||
onSelect,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default AndroidPlugin
|
@@ -1,64 +0,0 @@
|
||||
import Debug from 'debug'
|
||||
|
||||
/**
|
||||
* A plugin that adds the "before" browser-specific logic to the editor.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function DebugPlugin(namespace) {
|
||||
/**
|
||||
* Debug.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
const debug = Debug(namespace)
|
||||
|
||||
const events = [
|
||||
'onBeforeInput',
|
||||
'onBlur',
|
||||
'onClick',
|
||||
'onCompositionEnd',
|
||||
'onCompositionStart',
|
||||
'onCopy',
|
||||
'onCut',
|
||||
'onDragEnd',
|
||||
'onDragEnter',
|
||||
'onDragExit',
|
||||
'onDragLeave',
|
||||
'onDragOver',
|
||||
'onDragStart',
|
||||
'onDrop',
|
||||
'onFocus',
|
||||
'onInput',
|
||||
'onKeyDown',
|
||||
'onPaste',
|
||||
'onSelect',
|
||||
]
|
||||
|
||||
const plugin = {}
|
||||
|
||||
for (const eventName of events) {
|
||||
plugin[eventName] = function(event, editor, next) {
|
||||
debug(eventName, { event })
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the plugin.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
return plugin
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
export default DebugPlugin
|
@@ -1,6 +1,3 @@
|
||||
import { IS_ANDROID } from 'slate-dev-environment'
|
||||
import AndroidPlugin from './android'
|
||||
import DebugPlugin from './debug'
|
||||
import AfterPlugin from './after'
|
||||
import BeforePlugin from './before'
|
||||
|
||||
@@ -13,15 +10,9 @@ import BeforePlugin from './before'
|
||||
|
||||
function DOMPlugin(options = {}) {
|
||||
const { plugins = [] } = options
|
||||
// Add Android specific handling separately before it gets to the other
|
||||
// plugins because it is specific (other browser don't need it) and finicky
|
||||
// (it has to come before other plugins to work).
|
||||
const beforeBeforePlugins = IS_ANDROID
|
||||
? [AndroidPlugin(), DebugPlugin('slate:debug')]
|
||||
: []
|
||||
const beforePlugin = BeforePlugin()
|
||||
const afterPlugin = AfterPlugin()
|
||||
return [...beforeBeforePlugins, beforePlugin, ...plugins, afterPlugin]
|
||||
return [beforePlugin, ...plugins, afterPlugin]
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,49 +0,0 @@
|
||||
import { IS_ANDROID } from 'slate-dev-environment'
|
||||
|
||||
/**
|
||||
* Array of regular expression matchers and their API version
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const ANDROID_API_VERSIONS = [
|
||||
[/^9([.]0|)/, 28],
|
||||
[/^8[.]1/, 27],
|
||||
[/^8([.]0|)/, 26],
|
||||
[/^7[.]1/, 25],
|
||||
[/^7([.]0|)/, 24],
|
||||
[/^6([.]0|)/, 23],
|
||||
[/^5[.]1/, 22],
|
||||
[/^5([.]0|)/, 21],
|
||||
[/^4[.]4/, 20],
|
||||
]
|
||||
|
||||
/**
|
||||
* get the Android API version from the userAgent
|
||||
*
|
||||
* @return {number} version
|
||||
*/
|
||||
|
||||
function getApiVersion() {
|
||||
if (!IS_ANDROID) return null
|
||||
const { userAgent } = window.navigator
|
||||
const matchData = userAgent.match(/Android\s([0-9\.]+)/)
|
||||
if (matchData == null) return null
|
||||
const versionString = matchData[1]
|
||||
|
||||
for (const tuple of ANDROID_API_VERSIONS) {
|
||||
const [regex, version] = tuple
|
||||
if (versionString.match(regex)) return version
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const API_VERSION = getApiVersion()
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* type {number}
|
||||
*/
|
||||
|
||||
export default API_VERSION
|
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Returns the closest element that matches the selector.
|
||||
* Unlike the native `Element.closest` method, this doesn't require the
|
||||
* starting node to be an Element.
|
||||
*
|
||||
* @param {Node} node to start at
|
||||
* @param {String} css selector to match
|
||||
* @return {Element} the closest matching element
|
||||
*/
|
||||
|
||||
export default function closest(node, selector, win = window) {
|
||||
if (node.nodeType === win.Node.TEXT_NODE) {
|
||||
node = node.parentNode
|
||||
}
|
||||
return node.closest(selector)
|
||||
}
|
@@ -1,164 +0,0 @@
|
||||
import getWindow from 'get-window'
|
||||
|
||||
/**
|
||||
* Is the given node a text node?
|
||||
*
|
||||
* @param {node} node
|
||||
* @param {Window} window
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
function isTextNode(node, window) {
|
||||
return node.nodeType === window.Node.TEXT_NODE
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a node and returns a snapshot of the node.
|
||||
*
|
||||
* @param {node} node
|
||||
* @param {Window} window
|
||||
* @return {object} element snapshot
|
||||
*/
|
||||
|
||||
function getElementSnapshot(node, window) {
|
||||
const snapshot = {}
|
||||
snapshot.node = node
|
||||
|
||||
if (isTextNode(node, window)) {
|
||||
snapshot.text = node.textContent
|
||||
}
|
||||
|
||||
snapshot.children = Array.from(node.childNodes).map(childNode =>
|
||||
getElementSnapshot(childNode, window)
|
||||
)
|
||||
return snapshot
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array of elements and returns a snapshot
|
||||
*
|
||||
* @param {elements[]} elements
|
||||
* @param {Window} window
|
||||
* @return {object} snapshot
|
||||
*/
|
||||
|
||||
function getSnapshot(elements, window) {
|
||||
if (!elements.length) throw new Error(`elements must be an Array`)
|
||||
|
||||
const lastElement = elements[elements.length - 1]
|
||||
const snapshot = {
|
||||
elements: elements.map(element => getElementSnapshot(element, window)),
|
||||
parent: lastElement.parentElement,
|
||||
next: lastElement.nextElementSibling,
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an element snapshot and applies it to the element in the DOM.
|
||||
* Basically, it fixes the DOM to the point in time that the snapshot was
|
||||
* taken. This will put the DOM back in sync with React.
|
||||
*
|
||||
* @param {Object} snapshot
|
||||
* @param {Window} window
|
||||
*/
|
||||
|
||||
function applyElementSnapshot(snapshot, window) {
|
||||
const el = snapshot.node
|
||||
|
||||
if (isTextNode(el, window)) {
|
||||
// Update text if it is different
|
||||
if (el.textContent !== snapshot.text) {
|
||||
el.textContent = snapshot.text
|
||||
}
|
||||
}
|
||||
|
||||
snapshot.children.forEach(childSnapshot => {
|
||||
applyElementSnapshot(childSnapshot, window)
|
||||
el.appendChild(childSnapshot.node)
|
||||
})
|
||||
|
||||
// remove children that shouldn't be there
|
||||
const snapLength = snapshot.children.length
|
||||
|
||||
while (el.childNodes.length > snapLength) {
|
||||
el.removeChild(el.childNodes[0])
|
||||
}
|
||||
|
||||
// remove any clones from the DOM. This can happen when a block is split.
|
||||
const { dataset } = el
|
||||
if (!dataset) return // if there's no dataset, don't remove it
|
||||
const key = dataset.key
|
||||
if (!key) return // if there's no `data-key`, don't remove it
|
||||
const dups = new window.Set(
|
||||
Array.from(window.document.querySelectorAll(`[data-key='${key}']`))
|
||||
)
|
||||
dups.delete(el)
|
||||
dups.forEach(dup => dup.parentElement.removeChild(dup))
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a snapshot and applies it to the DOM. Rearranges both the contents
|
||||
* of the elements in the snapshot as well as putting the elements back into
|
||||
* position relative to each other and also makes sure the last element is
|
||||
* before the same element as it was when the snapshot was taken.
|
||||
*
|
||||
* @param {snapshot} snapshot
|
||||
* @param {Window} window
|
||||
*/
|
||||
|
||||
function applySnapshot(snapshot, window) {
|
||||
const { elements, next, parent } = snapshot
|
||||
elements.forEach(element => applyElementSnapshot(element, window))
|
||||
const lastElement = elements[elements.length - 1].node
|
||||
|
||||
if (snapshot.next) {
|
||||
parent.insertBefore(lastElement, next)
|
||||
} else {
|
||||
parent.appendChild(lastElement)
|
||||
}
|
||||
|
||||
let prevElement = lastElement
|
||||
|
||||
for (let i = elements.length - 2; i >= 0; i--) {
|
||||
const element = elements[i].node
|
||||
parent.insertBefore(element, prevElement)
|
||||
prevElement = element
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A snapshot of one or more elements.
|
||||
*/
|
||||
|
||||
export default class ElementSnapshot {
|
||||
/**
|
||||
* constructor
|
||||
* @param {elements[]} elements - array of element to snapshot. Must be in order.
|
||||
* @param {object} data - any arbitrary data you want to store with the snapshot
|
||||
*/
|
||||
|
||||
constructor(elements, data) {
|
||||
this.window = getWindow(elements[0])
|
||||
this.snapshot = getSnapshot(elements, this.window)
|
||||
this.data = data
|
||||
}
|
||||
|
||||
/**
|
||||
* apply the current snapshot to the DOM.
|
||||
*/
|
||||
|
||||
apply() {
|
||||
applySnapshot(this.snapshot, this.window)
|
||||
}
|
||||
|
||||
/**
|
||||
* get the data you passed into the constructor.
|
||||
*
|
||||
* @return {object} data
|
||||
*/
|
||||
|
||||
getData() {
|
||||
return this.data
|
||||
}
|
||||
}
|
@@ -1,96 +0,0 @@
|
||||
/**
|
||||
* A function that does nothing
|
||||
* @return {Function}
|
||||
*/
|
||||
|
||||
function noop() {}
|
||||
|
||||
/**
|
||||
* Creates an executor like a `resolver` or a `deleter` that handles
|
||||
* delayed execution of a method using a `requestAnimationFrame` or `setTimeout`.
|
||||
*
|
||||
* Unlike a `requestAnimationFrame`, after a method is cancelled, it can be
|
||||
* resumed. You can also optionally add a `timeout` after which time the
|
||||
* executor is automatically cancelled.
|
||||
*/
|
||||
|
||||
export default class Executor {
|
||||
/**
|
||||
* Executor
|
||||
* @param {window} window
|
||||
* @param {Function} fn - the function to execute when done
|
||||
* @param {Object} options
|
||||
*/
|
||||
|
||||
constructor(window, fn, options = {}) {
|
||||
this.fn = fn
|
||||
this.window = window
|
||||
this.resume()
|
||||
this.onCancel = options.onCancel
|
||||
this.__setTimeout__(options.timeout)
|
||||
}
|
||||
|
||||
__call__ = () => {
|
||||
// I don't clear the timeout since it will be noop'ed anyways. Less code.
|
||||
this.fn()
|
||||
this.preventFurtherCalls() // Ensure you can only call the function once
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure that the function cannot be executed any more, even if other
|
||||
* methods attempt to call `__call__`.
|
||||
*/
|
||||
|
||||
preventFurtherCalls = () => {
|
||||
this.fn = noop
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume the executor's timer, usually after it has been cancelled.
|
||||
*
|
||||
* @param {Number} [ms] - how long to wait by default it is until next frame
|
||||
*/
|
||||
|
||||
resume = ms => {
|
||||
// in case resume is called more than once, we don't want old timers
|
||||
// from executing because the `timeoutId` or `callbackId` is overwritten.
|
||||
this.cancel()
|
||||
|
||||
if (ms) {
|
||||
this.mode = 'timeout'
|
||||
this.timeoutId = this.window.setTimeout(this.__call__, ms)
|
||||
} else {
|
||||
this.mode = 'animationFrame'
|
||||
this.callbackId = this.window.requestAnimationFrame(this.__call__)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the executor from executing after the wait. This can be resumed
|
||||
* with the `resume` method.
|
||||
*/
|
||||
|
||||
cancel = () => {
|
||||
if (this.mode === 'timeout') {
|
||||
this.window.clearTimeout(this.timeoutId)
|
||||
} else {
|
||||
this.window.cancelAnimationFrame(this.callbackId)
|
||||
}
|
||||
|
||||
if (this.onCancel) this.onCancel()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a timeout after which this executor is automatically cancelled.
|
||||
* @param {Number} ms
|
||||
*/
|
||||
|
||||
__setTimeout__ = timeout => {
|
||||
if (timeout == null) return
|
||||
|
||||
this.window.setTimeout(() => {
|
||||
this.cancel()
|
||||
this.preventFurtherCalls()
|
||||
}, timeout)
|
||||
}
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
/**
|
||||
* Fixes a selection within the DOM when the cursor is in Slate's special
|
||||
* zero-width block. Slate handles empty blocks in a special manner and the
|
||||
* cursor can end up either before or after the non-breaking space. This
|
||||
* causes different behavior in Android and so we make sure the seleciton is
|
||||
* always before the zero-width space.
|
||||
*
|
||||
* @param {Window} window
|
||||
*/
|
||||
|
||||
export default function fixSelectionInZeroWidthBlock(window) {
|
||||
const domSelection = window.getSelection()
|
||||
const { anchorNode } = domSelection
|
||||
const { dataset } = anchorNode.parentElement
|
||||
const isZeroWidth = dataset ? dataset.slateZeroWidth === 'n' : false
|
||||
|
||||
// We are doing three checks to see if we need to move the cursor.
|
||||
// Is this a zero-width slate span?
|
||||
// Is the current cursor position not at the start of it?
|
||||
// Is there more than one character (i.e. the zero-width space char) in here?
|
||||
if (
|
||||
isZeroWidth &&
|
||||
anchorNode.textContent.length === 1 &&
|
||||
domSelection.anchorOffset !== 0
|
||||
) {
|
||||
const range = window.document.createRange()
|
||||
range.setStart(anchorNode, 0)
|
||||
range.setEnd(anchorNode, 0)
|
||||
domSelection.removeAllRanges()
|
||||
domSelection.addRange(range)
|
||||
}
|
||||
}
|
@@ -1,77 +0,0 @@
|
||||
import findRange from './find-range'
|
||||
|
||||
export default function getSelectionFromDOM(window, editor, domSelection) {
|
||||
const { value } = editor
|
||||
const { document } = value
|
||||
|
||||
// If there are no ranges, the editor was blurred natively.
|
||||
if (!domSelection.rangeCount) {
|
||||
editor.blur()
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, determine the Slate selection from the native one.
|
||||
let range = findRange(domSelection, editor)
|
||||
|
||||
if (!range) {
|
||||
return
|
||||
}
|
||||
|
||||
const { anchor, focus } = range
|
||||
const anchorText = document.getNode(anchor.key)
|
||||
const focusText = document.getNode(focus.key)
|
||||
const anchorInline = document.getClosestInline(anchor.key)
|
||||
const focusInline = document.getClosestInline(focus.key)
|
||||
const focusBlock = document.getClosestBlock(focus.key)
|
||||
const anchorBlock = document.getClosestBlock(anchor.key)
|
||||
|
||||
// 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 <span>'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 &&
|
||||
!editor.isVoid(anchorBlock) &&
|
||||
anchor.offset == 0 &&
|
||||
focusBlock &&
|
||||
editor.isVoid(focusBlock) &&
|
||||
focus.offset != 0
|
||||
) {
|
||||
range = range.setFocus(focus.setOffset(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 &&
|
||||
!editor.isVoid(anchorInline) &&
|
||||
anchor.offset == anchorText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(anchor.key)
|
||||
const nextText = block.getNextText(anchor.key)
|
||||
if (nextText) range = range.moveAnchorTo(nextText.key, 0)
|
||||
}
|
||||
|
||||
if (
|
||||
focusInline &&
|
||||
!editor.isVoid(focusInline) &&
|
||||
focus.offset == focusText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(focus.key)
|
||||
const nextText = block.getNextText(focus.key)
|
||||
if (nextText) range = range.moveFocusTo(nextText.key, 0)
|
||||
}
|
||||
|
||||
let selection = document.createSelection(range)
|
||||
selection = selection.setIsFocused(true)
|
||||
|
||||
// Preserve active marks from the current selection.
|
||||
// They will be cleared by `editor.select` if the selection actually moved.
|
||||
selection = selection.set('marks', value.selection.marks)
|
||||
|
||||
return selection
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* In Android API 26 and 27 we can tell if the input key was pressed by
|
||||
* waiting for the `beforeInput` event and seeing that the last character
|
||||
* of its `data` property is char code `10`.
|
||||
*
|
||||
* Note that at this point it is too late to prevent the event from affecting
|
||||
* the DOM so we use other methods to clean the DOM up after we have detected
|
||||
* the input.
|
||||
*
|
||||
* @param {String} data
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
export default function isInputDataEnter(data) {
|
||||
if (data == null) return false
|
||||
const lastChar = data[data.length - 1]
|
||||
const charCode = lastChar.charCodeAt(0)
|
||||
return charCode === 10
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* In Android sometimes the only way to tell what the user is trying to do
|
||||
* is to look at an event's `data` property and see if the last characters
|
||||
* matches a character. This method helps us make that determination.
|
||||
*
|
||||
* @param {String} data
|
||||
* @param {[String]} chars
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
export default function isInputDataLastChar(data, chars) {
|
||||
if (!Array.isArray(chars))
|
||||
throw new Error(`chars must be an array of one character strings`)
|
||||
if (data == null) return false
|
||||
const lastChar = data[data.length - 1]
|
||||
return chars.includes(lastChar)
|
||||
}
|
@@ -1,14 +1,77 @@
|
||||
import getSelectionFromDOM from './get-selection-from-dom'
|
||||
|
||||
/**
|
||||
* Looks at the DOM and generates the equivalent Slate Selection.
|
||||
*
|
||||
* @param {Window} window
|
||||
* @param {Editor} editor
|
||||
* @param {Selection} domSelection - The DOM's selection Object
|
||||
*/
|
||||
import findRange from './find-range'
|
||||
|
||||
export default function setSelectionFromDOM(window, editor, domSelection) {
|
||||
const selection = getSelectionFromDOM(window, editor, domSelection)
|
||||
const { value } = editor
|
||||
const { document } = value
|
||||
|
||||
// If there are no ranges, the editor was blurred natively.
|
||||
if (!domSelection.rangeCount) {
|
||||
editor.blur()
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, determine the Slate selection from the native one.
|
||||
let range = findRange(domSelection, editor)
|
||||
|
||||
if (!range) {
|
||||
return
|
||||
}
|
||||
|
||||
const { anchor, focus } = range
|
||||
const anchorText = document.getNode(anchor.key)
|
||||
const focusText = document.getNode(focus.key)
|
||||
const anchorInline = document.getClosestInline(anchor.key)
|
||||
const focusInline = document.getClosestInline(focus.key)
|
||||
const focusBlock = document.getClosestBlock(focus.key)
|
||||
const anchorBlock = document.getClosestBlock(anchor.key)
|
||||
|
||||
// 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 <span>'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 &&
|
||||
!editor.isVoid(anchorBlock) &&
|
||||
anchor.offset == 0 &&
|
||||
focusBlock &&
|
||||
editor.isVoid(focusBlock) &&
|
||||
focus.offset != 0
|
||||
) {
|
||||
range = range.setFocus(focus.setOffset(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 &&
|
||||
!editor.isVoid(anchorInline) &&
|
||||
anchor.offset == anchorText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(anchor.key)
|
||||
const nextText = block.getNextText(anchor.key)
|
||||
if (nextText) range = range.moveAnchorTo(nextText.key, 0)
|
||||
}
|
||||
|
||||
if (
|
||||
focusInline &&
|
||||
!editor.isVoid(focusInline) &&
|
||||
focus.offset == focusText.text.length
|
||||
) {
|
||||
const block = document.getClosestBlock(focus.key)
|
||||
const nextText = block.getNextText(focus.key)
|
||||
if (nextText) range = range.moveFocusTo(nextText.key, 0)
|
||||
}
|
||||
|
||||
let selection = document.createSelection(range)
|
||||
selection = selection.setIsFocused(true)
|
||||
|
||||
// Preserve active marks from the current selection.
|
||||
// They will be cleared by `editor.select` if the selection actually moved.
|
||||
selection = selection.set('marks', value.selection.marks)
|
||||
|
||||
editor.select(selection)
|
||||
}
|
||||
|
@@ -1,19 +1,5 @@
|
||||
import findPoint from './find-point'
|
||||
|
||||
/**
|
||||
* setTextFromDomNode lets us take a domNode and reconcile the text in the
|
||||
* editor's Document such that it reflects the text in the DOM. This is the
|
||||
* opposite of what the Editor usually does which takes the Editor's Document
|
||||
* and React modifies the DOM to match. The purpose of this method is for
|
||||
* composition changes where we don't know what changes the user made by
|
||||
* looking at events. Instead we wait until the DOM is in a safe state, we
|
||||
* read from it, and update the Editor's Document.
|
||||
*
|
||||
* @param {Window} window
|
||||
* @param {Editor} editor
|
||||
* @param {Node} domNode
|
||||
*/
|
||||
|
||||
export default function setTextFromDomNode(window, editor, domNode) {
|
||||
const point = findPoint(domNode, 0, editor)
|
||||
if (!point) return
|
||||
|
@@ -1,52 +0,0 @@
|
||||
import closest from './closest'
|
||||
import getSelectionFromDom from './get-selection-from-dom'
|
||||
import ElementSnapshot from './element-snapshot'
|
||||
|
||||
/**
|
||||
* A SlateSnapshot remembers the state of elements at a given point in time
|
||||
* and also remembers the state of the Editor at that time as well.
|
||||
* The state can be applied to the DOM at a time in the future.
|
||||
*/
|
||||
|
||||
export default class SlateSnapshot {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param {Window} window
|
||||
* @param {Editor} editor
|
||||
* @param {Boolean} options.before - should we remember the element before the one passed in
|
||||
*/
|
||||
|
||||
constructor(window, editor, { before = false } = {}) {
|
||||
const domSelection = window.getSelection()
|
||||
const { anchorNode } = domSelection
|
||||
const subrootEl = closest(anchorNode, '[data-slate-editor] > *')
|
||||
const elements = [subrootEl]
|
||||
|
||||
// The before option is for when we need to take a snapshot of the current
|
||||
// subroot and the element before when the user hits the backspace key.
|
||||
if (before) {
|
||||
const { previousElementSibling } = subrootEl
|
||||
|
||||
if (previousElementSibling) {
|
||||
elements.unshift(previousElementSibling)
|
||||
}
|
||||
}
|
||||
|
||||
this.snapshot = new ElementSnapshot(elements)
|
||||
this.selection = getSelectionFromDom(window, editor, domSelection)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the snapshot to the DOM and set the selection in the Editor.
|
||||
*
|
||||
* @param {Editor} editor
|
||||
*/
|
||||
|
||||
apply(editor) {
|
||||
if (editor == null) throw new Error('editor is required')
|
||||
const { snapshot, selection } = this
|
||||
snapshot.apply()
|
||||
editor.moveTo(selection.anchor.key, selection.anchor.offset)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user