From ac59e94a15a35eaaed95c011ad22139ca010f079 Mon Sep 17 00:00:00 2001 From: Ian Storm Taylor Date: Fri, 22 Jul 2016 12:03:55 -0700 Subject: [PATCH] fix to use index instead of start and end in offset keys --- lib/components/content.js | 4 +- lib/components/leaf.js | 26 ++++++------- lib/components/text.js | 5 +-- lib/components/void.js | 9 ++--- lib/models/text.js | 20 ++++++++++ lib/utils/memoize.js | 4 +- lib/utils/offset-key.js | 82 +++++++++++++++++++++++++-------------- 7 files changed, 93 insertions(+), 57 deletions(-) diff --git a/lib/components/content.js b/lib/components/content.js index 0de38ca8d..4d997c639 100644 --- a/lib/components/content.js +++ b/lib/components/content.js @@ -379,8 +379,8 @@ class Content extends React.Component { } const { anchorNode, anchorOffset, focusNode, focusOffset } = native - const anchor = OffsetKey.findPoint(anchorNode, anchorOffset) - const focus = OffsetKey.findPoint(focusNode, focusOffset) + const anchor = OffsetKey.findPoint(anchorNode, anchorOffset, state) + const focus = OffsetKey.findPoint(focusNode, focusOffset, state) state = state .transform() diff --git a/lib/components/leaf.js b/lib/components/leaf.js index 04bc417a5..e958b6d1b 100644 --- a/lib/components/leaf.js +++ b/lib/components/leaf.js @@ -14,11 +14,10 @@ class Leaf extends React.Component { */ static propTypes = { - end: React.PropTypes.number.isRequired, + index: React.PropTypes.number.isRequired, marks: React.PropTypes.object.isRequired, node: React.PropTypes.object.isRequired, renderMark: React.PropTypes.func.isRequired, - start: React.PropTypes.number.isRequired, state: React.PropTypes.object.isRequired, text: React.PropTypes.string.isRequired }; @@ -31,18 +30,19 @@ class Leaf extends React.Component { */ shouldComponentUpdate(props) { - const { start, end, node, state } = props + const { index, node, state } = props const { selection } = state - const should = ( - selection.hasEdgeBetween(node, start, end) || - props.start != this.props.start || - props.end != this.props.end || + if ( + props.index != this.props.index || props.text != this.props.text || props.marks != this.props.marks - ) + ) { + return true + } - return should + const { start, end } = OffsetKey.findBounds(node.key, index, state) + return selection.hasEdgeBetween(node, start, end) } componentDidMount() { @@ -61,7 +61,8 @@ class Leaf extends React.Component { if (!selection.isFocused) return const { anchorOffset, focusOffset } = selection - const { node, start, end } = this.props + const { node, index } = this.props + const { start, end } = OffsetKey.findBounds(node.key, index, state) // If neither matches, the selection doesn't start or end here, so exit. const hasAnchor = selection.hasAnchorBetween(node, start, end) @@ -119,11 +120,10 @@ class Leaf extends React.Component { } render() { - const { node, text, marks, start, end, renderMark } = this.props + const { node, index, text, marks, renderMark } = this.props const offsetKey = OffsetKey.stringify({ key: node.key, - start, - end + index }) const style = marks.reduce((memo, mark) => { diff --git a/lib/components/text.js b/lib/components/text.js index 480359761..d0600ac04 100644 --- a/lib/components/text.js +++ b/lib/components/text.js @@ -84,16 +84,13 @@ class Text extends React.Component { const { node, renderMark, state } = this.props const text = range.text const marks = range.marks - const start = offset - const end = offset + text.length return ( diff --git a/lib/models/text.js b/lib/models/text.js index 78ad32ec4..6d80d51ad 100644 --- a/lib/models/text.js +++ b/lib/models/text.js @@ -1,6 +1,8 @@ import Character from './character' import Mark from './mark' +import groupByMarks from '../utils/group-by-marks' +import memoize from '../utils/memoize' import uid from '../utils/uid' import { List, Record } from 'immutable' @@ -101,6 +103,16 @@ class Text extends new Record(DEFAULTS) { }) } + /** + * Get the characters grouped by marks. + * + * @return {List} + */ + + getRanges() { + return groupByMarks(this.decorations || this.characters) + } + /** * Remove characters from the text node from `start` to `end`. * @@ -152,6 +164,14 @@ class Text extends new Record(DEFAULTS) { } +/** + * Memoize read methods. + */ + +memoize(Text.prototype, [ + 'getRanges' +]) + /** * Export. */ diff --git a/lib/utils/memoize.js b/lib/utils/memoize.js index 801301704..059cc4e0f 100644 --- a/lib/utils/memoize.js +++ b/lib/utils/memoize.js @@ -29,12 +29,12 @@ function memoize(object, properties) { object[property] = function (...args) { const keys = [property, ...args, LEAF] - const cache = this.cache = this.cache || new Map() + const cache = this.__cache = this.__cache || new Map() if (cache.hasIn(keys)) return cache.getIn(keys) const value = original.apply(this, args) - this.cache = cache.setIn(keys, value) + this.__cache = cache.setIn(keys, value) return value } } diff --git a/lib/utils/offset-key.js b/lib/utils/offset-key.js index b2516731e..4a7880aa2 100644 --- a/lib/utils/offset-key.js +++ b/lib/utils/offset-key.js @@ -3,7 +3,7 @@ * Offset key parser regex. */ -const PARSER = /^(\w+)(?::(\d+)-(\d+))?$/ +const PARSER = /^(\w+)(?:-(\d+))?$/ /** * Offset key attribute name. @@ -13,26 +13,51 @@ const ATTRIBUTE = 'data-offset-key' const SELECTOR = `[${ATTRIBUTE}]` /** - * From a `node`, find the closest parent's offset key. + * Find the start and end bounds from a node's `key` and `index`. * - * @param {Node} node + * @param {String} key + * @param {Number} index + * @param {State} state + * @return {Object} + */ + +function findBounds(key, index, state) { + const text = state.document.assertDescendant(key) + const ranges = text.getRanges() + const range = ranges.get(index) + const start = ranges + .slice(0, index) + .reduce((memo, r) => { + return memo += r.text.length + }, 0) + + return { + start, + end: start + range.text.length + } +} + +/** + * From a `element`, find the closest parent's offset key. + * + * @param {Element} element * @return {String} key */ -function findKey(node) { - if (node.nodeType == 3) node = node.parentNode +function findKey(element) { + if (element.nodeType == 3) element = element.parentNode // If a parent with an offset key exists, use it. - const parent = node.closest(SELECTOR) + const parent = element.closest(SELECTOR) if (parent) return parent.getAttribute(ATTRIBUTE) // Otherwise, if a child with an offset key exists, use it. - const child = node.querySelector(SELECTOR) + const child = element.querySelector(SELECTOR) if (child) return child.getAttribute(ATTRIBUTE) // Otherwise, move up the tree looking for cousin offset keys in parents. - while (node = node.parentNode) { - const cousin = node.querySelector(SELECTOR) + while (element = element.parentNode) { + const cousin = element.querySelector(SELECTOR) if (cousin) return cousin.getAttribute(ATTRIBUTE) } @@ -41,24 +66,26 @@ function findKey(node) { } /** - * From a `node` and `offset`, find the closest parent's point. + * Find the selection point from an `element`, `offset`, and list of `ranges`. * - * @param {Node} node + * @param {Element} element * @param {Offset} offset + * @param {State} state * @return {String} key */ -function findPoint(node, offset) { - const key = findKey(node) - const parsed = parse(key) +function findPoint(element, offset, state) { + const offsetKey = findKey(element) + const { key, index } = parse(offsetKey) + const { start, end } = findBounds(key, index, state) - // Don't let the offset be outside the start and end bounds. - offset = parsed.start + offset - offset = Math.max(offset, parsed.start) - offset = Math.min(offset, parsed.end) + // Don't let the offset be outside of the start and end bounds. + offset = start + offset + offset = Math.max(offset, start) + offset = Math.min(offset, end) return { - key: parsed.key, + key, offset } } @@ -67,21 +94,16 @@ function findPoint(node, offset) { * Parse an offset key `string`. * * @param {String} string - * @return {Object} parsed + * @return {Object} */ function parse(string) { const matches = PARSER.exec(string) if (!matches) throw new Error(`Invalid offset key string "${string}".`) - - let [ original, key, start, end ] = matches - start = parseInt(start, 10) - end = parseInt(end, 10) - + const [ original, key, index ] = matches return { key, - start, - end + index } } @@ -90,13 +112,12 @@ function parse(string) { * * @param {Object} object * @property {String} key - * @property {Number} start - * @property {Number} end + * @property {Number} index * @return {String} key */ function stringify(object) { - return `${object.key}:${object.start}-${object.end}` + return `${object.key}-${object.index}` } /** @@ -104,6 +125,7 @@ function stringify(object) { */ export default { + findBounds, findKey, findPoint, parse,