added hotkeys to navigate in stream

This commit is contained in:
Steffen van Bergerem 2013-03-08 23:18:19 +01:00
parent 85ae8e2ac5
commit a693a0970b
6 changed files with 306 additions and 2 deletions

View file

@ -34,6 +34,7 @@
* Show timestamp when hovering on comment time-ago string. [#4042](https://github.com/diaspora/diaspora/issues/4042)
* If sharing a post with photos to Facebook, always include URL to post [#3706](https://github.com/diaspora/diaspora/issues/3706)
* Add multiphoto for mobile post. [#4065](https://github.com/diaspora/diaspora/issues/4065)
* Add hotkeys to navigate in stream [#4089](https://github.com/diaspora/diaspora/pull/4089)
# 0.0.3.4

View file

@ -0,0 +1,108 @@
app.views.StreamShortcuts = {
_headerSize: 50,
_borderStyle: "3px solid rgb(63, 143, 186)",
setupShortcuts : function() {
$(document).on('keydown', _.bind(this._onHotkeyDown, this));
$(document).on('keyup', _.bind(this._onHotkeyUp, this));
this.on('hotkey:gotoNext', this.gotoNext, this);
this.on('hotkey:gotoPrev', this.gotoPrev, this);
this.on('hotkey:likeSelected', this.likeSelected, this);
this.on('hotkey:commentSelected', this.commentSelected, this);
},
_onHotkeyDown: function(event) {
//make sure that the user is not typing in an input field
var textAcceptingInputTypes = ["textarea", "select", "text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime", "datetime-local", "search", "color"];
if(jQuery.inArray(event.target.type, textAcceptingInputTypes) > -1){
return;
}
// trigger the events based on what key was pressed
switch (String.fromCharCode( event.which ).toLowerCase()) {
case "j":
this.trigger('hotkey:gotoNext');
break;
case "k":
this.trigger('hotkey:gotoPrev');
break;
default:
}
},
_onHotkeyUp: function(event) {
//make sure that the user is not typing in an input field
var textAcceptingInputTypes = ["textarea", "select", "text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime", "datetime-local", "search", "color"];
if(jQuery.inArray(event.target.type, textAcceptingInputTypes) > -1){
return;
}
// trigger the events based on what key was pressed
switch (String.fromCharCode( event.which ).toLowerCase()) {
case "c":
this.trigger('hotkey:commentSelected');
break;
case "l":
this.trigger('hotkey:likeSelected');
break;
default:
}
},
gotoNext: function() {
// select next post: take the first post under the header
var stream_elements = this.$('div.stream_element.loaded');
var posUser = window.pageYOffset;
for (var i = 0; i < stream_elements.length; i++) {
if(stream_elements[i].offsetTop>posUser+this._headerSize){
this.selectPost(stream_elements[i]);
return;
}
}
// standard: last post
if(stream_elements[stream_elements.length-1]){
this.selectPost(stream_elements[stream_elements.length-1]);
}
},
gotoPrev: function() {
// select previous post: take the first post above the header
var stream_elements = this.$('div.stream_element.loaded');
var posUser = window.pageYOffset;
for (var i = stream_elements.length-1; i >=0; i--) {
if(stream_elements[i].offsetTop<posUser+this._headerSize){
this.selectPost(stream_elements[i]);
return;
}
}
// standard: first post
if(stream_elements[0]){
this.selectPost(stream_elements[0]);
}
},
commentSelected: function() {
$('a.focus_comment_textarea',this.$('div.stream_element.loaded.shortcut_selected')).click();
},
likeSelected: function() {
$('a.like:first',this.$('div.stream_element.loaded.shortcut_selected')).click();
},
selectPost: function(element){
//remove the selection and selected-class from all posts
var selected=this.$('div.stream_element.loaded.shortcut_selected');
selected.css( "border-left", "" );
selected.removeClass('shortcut_selected');
//move to new post
window.scrollTo(window.pageXOffset, element.offsetTop-this._headerSize);
//add the selection and selected-class to new post
element.style.borderLeft=this._borderStyle;
element.className+=" shortcut_selected";
},
};

View file

@ -1,4 +1,8 @@
app.views.Stream = app.views.InfScroll.extend({
//= require ./stream/shortcuts
app.views.Stream = app.views.InfScroll.extend(_.extend(
app.views.StreamShortcuts, {
initialize: function(options) {
this.stream = this.model
this.collection = this.stream.items
@ -8,6 +12,7 @@ app.views.Stream = app.views.InfScroll.extend({
this.setupNSFW()
this.setupLightbox()
this.setupInfiniteScroll()
this.setupShortcuts()
},
postClass : app.views.StreamPost,
@ -24,4 +29,4 @@ app.views.Stream = app.views.InfScroll.extend({
_.map(this.postViews, function(view){ view.render() })
}
}
});
}));

View file

@ -0,0 +1,40 @@
@javascript
Feature: Keyboard navigation
In order not to have to move my hand to the mouse
As a user
I want to be able to navigate the stream by keyboard
Background:
Given many posts from alice for bob
And I sign in as "bob@bob.bob"
Scenario: navigate downwards
When I am on the home page
When I wait for the ajax to finish
And I press the "J" key somewhere
Then post 1 should be highlighted
And I should have navigated to the highlighted post
When I press the "J" key somewhere
Then post 2 should be highlighted
And I should have navigated to the highlighted post
Given I expand the publisher
When I press the "J" key in the publisher
Then post 2 should be highlighted
Scenario: navigate upwards
When I am on the home page
When I wait for the ajax to finish
And I scroll to post 3
When I press the "K" key somewhere
Then post 2 should be highlighted
And I should have navigated to the highlighted post
Scenario: expand the comment form in the main stream
When I am on the home page
When I wait for the ajax to finish
Then the first comment field should be closed
When I press the "J" key somewhere
And I press the "C" key somewhere
Then the first comment field should be open

View file

@ -0,0 +1,20 @@
When /^I press the "([^\"]*)" key somewhere$/ do |key|
find("div.stream_element").native.send_keys(key)
end
When /^I press the "([^\"]*)" key in the publisher$/ do |key|
find("#status_message_fake_text").native.send_keys(key)
end
Then /^post (\d+) should be highlighted$/ do |position|
find(".shortcut_selected .post-content").text.should == stream_element_numbers_content(position).text
end
And /^I should have navigated to the highlighted post$/ do
find(".shortcut_selected")["offsetTop"].to_i.should == page.evaluate_script("window.pageYOffset + 50").to_i
end
When /^I scroll to post (\d+)$/ do |position|
page.driver.browser.execute_script("var element = $('div.stream_element')[" + position + " - 1];
window.scrollTo(window.pageXOffset, element.offsetTop-50);")
end

View file

@ -0,0 +1,130 @@
describe("app.views.StreamShortcuts", function () {
beforeEach(function() {
this.post1 = factory.post({author : factory.author({name : "Rebecca Black", id : 1492})})
this.post2 = factory.post({author : factory.author({name : "John Stamos", id : 1987})})
this.stream = new app.models.Stream();
this.stream.add([this.post1, this.post2]);
this.view = new app.views.Stream({model : this.stream});
this.view.render();
expect(this.view.$('div.stream_element.loaded').length).toBe(2);
});
describe("loading the stream", function(){
it("should setup the shortcuts", function(){
spyOn(this.view, 'setupShortcuts');
this.view.initialize();
expect(this.view.setupShortcuts).toHaveBeenCalled();
});
});
describe("pressing 'j'", function(){
it("should call 'gotoNext' if not pressed in an input field", function(){
spyOn(this.view, 'gotoNext');
this.view.initialize();
var e = $.Event("keydown", { which: 74, target: {type: "div"} });
//verify that the test is correct
expect(String.fromCharCode( e.which ).toLowerCase()).toBe('j');
this.view._onHotkeyDown(e);
expect(this.view.gotoNext).toHaveBeenCalled();
});
it("'gotoNext' should call 'selectPost'", function(){
spyOn(this.view, 'selectPost');
this.view.gotoNext();
expect(this.view.selectPost).toHaveBeenCalled();
});
it("shouldn't do anything if the user types in an input field", function(){
spyOn(this.view, 'gotoNext');
spyOn(this.view, 'selectPost');
this.view.initialize();
var e = $.Event("keydown", { which: 74, target: {type: "textarea"} });
//verify that the test is correct
expect(String.fromCharCode( e.which ).toLowerCase()).toBe('j');
this.view._onHotkeyDown(e);
expect(this.view.gotoNext).not.toHaveBeenCalled();
expect(this.view.selectPost).not.toHaveBeenCalled();
});
});
describe("pressing 'k'", function(){
it("should call 'gotoPrev' if not pressed in an input field", function(){
spyOn(this.view, 'gotoPrev');
this.view.initialize();
var e = $.Event("keydown", { which: 75, target: {type: "div"} });
//verify that the test is correct
expect(String.fromCharCode( e.which ).toLowerCase()).toBe('k');
this.view._onHotkeyDown(e);
expect(this.view.gotoPrev).toHaveBeenCalled();
});
it("'gotoPrev' should call 'selectPost'", function(){
spyOn(this.view, 'selectPost');
this.view.gotoPrev();
expect(this.view.selectPost).toHaveBeenCalled();
});
it("shouldn't do anything if the user types in an input field", function(){
spyOn(this.view, 'gotoPrev');
spyOn(this.view, 'selectPost');
this.view.initialize();
var e = $.Event("keydown", { which: 75, target: {type: "textarea"} });
//verify that the test is correct
expect(String.fromCharCode( e.which ).toLowerCase()).toBe('k');
this.view._onHotkeyDown(e);
expect(this.view.gotoPrev).not.toHaveBeenCalled();
expect(this.view.selectPost).not.toHaveBeenCalled();
});
});
describe("pressing 'c'", function(){
it("should click on the comment-button if not pressed in an input field", function(){
spyOn(this.view, 'commentSelected');
this.view.initialize();
var e = $.Event("keyup", { which: 67, target: {type: "div"} });
//verify that the test is correct
expect(String.fromCharCode( e.which ).toLowerCase()).toBe('c');
this.view._onHotkeyUp(e);
expect(this.view.commentSelected).toHaveBeenCalled();
});
it("shouldn't do anything if the user types in an input field", function(){
spyOn(this.view, 'commentSelected');
this.view.initialize();
var e = $.Event("keyup", { which: 67, target: {type: "textarea"} });
//verify that the test is correct
expect(String.fromCharCode( e.which ).toLowerCase()).toBe('c');
this.view._onHotkeyUp(e);
expect(this.view.commentSelected).not.toHaveBeenCalled();
});
});
describe("pressing 'l'", function(){
it("should click on the like-button if not pressed in an input field", function(){
spyOn(this.view, 'likeSelected');
this.view.initialize();
var e = $.Event("keyup", { which: 76, target: {type: "div"} });
//verify that the test is correct
expect(String.fromCharCode( e.which ).toLowerCase()).toBe('l');
this.view._onHotkeyUp(e);
expect(this.view.likeSelected).toHaveBeenCalled();
});
it("shouldn't do anything if the user types in an input field", function(){
spyOn(this.view, 'likeSelected');
this.view.initialize();
var e = $.Event("keyup", { which: 76, target: {type: "textarea"} });
//verify that the test is correct
expect(String.fromCharCode( e.which ).toLowerCase()).toBe('l');
this.view._onHotkeyUp(e);
expect(this.view.likeSelected).not.toHaveBeenCalled();
});
});
})