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); + } }, }); }