CustomJS for Obsidian

repository·master·Indexed 18 days ago

https://github.com/saml-dev/obsidian-custom-js

An Obsidian plugin (v1.0.21) that enables the use of custom JavaScript classes across a vault, specifically within DataviewJS blocks and Templater templates. It provides a global `window.customJS` object and an `await cJS()` function to access loaded classes. Features include support for singleton instances, Invocable Scripts that can be bound to hotkeys or commands, and deconstructor methods for cleanup during script reloads.

Tokens
2.3K
Snippets
5
Records
12
Agent score
14%

What's inside CustomJS

  1. Understand the window.customJS global object

    master

    The window.customJS object is available globally and holds instances of your loaded classes. It also contains special properties:

    • customJS.state: An object used to persist data. Since window.customJS is overwritten whenever JS files are modified, you should store any data you want to keep in customJS.state.
    • customJS.obsidian: Access to the Obsidian API.
    • customJS.app: The Obsidian App instance.

    Class Properties: For every class MyModule you define, CustomJS adds:

    • customJS.MyModule: The singleton instance of your class.
    • customJS.createMyModuleInstance: A function to create a new instance (cannot pass arguments to the constructor).
  2. How to write valid CustomJS classes

    master

    CustomJS only works with JavaScript classes. Each file must contain exactly one class and no other top-level code.

    Rules for valid files:

    • Must contain a single class Name { ... } definition.
    • Do NOT use module.exports.
    • Do NOT use bare functions (e.g., function myFunc() {}).
    • Do NOT use variable exports (e.g., export const x = 1).
    • Do NOT define any code (imports, constants, etc.) outside of the class definition.

    Note on Instances: By default, CustomJS initializes every class as a singleton. To create a new, isolated instance, use the naming convention create${ClassName}Instance (e.g., createMyClassInstance()).

    // Valid: scripts/MyClass.js
    class MyClass {
        doSomething() {
            return "Hello";
        }
    }
  3. Use deconstructor for cleanup on reload

    master

    Because window.customJS is overwritten every time a JS file is modified in your vault, you can define a deconstructor() method in your class to perform cleanup work (like removing event listeners) before the reload occurs.

    Example: Implementing a Context Menu entry safely

    class AddCustomMenuEntry {
      constructor() {
        this.eventHandler = this.eventHandler.bind(this);
      }
    
      async invoke() {
        // Register the event when the script is run/loaded
        this.app.workspace.on('file-menu', this.eventHandler);
      }
    
      deconstructor() {
        // Remove the event listener when the file is reloaded to prevent duplicates
        this.app.workspace.off('file-menu', this.eventHandler);
      }
    
      eventHandler(menu, file) {
        menu.addItem((item) => {
          item.setTitle('Custom menu entry').onClick(() => { /* logic */ });
        });
      }
    }
  4. How CustomJS classes and invocable scripts work

    master

    CustomJS works by evaluating .js files and attaching the resulting classes to a global window.customJS object.

    Class Structure

    When you define a class in a .js file, CustomJS automatically provides:

    1. The Class itself: Attached to window.customJS[ClassName].
    2. A Factory Function: A function named create[ClassName]Instance() is added to window.customJS to allow easy instantiation.
    3. Deconstructor Support: If your class defines a deconstructor() method, CustomJS will call it when the scripts are reloaded or the plugin is unloaded to allow for proper cleanup.

    Invocable Scripts

    A script is considered "invocable" if the class assigned to its name has an async invoke() method. Invocable scripts can be:

    • Startup Scripts: Automatically executed when the plugin loads or scripts are reloaded.
    • Registered Invocable Scripts: Scripts that are registered in the plugin settings and bound to an Obsidian Command, allowing you to trigger them via the Command Palette or a Hotkey.
    // Example of a CustomJS class file (e.g., MyScript.js)
    class MyScript {
      async invoke() {
        console.log("Script invoked!");
      }
    
      deconstructor() {
        console.log("Cleaning up...");
      }
    }
    
    MyScript;
  5. Configure CustomJS loading settings

    master

    CustomJS allows you to specify which scripts to load via the plugin settings. Use forward slashes (/) for all paths to ensure cross-platform compatibility.

    • Individual files: A comma-separated list of specific file paths.
    • Folder: A path to a folder. CustomJS will load all *.js files in that folder recursively. Files are loaded in alphabetical order by filename to support dependencies.
    • Registered invocable scripts: Bind an Invocable Script to a hotkey.
    • Startup scripts: Execute Invocable Scripts when the plugin loads (e.g., for initialization).
  6. Register and use Invocable Scripts

    master

    To turn a class into a callable command in Obsidian:

    1. Ensure your class has an async invoke() method.
    2. Open the CustomJS settings.
    3. Click "Register invocable script" and select your class from the list.
    4. Once registered, the script appears in the Obsidian Command Palette. You can also assign a Hotkey to it via the standard Obsidian Hotkeys settings (search for CustomJS: [YourScriptName]).
  7. Create Invocable Scripts

    master

    An Invocable Script is a class that defines an async invoke() method. These scripts can be triggered via the CustomJS: Invoke Script command, or registered in settings to be bound to specific hotkeys and commands (e.g., CustomJS: MyScriptName).

    Example structure:

    class MyScript {
        async invoke() {
            // Your logic here
        }
    }
  8. Access custom classes using cJS()

    master

    Because CustomJS loads asynchronously during Obsidian's startup, you should use the global await cJS() function to ensure your classes are fully loaded before use. This prevents customJS is not defined errors in templates or automated notes.

    Usage Patterns:

    1. Get the full global object: await cJS()
    2. Get a specific module: await cJS('ModuleName')
    3. Run code via callback: await cJS(callback)

    Examples:

    In a DataviewJS block:

    const {MyClass} = await cJS();
    const result = MyClass.doSomething();

    In a Templater template:

    <%* 
    const {MyClass} = await cJS();
    tR += MyClass.doSomething();
    %>```
    
    **Using a callback (cleaner for one-liners):**
    ```javascript
    await cJS(async (customJS) => {
        await customJS.MyClass.doSomethingAsync();
    });
    // Accessing a single module directly
    const MyClass = await cJS('MyClass');
    MyClass.doSomething();
  9. Configure CustomJS file loading

    master

    You can control which JavaScript files are loaded by the plugin through the CustomJS settings tab. There are two primary ways to specify files:

    1. Individual files: Provide a comma-separated list of absolute or relative paths to specific .js files (e.g., scripts/util.js, scripts/math.js).
    2. Folder: Provide a folder path. The plugin will automatically load all .js files found within that folder.

    If you enable "Re-execute the start scripts when reloading", any scripts listed in your Startup scripts configuration will run again whenever a .js file in your configured paths is modified.

  10. Access CustomJS via the `cJS` global function

    master

    The cJS function is the primary way to interact with loaded CustomJS classes from within Obsidian (e.g., from DataviewJS or other scripts). It ensures that the CustomJS environment is initialized before returning the requested module or the full customJS object.

    Usage Patterns:

    • Get the full customJS object: Call cJS() without arguments.
    • Get a specific class/module: Call cJS('ClassName') to return the class directly.
    • Execute a callback with the customJS object: Pass a function to cJS((customJS) => { ... }) to perform operations once the environment is ready.
    // Get the entire customJS object
    const customJS = await cJS();
    
    // Get a specific class by its name
    const MyClass = await cJS('MyClass');
    
    // Use a callback to ensure readiness
    await cJS((customJS) => {
      const instance = new customJS.MyClass();
      // ...
    });
  11. Reference: CustomJS Global Object Structure

    master

    The window.customJS object is the central registry for all loaded scripts. It contains:

    • obsidian: The full Obsidian API object.
    • app: The Obsidian App instance.
    • state: An object containing internal state, including state._ready (boolean indicating if loading is complete).
    • [ClassName]: The class constructor itself.
    • create[ClassName]Instance: A function that returns a new instance of [ClassName].