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.

import pathlib
import aqt

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

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

def generate_js() -> str:
    js = ""
    urls = []
    for file_path in sorted(assets_dir.glob("*.*")):
        if file_path.name.endswith((".css", ".js")):
    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:

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)")

except AttributeError:
    # for Anki 2.1.35 or earlier

(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');

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.


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.