Skip to content

Commit

Permalink
Change Range#extend and Position#move to move multiple units
Browse files Browse the repository at this point in the history
  • Loading branch information
bantic committed Apr 26, 2016
1 parent be85869 commit bff7bc7
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 48 deletions.
2 changes: 1 addition & 1 deletion src/js/editor/event-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export default class EventManager {
case key.isHorizontalArrow():
let newRange;
if (key.isShift()) {
newRange = range.extend(key.direction);
newRange = range.extend(key.direction * 1);
} else {
newRange = range.move(key.direction);
}
Expand Down
28 changes: 16 additions & 12 deletions src/js/utils/cursor/position.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { isTextNode } from 'mobiledoc-kit/utils/dom-utils';
import { DIRECTION } from 'mobiledoc-kit/utils/key';
import assert from 'mobiledoc-kit/utils/assert';
import {
HIGH_SURROGATE_RANGE,
Expand Down Expand Up @@ -148,25 +147,29 @@ const Position = class Position {
/**
* Move the position 1 unit in `direction`.
*
* @param {Direction} direction to move
* @return {Position|null} Return a new position one unit in the given
* direction or null if it is not possible to move that direction
* @param {Number} units to move. > 0 moves right, < 0 moves left
* @return {Position} Return a new position one unit in the given
* direction. If the position is moving left and at the beginning of the post,
* the same position will be returned. Same if the position is moving right and
* at the end of the post.
*/
move(direction) {
switch (direction) {
case DIRECTION.BACKWARD:
return this.moveLeft();
case DIRECTION.FORWARD:
return this.moveRight();
default:
assert('Must pass a valid direction to Position.move', false);
move(units) {
assert('Must pass integer to Position#move', typeof units === 'number');

if (units < 0) {
return this.moveLeft().move(++units);
} else if (units > 0) {
return this.moveRight().move(--units);
} else {
return this;
}
}

/**
* The position to the left of this position.
* If this position is the post's headPosition it returns itself.
* @return {Position}
* @private
*/
moveLeft() {
if (this.isHead()) {
Expand All @@ -188,6 +191,7 @@ const Position = class Position {
* The position to the right of this position.
* If this position is the post's tailPosition it returns itself.
* @return {Position}
* @private
*/
moveRight() {
if (this.isTail()) {
Expand Down
20 changes: 15 additions & 5 deletions src/js/utils/cursor/range.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Position from './position';
import { DIRECTION } from '../key';
import assert from 'mobiledoc-kit/utils/assert';

/**
* A logical range of a {@link Post}.
Expand Down Expand Up @@ -76,19 +77,25 @@ class Range {
* Expands the range 1 unit in the given direction
* If the range is expandable in the given direction, always returns a
* non-collapsed range.
* @param {Direction} direction
* @param {Number} units If units is > 0, the range is extended to the right,
* otherwise range is extended to the left.
* @return {Range}
* @public
*/
extend(direction) {
extend(units) {
assert(`Must pass integer to Range#extend`, typeof units === 'number');

if (units === 0) { return this; }

let { head, tail, direction: currentDirection } = this;
switch (currentDirection) {
case DIRECTION.FORWARD:
return new Range(head, tail.move(direction), currentDirection);
return new Range(head, tail.move(units), currentDirection);
case DIRECTION.BACKWARD:
return new Range(head.move(direction), tail, currentDirection);
return new Range(head.move(units), tail, currentDirection);
default:
return new Range(head, tail, direction).extend(direction);
let newDirection = units > 0 ? DIRECTION.FORWARD : DIRECTION.BACKWARD;
return new Range(head, tail, newDirection).extend(units);
}
}

Expand All @@ -102,6 +109,9 @@ class Range {
* @public
*/
move(direction) {
assert(`Must pass DIRECTION.FORWARD (${DIRECTION.FORWARD}) or DIRECTION.BACKWARD (${DIRECTION.BACKWARD}) to Range#move`,
direction === DIRECTION.FORWARD || direction === DIRECTION.BACKWARD);

let { focusedPosition, isCollapsed } = this;

if (isCollapsed) {
Expand Down
61 changes: 39 additions & 22 deletions tests/unit/utils/cursor-position-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ test('#move moves forward and backward in markup section', (assert) => {
let rightPosition = new Position(post.sections.head, 'abc'.length);
let leftPosition = new Position(post.sections.head, 'a'.length);

assert.positionIsEqual(position.moveRight(), rightPosition, 'right position');
assert.positionIsEqual(position.moveLeft(), leftPosition, 'left position');
assert.positionIsEqual(position.move(1), rightPosition, 'right position');
assert.positionIsEqual(position.move(-1), leftPosition, 'left position');
});

test('#move is emoji-aware', (assert) => {
Expand All @@ -38,20 +38,20 @@ test('#move is emoji-aware', (assert) => {
assert.equal(marker.length, 'a'.length + 2 + 'z'.length); // precond
let position = post.sections.head.headPosition();

position = position.moveRight();
position = position.move(1);
assert.equal(position.offset, 1);
position = position.moveRight();
position = position.move(1);
assert.equal(position.offset, 3); // l-to-r across emoji
position = position.moveRight();
position = position.move(1);
assert.equal(position.offset, 4);

position = position.moveLeft();
position = position.move(-1);
assert.equal(position.offset, 3);

position = position.moveLeft(); // r-to-l across emoji
position = position.move(-1); // r-to-l across emoji
assert.equal(position.offset, 1);

position = position.moveLeft();
position = position.move(-1);
assert.equal(position.offset, 0);
});

Expand All @@ -69,8 +69,8 @@ test('#move moves forward and backward between markup sections', (assert) => {
let aTail = post.sections.head.tailPosition();
let cHead = post.sections.tail.headPosition();

assert.positionIsEqual(midHead.moveLeft(), aTail, 'left to prev section');
assert.positionIsEqual(midTail.moveRight(), cHead, 'right to next section');
assert.positionIsEqual(midHead.move(-1), aTail, 'left to prev section');
assert.positionIsEqual(midTail.move(1), cHead, 'right to next section');
});

test('#move from one nested section to another', (assert) => {
Expand All @@ -88,8 +88,8 @@ test('#move from one nested section to another', (assert) => {
let aTail = post.sections.head.items.head.tailPosition();
let cHead = post.sections.tail.items.tail.headPosition();

assert.positionIsEqual(midHead.moveLeft(), aTail, 'left to prev section');
assert.positionIsEqual(midTail.moveRight(), cHead, 'right to next section');
assert.positionIsEqual(midHead.move(-1), aTail, 'left to prev section');
assert.positionIsEqual(midTail.move(1), cHead, 'right to next section');
});

test('#move from last nested section to next un-nested section', (assert) => {
Expand All @@ -107,8 +107,8 @@ test('#move from last nested section to next un-nested section', (assert) => {
let aTail = post.sections.head.tailPosition();
let cHead = post.sections.tail.headPosition();

assert.positionIsEqual(midHead.moveLeft(), aTail, 'left to prev section');
assert.positionIsEqual(midTail.moveRight(), cHead, 'right to next section');
assert.positionIsEqual(midHead.move(-1), aTail, 'left to prev section');
assert.positionIsEqual(midTail.move(1), cHead, 'right to next section');
});

test('#move across and beyond card section', (assert) => {
Expand All @@ -126,10 +126,10 @@ test('#move across and beyond card section', (assert) => {
let aTail = post.sections.head.tailPosition();
let cHead = post.sections.tail.headPosition();

assert.positionIsEqual(midHead.moveLeft(), aTail, 'left to prev section');
assert.positionIsEqual(midTail.moveRight(), cHead, 'right to next section');
assert.positionIsEqual(midHead.moveRight(), midTail, 'move l-to-r across card');
assert.positionIsEqual(midTail.moveLeft(), midHead, 'move r-to-l across card');
assert.positionIsEqual(midHead.move(-1), aTail, 'left to prev section');
assert.positionIsEqual(midTail.move(1), cHead, 'right to next section');
assert.positionIsEqual(midHead.move(1), midTail, 'move l-to-r across card');
assert.positionIsEqual(midTail.move(-1), midHead, 'move r-to-l across card');
});

test('#move across and beyond card section into list section', (assert) => {
Expand All @@ -153,8 +153,8 @@ test('#move across and beyond card section into list section', (assert) => {
let aTail = post.sections.head.items.tail.tailPosition();
let cHead = post.sections.tail.items.head.headPosition();

assert.positionIsEqual(midHead.moveLeft(), aTail, 'left to prev section');
assert.positionIsEqual(midTail.moveRight(), cHead, 'right to next section');
assert.positionIsEqual(midHead.move(-1), aTail, 'left to prev section');
assert.positionIsEqual(midTail.move(1), cHead, 'right to next section');
});

test('#move left at headPosition or right at tailPosition returns self', (assert) => {
Expand All @@ -167,8 +167,25 @@ test('#move left at headPosition or right at tailPosition returns self', (assert

let head = post.headPosition(),
tail = post.tailPosition();
assert.positionIsEqual(head.moveLeft(), head, 'head move left is head');
assert.positionIsEqual(tail.moveRight(), tail, 'tail move right is tail');
assert.positionIsEqual(head.move(-1), head, 'head move left is head');
assert.positionIsEqual(tail.move(1), tail, 'tail move right is tail');
});

test('#move can move multiple units', (assert) => {
let post = Helpers.postAbstract.build(({post, markupSection, marker}) => {
return post([
markupSection('p', [marker('abc')]),
markupSection('p', [marker('def')])
]);
});

let head = post.headPosition(),
tail = post.tailPosition();

assert.positionIsEqual(head.move('abc'.length + 1 + 'def'.length), tail, 'head can move to tail');
assert.positionIsEqual(tail.move(-1 * ('abc'.length + 1 + 'def'.length)), head, 'tail can move to head');

assert.positionIsEqual(head.move(0), head, 'move(0) is no-op');
});

test('#fromNode when node is marker text node', (assert) => {
Expand Down
79 changes: 71 additions & 8 deletions tests/unit/utils/cursor-range-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,81 @@ test('#extend expands range in direction', (assert) => {
let nonCollapsedRangeBackward = Range.create(section, 0, section, 2, BACKWARD);

assert.ok(collapsedRange.isCollapsed, 'precond - collapsedRange.isCollapsed');
assert.rangeIsEqual(collapsedRange.extend(FORWARD),
collapsedRangeForward,
assert.rangeIsEqual(collapsedRange.extend(FORWARD), collapsedRangeForward,
'collapsedRange extend forward');
assert.rangeIsEqual(collapsedRange.extend(BACKWARD),
collapsedRangeBackward,
assert.rangeIsEqual(collapsedRange.extend(BACKWARD), collapsedRangeBackward,
'collapsedRange extend backward');

assert.ok(!nonCollapsedRange.isCollapsed, 'precond -nonCollapsedRange.isCollapsed');
assert.rangeIsEqual(nonCollapsedRange.extend(FORWARD),
nonCollapsedRangeForward,
assert.rangeIsEqual(nonCollapsedRange.extend(FORWARD), nonCollapsedRangeForward,
'nonCollapsedRange extend forward');
assert.rangeIsEqual(nonCollapsedRange.extend(BACKWARD),
nonCollapsedRangeBackward,
assert.rangeIsEqual(nonCollapsedRange.extend(BACKWARD), nonCollapsedRangeBackward,
'nonCollapsedRange extend backward');
});

test('#extend expands range in multiple units in direction', (assert) => {
let post = Helpers.postAbstract.build(({post, markupSection, marker}) => {
return post([
markupSection('p', [marker('abcd')]),
markupSection('p', [marker('1234')])
]);
});

let headSection = post.sections.head;
let tailSection = post.sections.tail;

// FORWARD
let collapsedRange = Range.create(headSection, 0);
let nonCollapsedRange = Range.create(headSection, 0, headSection, 1);
assert.rangeIsEqual(collapsedRange.extend(FORWARD*2),
Range.create(headSection, 0, headSection, 2, FORWARD),
'extend forward 2');

assert.rangeIsEqual(collapsedRange.extend(FORWARD*(('abcd' + '12').length+1)),
Range.create(headSection, 0, tailSection, 2, FORWARD),
'extend forward across sections');

assert.rangeIsEqual(nonCollapsedRange.extend(FORWARD*2),
Range.create(headSection, 0, headSection, 3, FORWARD),
'extend non-collapsed forward 2');

assert.rangeIsEqual(nonCollapsedRange.extend(FORWARD*(('bcd' + '12').length+1)),
Range.create(headSection, 0, tailSection, 2, FORWARD),
'extend non-collapsed across sections');

// BACKWARD
collapsedRange = Range.create(tailSection, '1234'.length);
nonCollapsedRange = Range.create(tailSection, '12'.length, tailSection, '1234'.length);
assert.rangeIsEqual(collapsedRange.extend(BACKWARD*'12'.length),
Range.create(tailSection, '12'.length, tailSection, '1234'.length, BACKWARD),
'extend backward 2');

assert.rangeIsEqual(collapsedRange.extend(BACKWARD*(('1234' + 'cd').length+1)),
Range.create(headSection, 'ab'.length, tailSection, '1234'.length, BACKWARD),
'extend backward across sections');

assert.rangeIsEqual(nonCollapsedRange.extend(BACKWARD*2),
Range.create(tailSection, 0, tailSection, '1234'.length, BACKWARD),
'extend non-collapsed backward 2');

assert.rangeIsEqual(nonCollapsedRange.extend(BACKWARD*(('bcd' + '12').length+1)),
Range.create(headSection, 'a'.length, tailSection, '1234'.length, BACKWARD),
'extend non-collapsed backward across sections');
});

test('#extend(0) returns same range', (assert) => {
let post = Helpers.postAbstract.build(({post, markupSection, marker}) => {
return post([
markupSection('p', [marker('abcd')]),
markupSection('p', [marker('1234')])
]);
});

let headSection = post.sections.head;

let collapsedRange = Range.create(headSection, 0);
let nonCollapsedRange = Range.create(headSection, 0, headSection, 1);

assert.rangeIsEqual(collapsedRange.extend(0), collapsedRange, 'extending collapsed range 0 is no-op');
assert.rangeIsEqual(nonCollapsedRange.extend(0), nonCollapsedRange, 'extending non-collapsed range 0 is no-op');
});

0 comments on commit bff7bc7

Please sign in to comment.