Compendium filter
This commit is contained in:
@@ -24,6 +24,133 @@ export const L5R5E = {
|
||||
mass_battle: "command",
|
||||
},
|
||||
noHonorSkillsList: ["commerce", "skulduggery", "medicine", "seafaring", "survival", "labor"],
|
||||
source_reference: {
|
||||
"core_rulebook": {
|
||||
value: "core_rulebook",
|
||||
label: "l5r5e.source_reference.core_rulebook",
|
||||
type: "Rules"
|
||||
},
|
||||
"emerald_empire": {
|
||||
value: "emerald_empire",
|
||||
label: "l5r5e.source_reference.emerald_empire",
|
||||
type: "Rules"
|
||||
},
|
||||
"shadowlands": {
|
||||
value: "shadowlands",
|
||||
label: "l5r5e.source_reference.shadowlands",
|
||||
type: "Rules"
|
||||
},
|
||||
"court_of_stones": {
|
||||
value: "court_of_stones",
|
||||
label: "l5r5e.source_reference.court_of_stones",
|
||||
type: "Rules"
|
||||
},
|
||||
"path_of_waves": {
|
||||
value: "path_of_waves",
|
||||
label: "l5r5e.source_reference.path_of_waves",
|
||||
type: "Rules"
|
||||
},
|
||||
"celestial_realms": {
|
||||
value: "celestial_realms",
|
||||
label: "l5r5e.source_reference.celestial_realms",
|
||||
type: "Rules"
|
||||
},
|
||||
"fields_of_victory": {
|
||||
value: "fields_of_victory",
|
||||
label: "l5r5e.source_reference.fields_of_victory",
|
||||
type: "Rules"
|
||||
},
|
||||
"writ_of_the_wild": {
|
||||
value: "writ_of_the_wild",
|
||||
label: "l5r5e.source_reference.writ_of_the_wild",
|
||||
type: "Rules"
|
||||
},
|
||||
"gm_kit": {
|
||||
value: "gm_kit",
|
||||
label: "l5r5e.source_reference.gm_kit",
|
||||
type: "Rules"
|
||||
},
|
||||
"beginner_game": {
|
||||
value: "beginner_game",
|
||||
label: "l5r5e.source_reference.beginner_game",
|
||||
type: "Rules"
|
||||
},
|
||||
"the_mantis_clan": {
|
||||
value: "the_mantis_clan",
|
||||
label: "l5r5e.source_reference.the_mantis_clan",
|
||||
type: "Supplement"
|
||||
},
|
||||
"legacies_of_war": {
|
||||
value: "legacies_of_war",
|
||||
label: "l5r5e.source_reference.legacies_of_war",
|
||||
type: "Supplement"
|
||||
},
|
||||
"mask_of_the_oni": {
|
||||
value: "mask_of_the_oni",
|
||||
label: "l5r5e.source_reference.mask_of_the_oni",
|
||||
type: "Adventure"
|
||||
},
|
||||
"winters_embrace": {
|
||||
value: "winters_embrace",
|
||||
label: "l5r5e.source_reference.winters_embrace",
|
||||
type: "Adventure"
|
||||
},
|
||||
"sins_of_regret": {
|
||||
value: "sins_of_regret",
|
||||
label: "l5r5e.source_reference.sins_of_regret",
|
||||
type: "Adventure"
|
||||
},
|
||||
"wheele_of_judgment": {
|
||||
value: "wheele_of_judgment",
|
||||
label: "l5r5e.source_reference.wheele_of_judgment",
|
||||
type: "Adventure"
|
||||
},
|
||||
"blood_of_the_lioness": {
|
||||
value: "blood_of_the_lioness",
|
||||
label: "l5r5e.source_reference.blood_of_the_lioness",
|
||||
type: "Adventure"
|
||||
},
|
||||
"imperfect_land": {
|
||||
value: "imperfect_land",
|
||||
label: "l5r5e.source_reference.imperfect_land",
|
||||
type: "Adventure"
|
||||
},
|
||||
"in_the_palace_of_the_emerald_champion": {
|
||||
value: "in_the_palace_of_the_emerald_champion",
|
||||
label: "l5r5e.source_reference.in_the_palace_of_the_emerald_champion",
|
||||
type: "Adventure"
|
||||
},
|
||||
"the_highwayman": {
|
||||
value: "the_highwayman",
|
||||
label: "l5r5e.source_reference.the_highwayman",
|
||||
type: "Adventure"
|
||||
},
|
||||
"wedding_at_kyotei_castle": {
|
||||
value: "wedding_at_kyotei_castle",
|
||||
label: "l5r5e.source_reference.wedding_at_kyotei_castle",
|
||||
type: "Adventure"
|
||||
},
|
||||
"the_knotted_tails": {
|
||||
value: "the_knotted_tails",
|
||||
label: "l5r5e.source_reference.the_knotted_tails",
|
||||
type: "Adventure"
|
||||
},
|
||||
"cresting_waves": {
|
||||
value: "cresting_waves",
|
||||
label: "l5r5e.source_reference.cresting_waves",
|
||||
type: "Adventure"
|
||||
},
|
||||
"deathly_turns": {
|
||||
value: "deathly_turns",
|
||||
label: "l5r5e.source_reference.deathly_turns",
|
||||
type: "Adventure"
|
||||
},
|
||||
"the_scroll_or_the_blade": {
|
||||
value: "the_scroll_or_the_blade",
|
||||
label: "l5r5e.source_reference.the_scroll_or_the_blade",
|
||||
type: "Adventure"
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// *** Techniques ***
|
||||
|
||||
47
system/scripts/data/l5r5e-setfield.js
Normal file
47
system/scripts/data/l5r5e-setfield.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { HTML_l5r5e_MultiSelectElement } from "../misc/l5r5e-multiselect.js";
|
||||
/**
|
||||
* A subclass of [ArrayField]{@link ArrayField} which supports a set of contained elements.
|
||||
* Elements in this set are treated as fungible and may be represented in any order or discarded if invalid.
|
||||
*/
|
||||
export class l5r5e_SetField extends foundry.data.fields.SetField {
|
||||
|
||||
// We don't get the options we expect when we convert this to input,
|
||||
// So store them here
|
||||
#savedOptions;
|
||||
|
||||
constructor(options={}, context={}) {
|
||||
super(new foundry.data.fields.StringField({
|
||||
choices: options.options.map((option) => option.value)
|
||||
}), options, context);
|
||||
|
||||
this.#savedOptions = options;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
initialize(value, model, options={}) {
|
||||
if ( !value ) return value;
|
||||
return new Set(super.initialize(value, model, options));
|
||||
}
|
||||
|
||||
/** @override */
|
||||
toObject(value) {
|
||||
if ( !value ) return value;
|
||||
return Array.from(value).map(v => this.element.toObject(v));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Form Field Integration */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_toInput(config) {
|
||||
const e = this.element;
|
||||
return HTML_l5r5e_MultiSelectElement.create({
|
||||
name: config.name,
|
||||
options: this.#savedOptions.options,
|
||||
groups: this.#savedOptions.groups,
|
||||
value: config.value,
|
||||
localize: config.localize
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { HTML_l5r5e_MultiSelectElement } from "./misc/l5r5e-multiselect.js";
|
||||
|
||||
export default class HooksL5r5e {
|
||||
/**
|
||||
* Do anything after initialization but before ready
|
||||
@@ -55,6 +57,19 @@ export default class HooksL5r5e {
|
||||
if (disclaimer !== "" && disclaimer !== "l5r5e.global.edge_translation_disclaimer") {
|
||||
ui.notifications.info(disclaimer);
|
||||
}
|
||||
|
||||
// Find all additional source references that is not the official ones:
|
||||
const references = new Set(Object.keys(CONFIG.l5r5e.source_reference));
|
||||
for(let pack of game.packs) {
|
||||
if(pack.metadata.packageType === "system")
|
||||
continue;
|
||||
const documents = await pack.getDocuments();
|
||||
for(let document of documents) {
|
||||
if(document?.system?.source_reference)
|
||||
references.add(document.system.source_reference.source);
|
||||
}
|
||||
}
|
||||
game.settings.set(CONFIG.l5r5e.namespace, "all-compendium-references", Array.from(references));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,48 +228,234 @@ export default class HooksL5r5e {
|
||||
*/
|
||||
static async renderCompendium(app, html, data) {
|
||||
if (app.collection.documentName === "Item") {
|
||||
const content = await app.collection.getDocuments();
|
||||
const content = await app.collection.getDocuments();
|
||||
let sources_in_this_compendium = new Set([]);
|
||||
let filters_to_show = {
|
||||
rank: false,
|
||||
rarity: false,
|
||||
source: false,
|
||||
ring: false,
|
||||
}
|
||||
|
||||
// Add rank filter for techniques
|
||||
if (
|
||||
content[0].type === "technique" &&
|
||||
!["l5r5e.core-techniques-school", "l5r5e.core-techniques-mastery"].includes(data.collection.collection)
|
||||
) {
|
||||
const rankFilter = (event, rank) => {
|
||||
html[0].querySelectorAll(".directory-item").forEach((line) => {
|
||||
$(line).css("display", rank === 0 || $(line)[0].innerText?.endsWith(rank) ? "flex" : "none");
|
||||
});
|
||||
};
|
||||
const elmt = html.find(".directory-header");
|
||||
if (elmt.length > 0) {
|
||||
const div = $('<div class="flexrow"></div>');
|
||||
for (let rank = 0; rank < 6; rank++) {
|
||||
const bt = $(`<a>${rank === 0 ? "x" : rank}</a>`);
|
||||
bt.on("click", (event) => rankFilter(event, rank));
|
||||
div.append(bt);
|
||||
// Add additional data to the entries to make it faster to lookup.
|
||||
// Add Ring/rank/rarity information
|
||||
content.forEach(async (document) => {
|
||||
const entry = html.find(`[data-document-id="${document.id}"]`);
|
||||
if(document.system?.rank) {
|
||||
entry.data("rank", document.system.rank);
|
||||
filters_to_show.rank = true;
|
||||
}
|
||||
|
||||
if(document.system?.source_reference) {
|
||||
sources_in_this_compendium.add(document.system.source_reference.source);
|
||||
entry.data("source", document.system.source_reference);
|
||||
filters_to_show.source = true;
|
||||
}
|
||||
|
||||
if(document.system?.ring) {
|
||||
entry.data("ring", document.system.ring);
|
||||
filters_to_show.ring = true
|
||||
}
|
||||
|
||||
if(document.system?.rarity) {
|
||||
entry.data("rarity", document.system.rarity);
|
||||
filters_to_show.rarity = true;
|
||||
}
|
||||
|
||||
// Add ring/rank/rarity information on the item in the compendium view
|
||||
if(document.system?.ring || document.system?.rarity || document.system?.ring) {
|
||||
const ring_rarity_rank = await renderTemplate(`${CONFIG.l5r5e.paths.templates}compendium/ring-rarity-rank.html`, document.system);
|
||||
entry.append(ring_rarity_rank);
|
||||
}
|
||||
});
|
||||
|
||||
//Setup what the player cannot see.
|
||||
const officialcontent = game.settings.get(CONFIG.l5r5e.namespace, "compendium-official-content-for-players");
|
||||
const inofficialcontent = game.settings.get(CONFIG.l5r5e.namespace, "compendium-inofficial-content-for-players");
|
||||
let sources_to_mark_as_innaccessable_to_players = game.settings.get(CONFIG.l5r5e.namespace, "all-compendium-references")
|
||||
.filter((element) => {
|
||||
if(CONFIG.l5r5e.source_reference[element])
|
||||
return !officialcontent.includes(element);
|
||||
else
|
||||
return inofficialcontent.includes(element);
|
||||
});
|
||||
|
||||
// Create the function that will hide/show elements based on various factors
|
||||
const header = html.find(".directory-header");
|
||||
const applyCompendiumFilter = function() {
|
||||
const rank_filter = header.find(".rank-filter").find(".selected").data("rank");
|
||||
const user_filter = header.find("l5r5e-multi-select").val();
|
||||
const ring_filter = header.find(".ring-filter").find(".selected").data("ring");
|
||||
const rarity_filter = header.find(".rarity-filter").find(".selected").data("rarity");
|
||||
$(html).find(".directory-item").each(function() {
|
||||
const lineSource = $(this).data("source")?.source;
|
||||
if(lineSource === null || lineSource === undefined)
|
||||
return; // We might have stuff in the compendium view that does not have a source (folders etc.) Ignore those.
|
||||
|
||||
let should_show = true;
|
||||
if(sources_to_mark_as_innaccessable_to_players.includes(lineSource)) {
|
||||
if(game.user.isGM) {
|
||||
should_show &= true;
|
||||
$(this).addClass("not-for-players");
|
||||
$(this).attr("data-tooltip", game.i18n.localize("l5r5e.compendium.not_for_players"));
|
||||
}
|
||||
else {
|
||||
should_show &= false;
|
||||
}
|
||||
}
|
||||
elmt.append(div);
|
||||
|
||||
if(lineSource === "" && game.settings.get(CONFIG.l5r5e.namespace, "compendium-hide-empty-sources-from-players")) {
|
||||
if(game.user.isGM) {
|
||||
should_show &= true;
|
||||
$(this).addClass("not-for-players");
|
||||
$(this).attr("data-tooltip", game.i18n.localize("l5r5e.compendium.not_for_players"))
|
||||
}
|
||||
else {
|
||||
should_show &= false;
|
||||
}
|
||||
}
|
||||
|
||||
if(rank_filter) {
|
||||
should_show &= $(this).data("rank") == rank_filter;
|
||||
}
|
||||
|
||||
if(user_filter.length) {
|
||||
should_show &= user_filter.includes(lineSource);
|
||||
}
|
||||
|
||||
if(ring_filter) {
|
||||
should_show &= $(this).data("ring") == ring_filter
|
||||
}
|
||||
|
||||
if(rarity_filter >= 0) {
|
||||
should_show &= $(this).data("rarity") == rarity_filter
|
||||
}
|
||||
|
||||
if(should_show)
|
||||
$(this).show();
|
||||
else
|
||||
$(this).hide();
|
||||
});
|
||||
}
|
||||
|
||||
// Rank filter: Add the HTML element and on click handling
|
||||
if (filters_to_show.rank) {
|
||||
header.append(await renderTemplate(`${CONFIG.l5r5e.paths.templates}compendium/rank-filter.html`, {type: "rank", number:[1,2,3,4,5]}));
|
||||
header.find(".rank-filter").children().each(function() {
|
||||
$(this).on("click", (event, rank=$(this).data("rank")) => {
|
||||
const already_selected = $(event.target).hasClass("selected");
|
||||
$(html).find(".rank-filter").children().each(function() {
|
||||
$(this).removeClass("selected");
|
||||
});
|
||||
|
||||
// Only select valid values
|
||||
if(rank) {
|
||||
$(event.target).addClass("selected");
|
||||
}
|
||||
// we click the same button to unselect
|
||||
if(already_selected) {
|
||||
$(event.target).removeClass("selected");
|
||||
}
|
||||
|
||||
applyCompendiumFilter();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if(filters_to_show.rarity) {
|
||||
header.append(await renderTemplate(`${CONFIG.l5r5e.paths.templates}compendium/rank-filter.html`, {type: "rarity", number:[0,1,2,3,4,5,6,7,8,9,10]}));
|
||||
header.find(".rarity-filter").children().each(function() {
|
||||
$(this).on("click", (event, rarity=$(this).data("rarity")) => {
|
||||
const already_selected = $(event.target).hasClass("selected");
|
||||
$(html).find(".rarity-filter").children().each(function() {
|
||||
$(this).removeClass("selected");
|
||||
});
|
||||
|
||||
// Only select valid values
|
||||
if(Number.isInteger(rarity)) {
|
||||
$(event.target).addClass("selected");
|
||||
}
|
||||
// we click the same button to unselect
|
||||
if(already_selected) {
|
||||
$(event.target).removeClass("selected");
|
||||
}
|
||||
|
||||
applyCompendiumFilter();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if(filters_to_show.ring) {
|
||||
header.append(await renderTemplate(`${CONFIG.l5r5e.paths.templates}compendium/ring-filter.html`));
|
||||
header.find(".ring-filter").children().each(function() {
|
||||
$(this).on("click", (event, ring=$(this).data("ringid")) => {
|
||||
const already_selected = $(event.target).hasClass("selected");
|
||||
$(html).find(".ring-filter").children().each(function() {
|
||||
$(this).removeClass("selected");
|
||||
});
|
||||
|
||||
if(ring) { // Do not keep the "reset" button highlighted
|
||||
$(event.target).addClass("selected");
|
||||
}
|
||||
// we click the same button to unselect
|
||||
if(already_selected) {
|
||||
$(event.target).removeClass("selected");
|
||||
}
|
||||
|
||||
applyCompendiumFilter();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if(filters_to_show.source) {
|
||||
// Setup the source select and add it to the document with change callback
|
||||
const selectable_sources = game.settings.get(CONFIG.l5r5e.namespace, "all-compendium-references")
|
||||
.map((reference) => {
|
||||
return {
|
||||
disable: !sources_in_this_compendium.has(reference),
|
||||
source: reference
|
||||
}
|
||||
})
|
||||
.map((reference) => {
|
||||
return {
|
||||
value: reference.source,
|
||||
label: CONFIG.l5r5e.source_reference[reference.source]?.label ?? reference.source,
|
||||
translate: true,
|
||||
group: CONFIG.l5r5e.source_reference[reference.source]?.type.split(",")[0] ?? "Other",
|
||||
disabled: reference.disable
|
||||
}
|
||||
});
|
||||
|
||||
const filterSourcesBox = HTML_l5r5e_MultiSelectElement.create({
|
||||
name: "filter-sources",
|
||||
options: selectable_sources,
|
||||
localize: true,
|
||||
});
|
||||
header.append(filterSourcesBox.outerHTML);
|
||||
$("l5r5e-multi-select").on("change", (event) => {
|
||||
applyCompendiumFilter();
|
||||
});
|
||||
|
||||
// If gm add a extra button to easily filter the content to see the same stuff as a player
|
||||
if(game.user.isGM) {
|
||||
const buttonHTML = '<button type="button" class="gm" data-tooltip="Apply player filter">Player filter</button>'
|
||||
$(buttonHTML).appendTo($(header).find("l5r5e-multi-select")).click(function() {
|
||||
const filter = game.settings.get(CONFIG.l5r5e.namespace, "all-compendium-references").filter((reference) => {
|
||||
return !sources_to_mark_as_innaccessable_to_players.includes(reference);
|
||||
})
|
||||
|
||||
header.find("l5r5e-multi-select")[0].value = filter.filter((element) => element !== "");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Items : add Rarity
|
||||
// Techniques / Peculiarities : add Ring / Rank
|
||||
content.forEach((document) => {
|
||||
if (["weapon", "armor", "item", "peculiarity", "technique", "peculiarity"].includes(document.type)) {
|
||||
html.find(`[data-document-id="${document.id}"]`).append(
|
||||
`<i` +
|
||||
(document.system.ring ? ` class="i_${document.system.ring}"` : ``) +
|
||||
`>` +
|
||||
(document.system.rarity
|
||||
? `${game.i18n.localize("l5r5e.sheets.rarity")} ${document.system.rarity}`
|
||||
: "") +
|
||||
(document.system.rank
|
||||
? game.i18n.localize("l5r5e.sheets.rank") + " " + document.system.rank
|
||||
: "") +
|
||||
`</i>`
|
||||
);
|
||||
}
|
||||
});
|
||||
// This is ugly but if we hide the content too early then it won't be hidden for some reason.
|
||||
// Current guess is that the foundry search filter is doing something.
|
||||
// Adding a delay here so that we hide the content. This will fail on slow computers/network...
|
||||
setTimeout(() => {
|
||||
applyCompendiumFilter();
|
||||
}, 250)
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -320,6 +521,19 @@ export default class HooksL5r5e {
|
||||
}
|
||||
}
|
||||
|
||||
static updateCompendium(pack, documents, options, userId) {
|
||||
documents.forEach((document) => {
|
||||
const inc_reference = document?.system?.source_reference?.source;
|
||||
if(inc_reference) {
|
||||
const references = game.settings.get(CONFIG.l5r5e.namespace, "all-compendium-references");
|
||||
if(!references.includes(inc_reference)) {
|
||||
references.push(inc_reference);
|
||||
game.settings.set(CONFIG.l5r5e.namespace, "all-compendium-references", references);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to create a macro from the dropped data. Will use an existing macro if one exists.
|
||||
* @param {object} dropData The dropped data
|
||||
|
||||
@@ -50,9 +50,42 @@ export class BaseItemSheetL5r5e extends ItemSheet {
|
||||
sheetData.editable = this.isEditable;
|
||||
sheetData.options.editable = sheetData.editable;
|
||||
|
||||
// Translate current reference
|
||||
const reference = this.item.system.source_reference.source;
|
||||
const label_or_reference = CONFIG.l5r5e.source_reference[reference]?.label ?? reference
|
||||
sheetData.data.system.source_reference.source = game.i18n.localize(label_or_reference);
|
||||
|
||||
// Translate list of available references
|
||||
const all_references = game.settings.get(CONFIG.l5r5e.namespace, "all-compendium-references");
|
||||
sheetData.source_references = all_references.map((reference) => {
|
||||
const label_or_reference = CONFIG.l5r5e.source_reference[reference]?.label ?? reference
|
||||
return game.i18n.localize(label_or_reference)
|
||||
})
|
||||
|
||||
return sheetData;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called upon form submission after form data is validated
|
||||
* @param {Event} event The initial triggering submission event
|
||||
* @param {Object} formData The object of validated form data with which to update the object
|
||||
* @returns {Promise} A Promise which resolves once the update operation has completed
|
||||
* @override
|
||||
*/
|
||||
async _updateObject(event, formData) {
|
||||
|
||||
// If we have an official source then store the id instead
|
||||
if(event.currentTarget.name === "system.source_reference.source") {
|
||||
Object.entries(CONFIG.l5r5e.source_reference).forEach(([id, value]) => {
|
||||
if(game.i18n.localize(value.label) === formData["system.source_reference.source"]) {
|
||||
formData["system.source_reference.source"] = id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return super._updateObject(event,formData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a named TinyMCE text editor
|
||||
* @param {string} name The named data field which the editor modifies.
|
||||
|
||||
@@ -43,6 +43,10 @@ import { MigrationL5r5e } from "./migration.js";
|
||||
import { GmToolbox } from "./gm/gm-toolbox.js";
|
||||
import { GmMonitor } from "./gm/gm-monitor.js";
|
||||
import { Storage } from "./storage.js";
|
||||
// Misc
|
||||
import { HTML_l5r5e_MultiSelectElement } from "./misc/l5r5e-multiselect.js";
|
||||
|
||||
window.customElements.define(HTML_l5r5e_MultiSelectElement.tagName, HTML_l5r5e_MultiSelectElement);
|
||||
|
||||
/* ------------------------------------ */
|
||||
/* Initialize system */
|
||||
@@ -257,3 +261,4 @@ Hooks.on("renderChatMessage", (message, html, data) => HooksL5r5e.renderChatMess
|
||||
Hooks.on("renderCombatTracker", (app, html, data) => HooksL5r5e.renderCombatTracker(app, html, data));
|
||||
Hooks.on("renderCompendium", async (app, html, data) => HooksL5r5e.renderCompendium(app, html, data));
|
||||
Hooks.on("diceSoNiceRollStart", (messageId, context) => HooksL5r5e.diceSoNiceRollStart(messageId, context));
|
||||
Hooks.on("updateCompendium", (pack, documents, options, userId) => HooksL5r5e.updateCompendium(pack, documents, options, userId));
|
||||
|
||||
@@ -285,8 +285,24 @@ export class MigrationL5r5e {
|
||||
* @param options
|
||||
*/
|
||||
static _migrateItemData(item, options = { force: false }) {
|
||||
// Nothing for now
|
||||
return {};
|
||||
const updateData = {};
|
||||
|
||||
// ***** Start of 1.12.3 *****
|
||||
if (options?.force || MigrationL5r5e.needUpdate("1.12.3")) {
|
||||
if(item.system.book_reference) {
|
||||
const book_reference = item.system.book_reference.match("(.+) p\.(\\d+)");
|
||||
if(book_reference === null) {
|
||||
console.warn(`L5R5E | Migration | Failed to properly migrate item document ${item.name}[${item._id}]: Could not parse the book_reference`);
|
||||
updateData["system.source_reference.source"] = item.system.book_reference;
|
||||
return updateData;
|
||||
}
|
||||
updateData["system.source_reference.source"] = book_reference[1].trim();
|
||||
updateData["system.source_reference.page_nr"] = book_reference[2];
|
||||
}
|
||||
}
|
||||
// ***** End of 1.12.3 *****
|
||||
|
||||
return updateData;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
272
system/scripts/misc/l5r5e-multiselect.js
Normal file
272
system/scripts/misc/l5r5e-multiselect.js
Normal file
@@ -0,0 +1,272 @@
|
||||
const { AbstractMultiSelectElement } = foundry.applications.elements;
|
||||
|
||||
/**
|
||||
* Provide a multi-select workflow using a select element as the input mechanism.
|
||||
* It is a expanded copy of the HTMLMultiselect with support for disabling options
|
||||
* and a clear all button. Also have support for hover-over information using titlea
|
||||
*
|
||||
* @example Multi-Select HTML Markup
|
||||
* ```html
|
||||
* <l5r5e-multi-select name="select-many-things">
|
||||
* <optgroup label="Basic Options">
|
||||
* <option value="foo">Foo</option>
|
||||
* <option value="bar">Bar</option>
|
||||
* <option value="baz">Baz</option>
|
||||
* </optgroup>
|
||||
* <optgroup label="Advanced Options">
|
||||
* <option value="fizz">Fizz</option>
|
||||
* <option value="buzz">Buzz</option>
|
||||
* </optgroup>
|
||||
* </l5r5e-multi-select>
|
||||
* ```
|
||||
*/
|
||||
export class HTML_l5r5e_MultiSelectElement extends AbstractMultiSelectElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#setup();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static tagName = "l5r5e-multi-select";
|
||||
|
||||
/**
|
||||
* A select element used to choose options.
|
||||
* @type {HTMLSelectElement}
|
||||
*/
|
||||
#select;
|
||||
|
||||
/**
|
||||
* A display element which lists the chosen options.
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
#tags;
|
||||
|
||||
/**
|
||||
* A button element which clear all the options.
|
||||
* @type {HTMLButtonElement}
|
||||
*/
|
||||
#clearAll;
|
||||
|
||||
/**
|
||||
* A Set containing the values that should always be disabled.
|
||||
* @type {Set}
|
||||
*/
|
||||
#disabledValues;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// We will call initalize twice (one in the parent constructor) then one in #setup
|
||||
// required since when we want to build the elements we should to an initialize first
|
||||
// and we cannot override _initialize since we don't have access to #disabledValues there
|
||||
#setup() {
|
||||
super._initialize();
|
||||
this.#disabledValues = new Set();
|
||||
for (const option of this.querySelectorAll("option")) {
|
||||
if (option.value === "") {
|
||||
option.label = game.i18n.localize("l5r5e.multiselect.empty_tag");
|
||||
this._choices[option.value] = game.i18n.localize("l5r5e.multiselect.empty_tag");
|
||||
}
|
||||
if (option.disabled) {
|
||||
this.#disabledValues.add(option.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_buildElements() {
|
||||
this.#setup();
|
||||
|
||||
// Create select element
|
||||
this.#select = this._primaryInput = document.createElement("select");
|
||||
this.#select.insertAdjacentHTML("afterbegin", `<option id="l5r5e-multiselect-placeholder" value="" disabled selected hidden>${game.i18n.localize("l5r5e.multiselect.placeholder")}</option>`);
|
||||
this.#select.append(...this._options);
|
||||
this.#select.disabled = !this.editable;
|
||||
|
||||
// Create a div element for display
|
||||
this.#tags = document.createElement("div");
|
||||
this.#tags.className = "tags input-element-tags";
|
||||
|
||||
// Create a clear all button
|
||||
this.#clearAll = document.createElement("button");
|
||||
this.#clearAll.textContent = "X";
|
||||
return [this.#select, this.#clearAll, this.#tags];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_refresh() {
|
||||
// Update the displayed tags
|
||||
const tags = Array.from(this._value).map(id => {
|
||||
return foundry.applications.elements.HTMLStringTagsElement.renderTag(id, this._choices[id], this.editable);
|
||||
});
|
||||
this.#tags.replaceChildren(...tags);
|
||||
|
||||
// Disable selected options
|
||||
for (const option of this.#select) {
|
||||
if (this._value.has(option.value)) {
|
||||
option.disabled = true;
|
||||
option.title = game.i18n.localize("l5r5e.multiselect.already_in_filter");
|
||||
continue;
|
||||
}
|
||||
if (this.#disabledValues.has(option.value)) {
|
||||
option.disabled = true;
|
||||
continue;
|
||||
}
|
||||
option.disabled = false;
|
||||
option.removeAttribute("title");
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_activateListeners() {
|
||||
this.#select.addEventListener("change", this.#onChangeSelect.bind(this));
|
||||
this.#clearAll.addEventListener("click", this.#onClickClearAll.bind(this));
|
||||
this.#tags.addEventListener("click", this.#onClickTag.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle changes to the Select input, marking the selected option as a chosen value.
|
||||
* @param {Event} event The change event on the select element
|
||||
*/
|
||||
#onChangeSelect(event) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
const select = event.currentTarget;
|
||||
if (select.valueIndex === 0)
|
||||
return; // Ignore placeholder
|
||||
this.select(select.value);
|
||||
select.value = "";
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle click events on a tagged value, removing it from the chosen set.
|
||||
* @param {PointerEvent} event The originating click event on a chosen tag
|
||||
*/
|
||||
#onClickTag(event) {
|
||||
event.preventDefault();
|
||||
if (!event.target.classList.contains("remove"))
|
||||
return;
|
||||
if (!this.editable)
|
||||
return;
|
||||
const tag = event.target.closest(".tag");
|
||||
this.unselect(tag.dataset.key);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle clickling the clear all button
|
||||
* @param {Event} event The originating click event on the clear all button
|
||||
*/
|
||||
#onClickClearAll(event) {
|
||||
event.preventDefault();
|
||||
var _this = this;
|
||||
$(this.#tags).children().each(function () {
|
||||
_this.unselect($(this).data("key"));
|
||||
})
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/** @override */
|
||||
_toggleDisabled(disabled) {
|
||||
this.#select.toggleAttribute("disabled", disabled);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a HTML_l5r5e_MultiSelectElement using provided configuration data.
|
||||
* @param {FormInputConfig<string[]> & Omit<SelectInputConfig, "blank">} config
|
||||
* @returns {HTML_l5r5e_MultiSelectElement}
|
||||
*/
|
||||
static create(config) {
|
||||
// Foundry creates either a select with tag multi-select or multi-checkboxes. We want a l5r5e-multi-select
|
||||
// Copied the implementation from foundry.applications.fields.createMultiSelectInput with our required changes.
|
||||
const groups = prepareSelectOptionGroups(config);
|
||||
|
||||
//Setup the HTML
|
||||
const select = document.createElement(HTML_l5r5e_MultiSelectElement.tagName);
|
||||
select.name = config.name;
|
||||
foundry.applications.fields.setInputAttributes(select, config);
|
||||
for (const group_entry of groups) {
|
||||
let parent = select;
|
||||
if (group_entry.group) {
|
||||
parent = _appendOptgroupHtml(group_entry.group, select);
|
||||
}
|
||||
for (const option_entry of group_entry.options) {
|
||||
_appendOptionHtml(option_entry, parent);
|
||||
}
|
||||
}
|
||||
return select;
|
||||
}
|
||||
}
|
||||
|
||||
/** Stolen from foundry.applications.fields.prepareSelectOptionGroups: Needed to add support for tooltips
|
||||
*
|
||||
*/
|
||||
function prepareSelectOptionGroups(config) {
|
||||
const result = foundry.applications.fields.prepareSelectOptionGroups(config);
|
||||
|
||||
// Disable options based on input
|
||||
config.options.filter((option) => option?.disabled || option?.tooltip).forEach((SpecialOption) => {
|
||||
result.forEach((group) => {
|
||||
group.options.forEach((option) => {
|
||||
if (SpecialOption.value === option.value) {
|
||||
option.disabled = SpecialOption.disabled;
|
||||
option.tooltip = SpecialOption?.tooltip;
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Stolen from foundry.applications.fields
|
||||
* Create and append an optgroup element to a parent select.
|
||||
* @param {string} label
|
||||
* @param {HTMLSelectElement} parent
|
||||
* @returns {HTMLOptGroupElement}
|
||||
* @internal
|
||||
*/
|
||||
function _appendOptgroupHtml(label, parent) {
|
||||
const optgroup = document.createElement("optgroup");
|
||||
optgroup.label = label;
|
||||
parent.appendChild(optgroup);
|
||||
return optgroup;
|
||||
}
|
||||
|
||||
/** Stolen from foundry.applications.fields
|
||||
* Create and append an option element to a parent select or optgroup.
|
||||
* @param {FormSelectOption} option
|
||||
* @param {HTMLSelectElement|HTMLOptGroupElement} parent
|
||||
* @internal
|
||||
*/
|
||||
function _appendOptionHtml(option, parent) {
|
||||
const { value, label, selected, disabled, rule, tooltip } = option;
|
||||
if ((value !== undefined) && (label !== undefined)) {
|
||||
const option_html = document.createElement("option");
|
||||
option_html.value = value;
|
||||
option_html.innerText = label;
|
||||
if (selected) {
|
||||
option_html.toggleAttribute("selected", true);
|
||||
}
|
||||
if (disabled) {
|
||||
option_html.toggleAttribute("disabled", true);
|
||||
}
|
||||
if (tooltip) {
|
||||
option_html.setAttribute("title", tooltip);
|
||||
}
|
||||
parent.appendChild(option_html);
|
||||
}
|
||||
if (rule) {
|
||||
parent.insertAdjacentHTML("beforeend", "<hr>");
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ export const PreloadTemplates = async function () {
|
||||
`${tpl}items/item/item-value.html`,
|
||||
`${tpl}items/item/item-sheet.html`,
|
||||
`${tpl}items/item/item-infos.html`,
|
||||
`${tpl}items/item/item-text-partial-reference.html`,
|
||||
`${tpl}items/item/item-text.html`,
|
||||
`${tpl}items/item-pattern/item-pattern-entry.html`,
|
||||
`${tpl}items/item-pattern/item-pattern-sheet.html`,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { l5r5e_SetField } from "./data/l5r5e-setfield.js";
|
||||
|
||||
/**
|
||||
* Custom system settings register
|
||||
*/
|
||||
@@ -46,6 +48,54 @@ export const RegisterSettings = function () {
|
||||
}
|
||||
});
|
||||
|
||||
/* -------------------------------------- */
|
||||
/* Compendium view Settings (GM only) */
|
||||
/* -------------------------------------- */
|
||||
// This value is updated wheneveer we add a reference and on boot
|
||||
game.settings.register(CONFIG.l5r5e.namespace, "all-compendium-references", {
|
||||
type: new foundry.data.fields.SetField(new foundry.data.fields.StringField()),
|
||||
default: Object.keys(CONFIG.l5r5e.source_reference),
|
||||
config: false,
|
||||
scope: "world",
|
||||
});
|
||||
|
||||
|
||||
game.settings.register(CONFIG.l5r5e.namespace, "compendium-official-content-for-players", {
|
||||
name: "SETTINGS.Compendium.AllowedOfficialSources.Title",
|
||||
hint: "SETTINGS.Compendium.AllowedOfficialSources.Hint",
|
||||
type: new l5r5e_SetField( {
|
||||
groups: Array.from(new Set(Object.values(CONFIG.l5r5e.source_reference).map(value => (value.type.split(",")).map(value => value.trim())).flat())).concat("Other"),
|
||||
options: Object.values(CONFIG.l5r5e.source_reference).map((reference) => {
|
||||
return {
|
||||
...reference,
|
||||
localize: true,
|
||||
group: CONFIG.l5r5e.source_reference[reference.value]?.type ?? "Other"
|
||||
};
|
||||
})
|
||||
}),
|
||||
default: [],
|
||||
config: true,
|
||||
scope: "world",
|
||||
});
|
||||
|
||||
game.settings.register(CONFIG.l5r5e.namespace, "compendium-inofficial-content-for-players", {
|
||||
name: "SETTINGS.Compendium.AllowedInOfficialSources.Title",
|
||||
hint: "SETTINGS.Compendium.AllowedInOfficialSources.Hint",
|
||||
type: new foundry.data.fields.SetField(new foundry.data.fields.StringField()),
|
||||
default: [],
|
||||
config: true,
|
||||
scope: "world",
|
||||
});
|
||||
|
||||
game.settings.register(CONFIG.l5r5e.namespace, "compendium-hide-empty-sources-from-players", {
|
||||
name: "SETTINGS.Compendium.HideEmptySourcesFromPlayers.Title",
|
||||
hint: "SETTINGS.Compendium.HideEmptySourcesFromPlayers.Hint",
|
||||
type: Boolean,
|
||||
default: false,
|
||||
config: true,
|
||||
scope: "world",
|
||||
});
|
||||
|
||||
/* ------------------------------------ */
|
||||
/* Client preferences */
|
||||
/* ------------------------------------ */
|
||||
|
||||
Reference in New Issue
Block a user