1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-29 09:59:48 +02:00

Add schema first/last definitions (#1360)

* add `first` and `last` validations to schema

* update docs

* update schema usage in images example

* fix forced-layout example
This commit is contained in:
Ian Storm Taylor
2017-10-31 21:11:05 -07:00
committed by GitHub
parent 6dc3c6441c
commit 98ed83c23b
12 changed files with 418 additions and 23 deletions

View File

@@ -69,7 +69,9 @@ A dictionary of inlines by type, each with its own set of validation rules.
```js ```js
{ {
data: Object, data: Object,
first: Object,
isVoid: Boolean, isVoid: Boolean,
last: Object,
nodes: Array, nodes: Array,
normalize: Function, normalize: Function,
parent: Object, parent: Object,
@@ -90,7 +92,18 @@ Slate schemas are built up of a set of validation rules. Each of the properties
} }
``` ```
A dictionary of A dictionary of data attributes and their corresponding validation functions. The functions should return a boolean indicating whether the data value is valid or not.
### `first`
`Object`
```js
{
first: { types: ['quote', 'paragraph'] },
}
```
Will validate the first child node. The `first` definition can declare `kinds` and `types` properties.
### `isVoid` ### `isVoid`
`Boolean` `Boolean`
@@ -103,6 +116,17 @@ A dictionary of
Will validate a node's `isVoid` property. Will validate a node's `isVoid` property.
### `last`
`Object`
```js
{
last: { types: ['quote', 'paragraph'] },
}
```
Will validate the last child node. The `last` definition can declare `kinds` and `types` properties.
### `nodes` ### `nodes`
`Array` `Array`
@@ -115,7 +139,9 @@ Will validate a node's `isVoid` property.
} }
``` ```
Will validate a node's children. The node definitions can declare the `kinds`, `types`, `min` and `max` properties. Will validate a node's children. The `nodes` definitions can declare the `kinds`, `types`, `min` and `max` properties.
> 🤖 The `nodes` array is order-sensitive! The example above will require that the first node be either an `image` or `video`, and that it be followed by one or more `paragraph` nodes.
### `normalize` ### `normalize`
`normalize(change: Change, reason: String, context: Object) => Void` `normalize(change: Change, reason: String, context: Object) => Void`
@@ -233,6 +259,46 @@ When supplying your own `normalize` property for a schema rule, it will be calle
} }
``` ```
### `first_child_kind_invalid`
```js
{
child: Node,
node: Node,
rule: Object,
}
```
### `first_child_type_invalid`
```js
{
child: Node,
node: Node,
rule: Object,
}
```
### `last_child_kind_invalid`
```js
{
child: Node,
node: Node,
rule: Object,
}
```
### `last_child_type_invalid`
```js
{
child: Node,
node: Node,
rule: Object,
}
```
### `node_data_invalid` ### `node_data_invalid`
```js ```js

View File

@@ -74,7 +74,6 @@ class ForcedLayout extends React.Component {
schema={schema} schema={schema}
onChange={this.onChange} onChange={this.onChange}
renderNode={this.renderNode} renderNode={this.renderNode}
validateNode={this.validateNode}
/> />
</div> </div>
) )

View File

@@ -27,6 +27,26 @@ function insertImage(change, src, target) {
}) })
} }
/**
* A schema to enforce that there's always a paragraph as the last block.
*
* @type {Object}
*/
const schema = {
document: {
last: { types: ['paragraph'] },
normalize: (change, reason, { node, child }) => {
switch (reason) {
case 'last_child_type_invalid': {
const paragraph = Block.create('paragraph')
return change.insertNodeByKey(node.key, node.nodes.size, paragraph)
}
}
}
}
}
/** /**
* The images example. * The images example.
* *
@@ -88,11 +108,11 @@ class Images extends React.Component {
<Editor <Editor
placeholder="Enter some text..." placeholder="Enter some text..."
value={this.state.value} value={this.state.value}
schema={schema}
onChange={this.onChange} onChange={this.onChange}
onDrop={this.onDrop} onDrop={this.onDrop}
onPaste={this.onPaste} onPaste={this.onPaste}
renderNode={this.renderNode} renderNode={this.renderNode}
validateNode={this.validateNode}
/> />
</div> </div>
) )
@@ -119,24 +139,6 @@ class Images extends React.Component {
} }
} }
/**
* Perform node validation on the document.
*
* @param {Node} node
* @return {Function|Void}
*/
validateNode = (node) => {
if (node.kind != 'document') return
const last = node.nodes.last()
if (!last || last.type != 'paragraph') {
const index = node.nodes.size
const paragraph = Block.create('paragraph')
return change => change.insertNodeByKey(node.key, index, paragraph)
}
}
/** /**
* On change. * On change.
* *

View File

@@ -19,6 +19,10 @@ const CHILD_KIND_INVALID = 'child_kind_invalid'
const CHILD_REQUIRED = 'child_required' const CHILD_REQUIRED = 'child_required'
const CHILD_TYPE_INVALID = 'child_type_invalid' const CHILD_TYPE_INVALID = 'child_type_invalid'
const CHILD_UNKNOWN = 'child_unknown' const CHILD_UNKNOWN = 'child_unknown'
const FIRST_CHILD_KIND_INVALID = 'first_child_kind_invalid'
const FIRST_CHILD_TYPE_INVALID = 'first_child_type_invalid'
const LAST_CHILD_KIND_INVALID = 'last_child_kind_invalid'
const LAST_CHILD_TYPE_INVALID = 'last_child_type_invalid'
const NODE_DATA_INVALID = 'node_data_invalid' const NODE_DATA_INVALID = 'node_data_invalid'
const NODE_IS_VOID_INVALID = 'node_is_void_invalid' const NODE_IS_VOID_INVALID = 'node_is_void_invalid'
const NODE_MARK_INVALID = 'node_mark_invalid' const NODE_MARK_INVALID = 'node_mark_invalid'
@@ -204,7 +208,11 @@ class Schema extends Record(DEFAULTS) {
switch (reason) { switch (reason) {
case CHILD_KIND_INVALID: case CHILD_KIND_INVALID:
case CHILD_TYPE_INVALID: case CHILD_TYPE_INVALID:
case CHILD_UNKNOWN: { case CHILD_UNKNOWN:
case FIRST_CHILD_KIND_INVALID:
case FIRST_CHILD_TYPE_INVALID:
case LAST_CHILD_KIND_INVALID:
case LAST_CHILD_TYPE_INVALID: {
const { child, node } = context const { child, node } = context
return child.kind == 'text' && node.kind == 'block' && node.nodes.size == 1 return child.kind == 'text' && node.kind == 'block' && node.nodes.size == 1
? change.removeNodeByKey(node.key) ? change.removeNodeByKey(node.key)
@@ -295,6 +303,30 @@ class Schema extends Record(DEFAULTS) {
} }
} }
if (rule.first != null) {
const first = node.nodes.first()
if (rule.first.kinds != null && !rule.first.kinds.includes(first.kind)) {
return this.fail(FIRST_CHILD_KIND_INVALID, { ...ctx, child: first })
}
if (rule.first.types != null && !rule.first.types.includes(first.type)) {
return this.fail(FIRST_CHILD_TYPE_INVALID, { ...ctx, child: first })
}
}
if (rule.last != null) {
const last = node.nodes.last()
if (rule.last.kinds != null && !rule.last.kinds.includes(last.kind)) {
return this.fail(LAST_CHILD_KIND_INVALID, { ...ctx, child: last })
}
if (rule.last.types != null && !rule.last.types.includes(last.type)) {
return this.fail(LAST_CHILD_TYPE_INVALID, { ...ctx, child: last })
}
}
if (rule.nodes != null || parents != null) { if (rule.nodes != null || parents != null) {
const children = node.nodes.toArray() const children = node.nodes.toArray()
const defs = rule.nodes != null ? rule.nodes.slice() : [] const defs = rule.nodes != null ? rule.nodes.slice() : []
@@ -470,6 +502,8 @@ function resolveNodeRule(kind, type, obj) {
data: {}, data: {},
isVoid: null, isVoid: null,
nodes: null, nodes: null,
first: null,
last: null,
parent: null, parent: null,
text: null, text: null,
...obj, ...obj,

View File

@@ -0,0 +1,39 @@
/** @jsx h */
import h from '../../helpers/h'
export const schema = {
blocks: {
paragraph: {},
quote: {
first: { kinds: ['block'] },
normalize: (change, reason, { child }) => {
if (reason == 'first_child_kind_invalid') {
change.wrapBlockByKey(child.key, 'paragraph')
}
}
}
}
}
export const input = (
<value>
<document>
<quote>
text
</quote>
</document>
</value>
)
export const output = (
<value>
<document>
<quote>
<paragraph>
text
</paragraph>
</quote>
</document>
</value>
)

View File

@@ -0,0 +1,30 @@
/** @jsx h */
import h from '../../helpers/h'
export const schema = {
blocks: {
paragraph: {},
quote: {
first: { kinds: ['text'] },
}
}
}
export const input = (
<value>
<document>
<quote>
<paragraph />
</quote>
</document>
</value>
)
export const output = (
<value>
<document>
<quote />
</document>
</value>
)

View File

@@ -0,0 +1,43 @@
/** @jsx h */
import h from '../../helpers/h'
export const schema = {
blocks: {
paragraph: {},
quote: {
first: { types: ['paragraph'] },
normalize: (change, reason, { child }) => {
if (reason == 'first_child_type_invalid') {
change.wrapBlockByKey(child.key, 'paragraph')
}
}
}
}
}
export const input = (
<value>
<document>
<quote>
<image />
<paragraph />
<paragraph />
</quote>
</document>
</value>
)
export const output = (
<value>
<document>
<quote>
<paragraph>
<image />
</paragraph>
<paragraph />
<paragraph />
</quote>
</document>
</value>
)

View File

@@ -0,0 +1,35 @@
/** @jsx h */
import h from '../../helpers/h'
export const schema = {
blocks: {
paragraph: {},
quote: {
first: { types: ['paragraph'] }
}
}
}
export const input = (
<value>
<document>
<quote>
<image />
<paragraph />
<paragraph />
</quote>
</document>
</value>
)
export const output = (
<value>
<document>
<quote>
<paragraph />
<paragraph />
</quote>
</document>
</value>
)

View File

@@ -0,0 +1,39 @@
/** @jsx h */
import h from '../../helpers/h'
export const schema = {
blocks: {
paragraph: {},
quote: {
last: { kinds: ['block'] },
normalize: (change, reason, { child }) => {
if (reason == 'last_child_kind_invalid') {
change.wrapBlockByKey(child.key, 'paragraph')
}
}
}
}
}
export const input = (
<value>
<document>
<quote>
text
</quote>
</document>
</value>
)
export const output = (
<value>
<document>
<quote>
<paragraph>
text
</paragraph>
</quote>
</document>
</value>
)

View File

@@ -0,0 +1,30 @@
/** @jsx h */
import h from '../../helpers/h'
export const schema = {
blocks: {
paragraph: {},
quote: {
last: { kinds: ['text'] },
}
}
}
export const input = (
<value>
<document>
<quote>
<paragraph />
</quote>
</document>
</value>
)
export const output = (
<value>
<document>
<quote />
</document>
</value>
)

View File

@@ -0,0 +1,43 @@
/** @jsx h */
import h from '../../helpers/h'
export const schema = {
blocks: {
paragraph: {},
quote: {
last: { types: ['paragraph'] },
normalize: (change, reason, { child }) => {
if (reason == 'last_child_type_invalid') {
change.wrapBlockByKey(child.key, 'paragraph')
}
}
}
}
}
export const input = (
<value>
<document>
<quote>
<paragraph />
<paragraph />
<image />
</quote>
</document>
</value>
)
export const output = (
<value>
<document>
<quote>
<paragraph />
<paragraph />
<paragraph>
<image />
</paragraph>
</quote>
</document>
</value>
)

View File

@@ -0,0 +1,35 @@
/** @jsx h */
import h from '../../helpers/h'
export const schema = {
blocks: {
paragraph: {},
quote: {
last: { types: ['paragraph'] }
}
}
}
export const input = (
<value>
<document>
<quote>
<paragraph />
<paragraph />
<image />
</quote>
</document>
</value>
)
export const output = (
<value>
<document>
<quote>
<paragraph />
<paragraph />
</quote>
</document>
</value>
)