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

broke lots of stuff, but added tests

This commit is contained in:
Ian Storm Taylor
2016-06-22 18:42:49 -07:00
parent b42fd1849c
commit 74cab690e3
241 changed files with 4150 additions and 324 deletions

View File

@@ -31,6 +31,10 @@ dist: $(shell find ./lib)
example-auto-markdown:
@ $(browserify) --debug --transform babelify --outfile ./examples/auto-markdown/build.js ./examples/auto-markdown/index.js
# Build the links example.
example-links:
@ $(browserify) --debug --transform babelify --outfile ./examples/links/build.js ./examples/links/index.js
# Build the plain-text example.
example-plain-text:
@ $(browserify) --debug --transform babelify --outfile ./examples/plain-text/build.js ./examples/plain-text/index.js
@@ -52,24 +56,28 @@ lint:
@ $(standard) ./lib
# Build the test source.
test/support/build.js: $(shell find ./lib) ./test/browser.js
@ $(browserify) --debug --transform babelify --outfile ./test/support/build.js ./test/browser.js
test/browser/support/build.js: $(shell find ./lib) ./test/browser/index.js
@ $(browserify) --debug --transform babelify --outfile ./test/browser/support/build.js ./test/browser/index.js
# Run the tests.
test: test-browser test-server
# Run the browser-side tests.
test-browser: ./test/support/build.js
@ $(mocha-phantomjs) --reporter spec --timeout 5000 ./test/support/browser.html
test-browser: ./test/browser/support/build.js
@ $(mocha-phantomjs) --reporter spec --timeout 5000 ./test/browser/support/browser.html
# Run the server-side tests.
test-server:
@ $(mocha) --reporter spec --timeout 5000 ./test/server.js
@ $(mocha) --compilers js:babel-core/register --reporter spec --timeout 5000 ./test/server
# Watch the auto-markdown example.
watch-example-auto-markdown:
@ $(MAKE) example-auto-markdown browserify=$(watchify)
# Watch the links example.
watch-example-links:
@ $(MAKE) example-links browserify=$(watchify)
# Watch the plain-text example.
watch-example-plain-text:
@ $(MAKE) example-plain-text browserify=$(watchify)

View File

@@ -24,6 +24,7 @@ class App extends React.Component {
};
/**
*
* Render the example.
*
* @return {Component} component
@@ -153,12 +154,10 @@ class App extends React.Component {
case '*':
case '-':
case '+':
if (node.type == 'list-item') break
transform = node.type == 'list-item'
? transform
: transform
.setType('list-item')
.wrap('bulleted-list')
if (node.type == 'list-item') return
transform = transform
.setType('list-item')
.wrapBlock('bulleted-list')
break
default:
return
@@ -196,7 +195,7 @@ class App extends React.Component {
.transform()
.setType('paragraph')
if (node.type == 'list-item') transform = transform.unwrap('bulleted-list')
if (node.type == 'list-item') transform = transform.unwrapBlock('bulleted-list')
state = transform.apply()
return state

48
examples/links/index.css Normal file
View File

@@ -0,0 +1,48 @@
html {
background: #eee;
padding: 20px;
}
main {
background: #fff;
padding: 10px;
max-width: 40em;
margin: 0 auto;
}
p {
margin: 0;
}
.editor > * > * + * {
margin-top: 1em;
}
.menu {
margin: 0 -10px;
padding: 1px 0 9px 8px;
border-bottom: 2px solid #eee;
margin-bottom: 10px;
}
.menu > * {
display: inline-block;
}
.menu > * + * {
margin-left: 10px;
}
.button {
color: #ccc;
cursor: pointer;
}
.button[data-active="true"] {
color: black;
}
.material-icons {
font-size: 18px;
}

12
examples/links/index.html Normal file
View File

@@ -0,0 +1,12 @@
<html>
<head>
<meta charset="utf-8" />
<title>Editor | Links Example</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="index.css">
</head>
<body>
<main></main>
<script src="build.js"></script>
</body>
</html>

159
examples/links/index.js Normal file
View File

@@ -0,0 +1,159 @@
import Editor, { Mark, Raw } from '../..'
import React from 'react'
import ReactDOM from 'react-dom'
import state from './state.json'
import { Map } from 'immutable'
/**
* App.
*/
class App extends React.Component {
state = {
state: Raw.deserialize(state)
};
/**
* Check whether the current selection has a link in it.
*
* @return {Boolean} hasLinks
*/
hasLinks() {
let { state } = this.state
const { currentInlineNodes } = state
const hasLinks = currentInlineNodes.some(inline => inline.type == 'link')
return hasLinks
}
/**
* When clicking a link, if the selection has a link in it, remove the link.
* Otherwise, add a new link with an href and text.
*
* @param {Event} e
*/
onClickLink(e) {
e.preventDefault()
let { state } = this.state
const hasLinks = this.hasLinks()
if (hasLinks) {
state = state
.transform()
.unwrapInline('link')
.apply()
}
else if (state.isCurrentlyExpanded) {
// const href = window.prompt('Enter the URL of the link:')
state = state
.transform()
.wrapInline('link', new Map({ href: 'https://google.com' }))
.apply()
}
else {
const href = window.prompt('Enter the URL of the link:')
const text = window.prompt('Enter the text for the link:')
state = state
.transform()
.insertText(text)
.extendBackward(text.length)
.wrapInline('link', new Map({ href }))
.apply()
}
this.setState({ state })
}
/**
* Render the app.
*
* @return {Component} component
*/
render() {
return (
<div>
{this.renderToolbar()}
{this.renderEditor()}
</div>
)
}
/**
* Render the toolbar.
*
* @return {Component} component
*/
renderToolbar() {
const hasLinks = this.hasLinks()
return (
<div className="menu">
<span className="button" onMouseDown={e => this.onClickLink(e)} data-active={hasLinks}>
<span className="material-icons">link</span>
</span>
</div>
)
}
/**
* Render the editor.
*
* @return {Component} component
*/
renderEditor() {
return (
<div className="editor">
<Editor
state={this.state.state}
renderNode={node => this.renderNode(node)}
onChange={(state) => {
console.groupCollapsed('Change!')
console.log('Document:', state.document.toJS())
console.log('Selection:', state.selection.toJS())
console.log('Content:', Raw.serialize(state))
console.groupEnd()
this.setState({ state })
}}
/>
</div>
)
}
/**
* Render our custom `node`.
*
* @param {Node} node
* @return {Component} component
*/
renderNode(node) {
switch (node.type) {
case 'link': {
return (props) => {
const { data } = props.node
const href = data.get('href')
return <a href={href}>{props.children}</a>
}
}
case 'paragraph': {
return (props) => <p>{props.children}</p>
}
}
}
}
/**
* Attach.
*/
const app = <App />
const root = document.body.querySelector('main')
ReactDOM.render(app, root)

57
examples/links/state.json Normal file
View File

@@ -0,0 +1,57 @@
{
"nodes": [
{
"kind": "block",
"type": "paragraph",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "In addition to block nodes, you can create inline nodes, like "
},
]
},
{
"kind": "inline",
"type": "link",
"data": {
"href": "https://en.wikipedia.org/wiki/Hypertext"
},
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "hyperlinks"
},
]
},
]
},
{
"kind": "text",
"ranges": [
{
"text": "!"
},
]
}
]
},
{
"kind": "block",
"type": "paragraph",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "This example shows hyperlinks in action. It features two ways to add links. You can either add a link via the toolbar icon above, or if you want in on a little secret, copy a URL to your keyboard and paste it while a range of text is selected."
}
]
}
]
}
]
}

View File

@@ -1,7 +1,7 @@
<html>
<head>
<meta charset="utf-8" />
<title>Editor | Auto-markdown Example</title>
<title>Editor | Table Example</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="index.css">
</head>

View File

@@ -83,7 +83,7 @@ class Content extends React.Component {
const { anchorNode, anchorOffset, focusNode, focusOffset } = native
const anchor = OffsetKey.findPoint(anchorNode, anchorOffset)
const focus = OffsetKey.findPoint(focusNode, focusOffset)
const edges = document.filterNodes((node) => {
const edges = document.filterDeep((node) => {
return node.key == anchor.key || node.key == focus.key
})

View File

@@ -12,6 +12,7 @@ export default Editor
export { default as Block } from './models/block'
export { default as Character } from './models/character'
export { default as Data } from './models/data'
export { default as Document } from './models/document'
export { default as Inline } from './models/inline'
export { default as Mark } from './models/mark'

File diff suppressed because it is too large Load Diff

View File

@@ -60,6 +60,16 @@ class Selection extends SelectionRecord {
return this.isExpanded
}
/**
* Get whether the range's anchor of focus keys are not set yet.
*
* @return {Boolean} isUnset
*/
get isUnset() {
return this.anchorKey == null || this.focusKey == null
}
/**
* Get whether the selection is forward.
*
@@ -109,7 +119,7 @@ class Selection extends SelectionRecord {
isAtStartOf(node) {
const { startKey, startOffset } = this
const first = node.kind == 'text' ? node : node.getFirstTextNode()
const first = node.kind == 'text' ? node : node.getFirstText()
return startKey == first.key && startOffset == 0
}
@@ -122,7 +132,7 @@ class Selection extends SelectionRecord {
isAtEndOf(node) {
const { endKey, endOffset } = this
const last = node.kind == 'text' ? node : node.getLastTextNode()
const last = node.kind == 'text' ? node : node.getLastText()
return endKey == last.key && endOffset == last.length
}
@@ -142,25 +152,25 @@ class Selection extends SelectionRecord {
if (anchorKey == null || focusKey == null) return selection
// Asset that the anchor and focus nodes exist in the node tree.
node.assertHasNode(anchorKey)
node.assertHasNode(focusKey)
let anchorNode = node.getNode(anchorKey)
let focusNode = node.getNode(focusKey)
node.assertHasDeep(anchorKey)
node.assertHasDeep(focusKey)
let anchorNode = node.getDeep(anchorKey)
let focusNode = node.getDeep(focusKey)
// If the anchor node isn't a text node, match it to one.
if (anchorNode.kind != 'text') {
anchorNode = node.getTextNodeAtOffset(anchorOffset)
let parent = node.getParentNode(anchorNode)
let offset = parent.getNodeOffset(anchorNode)
anchorNode = node.getTextAtOffset(anchorOffset)
let parent = node.getParent(anchorNode)
let offset = parent.getOffset(anchorNode)
anchorOffset = anchorOffset - offset
anchorKey = anchorNode.key
}
// If the focus node isn't a text node, match it to one.
if (focusNode.kind != 'text') {
focusNode = node.getTextNodeAtOffset(focusOffset)
let parent = node.getParentNode(focusNode)
let offset = parent.getNodeOffset(focusNode)
focusNode = node.getTextAtOffset(focusOffset)
let parent = node.getParent(focusNode)
let offset = parent.getOffset(focusNode)
focusOffset = focusOffset - offset
focusKey = focusNode.key
}

View File

@@ -32,8 +32,16 @@ const NODE_LIKE_METHODS = [
'deleteAtRange',
'deleteBackwardAtRange',
'deleteForwardAtRange',
'insertAtRange',
'splitAtRange'
'insertTextAtRange',
'markAtRange',
'setBlockAtRange',
'setInlineAtRange',
'splitBlockAtRange',
'splitInlineAtRange',
'unmarkAtRange',
'unwrapBlockAtRange',
'wrapBlockAtRange',
'wrapInlineAtRange'
]
/**
@@ -140,7 +148,17 @@ class State extends Record(DEFAULTS) {
*/
get currentBlockNodes() {
return this.document.getBlockNodesAtRange(this.selection)
return this.document.getBlocksAtRange(this.selection)
}
/**
* Get the inline nodes in the current selection.
*
* @return {OrderedMap} nodes
*/
get currentInlineNodes() {
return this.document.getInlinesAtRange(this.selection)
}
/**
@@ -198,7 +216,7 @@ class State extends Record(DEFAULTS) {
// Determine what the selection should be after deleting.
const { startKey } = selection
const startNode = document.getNode(startKey)
const startNode = document.getDeep(startKey)
if (selection.isExpanded) {
after = selection.moveToStart()
@@ -209,8 +227,8 @@ class State extends Record(DEFAULTS) {
}
else if (selection.isAtStartOf(startNode)) {
const parent = document.getParentNode(startNode)
const previous = document.getPreviousNode(parent).nodes.first()
const parent = document.getParent(startNode)
const previous = document.getPrevious(parent).nodes.first()
after = selection.moveToEndOf(previous)
}
@@ -283,16 +301,31 @@ class State extends Record(DEFAULTS) {
}
/**
* Set the nodes in the current selection to `type`.
* Set the block nodes in the current selection to `type`.
*
* @param {String} type
* @return {State} state
*/
setType(type) {
setBlock(type, data) {
let state = this
let { document, selection } = state
document = document.setTypeAtRange(selection, type)
document = document.setBlockAtRange(selection, type, data)
state = state.merge({ document })
return state
}
/**
* Set the inline nodes in the current selection to `type`.
*
* @param {String} type
* @return {State} state
*/
setInline(type, data) {
let state = this
let { document, selection } = state
document = document.setInlineAtRange(selection, type, data)
state = state.merge({ document })
return state
}
@@ -313,9 +346,9 @@ class State extends Record(DEFAULTS) {
// Determine what the selection should be after splitting.
const { startKey } = selection
const startNode = document.getNode(startKey)
const parent = document.getParentNode(startNode)
const next = document.getNextNode(parent)
const startNode = document.getDeep(startKey)
const parent = document.getParent(startNode)
const next = document.getNext(parent)
const text = next.nodes.first()
selection = selection.moveToStartOf(text)
@@ -345,10 +378,10 @@ class State extends Record(DEFAULTS) {
* @return {State} state
*/
wrap(type) {
wrapBlock(type) {
let state = this
let { document, selection } = state
document = document.wrapAtRange(selection, type)
document = document.wrapBlockAtRange(selection, type)
state = state.merge({ document })
return state
}
@@ -360,11 +393,43 @@ class State extends Record(DEFAULTS) {
* @return {State} state
*/
unwrap(type) {
unwrapBlock(type) {
let state = this
let { document, selection } = state
selection = selection.normalize(document)
document = document.unwrapAtRange(selection, type)
document = document.unwrapBlockAtRange(selection, type)
state = state.merge({ document, selection })
return state
}
/**
* Wrap the current selection in new inline nodes of `type`.
*
* @param {String} type
* @param {Map} data
* @return {State} state
*/
wrapInline(type, data) {
let state = this
let { document, selection } = state
document = document.wrapInlineAtRange(selection, type, data)
state = state.merge({ document })
return state
}
/**
* Unwrap the current selection from a parent of `type`.
*
* @param {String} type
* @return {State} state
*/
unwrapInline(type) {
let state = this
let { document, selection } = state
selection = selection.normalize(document)
document = document.unwrapInlineAtRange(selection, type)
state = state.merge({ document, selection })
return state
}

View File

@@ -46,16 +46,24 @@ const TRANSFORM_TYPES = [
'insertTextAtRange',
'mark',
'markAtRange',
'setType',
'setTypeAtRange',
'split',
'splitAtRange',
'setBlock',
'setBlockAtRange',
'setInline',
'setInlineAtRange',
'splitBlock',
'splitBlockAtRange',
'splitInline',
'splitInlineAtRange',
'unmark',
'unmarkAtRange',
'unwrap',
'unwrapAtRange',
'wrap',
'wrapAtRange'
'unwrapBlock',
'unwrapBlockAtRange',
'unwrapInline',
'unwrapInlineAtRange',
'wrapBlock',
'wrapBlockAtRange',
'wrapInline',
'wrapInlineAtRange'
]
/**

View File

@@ -1,7 +1,7 @@
import React from 'react'
import keycode from 'keycode'
import { IS_WINDOWS, IS_MAC } from '../utils/environment'
import environment from '../utils/environment'
/**
* Export.
@@ -20,6 +20,7 @@ export default {
onKeyDown(e, state, editor) {
const key = keycode(e.which)
const { IS_WINDOWS, IS_MAC } = environment()
switch (key) {
case 'enter': {

View File

@@ -17,9 +17,7 @@ import { Map } from 'immutable'
*/
function serialize(state) {
return {
nodes: serializeNode(state.document)
}
return serializeNode(state.document)
}
/**
@@ -44,12 +42,12 @@ function serializeNode(node) {
}
case 'block':
case 'inline': {
return {
data: node.data.toJSON(),
kind: node.kind,
nodes: node.nodes.toArray().map(node => serializeNode(node)),
type: node.type
}
const obj = {}
obj.kind = node.kind
obj.type = node.type
obj.nodes = node.nodes.toArray().map(node => serializeNode(node))
if (node.data.size) obj.data = node.data.toJSON()
return obj
}
}
}
@@ -65,10 +63,10 @@ function serializeCharacters(characters) {
return groupByMarks(characters)
.toArray()
.map((range) => {
return {
text: range.text,
marks: range.marks.map(serializeMark)
}
const obj = {}
obj.text = range.text
if (range.marks.size) obj.marks = range.marks.toArray().map(serializeMark)
return obj
})
}
@@ -80,10 +78,10 @@ function serializeCharacters(characters) {
*/
function serializeMark(mark) {
return {
type: mark.type,
data: mark.data.toJSON()
}
const obj = {}
obj.type = mark.type
if (mark.data.size) obj.data = mark.data.toJSON()
return obj
}
/**

View File

@@ -3,16 +3,28 @@ import browser from 'detect-browser'
import Parser from 'ua-parser-js'
/**
* Detections.
* Read the environment.
*
* @return {Object} environment
*/
export const IS_ANDROID = browser.name === 'android'
export const IS_CHROME = browser.name === 'chrome'
export const IS_EDGE = browser.name === 'edge'
export const IS_FIREFOX = browser.name === 'firefox'
export const IS_IE = browser.name === 'ie'
export const IS_IOS = browser.name === 'ios'
export const IS_MAC = new Parser().getOS().name === 'Mac OS'
export const IS_UBUNTU = new Parser().getOS().name === 'Ubuntu'
export const IS_SAFARI = browser.name === 'safari'
export const IS_WINDOWS = new Parser().getOS().name.includes('Windows')
function environment() {
return {
IS_ANDROID: browser.name === 'android',
IS_CHROME: browser.name === 'chrome',
IS_EDGE: browser.name === 'edge',
IS_FIREFOX: browser.name === 'firefox',
IS_IE: browser.name === 'ie',
IS_IOS: browser.name === 'ios',
IS_MAC: new Parser().getOS().name === 'Mac OS',
IS_UBUNTU: new Parser().getOS().name === 'Ubuntu',
IS_SAFARI: browser.name === 'safari',
IS_WINDOWS: new Parser().getOS().name.includes('Windows')
}
}
/**
* Export.
*/
export default environment

View File

@@ -8,7 +8,6 @@
"keycode": "^2.1.2",
"lodash": "^4.13.1",
"react": "^15.1.0",
"to-camel-case": "^1.0.0",
"ua-parser-js": "^0.7.10",
"uid": "0.0.2"
},
@@ -21,10 +20,13 @@
"babel-preset-stage-0": "^6.5.0",
"babelify": "^7.3.0",
"browserify": "^13.0.1",
"component-type": "^1.2.1",
"mocha": "^2.5.3",
"mocha-phantomjs": "^4.0.2",
"react-dom": "^15.1.0",
"read-metadata": "^1.0.0",
"standard": "^7.1.2",
"to-camel-case": "^1.0.0",
"watchify": "^3.7.0"
}
}

185
test/helpers/assert-json.js Normal file
View File

@@ -0,0 +1,185 @@
import assert from 'assert'
import type from 'component-type'
/**
* Assertion error.
*/
const AssertionError = assert.AssertionError
/**
* Assert that an `actual` JSON object equals an `expected` value.
*
* @param {Object} actual
* @param {Object} expected
* @throws {AssertionError}
*/
export function equal(actual, expected, message) {
if (!test(actual, expected)) {
throw new AssertionError({
actual: actual,
expected: wrap(actual, expected),
operator: '==',
stackStartFunction: equal
})
}
}
/**
* Assert that an `actual` JSON object does not equal an `expected` value.
*
* @param {Object} actual
* @param {Object} expected
* @throws {AssertionError}
*/
export function notEqual(actual, expected, message) {
if (test(actual, expected)) {
throw new AssertionError({
actual: actual,
expected: wrap(actual, expected),
operator: '!=',
stackStartFunction: notEqual
})
}
}
/**
* Assert that an `actual` JSON object strict equals an `expected` value.
*
* @param {Object} actual
* @param {Object} expected
* @throws {AssertionError}
*/
export function strictEqual(actual, expected, message) {
if (!test(actual, expected, true)) {
throw new AssertionError({
actual: actual,
expected: wrap(actual, expected),
operator: '===',
stackStartFunction: equal
})
}
}
/**
* Assert that an `actual` JSON object does not strict equal an `expected` value.
*
* @param {Object} actual
* @param {Object} expected
* @throws {AssertionError}
*/
export function notStrictEqual(actual, expected, message) {
if (test(actual, expected, true)) {
throw new AssertionError({
actual: actual,
expected: wrap(actual, expected),
operator: '!==',
stackStartFunction: notEqual
})
}
}
/**
* Test that an `actual` JSON value equals an `expected` JSON value.
*
* If a function is passed as any value, it is called with the actual value and
* must return a boolean.
*
* Strict mode uses strict equality, forces arrays to be of the same length, and
* objects to have the same keys.
*
* @param {Mixed} actual
* @param {Mixed} expected
* @param {Boolean} strict
* @return {Boolean}
*/
function test(actual, expected, strict) {
if (type(expected) == 'function') return !! expected(actual)
if (type(actual) != type(expected)) return false
switch (type(expected)) {
case 'object':
return object(actual, expected, strict)
case 'array':
return array(actual, expected, strict)
default:
return strict ? actual === expected : actual == expected
}
}
/**
* Test that an `actual` object equals an `expected` object.
*
* @param {Object} object
* @param {Object} expected
* @param {Boolean} strict
* @return {Boolean}
*/
function object(actual, expected, strict) {
if (strict) {
var ka = Object.keys(actual).sort()
var ke = Object.keys(expected).sort()
if (!test(ka, ke, strict)) return false
}
for (var key in expected) {
if (!test(actual[key], expected[key], strict)) return false
}
return true
}
/**
* Test that an `actual` array equals an `expected` array.
*
* @param {Array} actual
* @param {Array} expected
* @param {Boolean} strict
* @return {Boolean}
*/
function array(actual, expected, strict) {
if (strict) {
if (!test(actual.length, expected.length, strict)) return false
}
for (var i = 0; i < expected.length; i++) {
if (!test(actual[i], expected[i], strict)) return false
}
return true
}
/**
* Wrap an expected value to remove annoying false negatives.
*
* @param {Mixed} actual
* @param {Mixed} expected
* @return {Mixed}
*/
function wrap(actual, expected) {
if (type(expected) == 'function') return expected(actual) ? actual : expected
if (type(actual) != type(expected)) return expected
if (type(expected) == 'object') {
for (var key in expected) {
expected[key] = wrap(actual[key], expected[key])
}
}
if (type(expected) == 'array') {
for (var i = 0; i < expected.length; i++) {
expected[i] = wrap(actual[i], expected[i])
}
}
return expected
}

View File

@@ -1,11 +0,0 @@
const assert = require('assert')
const Editor = require('..')
/**
* Tests.
*/
describe('server', () => {
})

2
test/server/index.js Normal file
View File

@@ -0,0 +1,2 @@
import './transforms'

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const second = texts.last()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 2,
focusKey: second.key,
focusOffset: 2
})
return state
.transform()
.deleteAtRange(range)
.apply()
}

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const second = texts.last()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: first.length,
focusKey: second.key,
focusOffset: 0
})
return state
.transform()
.deleteAtRange(range)
.apply()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const second = texts.last()
const range = selection.merge({
anchorKey: second.key,
anchorOffset: 0,
focusKey: second.key,
focusOffset: 0
})
return state
.transform()
.deleteBackwardAtRange(range)
.apply()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
nodes:
- 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.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: first.length,
focusKey: first.key,
focusOffset: first.length
})
return state
.transform()
.deleteForwardAtRange(range)
.apply()
}

View File

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

View File

@@ -0,0 +1,8 @@
nodes:
- 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.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.deleteForwardAtRange(range)
.apply()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: w
- text: or
marks:
- type: bold
- text: d

View File

@@ -0,0 +1,12 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: w
- text: ora
marks:
- type: bold
- text: d

View File

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

View File

@@ -0,0 +1,12 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: w
- text: or
marks:
- type: bold
- text: d

View File

@@ -0,0 +1,12 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: wa
- text: or
marks:
- type: bold
- text: d

View File

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

View File

@@ -0,0 +1,12 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: w
- text: or
marks:
- type: bold
- text: d

View File

@@ -0,0 +1,12 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: w
- text: oar
marks:
- type: bold
- text: d

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
nodes:
- 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.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.insertTextAtRange(range, 'a few words ')
.apply()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
nodes:
- 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.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: first.length,
focusKey: first.key,
focusOffset: first.length
})
return state
.transform()
.insertTextAtRange(range, ' a few words')
.apply()
}

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More