Skip to content

Instantly share code, notes, and snippets.

@zoghal
Created October 13, 2012 20:15
Show Gist options
  • Save zoghal/3885987 to your computer and use it in GitHub Desktop.
Save zoghal/3885987 to your computer and use it in GitHub Desktop.
KeyResponder
(function() {
var get = Ember.get, set = Ember.set;
/*
Holds a stack of key responder views. With this we can neatly handle restoring the previous key responder
when some modal UI element is closed. There's a few simple rules that governs the usage of the stack:
- mouse click does .replace (this should also be used for programmatically taking focus when not a modal element)
- opening a modal UI element does .push
- closing a modal element does .pop
Also noteworthy is that a view will be signaled that it loses the key focus only when it's popped off the
stack, not when something is pushed on top. The idea is that when a modal UI element is opened, we know
that the previously focused view will re-gain the focus as soon as the modal element is closed. So if the
previously focused view was e.g. in the middle of some edit operation, it shouldn't cancel that operation.
*/
Ember.KeyResponder = Ember.ArrayProxy.extend({
content: Ember.computed(function() {
return Ember.A();
}),
current: function() {
return get(this, 'lastObject');
}.property('lastObject'),
pushView: function(view) {
if (!Ember.none(view)) {
view.trigger('willBecomeKeyResponder');
set(view, 'isFocused', true);
this.pushObject(view);
view.trigger('didBecomeKeyResponder');
}
return view;
},
popView: function() {
if (get(this, 'length') > 0) {
var view = get(this, 'current');
if (view) {
view.trigger('willLoseKeyResponder');
}
view = this.popObject();
set(view, 'isFocused', false);
view.trigger('didLoseKeyResponder');
return view;
} else {
return undefined;
}
},
replaceView: function(view) {
if (get(this, 'current') !== view) {
this.popView();
return this.pushView(view);
}
}
});
Ember.Router.reopen({
keyResponder: Ember.computed(function() {
return Ember.KeyResponder.create();
})
});
Ember.KEY_EVENTS = {
8: 'deleteBackward',
9: 'insertTab',
13: 'insertNewline',
27: 'cancel',
32: 'insertSpace',
37: 'moveLeft',
38: 'moveUp',
39: 'moveRight',
40: 'moveDown',
46: 'deleteForward'
};
Ember.MODIFIED_KEY_EVENTS = {
8: 'deleteForward',
9: 'insertBacktab',
37: 'moveLeftAndModifySelection',
38: 'moveUpAndModifySelection',
39: 'moveRightAndModifySelection',
40: 'moveDownAndModifySelection'
};
// This logic is needed so that the view that handled mouseDown will receive mouseMoves and the eventual mouseUp, even if the
// pointer no longer is on top of that view. Without this, you get inconsistencies with buttons and all controls that handle
// mouse click events. The sproutcore event dispatcher always first looks up 'eventManager' property on the view that's
// receiving an event, and let's that handle the event, if defined. So this should be mixed in to all the Flame views.
Ember.KeyResponderSupport = Ember.Mixin.create({
// Set to true in your view if you want to accept key responder status (which is needed for handling key events)
acceptsKeyResponder: false,
keyResponder: Ember.computed('controller.target.keyResponder', function() {
return get(this, 'controller.target.keyResponder');
}),
canBecomeKeyResponder: Ember.computed('acceptsKeyResponder', 'isDisabled', 'isVisible', function() {
return get(this, 'acceptsKeyResponder') && !get(this, 'isDisabled') && get(this, 'isVisible');
}),
/*
Sets this view as the target of key events. Call this if you need to make this happen programmatically.
This gets also called on mouseDown if the view handles that, returns true and doesn't have property 'acceptsKeyResponder'
set to false. If mouseDown returned true but 'acceptsKeyResponder' is false, this call is propagated to the parent view.
If called with no parameters or with replace = true, the current key responder is first popped off the stack and this
view is then pushed. See comments for Ember.KeyResponderStack above for more insight.
*/
becomeKeyResponder: function(replace) {
var keyResponder = get(this, 'keyResponder'), parent;
if (!keyResponder) { return; }
if (get(keyResponder, 'current') === this) { return; }
if (get(this, 'canBecomeKeyResponder')) {
if (replace === undefined || replace === true) {
return keyResponder.replaceView(this);
} else {
return keyResponder.pushView(this);
}
} else {
parent = get(this, 'parentView');
if (parent && parent.becomeKeyResponder) {
return parent.becomeKeyResponder(replace);
}
}
},
/*
Resign key responder status by popping the head off the stack. The head might or might not be this view,
depending on whether user clicked anything since this view became the key responder. The new key responder
will be the next view in the stack, if any.
*/
resignKeyResponder: function() {
var keyResponder = get(this, 'keyResponder');
if (!keyResponder) { return; }
keyResponder.popView();
},
init: function() {
this._super();
this.on('mouseDown', this, function() {
this.becomeKeyResponder();
});
this.on('keyDown', this, 'respondToKeyEvent');
},
respondToKeyEvent: function(event) {
var view = get(this, 'keyResponder.current');
if (view && get(view, 'canBecomeKeyResponder')) {
view.interpretKeyEvents(event);
}
},
interpretKeyEvents: function(event) {
var mapping = event.shiftKey ? Ember.MODIFIED_KEY_EVENTS : Ember.KEY_EVENTS,
eventName = mapping[event.keyCode];
if (eventName && this.has(eventName)) {
return this.trigger(eventName, event);
}
return false;
}
});
Ember.View.reopen(Ember.KeyResponderSupport);
Ember.InputSupport = Ember.Mixin.create({
canBecomeKeyView: true,
acceptsKeyResponder: true,
isDisabled: Ember.computed('disabled', function() {
return get(this, 'disabled');
}),
init: function() {
this._super();
this.on('focusIn', this, function(event) {
this.becomeKeyResponder();
});
this.on('focusOut', this, function(event) {
this.resignKeyResponder();
});
this.on('didBecomeKeyResponder', this, function() {
Ember.tryInvoke(this.$(), 'focus');
});
this.on('didLoseKeyResponder', this, function() {
Ember.tryInvoke(this.$(), 'blur');
});
}
});
Ember.TextSupport.reopen(Ember.InputSupport);
Ember.Checkbox.reopen(Ember.InputSupport);
Ember.Select.reopen(Ember.InputSupport);
Ember.Button.reopen(Ember.InputSupport);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment