Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove is native #1088

Merged
merged 5 commits into from
Sep 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions src/changes/on-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,6 @@

const Changes = {}

/**
* Set the `isNative` flag on the underlying state to prevent re-renders.
*
* @param {Change} change
* @param {Boolean} value
*/

Changes.setIsNative = (change, value) => {
let { state } = change
state = state.set('isNative', value)
change.state = state
}

/**
* Set `properties` on the top-level state's data.
*
Expand Down
28 changes: 0 additions & 28 deletions src/components/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,33 +91,6 @@ class Content extends React.Component {
this.tmp.forces = 0
}

/**
* Should the component update?
*
* @param {Object} props
* @param {Object} state
* @return {Boolean}
*/

shouldComponentUpdate = (props, state) => {
// If the readOnly state has changed, we need to re-render so that
// the cursor will be added or removed again.
if (props.readOnly != this.props.readOnly) return true

// If the state has been changed natively, never re-render, or else we'll
// end up duplicating content.
if (props.state.isNative) return false

return (
props.className != this.props.className ||
props.schema != this.props.schema ||
props.autoCorrect != this.props.autoCorrect ||
props.spellCheck != this.props.spellCheck ||
props.state != this.props.state ||
props.style != this.props.style
)
}

/**
* When the editor first mounts in the DOM we need to:
*
Expand Down Expand Up @@ -753,7 +726,6 @@ class Content extends React.Component {
// If there are no ranges, the editor was blurred natively.
if (!native.rangeCount) {
data.selection = selection.set('isFocused', false)
data.isNative = true
}

// Otherwise, determine the Slate selection from the native one.
Expand Down
28 changes: 1 addition & 27 deletions src/components/leaf.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@

import Debug from 'debug'
import React from 'react'
import ReactDOM from 'react-dom'
import Types from 'prop-types'

import OffsetKey from '../utils/offset-key'
import SlateTypes from '../utils/prop-types'
import findDeepestNode from '../utils/find-deepest-node'
import { IS_FIREFOX } from '../constants/environment'

/**
Expand Down Expand Up @@ -45,18 +43,6 @@ class Leaf extends React.Component {
text: Types.string.isRequired,
}

/**
* Constructor.
*
* @param {Object} props
*/

constructor(props) {
super(props)
this.tmp = {}
this.tmp.renders = 0
}

/**
* Debug.
*
Expand Down Expand Up @@ -86,12 +72,6 @@ class Leaf extends React.Component {
return true
}

// If the DOM text does not equal the `text` property, re-render, this can
// happen because React gets out of sync when previously natively rendered.
const el = findDeepestNode(ReactDOM.findDOMNode(this))
const text = this.renderText(props)
if (el.textContent != text) return true

// Otherwise, don't update.
return false
}
Expand All @@ -110,16 +90,10 @@ class Leaf extends React.Component {
index
})

// Increment the renders key, which forces a re-render whenever this
// component is told it should update. This is required because "native"
// renders where we don't update the leaves cause React's internal state to
// get out of sync, causing it to not realize the DOM needs updating.
this.tmp.renders++

this.debug('render', { props })

return (
<span key={this.tmp.renders} data-offset-key={offsetKey}>
<span data-offset-key={offsetKey}>
{this.renderMarks(props)}
</span>
)
Expand Down
1 change: 0 additions & 1 deletion src/models/change.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ class Change {
this.state = state
this.operations = []
this.flags = pick(attrs, ['merge', 'save'])
this.setIsNative(attrs.isNative === undefined ? false : attrs.isNative)
}

/**
Expand Down
1 change: 0 additions & 1 deletion src/models/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const DEFAULTS = {
selection: Selection.create(),
history: History.create(),
data: new Map(),
isNative: false,
}

/**
Expand Down
129 changes: 26 additions & 103 deletions src/plugins/core.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

import Base64 from '../serializers/base-64'
import Block from '../models/block'
import Character from '../models/character'
import Content from '../components/content'
import Debug from 'debug'
import Inline from '../models/inline'
Expand Down Expand Up @@ -50,10 +49,6 @@ function Plugin(options = {}) {
const schema = editor.getSchema()
const prevState = editor.getState()

// PERF: Skip normalizing if the change is native, since we know that it
// can't have changed anything that requires a core schema fix.
if (state.isNative) return

// PERF: Skip normalizing if the document hasn't changed, since the core
// schema only normalizes changes to the document, not selection.
if (prevState && state.document == prevState.document) return
Expand All @@ -63,8 +58,7 @@ function Plugin(options = {}) {
}

/**
* On before input, see if we can let the browser continue with it's native
* input behavior, to avoid a re-render for performance.
* On before input, correct any browser inconsistencies.
*
* @param {Event} e
* @param {Object} data
Expand All @@ -73,110 +67,39 @@ function Plugin(options = {}) {
*/

function onBeforeInput(e, data, change, editor) {
const { state } = change
const { document, startKey, startBlock, startOffset, startInline, startText } = state
const pText = startBlock.getPreviousText(startKey)
const pInline = pText && startBlock.getClosestInline(pText.key)
const nText = startBlock.getNextText(startKey)
const nInline = nText && startBlock.getClosestInline(nText.key)

// Determine what the characters would be if natively inserted.
const schema = editor.getSchema()
const decorators = document.getDescendantDecorators(startKey, schema)
const initialChars = startText.getDecorations(decorators)
const prevChar = startOffset === 0 ? null : initialChars.get(startOffset - 1)
const nextChar = startOffset === initialChars.size ? null : initialChars.get(startOffset)
const char = Character.create({
text: e.data,
// When cursor is at start of a range of marks, without preceding text,
// the native behavior is to insert inside the range of marks.
marks: (
(prevChar && prevChar.marks) ||
(nextChar && nextChar.marks) ||
[]
)
})
debug('onBeforeInput', { data })
e.preventDefault()

const chars = initialChars.insert(startOffset, char)
const { state } = change
const { selection } = state
const { anchorKey, anchorOffset, focusKey, focusOffset } = selection

// COMPAT: In iOS, when choosing from the predictive text suggestions, the
// native selection will be changed to span the existing word, so that the word
// is replaced. But the `select` event for this change doesn't fire until after
// the `beforeInput` event, even though the native selection is updated. So we
// need to manually adjust the selection to be in sync. (03/18/2017)
// COMPAT: In iOS, when using predictive text suggestions, the native
// selection will be changed to span the existing word, so that the word is
// replaced. But the `select` fires after the `beforeInput` event, even
// though the native selection is updated. So we need to manually check if
// the selection has gotten out of sync, and adjust it if so. (03/18/2017)
const window = getWindow(e.target)
const native = window.getSelection()
const { anchorNode, anchorOffset, focusNode, focusOffset } = native
const anchorPoint = getPoint(anchorNode, anchorOffset, state, editor)
const focusPoint = getPoint(focusNode, focusOffset, state, editor)
if (anchorPoint && focusPoint) {
const { selection } = state
if (
selection.anchorKey !== anchorPoint.key ||
selection.anchorOffset !== anchorPoint.offset ||
selection.focusKey !== focusPoint.key ||
selection.focusOffset !== focusPoint.offset
) {
change = change
.select({
anchorKey: anchorPoint.key,
anchorOffset: anchorPoint.offset,
focusKey: focusPoint.key,
focusOffset: focusPoint.offset
})
}
}

// Determine what the characters should be, if not natively inserted.
change.insertText(e.data)
const next = change.state
const nextText = next.startText
const nextChars = nextText.getDecorations(decorators)

// We do not have to re-render if the current selection is collapsed, the
// current node is not empty, there are no marks on the cursor, the cursor
// is not at the edge of an inline node, the cursor isn't at the starting
// edge of a text node after an inline node, and the natively inserted
// characters would be the same as the non-native.
const isNative = (
// If the selection is expanded, we don't know what the edit will look
// like so we can't let it happen natively.
(state.isCollapsed) &&
// If the selection has marks, then we need to render it non-natively
// because we need to create the new marks as well.
(state.selection.marks == null) &&
// If the text node in question has no content, browsers might do weird
// things so we need to insert it normally instead.
(state.startText.text != '') &&
// COMPAT: Browsers do weird things when typing at the edges of inline
// nodes, so we can't let them render natively. (?)
(!startInline || !state.selection.isAtStartOf(startInline)) &&
(!startInline || !state.selection.isAtEndOf(startInline)) &&
// COMPAT: In Chrome & Safari, it isn't possible to have a selection at
// the starting edge of a text node after another inline node. It will
// have been automatically changed. So we can't render natively because
// the cursor isn't technically in the right spot. (2016/12/01)
(!(pInline && !pInline.isVoid && startOffset == 0)) &&
(!(nInline && !nInline.isVoid && startOffset == startText.text.length)) &&
// COMPAT: When inserting a Space character, Chrome will sometimes
// split the text node into two adjacent text nodes. See:
// https://github.com/ianstormtaylor/slate/issues/938
(!(e.data === ' ' && IS_CHROME)) &&
// If the
(chars.equals(nextChars))
const a = getPoint(native.anchorNode, native.anchorOffset, state, editor)
const f = getPoint(native.focusNode, native.focusOffset, state, editor)
const hasMismatch = a && f && (
anchorKey != a.key ||
anchorOffset != a.offset ||
focusKey != f.key ||
focusOffset != f.offset
)

// If `isNative`, set the flag on the change.
if (isNative) {
change.setIsNative(true)
}

// Otherwise, prevent default so that the DOM remains untouched.
else {
e.preventDefault()
if (hasMismatch) {
change.select({
anchorKey: a.key,
anchorOffset: a.offset,
focusKey: f.key,
focusOffset: f.offset
})
}

debug('onBeforeInput', { data, isNative })
change.insertText(e.data)
}

/**
Expand Down