From d14135fdc420150b0acee3f7b4c345beed727a5e Mon Sep 17 00:00:00 2001 From: Isaiah Odhner Date: Wed, 16 Dec 2020 20:02:10 -0500 Subject: [PATCH] Handle RTL layout in menus --- lib/os-gui/$MenuBar.js | 97 +++++++++++++++++++++++++++++++++--------- 1 file changed, 77 insertions(+), 20 deletions(-) diff --git a/lib/os-gui/$MenuBar.js b/lib/os-gui/$MenuBar.js index 5800184..6f59edb 100644 --- a/lib/os-gui/$MenuBar.js +++ b/lib/os-gui/$MenuBar.js @@ -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(/(?$2").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 => `${m[1]}`); - 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(''); - + 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"); }