hotkey.js: Add hotkey for drafts.

* 'd' in message view opens drafts.

This also adds hotkeys within the drafts UI:
* Up/down arrow keys navigate the drafts.
* Pressing enter edits the selected draft.
* Pressing backspace deletes the selected draft.

Some variable names tweaked by tabbott.
This commit is contained in:
Jonathan Pan
2017-03-18 13:26:24 -07:00
committed by Tim Abbott
parent d17b759fa2
commit 5556d2341c
6 changed files with 149 additions and 3 deletions

View File

@@ -14,6 +14,11 @@
set_global('activity', { set_global('activity', {
}); });
set_global('drafts', {
drafts_handle_events: function () { return; },
drafts_overlay_open: function () { return; },
});
set_global('$', function () { set_global('$', function () {
return { return {
// Hack: Used for reactions hotkeys; may want to restructure. // Hack: Used for reactions hotkeys; may want to restructure.
@@ -139,7 +144,7 @@ function stubbing(func_name_to_stub, test_function) {
// Unmapped keys should immediately return false, without // Unmapped keys should immediately return false, without
// calling any functions outside of hotkey.js. // calling any functions outside of hotkey.js.
assert_unmapped('abdefhlmoptuxyz'); assert_unmapped('abefhlmoptuxyz');
assert_unmapped('BEFHILNOQTWXYZ'); assert_unmapped('BEFHILNOQTWXYZ');
// We have to skip some checks due to the way the code is // We have to skip some checks due to the way the code is
@@ -193,6 +198,7 @@ function stubbing(func_name_to_stub, test_function) {
assert_mapping('C', 'compose_actions.start'); assert_mapping('C', 'compose_actions.start');
assert_mapping('P', 'narrow.by'); assert_mapping('P', 'narrow.by');
assert_mapping('g', 'gear_menu.open'); assert_mapping('g', 'gear_menu.open');
assert_mapping('d', 'drafts.toggle');
// Next, test keys that only work on a selected message. // Next, test keys that only work on a selected message.
var message_view_only_keys = '@*+rRjJkKsSvi:GM'; var message_view_only_keys = '@*+rRjJkKsSvi:GM';

View File

@@ -203,9 +203,120 @@ exports.setup_page = function (callback) {
populate_and_fill(); populate_and_fill();
}; };
exports.drafts_overlay_open = function () {
return $("#draft_overlay").hasClass("show");
};
exports.drafts_handle_events = function (e, event_key) {
var draft_arrow = draft_model.get();
var draft_id_arrow = Object.getOwnPropertyNames(draft_arrow);
// This detects up arrow key presses when the draft overlay
// is open and scrolls through the drafts.
if (event_key === "up_arrow") {
if ($(".draft-info-box:focus")[0] === undefined) {
if (draft_id_arrow.length > 0) {
var last_draft = draft_id_arrow[draft_id_arrow.length-1];
var last_draft_element = document.querySelectorAll('[data-draft-id="' + last_draft + '"]');
var focus_up_element = last_draft_element[0].children[0];
focus_up_element.focus();
}
}
var focus_draft_up_row = $(".draft-info-box:focus")[0].parentElement;
var prev_focus_draft_row = $(focus_draft_up_row).prev();
if ($(".draft-info-box:first")[0].parentElement === prev_focus_draft_row[0]) {
$(".drafts-list")[0].scrollTop = 0;
}
if (prev_focus_draft_row[0].children[0] !== undefined) {
prev_focus_draft_row[0].children[0].focus();
// If the next draft is cut off, scroll more.
if (prev_focus_draft_row.position().top < 55) {
// 55 is the minimum distance from the top that will require extra scrolling.
$(".drafts-list")[0].scrollTop = $(".drafts-list")[0].scrollTop - (55 - prev_focus_draft_row.position().top);
}
e.preventDefault();
}
return true;
}
// This detects down arrow key presses when the draft overlay
// is open and scrolls through the drafts.
if (event_key === "down_arrow") {
if ($(".draft-info-box:focus")[0] === undefined) {
if (draft_id_arrow.length > 0) {
var first_draft = draft_id_arrow[0];
var first_draft_element = document.querySelectorAll('[data-draft-id="' + first_draft + '"]');
var focus_down_element = first_draft_element[0].children[0];
focus_down_element.focus();
}
}
var focus_draft_down_row = $(".draft-info-box:focus")[0].parentElement;
var next_focus_draft_row = $(focus_draft_down_row).next();
if ($(".draft-info-box:last")[0].parentElement === next_focus_draft_row[0]) {
$(".drafts-list")[0].scrollTop = $('.drafts-list')[0].scrollHeight - $('.drafts-list').height();
}
if (next_focus_draft_row[0] !== undefined) {
next_focus_draft_row[0].children[0].focus();
// If the next draft is cut off, scroll more.
if (next_focus_draft_row.position() !== undefined) {
var dist_from_top = next_focus_draft_row.position().top;
var total_dist = dist_from_top + next_focus_draft_row[0].clientHeight;
var dist_from_bottom = $(".drafts-container")[0].clientHeight - total_dist;
if (dist_from_bottom < -4) {
//-4 is the min dist from the bottom that will require extra scrolling.
$(".drafts-list")[0].scrollTop = $(".drafts-list")[0].scrollTop + 2 - (dist_from_bottom);
}
}
e.preventDefault();
}
return true;
}
// Allows user to delete drafts with backspace
if (event_key === "backspace") {
var elt = document.activeElement;
if (elt.parentElement.hasAttribute("data-draft-id")) {
var focused_draft = $(elt.parentElement)[0].getAttribute("data-draft-id");
var focus_draft_back_row = $(elt)[0].parentElement;
var backnext_focus_draft_row = $(focus_draft_back_row).next();
var backprev_focus_draft_row = $(focus_draft_back_row).prev();
var delete_id;
if (backnext_focus_draft_row[0] !== undefined) {
delete_id = backnext_focus_draft_row[0].getAttribute("data-draft-id");
} else if (backprev_focus_draft_row[0] !== undefined) {
delete_id = backprev_focus_draft_row[0].getAttribute("data-draft-id");
}
drafts.draft_model.deleteDraft(focused_draft);
document.activeElement.parentElement.remove();
var new_focus_element = document.querySelectorAll('[data-draft-id="' + delete_id + '"]');
if (new_focus_element[0] !== undefined) {
new_focus_element[0].children[0].focus();
}
if ($("#drafts_table .draft-row").length === 0) {
$('#drafts_table .no-drafts').show();
}
return true;
}
}
};
exports.toggle = function () {
if (exports.drafts_overlay_open()) {
modals.close_modal("drafts");
} else {
exports.launch();
}
};
exports.launch = function () { exports.launch = function () {
exports.setup_page(function () { exports.setup_page(function () {
$("#draft_overlay").addClass("show"); $("#draft_overlay").addClass("show");
var draft_list = drafts.draft_model.get();
var draft_id_list = Object.getOwnPropertyNames(draft_list);
if (draft_id_list.length > 0) {
var last_draft = draft_id_list[draft_id_list.length-1];
var last_draft_element = document.querySelectorAll('[data-draft-id="' + last_draft + '"]');
var focus_element = last_draft_element[0].children[0];
focus_element.focus();
$(".drafts-list")[0].scrollTop = $('.drafts-list')[0].scrollHeight - $('.drafts-list').height();
}
}); });
}; };

View File

@@ -110,6 +110,7 @@ var keypress_mappings = {
85: {name: 'keyboard_sub', message_view_only: false}, //'U' 85: {name: 'keyboard_sub', message_view_only: false}, //'U'
86: {name: 'view_selected_stream', message_view_only: false}, //'V' 86: {name: 'view_selected_stream', message_view_only: false}, //'V'
99: {name: 'compose', message_view_only: true}, // 'c' 99: {name: 'compose', message_view_only: true}, // 'c'
100: {name: 'open_drafts', message_view_only: false}, // 'd'
103: {name: 'gear_menu', message_view_only: true}, // 'g' 103: {name: 'gear_menu', message_view_only: true}, // 'g'
105: {name: 'message_actions', message_view_only: true}, // 'i' 105: {name: 'message_actions', message_view_only: true}, // 'i'
106: {name: 'vim_down', message_view_only: true}, // 'j' 106: {name: 'vim_down', message_view_only: true}, // 'j'
@@ -207,7 +208,7 @@ exports.process_escape_key = function (e) {
return true; return true;
} }
if ($("#draft_overlay").hasClass("show")) { if (drafts.drafts_overlay_open()) {
modals.close_modal("drafts"); modals.close_modal("drafts");
return true; return true;
} }
@@ -333,6 +334,21 @@ exports.process_enter_key = function (e) {
return false; return false;
} }
// This handles when pressing enter while looking at drafts.
// It restores draft that is focused.
if (drafts.drafts_overlay_open()) {
var draft_list = drafts.draft_model.get();
if (document.activeElement.parentElement.hasAttribute("data-draft-id")) {
var focused_draft = document.activeElement.parentElement.getAttribute("data-draft-id");
drafts.restore_draft(focused_draft);
} else {
var draft_id_list = Object.getOwnPropertyNames(draft_list);
var first_draft = draft_id_list[draft_id_list.length-1];
drafts.restore_draft(first_draft);
}
return true;
}
// If we're on a button or a link and have pressed enter, let the // If we're on a button or a link and have pressed enter, let the
// browser handle the keypress // browser handle the keypress
// //
@@ -435,6 +451,10 @@ exports.process_hotkey = function (e, hotkey) {
return exports.process_escape_key(e); return exports.process_escape_key(e);
} }
if (drafts.drafts_overlay_open()) {
drafts.drafts_handle_events(e, event_name);
}
if (hotkey.message_view_only && ui_state.home_tab_obscured()) { if (hotkey.message_view_only && ui_state.home_tab_obscured()) {
if ((event_name === 'up_arrow' || event_name === 'down_arrow') && exports.is_subs()) { if ((event_name === 'up_arrow' || event_name === 'down_arrow') && exports.is_subs()) {
subs.switch_rows(event_name); subs.switch_rows(event_name);
@@ -598,6 +618,9 @@ exports.process_hotkey = function (e, hotkey) {
subs.new_stream_clicked(); subs.new_stream_clicked();
} }
return true; return true;
case 'open_drafts':
drafts.toggle();
return true;
} }
if (current_msg_list.empty()) { if (current_msg_list.empty()) {

View File

@@ -1,5 +1,5 @@
<div class="draft-row" data-draft-id="{{draft_id}}"> <div class="draft-row" data-draft-id="{{draft_id}}">
<div class="draft-info-box"> <div class="draft-info-box" tabindex="0">
{{#if is_stream}} {{#if is_stream}}
<div class="message_header message_header_stream"> <div class="message_header message_header_stream">
<div class="message-header-contents"> <div class="message-header-contents">

View File

@@ -57,6 +57,8 @@ Zulip keyboard shortcuts are divided into four categories:
shortcut allows the user to send the message that they've written. shortcut allows the user to send the message that they've written.
* **Cancel compose**: `Esc` - This shortcut allows the user to cancel * **Cancel compose**: `Esc` - This shortcut allows the user to cancel
and discard their unsent message. and discard their unsent message.
* **View drafts**: `d` - This shortcut allows the user to open the
drafts overlay.
## Narrowing ## Narrowing

View File

@@ -80,6 +80,10 @@
<td class="hotkey">Esc</td> <td class="hotkey">Esc</td>
<td class="definition">{% trans %}Cancel compose{% endtrans %}</td> <td class="definition">{% trans %}Cancel compose{% endtrans %}</td>
</tr> </tr>
<tr>
<td class="hotkey">d</td>
<td class="definition">{% trans %}View drafts{% endtrans %}</td>
</tr>
</table> </table>
</div> </div>