Skip to content

Commit

Permalink
[foundryvtt#1401] Rework ItemChoiceFlow to be responsive, add hint to…
Browse files Browse the repository at this point in the history
… this advancement's data
  • Loading branch information
arbron committed Aug 22, 2022
1 parent 390eb95 commit 399f17f
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 71 deletions.
4 changes: 4 additions & 0 deletions dnd5e.css
Original file line number Diff line number Diff line change
Expand Up @@ -1433,6 +1433,10 @@
height: calc(var(--form-field-height) + 5px);
line-height: 1.5em;
}
.dnd5e.advancement.flow form[data-type="ItemChoice"] .item-name .item-delete {
flex: 0 0 20px;
margin-inline-end: 1px;
}
.dnd5e.advancement.flow input.error {
outline: 2px solid red;
}
Expand Down
7 changes: 7 additions & 0 deletions less/advancement.less
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,13 @@
}
}

form[data-type="ItemChoice"] {
.item-name .item-delete {
flex: 0 0 20px;
margin-inline-end: 1px;
}
}

input.error {
outline: 2px solid red;
}
Expand Down
135 changes: 67 additions & 68 deletions module/advancement/types/item-choice.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class ItemChoiceAdvancement extends Advancement {
return foundry.utils.mergeObject(super.metadata, {
defaults: {
configuration: {
hint: "",
choices: {},
allowDrops: true,
pool: []
Expand Down Expand Up @@ -104,6 +105,32 @@ export class ItemChoiceConfig extends AdvancementConfig {
* @extends {AdvancementFlow}
*/
export class ItemChoiceFlow extends AdvancementFlow {
constructor(...args) {
super(...args);

/**
* Set of selected UUIDs.
* @type {Set<string>}
*/
this.selected = new Set(
this.retainedData?.items.map(i => foundry.utils.getProperty(i, "flags.dnd5e.sourceId"))
?? Object.values(this.advancement.data.value[this.level] ?? {})
);

/**
* Cached items from the advancement's pool.
* @type {Array<Item5e>}
*/
this.pool = undefined;

/**
* List of dropped items.
* @type {Array<Item5e>}
*/
this.dropped = [];
}

/* -------------------------------------------- */

/** @inheritdoc */
static get defaultOptions() {
Expand All @@ -117,26 +144,23 @@ export class ItemChoiceFlow extends AdvancementFlow {

/** @inheritdoc */
async getData() {
const maxChoices = this.advancement.data.configuration.choices[this.level];
const added = this.retainedData?.items.map(i => foundry.utils.getProperty(i, "flags.dnd5e.sourceId"))
?? this.advancement.data.value[this.level];
const checked = new Set(Object.values(added ?? {}));

const allUuids = new Set([...this.advancement.data.configuration.pool, ...checked]);
const items = await Promise.all(Array.from(allUuids).map(async uuid => {
const item = await fromUuid(uuid);
item.checked = checked.has(uuid);
return item;
}));

return foundry.utils.mergeObject(super.getData(), {
choices: {
max: maxChoices,
current: checked.size,
full: checked.size >= maxChoices
},
items
this.pool ??= await Promise.all(this.advancement.data.configuration.pool.map(fromUuid));

const max = this.advancement.data.configuration.choices[this.level];
const choices = { max, current: this.selected.size, full: this.selected.size >= max };

// TODO: Make any choices made at previous levels unavailable
// TODO: Make any items already on actor unavailable?
// TODO: Make sure selected works properly with retained data
// TODO: Ensure everything is populated properly when going forward and backward through steps

const items = [...this.pool, ...this.dropped];
items.forEach(i => {
i.checked = this.selected.has(i.uuid);
i.disabled = !i.checked && choices.full;
});

return foundry.utils.mergeObject(super.getData(), { choices, items });
}

/* -------------------------------------------- */
Expand All @@ -145,37 +169,16 @@ export class ItemChoiceFlow extends AdvancementFlow {
activateListeners(html) {
super.activateListeners(html);
html.find("a[data-uuid]").click(this._onClickFeature.bind(this));
html.find(".item-delete").click(this._onItemDelete.bind(this));
}

/* -------------------------------------------- */

/** @inheritdoc */
_onChangeInput(event) {
this._calculateCurrent();
}

/* -------------------------------------------- */

/**
* Adjust the currently selected number to reflect for and enable/disable inputs as necessary.
*/
_calculateCurrent() {
// Recalculate the currently selected number
const current = Array.from(this.form.querySelectorAll("input")).reduce((current, i) => {
return ( (i.type === "hidden") || i.checked ) ? current + 1 : current;
}, 0);
this.form.dataset.current = current;

// Enabled/disabled non-selected checkboxes
const checkboxes = this.form.querySelectorAll("input[type='checkbox']");
if ( current >= parseInt(this.form.dataset.max) ) {
checkboxes.forEach(c => c.disabled = !c.checked);
} else {
checkboxes.forEach(c => c.disabled = false);
}

// Update the current value listed in the UI
this.form.querySelector(".current").innerText = current;
if ( event.target.checked ) this.selected.add(event.target.name);
else this.selected.delete(event.target.name);
this.render();
}

/* -------------------------------------------- */
Expand All @@ -195,28 +198,24 @@ export class ItemChoiceFlow extends AdvancementFlow {
/* -------------------------------------------- */

/**
* Handle deleting an existing Item entry from the Advancement.
* @param {Event} event The originating click event.
* @returns {Promise<Item5e>} The updated parent Item after the application re-renders.
* @private
* Handle deleting a dropped item.
* @param {Event} event The originating click event.
* @protected
*/
async _onItemDelete(event) {
// event.preventDefault();
// const uuidToDelete = event.currentTarget.closest("[data-item-uuid]")?.dataset.itemUuid;
// if ( !uuidToDelete ) return;
// const items = foundry.utils.getProperty(this.advancement.data.configuration, this.options.dropKeyPath);
// const updates = { configuration: this.prepareConfigurationUpdate({
// [this.options.dropKeyPath]: items.filter(uuid => uuid !== uuidToDelete)
// }) };
// await this.advancement.update(updates);
// this.render();
event.preventDefault();
const uuidToDelete = event.currentTarget.closest(".item-name")?.querySelector("input")?.name;
if ( !uuidToDelete ) return;
this.dropped.findSplice(i => i.uuid === uuidToDelete);
this.selected.delete(uuidToDelete);
this.render();
}

/* -------------------------------------------- */

/** @inheritdoc */
async _onDrop(event) {
if ( parseInt(this.form.dataset.current) >= parseInt(this.form.dataset.max) ) return false;
if ( this.selected.size >= this.advancement.data.configuration.choices[this.level] ) return false;

// Try to extract the data
let data;
Expand All @@ -228,20 +227,20 @@ export class ItemChoiceFlow extends AdvancementFlow {

if ( data.type !== "Item" ) return false;
const item = await Item.implementation.fromDropData(data);
item.dropped = true;

const existingItems = Array.from(this.form.querySelectorAll("input")).map(i => i.name);
// If the item is already been marked as selected, no need to go further
if ( this.selected.has(item.uuid) ) return false;

// Check to ensure the dropped item doesn't already exist on the actor
// TODO: Check to ensure the dropped item doesn't already exist on the actor

// If the item already exists, simply check its checkbox
if ( existingItems.includes(item.uuid) ) {
const existingInput = this.form.querySelector(`input[name='${item.uuid}']`);
existingInput.checked = true;
this._calculateCurrent();
return false;
}
// Mark the item as selected
this.selected.add(item.uuid);

// If the item doesn't already exist in the pool, add it
if ( !this.pool.find(i => i.uuid === item.uuid) ) this.dropped.push(item);

// TODO: Add a new input
this.render();
}

/* -------------------------------------------- */
Expand Down
5 changes: 5 additions & 0 deletions templates/advancement/item-choice-config.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
{{log this}}
<div class="left-column">
{{> "systems/dnd5e/templates/advancement/parts/advancement-controls.html"}}
<div class="form-group">
<label>Hint</label>
<textarea name="data.configuration.hint">{{data.configuration.hint}}</textarea>
</div>

<div class="form-group">
<label>{{localize "DND5E.AdvancementItemChoiceAllowDrops"}}</label>
<div class="form-fields">
Expand Down
13 changes: 10 additions & 3 deletions templates/advancement/item-choice-flow.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
data-current="{{choices.current}}" data-max="{{choices.max}}">
<h3>{{{title}}}</h3>

<p>Chosen: <span class="current">{{choices.current}}</span> of {{choices.max}}</p>
{{#if advancement.data.configuration.hint}}
<p>{{advancement.data.configuration.hint}}</p>
{{/if}}

<h4 class="form-header">Chosen: <span class="current">{{choices.current}}</span> of {{choices.max}}</h4>

{{log this}}
<div class="drop-target">
{{#each items}}
<div class="item-name flexrow">
Expand All @@ -13,10 +18,12 @@ <h4>
<a data-uuid="{{uuid}}">{{this.name}}</a>
</h4>
{{#if this.dropped}}
<a class="item-control item-delete" title="{{localize DND5E.ItemDelete}}"><i class="fas fa-trash"></i></a>
<a class="item-control item-delete" title="{{localize 'DND5E.ItemDelete'}}">
<i class="fas fa-trash"></i>
</a>
<input type="hidden" name="{{this.uuid}}" value="true" data-dtype="Boolean">
{{else}}
<input type="checkbox" name="{{this.uuid}}" {{checked this.checked}} {{disabled @root.choices.full}}>
<input type="checkbox" name="{{this.uuid}}" {{checked this.checked}} {{disabled this.disabled}}>
{{/if}}
</label>
</div>
Expand Down

0 comments on commit 399f17f

Please sign in to comment.