diff --git a/docs/walkthroughs/using-plugins.md b/docs/walkthroughs/using-plugins.md index 67fc7881e..5b582a5fa 100644 --- a/docs/walkthroughs/using-plugins.md +++ b/docs/walkthroughs/using-plugins.md @@ -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: ```js -function BoldMark(props) { - return {props.children} -} - // Initialize our bold-mark-adding plugin. const boldPlugin = MarkHotkey({ type: 'bold', diff --git a/packages/slate-html-serializer/src/index.js b/packages/slate-html-serializer/src/index.js index 99b5fe7e1..3794354d4 100644 --- a/packages/slate-html-serializer/src/index.js +++ b/packages/slate-html-serializer/src/index.js @@ -27,7 +27,7 @@ const String = new Record({ const TEXT_RULE = { deserialize(el) { - if (el.tagName.toLowerCase() == 'br') { + if (el.tagName && el.tagName.toLowerCase() === 'br') { return { kind: 'text', ranges: [{ text: '\n' }], @@ -45,7 +45,7 @@ const TEXT_RULE = { }, serialize(obj, children) { - if (obj.kind == 'string') { + if (obj.kind === 'string') { return children .split('\n') .reduce((array, text, i) => { @@ -66,7 +66,7 @@ const TEXT_RULE = { */ 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.') } @@ -341,7 +341,7 @@ class Html { */ serializeNode = (node) => { - if (node.kind == 'text') { + if (node.kind === 'text') { const ranges = node.getRanges() return ranges.map(this.serializeRange) } @@ -405,7 +405,7 @@ class Html { */ cruftNewline = (element) => { - return !(element.nodeName == '#text' && element.value == '\n') + return !(element.nodeName === '#text' && element.value == '\n') } } diff --git a/packages/slate-react/src/components/content.js b/packages/slate-react/src/components/content.js index e3c3ae168..a172068cb 100644 --- a/packages/slate-react/src/components/content.js +++ b/packages/slate-react/src/components/content.js @@ -15,6 +15,7 @@ import findClosestNode from '../utils/find-closest-node' import getCaretPosition from '../utils/get-caret-position' import getHtmlFromNativePaste from '../utils/get-html-from-native-paste' import getPoint from '../utils/get-point' +import getDropPoint from '../utils/get-drop-point' import getTransferData from '../utils/get-transfer-data' import setTransferData from '../utils/set-transfer-data' import scrollToSelection from '../utils/scroll-to-selection' @@ -369,7 +370,7 @@ class Content extends React.Component { */ onDragEnd = (event) => { - if (!this.isInEditor(event.target)) return + event.stopPropagation() this.tmp.isDragging = false this.tmp.isInternalDrag = null @@ -384,7 +385,8 @@ class Content extends React.Component { */ onDragOver = (event) => { - if (!this.isInEditor(event.target)) return + event.stopPropagation() + if (this.tmp.isDragging) return this.tmp.isDragging = true this.tmp.isInternalDrag = false @@ -425,33 +427,21 @@ class Content extends React.Component { */ onDrop = (event) => { + event.stopPropagation() event.preventDefault() if (this.props.readOnly) return - if (!this.isInEditor(event.target)) return - const window = getWindow(event.target) - const { state, editor } = this.props + const { editor, state } = this.props const { nativeEvent } = event - const { dataTransfer, x, y } = nativeEvent + const { dataTransfer } = nativeEvent 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 - const target = Selection.create({ + // Add drop-specific information to the data. + data.target = Selection.create({ anchorKey: point.key, anchorOffset: point.offset, focusKey: point.key, @@ -459,9 +449,6 @@ class Content extends React.Component { isFocused: true }) - // Add drop-specific information to the data. - data.target = target - // COMPAT: Edge throws "Permission denied" errors when // accessing `dropEffect` or `effectAllowed` (2017/7/12) try { diff --git a/packages/slate-react/src/components/void.js b/packages/slate-react/src/components/void.js index dc6e79fd4..11facdef7 100644 --- a/packages/slate-react/src/components/void.js +++ b/packages/slate-react/src/components/void.js @@ -41,17 +41,6 @@ class Void extends React.Component { state: SlateTypes.state.isRequired, } - /** - * State - * - * @type {Object} - */ - - state = { - dragCounter: 0, - editable: false, - } - /** * Debug. * @@ -90,44 +79,9 @@ class Void extends React.Component { }) } - /** - * 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 - */ + onDragOver = event => event.preventDefault() - onDragEnter = () => { - 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 }) - } + onDragEnter = event => event.preventDefault() /** * Render. @@ -145,13 +99,13 @@ class Void extends React.Component { return ( {this.renderSpacer()} - + {children} diff --git a/packages/slate-react/src/plugins/core.js b/packages/slate-react/src/plugins/core.js index 1c58b3ce2..48b5397ca 100644 --- a/packages/slate-react/src/plugins/core.js +++ b/packages/slate-react/src/plugins/core.js @@ -163,7 +163,7 @@ function Plugin(options = {}) { const window = getWindow(e.target) const native = window.getSelection() const { state } = change - const { endBlock, endInline } = state + const { startKey, endKey, startText, endBlock, endInline } = state const isVoidBlock = endBlock && endBlock.isVoid const isVoidInline = endInline && endInline.isVoid const isVoid = isVoidBlock || isVoidInline @@ -187,6 +187,25 @@ function Plugin(options = {}) { 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 // show up elsewhere when pasted. const zws = [].slice.call(contents.querySelectorAll('[data-slate-zero-width]')) diff --git a/packages/slate-react/src/utils/get-drop-point.js b/packages/slate-react/src/utils/get-drop-point.js new file mode 100644 index 000000000..96671d927 --- /dev/null +++ b/packages/slate-react/src/utils/get-drop-point.js @@ -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 diff --git a/packages/slate/src/models/node.js b/packages/slate/src/models/node.js index 85052813a..03af04e61 100644 --- a/packages/slate/src/models/node.js +++ b/packages/slate/src/models/node.js @@ -1448,7 +1448,7 @@ class Node { } // 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