Handle RTL layout in menus

main
Isaiah Odhner 2020-12-16 20:02:10 -05:00
parent 3c7ae6ce44
commit d14135fdc4
1 changed files with 77 additions and 20 deletions

View File

@ -5,7 +5,40 @@ function E(t){
return document.createElement(t);
}
// @TODO: make menus not take focus so we can support copy/pasting text in the text tool textarea from the menus
// @TODO: DRY hotkey helpers with jspaint (export them?)
// & defines accelerators (hotkeys) in menus and buttons and things, which get underlined in the UI.
// & can be escaped by doubling it, e.g. "&Taskbar && Start Menu"
function index_of_hotkey(text) {
// Returns the index of the ampersand that defines a hotkey, or -1 if not present.
// return english_text.search(/(?<!&)&(?!&|\s)/); // not enough browser support for negative lookbehind assertions
// The space here handles beginning-of-string matching and counteracts the offset for the [^&] so it acts like a negative lookbehind
return ` ${text}`.search(/[^&]&[^&\s]/);
}
// function has_hotkey(text) {
// return index_of_hotkey(text) !== -1;
// }
// function remove_hotkey(text) {
// return text.replace(/\s?\(&.\)/, "").replace(/([^&]|^)&([^&\s])/, "$1$2");
// }
function display_hotkey(text) {
// TODO: use a more general term like .hotkey or .accelerator?
return text.replace(/([^&]|^)&([^&\s])/, "$1<span class='menu-hotkey'>$2</span>").replace(/&&/g, "&");
}
function get_hotkey(text) {
return text[index_of_hotkey(text) + 1].toUpperCase();
}
// returns writing/layout direction, "ltr" or "rtl"
function get_direction() {
return window.get_direction ? window.get_direction() : getComputedStyle(document.body).direction;
}
// TODO: support copy/pasting text in the text tool textarea from the menus
// probably by recording document.activeElement on pointer down,
// and restoring focus before executing menu item actions.
const MENU_DIVIDER = "MENU_DIVIDER";
@ -19,9 +52,6 @@ function $MenuBar(menus){
$menus.attr("touch-action", "none");
let selecting_menus = false;
const _html = menus_key => menus_key.replace(/&(.)/, m => `<span class='menu-hotkey'>${m[1]}</span>`);
const _hotkey = menus_key => menus_key[menus_key.indexOf("&")+1].toUpperCase();
const close_menus = () => {
$menus.find(".menu-button").trigger("release");
// Close any rogue floating submenus
@ -38,7 +68,7 @@ function $MenuBar(menus){
}
};
// @TODO: API for context menus (i.e. floating menu popups)
// TODO: API for context menus (i.e. floating menu popups)
function $MenuPopup(menu_items){
const $menu_popup = $(E("div")).addClass("menu-popup");
const $menu_popup_table = $(E("table")).addClass("menu-popup-table").appendTo($menu_popup);
@ -59,7 +89,7 @@ function $MenuBar(menus){
$item.attr("tabIndex", -1);
$label.html(_html(item.item));
$label.html(display_hotkey(item.item));
$shortcut.text(item.shortcut);
$menu_popup.on("update", () => {
@ -79,7 +109,10 @@ function $MenuBar(menus){
if(item.submenu){
$submenu_area.html('<svg xmlns="http://www.w3.org/2000/svg" width="10" height="11" viewBox="0 0 10 11" style="fill:currentColor;display:inline-block;vertical-align:middle"><path d="M7.5 4.33L0 8.66L0 0z"/></svg>');
if (get_direction() === "rtl") {
$submenu_area.find("svg").css("transform", "scaleX(-1)");
}
const $submenu_popup = $MenuPopup(item.submenu).appendTo("body");
$submenu_popup.hide();
@ -87,21 +120,40 @@ function $MenuBar(menus){
$submenu_popup.show();
$submenu_popup.triggerHandler("update");
const rect = $item[0].getBoundingClientRect();
let submenu_popup_rect = $submenu_popup[0].getBoundingClientRect();
$submenu_popup.css({
position: "absolute",
left: rect.right + window.scrollX,
right: "unset", // needed for RTL layout
left: (get_direction() === "rtl" ? rect.left - submenu_popup_rect.width : rect.right) + window.scrollX,
top: rect.top + window.scrollY,
});
let submenu_popup_rect = $submenu_popup[0].getBoundingClientRect();
if (submenu_popup_rect.right > innerWidth) {
$submenu_popup.css({
left: rect.left - submenu_popup_rect.width,
});
submenu_popup_rect = $submenu_popup[0].getBoundingClientRect();
submenu_popup_rect = $submenu_popup[0].getBoundingClientRect();
// This is surely not the cleanest way of doing this,
// and the logic is not very robust in the first place,
// but I want to get RTL support done and so I'm mirroring this in the simplest way possible.
if (get_direction() === "rtl") {
if (submenu_popup_rect.left < 0) {
$submenu_popup.css({
left: 0,
left: rect.right,
});
submenu_popup_rect = $submenu_popup[0].getBoundingClientRect();
if (submenu_popup_rect.right > innerWidth) {
$submenu_popup.css({
left: innerWidth - submenu_popup_rect.width,
});
}
}
} else {
if (submenu_popup_rect.right > innerWidth) {
$submenu_popup.css({
left: rect.left - submenu_popup_rect.width,
});
submenu_popup_rect = $submenu_popup[0].getBoundingClientRect();
if (submenu_popup_rect.left < 0) {
$submenu_popup.css({
left: 0,
});
}
}
}
};
@ -175,7 +227,7 @@ function $MenuBar(menus){
if(e.ctrlKey || e.shiftKey || e.altKey || e.metaKey){
return;
}
if(String.fromCharCode(e.keyCode) === _hotkey(item.item)){
if(String.fromCharCode(e.keyCode) === get_hotkey(item.item)){
e.preventDefault();
$item.trogger("click");
}
@ -193,11 +245,16 @@ function $MenuBar(menus){
const $menu_popup = $MenuPopup(menu_items).appendTo($menu_container);
const update_position_from_containing_bounds = ()=> {
$menu_popup.css("left", "");
$menu_popup.css("left", "unset");
$menu_popup.css("right", "unset"); // needed for RTL layout
const uncorrected_rect = $menu_popup[0].getBoundingClientRect();
if(uncorrected_rect.right > innerWidth) {
// rounding down is needed for RTL layout for the rightmost menu
if(Math.floor(uncorrected_rect.right) > innerWidth) {
$menu_popup.css("left", innerWidth - uncorrected_rect.width - uncorrected_rect.left);
}
if(Math.ceil(uncorrected_rect.left) < 0) {
$menu_popup.css("left", 0);
}
};
$G.on("resize", update_position_from_containing_bounds);
$menu_popup.on("update", update_position_from_containing_bounds);
@ -207,7 +264,7 @@ function $MenuBar(menus){
$menu_button.addClass(`${menu_id}-menu-button`);
$menu_popup.hide();
$menu_button.html(_html(menus_key));
$menu_button.html(display_hotkey(menus_key));
$menu_button.attr("tabIndex", -1)
$menu_container.on("keydown", e => {
const $focused_item = $menu_popup.find(".menu-item:focus");
@ -258,7 +315,7 @@ function $MenuBar(menus){
return;
}
if(e.altKey){
if(String.fromCharCode(e.keyCode) === _hotkey(menus_key)){
if(String.fromCharCode(e.keyCode) === get_hotkey(menus_key)){
e.preventDefault();
$menu_button.trigger("pointerdown");
}