Merge pull request #4089 from svbergerem/feature/add-hotkeys
added hotkeys to navigate in stream
This commit is contained in:
commit
f08f4a22f7
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)
|
* 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)
|
* 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 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
|
# 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) {
|
initialize: function(options) {
|
||||||
this.stream = this.model
|
this.stream = this.model
|
||||||
this.collection = this.stream.items
|
this.collection = this.stream.items
|
||||||
|
|
@ -8,6 +12,7 @@ app.views.Stream = app.views.InfScroll.extend({
|
||||||
this.setupNSFW()
|
this.setupNSFW()
|
||||||
this.setupLightbox()
|
this.setupLightbox()
|
||||||
this.setupInfiniteScroll()
|
this.setupInfiniteScroll()
|
||||||
|
this.setupShortcuts()
|
||||||
},
|
},
|
||||||
|
|
||||||
postClass : app.views.StreamPost,
|
postClass : app.views.StreamPost,
|
||||||
|
|
@ -24,4 +29,4 @@ app.views.Stream = app.views.InfScroll.extend({
|
||||||
_.map(this.postViews, function(view){ view.render() })
|
_.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