diff --git a/examples/rich-text/index.js b/examples/rich-text/index.js index 8ff1d9444..dd2bcc6b3 100644 --- a/examples/rich-text/index.js +++ b/examples/rich-text/index.js @@ -59,7 +59,7 @@ class RichText extends React.Component { state = { state: Raw.deserialize(initialState, { terse: true }) - }; + } /** * Check if the current selection has a mark with `type` in it. diff --git a/src/components/content.js b/src/components/content.js index 2c80833ff..43f993967 100644 --- a/src/components/content.js +++ b/src/components/content.js @@ -9,13 +9,14 @@ import TRANSFER_TYPES from '../constants/transfer-types' import Base64 from '../serializers/base-64' import Node from './node' import Selection from '../models/selection' +import SlateTypes from '../utils/prop-types' import extendSelection from '../utils/extend-selection' import findClosestNode from '../utils/find-closest-node' import findDeepestNode from '../utils/find-deepest-node' +import getHtmlFromNativePaste from '../utils/get-html-from-native-paste' import getPoint from '../utils/get-point' import getTransferData from '../utils/get-transfer-data' import setTransferData from '../utils/set-transfer-data' -import getHtmlFromNativePaste from '../utils/get-html-from-native-paste' import { IS_FIREFOX, IS_MAC, IS_IE } from '../constants/environment' /** @@ -58,12 +59,12 @@ class Content extends React.Component { onSelect: Types.func.isRequired, readOnly: Types.bool.isRequired, role: Types.string, - schema: Types.object, + schema: SlateTypes.schema.isRequired, spellCheck: Types.bool.isRequired, - state: Types.object.isRequired, + state: SlateTypes.state.isRequired, style: Types.object, tabIndex: Types.number, - tagName: Types.string + tagName: Types.string, } /** @@ -74,7 +75,7 @@ class Content extends React.Component { static defaultProps = { style: {}, - tagName: 'div' + tagName: 'div', } /** diff --git a/src/components/editor.js b/src/components/editor.js index 5198a702a..58525f401 100644 --- a/src/components/editor.js +++ b/src/components/editor.js @@ -6,7 +6,7 @@ import Types from 'prop-types' import Stack from '../models/stack' import State from '../models/state' -import SlatePropTypes from '../utils/prop-types' +import SlateTypes from '../utils/prop-types' import noop from '../utils/noop' /** @@ -81,7 +81,7 @@ class Editor extends React.Component { role: Types.string, schema: Types.object, spellCheck: Types.bool, - state: SlatePropTypes.state.isRequired, + state: SlateTypes.state.isRequired, style: Types.object, tabIndex: Types.number, } diff --git a/src/components/leaf.js b/src/components/leaf.js index 2a370a01a..d928845d4 100644 --- a/src/components/leaf.js +++ b/src/components/leaf.js @@ -5,6 +5,7 @@ import ReactDOM from 'react-dom' import Types from 'prop-types' import OffsetKey from '../utils/offset-key' +import SlateTypes from '../utils/prop-types' import findDeepestNode from '../utils/find-deepest-node' import { IS_FIREFOX } from '../constants/environment' @@ -31,17 +32,17 @@ class Leaf extends React.Component { */ static propTypes = { - block: Types.object.isRequired, + block: SlateTypes.block.isRequired, editor: Types.object.isRequired, index: Types.number.isRequired, - marks: Types.object.isRequired, - node: Types.object.isRequired, + marks: SlateTypes.marks.isRequired, + node: SlateTypes.node.isRequired, offset: Types.number.isRequired, - parent: Types.object.isRequired, - ranges: Types.object.isRequired, - schema: Types.object.isRequired, - state: Types.object.isRequired, - text: Types.string.isRequired + parent: SlateTypes.node.isRequired, + ranges: SlateTypes.ranges.isRequired, + schema: SlateTypes.schema.isRequired, + state: SlateTypes.state.isRequired, + text: Types.string.isRequired, } /** diff --git a/src/components/node.js b/src/components/node.js index 570354456..5847a2e95 100644 --- a/src/components/node.js +++ b/src/components/node.js @@ -7,6 +7,7 @@ import Types from 'prop-types' import TRANSFER_TYPES from '../constants/transfer-types' import Base64 from '../serializers/base-64' import Leaf from './leaf' +import SlateTypes from '../utils/prop-types' import Void from './void' import getWindow from 'get-window' import scrollToSelection from '../utils/scroll-to-selection' @@ -35,13 +36,13 @@ class Node extends React.Component { */ static propTypes = { - block: Types.object, + block: SlateTypes.block, editor: Types.object.isRequired, - node: Types.object.isRequired, - parent: Types.object.isRequired, + node: SlateTypes.node.isRequired, + parent: SlateTypes.node.isRequired, readOnly: Types.bool.isRequired, - schema: Types.object.isRequired, - state: Types.object.isRequired + schema: SlateTypes.schema.isRequired, + state: SlateTypes.state.isRequired, } /** diff --git a/src/components/placeholder.js b/src/components/placeholder.js index 2387cc917..63f35abc8 100644 --- a/src/components/placeholder.js +++ b/src/components/placeholder.js @@ -2,6 +2,8 @@ import React from 'react' import Types from 'prop-types' +import SlateTypes from '../utils/prop-types' + /** * Placeholder. * @@ -20,10 +22,10 @@ class Placeholder extends React.Component { children: Types.any.isRequired, className: Types.string, firstOnly: Types.bool, - node: Types.object.isRequired, - parent: Types.object, - state: Types.object.isRequired, - style: Types.object + node: SlateTypes.node.isRequired, + parent: SlateTypes.node, + state: SlateTypes.state.isRequired, + style: Types.object, } /** @@ -33,7 +35,7 @@ class Placeholder extends React.Component { */ static defaultProps = { - firstOnly: true + firstOnly: true, } /** diff --git a/src/components/void.js b/src/components/void.js index 0dd1cf14f..042b050a2 100644 --- a/src/components/void.js +++ b/src/components/void.js @@ -6,6 +6,7 @@ import Types from 'prop-types' import Leaf from './leaf' import Mark from '../models/mark' import OffsetKey from '../utils/offset-key' +import SlateTypes from '../utils/prop-types' import { IS_FIREFOX } from '../constants/environment' /** @@ -31,14 +32,14 @@ class Void extends React.Component { */ static propTypes = { - block: Types.object, + block: SlateTypes.block, children: Types.any.isRequired, editor: Types.object.isRequired, - node: Types.object.isRequired, - parent: Types.object.isRequired, + node: SlateTypes.node.isRequired, + parent: SlateTypes.node.isRequired, readOnly: Types.bool.isRequired, - schema: Types.object.isRequired, - state: Types.object.isRequired, + schema: SlateTypes.schema.isRequired, + state: SlateTypes.state.isRequired, } /** @@ -49,7 +50,7 @@ class Void extends React.Component { state = { dragCounter: 0, - editable: false + editable: false, } /** diff --git a/src/models/block.js b/src/models/block.js index 52f739bdd..2cd522ae8 100644 --- a/src/models/block.js +++ b/src/models/block.js @@ -108,6 +108,17 @@ class Block extends Record(DEFAULTS) { return !!(value && value[MODEL_TYPES.BLOCK]) } + /** + * Check if a `value` is a block list. + * + * @param {Any} value + * @return {Boolean} + */ + + static isBlockList(value) { + return List.isList(value) && value.size > 0 && Block.isBlock(value.first()) + } + /** * Get the node's kind. * diff --git a/src/models/character.js b/src/models/character.js index 3c81826a4..7ac4a1a50 100644 --- a/src/models/character.js +++ b/src/models/character.js @@ -85,6 +85,17 @@ class Character extends Record(DEFAULTS) { return !!(value && value[MODEL_TYPES.CHARACTER]) } + /** + * Check if a `value` is a character list. + * + * @param {Any} value + * @return {Boolean} + */ + + static isCharacterList(value) { + return List.isList(value) && value.size > 0 && Character.isCharacter(value.first()) + } + /** * Deprecated. */ diff --git a/src/models/inline.js b/src/models/inline.js index 61187493d..ee1dbbf40 100644 --- a/src/models/inline.js +++ b/src/models/inline.js @@ -108,6 +108,17 @@ class Inline extends Record(DEFAULTS) { return !!(value && value[MODEL_TYPES.INLINE]) } + /** + * Check if a `value` is a list of inlines. + * + * @param {Any} value + * @return {Boolean} + */ + + static isInlineList(value) { + return List.isList(value) && value.size > 0 && Inline.isInline(value.first()) + } + /** * Get the node's kind. * diff --git a/src/models/mark.js b/src/models/mark.js index 0f62fbb21..bfcb4e5a3 100644 --- a/src/models/mark.js +++ b/src/models/mark.js @@ -118,6 +118,17 @@ class Mark extends Record(DEFAULTS) { return !!(value && value[MODEL_TYPES.MARK]) } + /** + * Check if a `value` is a set of marks. + * + * @param {Any} value + * @return {Boolean} + */ + + static isMarkSet(value) { + return Set.isSet(value) && value.size > 0 && Mark.isMark(value.first()) + } + /** * Get the kind. */ diff --git a/src/models/node.js b/src/models/node.js index 36a14f99f..7794069a2 100644 --- a/src/models/node.js +++ b/src/models/node.js @@ -113,6 +113,17 @@ class Node { ) } + /** + * Check if a `value` is a list of nodes. + * + * @param {Any} value + * @return {Boolean} + */ + + static isNodeList(value) { + return List.isList(value) && value.size > 0 && Node.isNode(value.first()) + } + /** * True if the node has both descendants in that order, false otherwise. The * order is depth-first, post-order. diff --git a/src/models/range.js b/src/models/range.js index 90c466aa3..42ce22dbd 100644 --- a/src/models/range.js +++ b/src/models/range.js @@ -3,7 +3,7 @@ import MODEL_TYPES from '../constants/model-types' import Character from './character' import Mark from './mark' import isPlainObject from 'is-plain-object' -import { Record, Set } from 'immutable' +import { List, Record, Set } from 'immutable' /** * Default properties. @@ -64,6 +64,17 @@ class Range extends Record(DEFAULTS) { return !!(value && value[MODEL_TYPES.RANGE]) } + /** + * Check if a `value` is a list of ranges. + * + * @param {Any} value + * @return {Boolean} + */ + + static isRangeList(value) { + return List.isList(value) && value.size > 0 && Range.isRange(value.first()) + } + /** * Get the node's kind. * diff --git a/src/models/text.js b/src/models/text.js index 449291329..6f354702c 100644 --- a/src/models/text.js +++ b/src/models/text.js @@ -94,6 +94,17 @@ class Text extends Record(DEFAULTS) { return !!(value && value[MODEL_TYPES.TEXT]) } + /** + * Check if a `value` is a listĀ of texts. + * + * @param {Any} value + * @return {Boolean} + */ + + static isTextList(value) { + return List.isList(value) && value.size > 0 && Text.isText(value.first()) + } + /** * Deprecated. */ diff --git a/src/utils/extend-selection.js b/src/utils/extend-selection.js index 1ec8f2596..00dfaa2c6 100644 --- a/src/utils/extend-selection.js +++ b/src/utils/extend-selection.js @@ -1,41 +1,49 @@ /** - * Extends the given selection to a given node and offset + * Extends a DOM `selection` to a given `el` and `offset`. * - * @param {Selection} selection Selection instance - * @param {Element} el Node to extend to - * @param {Number} offset Text offset to extend to - * @returns {Selection} Mutated Selection instance + * COMPAT: In IE11, `selection.extend` doesn't exist natively, so we have to + * polyfill it with this. (2017/09/06) + * + * https://gist.github.com/tyler-johnson/0a3e8818de3f115b2a2dc47468ac0099 + * + * @param {Selection} selection + * @param {Element} el + * @param {Number} offset + * @return {Selection} */ function extendSelection(selection, el, offset) { - // Use native method when possible - if (typeof selection.extend === 'function') return selection.extend(el, offset) + // Use native method whenever possible. + if (typeof selection.extend === 'function') { + return selection.extend(el, offset) + } - // See https://gist.github.com/tyler-johnson/0a3e8818de3f115b2a2dc47468ac0099 const range = document.createRange() const anchor = document.createRange() - anchor.setStart(selection.anchorNode, selection.anchorOffset) - const focus = document.createRange() + anchor.setStart(selection.anchorNode, selection.anchorOffset) focus.setStart(el, offset) const v = focus.compareBoundaryPoints(Range.START_TO_START, anchor) - if (v >= 0) { // Focus is after anchor + + // If the focus is after the anchor... + if (v >= 0) { range.setStart(selection.anchorNode, selection.anchorOffset) range.setEnd(el, offset) - } else { // Anchor is after focus + } + + // Otherwise, if the anchor if after the focus... + else { range.setStart(el, offset) range.setEnd(selection.anchorNode, selection.anchorOffset) } selection.removeAllRanges() selection.addRange(range) - return selection } - /** * Export. * diff --git a/src/utils/find-closest-node.js b/src/utils/find-closest-node.js index f55177acf..134c1bdde 100644 --- a/src/utils/find-closest-node.js +++ b/src/utils/find-closest-node.js @@ -2,6 +2,11 @@ /** * Find the closest ancestor of a DOM `element` that matches a given selector. * + * COMPAT: In IE11, the `Node.closest` method doesn't exist natively, so we + * have to polyfill it. (2017/09/06) + * + * https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill + * * @param {Element} node * @param {String} selector * @return {Element} @@ -10,10 +15,10 @@ function findClosestNode(node, selector) { if (typeof node.closest === 'function') return node.closest(selector) - // See https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill const matches = (node.document || node.ownerDocument).querySelectorAll(selector) - let i let parentNode = node + let i + do { i = matches.length while (--i >= 0 && matches.item(i) !== parentNode); diff --git a/src/utils/find-dom-node.js b/src/utils/find-dom-node.js index 1292b1be2..2702bee8d 100644 --- a/src/utils/find-dom-node.js +++ b/src/utils/find-dom-node.js @@ -10,9 +10,7 @@ function findDOMNode(node) { const el = window.document.querySelector(`[data-key="${node.key}"]`) if (!el) { - throw new Error(`Unable to find a DOM node for "${node.key}". This is -often because of forgetting to add \`props.attributes\` to a component -returned from \`renderNode\`.`) + throw new Error(`Unable to find a DOM node for "${node.key}". This is often because of forgetting to add \`props.attributes\` to a component returned from \`renderNode\`.`) } return el diff --git a/src/utils/get-html-from-native-paste.js b/src/utils/get-html-from-native-paste.js index 71efc853e..bdc40b140 100644 --- a/src/utils/get-html-from-native-paste.js +++ b/src/utils/get-html-from-native-paste.js @@ -1,3 +1,4 @@ + import { findDOMNode } from 'react-dom' /** @@ -7,28 +8,27 @@ import { findDOMNode } from 'react-dom' * is needed to return the HTML content. This solution was adapted from * http://stackoverflow.com/a/6804718. * - * @param {React.Component} component + * @param {Component} component * @param {Function} callback */ function getHtmlFromNativePaste(component, callback) { const el = findDOMNode(component) - // Clone contentedible element, move out of screen and set focus. + // Create an off-screen clone of the element and give it focus. const clone = el.cloneNode() clone.setAttribute('class', '') clone.setAttribute('style', 'position: fixed; left: -9999px') el.parentNode.insertBefore(clone, el) clone.focus() - // Clear call stack to let native paste behaviour occur on cloned element, - // then get what was pasted from the DOM and remove cloned element. + // Tick forward so the native paste behaviour occurs in cloned element and we + // can get what was pasted from the DOM. setTimeout(() => { if (clone.childElementCount > 0) { // If the node contains any child nodes, that is the HTML content. const html = clone.innerHTML clone.parentNode.removeChild(clone) - callback(html) } else { // Only plain text, no HTML. diff --git a/src/utils/prop-types.js b/src/utils/prop-types.js index c0111670b..12b9ff2b4 100644 --- a/src/utils/prop-types.js +++ b/src/utils/prop-types.js @@ -1,172 +1,84 @@ + +import { Set } from 'immutable' + import Block from '../models/block' +import Change from '../models/change' import Character from '../models/character' +import Data from '../models/data' import Document from '../models/document' +import History from '../models/history' import Inline from '../models/inline' import Mark from '../models/mark' +import Node from '../models/node' import Range from '../models/range' import Schema from '../models/schema' import Selection from '../models/selection' +import Stack from '../models/stack' import State from '../models/state' import Text from '../models/text' /** - * HOC Function that takes in a predicate prop type function, and allows an isRequired chain + * Create a prop type checker for Slate objects with `name` and `validate`. * - * @param {Function} predicate + * @param {String} name + * @param {Function} validate * @return {Function} */ -function createChainablePropType(predicate) { - function propType(props, propName, componentName) { - if (props[propName] == null) return - - return predicate(props, propName, componentName) +function create(name, validate) { + function check(isRequired, props, propName, componentName, location) { + const value = props[propName] + if (value == null && !isRequired) return null + if (value == null && isRequired) return new Error(`The ${location} \`${propName}\` is marked as required in \`${componentName}\`, but it was not supplied.`) + if (validate(value)) return null + return new Error(`Invalid ${location} \`${propName}\` supplied to \`${componentName}\`, expected a Slate \`${name}\` but received: ${value}`) } - propType.isRequired = (props, propName, componentName) => { - if (props[propName] == null) return new Error(`Required prop \`${propName}\` was not specified in \`${componentName}\`.`) + function propType(...args) { + return check(false, ...args) + } - return predicate(props, propName, componentName) + propType.isRequired = function (...args) { + return check(true, ...args) } return propType } /** - * Exported Slate proptype that checks if a prop is a Slate Block - * - * @type {Function} - */ - -const block = createChainablePropType( - (props, propName, componentName) => { - return !Block.isBlock(props[propName]) ? new Error(`${propName} in ${componentName} is not a Slate Block`) : null - } -) - -/** - * Exported Slate proptype that checks if a prop is a Slate Character - * - * @type {Function} - */ - -const character = createChainablePropType( - (props, propName, componentName) => { - return !Character.isCharacter(props[propName]) ? new Error(`${propName} in ${componentName} is not a Slate Character`) : null - } -) - -/** - * Exported Slate proptype that checks if a prop is a Slate Document - * - * @type {Function} - */ - -const document = createChainablePropType( - (props, propName, componentName) => { - return !Document.isDocument(props[propName]) ? new Error(`${propName} in ${componentName} is not a Slate Document`) : null - } -) - -/** - * Exported Slate proptype that checks if a prop is a Slate Inline - * - * @type {Function} - */ - -const inline = createChainablePropType( - (props, propName, componentName) => { - return !Inline.isInline(props[propName]) ? new Error(`${propName} in ${componentName} is not a Slate Inline`) : null - } -) - -/** - * Exported Slate proptype that checks if a prop is a Slate Mark - * - * @type {Function} - */ - -const mark = createChainablePropType( - (props, propName, componentName) => { - return !Mark.isMark(props[propName]) ? new Error(`${propName} in ${componentName} is not a Slate Mark`) : null - } -) - -/** - * Exported Slate proptype that checks if a prop is a Slate Range - * - * @type {Function} - */ - -const range = createChainablePropType( - (props, propName, componentName) => { - return !Range.isRange(props[propName]) ? new Error(`${propName} in ${componentName} is not a Slate Range`) : null - } -) - -/** - * Exported Slate proptype that checks if a prop is a Slate Schema - * - * @type {Function} - */ - -const schema = createChainablePropType( - (props, propName, componentName) => { - return !Schema.isSchema(props[propName]) ? new Error(`${propName} in ${componentName} is not a Slate Schema`) : null - } -) - -/** - * Exported Slate proptype that checks if a prop is a Slate Selection - * - * @type {Function} - */ - -const selection = createChainablePropType( - (props, propName, componentName) => { - return !Selection.isSelection(props[propName]) ? new Error(`${propName} in ${componentName} is not a Slate Selection`) : null - } -) - -/** - * Exported Slate proptype that checks if a prop is a Slate State - * - * @type {Function} - */ - -const state = createChainablePropType( - (props, propName, componentName) => { - return !State.isState(props[propName]) ? new Error(`${propName} in ${componentName} is not a Slate State ${props[propName]}`) : null - } -) - -/** - * Exported Slate proptype that checks if a prop is a Slate Text - * - * @type {Function} - */ - -const text = createChainablePropType( - (props, propName, componentName) => { - return !Text.isText(props[propName]) ? new Error(`${propName} in ${componentName} is not a Slate Text`) : null - } -) - -/** - * Exported Slate proptypes + * Prop type checkers. * * @type {Object} */ -export default { - block, - character, - document, - inline, - mark, - range, - schema, - selection, - state, - text, +const Types = { + block: create('Block', v => Block.isBlock(v)), + blocks: create('List', v => Block.isBlockList(v)), + change: create('Change', v => Change.isChange(v)), + character: create('Character', v => Character.isCharacter(v)), + characters: create('List', v => Character.isCharacterList(v)), + data: create('Data', v => Data.isData(v)), + document: create('Document', v => Document.isDocument(v)), + history: create('History', v => History.isHistory(v)), + inline: create('Inline', v => Inline.isInline(v)), + mark: create('Mark', v => Mark.isMark(v)), + marks: create('Set', v => (Set.isSet(v) && v.size === 0) || Mark.isMarkSet(v)), + node: create('Node', v => Node.isNode(v)), + nodes: create('List', v => Node.isNodeList(v)), + range: create('Range', v => Range.isRange(v)), + ranges: create('List', v => Range.isRangeList(v)), + schema: create('Schema', v => Schema.isSchema(v)), + selection: create('Selection', v => Selection.isSelection(v)), + stack: create('Stack', v => Stack.isStack(v)), + state: create('State', v => State.isState(v)), + text: create('Text', v => Text.isText(v)), + texts: create('List', v => Text.isTextList(v)), } + +/** + * Export. + * + * @type {Object} + */ + +export default Types diff --git a/src/utils/set-transfer-data.js b/src/utils/set-transfer-data.js index 3143cd676..95eecc580 100644 --- a/src/utils/set-transfer-data.js +++ b/src/utils/set-transfer-data.js @@ -1,5 +1,7 @@ + /** - * Set data on dataTransfer + * Set data with `type` and `content` on a `dataTransfer` object. + * * COMPAT: In Edge, custom types throw errors, so embed all non-standard * types in text/plain compound object. (2017/7/12) * @@ -13,23 +15,26 @@ function setTransferData(dataTransfer, type, content) { dataTransfer.setData(type, content) } catch (err) { const prefix = 'SLATE-DATA-EMBED::' - let obj = {} const text = dataTransfer.getData('text/plain') + let obj = {} - // If prefixed, assume embedded drag data + // If the existing plain text data is prefixed, it's Slate JSON data. if (text.substring(0, prefix.length) === prefix) { try { obj = JSON.parse(text.substring(prefix.length)) - } catch (err2) { - throw new Error('Unable to parse custom embedded drag data') + } catch (e) { + throw new Error('Failed to parse Slate data from `DataTransfer` object.') } - } else { + } + + // Otherwise, it's just set it as is. + else { obj['text/plain'] = text } obj[type] = content - - dataTransfer.setData('text/plain', `${prefix}${JSON.stringify(obj)}`) + const string = `${prefix}${JSON.stringify(obj)}` + dataTransfer.setData('text/plain', string) } }