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

add draggable nodes, first steps

This commit is contained in:
Ian Storm Taylor
2016-07-24 17:52:22 -07:00
parent c7fac3456e
commit 3bd000d118
78 changed files with 1405 additions and 143 deletions

View File

@@ -1,11 +1,10 @@
import { Editor, Mark, Raw, Void } from '../..'
import { Editor, Draggable, Raw, Void } from '../..'
import React from 'react'
import ReactDOM from 'react-dom'
import initialState from './state.json'
import isImage from 'is-image'
import isUrl from 'is-url'
import { Map } from 'immutable'
/**
* Define a set of node renderers.
@@ -18,9 +17,11 @@ const NODES = {
const { node, state } = props
const src = node.data.get('src')
return (
<Void {...props} className="image-block">
<img {...props.attributes} src={src} />
</Void>
<Draggable {...props}>
<Void {...props} className="image-block">
<img {...props.attributes} src={src} />
</Void>
</Draggable>
)
}
}

View File

@@ -138,6 +138,6 @@ td {
outline: none;
}
.image-block:focus > * > img {
.image-block:focus img {
box-shadow: 0 0 0 2px blue;
}

View File

@@ -1,11 +1,12 @@
import Fragment from '../utils/fragment'
import Base64 from '../serializers/base-64'
import Key from '../utils/key'
import OffsetKey from '../utils/offset-key'
import Raw from '../serializers/raw'
import React from 'react'
import Selection from '../models/selection'
import Text from './text'
import TYPES from '../utils/types'
import includes from 'lodash/includes'
import keycode from 'keycode'
import { IS_FIREFOX } from '../utils/environment'
@@ -18,18 +19,6 @@ import { IS_FIREFOX } from '../utils/environment'
function noop() {}
/**
* Content types.
*
* @type {Object}
*/
const TYPES = {
HTML: 'text/html',
SLATE: 'application/x-slate',
TEXT: 'text/plain'
}
/**
* Content.
*
@@ -235,7 +224,7 @@ class Content extends React.Component {
const { state } = this.props
const { fragment } = state
const encoded = Fragment.serialize(fragment)
const encoded = Base64.serializeNode(fragment)
// Wrap the first character of the selection in a span that has the encoded
// fragment attached as an attribute, so it will show up in the copied HTML.
@@ -286,6 +275,7 @@ class Content extends React.Component {
onDragEnd = (e) => {
this.tmp.isDragging = false
this.tmp.isInternalDrag = null
}
/**
@@ -295,6 +285,15 @@ class Content extends React.Component {
*/
onDragOver = (e) => {
const data = e.nativeEvent.dataTransfer
// COMPAT: In Firefox, `types` is array-like. (2016/06/21)
const types = Array.from(data.types)
// Prevent default when nodes are dragged to allow dropping.
if (includes(types, TYPES.NODE)) {
e.preventDefault()
}
if (this.tmp.isDragging) return
this.tmp.isDragging = true
this.tmp.isInternalDrag = false
@@ -309,11 +308,17 @@ class Content extends React.Component {
onDragStart = (e) => {
this.tmp.isDragging = true
this.tmp.isInternalDrag = true
const data = e.nativeEvent.dataTransfer
// COMPAT: In Firefox, `types` is array-like. (2016/06/21)
const types = Array.from(data.types)
// If it's a node being dragged, the data type is already set.
if (includes(types, TYPES.NODE)) return
const { state } = this.props
const { fragment } = state
const encoded = Fragment.serialize(fragment)
const data = e.nativeEvent.dataTransfer
data.setData(TYPES.SLATE, encoded)
const encoded = Base64.serializeNode(fragment)
data.setData(TYPES.FRAGMENT, encoded)
}
/**
@@ -331,13 +336,16 @@ class Content extends React.Component {
const data = e.nativeEvent.dataTransfer
const drop = {}
// COMPAT: In Firefox, `types` is array-like. (2016/06/21)
const types = Array.from(data.types)
// Resolve the point where the drop occured.
const { x, y } = e.nativeEvent
const range = window.document.caretRangeFromPoint(x, y)
const startNode = range.startContainer
const startOffset = range.startOffset
const point = OffsetKey.findPoint(startNode, startOffset, state)
let target = Selection.create({
const target = Selection.create({
anchorKey: point.key,
anchorOffset: point.offset,
focusKey: point.key,
@@ -348,41 +356,22 @@ class Content extends React.Component {
// If the target is inside a void node, abort.
if (state.document.hasVoidParent(point.key)) return
// If the drag is internal, handle it now. And it the target is after the
// selection, it needs to account for the selection's content being deleted.
if (this.tmp.isInternalDrag) {
if (
selection.endKey == target.endKey &&
selection.endOffset < target.endOffset
) {
const width = selection.startKey == selection.endKey
? selection.endOffset - selection.startOffset
: selection.endOffset
target = target.moveBackward(width)
}
const fragment = state.fragment
const next = state
.transform()
.delete()
.moveTo(target)
.insertFragment(fragment)
.apply()
this.onChange(next)
return
}
// COMPAT: In Firefox, `types` is array-like. (2016/06/21)
const types = Array.from(data.types)
// Handle external Slate drags.
if (includes(types, TYPES.SLATE)) {
const encoded = data.getData(TYPES.SLATE)
const fragment = Fragment.deserialize(encoded)
// Handle Slate fragments.
if (includes(types, TYPES.FRAGMENT)) {
const encoded = data.getData(TYPES.FRAGMENT)
const fragment = Base64.deserializeDocument(encoded)
drop.type = 'fragment'
drop.fragment = fragment
drop.isInternal = this.tmp.isInternalDrag
}
// Handle Slate nodes.
else if (includes(types, TYPES.NODE)) {
const encoded = data.getData(TYPES.NODE)
const node = Base64.deserializeNode(encoded)
drop.type = 'node'
drop.node = node
drop.isInternal = this.tmp.isInternalDrag
}
// Handle files.
@@ -529,7 +518,7 @@ class Content extends React.Component {
const regexp = /data-fragment="([^\s]+)"/
const matches = regexp.exec(paste.html)
const [ full, encoded ] = matches
const fragment = Fragment.deserialize(encoded)
const fragment = Base64.deserializeDocument(encoded)
let { state } = this.props
state = state

View File

@@ -0,0 +1,63 @@
import Base64 from '../serializers/base-64'
import React from 'react'
import TYPES from '../utils/types'
/**
* Draggable.
*
* @type {Component}
*/
class Draggable extends React.Component {
static propTypes = {
children: React.PropTypes.any.isRequired,
className: React.PropTypes.string,
editor: React.PropTypes.object.isRequired,
node: React.PropTypes.object.isRequired,
state: React.PropTypes.object.isRequired,
style: React.PropTypes.object
};
static defaultProps = {
style: {}
}
shouldComponentUpdate = (props) => {
return (
props.node != this.props.node ||
props.state.selection.hasEdgeIn(props.node) ||
this.props.state.selection.hasEdgeIn(this.props.node)
)
}
onDragStart = (e) => {
const { node } = this.props
const encoded = Base64.serializeNode(node)
const data = e.nativeEvent.dataTransfer
data.setData(TYPES.NODE, encoded)
}
render = () => {
const { children, node, className, style } = this.props
const Tag = node.kind == 'block' ? 'div' : 'span'
return (
<Tag
draggable
onDragStart={this.onDragStart}
className={className}
style={style}
>
{children}
</Tag>
)
}
}
/**
* Export.
*/
export default Draggable

View File

@@ -3,6 +3,7 @@
* Components.
*/
import Draggable from './components/draggable'
import Editor from './components/editor'
import Placeholder from './components/placeholder'
import Void from './components/void'
@@ -50,6 +51,7 @@ export {
Character,
Data,
Document,
Draggable,
Editor,
Html,
Inline,
@@ -69,6 +71,7 @@ export default {
Character,
Data,
Document,
Draggable,
Editor,
Html,
Inline,

View File

@@ -78,6 +78,20 @@ class Block extends new Record(DEFAULTS) {
return 'block'
}
/**
* Is the node empty?
*
* @return {Boolean} isEmpty
*/
get isEmpty() {
return (
this.nodes.size == 1 &&
this.nodes.first().kind == 'text' &&
this.length == 0
)
}
/**
* Get the length of the concatenated text of the node.
*

View File

@@ -78,6 +78,20 @@ class Inline extends new Record(DEFAULTS) {
return 'inline'
}
/**
* Is the node empty?
*
* @return {Boolean} isEmpty
*/
get isEmpty() {
return (
this.nodes.size == 1 &&
this.nodes.first().kind == 'text' &&
this.length == 0
)
}
/**
* Get the length of the concatenated text of the node.
*

View File

@@ -654,6 +654,31 @@ class State extends new Record(DEFAULTS) {
return state
}
/**
* Insert a `block` at the current selection.
*
* @param {String || Object || Block} block
* @return {State} state
*/
insertBlock(block) {
let state = this
let { document, selection } = state
let after = selection
// Insert the block
document = document.insertBlockAtRange(selection, block)
// Determine what the selection should be after inserting.
const keys = state.document.getTexts().map(text => text.key)
const text = document.getTexts().find(n => !keys.includes(n.key))
selection = selection.collapseToEndOf(text)
// Update the document and selection.
state = state.merge({ document, selection })
return state
}
/**
* Insert a `fragment` at the current selection.
*

View File

@@ -70,6 +70,16 @@ class Text extends new Record(DEFAULTS) {
return 'text'
}
/**
* Is the node empty?
*
* @return {Boolean} isEmpty
*/
get isEmpty() {
return this.length == 0
}
/**
* Get the length of the concatenated text of the node.
*

View File

@@ -30,7 +30,9 @@ const DOCUMENT_RANGE_TRANSFORMS = [
'deleteAtRange',
'deleteBackwardAtRange',
'deleteForwardAtRange',
'insertBlockAtRange',
'insertFragmentAtRange',
'insertInlineAtRange',
'insertTextAtRange',
'addMarkAtRange',
'setBlockAtRange',
@@ -84,7 +86,9 @@ const STATE_DOCUMENT_TRANSFORMS = [
'delete',
'deleteBackward',
'deleteForward',
'insertBlock',
'insertFragment',
'insertInline',
'insertText',
'addMark',
'setBlock',

View File

@@ -18,6 +18,45 @@ import { List, Map, Set } from 'immutable'
const Transforms = {
/**
* Add a new `mark` to the characters at `range`.
*
* @param {Selection} range
* @param {Mark or String} mark
* @return {Node} node
*/
addMarkAtRange(range, mark) {
mark = normalizeMark(mark)
let node = this
// When the range is collapsed, do nothing.
if (range.isCollapsed) return node
// Otherwise, find each of the text nodes within the range.
const { startKey, startOffset, endKey, endOffset } = range
let texts = node.getTextsAtRange(range)
// Apply the mark to each of the text nodes's matching characters.
texts = texts.map((text) => {
let characters = text.characters.map((char, i) => {
if (!isInRange(i, text, range)) return char
let { marks } = char
marks = marks.add(mark)
return char.merge({ marks })
})
return text.merge({ characters })
})
// Update each of the text nodes.
texts.forEach((text) => {
node = node.updateDescendant(text)
})
return node
},
/**
* Delete everything in a `range`.
*
@@ -176,6 +215,76 @@ const Transforms = {
return node.deleteAtRange(range)
},
/**
* Insert a block `node` at `range`.
*
* @param {Selection} range
* @param {Node} node
* @return {Node} node
*/
insertBlockAtRange(range, node) {
let doc = this
// If expanded, delete the range first.
if (range.isExpanded) {
doc = doc.deleteAtRange(range)
range = range.collapseToStart()
}
// Allow for passing just a type string.
if (typeof node == 'string') node = { type: node }
// Allow for passing a plain object of properties.
node = Block.create(node)
const { startKey, startOffset } = range
let startBlock = doc.getClosestBlock(startKey)
let parent = doc.getParent(startBlock)
let nodes = Block.createList([node])
const isParent = parent == doc
// If the start block is void, insert after it.
if (startBlock.isVoid) {
parent = parent.insertChildrenAfter(startBlock, nodes)
}
// If the block is empty, replace it.
else if (startBlock.isEmpty) {
parent = parent.merge({ nodes })
}
// If the range is at the start of the block, insert before.
else if (range.isAtStartOf(startBlock)) {
nodes = nodes.concat(parent.nodes)
parent = parent.merge({ nodes })
}
// If the range is at the end of the block, insert after.
else if (range.isAtEndOf(startBlock)) {
nodes = parent.nodes.concat(nodes)
parent = parent.merge({ nodes })
}
// Otherwise, split the block and insert between.
else {
doc = doc.splitBlockAtRange(range)
parent = doc.getParent(startBlock)
startBlock = doc.getClosestBlock(startKey)
nodes = parent.nodes.takeUntil(n => n == startBlock)
.push(startBlock)
.push(node)
.concat(parent.nodes.skipUntil(n => n == startBlock).rest())
parent = parent.merge({ nodes })
}
doc = isParent
? parent
: doc.updateDescendant(parent)
return doc.normalize()
},
/**
* Insert a `fragment` at a `range`.
*
@@ -275,6 +384,46 @@ const Transforms = {
return node.normalize()
},
/**
* Insert an inline `node` at `range`.
*
* @param {Selection} range
* @param {Node} node
* @return {Node} node
*/
insertInlineAtRange(range, node) {
let doc = this
// If expanded, delete the range first.
if (range.isExpanded) {
doc = doc.deleteAtRange(range)
range = range.collapseToStart()
}
// Allow for passing a type string.
if (typeof node == 'string') node = { type: node }
// Allow for passing a plain object of properties.
node = Inline.create(node)
// Split the text nodes at the cursor.
doc = doc.splitTextAtRange(range)
// Insert the node between the split text nodes.
const { startKey, endKey, startOffset, endOffset } = range
const startText = doc.getDescendant(startKey)
let parent = doc.getParent(startKey)
const nodes = parent.nodes.takeUntil(n => n == startText)
.push(startText)
.push(node)
.concat(parent.nodes.skipUntil(n => n == startText).rest())
parent = parent.merge({ nodes })
doc = doc.updateDescendant(parent)
return doc.normalize()
},
/**
* Insert text `string` at a `range`, with optional `marks`.
*
@@ -303,14 +452,14 @@ const Transforms = {
},
/**
* Add a new `mark` to the characters at `range`.
* Remove an existing `mark` to the characters at `range`.
*
* @param {Selection} range
* @param {Mark or String} mark
* @param {Mark or String} mark (optional)
* @return {Node} node
*/
addMarkAtRange(range, mark) {
removeMarkAtRange(range, mark) {
mark = normalizeMark(mark)
let node = this
@@ -318,7 +467,6 @@ const Transforms = {
if (range.isCollapsed) return node
// Otherwise, find each of the text nodes within the range.
const { startKey, startOffset, endKey, endOffset } = range
let texts = node.getTextsAtRange(range)
// Apply the mark to each of the text nodes's matching characters.
@@ -326,7 +474,9 @@ const Transforms = {
let characters = text.characters.map((char, i) => {
if (!isInRange(i, text, range)) return char
let { marks } = char
marks = marks.add(mark)
marks = mark
? marks.remove(mark)
: marks.clear()
return char.merge({ marks })
})
@@ -552,46 +702,6 @@ const Transforms = {
return node
},
/**
* Remove an existing `mark` to the characters at `range`.
*
* @param {Selection} range
* @param {Mark or String} mark (optional)
* @return {Node} node
*/
removeMarkAtRange(range, mark) {
mark = normalizeMark(mark)
let node = this
// When the range is collapsed, do nothing.
if (range.isCollapsed) return node
// Otherwise, find each of the text nodes within the range.
let texts = node.getTextsAtRange(range)
// Apply the mark to each of the text nodes's matching characters.
texts = texts.map((text) => {
let characters = text.characters.map((char, i) => {
if (!isInRange(i, text, range)) return char
let { marks } = char
marks = mark
? marks.remove(mark)
: marks.clear()
return char.merge({ marks })
})
return text.merge({ characters })
})
// Update each of the text nodes.
texts.forEach((text) => {
node = node.updateDescendant(text)
})
return node
},
/**
* Add or remove a `mark` from the characters at `range`, depending on whether
* it's already there.

View File

@@ -151,12 +151,39 @@ function Plugin(options = {}) {
onDrop(e, drop, state, editor) {
switch (drop.type) {
case 'fragment': {
const { selection } = state
let { fragment, target, isInternal } = drop
// If the drag is internal and the target is after the selection, it
// needs to account for the selection's content being deleted.
if (
isInternal &&
selection.endKey == target.endKey &&
selection.endOffset < target.endOffset
) {
target = target.moveBackward(selection.startKey == selection.endKey
? selection.endOffset - selection.startOffset
: selection.endOffset)
}
let transform = state.transform()
if (isInternal) transform = transform.delete()
return transform
.moveTo(target)
.insertFragment(fragment)
.apply()
}
case 'node': {
return state
.transform()
.moveTo(drop.target)
.insertFragment(drop.fragment)
[drop.node.kind == 'block' ? 'insertBlock' : 'insertInline'](drop.node)
.apply()
}
case 'text':
case 'html': {
let transform = state

105
lib/serializers/base-64.js Normal file
View File

@@ -0,0 +1,105 @@
import Raw from './raw'
/**
* Encode a JSON `object` as base-64 `string`.
*
* @param {Object} object
* @return {String} encoded
*/
function encode(object) {
const string = JSON.stringify(object)
const encoded = window.btoa(window.unescape(window.encodeURIComponent(string)))
return encoded
}
/**
* Decode a base-64 `string` to a JSON `object`.
*
* @param {String} string
* @return {Object} object
*/
function decode(string) {
const decoded = window.decodeURIComponent(window.escape(window.atob(string)))
const object = JSON.parse(decoded)
return object
}
/**
* Deserialize a State `string`.
*
* @param {String} string
* @return {State} state
*/
function deserialize(string) {
const raw = decode(string)
const state = Raw.deserialize(raw)
return state
}
/**
* Deserialize a Document `string`.
*
* @param {String} string
* @return {Document} document
*/
function deserializeDocument(string) {
const raw = decode(string)
const state = Raw.deserialize(raw)
return state.document
}
/**
* Deserialize a Node `string`.
*
* @param {String} string
* @return {Node} node
*/
function deserializeNode(string) {
const raw = decode(string)
const node = Raw.deserializeNode(raw)
return node
}
/**
* Serialize a `state`.
*
* @param {State} state
* @return {String} encoded
*/
function serialize(state) {
const raw = Raw.serialize(state)
const encoded = encode(raw)
return encoded
}
/**
* Serialize a `node`.
*
* @param {Node} node
* @return {String} encoded
*/
function serializeNode(node) {
const raw = Raw.serializeNode(node)
const encoded = encode(raw)
return encoded
}
/**
* Export.
*/
export default {
deserialize,
deserializeDocument,
deserializeNode,
serialize,
serializeNode
}

View File

@@ -1,39 +0,0 @@
import Raw from '../serializers/raw'
/**
* Serialize a `string` as Base64.
*
* @param {Document} fragment
* @return {String} encoded
*/
function serialize(fragment) {
const raw = Raw.serializeNode(fragment)
const string = JSON.stringify(raw)
const encoded = window.btoa(window.unescape(window.encodeURIComponent(string)))
return encoded
}
/**
* Deserialize a `fragment` as Base64.
*
* @param {String} encoded
* @return {Document} fragment
*/
function deserialize(encoded) {
const string = window.decodeURIComponent(window.escape(window.atob(encoded)))
const json = JSON.parse(string)
const state = Raw.deserialize(json)
return state.document
}
/**
* Export.
*/
export default {
serialize,
deserialize
}

19
lib/utils/types.js Normal file
View File

@@ -0,0 +1,19 @@
/**
* Content types.
*
* @type {Object}
*/
const TYPES = {
FRAGMENT: 'application/x-slate-fragment',
HTML: 'text/html',
NODE: 'application/x-slate-node',
TEXT: 'text/plain'
}
/**
* Export.
*/
export default TYPES

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: first.length,
focusKey: first.key,
focusOffset: first.length
})
return state
.transform()
.insertBlockAtRange(range, 'image')
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,14 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 2,
focusKey: first.key,
focusOffset: 2
})
return state
.transform()
.insertBlockAtRange(range, 'image')
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,20 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: wo
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: rd

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.insertBlockAtRange(range, 'image')
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,14 @@
nodes:
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.insertBlockAtRange(range, 'image')
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.insertBlockAtRange(range, 'image')
.apply()
}

View File

@@ -0,0 +1,9 @@
nodes:
- kind: block
type: image
isVoid: true
nodes:
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,15 @@
nodes:
- kind: block
type: image
isVoid: true
nodes:
- kind: text
ranges:
- text: ""
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,19 @@
import { Block } from '../../../../..'
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.insertBlockAtRange(range, Block.create({ type: 'image' }))
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,14 @@
nodes:
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.insertBlockAtRange(range, { type: 'image' })
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,14 @@
nodes:
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,29 @@
import assert from 'assert'
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: first.length,
focusKey: first.key,
focusOffset: first.length
})
const next = state
.transform()
.moveTo(range)
.insertBlock('image')
.apply()
const updated = next.document.getTexts().last()
assert.deepEqual(
next.selection.toJS(),
range.collapseToStartOf(updated).toJS()
)
return next
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,14 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,29 @@
import assert from 'assert'
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 2,
focusKey: first.key,
focusOffset: 2
})
const next = state
.transform()
.moveTo(range)
.insertBlock('image')
.apply()
const updated = next.document.getTexts().get(1)
assert.deepEqual(
next.selection.toJS(),
range.collapseToStartOf(updated).toJS()
)
return next
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,20 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: wo
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: rd

View File

@@ -0,0 +1,29 @@
import assert from 'assert'
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
const next = state
.transform()
.moveTo(range)
.insertBlock('image')
.apply()
const updated = next.document.getTexts().first()
assert.deepEqual(
next.selection.toJS(),
range.collapseToStartOf(updated).toJS()
)
return next
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,14 @@
nodes:
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,29 @@
import assert from 'assert'
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
const next = state
.transform()
.moveTo(range)
.insertBlock('image')
.apply()
const updated = next.document.getTexts().first()
assert.deepEqual(
next.selection.toJS(),
range.collapseToStartOf(updated).toJS()
)
return next
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,29 @@
import assert from 'assert'
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
const next = state
.transform()
.moveTo(range)
.insertBlock('image')
.apply()
const updated = next.document.getTexts().last()
assert.deepEqual(
next.selection.toJS(),
range.collapseToStartOf(updated).toJS()
)
return next
}

View File

@@ -0,0 +1,9 @@
nodes:
- kind: block
type: image
isVoid: true
nodes:
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,15 @@
nodes:
- kind: block
type: image
isVoid: true
nodes:
- kind: text
ranges:
- text: ""
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,30 @@
import { Block } from '../../../../..'
import assert from 'assert'
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
const next = state
.transform()
.moveTo(range)
.insertBlock(Block.create({ type: 'image' }))
.apply()
const updated = next.document.getTexts().first()
assert.deepEqual(
next.selection.toJS(),
range.collapseToStartOf(updated).toJS()
)
return next
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,14 @@
nodes:
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,29 @@
import assert from 'assert'
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
const next = state
.transform()
.moveTo(range)
.insertBlock({ type: 'image' })
.apply()
const updated = next.document.getTexts().first()
assert.deepEqual(
next.selection.toJS(),
range.collapseToStartOf(updated).toJS()
)
return next
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,14 @@
nodes:
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: first.length,
focusKey: first.key,
focusOffset: first.length
})
return state
.transform()
.insertInlineAtRange(range, 'hashtag')
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,17 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word
- kind: inline
type: hashtag
nodes:
- kind: text
ranges:
- text: ""
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 2,
focusKey: first.key,
focusOffset: 2
})
return state
.transform()
.insertBlockAtRange(range, 'image')
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,20 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: wo
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: rd

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.insertBlockAtRange(range, 'image')
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,14 @@
nodes:
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.insertBlockAtRange(range, 'image')
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.insertBlockAtRange(range, 'image')
.apply()
}

View File

@@ -0,0 +1,9 @@
nodes:
- kind: block
type: image
isVoid: true
nodes:
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,15 @@
nodes:
- kind: block
type: image
isVoid: true
nodes:
- kind: text
ranges:
- text: ""
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,19 @@
import { Block } from '../../../../..'
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.insertBlockAtRange(range, Block.create({ type: 'image' }))
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,14 @@
nodes:
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTexts()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.insertBlockAtRange(range, { type: 'image' })
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,14 @@
nodes:
- kind: block
type: image
nodes:
- kind: text
ranges:
- text: ""
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word