2016-06-15 12:07:12 -07:00
|
|
|
|
2016-07-27 13:33:36 -07:00
|
|
|
import Base64 from '../serializers/base-64'
|
2016-07-27 11:38:57 -07:00
|
|
|
import Character from '../models/character'
|
2016-08-01 18:09:30 -07:00
|
|
|
import Debug from 'debug'
|
2016-07-11 18:36:45 -07:00
|
|
|
import Placeholder from '../components/placeholder'
|
2016-06-17 19:57:37 -07:00
|
|
|
import React from 'react'
|
2016-07-14 13:42:10 -07:00
|
|
|
import String from '../utils/string'
|
2016-08-05 12:40:54 -07:00
|
|
|
import getWindow from 'get-window'
|
2016-06-15 12:07:12 -07:00
|
|
|
|
2016-08-01 18:09:30 -07:00
|
|
|
/**
|
|
|
|
* Debug.
|
|
|
|
*
|
|
|
|
* @type {Function}
|
|
|
|
*/
|
|
|
|
|
|
|
|
const debug = Debug('slate:core')
|
|
|
|
|
2016-07-06 14:48:40 -07:00
|
|
|
/**
|
2016-07-11 18:36:45 -07:00
|
|
|
* The default plugin.
|
2016-07-06 20:19:19 -07:00
|
|
|
*
|
2016-07-11 18:36:45 -07:00
|
|
|
* @param {Object} options
|
2016-07-11 19:24:10 -07:00
|
|
|
* @property {Element} placeholder
|
|
|
|
* @property {String} placeholderClassName
|
|
|
|
* @property {Object} placeholderStyle
|
2016-07-11 18:36:45 -07:00
|
|
|
* @return {Object}
|
2016-07-06 14:48:40 -07:00
|
|
|
*/
|
|
|
|
|
2016-07-11 18:36:45 -07:00
|
|
|
function Plugin(options = {}) {
|
2016-07-11 19:24:10 -07:00
|
|
|
const {
|
|
|
|
placeholder,
|
|
|
|
placeholderClassName,
|
2016-08-03 03:27:33 +03:00
|
|
|
placeholderStyle,
|
2016-07-11 19:24:10 -07:00
|
|
|
} = options
|
2016-06-24 10:46:01 -07:00
|
|
|
|
2016-08-12 11:33:48 -07:00
|
|
|
/**
|
|
|
|
* On before change, enforce the editor's schema.
|
|
|
|
*
|
|
|
|
* @param {State} state
|
|
|
|
* @param {Editor} schema
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
|
|
|
function onBeforeChange(state, editor) {
|
|
|
|
if (state.isNative) return state
|
2016-08-13 16:18:07 -07:00
|
|
|
const schema = editor.getSchema()
|
|
|
|
return state.normalize(schema)
|
2016-08-12 11:33:48 -07:00
|
|
|
}
|
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
/**
|
|
|
|
* On before input, see if we can let the browser continue with it's native
|
|
|
|
* input behavior, to avoid a re-render for performance.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
2016-07-27 13:12:19 -07:00
|
|
|
* @param {Object} data
|
2016-07-27 12:54:04 -07:00
|
|
|
* @param {State} state
|
|
|
|
* @param {Editor} editor
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
2016-07-27 13:12:19 -07:00
|
|
|
function onBeforeInput(e, data, state, editor) {
|
2016-08-13 19:38:59 -07:00
|
|
|
const { document, startKey, startOffset, startText } = state
|
2016-07-27 12:54:04 -07:00
|
|
|
|
|
|
|
// Determine what the characters would be if natively inserted.
|
2016-08-13 19:38:59 -07:00
|
|
|
const schema = editor.getSchema()
|
|
|
|
const decorators = document.getDescendantDecorators(startKey, schema)
|
|
|
|
const prevChars = startText.getDecorations(decorators)
|
2016-08-01 18:09:30 -07:00
|
|
|
const prevChar = prevChars.get(startOffset - 1)
|
|
|
|
const char = Character.create({
|
|
|
|
text: e.data,
|
|
|
|
marks: prevChar && prevChar.marks
|
|
|
|
})
|
|
|
|
|
|
|
|
const chars = prevChars
|
2016-07-27 12:54:04 -07:00
|
|
|
.slice(0, startOffset)
|
2016-08-01 18:09:30 -07:00
|
|
|
.push(char)
|
|
|
|
.concat(prevChars.slice(startOffset))
|
2016-07-27 12:54:04 -07:00
|
|
|
|
|
|
|
// Determine what the characters should be, if not natively inserted.
|
|
|
|
let next = state
|
|
|
|
.transform()
|
|
|
|
.insertText(e.data)
|
|
|
|
.apply()
|
|
|
|
|
|
|
|
const nextText = next.startText
|
2016-08-13 19:38:59 -07:00
|
|
|
const nextChars = nextText.getDecorations(decorators)
|
2016-07-27 12:54:04 -07:00
|
|
|
|
|
|
|
// We do not have to re-render if the current selection is collapsed, the
|
|
|
|
// current node is not empty, there are no marks on the cursor, and the
|
|
|
|
// natively inserted characters would be the same as the non-native.
|
|
|
|
const isNative = (
|
|
|
|
state.isCollapsed &&
|
|
|
|
state.startText.text != '' &&
|
|
|
|
state.cursorMarks == null &&
|
|
|
|
chars.equals(nextChars)
|
|
|
|
)
|
|
|
|
|
|
|
|
// Add the `isNative` flag directly, so we don't have to re-transform.
|
|
|
|
if (isNative) {
|
|
|
|
next = next.merge({ isNative })
|
2016-07-11 18:36:45 -07:00
|
|
|
}
|
2016-06-30 14:37:29 -07:00
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
// If not native, prevent default so that the DOM remains untouched.
|
|
|
|
if (!isNative) e.preventDefault()
|
|
|
|
|
2016-08-01 18:09:30 -07:00
|
|
|
debug('onBeforeInput', { data, isNative })
|
2016-07-27 12:54:04 -07:00
|
|
|
return next
|
|
|
|
}
|
|
|
|
|
2016-07-27 13:07:52 -07:00
|
|
|
/**
|
|
|
|
* On blur.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
|
|
|
* @param {Object} data
|
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
|
|
|
function onBlur(e, data, state) {
|
2016-08-01 18:09:30 -07:00
|
|
|
const isNative = true
|
|
|
|
|
|
|
|
debug('onBlur', { data, isNative })
|
|
|
|
|
2016-07-27 13:07:52 -07:00
|
|
|
return state
|
|
|
|
.transform()
|
|
|
|
.blur()
|
2016-08-01 18:09:30 -07:00
|
|
|
.apply({ isNative })
|
2016-07-27 13:07:52 -07:00
|
|
|
}
|
|
|
|
|
2016-07-27 13:33:36 -07:00
|
|
|
/**
|
|
|
|
* On copy.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
|
|
|
* @param {Object} data
|
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
|
|
|
function onCopy(e, data, state) {
|
2016-08-01 18:09:30 -07:00
|
|
|
debug('onCopy', data)
|
2016-07-27 13:33:36 -07:00
|
|
|
onCutOrCopy(e, data, state)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* On cut.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
|
|
|
* @param {Object} data
|
|
|
|
* @param {State} state
|
|
|
|
* @param {Editor} editor
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
|
|
|
function onCut(e, data, state, editor) {
|
2016-08-01 18:09:30 -07:00
|
|
|
debug('onCut', data)
|
2016-07-27 13:33:36 -07:00
|
|
|
onCutOrCopy(e, data, state)
|
2016-08-05 12:40:54 -07:00
|
|
|
const window = getWindow(e.target)
|
2016-07-27 13:33:36 -07:00
|
|
|
|
|
|
|
// Once the fake cut content has successfully been added to the clipboard,
|
|
|
|
// delete the content in the current selection.
|
|
|
|
window.requestAnimationFrame(() => {
|
|
|
|
const next = editor
|
|
|
|
.getState()
|
|
|
|
.transform()
|
|
|
|
.delete()
|
|
|
|
.apply()
|
|
|
|
|
|
|
|
editor.onChange(next)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* On cut or copy, create a fake selection so that we can add a Base 64
|
|
|
|
* encoded copy of the fragment to the HTML, to decode on future pastes.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
|
|
|
* @param {Object} data
|
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
|
|
|
function onCutOrCopy(e, data, state) {
|
2016-08-05 12:40:54 -07:00
|
|
|
const window = getWindow(e.target)
|
|
|
|
const native = window.getSelection()
|
2016-07-27 13:33:36 -07:00
|
|
|
if (!native.rangeCount) return
|
|
|
|
|
|
|
|
const { fragment } = data
|
|
|
|
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.
|
|
|
|
const range = native.getRangeAt(0)
|
|
|
|
const contents = range.cloneContents()
|
2016-08-05 12:40:54 -07:00
|
|
|
const wrapper = window.document.createElement('span')
|
2016-07-27 13:33:36 -07:00
|
|
|
const text = contents.childNodes[0]
|
|
|
|
const char = text.textContent.slice(0, 1)
|
2016-08-05 12:40:54 -07:00
|
|
|
const first = window.document.createTextNode(char)
|
2016-07-27 13:33:36 -07:00
|
|
|
const rest = text.textContent.slice(1)
|
|
|
|
text.textContent = rest
|
|
|
|
wrapper.appendChild(first)
|
2016-08-09 12:25:08 -07:00
|
|
|
wrapper.setAttribute('data-slate-fragment', encoded)
|
2016-07-27 13:33:36 -07:00
|
|
|
contents.insertBefore(wrapper, text)
|
|
|
|
|
|
|
|
// Add the phony content to the DOM, and select it, so it will be copied.
|
2016-08-05 12:40:54 -07:00
|
|
|
const body = window.document.querySelector('body')
|
|
|
|
const div = window.document.createElement('div')
|
2016-07-27 13:33:36 -07:00
|
|
|
div.setAttribute('contenteditable', true)
|
|
|
|
div.style.position = 'absolute'
|
|
|
|
div.style.left = '-9999px'
|
|
|
|
div.appendChild(contents)
|
|
|
|
body.appendChild(div)
|
|
|
|
|
|
|
|
// COMPAT: In Firefox, trying to use the terser `native.selectAllChildren`
|
|
|
|
// throws an error, so we use the older `range` equivalent. (2016/06/21)
|
2016-08-05 12:40:54 -07:00
|
|
|
const r = window.document.createRange()
|
2016-07-27 13:33:36 -07:00
|
|
|
r.selectNodeContents(div)
|
|
|
|
native.removeAllRanges()
|
|
|
|
native.addRange(r)
|
|
|
|
|
|
|
|
// Revert to the previous selection right after copying.
|
|
|
|
window.requestAnimationFrame(() => {
|
|
|
|
body.removeChild(div)
|
|
|
|
native.removeAllRanges()
|
|
|
|
native.addRange(range)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
/**
|
|
|
|
* On drop.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
|
|
|
* @param {Object} data
|
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
|
|
|
function onDrop(e, data, state) {
|
2016-08-01 18:09:30 -07:00
|
|
|
debug('onDrop', { data })
|
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
switch (data.type) {
|
|
|
|
case 'text':
|
|
|
|
case 'html':
|
|
|
|
return onDropText(e, data, state)
|
|
|
|
case 'fragment':
|
|
|
|
return onDropFragment(e, data, state)
|
2016-06-15 12:07:12 -07:00
|
|
|
}
|
2016-07-11 18:36:45 -07:00
|
|
|
}
|
2016-06-17 19:57:37 -07:00
|
|
|
|
2016-06-24 12:06:59 -07:00
|
|
|
/**
|
2016-07-27 12:54:04 -07:00
|
|
|
* On drop fragment.
|
2016-06-24 12:06:59 -07:00
|
|
|
*
|
2016-07-27 12:54:04 -07:00
|
|
|
* @param {Event} e
|
|
|
|
* @param {Object} data
|
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
2016-06-24 12:06:59 -07:00
|
|
|
*/
|
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
function onDropFragment(e, data, state) {
|
2016-08-01 18:09:30 -07:00
|
|
|
debug('onDropFragment', { data })
|
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
const { selection } = state
|
|
|
|
let { fragment, target, isInternal } = data
|
|
|
|
|
|
|
|
// 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)
|
2016-07-11 18:36:45 -07:00
|
|
|
}
|
2016-07-27 12:54:04 -07:00
|
|
|
|
|
|
|
let transform = state.transform()
|
|
|
|
|
|
|
|
if (isInternal) transform = transform.delete()
|
|
|
|
|
|
|
|
return transform
|
|
|
|
.moveTo(target)
|
|
|
|
.insertFragment(fragment)
|
|
|
|
.apply()
|
2016-07-11 18:36:45 -07:00
|
|
|
}
|
2016-06-24 12:06:59 -07:00
|
|
|
|
2016-06-17 19:57:37 -07:00
|
|
|
/**
|
2016-07-27 12:54:04 -07:00
|
|
|
* On drop text, split the blocks at new lines.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
|
|
|
* @param {Object} data
|
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
2016-06-17 19:57:37 -07:00
|
|
|
*/
|
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
function onDropText(e, data, state) {
|
2016-08-01 18:09:30 -07:00
|
|
|
debug('onDropText', { data })
|
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
const { text, target } = data
|
|
|
|
let transform = state
|
|
|
|
.transform()
|
|
|
|
.moveTo(target)
|
|
|
|
|
|
|
|
text
|
|
|
|
.split('\n')
|
|
|
|
.forEach((line, i) => {
|
|
|
|
if (i > 0) transform = transform.splitBlock()
|
|
|
|
transform = transform.insertText(line)
|
|
|
|
})
|
2016-07-11 18:36:45 -07:00
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
return transform.apply()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* On key down.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
2016-07-27 14:30:09 -07:00
|
|
|
* @param {Object} data
|
2016-07-27 12:54:04 -07:00
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
2016-07-27 14:30:09 -07:00
|
|
|
function onKeyDown(e, data, state) {
|
2016-08-01 18:09:30 -07:00
|
|
|
debug('onKeyDown', { data })
|
|
|
|
|
2016-07-27 14:30:09 -07:00
|
|
|
switch (data.key) {
|
|
|
|
case 'enter': return onKeyDownEnter(e, data, state)
|
|
|
|
case 'backspace': return onKeyDownBackspace(e, data, state)
|
|
|
|
case 'delete': return onKeyDownDelete(e, data, state)
|
|
|
|
case 'y': return onKeyDownY(e, data, state)
|
|
|
|
case 'z': return onKeyDownZ(e, data, state)
|
2016-07-27 12:54:04 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* On `enter` key down, split the current block in half.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
2016-07-27 14:30:09 -07:00
|
|
|
* @param {Object} data
|
2016-07-27 12:54:04 -07:00
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
2016-07-27 14:30:09 -07:00
|
|
|
function onKeyDownEnter(e, data, state) {
|
2016-08-01 18:09:30 -07:00
|
|
|
debug('onKeyDownEnter', { data })
|
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
const { document, startKey, startBlock } = state
|
|
|
|
|
|
|
|
// For void blocks, we don't want to split. Instead we just move to the
|
|
|
|
// start of the next text node if one exists.
|
|
|
|
if (startBlock && startBlock.isVoid) {
|
|
|
|
const text = document.getNextText(startKey)
|
|
|
|
if (!text) return
|
|
|
|
return state
|
2016-07-26 16:58:42 -07:00
|
|
|
.transform()
|
2016-07-27 12:54:04 -07:00
|
|
|
.collapseToStartOf(text)
|
2016-07-26 16:58:42 -07:00
|
|
|
.apply()
|
2016-07-27 12:54:04 -07:00
|
|
|
}
|
2016-07-26 16:58:42 -07:00
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
return state
|
|
|
|
.transform()
|
|
|
|
.splitBlock()
|
|
|
|
.apply()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* On `backspace` key down, delete backwards.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
2016-07-27 14:30:09 -07:00
|
|
|
* @param {Object} data
|
2016-07-27 12:54:04 -07:00
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
2016-07-27 14:30:09 -07:00
|
|
|
function onKeyDownBackspace(e, data, state) {
|
2016-08-01 18:09:30 -07:00
|
|
|
debug('onKeyDownBackspace', { data })
|
2016-07-28 15:38:17 -07:00
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
// If expanded, delete regularly.
|
|
|
|
if (state.isExpanded) {
|
2016-07-26 11:07:31 -07:00
|
|
|
return state
|
2016-07-25 16:59:12 -07:00
|
|
|
.transform()
|
2016-07-27 12:54:04 -07:00
|
|
|
.delete()
|
|
|
|
.apply()
|
2016-07-11 18:36:45 -07:00
|
|
|
}
|
2016-07-27 12:54:04 -07:00
|
|
|
|
|
|
|
const { startOffset, startBlock } = state
|
|
|
|
const text = startBlock.text
|
|
|
|
let n
|
|
|
|
|
|
|
|
// Determine how far backwards to delete.
|
2016-07-27 14:30:09 -07:00
|
|
|
if (data.isWord) {
|
2016-07-27 12:54:04 -07:00
|
|
|
n = String.getWordOffsetBackward(text, startOffset)
|
2016-07-27 14:30:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
else if (data.isLine) {
|
2016-07-27 12:54:04 -07:00
|
|
|
n = startOffset
|
2016-07-27 14:30:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
else {
|
2016-07-27 12:54:04 -07:00
|
|
|
n = String.getCharOffsetBackward(text, startOffset)
|
|
|
|
}
|
|
|
|
|
|
|
|
return state
|
|
|
|
.transform()
|
|
|
|
.deleteBackward(n)
|
|
|
|
.apply()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* On `delete` key down, delete forwards.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
2016-07-27 14:30:09 -07:00
|
|
|
* @param {Object} data
|
2016-07-27 12:54:04 -07:00
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
2016-07-27 14:30:09 -07:00
|
|
|
function onKeyDownDelete(e, data, state) {
|
2016-08-01 18:09:30 -07:00
|
|
|
debug('onKeyDownDelete', { data })
|
2016-07-28 15:38:17 -07:00
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
// If expanded, delete regularly.
|
|
|
|
if (state.isExpanded) {
|
|
|
|
return state
|
|
|
|
.transform()
|
|
|
|
.delete()
|
|
|
|
.apply()
|
|
|
|
}
|
|
|
|
|
|
|
|
const { startOffset, startBlock } = state
|
|
|
|
const text = startBlock.text
|
|
|
|
let n
|
|
|
|
|
|
|
|
// Determine how far forwards to delete.
|
2016-07-27 14:30:09 -07:00
|
|
|
if (data.isWord) {
|
2016-07-27 12:54:04 -07:00
|
|
|
n = String.getWordOffsetForward(text, startOffset)
|
2016-07-27 14:30:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
else if (data.isLine) {
|
2016-07-27 12:54:04 -07:00
|
|
|
n = text.length - startOffset
|
2016-07-27 14:30:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
else {
|
2016-07-27 12:54:04 -07:00
|
|
|
n = String.getCharOffsetForward(text, startOffset)
|
|
|
|
}
|
|
|
|
|
|
|
|
return state
|
|
|
|
.transform()
|
|
|
|
.deleteForward(n)
|
|
|
|
.apply()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* On `y` key down, redo.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
2016-07-27 14:30:09 -07:00
|
|
|
* @param {Object} data
|
2016-07-27 12:54:04 -07:00
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
2016-07-27 14:30:09 -07:00
|
|
|
function onKeyDownY(e, data, state) {
|
|
|
|
if (!data.isMod) return
|
2016-08-01 18:09:30 -07:00
|
|
|
|
|
|
|
debug('onKeyDownY', { data })
|
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
return state
|
|
|
|
.transform()
|
|
|
|
.redo()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* On `z` key down, undo or redo.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
2016-07-27 14:30:09 -07:00
|
|
|
* @param {Object} data
|
2016-07-27 12:54:04 -07:00
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
2016-07-27 14:30:09 -07:00
|
|
|
function onKeyDownZ(e, data, state) {
|
|
|
|
if (!data.isMod) return
|
2016-08-01 18:09:30 -07:00
|
|
|
|
|
|
|
debug('onKeyDownZ', { data })
|
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
return state
|
|
|
|
.transform()
|
2016-07-27 14:30:09 -07:00
|
|
|
[data.isShift ? 'redo' : 'undo']()
|
2016-07-27 12:54:04 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* On paste.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
|
|
|
* @param {Object} data
|
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
|
|
|
function onPaste(e, data, state) {
|
2016-08-01 18:09:30 -07:00
|
|
|
debug('onPaste', { data })
|
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
switch (data.type) {
|
2016-07-27 14:34:11 -07:00
|
|
|
case 'fragment':
|
|
|
|
return onPasteFragment(e, data, state)
|
2016-07-27 12:54:04 -07:00
|
|
|
case 'text':
|
|
|
|
case 'html':
|
|
|
|
return onPasteText(e, data, state)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-07-27 14:34:11 -07:00
|
|
|
/**
|
|
|
|
* On paste fragment.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
|
|
|
* @param {Object} data
|
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
|
|
|
function onPasteFragment(e, data, state) {
|
2016-08-01 18:09:30 -07:00
|
|
|
debug('onPasteFragment', { data })
|
|
|
|
|
2016-07-27 14:34:11 -07:00
|
|
|
return state
|
|
|
|
.transform()
|
|
|
|
.insertFragment(data.fragment)
|
|
|
|
.apply()
|
|
|
|
}
|
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
/**
|
|
|
|
* On paste text, split blocks at new lines.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
|
|
|
* @param {Object} data
|
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
|
|
|
function onPasteText(e, data, state) {
|
2016-08-01 18:09:30 -07:00
|
|
|
debug('onPasteText', { data })
|
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
let transform = state.transform()
|
|
|
|
|
|
|
|
data.text
|
|
|
|
.split('\n')
|
|
|
|
.forEach((line, i) => {
|
|
|
|
if (i > 0) transform = transform.splitBlock()
|
|
|
|
transform = transform.insertText(line)
|
|
|
|
})
|
|
|
|
|
|
|
|
return transform.apply()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* On select.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
|
|
|
* @param {Object} data
|
|
|
|
* @param {State} state
|
|
|
|
* @return {State}
|
|
|
|
*/
|
|
|
|
|
|
|
|
function onSelect(e, data, state) {
|
2016-07-28 16:04:41 -07:00
|
|
|
const { selection } = data
|
2016-08-01 18:09:30 -07:00
|
|
|
|
|
|
|
debug('onSelect', { data, selection: selection.toJS() })
|
|
|
|
|
2016-07-27 12:54:04 -07:00
|
|
|
return state
|
|
|
|
.transform()
|
|
|
|
.moveTo(selection)
|
|
|
|
.focus()
|
2016-07-28 16:04:41 -07:00
|
|
|
.apply()
|
2016-07-27 12:54:04 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-08-14 15:51:07 -07:00
|
|
|
* A default schema rule to render block nodes.
|
|
|
|
*
|
|
|
|
* @type {Object}
|
|
|
|
*/
|
|
|
|
|
|
|
|
const BLOCK_RENDER_RULE = {
|
|
|
|
match: (node) => {
|
|
|
|
return node.kind == 'block'
|
|
|
|
},
|
|
|
|
render: (props) => {
|
|
|
|
return (
|
|
|
|
<div {...props.attributes} style={{ position: 'relative' }}>
|
|
|
|
{props.children}
|
|
|
|
{placeholder
|
|
|
|
? <Placeholder
|
|
|
|
className={placeholderClassName}
|
|
|
|
node={props.node}
|
|
|
|
parent={props.state.document}
|
|
|
|
state={props.state}
|
|
|
|
style={placeholderStyle}
|
|
|
|
>
|
|
|
|
{placeholder}
|
|
|
|
</Placeholder>
|
|
|
|
: null}
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A default schema rule to render inline nodes.
|
|
|
|
*
|
|
|
|
* @type {Object}
|
|
|
|
*/
|
|
|
|
|
|
|
|
const INLINE_RENDER_RULE = {
|
|
|
|
match: (node) => {
|
|
|
|
return node.kind == 'inline'
|
|
|
|
},
|
|
|
|
render: (props) => {
|
|
|
|
return (
|
|
|
|
<span {...props.attributes} style={{ position: 'relative' }}>
|
|
|
|
{props.children}
|
|
|
|
</span>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A default schema rule to only allow block nodes in documents.
|
|
|
|
*
|
|
|
|
* @type {Object}
|
|
|
|
*/
|
|
|
|
|
|
|
|
const DOCUMENT_CHILDREN_RULE = {
|
|
|
|
match: (node) => {
|
|
|
|
return node.kind == 'document'
|
|
|
|
},
|
|
|
|
validate: (document) => {
|
|
|
|
const { nodes } = document
|
|
|
|
const invalids = nodes.filter(n => n.kind != 'block')
|
|
|
|
return invalids.size ? invalids : null
|
|
|
|
},
|
2016-08-14 18:25:12 -07:00
|
|
|
normalize: (transform, document, invalids) => {
|
2016-08-14 15:51:07 -07:00
|
|
|
return invalids.reduce((t, n) => t.removeNodeByKey(n.key), transform)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A default schema rule to only allow block, inline and text nodes in blocks.
|
|
|
|
*
|
|
|
|
* @type {Object}
|
|
|
|
*/
|
|
|
|
|
|
|
|
const BLOCK_CHILDREN_RULE = {
|
|
|
|
match: (node) => {
|
|
|
|
return node.kind == 'block'
|
|
|
|
},
|
|
|
|
validate: (block) => {
|
|
|
|
const { nodes } = block
|
|
|
|
const invalids = nodes.filter(n => n.kind != 'block' && n.kind != 'inline' && n.kind != 'text')
|
|
|
|
return invalids.size ? invalids : null
|
|
|
|
},
|
2016-08-14 18:25:12 -07:00
|
|
|
normalize: (transform, block, invalids) => {
|
2016-08-14 15:51:07 -07:00
|
|
|
return invalids.reduce((t, n) => t.removeNodeByKey(n.key), transform)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A default schema rule to only allow inline and text nodes in inlines.
|
2016-07-27 12:54:04 -07:00
|
|
|
*
|
2016-08-14 15:51:07 -07:00
|
|
|
* @type {Object}
|
2016-07-27 12:54:04 -07:00
|
|
|
*/
|
|
|
|
|
2016-08-14 15:51:07 -07:00
|
|
|
const INLINE_CHILDREN_RULE = {
|
|
|
|
match: (object) => {
|
|
|
|
return object.kind == 'inline'
|
|
|
|
},
|
|
|
|
validate: (inline) => {
|
|
|
|
const { nodes } = inline
|
|
|
|
const invalids = nodes.filter(n => n.kind != 'inline' && n.kind != 'text')
|
|
|
|
return invalids.size ? invalids : null
|
|
|
|
},
|
2016-08-14 18:25:12 -07:00
|
|
|
normalize: (transform, inline, invalids) => {
|
2016-08-14 15:51:07 -07:00
|
|
|
return invalids.reduce((t, n) => t.removeNodeByKey(n.key), transform)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The default schema.
|
|
|
|
*
|
|
|
|
* @type {Object}
|
|
|
|
*/
|
|
|
|
|
|
|
|
const schema = {
|
|
|
|
rules: [
|
|
|
|
BLOCK_RENDER_RULE,
|
|
|
|
INLINE_RENDER_RULE,
|
|
|
|
DOCUMENT_CHILDREN_RULE,
|
|
|
|
BLOCK_CHILDREN_RULE,
|
|
|
|
INLINE_CHILDREN_RULE,
|
|
|
|
]
|
2016-07-27 12:54:04 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the core plugin.
|
|
|
|
*/
|
|
|
|
|
|
|
|
return {
|
2016-08-12 11:33:48 -07:00
|
|
|
onBeforeChange,
|
2016-07-27 12:54:04 -07:00
|
|
|
onBeforeInput,
|
2016-07-27 13:33:36 -07:00
|
|
|
onBlur,
|
|
|
|
onCopy,
|
|
|
|
onCut,
|
2016-07-27 12:54:04 -07:00
|
|
|
onDrop,
|
|
|
|
onKeyDown,
|
|
|
|
onPaste,
|
|
|
|
onSelect,
|
2016-08-14 15:51:07 -07:00
|
|
|
schema,
|
2016-06-15 12:07:12 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-07-11 18:36:45 -07:00
|
|
|
/**
|
|
|
|
* Export.
|
|
|
|
*/
|
|
|
|
|
|
|
|
export default Plugin
|