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") {
compendiumButton = `
<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="items"><i class="fas fa-book"></i>${game.i18n.localize('CMPBrowser.Tab.ItemBrowser')}</button>
</div>`;
<div class="flexrow">
<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-suitcase"></i>${game.i18n.localize('CMPBrowser.Tab.ItemBrowser')}</button>
</div>`;
}
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.
htmlElement.querySelector(".directory-footer").insertAdjacentHTML("beforeend", compendiumButton);

View File

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

View File

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

View File

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