added hotkeys to navigate in stream
This commit is contained in:
parent
85ae8e2ac5
commit
a693a0970b
6 changed files with 306 additions and 2 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
108
app/assets/javascripts/app/views/stream/shortcuts.js
Normal file
108
app/assets/javascripts/app/views/stream/shortcuts.js
Normal 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";
|
||||
},
|
||||
};
|
||||
|
|
@ -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() })
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
|
|
|||
40
features/keyboard_navigation.feature
Normal file
40
features/keyboard_navigation.feature
Normal 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
|
||||
20
features/step_definitions/keyboard_navigation_steps.rb
Normal file
20
features/step_definitions/keyboard_navigation_steps.rb
Normal 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
|
||||
130
spec/javascripts/app/views/stream/shortcuts_spec.js
Normal file
130
spec/javascripts/app/views/stream/shortcuts_spec.js
Normal 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();
|
||||
});
|
||||
});
|
||||
})
|
||||
Loading…
Reference in a new issue