Skip to content

Commit

Permalink
Register combo commands (“CTRL+X”) and allow special key names (“enter”)
Browse files Browse the repository at this point in the history
This translates a string “CTRL+X” to a code and modifier part and 
is 100% backwards compatible with the existing str/modifier commands.
  • Loading branch information
rlivsey committed Oct 14, 2015
1 parent 0140bd9 commit f6cfe26
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 16 deletions.
5 changes: 3 additions & 2 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
DEFAULT_TEXT_EXPANSIONS, findExpansion, validateExpansion
} from './text-expansions';
import {
DEFAULT_KEY_COMMANDS, findKeyCommands, validateKeyCommand
DEFAULT_KEY_COMMANDS, buildKeyCommand, findKeyCommands, validateKeyCommand
} from './key-commands';
import { capitalize } from '../utils/string-utils';
import LifecycleCallbacksMixin from '../utils/lifecycle-callbacks';
Expand Down Expand Up @@ -192,7 +192,8 @@ class Editor {
* is invoked
* @public
*/
registerKeyCommand(keyCommand) {
registerKeyCommand(rawKeyCommand) {
const keyCommand = buildKeyCommand(rawKeyCommand);
if (!validateKeyCommand(keyCommand)) {
throw new Error('Key Command is not valid');
}
Expand Down
43 changes: 39 additions & 4 deletions src/js/editor/key-commands.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Key from '../utils/key';
import { MODIFIERS } from '../utils/key';
import { MODIFIERS, SPECIAL_KEYS } from '../utils/key';
import { filter } from '../utils/array-utils';
import LinkCommand from '../commands/link';

Expand Down Expand Up @@ -46,14 +46,49 @@ export const DEFAULT_KEY_COMMANDS = [{
}
}];

function stringToModifier(string) {
return MODIFIERS[string.toUpperCase()];
}

function characterToCode(character) {
const upperCharacter = character.toUpperCase();
const special = SPECIAL_KEYS[upperCharacter];
if (special) {
return special;
} else {
return upperCharacter.charCodeAt(0);
}
}

export function buildKeyCommand(keyCommand) {
if (!keyCommand.str) {
return keyCommand;
}

const str = keyCommand.str;
if (str.indexOf('+') !== -1) {
const [modifierName, character] = str.split('+');
keyCommand.modifier = stringToModifier(modifierName);
keyCommand.code = characterToCode(character);
} else {
keyCommand.code = characterToCode(str);
}

return keyCommand;
}

export function validateKeyCommand(keyCommand) {
return !!keyCommand.modifier && !!keyCommand.str && !!keyCommand.run;
return !!keyCommand.code && !!keyCommand.run;
}

export function findKeyCommands(keyCommands, keyEvent) {
const key = Key.fromEvent(keyEvent);

return filter(keyCommands, ({modifier, str}) => {
return key.hasModifier(modifier) && key.isChar(str);
return filter(keyCommands, ({modifier, code}) => {
if (key.keyCode !== code) {
return false;
}

return (modifier && key.hasModifier(modifier)) || (!modifier && !key.hasAnyModifier());
});
}
24 changes: 23 additions & 1 deletion src/js/utils/key.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Keycodes from './keycodes';
export const DIRECTION = {
FORWARD: 1,
BACKWARD: -1
BACKWARD: -1
};

export const MODIFIERS = {
Expand All @@ -10,6 +10,24 @@ export const MODIFIERS = {
SHIFT: 3
};

export const SPECIAL_KEYS = {
BACKSPACE: 8,
TAB: 9,
ENTER: 13,
ESC: 27,
SPACE: 32,
PAGEUP: 33,
PAGEDOWN: 34,
END: 35,
HOME: 36,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
INS: 45,
DEL: 46
};

/**
* An abstraction around a KeyEvent
* that key listeners in the editor can use
Expand Down Expand Up @@ -67,6 +85,10 @@ const Key = class Key {
}
}

hasAnyModifier() {
return this.metaKey || this.ctrlKey || this.shiftKey;
}

get ctrlKey() {
return this.event.ctrlKey;
}
Expand Down
40 changes: 31 additions & 9 deletions tests/acceptance/editor-key-commands-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,29 @@ test('new key commands can be registered', (assert) => {
let passedEditor;
editor = new Editor({mobiledoc});
editor.registerKeyCommand({
modifier: MODIFIERS.CTRL,
str: 'ctrl+x',
run(editor) { passedEditor = editor; }
});
editor.render(editorElement);

Helpers.dom.triggerKeyCommand(editor, 'Y', MODIFIERS.CTRL);

assert.ok(!passedEditor, 'incorrect key combo does not trigger key command');

Helpers.dom.triggerKeyCommand(editor, 'X', MODIFIERS.CTRL);

assert.ok(!!passedEditor && passedEditor === editor, 'run method is called');
});

test('new key commands can be registered without modifiers', (assert) => {
const mobiledoc = Helpers.mobiledoc.build(
({post, markupSection, marker}) => post([
markupSection('p', [marker('something')])
]));

let passedEditor;
editor = new Editor({mobiledoc});
editor.registerKeyCommand({
str: 'X',
run(editor) { passedEditor = editor; }
});
Expand All @@ -70,6 +92,10 @@ test('new key commands can be registered', (assert) => {

Helpers.dom.triggerKeyCommand(editor, 'X', MODIFIERS.CTRL);

assert.ok(!passedEditor, 'key with modifier combo does not trigger key command');

Helpers.dom.triggerKeyCommand(editor, 'X');

assert.ok(!!passedEditor && passedEditor === editor, 'run method is called');
});

Expand All @@ -82,13 +108,11 @@ test('duplicate key commands can be registered with the last registered winning'
let firstCommandRan, secondCommandRan;
editor = new Editor({mobiledoc});
editor.registerKeyCommand({
modifier: MODIFIERS.CTRL,
str: 'X',
str: 'ctrl+x',
run() { firstCommandRan = true; }
});
editor.registerKeyCommand({
modifier: MODIFIERS.CTRL,
str: 'X',
str: 'ctrl+x',
run() { secondCommandRan = true; }
});
editor.render(editorElement);
Expand All @@ -108,13 +132,11 @@ test('returning false from key command causes next match to run', (assert) => {
let firstCommandRan, secondCommandRan;
editor = new Editor({mobiledoc});
editor.registerKeyCommand({
modifier: MODIFIERS.CTRL,
str: 'X',
str: 'ctrl+x',
run() { firstCommandRan = true; }
});
editor.registerKeyCommand({
modifier: MODIFIERS.CTRL,
str: 'X',
str: 'ctrl+x',
run() {
secondCommandRan = true;
return false;
Expand Down
72 changes: 72 additions & 0 deletions tests/unit/editor/key-commands-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { buildKeyCommand } from 'content-kit-editor/editor/key-commands';
import { MODIFIERS, SPECIAL_KEYS } from 'content-kit-editor/utils/key';
import Keycodes from 'content-kit-editor/utils/keycodes';

import Helpers from '../../test-helpers';

const { module, test } = Helpers;

module('Unit: Editor key commands');

test('leaves modifier, code and run in place if they exist', (assert) => {
const fn = function() {};

const {
modifier, code, run
} = buildKeyCommand({
code: Keycodes.ENTER,
modifier: MODIFIERS.META,
run: fn
});

assert.equal(modifier, MODIFIERS.META, 'keeps modifier');
assert.equal(code, Keycodes.ENTER, 'keeps code');
assert.equal(run, fn, 'keeps run');
});

test('translates MODIFIER+CHARACTER string to modifier and code', (assert) => {

const { modifier, code } = buildKeyCommand({ str: 'meta+k' });

assert.equal(modifier, MODIFIERS.META, 'translates string to modifier');
assert.equal(code, 75, 'translates string to code');
});

test('translates modifier+character string to modifier and code', (assert) => {

const { modifier, code } = buildKeyCommand({ str: 'META+K' });

assert.equal(modifier, MODIFIERS.META, 'translates string to modifier');
assert.equal(code, 75, 'translates string to code');
});

test('translates uppercase character string to code', (assert) => {

const { modifier, code } = buildKeyCommand({ str: 'K' });

assert.equal(modifier, undefined, 'no modifier given');
assert.equal(code, 75, 'translates string to code');
});

test('translates lowercase character string to code', (assert) => {

const { modifier, code } = buildKeyCommand({ str: 'k' });

assert.equal(modifier, undefined, 'no modifier given');
assert.equal(code, 75, 'translates string to code');

});

test('translates uppercase special key names to codes', (assert) => {
Object.keys(SPECIAL_KEYS).forEach(name => {
const { code } = buildKeyCommand({ str: name.toUpperCase() });
assert.equal(code, SPECIAL_KEYS[name], `translates ${name} string to code`);
});
});

test('translates lowercase special key names to codes', (assert) => {
Object.keys(SPECIAL_KEYS).forEach(name => {
const { code } = buildKeyCommand({ str: name.toLowerCase() });
assert.equal(code, SPECIAL_KEYS[name], `translates ${name} string to code`);
});
});

0 comments on commit f6cfe26

Please sign in to comment.