mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-02-01 05:16:10 +01:00
Add the Stack
concept (#513)
* trying to get testing in browser to work * add the "stack" concept * remove old things from package.json * minor fixes
This commit is contained in:
parent
e080e5a998
commit
ad2642a3b5
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,6 +4,7 @@ examples/build.dev.js
|
||||
examples/build.prod.js
|
||||
lib
|
||||
perf/reference.json
|
||||
test/support/build.js
|
||||
|
||||
# Temporary files.
|
||||
tmp
|
||||
|
@ -36,7 +36,6 @@
|
||||
"browserify-shim": "^3.8.12",
|
||||
"disc": "^1.3.2",
|
||||
"envify": "^3.4.1",
|
||||
"enzyme": "^2.4.1",
|
||||
"eslint": "^3.8.1",
|
||||
"eslint-plugin-import": "^2.0.1",
|
||||
"eslint-plugin-react": "^6.4.1",
|
||||
@ -57,7 +56,6 @@
|
||||
"prismjs": "^1.5.1",
|
||||
"react": "~15.2.0",
|
||||
"react-addons-perf": "~15.2.1",
|
||||
"react-addons-test-utils": "~15.1.0",
|
||||
"react-dom": "~15.2.0",
|
||||
"react-frame-aware-selection-plugin": "0.0.5",
|
||||
"react-frame-component": "^0.6.2",
|
||||
|
@ -1,9 +1,8 @@
|
||||
|
||||
import Content from './content'
|
||||
import CorePlugin from '../plugins/core'
|
||||
import Debug from 'debug'
|
||||
import React from 'react'
|
||||
import Schema from '../models/schema'
|
||||
import Stack from '../models/stack'
|
||||
import State from '../models/state'
|
||||
import noop from '../utils/noop'
|
||||
|
||||
@ -29,7 +28,19 @@ const EVENT_HANDLERS = [
|
||||
'onDrop',
|
||||
'onKeyDown',
|
||||
'onPaste',
|
||||
'onSelect'
|
||||
'onSelect',
|
||||
]
|
||||
|
||||
/**
|
||||
* Plugin-related properties of the editor.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const PLUGINS_PROPS = [
|
||||
...EVENT_HANDLERS,
|
||||
'plugins',
|
||||
'schema',
|
||||
]
|
||||
|
||||
/**
|
||||
@ -80,7 +91,7 @@ class Editor extends React.Component {
|
||||
};
|
||||
|
||||
/**
|
||||
* When created, compute the plugins from `props`.
|
||||
* When constructed, create a new `Stack` and run `onBeforeChange`.
|
||||
*
|
||||
* @param {Object} props
|
||||
*/
|
||||
@ -89,40 +100,47 @@ class Editor extends React.Component {
|
||||
super(props)
|
||||
this.tmp = {}
|
||||
this.state = {}
|
||||
this.state.plugins = this.resolvePlugins(props)
|
||||
this.state.schema = this.resolveSchema(this.state.plugins)
|
||||
|
||||
const state = this.onBeforeChange(props.state)
|
||||
// Create a new `Stack`, omitting the `onChange` property since that has
|
||||
// special significance on the editor itself.
|
||||
const { onChange, ...rest } = props // eslint-disable-line no-unused-vars
|
||||
const stack = Stack.create(rest)
|
||||
this.state.stack = stack
|
||||
|
||||
// Resolve the state, running `onBeforeChange` first.
|
||||
const state = stack.onBeforeChange(props.state, this)
|
||||
this.cacheState(state)
|
||||
this.state.state = state
|
||||
|
||||
// Mix in the event handlers.
|
||||
// Create a bound event handler for each event.
|
||||
for (const method of EVENT_HANDLERS) {
|
||||
this[method] = (...args) => {
|
||||
this.onEvent(method, ...args)
|
||||
const next = this.state.stack[method](this.state.state, this, ...args)
|
||||
this.onChange(next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the `props` are updated, recompute the state and plugins.
|
||||
* When the `props` are updated, create a new `Stack` if necessary, and
|
||||
* run `onBeforeChange`.
|
||||
*
|
||||
* @param {Object} props
|
||||
*/
|
||||
|
||||
componentWillReceiveProps = (props) => {
|
||||
if (props.plugins != this.props.plugins) {
|
||||
const plugins = this.resolvePlugins(props)
|
||||
const schema = this.resolveSchema(plugins)
|
||||
this.setState({ plugins, schema })
|
||||
let { stack } = this.state
|
||||
|
||||
// If any plugin-related properties will change, create a new `Stack`.
|
||||
for (const prop of PLUGINS_PROPS) {
|
||||
if (props[prop] == this.props[prop]) continue
|
||||
const { onChange, ...rest } = props // eslint-disable-line no-unused-vars
|
||||
stack = Stack.create(rest)
|
||||
this.setState({ stack })
|
||||
}
|
||||
|
||||
else if (props.schema != this.props.schema) {
|
||||
const schema = this.resolveSchema(this.state.plugins)
|
||||
this.setState({ schema })
|
||||
}
|
||||
|
||||
const state = this.onBeforeChange(props.state)
|
||||
// Resolve the state, running the before change handler of the stack.
|
||||
const state = stack.onBeforeChange(props.state, this)
|
||||
this.cacheState(state)
|
||||
this.setState({ state })
|
||||
}
|
||||
@ -166,17 +184,17 @@ class Editor extends React.Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the editor's current `schema`.
|
||||
* Get the editor's current schema.
|
||||
*
|
||||
* @return {Schema}
|
||||
*/
|
||||
|
||||
getSchema = () => {
|
||||
return this.state.schema
|
||||
return this.state.stack.schema
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the editor's current `state`.
|
||||
* Get the editor's current state.
|
||||
*
|
||||
* @return {State}
|
||||
*/
|
||||
@ -185,26 +203,6 @@ class Editor extends React.Component {
|
||||
return this.state.state
|
||||
}
|
||||
|
||||
/**
|
||||
* When the editor receives a new 'state'
|
||||
*
|
||||
* @param {State} state
|
||||
* @return {State}
|
||||
*/
|
||||
|
||||
onBeforeChange = (state) => {
|
||||
if (state == this.state.state) return state
|
||||
|
||||
for (const plugin of this.state.plugins) {
|
||||
if (!plugin.onBeforeChange) continue
|
||||
const newState = plugin.onBeforeChange(state, this)
|
||||
if (newState == null) continue
|
||||
state = newState
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* When the `state` changes, pass through plugins, then bubble up.
|
||||
*
|
||||
@ -213,45 +211,18 @@ class Editor extends React.Component {
|
||||
|
||||
onChange = (state) => {
|
||||
if (state == this.state.state) return
|
||||
const { tmp, props } = this
|
||||
const { stack } = this.state
|
||||
const { onChange, onDocumentChange, onSelectionChange } = props
|
||||
|
||||
for (const plugin of this.state.plugins) {
|
||||
if (!plugin.onChange) continue
|
||||
const newState = plugin.onChange(state, this)
|
||||
if (newState == null) continue
|
||||
state = newState
|
||||
}
|
||||
|
||||
this.props.onChange(state)
|
||||
|
||||
if (state.document != this.tmp.document) {
|
||||
this.props.onDocumentChange(state.document, state)
|
||||
}
|
||||
|
||||
if (state.selection != this.tmp.selection) {
|
||||
this.props.onSelectionChange(state.selection, state)
|
||||
}
|
||||
state = stack.onChange(state, this)
|
||||
onChange(state)
|
||||
if (state.document != tmp.document) onDocumentChange(state.document, state)
|
||||
if (state.selection != tmp.selection) onSelectionChange(state.selection, state)
|
||||
|
||||
this.cacheState(state)
|
||||
}
|
||||
|
||||
/**
|
||||
* When an event by `name` fires, pass it through the plugins, and update the
|
||||
* state if one of them chooses to.
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {Mixed} ...args
|
||||
*/
|
||||
|
||||
onEvent = (name, ...args) => {
|
||||
for (const plugin of this.state.plugins) {
|
||||
if (!plugin[name]) continue
|
||||
const newState = plugin[name](...args, this.state.state, this)
|
||||
if (!newState) continue
|
||||
this.onChange(newState)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the editor.
|
||||
*
|
||||
@ -260,7 +231,7 @@ class Editor extends React.Component {
|
||||
|
||||
render = () => {
|
||||
const { props, state } = this
|
||||
const handlers = { onChange: this.onChange }
|
||||
const handlers = {}
|
||||
|
||||
for (const property of EVENT_HANDLERS) {
|
||||
handlers[property] = this[property]
|
||||
@ -271,62 +242,18 @@ class Editor extends React.Component {
|
||||
return (
|
||||
<Content
|
||||
{...handlers}
|
||||
className={props.className}
|
||||
editor={this}
|
||||
onChange={this.onChange}
|
||||
schema={this.getSchema()}
|
||||
state={this.getState()}
|
||||
className={props.className}
|
||||
readOnly={props.readOnly}
|
||||
schema={state.schema}
|
||||
spellCheck={props.spellCheck}
|
||||
state={state.state}
|
||||
style={props.style}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the editor's current plugins from `props` when they change.
|
||||
*
|
||||
* Add a plugin made from the editor's own `props` at the beginning of the
|
||||
* stack. That way, you can add a `onKeyDown` handler to the editor itself,
|
||||
* and it will override all of the existing plugins.
|
||||
*
|
||||
* Also add the "core" functionality plugin that handles the most basic events
|
||||
* for the editor, like delete characters and such.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @return {Array}
|
||||
*/
|
||||
|
||||
resolvePlugins = (props) => {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { onChange, plugins, ...editorPlugin } = props
|
||||
const corePlugin = CorePlugin(props)
|
||||
return [
|
||||
editorPlugin,
|
||||
...plugins,
|
||||
corePlugin
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the editor's schema from a set of `plugins`.
|
||||
*
|
||||
* @param {Array} plugins
|
||||
* @return {Schema}
|
||||
*/
|
||||
|
||||
resolveSchema = (plugins) => {
|
||||
let rules = []
|
||||
|
||||
for (const plugin of plugins) {
|
||||
if (!plugin.schema) continue
|
||||
const schema = Schema.create(plugin.schema)
|
||||
rules = rules.concat(schema.rules)
|
||||
}
|
||||
|
||||
const schema = Schema.create({ rules })
|
||||
return schema
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -18,6 +18,7 @@ import Inline from './models/inline'
|
||||
import Mark from './models/mark'
|
||||
import Schema from './models/schema'
|
||||
import Selection from './models/selection'
|
||||
import Stack from './models/stack'
|
||||
import State from './models/state'
|
||||
import Text from './models/text'
|
||||
import Range from './models/range'
|
||||
@ -64,6 +65,7 @@ export {
|
||||
Raw,
|
||||
Schema,
|
||||
Selection,
|
||||
Stack,
|
||||
State,
|
||||
Text,
|
||||
Transforms,
|
||||
@ -87,6 +89,7 @@ export default {
|
||||
Raw,
|
||||
Schema,
|
||||
Selection,
|
||||
Stack,
|
||||
State,
|
||||
Text,
|
||||
Transforms,
|
||||
|
@ -11,7 +11,7 @@ import { Record, Set } from 'immutable'
|
||||
|
||||
const DEFAULTS = {
|
||||
marks: new Set(),
|
||||
text: ''
|
||||
text: '',
|
||||
}
|
||||
|
||||
/**
|
||||
|
197
src/models/stack.js
Normal file
197
src/models/stack.js
Normal file
@ -0,0 +1,197 @@
|
||||
|
||||
import CorePlugin from '../plugins/core'
|
||||
import Debug from 'debug'
|
||||
import Schema from './schema'
|
||||
import State from './state'
|
||||
import { Record } from 'immutable'
|
||||
|
||||
/**
|
||||
* Debug.
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
|
||||
const debug = Debug('slate:stack')
|
||||
|
||||
/**
|
||||
* Methods that are triggered on events and can change the state.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const EVENT_METHODS = [
|
||||
'onBeforeInput',
|
||||
'onBlur',
|
||||
'onCopy',
|
||||
'onCut',
|
||||
'onDrop',
|
||||
'onKeyDown',
|
||||
'onPaste',
|
||||
'onSelect',
|
||||
]
|
||||
|
||||
/**
|
||||
* Methods that accumulate an updated state.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const ACCUMULATOR_METHODS = [
|
||||
'onBeforeChange',
|
||||
'onChange',
|
||||
]
|
||||
|
||||
/**
|
||||
* All the runnable methods.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const RUNNABLE_METHODS = []
|
||||
.concat(EVENT_METHODS)
|
||||
.concat(ACCUMULATOR_METHODS)
|
||||
|
||||
/**
|
||||
* Default properties.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
const DEFAULTS = {
|
||||
plugins: [],
|
||||
schema: new Schema(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Stack.
|
||||
*
|
||||
* @type {Stack}
|
||||
*/
|
||||
|
||||
class Stack extends new Record(DEFAULTS) {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param {Object} properties
|
||||
* @property {Array} plugins
|
||||
* @property {Schema|Object} schema
|
||||
* @property {Function} ...handlers
|
||||
*/
|
||||
|
||||
static create(properties) {
|
||||
const plugins = resolvePlugins(properties)
|
||||
const schema = resolveSchema(plugins)
|
||||
return new Stack({ plugins, schema })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the kind.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
|
||||
get kind() {
|
||||
return 'stack'
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a `method` in the stack with `state`.
|
||||
*
|
||||
* @param {String} method
|
||||
* @param {State} state
|
||||
* @param {Editor} editor
|
||||
* @param {Mixed} ...args
|
||||
* @return {State}
|
||||
*/
|
||||
|
||||
run(method, state, editor, ...args) {
|
||||
debug(method)
|
||||
|
||||
if (method == 'onChange') {
|
||||
state = this.onBeforeChange(state, editor)
|
||||
}
|
||||
|
||||
for (const plugin of this.plugins) {
|
||||
if (!plugin[method]) continue
|
||||
const next = plugin[method](...args, state, editor)
|
||||
|
||||
if (next == null) {
|
||||
continue
|
||||
} else if (next instanceof State) {
|
||||
state = next
|
||||
if (!ACCUMULATOR_METHODS.includes(method)) break
|
||||
} else {
|
||||
throw new Error(`A plugin returned an unexpected state value: ${next}`)
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Mix in the runnable methods.
|
||||
*/
|
||||
|
||||
for (const method of RUNNABLE_METHODS) {
|
||||
Stack.prototype[method] = function (...args) {
|
||||
return this.run(method, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a schema from a set of `plugins`.
|
||||
*
|
||||
* @param {Array} plugins
|
||||
* @return {Schema}
|
||||
*/
|
||||
|
||||
function resolveSchema(plugins) {
|
||||
let rules = []
|
||||
|
||||
for (const plugin of plugins) {
|
||||
if (plugin.schema == null) continue
|
||||
const schema = Schema.create(plugin.schema)
|
||||
rules = rules.concat(schema.rules)
|
||||
}
|
||||
|
||||
const schema = Schema.create({ rules })
|
||||
return schema
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an array of plugins from `properties`.
|
||||
*
|
||||
* In addition to the plugins provided in `properties.plugins`, this will
|
||||
* create two other plugins:
|
||||
*
|
||||
* - A plugin made from the top-level `properties` themselves, which are
|
||||
* placed at the beginning of the stack. That way, you can add a `onKeyDown`
|
||||
* handler, and it will override all of the existing plugins.
|
||||
*
|
||||
* - A "core" functionality plugin that handles the most basic events in Slate,
|
||||
* like deleting characters, splitting blocks, etc.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @return {Array}
|
||||
*/
|
||||
|
||||
function resolvePlugins(props) {
|
||||
const { plugins = [], ...overridePlugin } = props
|
||||
const corePlugin = CorePlugin(props)
|
||||
return [
|
||||
overridePlugin,
|
||||
...plugins,
|
||||
corePlugin
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Stack}
|
||||
*/
|
||||
|
||||
export default Stack
|
@ -22,7 +22,7 @@ class Transform {
|
||||
* Constructor.
|
||||
*
|
||||
* @param {Object} properties
|
||||
* @property {State} properties.state
|
||||
* @property {State} state
|
||||
*/
|
||||
|
||||
constructor(properties) {
|
||||
|
@ -45,7 +45,7 @@ function Plugin(options = {}) {
|
||||
if (state.isNative) return state
|
||||
|
||||
const schema = editor.getSchema()
|
||||
const { state: prevState } = editor.state
|
||||
const prevState = editor.getState()
|
||||
|
||||
// Since schema can only normalize the document, we avoid creating
|
||||
// a transform and normalize the selection if the document is the same
|
||||
@ -55,6 +55,7 @@ function Plugin(options = {}) {
|
||||
.normalize(schema)
|
||||
.apply({ save: false })
|
||||
|
||||
debug('onBeforeChange')
|
||||
return newState
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import Character from '../models/character'
|
||||
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'
|
||||
import isEmpty from 'is-empty'
|
||||
@ -144,6 +145,24 @@ const Raw = {
|
||||
}))
|
||||
},
|
||||
|
||||
/**
|
||||
* Deserialize a JSON `object` representing a `Selection`.
|
||||
*
|
||||
* @param {Object} object
|
||||
* @param {Object} options (optional)
|
||||
* @return {State}
|
||||
*/
|
||||
|
||||
deserializeSelection(object, options = {}) {
|
||||
return Selection.create({
|
||||
anchorKey: object.anchorKey,
|
||||
anchorOffset: object.anchorOffset,
|
||||
focusKey: object.focusKey,
|
||||
focusOffset: object.focusOffset,
|
||||
isFocused: object.isFocused,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Deserialize a JSON `object` representing a `State`.
|
||||
*
|
||||
@ -155,9 +174,14 @@ const Raw = {
|
||||
deserializeState(object, options = {}) {
|
||||
if (options.terse) object = Raw.untersifyState(object)
|
||||
|
||||
return State.create({
|
||||
document: Raw.deserializeDocument(object.document, options)
|
||||
})
|
||||
const document = Raw.deserializeDocument(object.document, options)
|
||||
let selection
|
||||
|
||||
if (object.selection != null) {
|
||||
selection = Raw.deserializeSelection(object.selection, options)
|
||||
}
|
||||
|
||||
return State.create({ document, selection })
|
||||
},
|
||||
|
||||
/**
|
||||
@ -337,6 +361,30 @@ const Raw = {
|
||||
: object
|
||||
},
|
||||
|
||||
/**
|
||||
* Serialize a `selection`.
|
||||
*
|
||||
* @param {Selection} selection
|
||||
* @param {Object} options (optional)
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
serializeSelection(selection, options = {}) {
|
||||
const object = {
|
||||
kind: selection.kind,
|
||||
anchorKey: selection.anchorKey,
|
||||
anchorOffset: selection.anchorOffset,
|
||||
focusKey: selection.focusKey,
|
||||
focusOffset: selection.focusOffset,
|
||||
isBackward: selection.isBackward,
|
||||
isFocused: selection.isFocused,
|
||||
}
|
||||
|
||||
return options.terse
|
||||
? Raw.tersifySelection(object)
|
||||
: object
|
||||
},
|
||||
|
||||
/**
|
||||
* Serialize a `state`.
|
||||
*
|
||||
@ -348,12 +396,19 @@ const Raw = {
|
||||
serializeState(state, options = {}) {
|
||||
const object = {
|
||||
document: Raw.serializeDocument(state.document, options),
|
||||
selection: Raw.serializeSelection(state.selection, options),
|
||||
kind: state.kind
|
||||
}
|
||||
|
||||
return options.terse
|
||||
if (!options.preserveSelection) {
|
||||
delete object.selection
|
||||
}
|
||||
|
||||
const ret = options.terse
|
||||
? Raw.tersifyState(object)
|
||||
: object
|
||||
|
||||
return ret
|
||||
},
|
||||
|
||||
/**
|
||||
@ -461,6 +516,23 @@ const Raw = {
|
||||
return ret
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a terse representation of a selection `object.`
|
||||
*
|
||||
* @param {Object} object
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
tersifySelection(object) {
|
||||
return {
|
||||
anchorKey: object.anchorKey,
|
||||
anchorOffset: object.anchorOffset,
|
||||
focusKey: object.focusKey,
|
||||
focusOffset: object.focusOffset,
|
||||
isFocused: object.isFocused,
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a terse representation of a state `object`.
|
||||
*
|
||||
@ -469,7 +541,14 @@ const Raw = {
|
||||
*/
|
||||
|
||||
tersifyState(object) {
|
||||
return object.document
|
||||
if (object.selection == null) {
|
||||
return object.document
|
||||
}
|
||||
|
||||
return {
|
||||
document: object.document,
|
||||
selection: object.selection
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -562,6 +641,25 @@ const Raw = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert a terse representation of a selection `object` into a non-terse one.
|
||||
*
|
||||
* @param {Object} object
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
untersifySelection(object) {
|
||||
return {
|
||||
kind: 'selection',
|
||||
anchorKey: object.anchorKey,
|
||||
anchorOffset: object.anchorOffset,
|
||||
focusKey: object.focusKey,
|
||||
focusOffset: object.focusOffset,
|
||||
isBackward: null,
|
||||
isFocused: false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert a terse representation of a state `object` into a non-terse one.
|
||||
*
|
||||
@ -570,6 +668,14 @@ const Raw = {
|
||||
*/
|
||||
|
||||
untersifyState(object) {
|
||||
if (object.selection != null) {
|
||||
return {
|
||||
kind: 'state',
|
||||
document: object.document,
|
||||
selection: object.selection,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'state',
|
||||
document: {
|
||||
|
6
test/behavior/fixtures/on-blur/blur-selection/index.js
Normal file
6
test/behavior/fixtures/on-blur/blur-selection/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
import Simulate from '../../../../helpers/simulate'
|
||||
|
||||
export default function (state, stack) {
|
||||
return Simulate.blur(stack, state)
|
||||
}
|
17
test/behavior/fixtures/on-blur/blur-selection/input.yaml
Normal file
17
test/behavior/fixtures/on-blur/blur-selection/input.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
document:
|
||||
key: a
|
||||
nodes:
|
||||
- key: b
|
||||
kind: block
|
||||
type: paragraph
|
||||
nodes:
|
||||
- key: c
|
||||
kind: text
|
||||
text: ""
|
||||
selection:
|
||||
anchorKey: c
|
||||
anchorOffset: 0
|
||||
focusKey: c
|
||||
focusOffset: 0
|
||||
isFocused: true
|
17
test/behavior/fixtures/on-blur/blur-selection/output.yaml
Normal file
17
test/behavior/fixtures/on-blur/blur-selection/output.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
document:
|
||||
key: a
|
||||
nodes:
|
||||
- key: b
|
||||
kind: block
|
||||
type: paragraph
|
||||
nodes:
|
||||
- key: c
|
||||
kind: text
|
||||
text: ""
|
||||
selection:
|
||||
anchorKey: c
|
||||
anchorOffset: 0
|
||||
focusKey: c
|
||||
focusOffset: 0
|
||||
isFocused: false
|
@ -0,0 +1,6 @@
|
||||
|
||||
import Simulate from '../../../../helpers/simulate'
|
||||
|
||||
export default function (state, stack) {
|
||||
return Simulate.keyDown(stack, state, null, { key: 'enter' })
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
|
||||
document:
|
||||
key: a
|
||||
nodes:
|
||||
- key: b
|
||||
kind: block
|
||||
type: paragraph
|
||||
nodes:
|
||||
- key: c
|
||||
kind: text
|
||||
text: ""
|
||||
- key: d
|
||||
kind: block
|
||||
type: code
|
||||
nodes:
|
||||
- key: e
|
||||
kind: text
|
||||
text: ""
|
||||
selection:
|
||||
anchorKey: e
|
||||
anchorOffset: 0
|
||||
focusKey: e
|
||||
focusOffset: 0
|
||||
isFocused: true
|
@ -0,0 +1,31 @@
|
||||
|
||||
document:
|
||||
key: a
|
||||
nodes:
|
||||
- key: b
|
||||
kind: block
|
||||
type: paragraph
|
||||
nodes:
|
||||
- key: c
|
||||
kind: text
|
||||
text: ""
|
||||
- key: d
|
||||
kind: block
|
||||
type: code
|
||||
nodes:
|
||||
- key: e
|
||||
kind: text
|
||||
text: ""
|
||||
- key: "1"
|
||||
kind: block
|
||||
type: code
|
||||
nodes:
|
||||
- key: "0"
|
||||
kind: text
|
||||
text: ""
|
||||
selection:
|
||||
anchorKey: "0"
|
||||
anchorOffset: 0
|
||||
focusKey: "0"
|
||||
focusOffset: 0
|
||||
isFocused: true
|
50
test/behavior/index.js
Normal file
50
test/behavior/index.js
Normal file
@ -0,0 +1,50 @@
|
||||
|
||||
import assert from 'assert'
|
||||
import fs from 'fs'
|
||||
import readYaml from 'read-yaml-promise'
|
||||
import toCamel from 'to-camel-case'
|
||||
import { Stack, Raw } from '../..'
|
||||
import { resolve } from 'path'
|
||||
|
||||
/**
|
||||
* Tests.
|
||||
*/
|
||||
|
||||
describe('behavior', () => {
|
||||
const fixturesDir = resolve(__dirname, 'fixtures')
|
||||
const events = fs.readdirSync(fixturesDir)
|
||||
|
||||
for (const event of events) {
|
||||
if (event[0] == '.') continue
|
||||
|
||||
describe(`${toCamel(event)}`, () => {
|
||||
const testsDir = resolve(__dirname, 'fixtures', event)
|
||||
const tests = fs.readdirSync(testsDir)
|
||||
|
||||
for (const test of tests) {
|
||||
if (test[0] === '.') continue
|
||||
|
||||
it(test, async () => {
|
||||
const dir = resolve(__dirname, 'fixtures', event, test)
|
||||
const input = await readYaml(resolve(dir, 'input.yaml'))
|
||||
const expected = await readYaml(resolve(dir, 'output.yaml'))
|
||||
const module = require(dir)
|
||||
const fn = module.default
|
||||
|
||||
let state = Raw.deserialize(input, { terse: true })
|
||||
const props = module.props || {}
|
||||
const stack = Stack.create(props)
|
||||
state = fn(state, stack)
|
||||
|
||||
const output = Raw.serialize(state, {
|
||||
terse: true,
|
||||
preserveKeys: true,
|
||||
preserveSelection: true,
|
||||
})
|
||||
|
||||
assert.deepEqual(output, expected)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
121
test/helpers/simulate.js
Normal file
121
test/helpers/simulate.js
Normal file
@ -0,0 +1,121 @@
|
||||
|
||||
/**
|
||||
* Event handlers that can be simulated.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const EVENT_HANDLERS = [
|
||||
'onBeforeInput',
|
||||
'onBlur',
|
||||
'onCopy',
|
||||
'onCut',
|
||||
'onDrop',
|
||||
'onKeyDown',
|
||||
'onPaste',
|
||||
'onSelect',
|
||||
]
|
||||
|
||||
/**
|
||||
* Change handlers that can be simulated.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
|
||||
const CHANGE_HANDLERS = [
|
||||
'onBeforeChange',
|
||||
'onChange'
|
||||
]
|
||||
|
||||
/**
|
||||
* Simulate utility.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
const Simulate = {}
|
||||
|
||||
/**
|
||||
* Generate the event simulators.
|
||||
*/
|
||||
|
||||
EVENT_HANDLERS.forEach((handler) => {
|
||||
const method = getMethodName(handler)
|
||||
|
||||
Simulate[method] = function (stack, state, e, data) {
|
||||
const editor = createEditor(stack, state)
|
||||
const event = createEvent(e || {})
|
||||
|
||||
let next = stack[handler](state, editor, event, data)
|
||||
if (next == state) return state
|
||||
|
||||
next = stack.onChange(next, editor)
|
||||
return next
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Generate the change simulators.
|
||||
*/
|
||||
|
||||
CHANGE_HANDLERS.forEach((handler) => {
|
||||
const method = getMethodName(handler)
|
||||
|
||||
Simulate[method] = function (stack, state) {
|
||||
const editor = createEditor(stack, state)
|
||||
const next = stack[handler](state, editor)
|
||||
return next
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Get the method name from a `handler` name.
|
||||
*
|
||||
* @param {String} handler
|
||||
* @return {String}
|
||||
*/
|
||||
|
||||
function getMethodName(handler) {
|
||||
return handler.charAt(2).toLowerCase() + handler.slice(3)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fake editor from a `stack` and `state`.
|
||||
*
|
||||
* @param {Stack} stack
|
||||
* @param {State} state
|
||||
*/
|
||||
|
||||
function createEditor(stack, state) {
|
||||
return {
|
||||
getSchema: () => stack.schema,
|
||||
getState: () => state,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fake event with `attributes`.
|
||||
*
|
||||
* @param {Object} attributes
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function createEvent(attributes) {
|
||||
let event = {
|
||||
preventDefault: () => event.isDefaultPrevented = true,
|
||||
stopPropagation: () => event.isPropagationStopped = true,
|
||||
isDefaultPrevented: false,
|
||||
isPropagationStopped: false,
|
||||
...attributes,
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
export default Simulate
|
@ -1,4 +1,10 @@
|
||||
|
||||
import { resetKeyGenerator } from '..'
|
||||
|
||||
/**
|
||||
* Polyfills.
|
||||
*/
|
||||
|
||||
import 'babel-polyfill'
|
||||
|
||||
/**
|
||||
@ -9,3 +15,12 @@ import './rendering'
|
||||
import './schema'
|
||||
import './serializers'
|
||||
import './transforms'
|
||||
import './behavior'
|
||||
|
||||
/**
|
||||
* Reset Slate's internal state before each text.
|
||||
*/
|
||||
|
||||
beforeEach(() => {
|
||||
resetKeyGenerator()
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user