diff --git a/package-lock.json b/package-lock.json index 7ccc8b0dbb..abd3451b5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,9 @@ "version": "0.1.0", "dependencies": { "@popperjs/core": "^2.11.6", + "@types/pako": "^2.0.0", "bootstrap": "^5.3.0", - "pako": "^1.0.11", + "pako": "^2.0.4", "tippy.js": "^6.3.7", "tsx-vanilla": "^1.0.0" }, @@ -1144,6 +1145,11 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/pako": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.0.tgz", + "integrity": "sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==" + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -4215,8 +4221,9 @@ } }, "node_modules/pako": { - "version": "1.0.11", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==" }, "node_modules/parent-module": { "version": "1.0.1", diff --git a/package.json b/package.json index bbb72942a7..b68690a072 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,9 @@ }, "dependencies": { "@popperjs/core": "^2.11.6", + "@types/pako": "^2.0.0", "bootstrap": "^5.3.0", - "pako": "^1.0.11", + "pako": "^2.0.4", "tippy.js": "^6.3.7", "tsx-vanilla": "^1.0.0" }, diff --git a/ui/core/components/encounter_picker.ts b/ui/core/components/encounter_picker.ts index 21a7f93b92..cd4f231b92 100644 --- a/ui/core/components/encounter_picker.ts +++ b/ui/core/components/encounter_picker.ts @@ -128,10 +128,11 @@ export class EncounterPicker extends Component { makeTargetInputsPicker(this.rootElem, modEncounter, 0); + const advancedModal = new AdvancedEncounterModal(simUI.rootElem, simUI, modEncounter); const advancedButton = document.createElement('button'); advancedButton.classList.add('advanced-button', 'btn', 'btn-primary'); advancedButton.textContent = 'Advanced'; - advancedButton.addEventListener('click', () => new AdvancedEncounterModal(simUI.rootElem, simUI, modEncounter)); + advancedButton.addEventListener('click', () => advancedModal.open()); this.rootElem.appendChild(advancedButton); }); } diff --git a/ui/core/components/exporters.tsx b/ui/core/components/exporters.tsx index c9c682b356..58435d460d 100644 --- a/ui/core/components/exporters.tsx +++ b/ui/core/components/exporters.tsx @@ -1,3 +1,4 @@ +import * as pako from 'pako'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { element, ref } from 'tsx-vanilla'; @@ -17,8 +18,6 @@ import { BooleanPicker } from './boolean_picker'; import { CopyButton } from './copy_button'; import { IndividualLinkImporter, IndividualWowheadGearPlannerImporter } from './importers'; -declare let pako: any; - interface ExporterOptions { title: string; header?: boolean; @@ -150,7 +149,10 @@ export class IndividualLinkExporter extends Exporter { changedEvent: () => this.changedEvent, }); }); + } + open() { + super.open(); this.init(); } @@ -190,6 +192,10 @@ export class IndividualJsonExporter extends Exporter { constructor(parent: HTMLElement, simUI: IndividualSimUI) { super(parent, simUI, { title: 'JSON Export', allowDownload: true }); this.simUI = simUI; + } + + open() { + super.open(); this.init(); } @@ -218,14 +224,18 @@ export class IndividualWowheadGearPlannerExporter extends constructor(parent: HTMLElement, simUI: IndividualSimUI) { super(parent, simUI, { title: 'Wowhead Export', allowDownload: true }); this.simUI = simUI; + } + + open() { + super.open(); this.init(); } getData(): string { const player = this.simUI.player; - const classStr = player.getPlayerClass().friendlyName.replaceAll(/\s/, '-').toLowerCase(); - const raceStr = raceNames.get(player.getRace())!.replaceAll(/\s/, '-').toLowerCase(); + const classStr = player.getPlayerClass().friendlyName.replaceAll(/\s/g, '-').toLowerCase(); + const raceStr = raceNames.get(player.getRace())!.replaceAll(/\s/g, '-').toLowerCase(); const url = `https://www.wowhead.com/cata/gear-planner/${classStr}/${raceStr}/`; // See comments on the importer for how the binary formatting is structured. @@ -327,6 +337,10 @@ export class Individual80UEPExporter extends Exporter { constructor(parent: HTMLElement, simUI: IndividualSimUI) { super(parent, simUI, { title: '80Upgrades EP Export', allowDownload: true }); this.simUI = simUI; + } + + open() { + super.open(); this.init(); } @@ -415,6 +429,10 @@ export class IndividualPawnEPExporter extends Exporter { constructor(parent: HTMLElement, simUI: IndividualSimUI) { super(parent, simUI, { title: 'Pawn EP Export', allowDownload: true }); this.simUI = simUI; + } + + open() { + super.open(); this.init(); } @@ -504,6 +522,10 @@ export class IndividualCLIExporter extends Exporter { constructor(parent: HTMLElement, simUI: IndividualSimUI) { super(parent, simUI, { title: 'CLI Export', allowDownload: true }); this.simUI = simUI; + } + + open() { + super.open(); this.init(); } diff --git a/ui/core/components/gear_picker/gear_picker.tsx b/ui/core/components/gear_picker/gear_picker.tsx index 5bb9b7d2a2..a085fc912e 100644 --- a/ui/core/components/gear_picker/gear_picker.tsx +++ b/ui/core/components/gear_picker/gear_picker.tsx @@ -349,7 +349,7 @@ export class ItemPicker extends Component { private addQuickGemHelpers() { if (!this._equippedItem) return; - const openGemDetailTab = (socketIdx: number) => this.openSelectorModal(`Gem ${socketIdx + 1}` as SelectorModalTabs); + const openGemDetailTab = (socketIdx: number) => this.openSelectorModal(`Gem${socketIdx + 1}` as SelectorModalTabs); this.itemElem.socketsElem?.forEach(element => { const socketIdx = Number(element.dataset.socketIdx) || 0; element.addEventListener('click', event => { @@ -453,7 +453,7 @@ export enum SelectorModalTabs { export class SelectorModal extends BaseModal { private readonly simUI: SimUI; private player: Player; - private gearPicker: GearPicker; + private gearPicker: GearPicker | undefined; private ilists: ItemList[] = []; private updateReforgeList: (newReforgeData: Array) => void; @@ -464,7 +464,7 @@ export class SelectorModal extends BaseModal { private currentSlot: ItemSlot = ItemSlot.ItemSlotHead; private currentTab: SelectorModalTabs = SelectorModalTabs.Items; - constructor(parent: HTMLElement, simUI: SimUI, player: Player, gearPicker: GearPicker) { + constructor(parent: HTMLElement, simUI: SimUI, player: Player, gearPicker?: GearPicker) { super(parent, 'selector-modal'); this.simUI = simUI; @@ -506,16 +506,29 @@ export class SelectorModal extends BaseModal { private setData(selectedSlot: ItemSlot, selectedTab: SelectorModalTabs, gearData: GearData) { this.tabsElem.innerText = ''; this.contentElem.innerText = ''; - this.ilists = []; - this.currentSlot = selectedSlot; - this.currentTab = selectedTab; - const eligibleItems = this.player.getItems(selectedSlot); const eligibleEnchants = this.player.getEnchants(selectedSlot); const equippedItem = this.player.getEquippedItem(selectedSlot); + this.currentSlot = selectedSlot; + + // If the enchant tab is selected but the item has no eligible enchants, default to items + if (selectedTab == SelectorModalTabs.Enchants && eligibleEnchants.length == 0) { + selectedTab = SelectorModalTabs.Items; + } + + // If a gem tab is selected but the item has no eligible sockets, default to items + if ( + [SelectorModalTabs.Gem1, SelectorModalTabs.Gem2, SelectorModalTabs.Gem3].includes(selectedTab) && + equippedItem?.numSockets(this.player.isBlacksmithing()) == 0 + ) { + selectedTab = SelectorModalTabs.Items; + } + + this.currentTab = selectedTab; + this.addTab( SelectorModalTabs.Items, gearData, @@ -587,6 +600,10 @@ export class SelectorModal extends BaseModal { } private addItemSlotTabs() { + if (!this.gearPicker) { + return; + } + this.dialog.prepend(
{this.gearPicker.itemPickers.map(picker => { @@ -606,6 +623,7 @@ export class SelectorModal extends BaseModal { ) as HTMLAnchorElement; picker.onUpdate(() => { if (picker.item) { + this.player.setWowheadData(picker.item, anchor); picker.item .asActionId() .fill() @@ -646,7 +664,7 @@ export class SelectorModal extends BaseModal { const socketBonusEP = this.player.computeStatsEP(new Stats(equippedItem.item.socketBonus)) / (equippedItem.item.gemSockets.length || 1); equippedItem.curSocketColors(this.player.isBlacksmithing()).forEach((socketColor, socketIdx) => { this.addTab( - SelectorModalTabs[('Gem' + (socketIdx + 1)) as keyof typeof SelectorModalTabs], + SelectorModalTabs[`Gem${socketIdx + 1}` as keyof typeof SelectorModalTabs], gearData, this.player.getGems(socketColor).map((gem: Gem) => { return { diff --git a/ui/core/components/individual_sim_ui/bulk_tab.ts b/ui/core/components/individual_sim_ui/bulk_tab.ts index 04b2d40f91..27d826202f 100644 --- a/ui/core/components/individual_sim_ui/bulk_tab.ts +++ b/ui/core/components/individual_sim_ui/bulk_tab.ts @@ -13,7 +13,7 @@ import { BaseModal } from '../base_modal'; import { BooleanPicker } from '../boolean_picker'; import { Component } from '../component'; import { ContentBlock } from '../content_block'; -import { ItemData, ItemList, ItemRenderer, SelectorModal, SelectorModalTabs } from '../gear_picker/gear_picker'; +import { GearData, ItemData, ItemList, ItemRenderer, SelectorModal, SelectorModalTabs } from '../gear_picker/gear_picker'; import { Importer } from '../importers'; import { ResultsViewer } from '../results_viewer'; import { SimTab } from '../sim_tab'; @@ -132,6 +132,8 @@ class BulkSimResultRenderer { export class BulkItemPicker extends Component { private readonly itemElem: ItemRenderer; + private readonly selectorModal: SelectorModal; + readonly simUI: IndividualSimUI; readonly bulkUI: BulkTab; readonly index: number; @@ -145,6 +147,7 @@ export class BulkItemPicker extends Component { this.index = index; this.item = item; this.itemElem = new ItemRenderer(parent, this.rootElem, simUI.player); + this.selectorModal = new SelectorModal(this.bulkUI.rootElem, this.simUI, this.simUI.player); this.simUI.sim.waitForInit().then(() => { this.setItem(item); @@ -152,32 +155,11 @@ export class BulkItemPicker extends Component { const eligibleEnchants = this.simUI.sim.db.getEnchants(slot); const openEnchantGemSelector = (event: Event) => { event.preventDefault(); - const changeEvent = new TypedEvent(); - const modal = new SelectorModal(this.bulkUI.rootElem, this.simUI, this.simUI.player, { - selectedTab: SelectorModalTabs.Enchants, - slot: slot, - equippedItem: this.item, - eligibleItems: new Array(), - eligibleEnchants: eligibleEnchants, - gearData: { - equipItem: (eventID: EventID, equippedItem: EquippedItem | null) => { - if (equippedItem) { - const allItems = this.bulkUI.getItems(); - allItems[this.index] = equippedItem.asSpec(); - this.item = equippedItem; - this.bulkUI.setItems(allItems); - changeEvent.emit(TypedEvent.nextEventID()); - } - }, - getEquippedItem: () => this.item, - changeEvent: changeEvent, - }, - }); if (eligibleEnchants.length > 0) { - modal.openTabName('Enchants'); + this.selectorModal.openTab(slot, SelectorModalTabs.Enchants, this.createGearData()); } else if (this.item._gems.length > 0) { - modal.openTabName('Gem1'); + this.selectorModal.openTab(slot, SelectorModalTabs.Gem1, this.createGearData()); } const destroyItemButton = document.createElement('button'); @@ -189,11 +171,11 @@ export class BulkItemPicker extends Component { return idx != this.index; }), ); - modal.close(); + this.selectorModal.close(); }; - const closeX = modal.header?.querySelector('.close-button'); + const closeX = this.selectorModal.header?.querySelector('.close-button'); if (closeX != undefined) { - modal.header?.insertBefore(destroyItemButton, closeX); + this.selectorModal.header?.insertBefore(destroyItemButton, closeX); } }; @@ -215,6 +197,23 @@ export class BulkItemPicker extends Component { this.itemElem.rootElem.style.alignItems = 'center'; } } + + private createGearData(): GearData { + const changeEvent = new TypedEvent(); + return { + equipItem: (eventID: EventID, equippedItem: EquippedItem | null) => { + if (equippedItem) { + const allItems = this.bulkUI.getItems(); + allItems[this.index] = equippedItem.asSpec(); + this.item = equippedItem; + this.bulkUI.setItems(allItems); + changeEvent.emit(TypedEvent.nextEventID()); + } + }, + getEquippedItem: () => this.item, + changeEvent: changeEvent, + }; + } } export class BulkTab extends SimTab { @@ -924,22 +923,17 @@ class GemSelectorModal extends BaseModal { this.ilist = new ItemList( this.body, this.simUI, + ItemSlot.ItemSlotHead, + SelectorModalTabs.Gem1, + this.simUI.player, + 'Gem1', { - selectedTab: SelectorModalTabs.Gem1, - slot: ItemSlot.ItemSlotHead, - equippedItem: null, - eligibleItems: new Array(), - eligibleEnchants: new Array(), - gearData: { - equipItem: (_eventID: EventID, _equippedItem: EquippedItem | null) => { - return; - }, - getEquippedItem: () => null, - changeEvent: new TypedEvent(), // FIXME + equipItem: (_eventID: EventID, _equippedItem: EquippedItem | null) => { + return; }, + getEquippedItem: () => null, + changeEvent: new TypedEvent(), // FIXME }, - this.simUI.player, - 'Gem1', this.simUI.player.getGems(this.socketColor).map((gem: UIGem) => { return { item: gem, diff --git a/ui/core/components/sim_header.tsx b/ui/core/components/sim_header.tsx index 39d4e1787c..1990a415db 100644 --- a/ui/core/components/sim_header.tsx +++ b/ui/core/components/sim_header.tsx @@ -4,6 +4,8 @@ import { element } from 'tsx-vanilla'; import { SimUI } from '../sim_ui'; import { Component } from './component'; +import { Exporter } from './exporters'; +import { Importer } from './importers'; import { SettingsMenu } from './settings_menu'; import { SimTab } from './sim_tab'; import { SocialLinks } from './social_links'; @@ -83,16 +85,15 @@ export class SimHeader extends Component { this.simTabsContainer.appendChild(tab.navItem); } - addImportLink(label: string, onClick: (parent: HTMLElement) => void, hideInRaidSim?: boolean) { - this.addImportExportLink('import-dropdown', label, onClick, hideInRaidSim); + addImportLink(label: string, importer: Importer, hideInRaidSim?: boolean) { + this.addImportExportLink('import-dropdown', label, importer, hideInRaidSim); } - addExportLink(label: string, onClick: (parent: HTMLElement) => void, hideInRaidSim?: boolean) { - this.addImportExportLink('export-dropdown', label, onClick, hideInRaidSim); + addExportLink(label: string, exporter: Exporter, hideInRaidSim?: boolean) { + this.addImportExportLink('export-dropdown', label, exporter, hideInRaidSim); } - private addImportExportLink(cssClass: string, label: string, onClick: (parent: HTMLElement) => void, hideInRaidSim?: boolean) { + private addImportExportLink(cssClass: string, label: string, importerExporter: Importer | Exporter, _hideInRaidSim?: boolean) { const dropdownElem = this.rootElem.getElementsByClassName(cssClass)[0] as HTMLElement; const menuElem = dropdownElem.getElementsByClassName('dropdown-menu')[0] as HTMLElement; - const itemElem = (
  • onClick(menuElem)); + linkElem.addEventListener('click', () => importerExporter.open()); menuElem.appendChild(itemElem); } @@ -199,12 +200,13 @@ export class SimHeader extends Component { } private addSimOptionsLink() { + const settingsMenu = new SettingsMenu(this.simUI.rootElem, this.simUI); this.addToolbarLink({ parent: this.simToolbar, icon: 'fas fa-cog fa-lg', tooltip: 'Show Sim Options', classes: 'sim-options', - onclick: () => new SettingsMenu(this.simUI.rootElem, this.simUI), + onclick: () => settingsMenu.open(), }); } diff --git a/ui/core/components/stat_weights_action.ts b/ui/core/components/stat_weights_action.ts index 42148b44db..b79b6059eb 100644 --- a/ui/core/components/stat_weights_action.ts +++ b/ui/core/components/stat_weights_action.ts @@ -18,7 +18,8 @@ import { ResultsViewer } from './results_viewer.js'; export function addStatWeightsAction(simUI: IndividualSimUI, epStats: Array, epPseudoStats: Array | undefined, epReferenceStat: Stat) { simUI.addAction('Stat Weights', 'ep-weights-action', () => { - new EpWeightsMenu(simUI, epStats, epPseudoStats || [], epReferenceStat); + // TODO: Make this so we can initialize the menu once outside of this function + new EpWeightsMenu(simUI, epStats, epPseudoStats || [], epReferenceStat).open(); }); } diff --git a/ui/core/individual_sim_ui.ts b/ui/core/individual_sim_ui.ts index f52206936d..2c669bc680 100644 --- a/ui/core/individual_sim_ui.ts +++ b/ui/core/individual_sim_ui.ts @@ -416,17 +416,17 @@ export abstract class IndividualSimUI extends SimUI { } private addTopbarComponents() { - this.simHeader.addImportLink('JSON', _parent => new Importers.IndividualJsonImporter(this.rootElem, this), true); - this.simHeader.addImportLink('80U', _parent => new Importers.Individual80UImporter(this.rootElem, this), true); - this.simHeader.addImportLink('WoWHead', _parent => new Importers.IndividualWowheadGearPlannerImporter(this.rootElem, this), false); - this.simHeader.addImportLink('Addon', _parent => new Importers.IndividualAddonImporter(this.rootElem, this), true); - - this.simHeader.addExportLink('Link', _parent => new Exporters.IndividualLinkExporter(this.rootElem, this), false); - this.simHeader.addExportLink('JSON', _parent => new Exporters.IndividualJsonExporter(this.rootElem, this), true); - this.simHeader.addExportLink('WoWHead', _parent => new Exporters.IndividualWowheadGearPlannerExporter(this.rootElem, this), false); - this.simHeader.addExportLink('80U EP', _parent => new Exporters.Individual80UEPExporter(this.rootElem, this), false); - this.simHeader.addExportLink('Pawn EP', _parent => new Exporters.IndividualPawnEPExporter(this.rootElem, this), false); - this.simHeader.addExportLink('CLI', _parent => new Exporters.IndividualCLIExporter(this.rootElem, this), true); + this.simHeader.addImportLink('JSON', new Importers.IndividualJsonImporter(this.rootElem, this), true); + this.simHeader.addImportLink('80U', new Importers.Individual80UImporter(this.rootElem, this), true); + this.simHeader.addImportLink('WoWHead', new Importers.IndividualWowheadGearPlannerImporter(this.rootElem, this), false); + this.simHeader.addImportLink('Addon', new Importers.IndividualAddonImporter(this.rootElem, this), true); + + this.simHeader.addExportLink('Link', new Exporters.IndividualLinkExporter(this.rootElem, this), false); + this.simHeader.addExportLink('JSON', new Exporters.IndividualJsonExporter(this.rootElem, this), true); + this.simHeader.addExportLink('WoWHead', new Exporters.IndividualWowheadGearPlannerExporter(this.rootElem, this), false); + this.simHeader.addExportLink('80U EP', new Exporters.Individual80UEPExporter(this.rootElem, this), false); + this.simHeader.addExportLink('Pawn EP', new Exporters.IndividualPawnEPExporter(this.rootElem, this), false); + this.simHeader.addExportLink('CLI', new Exporters.IndividualCLIExporter(this.rootElem, this), true); } applyDefaultRotation(eventID: EventID) { diff --git a/ui/core/sim_ui.ts b/ui/core/sim_ui.ts index 43993ab43a..41e045f6eb 100644 --- a/ui/core/sim_ui.ts +++ b/ui/core/sim_ui.ts @@ -305,7 +305,7 @@ export abstract class SimUI extends Component { issueBody += '...'; // The raid links are too large and will always cause truncation. // Prompt the user to add more information to the issue. - new CrashModal(this.rootElem, link); + new CrashModal(this.rootElem, link).open(); } window.open(base + issueBody, '_blank'); } diff --git a/ui/core/talents/glyphs_picker.tsx b/ui/core/talents/glyphs_picker.tsx index 9ffbe97a0d..4e91e212bf 100644 --- a/ui/core/talents/glyphs_picker.tsx +++ b/ui/core/talents/glyphs_picker.tsx @@ -149,9 +149,10 @@ class GlyphPicker extends Input, number> { this.iconElem = iconElemRef.value!; this.nameElem = nameElemRef.value!; + const selectorModal = new GlyphSelectorModal(this.rootElem.closest('.individual-sim-ui')!, this, this.glyphOptions); const openGlyphSelectorModal = (event: Event) => { event.preventDefault(); - new GlyphSelectorModal(this.rootElem.closest('.individual-sim-ui')!, this, this.glyphOptions); + selectorModal.open(); }; this.anchorElem.addEventListener('click', openGlyphSelectorModal); diff --git a/ui/index_template.html b/ui/index_template.html index 32380d8477..1c60f9c21e 100644 --- a/ui/index_template.html +++ b/ui/index_template.html @@ -34,7 +34,6 @@ integrity="sha512-Wt1bJGtlnMtGP0dqNFH1xlkLBNpEodaiQ8ZN5JLA5wpc1sUlk/O5uuOMNgvzddzkpvZ9GLyYNa8w2s7rqiTk5Q==" crossorigin="anonymous" referrerpolicy="no-referrer"> -