270 lines
9.3 KiB
JavaScript
270 lines
9.3 KiB
JavaScript
(() => {
|
|
const defaults = {
|
|
rootSelector: "",
|
|
contentSelector: "",
|
|
fragmentSelector: "",
|
|
statusTextSelector: "",
|
|
tabSelector: "",
|
|
panelSelector: "",
|
|
selectSelector: "",
|
|
tabDataKey: "",
|
|
panelDataKey: "",
|
|
storageKey: "",
|
|
defaultTab: "",
|
|
defaultEndpoint: "",
|
|
source: "WritingMode",
|
|
allowChapterOnly: false,
|
|
waitingStatus: "Connected, waiting for scene",
|
|
updatingStatus: "Updating",
|
|
disconnectedStatus: "Word not connected"
|
|
};
|
|
|
|
const parsePositiveInt = (value) => {
|
|
const parsed = Number.parseInt(value || "", 10);
|
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
};
|
|
|
|
const initialize = (options) => {
|
|
const config = { ...defaults, ...options };
|
|
const root = document.querySelector(config.rootSelector);
|
|
if (!root) {
|
|
return null;
|
|
}
|
|
|
|
const contentHost = root.querySelector(config.contentSelector);
|
|
const statusText = root.querySelector(config.statusTextSelector);
|
|
const endpoint = root.dataset.contextUrl || config.defaultEndpoint;
|
|
const scopedProjectId = parsePositiveInt(root.dataset.scopeProjectId);
|
|
const scopedBookId = parsePositiveInt(root.dataset.scopeBookId);
|
|
let currentFragment = contentHost?.querySelector(config.fragmentSelector);
|
|
let currentSceneId = parsePositiveInt(currentFragment?.dataset.sceneId);
|
|
let currentChapterId = parsePositiveInt(currentFragment?.dataset.chapterId);
|
|
let loadingSceneId = null;
|
|
let loadingChapterId = null;
|
|
|
|
const setStatus = (value) => {
|
|
if (statusText && value) {
|
|
statusText.textContent = value;
|
|
}
|
|
};
|
|
|
|
const registerWorkspaceRole = () => {
|
|
const role = config.source === "WritingContext"
|
|
? window.PlotDirectorWritingWorkspace?.roles?.context
|
|
: config.source === "WritingIntent"
|
|
? window.PlotDirectorWritingWorkspace?.roles?.intent
|
|
: null;
|
|
if (!role || !window.PlotDirectorWritingWorkspace?.registerWindow) {
|
|
return;
|
|
}
|
|
|
|
const fragment = contentHost?.querySelector(config.fragmentSelector);
|
|
void window.PlotDirectorWritingWorkspace.registerWindow({
|
|
projectID: fragment?.dataset.projectId || root.dataset.scopeProjectId,
|
|
bookID: fragment?.dataset.bookId || root.dataset.scopeBookId,
|
|
writingRole: role,
|
|
displayLabel: config.source === "WritingContext" ? "Writing Context" : "Writing Intent"
|
|
}).catch(() => null);
|
|
};
|
|
|
|
const storyPositionFromWorkspaceStatus = (status) => {
|
|
const position = status?.currentStoryPosition || status?.CurrentStoryPosition;
|
|
if (!position) {
|
|
return null;
|
|
}
|
|
|
|
return window.PlotDirectorStoryPosition?.normalise(position, "WordCompanion") || position;
|
|
};
|
|
|
|
const readStoredTab = () => {
|
|
try {
|
|
return localStorage.getItem(config.storageKey) || config.defaultTab;
|
|
} catch {
|
|
return config.defaultTab;
|
|
}
|
|
};
|
|
|
|
const activeTab = () => {
|
|
const active = contentHost?.querySelector(`${config.tabSelector}.active`)?.dataset[config.tabDataKey];
|
|
return active || readStoredTab();
|
|
};
|
|
|
|
const activateTab = (tabName) => {
|
|
const nextTab = tabName || config.defaultTab;
|
|
contentHost?.querySelectorAll(config.tabSelector).forEach((button) => {
|
|
const active = button.dataset[config.tabDataKey] === nextTab;
|
|
button.classList.toggle("active", active);
|
|
button.setAttribute("aria-selected", String(active));
|
|
});
|
|
contentHost?.querySelectorAll(config.panelSelector).forEach((panel) => {
|
|
const active = panel.dataset[config.panelDataKey] === nextTab;
|
|
panel.classList.toggle("active", active);
|
|
panel.hidden = !active;
|
|
});
|
|
try {
|
|
localStorage.setItem(config.storageKey, nextTab);
|
|
} catch {
|
|
// Remembering the selected tab is a convenience only.
|
|
}
|
|
};
|
|
|
|
const wireFragment = () => {
|
|
activateTab(activeTab());
|
|
const fragment = contentHost?.querySelector(config.fragmentSelector);
|
|
const fragmentStatus = fragment?.dataset.followStatus;
|
|
if (fragmentStatus) {
|
|
setStatus(fragmentStatus);
|
|
}
|
|
registerWorkspaceRole();
|
|
|
|
const fragmentSceneId = parsePositiveInt(fragment?.dataset.sceneId);
|
|
if (fragmentSceneId) {
|
|
currentSceneId = fragmentSceneId;
|
|
}
|
|
|
|
const fragmentChapterId = parsePositiveInt(fragment?.dataset.chapterId);
|
|
if (fragmentChapterId) {
|
|
currentChapterId = fragmentChapterId;
|
|
}
|
|
|
|
contentHost?.querySelectorAll(config.tabSelector).forEach((button) => {
|
|
button.addEventListener("click", () => activateTab(button.dataset[config.tabDataKey]));
|
|
});
|
|
|
|
contentHost?.querySelector(config.selectSelector)?.addEventListener("change", (event) => {
|
|
const sceneId = parsePositiveInt(event.target.value);
|
|
if (!sceneId) {
|
|
return;
|
|
}
|
|
|
|
const fragment = contentHost?.querySelector(config.fragmentSelector);
|
|
window.PlotDirectorStoryPosition?.publishChanged({
|
|
source: config.source,
|
|
projectId: fragment?.dataset.projectId,
|
|
bookId: fragment?.dataset.bookId,
|
|
chapterId: fragment?.dataset.chapterId,
|
|
sceneId
|
|
}, config.source);
|
|
});
|
|
|
|
contentHost?.querySelector("[data-writing-intent-chapter-select]")?.addEventListener("change", (event) => {
|
|
const chapterId = parsePositiveInt(event.target.value);
|
|
if (!chapterId) {
|
|
return;
|
|
}
|
|
|
|
const fragment = contentHost?.querySelector(config.fragmentSelector);
|
|
window.PlotDirectorStoryPosition?.publishChanged({
|
|
source: config.source,
|
|
projectId: fragment?.dataset.projectId,
|
|
bookId: fragment?.dataset.bookId,
|
|
chapterId
|
|
}, config.source);
|
|
});
|
|
};
|
|
|
|
const inScope = (position) => {
|
|
if (scopedProjectId && position.projectId !== scopedProjectId) {
|
|
return false;
|
|
}
|
|
|
|
if (scopedBookId && position.bookId && position.bookId !== scopedBookId) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
const loadPosition = async (position) => {
|
|
const hasScene = Number.isInteger(position?.sceneId) && position.sceneId > 0;
|
|
const hasChapter = Number.isInteger(position?.chapterId) && position.chapterId > 0;
|
|
if (!position || !inScope(position) || (!hasScene && (!config.allowChapterOnly || !hasChapter))) {
|
|
return;
|
|
}
|
|
|
|
if (hasScene && (position.sceneId === currentSceneId || position.sceneId === loadingSceneId)) {
|
|
return;
|
|
}
|
|
|
|
if (!hasScene && (position.chapterId === currentChapterId || position.chapterId === loadingChapterId)) {
|
|
return;
|
|
}
|
|
|
|
const tab = activeTab();
|
|
loadingSceneId = hasScene ? position.sceneId : null;
|
|
loadingChapterId = !hasScene ? position.chapterId : null;
|
|
setStatus(config.updatingStatus);
|
|
const url = new URL(endpoint, window.location.origin);
|
|
if (position.projectId) url.searchParams.set("projectId", position.projectId);
|
|
if (position.bookId) url.searchParams.set("bookId", position.bookId);
|
|
if (position.chapterId) url.searchParams.set("chapterId", position.chapterId);
|
|
if (hasScene) url.searchParams.set("sceneId", position.sceneId);
|
|
|
|
try {
|
|
const response = await fetch(url, { headers: { "X-Requested-With": "XMLHttpRequest" } });
|
|
if (!response.ok) {
|
|
throw new Error(`${config.source} request failed: ${response.status}`);
|
|
}
|
|
|
|
contentHost.innerHTML = await response.text();
|
|
if (hasScene) {
|
|
currentSceneId = position.sceneId;
|
|
}
|
|
if (hasChapter) {
|
|
currentChapterId = position.chapterId;
|
|
}
|
|
wireFragment();
|
|
activateTab(tab);
|
|
} catch (error) {
|
|
console.debug(`${config.source} could not update.`, error);
|
|
setStatus(config.waitingStatus);
|
|
} finally {
|
|
loadingSceneId = null;
|
|
loadingChapterId = null;
|
|
}
|
|
};
|
|
|
|
document.addEventListener(window.PlotDirectorStoryPosition?.eventName || "plotdirector:story-position-changed", (event) => {
|
|
void loadPosition(event.detail);
|
|
});
|
|
|
|
if (window.signalR) {
|
|
const connection = new signalR.HubConnectionBuilder()
|
|
.withUrl("/hubs/word-companion-follow")
|
|
.withAutomaticReconnect()
|
|
.build();
|
|
|
|
connection.on("storyPositionChanged", (payload) => {
|
|
window.PlotDirectorStoryPosition?.publishChanged(payload, payload?.source || payload?.Source || "WordCompanion");
|
|
});
|
|
|
|
connection.start()
|
|
.then(async () => {
|
|
const status = await connection.invoke("WatchWritingWorkspace", {
|
|
projectID: scopedProjectId,
|
|
bookID: scopedBookId
|
|
}).catch(() => null);
|
|
const position = storyPositionFromWorkspaceStatus(status);
|
|
if (position?.sceneId || (config.allowChapterOnly && position?.chapterId)) {
|
|
await loadPosition(position);
|
|
}
|
|
|
|
if (!currentSceneId && !(config.allowChapterOnly && currentChapterId)) {
|
|
setStatus(config.waitingStatus);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.debug(`${config.source} follow connection unavailable.`, error);
|
|
setStatus(config.disconnectedStatus);
|
|
});
|
|
} else {
|
|
setStatus(config.disconnectedStatus);
|
|
}
|
|
|
|
wireFragment();
|
|
return { loadPosition, activateTab };
|
|
};
|
|
|
|
window.PlotDirectorWritingFollow = { initialize };
|
|
})();
|