1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-30 02:19:52 +02:00

fix firefox support

This commit is contained in:
Ian Storm Taylor
2016-07-07 19:37:34 -07:00
parent db1151bd15
commit 9b3bcd837d
13 changed files with 428 additions and 238 deletions

View File

@@ -1,7 +1,8 @@
import { Editor, Mark, Raw } from '../..'
import { Editor, Mark, Raw, Utils } from '../..'
import React from 'react'
import initialState from './state.json'
import keycode from 'keycode'
/**
* Node renderers.
@@ -55,42 +56,6 @@ class RichText extends React.Component {
state: Raw.deserialize(initialState)
};
hasMark = (type) => {
const { state } = this.state
return state.marks.some(mark => mark.type == type)
}
hasBlock = (type) => {
const { state } = this.state
return state.blocks.some(node => node.type == type)
}
onClickMark = (e, type) => {
e.preventDefault()
const isActive = this.hasMark(type)
let { state } = this.state
state = state
.transform()
[isActive ? 'unmark' : 'mark'](type)
.apply()
this.setState({ state })
}
onClickBlock = (e, type) => {
e.preventDefault()
const isActive = this.hasBlock(type)
let { state } = this.state
state = state
.transform()
.setBlock(isActive ? 'paragraph' : type)
.apply()
this.setState({ state })
}
render = () => {
return (
<div>
@@ -146,6 +111,7 @@ class RichText extends React.Component {
renderNode={this.renderNode}
renderMark={this.renderMark}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
/>
</div>
)
@@ -159,6 +125,16 @@ class RichText extends React.Component {
return MARKS[mark.type]
}
hasMark = (type) => {
const { state } = this.state
return state.marks.some(mark => mark.type == type)
}
hasBlock = (type) => {
const { state } = this.state
return state.blocks.some(node => node.type == type)
}
onChange = (state) => {
console.groupCollapsed('Change!')
console.log('Document:', state.document.toJS())
@@ -168,6 +144,63 @@ class RichText extends React.Component {
this.setState({ state })
}
onKeyDown = (e, state) => {
if (!Utils.Key.isCommand(e)) return
const key = keycode(e.which)
let mark
switch (key) {
case 'b':
mark = 'bold'
break
case 'i':
mark = 'italic'
break
case 'u':
mark = 'underlined'
break
case '`':
mark = 'code'
break
default:
return
}
state = state
.transform()
[this.hasMark(mark) ? 'unmark' : 'mark'](mark)
.apply()
e.preventDefault()
return state
}
onClickMark = (e, type) => {
e.preventDefault()
const isActive = this.hasMark(type)
let { state } = this.state
state = state
.transform()
[isActive ? 'unmark' : 'mark'](type)
.apply()
this.setState({ state })
}
onClickBlock = (e, type) => {
e.preventDefault()
const isActive = this.hasBlock(type)
let { state } = this.state
state = state
.transform()
.setBlock(isActive ? 'paragraph' : type)
.apply()
this.setState({ state })
}
}
/**

View File

@@ -65,7 +65,7 @@
}
]
},{
"text": ", or add a semanticlly rendered block quote in the middle of the page, like this:"
"text": ", or add a semantically rendered block quote in the middle of the page, like this:"
}
]
}

View File

@@ -1,11 +1,18 @@
import Key from '../utils/key'
import OffsetKey from '../utils/offset-key'
import Raw from '../serializers/raw'
import React from 'react'
import Text from './text'
import Void from './void'
import keycode from 'keycode'
import { Raw } from '..'
import { isCommand, isWindowsCommand } from '../utils/event'
import { IS_FIREFOX } from '../utils/environment'
/**
* Noop.
*/
function noop() {}
/**
* Content.
@@ -63,20 +70,10 @@ class Content extends React.Component {
* @param {Object} props
*/
componentWillMount = () => {
this.tmp.isRendering = true
}
componentWillUpdate = (props, state) => {
this.tmp.isRendering = true
}
componentDidMount = () => {
setTimeout(() => {
this.tmp.isRendering = false
})
}
componentDidUpdate = (props, state) => {
setTimeout(() => {
this.tmp.isRendering = false
@@ -214,10 +211,10 @@ class Content extends React.Component {
(key == 'enter') ||
(key == 'backspace') ||
(key == 'delete') ||
(key == 'b' && isCommand(e)) ||
(key == 'i' && isCommand(e)) ||
(key == 'y' && isWindowsCommand(e)) ||
(key == 'z' && isCommand(e))
(key == 'b' && Key.isCommand(e)) ||
(key == 'i' && Key.isCommand(e)) ||
(key == 'y' && Key.isWindowsCommand(e)) ||
(key == 'z' && Key.isCommand(e))
) {
e.preventDefault()
}
@@ -234,7 +231,7 @@ class Content extends React.Component {
onPaste = (e) => {
e.preventDefault()
const data = e.clipboardData
const { types } = data
const types = Array.from(data.types)
const paste = {}
// Handle files.
@@ -308,13 +305,13 @@ class Content extends React.Component {
state = state
.transform()
.focus()
.moveTo({
anchorKey: anchor.key,
anchorOffset: anchor.offset,
focusKey: focus.key,
focusOffset: focus.offset
})
.focus()
.apply({ isNative: true })
this.onChange(state)
@@ -350,6 +347,7 @@ class Content extends React.Component {
onKeyDown={this.onKeyDown}
onPaste={this.onPaste}
onSelect={this.onSelect}
onKeyUp={noop}
>
{children}
</div>

View File

@@ -27,17 +27,22 @@ class Leaf extends React.Component {
* Should component update?
*
* @param {Object} props
* @param {Object} state
* @return {Boolean} shouldUpdate
*/
shouldComponentUpdate(props, state) {
return (
shouldComponentUpdate(props) {
const { start, end, node, state } = props
const { selection } = state
const should = (
selection.hasEdgeBetween(node, start, end) ||
props.start != this.props.start ||
props.end != this.props.end ||
props.text != this.props.text ||
props.marks != this.props.marks
)
return should
}
componentDidMount() {
@@ -55,19 +60,19 @@ class Leaf extends React.Component {
// If the selection is not focused we have nothing to do.
if (!selection.isFocused) return
const { anchorKey, anchorOffset, focusKey, focusOffset } = selection
const { anchorOffset, focusOffset } = selection
const { node, start, end } = this.props
const { key } = node
// If neither matches, the selection doesn't start or end here, so exit.
const hasStart = key == anchorKey && start <= anchorOffset && anchorOffset <= end
const hasEnd = key == focusKey && start <= focusOffset && focusOffset <= end
const hasStart = selection.hasStartBetween(node, start, end)
const hasEnd = selection.hasEndBetween(node, start, end)
if (!hasStart && !hasEnd) return
// We have a selection to render, so prepare a few things...
const native = window.getSelection()
const el = ReactDOM.findDOMNode(this).firstChild
// If both the start and end are here, set the selection all at once.
if (hasStart && hasEnd) {
native.removeAllRanges()

View File

@@ -1,6 +1,5 @@
import Leaf from './leaf'
import OffsetKey from '../utils/offset-key'
import React from 'react'
import groupByMarks from '../utils/group-by-marks'
import { List } from 'immutable'
@@ -32,6 +31,7 @@ class Text extends React.Component {
shouldComponentUpdate(props, state) {
return (
props.state.selection.hasEdgeIn(props.node) ||
props.node.decorations != this.props.node.decorations ||
props.node.characters != this.props.node.characters
)
@@ -67,7 +67,7 @@ class Text extends React.Component {
const offset = previous.size
? previous.map(r => r.text).join('').length
: 0
return this.renderLeaf(range, offset)
return this.renderLeaf(range, i, offset)
})
}
@@ -75,25 +75,21 @@ class Text extends React.Component {
* Render a single leaf node given a `range` and `offset`.
*
* @param {Object} range
* @param {Number} index
* @param {Number} offset
* @return {Element} leaf
*/
renderLeaf(range, offset) {
renderLeaf(range, index, offset) {
const { node, renderMark, state } = this.props
const text = range.text
const marks = range.marks
const start = offset
const end = offset + text.length
const offsetKey = OffsetKey.stringify({
key: node.key,
start,
end
})
return (
<Leaf
key={offsetKey}
key={`${node.key}-${index}`}
state={state}
node={node}
start={start}

View File

@@ -3,25 +3,69 @@
* Components.
*/
export { default as Editor } from './components/editor'
import Editor from './components/editor'
/**
* Models.
*/
export { default as Block } from './models/block'
export { default as Character } from './models/character'
export { default as Data } from './models/data'
export { default as Document } from './models/document'
export { default as Inline } from './models/inline'
export { default as Mark } from './models/mark'
export { default as Selection } from './models/selection'
export { default as State } from './models/state'
export { default as Text } from './models/text'
import Block from './models/block'
import Character from './models/character'
import Data from './models/data'
import Document from './models/document'
import Inline from './models/inline'
import Mark from './models/mark'
import Selection from './models/selection'
import State from './models/state'
import Text from './models/text'
/**
* Serializers.
*/
export { default as Html } from './serializers/html'
export { default as Raw } from './serializers/raw'
import Html from './serializers/html'
import Raw from './serializers/raw'
/**
* Utils.
*/
import Key from './utils/key'
const Utils = { Key }
/**
* Export.
*/
export {
Block,
Character,
Data,
Document,
Editor,
Html,
Inline,
Mark,
Raw,
Selection,
State,
Text,
Utils
}
export default {
Block,
Character,
Data,
Document,
Editor,
Html,
Inline,
Mark,
Raw,
Selection,
State,
Text,
Utils
}

View File

@@ -2,7 +2,26 @@
import { Record } from 'immutable'
/**
* Record.
* Start-and-end convenience methods to auto-generate.
*/
const START_END_METHODS = [
'moveTo%'
]
/**
* Start-end-and-edge convenience methods to auto-generate.
*/
const EDGE_METHODS = [
'has%AtStartOf',
'has%AtEndOf',
'has%Between',
'has%In'
]
/**
* Default properties.
*/
const DEFAULTS = {
@@ -115,6 +134,114 @@ class Selection extends new Record(DEFAULTS) {
: this.focusOffset
}
/**
* Check whether anchor point of the selection is at the start of a `node`.
*
* @param {Node} node
* @return {Boolean}
*/
hasAnchorAtStartOf(node) {
const first = node.kind == 'text' ? node : node.getTextNodes().first()
return this.anchorKey == first.key && this.anchorOffset == 0
}
/**
* Check whether anchor point of the selection is at the end of a `node`.
*
* @param {Node} node
* @return {Boolean}
*/
hasAnchorAtEndOf(node) {
const last = node.kind == 'text' ? node : node.getTextNodes().last()
return this.anchorKey == last.key && this.anchorOffset == last.length
}
/**
* Check whether the anchor edge of a selection is in a `node` and at an
* offset between `start` and `end`.
*
* @param {Node} node
* @param {Number} start
* @param {Number} end
* @return {Boolean}
*/
hasAnchorBetween(node, start, end) {
return (
this.hasAnchorIn(node) &&
start <= this.anchorOffset &&
this.anchorOffset <= end
)
}
/**
* Check whether the anchor edge of a selection is in a `node`.
*
* @param {Node} node
* @return {Boolean}
*/
hasAnchorIn(node) {
const nodes = node.kind == 'text' ? [node] : node.getTextNodes()
return nodes.some(n => n.key == this.anchorKey)
}
/**
* Check whether focus point of the selection is at the end of a `node`.
*
* @param {Node} node
* @return {Boolean}
*/
hasFocusAtEndOf(node) {
const last = node.kind == 'text' ? node : node.getTextNodes().last()
return this.focusKey == last.key && this.focusOffset == last.length
}
/**
* Check whether focus point of the selection is at the start of a `node`.
*
* @param {Node} node
* @return {Boolean}
*/
hasFocusAtStartOf(node) {
const first = node.kind == 'text' ? node : node.getTextNodes().first()
return this.focusKey == first.key && this.focusOffset == 0
}
/**
* Check whether the focus edge of a selection is in a `node` and at an
* offset between `start` and `end`.
*
* @param {Node} node
* @param {Number} start
* @param {Number} end
* @return {Boolean}
*/
hasFocusBetween(node, start, end) {
return (
this.hasFocusIn(node) &&
start <= this.focusOffset &&
this.focusOffset <= end
)
}
/**
* Check whether the focus edge of a selection is in a `node`.
*
* @param {Node} node
* @return {Boolean}
*/
hasFocusIn(node) {
const nodes = node.kind == 'text' ? [node] : node.getTextNodes()
return nodes.some(n => n.key == this.focusKey)
}
/**
* Check whether the selection is at the start of a `node`.
*
@@ -141,38 +268,6 @@ class Selection extends new Record(DEFAULTS) {
return this.isCollapsed && endKey == last.key && endOffset == last.length
}
/**
* Check whether the selection has an edge at the start of a `node`.
*
* @param {Node} node
* @return {Boolean} hasEdgeAtStart
*/
hasEdgeAtStartOf(node) {
const { startKey, startOffset, endKey, endOffset } = this
const first = node.kind == 'text' ? node : node.getTextNodes().first()
return (
(startKey == first.key && startOffset == 0) ||
(endKey == first.key && endOffset == 0)
)
}
/**
* Check whether the selection has an edge at the end of a `node`.
*
* @param {Node} node
* @return {Boolean} hasEdgeAtEnd
*/
hasEdgeAtEndOf(node) {
const { startKey, startOffset, endKey, endOffset } = this
const last = node.kind == 'text' ? node : node.getTextNodes().last()
return (
(startKey == last.key && startOffset == last.length) ||
(endKey == last.key && endOffset == last.length)
)
}
/**
* Normalize the selection, relative to a `node`, ensuring that the anchor
* and focus nodes of the selection always refer to leaf text nodes.
@@ -221,36 +316,6 @@ class Selection extends new Record(DEFAULTS) {
: anchorIndex > focusIndex
}
// If the selection is expanded and has an edge on a void block, move it.
// if (isExpanded) {
// let anchorBlock = node.getClosestBlock(anchorNode)
// let focusBlock = node.getClosestBlock(focusNode)
// if (anchorBlock.isVoid) {
// while (anchorBanchorBlock.isVoid) {
// anchorBlock = isBackward
// ? node.getPreviousBlock(anchorBlock)
// : node.getNextBlock(anchorBlock)
// }
// anchorNode = isBackward
// ? anchorBlock.getTextNodes().last()
// : anchorBlock.getTextNodes().first()
// anchorOffset = isBackward
// ? anchorNode.length
// : 0
// }
// else if (focusBlock.isVoid) {
// focusNode = isBackward
// ? node.getNextBlock(focusBlock).getTextNodes().first()
// : node.getPreviousBlock(focusBlock).getTextNodes().last()
// focusOffset = isBackward
// ? 0
// : focusNode.length
// }
// }
// Merge in any updated properties.
return selection.merge({
anchorKey: anchorNode.key,
@@ -285,17 +350,6 @@ class Selection extends new Record(DEFAULTS) {
})
}
/**
* Move the selection to a specific anchor and focus point.
*
* @param {Object} properties
* @return {Selection} selection
*/
moveTo(properties) {
return this.merge(properties)
}
/**
* Move the focus point to the anchor point.
*
@@ -324,46 +378,6 @@ class Selection extends new Record(DEFAULTS) {
})
}
/**
* Move the end point to the start point.
*
* @return {Selection} selection
*/
moveToStart() {
return this.isBackward
? this.merge({
anchorKey: this.focusKey,
anchorOffset: this.focusOffset,
isBackward: false
})
: this.merge({
focusKey: this.anchorKey,
focusOffset: this.anchorOffset,
isBackward: false
})
}
/**
* Move the start point to the end point.
*
* @return {Selection} selection
*/
moveToEnd() {
return this.isBackward
? this.merge({
focusKey: this.anchorKey,
focusOffset: this.anchorOffset,
isBackward: false
})
: this.merge({
anchorKey: this.focusKey,
anchorOffset: this.focusOffset,
isBackward: false
})
}
/**
* Move to the start of a `node`.
*
@@ -502,6 +516,41 @@ class Selection extends new Record(DEFAULTS) {
}
/**
* Add start, end and edge convenience methods.
*/
START_END_METHODS.concat(EDGE_METHODS).forEach((pattern) => {
const [ p, s ] = pattern.split('%')
const anchor = `${p}Anchor${s}`
const edge = `${p}Edge${s}`
const end = `${p}End${s}`
const focus = `${p}Focus${s}`
const start = `${p}Start${s}`
Selection.prototype[start] = function (...args) {
return this.isBackward
? this[focus](...args)
: this[anchor](...args)
}
Selection.prototype[end] = function (...args) {
return this.isBackward
? this[anchor](...args)
: this[focus](...args)
}
if (!EDGE_METHODS.includes(pattern)) return
Selection.prototype[edge] = function (...args) {
return this[anchor](...args) || this[focus](...args)
}
})
/**
* Add edge methods.
*/
/**
* Export.
*/

View File

@@ -519,6 +519,31 @@ class State extends new Record(DEFAULTS) {
return state
}
/**
* Move the selection to a specific anchor and focus point.
*
* @param {Object} properties
* @return {State} state
*/
moveTo(properties) {
let state = this
let { document, selection } = state
// Pass in properties, and force `isBackward` to be re-resolved.
selection = selection.merge({
anchorKey: properties.anchorKey,
anchorOffset: properties.anchorOffset,
focusKey: properties.focusKey,
focusOffset: properties.focusOffset,
isBackward: null
})
selection = selection.normalize(document)
state = state.merge({ selection })
return state
}
/**
* Move the selection to the start of the previous block.
*

View File

@@ -57,7 +57,6 @@ const SELECTION_TRANSFORMS = [
'focus',
'moveBackward',
'moveForward',
'moveTo',
'moveToAnchor',
'moveToEnd',
'moveToEndOf',
@@ -78,6 +77,7 @@ const STATE_TRANSFORMS = [
'insertFragment',
'insertText',
'mark',
'moveTo',
'moveToStartOfPreviousBlock',
'moveToEndOfPreviousBlock',
'moveToStartOfNextBlock',

View File

@@ -1,7 +1,7 @@
import Key from '../utils/key'
import React from 'react'
import keycode from 'keycode'
import { isCommand, isCtrl, isWindowsCommand, isWord } from '../utils/event'
import { IS_WINDOWS, IS_MAC } from '../utils/environment'
/**
@@ -93,13 +93,13 @@ export default {
}
case 'backspace': {
return isWord(e)
return Key.isWord(e)
? transform.backspaceWord().apply()
: transform.deleteBackward().apply()
}
case 'delete': {
return isWord(e)
return Key.isWord(e)
? transform.deleteWord().apply()
: transform.deleteForward().apply()
}
@@ -137,13 +137,13 @@ export default {
}
case 'y': {
if (!isWindowsCommand(e)) return
if (!Key.isWindowsCommand(e)) return
return transform.redo()
}
case 'z': {
if (!isCommand(e)) return
return IS_MAC && e.shiftKey
if (!Key.isCommand(e)) return
return IS_MAC && Key.isShift(e)
? transform.redo()
: transform.undo()
}

View File

@@ -16,3 +16,16 @@ export const IS_MAC = process.browser && new Parser().getOS().name == 'Mac OS'
export const IS_SAFARI = process.browser && browser.name == 'safari'
export const IS_UBUNTU = process.browser && new Parser().getOS().name == 'Ubuntu'
export const IS_WINDOWS = process.browser && new Parser().getOS().name.includes('Windows')
export default {
IS_ANDROID,
IS_CHROME,
IS_EDGE,
IS_FIREFOX,
IS_IE,
IS_IOS,
IS_MAC,
IS_SAFARI,
IS_UBUNTU,
IS_WINDOWS
}

View File

@@ -2,16 +2,27 @@
import { IS_MAC, IS_WINDOWS } from './environment'
/**
* Does an `e` have the word-level modifier?
* Does an `e` have the alt modifier?
*
* @param {Event} e
* @return {Boolean}
*/
export function isWord(e) {
function isAlt(e) {
return e.altKey
}
/**
* Does an `e` have the command modifier?
*
* @param {Event} e
* @return {Boolean}
*/
function isCommand(e) {
return IS_MAC
? e.altKey
: e.ctrlKey
? e.metaKey && !e.altKey
: e.ctrlKey && !e.altKey
}
/**
@@ -21,10 +32,21 @@ export function isWord(e) {
* @return {Boolean}
*/
export function isCtrl(e) {
function isCtrl(e) {
return e.ctrlKey && !e.altKey
}
/**
* Does an `e` have the Mac command modifier?
*
* @param {Event} e
* @return {Boolean}
*/
function isMacCommand(e) {
return IS_MAC && isCommand(e)
}
/**
* Does an `e` have the option modifier?
*
@@ -32,7 +54,7 @@ export function isCtrl(e) {
* @return {Boolean}
*/
export function isOption(e) {
function isOption(e) {
return IS_MAC && e.altKey
}
@@ -43,34 +65,10 @@ export function isOption(e) {
* @return {Boolean}
*/
export function isShift(e) {
function isShift(e) {
return e.shiftKey
}
/**
* Does an `e` have the command modifier?
*
* @param {Event} e
* @return {Boolean}
*/
export function isCommand(e) {
return IS_MAC
? e.metaKey && !e.altKey
: e.ctrlKey && !e.altKey
}
/**
* Does an `e` have the Mac command modifier?
*
* @param {Event} e
* @return {Boolean}
*/
export function isMacCommand(e) {
return IS_MAC && isCommand(e)
}
/**
* Does an `e` have the Windows command modifier?
*
@@ -78,6 +76,34 @@ export function isMacCommand(e) {
* @return {Boolean}
*/
export function isWindowsCommand(e) {
function isWindowsCommand(e) {
return IS_WINDOWS && isCommand(e)
}
/**
* Does an `e` have the word-level modifier?
*
* @param {Event} e
* @return {Boolean}
*/
function isWord(e) {
return IS_MAC
? e.altKey
: e.ctrlKey
}
/**
* Export.
*/
export default {
isAlt,
isCommand,
isCtrl,
isMacCommand,
isOption,
isShift,
isWindowsCommand,
isWord
}

View File

@@ -3,7 +3,7 @@
"version": "0.1.0",
"license": "MIT",
"repository": "git://github.com/ianstormtaylor/slate.git",
"main": "./dist/index.js",
"main": "./lib/index.js",
"scripts": {
"prepublish": "make dist",
"test": "make check"
@@ -32,6 +32,7 @@
"babelify": "^7.3.0",
"browserify": "^13.0.1",
"component-type": "^1.2.1",
"draft-js": "^0.7.0",
"eslint": "^3.0.1",
"eslint-plugin-import": "^1.10.2",
"eslint-plugin-react": "^5.2.2",