How to include external files in your template (JS, CSS, etc.) | [Guide]

In another thread I announced that I’ll create a guide on how to include external JavaScript libraries in a note template. I will use Tippy.js as an example, since it covers both the JavaScript and the CSS part.

Update 01.08.21: The sample script has been updated to the format used by the Tippy Tooltips add-on.

Disclaimer: JavaScript is not officially supported by Anki. Therefore, if you run into trouble, there are no official documents to help you out. Things might break in the future and if you don’t understand the code you’re using, you’re at the mercy of other people to fix things. I’ll try to maintain this post for some time, but I can’t promise anything. If you encounter any issues, please let me know in the comments.

Tippy Tooltips sample

You can download a working sample note type here: https://ankiweb.net/shared/info/1782812971


How to include external libraries

Anki cards are like websites in many ways. A common way to use external libraries on the web is to link to their their content delivery network (e.g. https://unpkg.com):

But our cards should be usable without an internet connection too. That’s why we want to store our external libraries inside the collection.media folder.

🛈 collection.media is synced between all clients (Anki Desktop, AnkiMobile etc.), so we only need to do this once.

We should also strive for cross-platform compatibility, so we can share our deck knowing that other users can enjoy it as well. Keeping these considerations in mind, I will show you an approach that works on all Anki clients - offline.


Download the necessary files

Every good JavaScript library has setup instructions somewhere on its landing page. As we can see on the “Getting Started” page of Tippy, the following files are required:

Optional:

🛈 Always download the minified version (.min) - if available - to save space.

image

When presented with a raw text page like the ones from UNPKG, hit Ctrl+S to save the file to your computer.


Rename and move to media folder:

After we finished downloading the code, it’s time to move our files to collection.media.

Attention: Because the files aren’t referenced directly in our fields, Anki will count them as unused when we click on Check Media:

To prevent this, we must prepend the filenames with an underscore:

image


Import JavaScript files

I once explained on the AnkiDroid repo how to import external files into an Anki template.

Now we got a special challenge: Popper needs to be loaded before Tippy, because Tippy depends on Popper. The following script is an improved version of the one on GitHub. It allows us to import a hierarchical structure of dependent libraries.

Cross-platform script loader
var files = [{
    name: "Popper",
    url: "_popper.min.js",
    dependent: {
        name: "Tippy",
        url: "_tippy.umd.min.js",
        init: initTooltips
    }
}]

function getAnkiPrefix() {
    return globalThis.ankiPlatform === "desktop" ?
        "" :
        globalThis.AnkiDroidJS ?
        "https://appassets.androidplatform.net" :
        "."
}

if (!globalThis.ExternalScriptsLoaded) {
    var promises = []
    for (file of files) {
        promises.push(loadScript(file))
    }
    Promise.all(promises).then(() => {
        globalThis.ExternalScriptsLoaded = true
    })
}

async function loadScript(file) {
    var scriptPromise = import (`${getAnkiPrefix()}/${file.url}`)
    scriptPromise.then(() => {
            console.log(`Loaded ${file.name}.`)
            if (file.init) file.init()
            if (file.dependent) loadScript(file.dependent)
        },
        (error) => console.log(`An error occured while loading ${file.name}:`, error)
    )
    return scriptPromise
}

🛈 You can use this script as a template for your own JavaScript libraries. Simply exchange/extend the contents of files.

As you can see, the files can have a property called init. This can be any function we want to execute right after the library has been loaded:

...
    {
        name: "Tippy",
        url: "_tippy.umd.min.js",
        init: helloTippy // <- executed after Tippy has loaded
    }
...
function helloTippy() {console.log("Hello, Tippy!")}

image

🛈 The Tippy file is nested into the Popper file as dependent. Analogous to init for functions, the dependent file (Tippy) will start loading once the dependency (Popper) has finished loading.

var files = [{
    name: "Popper",                  // <- dependency for Tippy
    url: "_popper.min.js",
    dependent: { 
        name: "Tippy",               // <- this file will load
        url: "_tippy.umd.min.js",    // once the dependency (Popper) 
        init: initTooltips           // has finished loading
    }
}]

Use imported library

Now that we have told Anki how to import our library, we can use it in our own template code.

Updated (01.08.21): This works in conjunction with the Tippy Tooltips add-on.

Here is a simple script that converts elements with a data-tippy-content attribute to working tooltips (CSS provided in sample note type):

Tooltip script - Updated for Tippy Tooltips add-on
function initTooltips() {
    globalThis.tippyAvailable = true
    createTooltips()
}

if (globalThis.tippyAvailable) setTimeout(() => {createTooltips()}, 0)

function createTooltips() {
    tippy("[data-tippy-content]", {
        placement: "top",
        allowHTML: true,
        theme: "sample",
        interactive: true,
        trigger: "mouseenter click",
        animation: "scale-extreme",
        appendTo: document.body,
    })
}

🛈 Notice the function initTooltips(), which was called by our import script above? It sets the global variable tippyAvailable = true, allowing the execution of createTooltips() on further cards.

Input

Output

Kooha-2021-08-01-10_02_49


Import CSS files

Because of the non-dynamic nature of CSS, the process of importing external stylesheets is much simpler.

For example, we want to include the following two CSS files for our tooltips:

All we need to do is to put the following lines on top of the Styling section of our template:

@import url("_tippy.css");
@import url("_scale-extreme.css");
Screenshot

image


This was a pretty long post. Thanks for bearing with me - I hope you could get something out of it! I’d like to give special thanks to @hengiesel for his help in getting me where I am in terms of Anki-specific JavaScript. He’s been a major source of inspiration for a lot of my work.

17 Likes

First of all, thanks for sharing this awesome feature.

But when a strike a quite sizable text, the tooltip stays above the word, getting out of my card.

maIy4p85ra

Another thing, for example, let’s say i have a quite big text section striked and because of that editing the card gets messy, , is there a way a i can hide the striked text in the editor and unhide when needed?

This might be a limitation caused by the Anki webview itself. I can replicate it by using Anki in window-mode (not maximized):

Kooha-2021-07-28-16_25_21

The viewport information seems to be off when the window is not maximized. Tippy (actually Popper) thinks it has enough space for the tooltips.

@01101 a quick fix for this would be to change the default top positioning to bottom:

tippy(tip, {
            content: strike.innerHTML,
            placement: "bottom",    // <- changed to bottom (from top)
            allowHTML: true,
            theme: "sample",
            interactive: true,
            trigger: "mouseenter click",
            animation: "scale-extreme",
            appendTo: document.body,
        })

The following idea is certainly worth looking into:


An advantage of the <strike> approach is that we can easily batch-edit them via RegEx into another format. Should the abovementioned idea get implemented one day, I can help you with that switch.

1 Like

I was about to say that my window was already maximized and the tooltip still gets off. But change “top” to “bottom” solved my issue.

I took a look, but I don’t understand a lot of programming.

I searched on Google and find this HTML details Tag, but didn’t work the way wanted. :disappointed_relieved:
Anyway, thanks a lot for helping me out, i’m gonna keep searching on Google and see if i can find a solution.

@Casartelli @gbrl.sc I’m in the process of creating an add-on for this:

Kooha-2021-07-29-12_23_37

It’s basically a stripped down version of Add Hyperlink with CSS injection for the editor fields.
@ijgnd I hope that’s alright. I will create a fork of your repo before I publish anything on AnkiWeb.
(& sorry for the misspelling in the GIF :sweat_smile:)

That would be awesome!

By the way, i’ve been using your tooltip template, but in cloze seems not to work properly when i don’t have < u > before the strike.

MTnKXVR3LV

1 Like

Thanks for testing!

To be honest, I didn’t really think this through properly.
The approach I’m taking with the add-on will be more stable and user friendly, but it will take some time.
Perhaps it’s best to hold off on that functionality for a while, until I have refined the process.


I think I’ll write that add-on from scratch, rather than modifying Add Hyperlink, because I want it to be usable in-editor without a dialog:

Thanks for your hard work on @gbrl.sc idea. I’m sure there are many people who will make great use of the add on.

Great guide and addon.

For people using the code from this site (and not the one included in the addon sample note type) - does your tooltip arrow also sometimes look strange?

Your link for sample note type doesn’t work anymore

1 Like

@kleinerpirat , thanks a lot for this guide! Could you re-upload the sample , the link does not work anymore. Thanks!

I followed your step by step, I have that tip bubble in the review interface now when the mouse across. But I note you have that chat logo on the side

image

I don’t have and any logo to marking. But I have put the @import in the Styling box as your post:
image

My Anki is in version 2.1.49. This is my Anki-desktop:
image

And my ankidroid is in verion 2.15.6 :
image

How to make that chat logo as you on the side?

The :left_speech_bubble: emoji is used only when creating a tooltip without any selected text. If you selected text, it will turn that selected text into a link that’s indicated with an underline.

Oh, so that’s it, there seems to be no problem. :slight_smile:. So what are the two .css files you used for?I wonder if I can select those <a> tags that have the data-tippy-content attribute and define a custom style

One is the base Tippy styling and the other one is an animation that’s used when the tooltips are shown.

You can customize the tooltips with CSS like any other HTML element. Here’s the official documentation: Themes | Tippy.js


For the <a> tags, you could use a[data-tippy-content].

Thank you for such an amazing little add-on, which kept me awake last night with excitement. But I have some questions to report:

  1. The image is show normally in Anki-desktop, but is abnormal in Ankidroid
  • Anki-desktop:

image

  • Ankidroid:

image

And I note we should use data-tippy-content="img sample:<img src='imagepath.jpg'>" instead of your data-tippy-content="img sample:<img src=&quot;imagepath.jpg&quot;>". Then both will show noramlly. Hope fix this bug.

  1. Every time I paste text in the tooltips, The text is automatically pasted at the frontmost of the line. But my input cursor is not at the top of the line

image

  1. As your description in 07.12.21, the add-on support HTML, but it seem fail to render in my here:
    image

I guess this is also a problem due to symbolic escaping

  1. This tooltip seems to cause the mouse to fail to select the text, for example in the screenshot below, I just want to select “bc cc” with the mouse.
    image

  2. The latex formula cannot be displayed properly, but this is normal in the card. Maybe this request is a bit too much, because even if your Tippy can’t show the formula it’s already excellent
    image

a[data-tippy-content] will select the <a> of both kinds of tooltips. I wonder if I can individually select the <a> of the second type tooltip to define custom css style?

I would like to use this javascript 3D viewer but so far I have failed to get it to work.

Based on your post and the documentation of the viewer I should have to :

  1. Load the libraries using your script
  2. Call the initialization function OV.Init3DViewerElements ()
  3. Add an element containing the viewer

That's what I did and for the moment every time I try to call the initialization code I get an error in the console telling me the init function is not defined, even though the libraries were successfully loaded.
Console error

image

Front template
{{Front}}

<script data-name="3dViewer Init" data-version="v1.0.1">
    function initO3dv() {
        OV.Init3DViewerElements ()
    }    
</script>

<script data-name="Script Loader" data-version="v0.1">
    var files = [{
        name: "Three",
        url: "_three.min.js",
        dependent: {
            name: "OV",
            url: "_o3dv.min.js",
            init: initO3dv
        }
    }]

    function getAnkiPrefix() {
        return globalThis.ankiPlatform === "desktop" ?
            "" :
            globalThis.AnkiDroidJS ?
            "https://appassets.androidplatform.net" :
            "."
    }

    if (!globalThis.ExternalScriptsLoaded) {
        var promises = []
        for (file of files) {
            promises.push(loadScript(file))
        }
        Promise.all(promises).then(() => {
            globalThis.ExternalScriptsLoaded = true
        })
    }

    async function loadScript(file) {
        var scriptPromise = import (`${getAnkiPrefix()}/${file.url}`)
        scriptPromise.then(() => {
                console.log(`Loaded ${file.name}.`)
                if (file.init) file.init()
                if (file.dependent) loadScript(file.dependent)
            },
            (error) => console.log(`An error occured while loading ${file.name}:`, error)
        )
        return scriptPromise
    }
</script>


I have never used javascript before and I have no idea how to make it work. If anyone has any idea on how to get this thing to work it would be really awesome.

These directions don’t seem to work with the latest version of Anki. I have v2.1.54 installed on Windows. I only implemented the separate CSS file. The card templates work fine with the external CSS file on the desktop but it refuses to upload _anki_base.css to Ankiweb. I’ve tried enabling media sync, enabling full sync, cleaning the media, using check media to delete extra files, and modifying the media directory.

In version 2.1.45,I often cannot find that clickable text, then the edit box does not pop up:
image