diff --git a/lib/models/transforms.js b/lib/models/transforms.js index d0d9543ee..05717dcf3 100644 --- a/lib/models/transforms.js +++ b/lib/models/transforms.js @@ -7,6 +7,7 @@ import Inline from './inline' import Mark from './mark' import Selection from './selection' import Text from './text' +import typeOf from 'type-of' import uid from '../utils/uid' import { List, Map, Set } from 'immutable' @@ -22,7 +23,7 @@ const Transforms = { * Add a new `mark` to the characters at `range`. * * @param {Selection} range - * @param {Mark or String} mark + * @param {Mark or String or Object} mark * @return {Node} node */ @@ -216,33 +217,28 @@ const Transforms = { }, /** - * Insert a block `node` at `range`. + * Insert a `block` node at `range`. * * @param {Selection} range - * @param {Node} node + * @param {Block or String or Object} block * @return {Node} node */ - insertBlockAtRange(range, node) { - let doc = this + insertBlockAtRange(range, block) { + block = normalizeBlock(block) + let node = this // If expanded, delete the range first. if (range.isExpanded) { - doc = doc.deleteAtRange(range) + node = node.deleteAtRange(range) range = range.collapseToStart() } - // Allow for passing just a type string. - if (typeof node == 'string') node = { type: node } - - // Allow for passing a plain object of properties. - node = Block.create(node) - const { startKey, startOffset } = range - let startBlock = doc.getClosestBlock(startKey) - let parent = doc.getParent(startBlock) - let nodes = Block.createList([node]) - const isParent = parent == doc + let startBlock = node.getClosestBlock(startKey) + let parent = node.getParent(startBlock) + let nodes = Block.createList([block]) + const isParent = parent == node // If the start block is void, insert after it. if (startBlock.isVoid) { @@ -268,21 +264,21 @@ const Transforms = { // Otherwise, split the block and insert between. else { - doc = doc.splitBlockAtRange(range) - parent = doc.getParent(startBlock) - startBlock = doc.getClosestBlock(startKey) + node = node.splitBlockAtRange(range) + parent = node.getParent(startBlock) + startBlock = node.getClosestBlock(startKey) nodes = parent.nodes.takeUntil(n => n == startBlock) .push(startBlock) - .push(node) + .push(block) .concat(parent.nodes.skipUntil(n => n == startBlock).rest()) parent = parent.merge({ nodes }) } - doc = isParent + node = isParent ? parent - : doc.updateDescendant(parent) + : node.updateDescendant(parent) - return doc.normalize() + return node.normalize() }, /** @@ -385,51 +381,46 @@ const Transforms = { }, /** - * Insert an inline `node` at `range`. + * Insert an `inline` node at `range`. * * @param {Selection} range - * @param {Node} node + * @param {Inline or String or Object} inline * @return {Node} node */ - insertInlineAtRange(range, node) { - let doc = this + insertInlineAtRange(range, inline) { + inline = normalizeInline(inline) + let node = this // If expanded, delete the range first. if (range.isExpanded) { - doc = doc.deleteAtRange(range) + node = node.deleteAtRange(range) range = range.collapseToStart() } const { startKey, endKey, startOffset, endOffset } = range // If the range is inside a void, abort. - const block = doc.getClosestBlock(startKey) - if (block && block.isVoid) return doc + const startBlock = node.getClosestBlock(startKey) + if (startBlock && startBlock.isVoid) return node - const inline = doc.getClosestInline(startKey) - if (inline && inline.isVoid) return doc - - // Allow for passing a type string. - if (typeof node == 'string') node = { type: node } - - // Allow for passing a plain object of properties. - node = Inline.create(node) + const startInline = node.getClosestInline(startKey) + if (startInline && startInline.isVoid) return node // Split the text nodes at the cursor. - doc = doc.splitTextAtRange(range) + node = node.splitTextAtRange(range) - // Insert the node between the split text nodes. - const startText = doc.getDescendant(startKey) - let parent = doc.getParent(startKey) + // Insert the inline between the split text nodes. + const startText = node.getDescendant(startKey) + let parent = node.getParent(startKey) const nodes = parent.nodes.takeUntil(n => n == startText) .push(startText) - .push(node) + .push(inline) .concat(parent.nodes.skipUntil(n => n == startText).rest()) parent = parent.merge({ nodes }) - doc = doc.updateDescendant(parent) - return doc.normalize() + node = node.updateDescendant(parent) + return node.normalize() }, /** @@ -521,20 +512,10 @@ const Transforms = { */ setBlockAtRange(range, properties = {}) { + properties = normalizeProperties(properties) let node = this - - // Allow for properties to be a string `type` for convenience. - if (typeof properties == 'string') { - properties = { type: properties } - } - if (properties.data) { - properties.data = Data.create(properties.data) - } else { - delete properties.data - } - - // Update each of the blocks. const blocks = node.getBlocksAtRange(range) + blocks.forEach((block) => { block = block.merge(properties) node = node.updateDescendant(block) @@ -552,17 +533,11 @@ const Transforms = { */ setInlineAtRange(range, properties = {}) { + properties = normalizeProperties(properties) let node = this - - // Allow for properties to be a string `type` for convenience. - if (typeof properties == 'string') { - properties = { type: properties } - } - - // Update each of the inlines. const inlines = node.getInlinesAtRange(range) + inlines.forEach((inline) => { - if (properties.data) properties.data = Data.create(properties.data) inline = inline.merge(properties) node = node.updateDescendant(inline) }) @@ -579,15 +554,9 @@ const Transforms = { */ setNodeByKey(key, properties) { + properties = normalizeProperties(properties) let node = this let descendant = node.assertDescendant(key) - - // Allow for properties to be a string `type` for convenience. - if (typeof properties == 'string') properties = { type: properties } - - // Ensure that `data` is immutable. - if (properties.data) properties.data = Data.create(properties.data) - descendant = descendant.merge(properties) node = node.updateDescendant(descendant) return node @@ -1093,13 +1062,103 @@ function isInRange(index, text, range) { */ function normalizeMark(mark) { - if (typeof mark == 'string') { - return Mark.create({ type: mark }) - } else { - return Mark.create(mark) + if (mark instanceof Mark) return mark + + const type = typeOf(mark) + + switch (type) { + case 'string': + case 'object': { + return Mark.create(normalizeProperties(mark)) + } + default: { + throw new Error(`A \`mark\` argument must be a mark, an object or a string, but you passed: "${type}".`) + } } } +/** + * Normalize a `block` argument, which can be a string or plain object too. + * + * @param {Block or String or Object} block + * @return {Block} + */ + +function normalizeBlock(block) { + if (block instanceof Block) return block + + const type = typeOf(block) + + switch (type) { + case 'string': + case 'object': { + return Block.create(normalizeProperties(block)) + } + default: { + throw new Error(`A \`block\` argument must be a block, an object or a string, but you passed: "${type}".`) + } + } +} + +/** + * Normalize an `inline` argument, which can be a string or plain object too. + * + * @param {Inline or String or Object} inline + * @return {Inline} + */ + +function normalizeInline(inline) { + if (inline instanceof Inline) return inline + + const type = typeOf(inline) + + switch (type) { + case 'string': + case 'object': { + return Inline.create(normalizeProperties(inline)) + } + default: { + throw new Error(`An \`inline\` argument must be an inline, an object or a string, but you passed: "${type}".`) + } + } +} + +/** + * Normalize the `properties` of a node or mark, which can be either a type + * string or a dictionary of properties. If it's a dictionary, `data` is + * optional and shouldn't be set if null or undefined. + * + * @param {String or Object} properties + * @return {Object} + */ + +function normalizeProperties(properties = {}) { + const ret = {} + const type = typeOf(properties) + + switch (type) { + case 'string': { + ret.type = properties + break + } + case 'object': { + for (const key in properties) { + if (key == 'data') { + if (properties[key] != null) ret[key] = Data.create(properties[key]) + } else { + ret[key] = properties[key] + } + } + break + } + default: { + throw new Error(`A \`properties\` argument must be an object or a string, but you passed: "${type}".`) + } + } + + return ret +} + /** * Export. */ diff --git a/package.json b/package.json index c845f1e82..8c8ced673 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "keycode": "^2.1.2", "lodash": "^4.13.1", "react-portal": "^2.2.0", + "type-of": "^2.0.1", "ua-parser-js": "^0.7.10", "uid": "0.0.2" },