zulip/web/src/messages_overlay_ui.js

182 lines
6.3 KiB
JavaScript
Raw Normal View History

import $ from "jquery";
export function row_with_focus(context) {
const focused_item = $(`.${CSS.escape(context.box_item_selector)}:focus`)[0];
return $(focused_item).parent(`.${CSS.escape(context.row_item_selector)}`);
}
export function activate_element(elem, context) {
$(`.${CSS.escape(context.box_item_selector)}`).removeClass("active");
$(elem).expectOne().addClass("active");
elem.focus();
}
export function get_focused_element_id(context) {
return row_with_focus(context).attr(context.id_attribute_name);
}
export function focus_on_sibling_element(context) {
const $next_row = row_after_focus(context);
const $prev_row = row_before_focus(context);
let elem_to_be_focused_id;
// Try to get the next item in the list and 'focus' on it.
// Use previous item as a fallback.
if ($next_row[0] !== undefined) {
elem_to_be_focused_id = $next_row.attr(context.id_attribute_name);
} else if ($prev_row[0] !== undefined) {
elem_to_be_focused_id = $prev_row.attr(context.id_attribute_name);
}
const new_focus_element = get_element_by_id(elem_to_be_focused_id, context);
if (new_focus_element[0] !== undefined) {
activate_element(new_focus_element[0].children[0], context);
}
}
export function modals_handle_events(event_key, context) {
initialize_focus(event_key, context);
// This detects up arrow key presses when the overlay
// is open and scrolls through.
if (event_key === "up_arrow" || event_key === "vim_up") {
scroll_to_element(row_before_focus(context), context);
}
// This detects down arrow key presses when the overlay
// is open and scrolls through.
if (event_key === "down_arrow" || event_key === "vim_down") {
scroll_to_element(row_after_focus(context), context);
}
if (event_key === "backspace" || event_key === "delete") {
context.on_delete();
}
if (event_key === "enter") {
context.on_enter();
}
}
export function set_initial_element(element_id, context) {
if (element_id) {
const current_element = get_element_by_id(element_id, context);
const focus_element = current_element[0].children[0];
activate_element(focus_element, context);
$(`.${CSS.escape(context.items_list_selector)}`)[0].scrollTop = 0;
}
}
function row_before_focus(context) {
const $focused_row = row_with_focus(context);
const $prev_row = $focused_row.prev(`.${CSS.escape(context.row_item_selector)}:visible`);
// The draft modal can have two sub-sections. This handles the edge case
// when the user moves from the second "Other drafts" section to the first
// section which contains drafts from a particular narrow.
if (
$prev_row.length === 0 &&
$focused_row.parent().attr("id") === "other-drafts" &&
$("#drafts-from-conversation").is(":visible")
) {
return $($("#drafts-from-conversation").children(".draft-row:visible").last());
}
return $prev_row;
}
function row_after_focus(context) {
const $focused_row = row_with_focus(context);
const $next_row = $focused_row.next(`.${CSS.escape(context.row_item_selector)}:visible`);
// The draft modal can have two sub-sections. This handles the edge case
// when the user moves from the first section (drafts from a particular
// narrow) to the second section which contains the rest of the drafts.
if (
$next_row.length === 0 &&
$focused_row.parent().attr("id") === "drafts-from-conversation" &&
$("#other-drafts").is(":visible")
) {
return $("#other-drafts").children(".draft-row:visible").first();
}
return $next_row;
}
function initialize_focus(event_name, context) {
// If an item is not focused in modal, then focus the last item
// if up_arrow is clicked or the first item if down_arrow is clicked.
if (
(event_name !== "up_arrow" && event_name !== "down_arrow") ||
$(`.${CSS.escape(context.box_item_selector)}:focus`)[0]
) {
return;
}
const modal_items_ids = context.get_items_ids();
if (modal_items_ids.length === 0) {
// modal is empty
return;
}
let element;
function get_last_element() {
const last_id = modal_items_ids.at(-1);
return get_element_by_id(last_id, context);
}
function get_first_element() {
const first_id = modal_items_ids[0];
return get_element_by_id(first_id, context);
}
if (event_name === "up_arrow") {
element = get_last_element();
} else if (event_name === "down_arrow") {
element = get_first_element();
}
const focus_element = element[0].children[0];
activate_element(focus_element, context);
}
function scroll_to_element($element, context) {
if ($element[0] === undefined) {
return;
}
if ($element[0].children[0] === undefined) {
return;
}
activate_element($element[0].children[0], context);
const $items_list = $(`.${CSS.escape(context.items_list_selector)}`);
const $items_container = $(`.${CSS.escape(context.items_container_selector)}`);
const $box_item = $(`.${CSS.escape(context.box_item_selector)}`);
// If focused element is first, scroll to the top.
if ($box_item.first()[0].parentElement === $element[0]) {
$items_list[0].scrollTop = 0;
}
// If focused element is last, scroll to the bottom.
if ($box_item.last()[0].parentElement === $element[0]) {
$items_list[0].scrollTop = $items_list[0].scrollHeight - $items_list.height();
}
// If focused element is cut off from the top, scroll up halfway in modal.
if ($element.position().top < 55) {
// 55 is the minimum distance from the top that will require extra scrolling.
$items_list[0].scrollTop -= $items_list[0].clientHeight / 2;
}
// If focused element is cut off from the bottom, scroll down halfway in modal.
const dist_from_top = $element.position().top;
const total_dist = dist_from_top + $element[0].clientHeight;
const dist_from_bottom = $items_container[0].clientHeight - total_dist;
if (dist_from_bottom < -4) {
// -4 is the min dist from the bottom that will require extra scrolling.
$items_list[0].scrollTop += $items_list[0].clientHeight / 2;
}
}
function get_element_by_id(id, context) {
return $(`[${context.id_attribute_name}='${id}']`);
}