Files
Foundry-VTT-Docker/resources/app/client/apps/sidebar/tabs/compendium-directory.js
2025-01-04 00:34:03 +01:00

433 lines
14 KiB
JavaScript

/**
* A compendium of knowledge arcane and mystical!
* Renders the sidebar directory of compendium packs
* @extends {SidebarTab}
* @mixes {DirectoryApplication}
*/
class CompendiumDirectory extends DirectoryApplicationMixin(SidebarTab) {
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "compendium",
template: "templates/sidebar/compendium-directory.html",
title: "COMPENDIUM.SidebarTitle",
contextMenuSelector: ".directory-item.compendium",
entryClickSelector: ".compendium"
});
}
/**
* A reference to the currently active compendium types. If empty, all types are shown.
* @type {string[]}
*/
#activeFilters = [];
get activeFilters() {
return this.#activeFilters;
}
/* -------------------------------------------- */
/** @override */
entryType = "Compendium";
/* -------------------------------------------- */
/** @override */
static entryPartial = "templates/sidebar/partials/pack-partial.html";
/* -------------------------------------------- */
/** @override */
_entryAlreadyExists(entry) {
return this.collection.has(entry.collection);
}
/* -------------------------------------------- */
/** @override */
_getEntryDragData(entryId) {
const pack = this.collection.get(entryId);
return {
type: "Compendium",
id: pack.collection
};
}
/* -------------------------------------------- */
/** @override */
_entryIsSelf(entry, otherEntry) {
return entry.metadata.id === otherEntry.metadata.id;
}
/* -------------------------------------------- */
/** @override */
async _sortRelative(entry, sortData) {
// We build up a single update object for all compendiums to prevent multiple re-renders
const packConfig = game.settings.get("core", "compendiumConfiguration");
const targetFolderId = sortData.updateData.folder;
packConfig[entry.collection] = foundry.utils.mergeObject(packConfig[entry.collection] || {}, {
folder: targetFolderId
});
// Update sorting
const sorting = SortingHelpers.performIntegerSort(entry, sortData);
for ( const s of sorting ) {
const pack = s.target;
const existingConfig = packConfig[pack.collection] || {};
existingConfig.sort = s.update.sort;
}
await game.settings.set("core", "compendiumConfiguration", packConfig);
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
html.find(".filter").click(this._displayFilterCompendiumMenu.bind(this));
}
/* -------------------------------------------- */
/**
* Display a menu of compendium types to filter by
* @param {PointerEvent} event The originating pointer event
* @returns {Promise<void>}
* @protected
*/
async _displayFilterCompendiumMenu(event) {
// If there is a current dropdown menu, remove it
const dropdown = document.getElementsByClassName("dropdown-menu")[0];
if ( dropdown ) {
dropdown.remove();
return;
}
const button = event.currentTarget;
// Display a menu of compendium types to filter by
const choices = CONST.COMPENDIUM_DOCUMENT_TYPES.map(t => {
const config = CONFIG[t];
return {
name: game.i18n.localize(config.documentClass.metadata.label),
icon: config.sidebarIcon,
type: t,
callback: (event) => this._onToggleCompendiumFilterType(event, t)
};
});
// If there are active filters, add a "Clear Filters" option
if ( this.#activeFilters.length ) {
choices.unshift({
name: game.i18n.localize("COMPENDIUM.ClearFilters"),
icon: "fas fa-times",
type: null,
callback: (event) => this._onToggleCompendiumFilterType(event, null)
});
}
// Create a vertical list of buttons contained in a div
const menu = document.createElement("div");
menu.classList.add("dropdown-menu");
const list = document.createElement("div");
list.classList.add("dropdown-list", "flexcol");
menu.appendChild(list);
for ( let c of choices ) {
const dropdownItem = document.createElement("a");
dropdownItem.classList.add("dropdown-item");
if ( this.#activeFilters.includes(c.type) ) dropdownItem.classList.add("active");
dropdownItem.innerHTML = `<i class="${c.icon}"></i> ${c.name}`;
dropdownItem.addEventListener("click", c.callback);
list.appendChild(dropdownItem);
}
// Position the menu
const pos = {
top: button.offsetTop + 10,
left: button.offsetLeft + 10
};
menu.style.top = `${pos.top}px`;
menu.style.left = `${pos.left}px`;
button.parentElement.appendChild(menu);
}
/* -------------------------------------------- */
/**
* Handle toggling a compendium type filter
* @param {PointerEvent} event The originating pointer event
* @param {string|null} type The compendium type to filter by. If null, clear all filters.
* @protected
*/
_onToggleCompendiumFilterType(event, type) {
if ( type === null ) this.#activeFilters = [];
else this.#activeFilters = this.#activeFilters.includes(type) ?
this.#activeFilters.filter(t => t !== type) : this.#activeFilters.concat(type);
this.render();
}
/* -------------------------------------------- */
/**
* The collection of Compendium Packs which are displayed in this Directory
* @returns {CompendiumPacks<string, CompendiumCollection>}
*/
get collection() {
return game.packs;
}
/* -------------------------------------------- */
/**
* Get the dropped Entry from the drop data
* @param {object} data The data being dropped
* @returns {Promise<object>} The dropped Entry
* @protected
*/
async _getDroppedEntryFromData(data) {
return game.packs.get(data.id);
}
/* -------------------------------------------- */
/** @override */
async _createDroppedEntry(document, folder) {
throw new Error("The _createDroppedEntry shouldn't be called for CompendiumDirectory");
}
/* -------------------------------------------- */
/** @override */
_getEntryName(entry) {
return entry.metadata.label;
}
/* -------------------------------------------- */
/** @override */
_getEntryId(entry) {
return entry.metadata.id;
}
/* -------------------------------------------- */
/** @override */
async getData(options={}) {
let context = await super.getData(options);
// For each document, assign a default image if one is not already present, and calculate the style string
const packageTypeIcons = {
"world": World.icon,
"system": System.icon,
"module": Module.icon
};
const packContext = {};
for ( const pack of this.collection ) {
packContext[pack.collection] = {
locked: pack.locked,
customOwnership: "ownership" in pack.config,
collection: pack.collection,
name: pack.metadata.packageName,
label: pack.metadata.label,
icon: CONFIG[pack.metadata.type].sidebarIcon,
hidden: this.#activeFilters?.length ? !this.#activeFilters.includes(pack.metadata.type) : false,
banner: pack.banner,
sourceIcon: packageTypeIcons[pack.metadata.packageType]
};
}
// Return data to the sidebar
context = foundry.utils.mergeObject(context, {
folderIcon: CONFIG.Folder.sidebarIcon,
label: game.i18n.localize("PACKAGE.TagCompendium"),
labelPlural: game.i18n.localize("SIDEBAR.TabCompendium"),
sidebarIcon: "fas fa-atlas",
filtersActive: !!this.#activeFilters.length
});
context.packContext = packContext;
return context;
}
/* -------------------------------------------- */
/** @override */
async render(force=false, options={}) {
game.packs.initializeTree();
return super.render(force, options);
}
/* -------------------------------------------- */
/** @override */
_getEntryContextOptions() {
if ( !game.user.isGM ) return [];
return [
{
name: "OWNERSHIP.Configure",
icon: '<i class="fa-solid fa-user-lock"></i>',
callback: li => {
const pack = game.packs.get(li.data("pack"));
return pack.configureOwnershipDialog();
}
},
{
name: "FOLDER.Clear",
icon: '<i class="fas fa-folder"></i>',
condition: header => {
const li = header.closest(".directory-item");
const entry = this.collection.get(li.data("entryId"));
return !!entry.folder;
},
callback: header => {
const li = header.closest(".directory-item");
const entry = this.collection.get(li.data("entryId"));
entry.setFolder(null);
}
},
{
name: "COMPENDIUM.ToggleLocked",
icon: '<i class="fas fa-lock"></i>',
callback: li => {
let pack = game.packs.get(li.data("pack"));
const isUnlock = pack.locked;
if ( isUnlock && (pack.metadata.packageType !== "world")) {
return Dialog.confirm({
title: `${game.i18n.localize("COMPENDIUM.ToggleLocked")}: ${pack.title}`,
content: `<p><strong>${game.i18n.localize("Warning")}:</strong> ${game.i18n.localize("COMPENDIUM.ToggleLockedWarning")}</p>`,
yes: () => pack.configure({locked: !pack.locked}),
options: {
top: Math.min(li[0].offsetTop, window.innerHeight - 350),
left: window.innerWidth - 720,
width: 400
}
});
}
else return pack.configure({locked: !pack.locked});
}
},
{
name: "COMPENDIUM.Duplicate",
icon: '<i class="fas fa-copy"></i>',
callback: li => {
let pack = game.packs.get(li.data("pack"));
const html = `<form>
<div class="form-group">
<label>${game.i18n.localize("COMPENDIUM.DuplicateTitle")}</label>
<input type="text" name="label" value="${game.i18n.format("DOCUMENT.CopyOf", {name: pack.title})}"/>
<p class="notes">${game.i18n.localize("COMPENDIUM.DuplicateHint")}</p>
</div>
</form>`;
return Dialog.confirm({
title: `${game.i18n.localize("COMPENDIUM.Duplicate")}: ${pack.title}`,
content: html,
yes: html => {
const label = html.querySelector('input[name="label"]').value;
return pack.duplicateCompendium({label});
},
options: {
top: Math.min(li[0].offsetTop, window.innerHeight - 350),
left: window.innerWidth - 720,
width: 400,
jQuery: false
}
});
}
},
{
name: "COMPENDIUM.ImportAll",
icon: '<i class="fas fa-download"></i>',
condition: li => game.packs.get(li.data("pack"))?.documentName !== "Adventure",
callback: li => {
let pack = game.packs.get(li.data("pack"));
return pack.importDialog({
top: Math.min(li[0].offsetTop, window.innerHeight - 350),
left: window.innerWidth - 720,
width: 400
});
}
},
{
name: "COMPENDIUM.Delete",
icon: '<i class="fas fa-trash"></i>',
condition: li => {
let pack = game.packs.get(li.data("pack"));
return pack.metadata.packageType === "world";
},
callback: li => {
let pack = game.packs.get(li.data("pack"));
return this._onDeleteCompendium(pack);
}
}
];
}
/* -------------------------------------------- */
/** @override */
async _onClickEntryName(event) {
event.preventDefault();
const element = event.currentTarget;
const packId = element.closest("[data-pack]").dataset.pack;
const pack = game.packs.get(packId);
pack.render(true);
}
/* -------------------------------------------- */
/** @override */
async _onCreateEntry(event) {
event.preventDefault();
event.stopPropagation();
const li = event.currentTarget.closest(".directory-item");
const targetFolderId = li ? li.dataset.folderId : null;
const types = CONST.COMPENDIUM_DOCUMENT_TYPES.map(documentName => {
return { value: documentName, label: game.i18n.localize(getDocumentClass(documentName).metadata.label) };
});
game.i18n.sortObjects(types, "label");
const folders = this.collection._formatFolderSelectOptions();
const html = await renderTemplate("templates/sidebar/compendium-create.html",
{types, folders, folder: targetFolderId, hasFolders: folders.length >= 1});
return Dialog.prompt({
title: game.i18n.localize("COMPENDIUM.Create"),
content: html,
label: game.i18n.localize("COMPENDIUM.Create"),
callback: async html => {
const form = html.querySelector("#compendium-create");
const fd = new FormDataExtended(form);
const metadata = fd.object;
let targetFolderId = metadata.folder;
if ( metadata.folder ) delete metadata.folder;
if ( !metadata.label ) {
let defaultName = game.i18n.format("DOCUMENT.New", {type: game.i18n.localize("PACKAGE.TagCompendium")});
const count = game.packs.size;
if ( count > 0 ) defaultName += ` (${count + 1})`;
metadata.label = defaultName;
}
const pack = await CompendiumCollection.createCompendium(metadata);
if ( targetFolderId ) await pack.setFolder(targetFolderId);
},
rejectClose: false,
options: { jQuery: false }
});
}
/* -------------------------------------------- */
/**
* Handle a Compendium Pack deletion request
* @param {object} pack The pack object requested for deletion
* @private
*/
_onDeleteCompendium(pack) {
return Dialog.confirm({
title: `${game.i18n.localize("COMPENDIUM.Delete")}: ${pack.title}`,
content: `<h4>${game.i18n.localize("AreYouSure")}</h4><p>${game.i18n.localize("COMPENDIUM.DeleteWarning")}</p>`,
yes: () => pack.deleteCompendium(),
defaultYes: false
});
}
}