JS injection into new Stats page

Seems like the ability to inject external JS files is not available for the new stats page (i.e. webview_will_set_content hook is not called). Looking thru webview.js, I see that the stats page is being loaded via an HTML page, so I guess that hook isn’t really useful anymore. Wondering if JS injection will be a thing moving forward. Could parse the html file beforehand, add the necessary JS/CSS files into the DOM tree, and then construct a QUrl (not sure if this part is possible) to load from.

See gui_hooks.webview_did_inject_style_into_page

There may be a simpler way, but I use javascript to inject external js/css files into the new stats window. The following is the code that is extracted from my personal add-on.

/addons21/inject_assets_to_new_stats/__init__.py
import pathlib
import aqt

root = pathlib.Path(__file__).parent.resolve()
assets_dir = root / "assets"

INJECT_ASSETS = """
const injectAssets = (assets) => {
    assets.forEach((url) => {
        if (url.endsWith('.js')) {
            const script = document.createElement('script');
            script.src = url;
            script.async = true;
            document.head.appendChild(script);
        } else if (url.endsWith('.css')) {
            const link = document.createElement('link');
            link.href = url;
            link.rel = 'stylesheet';
            document.head.appendChild(link);
        }
    });
};
"""


def generate_js() -> str:
    js = ""
    urls = []
    for file_path in sorted(assets_dir.glob("*.*")):
        if file_path.name.endswith((".css", ".js")):
            urls.append(f"/_addons/{root.name}/assets/{file_path.name}")
    if urls:
        urls_string = ",".join(f'"{url}"' for url in urls)
        js = f"{INJECT_ASSETS}injectAssets([{urls_string}]);"
    return js


def on_webview_did_inject_style_into_page(webview: aqt.webview.AnkiWebView):
    if isinstance(webview.parent(), aqt.stats.NewDeckStats):
        js = generate_js()
        if js:
            webview.eval(js)


def on_stats_dialog_will_show(dialog: aqt.stats.NewDeckStats):
    js = generate_js()
    if js:
        dialog.form.web.loadFinished.connect(lambda *_: dialog.form.web.eval(js))


aqt.mw.addonManager.setWebExports(__name__, r".+\.(css|js)")

try:
    aqt.gui_hooks.webview_did_inject_style_into_page.append(
        on_webview_did_inject_style_into_page
    )
except AttributeError:
    # for Anki 2.1.35 or earlier
    aqt.gui_hooks.stats_dialog_will_show.append(on_stats_dialog_will_show)

/addons21/inject_assets_to_new_stats/assets/move_calendar_to_top.js
(async () => {
    // wait for the DOM to be ready
    while (!document.querySelector('#graph-calendar')) {
        await new Promise((resolve) => setTimeout(resolve, 100));
    }
    const rangeBoxPad = document.querySelector('.range-box-pad');
    const calender = document.querySelector('#graph-calendar');
    rangeBoxPad.after(calender);
})();

Note that if you use this method, DOMContentLoaded event may not be available because external files are dynamically loaded after the page is loaded, unlike loading files using webview_will_set_content.

screenshot:

1 Like

Thanks! The code snippets really helped.

It might be worth updating the documentation to explain this new method of JS injection, esp if it’ll be propagated across the board.