mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 01:06:35 -04:00
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:
parent
cef672a6a1
commit
91d563278f
4 changed files with 142 additions and 6 deletions
|
@ -6,8 +6,33 @@
|
||||||
@use "sass/elevation" as *;
|
@use "sass/elevation" as *;
|
||||||
@use "sass/button-mixins" as button;
|
@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 {
|
.toolbar {
|
||||||
display: inline-block;
|
height: 31px;
|
||||||
|
justify-self: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-bottom-left-radius: prop(border-radius-large);
|
border-bottom-left-radius: prop(border-radius-large);
|
||||||
|
|
|
@ -27,3 +27,63 @@ function updateSyncColor(state: SyncState) {
|
||||||
break;
|
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);
|
||||||
|
|
|
@ -114,8 +114,13 @@ class Toolbar:
|
||||||
web_context = web_context or TopToolbar(self)
|
web_context = web_context or TopToolbar(self)
|
||||||
link_handler = link_handler or self._linkHandler
|
link_handler = link_handler or self._linkHandler
|
||||||
self.web.set_bridge_command(link_handler, web_context)
|
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.web.stdHtml(
|
||||||
self._body % self._centerLinks(),
|
body,
|
||||||
css=["css/toolbar.css"],
|
css=["css/toolbar.css"],
|
||||||
js=["js/vendor/jquery.min.js", "js/toolbar.js"],
|
js=["js/vendor/jquery.min.js", "js/toolbar.js"],
|
||||||
context=web_context,
|
context=web_context,
|
||||||
|
@ -204,6 +209,22 @@ class Toolbar:
|
||||||
|
|
||||||
return "\n".join(links)
|
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
|
# Sync
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
@ -265,11 +286,11 @@ class Toolbar:
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
_body = """
|
_body = """
|
||||||
<center id="outer">
|
<div class="header">
|
||||||
<div id="header">
|
<div class="left-tray">{left_tray_content}</div>
|
||||||
<div class="toolbar">%s<div>
|
<div class="toolbar">{toolbar_content}</div>
|
||||||
|
<div class="right-tray">{right_tray_content}</div>
|
||||||
</div>
|
</div>
|
||||||
</center>
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -795,6 +795,36 @@ gui_hooks.webview_did_inject_style_into_page.append(mytest)
|
||||||
links.append(my_link)
|
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(
|
Hook(
|
||||||
name="top_toolbar_did_redraw",
|
name="top_toolbar_did_redraw",
|
||||||
args=["top_toolbar: aqt.toolbar.Toolbar"],
|
args=["top_toolbar: aqt.toolbar.Toolbar"],
|
||||||
|
|
Loading…
Reference in a new issue