Skip to content

Commit

Permalink
Add Editor#stateDidChange, refactor state change tracking
Browse files Browse the repository at this point in the history
Also add Editor#_notifyRangeChange (supersedes Editor#_resetRange), used
to notify the editor that the range may have changed and it should
re-check for cursor and state changes.

refs #319
  • Loading branch information
bantic committed Apr 6, 2016
1 parent 0c1ad10 commit 281caf7
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 110 deletions.
82 changes: 50 additions & 32 deletions src/js/editor/edit-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,59 @@ import { contains, isArrayEqual } from 'mobiledoc-kit/utils/array-utils';
export default class EditState {
constructor(editor) {
this.editor = editor;
this._activeMarkups = [];
this._activeSections = [];
this.prevState = this.state = this._readState();
}

get activeSections() {
let { editor: { range, post } } = this;
if (range.isBlank) {
return [];
} else {
return post.sections.readRange(range.head.section, range.tail.section);
}
reset() {
this.prevState = this.state;
this.state = this._readState();
}

get activeMarkups() {
let { editor: { cursor, post, range } } = this;
stateDidChange() {
let { state, prevState } = this;
return (!isArrayEqual(state.activeMarkups, prevState.activeMarkups) ||
!isArrayEqual(state.activeSections, prevState.activeSections));
}

if (!cursor.hasCursor()) {
return [];
} else if (!this._activeMarkups) {
this._activeMarkups = post.markupsInRange(range);
}
rangeDidChange() {
let { state, prevState } = this;
return !state.range.isEqual(prevState.range);
}

_readState() {
let range = this._readRange();
return {
range: range,
activeMarkups: this._readActiveMarkups(range),
activeSections: this._readActiveSections(range)
};
}

_readRange() {
return this.editor.range;
}

_readActiveSections(range) {
let { head, tail } = range;
let { editor: { post } } = this;
return post.sections.readRange(head.section, tail.section);
}

_readActiveMarkups(range) {
let { editor: { post } } = this;
return post.markupsInRange(range);
}

get range() {
return this.state.range;
}

get activeSections() {
return this.state.activeSections;
}

return this._activeMarkups;
get activeMarkups() {
return this.state.activeMarkups;
}

toggleMarkupState(markup) {
Expand All @@ -36,24 +66,12 @@ export default class EditState {
}
}

/**
* @return {Boolean} Whether the markups after reset have changed
*/
resetActiveMarkups() {
let prevMarkups = this._activeMarkups || [];
delete this._activeMarkups;
let markups = this.activeMarkups || [];

let didChange = !isArrayEqual(prevMarkups, markups);
return didChange;
}

_removeActiveMarkup(markup) {
let index = this._activeMarkups.indexOf(markup);
this._activeMarkups.splice(index, 1);
let index = this.state.activeMarkups.indexOf(markup);
this.state.activeMarkups.splice(index, 1);
}

_addActiveMarkup(markup) {
this._activeMarkups.push(markup);
this.state.activeMarkups.push(markup);
}
}
116 changes: 49 additions & 67 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import RenderTree from 'mobiledoc-kit/models/render-tree';
import mobiledocRenderers from '../renderers/mobiledoc';
import { mergeWithOptions } from '../utils/merge';
import { clearChildNodes, addClassName } from '../utils/dom-utils';
import { forEach, filter, contains, isArrayEqual, values } from '../utils/array-utils';
import { forEach, filter, contains, values } from '../utils/array-utils';
import { setData } from '../utils/element-utils';
import Cursor from '../utils/cursor';
import Range from '../utils/cursor/range';
Expand Down Expand Up @@ -71,7 +71,8 @@ const CALLBACK_QUEUES = {
DID_RENDER: 'didRender',
CURSOR_DID_CHANGE: 'cursorDidChange',
DID_REPARSE: 'didReparse',
POST_DID_CHANGE: 'postDidChange'
POST_DID_CHANGE: 'postDidChange',
STATE_DID_CHANGE: 'stateDidChange'
};

/**
Expand Down Expand Up @@ -340,6 +341,12 @@ class Editor {
this._reportSelectionState();
}

/**
* Selects the given range. If range is collapsed, this positions the cursor
* at the range's position, otherwise a selection is created in the editor
* surface.
* @param {Range}
*/
selectRange(range) {
this.renderRange(range);
}
Expand All @@ -350,17 +357,8 @@ class Editor {
* @private
*/
renderRange(range) {
let prevRange = this._range;
if (range.isBlank) {
this.cursor.clearSelection();
} else {
this.cursor.selectRange(range);
}
this.range = range;

if (prevRange && !prevRange.isEqual(range)) {
this._rangeDidChange();
}
this.cursor.selectRange(range);
this._notifyRangeChange();
}

get cursor() {
Expand All @@ -378,28 +376,33 @@ class Editor {
return this._range;
}
let range = this.cursor.offsets;
if (!range.isBlank) {
if (!range.isBlank) { // do not cache blank ranges
this._range = range;
}
return range;
}

set range(newRange) {
this._range = newRange;
}

/**
* force re-reading range from dom
* Fires `rangeDidChange`-related callbacks if the range is different
* Used to notify the editor that the range (or state) may
* have changed (e.g. in response to a mouseup or keyup) and
* that the editor should re-read values from DOM and fire the
* necessary callbacks
* @private
*/
_resetRange() {
let prevRange = this._range;
delete this._range;
let range = this.range;
if (!range.isEqual(prevRange)) {
_notifyRangeChange() {
this._resetRange();
this._editState.reset();

if (this._editState.rangeDidChange()) {
this._rangeDidChange();
}
if (this._editState.stateDidChange()) {
this._stateDidChange();
}
}

_resetRange() {
delete this._range;
}

setPlaceholder(placeholder) {
Expand Down Expand Up @@ -664,14 +667,6 @@ class Editor {
* @public
*/
run(callback) {
// FIXME we must keep track of the activeSectionTagNames before and after
// changing the post so that we can fire the cursorDidChange callback if the
// active sections changed.
// This is necessary for the ember-mobiledoc-editor's toolbar to update
// when toggling a section on/off (it only listens to the cursorDidChange
// action)
let activeSectionTagNames = this.activeSections.map(s => s.tagName);

const postEditor = new PostEditor(this);
postEditor.begin();
this._editHistory.snapshot();
Expand All @@ -683,10 +678,13 @@ class Editor {
}
this._editHistory.storeSnapshot();

// FIXME This should be handled within the EditState object
let newActiveSectionTagNames = this.activeSections.map(s => s.tagName);
if (!isArrayEqual(activeSectionTagNames, newActiveSectionTagNames)) {
this._activeSectionsDidChange();
this._resetRange();
this._editState.reset();
if (this._editState.rangeDidChange()) {
this._rangeDidChange();
}
if (this._editState.stateDidChange()) {
this._stateDidChange();
}

return result;
Expand All @@ -709,6 +707,14 @@ class Editor {
this.addCallback(CALLBACK_QUEUES.POST_DID_CHANGE, callback);
}

/**
* @param {Function} callback Called when the editor's state (active markups or
* active sections) has changed, either via user input or programmatically
*/
stateDidChange(callback) {
this.addCallback(CALLBACK_QUEUES.STATE_DID_CHANGE, callback);
}

/**
* @param {Function} callback This callback will be called before the editor
* is rendered.
Expand Down Expand Up @@ -751,7 +757,6 @@ class Editor {

_rangeDidChange() {
this._cursorDidChange();
this._resetActiveMarkups();
}

_cursorDidChange() {
Expand All @@ -760,32 +765,8 @@ class Editor {
}
}

/**
* Clear the cached active markups and force a re-read of the markups
* from the current range.
* If markups have changed, fires an event
* @private
*/
_resetActiveMarkups() {
let activeMarkupsDidChange = this._editState.resetActiveMarkups();

if (activeMarkupsDidChange) {
this._activeMarkupsDidChange();
}
}

_activeMarkupsDidChange() {
// FIXME use a different callback queue for _activeMarkupsDidChange
// Using the cursorDidChange callback is necessary for the ember-mobiledoc-editor to notice
// when markups change but the cursor doesn't (i.e., type cmd-B)
this._cursorDidChange();
}

_activeSectionsDidChange() {
// FIXME use a different callback queue for _activeSectionsDidChange
// Using the cursorDidChange callback is necessary for the ember-mobiledoc-editor to notice
// when markups change but the cursor doesn't (i.e., type cmd-B)
this._cursorDidChange();
_stateDidChange() {
this.runCallbacks(CALLBACK_QUEUES.STATE_DID_CHANGE);
}

_insertEmptyMarkupSectionAtCursor() {
Expand All @@ -808,11 +789,12 @@ class Editor {
*/
toggleMarkup(markup) {
markup = this.post.builder.createMarkup(markup);
if (this.range.isCollapsed) {
let { range } = this;
if (range.isCollapsed) {
this._editState.toggleMarkupState(markup);
this._activeMarkupsDidChange();
this._stateDidChange();
} else {
this.run(postEditor => postEditor.toggleMarkup(markup));
this.run(postEditor => postEditor.toggleMarkup(markup, range));
}
}

Expand Down
8 changes: 6 additions & 2 deletions src/js/editor/event-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ export default class EventManager {
this.isShift = false;
}

setTimeout(() => this.editor._resetRange(), 0);
setTimeout(() => {
this.editor._notifyRangeChange();
});
}

cut(event) {
Expand Down Expand Up @@ -198,7 +200,9 @@ export default class EventManager {

mouseup(/* event */) {
// mouseup does not correctly report a selection until the next tick
setTimeout(() => this.editor._resetRange(), 0);
setTimeout(() => {
this.editor._notifyRangeChange();
});
}

drop(event) {
Expand Down
2 changes: 1 addition & 1 deletion src/js/editor/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -1291,7 +1291,7 @@ class PostEditor {
this.runCallbacks(CALLBACK_QUEUES.COMPLETE);
this.runCallbacks(CALLBACK_QUEUES.AFTER_COMPLETE);

this.editor._resetRange();
this.editor._notifyRangeChange();
}

undoLastChange() {
Expand Down
5 changes: 5 additions & 0 deletions src/js/utils/cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ const Cursor = class Cursor {
}

selectRange(range) {
if (range.isBlank) {
this.clearSelection();
return;
}

const { head, tail, direction } = range;
const { node:headNode, offset:headOffset } = this._findNodeForPosition(head),
{ node:tailNode, offset:tailOffset } = this._findNodeForPosition(tail);
Expand Down
4 changes: 2 additions & 2 deletions tests/helpers/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function selectText(editor,
const startOffset = startTextNode.textContent.indexOf(startText),
endOffset = endTextNode.textContent.indexOf(endText) + endText.length;
selectRange(startTextNode, startOffset, endTextNode, endOffset);
editor._resetRange();
editor._notifyRangeChange();
}

function moveCursorWithoutNotifyingEditorTo(editor, node, offset=0, endNode=node, endOffset=offset) {
Expand All @@ -82,7 +82,7 @@ function moveCursorTo(editor, node, offset=0, endNode=node, endOffset=offset) {
}
if (!node) { throw new Error('Cannot moveCursorTo node without node'); }
moveCursorWithoutNotifyingEditorTo(editor, node, offset, endNode, endOffset);
editor._resetRange();
editor._notifyRangeChange();
}

function triggerEvent(node, eventType) {
Expand Down
Loading

0 comments on commit 281caf7

Please sign in to comment.