Refactored table from MethodsForEpisodeView into a reusable Table widget.

This commit is contained in:
Daan Vanden Bosch 2019-09-07 17:49:21 +02:00
parent 1c2473c24f
commit a28a8ce624
10 changed files with 344 additions and 289 deletions

72
src/core/gui/Table.css Normal file
View File

@ -0,0 +1,72 @@
.core_Table {
display: block;
box-sizing: border-box;
overflow: auto;
background-color: var(--bg-color);
border-collapse: collapse;
}
.core_Table tr {
display: flex;
align-items: stretch;
}
.core_Table thead tr {
position: sticky;
top: 0;
z-index: 2;
}
.core_Table th,
.core_Table td {
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
padding: 3px 6px;
border-right: solid 1px var(--border-color);
border-bottom: solid 1px var(--border-color);
background-color: var(--bg-color);
}
.core_Table tbody {
user-select: text;
cursor: text;
}
.core_Table tbody th,
.core_Table tbody td {
white-space: nowrap;
}
.core_Table tbody th {
text-align: left;
}
.core_Table th.input {
padding: 0;
overflow: visible;
}
.core_Table th.input .core_DurationInput {
z-index: 0;
height: 100%;
width: 100%;
border: none;
}
.core_Table th.input .core_DurationInput:hover,
.core_Table th.input .core_DurationInput:focus-within {
margin: -1px;
height: calc(100% + 2px);
width: calc(100% + 2px);
}
.core_Table th.input .core_DurationInput:hover {
z-index: 4;
border: var(--input-border-hover);
}
.core_Table th.input .core_DurationInput:focus-within {
z-index: 6;
border: var(--input-border-focus);
}

112
src/core/gui/Table.ts Normal file
View File

@ -0,0 +1,112 @@
import { Widget, WidgetOptions } from "./Widget";
import { el } from "./dom";
import {
ListChangeType,
ListProperty,
ListPropertyChangeEvent,
} from "../observable/property/list/ListProperty";
import { Disposer } from "../observable/Disposer";
import "./Table.css";
export type Column<T> = {
title: string;
sticky?: boolean;
width?: number;
create_cell(value: T, disposer: Disposer): HTMLTableCellElement;
};
export type TableOptions<T> = WidgetOptions & {
values: ListProperty<T>;
columns: Column<T>[];
};
export class Table<T> extends Widget<HTMLTableElement> {
private readonly table_disposer = this.disposable(new Disposer());
private readonly tbody_element = el.tbody();
private readonly values: ListProperty<T>;
private readonly columns: Column<T>[];
constructor(options: TableOptions<T>) {
super(el.table({ class: "core_Table" }), options);
this.values = options.values;
this.columns = options.columns;
const thead_element = el.thead();
const header_tr_element = el.tr();
let left = 0;
header_tr_element.append(
...this.columns.map(column => {
const th = el.th({
text: column.title,
});
if (column.width != undefined) th.style.width = `${column.width}px`;
if (column.sticky) {
th.style.position = "sticky";
th.style.left = `${left}px`;
left += column.width || 0;
}
return th;
}),
);
thead_element.append(header_tr_element);
this.tbody_element = el.tbody();
this.element.append(thead_element, this.tbody_element);
this.disposables(this.values.observe_list(this.update_table));
}
private update_table = (change: ListPropertyChangeEvent<T>): void => {
if (change.type === ListChangeType.ListChange) {
this.splice_rows(change.index, change.removed.length, change.inserted);
} else if (change.type === ListChangeType.ValueChange) {
// TODO: update rows
}
};
private splice_rows = (index: number, amount: number, inserted: T[]) => {
for (let i = 0; i < amount; i++) {
this.tbody_element.children[index].remove();
}
this.table_disposer.dispose_at(index, amount);
const rows = inserted.map((value, i) => this.create_row(index + i, value));
if (index >= this.tbody_element.childElementCount) {
this.tbody_element.append(...rows);
} else {
for (let i = 0; i < amount; i++) {
this.tbody_element.children[index + i].insertAdjacentElement("afterend", rows[i]);
}
}
};
private create_row = (index: number, value: T): HTMLTableRowElement => {
const disposer = this.table_disposer.add(new Disposer());
let left = 0;
return el.tr(
{},
...this.columns.map(column => {
const cell = column.create_cell(value, disposer);
if (column.width != undefined) cell.style.width = `${column.width}px`;
if (column.sticky) {
cell.style.position = "sticky";
cell.style.left = `${left}px`;
left += column.width || 0;
}
return cell;
}),
);
};
}

View File

@ -7,6 +7,7 @@ import { WidgetProperty } from "../observable/property/WidgetProperty";
import { Property } from "../observable/property/Property"; import { Property } from "../observable/property/Property";
export type WidgetOptions = { export type WidgetOptions = {
class?: string;
enabled?: boolean | Property<boolean>; enabled?: boolean | Property<boolean>;
tooltip?: string | Property<string>; tooltip?: string | Property<string>;
}; };
@ -52,6 +53,10 @@ export abstract class Widget<E extends HTMLElement = HTMLElement> implements Dis
this.tooltip = this._tooltip; this.tooltip = this._tooltip;
if (options) { if (options) {
if (options.class) {
this.element.classList.add(options.class);
}
if (typeof options.enabled === "boolean") { if (typeof options.enabled === "boolean") {
this.enabled.val = options.enabled; this.enabled.val = options.enabled;
} else if (options.enabled) { } else if (options.enabled) {

View File

@ -19,7 +19,11 @@ export class Disposer implements Disposable {
} }
private _disposed = false; private _disposed = false;
private readonly disposables: Disposable[] = []; private readonly disposables: Disposable[];
constructor(...disposables: Disposable[]) {
this.disposables = disposables;
}
/** /**
* Add a single disposable and return the given disposable. * Add a single disposable and return the given disposable.
@ -32,6 +36,17 @@ export class Disposer implements Disposable {
return disposable; return disposable;
} }
/**
* Insert a single disposable at the given index and return the given disposable.
*/
insert<T extends Disposable>(index: number, disposable: T): T {
if (!this._disposed) {
this.disposables.splice(index, 0, disposable);
}
return disposable;
}
/** /**
* Add 0 or more disposables. * Add 0 or more disposables.
*/ */
@ -47,13 +62,7 @@ export class Disposer implements Disposable {
* Disposes all held disposables. * Disposes all held disposables.
*/ */
dispose_all(): void { dispose_all(): void {
for (const disposable of this.disposables.splice(0, this.disposables.length)) { this.dispose_at(0, this.disposables.length);
try {
disposable.dispose();
} catch (e) {
logger.warn("Error while disposing.", e);
}
}
} }
/** /**
@ -63,4 +72,14 @@ export class Disposer implements Disposable {
this.dispose_all(); this.dispose_all();
this._disposed = true; this._disposed = true;
} }
dispose_at(index: number, amount: number = 1): void {
for (const disposable of this.disposables.splice(index, amount)) {
try {
disposable.dispose();
} catch (e) {
logger.warn("Error while disposing.", e);
}
}
}
} }

View File

@ -2,45 +2,23 @@ import { Property } from "../Property";
import { Disposable } from "../../Disposable"; import { Disposable } from "../../Disposable";
export enum ListChangeType { export enum ListChangeType {
Insertion, ListChange,
Removal, ValueChange,
Replacement,
Update,
} }
export type ListPropertyChangeEvent<T> = export type ListPropertyChangeEvent<T> = ListChange<T> | ListValueChange<T>;
| ListInsertion<T>
| ListRemoval<T>
| ListReplacement<T>
| ListUpdate<T>;
export type ListInsertion<T> = { export type ListChange<T> = {
readonly type: ListChangeType.Insertion; readonly type: ListChangeType.ListChange;
readonly inserted: T[];
readonly from: number;
readonly to: number;
};
export type ListRemoval<T> = {
readonly type: ListChangeType.Removal;
readonly removed: T[];
readonly from: number;
readonly to: number;
};
export type ListReplacement<T> = {
readonly type: ListChangeType.Replacement;
readonly removed: T[];
readonly inserted: T[];
readonly from: number;
readonly removed_to: number;
readonly inserted_to: number;
};
export type ListUpdate<T> = {
readonly type: ListChangeType.Update;
readonly updated: T[];
readonly index: number; readonly index: number;
readonly removed: T[];
readonly inserted: T[];
};
export type ListValueChange<T> = {
readonly type: ListChangeType.ValueChange;
readonly index: number;
readonly updated: T[];
}; };
export interface ListProperty<T> extends Property<T[]> { export interface ListProperty<T> extends Property<T[]> {

View File

@ -23,45 +23,43 @@ export class SimpleListProperty<T> extends AbstractProperty<T[]>
} }
get_val(): T[] { get_val(): T[] {
return this.elements; return this.values;
} }
set_val(elements: T[]): T[] { set_val(elements: T[]): T[] {
const removed = this.elements.splice(0, this.elements.length, ...elements); const removed = this.values.splice(0, this.values.length, ...elements);
this.finalize_update({ this.finalize_update({
type: ListChangeType.Replacement, type: ListChangeType.ListChange,
index: 0,
removed, removed,
inserted: elements, inserted: elements,
from: 0,
removed_to: removed.length,
inserted_to: elements.length,
}); });
return removed; return removed;
} }
private readonly _length = property(0); private readonly _length = property(0);
private readonly elements: T[]; private readonly values: T[];
private readonly extract_observables?: (element: T) => Observable<any>[]; private readonly extract_observables?: (element: T) => Observable<any>[];
/** /**
* Internal observers which observe observables related to this list's elements so that their * Internal observers which observe observables related to this list's values so that their
* changes can be propagated via update events. * changes can be propagated via update events.
*/ */
private readonly element_observers: { index: number; disposables: Disposable[] }[] = []; private readonly value_observers: { index: number; disposables: Disposable[] }[] = [];
/** /**
* External observers which are observing this list. * External observers which are observing this list.
*/ */
private readonly list_observers: ((change: ListPropertyChangeEvent<T>) => void)[] = []; private readonly list_observers: ((change: ListPropertyChangeEvent<T>) => void)[] = [];
/** /**
* @param extract_observables - Extractor function called on each element in this list. Changes * @param extract_observables - Extractor function called on each value in this list. Changes
* to the returned observables will be propagated via update events. * to the returned observables will be propagated via update events.
* @param elements - Initial elements of this list. * @param values - Initial values of this list.
*/ */
constructor(extract_observables?: (element: T) => Observable<any>[], ...elements: T[]) { constructor(extract_observables?: (element: T) => Observable<any>[], ...values: T[]) {
super(); super();
this.length = this._length; this.length = this._length;
this.elements = elements; this.values = values;
this.extract_observables = extract_observables; this.extract_observables = extract_observables;
} }
@ -69,8 +67,8 @@ export class SimpleListProperty<T> extends AbstractProperty<T[]>
observer: (change: ListPropertyChangeEvent<T>) => void, observer: (change: ListPropertyChangeEvent<T>) => void,
options?: { call_now?: true }, options?: { call_now?: true },
): Disposable { ): Disposable {
if (this.element_observers.length === 0 && this.extract_observables) { if (this.value_observers.length === 0 && this.extract_observables) {
this.replace_element_observers(this.elements, 0, Infinity); this.replace_element_observers(0, Infinity, this.values);
} }
if (!this.list_observers.includes(observer)) { if (!this.list_observers.includes(observer)) {
@ -79,10 +77,10 @@ export class SimpleListProperty<T> extends AbstractProperty<T[]>
if (options && options.call_now) { if (options && options.call_now) {
this.call_list_observer(observer, { this.call_list_observer(observer, {
type: ListChangeType.Insertion, type: ListChangeType.ListChange,
inserted: this.elements, index: 0,
from: 0, removed: [],
to: this.elements.length, inserted: this.values,
}); });
} }
@ -95,13 +93,13 @@ export class SimpleListProperty<T> extends AbstractProperty<T[]>
} }
if (this.list_observers.length === 0) { if (this.list_observers.length === 0) {
for (const { disposables } of this.element_observers) { for (const { disposables } of this.value_observers) {
for (const disposable of disposables) { for (const disposable of disposables) {
disposable.dispose(); disposable.dispose();
} }
} }
this.element_observers.splice(0, Infinity); this.value_observers.splice(0, Infinity);
} }
}, },
}; };
@ -116,35 +114,31 @@ export class SimpleListProperty<T> extends AbstractProperty<T[]>
} }
update(f: (element: T[]) => T[]): void { update(f: (element: T[]) => T[]): void {
this.splice(0, this.elements.length, ...f(this.elements)); this.splice(0, this.values.length, ...f(this.values));
} }
get(index: number): T { get(index: number): T {
return this.elements[index]; return this.values[index];
} }
set(index: number, element: T): void { set(index: number, element: T): void {
const removed = [this.elements[index]]; const removed = [this.values[index]];
this.elements[index] = element; this.values[index] = element;
this.finalize_update({ this.finalize_update({
type: ListChangeType.Replacement, type: ListChangeType.ListChange,
index,
removed, removed,
inserted: [element], inserted: [element],
from: index,
removed_to: index + 1,
inserted_to: index + 1,
}); });
} }
clear(): void { clear(): void {
const removed = this.elements.splice(0, this.elements.length); const removed = this.values.splice(0, this.values.length);
this.finalize_update({ this.finalize_update({
type: ListChangeType.Replacement, type: ListChangeType.ListChange,
index: 0,
removed, removed,
inserted: [], inserted: [],
from: 0,
removed_to: removed.length,
inserted_to: 0,
}); });
} }
@ -152,18 +146,16 @@ export class SimpleListProperty<T> extends AbstractProperty<T[]>
let removed: T[]; let removed: T[];
if (delete_count == undefined) { if (delete_count == undefined) {
removed = this.elements.splice(index); removed = this.values.splice(index);
} else { } else {
removed = this.elements.splice(index, delete_count, ...items); removed = this.values.splice(index, delete_count, ...items);
} }
this.finalize_update({ this.finalize_update({
type: ListChangeType.Replacement, type: ListChangeType.ListChange,
index,
removed, removed,
inserted: items, inserted: items,
from: index,
removed_to: index + removed.length,
inserted_to: index + items.length,
}); });
return removed; return removed;
@ -171,39 +163,27 @@ export class SimpleListProperty<T> extends AbstractProperty<T[]>
/** /**
* Does the following in the given order: * Does the following in the given order:
* - Updates element observers * - Updates value observers
* - Emits ListPropertyChangeEvent * - Emits ListPropertyChangeEvent
* - Emits PropertyChangeEvent * - Emits PropertyChangeEvent
* - Sets length * - Sets length
*/ */
protected finalize_update(change: ListPropertyChangeEvent<T>): void { protected finalize_update(change: ListPropertyChangeEvent<T>): void {
if (this.list_observers.length && this.extract_observables) { if (
switch (change.type) { this.list_observers.length &&
case ListChangeType.Insertion: this.extract_observables &&
this.replace_element_observers(change.inserted, change.from, 0); change.type === ListChangeType.ListChange
break; ) {
this.replace_element_observers(change.index, change.removed.length, change.inserted);
case ListChangeType.Removal:
this.replace_element_observers([], change.from, change.removed.length);
break;
case ListChangeType.Replacement:
this.replace_element_observers(
change.inserted,
change.from,
change.removed.length,
);
break;
}
} }
for (const observer of this.list_observers) { for (const observer of this.list_observers) {
this.call_list_observer(observer, change); this.call_list_observer(observer, change);
} }
this.emit(this.elements); this.emit(this.values);
this._length.val = this.elements.length; this._length.val = this.values.length;
} }
private call_list_observer( private call_list_observer(
@ -217,10 +197,10 @@ export class SimpleListProperty<T> extends AbstractProperty<T[]>
} }
} }
private replace_element_observers(new_elements: T[], from: number, amount: number): void { private replace_element_observers(from: number, amount: number, new_elements: T[]): void {
let index = from; let index = from;
const removed = this.element_observers.splice( const removed = this.value_observers.splice(
from, from,
amount, amount,
...new_elements.map(element => { ...new_elements.map(element => {
@ -229,7 +209,7 @@ export class SimpleListProperty<T> extends AbstractProperty<T[]>
disposables: this.extract_observables!(element).map(observable => disposables: this.extract_observables!(element).map(observable =>
observable.observe(() => { observable.observe(() => {
this.finalize_update({ this.finalize_update({
type: ListChangeType.Update, type: ListChangeType.ValueChange,
updated: [element], updated: [element],
index: obj.index, index: obj.index,
}); });
@ -247,8 +227,8 @@ export class SimpleListProperty<T> extends AbstractProperty<T[]>
} }
} }
while (index < this.element_observers.length) { while (index < this.value_observers.length) {
this.element_observers[index].index += index; this.value_observers[index].index += index;
} }
} }
} }

View File

@ -1,91 +1,4 @@
.hunt_optimizer_MethodsForEpisodeView table { .hunt_optimizer_MethodsForEpisodeView_table {
display: block;
box-sizing: border-box;
overflow: auto;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: var(--bg-color);
border-collapse: collapse;
}
.hunt_optimizer_MethodsForEpisodeView tr {
display: flex;
align-items: stretch;
}
.hunt_optimizer_MethodsForEpisodeView thead tr {
position: sticky;
top: 0;
z-index: 2;
}
.hunt_optimizer_MethodsForEpisodeView th,
.hunt_optimizer_MethodsForEpisodeView td {
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
width: 80px;
padding: 3px 6px;
border-right: solid 1px var(--border-color);
border-bottom: solid 1px var(--border-color);
background-color: var(--bg-color);
}
.hunt_optimizer_MethodsForEpisodeView th:first-child {
position: sticky;
left: 0;
width: 250px;
}
.hunt_optimizer_MethodsForEpisodeView th:nth-child(2) {
position: sticky;
left: 250px;
width: 60px;
}
.hunt_optimizer_MethodsForEpisodeView tbody {
user-select: text;
cursor: text;
}
.hunt_optimizer_MethodsForEpisodeView tbody th,
.hunt_optimizer_MethodsForEpisodeView tbody td {
white-space: nowrap;
}
.hunt_optimizer_MethodsForEpisodeView tbody th {
text-align: left;
}
.hunt_optimizer_MethodsForEpisodeView tbody td {
text-align: right;
}
.hunt_optimizer_MethodsForEpisodeView th.input {
padding: 0;
overflow: visible;
}
.hunt_optimizer_MethodsForEpisodeView th.input .core_DurationInput {
z-index: 0;
height: 100%;
width: 100%;
border: none;
}
.hunt_optimizer_MethodsForEpisodeView th.input .core_DurationInput:hover,
.hunt_optimizer_MethodsForEpisodeView th.input .core_DurationInput:focus-within {
margin: -1px;
height: calc(100% + 2px);
width: calc(100% + 2px);
}
.hunt_optimizer_MethodsForEpisodeView th.input .core_DurationInput:hover {
z-index: 4;
border: var(--input-border-hover);
}
.hunt_optimizer_MethodsForEpisodeView th.input .core_DurationInput:focus-within {
z-index: 6;
border: var(--input-border-focus);
} }

View File

@ -12,12 +12,12 @@ import "./MethodsForEpisodeView.css";
import { Disposer } from "../../core/observable/Disposer"; import { Disposer } from "../../core/observable/Disposer";
import { DurationInput } from "../../core/gui/DurationInput"; import { DurationInput } from "../../core/gui/DurationInput";
import { Disposable } from "../../core/observable/Disposable"; import { Disposable } from "../../core/observable/Disposable";
import { Table } from "../../core/gui/Table";
import { list_property } from "../../core/observable";
export class MethodsForEpisodeView extends ResizableWidget { export class MethodsForEpisodeView extends ResizableWidget {
private readonly episode: Episode; private readonly episode: Episode;
private readonly enemy_types: NpcType[]; private readonly enemy_types: NpcType[];
private readonly tbody_element: HTMLTableSectionElement;
private readonly time_disposer = this.disposable(new Disposer());
private hunt_methods_observer?: Disposable; private hunt_methods_observer?: Disposable;
constructor(episode: Episode) { constructor(episode: Episode) {
@ -27,25 +27,55 @@ export class MethodsForEpisodeView extends ResizableWidget {
this.enemy_types = ENEMY_NPC_TYPES.filter(type => npc_data(type).episode === this.episode); this.enemy_types = ENEMY_NPC_TYPES.filter(type => npc_data(type).episode === this.episode);
const table_element = el.table(); const hunt_methods = list_property<HuntMethodModel>();
const thead_element = el.thead();
const header_tr_element = el.tr();
header_tr_element.append(el.th({ text: "Method" }), el.th({ text: "Time" })); const table = this.disposable(
new Table({
class: "hunt_optimizer_MethodsForEpisodeView_table",
values: hunt_methods,
columns: [
{
title: "Method",
sticky: true,
width: 250,
create_cell(method: HuntMethodModel): HTMLTableDataCellElement {
return el.th({ text: method.name });
},
},
{
title: "Time",
sticky: true,
width: 60,
create_cell(
method: HuntMethodModel,
disposer: Disposer,
): HTMLTableDataCellElement {
const time_input = disposer.add(new DurationInput(method.time.val));
for (const enemy_type of this.enemy_types) { disposer.add(
header_tr_element.append( time_input.value.observe(({ value }) =>
el.th({ method.set_user_time(value),
text: npc_data(enemy_type).simple_name, ),
);
return el.th({ class: "input" }, time_input.element);
},
},
...this.enemy_types.map(enemy_type => {
return {
title: npc_data(enemy_type).simple_name,
width: 80,
create_cell(method: HuntMethodModel): HTMLTableDataCellElement {
const count = method.enemy_counts.get(enemy_type);
return el.td({ text: count == undefined ? "" : count.toString() });
},
};
}),
],
}), }),
); );
}
this.tbody_element = el.tbody(); this.element.append(table.element);
thead_element.append(header_tr_element);
table_element.append(thead_element, this.tbody_element);
this.element.append(table_element);
this.disposable( this.disposable(
hunt_method_stores.observe_current( hunt_method_stores.observe_current(
@ -55,7 +85,11 @@ export class MethodsForEpisodeView extends ResizableWidget {
} }
this.hunt_methods_observer = hunt_method_store.methods.observe( this.hunt_methods_observer = hunt_method_store.methods.observe(
this.update_table, ({ value }) => {
hunt_methods.val = value.filter(
method => method.episode === this.episode,
);
},
{ {
call_now: true, call_now: true,
}, },
@ -73,35 +107,4 @@ export class MethodsForEpisodeView extends ResizableWidget {
this.hunt_methods_observer.dispose(); this.hunt_methods_observer.dispose();
} }
} }
private update_table = ({ value: methods }: { value: HuntMethodModel[] }) => {
this.time_disposer.dispose_all();
const frag = document.createDocumentFragment();
for (const method of methods) {
if (method.episode === this.episode) {
const time_input = this.time_disposer.add(new DurationInput(method.time.val));
this.time_disposer.add(
time_input.value.observe(({ value }) => method.set_user_time(value)),
);
const cells: HTMLTableCellElement[] = [
el.th({ text: method.name }),
el.th({ class: "input" }, time_input.element),
];
// One cell per enemy type.
for (const enemy_type of this.enemy_types) {
const count = method.enemy_counts.get(enemy_type);
cells.push(el.td({ text: count == undefined ? "" : count.toString() }));
}
frag.append(el.tr({}, ...cells));
}
}
this.tbody_element.innerHTML = "";
this.tbody_element.append(frag);
};
} }

View File

@ -58,51 +58,24 @@ export class WantedItemsView extends Widget {
} }
private update_table = (change: ListPropertyChangeEvent<WantedItemModel>): void => { private update_table = (change: ListPropertyChangeEvent<WantedItemModel>): void => {
switch (change.type) { if (change.type === ListChangeType.ListChange) {
case ListChangeType.Insertion: for (let i = 0; i < change.removed.length; i++) {
{ this.tbody_element.children[change.index].remove();
}
const rows = change.inserted.map(this.create_row); const rows = change.inserted.map(this.create_row);
if (change.from >= this.tbody_element.childElementCount) { if (change.index >= this.tbody_element.childElementCount) {
this.tbody_element.append(...rows); this.tbody_element.append(...rows);
} else { } else {
for (let i = change.from; i < change.to; i++) { for (let i = 0; i < change.inserted.length; i++) {
this.tbody_element.children[i].insertAdjacentElement( this.tbody_element.children[change.index + i].insertAdjacentElement(
"afterend", "afterend",
rows[i - change.from], rows[i],
); );
} }
} }
} }
break;
case ListChangeType.Removal:
for (let i = change.from; i < change.to; i++) {
this.tbody_element.children[change.from].remove();
}
break;
case ListChangeType.Replacement:
{
const rows = change.inserted.map(this.create_row);
for (let i = change.from; i < change.removed_to; i++) {
this.tbody_element.children[change.from].remove();
}
if (change.from >= this.tbody_element.childElementCount) {
this.tbody_element.append(...rows);
} else {
for (let i = change.from; i < change.inserted_to; i++) {
this.tbody_element.children[i].insertAdjacentElement(
"afterend",
rows[i - change.from],
);
}
}
}
break;
}
}; };
private create_row = (wanted_item: WantedItemModel): HTMLTableRowElement => { private create_row = (wanted_item: WantedItemModel): HTMLTableRowElement => {

View File

@ -24,7 +24,7 @@ export class HuntMethodStore implements Disposable {
private readonly disposer = new Disposer(); private readonly disposer = new Disposer();
constructor(server: Server, methods: HuntMethodModel[]) { constructor(server: Server, methods: HuntMethodModel[]) {
this.methods = list_property(undefined, ...methods); this.methods = list_property(method => [method.user_time], ...methods);
this.disposer.add( this.disposer.add(
this.methods.observe_list(() => this.methods.observe_list(() =>