Add customizable tags and categories with UI management

- Introduced settings for tags and categories in `settings.js`:
  - Added `tags` and `categories` settings with default values and support for customization.
  - Registered a new settings menu for managing tags and categories dynamically.
- Enhanced `main.js` to use the customizable tags and categories in templates.
  - Adjusted `AscAssetManager.TEMPLATES` to include a new `SETTINGS_TAGS_AND_CATEGORIES` template.
  - Updated the `renderUploadForm` hook to fetch tags and categories dynamically from settings.
- Added a new Handlebars template `settings-tags-and-categories.hbs`:
  - Provides a user interface for managing tags and categories.
  - Includes functionality to add, reset, and delete rows dynamically.

These updates allow users to define and manage tags and categories via the module's settings menu, improving flexibility and user experience.
This commit is contained in:
2025-01-23 10:24:54 -06:00
parent 4cd01570f7
commit 3fdaadf7e0
3 changed files with 231 additions and 3 deletions

View File

@@ -8,7 +8,8 @@ export class AscAssetManager {
static TEMPLATES = {
UPLOAD_CHOOSE:`modules/${this.ID}/templates/upload-choose.hbs`,
UPLOAD_FORM:`modules/${this.ID}/templates/upload-form.hbs`,
SETTINGS_MENU_MACRO:`modules/${this.ID}/templates/settings-menu-macro.hbs`
SETTINGS_MENU_MACRO:`modules/${this.ID}/templates/settings-menu-macro.hbs`,
SETTINGS_TAGS_AND_CATEGORIES:`modules/${this.ID}/templates/settings-tags-and-categories.hbs`
}
static getDirectory () {
@@ -148,7 +149,11 @@ Hooks.on("renderHotbar", ({macros}, html) => {
});
Hooks.on("ascAssetManager.renderUploadForm", (data={})=>{
const templateData = {moduleId: AscAssetManager.ID, fileCategories: AscAssetManager.fileCategories, fileTags: AscAssetManager.fileTags}
const templateData = {
moduleId: AscAssetManager.ID,
fileCategories: game.settings.get(AscAssetManager.ID, "categories"),
fileTags: game.settings.get(AscAssetManager.ID, "tags")
}
renderTemplate(AscAssetManager.TEMPLATES.UPLOAD_FORM, templateData).then(content => {
let {file} = data
const dialog = new Dialog({

View File

@@ -16,6 +16,41 @@ export function registerSettings(AscAssetManager) {
}
});
// Register default tags setting
game.settings.register(ID, "tags", {
name: "Tags",
hint: "A list of tags to use in the module.",
scope: "world", // "world" means the setting is shared across all users; "client" means it's per-user
config: false, // Whether the setting shows up in the settings menu
type: Object, // Data type
default: [
{id:"tk", label: "Token"},
{id:"sq", label: "Square"}
], // Default tags
onChange: (value) => console.log("Tags updated to:", value) // Optional: Triggered when setting is updated
});
// Register default categories setting
game.settings.register(ID, "categories", {
name: "Categories",
hint: "A list of categories to use in the module.",
scope: "world",
config: false,
type: Object,
default: [
{id: "npcn", label: "NPC (Named)"},
{id: "npcu", label: "NPC (Unnamed)"},
{id: "scene", label: "Scene Background"},
{id: "pc", label: "PC"},
{id: "inset", label: "Inset"},
{id: "vehicle", label: "Vehicle"},
{id: "weapon", label: "Weapon"},
{id: "icon", label: "Icon"},
{id: "map", label: "Map"}
],
onChange: (value) => console.log("Categories updated to:", value)
});
// Register the "customMessage" setting
game.settings.register(ID, "rootDirectory", {
name: "Root Directory",
@@ -38,7 +73,7 @@ export function registerSettings(AscAssetManager) {
config: true,
type: class extends FormApplication {
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
return foundry.utils.FormApplicationmergeObject(super.defaultOptions, {
id: ID, // Unique ID for the application
title: "Simple Form Application", // Title of the window
template: AscAssetManager.TEMPLATES.SETTINGS_MENU_MACRO, // Path to your Handlebars template
@@ -55,4 +90,148 @@ export function registerSettings(AscAssetManager) {
}
},
});
game.settings.registerMenu(ID, "categories", {
name: "Set Tags and Categories",
label: "Set Tags and Categories",
scope: "world",
icon: "fas fa-list",
config: true,
type: class extends FormApplication {
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: ID, // Unique ID for the application
title: "Simple Form Application", // Title of the window
template: AscAssetManager.TEMPLATES.SETTINGS_TAGS_AND_CATEGORIES, // Path to your Handlebars template
width: 300, // Width of the form
height: "auto", // Adjust height automatically
});
}
getData() {
return {
moduleId: ID,
categories: game.settings.get(ID, "categories"),
tags: game.settings.get(ID, "tags")
}
}
activateListeners(html) {
super.activateListeners(html);
function addRow (html) {
// Find the nearest parent div
const parentDiv = html.closest("div:has(table)");
// Find the last row in the body
const lastRow = parentDiv.find("tbody tr:last");
// Clone the last row
const newRow = lastRow.clone();
// Loop through each input in the new row to reset values and update names/ids
newRow.find("input").each(function () {
const input = $(this);
// Reset the input value
input.val("");
// Update the name and id to n+1
const name = input.attr("name");
const id = input.attr("id");
if (name) {
const match = name.match(/(\d+)/); // Find the index in the name
if (match) {
const index = parseInt(match[1], 10) + 1;
input.attr("name", name.replace(/\d+/, index));
}
}
if (id) {
const match = id.match(/(\d+)/); // Find the index in the id
if (match) {
const index = parseInt(match[1], 10) + 1;
input.attr("id", id.replace(/\d+/, index));
}
}
});
// Append the new row to the table
parentDiv.find("tbody").append(newRow);
}
html.find('#add-category, #add-tag').click((evt)=>{
evt.preventDefault()
addRow($(evt.currentTarget));
})
html.find('#reset-category, #reset-tag').click((evt)=>{
evt.preventDefault()
let data
let item_id
if (evt.currentTarget.id == "reset-category"){
item_id = "category"
data = game.settings.settings.get(`${ID}.categories`).default
} else if (evt.currentTarget.id == "reset-tag") {
item_id = "tag"
data = game.settings.settings.get(`${ID}.tags`).default
}
const tbody = $(evt.currentTarget).closest('div:has(table)').find("tbody")
tbody.empty();
for (let [idx, {id, label}] of data.entries()){
const newRow = $(`<tr></tr>`)
newRow.append(`<td><input type="text" name="${item_id}-id-${idx}" value="${id}"></input></td>`)
newRow.append(`<td><input type="text" name="${item_id}-label-${idx}" value="${label}"></input></td>`)
newRow.append(`<td><button id="delete-row-${idx}" class="delete-row"><i class="delete-row fa-solid fa-trash-can"></i></button></td>`)
tbody.append(newRow)
}
})
html.find('.delete-row').click((evt)=>{
evt.preventDefault();
const button = $(evt.currentTarget)
button.closest('tr').remove()
})
}
async _updateObject(event, formData) {
// Save updates when the form is submitted
const categories = [];
const tags = [];
// Dynamically group rows for categories and tags
for (const [key, value] of Object.entries(formData)) {
// Match category rows
const categoryMatch = key.match(/^category-(id|label)-(\d+)$/);
if (categoryMatch) {
const [_, field, index] = categoryMatch; // Extract field (id/label) and index
const i = parseInt(index);
// Ensure the index exists in the categories array
if (!categories[i]) categories[i] = { id: "", label: "" };
categories[i][field] = value;
continue;
}
// Match tag rows
const tagMatch = key.match(/^tag-(id|label)-(\d+)$/);
if (tagMatch) {
const [_, field, index] = tagMatch; // Extract field (id/label) and index
const i = parseInt(index);
// Ensure the index exists in the tags array
if (!tags[i]) tags[i] = { id: "", label: "" };
tags[i][field] = value;
continue;
}
}
await game.settings.set(ID, "tags", tags);
await game.settings.set(ID, "categories", categories);
ui.notifications.info("Settings updated!");
}
},
});
game
}

View File

@@ -0,0 +1,44 @@
{{#*inline "table"}}
<table>
<thead>
<tr><th colspan=3>{{collection_title}}</th></tr>
<tr>
<th>ID</th>
<th>Label</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each collection}}
<tr>
<td><input type="text" name="{{item_id}}-id-{{@index}}" value="{{id}}"></input></td>
<td><input type="text" name="{{item_id}}-label-{{@index}}" value="{{label}}"></input></td>
<td><button id="delete-row-{{@index}}" class="delete-row"><i class="delete-row fa-solid fa-trash-can"></i></button></td>
</tr>
{{else}}
<tr>
<td><input type="text" name="{{item_id}}-id-0" value="" placeholder="id"></input></td>
<td><input type="text" name="{{item_id}}-label-0" value="" placeholder="label"></input></td>
<td><button id="delete-row-0" class="delete-row"><i class="delete-row fa-solid fa-trash-can"></i></button></td>
</tr>
{{/each}}
</tbody>
</table>
<div class="dialog-buttons">
<button id="add-{{item_id}}" class="dialog-button"><i class="fa-solid fa-plus"></i>Add {{item_title}}</button>
<button id="reset-{{item_id}}" class="dialog-button"><i class="fa-solid fa-arrow-rotate-left"></i>Reset {{collection_ttitle}}</button>
</div>
{{/inline}}
<div class="{{moduleId}}">
<form id="form-categories-and-tags">
<div>
{{> table collection_title="Categories" item_title="Category" collection=categories item_id="category"}}
</div>
<div>
{{> table collection_title="Tags" item_title="Tag" collection=tags item_id="tag"}}
</div>
<hr>
<button id="save" class="dialog-button"><i class="fa-solid fa-check"></i> Save</button>
</form>
</div>