diff --git a/src/core/data_formats/parsing/quest/areas.ts b/src/core/data_formats/parsing/quest/areas.ts index ca6faf98..fbcd90d3 100644 --- a/src/core/data_formats/parsing/quest/areas.ts +++ b/src/core/data_formats/parsing/quest/areas.ts @@ -60,9 +60,10 @@ AREAS[Episode.I] = [ create_area(9, "Ruins 2", order++, 5), create_area(10, "Ruins 3", order++, 5), create_area(14, "Dark Falz", order++, 1), - create_area(15, "BA Ruins", order++, 3), - create_area(16, "BA Spaceship", order++, 3), - create_area(17, "Lobby", order++, 15), + // TODO: + // create_area(15, "BA Ruins", order++, 3), + // create_area(16, "BA Spaceship", order++, 3), + // create_area(17, "Lobby", order++, 15), ]; order = 0; AREAS[Episode.II] = [ diff --git a/src/core/gui/ComboBox.css b/src/core/gui/ComboBox.css new file mode 100644 index 00000000..1c88be3f --- /dev/null +++ b/src/core/gui/ComboBox.css @@ -0,0 +1,30 @@ +.core_ComboBox { + box-sizing: border-box; + position: relative; +} + +.core_ComboBox_inner { + box-sizing: border-box; + display: flex; + align-items: center; +} + +.core_ComboBox_inner input { + flex: 1; + padding: 0; + border: none; + margin: 0; + color: var(--input-text-color); + background-color: transparent; + outline: none; +} + +.core_ComboBox.disabled input { + color: var(--input-text-color-disabled); +} + +.core_ComboBox .core_Menu { + top: 23px; + left: -2px; + min-width: calc(100% + 4px); +} diff --git a/src/core/gui/ComboBox.ts b/src/core/gui/ComboBox.ts new file mode 100644 index 00000000..ddb841ec --- /dev/null +++ b/src/core/gui/ComboBox.ts @@ -0,0 +1,87 @@ +import { LabelledControl, LabelledControlOptions } from "./LabelledControl"; +import { create_element, el, Icon, icon } from "./dom"; +import "./ComboBox.css"; +import "./Input.css"; +import { Menu } from "./Menu"; +import { Property } from "../observable/property/Property"; +import { property } from "../observable"; +import { WritableProperty } from "../observable/property/WritableProperty"; +import { WidgetProperty } from "../observable/property/WidgetProperty"; + +export type ComboBoxOptions = LabelledControlOptions & { + items: T[] | Property; + to_label: (item: T) => string; + placeholder_text?: string; +}; + +export class ComboBox extends LabelledControl { + readonly preferred_label_position = "left"; + + readonly selected: WritableProperty; + + private readonly to_label: (element: T) => string; + private readonly menu: Menu; + private readonly input_element: HTMLInputElement = create_element("input"); + private readonly _selected: WidgetProperty; + + constructor(options: ComboBoxOptions) { + super(el.span({ class: "core_ComboBox core_Input" }), options); + + this.to_label = options.to_label; + + this._selected = new WidgetProperty(this, undefined, this.set_selected); + this.selected = this._selected; + + const menu_visible = property(false); + + this.menu = this.disposable(new Menu(options.items, options.to_label, this.element)); + this.menu.element.onmousedown = e => e.preventDefault(); + + this.input_element.placeholder = options.placeholder_text || ""; + this.input_element.onmousedown = () => { + menu_visible.val = true; + }; + this.input_element.onblur = () => { + menu_visible.val = false; + }; + + const down_arrow_element = el.span({}, icon(Icon.TriangleDown)); + this.bind_hidden(down_arrow_element, menu_visible); + + const up_arrow_element = el.span({}, icon(Icon.TriangleUp)); + this.bind_hidden(up_arrow_element, menu_visible.map(v => !v)); + + const button_element = el.span( + { class: "core_ComboBox_button" }, + down_arrow_element, + up_arrow_element, + ); + button_element.onmousedown = e => { + e.preventDefault(); + menu_visible.val = !menu_visible.val; + }; + + this.element.append( + el.span( + { class: "core_ComboBox_inner core_Input_inner" }, + this.input_element, + button_element, + ), + this.menu.element, + ); + + this.disposables( + this.menu.visible.bind_bi(menu_visible), + + this.menu.selected.observe(({ value }) => { + this.selected.set_val(value, { silent: false }); + this.input_element.focus(); + }), + ); + } + + protected set_selected(selected?: T): void { + this.input_element.value = selected ? this.to_label(selected) : ""; + this.menu.selected.val = selected; + } +} diff --git a/src/core/gui/DropDownButton.css b/src/core/gui/DropDown.css similarity index 57% rename from src/core/gui/DropDownButton.css rename to src/core/gui/DropDown.css index 772d1fb4..13c35e3b 100644 --- a/src/core/gui/DropDownButton.css +++ b/src/core/gui/DropDown.css @@ -1,8 +1,8 @@ -.core_DropDownButton { +.core_DropDown { position: relative; } -.core_DropDownButton .core_Menu { +.core_DropDown .core_Menu { top: 25px; left: 0; min-width: 100%; diff --git a/src/core/gui/DropDownButton.ts b/src/core/gui/DropDown.ts similarity index 89% rename from src/core/gui/DropDownButton.ts rename to src/core/gui/DropDown.ts index 16edccca..80010a71 100644 --- a/src/core/gui/DropDownButton.ts +++ b/src/core/gui/DropDown.ts @@ -1,5 +1,5 @@ import { disposable_listener, el, Icon } from "./dom"; -import "./DropDownButton.css"; +import "./DropDown.css"; import { Property } from "../observable/property/Property"; import { Button, ButtonOptions } from "./Button"; import { Menu } from "./Menu"; @@ -8,9 +8,9 @@ import { Observable } from "../observable/Observable"; import { Emitter } from "../observable/Emitter"; import { emitter } from "../observable"; -export type DropDownButtonOptions = ButtonOptions; +export type DropDownOptions = ButtonOptions; -export class DropDownButton extends Control { +export class DropDown extends Control { readonly chosen: Observable; private readonly button: Button; @@ -22,9 +22,9 @@ export class DropDownButton extends Control { text: string, items: T[] | Property, to_label: (element: T) => string, - options?: DropDownButtonOptions, + options?: DropDownOptions, ) { - const element = el.div({ class: "core_DropDownButton" }); + const element = el.div({ class: "core_DropDown" }); const button = new Button(text, { icon_left: options && options.icon_left, icon_right: Icon.TriangleDown, diff --git a/src/core/gui/Menu.css b/src/core/gui/Menu.css index affa8779..4b75ef68 100644 --- a/src/core/gui/Menu.css +++ b/src/core/gui/Menu.css @@ -3,15 +3,19 @@ position: absolute; box-sizing: border-box; border: var(--control-border); + --scrollbar-color: hsl(0, 0%, 18%); + --scrollbar-thumb-color: hsl(0, 0%, 22%); } .core_Menu .core_Menu_inner { + overflow: auto; background-color: var(--control-bg-color); + max-height: 500px; border: var(--control-inner-border); } .core_Menu .core_Menu_inner > * { - padding: 5px 8px; + padding: 4px 8px; white-space: nowrap; } diff --git a/src/core/gui/Menu.ts b/src/core/gui/Menu.ts index b907894a..1eb46295 100644 --- a/src/core/gui/Menu.ts +++ b/src/core/gui/Menu.ts @@ -48,11 +48,11 @@ export class Menu extends Widget { { call_now: true }, ), - disposable_listener(document, "mousedown", (e: Event) => this.document_mousedown(e), { + disposable_listener(document, "mousedown", this.document_mousedown, { capture: true, }), - disposable_listener(document, "keydown", () => this.document_keydown()), + disposable_listener(document, "keydown", this.document_keydown), ); } @@ -70,20 +70,22 @@ export class Menu extends Widget { if (!element) return; this.selected.set_val(element, { silent: false }); - this.visible.val = false; + this.visible.set_val(false, { silent: false }); } - private document_mousedown(e: Event): void { + private document_mousedown = (e: Event): void => { if ( this.visible.val && !this.element.contains(e.target as Node) && !this.related_element.contains(e.target as Node) ) { - this.visible.val = false; + this.visible.set_val(false, { silent: false }); } - } + }; - private document_keydown(): void { - this.visible.val = false; - } + private document_keydown = (e: Event): void => { + if ((e as KeyboardEvent).key === "Escape") { + this.visible.set_val(false, { silent: false }); + } + }; } diff --git a/src/core/gui/dom.ts b/src/core/gui/dom.ts index 4ae4d41e..757a0347 100644 --- a/src/core/gui/dom.ts +++ b/src/core/gui/dom.ts @@ -108,6 +108,7 @@ export enum Icon { File, NewFile, Save, + TriangleUp, TriangleDown, Undo, Redo, @@ -127,6 +128,9 @@ export function icon(icon: Icon): HTMLElement { case Icon.Save: icon_str = "fa-save"; break; + case Icon.TriangleUp: + icon_str = "fa-caret-up"; + break; case Icon.TriangleDown: icon_str = "fa-caret-down"; break; diff --git a/src/core/observable/property/Property.ts b/src/core/observable/property/Property.ts index 192f099e..94ef327e 100644 --- a/src/core/observable/property/Property.ts +++ b/src/core/observable/property/Property.ts @@ -27,5 +27,5 @@ export function is_property(observable: Observable): observable is Propert } export function is_any_property(observable: any): observable is Property { - return observable && (observable as any).is_property; + return observable && observable.is_property; } diff --git a/src/core/observable/property/list/ListProperty.ts b/src/core/observable/property/list/ListProperty.ts index a1efd625..162eb5ec 100644 --- a/src/core/observable/property/list/ListProperty.ts +++ b/src/core/observable/property/list/ListProperty.ts @@ -1,5 +1,6 @@ import { Property } from "../Property"; import { Disposable } from "../../Disposable"; +import { Observable } from "../../Observable"; export enum ListChangeType { ListChange, @@ -22,6 +23,8 @@ export type ListValueChange = { }; export interface ListProperty extends Property { + readonly is_list_property: true; + readonly length: Property; get(index: number): T; @@ -31,3 +34,11 @@ export interface ListProperty extends Property { options?: { call_now?: boolean }, ): Disposable; } + +export function is_list_property(observable: Observable): observable is ListProperty { + return (observable as any).is_list_property; +} + +export function is_any_list_property(observable: any): observable is ListProperty { + return observable && observable.is_list_property; +} diff --git a/src/core/observable/property/list/SimpleListProperty.ts b/src/core/observable/property/list/SimpleListProperty.ts index fe39a1ef..8bd4adce 100644 --- a/src/core/observable/property/list/SimpleListProperty.ts +++ b/src/core/observable/property/list/SimpleListProperty.ts @@ -5,34 +5,36 @@ import { Observable } from "../../Observable"; import { property } from "../../index"; import { AbstractProperty } from "../AbstractProperty"; import { Property } from "../Property"; -import { ListChangeType, ListPropertyChangeEvent } from "./ListProperty"; +import { is_list_property, ListChangeType, ListPropertyChangeEvent } from "./ListProperty"; import Logger from "js-logger"; const logger = Logger.get("core/observable/property/list/SimpleListProperty"); export class SimpleListProperty extends AbstractProperty implements WritableListProperty { + readonly is_list_property = true; + readonly length: Property; get val(): T[] { return this.get_val(); } - set val(elements: T[]) { - this.set_val(elements); + set val(values: T[]) { + this.set_val(values); } get_val(): T[] { return this.values; } - set_val(elements: T[]): T[] { - const removed = this.values.splice(0, this.values.length, ...elements); + set_val(values: T[]): T[] { + const removed = this.values.splice(0, this.values.length, ...values); this.finalize_update({ type: ListChangeType.ListChange, index: 0, removed, - inserted: elements, + inserted: values, }); return removed; } @@ -94,11 +96,26 @@ export class SimpleListProperty extends AbstractProperty } bind_to(observable: Observable): Disposable { - /* TODO */ throw new Error("not implemented"); + if (is_list_property(observable)) { + return observable.observe_list(change => { + if (change.type === ListChangeType.ListChange) { + this.splice(change.index, change.removed.length, ...change.inserted); + } + }); + } else { + return observable.observe(({ value }) => this.set_val(value)); + } } bind_bi(property: WritableProperty): Disposable { - /* TODO */ throw new Error("not implemented"); + const bind_1 = this.bind_to(property); + const bind_2 = property.bind_to(this); + return { + dispose(): void { + bind_1.dispose(); + bind_2.dispose(); + }, + }; } update(f: (element: T[]) => T[]): void { @@ -120,6 +137,34 @@ export class SimpleListProperty extends AbstractProperty }); } + push(...values: T[]): number { + const index = this.values.length; + this.values.push(...values); + + this.finalize_update({ + type: ListChangeType.ListChange, + index, + removed: [], + inserted: values, + }); + + return this.length.val; + } + + remove(...values: T[]): void { + for (const value of values) { + const index = this.values.indexOf(value); + this.values.splice(index, 1); + + this.finalize_update({ + type: ListChangeType.ListChange, + index, + removed: [value], + inserted: [], + }); + } + } + clear(): void { const removed = this.values.splice(0, this.values.length); this.finalize_update({ @@ -130,20 +175,20 @@ export class SimpleListProperty extends AbstractProperty }); } - splice(index: number, delete_count?: number, ...items: T[]): T[] { + splice(index: number, delete_count?: number, ...values: T[]): T[] { let removed: T[]; if (delete_count == undefined) { removed = this.values.splice(index); } else { - removed = this.values.splice(index, delete_count, ...items); + removed = this.values.splice(index, delete_count, ...values); } this.finalize_update({ type: ListChangeType.ListChange, index, removed, - inserted: items, + inserted: values, }); return removed; @@ -152,9 +197,10 @@ export class SimpleListProperty extends AbstractProperty /** * Does the following in the given order: * - Updates value observers + * - Sets length silently * - Emits ListPropertyChangeEvent * - Emits PropertyChangeEvent - * - Sets length + * - Emits length PropertyChangeEvent if necessary */ protected finalize_update(change: ListPropertyChangeEvent): void { if ( @@ -165,13 +211,18 @@ export class SimpleListProperty extends AbstractProperty this.replace_element_observers(change.index, change.removed.length, change.inserted); } + const old_length = this._length.val; + this._length.set_val(this.values.length, { silent: true }); + for (const observer of this.list_observers) { this.call_list_observer(observer, change); } this.emit(this.values); - this._length.val = this.values.length; + // Set length to old length first to ensure an event is emitted. + this._length.set_val(old_length, { silent: true }); + this._length.set_val(this.values.length, { silent: false }); } private call_list_observer( @@ -215,8 +266,10 @@ export class SimpleListProperty extends AbstractProperty } } + const shift = new_elements.length - amount; + while (index < this.value_observers.length) { - this.value_observers[index].index += index; + this.value_observers[index++].index += shift; } } } diff --git a/src/core/observable/property/list/WritableListProperty.ts b/src/core/observable/property/list/WritableListProperty.ts index d3250a2d..f09ceb3f 100644 --- a/src/core/observable/property/list/WritableListProperty.ts +++ b/src/core/observable/property/list/WritableListProperty.ts @@ -6,8 +6,12 @@ export interface WritableListProperty extends ListProperty, WritableProper set(index: number, value: T): void; + push(...values: T[]): number; + splice(index: number, delete_count?: number): T[]; - splice(index: number, delete_count: number, ...items: T[]): T[]; + splice(index: number, delete_count: number, ...values: T[]): T[]; + + remove(...values: T[]): void; clear(): void; } diff --git a/src/hunt_optimizer/gui/WantedItemsView.css b/src/hunt_optimizer/gui/WantedItemsView.css index 4352ce80..e0eecb4b 100644 --- a/src/hunt_optimizer/gui/WantedItemsView.css +++ b/src/hunt_optimizer/gui/WantedItemsView.css @@ -2,15 +2,15 @@ display: flex; flex-direction: column; align-items: stretch; - overflow: hidden; - padding-left: 6px; + padding: 0 6px; min-width: 200px; } .hunt_optimizer_WantedItemsView .hunt_optimizer_WantedItemsView_table_wrapper { flex: 1; - width: 100%; + width: calc(100% + 6px); overflow: auto; + margin: 4px -3px; } .hunt_optimizer_WantedItemsView .hunt_optimizer_WantedItemsView_table_wrapper table { @@ -19,5 +19,5 @@ } .hunt_optimizer_WantedItemsView .hunt_optimizer_WantedItemsView_table_wrapper td { - padding: 0 6px 3px 0; + padding: 1px 3px; } diff --git a/src/hunt_optimizer/gui/WantedItemsView.ts b/src/hunt_optimizer/gui/WantedItemsView.ts index 03d61d48..a87e909f 100644 --- a/src/hunt_optimizer/gui/WantedItemsView.ts +++ b/src/hunt_optimizer/gui/WantedItemsView.ts @@ -10,54 +10,70 @@ import { import { WantedItemModel } from "../model"; import { NumberInput } from "../../core/gui/NumberInput"; import { hunt_optimizer_stores } from "../stores/HuntOptimizerStore"; -import { Disposable } from "../../core/observable/Disposable"; +import { ComboBox } from "../../core/gui/ComboBox"; +import { list_property } from "../../core/observable"; +import { ItemType } from "../../core/model/items"; export class WantedItemsView extends Widget { private readonly tbody_element = el.tbody(); private readonly table_disposer = this.disposable(new Disposer()); - private wanted_items_observer?: Disposable; + private readonly store_disposer = this.disposable(new Disposer()); constructor() { super(el.div({ class: "hunt_optimizer_WantedItemsView" })); + const huntable_items = list_property(); + + const combo_box = this.disposable( + new ComboBox({ + items: huntable_items, + to_label: item_type => item_type.name, + placeholder_text: "Add an item", + }), + ); + this.element.append( el.h2({ text: "Wanted Items" }), + combo_box.element, el.div( { class: "hunt_optimizer_WantedItemsView_table_wrapper" }, el.table({}, this.tbody_element), ), ); - this.disposable( + this.disposables( hunt_optimizer_stores.observe_current( hunt_optimizer_store => { - if (this.wanted_items_observer) { - this.wanted_items_observer.dispose(); - } + this.store_disposer.dispose_all(); - this.wanted_items_observer = hunt_optimizer_store.wanted_items.observe_list( - this.update_table, + this.store_disposer.add_all( + hunt_optimizer_store.wanted_items.observe_list(this.update_table), + + combo_box.selected.observe(({ value: item_type }) => { + if (item_type) { + hunt_optimizer_store.add_wanted_item(item_type); + combo_box.selected.val = undefined; + } + }), ); + + huntable_items.val = hunt_optimizer_store.huntable_item_types + .slice() + .sort((a, b) => a.name.localeCompare(b.name)); }, { call_now: true }, ), ); } - dispose(): void { - super.dispose(); - - if (this.wanted_items_observer) { - this.wanted_items_observer.dispose(); - } - } - private update_table = (change: ListPropertyChangeEvent): void => { if (change.type === ListChangeType.ListChange) { for (let i = 0; i < change.removed.length; i++) { this.tbody_element.children[change.index].remove(); } + this.table_disposer.dispose_at(change.index, change.removed.length); + const rows = change.inserted.map(this.create_row); if (change.index >= this.tbody_element.childElementCount) { @@ -74,16 +90,24 @@ export class WantedItemsView extends Widget { }; private create_row = (wanted_item: WantedItemModel): HTMLTableRowElement => { - const amount_input = this.table_disposer.add( + const row_disposer = this.table_disposer.add(new Disposer()); + + const amount_input = row_disposer.add( new NumberInput(wanted_item.amount.val, { min: 0, step: 1 }), ); - this.table_disposer.add_all( + row_disposer.add_all( amount_input.value.bind_to(wanted_item.amount), amount_input.value.observe(({ value }) => wanted_item.set_amount(value)), ); - const remove_button = this.table_disposer.add(new Button("", { icon_left: Icon.Remove })); + const remove_button = row_disposer.add(new Button("", { icon_left: Icon.Remove })); + + row_disposer.add( + remove_button.click.observe(async () => + (await hunt_optimizer_stores.current.val).remove_wanted_item(wanted_item), + ), + ); return el.tr( {}, diff --git a/src/hunt_optimizer/stores/HuntOptimizerStore.ts b/src/hunt_optimizer/stores/HuntOptimizerStore.ts index a1a3690e..175aac26 100644 --- a/src/hunt_optimizer/stores/HuntOptimizerStore.ts +++ b/src/hunt_optimizer/stores/HuntOptimizerStore.ts @@ -63,6 +63,16 @@ class HuntOptimizerStore implements Disposable { this.disposer.dispose(); } + add_wanted_item(item_type: ItemType): void { + if (!this._wanted_items.val.find(wanted => wanted.item_type === item_type)) { + this._wanted_items.push(new WantedItemModel(item_type, 1)); + } + } + + remove_wanted_item(wanted_item: WantedItemModel): void { + this._wanted_items.remove(wanted_item); + } + private optimize = ( wanted_items: WantedItemModel[], methods: HuntMethodModel[], diff --git a/src/old/core/ui/BigSelect.css b/src/old/core/ui/BigSelect.css deleted file mode 100644 index 46e6b430..00000000 --- a/src/old/core/ui/BigSelect.css +++ /dev/null @@ -1,41 +0,0 @@ -.main:global(.Select > .Select-control) { - cursor: pointer; - background-color: var(--background-color); - color: var(--text-color); - height: 28px; - border-color: var(--input-border-color); - border-radius: 0; -} - -.main:global(.Select .Select-control .Select-value .Select-value-label) { - color: white !important; -} - -.main:global(.Select .Select-placeholder), -.main:global(.Select .Select--single > .Select-control .Select-value) { - line-height: 28px; -} - -.main:global(.Select .Select-input) { - height: 26px; -} - -.main:global(.Select:hover > .Select-control) { - border-color: var(--hover-color); -} - -.main:global(.Select.is-focused > .Select-control) { - background-color: var(--background-color); - border-color: var(--hover-color); -} - -.main:global(.Select.is-focused:not(.is-open) > .Select-control) { - background-color: var(--background-color); - border-color: var(--hover-color); -} - -.main:global(.Select > .Select-menu-outer) { - margin-top: 0; - background-color: var(--background-color); - border-color: var(--border-color); -} diff --git a/src/old/core/ui/BigSelect.tsx b/src/old/core/ui/BigSelect.tsx deleted file mode 100644 index 28a8dfe1..00000000 --- a/src/old/core/ui/BigSelect.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { PureComponent, ReactNode } from "react"; -import { - OptionValues, - ReactAsyncSelectProps, - ReactCreatableSelectProps, - ReactSelectProps, -} from "react-select"; -import VirtualizedSelect, { AdditionalVirtualizedSelectProps } from "react-virtualized-select"; -import styles from "./BigSelect.css"; - -/** - * Simply wraps {@link VirtualizedSelect} to provide consistent styling. - */ -export class BigSelect extends PureComponent< - VirtualizedSelectProps -> { - render(): ReactNode { - return ; - } -} - -// Copied from react-virtualized-select. -type VirtualizedSelectProps = - | (ReactCreatableSelectProps & - ReactAsyncSelectProps & - AdditionalVirtualizedSelectProps & { async: true }) - | ReactCreatableSelectProps & - ReactSelectProps & - AdditionalVirtualizedSelectProps; diff --git a/src/old/core/ui/BigTable.css b/src/old/core/ui/BigTable.css deleted file mode 100644 index 74daaa7b..00000000 --- a/src/old/core/ui/BigTable.css +++ /dev/null @@ -1,98 +0,0 @@ -.main { - /* - position: relative; necessary to avoid background and border disappearing while antd animates - dropdowns in Chrome. No idea why this prevents it... - */ - position: relative; - border: solid 1px var(--table-border-color); - background-color: var(--foreground-color); -} - -.main * { - scrollbar-color: var(--table-scrollbar-thumb-color) var(--table-scrollbar-color); -} - -.main ::-webkit-scrollbar { - background-color: var(--table-scrollbar-color); -} - -.main ::-webkit-scrollbar-track { - background-color: var(--table-scrollbar-color); -} - -.main ::-webkit-scrollbar-thumb { - background-color: var(--table-scrollbar-thumb-color); -} - -.main ::-webkit-scrollbar-corner { - background-color: var(--table-scrollbar-color); -} - -.header { - user-select: none; - background-color: hsl(0, 0%, 32%); - font-weight: bold; -} - -.header .cell { - border-right: solid 1px var(--table-border-color); -} - -.header .cell.sortable { - cursor: pointer; -} - -.header .cell .sort_indictator { - fill: currentColor; -} - -.cell { - display: flex; - align-items: center; - box-sizing: border-box; - padding: 0 5px; - border-bottom: solid 1px var(--table-border-color); - border-right: solid 1px hsl(0, 0%, 29%); -} - -.cell.last_in_row { - border-right: solid 1px var(--table-border-color); -} - -.cell:global(.number) { - justify-content: flex-end; -} - -.cell.footer_cell { - font-weight: bold; -} - -.cell.custom { - padding: 0; -} - -.cell > .cell_text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.cell > :global(.ant-time-picker) { - /* Cover the default borders. */ - margin: -1px; - height: calc(100% + 2px); -} - -/* Make sure the glowing border is entirely visible. */ -.cell > :global(.ant-time-picker):hover { - z-index: 10; -} - -.cell > :global(.ant-time-picker) input { - height: 100%; -} - -.no_result { - margin: 20px; - color: var(--text-color-disabled); -} diff --git a/src/old/core/ui/BigTable.tsx b/src/old/core/ui/BigTable.tsx deleted file mode 100644 index 7c864710..00000000 --- a/src/old/core/ui/BigTable.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import React, { ReactNode, Component } from "react"; -import { - GridCellRenderer, - Index, - MultiGrid, - SortDirectionType, - SortDirection, -} from "react-virtualized"; -import styles from "./BigTable.css"; - -export interface Column { - key?: string; - name: string; - width: number; - cell_renderer: (record: T) => ReactNode; - tooltip?: (record: T) => string; - footer_value?: string; - footer_tooltip?: string; - /** - * "number" has special meaning. - */ - class_name?: string; - sortable?: boolean; -} - -export type ColumnSort = { column: Column; direction: SortDirectionType }; - -/** - * A table with a fixed header. Optionally has fixed columns and a footer. - * Uses windowing to support large amounts of rows and columns. - * TODO: no-content message. - */ -export class BigTable extends Component<{ - width: number; - height: number; - row_count: number; - overscan_row_count?: number; - columns: Column[]; - fixed_column_count?: number; - overscan_column_count?: number; - record: (index: Index) => T; - footer?: boolean; - /** - * When this changes, the DataTable will re-render. - */ - update_trigger?: any; - sort?: (sort_columns: ColumnSort[]) => void; -}> { - private sort_columns = new Array>(); - - render(): ReactNode { - return ( -
- -
- ); - } - - private column_width = ({ index }: Index): number => { - return this.props.columns[index].width; - }; - - private cell_renderer: GridCellRenderer = ({ columnIndex, rowIndex, style }): ReactNode => { - const column = this.props.columns[columnIndex]; - let cell: ReactNode; - let sort_indicator: ReactNode; - let title: string | undefined; - const classes = [styles.cell]; - - if (columnIndex === this.props.columns.length - 1) { - classes.push(styles.last_in_row); - } - - if (rowIndex === 0) { - // Header row - cell = title = column.name; - - if (column.sortable) { - classes.push(styles.sortable); - - const sort = this.sort_columns[0]; - - if (sort && sort.column === column) { - if (sort.direction === SortDirection.ASC) { - sort_indicator = ( - - - - - ); - } else { - sort_indicator = ( - - - - - ); - } - } - } - } else { - // Record or footer row - if (column.class_name) { - classes.push(column.class_name); - } - - if (this.props.footer && rowIndex === 1 + this.props.row_count) { - // Footer row - classes.push(styles.footer_cell); - cell = column.footer_value == null ? "" : column.footer_value; - title = column.footer_tooltip == null ? "" : column.footer_tooltip; - } else { - // Record row - const result = this.props.record({ index: rowIndex - 1 }); - - cell = column.cell_renderer(result); - - if (column.tooltip) { - title = column.tooltip(result); - } - } - } - - if (typeof cell !== "string") { - classes.push(styles.custom); - } - - const on_click = - rowIndex === 0 && column.sortable ? () => this.header_clicked(column) : undefined; - - return ( -
- {typeof cell === "string" ? {cell} : cell} - {sort_indicator} -
- ); - }; - - private header_clicked = (column: Column): void => { - const old_index = this.sort_columns.findIndex(sc => sc.column === column); - let old = old_index === -1 ? undefined : this.sort_columns.splice(old_index, 1)[0]; - - const direction = - old_index === 0 && old && old.direction === SortDirection.ASC - ? SortDirection.DESC - : SortDirection.ASC; - - this.sort_columns.unshift({ column, direction }); - this.sort_columns.splice(10); - - if (this.props.sort) { - this.props.sort(this.sort_columns); - } - }; -} diff --git a/src/quest_editor/gui/QuestEditorToolBar.ts b/src/quest_editor/gui/QuestEditorToolBar.ts index 66724746..e3857883 100644 --- a/src/quest_editor/gui/QuestEditorToolBar.ts +++ b/src/quest_editor/gui/QuestEditorToolBar.ts @@ -7,7 +7,7 @@ import { Select } from "../../core/gui/Select"; import { list_property } from "../../core/observable"; import { AreaModel } from "../model/AreaModel"; import { Icon } from "../../core/gui/dom"; -import { DropDownButton } from "../../core/gui/DropDownButton"; +import { DropDown } from "../../core/gui/DropDown"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { area_store } from "../stores/AreaStore"; import { gui_store, GuiTool } from "../../core/stores/GuiStore"; @@ -15,7 +15,7 @@ import { asm_editor_store } from "../stores/AsmEditorStore"; export class QuestEditorToolBar extends ToolBar { constructor() { - const new_quest_button = new DropDownButton( + const new_quest_button = new DropDown( "New quest", [Episode.I], episode => `Episode ${Episode[episode]}`,