diff --git a/examples/app.js b/examples/app.js index 2da89d5cc..c0be11d24 100644 --- a/examples/app.js +++ b/examples/app.js @@ -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 { {EXAMPLES.map(([name, Component, path]) => ( - {({ match }) => ( -
- - - -
- )} +
+ + + +
))} @@ -294,7 +290,7 @@ export default class App extends React.Component { {EXAMPLES.map(([name, Component, path]) => ( - + {name} ))} diff --git a/examples/composition/index.js b/examples/composition/index.js deleted file mode 100644 index 101b6bd58..000000000 --- a/examples/composition/index.js +++ /dev/null @@ -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 }) => - -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 - return ( -
- - - {SUBPAGES.map(([name, Component, subpage]) => { - const active = subpage === this.props.params.subpage - return ( - - {name} - - ) - })} - -
{this.state.text}
-
- - {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')} - - -
- ) - } - - /** - * Render a mark-toggling toolbar button. - * - * @param {String} type - * @param {String} icon - * @return {Element} - */ - - renderMarkButton = (type, icon) => { - const isActive = this.hasMark(type) - - return ( - - ) - } - - /** - * 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 ( - - ) - } - - /** - * 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
{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}
    - 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 {children} - case 'code': - return {children} - case 'italic': - return {children} - case 'underlined': - return {children} - 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 diff --git a/examples/composition/insert.js b/examples/composition/insert.js deleted file mode 100644 index c38569931..000000000 --- a/examples/composition/insert.js +++ /dev/null @@ -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(), - ], - }, -} diff --git a/examples/composition/special.js b/examples/composition/special.js deleted file mode 100644 index fab03c65d..000000000 --- a/examples/composition/special.js +++ /dev/null @@ -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(''), - ], - }, -} diff --git a/examples/composition/split-join.js b/examples/composition/split-join.js deleted file mode 100644 index a72fabc99..000000000 --- a/examples/composition/split-join.js +++ /dev/null @@ -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') - ), - ], - }, -} diff --git a/examples/composition/util.js b/examples/composition/util.js deleted file mode 100644 index 6facdb4f6..000000000 --- a/examples/composition/util.js +++ /dev/null @@ -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' }] } -} diff --git a/examples/composition/value.json b/examples/composition/value.json deleted file mode 100644 index 8817f5880..000000000 --- a/examples/composition/value.json +++ /dev/null @@ -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!" - } - ] - } - ] - } - ] - } -} diff --git a/package.json b/package.json index 61508f9f5..70aa82c92 100644 --- a/package.json +++ b/package.json @@ -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}'", diff --git a/packages/slate-react/src/components/content.js b/packages/slate-react/src/components/content.js index 2fe5fc448..fa1451616 100644 --- a/packages/slate-react/src/components/content.js +++ b/packages/slate-react/src/components/content.js @@ -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 ( 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"`. diff --git a/packages/slate-react/src/plugins/android.js b/packages/slate-react/src/plugins/android.js deleted file mode 100644 index 2c57bec1d..000000000 --- a/packages/slate-react/src/plugins/android.js +++ /dev/null @@ -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 diff --git a/packages/slate-react/src/plugins/debug.js b/packages/slate-react/src/plugins/debug.js deleted file mode 100644 index 1b6aee4d8..000000000 --- a/packages/slate-react/src/plugins/debug.js +++ /dev/null @@ -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 diff --git a/packages/slate-react/src/plugins/dom.js b/packages/slate-react/src/plugins/dom.js index 3ea5ff0f1..f32e10eab 100644 --- a/packages/slate-react/src/plugins/dom.js +++ b/packages/slate-react/src/plugins/dom.js @@ -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] } /** diff --git a/packages/slate-react/src/utils/android-api-version.js b/packages/slate-react/src/utils/android-api-version.js deleted file mode 100644 index 6c405bfcd..000000000 --- a/packages/slate-react/src/utils/android-api-version.js +++ /dev/null @@ -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 diff --git a/packages/slate-react/src/utils/closest.js b/packages/slate-react/src/utils/closest.js deleted file mode 100644 index 89b455c36..000000000 --- a/packages/slate-react/src/utils/closest.js +++ /dev/null @@ -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) -} diff --git a/packages/slate-react/src/utils/element-snapshot.js b/packages/slate-react/src/utils/element-snapshot.js deleted file mode 100644 index 0a53e13e3..000000000 --- a/packages/slate-react/src/utils/element-snapshot.js +++ /dev/null @@ -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 - } -} diff --git a/packages/slate-react/src/utils/executor.js b/packages/slate-react/src/utils/executor.js deleted file mode 100644 index 483298e9e..000000000 --- a/packages/slate-react/src/utils/executor.js +++ /dev/null @@ -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) - } -} diff --git a/packages/slate-react/src/utils/fix-selection-in-zero-width-block.js b/packages/slate-react/src/utils/fix-selection-in-zero-width-block.js deleted file mode 100644 index 81a33e82f..000000000 --- a/packages/slate-react/src/utils/fix-selection-in-zero-width-block.js +++ /dev/null @@ -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) - } -} diff --git a/packages/slate-react/src/utils/get-selection-from-dom.js b/packages/slate-react/src/utils/get-selection-from-dom.js deleted file mode 100644 index 6f7a182a3..000000000 --- a/packages/slate-react/src/utils/get-selection-from-dom.js +++ /dev/null @@ -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 '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 -} diff --git a/packages/slate-react/src/utils/is-input-data-enter.js b/packages/slate-react/src/utils/is-input-data-enter.js deleted file mode 100644 index 7384429e4..000000000 --- a/packages/slate-react/src/utils/is-input-data-enter.js +++ /dev/null @@ -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 -} diff --git a/packages/slate-react/src/utils/is-input-data-last-char.js b/packages/slate-react/src/utils/is-input-data-last-char.js deleted file mode 100644 index f9635c1e7..000000000 --- a/packages/slate-react/src/utils/is-input-data-last-char.js +++ /dev/null @@ -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) -} diff --git a/packages/slate-react/src/utils/set-selection-from-dom.js b/packages/slate-react/src/utils/set-selection-from-dom.js index 90b6b412d..301d0fdde 100644 --- a/packages/slate-react/src/utils/set-selection-from-dom.js +++ b/packages/slate-react/src/utils/set-selection-from-dom.js @@ -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 '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) } diff --git a/packages/slate-react/src/utils/set-text-from-dom-node.js b/packages/slate-react/src/utils/set-text-from-dom-node.js index 5a9d3f4d5..e9b7eada0 100644 --- a/packages/slate-react/src/utils/set-text-from-dom-node.js +++ b/packages/slate-react/src/utils/set-text-from-dom-node.js @@ -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 diff --git a/packages/slate-react/src/utils/slate-snapshot.js b/packages/slate-react/src/utils/slate-snapshot.js deleted file mode 100644 index e485e5b0e..000000000 --- a/packages/slate-react/src/utils/slate-snapshot.js +++ /dev/null @@ -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) - } -}