1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-02-01 05:16:10 +01:00

add Decoration and Selection models (#2112)

#### Is this adding or improving a _feature_ or fixing a _bug_?

Improvement.

#### What's the new behavior?

This introduces two new models: `Decoration` and `Selection`, which both implement the simpler `Range` interface. This way we can introduce properties to these concepts without having to have them live on all ranges, and we can start to introduce more helpful methods specific to each one's needs.

It also means we don't need to move `isFocused` to value, which saves some complexity on the operations side, retaining `set_selection` as the only way selections are modified.

In the process, it also cleans up a lot of the existing model logic for implementing the `Node` interface, and introduces another `Common` interface for shared properties of all Slate models.

#### How does this change work?

It introduces a new `interfaces/` directory where common sets of properties can be declared, and mixed in to the models with the new (simple) `mixin` utility.

#### Have you checked that...?

* [x] The new code matches the existing patterns and styles.
* [x] The tests pass with `yarn test`.
* [x] The linter passes with `yarn lint`. (Fix errors with `yarn prettier`.)
* [x] The relevant examples still work. (Run examples with `yarn watch`.)

#### Does this fix any issues or need any specific reviewers?

Fixes: #1952 
Fixes: #1807 
Fixes: https://github.com/ianstormtaylor/slate/issues/2110
This commit is contained in:
Ian Storm Taylor 2018-08-22 12:25:22 -07:00 committed by GitHub
parent 1f317cd9b4
commit ecf48926cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 4470 additions and 3988 deletions

View File

@ -225,7 +225,7 @@ class CodeHighlighting extends React.Component {
}
if (typeof token != 'string') {
const range = {
const dec = {
anchor: {
key: startText.key,
offset: startOffset,
@ -234,10 +234,12 @@ class CodeHighlighting extends React.Component {
key: endText.key,
offset: endOffset,
},
marks: [{ type: token.type }],
mark: {
type: token.type,
},
}
decorations.push(range)
decorations.push(dec)
}
start = end

View File

@ -182,7 +182,7 @@ class MarkdownPreview extends React.Component {
}
if (typeof token != 'string') {
const range = {
const dec = {
anchor: {
key: startText.key,
offset: startOffset,
@ -191,10 +191,12 @@ class MarkdownPreview extends React.Component {
key: endText.key,
offset: endOffset,
},
marks: [{ type: token.type }],
mark: {
type: token.type,
},
}
decorations.push(range)
decorations.push(dec)
}
start = end

View File

@ -45,6 +45,20 @@ class SearchHighlighting extends React.Component {
value: Value.fromJSON(initialValue),
}
/**
* The editor's schema.
*
* @type {Object}
*/
schema = {
marks: {
highlight: {
isAtomic: true,
},
},
}
/**
* Render.
*
@ -67,6 +81,7 @@ class SearchHighlighting extends React.Component {
<Editor
placeholder="Enter some rich text..."
value={this.state.value}
schema={this.schema}
onChange={this.onChange}
renderMark={this.renderMark}
spellCheck
@ -127,8 +142,7 @@ class SearchHighlighting extends React.Component {
decorations.push({
anchor: { key, offset: offset - string.length },
focus: { key, offset },
marks: [{ type: 'highlight' }],
isAtomic: true,
mark: { type: 'highlight' },
})
}

View File

@ -4,6 +4,20 @@ This document maintains a list of changes to the `slate-hyperscript` package wit
---
### `0.9.0` — August 22, 2018
###### NEW
**Introducing the `schema` option.** You can now pass in a `schema` option to the `createHyperscript` factory, which will ensure that schema rules are bound whenever you use the `<value>` tag. This is helpful for defining atomicity of decorations, or the voidness of nodes in the future.
###### BREAKING
**The `isFocused` prop of `<selection>` is now `focused`.** This is just to match the other boolean properties in this library which all omit the `is*` prefix to stay consistent with the DOM-style.
**The `atomic` prop of decorations is now controlled by the schema.** Previously each individual decoration could control whether it was atomic or not, but now this is controlled in the schema definition for the mark itself.
---
### `0.8.0` — August 15, 2018
###### BREAKING

View File

@ -1,13 +1,14 @@
import isPlainObject from 'is-plain-object'
import {
Block,
Decoration,
Document,
Inline,
Mark,
Node,
Point,
Range,
Schema,
Selection,
Text,
Value,
} from 'slate'
@ -45,19 +46,19 @@ class FocusPoint {
class DecorationPoint {
constructor(attrs) {
const { key = null, data = {}, marks } = attrs
const { key = null, data = {}, type } = attrs
this.id = key
this.offset = 0
this.marks = marks
this.attribs = data || {}
this.isAtomic = !!this.attribs.atomic
delete this.attribs.atomic
return this
this.type = type
this.data = data
}
combine = focus => {
if (!(focus instanceof DecorationPoint))
if (!(focus instanceof DecorationPoint)) {
throw new Error('misaligned decorations')
return Range.create({
}
return Decoration.create({
anchor: {
key: this.key,
offset: this.offset,
@ -66,9 +67,10 @@ class DecorationPoint {
key: focus.key,
offset: focus.offset,
},
marks: this.marks,
isAtomic: this.isAtomic,
...this.attribs,
mark: {
type: this.type,
data: this.data,
},
})
}
}
@ -95,6 +97,29 @@ const CREATORS = {
return new CursorPoint()
},
decoration(tagName, attributes, children) {
const { key, data } = attributes
const type = tagName
if (key) {
return new DecorationPoint({ key, type, data })
}
const nodes = createChildren(children)
const node = nodes[0]
const { __decorations = [] } = node
const __decoration = {
anchorOffset: 0,
focusOffset: nodes.reduce((len, n) => len + n.text.length, 0),
type,
data,
}
__decorations.push(__decoration)
node.__decorations = __decorations
return nodes
},
document(tagName, attributes, children) {
return Document.create({
...attributes,
@ -119,34 +144,13 @@ const CREATORS = {
return nodes
},
decoration(tagName, attributes, children) {
if (attributes.key) {
return new DecorationPoint({
...attributes,
marks: [{ type: tagName }],
})
}
const nodes = createChildren(children)
const node = nodes[0]
const { __decorations = [] } = node
const __decoration = {
anchorOffset: 0,
focusOffset: nodes.reduce((len, n) => len + n.text.length, 0),
marks: [{ type: tagName }],
isAtomic: !!attributes.data.atomic,
}
__decorations.push(__decoration)
node.__decorations = __decorations
return nodes
},
selection(tagName, attributes, children) {
const anchor = children.find(c => c instanceof AnchorPoint)
const focus = children.find(c => c instanceof FocusPoint)
const selection = Range.create({
...attributes,
const { marks, focused } = attributes
const selection = Selection.create({
marks,
isFocused: focused,
anchor: anchor && {
key: anchor.key,
offset: anchor.offset,
@ -162,10 +166,16 @@ const CREATORS = {
return selection
},
text(tagName, attributes, children) {
const nodes = createChildren(children, { key: attributes.key })
return nodes
},
value(tagName, attributes, children) {
const { data, normalize = true } = attributes
const schema = Schema.create(attributes.schema || {})
const document = children.find(Document.isDocument)
let selection = children.find(Range.isRange) || Range.create()
let selection = children.find(Selection.isSelection) || Selection.create()
let anchor
let focus
let decorations = []
@ -189,7 +199,7 @@ const CREATORS = {
let range
if (!id) {
range = Range.create({
range = Decoration.create({
anchor: {
key: text.key,
offset: dec.anchorOffset,
@ -198,14 +208,16 @@ const CREATORS = {
key: text.key,
offset: dec.focusOffset,
},
marks: dec.marks,
isAtomic: dec.isAtomic,
mark: {
type: dec.type,
data: dec.data,
},
})
} else if (partials[id]) {
const partial = partials[id]
delete partials[id]
range = Range.create({
range = Decoration.create({
anchor: {
key: partial.key,
offset: partial.offset,
@ -214,8 +226,10 @@ const CREATORS = {
key: text.key,
offset: dec.offset,
},
marks: partial.marks,
isAtomic: partial.isAtomic,
mark: {
type: dec.type,
data: dec.data,
},
})
} else {
dec.key = text.key
@ -248,28 +262,26 @@ const CREATORS = {
)
}
let value = Value.fromJSON({ data, document, selection }, { normalize })
let value = Value.fromJSON(
{ data, document, selection, schema },
{ normalize }
)
if (anchor || focus) {
selection = selection.setPoints([anchor, focus])
selection = selection.merge({ isFocused: true })
selection = selection.setIsFocused(true)
selection = selection.normalize(value.document)
value = value.set('selection', selection)
}
if (decorations.length > 0) {
decorations = decorations.map(d => d.normalize(value.document))
decorations = Range.createList(decorations)
decorations = Decoration.createList(decorations)
value = value.set('decorations', decorations)
}
return value
},
text(tagName, attributes, children) {
const nodes = createChildren(children, { key: attributes.key })
return nodes
},
}
/**
@ -445,7 +457,13 @@ function createChildren(children, options = {}) {
*/
function resolveCreators(options) {
const { blocks = {}, inlines = {}, marks = {}, decorations = {} } = options
const {
blocks = {},
inlines = {},
marks = {},
decorations = {},
schema,
} = options
const creators = {
...CREATORS,
@ -468,6 +486,11 @@ function resolveCreators(options) {
creators[key] = normalizeNode(key, decorations[key], 'decoration')
})
creators.value = (tagName, attributes = {}, children) => {
const attrs = { schema, ...attributes }
return CREATORS.value(tagName, attrs, children)
}
return creators
}

View File

@ -47,7 +47,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -61,7 +61,6 @@ export const output = {
offset: 3,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -136,7 +136,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -150,7 +150,6 @@ export const output = {
offset: 1,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -70,7 +70,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -84,7 +84,6 @@ export const output = {
offset: 3,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -70,7 +70,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -84,7 +84,6 @@ export const output = {
offset: 1,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -70,7 +70,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -84,7 +84,6 @@ export const output = {
offset: 0,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -91,7 +91,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -105,7 +105,6 @@ export const output = {
offset: 5,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -91,7 +91,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -105,7 +105,6 @@ export const output = {
offset: 1,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -91,7 +91,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -105,7 +105,6 @@ export const output = {
offset: 0,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -47,7 +47,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -61,7 +61,6 @@ export const output = {
offset: 3,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -47,7 +47,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -61,7 +61,6 @@ export const output = {
offset: 1,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -47,7 +47,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -61,7 +61,6 @@ export const output = {
offset: 0,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -53,7 +53,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -67,7 +67,6 @@ export const output = {
offset: 1,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -82,7 +82,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -96,7 +96,6 @@ export const output = {
offset: 3,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -82,7 +82,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -96,7 +96,6 @@ export const output = {
offset: 0,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -82,7 +82,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -96,7 +96,6 @@ export const output = {
offset: 1,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -67,7 +67,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -81,7 +81,6 @@ export const output = {
offset: 6,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -67,7 +67,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -81,7 +81,6 @@ export const output = {
offset: 3,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -67,7 +67,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -81,7 +81,6 @@ export const output = {
offset: 4,
},
isFocused: true,
isAtomic: false,
marks: null,
},
}

View File

@ -80,7 +80,7 @@ export const output = {
},
decorations: [
{
object: 'range',
object: 'decoration',
anchor: {
object: 'point',
key: '0',
@ -93,15 +93,11 @@ export const output = {
path: [1, 0],
offset: 2,
},
isFocused: false,
isAtomic: false,
marks: [
{
object: 'mark',
type: 'highlight',
data: {},
},
],
mark: {
object: 'mark',
type: 'highlight',
data: {},
},
},
],
}

View File

@ -57,7 +57,7 @@ export const output = {
},
decorations: [
{
object: 'range',
object: 'decoration',
anchor: {
object: 'point',
key: '0',
@ -70,15 +70,11 @@ export const output = {
path: [0, 0],
offset: 6,
},
isFocused: false,
isAtomic: false,
marks: [
{
object: 'mark',
type: 'highlight',
data: {},
},
],
mark: {
object: 'mark',
type: 'highlight',
data: {},
},
},
],
}

View File

@ -51,7 +51,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: 'a',
@ -65,7 +65,6 @@ export const output = {
offset: 2,
},
isFocused: false,
isAtomic: false,
marks: null,
},
}

View File

@ -4,6 +4,14 @@ This document maintains a list of changes to the `slate-react` package with each
---
### `0.17.0` — August 22, 2018
###### NEW
**Updated to work with `slate@0.39.0` with the new `Decoration` and `Selection`.** This isn't a breaking change to any of the API's in `slate-react`, but it does update it to work with the newly introduced models and breaking changed in the newest version of Slate core.
---
### `0.16.0` — August 21, 2018
###### NEW

View File

@ -297,7 +297,7 @@ class Content extends React.Component {
const native = window.getSelection()
const range = findRange(native, value)
if (range && range.equals(selection)) {
if (range && range.equals(selection.toRange())) {
this.updateSelection()
return
}
@ -380,7 +380,7 @@ class Content extends React.Component {
const Container = tagName
const { document, selection, decorations } = value
const indexes = document.getSelectionIndexes(selection)
const decs = document.getDecorations(stack).concat(decorations || [])
const decs = document.getDecorations(stack).concat(decorations)
const childrenDecorations = getChildrenDecorations(document, decs)
const children = document.nodes.toArray().map((child, i) => {

View File

@ -671,8 +671,9 @@ function AfterPlugin() {
if (next) range = range.moveFocusTo(next.key, 0)
}
range = document.resolveRange(range)
change.select(range)
let selection = document.createSelection(range)
selection = selection.setIsFocused(true)
change.select(selection)
}
/**

View File

@ -1,5 +1,4 @@
import getWindow from 'get-window'
import isBackward from 'selection-is-backward'
import { IS_IE, IS_EDGE } from 'slate-dev-environment'
import findPoint from './find-point'
@ -63,8 +62,6 @@ function findRange(native, value) {
const range = document.createRange({
anchor,
focus,
isBackward: isCollapsed ? false : isBackward(native),
isFocused: true,
})
return range

View File

@ -1,12 +1,12 @@
/** @jsx h */
import h from '../../../helpers/h'
import { Range } from 'slate'
import { Selection } from 'slate'
export default function(simulator) {
const { value } = simulator
const text = value.document.getTexts().first()
const selection = Range.create()
const selection = Selection.create()
.collapseToStartOf(text)
.move(1)
.focus()

View File

@ -15,7 +15,9 @@ function decorateNode(block) {
key: text.key,
offset: 2,
},
marks: [{ type: 'bold' }],
mark: {
type: 'bold',
},
},
]
}

View File

@ -4,11 +4,98 @@ A list of changes to the `slate` package with each new version. Until `1.0.0` is
---
### `0.39.0` — August 22, 2018
###### NEW
**Introducing the `Range` model _and_ interface.** Previously the "range" concept was used in multiple different places, for the selection, for decorations, and for acting on ranges of the document. This worked okay, but it was hiding the underlying system which is that `Range` is really an interface that other models can choose to implement. Now, we still use the `Range` model for referencing parts of the document, but it can also be implemented by other models that need to attach more semantic meaning...
**Introducing the `Decoration` and `Selection` models.** These two new models both implement the new `Range` interface. Where previously they had to mis-use the `Range` model itself with added semantics. This just cleans up some of the confusion around overlapping properties, and allows us to add even more domain-specific methods and properties in the future without trouble.
###### BREAKING
**Decorations have changed!** Previously, decorations piggybacked on the `Range` model, using the existing `marks` property, and introducing their own `isAtomic` property. However, they have now been split out into their own `Decoration` model with a single `mark` and with the `isAtomic` property controlled by the schema. What previously would have looked like:
```js
Range.create({
anchor: { ... },
focus: { ... },
marks: [{ type: 'highlight' }],
isAtomic: true,
})
```
Is now:
```js
Decoration.create({
anchor: { ... },
focus: { ... },
mark: { type: 'highlight' },
})
```
Each decoration maps to a single `mark` object. And the atomicity of the mark controlled in the schema instead, for example:
```js
const schema = {
marks: {
highlight: {
isAtomic: true,
},
},
}
```
**The `Range` model has reduced semantics.** Previously, since all decorations and selections were ranges, you could create ranges with an `isFocused`, `isAtomic` or `marks` properties. Now `Range` objects are much simpler, offering only an `anchor` and a `focus`, and can be extended by other models implementing the range interface. However, this means that using `Range.create` or `document.createRange` might not be what you want anymore. For example, for creating a new selection, you used to use:
```js
const selection = document.createRange({
isFocused: true,
anchor: { ... },
focus: { ... },
})
```
But now, you'll need to use `document.createSelection` instead:
```js
const selection = document.createSelection({
isFocused: true,
anchor: { ... },
focus: { ... },
})
```
**The `value.decorations` property is no longer nullable.** Previously when no decorations were applied to the value, the `decorations` property would be set to `null`. Now it will be an empty `List` object, so that the interface is more consistent.
###### DEPRECATED
**The `Node.createChildren` static method is deprecated.** This was just an alias for `Node.createList` and wasn't necessary. You can use `Node.createList` going forward for the same effect.
---
### `0.38.0` — August 21, 2018
###### DEPRECATED
**`Node.isVoid` access is deprecated.** Previously the "voidness" of a node was hardcoded in the data model. Soon it will be determined at runtime based on your editor's schema. This deprecation just ensures that you aren't using the `node.isVoid` property which will not work in future verisons.
**`Node.isVoid` access is deprecated.** Previously the "voidness" of a node was hardcoded in the data model. Soon it will be determined at runtime based on your editor's schema. This deprecation just ensures that you aren't using the `node.isVoid` property which will not work in future verisons. What previously would have been:
```js
if (node.isVoid) {
...
}
```
Now becomes:
```js
if (schema.isVoid(node)) {
...
}
```
This requires you to have a reference to the `schema` object, which can be access as `value.schema`.
**`Value.isFocused/isBlurred` and `Value.hasUndos/hasRedos` are deprecated.** These properties are easily available via `value.selection` and `value.history` instead, and are now deprecated to reduce the complexity and number of different ways of doing things.

View File

@ -376,7 +376,6 @@ Changes.replaceNodeByPath = (change, path, newNode, options) => {
* @param {string} text
* @param {Set<Mark>} marks (optional)
* @param {Object} options
*
*/
Changes.replaceTextByPath = (

View File

@ -3,7 +3,7 @@ import isEmpty from 'is-empty'
import logger from 'slate-dev-logger'
import pick from 'lodash/pick'
import Range from '../models/range'
import Selection from '../models/selection'
const Changes = {}
@ -12,7 +12,7 @@ Changes.blur = change => {
}
Changes.deselect = change => {
const range = Range.create()
const range = Selection.create()
change.select(range)
}
@ -549,13 +549,13 @@ Changes.moveToStartOfText = change => {
}
Changes.select = (change, properties, options = {}) => {
properties = Range.createProperties(properties)
properties = Selection.createProperties(properties)
const { snapshot = false } = options
const { value } = change
const { document, selection } = value
const props = {}
let next = selection.setProperties(properties)
next = document.resolveRange(next)
next = document.resolveSelection(next)
// Re-compute the properties, to ensure that we get their normalized values.
properties = pick(next, Object.keys(properties))

View File

@ -7,6 +7,7 @@
const MODEL_TYPES = {
BLOCK: '@@__SLATE_BLOCK__@@',
CHANGE: '@@__SLATE_CHANGE__@@',
DECORATION: '@@__SLATE_DECORATION__@@',
DOCUMENT: '@@__SLATE_DOCUMENT__@@',
HISTORY: '@@__SLATE_HISTORY__@@',
INLINE: '@@__SLATE_INLINE__@@',
@ -16,6 +17,7 @@ const MODEL_TYPES = {
POINT: '@@__SLATE_POINT__@@',
RANGE: '@@__SLATE_RANGE__@@',
SCHEMA: '@@__SLATE_SCHEMA__@@',
SELECTION: '@@__SLATE_SELECTION__@@',
STACK: '@@__SLATE_STACK__@@',
TEXT: '@@__SLATE_TEXT__@@',
VALUE: '@@__SLATE_VALUE__@@',

View File

@ -1,7 +1,11 @@
import './interfaces/common'
import './interfaces/node'
import './interfaces/range'
import Block from './models/block'
import Change from './models/change'
import Changes from './changes'
import Data from './models/data'
import Decoration from './models/decoration'
import Document from './models/document'
import History from './models/history'
import Inline from './models/inline'
@ -15,6 +19,7 @@ import PathUtils from './utils/path-utils'
import Point from './models/point'
import Range from './models/range'
import Schema from './models/schema'
import Selection from './models/selection'
import Stack from './models/stack'
import Text from './models/text'
import TextUtils from './utils/text-utils'
@ -33,6 +38,7 @@ export {
Change,
Changes,
Data,
Decoration,
Document,
History,
Inline,
@ -48,6 +54,7 @@ export {
resetKeyGenerator,
resetMemoization,
Schema,
Selection,
setKeyGenerator,
Stack,
Text,
@ -60,6 +67,7 @@ export default {
Block,
Changes,
Data,
Decoration,
Document,
History,
Inline,
@ -75,6 +83,7 @@ export default {
resetKeyGenerator,
resetMemoization,
Schema,
Selection,
setKeyGenerator,
Stack,
Text,

View File

@ -0,0 +1,83 @@
import logger from 'slate-dev-logger'
import mixin from '../utils/mixin'
import Block from '../models/block'
import Change from '../models/change'
import Decoration from '../models/decoration'
import Document from '../models/document'
import History from '../models/history'
import Inline from '../models/inline'
import Leaf from '../models/leaf'
import Mark from '../models/mark'
import Node from '../models/node'
import Operation from '../models/operation'
import Point from '../models/point'
import Range from '../models/range'
import Schema from '../models/schema'
import Selection from '../models/selection'
import Stack from '../models/stack'
import Text from '../models/text'
import Value from '../models/value'
/**
* The interface that all Slate models implement.
*
* @type {Class}
*/
class CommonInterface {
/**
* Alias `fromJS`.
*/
static fromJS(...args) {
return this.fromJSON(...args)
}
/**
* Alias `toJS`.
*/
toJS(...args) {
return this.toJSON(...args)
}
/**
* Deprecated.
*/
get kind() {
logger.deprecate(
'slate@0.32.0',
'The `kind` property of Slate objects has been renamed to `object`.'
)
return this.object
}
}
/**
* Mix in the common interface.
*
* @param {Record}
*/
mixin(CommonInterface, [
Block,
Change,
Decoration,
Document,
History,
Inline,
Leaf,
Mark,
Node,
Operation,
Point,
Range,
Schema,
Selection,
Stack,
Text,
Value,
])

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,9 @@
/**
* Dependencies.
*/
import isPlainObject from 'is-plain-object'
import logger from 'slate-dev-logger'
import { List, Map, Record } from 'immutable'
import MODEL_TYPES, { isType } from '../constants/model-types'
import KeyUtils from '../utils/key-utils'
import MODEL_TYPES, { isType } from '../constants/model-types'
import Node from './node'
/**
* Default properties.
@ -102,18 +98,12 @@ class Block extends Record(DEFAULTS) {
type,
isVoid: !!isVoid,
data: Map(data),
nodes: Block.createChildren(nodes),
nodes: Node.createList(nodes),
})
return block
}
/**
* Alias `fromJS`.
*/
static fromJS = Block.fromJSON
/**
* Check if `any` is a `Block`.
*
@ -144,47 +134,6 @@ class Block extends Record(DEFAULTS) {
return 'block'
}
get kind() {
logger.deprecate(
'slate@0.32.0',
'The `kind` property of Slate objects has been renamed to `object`.'
)
return this.object
}
get isVoid() {
logger.deprecate(
'0.38.0',
'The `Node.isVoid` property is deprecated, please use the `Schema.isVoid()` checking method instead.'
)
return this.get('isVoid')
}
/**
* Check if the block is empty.
* Returns true if block is not void and all it's children nodes are empty.
* Void node is never empty, regardless of it's content.
*
* @return {Boolean}
*/
get isEmpty() {
logger.deprecate('0.38.0', 'The `Node.isEmpty` property is deprecated.')
return !this.get('isVoid') && !this.nodes.some(child => !child.isEmpty)
}
/**
* Get the concatenated text of all the block's children.
*
* @return {String}
*/
get text() {
return this.getText()
}
/**
* Return a JSON representation of the block.
*
@ -207,14 +156,6 @@ class Block extends Record(DEFAULTS) {
return object
}
/**
* Alias `toJS`.
*/
toJS(options) {
return this.toJSON(options)
}
}
/**

View File

@ -1,6 +1,5 @@
import Debug from 'debug'
import isPlainObject from 'is-plain-object'
import logger from 'slate-dev-logger'
import pick from 'lodash/pick'
import { List } from 'immutable'
@ -61,14 +60,6 @@ class Change {
return 'change'
}
get kind() {
logger.deprecate(
'slate@0.32.0',
'The `kind` property of Slate objects has been renamed to `object`.'
)
return this.object
}
/**
* Apply an `operation` to the current value, saving the operation to the
* history if needed.

View File

@ -0,0 +1,210 @@
import isPlainObject from 'is-plain-object'
import logger from 'slate-dev-logger'
import { List, Record } from 'immutable'
import Mark from './mark'
import MODEL_TYPES from '../constants/model-types'
import Point from './point'
import Range from './range'
/**
* Default properties.
*
* @type {Object}
*/
const DEFAULTS = {
anchor: Point.create(),
focus: Point.create(),
mark: undefined,
}
/**
* Decoration.
*
* @type {Decoration}
*/
class Decoration extends Record(DEFAULTS) {
/**
* Create a new `Decoration` with `attrs`.
*
* @param {Object|Decoration} attrs
* @return {Decoration}
*/
static create(attrs = {}) {
if (Decoration.isDecoration(attrs)) {
return attrs
}
if (Range.isRange(attrs)) {
return Decoration.fromJSON(Range.createProperties(attrs))
}
if (isPlainObject(attrs)) {
return Decoration.fromJSON(attrs)
}
throw new Error(
`\`Decoration.create\` only accepts objects or decorations, but you passed it: ${attrs}`
)
}
/**
* Create a list of `Ranges` from `elements`.
*
* @param {Array<Decoration|Object>|List<Decoration|Object>} elements
* @return {List<Decoration>}
*/
static createList(elements = []) {
if (List.isList(elements) || Array.isArray(elements)) {
const list = new List(elements.map(Decoration.create))
return list
}
throw new Error(
`\`Decoration.createList\` only accepts arrays or lists, but you passed it: ${elements}`
)
}
/**
* Create a dictionary of settable decoration properties from `attrs`.
*
* @param {Object|String|Decoration} attrs
* @return {Object}
*/
static createProperties(a = {}) {
if (Decoration.isDecoration(a)) {
return {
anchor: Point.createProperties(a.anchor),
focus: Point.createProperties(a.focus),
mark: Mark.create(a.mark),
}
}
if (isPlainObject(a)) {
const p = {}
if ('anchor' in a) p.anchor = Point.create(a.anchor)
if ('focus' in a) p.focus = Point.create(a.focus)
if ('mark' in a) p.mark = Mark.create(a.mark)
return p
}
throw new Error(
`\`Decoration.createProperties\` only accepts objects or decorations, but you passed it: ${a}`
)
}
/**
* Create a `Decoration` from a JSON `object`.
*
* @param {Object} object
* @return {Decoration}
*/
static fromJSON(object) {
const { anchor, focus } = object
let { mark } = object
if (object.marks) {
logger.deprecate(
'0.39.0',
'The `marks` property of decorations has been changed to a single `mark` property instead.'
)
mark = object.marks[0]
}
const decoration = new Decoration({
anchor: Point.fromJSON(anchor || {}),
focus: Point.fromJSON(focus || {}),
mark: Mark.fromJSON(mark),
})
return decoration
}
/**
* Check if an `obj` is a `Decoration`.
*
* @param {Any} obj
* @return {Boolean}
*/
static isDecoration(obj) {
return !!(obj && obj[MODEL_TYPES.DECORATION])
}
/**
* Object.
*
* @return {String}
*/
get object() {
return 'decoration'
}
/**
* Set new `properties` on the decoration.
*
* @param {Object|Range|Selection} properties
* @return {Range}
*/
setProperties(properties) {
properties = Decoration.createProperties(properties)
const { anchor, focus, mark } = properties
const props = {}
if (anchor) {
props.anchor = Point.create(anchor)
}
if (focus) {
props.focus = Point.create(focus)
}
if (mark) {
props.mark = Mark.create(mark)
}
const decoration = this.merge(props)
return decoration
}
/**
* Return a JSON representation of the decoration.
*
* @param {Object} options
* @return {Object}
*/
toJSON(options = {}) {
const object = {
object: this.object,
anchor: this.anchor.toJSON(options),
focus: this.focus.toJSON(options),
mark: this.mark.toJSON(options),
}
return object
}
}
/**
* Attach a pseudo-symbol for type checking.
*/
Decoration.prototype[MODEL_TYPES.DECORATION] = true
/**
* Export.
*
* @type {Decoration}
*/
export default Decoration

View File

@ -1,9 +1,9 @@
import isPlainObject from 'is-plain-object'
import logger from 'slate-dev-logger'
import { List, Map, Record } from 'immutable'
import KeyUtils from '../utils/key-utils'
import MODEL_TYPES, { isType } from '../constants/model-types'
import Node from './node'
/**
* Default properties.
@ -66,18 +66,12 @@ class Document extends Record(DEFAULTS) {
const document = new Document({
key,
data: new Map(data),
nodes: Document.createChildren(nodes),
nodes: Node.createList(nodes),
})
return document
}
/**
* Alias `fromJS`.
*/
static fromJS = Document.fromJSON
/**
* Check if `any` is a `Document`.
*
@ -97,45 +91,6 @@ class Document extends Record(DEFAULTS) {
return 'document'
}
get kind() {
logger.deprecate(
'slate@0.32.0',
'The `kind` property of Slate objects has been renamed to `object`.'
)
return this.object
}
get isVoid() {
logger.deprecate(
'0.38.0',
'The `Node.isVoid` property is deprecated, please use the `Schema.isVoid()` checking method instead.'
)
return this.get('isVoid')
}
/**
* Check if the document is empty.
* Returns true if all it's children nodes are empty.
*
* @return {Boolean}
*/
get isEmpty() {
logger.deprecate('0.38.0', 'The `Node.isEmpty` property is deprecated.')
return !this.nodes.some(child => !child.isEmpty)
}
/**
* Get the concatenated text of all the document's children.
*
* @return {String}
*/
get text() {
return this.getText()
}
/**
* Return a JSON representation of the document.
*
@ -156,14 +111,6 @@ class Document extends Record(DEFAULTS) {
return object
}
/**
* Alias `toJS`.
*/
toJS(options) {
return this.toJSON(options)
}
}
/**

View File

@ -1,7 +1,6 @@
import Debug from 'debug'
import isEqual from 'lodash/isEqual'
import isPlainObject from 'is-plain-object'
import logger from 'slate-dev-logger'
import { List, Record, Stack } from 'immutable'
import MODEL_TYPES, { isType } from '../constants/model-types'
@ -92,12 +91,6 @@ class History extends Record(DEFAULTS) {
return history
}
/**
* Alias `fromJS`.
*/
static fromJS = History.fromJSON
/**
* Check if `any` is a `History`.
*
@ -117,14 +110,6 @@ class History extends Record(DEFAULTS) {
return 'history'
}
get kind() {
logger.deprecate(
'slate@0.32.0',
'The `kind` property of Slate objects has been renamed to `object`.'
)
return this.object
}
/**
* Save an `operation` into the history.
*
@ -187,14 +172,6 @@ class History extends Record(DEFAULTS) {
return object
}
/**
* Alias `toJS`.
*/
toJS() {
return this.toJSON()
}
}
/**

View File

@ -1,13 +1,9 @@
/**
* Dependencies.
*/
import isPlainObject from 'is-plain-object'
import logger from 'slate-dev-logger'
import { List, Map, Record } from 'immutable'
import MODEL_TYPES, { isType } from '../constants/model-types'
import KeyUtils from '../utils/key-utils'
import MODEL_TYPES, { isType } from '../constants/model-types'
import Node from './node'
/**
* Default properties.
@ -102,18 +98,12 @@ class Inline extends Record(DEFAULTS) {
type,
isVoid: !!isVoid,
data: new Map(data),
nodes: Inline.createChildren(nodes),
nodes: Node.createList(nodes),
})
return inline
}
/**
* Alias `fromJS`.
*/
static fromJS = Inline.fromJSON
/**
* Check if `any` is a `Inline`.
*
@ -144,46 +134,6 @@ class Inline extends Record(DEFAULTS) {
return 'inline'
}
get kind() {
logger.deprecate(
'slate@0.32.0',
'The `kind` property of Slate objects has been renamed to `object`.'
)
return this.object
}
get isVoid() {
logger.deprecate(
'0.38.0',
'The `Node.isVoid` property is deprecated, please use the `Schema.isVoid()` checking method instead.'
)
return this.get('isVoid')
}
/**
* Check if the inline is empty.
* Returns true if inline is not void and all it's children nodes are empty.
* Void node is never empty, regardless of it's content.
*
* @return {Boolean}
*/
get isEmpty() {
logger.deprecate('0.38.0', 'The `Node.isEmpty` property is deprecated.')
return !this.get('isVoid') && !this.nodes.some(child => !child.isEmpty)
}
/**
* Get the concatenated text of all the inline's children.
*
* @return {String}
*/
get text() {
return this.getText()
}
/**
* Return a JSON representation of the inline.
*
@ -206,14 +156,6 @@ class Inline extends Record(DEFAULTS) {
return object
}
/**
* Alias `toJS`.
*/
toJS(options) {
return this.toJSON(options)
}
}
/**

View File

@ -1,5 +1,4 @@
import isPlainObject from 'is-plain-object'
import logger from 'slate-dev-logger'
import { List, Record, Set } from 'immutable'
import MODEL_TYPES, { isType } from '../constants/model-types'
@ -193,12 +192,6 @@ class Leaf extends Record(DEFAULTS) {
return leaf
}
/**
* Alias `fromJS`.
*/
static fromJS = Leaf.fromJSON
/**
* Check if `any` is a `Leaf`.
*
@ -229,14 +222,6 @@ class Leaf extends Record(DEFAULTS) {
return 'leaf'
}
get kind() {
logger.deprecate(
'slate@0.32.0',
'The `kind` property of Slate objects has been renamed to `object`.'
)
return this.object
}
/**
* Update a `mark` at leaf, replace with newMark
*
@ -256,7 +241,19 @@ class Leaf extends Record(DEFAULTS) {
}
/**
* Add a `set` of marks at `index` and `length`.
* Add a `mark` to the leaf.
*
* @param {Mark} mark
* @returns {Text}
*/
addMark(mark) {
const { marks } = this
return this.set('marks', marks.add(mark))
}
/**
* Add a `set` of marks to the leaf.
*
* @param {Set<Mark>} set
* @returns {Text}
@ -268,7 +265,7 @@ class Leaf extends Record(DEFAULTS) {
}
/**
* Remove a `mark` at `index` and `length`.
* Remove a `mark` from the leaf.
*
* @param {Mark} mark
* @returns {Text}
@ -294,14 +291,6 @@ class Leaf extends Record(DEFAULTS) {
return object
}
/**
* Alias `toJS`.
*/
toJS() {
return this.toJSON()
}
}
/**

View File

@ -1,10 +1,8 @@
import isPlainObject from 'is-plain-object'
import logger from 'slate-dev-logger'
import { Map, Record, Set } from 'immutable'
import MODEL_TYPES, { isType } from '../constants/model-types'
import Data from './data'
import memoize from '../utils/memoize'
/**
* Default properties.
@ -124,12 +122,6 @@ class Mark extends Record(DEFAULTS) {
return mark
}
/**
* Alias `fromJS`.
*/
static fromJS = Mark.fromJSON
/**
* Check if `any` is a `Mark`.
*
@ -158,25 +150,6 @@ class Mark extends Record(DEFAULTS) {
return 'mark'
}
get kind() {
logger.deprecate(
'slate@0.32.0',
'The `kind` property of Slate objects has been renamed to `object`.'
)
return this.object
}
/**
* Get the component for the node from a `schema`.
*
* @param {Schema} schema
* @return {Component|Void}
*/
getComponent(schema) {
return schema.__getComponent(this)
}
/**
* Return a JSON representation of the mark.
*
@ -192,14 +165,6 @@ class Mark extends Record(DEFAULTS) {
return object
}
/**
* Alias `toJS`.
*/
toJS() {
return this.toJSON()
}
}
/**
@ -208,12 +173,6 @@ class Mark extends Record(DEFAULTS) {
Mark.prototype[MODEL_TYPES.MARK] = true
/**
* Memoize read methods.
*/
memoize(Mark.prototype, ['getComponent'])
/**
* Export.
*

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,11 @@
import isPlainObject from 'is-plain-object'
import logger from 'slate-dev-logger'
import { List, Record } from 'immutable'
import MODEL_TYPES from '../constants/model-types'
import Mark from './mark'
import Node from './node'
import PathUtils from '../utils/path-utils'
import Range from './range'
import Selection from './selection'
import Value from './value'
/**
@ -155,7 +154,7 @@ class Operation extends Record(DEFAULTS) {
}
if (key === 'selection') {
v = Range.create(v)
v = Selection.create(v)
}
if (key === 'value') {
@ -175,7 +174,7 @@ class Operation extends Record(DEFAULTS) {
}
if (key === 'properties' && type === 'set_selection') {
v = Range.createProperties(v)
v = Selection.createProperties(v)
}
if (key === 'properties' && type === 'set_value') {
@ -193,12 +192,6 @@ class Operation extends Record(DEFAULTS) {
return node
}
/**
* Alias `fromJS`.
*/
static fromJS = Operation.fromJSON
/**
* Check if `any` is a `Operation`.
*
@ -231,14 +224,6 @@ class Operation extends Record(DEFAULTS) {
return 'operation'
}
get kind() {
logger.deprecate(
'slate@0.32.0',
'The `kind` property of Slate objects has been renamed to `object`.'
)
return this.object
}
/**
* Return a JSON representation of the operation.
*
@ -316,14 +301,6 @@ class Operation extends Record(DEFAULTS) {
return json
}
/**
* Alias `toJS`.
*/
toJS(options) {
return this.toJSON(options)
}
}
/**

View File

@ -101,12 +101,6 @@ class Point extends Record(DEFAULTS) {
return point
}
/**
* Alias `fromJS`.
*/
static fromJS = Point.fromJSON
/**
* Check if an `obj` is a `Point`.
*
@ -403,11 +397,17 @@ class Point extends Record(DEFAULTS) {
}
/**
* Alias `toJS`.
* Unset the point.
*
* @return {Point}
*/
toJS() {
return this.toJSON()
unset() {
return this.merge({
key: null,
offset: null,
path: null,
})
}
}

File diff suppressed because it is too large Load Diff

View File

@ -218,7 +218,7 @@ class Schema extends Record(DEFAULTS) {
for (const plugin of plugins) {
const { schema = {} } = plugin
const { blocks = {}, inlines = {} } = schema
const { blocks = {}, inlines = {}, marks = {} } = schema
if (schema.rules) {
rules = rules.concat(schema.rules)
@ -244,6 +244,13 @@ class Schema extends Record(DEFAULTS) {
...inlines[key],
})
}
for (const key in marks) {
rules.push({
match: [{ object: 'mark', type: key }],
...marks[key],
})
}
}
const stack = Stack.create({ plugins })
@ -251,12 +258,6 @@ class Schema extends Record(DEFAULTS) {
return ret
}
/**
* Alias `fromJS`.
*/
static fromJS = Schema.fromJSON
/**
* Check if `any` is a `Schema`.
*
@ -278,14 +279,6 @@ class Schema extends Record(DEFAULTS) {
return 'schema'
}
get kind() {
logger.deprecate(
'slate@0.32.0',
'The `kind` property of Slate objects has been renamed to `object`.'
)
return this.object
}
/**
* Validate a `node` with the schema, returning an error if it's invalid.
*
@ -359,6 +352,21 @@ class Schema extends Record(DEFAULTS) {
}
}
/**
* Check if a mark is void.
*
* @param {Mark}
* @return {Boolean}
*/
isAtomic(mark) {
const rule = this.rules.find(
r => 'isAtomic' in r && testRules(mark, r.match)
)
return rule ? rule.isAtomic : false
}
/**
* Check if a node is void.
*
@ -386,14 +394,6 @@ class Schema extends Record(DEFAULTS) {
return object
}
/**
* Alias `toJS`.
*/
toJS() {
return this.toJSON()
}
}
/**
@ -465,28 +465,28 @@ function defaultNormalize(change, error) {
}
/**
* Check that a `node` matches one of a set of `rules`.
* Check that an `object` matches one of a set of `rules`.
*
* @param {Node} node
* @param {Mixed} object
* @param {Object|Array} rules
* @return {Boolean}
*/
function testRules(node, rules) {
const error = validateRules(node, rules)
function testRules(object, rules) {
const error = validateRules(object, rules)
return !error
}
/**
* Validate that a `node` matches a `rule` object or array.
* Validate that a `object` matches a `rule` object or array.
*
* @param {Node} node
* @param {Mixed} object
* @param {Object|Array} rule
* @param {Array|Void} rules
* @return {Error|Void}
*/
function validateRules(node, rule, rules, options = {}) {
function validateRules(object, rule, rules, options = {}) {
const { every = false } = options
if (Array.isArray(rule)) {
@ -494,7 +494,7 @@ function validateRules(node, rule, rules, options = {}) {
let first
for (const r of array) {
const error = validateRules(node, r, rules)
const error = validateRules(object, r, rules)
first = first || error
if (every && error) return error
if (!every && !error) return
@ -504,15 +504,15 @@ function validateRules(node, rule, rules, options = {}) {
}
const error =
validateObject(node, rule) ||
validateType(node, rule) ||
validateIsVoid(node, rule) ||
validateData(node, rule) ||
validateMarks(node, rule) ||
validateText(node, rule) ||
validateFirst(node, rule) ||
validateLast(node, rule) ||
validateNodes(node, rule, rules)
validateObject(object, rule) ||
validateType(object, rule) ||
validateIsVoid(object, rule) ||
validateData(object, rule) ||
validateMarks(object, rule) ||
validateText(object, rule) ||
validateFirst(object, rule) ||
validateLast(object, rule) ||
validateNodes(object, rule, rules)
return error
}

View File

@ -0,0 +1,256 @@
import isPlainObject from 'is-plain-object'
import logger from 'slate-dev-logger'
import { Record, Set } from 'immutable'
import MODEL_TYPES from '../constants/model-types'
import Mark from './mark'
import Point from './point'
import Range from './range'
/**
* Default properties.
*
* @type {Object}
*/
const DEFAULTS = {
anchor: Point.create(),
focus: Point.create(),
isFocused: false,
marks: null,
}
/**
* Selection.
*
* @type {Selection}
*/
class Selection extends Record(DEFAULTS) {
/**
* Create a new `Selection` with `attrs`.
*
* @param {Object|Selection} attrs
* @return {Selection}
*/
static create(attrs = {}) {
if (Selection.isSelection(attrs)) {
return attrs
}
if (Range.isRange(attrs)) {
return Selection.fromJSON(Range.createProperties(attrs))
}
if (isPlainObject(attrs)) {
return Selection.fromJSON(attrs)
}
throw new Error(
`\`Selection.create\` only accepts objects, ranges or selections, but you passed it: ${attrs}`
)
}
/**
* Create a dictionary of settable selection properties from `attrs`.
*
* @param {Object|String|Selection} attrs
* @return {Object}
*/
static createProperties(a = {}) {
if (Selection.isSelection(a)) {
return {
anchor: Point.createProperties(a.anchor),
focus: Point.createProperties(a.focus),
isFocused: a.isFocused,
marks: a.marks,
}
}
if (Range.isRange(a)) {
return {
anchor: Point.createProperties(a.anchor),
focus: Point.createProperties(a.focus),
}
}
if (isPlainObject(a)) {
const p = {}
if ('anchor' in a) p.anchor = Point.create(a.anchor)
if ('focus' in a) p.focus = Point.create(a.focus)
if ('isFocused' in a) p.isFocused = a.isFocused
if ('marks' in a)
p.marks = a.marks == null ? null : Mark.createSet(a.marks)
return p
}
throw new Error(
`\`Selection.createProperties\` only accepts objects, ranges or selections, but you passed it: ${a}`
)
}
/**
* Create a `Selection` from a JSON `object`.
*
* @param {Object} object
* @return {Selection}
*/
static fromJSON(object) {
let { anchor, focus, isFocused = false, marks = null } = object
if (
!anchor &&
(object.anchorKey || object.anchorOffset || object.anchorPath)
) {
logger.deprecate(
'0.37.0',
'`Selection` objects now take a `Point` object as an `anchor` instead of taking `anchorKey/Offset/Path` properties. But you passed:',
object
)
anchor = {
key: object.anchorKey,
offset: object.anchorOffset,
path: object.anchorPath,
}
}
if (!focus && (object.focusKey || object.focusOffset || object.focusPath)) {
logger.deprecate(
'0.37.0',
'`Selection` objects now take a `Point` object as a `focus` instead of taking `focusKey/Offset/Path` properties. But you passed:',
object
)
focus = {
key: object.focusKey,
offset: object.focusOffset,
path: object.focusPath,
}
}
const selection = new Selection({
anchor: Point.fromJSON(anchor || {}),
focus: Point.fromJSON(focus || {}),
isFocused,
marks: marks == null ? null : new Set(marks.map(Mark.fromJSON)),
})
return selection
}
/**
* Check if an `obj` is a `Selection`.
*
* @param {Any} obj
* @return {Boolean}
*/
static isSelection(obj) {
return !!(obj && obj[MODEL_TYPES.SELECTION])
}
/**
* Object.
*
* @return {String}
*/
get object() {
return 'selection'
}
/**
* Check whether the selection is blurred.
*
* @return {Boolean}
*/
get isBlurred() {
return !this.isFocused
}
/**
* Set the `isFocused` property to a new `value`.
*
* @param {Boolean} value
* @return {Selection}
*/
setIsFocused(value) {
const selection = this.set('isFocused', value)
return selection
}
/**
* Set the `marks` property to a new set of `marks`.
*
* @param {Set} marks
* @return {Selection}
*/
setMarks(marks) {
const selection = this.set('marks', marks)
return selection
}
/**
* Set new `properties` on the selection.
*
* @param {Object|Range|Selection} properties
* @return {Range}
*/
setProperties(properties) {
properties = Selection.createProperties(properties)
const { anchor, focus, ...props } = properties
if (anchor) {
props.anchor = Point.create(anchor)
}
if (focus) {
props.focus = Point.create(focus)
}
const selection = this.merge(props)
return selection
}
/**
* Return a JSON representation of the selection.
*
* @param {Object} options
* @return {Object}
*/
toJSON(options = {}) {
const object = {
object: this.object,
anchor: this.anchor.toJSON(options),
focus: this.focus.toJSON(options),
isFocused: this.isFocused,
marks:
this.marks == null ? null : this.marks.toArray().map(m => m.toJSON()),
}
return object
}
}
/**
* Attach a pseudo-symbol for type checking.
*/
Selection.prototype[MODEL_TYPES.SELECTION] = true
/**
* Export.
*
* @type {Selection}
*/
export default Selection

View File

@ -1,4 +1,3 @@
import logger from 'slate-dev-logger'
import { Record } from 'immutable'
import MODEL_TYPES from '../constants/model-types'
@ -54,14 +53,6 @@ class Stack extends Record(DEFAULTS) {
return 'stack'
}
get kind() {
logger.deprecate(
'slate@0.32.0',
'The `kind` property of Slate objects has been renamed to `object`.'
)
return this.object
}
/**
* Get all plugins with `property`.
*

View File

@ -117,12 +117,6 @@ class Text extends Record(DEFAULTS) {
return node
}
/**
* Alias `fromJS`.
*/
static fromJS = Text.fromJSON
/**
* Check if `any` is a `Text`.
*
@ -165,14 +159,6 @@ class Text extends Record(DEFAULTS) {
return 'text'
}
get kind() {
logger.deprecate(
'slate@0.32.0',
'The `kind` property of Slate objects has been renamed to `object`.'
)
return this.object
}
/**
* Is the node empty?
*
@ -289,17 +275,6 @@ class Text extends Record(DEFAULTS) {
return this.setLeaves(leaves)
}
/**
* Get the decorations for the node from a `schema`.
*
* @param {Schema} schema
* @return {Array}
*/
getDecorations(schema) {
return schema.__getDecorations(this)
}
/**
* Derive the leaves for a list of `decorations`.
*
@ -314,8 +289,8 @@ class Text extends Record(DEFAULTS) {
if (this.text.length === 0) return leaves
const { key } = this
decorations.forEach(range => {
const { start, end, marks } = range
decorations.forEach(dec => {
const { start, end, mark } = dec
const hasStart = start.key == key
const hasEnd = end.key == key
@ -329,12 +304,12 @@ class Text extends Record(DEFAULTS) {
if (index !== 0 || length < this.text.length) {
const [before, bundle] = Leaf.splitLeaves(leaves, index)
const [middle, after] = Leaf.splitLeaves(bundle, length)
leaves = before.concat(middle.map(x => x.addMarks(marks)), after)
leaves = before.concat(middle.map(x => x.addMark(mark)), after)
return
}
}
leaves = leaves.map(x => x.addMarks(marks))
leaves = leaves.map(x => x.addMark(mark))
})
if (leaves === this.leaves) return leaves
@ -674,14 +649,6 @@ class Text extends Record(DEFAULTS) {
return object
}
/**
* Alias `toJS`.
*/
toJS(options) {
return this.toJSON(options)
}
/**
* Update a `mark` at `index` and `length` with `properties`.
*
@ -811,7 +778,6 @@ Text.prototype[MODEL_TYPES.TEXT] = true
*/
memoize(Text.prototype, [
'getDecorations',
'getActiveMarks',
'getMarks',
'getMarksAsArray',

View File

@ -6,9 +6,10 @@ import MODEL_TYPES from '../constants/model-types'
import PathUtils from '../utils/path-utils'
import Change from './change'
import Data from './data'
import Decoration from './decoration'
import Document from './document'
import History from './history'
import Range from './range'
import Selection from './selection'
import Schema from './schema'
/**
@ -18,12 +19,12 @@ import Schema from './schema'
*/
const DEFAULTS = {
data: new Map(),
decorations: null,
data: Map(),
decorations: List(),
document: Document.create(),
history: History.create(),
schema: Schema.create(),
selection: Range.create(),
selection: Selection.create(),
}
/**
@ -74,7 +75,8 @@ class Value extends Record(DEFAULTS) {
if (isPlainObject(a)) {
const p = {}
if ('data' in a) p.data = Data.create(a.data)
if ('decorations' in a) p.decorations = Range.createList(a.decorations)
if ('decorations' in a)
p.decorations = Decoration.createList(a.decorations)
if ('schema' in a) p.schema = Schema.create(a.schema)
return p
}
@ -98,7 +100,7 @@ class Value extends Record(DEFAULTS) {
let { document = {}, selection = {}, schema = {}, history = {} } = object
let data = new Map()
document = Document.fromJSON(document)
selection = Range.fromJSON(selection)
selection = Selection.fromJSON(selection)
schema = Schema.fromJSON(schema)
history = History.fromJSON(history)
@ -114,15 +116,15 @@ class Value extends Record(DEFAULTS) {
data = data.merge(object.data)
}
selection = document.createRange(selection)
selection = document.createSelection(selection)
if (selection.isUnset) {
const text = document.getFirstText()
if (text) selection = selection.moveToStartOfNode(text)
selection = document.createRange(selection)
selection = document.createSelection(selection)
}
selection = document.createRange(selection)
selection = document.createSelection(selection)
let value = new Value({
data,
@ -139,12 +141,6 @@ class Value extends Record(DEFAULTS) {
return value
}
/**
* Alias `fromJS`.
*/
static fromJS = Value.fromJSON
/**
* Check if a `value` is a `Value`.
*
@ -166,14 +162,6 @@ class Value extends Record(DEFAULTS) {
return 'value'
}
get kind() {
logger.deprecate(
'slate@0.32.0',
'The `kind` property of Slate objects has been renamed to `object`.'
)
return this.object
}
/**
* Get the current start text node's closest block parent.
*
@ -543,7 +531,7 @@ class Value extends Record(DEFAULTS) {
insertText(path, offset, text, marks) {
let value = this
let { document } = value
let { document, schema } = value
document = document.insertText(path, offset, text, marks)
value = value.set('document', document)
@ -551,7 +539,9 @@ class Value extends Record(DEFAULTS) {
const node = document.assertNode(path)
value = value.mapRanges(range => {
const { anchor, focus, isBackward, isAtomic } = range
const { anchor, focus, isBackward } = range
const isAtomic =
Decoration.isDecoration(range) && schema.isAtomic(range.mark)
if (
anchor.key === node.key &&
@ -682,13 +672,13 @@ class Value extends Record(DEFAULTS) {
if (node.hasNode(start.key)) {
range = prev
? range.moveStartTo(prev.key, prev.text.length)
: next ? range.moveStartTo(next.key, 0) : Range.create()
: next ? range.moveStartTo(next.key, 0) : range.unset()
}
if (node.hasNode(end.key)) {
range = prev
? range.moveEndTo(prev.key, prev.text.length)
: next ? range.moveEndTo(next.key, 0) : Range.create()
: next ? range.moveEndTo(next.key, 0) : range.unset()
}
range = range.updatePoints(point => point.setPath(null))
@ -717,6 +707,7 @@ class Value extends Record(DEFAULTS) {
const node = document.assertNode(path)
const { length } = text
const rangeOffset = offset + length
value = value.clearAtomicRanges(node.key, offset, offset + length)
value = value.mapRanges(range => {
@ -781,6 +772,41 @@ class Value extends Record(DEFAULTS) {
return value
}
/**
* Set `properties` on the value.
*
* @param {Object} properties
* @return {Value}
*/
setProperties(properties) {
let value = this
const { document } = value
const { data, decorations, history, schema } = properties
const props = {}
if (data) {
props.data = data
}
if (history) {
props.history = history
}
if (schema) {
props.schema = schema
}
if (decorations) {
props.decorations = decorations.map(d => {
return d.isSet ? d : document.resolveDecoration(d)
})
}
value = value.merge(props)
return value
}
/**
* Set `properties` on the selection.
*
@ -793,7 +819,7 @@ class Value extends Record(DEFAULTS) {
let value = this
let { document, selection } = value
const next = selection.setProperties(properties)
selection = document.resolveRange(next)
selection = document.resolveSelection(next)
value = value.set('selection', selection)
return value
}
@ -848,25 +874,19 @@ class Value extends Record(DEFAULTS) {
let value = this
const { document, selection, decorations } = value
if (selection) {
let next = selection.isSet ? iterator(selection) : selection
if (!next) next = Range.create()
if (next !== selection) next = document.createRange(next)
value = value.set('selection', next)
}
let sel = selection.isSet ? iterator(selection) : selection
if (!sel) sel = selection.unset()
if (sel !== selection) sel = document.createSelection(sel)
value = value.set('selection', sel)
if (decorations) {
let next = decorations.map(decoration => {
let n = decoration.isSet ? iterator(decoration) : decoration
if (n && n !== decoration) n = document.createRange(n)
return n
})
next = next.filter(decoration => !!decoration)
next = next.size ? next : null
value = value.set('decorations', next)
}
let decs = decorations.map(decoration => {
let n = decoration.isSet ? iterator(decoration) : decoration
if (n && n !== decoration) n = document.createDecoration(n)
return n
})
decs = decs.filter(decoration => !!decoration)
value = value.set('decorations', decs)
return value
}
@ -880,8 +900,13 @@ class Value extends Record(DEFAULTS) {
*/
clearAtomicRanges(key, from, to = null) {
return this.mapRanges(range => {
const { isAtomic, start, end } = range
let value = this
const { schema } = value
value = this.mapRanges(range => {
if (!Decoration.isDecoration(range)) return range
const { start, end, mark } = range
const isAtomic = schema.isAtomic(mark)
if (!isAtomic) return range
if (start.key !== key) return range
@ -899,6 +924,8 @@ class Value extends Record(DEFAULTS) {
return range
})
return value
}
/**
@ -920,8 +947,8 @@ class Value extends Record(DEFAULTS) {
if (options.preserveDecorations) {
object.decorations = this.decorations
? this.decorations.toArray().map(d => d.toJSON(options))
: null
.toArray()
.map(d => d.toJSON(options))
}
if (options.preserveHistory) {
@ -939,14 +966,6 @@ class Value extends Record(DEFAULTS) {
return object
}
/**
* Alias `toJS`.
*/
toJS(options) {
return this.toJSON(options)
}
/**
* Deprecated.
*/

View File

@ -92,7 +92,7 @@ function applyOperation(value, op) {
case 'set_value': {
const { properties } = op
const next = value.merge(properties)
const next = value.setProperties(properties)
return next
}

View File

@ -0,0 +1,22 @@
/**
* Mix in an `Interface` to a `Class`.
*
* @param {Class} Class
* @param {Class} Interface
*/
export default function mixin(Interface, Classes) {
for (const Class of Classes) {
for (const name of Object.getOwnPropertyNames(Interface)) {
if (Class.hasOwnProperty(name)) continue
const desc = Object.getOwnPropertyDescriptor(Interface, name)
Object.defineProperty(Class, name, desc)
}
for (const name of Object.getOwnPropertyNames(Interface.prototype)) {
if (Class.prototype.hasOwnProperty(name)) continue
const desc = Object.getOwnPropertyDescriptor(Interface.prototype, name)
Object.defineProperty(Class.prototype, name, desc)
}
}
}

View File

@ -19,11 +19,6 @@ export const input = (
export const output = (
<value>
<document />
<selection
anchorKey={null}
anchorOffset={0}
focusKey={null}
focusOffset={0}
/>
<selection focused />
</value>
)

View File

@ -19,11 +19,6 @@ export const input = (
export const output = (
<value>
<document />
<selection
anchorKey={null}
anchorOffset={0}
focusKey={null}
focusOffset={0}
/>
<selection focused />
</value>
)

View File

@ -19,11 +19,6 @@ export const input = (
export const output = (
<value>
<document />
<selection
anchorKey={null}
anchorOffset={0}
focusKey={null}
focusOffset={0}
/>
<selection focused />
</value>
)

View File

@ -29,8 +29,16 @@ const h = createHyperscript({
fontSize: 'font-size',
},
decorations: {
result: 'result',
highlight: 'highlight',
},
schema: {
marks: {
result: {
isAtomic: true,
},
},
},
})
export default h

View File

@ -16,7 +16,7 @@ export const input = (
<value>
<document>
<paragraph>
wor<highlight atomic>d</highlight>
wor<result>d</result>
</paragraph>
</document>
</value>
@ -26,7 +26,7 @@ export const output = (
<value>
<document>
<paragraph>
w<highlight atomic>d</highlight>
w<result>d</result>
</paragraph>
</document>
</value>

View File

@ -16,7 +16,7 @@ export const input = (
<value>
<document>
<paragraph>
<highlight atomic>w</highlight>ord
<result>w</result>ord
</paragraph>
</document>
</value>
@ -26,7 +26,7 @@ export const output = (
<value>
<document>
<paragraph>
<highlight atomic>w</highlight>d
<result>w</result>d
</paragraph>
</document>
</value>

View File

@ -16,7 +16,7 @@ export const input = (
<value>
<document>
<paragraph>
w<highlight atomic>or</highlight>d
w<result>or</result>d
</paragraph>
</document>
</value>

View File

@ -42,7 +42,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
key: '0',
@ -57,7 +57,6 @@ export const output = {
},
isFocused: false,
marks: null,
isAtomic: false,
},
}

View File

@ -37,7 +37,7 @@ export const output = {
],
},
selection: {
object: 'range',
object: 'selection',
anchor: {
object: 'point',
path: [0, 0],
@ -50,7 +50,6 @@ export const output = {
},
isFocused: false,
marks: null,
isAtomic: false,
},
}