Fix toolbar add-on breakages and introduce toolbar tray layout & API (#2301)

* Layout toolbar using CSS grid, introducing left and right trays

The trays provide a space for add-ons to introduce their own widgets to the toolbar without interfering with each other.

* Align tray items to the top

* Move absolutely positioned add-on items to right toolbar tray

Workaround that fixes breakages in add-ons like AMBOSS, Study Timer, and potentially others that currently still inject absolutely positioned elements into the toolbar using `top_toolbar_did_init_links`.

* Account for add-ons that add manual padding (e.g. Study Timer)

* Add docstrings and slightly refactor

* Tweak item alignment

* Introduce hooks for extending left and right toolbar trays

* Assign CSS classes to all tray items

* Add disclaimer on transitional nature of new hooks
This commit is contained in:
Aristotelis 2023-01-09 23:48:50 +01:00 committed by GitHub
parent cef672a6a1
commit 91d563278f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 142 additions and 6 deletions

View file

@ -6,8 +6,33 @@
@use "sass/elevation" as *;
@use "sass/button-mixins" as button;
.header {
height: 41px;
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: start;
align-content: space-between;
}
.left-tray {
justify-self: start;
}
.right-tray {
justify-self: end;
}
.left-tray,
.right-tray {
align-self: start;
display: flex;
flex-direction: row;
align-items: start;
}
.toolbar {
display: inline-block;
height: 31px;
justify-self: center;
white-space: nowrap;
overflow: hidden;
border-bottom-left-radius: prop(border-radius-large);

View file

@ -27,3 +27,63 @@ function updateSyncColor(state: SyncState) {
break;
}
}
// Dealing with legacy add-ons that used CSS to absolutely position
// themselves at toolbar edges
function isAbsolutelyPositioned(node: Node): boolean {
if (!(node instanceof HTMLElement)) {
return false;
}
return getComputedStyle(node).position === "absolute";
}
function isLegacyAddonElement(node: Node): boolean {
if (isAbsolutelyPositioned(node)) {
return true;
}
for (const child of node.childNodes) {
if (isAbsolutelyPositioned(child)) {
return true;
}
}
return false;
}
function getElementDimensions(element: HTMLElement): [number, number] {
const widths = [element.offsetWidth];
const heights = [element.offsetHeight];
// Some add-ons inject spans or anchors into the toolbar whose dimensions,
// as reported by the properties above are zero, but still occupy space due
// to their child elements:
for (const child of element.childNodes) {
if (!(child instanceof HTMLElement)) {
continue;
}
widths.push(child.offsetWidth);
heights.push(child.offsetHeight);
}
return [Math.max(...widths), Math.max(...heights)];
}
function moveLegacyAddonsToTray() {
const rightTray = document.getElementsByClassName("right-tray")[0];
const toolbarChildren = document.querySelectorAll<HTMLElement>(".toolbar > *");
const legacyAddonElements: HTMLElement[] = Array.from(toolbarChildren)
.reverse() // restore original add-on load order
.filter(isLegacyAddonElement);
for (const element of legacyAddonElements) {
const wrapperElement = document.createElement("div");
const dimensions = getElementDimensions(element);
element.style.right = "0px"; // remove manual padding
wrapperElement.append(element);
wrapperElement.style.cssText = `\
width: ${dimensions[0]}px; height: ${dimensions[1]}}px;
margin-left: 5px; margin-right: 5px; position: relative;`;
wrapperElement.className = "tray-item tray-item-legacy";
rightTray.append(wrapperElement);
}
}
document.addEventListener("DOMContentLoaded", moveLegacyAddonsToTray);

View file

@ -114,8 +114,13 @@ class Toolbar:
web_context = web_context or TopToolbar(self)
link_handler = link_handler or self._linkHandler
self.web.set_bridge_command(link_handler, web_context)
body = self._body.format(
toolbar_content=self._centerLinks(),
left_tray_content=self._left_tray_content(),
right_tray_content=self._right_tray_content(),
)
self.web.stdHtml(
self._body % self._centerLinks(),
body,
css=["css/toolbar.css"],
js=["js/vendor/jquery.min.js", "js/toolbar.js"],
context=web_context,
@ -204,6 +209,22 @@ class Toolbar:
return "\n".join(links)
# Add-ons
######################################################################
def _left_tray_content(self) -> str:
left_tray_content: list[str] = []
gui_hooks.top_toolbar_will_set_left_tray_content(left_tray_content, self)
return self._process_tray_content(left_tray_content)
def _right_tray_content(self) -> str:
right_tray_content: list[str] = []
gui_hooks.top_toolbar_will_set_right_tray_content(right_tray_content, self)
return self._process_tray_content(right_tray_content)
def _process_tray_content(self, content: list[str]) -> str:
return "\n".join(f"""<div class="tray-item">{item}</div>""" for item in content)
# Sync
######################################################################
@ -265,11 +286,11 @@ class Toolbar:
######################################################################
_body = """
<center id="outer">
<div id="header">
<div class="toolbar">%s<div>
<div class="header">
<div class="left-tray">{left_tray_content}</div>
<div class="toolbar">{toolbar_content}</div>
<div class="right-tray">{right_tray_content}</div>
</div>
</center>
"""

View file

@ -795,6 +795,36 @@ gui_hooks.webview_did_inject_style_into_page.append(mytest)
links.append(my_link)
""",
),
Hook(
name="top_toolbar_will_set_left_tray_content",
args=["content: list[str]", "top_toolbar: aqt.toolbar.Toolbar"],
doc="""Used to add custom add-on components to the *left* area of Anki's main
window toolbar
'content' is a list of HTML strings added by add-ons which you can append your
own components or elements to. To equip your components with logic and styling
please see `webview_will_set_content` and `webview_did_receive_js_message`.
Please note that Anki's main screen is due to undergo a significant refactor
in the future and, as a result, add-ons subscribing to this hook will likely
require changes to continue working.
""",
),
Hook(
name="top_toolbar_will_set_right_tray_content",
args=["content: list[str]", "top_toolbar: aqt.toolbar.Toolbar"],
doc="""Used to add custom add-on components to the *right* area of Anki's main
window toolbar
'content' is a list of HTML strings added by add-ons which you can append your
own components or elements to. To equip your components with logic and styling
please see `webview_will_set_content` and `webview_did_receive_js_message`.
Please note that Anki's main screen is due to undergo a significant refactor
in the future and, as a result, add-ons subscribing to this hook will likely
require changes to continue working.
""",
),
Hook(
name="top_toolbar_did_redraw",
args=["top_toolbar: aqt.toolbar.Toolbar"],