Skip to content

Commit

Permalink
Add cursorDidChange lifecycle callback to editor
Browse files Browse the repository at this point in the history
Remove editor#reportSelection and editor#reportNoSelection in favor
of funneling all selection-change-detection through
`editor#_reportSelectionState`

fixes #157
  • Loading branch information
bantic committed Sep 22, 2015
1 parent 9ead5b3 commit cb20368
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 60 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ The available lifecycle hooks are:
the DOM is updated.
* `editor.didRender()` - After the DOM has been updated to match the
edited post.
* `editor.cursorDidChange()` - When the cursor (or selection) changes as a result of arrow-key
movement or clicking in the document.

### Programmatic Post Editing

Expand Down
99 changes: 57 additions & 42 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
DEFAULT_KEY_COMMANDS, findKeyCommand, validateKeyCommand
} from './key-commands';
import { capitalize } from '../utils/string-utils';
import LifecycleCallbacksMixin from '../utils/lifecycle-callbacks';

export const EDITOR_ELEMENT_CLASS_NAME = 'ck-editor';

Expand All @@ -50,12 +51,12 @@ const defaults = {
html: null
};

function runCallbacks(callbacks, args) {
let i;
for (i=0;i<callbacks.length;i++) {
callbacks[i].apply(null, args);
}
}
const CALLBACK_QUEUES = {
DID_UPDATE: 'didUpdate',
WILL_RENDER: 'willRender',
DID_RENDER: 'didRender',
CURSOR_DID_CHANGE: 'cursorDidChange'
};

/**
* @class Editor
Expand All @@ -72,10 +73,6 @@ class Editor {
this._views = [];
this.isEditable = null;

this._didUpdatePostCallbacks = [];
this._willRenderCallbacks = [];
this._didRenderCallbacks = [];

// FIXME: This should merge onto this.options
mergeWithOptions(this, defaults, options);

Expand Down Expand Up @@ -125,9 +122,9 @@ class Editor {
postRenderNode.markDirty();
}

runCallbacks(this._willRenderCallbacks, []);
this.runCallbacks(CALLBACK_QUEUES.WILL_RENDER);
this._renderer.render(this._renderTree);
runCallbacks(this._didRenderCallbacks, []);
this.runCallbacks(CALLBACK_QUEUES.DID_RENDER);
}

render(element) {
Expand Down Expand Up @@ -260,22 +257,6 @@ class Editor {
callback(window.prompt(message, defaultValue));
}

reportSelection() {
if (!this._hasSelection) {
this.trigger('selection');
} else {
this.trigger('selectionUpdated');
}
this._hasSelection = true;
}

reportNoSelection() {
if (this._hasSelection) {
this.trigger('selectionEnded');
}
this._hasSelection = false;
}

cancelSelection() {
if (this._hasSelection) {
const range = this.cursor.offsets;
Expand All @@ -290,25 +271,20 @@ class Editor {
selectSections(sections=[]) {
if (sections.length) {
this.cursor.selectSections(sections);
this.reportSelection();
} else {
this.cursor.clearSelection();
this.reportNoSelection();
}
this._reportSelectionState();
}

selectRange(range){
this.cursor.selectRange(range);
if (range.isCollapsed) {
this.reportNoSelection();
} else {
this.reportSelection();
}
this._reportSelectionState();
}

moveToPosition(position) {
this.cursor.moveToPosition(position);
this.reportNoSelection();
this._reportSelectionState();
}

get cursor() {
Expand Down Expand Up @@ -472,21 +448,49 @@ class Editor {
run(callback) {
const postEditor = new PostEditor(this);
const result = callback(postEditor);
runCallbacks(this._didUpdatePostCallbacks, [postEditor]);
this.runCallbacks(CALLBACK_QUEUES.DID_UPDATE, [postEditor]);
postEditor.complete();
return result;
}

/**
* @method didUpdatePost
* @param {Function} callback This callback will be called with `postEditor`
* argument when the post is updated
* @public
*/
didUpdatePost(callback) {
this._didUpdatePostCallbacks.push(callback);
this.addCallback(CALLBACK_QUEUES.DID_UPDATE, callback);
}

/**
* @method willRender
* @param {Function} callback This callback will be called before the editor
* is rendered.
* @public
*/
willRender(callback) {
this._willRenderCallbacks.push(callback);
this.addCallback(CALLBACK_QUEUES.WILL_RENDER, callback);
}

/**
* @method didRender
* @param {Function} callback This callback will be called after the editor
* is rendered.
* @public
*/
didRender(callback) {
this._didRenderCallbacks.push(callback);
this.addCallback(CALLBACK_QUEUES.DID_RENDER, callback);
}

/**
* @method cursorDidChange
* @param {Function} callback This callback will be called after the cursor
* position (or selection) changes.
* @public
*/
cursorDidChange(callback) {
this.addCallback(CALLBACK_QUEUES.CURSOR_DID_CHANGE, callback);
}

_addEmbedIntent() {
Expand Down Expand Up @@ -543,10 +547,20 @@ class Editor {
* ctrl-click -> context menu -> click "select all"
*/
_reportSelectionState() {
this.runCallbacks(CALLBACK_QUEUES.CURSOR_DID_CHANGE);

if (this.cursor.hasSelection()) {
this.reportSelection();
if (!this._hasSelection) {
this._hasSelection = true;
this.trigger('selection'); // new selection
} else {
this.trigger('selectionUpdated'); // already had selection
}
} else {
this.reportNoSelection();
if (this._hasSelection) {
this.trigger('selectionEnded');
this._hasSelection = false;
}
}
}

Expand Down Expand Up @@ -607,5 +621,6 @@ class Editor {

mixin(Editor, EventEmitter);
mixin(Editor, EventListenerMixin);
mixin(Editor, LifecycleCallbacksMixin);

export default Editor;
20 changes: 20 additions & 0 deletions src/js/utils/lifecycle-callbacks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { forEach } from './array-utils';

export default class LifecycleCallbacksMixin {
get callbackQueues() {
if (!this._callbackQueues) { this._callbackQueues = {}; }
return this._callbackQueues;
}
runCallbacks(queueName, args=[]) {
if (!queueName) { throw new Error('Must pass queue name to runCallbacks'); }
const callbacks = this.callbackQueues[queueName] || [];
forEach(callbacks, cb => cb(...args));
}
addCallback(queueName, callback) {
if (!queueName) { throw new Error('Must pass queue name to addCallback'); }
if (!this.callbackQueues[queueName]) {
this.callbackQueues[queueName] = [];
}
this.callbackQueues[queueName].push(callback);
}
}
9 changes: 8 additions & 1 deletion tests/helpers/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@ function triggerKeyCommand(editor, string, modifier) {
editor.triggerEvent(editor.element, 'keydown', keyEvent);
}

function triggerRightArrowKey(editor) {
if (!editor) { throw new Error('Must pass editor to triggerRightArrowKey'); }
const event = {preventDefault() {}, keyCode: 39};
editor.triggerEvent(editor.element, 'keyup', event);
}

const DOMHelper = {
moveCursorTo,
selectText,
Expand All @@ -185,7 +191,8 @@ const DOMHelper = {
triggerForwardDelete,
triggerEnter,
insertText,
triggerKeyCommand
triggerKeyCommand,
triggerRightArrowKey
};

export { triggerEvent };
Expand Down
63 changes: 46 additions & 17 deletions tests/unit/editor/editor-events-test.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
const { module, test } = QUnit;
import Helpers from '../../test-helpers';
import { MOBILEDOC_VERSION } from 'content-kit-editor/renderers/mobiledoc';

import { Editor } from 'content-kit-editor';

const { module, test } = Helpers;

let editor, editorElement;
let triggered = [];

const mobiledoc = {
version: MOBILEDOC_VERSION,
sections: [
[],
[[
1, 'P', [[[], 0, 'this is the editor']]
]]
]
};

module('Unit: Editor: events', {
beforeEach() {
editorElement = document.createElement('div');
document.getElementById('qunit-fixture').appendChild(editorElement);
const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker}) => {
return post([markupSection('p', [marker('this is the editor')])]);
});


module('Unit: Editor: events and lifecycle callbacks', {
beforeEach() {
editorElement = $('<div id="editor"></div>').appendTo('#qunit-fixture')[0];
editor = new Editor({mobiledoc});
editor.render(editorElement);
editor.trigger = (name) => triggered.push(name);
Expand Down Expand Up @@ -108,3 +100,40 @@ test('mouseup after text was selected triggers "selectionEnded" event', (assert)
});
});
});

test('"cursorChanged" callbacks fired on mouseup', (assert) => {
const done = assert.async();

let cursorChanged = 0;
editor.cursorDidChange(() => cursorChanged++);
const textNode = $('#editor p')[0].childNodes[0];
Helpers.dom.moveCursorTo(textNode, 0);

assert.equal(cursorChanged, 0, 'precond');

Helpers.dom.triggerEvent(document, 'mouseup');

setTimeout(() => {
assert.equal(cursorChanged, 1, 'cursor changed');
cursorChanged = 0;

Helpers.dom.moveCursorTo(textNode, textNode.textContent.length);
Helpers.dom.triggerEvent(document, 'mouseup');

setTimeout(() => {
assert.equal(cursorChanged, 1, 'cursor changed again');
done();
});
});
});

test('"cursorChanged" callback called after hitting arrow key', (assert) => {
let cursorChanged = 0;
editor.cursorDidChange(() => cursorChanged++);
const textNode = $('#editor p')[0].childNodes[0];
Helpers.dom.moveCursorTo(textNode, 0);

assert.equal(cursorChanged, 0, 'precond');
Helpers.dom.triggerRightArrowKey(editor);
assert.equal(cursorChanged, 1, 'cursor changed');
});

0 comments on commit cb20368

Please sign in to comment.