Cleanup for styling and indentation

pull/12/head
Matt Smith 2024-03-30 09:42:03 -05:00
parent dc70f366f4
commit 87fc5da8bd
10 changed files with 1607 additions and 1521 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1613,13 +1613,13 @@ Hooks.on("renderDocumentDirectory", (app, html, options) => {
if (options.tabName == "items") { if (options.tabName == "items") {
compendiumButton = ` compendiumButton = `
<div class="flexrow"> <div class="flexrow">
<button type="button" class="open-compendium-browser" data-tab="powers"><i class="fas fa-book"></i>${game.i18n.localize('CMPBrowser.Tab.SpellBrowser')}</button> <button type="button" class="open-compendium-browser" data-tab="powers"><i class="fas fa-atlas"></i>${game.i18n.localize('CMPBrowser.Tab.SpellBrowser')}</button>
<button type="button" class="open-compendium-browser" data-tab="items"><i class="fas fa-book"></i>${game.i18n.localize('CMPBrowser.Tab.ItemBrowser')}</button> <button type="button" class="open-compendium-browser" data-tab="items"><i class="fas fa-suitcase"></i>${game.i18n.localize('CMPBrowser.Tab.ItemBrowser')}</button>
</div>`; </div>`;
} }
else { else {
compendiumButton = `<button type="button" class="open-compendium-browser" data-tab="creatures"><i class="fas fa-book"></i>${game.i18n.localize('CMPBrowser.Tab.NPCBrowser')}</button>`; compendiumButton = `<button type="button" class="open-compendium-browser" data-tab="creatures"><i class="fas fa-user"></i>${game.i18n.localize('CMPBrowser.Tab.NPCBrowser')}</button>`;
} }
// Append button. Click handler added in 'ready' hook. // Append button. Click handler added in 'ready' hook.
htmlElement.querySelector(".directory-footer").insertAdjacentHTML("beforeend", compendiumButton); htmlElement.querySelector(".directory-footer").insertAdjacentHTML("beforeend", compendiumButton);

View File

@ -1,106 +1,183 @@
<template> <template>
<div class="compendium-browser-vue parent flexcol"> <div class="compendium-browser-vue parent flexcol">
<!-- Tabs. --> <!-- Tabs. -->
<!-- <section class="container container--top"> --> <Tabs group="primary" :tabs="tabs.primary"/>
<Tabs group="primary" :tabs="tabs.primary"/>
<!-- </section> -->
<!-- Filters + Content. --> <!-- Filters + Content. -->
<section class="content"> <section class="content">
<!-- <!--
Render each tab wrapper and their contents. The CompendiumBrowser<Type> components Render each tab wrapper and their contents. The CompendiumBrowser<Type> components
each have a v-if on them that causes them to only render if they're active or if they each have a v-if on them that causes them to only render if they're active or if they
have been opened at least once. have been opened at least once.
--> -->
<Tab group="primary" :tab="tabs.primary.creatures" classes="container container--bottom flexrow"> <Tab group="primary" :tab="tabs.primary.creatures" classes="container container--bottom flexrow">
<!-- <Stub>Creatures</Stub> --> <CompendiumBrowserCreatures v-if="tabs.primary.creatures.active || tabs.primary.creatures.opened" :tab="tabs.primary.creatures"/>
<CompendiumBrowserCreatures v-if="tabs.primary.creatures.active || tabs.primary.creatures.opened" :tab="tabs.primary.creatures"/> </Tab>
</Tab>
<Tab group="primary" :tab="tabs.primary.powers" classes="container container--bottom flexrow"> <Tab group="primary" :tab="tabs.primary.powers" classes="container container--bottom flexrow">
<Stub>Spells</Stub> <Stub>Spells</Stub>
<!-- <CompendiumBrowserPowers v-if="tabs.primary.powers.active || tabs.primary.powers.opened" :tab="tabs.primary.powers" :escalation="this.context.escalationDie"/> --> <!-- <CompendiumBrowserPowers v-if="tabs.primary.powers.active || tabs.primary.powers.opened" :tab="tabs.primary.powers" :escalation="this.context.escalationDie"/> -->
</Tab> </Tab>
<Tab group="primary" :tab="tabs.primary.items" classes="container container--bottom flexrow"> <Tab group="primary" :tab="tabs.primary.items" classes="container container--bottom flexrow">
<Stub>Items</Stub> <Stub>Items</Stub>
<!-- <CompendiumBrowserItems v-if="tabs.primary.items.active || tabs.primary.items.opened" :tab="tabs.primary.items"/> --> <!-- <CompendiumBrowserItems v-if="tabs.primary.items.active || tabs.primary.items.opened" :tab="tabs.primary.items"/> -->
</Tab> </Tab>
</section> </section>
</div> </div>
</template> </template>
<script> <script>
// Import component dependencies. // Import component dependencies.
import Tabs from '@/components/parts/Tabs.vue'; import Tabs from '@/components/parts/Tabs.vue';
import Tab from '@/components/parts/Tab.vue'; import Tab from '@/components/parts/Tab.vue';
import Stub from '@/components/dialogs/compendium-browser/stub.vue';
import CompendiumBrowserCreatures from '@/components/dialogs/compendium-browser/CompendiumBrowserCreatures.vue'; import CompendiumBrowserCreatures from '@/components/dialogs/compendium-browser/CompendiumBrowserCreatures.vue';
// import CompendiumBrowserPowers from '@/components/dialogs/compendium-browser/CompendiumBrowserPowers.vue'; // import CompendiumBrowserPowers from '@/components/dialogs/compendium-browser/CompendiumBrowserPowers.vue';
// import CompendiumBrowserItems from '@/components/dialogs/compendium-browser/CompendiumBrowserItems.vue'; // import CompendiumBrowserItems from '@/components/dialogs/compendium-browser/CompendiumBrowserItems.vue';
// Stub is an example component and should be removed once the others are all working.
import Stub from '@/components/dialogs/compendium-browser/Stub.vue';
export default { export default {
name: 'ArchmageCompendiumBrowser', name: 'ArchmageCompendiumBrowser',
props: [`context`], props: [`context`],
components: { components: {
Tabs, Tabs,
Tab, Tab,
Stub, Stub,
CompendiumBrowserCreatures, CompendiumBrowserCreatures,
// CompendiumBrowserPowers, // CompendiumBrowserPowers,
// CompendiumBrowserItems // CompendiumBrowserItems
}, },
setup() { setup() {
return { return {
CONFIG, CONFIG,
game game
} }
}, },
data() { data() {
return { return {
// The only variable we actually need to track is the active tab. // The only variable we actually need to track is the active tab.
tabs: { tabs: {
primary: { primary: {
// Default tab is assigned in the flags() computed property. // Default tab is assigned based on the context prop passed in
creatures: { // from the Application class.
key: 'creatures', creatures: {
label: game.i18n.localize('CMPBrowser.Tab.NPCBrowser'), key: 'creatures',
active: this.context?.defaultTab === 'creatures' ?? false, label: game.i18n.localize('CMPBrowser.Tab.NPCBrowser'),
opened: false active: this.context?.defaultTab === 'creatures' ?? false,
}, opened: false
powers: { },
key: 'powers', powers: {
label: game.i18n.localize('CMPBrowser.Tab.SpellBrowser'), key: 'powers',
active: this.context?.defaultTab === 'powers' ?? false, label: game.i18n.localize('CMPBrowser.Tab.SpellBrowser'),
opened: false active: this.context?.defaultTab === 'powers' ?? false,
}, opened: false
items: { },
key: 'items', items: {
label: game.i18n.localize('CMPBrowser.Tab.ItemBrowser'), key: 'items',
active: this.context?.defaultTab === 'items' ?? false, label: game.i18n.localize('CMPBrowser.Tab.ItemBrowser'),
opened: false active: this.context?.defaultTab === 'items' ?? false,
} opened: false
} }
} }
} }
}, }
methods: {}, },
computed: {}, methods: {},
watch: {}, computed: {},
async created() { watch: {},
console.log("Creating compendium browser..."); async created() {
}, console.log("Creating compendium browser...");
async mounted() { },
// const defaultTab = this?.context?.defaultTab ?? 'creatures'; async mounted() {
// if (this.tabs.primary?.[defaultTab]) { console.log("Compendium browser mounted.");
// this.tabs.primary[defaultTab].active = true; }
// }
// else {
// this.tabs.primary.creatures.active = true;
// }
console.log("Compendium browser mounted.");
}
} }
</script> </script>
<style lang="less">
// Import our style dependencies used in subcomponents.
@import "@vueform/slider/themes/default.css";
@import "@vueform/multiselect/themes/default.css";
:root {
--slider-bg: #00000025;
--slider-connect-bg: cornflowerblue;
--slider-handle-ring-color: cornflowerblue;
--color-blue: cornflowerblue;
--font-roboto: Roboto, sans-serif;
}
.compendium-browser-loading {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
text-align: center;
i {
margin-right: 10px;
color: var(--color-blue);
}
}
// Override ranger slider styles.
.slider-wrapper {
width: 227px;
padding: 0 10px 0 20px;
}
.level-range {
align-items: center;
}
.level-label {
flex: 0 auto;
font-family: var(--font-roboto);
line-height: 1;
}
// Override multiselect styles.
.multiselect-tag {
background-color: var(--color-blue);
}
.multiselect {
&.is-active {
box-shadow: none;
}
}
// Override compendium browser base styles.
.compendium-browser {
.control-area {
.filtercontainer {
label {
font-weight: bold;
}
.slider-wrapper div {
margin: 0;
}
.multiselect,
.multiselect div {
margin: auto;
}
.multiselect .multiselect-tags {
margin: 5px 0 0 5px;
padding: 0;
.multiselect-tag {
margin: 0 5px 5px 0;
}
}
}
}
}
</style>

View File

@ -1,85 +1,83 @@
<template> <template>
<div class="npc-browser browser flexrow"> <div class="npc-browser browser flexrow">
<section class="control-area"> <section class="control-area">
<div class="filtercontainer"> <div class="filtercontainer">
<!-- Name filter. --> <!-- Name filter. -->
<div class="filter"> <div class="filter">
<label class="unit-title" for="compendiumBrowser.name">{{ game.i18n.localize('Name') }}</label> <label class="unit-title" for="compendiumBrowser.name">{{ game.i18n.localize('Name') }}</label>
<input type="text" name="compendiumBrowser.name" v-model="name" placeholder="Hydra"/> <input type="text" name="compendiumBrowser.name" v-model="name" placeholder="Hydra"/>
</div> </div>
<!-- Sort --> <!-- Sort -->
<dl class="sorter"> <dl class="sorter">
<dt>{{ game.i18n.localize('Sort by:') }}</dt> <dt>{{ game.i18n.localize('Sort by:') }}</dt>
<dd> <dd>
<select class="sort" name="sortorder" v-model="sortBy"> <select class="sort" name="sortorder" v-model="sortBy">
<option v-for="(option, index) in sortOptions" :key="index" :value="option.value">{{ option.label }}</option> <option v-for="(option, index) in sortOptions" :key="index" :value="option.value">{{ option.label }}</option>
</select> </select>
</dd> </dd>
</dl> </dl>
<!-- Reset. --> <!-- Reset. -->
<button type="reset" @click="resetFilters()">{{ game.i18n.localize('Reset Filters') }}</button> <button type="reset" @click="resetFilters()">{{ game.i18n.localize('Reset Filters') }}</button>
</div> </div>
<div class="filtercontainer"> <div class="filtercontainer">
<h3>{{ game.i18n.localize('General') }}</h3> <h3>{{ game.i18n.localize('General') }}</h3>
<!-- Level range slider. --> <!-- Level range slider. -->
<div class="filter"> <div class="filter">
<dt class="unit-title" for="compendiumBrowser.level">{{ game.i18n.localize('Challenge Rating') }}</dt> <label class="unit-title" for="compendiumBrowser.level">{{ game.i18n.localize('Challenge Rating') }}</label>
<div class="level-range flexrow"> <div class="level-range flexrow">
<div class="level-label"><span>{{ crRange[0] }}</span><span v-if="crRange[0] !== crRange[1]"> - {{ crRange[1] }}</span></div> <div class="level-label"><span>{{ crRange[0] }}</span><span v-if="crRange[0] !== crRange[1]"> - {{ crRange[1] }}</span></div>
<div class="level-input slider-wrapper flexrow"> <div class="level-input slider-wrapper flexrow">
<Slider v-model="crRange" :min="1" :max="30" :tooltips="false"/> <Slider v-model="crRange" :min="0" :max="30" :tooltips="false"/>
</div> </div>
</div> </div>
</div> </div>
<!-- Size filter. --> <!-- Size filter. -->
<div class="filter"> <div class="filter">
<dt class="unit-title" for="compendiumBrowser.size">{{ game.i18n.localize('Size') }}</dt> <label class="unit-title" for="compendiumBrowser.size">{{ game.i18n.localize('Size') }}</label>
<dd> <Multiselect
<Multiselect v-model="size"
v-model="size" mode="tags"
mode="tags" :searchable="false"
:searchable="false" :create-option="false"
:create-option="false" :options="getOptions(CONFIG.DND5E.actorSizes)"
:options="getOptions(CONFIG.DND5E.actorSizes)" />
/> </div>
</dd> </div>
</div>
</div>
</section> </section>
<div class="list-area flexcol"> <div class="list-area flexcol">
<!-- Creatures results. --> <!-- Creatures results. -->
<!-- <section class="section section--npcs section--main flexcol"> --> <!-- <section class="section section--npcs section--main flexcol"> -->
<ul v-if="loaded" class="compendium-browser-results compendium-browser-npcs"> <ul v-if="loaded" class="compendium-browser-results compendium-browser-npcs">
<!-- Individual creature entries. --> <!-- Individual creature entries. -->
<li v-for="(entry, entryKey) in entries" :key="entryKey" :class="`npc flexrow draggable compendium-browser-row${entryKey >= pager.lastIndex - 1 && entryKey < pager.totalRows - 1 ? ' compendium-browser-row-observe': ''} document actor`" :data-document-id="entry._id" @click="openDocument(entry.uuid)" @dragstart="startDrag($event, entry, 'Actor')" draggable="true"> <li v-for="(entry, entryKey) in entries" :key="entryKey" :class="`npc flexrow draggable compendium-browser-row${entryKey >= pager.lastIndex - 1 && entryKey < pager.totalRows - 1 ? ' compendium-browser-row-observe': ''} document actor`" :data-document-id="entry._id" @click="openDocument(entry.uuid)" @dragstart="startDrag($event, entry, 'Actor')" draggable="true">
<!-- Both the image and title have drag events. These are primarily separated so that --> <!-- Both the image and title have drag events. These are primarily separated so that -->
<!-- if a user drags the token, it will only show the token as their drag preview. --> <!-- if a user drags the token, it will only show the token as their drag preview. -->
<div class="npc-image"> <div class="npc-image">
<img :src="entry.img ?? 'icons/svg/mystery-man.svg'"/> <img :src="entry.img ?? 'icons/svg/mystery-man.svg'"/>
</div> </div>
<div class="npc-line"> <div class="npc-line">
<!-- First row is the title. --> <!-- First row is the title. -->
<div class="npc-name"> <div class="npc-name">
<a>{{ entry.name }}</a> <a>{{ entry.name }}</a>
</div> </div>
<!-- Second row is supplemental info. --> <!-- Second row is supplemental info. -->
<div class="npc-tags"> <div class="npc-tags">
<span class="cr" :data-tooltip="game.i18n.localize('Challenge rating')">{{ entry.system.details.cr }}</span> <span class="cr" :data-tooltip="game.i18n.localize('Challenge rating')">{{ game.dnd5e.utils.formatCR(entry.system.details.cr) }}</span>
<span class="size">{{ entry.system.traits.size }}</span> <span class="size">{{ CONFIG.DND5E.actorSizes?.[entry.system.traits.size].label ?? entry.system.traits.size }}</span>
<span class="type">{{ entry.system.details.type.value }}</span> <span class="type">{{ CONFIG.DND5E.creatureTypes?.[entry.system.details.type.value]?.label ?? entry.system.details.type.value }}</span>
</div> </div>
</div> </div>
</li> </li>
</ul> </ul>
<div v-else class="compendium-browser-loading"><p><i class="fas fa-circle-notch fa-spin"></i>Please wait, loading...</p></div> <div v-else class="compendium-browser-loading"><p><i class="fas fa-circle-notch fa-spin"></i>Please wait, loading...</p></div>
<!-- </section> --> <!-- </section> -->
</div> </div>
</div> </div>
</template> </template>
<script> <script>
@ -90,215 +88,218 @@ import Slider from '@vueform/slider';
import Multiselect from '@vueform/multiselect'; import Multiselect from '@vueform/multiselect';
// Helper methods. // Helper methods.
import { import {
getPackIndex, getPackIndex,
getActorModuleArt, getActorModuleArt,
openDocument, openDocument,
startDrag, startDrag,
} from '@/methods/Helpers.js'; } from '@/methods/Helpers.js';
export default { export default {
name: 'CompendiumBrowserPowers', name: 'CompendiumBrowserPowers',
props: ['tab'], props: ['tab'],
// Imported components that need to be available in the <template> // Imported components that need to be available in the <template>
components: { components: {
Slider, Slider,
Multiselect, Multiselect,
}, },
setup() { setup() {
return { return {
// Imported methods that need to be available in the <template> // Imported methods that need to be available in the <template>
getActorModuleArt, getActorModuleArt,
openDocument, openDocument,
startDrag, startDrag,
// Foundry base props and methods. // Foundry base props and methods. These need to be included here if you
CONFIG, // want to access them in the template section above.
game, CONFIG,
} game,
}, }
data() { },
return { data() {
// Props used for infinite scroll and pagination. return {
observer: null, // Props used for infinite scroll and pagination.
loaded: false, observer: null,
pager: { loaded: false,
perPage: 50, pager: {
firstIndex: 0, perPage: 50,
lastIndex: 50, firstIndex: 0,
totalRows: 0, lastIndex: 50,
}, totalRows: 0,
// Sorting. },
sortBy: 'name', // Sorting.
sortOptions: [ sortBy: 'name',
{ value: 'name', label: game.i18n.localize('Name') }, sortOptions: [
{ value: 'cr', label: game.i18n.localize('Challenge Rating') }, { value: 'name', label: game.i18n.localize('Name') },
{ value: 'size', label: game.i18n.localize('Size') }, { value: 'cr', label: game.i18n.localize('Challenge Rating') },
], { value: 'size', label: game.i18n.localize('Size') },
// Our list of pseudo documents returned from the compendium. ],
packIndex: [], // Our list of pseudo documents returned from the compendium.
// Filters. packIndex: [],
name: '', // Filters.
// @todo partial CRs like 1/4. name: '',
crRange: [1, 30], // Mixed decimals and ints aren't supported by the slider, so
size: [], // just use 0 for all CRs below 1.
} crRange: [0, 30],
}, size: [],
methods: { }
/** },
* Callback for the infinite scroll IntersectionObserver. methods: {
* /**
* @param {Array} List of IntersectionObserverEntry objects. * Callback for the infinite scroll IntersectionObserver.
*/ *
infiniteScroll(entries) { * @param {Array} List of IntersectionObserverEntry objects.
// Iterate over our possible elements. */
entries.forEach(({target, isIntersecting}) => { infiniteScroll(entries) {
// If the element isn't visible, do nothing. // Iterate over our possible elements.
if (!isIntersecting) { entries.forEach(({target, isIntersecting}) => {
return; // If the element isn't visible, do nothing.
} if (!isIntersecting) {
return;
}
// Otherwise, remove the observer and update our pager properties. // Otherwise, remove the observer and update our pager properties.
// We need to increase the lastIndex for our filter by an amount // We need to increase the lastIndex for our filter by an amount
// equal to our number of entries per page. // equal to our number of entries per page.
this.observer.unobserve(target); this.observer.unobserve(target);
this.pager.lastIndex = Math.min( this.pager.lastIndex = Math.min(
this.pager.lastIndex + this.pager.perPage, this.pager.lastIndex + this.pager.perPage,
this.pager.totalRows this.pager.totalRows
); );
}); });
}, },
/** /**
* Click event to reset our filters. * Click event to reset our filters.
*/ */
resetFilters() { resetFilters() {
this.sortBy = 'name'; this.sortBy = 'name';
this.name = ''; this.name = '';
this.crRange = [1, 30]; this.crRange = [0, 30];
this.size = []; this.size = [];
}, },
/** /**
* Get multiselect options. * Get multiselect options.
*/ */
getOptions(config) { getOptions(config) {
const options = {}; const options = {};
for (let [key, value] of Object.entries(config)) { for (let [key, value] of Object.entries(config)) {
options[key] = value.label; options[key] = value.label;
} }
return options; return options;
} }
}, },
computed: { computed: {
entries() { entries() {
// Build our results array. Exit early if the length is 0. // Build our results array. Exit early if the length is 0.
let result = this.packIndex; let result = this.packIndex;
if (result.length < 1) { if (result.length < 1) {
this.pager.totalRows = 0; this.pager.totalRows = 0;
return []; return [];
} }
// Filter by name. // Filter by name.
if (this.name && this.name.length > 0) { if (this.name && this.name.length > 0) {
const name = this.name.toLocaleLowerCase(); const name = this.name.toLocaleLowerCase();
result = result.filter(entry => entry.name.toLocaleLowerCase().includes(name)); result = result.filter(entry => entry.name.toLocaleLowerCase().includes(name));
} }
// // Filter by level range. // // Filter by level range.
if (this.crRange.length == 2) { if (this.crRange.length == 2) {
result = result.filter(entry => result = result.filter(entry =>
Number(entry.system.details.cr) >= this.crRange[0] && Number(entry.system.details.cr) >= this.crRange[0] &&
Number(entry.system.details.cr) <= this.crRange[1] Number(entry.system.details.cr) <= this.crRange[1]
); );
} }
// Handle multiselect filters, which use arrays as their values. // Handle multiselect filters, which use arrays as their values.
if (Array.isArray(this.size) && this.size.length > 0) { if (Array.isArray(this.size) && this.size.length > 0) {
result = result.filter(entry => this.size.includes(entry.system.traits.size)); result = result.filter(entry => this.size.includes(entry.system.traits.size));
} }
// Reflow pager. // Reflow pager.
if (result.length > this.pager.perPage) { if (result.length > this.pager.perPage) {
this.pager.totalRows = result.length; this.pager.totalRows = result.length;
if (this.pager.lastIndex == 0) { if (this.pager.lastIndex == 0) {
this.pager.lastIndex = this.pager.perPage - 1; this.pager.lastIndex = this.pager.perPage - 1;
} }
} }
else { else {
this.pager.totalRows = 0; this.pager.totalRows = 0;
} }
// Sort. // Sort.
result = result.sort((a, b) => { result = result.sort((a, b) => {
switch (this.sortBy) { // Add sorts here.
case 'name': switch (this.sortBy) {
return a.name.localeCompare(b.name); case 'name':
case 'size': return a.name.localeCompare(b.name);
return a.system.traits.size.localeCompare(b.system.traits.size); case 'size':
} return a.system.traits.size.localeCompare(b.system.traits.size);
return a.system.details.cr - b.system.details.cr; }
}); // If no sorts match, sort by CR.
return a.system.details.cr - b.system.details.cr;
});
// Return results. // Return results.
return this.pager.totalRows > 0 return this.pager.totalRows > 0
? result.slice(this.pager.firstIndex, this.pager.lastIndex) ? result.slice(this.pager.firstIndex, this.pager.lastIndex)
: result; : result;
}, },
}, },
watch: {}, watch: {},
// Handle created hook. // Handle created hook.
async created() { async created() {
console.log("Creating compendium browser creatures tab..."); console.log("Creating compendium browser creatures tab...");
// Load the pack index with the fields we need. // Load the pack index with the fields we need.
getPackIndex([ getPackIndex([
'dnd5e.monsters', 'dnd5e.monsters',
// insert additional packs as needed. // insert additional packs as needed.
], [ ], [
'img', 'system.details.cr',
'system.details.cr', 'system.details.type',
'system.details.type', 'system.traits.size'
'system.traits.size' // insert additional properties as needed.
// insert additional properties as needed. ]).then(packIndex => {
]).then(packIndex => { // Loading a new index blows away the module art that was loaded by the system.
// Restore the pack art. // Step through the records and reassign their pack art.
if (!game.dnd5e?.moduleArt?.suppressArt && game.dnd5e?.moduleArt?.map?.size > 0) { if (!game.dnd5e?.moduleArt?.suppressArt && game.dnd5e?.moduleArt?.map?.size > 0) {
for (let record of packIndex) { for (let record of packIndex) {
record.img = getActorModuleArt(record); record.img = getActorModuleArt(record);
} }
} }
this.packIndex = packIndex; // Store our pack index for filtering, and remove the loading indicator.
this.loaded = true; this.packIndex = packIndex;
}); this.loaded = true;
});
// Create our intersection observer for infinite scroll. // Create our intersection observer for infinite scroll.
this.observer = new IntersectionObserver(this.infiniteScroll, { this.observer = new IntersectionObserver(this.infiniteScroll, {
root: this.$el, root: this.$el,
threshold: 0.1, threshold: 0.1,
}); });
}, },
// Handle mounted hook. // Handle mounted hook.
async mounted() { async mounted() {
console.log("Compendium browser creatures tab mounted."); console.log("Compendium browser creatures tab mounted.");
// Note that our tab has beened opened so that it won't de-render later. // Note that our tab has beened opened so that it won't de-render later.
this.tab.opened = true; this.tab.opened = true;
// Adjust our observers whenever the results of the compendium browser // Adjust our observers whenever the results of the compendium browser
// are updated. // are updated.
onUpdated(() => { onUpdated(() => {
const target = document.querySelector('.compendium-browser-npcs .compendium-browser-row-observe'); const target = document.querySelector('.compendium-browser-npcs .compendium-browser-row-observe');
if (target) { if (target) {
this.observer.observe(target); this.observer.observe(target);
} }
}); });
}, },
// Handle the unmount hook. // Handle the unmount hook.
async beforeUnmount() { async beforeUnmount() {
// Disconnect intersection observers when we unmount. // Disconnect intersection observers when we unmount.
this.observer.disconnect(); this.observer.disconnect();
} }
} }
</script> </script>
<style> <style lang="less">
@import "@vueform/slider/themes/default.css";
@import "@vueform/multiselect/themes/default.css";
</style> </style>

View File

@ -1,107 +1,107 @@
<template> <template>
<section class="section section--sidebar flexcol filters"> <section class="section section--sidebar flexcol filters">
<!-- Sort. --> <!-- Sort. -->
<div class="unit unit--input"> <div class="unit unit--input">
<label for="compendiumBrowser.sort" class="unit-title">{{ localize('ARCHMAGE.sort') }}</label> <label for="compendiumBrowser.sort" class="unit-title">{{ localize('ARCHMAGE.sort') }}</label>
<select class="sort" name="compendiumBrowser.sort" v-model="sortBy"> <select class="sort" name="compendiumBrowser.sort" v-model="sortBy">
<option v-for="(option, index) in sortOptions" :key="index" :value="option.value">{{ option.label }}</option> <option v-for="(option, index) in sortOptions" :key="index" :value="option.value">{{ option.label }}</option>
</select> </select>
</div> </div>
<!-- Name filter. --> <!-- Name filter. -->
<div class="unit unit--input"> <div class="unit unit--input">
<label class="unit-title" for="compendiumBrowser.itemName">{{ localize('ARCHMAGE.name') }}</label> <label class="unit-title" for="compendiumBrowser.itemName">{{ localize('ARCHMAGE.name') }}</label>
<input type="text" name="compendiumBrowser.itemName" v-model="name" placeholder="Sword"/> <input type="text" name="compendiumBrowser.itemName" v-model="name" placeholder="Sword"/>
</div> </div>
<!-- Chakra filter. --> <!-- Chakra filter. -->
<div class="unit unit--input"> <div class="unit unit--input">
<label class="unit-title" for="compendiumBrowser.chakra">{{ localize('ARCHMAGE.chakra') }}</label> <label class="unit-title" for="compendiumBrowser.chakra">{{ localize('ARCHMAGE.chakra') }}</label>
<Multiselect <Multiselect
v-model="chakra" v-model="chakra"
mode="tags" mode="tags"
:searchable="false" :searchable="false"
:create-option="false" :create-option="false"
:options="chakraSlots" :options="chakraSlots"
/> />
</div> </div>
<!-- Bonuses filter. --> <!-- Bonuses filter. -->
<div class="unit unit--input"> <div class="unit unit--input">
<h2 class="unit-title" for="compendiumBrowser.bonuses">{{ localize('ARCHMAGE.bonus') }}</h2> <h2 class="unit-title" for="compendiumBrowser.bonuses">{{ localize('ARCHMAGE.bonus') }}</h2>
<Multiselect <Multiselect
v-model="bonuses" v-model="bonuses"
mode="tags" mode="tags"
:searchable="false" :searchable="false"
:create-option="false" :create-option="false"
:options="bonusOptions" :options="bonusOptions"
/> />
</div> </div>
<!-- Recharge filter. --> <!-- Recharge filter. -->
<div class="unit unit--input"> <div class="unit unit--input">
<h2 class="unit-title" for="compendiumBrowser.recharge">{{ localize('ARCHMAGE.recharge') }}</h2> <h2 class="unit-title" for="compendiumBrowser.recharge">{{ localize('ARCHMAGE.recharge') }}</h2>
<Multiselect <Multiselect
v-model="recharge" v-model="recharge"
mode="tags" mode="tags"
:searchable="false" :searchable="false"
:create-option="false" :create-option="false"
:options="rechargeOptions" :options="rechargeOptions"
/> />
</div> </div>
<!-- Usage filter. --> <!-- Usage filter. -->
<div class="unit unit--input"> <div class="unit unit--input">
<label class="unit-title" for="compendiumBrowser.powerUsage">{{ localize('ARCHMAGE.GROUPS.powerUsage') }}</label> <label class="unit-title" for="compendiumBrowser.powerUsage">{{ localize('ARCHMAGE.GROUPS.powerUsage') }}</label>
<Multiselect <Multiselect
v-model="powerUsage" v-model="powerUsage"
mode="tags" mode="tags"
:searchable="false" :searchable="false"
:create-option="false" :create-option="false"
:options="CONFIG.ARCHMAGE.powerUsages" :options="CONFIG.ARCHMAGE.powerUsages"
/> />
</div> </div>
<!-- Reset. --> <!-- Reset. -->
<div class="unit unit--input flexrow"> <div class="unit unit--input flexrow">
<button type="reset" @click="resetFilters()">{{ localize('Reset') }}</button> <button type="reset" @click="resetFilters()">{{ localize('Reset') }}</button>
</div> </div>
</section> </section>
<section class="section section--no-overflow"> <section class="section section--no-overflow">
<!-- Items results. --> <!-- Items results. -->
<section class="section section--main section--inventory flexcol"> <section class="section section--main section--inventory flexcol">
<ul v-if="loaded" class="compendium-browser-results compendium-browser-items"> <ul v-if="loaded" class="compendium-browser-results compendium-browser-items">
<!-- Individual items entries. --> <!-- Individual items entries. -->
<li v-for="(equipment, equipmentKey) in entries" :key="equipmentKey" :class="`equipment-summary equipment compendium-browser-row${equipmentKey >= pager.lastIndex - 1 && equipmentKey < pager.totalRows - 1 ? ' compendium-browser-row-observe': ''} flexrow document item equipment-item`" :data-document-id="equipment._id" @click="openDocument(equipment.uuid, 'Item')"> <li v-for="(equipment, equipmentKey) in entries" :key="equipmentKey" :class="`equipment-summary equipment compendium-browser-row${equipmentKey >= pager.lastIndex - 1 && equipmentKey < pager.totalRows - 1 ? ' compendium-browser-row-observe': ''} flexrow document item equipment-item`" :data-document-id="equipment._id" @click="openDocument(equipment.uuid, 'Item')">
<!-- Both the image and title have drag events. These are primarily separated so that --> <!-- Both the image and title have drag events. These are primarily separated so that -->
<!-- if a user drags the token, it will only show the token as their drag preview. --> <!-- if a user drags the token, it will only show the token as their drag preview. -->
<img :src="equipment.img" class="equipment-image" @dragstart="startDrag($event, equipment, 'Item')" draggable="true"/> <img :src="equipment.img" class="equipment-image" @dragstart="startDrag($event, equipment, 'Item')" draggable="true"/>
<div class="flexcol equipment-contents" @dragstart="startDrag($event, equipment, 'Item')" draggable="true"> <div class="flexcol equipment-contents" @dragstart="startDrag($event, equipment, 'Item')" draggable="true">
<!-- First row is the title. --> <!-- First row is the title. -->
<div class="equipment-title-wrapper"> <div class="equipment-title-wrapper">
<strong class="equipment-title unit-subtitle">{{equipment.name}}</strong> <strong class="equipment-title unit-subtitle">{{equipment.name}}</strong>
</div> </div>
<!-- Second row is supplemental info. --> <!-- Second row is supplemental info. -->
<div class="grid equipment-grid"> <div class="grid equipment-grid">
<div class="equipment-bonus flexrow" :data-tooltip="localize('ARCHMAGE.bonuses')" data-tooltip-direction="RIGHT" v-if="equipment.system.attributes"> <div class="equipment-bonus flexrow" :data-tooltip="localize('ARCHMAGE.bonuses')" data-tooltip-direction="RIGHT" v-if="equipment.system.attributes">
<span class="bonus" v-for="(bonus, bonusProp) in getBonuses(equipment)" :key="bonusProp"> <span class="bonus" v-for="(bonus, bonusProp) in getBonuses(equipment)" :key="bonusProp">
<span class="bonus-label">{{localizeEquipmentBonus(bonusProp)}} </span> <span class="bonus-label">{{localizeEquipmentBonus(bonusProp)}} </span>
<span class="bonus-value">{{numberFormat(bonus, 0, true)}}</span> <span class="bonus-value">{{numberFormat(bonus, 0, true)}}</span>
</span> </span>
</div> </div>
<div class="equipment-usage" v-if="equipment.system?.powerUsage?.value" :data-tooltip="localize('ARCHMAGE.GROUPS.powerUsage')">{{ CONFIG.ARCHMAGE.powerUsages[equipment.system?.powerUsage?.value ?? ''] ?? '' }}</div> <div class="equipment-usage" v-if="equipment.system?.powerUsage?.value" :data-tooltip="localize('ARCHMAGE.GROUPS.powerUsage')">{{ CONFIG.ARCHMAGE.powerUsages[equipment.system?.powerUsage?.value ?? ''] ?? '' }}</div>
<div class="equipment-chakra" :data-tooltip="localize('ARCHMAGE.chakra')" v-if="equipment.system.chackra">{{localize(`ARCHMAGE.CHAKRA.${equipment.system.chackra}Label`)}}</div> <div class="equipment-chakra" :data-tooltip="localize('ARCHMAGE.chakra')" v-if="equipment.system.chackra">{{localize(`ARCHMAGE.CHAKRA.${equipment.system.chackra}Label`)}}</div>
<div class="equipment-recharge" :data-tooltip="localize('ARCHMAGE.recharge')">{{ `${equipment.system?.recharge?.value > 0 ? Number(equipment.system.recharge.value) + '+' : ''}`}}</div> <div class="equipment-recharge" :data-tooltip="localize('ARCHMAGE.recharge')">{{ `${equipment.system?.recharge?.value > 0 ? Number(equipment.system.recharge.value) + '+' : ''}`}}</div>
</div> </div>
</div> </div>
</li> </li>
</ul> </ul>
<div v-else class="compendium-browser-loading"><p><i class="fas fa-circle-notch fa-spin"></i>Please wait, loading...</p></div> <div v-else class="compendium-browser-loading"><p><i class="fas fa-circle-notch fa-spin"></i>Please wait, loading...</p></div>
</section> </section>
</section> </section>
</template> </template>
<script> <script>
@ -112,373 +112,373 @@ import Slider from '@vueform/slider';
import Multiselect from '@vueform/multiselect'; import Multiselect from '@vueform/multiselect';
// Helper methods. // Helper methods.
import { import {
getPackIndex, getPackIndex,
localize, localize,
localizeEquipmentBonus, localizeEquipmentBonus,
numberFormat, numberFormat,
openDocument, openDocument,
startDrag startDrag
} from '@/methods/Helpers.js'; } from '@/methods/Helpers.js';
export default { export default {
name: 'CompendiumBrowserItems', name: 'CompendiumBrowserItems',
props: ['tab'], props: ['tab'],
// Imported components that need to be available in the <template> // Imported components that need to be available in the <template>
components: { components: {
Slider, Slider,
Multiselect Multiselect
}, },
setup() { setup() {
return { return {
// Imported methods that need to be available in the <template> // Imported methods that need to be available in the <template>
localize, localize,
localizeEquipmentBonus, localizeEquipmentBonus,
numberFormat, numberFormat,
openDocument, openDocument,
startDrag, startDrag,
// Foundry base props and methods. // Foundry base props and methods.
CONFIG, CONFIG,
foundry, foundry,
game, game,
getDocumentClass getDocumentClass
} }
}, },
data() { data() {
return { return {
// Props used for infinite scroll and pagination. // Props used for infinite scroll and pagination.
observer: null, observer: null,
loaded: false, loaded: false,
pager: { pager: {
perPage: 50, perPage: 50,
firstIndex: 0, firstIndex: 0,
lastIndex: 50, lastIndex: 50,
totalRows: 0, totalRows: 0,
}, },
// Sorting. // Sorting.
sortBy: 'name', sortBy: 'name',
sortOptions: [ sortOptions: [
{ value: 'name', label: game.i18n.localize('ARCHMAGE.name') }, { value: 'name', label: game.i18n.localize('ARCHMAGE.name') },
{ value: 'chakra', label: game.i18n.localize('ARCHMAGE.chakra') }, { value: 'chakra', label: game.i18n.localize('ARCHMAGE.chakra') },
{ value: 'recharge', label: game.i18n.localize('ARCHMAGE.recharge') }, { value: 'recharge', label: game.i18n.localize('ARCHMAGE.recharge') },
{ value: 'usage', label: game.i18n.localize('ARCHMAGE.GROUPS.powerUsage') }, { value: 'usage', label: game.i18n.localize('ARCHMAGE.GROUPS.powerUsage') },
], ],
// Our list of pseudo documents returned from the compendium. // Our list of pseudo documents returned from the compendium.
packIndex: [], packIndex: [],
// Filters. // Filters.
name: '', name: '',
chakra: [], chakra: [],
recharge: [], recharge: [],
bonuses: [], bonuses: [],
powerUsage: [], powerUsage: [],
} }
}, },
methods: { methods: {
/** /**
* Callback for the infinite scroll IntersectionObserver. * Callback for the infinite scroll IntersectionObserver.
* *
* @param {Array} List of IntersectionObserverEntry objects. * @param {Array} List of IntersectionObserverEntry objects.
*/ */
infiniteScroll(entries) { infiniteScroll(entries) {
// Iterate over our possible elements. // Iterate over our possible elements.
entries.forEach(({target, isIntersecting}) => { entries.forEach(({target, isIntersecting}) => {
// If the element isn't visible, do nothing. // If the element isn't visible, do nothing.
if (!isIntersecting) { if (!isIntersecting) {
return; return;
} }
// Otherwise, remove the observer and update our pager properties. // Otherwise, remove the observer and update our pager properties.
// We need to increase the lastIndex for our filter by an amount // We need to increase the lastIndex for our filter by an amount
// equal to our number of entries per page. // equal to our number of entries per page.
this.observer.unobserve(target); this.observer.unobserve(target);
this.pager.lastIndex = Math.min( this.pager.lastIndex = Math.min(
this.pager.lastIndex + this.pager.perPage, this.pager.lastIndex + this.pager.perPage,
this.pager.totalRows this.pager.totalRows
); );
}); });
}, },
/** /**
* Click event to reset our filters. * Click event to reset our filters.
*/ */
resetFilters() { resetFilters() {
this.sortBy = 'name'; this.sortBy = 'name';
this.name = ''; this.name = '';
this.chakra = []; this.chakra = [];
this.recharge = []; this.recharge = [];
this.bonuses = []; this.bonuses = [];
this.powerUsage = []; this.powerUsage = [];
}, },
getBonuses(equipment) { getBonuses(equipment) {
let bonuses = {}; let bonuses = {};
for (let [prop, value] of Object.entries(equipment.system.attributes)) { for (let [prop, value] of Object.entries(equipment.system.attributes)) {
if (value.bonus) { if (value.bonus) {
bonuses[prop] = value.bonus bonuses[prop] = value.bonus
} }
else if (prop == 'attack') { else if (prop == 'attack') {
for (let [atkProp, atkValue] of Object.entries(value)) { for (let [atkProp, atkValue] of Object.entries(value)) {
if (atkValue.bonus) { if (atkValue.bonus) {
bonuses[atkProp] = atkValue.bonus; bonuses[atkProp] = atkValue.bonus;
} }
} }
} }
} }
return bonuses; return bonuses;
}, },
}, },
computed: { computed: {
bonusOptions() { bonusOptions() {
return [ return [
{ {
value: 'melee', value: 'melee',
dataProp: 'system.attributes.attack.melee.bonus', dataProp: 'system.attributes.attack.melee.bonus',
label: 'Melee', label: 'Melee',
}, },
{ {
value: 'ranged', value: 'ranged',
dataProp: 'system.attributes.attack.ranged.bonus', dataProp: 'system.attributes.attack.ranged.bonus',
label: 'Ranged', label: 'Ranged',
}, },
{ {
value: 'divine', value: 'divine',
dataProp: 'system.attributes.attack.divine.bonus', dataProp: 'system.attributes.attack.divine.bonus',
label: 'Divine', label: 'Divine',
}, },
{ {
value: 'arcane', value: 'arcane',
dataProp: 'system.attributes.attack.arcane.bonus', dataProp: 'system.attributes.attack.arcane.bonus',
label: 'Arcane', label: 'Arcane',
}, },
{ {
value: 'ac', value: 'ac',
dataProp: 'system.attributes.ac.bonus', dataProp: 'system.attributes.ac.bonus',
label: 'AC', label: 'AC',
}, },
{ {
value: 'md', value: 'md',
dataProp: 'system.attributes.md.bonus', dataProp: 'system.attributes.md.bonus',
label: 'PD', label: 'PD',
}, },
{ {
value: 'pd', value: 'pd',
dataProp: 'system.attributes.pd.bonus', dataProp: 'system.attributes.pd.bonus',
label: 'MD', label: 'MD',
}, },
{ {
value: 'hp', value: 'hp',
dataProp: 'system.attributes.hp.bonus', dataProp: 'system.attributes.hp.bonus',
label: 'HP', label: 'HP',
}, },
{ {
value: 'recoveries', value: 'recoveries',
dataProp: 'system.attributes.recoveries.bonus', dataProp: 'system.attributes.recoveries.bonus',
label: 'Recoveries', label: 'Recoveries',
}, },
{ {
value: 'save', value: 'save',
dataProp: 'system.attributes.save.bonus', dataProp: 'system.attributes.save.bonus',
label: 'Save', label: 'Save',
}, },
{ {
value: 'disengage', value: 'disengage',
dataProp: 'system.attributes.disengage.bonus', dataProp: 'system.attributes.disengage.bonus',
label: 'Disengage', label: 'Disengage',
}, },
]; ];
}, },
rechargeOptions() { rechargeOptions() {
return [ return [
{ {
value: 6, value: 6,
label: 'Easy (6+)', label: 'Easy (6+)',
next: 10, next: 10,
}, },
{ {
value: 11, value: 11,
label: 'Normal (11+)', label: 'Normal (11+)',
next: 15, next: 15,
}, },
{ {
value: 16, value: 16,
label: 'Hard (16+)', label: 'Hard (16+)',
next: 20, next: 20,
} }
] ]
}, },
chakraSlots() { chakraSlots() {
const result = {}; const result = {};
for (let [k,v] of Object.entries(CONFIG.ARCHMAGE.chakraSlots)) { for (let [k,v] of Object.entries(CONFIG.ARCHMAGE.chakraSlots)) {
result[k] = this.game.i18n.localize(`${v}Label`); result[k] = this.game.i18n.localize(`${v}Label`);
} }
return result; return result;
}, },
nightmode() { nightmode() {
return game.settings.get("archmage", "nightmode") ? 'nightmode' : ''; return game.settings.get("archmage", "nightmode") ? 'nightmode' : '';
}, },
entries() { entries() {
// Build our results array. Exit early if the length is 0. // Build our results array. Exit early if the length is 0.
let result = this.packIndex; let result = this.packIndex;
if (result.length < 1) { if (result.length < 1) {
this.pager.totalRows = 0; this.pager.totalRows = 0;
return []; return [];
} }
// Filter by name. // Filter by name.
if (this.name && this.name.length > 0) { if (this.name && this.name.length > 0) {
const name = this.name.toLocaleLowerCase(); const name = this.name.toLocaleLowerCase();
result = result.filter(entry => entry.name.toLocaleLowerCase().includes(name)); result = result.filter(entry => entry.name.toLocaleLowerCase().includes(name));
} }
// Handle multiselect filters, which use arrays as their values. // Handle multiselect filters, which use arrays as their values.
if (Array.isArray(this.chakra) && this.chakra.length > 0) { if (Array.isArray(this.chakra) && this.chakra.length > 0) {
// @todo chakra is misspelled in our data model. We need to fix that :( // @todo chakra is misspelled in our data model. We need to fix that :(
result = result.filter(entry => this.chakra.includes(entry.system?.chackra)); result = result.filter(entry => this.chakra.includes(entry.system?.chackra));
} }
if (Array.isArray(this.powerUsage) && this.powerUsage.length > 0) { if (Array.isArray(this.powerUsage) && this.powerUsage.length > 0) {
result = result.filter(entry => this.powerUsage.includes(entry.system?.powerUsage?.value ?? 'other')); result = result.filter(entry => this.powerUsage.includes(entry.system?.powerUsage?.value ?? 'other'));
} }
// Recharge. // Recharge.
if (Array.isArray(this.recharge) && this.recharge.length > 0) { if (Array.isArray(this.recharge) && this.recharge.length > 0) {
result = result.filter(entry => { result = result.filter(entry => {
let allowEntry = false; let allowEntry = false;
for (let rechargeOption of this.rechargeOptions) { for (let rechargeOption of this.rechargeOptions) {
if (this.recharge.includes(rechargeOption.value)) { if (this.recharge.includes(rechargeOption.value)) {
const rechargeEntry = parseInt(entry.system?.recharge?.value ?? 0); const rechargeEntry = parseInt(entry.system?.recharge?.value ?? 0);
if (rechargeEntry >= rechargeOption.value && rechargeEntry <= rechargeOption.next) { if (rechargeEntry >= rechargeOption.value && rechargeEntry <= rechargeOption.next) {
allowEntry = true; allowEntry = true;
break; break;
} }
} }
} }
return allowEntry; return allowEntry;
}); });
} }
// Bonus options. // Bonus options.
if (Array.isArray(this.bonuses) && this.bonuses.length > 0) { if (Array.isArray(this.bonuses) && this.bonuses.length > 0) {
result = result.filter(entry => { result = result.filter(entry => {
let allowEntry = false; let allowEntry = false;
for (let bonusOption of this.bonusOptions) { for (let bonusOption of this.bonusOptions) {
if (this.bonuses.includes(bonusOption.value)) { if (this.bonuses.includes(bonusOption.value)) {
const prop = this.foundry.utils.getProperty(entry, bonusOption.dataProp); const prop = this.foundry.utils.getProperty(entry, bonusOption.dataProp);
if (Number.isNumeric(prop) && prop !== 0) { if (Number.isNumeric(prop) && prop !== 0) {
allowEntry = true; allowEntry = true;
break; break;
} }
} }
} }
return allowEntry; return allowEntry;
}); });
} }
// Reflow pager. // Reflow pager.
if (result.length > this.pager.perPage) { if (result.length > this.pager.perPage) {
this.pager.totalRows = result.length; this.pager.totalRows = result.length;
if (this.pager.lastIndex == 0) { if (this.pager.lastIndex == 0) {
this.pager.lastIndex = this.pager.perPage - 1; this.pager.lastIndex = this.pager.perPage - 1;
} }
} }
else { else {
this.pager.totalRows = 0; this.pager.totalRows = 0;
} }
// Sort. // Sort.
result = result.sort((a, b) => { result = result.sort((a, b) => {
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}); });
// Sort. // Sort.
result = result.sort((a, b) => { result = result.sort((a, b) => {
switch (this.sortBy) { switch (this.sortBy) {
case 'chakra': case 'chakra':
return (a.system?.chackra ?? '').localeCompare((b.system?.chackra ?? '')); return (a.system?.chackra ?? '').localeCompare((b.system?.chackra ?? ''));
case 'usage': case 'usage':
return (a.system?.powerUsage?.value ?? '').localeCompare((b.system?.powerUsage?.value ?? '')); return (a.system?.powerUsage?.value ?? '').localeCompare((b.system?.powerUsage?.value ?? ''));
case 'recharge': case 'recharge':
return (a.system?.recharge?.value ?? 0) - (b.system?.recharge?.value ?? 0); return (a.system?.recharge?.value ?? 0) - (b.system?.recharge?.value ?? 0);
} }
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}); });
// Return results. // Return results.
return this.pager.totalRows > 0 return this.pager.totalRows > 0
? result.slice(this.pager.firstIndex, this.pager.lastIndex) ? result.slice(this.pager.firstIndex, this.pager.lastIndex)
: result; : result;
}, },
}, },
watch: {}, watch: {},
// Handle created hook. // Handle created hook.
async created() { async created() {
console.log("Creating compendium browser magic items tab..."); console.log("Creating compendium browser magic items tab...");
// Load the pack index with the fields we need. // Load the pack index with the fields we need.
getPackIndex([ getPackIndex([
'archmage.srd-magic-items-armors', 'archmage.srd-magic-items-armors',
'archmage.srd-magic-items-arrows', 'archmage.srd-magic-items-arrows',
'archmage.srd-magic-items-belts', 'archmage.srd-magic-items-belts',
'archmage.srd-magic-items-books', 'archmage.srd-magic-items-books',
'archmage.srd-magic-items-boots', 'archmage.srd-magic-items-boots',
'archmage.srd-magic-items-chuul-symbiotes', 'archmage.srd-magic-items-chuul-symbiotes',
'archmage.srd-magic-items-cloaks', 'archmage.srd-magic-items-cloaks',
'archmage.srd-magic-items-consumables', 'archmage.srd-magic-items-consumables',
'archmage.srd-magic-items-cursed', 'archmage.srd-magic-items-cursed',
'archmage.srd-magic-items-gloves', 'archmage.srd-magic-items-gloves',
'archmage.srd-magic-items-helmets', 'archmage.srd-magic-items-helmets',
'archmage.srd-magic-items-necklaces', 'archmage.srd-magic-items-necklaces',
'archmage.srd-magic-items-rings', 'archmage.srd-magic-items-rings',
'archmage.srd-magic-items-shields', 'archmage.srd-magic-items-shields',
'archmage.srd-magic-items-staffs', 'archmage.srd-magic-items-staffs',
'archmage.srd-magic-items-symbols', 'archmage.srd-magic-items-symbols',
'archmage.srd-magic-items-wands', 'archmage.srd-magic-items-wands',
'archmage.srd-magic-items-weapons', 'archmage.srd-magic-items-weapons',
'archmage.srd-magic-items-wondrous-items', 'archmage.srd-magic-items-wondrous-items',
], [ ], [
'system.chackra', 'system.chackra',
'system.recharge.value', 'system.recharge.value',
'system.powerUsage.value', 'system.powerUsage.value',
'system.attributes.attack', 'system.attributes.attack',
'system.attributes.ac', 'system.attributes.ac',
'system.attributes.md', 'system.attributes.md',
'system.attributes.pd', 'system.attributes.pd',
'system.attributes.hp', 'system.attributes.hp',
'system.attributes.recoveries', 'system.attributes.recoveries',
'system.attributes.save', 'system.attributes.save',
'system.attributes.disengage', 'system.attributes.disengage',
]).then(packIndex => { ]).then(packIndex => {
this.packIndex = packIndex; this.packIndex = packIndex;
this.loaded = true; this.loaded = true;
}); });
// Create our intersection observer for infinite scroll. // Create our intersection observer for infinite scroll.
this.observer = new IntersectionObserver(this.infiniteScroll, { this.observer = new IntersectionObserver(this.infiniteScroll, {
root: this.$el, root: this.$el,
threshold: 0.5, threshold: 0.5,
}); });
}, },
// Handle mounted hook. // Handle mounted hook.
async mounted() { async mounted() {
console.log("Compendium browser magic items tab mounted."); console.log("Compendium browser magic items tab mounted.");
// Note that our tab has beened opened so that it won't de-render later. // Note that our tab has beened opened so that it won't de-render later.
this.tab.opened = true; this.tab.opened = true;
// Adjust our observers whenever the results of the compendium browser // Adjust our observers whenever the results of the compendium browser
// are updated. // are updated.
onUpdated(() => { onUpdated(() => {
const target = document.querySelector('.compendium-browser-items .compendium-browser-row-observe'); const target = document.querySelector('.compendium-browser-items .compendium-browser-row-observe');
if (target) { if (target) {
this.observer.observe(target); this.observer.observe(target);
} }
}); });
}, },
// Handle the unmount hook. // Handle the unmount hook.
async beforeUnmount() { async beforeUnmount() {
// Handle the unmount hook. // Handle the unmount hook.
this.observer.disconnect(); this.observer.disconnect();
} }
} }
</script> </script>
<style lang="scss"> <style lang="scss">
@import "@vueform/slider/themes/default.css"; @import "@vueform/slider/themes/default.css";
@import "@vueform/multiselect/themes/default.css"; @import "@vueform/multiselect/themes/default.css";
</style> </style>

View File

@ -1,118 +1,118 @@
<template> <template>
<section class="section section--sidebar flexcol filters"> <section class="section section--sidebar flexcol filters">
<!-- Sort. --> <!-- Sort. -->
<div class="unit unit--input"> <div class="unit unit--input">
<label for="compendiumBrowser.sort" class="unit-title">{{ localize('ARCHMAGE.sort') }}</label> <label for="compendiumBrowser.sort" class="unit-title">{{ localize('ARCHMAGE.sort') }}</label>
<select class="sort" name="compendiumBrowser.sort" v-model="sortBy"> <select class="sort" name="compendiumBrowser.sort" v-model="sortBy">
<option v-for="(option, index) in sortOptions" :key="index" :value="option.value">{{ option.label }}</option> <option v-for="(option, index) in sortOptions" :key="index" :value="option.value">{{ option.label }}</option>
</select> </select>
</div> </div>
<!-- Level range slider. --> <!-- Level range slider. -->
<div class="unit unit--input"> <div class="unit unit--input">
<label class="unit-title" for="compendiumBrowser.powerLevel">{{ localize('ARCHMAGE.level') }}</label> <label class="unit-title" for="compendiumBrowser.powerLevel">{{ localize('ARCHMAGE.level') }}</label>
<div class="level-range flexrow"> <div class="level-range flexrow">
<div class="level-label"><span>{{ levelRange[0] }}</span><span v-if="levelRange[0] !== levelRange[1]"> - {{ levelRange[1] }}</span></div> <div class="level-label"><span>{{ levelRange[0] }}</span><span v-if="levelRange[0] !== levelRange[1]"> - {{ levelRange[1] }}</span></div>
<div class="level-input slider-wrapper flexrow"> <div class="level-input slider-wrapper flexrow">
<Slider v-model="levelRange" :min="1" :max="10" :tooltips="false"/> <Slider v-model="levelRange" :min="1" :max="10" :tooltips="false"/>
</div> </div>
</div> </div>
</div> </div>
<!-- Filter name. --> <!-- Filter name. -->
<div class="unit unit--input"> <div class="unit unit--input">
<label class="unit-title" for="compendiumBrowser.powerName">{{ localize('ARCHMAGE.name') }}</label> <label class="unit-title" for="compendiumBrowser.powerName">{{ localize('ARCHMAGE.name') }}</label>
<input type="text" name="compendiumBrowser.powerName" v-model="name" placeholder="Fireball"/> <input type="text" name="compendiumBrowser.powerName" v-model="name" placeholder="Fireball"/>
</div> </div>
<!-- Filter source. --> <!-- Filter source. -->
<div class="unit unit--input"> <div class="unit unit--input">
<label class="unit-title" for="compendiumBrowser.powerSourceName">{{ localize('ARCHMAGE.CHAT.powerSourceName') }}</label> <label class="unit-title" for="compendiumBrowser.powerSourceName">{{ localize('ARCHMAGE.CHAT.powerSourceName') }}</label>
<input type="text" name="compendiumBrowser.powerSourceName" v-model="powerSourceName" placeholder="Fighter"/> <input type="text" name="compendiumBrowser.powerSourceName" v-model="powerSourceName" placeholder="Fighter"/>
</div> </div>
<!-- Filter type. --> <!-- Filter type. -->
<div class="unit unit--input"> <div class="unit unit--input">
<label class="unit-title" for="compendiumBrowser.powerType">{{ localize('ARCHMAGE.type') }}</label> <label class="unit-title" for="compendiumBrowser.powerType">{{ localize('ARCHMAGE.type') }}</label>
<Multiselect <Multiselect
v-model="powerType" v-model="powerType"
mode="tags" mode="tags"
:searchable="false" :searchable="false"
:create-option="false" :create-option="false"
:options="CONFIG.ARCHMAGE.powerTypes" :options="CONFIG.ARCHMAGE.powerTypes"
/> />
</div> </div>
<!-- Filter usage. --> <!-- Filter usage. -->
<div class="unit unit--input"> <div class="unit unit--input">
<label class="unit-title" for="compendiumBrowser.powerUsage">{{ localize('ARCHMAGE.CHAT.powerUsage') }}</label> <label class="unit-title" for="compendiumBrowser.powerUsage">{{ localize('ARCHMAGE.CHAT.powerUsage') }}</label>
<Multiselect <Multiselect
v-model="powerUsage" v-model="powerUsage"
mode="tags" mode="tags"
:searchable="false" :searchable="false"
:create-option="false" :create-option="false"
:options="CONFIG.ARCHMAGE.powerUsages" :options="CONFIG.ARCHMAGE.powerUsages"
/> />
</div> </div>
<!-- Filter action. --> <!-- Filter action. -->
<div class="unit unit--input"> <div class="unit unit--input">
<label class="unit-title" for="compendiumBrowser.actionType">{{ localize('ARCHMAGE.action') }}</label> <label class="unit-title" for="compendiumBrowser.actionType">{{ localize('ARCHMAGE.action') }}</label>
<Multiselect <Multiselect
v-model="actionType" v-model="actionType"
mode="tags" mode="tags"
:searchable="false" :searchable="false"
:create-option="false" :create-option="false"
:options="CONFIG.ARCHMAGE.actionTypes" :options="CONFIG.ARCHMAGE.actionTypes"
/> />
</div> </div>
<!-- Filter trigger. --> <!-- Filter trigger. -->
<div class="unit unit--input"> <div class="unit unit--input">
<label class="unit-title" for="compendiumBrowser.trigger">{{ localize('ARCHMAGE.CHAT.trigger') }}</label> <label class="unit-title" for="compendiumBrowser.trigger">{{ localize('ARCHMAGE.CHAT.trigger') }}</label>
<input type="text" name="compendiumBrowser.trigger" v-model="trigger" placeholder="Even hit"/> <input type="text" name="compendiumBrowser.trigger" v-model="trigger" placeholder="Even hit"/>
</div> </div>
<!-- Reset. --> <!-- Reset. -->
<div class="unit unit--input flexrow"> <div class="unit unit--input flexrow">
<button type="reset" @click="resetFilters()">{{ localize('Reset') }}</button> <button type="reset" @click="resetFilters()">{{ localize('Reset') }}</button>
</div> </div>
</section> </section>
<section class="section section--no-overflow"> <section class="section section--no-overflow">
<!-- Power results. --> <!-- Power results. -->
<section class="section section--powers section--main flexcol"> <section class="section section--powers section--main flexcol">
<ul v-if="loaded" class="compendium-browser-results compendium-browser-powers" :key="escalation"> <ul v-if="loaded" class="compendium-browser-results compendium-browser-powers" :key="escalation">
<!-- Individual powers entries. --> <!-- Individual powers entries. -->
<li v-for="(entry, entryKey) in entries" :key="entryKey" :class="`power-summary ${powerUsageClass(entry)} compendium-browser-row${entryKey >= pager.lastIndex - 1 && entryKey < pager.totalRows - 1 ? ' compendium-browser-row-observe': ''} flexrow document item`" :data-document-id="entry._id" @click="openDocument(entry.uuid, 'Item')" :data-tooltip="CONFIG.ARCHMAGE.powerUsages[entry.system.powerUsage.value] ?? ''" data-tooltip-direction="RIGHT"> <li v-for="(entry, entryKey) in entries" :key="entryKey" :class="`power-summary ${powerUsageClass(entry)} compendium-browser-row${entryKey >= pager.lastIndex - 1 && entryKey < pager.totalRows - 1 ? ' compendium-browser-row-observe': ''} flexrow document item`" :data-document-id="entry._id" @click="openDocument(entry.uuid, 'Item')" :data-tooltip="CONFIG.ARCHMAGE.powerUsages[entry.system.powerUsage.value] ?? ''" data-tooltip-direction="RIGHT">
<!-- Both the image and title have drag events. These are primarily separated so that --> <!-- Both the image and title have drag events. These are primarily separated so that -->
<!-- if a user drags the token, it will only show the token as their drag preview. --> <!-- if a user drags the token, it will only show the token as their drag preview. -->
<img :src="entry.img" @dragstart="startDrag($event, entry, 'Item')" draggable="true"/> <img :src="entry.img" @dragstart="startDrag($event, entry, 'Item')" draggable="true"/>
<div class="flexcol power-contents" @dragstart="startDrag($event, entry, 'Item')" draggable="true"> <div class="flexcol power-contents" @dragstart="startDrag($event, entry, 'Item')" draggable="true">
<!-- First row is the title and class/source. --> <!-- First row is the title and class/source. -->
<div class="power-title-wrapper"> <div class="power-title-wrapper">
<strong class="power-title"><span v-if="entry?.system?.powerLevel?.value">[{{ entry.system.powerLevel.value }}]</span> {{ entry?.name }}</strong> <strong class="power-title"><span v-if="entry?.system?.powerLevel?.value">[{{ entry.system.powerLevel.value }}]</span> {{ entry?.name }}</strong>
<strong class="power-source" v-if="entry.system.powerSourceName.value">{{ entry.system.powerSourceName.value }}</strong> <strong class="power-source" v-if="entry.system.powerSourceName.value">{{ entry.system.powerSourceName.value }}</strong>
</div> </div>
<!-- Second row is supplemental info. --> <!-- Second row is supplemental info. -->
<div class="grid power-grid"> <div class="grid power-grid">
<div v-if="entry.system.trigger.value" class="power-trigger"><strong>Trigger:</strong> {{ entry.system.trigger.value }}</div> <div v-if="entry.system.trigger.value" class="power-trigger"><strong>Trigger:</strong> {{ entry.system.trigger.value }}</div>
<div class="power-feat-pips" :data-tooltip="localize('ARCHMAGE.feats')" v-if="hasFeats(entry)"> <div class="power-feat-pips" :data-tooltip="localize('ARCHMAGE.feats')" v-if="hasFeats(entry)">
<ul class="feat-pips"> <ul class="feat-pips">
<li v-for="(feat, tier) in filterFeats(entry.system.feats)" :key="tier" :class="`feat-pip active`"></li> <li v-for="(feat, tier) in filterFeats(entry.system.feats)" :key="tier" :class="`feat-pip active`"></li>
</ul> </ul>
</div> </div>
<div class="power-recharge" :data-tooltip="localize('ARCHMAGE.recharge')" v-if="entry.system.recharge.value && entry.system.powerUsage.value == 'recharge'">{{Number(entry.system.recharge.value) || 16}}+</div> <div class="power-recharge" :data-tooltip="localize('ARCHMAGE.recharge')" v-if="entry.system.recharge.value && entry.system.powerUsage.value == 'recharge'">{{Number(entry.system.recharge.value) || 16}}+</div>
<div class="power-action" :data-tooltip="localize('ARCHMAGE.CHAT.actionTYpe')" v-if="entry.system.actionType.value">{{getActionShort(entry.system.actionType.value)}}</div> <div class="power-action" :data-tooltip="localize('ARCHMAGE.CHAT.actionTYpe')" v-if="entry.system.actionType.value">{{getActionShort(entry.system.actionType.value)}}</div>
</div> </div>
</div> </div>
</li> </li>
</ul> </ul>
<div v-else class="compendium-browser-loading"><p><i class="fas fa-circle-notch fa-spin"></i>Please wait, loading...</p></div> <div v-else class="compendium-browser-loading"><p><i class="fas fa-circle-notch fa-spin"></i>Please wait, loading...</p></div>
</section> </section>
</section> </section>
</template> </template>
<script> <script>
@ -123,298 +123,298 @@ import Slider from '@vueform/slider';
import Multiselect from '@vueform/multiselect'; import Multiselect from '@vueform/multiselect';
// Helper methods. // Helper methods.
import { import {
getPackIndex, getPackIndex,
localize, localize,
openDocument, openDocument,
startDrag, startDrag,
} from '@/methods/Helpers.js'; } from '@/methods/Helpers.js';
export default { export default {
name: 'CompendiumBrowserPowers', name: 'CompendiumBrowserPowers',
props: ['tab', 'escalation'], props: ['tab', 'escalation'],
// Imported components that need to be available in the <template> // Imported components that need to be available in the <template>
components: { components: {
Slider, Slider,
Multiselect Multiselect
}, },
setup() { setup() {
return { return {
// Imported methods that need to be available in the <template> // Imported methods that need to be available in the <template>
localize, localize,
openDocument, openDocument,
startDrag, startDrag,
// Foundry base props and methods. // Foundry base props and methods.
CONFIG, CONFIG,
game, game,
getDocumentClass getDocumentClass
} }
}, },
data() { data() {
return { return {
// Props used for infinited scroll and pagination. // Props used for infinited scroll and pagination.
observer: null, observer: null,
loaded: false, loaded: false,
pager: { pager: {
perPage: 50, perPage: 50,
firstIndex: 0, firstIndex: 0,
lastIndex: 50, lastIndex: 50,
totalRows: 0, totalRows: 0,
}, },
// Sorting. // Sorting.
sortBy: 'level', sortBy: 'level',
sortOptions: [ sortOptions: [
{ value: 'level', label: game.i18n.localize('ARCHMAGE.level') }, { value: 'level', label: game.i18n.localize('ARCHMAGE.level') },
{ value: 'name', label: game.i18n.localize('ARCHMAGE.name') }, { value: 'name', label: game.i18n.localize('ARCHMAGE.name') },
{ value: 'source', label: game.i18n.localize('ARCHMAGE.CHAT.powerSourceName') }, { value: 'source', label: game.i18n.localize('ARCHMAGE.CHAT.powerSourceName') },
{ value: 'type', label: game.i18n.localize('ARCHMAGE.type') }, { value: 'type', label: game.i18n.localize('ARCHMAGE.type') },
{ value: 'usage', label: game.i18n.localize('ARCHMAGE.GROUPS.powerUsage') }, { value: 'usage', label: game.i18n.localize('ARCHMAGE.GROUPS.powerUsage') },
{ value: 'action', label: game.i18n.localize('ARCHMAGE.action') }, { value: 'action', label: game.i18n.localize('ARCHMAGE.action') },
], ],
// Our list of pseudo documents returned from the compendium. // Our list of pseudo documents returned from the compendium.
packIndex: [], packIndex: [],
// Filters. // Filters.
name: '', name: '',
levelRange: [1, 10], levelRange: [1, 10],
actionType: [], actionType: [],
powerType: [], powerType: [],
powerSourceName: '', powerSourceName: '',
powerUsage: [], powerUsage: [],
trigger: '', trigger: '',
} }
}, },
methods: { methods: {
/** /**
* Callback for the infinite scroll IntersectionObserver. * Callback for the infinite scroll IntersectionObserver.
* *
* @param {Array} List of IntersectionObserverEntry objects. * @param {Array} List of IntersectionObserverEntry objects.
*/ */
infiniteScroll(entries) { infiniteScroll(entries) {
entries.forEach(({target, isIntersecting}) => { entries.forEach(({target, isIntersecting}) => {
// If the element isn't visible, do nothing. // If the element isn't visible, do nothing.
if (!isIntersecting) { if (!isIntersecting) {
return; return;
} }
// Otherwise, remove the observer and update our pager properties. // Otherwise, remove the observer and update our pager properties.
// We need to increase the lastIndex for our filter by an amount // We need to increase the lastIndex for our filter by an amount
// equal to our number of entries per page. // equal to our number of entries per page.
this.observer.unobserve(target); this.observer.unobserve(target);
this.pager.lastIndex = Math.min(this.pager.lastIndex + this.pager.perPage, this.pager.totalRows); this.pager.lastIndex = Math.min(this.pager.lastIndex + this.pager.perPage, this.pager.totalRows);
}); });
}, },
/** /**
* Click event to reset our filters. * Click event to reset our filters.
*/ */
resetFilters() { resetFilters() {
this.sortBy = 'level'; this.sortBy = 'level';
this.name = ''; this.name = '';
this.levelRange = [1, 10]; this.levelRange = [1, 10];
this.actionType = []; this.actionType = [];
this.powerType = []; this.powerType = [];
this.powerSourceName = ''; this.powerSourceName = '';
this.powerUsage = []; this.powerUsage = [];
this.trigger = ''; this.trigger = '';
}, },
/** /**
* Retrieve the abbreviated action type, such as 'STD' or 'QCK'. * Retrieve the abbreviated action type, such as 'STD' or 'QCK'.
*/ */
getActionShort(actionType) { getActionShort(actionType) {
if (CONFIG.ARCHMAGE.actionTypesShort[actionType]) { if (CONFIG.ARCHMAGE.actionTypesShort[actionType]) {
return CONFIG.ARCHMAGE.actionTypesShort[actionType]; return CONFIG.ARCHMAGE.actionTypesShort[actionType];
} }
return CONFIG.ARCHMAGE.actionTypesShort['standard']; return CONFIG.ARCHMAGE.actionTypesShort['standard'];
}, },
/** /**
* Compute CSS class to assign based on special usage * Compute CSS class to assign based on special usage
*/ */
powerUsageClass(power) { powerUsageClass(power) {
let use = power.system.powerUsage.value ? power.system.powerUsage.value : 'other'; let use = power.system.powerUsage.value ? power.system.powerUsage.value : 'other';
if (['daily', 'daily-desperate'].includes(use)) use = 'daily'; if (['daily', 'daily-desperate'].includes(use)) use = 'daily';
else if (use == 'cyclic') { else if (use == 'cyclic') {
if (this.escalation > 0 if (this.escalation > 0
&& this.escalation % 2 == 0) { && this.escalation % 2 == 0) {
use = 'at-will cyclic'; use = 'at-will cyclic';
} else use = 'once-per-battle cyclic'; } else use = 'once-per-battle cyclic';
} }
return use; return use;
}, },
/** /**
* Determine if this power has one or more feats. * Determine if this power has one or more feats.
*/ */
hasFeats(power) { hasFeats(power) {
let hasFeats = false; let hasFeats = false;
if (power && power.system && power.system.feats) { if (power && power.system && power.system.feats) {
for (let [id, feat] of Object.entries(power.system.feats)) { for (let [id, feat] of Object.entries(power.system.feats)) {
if (feat.description.value || feat.isActive.value) { if (feat.description.value || feat.isActive.value) {
hasFeats = true; hasFeats = true;
break; break;
} }
} }
} }
return hasFeats; return hasFeats;
}, },
/** /**
* Filter empty feats * Filter empty feats
*/ */
filterFeats(feats) { filterFeats(feats) {
if (!feats) return {}; if (!feats) return {};
let res = {}; let res = {};
for (let [index, feat] of Object.entries(feats)) { for (let [index, feat] of Object.entries(feats)) {
if (feat.description.value) res[index] = feat; if (feat.description.value) res[index] = feat;
} }
return res; return res;
}, },
}, },
computed: { computed: {
nightmode() { nightmode() {
return game.settings.get("archmage", "nightmode") ? 'nightmode' : ''; return game.settings.get("archmage", "nightmode") ? 'nightmode' : '';
}, },
entries() { entries() {
// Build our results array. Exit early if the length is 0. // Build our results array. Exit early if the length is 0.
let result = this.packIndex; let result = this.packIndex;
if (result.length < 1) { if (result.length < 1) {
this.pager.totalRows = 0; this.pager.totalRows = 0;
return []; return [];
} }
// Filter by name. // Filter by name.
if (this.name && this.name.length > 0) { if (this.name && this.name.length > 0) {
const name = this.name.toLocaleLowerCase(); const name = this.name.toLocaleLowerCase();
result = result.filter(entry => entry.name.toLocaleLowerCase().includes(name)); result = result.filter(entry => entry.name.toLocaleLowerCase().includes(name));
} }
// Filter by level. // Filter by level.
if (this.levelRange.length == 2) { if (this.levelRange.length == 2) {
result = result.filter(entry => result = result.filter(entry =>
entry.system.powerLevel.value >= this.levelRange[0] && entry.system.powerLevel.value >= this.levelRange[0] &&
entry.system.powerLevel.value <= this.levelRange[1] entry.system.powerLevel.value <= this.levelRange[1]
); );
} }
// Filter by power source. // Filter by power source.
if (this.powerSourceName && this.powerSourceName.length > 0) { if (this.powerSourceName && this.powerSourceName.length > 0) {
const name = this.powerSourceName.toLocaleLowerCase(); const name = this.powerSourceName.toLocaleLowerCase();
result = result.filter(entry => entry.system.powerSourceName.value.toLocaleLowerCase().includes(name)); result = result.filter(entry => entry.system.powerSourceName.value.toLocaleLowerCase().includes(name));
} }
// Filter by triger. // Filter by triger.
if (this.trigger && this.trigger.length > 0) { if (this.trigger && this.trigger.length > 0) {
const name = this.trigger.toLocaleLowerCase(); const name = this.trigger.toLocaleLowerCase();
result = result.filter(entry => entry.system.trigger.value && entry.system.trigger.value.toLocaleLowerCase().includes(name)); result = result.filter(entry => entry.system.trigger.value && entry.system.trigger.value.toLocaleLowerCase().includes(name));
} }
// Handle multiselect filters, which use arrays as their values. // Handle multiselect filters, which use arrays as their values.
if (Array.isArray(this.powerType) && this.powerType.length > 0) { if (Array.isArray(this.powerType) && this.powerType.length > 0) {
result = result.filter(entry => this.powerType.includes(entry.system.powerType.value)); result = result.filter(entry => this.powerType.includes(entry.system.powerType.value));
} }
if (Array.isArray(this.powerUsage) && this.powerUsage.length > 0) { if (Array.isArray(this.powerUsage) && this.powerUsage.length > 0) {
result = result.filter(entry => this.powerUsage.includes(entry.system.powerUsage.value)); result = result.filter(entry => this.powerUsage.includes(entry.system.powerUsage.value));
} }
if (Array.isArray(this.actionType) && this.actionType.length > 0) { if (Array.isArray(this.actionType) && this.actionType.length > 0) {
result = result.filter(entry => this.actionType.includes(entry.system.actionType.value)); result = result.filter(entry => this.actionType.includes(entry.system.actionType.value));
} }
// Reflow pager. // Reflow pager.
if (result.length > this.pager.perPage) { if (result.length > this.pager.perPage) {
this.pager.totalRows = result.length; this.pager.totalRows = result.length;
if (this.pager.lastIndex == 0) { if (this.pager.lastIndex == 0) {
this.pager.lastIndex = this.pager.perPage - 1; this.pager.lastIndex = this.pager.perPage - 1;
} }
} }
else { else {
this.pager.totalRows = 0; this.pager.totalRows = 0;
} }
// Sort. // Sort.
result = result.sort((a, b) => { result = result.sort((a, b) => {
switch (this.sortBy) { switch (this.sortBy) {
case 'name': case 'name':
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
case 'source': case 'source':
return a.system?.powerSourceName?.value.localeCompare(b.system?.powerSourceName?.value); return a.system?.powerSourceName?.value.localeCompare(b.system?.powerSourceName?.value);
case 'type': case 'type':
return a.system?.powerType?.value.localeCompare(b.system?.powerType?.value); return a.system?.powerType?.value.localeCompare(b.system?.powerType?.value);
case 'usage': case 'usage':
return a.system?.powerUsage?.value.localeCompare(b.system?.powerUsage?.value); return a.system?.powerUsage?.value.localeCompare(b.system?.powerUsage?.value);
case 'action': case 'action':
return a.system?.actionType?.value.localeCompare(b.system?.actionType?.value); return a.system?.actionType?.value.localeCompare(b.system?.actionType?.value);
} }
return a.system.powerLevel.value - b.system.powerLevel.value; return a.system.powerLevel.value - b.system.powerLevel.value;
}); });
// Return results. // Return results.
return this.pager.totalRows > 0 return this.pager.totalRows > 0
? result.slice(this.pager.firstIndex, this.pager.lastIndex) ? result.slice(this.pager.firstIndex, this.pager.lastIndex)
: result; : result;
}, },
}, },
watch: {}, watch: {},
// Handle created hook. // Handle created hook.
async created() { async created() {
console.log("Creating compendium browser powers tab..."); console.log("Creating compendium browser powers tab...");
// Load the pack index with the fields we need. // Load the pack index with the fields we need.
getPackIndex([ getPackIndex([
'archmage.barbarian', 'archmage.barbarian',
'archmage.bard', 'archmage.bard',
'archmage.cleric', 'archmage.cleric',
'archmage.fighter', 'archmage.fighter',
'archmage.paladin', 'archmage.paladin',
'archmage.ranger', 'archmage.ranger',
'archmage.animal-companion', 'archmage.animal-companion',
'archmage.rogue', 'archmage.rogue',
'archmage.sorcerer', 'archmage.sorcerer',
'archmage.wizard', 'archmage.wizard',
'archmage.chaosmage', 'archmage.chaosmage',
'archmage.commander', 'archmage.commander',
'archmage.druid', 'archmage.druid',
'archmage.monk', 'archmage.monk',
'archmage.necromancer', 'archmage.necromancer',
'archmage.occultist', 'archmage.occultist',
], [ ], [
'system.powerSourceName.value', 'system.powerSourceName.value',
'system.powerType.value', 'system.powerType.value',
'system.powerLevel.value', 'system.powerLevel.value',
'system.powerUsage.value', 'system.powerUsage.value',
'system.actionType.value', 'system.actionType.value',
'system.recharge.value', 'system.recharge.value',
'system.trigger.value', 'system.trigger.value',
'system.feats' 'system.feats'
]).then(packIndex => { ]).then(packIndex => {
this.packIndex = packIndex; this.packIndex = packIndex;
this.loaded = true; this.loaded = true;
}); });
// Create our intersection observer for infinite scroll. // Create our intersection observer for infinite scroll.
this.observer = new IntersectionObserver(this.infiniteScroll, { this.observer = new IntersectionObserver(this.infiniteScroll, {
root: this.$el, root: this.$el,
threshold: 0.1, threshold: 0.1,
}); });
}, },
// Handle mounted hook. // Handle mounted hook.
async mounted() { async mounted() {
console.log("Compendium browser powers tab mounted."); console.log("Compendium browser powers tab mounted.");
// Note that our tab has beened opened so that it won't de-render later. // Note that our tab has beened opened so that it won't de-render later.
this.tab.opened = true; this.tab.opened = true;
// Adjust our observers whenever the results of the compendium browser // Adjust our observers whenever the results of the compendium browser
// are updated. // are updated.
onUpdated(() => { onUpdated(() => {
const target = document.querySelector('.compendium-browser-powers .compendium-browser-row-observe'); const target = document.querySelector('.compendium-browser-powers .compendium-browser-row-observe');
if (target) { if (target) {
this.observer.observe(target); this.observer.observe(target);
} }
}); });
}, },
// Handle the unmount hook. // Handle the unmount hook.
async beforeUnmount() { async beforeUnmount() {
// Handle the unmount hook. // Handle the unmount hook.
this.observer.disconnect(); this.observer.disconnect();
} }
} }
</script> </script>
<style lang="scss"> <style lang="scss">
@import "@vueform/slider/themes/default.css"; @import "@vueform/slider/themes/default.css";
@import "@vueform/multiselect/themes/default.css"; @import "@vueform/multiselect/themes/default.css";
</style> </style>

View File

@ -0,0 +1,21 @@
<template>
<h1>
<!-- Use <slot> to pass in content wrapped by this component's tag. -->
<slot></slot>
</h1>
</template>
<script>
export default {
name: 'Stub',
props: ['context']
}
</script>
<style lang="less" scoped>
// Scoped styles have special identifiers that prevent them
// from affecting anything outside of this component.
h1 {
color: red;
}
</style>

View File

@ -1,16 +0,0 @@
<template>
<h1><slot></slot></h1>
</template>
<script>
export default {
name: 'Stub',
props: ['context']
}
</script>
<style lang="less" scoped>
h1 {
color: red;
}
</style>