Wanted items can now be added and removed.

This commit is contained in:
Daan Vanden Bosch 2019-09-13 14:30:29 +02:00
parent c9820ceccb
commit a933c5e4c1
20 changed files with 291 additions and 418 deletions

View File

@ -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] = [

30
src/core/gui/ComboBox.css Normal file
View File

@ -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);
}

87
src/core/gui/ComboBox.ts Normal file
View File

@ -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<T> = LabelledControlOptions & {
items: T[] | Property<T[]>;
to_label: (item: T) => string;
placeholder_text?: string;
};
export class ComboBox<T> extends LabelledControl {
readonly preferred_label_position = "left";
readonly selected: WritableProperty<T | undefined>;
private readonly to_label: (element: T) => string;
private readonly menu: Menu<T>;
private readonly input_element: HTMLInputElement = create_element("input");
private readonly _selected: WidgetProperty<T | undefined>;
constructor(options: ComboBoxOptions<T>) {
super(el.span({ class: "core_ComboBox core_Input" }), options);
this.to_label = options.to_label;
this._selected = new WidgetProperty<T | undefined>(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;
}
}

View File

@ -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%;

View File

@ -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<T> extends Control {
export class DropDown<T> extends Control {
readonly chosen: Observable<T>;
private readonly button: Button;
@ -22,9 +22,9 @@ export class DropDownButton<T> extends Control {
text: string,
items: T[] | Property<T[]>,
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,

View File

@ -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;
}

View File

@ -48,11 +48,11 @@ export class Menu<T> 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<T> 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 });
}
};
}

View File

@ -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;

View File

@ -27,5 +27,5 @@ export function is_property<T>(observable: Observable<T>): observable is Propert
}
export function is_any_property(observable: any): observable is Property<any> {
return observable && (observable as any).is_property;
return observable && observable.is_property;
}

View File

@ -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<T> = {
};
export interface ListProperty<T> extends Property<T[]> {
readonly is_list_property: true;
readonly length: Property<number>;
get(index: number): T;
@ -31,3 +34,11 @@ export interface ListProperty<T> extends Property<T[]> {
options?: { call_now?: boolean },
): Disposable;
}
export function is_list_property<T>(observable: Observable<T[]>): observable is ListProperty<T> {
return (observable as any).is_list_property;
}
export function is_any_list_property(observable: any): observable is ListProperty<any> {
return observable && observable.is_list_property;
}

View File

@ -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<T> extends AbstractProperty<T[]>
implements WritableListProperty<T> {
readonly is_list_property = true;
readonly length: Property<number>;
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<T> extends AbstractProperty<T[]>
}
bind_to(observable: Observable<T[]>): 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<T[]>): 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<T> extends AbstractProperty<T[]>
});
}
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<T> extends AbstractProperty<T[]>
});
}
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<T> extends AbstractProperty<T[]>
/**
* 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<T>): void {
if (
@ -165,13 +211,18 @@ export class SimpleListProperty<T> extends AbstractProperty<T[]>
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<T> extends AbstractProperty<T[]>
}
}
const shift = new_elements.length - amount;
while (index < this.value_observers.length) {
this.value_observers[index].index += index;
this.value_observers[index++].index += shift;
}
}
}

View File

@ -6,8 +6,12 @@ export interface WritableListProperty<T> extends ListProperty<T>, 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;
}

View File

@ -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;
}

View File

@ -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<ItemType>();
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<WantedItemModel>): 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(
{},

View File

@ -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[],

View File

@ -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);
}

View File

@ -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<TValue = OptionValues> extends PureComponent<
VirtualizedSelectProps<TValue>
> {
render(): ReactNode {
return <VirtualizedSelect className={styles.main} {...this.props} />;
}
}
// Copied from react-virtualized-select.
type VirtualizedSelectProps<TValue = OptionValues> =
| (ReactCreatableSelectProps<TValue> &
ReactAsyncSelectProps<TValue> &
AdditionalVirtualizedSelectProps<TValue> & { async: true })
| ReactCreatableSelectProps<TValue> &
ReactSelectProps<TValue> &
AdditionalVirtualizedSelectProps<TValue>;

View File

@ -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);
}

View File

@ -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<T> {
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<T> = { column: Column<T>; 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<T> extends Component<{
width: number;
height: number;
row_count: number;
overscan_row_count?: number;
columns: Column<T>[];
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<T>[]) => void;
}> {
private sort_columns = new Array<ColumnSort<T>>();
render(): ReactNode {
return (
<div
className={styles.main}
style={{ width: this.props.width, height: this.props.height }}
>
<MultiGrid
width={this.props.width}
height={this.props.height}
rowHeight={26}
rowCount={this.props.row_count + 1 + (this.props.footer ? 1 : 0)}
fixedRowCount={1}
overscanRowCount={this.props.overscan_row_count}
columnWidth={this.column_width}
columnCount={this.props.columns.length}
fixedColumnCount={this.props.fixed_column_count}
overscanColumnCount={this.props.overscan_column_count}
cellRenderer={this.cell_renderer}
classNameTopLeftGrid={styles.header}
classNameTopRightGrid={styles.header}
updateTigger={this.props.update_trigger}
/>
</div>
);
}
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 = (
<svg
className={styles.sort_indictator}
width="18"
height="18"
viewBox="0 0 24 24"
>
<path d="M7 14l5-5 5 5z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>
);
} else {
sort_indicator = (
<svg
className={styles.sort_indictator}
width="18"
height="18"
viewBox="0 0 24 24"
>
<path d="M7 10l5 5 5-5z" />
<path d="M0 0h24v24H0z" fill="none" />
</svg>
);
}
}
}
} 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 (
<div
className={classes.join(" ")}
key={`${columnIndex}, ${rowIndex}`}
style={style}
title={title}
onClick={on_click}
>
{typeof cell === "string" ? <span className={styles.cell_text}>{cell}</span> : cell}
{sort_indicator}
</div>
);
};
private header_clicked = (column: Column<T>): 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);
}
};
}

View File

@ -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]}`,