Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(IText): deprecate KeyboardEvent#keyCode in favor of key #7865

Closed
wants to merge 15 commits into from
147 changes: 106 additions & 41 deletions src/mixins/itext_key_behavior.mixin.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
var NAVIGATION_KEYS = [
'PageUp',
'PageDown',
'End',
'Home',
'ArrowLeft',
'ArrowUp',
'ArrowRight',
'ArrowDown',
];

/**
*
* @param {string} key
*/
function isNavigationKey(key) {
return NAVIGATION_KEYS.includes(key);
}

function warnKeyCodeDeprecated() {
/* _DEV_MODE_START_ */
console.warn('fabric.IText: `KeyboardEvent#keyCode` is deprecated, use `key` or `code` instead.');
/* _DEV_MODE_END_ */
}

fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {

/**
Expand Down Expand Up @@ -43,55 +68,60 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
},

/**
* @typedef {string | ((e: KeyboardEvent) => any)} EventFunction pass a function or the name of a bound function
ShaMan123 marked this conversation as resolved.
Show resolved Hide resolved
* @typedef {{ [eventKey: number | string]: EventFunction }} EventKeyMap
* Prefer using [KeyboardEvent#key]{@link https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key}
* or [KeyboardEvent#code]{@link https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code}
* instead of **deprecated** [KeyboardEvent#keyCode]{@link https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode} as `eventKey`
*
* For functionalities on keyDown
* Map a special key to a function of the instance/prototype
* If you need different behaviour for ESC or TAB or arrows, you have to change
* this map setting the name of a function that you build on the fabric.Itext or
* your prototype.
* the map change will affect all Instances unless you need for only some text Instances
* in that case you have to clone this object and assign your Instance.
* this.keysMap = fabric.util.object.clone(this.keysMap);
* The function must be in fabric.Itext.prototype.myFunction And will receive event as args[0]
* @type {EventKeyMap}
*/
keysMap: {
9: 'exitEditing',
27: 'exitEditing',
33: 'moveCursorUp',
34: 'moveCursorDown',
35: 'moveCursorRight',
36: 'moveCursorLeft',
37: 'moveCursorLeft',
38: 'moveCursorUp',
39: 'moveCursorRight',
40: 'moveCursorDown',
Tab: 'exitEditing',
Escape: 'exitEditing',
PageUp: 'moveCursorUp',
PageDown: 'moveCursorDown',
End: 'moveCursorRight',
Home: 'moveCursorLeft',
ArrowLeft: 'moveCursorLeft',
ArrowUp: 'moveCursorUp',
ArrowRight: 'moveCursorRight',
ArrowDown: 'moveCursorDown',
},

/**
* For functionalities on keyDown when `direction === 'rtl'`
* @type {EventKeyMap}
*/
keysMapRtl: {
9: 'exitEditing',
27: 'exitEditing',
33: 'moveCursorUp',
34: 'moveCursorDown',
35: 'moveCursorLeft',
36: 'moveCursorRight',
37: 'moveCursorRight',
38: 'moveCursorUp',
39: 'moveCursorLeft',
40: 'moveCursorDown',
Tab: 'exitEditing',
Escape: 'exitEditing',
PageUp: 'moveCursorUp',
PageDown: 'moveCursorDown',
End: 'moveCursorLeft',
Home: 'moveCursorRight',
ArrowLeft: 'moveCursorRight',
ArrowUp: 'moveCursorUp',
ArrowRight: 'moveCursorLeft',
ArrowDown: 'moveCursorDown',
},

/**
* For functionalities on keyUp + ctrl || cmd
* @type {EventKeyMap}
*/
ctrlKeysMapUp: {
67: 'copy',
88: 'cut'
keyC: 'copy',
keyX: 'cut'
},

/**
* For functionalities on keyDown + ctrl || cmd
* @type {EventKeyMap}
*/
ctrlKeysMapDown: {
65: 'selectAll'
KeyA: 'selectAll'
},

onClick: function() {
Expand All @@ -116,19 +146,40 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
return;
}
var keyMap = this.direction === 'rtl' ? this.keysMapRtl : this.keysMap;
if (e.keyCode in keyMap) {
this[keyMap[e.keyCode]](e);
var func;
ShaMan123 marked this conversation as resolved.
Show resolved Hide resolved
if (e.key in keyMap) {
func = keyMap[e.key];
}
else if (e.code in keyMap) {
func = keyMap[e.code];
}
else if (e.keyCode in keyMap) {
warnKeyCodeDeprecated();
func = keyMap[e.keyCode];
}
else if ((e.key in this.ctrlKeysMapDown) && (e.ctrlKey || e.metaKey)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the ctrl maps come before the general key maps?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eventually yes. Probably there was not overlapping?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No overlapping in fabric. I had in mind the dev wanting to override what happens when a ctrl modifier is active.
Current logic doesn't allow.

Copy link
Contributor Author

@ShaMan123 ShaMan123 Apr 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a045103 fixes modifier priority

func = this.ctrlKeysMapDown[e.key];
}
else if ((e.code in this.ctrlKeysMapDown) && (e.ctrlKey || e.metaKey)) {
func = this.ctrlKeysMapDown[e.code];
}
else if ((e.keyCode in this.ctrlKeysMapDown) && (e.ctrlKey || e.metaKey)) {
this[this.ctrlKeysMapDown[e.keyCode]](e);
warnKeyCodeDeprecated();
func = this.ctrlKeysMapDown[e.keyCode];
}
if (typeof func === 'string' && typeof this[func] === 'function') {
func = this[func];
}
if (typeof func === 'function') {
func.call(this, e);
ShaMan123 marked this conversation as resolved.
Show resolved Hide resolved
}
else {
return;
}
e.stopImmediatePropagation();
e.preventDefault();
if (e.keyCode >= 33 && e.keyCode <= 40) {
// if i press an arrow key just update selection
if (isNavigationKey(e.key)) {
// if i press a navigation key just update selection
this.inCompositionMode = false;
this.clearContextTop();
this.renderCursorOrSelection();
Expand All @@ -149,8 +200,22 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
this._copyDone = false;
return;
}
if ((e.keyCode in this.ctrlKeysMapUp) && (e.ctrlKey || e.metaKey)) {
this[this.ctrlKeysMapUp[e.keyCode]](e);
var func;
if ((e.key in this.ctrlKeysMapUp) && (e.ctrlKey || e.metaKey)) {
func = this.ctrlKeysMapUp[e.key];
}
else if ((e.code in this.ctrlKeysMapUp) && (e.ctrlKey || e.metaKey)) {
func = this.ctrlKeysMapUp[e.code];
}
else if ((e.keyCode in this.ctrlKeysMapUp) && (e.ctrlKey || e.metaKey)) {
warnKeyCodeDeprecated();
func = this.ctrlKeysMapUp[e.keyCode];
}
if (typeof func === 'string' && typeof this[func] === 'function') {
func = this[func];
}
if (typeof func === 'function') {
func.call(this, e);
}
else {
return;
Expand Down Expand Up @@ -340,7 +405,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
cursorLocation = this.get2DCursorLocation(selectionProp),
lineIndex = cursorLocation.lineIndex;
// if on last line, down cursor goes to end of line
if (lineIndex === this._textLines.length - 1 || e.metaKey || e.keyCode === 34) {
if (lineIndex === this._textLines.length - 1 || e.metaKey || e.key === 'PageDown') {
// move to the end of a text
return this._text.length - selectionProp;
}
Expand Down Expand Up @@ -376,7 +441,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
var selectionProp = this._getSelectionForOffset(e, isRight),
cursorLocation = this.get2DCursorLocation(selectionProp),
lineIndex = cursorLocation.lineIndex;
if (lineIndex === 0 || e.metaKey || e.keyCode === 33) {
if (lineIndex === 0 || e.metaKey || e.key === 'PageUp') {
// if on first line, up cursor goes to start of line
return -selectionProp;
}
Expand Down Expand Up @@ -521,7 +586,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
if (e.altKey) {
newValue = this['findWordBoundary' + direction](this[prop]);
}
else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) {
else if (e.metaKey || e.key === 'End' || e.key === 'Home') {
newValue = this['findLineBoundary' + direction](this[prop]);
}
else {
Expand Down
31 changes: 31 additions & 0 deletions test/unit/itext_key_behaviour.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,37 @@
// needed or test hangs
iText.abortCursorAnimation();
});

QUnit.test('keyboard event map', function (assert) {
var event = {
stopPropagation: function () { },
preventDefault: function () { },
stopImmediatePropagation: function () { },
key: '\\',
keyCode: 0
};
var iText = new fabric.IText('test');
iText.__test = function (e) {
control.push(e.keyCode);
assert.equal(this, iText, 'method should be bound to iText');
}
var control = [];
iText.keysMap = {
0: '__test',
'\\': function (e) {
control.push(e.key);
assert.equal(this, iText, 'method should be bound to iText');
},
};
iText.enterEditing();
iText.onKeyDown(event);
delete iText.keysMap['\\'];
iText.onKeyDown(event);
assert.deepEqual(control, [event.key, event.keyCode], 'should fire events from key map');
iText.exitEditing();
});


// QUnit.test('copy and paste', function(assert) {
// var event = { stopPropagation: function(){}, preventDefault: function(){} };
// var iText = new fabric.IText('test', { styles: { 0: { 0: { fill: 'red' }, 1: { fill: 'blue' }}}});
Expand Down