forked from foundryvtt/pf2e
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add an "ABC Picker" as an alternative to opening compendiums from PC …
…sheets (foundryvtt#16896)
- Loading branch information
Showing
18 changed files
with
899 additions
and
165 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
<script lang="ts"> | ||
import type { ItemPF2e } from "@item"; | ||
import { ErrorPF2e, htmlQuery } from "@util"; | ||
import type { ABCPickerContext } from "./app.ts"; | ||
const { actor, foundryApp, state: data }: ABCPickerContext = $props(); | ||
const typePlural = game.i18n.localize(`PF2E.Item.${data.itemType.capitalize()}.Plural`); | ||
const searchPlaceholder = game.i18n.format("PF2E.Actor.Character.ABCPicker.SearchPlaceholder", { | ||
items: typePlural, | ||
}); | ||
let selection: string | null = $state(null); | ||
/** Show the confirmation button. */ | ||
function showConfirmation(button: HTMLButtonElement): void { | ||
const row = button.closest("li"); | ||
selection = selection === row?.dataset.uuid ? null : (row?.dataset.uuid ?? null); | ||
} | ||
/** Open an item sheet to show additional details. */ | ||
async function viewItemSheet(uuid: ItemUUID): Promise<void> { | ||
const item = await fromUuid<ItemPF2e>(uuid); | ||
item?.sheet.render(true); | ||
} | ||
/** Create a new embedded ABC item on the character. */ | ||
async function saveSelection(uuid: ItemUUID): Promise<void> { | ||
const item = await fromUuid<ItemPF2e>(uuid); | ||
if (!item) throw ErrorPF2e(`Unexpected error retrieving ${data.itemType}`); | ||
actor.createEmbeddedDocuments("Item", [item.clone().toObject()]); | ||
foundryApp.close(); | ||
} | ||
/** Search list and show or hide according to match result. */ | ||
const searchItems = fu.debounce((query: string) => { | ||
const regexp = new RegExp(RegExp.escape(query.trim()), "i"); | ||
for (const row of foundryApp.element.getElementsByTagName("li")) { | ||
row.hidden = !regexp.test(htmlQuery(row, "[data-name]")?.innerText ?? ""); | ||
} | ||
}, 200); | ||
</script> | ||
|
||
<header class="search"> | ||
<i class="fa-solid fa-search"></i> | ||
<input | ||
type="search" | ||
spellcheck="false" | ||
placeholder={searchPlaceholder} | ||
onkeyup={(event) => searchItems(event.currentTarget.value)} | ||
/> | ||
</header> | ||
|
||
<ul> | ||
{#each data.items as item} | ||
<li data-uuid={item.uuid}> | ||
<img src={item.img} loading="lazy" alt="Class icon" /> | ||
<button type="button" class="flat name-source" onclick={(e) => showConfirmation(e.currentTarget)}> | ||
<div class="name" data-name>{item.name}</div> | ||
<div class="source" class:publication={item.source.publication}>{item.source.name}</div> | ||
</button> | ||
<div class="buttons"> | ||
<button | ||
type="button" | ||
class="confirm" | ||
class:selected={selection === item.uuid} | ||
data-tooltip="PF2E.Actor.Character.ABCPicker.Tooltip.ConfirmSelection" | ||
onclick={() => saveSelection(item.uuid)}><i class="fa-solid fa-check"></i></button | ||
> | ||
<button | ||
type="button" | ||
data-tooltip="PF2E.Actor.Character.ABCPicker.Tooltip.ViewSheet" | ||
onclick={() => viewItemSheet(item.uuid)}><i class="fa-solid fa-info fa-fw"></i></button | ||
> | ||
</div> | ||
</li> | ||
{/each} | ||
</ul> | ||
|
||
<style> | ||
header.search { | ||
align-items: center; | ||
border-bottom: 1px solid var(--color-border); | ||
flex-flow: row nowrap; | ||
gap: var(--space-8); | ||
justify-content: start; | ||
padding: var(--space-8) var(--space-8); | ||
input::placeholder { | ||
color: var(--color-light-5); | ||
} | ||
} | ||
ul { | ||
flex-flow: column nowrap; | ||
height: 100%; | ||
list-style: none; | ||
margin: 0; | ||
overflow: hidden scroll; | ||
padding: var(--space-4) 0; | ||
& > li { | ||
align-items: center; | ||
border-top: 1px solid var(--color-border); | ||
display: flex; | ||
gap: var(--space-8); | ||
margin: 0; | ||
padding: var(--space-3) var(--space-6); | ||
img { | ||
color: pointer; | ||
border: none; | ||
height: 3rem; | ||
} | ||
button.name-source { | ||
display: flex; | ||
flex-flow: column nowrap; | ||
flex-grow: 1; | ||
justify-content: center; | ||
.name { | ||
color: var(--color-text-primary); | ||
} | ||
.source { | ||
color: var(--color-form-hint); | ||
font-size: var(--font-size-12); | ||
&.publication { | ||
font-style: italic; | ||
} | ||
} | ||
&:hover + .buttons button.confirm:not(.selected) { | ||
opacity: 0.33; | ||
visibility: visible; | ||
} | ||
} | ||
.buttons { | ||
align-items: center; | ||
display: flex; | ||
gap: var(--space-2); | ||
flex-flow: row nowrap; | ||
justify-content: end; | ||
button { | ||
font-size: var(--font-size-12); | ||
height: 1.5rem; | ||
width: 1.5rem; | ||
i { | ||
margin-right: 0; | ||
} | ||
&.confirm { | ||
opacity: 0; | ||
visibility: hidden; | ||
&:not(:hover) { | ||
color: darkgreen; | ||
} | ||
&.selected { | ||
opacity: 1; | ||
visibility: visible; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { CharacterPF2e } from "@actor"; | ||
import type { ItemPF2e } from "@item"; | ||
import type { ItemType } from "@item/base/data/index.ts"; | ||
import { SvelteApplicationMixin, SvelteApplicationRenderContext } from "@system/svelte/mixin.svelte.ts"; | ||
import { sluggify } from "@util"; | ||
import { UUIDUtils } from "@util/uuid.ts"; | ||
import * as R from "remeda"; | ||
import type { ApplicationConfiguration } from "types/foundry/client-esm/applications/_types.d.ts"; | ||
import type { ApplicationV2 } from "types/foundry/client-esm/applications/api/module.d.ts"; | ||
import Root from "./app.svelte"; | ||
|
||
type AhBCDType = Extract<ItemType, "ancestry" | "heritage" | "background" | "class" | "deity">; | ||
|
||
interface ABCPickerConfiguration extends ApplicationConfiguration { | ||
actor: CharacterPF2e; | ||
itemType: AhBCDType; | ||
} | ||
|
||
interface ABCItemRef { | ||
name: string; | ||
img: ImageFilePath; | ||
uuid: ItemUUID; | ||
source: { | ||
name: string; | ||
/** Whether the source comes from an item's publication data or is simply the providing module */ | ||
publication: boolean; | ||
}; | ||
hidden: boolean; | ||
} | ||
|
||
interface ABCPickerContext extends SvelteApplicationRenderContext { | ||
actor: CharacterPF2e; | ||
foundryApp: ABCPicker; | ||
state: { prompt: string; itemType: AhBCDType; items: ABCItemRef[] }; | ||
} | ||
|
||
/** A `Compendium`-like application for presenting A(H)BCD options for a character */ | ||
class ABCPicker extends SvelteApplicationMixin< | ||
AbstractConstructorOf<ApplicationV2> & { DEFAULT_OPTIONS: DeepPartial<ABCPickerConfiguration> } | ||
>(foundry.applications.api.ApplicationV2) { | ||
static override DEFAULT_OPTIONS: DeepPartial<ABCPickerConfiguration> = { | ||
id: "{id}", | ||
classes: ["abc-picker"], | ||
position: { width: 350, height: 650 }, | ||
window: { icon: "fa-solid fa-atlas", contentClasses: ["standard-form", "compact"] }, | ||
}; | ||
|
||
declare options: ABCPickerConfiguration; | ||
|
||
protected root = Root; | ||
|
||
override get title(): string { | ||
const type = game.i18n.localize(`TYPES.Item.${this.options.itemType}`); | ||
return game.i18n.format("PF2E.Actor.Character.ABCPicker.Title", { type }); | ||
} | ||
|
||
protected override _initializeApplicationOptions(options: Partial<ABCPickerConfiguration>): ABCPickerConfiguration { | ||
const initialized = super._initializeApplicationOptions(options) as ABCPickerConfiguration; | ||
initialized.window.icon = `fa-solid ${CONFIG.Item.typeIcons[initialized.itemType]}`; | ||
initialized.uniqueId = `abc-picker-${initialized.itemType}-${initialized.actor.uuid}`; | ||
return initialized; | ||
} | ||
|
||
/** Gather all items of the request type from the world and across all item compendiums. */ | ||
async #gatherItems(): Promise<ABCItemRef[]> { | ||
const { actor, itemType } = this.options; | ||
const worldItems = game.items.filter((i) => i.type === itemType && i.testUserPermission(game.user, "LIMITED")); | ||
const packItems = await UUIDUtils.fromUUIDs( | ||
game.packs | ||
.filter((p) => p.documentName === "Item" && p.testUserPermission(game.user, "LIMITED")) | ||
.flatMap((p) => p.index.filter((e) => e.type === itemType).map((e) => e.uuid as CompendiumItemUUID)), | ||
); | ||
|
||
const items = [...worldItems, ...packItems] | ||
.filter((item): item is ItemPF2e<null> => { | ||
if (item.type !== itemType || item.parent) return false; | ||
if (item.isOfType("heritage")) { | ||
const ancestrySlug = actor.ancestry ? (actor.ancestry.slug ?? sluggify(actor.ancestry.name)) : null; | ||
return item.system.ancestry?.slug === ancestrySlug || item.system.ancestry === null; | ||
} | ||
return true; | ||
}) | ||
.sort((a, b) => a.name.localeCompare(b.name)); | ||
|
||
/** Resolve a "source", preferring publication title if set and resorting to fallbacks. */ | ||
const resolveSource = (item: ItemPF2e): { name: string; publication: boolean } => { | ||
const publication = item.system.publication.title.trim(); | ||
if (publication) return { name: publication, publication: true }; | ||
if (item.uuid.startsWith("Item.")) return { name: game.world.title, publication: false }; | ||
const compendiumPack = game.packs.get(item.pack ?? ""); | ||
const module = game.modules.get(compendiumPack?.metadata.packageName ?? ""); | ||
const name = module?.title ?? compendiumPack?.title ?? "???"; | ||
return { name, publication: false }; | ||
}; | ||
|
||
return items.map( | ||
(i): ABCItemRef => ({ | ||
...R.pick(i, ["name", "img", "uuid"]), | ||
source: resolveSource(i), | ||
hidden: false, | ||
}), | ||
); | ||
} | ||
|
||
protected override async _prepareContext(): Promise<ABCPickerContext> { | ||
const itemType = this.options.itemType; | ||
return { | ||
actor: this.options.actor, | ||
foundryApp: this, | ||
state: { | ||
prompt: game.i18n.localize(`PF2E.Actor.Character.ABCPicker.Prompt.${itemType}`), | ||
itemType, | ||
items: await this.#gatherItems(), | ||
}, | ||
}; | ||
} | ||
} | ||
|
||
export { ABCPicker, type ABCPickerContext }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.