anki/qt/aqt/data/web/js/toolbar.ts
Aristotelis 91d563278f
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
2023-01-10 08:48:50 +10:00

90 lines
2.9 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/* eslint
@typescript-eslint/no-unused-vars: "off",
*/
enum SyncState {
NoChanges = 0,
Normal,
Full,
}
function updateSyncColor(state: SyncState) {
const elem = document.getElementById("sync");
switch (state) {
case SyncState.NoChanges:
elem.classList.remove("full-sync", "normal-sync");
break;
case SyncState.Normal:
elem.classList.add("normal-sync");
elem.classList.remove("full-sync");
break;
case SyncState.Full:
elem.classList.add("full-sync");
elem.classList.remove("normal-sync");
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);