diff --git a/lib/models/node.js b/lib/models/node.js index 4a88eec53..e74e8fd19 100644 --- a/lib/models/node.js +++ b/lib/models/node.js @@ -488,6 +488,31 @@ const Node = { .reduce((marks, char) => marks.union(char.marks), marks) }, + /** + * Get the block node before a descendant text node by `key`. + * + * @param {String or Node} key + * @return {Node or Null} node + */ + + getNextBlock(key) { + key = normalizeKey(key) + const child = this.getDescendant(key) + let last + + if (child.kind == 'block') { + last = child.getTextNodes().last() + } else { + const block = this.getClosestBlock(key) + last = block.getTextNodes().last() + } + + const next = this.getNextText(last) + if (!next) return null + + return this.getClosestBlock(next) + }, + /** * Get the node after a descendant by `key`. * diff --git a/lib/models/state.js b/lib/models/state.js index c9e70484b..ef00445a7 100644 --- a/lib/models/state.js +++ b/lib/models/state.js @@ -557,6 +557,161 @@ class State extends Record(DEFAULTS) { if (!previous) return state selection = selection.moveToStartOf(previous) + selection = selection.normalize(document) + state = state.merge({ selection }) + return state + } + + /** + * Move the selection to the end of the previous block. + * + * @return {State} state + */ + + moveToEndOfPreviousBlock() { + let state = this + let { document, selection } = state + let blocks = document.getBlocksAtRange(selection) + let block = blocks.first() + if (!block) return state + + let previous = document.getPreviousBlock(block) + if (!previous) return state + + selection = selection.moveToEndOf(previous) + selection = selection.normalize(document) + state = state.merge({ selection }) + return state + } + + /** + * Move the selection to the start of the next block. + * + * @return {State} state + */ + + moveToStartOfNextBlock() { + let state = this + let { document, selection } = state + let blocks = document.getBlocksAtRange(selection) + let block = blocks.last() + if (!block) return state + + let next = document.getNextBlock(block) + if (!next) return state + + selection = selection.moveToStartOf(next) + selection = selection.normalize(document) + state = state.merge({ selection }) + return state + } + + /** + * Move the selection to the end of the next block. + * + * @return {State} state + */ + + moveToEndOfNextBlock() { + let state = this + let { document, selection } = state + let blocks = document.getBlocksAtRange(selection) + let block = blocks.last() + if (!block) return state + + let next = document.getNextBlock(block) + if (!next) return state + + selection = selection.moveToEndOf(next) + selection = selection.normalize(document) + state = state.merge({ selection }) + return state + } + + /** + * Move the selection to the start of the previous text. + * + * @return {State} state + */ + + moveToStartOfPreviousText() { + let state = this + let { document, selection } = state + let texts = document.getTextsAtRange(selection) + let text = texts.first() + if (!text) return state + + let previous = document.getPreviousText(text) + if (!previous) return state + + selection = selection.moveToStartOf(previous) + selection = selection.normalize(document) + state = state.merge({ selection }) + return state + } + + /** + * Move the selection to the end of the previous text. + * + * @return {State} state + */ + + moveToEndOfPreviousText() { + let state = this + let { document, selection } = state + let texts = document.getTextsAtRange(selection) + let text = texts.first() + if (!text) return state + + let previous = document.getPreviousText(text) + if (!previous) return state + + selection = selection.moveToEndOf(previous) + selection = selection.normalize(document) + state = state.merge({ selection }) + return state + } + + /** + * Move the selection to the start of the next text. + * + * @return {State} state + */ + + moveToStartOfNextText() { + let state = this + let { document, selection } = state + let texts = document.getTextsAtRange(selection) + let text = texts.last() + if (!text) return state + + let next = document.getNextText(text) + if (!next) return state + + selection = selection.moveToStartOf(next) + selection = selection.normalize(document) + state = state.merge({ selection }) + return state + } + + /** + * Move the selection to the end of the next text. + * + * @return {State} state + */ + + moveToEndOfNextText() { + let state = this + let { document, selection } = state + let texts = document.getTextsAtRange(selection) + let text = texts.last() + if (!text) return state + + let next = document.getNextText(text) + if (!next) return state + + selection = selection.moveToEndOf(next) + selection = selection.normalize(document) state = state.merge({ selection }) return state } diff --git a/lib/models/transform.js b/lib/models/transform.js index f77dee2c2..cf74cde84 100644 --- a/lib/models/transform.js +++ b/lib/models/transform.js @@ -76,6 +76,13 @@ const STATE_TRANSFORMS = [ 'insertText', 'mark', 'moveToStartOfPreviousBlock', + 'moveToEndOfPreviousBlock', + 'moveToStartOfNextBlock', + 'moveToEndOfNextBlock', + 'moveToStartOfPreviousText', + 'moveToEndOfPreviousText', + 'moveToStartOfNextText', + 'moveToEndOfNextText', 'setBlock', 'setInline', 'splitBlock', diff --git a/lib/plugins/core.js b/lib/plugins/core.js index e57fbf070..f0d285f24 100644 --- a/lib/plugins/core.js +++ b/lib/plugins/core.js @@ -63,6 +63,38 @@ export default { : transform.deleteForward().apply() } + case 'up': { + if (state.isExpanded) return + const first = state.blocks.first() + if (!first || !first.isVoid) return + e.preventDefault() + return transform.moveToEndOfPreviousBlock().apply() + } + + case 'down': { + if (state.isExpanded) return + const first = state.blocks.first() + if (!first || !first.isVoid) return + e.preventDefault() + return transform.moveToStartOfNextBlock().apply() + } + + case 'left': { + if (state.isExpanded) return + const node = state.blocks.first() || state.inlines.first() + if (!node || !node.isVoid) return + e.preventDefault() + return transform.moveToEndOfPreviousText().apply() + } + + case 'right': { + if (state.isExpanded) return + const node = state.blocks.first() || state.inlines.first() + if (!node || !node.isVoid) return + e.preventDefault() + return transform.moveToStartOfNextText().apply() + } + case 'y': { if (!isWindowsCommand(e)) return return transform.redo()