diff --git a/ui/core/components/detailed_results/result_component.ts b/ui/core/components/detailed_results/result_component.ts index 30f4eba8a2..94b98ba1d5 100644 --- a/ui/core/components/detailed_results/result_component.ts +++ b/ui/core/components/detailed_results/result_component.ts @@ -20,7 +20,7 @@ export abstract class ResultComponent extends Component { private lastSimResult: SimResultData | null; constructor(config: ResultComponentConfig) { - super(config.parent, config.rootCssClass || ''); + super(config.parent, config.rootCssClass || 'result-component'); this.lastSimResult = null; config.resultsEmitter.on((eventID, resultData) => { diff --git a/ui/core/components/detailed_results/results_filter.ts b/ui/core/components/detailed_results/results_filter.ts index 7440aadf07..50d4314350 100644 --- a/ui/core/components/detailed_results/results_filter.ts +++ b/ui/core/components/detailed_results/results_filter.ts @@ -1,6 +1,6 @@ -import { SimResult, SimResultFilter, UnitMetrics } from '../../proto_utils/sim_result.js'; +import { SimResult, SimResultFilter } from '../../proto_utils/sim_result.js'; import { EventID, TypedEvent } from '../../typed_event.js'; -import { Input } from '../../components/input.js'; +import { UnitPicker } from '../../components/unit_picker.js'; import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; @@ -11,13 +11,20 @@ interface FilterData { target: number, }; +interface UnitFilterOption { + iconUrl: string, + text: string, + color: string, + value: number, +}; + export class ResultsFilter extends ResultComponent { private readonly currentFilter: FilterData; readonly changeEmitter: TypedEvent; - private readonly playerFilter: PlayerFilter; - private readonly targetFilter: TargetFilter; + private readonly playerFilter: UnitPicker; + private readonly targetFilter: UnitPicker; constructor(config: ResultComponentConfig) { config.rootCssClass = 'results-filter-root'; @@ -28,11 +35,27 @@ export class ResultsFilter extends ResultComponent { }; this.changeEmitter = new TypedEvent(); - this.playerFilter = new PlayerFilter(this.rootElem, this.currentFilter); - this.playerFilter.changeEmitter.on(eventID => this.changeEmitter.emit(eventID)); + this.playerFilter = new UnitPicker(this.rootElem, this.currentFilter, { + extraCssClasses: [ + 'player-filter-root', + ], + changedEvent: (filterData: FilterData) => this.changeEmitter, + getValue: (filterData: FilterData) => filterData.player, + setValue: (eventID: EventID, filterData: FilterData, newValue: number) => this.setPlayer(eventID, newValue), + equals: (a, b) => a == b, + values: [], + }); - this.targetFilter = new TargetFilter(this.rootElem, this.currentFilter); - this.targetFilter.changeEmitter.on(eventID => this.changeEmitter.emit(eventID)); + this.targetFilter = new UnitPicker(this.rootElem, this.currentFilter, { + extraCssClasses: [ + 'target-filter-root', + ], + changedEvent: (filterData: FilterData) => this.changeEmitter, + getValue: (filterData: FilterData) => filterData.target, + setValue: (eventID: EventID, filterData: FilterData, newValue: number) => this.setTarget(eventID, newValue), + equals: (a, b) => a == b, + values: [], + }); } getFilter(): SimResultFilter { @@ -43,191 +66,50 @@ export class ResultsFilter extends ResultComponent { } onSimResult(resultData: SimResultData) { - this.playerFilter.setOptions(resultData.eventID, resultData.result); - this.targetFilter.setOptions(resultData.eventID, resultData.result); + this.playerFilter.setOptions(this.getUnitOptions(resultData.eventID, resultData.result, true)); + this.targetFilter.setOptions(this.getUnitOptions(resultData.eventID, resultData.result, false)); } setPlayer(eventID: EventID, newPlayer: number | null) { this.currentFilter.player = (newPlayer === null) ? ALL_UNITS : newPlayer; - this.playerFilter.changeEmitter.emit(eventID); + this.changeEmitter.emit(eventID); } setTarget(eventID: EventID, newTarget: number | null) { this.currentFilter.target = (newTarget === null) ? ALL_UNITS : newTarget; - this.targetFilter.changeEmitter.emit(eventID); + this.changeEmitter.emit(eventID); } -} -interface UnitFilterOption { - iconUrl: string, - text: string, - color: string, - value: number, -}; - -// Dropdown menu for filtering by player. -abstract class UnitGroupFilter extends Input { - private readonly filterData: FilterData; - readonly changeEmitter: TypedEvent; - - private allUnitsOption: UnitFilterOption; - private currentOptions: Array; - - private readonly buttonElem: HTMLElement; - private readonly dropdownElem: HTMLElement; - - constructor(parent: HTMLElement, filterData: FilterData, allUnitsLabel: string) { - const changeEmitter = new TypedEvent(); - super(parent, 'unit-filter-root', filterData, { - extraCssClasses: [ - 'dropdown-root', - ], - changedEvent: (filterData: FilterData) => changeEmitter, - getValue: (filterData: FilterData) => this.getFilterDataValue(filterData), - setValue: (eventID: EventID, filterData: FilterData, newValue: number) => this.setFilterDataValue(filterData, newValue), - }); - this.filterData = filterData; - this.changeEmitter = changeEmitter; - - this.allUnitsOption = { + private getUnitOptions(eventID: EventID, simResult: SimResult, isPlayer: boolean): Array { + const allUnitsOption = { iconUrl: '', - text: allUnitsLabel, + text: isPlayer ? 'All Players' : 'All Targets', color: 'black', value: ALL_UNITS, }; - this.currentOptions = [this.allUnitsOption]; - this.rootElem.innerHTML = ` - - - `; - - this.buttonElem = this.rootElem.getElementsByClassName('unit-filter-button')[0] as HTMLElement; - this.dropdownElem = this.rootElem.getElementsByClassName('unit-filter-dropdown')[0] as HTMLElement; - - this.buttonElem.addEventListener('click', event => { - event.preventDefault(); - }); - - this.init(); - } - - abstract getFilterDataValue(filterData: FilterData): number; - abstract setFilterDataValue(filterData: FilterData, newValue: number): void; - abstract getAllUnits(simResult: SimResult): Array; - - setOptions(eventID: EventID, simResult: SimResult) { - this.currentOptions = [this.allUnitsOption].concat(this.getAllUnits(simResult).map(unit => { + const unitOptions = (isPlayer ? simResult.getPlayers() : simResult.getTargets()).map(unit => { return { iconUrl: unit.iconUrl || '', text: unit.label, color: unit.classColor || 'black', value: unit.unitIndex, }; - })); - - const hasSameOption = this.currentOptions.find(option => option.value == this.getInputValue()) != null; - if (!hasSameOption) { - this.setFilterDataValue(this.filterData, this.allUnitsOption.value); - this.changeEmitter.emit(eventID); - } - - this.dropdownElem.innerHTML = ''; - this.currentOptions.forEach(option => this.dropdownElem.appendChild(this.makeOption(option))); - } - - private makeOption(data: UnitFilterOption): HTMLElement { - const option = this.makeOptionElem(data); - - option.addEventListener('click', event => { - event.preventDefault(); - this.setFilterDataValue(this.filterData, data.value); - this.changeEmitter.emit(TypedEvent.nextEventID()); }); - return option; - } - - private makeOptionElem(data: UnitFilterOption): HTMLElement { - const optionContainer = document.createElement('div'); - optionContainer.classList.add('dropdown-option-container'); - - const option = document.createElement('div'); - option.classList.add('dropdown-option', 'unit-filter-option'); - optionContainer.appendChild(option); - - if (data.color) { - option.style.backgroundColor = data.color; - } - - if (data.iconUrl) { - const icon = document.createElement('img'); - icon.src = data.iconUrl; - icon.classList.add('unit-filter-icon'); - option.appendChild(icon); - } - - if (data.text) { - const label = document.createElement('span'); - label.textContent = data.text; - label.classList.add('unit-filter-label'); - option.appendChild(label); - } - - return optionContainer; - } - - getInputElem(): HTMLElement { - return this.buttonElem; - } - - getInputValue(): number { - return this.getFilterDataValue(this.filterData); - } + const options = [allUnitsOption].concat(unitOptions); - setInputValue(newValue: number) { - this.setFilterDataValue(this.filterData, newValue); - - const optionData = this.currentOptions.find(optionData => optionData.value == newValue); - if (!optionData) { - return; + const curValue = isPlayer ? this.currentFilter.player : this.currentFilter.target; + const hasSameOption = options.find(option => option.value == curValue) != null; + if (!hasSameOption) { + if (isPlayer) { + this.currentFilter.player = ALL_UNITS; + } else { + this.currentFilter.target = ALL_UNITS; + } + this.changeEmitter.emit(eventID); } - this.buttonElem.innerHTML = ''; - this.buttonElem.appendChild(this.makeOptionElem(optionData)); - } -} - -class PlayerFilter extends UnitGroupFilter { - constructor(parent: HTMLElement, filterData: FilterData) { - super(parent, filterData, 'All Players'); - this.rootElem.classList.add('player-filter-root'); - } - - getFilterDataValue(filterData: FilterData): number { - return filterData.player; - } - setFilterDataValue(filterData: FilterData, newValue: number): void { - filterData.player = newValue; - } - getAllUnits(simResult: SimResult): Array { - return simResult.getPlayers(); - } -} - -class TargetFilter extends UnitGroupFilter { - constructor(parent: HTMLElement, filterData: FilterData) { - super(parent, filterData, 'All Targets'); - this.rootElem.classList.add('target-filter-root'); - } - - getFilterDataValue(filterData: FilterData): number { - return filterData.target; - } - setFilterDataValue(filterData: FilterData, newValue: number): void { - filterData.target = newValue; - } - getAllUnits(simResult: SimResult): Array { - return simResult.getTargets(); + return options; } -} +} \ No newline at end of file diff --git a/ui/core/components/individual_sim_ui/apl_helpers.ts b/ui/core/components/individual_sim_ui/apl_helpers.ts index e57c0e6c7e..9aee2ef4da 100644 --- a/ui/core/components/individual_sim_ui/apl_helpers.ts +++ b/ui/core/components/individual_sim_ui/apl_helpers.ts @@ -116,7 +116,7 @@ export class APLActionIDPicker extends DropdownPicker, ActionID, Act const actionIdSet = actionIdSets[config.actionIdSet]; super(parent, player, { ...config, - sourceToValue: (src: ActionID) => ActionId.fromProto(src), + sourceToValue: (src: ActionID) => src ? ActionId.fromProto(src) : ActionId.fromEmpty(), valueToSource: (val: ActionId) => val.toProto(), defaultLabel: actionIdSet.defaultLabel, equals: (a, b) => ((a == null) == (b == null)) && (!a || a.equals(b!)), @@ -131,7 +131,7 @@ export class APLActionIDPicker extends DropdownPicker, ActionID, Act const textElem = document.createTextNode(actionId.name); button.appendChild(textElem); }, - createMissingValue: value => ((value instanceof ActionId) ? value : ActionId.fromProto(value as unknown as ActionID)).fill().then(filledId => { + createMissingValue: value => value.fill().then(filledId => { return { value: filledId, }; diff --git a/ui/core/components/unit_picker.ts b/ui/core/components/unit_picker.ts new file mode 100644 index 0000000000..da94b6ec81 --- /dev/null +++ b/ui/core/components/unit_picker.ts @@ -0,0 +1,42 @@ +import { DropdownPicker, DropdownPickerConfig, DropdownValueConfig } from './dropdown_picker.js'; + +export interface UnitValueConfig extends DropdownValueConfig { + text: string, + iconUrl?: string, + color?: string, +} + +export interface UnitPickerConfig extends Omit, 'values' | 'setOptionContent' | 'defaultLabel'> { + values: Array>, +} + +export class UnitPicker extends DropdownPicker { + constructor(parent: HTMLElement, modObject: ModObject, config: UnitPickerConfig) { + super(parent, modObject, { + ...config, + defaultLabel: 'Unit', + setOptionContent: (button: HTMLButtonElement, valueConfig: DropdownValueConfig) => { + const unitConfig = valueConfig as UnitValueConfig; + + if (unitConfig.color) { + button.style.backgroundColor = unitConfig.color; + } + + if (unitConfig.iconUrl) { + const icon = document.createElement('img'); + icon.src = unitConfig.iconUrl; + icon.classList.add('unit-filter-icon'); + button.appendChild(icon); + } + + if (unitConfig.text) { + const label = document.createElement('span'); + label.textContent = unitConfig.text; + label.classList.add('unit-filter-label'); + button.appendChild(label); + } + } + }); + this.rootElem.classList.add('unit-picker-root'); + } +} diff --git a/ui/scss/core/components/_unit_picker.scss b/ui/scss/core/components/_unit_picker.scss new file mode 100644 index 0000000000..71ef8357c5 --- /dev/null +++ b/ui/scss/core/components/_unit_picker.scss @@ -0,0 +1,22 @@ +.unit-picker-root { + border: 1px solid white; + color: white; + text-shadow: + 0 0 3px black, + 0 0 3px black, + 0 0 3px black; +} + +.unit-filter-option { + height: 30px; +} + +.unit-filter-icon { + margin: 2px; + height: 25px; +} + +.unit-filter-label { + font-size: 14px; + margin: 2px; +} diff --git a/ui/scss/core/components/detailed_results/_results_filter.scss b/ui/scss/core/components/detailed_results/_results_filter.scss index 66e19c40b1..75c5c72ff5 100644 --- a/ui/scss/core/components/detailed_results/_results_filter.scss +++ b/ui/scss/core/components/detailed_results/_results_filter.scss @@ -1,28 +1,7 @@ +@import "../unit_picker"; + .results-filter-root { display: flex; align-items: center; font-size: 20px; -} - -.unit-filter-root { - border: 1px solid white; - color: white; - text-shadow: - 0 0 3px black, - 0 0 3px black, - 0 0 3px black; -} - -.unit-filter-option { - height: 30px; -} - -.unit-filter-icon { - margin: 2px; - height: 25px; -} - -.unit-filter-label { - font-size: 14px; - margin: 2px; -} +} \ No newline at end of file diff --git a/ui/scss/core/individual_sim_ui/index.scss b/ui/scss/core/individual_sim_ui/index.scss index b473fd1e11..c446ab97f4 100644 --- a/ui/scss/core/individual_sim_ui/index.scss +++ b/ui/scss/core/individual_sim_ui/index.scss @@ -25,6 +25,7 @@ @import "../components/saved_data_manager"; @import "../components/settings_menu"; @import "../components/stat_weights_action"; +@import "../components/unit_picker"; @import "../components/individual_sim_ui/settings_tab"; @import "../components/individual_sim_ui/rotation_tab";