Skip to content

Commit

Permalink
Refined Atom behaviors wrt parsing, rerendering
Browse files Browse the repository at this point in the history
  * Atom splitting/cloning for section edits
  * Properly parse and render atom markup
  * Handle typing at end of atom better. wip. needs more testing
  * Change cursor#_findNodeForPosition to target the textnode after an
    atom's ending zwnj when there is one
  * Also changes the editor keyDown handler to reposition the cursor onto
    that next text node
  * Changes to dom parsing to read text content out of the zwnj on either
    side of an atom and merge it into the before/after marker (or create new
    marker(s) to accept that text)
  * Changes lifecycle callbacks #addCallbackOnce method to remove the
    once-added callback after flushing that queue
  * Add tests for content in atom headTextNode with nothing or marker before it
  * Test for atom headTextNode with atom before it, tailTextNode with nothing after it
  * Test tailTextNode with atom, marker after it
  * Tests for markupSection#length
  * Changes to fix small atom bugs:
  * Change _findCursorForPosition to focus on atom markers properly
  * DOM parser marks sections dirty when adding a new marker
  * postEditor#deleteBackwardFrom removes atom marker appropriately
  * Fix section.markersFor bug that truncated atom values, ctr-A, ctr-E
  * Fix bug with adding markup to a single atom
  * Add tests for complex atom (re-)rendering, refactor editor-dom renderer
  * Test that atoms are not coalesced. change marker.isEmpty -> marker.isBlank usage
  * Add atom#splitAtOffset
  * Remove marker#join
  * Add `canJoin` to atom and marker
  * Tests for inserting text in/around atoms, and reparsing atoms
  * Test for arrow movement across atoms
  * Test that selected text that includes/surrounds atoms can be deleted
  * Tests for position#move, lifecycle callbacks, other small cleanup
  • Loading branch information
mixonic authored and bantic committed Feb 4, 2016
1 parent e199416 commit b5957a6
Show file tree
Hide file tree
Showing 35 changed files with 1,364 additions and 405 deletions.
14 changes: 13 additions & 1 deletion src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ class Editor {
if (!isCollapsed) {
nextPosition = postEditor.deleteRange(range);
}

let isMarkerable = range.head.section.isMarkerable;
if (isMarkerable &&
(key.isTab() || key.isSpace())
Expand All @@ -670,6 +671,17 @@ class Editor {
shouldPreventDefault = true;
nextPosition = postEditor.insertText(nextPosition, toInsert);
}

if (nextPosition.marker && nextPosition.marker.isAtom) {
// ensure that the cursor is properly repositioned one character forward
// after typing on either side of an atom
this.addCallbackOnce(CALLBACK_QUEUES.DID_REPARSE, () => {
let position = nextPosition.move(DIRECTION.FORWARD);
let nextRange = new Range(position);

this.run(postEditor => postEditor.setRange(nextRange));
});
}
if (nextPosition && nextPosition !== range.head) {
postEditor.setRange(new Range(nextPosition));
}
Expand Down Expand Up @@ -758,7 +770,7 @@ class Editor {

// @private
_setCardMode(cardSection, mode) {
const renderNode = this._renderTree.getRenderNode(cardSection);
const renderNode = cardSection.renderNode;
if (renderNode && renderNode.isRendered) {
const cardNode = renderNode.cardNode;
cardNode[mode]();
Expand Down
20 changes: 20 additions & 0 deletions src/js/editor/key-commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,26 @@ export const DEFAULT_KEY_COMMANDS = [{
postEditor.setRange(new Range(nextPosition));
});
}
}, {
// FIXME restrict to OS X only?
str: 'CTRL+A',
run(editor) {
let range = editor.cursor.offsets;
let {head: {section}} = range;
editor.run(postEditor => {
postEditor.setRange(new Range(section.headPosition()));
});
}
}, {
// FIXME restrict to OS X only?
str: 'CTRL+E',
run(editor) {
let range = editor.cursor.offsets;
let {tail: {section}} = range;
editor.run(postEditor => {
postEditor.setRange(new Range(section.tailPosition()));
});
}
}, {
str: 'META+K',
run(editor) {
Expand Down
49 changes: 20 additions & 29 deletions src/js/editor/post.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Position from '../utils/cursor/position';
import { isArrayEqual, forEach, filter } from '../utils/array-utils';
import { forEach, filter } from '../utils/array-utils';
import { DIRECTION } from '../utils/key';
import LifecycleCallbacksMixin from '../utils/lifecycle-callbacks';
import mixin from '../utils/mixin';
Expand Down Expand Up @@ -193,14 +193,14 @@ class PostEditor {

_coalesceMarkers(section) {
if (section.isMarkerable) {
this._removeEmptyMarkers(section);
this._removeBlankMarkers(section);
this._joinSimilarMarkers(section);
}
}

_removeEmptyMarkers(section) {
_removeBlankMarkers(section) {
forEach(
filter(section.markers, m => m.isEmpty),
filter(section.markers, m => m.isBlank),
m => this.removeMarker(m)
);
}
Expand All @@ -212,10 +212,7 @@ class PostEditor {
while (marker && marker.next) {
nextMarker = marker.next;

if (
marker.type === nextMarker.type &&
isArrayEqual(marker.markups, nextMarker.markups)
) {
if (marker.canJoin(nextMarker)) {
nextMarker.value = marker.value + nextMarker.value;
this._markDirty(nextMarker);
this.removeMarker(marker);
Expand Down Expand Up @@ -401,19 +398,17 @@ class PostEditor {
* @private
*/
_deleteForwardFrom(position) {
const { section, offset } = position;
const { section } = position;

if (section.isBlank) {
// remove this section, focus on start of next markerable section
const nextPosition = position.clone();
const next = section.immediatelyNextMarkerableSection();
if (next) {
this.removeSection(section);
nextPosition.section = next;
nextPosition.offset = 0;
position = next.headPosition();
}
return nextPosition;
} else if (offset === section.length) {
return position;
} else if (position.isTail()) {
if (section.isCardSection) {
if (section.next && section.next.isBlank) {
this.removeSection(section.next);
Expand All @@ -424,12 +419,10 @@ class PostEditor {
return this._joinPositionToNextSection(position);
}
} else {
if (section.isCardSection) {
if (offset === 0) {
let newSection = this.builder.createMarkupSection();
this.replaceSection(section, newSection);
return newSection.headPosition();
}
if (section.isCardSection && position.isHead()) {
let newSection = this.builder.createMarkupSection();
this.replaceSection(section, newSection);
return newSection.headPosition();
} else {
return this._deleteForwardFromMarkerPosition(position.markerPosition);
}
Expand All @@ -438,7 +431,6 @@ class PostEditor {

_joinPositionToNextSection(position) {
const { section } = position;
let nextPosition = position.clone();

assert('Cannot join non-markerable section to next section',
section.isMarkerable);
Expand All @@ -450,12 +442,11 @@ class PostEditor {
this.removeSection(next);
}

return nextPosition;
return position;
}

/**
* delete 1 character forward from the markerPosition, which in turn is
* a {marker, offset} object.
* delete 1 character forward from the markerPosition
*
* @method _deleteForwardFromMarkerPosition
* @param {Object} markerPosition {marker, offset}
Expand Down Expand Up @@ -484,7 +475,7 @@ class PostEditor {
this.removeSection(nextSection);
}
}
} else if (marker.length === 1 && offset === 0) {
} else if (marker.isAtom) { // atoms are deleted "atomically"
this.removeMarker(marker);
} else {
marker.deleteValueAtOffset(offset);
Expand All @@ -502,9 +493,9 @@ class PostEditor {
* @private
*/
_deleteBackwardFrom(position) {
const { section, offset:sectionOffset } = position;
const { section } = position;

if (sectionOffset === 0) {
if (position.isHead()) {
if (section.isCardSection) {
if (section.prev && section.prev.isBlank) {
this.removeSection(section.prev);
Expand All @@ -515,7 +506,7 @@ class PostEditor {
}
}

// if position is end of a card, replace the card with a markup section
// if position is end of a card, replace the card with a blank markup section
if (section.isCardSection) {
let newSection = this.builder.createMarkupSection();
this.replaceSection(section, newSection);
Expand All @@ -527,7 +518,7 @@ class PostEditor {
const { marker, offset:markerOffset } = position.markerPosition;
const offsetToDeleteAt = markerOffset - 1;

if (marker.length === 1 && offsetToDeleteAt === 0) {
if (marker.isAtom) {
this.removeMarker(marker);
} else {
marker.deleteValueAtOffset(offsetToDeleteAt);
Expand Down
26 changes: 11 additions & 15 deletions src/js/models/_markerable.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Set from '../utils/set';

import LinkedList from '../utils/linked-list';
import Section from './_section';
import Position from '../utils/cursor/position';
import assert from '../utils/assert';

export default class Markerable extends Section {
Expand Down Expand Up @@ -35,14 +34,6 @@ export default class Markerable extends Section {
this.type, this.tagName, newMarkers);
}

headPosition() {
return new Position(this, 0);
}

tailPosition() {
return new Position(this, this.length);
}

get isBlank() {
if (!this.markers.length) {
return true;
Expand All @@ -56,7 +47,7 @@ export default class Markerable extends Section {
*
* @return {Number} The offset relative to the start of this section
*/
offsetOfMarker(marker, markerOffset) {
offsetOfMarker(marker, markerOffset=0) {
assert(`Cannot get offsetOfMarker for marker that is not child of this`,
marker.section === this);

Expand All @@ -82,7 +73,7 @@ export default class Markerable extends Section {
* @return {Array} the new markers that replaced `marker`
*/
splitMarker(marker, offset, endOffset=marker.length) {
const newMarkers = filter(marker.split(offset, endOffset), m => !m.isEmpty);
const newMarkers = filter(marker.split(offset, endOffset), m => !m.isBlank);
this.markers.splice(marker, 1, newMarkers);
return newMarkers;
}
Expand Down Expand Up @@ -203,7 +194,7 @@ export default class Markerable extends Section {
}

get length() {
return this.text.length;
return reduce(this.markers, (prev, m) => prev + m.length, 0);
}

/**
Expand All @@ -215,9 +206,14 @@ export default class Markerable extends Section {
tail: {section:this, offset:tailOffset}};

let markers = [];
this._markersInRange(range, (marker, {markerHead, markerTail}) => {
this._markersInRange(range, (marker, {markerHead, markerTail, isContained}) => {
const cloned = marker.clone();
cloned.value = marker.value.slice(markerHead, markerTail);
if (!isContained) {
// cannot do marker.value.slice if the marker is an atom -- this breaks the atom's "atomic" value
// If a marker is an atom `isContained` should always be true so
// we shouldn't hit this code path. FIXME add tests
cloned.value = marker.value.slice(markerHead, markerTail);
}
markers.push(cloned);
});
return markers;
Expand Down Expand Up @@ -266,7 +262,7 @@ export default class Markerable extends Section {
let afterMarker = null;

otherSection.markers.forEach(m => {
if (!m.isEmpty) {
if (!m.isBlank) {
m = m.clone();
this.markers.append(m);
if (!afterMarker) {
Expand Down
7 changes: 5 additions & 2 deletions src/js/models/_section.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { normalizeTagName } from '../utils/dom-utils';
import LinkedItem from '../utils/linked-item';
import assert from '../utils/assert';
import Position from '../utils/cursor/position';

function unimplementedMethod(methodName, me) {
assert(`\`${methodName}()\` must be implemented by ${me.constructor.name}`,
Expand Down Expand Up @@ -47,11 +48,13 @@ export default class Section extends LinkedItem {
}

headPosition() {
unimplementedMethod('headPosition', this);
return new Position(this, 0);
}

tailPosition() {
unimplementedMethod('tailPosition', this);
assert('Cannot determine tailPosition without length',
this.length !== undefined && this.length !== null);
return new Position(this, this.length);
}

join() {
Expand Down
65 changes: 64 additions & 1 deletion src/js/models/atom.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,83 @@ import { ATOM_TYPE } from './types';
import mixin from '../utils/mixin';
import MarkuperableMixin from '../utils/markuperable';
import LinkedItem from '../utils/linked-item';
import assert from '../utils/assert';

const ATOM_LENGTH = 1;

export default class Atom extends LinkedItem {
constructor(name, value, payload, markups=[]) {
super();
this.name = name;
this.value = value;
assert('Atom must have value', value !== undefined && value !== null);
this.payload = payload;
this.type = ATOM_TYPE;
this.isMarker = false;
this.isAtom = true;
this.length = 1;

this.markups = [];
markups.forEach(m => this.addMarkup(m));
}

clone() {
let clonedMarkups = this.markups.slice();
return this.builder.createAtom(
this.name, this.value, this.payload, clonedMarkups
);
}

get isBlank() {
return false;
}

get length() {
return ATOM_LENGTH;
}

canJoin(/* other */) {
return false;
}

split(offset=0, endOffset=1) {
let markers = [];

if (endOffset === 0) {
markers.push(this.builder.createMarker('', this.markups.slice()));
}

markers.push(this.clone());

if (offset === ATOM_LENGTH) {
markers.push(this.builder.createMarker('', this.markups.slice()));
}

return markers;
}

splitAtOffset(offset) {
assert('Cannot split a marker at an offset > its length',
offset <= this.length);

let { builder } = this;
let clone = this.clone();
let blankMarker = builder.createMarker('');
let pre, post;

if (offset === 0) {
([pre, post] = [clone, blankMarker]);
} else if (offset === ATOM_LENGTH) {
([pre, post] = [blankMarker, clone]);
} else {
assert(`Invalid offset given to Atom#splitAtOffset: "${offset}"`, false);
}

this.markups.forEach(markup => {
pre.addMarkup(markup);
post.addMarkup(markup);
});
return [pre, post];
}
}

mixin(Atom, MarkuperableMixin);
Loading

0 comments on commit b5957a6

Please sign in to comment.