diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc46997..eb59846 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Restored compatibility with PopOut! by cloning sidebar base options before overriding defaults so Combat Tracker and other stock tabs keep their pop-out buttons when realtime sync is enabled.
+## [1.3.10] - 2025-01-15
+
+### Fixed
+- Custom elements (sync button, create buttons, eye toggle buttons) in the Journals tab now mount immediately after world setup completes, without requiring a manual refresh. Fixed V13 ApplicationV2 element access and added automatic re-render triggers when world initialization completes.
+- Improved robustness of Journal Directory hooks with retry mechanisms to handle asynchronous DOM rendering.
+
### Changed
- Recap custom sheet now displays the session date as MM/DD/YYYY.
- After importing Recaps via the Sync dialog, Recaps in the Recaps folder are normalized to sort by the `sessionDate` flag ascending (oldest → newest); undated Recaps are placed at the end.
diff --git a/module.json b/module.json
index 5dc78c7..5943425 100644
--- a/module.json
+++ b/module.json
@@ -8,7 +8,7 @@
"email": "cameron.b.llewellyn@gmail.com"
}
],
- "version": "1.3.9",
+ "version": "1.3.10",
"compatibility": {
"minimum": "13.341",
"verified": "13.346"
diff --git a/package.json b/package.json
index 11d1686..6d15706 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "archivist-sync",
- "version": "1.3.9",
+ "version": "1.3.10",
"description": "A simple Foundry VTT module for fetching world data from an API endpoint using an API key.",
"type": "module",
"scripts": {
diff --git a/scripts/archivist-sync.js b/scripts/archivist-sync.js
index 3dc21a1..e801cae 100644
--- a/scripts/archivist-sync.js
+++ b/scripts/archivist-sync.js
@@ -249,6 +249,19 @@ Hooks.once('ready', async function () {
// Conditionally set up Archivist chat based on availability
updateArchivistChatAvailability();
+ // If world is already initialized, ensure Journal Directory renders with custom elements
+ // This handles cases where the directory was rendered before initialization
+ if (settingsManager.isWorldInitialized?.() && game.user?.isGM) {
+ setTimeout(async () => {
+ try {
+ await ui?.journal?.render?.({ force: true });
+ console.log('[Archivist Sync] Journal Directory re-rendered on ready (world already initialized)');
+ } catch (e) {
+ console.warn('[Archivist Sync] Failed to re-render Journal Directory on ready', e);
+ }
+ }, 500); // Delay to ensure everything is fully loaded
+ }
+
// Delegated renderer: when the archivist tab button is clicked, render chat into panel
try {
const sidebar = document.getElementById('sidebar');
@@ -438,28 +451,35 @@ Hooks.once('ready', async function () {
if (!isWorldInitialized) return;
if (!game.user?.isGM) return;
- const root = html instanceof jQuery ? html[0] : html?.element || html;
+ // V13 ApplicationV2: app.element is the root, fallback to html for compatibility
+ const root = app?.element || (html instanceof jQuery ? html[0] : html?.element || html);
if (!root) return;
- const header =
- root.querySelector('header.directory-header') ||
- root.querySelector('header.header') ||
- root.querySelector('header') ||
- root.querySelector('.directory-header') ||
- root.querySelector('.header');
- if (!header) return;
- if (header.querySelector?.('.archivist-sync-btn')) return;
- const btn = document.createElement('button');
- btn.type = 'button';
- btn.className = 'archivist-sync-btn';
- btn.textContent = 'Sync with Archivist';
- btn.addEventListener('click', (ev) => {
- ev.preventDefault();
- try {
- new SyncDialog().render(true);
- } catch (_) {}
+
+ // Use requestAnimationFrame to ensure DOM is ready
+ requestAnimationFrame(() => {
+ const header =
+ root.querySelector('header.directory-header') ||
+ root.querySelector('header.header') ||
+ root.querySelector('header') ||
+ root.querySelector('.directory-header') ||
+ root.querySelector('.header');
+ if (!header) return;
+ if (header.querySelector?.('.archivist-sync-btn')) return;
+ const btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'archivist-sync-btn';
+ btn.textContent = 'Sync with Archivist';
+ btn.addEventListener('click', (ev) => {
+ ev.preventDefault();
+ try {
+ new SyncDialog().render(true);
+ } catch (_) {}
+ });
+ header.appendChild(btn);
});
- header.appendChild(btn);
- } catch (_) {}
+ } catch (e) {
+ console.warn('[Archivist Sync] Failed to inject sync button', e);
+ }
});
// Inject quick-create buttons for Archivist sheets in the Journal Directory header
@@ -469,119 +489,125 @@ Hooks.once('ready', async function () {
const isWorldInitialized = settingsManager.isWorldInitialized?.();
if (!isWorldInitialized) return;
if (!game.user?.isGM) return;
- const root = html instanceof jQuery ? html[0] : html?.element || html;
+
+ // V13 ApplicationV2: app.element is the root, fallback to html for compatibility
+ const root = app?.element || (html instanceof jQuery ? html[0] : html?.element || html);
if (!root) return;
- const header =
- root.querySelector('header.directory-header') ||
- root.querySelector('header.header') ||
- root.querySelector('header') ||
- root.querySelector('.directory-header') ||
- root.querySelector('.header');
- if (!header) return;
- if (header.querySelector('.archivist-create-buttons')) return;
-
- const wrap = document.createElement('div');
- wrap.className = 'archivist-create-buttons';
- wrap.style.display = 'flex';
- wrap.style.flexWrap = 'wrap';
- wrap.style.gap = '6px';
- wrap.style.marginTop = '6px';
-
- const types = [
- { key: 'pc', label: 'PC', icon: 'fa-user', tooltip: 'Create New PC' },
- {
- key: 'npc',
- label: 'NPC',
- icon: 'fa-user-ninja',
- tooltip: 'Create New NPC',
- },
- {
- key: 'item',
- label: 'Item',
- icon: 'fa-gem',
- tooltip: 'Create New Item',
- },
- {
- key: 'location',
- label: 'Location',
- icon: 'fa-location-dot',
- tooltip: 'Create New Location',
- },
- {
- key: 'faction',
- label: 'Faction',
- icon: 'fa-people-group',
- tooltip: 'Create New Faction',
- },
- ];
-
- const promptForName = async (title) => {
- try {
- const name = await foundry.applications.api.DialogV2.prompt({
- window: { title },
- content: `
-
-
-
-
- `,
- ok: {
- icon: '',
- label: 'Create',
- callback: (event, button) => {
- const enteredName = button.form.elements.name.value.trim();
- return enteredName || null;
- },
- },
- cancel: { icon: '', label: 'Cancel' },
- rejectClose: true,
- });
- return name;
- } catch (_) {
- return null;
- }
- };
+
+ // Use requestAnimationFrame to ensure DOM is ready
+ requestAnimationFrame(() => {
+ const header =
+ root.querySelector('header.directory-header') ||
+ root.querySelector('header.header') ||
+ root.querySelector('header') ||
+ root.querySelector('.directory-header') ||
+ root.querySelector('.header');
+ if (!header) return;
+ if (header.querySelector('.archivist-create-buttons')) return;
+
+ const wrap = document.createElement('div');
+ wrap.className = 'archivist-create-buttons';
+ wrap.style.display = 'flex';
+ wrap.style.flexWrap = 'wrap';
+ wrap.style.gap = '6px';
+ wrap.style.marginTop = '6px';
+
+ const types = [
+ { key: 'pc', label: 'PC', icon: 'fa-user', tooltip: 'Create New PC' },
+ {
+ key: 'npc',
+ label: 'NPC',
+ icon: 'fa-user-ninja',
+ tooltip: 'Create New NPC',
+ },
+ {
+ key: 'item',
+ label: 'Item',
+ icon: 'fa-gem',
+ tooltip: 'Create New Item',
+ },
+ {
+ key: 'location',
+ label: 'Location',
+ icon: 'fa-location-dot',
+ tooltip: 'Create New Location',
+ },
+ {
+ key: 'faction',
+ label: 'Faction',
+ icon: 'fa-people-group',
+ tooltip: 'Create New Faction',
+ },
+ ];
- const makeBtn = (t) => {
- const b = document.createElement('button');
- b.type = 'button';
- b.className = 'archivist-create-btn';
- b.innerHTML = ``;
- b.title = t.tooltip;
- b.dataset.type = t.key;
- b.addEventListener('click', async (ev) => {
- ev.preventDefault();
+ const promptForName = async (title) => {
try {
- const worldId = settingsManager.getSelectedWorldId?.();
- const name = await promptForName(`Create ${t.label}`);
- if (!name) return;
-
- let journal = null;
- if (t.key === 'pc')
- journal = await Utils.createPcJournal({ name, worldId });
- else if (t.key === 'npc')
- journal = await Utils.createNpcJournal({ name, worldId });
- else if (t.key === 'item')
- journal = await Utils.createItemJournal({ name, worldId });
- else if (t.key === 'location')
- journal = await Utils.createLocationJournal({ name, worldId });
- else if (t.key === 'faction')
- journal = await Utils.createFactionJournal({ name, worldId });
-
- // Open the newly created sheet and bring it to front
- if (journal) {
- journal.sheet?.render?.(true);
- setTimeout(() => journal.sheet?.bringToFront?.(), 50);
- }
- } catch (e) {
- console.warn('[Archivist Sync] create button failed', e);
+ const name = await foundry.applications.api.DialogV2.prompt({
+ window: { title },
+ content: `
+
+
+
+
+ `,
+ ok: {
+ icon: '',
+ label: 'Create',
+ callback: (event, button) => {
+ const enteredName = button.form.elements.name.value.trim();
+ return enteredName || null;
+ },
+ },
+ cancel: { icon: '', label: 'Cancel' },
+ rejectClose: true,
+ });
+ return name;
+ } catch (_) {
+ return null;
}
- });
- return b;
- };
+ };
+
+ const makeBtn = (t) => {
+ const b = document.createElement('button');
+ b.type = 'button';
+ b.className = 'archivist-create-btn';
+ b.innerHTML = ``;
+ b.title = t.tooltip;
+ b.dataset.type = t.key;
+ b.addEventListener('click', async (ev) => {
+ ev.preventDefault();
+ try {
+ const worldId = settingsManager.getSelectedWorldId?.();
+ const name = await promptForName(`Create ${t.label}`);
+ if (!name) return;
+
+ let journal = null;
+ if (t.key === 'pc')
+ journal = await Utils.createPcJournal({ name, worldId });
+ else if (t.key === 'npc')
+ journal = await Utils.createNpcJournal({ name, worldId });
+ else if (t.key === 'item')
+ journal = await Utils.createItemJournal({ name, worldId });
+ else if (t.key === 'location')
+ journal = await Utils.createLocationJournal({ name, worldId });
+ else if (t.key === 'faction')
+ journal = await Utils.createFactionJournal({ name, worldId });
+
+ // Open the newly created sheet and bring it to front
+ if (journal) {
+ journal.sheet?.render?.(true);
+ setTimeout(() => journal.sheet?.bringToFront?.(), 50);
+ }
+ } catch (e) {
+ console.warn('[Archivist Sync] create button failed', e);
+ }
+ });
+ return b;
+ };
- for (const t of types) wrap.appendChild(makeBtn(t));
- header.appendChild(wrap);
+ for (const t of types) wrap.appendChild(makeBtn(t));
+ header.appendChild(wrap);
+ });
} catch (e) {
console.warn('[Archivist Sync] Failed to inject create buttons', e);
}
@@ -1437,57 +1463,82 @@ Hooks.on('getJournalDirectoryHeaderButtons', (app, buttons) => {
Hooks.on('renderJournalDirectory', (app, html) => {
try {
if (!game.user?.isGM) return;
- const root = html instanceof jQuery ? html[0] : html?.element || html;
+ if (!settingsManager.isWorldInitialized?.()) return;
+
+ // V13 ApplicationV2: app.element is the root, fallback to html for compatibility
+ const root = app?.element || (html instanceof jQuery ? html[0] : html?.element || html);
if (!root) return;
- const list =
- root.querySelector('ol.directory-list') ||
- root.querySelector('.directory-list') ||
- root;
- const items = list.querySelectorAll(
- 'li[data-document-id], li.directory-item, li.document, li.journal-entry'
- );
- const OBS = CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER;
- const NON = CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE;
- items.forEach((li) => {
- try {
- const id =
- li.getAttribute('data-document-id') ||
- li.getAttribute('data-entry-id');
- if (!id) return;
- if (li.querySelector('.archivist-eye')) return;
- const j = game.journal?.get?.(id);
- if (!j) return;
- // Only render for Archivist custom sheets (identified by our flags)
- let isCustom = false;
- try {
- const f = j.getFlag(CONFIG.MODULE_ID, 'archivist') || {};
- isCustom = !!(f.archivistId || f.sheetType);
- } catch (_) {
- isCustom = false;
- }
- if (!isCustom) return;
- const cur = Number(j?.ownership?.default ?? NON);
- const btn = document.createElement('button');
- btn.type = 'button';
- btn.className = 'archivist-eye';
- const icon = document.createElement('i');
- icon.className = cur >= OBS ? 'fas fa-eye' : 'fas fa-eye-slash';
- btn.title = cur >= OBS ? 'Hide from Players' : 'Show to Players';
- btn.appendChild(icon);
- btn.addEventListener('click', async (ev) => {
- ev.preventDefault();
- ev.stopPropagation();
- try {
- const now = Number(j?.ownership?.default ?? NON);
- const next = now >= OBS ? NON : OBS;
- await j.update({ ownership: { default: next } });
- icon.className = next >= OBS ? 'fas fa-eye' : 'fas fa-eye-slash';
- btn.title = next >= OBS ? 'Hide from Players' : 'Show to Players';
- } catch (_) {}
- });
- // Append to the end of the row
- li.appendChild(btn);
- } catch (_) {}
- });
- } catch (_) {}
+
+ // Retry mechanism: try multiple times with increasing delays to handle async rendering
+ const injectEyeButtons = (attempt = 0) => {
+ const maxAttempts = 5;
+ const delay = attempt * 100; // 0ms, 100ms, 200ms, 300ms, 400ms
+
+ requestAnimationFrame(() => {
+ setTimeout(() => {
+ const list =
+ root.querySelector('ol.directory-list') ||
+ root.querySelector('.directory-list') ||
+ root;
+ const items = list.querySelectorAll(
+ 'li[data-document-id], li.directory-item, li.document, li.journal-entry'
+ );
+
+ // If no items found and we haven't exhausted retries, try again
+ if (items.length === 0 && attempt < maxAttempts) {
+ injectEyeButtons(attempt + 1);
+ return;
+ }
+
+ const OBS = CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER;
+ const NON = CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE;
+ items.forEach((li) => {
+ try {
+ const id =
+ li.getAttribute('data-document-id') ||
+ li.getAttribute('data-entry-id');
+ if (!id) return;
+ if (li.querySelector('.archivist-eye')) return;
+ const j = game.journal?.get?.(id);
+ if (!j) return;
+ // Only render for Archivist custom sheets (identified by our flags)
+ let isCustom = false;
+ try {
+ const f = j.getFlag(CONFIG.MODULE_ID, 'archivist') || {};
+ isCustom = !!(f.archivistId || f.sheetType);
+ } catch (_) {
+ isCustom = false;
+ }
+ if (!isCustom) return;
+ const cur = Number(j?.ownership?.default ?? NON);
+ const btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'archivist-eye';
+ const icon = document.createElement('i');
+ icon.className = cur >= OBS ? 'fas fa-eye' : 'fas fa-eye-slash';
+ btn.title = cur >= OBS ? 'Hide from Players' : 'Show to Players';
+ btn.appendChild(icon);
+ btn.addEventListener('click', async (ev) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ try {
+ const now = Number(j?.ownership?.default ?? NON);
+ const next = now >= OBS ? NON : OBS;
+ await j.update({ ownership: { default: next } });
+ icon.className = next >= OBS ? 'fas fa-eye' : 'fas fa-eye-slash';
+ btn.title = next >= OBS ? 'Hide from Players' : 'Show to Players';
+ } catch (_) {}
+ });
+ // Append to the end of the row
+ li.appendChild(btn);
+ } catch (_) {}
+ });
+ }, delay);
+ });
+ };
+
+ injectEyeButtons();
+ } catch (e) {
+ console.warn('[Archivist Sync] Failed to inject eye buttons', e);
+ }
});
diff --git a/scripts/dialogs/world-setup-dialog.js b/scripts/dialogs/world-setup-dialog.js
index 117e546..6ef96a3 100644
--- a/scripts/dialogs/world-setup-dialog.js
+++ b/scripts/dialogs/world-setup-dialog.js
@@ -3164,6 +3164,17 @@ export class WorldSetupDialog extends foundry.applications.api.HandlebarsApplica
);
}
} catch (_) {}
+
+ // Force re-render Journal Directory to mount custom elements
+ // Use a small delay to ensure the dialog is fully closed first
+ setTimeout(async () => {
+ try {
+ await ui?.journal?.render?.({ force: true });
+ console.log('[Archivist Sync] Journal Directory re-rendered after setup');
+ } catch (e) {
+ console.warn('[Archivist Sync] Failed to re-render Journal Directory after setup', e);
+ }
+ }, 100);
} catch (error) {
console.error('Error completing world setup:', error);
ui.notifications.error(
diff --git a/scripts/modules/settings-manager.js b/scripts/modules/settings-manager.js
index e2914b6..d4f25a7 100644
--- a/scripts/modules/settings-manager.js
+++ b/scripts/modules/settings-manager.js
@@ -715,6 +715,18 @@ export class SettingsManager {
onChange: (value) => {
console.log(`${this.moduleTitle} | World initialized: ${value}`);
this._onChatAvailabilityChange();
+ // Trigger Journal Directory re-render when world initialization changes
+ // This ensures custom elements mount immediately after setup
+ if (value) {
+ setTimeout(async () => {
+ try {
+ await ui?.journal?.render?.({ force: true });
+ console.log('[Archivist Sync] Journal Directory re-rendered after world initialization change');
+ } catch (e) {
+ console.warn('[Archivist Sync] Failed to re-render Journal Directory on setting change', e);
+ }
+ }, 100);
+ }
},
});
}