Creating Custom Widgets

This guide explains how to create and register custom widgets for the Campaign Codex module. This allows other modules to add new functionality and interactive elements that can be embedded within Campaign Codex journal entries.

Prerequisites

Your module must list campaign-codex as a requirement in your module.json.

JSON

 "relationships": {
    "requires": [
      {
        "id": "campaign-codex",
        "type": "module",
        "compatibility": {}
      }
    ]
  },

Create Your Widget Class

To avoid loading-order issues, use a “factory function” to create your class. This function will receive the CampaignCodexWidget base class as an argument, which you will then extend.

Your widget class must implement the async render() method.

myWidget.js

JavaScript

/**
 * Factory function that creates and returns the myWidget class.
 * @param {typeof CampaignCodexWidget} CampaignCodexWidget - The base class to extend.
 * @returns {typeof myWidget} The new widget class.
 */
export function createMyWidget(CampaignCodexWidget) {

    class myWidget extends CampaignCodexWidget {
        
        /**
         * (Required) Returns the HTML structure for the widget.
         * @returns {Promise<string>} An HTML string.
         */
        async render() {
            // 'this.widgetId' is a unique ID for this instance
            // 'this.document' is the Journal Entry this widget lives on
            return `
                <div class="cc-widget mywidget" data-widget-id="${this.widgetId}">
                    <p>Hello from my custom widget!</p>
                    <button type="button">Click Me</button>
                </div>
            `;
        }

        /**
         * (Optional) Activates event listeners for the widget's HTML.
         * @param {HTMLElement} htmlElement - The widget's container element (the div returned by render).
         */
        async activateListeners(htmlElement) {
            super.activateListeners(htmlElement);

            // Example: Add a click listener using the inherited confirmationDialog
            htmlElement.querySelector("button").addEventListener("click", async () => {
                const proceed = await this.confirmationDialog("Are you sure?");
                if (proceed) {
                    ui.notifications.info("You clicked yes!");
                } else {
                    ui.notifications.warn("You clicked no!");
                }
            });
        }
    }

    // The factory function returns the new class
    return myWidget;
}

Register Your Widget

In your module’s main JavaScript file, use the Hooks.once("ready") event to get the Campaign Codex API and register your new widget.

main.js

JavaScript

// 1. Import your factory function
import { createMyWidget } from "./widgets/myWidget.js";

Hooks.once("ready", async function () {
    // 2. Get the Campaign Codex API
    const ccApi = game.modules.get('campaign-codex')?.api;

    // 3. Check if the API exists
    if (!ccApi) {
        return;
    }

    // 4. Destructure the API to get the base class and manager
    const { CampaignCodexWidget, widgetManager } = ccApi;

    // 5. Create your widget class by calling the factory function
    const myWidget = createMyWidget(CampaignCodexWidget);

    // 6. Register your widget
    // The first argument is the "widgetType" string users will type.
    widgetManager.registerWidget("mywidget", myWidget);
});

How End-Users Use Your Widget

Once registered, a user can add your widget to any Campaign Codex entry by using the widgetType string you defined in registerWidget:

[!cc-widget mywidget]


Inherited Methods & Properties

When you extend CampaignCodexWidget, your class inherits several useful helpers:

Properties

  • this.document: The Journal Entry document this widget is embedded in.
  • this.widgetId: A unique ID for this specific instance of the widget.
  • this.isGM: A boolean check for whether the current user is a GM.

Methods

this.confirmationDialog(message) Shows a “Yes/No” confirmation dialogue.

  • @param {string} [message="Are you sure?"] – The confirmation message.
  • @returns {Promise<boolean>} true if “Yes” was clicked, false otherwise.
  • Example: const proceed = await this.confirmationDialog("Delete this item?"); if (proceed) { // ...delete the item }

this._onOpenDocument(uuid, type) Opens a document’s sheet from its UUID.

  • @param {string} uuid – The document UUID.
  • @param {string} [type="document"] – A label for error messages (e.g., “Actor”, “Item”).
  • Example: const actorUuid = "Actor.xyz"; this._onOpenDocument(actorUuid, "Actor");

this.getData() Retrieves data saved by this specific widget instance.

  • @returns {Promise<object | null>} The widget’s data, or null.

this.saveData(data) Saves data for this specific widget instance and triggers a full sheet re-render.

  • @param {object} data – The data object to save.
  • @returns {Promise<Document | undefined>}

this.saveDataTemporal(data) Stores data on the client-side document object (using updateSource) without triggering a sheet re-render or an immediate database save. This is useful for storing frequently used, temporary information, such as a map’s position or a widget’s viewstate.

Important: This data is not “permanent” until a full document save is triggered (e.g., by this.saveData() or another sheet action). It may be lost on a refresh if no permanent save occurs.

  • @param {object} data – The data object to save locally.
  • @returns {Promise<void>}