Skip to content

Commit

Permalink
Add SelectionChangeObserver, use it for editor.range updates
Browse files Browse the repository at this point in the history
SelectionChangeObserver maintains a singleton instance that polls the
window using `requestAnimationFrame` for changes to the selection.
Editors' EventManager instances use a SelectionManager to listen for
`selectionchanged` events, and update the editor's `range` if necessary.

Removes code that would use some keyup events (when `key.isMovement()`)
and `mouseup` events to detect when the range could have changed.

The range-detection code was previously spread out over `EventManager`,
`Editor`, `Cursor` and `EditState`. This code change consolidates most
of the responsibility for knowing/reading/updating the editor's `range`
o its `EditState` instance. The editor now delegates `range` to the edit
state instance, and the edit state instance is responsible for knowing
when the editor's inputMode or range has changed.

Some tests assumed selection changes would be picked up synchronously;
for those tests a `Helpers.wait` helper is added that schedules a
callback with `requestAnimationFrame`.

Also:
 * use "hidepassed" for sauce tests to improve test failure debugging
 * remove unused `editor.selectSections`
 * remove now-unnecessary key commands for meta+arrow on mac
  • Loading branch information
bantic committed Jul 13, 2016
1 parent 7490ff3 commit 2ff0590
Show file tree
Hide file tree
Showing 34 changed files with 565 additions and 489 deletions.
47 changes: 27 additions & 20 deletions src/js/editor/edit-state.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { contains, isArrayEqual } from 'mobiledoc-kit/utils/array-utils';
import Range from 'mobiledoc-kit/utils/cursor/range';

/**
* Used by {@link Editor} to manage its current state (cursor, active markups
Expand All @@ -8,15 +9,34 @@ import { contains, isArrayEqual } from 'mobiledoc-kit/utils/array-utils';
class EditState {
constructor(editor) {
this.editor = editor;
this.prevState = this.state = this._readState();

let defaultState = {
range: Range.blankRange(),
activeMarkups: [],
activeSections: [],
activeSectionTagNames: []
};

this.prevState = this.state = defaultState;
}

updateRange(newRange) {
this.prevState = this.state;
this.state = this._readState(newRange);
}

destroy() {
this.editor = null;
this.prevState = this.state = null;
}

/**
* Cache the last state, force a reread of current state
* @return {Boolean}
*/
reset() {
this.prevState = this.state;
this.state = this._readState();
rangeDidChange() {
let { state: { range } , prevState: {range: prevRange} } = this;

return !prevRange.isEqual(range);
}

/**
Expand All @@ -29,14 +49,6 @@ class EditState {
!isArrayEqual(state.activeSectionTagNames, prevState.activeSectionTagNames));
}

/**
* @return {Boolean} Whether the range has changed.
*/
rangeDidChange() {
let { state, prevState } = this;
return !state.range.isEqual(prevState.range);
}

/**
* @return {Range}
*/
Expand Down Expand Up @@ -72,10 +84,9 @@ class EditState {
}
}

_readState() {
let range = this._readRange();
_readState(range) {
let state = {
range: range,
range,
activeMarkups: this._readActiveMarkups(range),
activeSections: this._readActiveSections(range)
};
Expand All @@ -89,10 +100,6 @@ class EditState {
return state;
}

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

_readActiveSections(range) {
let { head, tail } = range;
let { editor: { post } } = this;
Expand Down
103 changes: 29 additions & 74 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,12 @@ class Editor {
this.hasRendered = true;
this.rerender();

if (this.autofocus) {
this.element.focus();
}
this._mutationHandler.init();
this._eventManager.init();

if (this.autofocus) {
this.selectRange(new Range(this.post.headPosition()));
}
}

_addTooltip() {
Expand Down Expand Up @@ -350,34 +351,15 @@ class Editor {
this.runCallbacks(CALLBACK_QUEUES.POST_DID_CHANGE);
}

selectSections(sections=[]) {
if (sections.length) {
let headSection = sections[0],
tailSection = sections[sections.length - 1];
this.selectRange(new Range(headSection.headPosition(),
tailSection.tailPosition()));
} else {
this.cursor.clearSelection();
}
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);
}

/**
* @private
*/
renderRange(range) {
this.cursor.selectRange(range);
this._notifyRangeChange();
this.range = range;
}

get cursor() {
Expand All @@ -386,44 +368,29 @@ class Editor {

/**
* Return the current range for the editor (may be cached).
* The #_resetRange method forces a re-read of
* the range from DOM.
* @return {Range}
*/
get range() {
if (this._range) {
return this._range;
}
let range = this.cursor.offsets;
if (!range.isBlank) { // do not cache blank ranges
this._range = range;
}
return range;
return this._editState.range;
}

/**
* 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
*/
_notifyRangeChange() {
if (this.isEditable) {
this._resetRange();
this._editState.reset();
set range(newRange) {
this._editState.updateRange(newRange);

if (this._editState.rangeDidChange()) {
this._rangeDidChange();
}
if (this._editState.inputModeDidChange()) {
this._inputModeDidChange();
}
if (this._editState.rangeDidChange()) {
this._rangeDidChange();
}

if (this._editState.inputModeDidChange()) {
this._inputModeDidChange();
}
}

_resetRange() {
delete this._range;
_readRangeFromDOM() {
if (!this.isEditable) {
return;
}
this.range = this.cursor.offsets;
}

setPlaceholder(placeholder) {
Expand Down Expand Up @@ -610,7 +577,7 @@ class Editor {
* @public
*/
destroy() {
this._isDestroyed = true;
this.isDestroyed = true;
if (this.hasCursor()) {
this.cursor.clearSelection();
this.element.blur(); // FIXME This doesn't blur the element on IE11
Expand All @@ -619,6 +586,7 @@ class Editor {
this._eventManager.destroy();
this.removeAllViews();
this._renderer.destroy();
this._editState.destroy();
}

/**
Expand All @@ -628,10 +596,13 @@ class Editor {
* @public
*/
disableEditing() {
if (this.isEditable === false) { return; }

this.isEditable = false;
if (this.element) {
if (this.hasRendered) {
this.element.setAttribute('contentEditable', false);
this.setPlaceholder('');
this.selectRange(Range.blankRange());
}
}

Expand Down Expand Up @@ -705,11 +676,12 @@ class Editor {
const result = callback(postEditor);
this.runCallbacks(CALLBACK_QUEUES.DID_UPDATE, [postEditor]);
postEditor.complete();
this._readRangeFromDOM();

if (postEditor._shouldCancelSnapshot) {
this._editHistory._pendingSnapshot = null;
}
this._editHistory.storeSnapshot();
this._notifyRangeChange();

return result;
}
Expand Down Expand Up @@ -784,24 +756,7 @@ class Editor {
this.addCallback(CALLBACK_QUEUES.CURSOR_DID_CHANGE, callback);
}

/*
The following events/sequences can create a selection and are handled:
* mouseup -- can happen anywhere in document, must wait until next tick to read selection
* keyup when key is a movement key and shift is pressed -- in editor element
* keyup when key combo was cmd-A (alt-A) aka "select all"
* keyup when key combo was cmd-Z (browser may restore selection)
These cases can create a selection and are not handled:
* ctrl-click -> context menu -> click "select all"
*/
_reportSelectionState() {
this._cursorDidChange();
}

_rangeDidChange() {
this._cursorDidChange();
}

_cursorDidChange() {
if (this.hasRendered) {
this.runCallbacks(CALLBACK_QUEUES.CURSOR_DID_CHANGE);
}
Expand Down Expand Up @@ -830,7 +785,7 @@ class Editor {
* @see PostEditor#toggleMarkup
*/
toggleMarkup(markup) {
markup = this.post.builder.createMarkup(markup);
markup = this.builder.createMarkup(markup);
let { range } = this;
if (range.isCollapsed) {
this._editState.toggleMarkupState(markup);
Expand Down Expand Up @@ -1021,7 +976,7 @@ class Editor {
}

runCallbacks(...args) {
if (this._isDestroyed) {
if (this.isDestroyed) {
// TODO warn that callback attempted after editor was destroyed
return;
}
Expand Down
Loading

0 comments on commit 2ff0590

Please sign in to comment.