mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-16 12:14:14 +02:00
Merge branch 'master' of github.com:ianstormtaylor/slate
This commit is contained in:
@@ -91,10 +91,6 @@ Boom! Now we're getting somewhere. That code is reusable for any type of mark.
|
|||||||
Now that we have our plugin, let's remove the hard-coded logic from our app, and replace it with our brand new `MarkHotkey` plugin instead, passing in the same options that will keep our **bold** functionality intact:
|
Now that we have our plugin, let's remove the hard-coded logic from our app, and replace it with our brand new `MarkHotkey` plugin instead, passing in the same options that will keep our **bold** functionality intact:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function BoldMark(props) {
|
|
||||||
return <strong>{props.children}</strong>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize our bold-mark-adding plugin.
|
// Initialize our bold-mark-adding plugin.
|
||||||
const boldPlugin = MarkHotkey({
|
const boldPlugin = MarkHotkey({
|
||||||
type: 'bold',
|
type: 'bold',
|
||||||
|
@@ -27,7 +27,7 @@ const String = new Record({
|
|||||||
const TEXT_RULE = {
|
const TEXT_RULE = {
|
||||||
|
|
||||||
deserialize(el) {
|
deserialize(el) {
|
||||||
if (el.tagName.toLowerCase() == 'br') {
|
if (el.tagName && el.tagName.toLowerCase() === 'br') {
|
||||||
return {
|
return {
|
||||||
kind: 'text',
|
kind: 'text',
|
||||||
ranges: [{ text: '\n' }],
|
ranges: [{ text: '\n' }],
|
||||||
@@ -45,7 +45,7 @@ const TEXT_RULE = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
serialize(obj, children) {
|
serialize(obj, children) {
|
||||||
if (obj.kind == 'string') {
|
if (obj.kind === 'string') {
|
||||||
return children
|
return children
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.reduce((array, text, i) => {
|
.reduce((array, text, i) => {
|
||||||
@@ -66,7 +66,7 @@ const TEXT_RULE = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function defaultParseHtml(html) {
|
function defaultParseHtml(html) {
|
||||||
if (typeof DOMParser == 'undefined') {
|
if (typeof DOMParser === 'undefined') {
|
||||||
throw new Error('The native `DOMParser` global which the `Html` serializer uses by default is not present in this environment. You must supply the `options.parseHtml` function instead.')
|
throw new Error('The native `DOMParser` global which the `Html` serializer uses by default is not present in this environment. You must supply the `options.parseHtml` function instead.')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +341,7 @@ class Html {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
serializeNode = (node) => {
|
serializeNode = (node) => {
|
||||||
if (node.kind == 'text') {
|
if (node.kind === 'text') {
|
||||||
const ranges = node.getRanges()
|
const ranges = node.getRanges()
|
||||||
return ranges.map(this.serializeRange)
|
return ranges.map(this.serializeRange)
|
||||||
}
|
}
|
||||||
@@ -405,7 +405,7 @@ class Html {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
cruftNewline = (element) => {
|
cruftNewline = (element) => {
|
||||||
return !(element.nodeName == '#text' && element.value == '\n')
|
return !(element.nodeName === '#text' && element.value == '\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,7 @@ import findClosestNode from '../utils/find-closest-node'
|
|||||||
import getCaretPosition from '../utils/get-caret-position'
|
import getCaretPosition from '../utils/get-caret-position'
|
||||||
import getHtmlFromNativePaste from '../utils/get-html-from-native-paste'
|
import getHtmlFromNativePaste from '../utils/get-html-from-native-paste'
|
||||||
import getPoint from '../utils/get-point'
|
import getPoint from '../utils/get-point'
|
||||||
|
import getDropPoint from '../utils/get-drop-point'
|
||||||
import getTransferData from '../utils/get-transfer-data'
|
import getTransferData from '../utils/get-transfer-data'
|
||||||
import setTransferData from '../utils/set-transfer-data'
|
import setTransferData from '../utils/set-transfer-data'
|
||||||
import scrollToSelection from '../utils/scroll-to-selection'
|
import scrollToSelection from '../utils/scroll-to-selection'
|
||||||
@@ -369,7 +370,7 @@ class Content extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
onDragEnd = (event) => {
|
onDragEnd = (event) => {
|
||||||
if (!this.isInEditor(event.target)) return
|
event.stopPropagation()
|
||||||
|
|
||||||
this.tmp.isDragging = false
|
this.tmp.isDragging = false
|
||||||
this.tmp.isInternalDrag = null
|
this.tmp.isInternalDrag = null
|
||||||
@@ -384,7 +385,8 @@ class Content extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
onDragOver = (event) => {
|
onDragOver = (event) => {
|
||||||
if (!this.isInEditor(event.target)) return
|
event.stopPropagation()
|
||||||
|
|
||||||
if (this.tmp.isDragging) return
|
if (this.tmp.isDragging) return
|
||||||
this.tmp.isDragging = true
|
this.tmp.isDragging = true
|
||||||
this.tmp.isInternalDrag = false
|
this.tmp.isInternalDrag = false
|
||||||
@@ -425,33 +427,21 @@ class Content extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
onDrop = (event) => {
|
onDrop = (event) => {
|
||||||
|
event.stopPropagation()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
if (this.props.readOnly) return
|
if (this.props.readOnly) return
|
||||||
if (!this.isInEditor(event.target)) return
|
|
||||||
|
|
||||||
const window = getWindow(event.target)
|
const { editor, state } = this.props
|
||||||
const { state, editor } = this.props
|
|
||||||
const { nativeEvent } = event
|
const { nativeEvent } = event
|
||||||
const { dataTransfer, x, y } = nativeEvent
|
const { dataTransfer } = nativeEvent
|
||||||
const data = getTransferData(dataTransfer)
|
const data = getTransferData(dataTransfer)
|
||||||
|
const point = getDropPoint(event, state, editor)
|
||||||
|
|
||||||
// Resolve the point where the drop occured.
|
|
||||||
let range
|
|
||||||
|
|
||||||
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
|
|
||||||
if (window.document.caretRangeFromPoint) {
|
|
||||||
range = window.document.caretRangeFromPoint(x, y)
|
|
||||||
} else {
|
|
||||||
range = window.document.createRange()
|
|
||||||
range.setStart(nativeEvent.rangeParent, nativeEvent.rangeOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { startContainer, startOffset } = range
|
|
||||||
const point = getPoint(startContainer, startOffset, state, editor)
|
|
||||||
if (!point) return
|
if (!point) return
|
||||||
|
|
||||||
const target = Selection.create({
|
// Add drop-specific information to the data.
|
||||||
|
data.target = Selection.create({
|
||||||
anchorKey: point.key,
|
anchorKey: point.key,
|
||||||
anchorOffset: point.offset,
|
anchorOffset: point.offset,
|
||||||
focusKey: point.key,
|
focusKey: point.key,
|
||||||
@@ -459,9 +449,6 @@ class Content extends React.Component {
|
|||||||
isFocused: true
|
isFocused: true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add drop-specific information to the data.
|
|
||||||
data.target = target
|
|
||||||
|
|
||||||
// COMPAT: Edge throws "Permission denied" errors when
|
// COMPAT: Edge throws "Permission denied" errors when
|
||||||
// accessing `dropEffect` or `effectAllowed` (2017/7/12)
|
// accessing `dropEffect` or `effectAllowed` (2017/7/12)
|
||||||
try {
|
try {
|
||||||
|
@@ -41,17 +41,6 @@ class Void extends React.Component {
|
|||||||
state: SlateTypes.state.isRequired,
|
state: SlateTypes.state.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* State
|
|
||||||
*
|
|
||||||
* @type {Object}
|
|
||||||
*/
|
|
||||||
|
|
||||||
state = {
|
|
||||||
dragCounter: 0,
|
|
||||||
editable: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debug.
|
* Debug.
|
||||||
*
|
*
|
||||||
@@ -90,44 +79,9 @@ class Void extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
onDragOver = event => event.preventDefault()
|
||||||
* Increment counter, and temporarily switch node to editable to allow drop events
|
|
||||||
* Counter required as onDragLeave fires when hovering over child elements
|
|
||||||
*
|
|
||||||
* @param {Event} event
|
|
||||||
*/
|
|
||||||
|
|
||||||
onDragEnter = () => {
|
onDragEnter = event => event.preventDefault()
|
||||||
this.setState((prevState) => {
|
|
||||||
const dragCounter = prevState.dragCounter + 1
|
|
||||||
return { dragCounter, editable: undefined }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrement counter, and if counter 0, then no longer dragging over node
|
|
||||||
* and thus switch back to non-editable
|
|
||||||
*
|
|
||||||
* @param {Event} event
|
|
||||||
*/
|
|
||||||
|
|
||||||
onDragLeave = () => {
|
|
||||||
this.setState((prevState) => {
|
|
||||||
const dragCounter = prevState.dragCounter - 1
|
|
||||||
const editable = dragCounter === 0 ? false : undefined
|
|
||||||
return { dragCounter, editable }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If dropped item onto node, then reset state
|
|
||||||
*
|
|
||||||
* @param {Event} event
|
|
||||||
*/
|
|
||||||
|
|
||||||
onDrop = () => {
|
|
||||||
this.setState({ dragCounter: 0, editable: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render.
|
* Render.
|
||||||
@@ -145,13 +99,13 @@ class Void extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
data-slate-void
|
data-slate-void
|
||||||
|
data-key={node.key}
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
|
onDragOver={this.onDragOver}
|
||||||
onDragEnter={this.onDragEnter}
|
onDragEnter={this.onDragEnter}
|
||||||
onDragLeave={this.onDragLeave}
|
|
||||||
onDrop={this.onDrop}
|
|
||||||
>
|
>
|
||||||
{this.renderSpacer()}
|
{this.renderSpacer()}
|
||||||
<Tag contentEditable={this.state.editable}>
|
<Tag contentEditable={false}>
|
||||||
{children}
|
{children}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Tag>
|
</Tag>
|
||||||
|
@@ -163,7 +163,7 @@ function Plugin(options = {}) {
|
|||||||
const window = getWindow(e.target)
|
const window = getWindow(e.target)
|
||||||
const native = window.getSelection()
|
const native = window.getSelection()
|
||||||
const { state } = change
|
const { state } = change
|
||||||
const { endBlock, endInline } = state
|
const { startKey, endKey, startText, endBlock, endInline } = state
|
||||||
const isVoidBlock = endBlock && endBlock.isVoid
|
const isVoidBlock = endBlock && endBlock.isVoid
|
||||||
const isVoidInline = endInline && endInline.isVoid
|
const isVoidInline = endInline && endInline.isVoid
|
||||||
const isVoid = isVoidBlock || isVoidInline
|
const isVoid = isVoidBlock || isVoidInline
|
||||||
@@ -187,6 +187,25 @@ function Plugin(options = {}) {
|
|||||||
attach = contents.childNodes[contents.childNodes.length - 1].firstChild
|
attach = contents.childNodes[contents.childNodes.length - 1].firstChild
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// COMPAT: in Safari and Chrome when selecting a single marked word,
|
||||||
|
// marks are not preserved when copying.
|
||||||
|
// If the attatched is not void, and the startKey and endKey is the same,
|
||||||
|
// check if there is marks involved. If so, set the range start just before the
|
||||||
|
// startText node
|
||||||
|
if ((IS_CHROME || IS_SAFARI) && !isVoid && startKey === endKey) {
|
||||||
|
const hasMarks = startText.characters
|
||||||
|
.slice(state.selection.anchorOffset, state.selection.focusOffset)
|
||||||
|
.filter(char => char.marks.size !== 0)
|
||||||
|
.size !== 0
|
||||||
|
if (hasMarks) {
|
||||||
|
const r = range.cloneRange()
|
||||||
|
const node = findDOMNode(startText)
|
||||||
|
r.setStartBefore(node)
|
||||||
|
contents = r.cloneContents()
|
||||||
|
attach = contents.childNodes[contents.childNodes.length - 1].firstChild
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove any zero-width space spans from the cloned DOM so that they don't
|
// Remove any zero-width space spans from the cloned DOM so that they don't
|
||||||
// show up elsewhere when pasted.
|
// show up elsewhere when pasted.
|
||||||
const zws = [].slice.call(contents.querySelectorAll('[data-slate-zero-width]'))
|
const zws = [].slice.call(contents.querySelectorAll('[data-slate-zero-width]'))
|
||||||
|
89
packages/slate-react/src/utils/get-drop-point.js
Normal file
89
packages/slate-react/src/utils/get-drop-point.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
|
||||||
|
import getWindow from 'get-window'
|
||||||
|
|
||||||
|
import findClosestNode from './find-closest-node'
|
||||||
|
import getPoint from './get-point'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the target point for a drop event.
|
||||||
|
*
|
||||||
|
* @param {Event} event
|
||||||
|
* @param {State} state
|
||||||
|
* @param {Editor} editor
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getDropPoint(event, state, editor) {
|
||||||
|
const { document } = state
|
||||||
|
const { nativeEvent, target } = event
|
||||||
|
const { x, y } = nativeEvent
|
||||||
|
|
||||||
|
// Resolve the caret position where the drop occured.
|
||||||
|
const window = getWindow(target)
|
||||||
|
let n, o
|
||||||
|
|
||||||
|
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
|
||||||
|
if (window.document.caretRangeFromPoint) {
|
||||||
|
const range = window.document.caretRangeFromPoint(x, y)
|
||||||
|
n = range.startContainer
|
||||||
|
o = range.startOffset
|
||||||
|
} else {
|
||||||
|
const position = window.document.caretPositionFromPoint(x, y)
|
||||||
|
n = position.offsetNode
|
||||||
|
o = position.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeEl = findClosestNode(n, '[data-key]')
|
||||||
|
const nodeKey = nodeEl.getAttribute('data-key')
|
||||||
|
const node = document.key === nodeKey ? document : document.getDescendant(nodeKey)
|
||||||
|
|
||||||
|
// If the drop DOM target is inside an inline void node use last position of
|
||||||
|
// the previous sibling text node or first position of the next sibling text
|
||||||
|
// node as Slate target.
|
||||||
|
if (node.isVoid && node.kind === 'inline') {
|
||||||
|
const rect = nodeEl.getBoundingClientRect()
|
||||||
|
const previous = x - rect.left < rect.left + rect.width - x
|
||||||
|
const text = previous ?
|
||||||
|
document.getPreviousSibling(nodeKey) :
|
||||||
|
document.getNextSibling(nodeKey)
|
||||||
|
const key = text.key
|
||||||
|
const offset = previous ? text.characters.size : 0
|
||||||
|
|
||||||
|
return { key, offset }
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the drop DOM target is inside a block void node use last position of
|
||||||
|
// the previous sibling block node or first position of the next sibling block
|
||||||
|
// node as Slate target.
|
||||||
|
if (node.isVoid) {
|
||||||
|
const rect = nodeEl.getBoundingClientRect()
|
||||||
|
const previous = y - rect.top < rect.top + rect.height - y
|
||||||
|
|
||||||
|
if (previous) {
|
||||||
|
const block = document.getPreviousBlock(nodeKey)
|
||||||
|
const text = block.getLastText()
|
||||||
|
const { key } = text
|
||||||
|
const offset = text.characters.size
|
||||||
|
return { key, offset }
|
||||||
|
}
|
||||||
|
|
||||||
|
const block = document.getNextBlock(nodeKey)
|
||||||
|
const text = block.getLastText()
|
||||||
|
const { key } = text
|
||||||
|
const offset = 0
|
||||||
|
|
||||||
|
return { key, offset }
|
||||||
|
}
|
||||||
|
|
||||||
|
const point = getPoint(n, o, state, editor)
|
||||||
|
|
||||||
|
return point
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*
|
||||||
|
* @type {Function}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default getDropPoint
|
@@ -1448,7 +1448,7 @@ class Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PERF: exit early if both start and end have been found.
|
// PERF: exit early if both start and end have been found.
|
||||||
return start != null && end != null
|
return start == null || end == null
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isSelected && start == null) start = 0
|
if (isSelected && start == null) start = 0
|
||||||
|
Reference in New Issue
Block a user