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 7, 2016
1 parent 0c1ad10 commit 427e238
Show file tree
Hide file tree
Showing 14 changed files with 436 additions and 196 deletions.
80 changes: 52 additions & 28 deletions src/js/editor/edit-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,65 @@ 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;
reset() {
this.prevState = this.state;
this.state = this._readState();
}

stateDidChange() {
let { state, prevState } = this;
return (!isArrayEqual(state.activeMarkups, prevState.activeMarkups) ||
!isArrayEqual(state.activeSectionTagNames, prevState.activeSectionTagNames));
}

rangeDidChange() {
let { state, prevState } = this;
return !state.range.isEqual(prevState.range);
}

_readState() {
let range = this._readRange();
let state = {
range: range,
activeMarkups: this._readActiveMarkups(range),
activeSections: this._readActiveSections(range)
};
state.activeSectionTagNames = state.activeSections.map(s => s.tagName);
return state;
}

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

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

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

if (!cursor.hasCursor()) {
return [];
} else if (!this._activeMarkups) {
this._activeMarkups = post.markupsInRange(range);
}
get range() {
return this.state.range;
}

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

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

toggleMarkupState(markup) {
Expand All @@ -36,24 +72,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);
}
}
133 changes: 62 additions & 71 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,27 +341,22 @@ 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);
}

/**
* If the range is different from the previous range, this method will
* fire 'rangeDidChange'-related callbacks
* @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 +374,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 +665,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 +676,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 +705,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 +755,6 @@ class Editor {

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

_cursorDidChange() {
Expand All @@ -760,32 +763,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,14 +787,27 @@ 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));
}
}

/**
* Toggles the tagName for the current active section(s). This will skip
* non-markerable sections. E.g. if the editor's range includes a "P" MarkupSection
* and a CardSection, only the MarkupSection will be toggled.
* @param {String} tagName The new tagname to change to.
* @public
* @see PostEditor#toggleSection
*/
toggleSection(tagName) {
this.run(postEditor => postEditor.toggleSection(tagName, this.range));
}

/**
* Finds and runs first matching text expansion for this event
* @param {Event} event keyboard event
Expand Down Expand Up @@ -917,8 +909,7 @@ class Editor {
}

/**
* @deprecated
* @since 0.9.1
* @deprecated since 0.9.1
*/
on(eventName, callback) {
deprecate('`on` is deprecated. Use `postDidChange(callback)` instead to handle post changes');
Expand Down
7 changes: 5 additions & 2 deletions src/js/editor/event-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,10 @@ export default class EventManager {
this.isShift = false;
}

setTimeout(() => this.editor._resetRange(), 0);
// Only movement-related keys require re-checking the active range
if (key.isMovement()) {
setTimeout(() => this.editor._notifyRangeChange());
}
}

cut(event) {
Expand Down Expand Up @@ -198,7 +201,7 @@ 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
Loading

0 comments on commit 427e238

Please sign in to comment.