Refactored HTML element creation code. Removed PropertyChangeEvent, properties don't emit their old value anymore. Added an EventsController and moved some code from EventsView and QuestEditorStore to it.

This commit is contained in:
Daan Vanden Bosch 2019-12-27 00:55:32 +01:00
parent 89d9de0f12
commit 994afa7387
63 changed files with 548 additions and 543 deletions

View File

@ -1,8 +1,8 @@
import { NavigationView } from "./NavigationView";
import { MainContentView } from "./MainContentView";
import { el } from "../../core/gui/dom";
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { GuiStore, GuiTool } from "../../core/stores/GuiStore";
import { div } from "../../core/gui/dom";
/**
* The top-level view which contains all other views.
@ -19,8 +19,8 @@ export class ApplicationView extends ResizableWidget {
this.menu_view = this.disposable(new NavigationView(gui_store));
this.main_content_view = this.disposable(new MainContentView(gui_store, tool_views));
this.element = el.div(
{ class: "application_ApplicationView" },
this.element = div(
{ className: "application_ApplicationView" },
this.menu_view.element,
this.main_content_view.element,
);

View File

@ -1,11 +1,11 @@
import { el } from "../../core/gui/dom";
import { GuiStore, GuiTool } from "../../core/stores/GuiStore";
import { LazyWidget } from "../../core/gui/LazyWidget";
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { ChangeEvent } from "../../core/observable/Observable";
import { div } from "../../core/gui/dom";
export class MainContentView extends ResizableWidget {
readonly element = el.div({ class: "application_MainContentView" });
readonly element = div({ className: "application_MainContentView" });
private tool_views: Map<GuiTool, LazyWidget>;

View File

@ -1,13 +1,13 @@
import { Widget } from "../../core/gui/Widget";
import { create_element, el } from "../../core/gui/dom";
import { GuiTool } from "../../core/stores/GuiStore";
import "./NavigationButton.css";
import { input, label, span } from "../../core/gui/dom";
export class NavigationButton extends Widget {
readonly element = el.span({ class: "application_NavigationButton" });
readonly element = span({ className: "application_NavigationButton" });
private input: HTMLInputElement = create_element("input");
private label: HTMLLabelElement = create_element("label");
private input: HTMLInputElement = input();
private label: HTMLLabelElement = label();
constructor(tool: GuiTool, text: string) {
super();

View File

@ -1,4 +1,4 @@
import { el, icon, Icon } from "../../core/gui/dom";
import { a, div, icon, Icon, span } from "../../core/gui/dom";
import "./NavigationView.css";
import { GuiStore, GuiTool } from "../../core/stores/GuiStore";
import { Widget } from "../../core/gui/Widget";
@ -26,22 +26,22 @@ export class NavigationView extends Widget {
}),
);
readonly element = el.div(
{ class: "application_NavigationView" },
readonly element = div(
{ className: "application_NavigationView" },
...[...this.buttons.values()].map(button => button.element),
el.div({ class: "application_NavigationView_spacer" }),
div({ className: "application_NavigationView_spacer" }),
el.span(
{ class: "application_NavigationView_server" },
span(
{ className: "application_NavigationView_server" },
this.server_select.label!.element,
this.server_select.element,
),
el.a(
a(
{
class: "application_NavigationView_github",
className: "application_NavigationView_github",
href: "https://github.com/DaanVandenBosch/phantasmal-world",
title: "GitHub",
},

View File

@ -1,4 +1,4 @@
import { el, Icon, icon } from "./dom";
import { button, Icon, icon, span } from "./dom";
import "./Button.css";
import { Observable } from "../observable/Observable";
import { emitter } from "../observable";
@ -15,7 +15,7 @@ export type ButtonOptions = WidgetOptions & {
};
export class Button extends Control {
readonly element = el.button({ class: "core_Button" });
readonly element = button({ className: "core_Button" });
readonly mousedown: Observable<MouseEvent>;
readonly mouseup: Observable<MouseEvent>;
readonly click: Observable<MouseEvent>;
@ -30,18 +30,20 @@ export class Button extends Control {
constructor(text: string | Property<string>, options?: ButtonOptions) {
super(options);
const inner_element = el.span({ class: "core_Button_inner" });
const inner_element = span({ className: "core_Button_inner" });
this.center_element = el.span({ class: "core_Button_center" });
this.center_element = span({ className: "core_Button_center" });
if (options && options.icon_left != undefined) {
inner_element.append(el.span({ class: "core_Button_left" }, icon(options.icon_left)));
inner_element.append(span({ className: "core_Button_left" }, icon(options.icon_left)));
}
inner_element.append(this.center_element);
if (options && options.icon_right != undefined) {
inner_element.append(el.span({ class: "core_Button_right" }, icon(options.icon_right)));
inner_element.append(
span({ className: "core_Button_right" }, icon(options.icon_right)),
);
}
this._mousedown = emitter<MouseEvent>();

View File

@ -1,12 +1,12 @@
import { create_element } from "./dom";
import { WritableProperty } from "../observable/property/WritableProperty";
import { LabelledControl, LabelledControlOptions } from "./LabelledControl";
import { WidgetProperty } from "../observable/property/WidgetProperty";
import { input } from "./dom";
export type CheckBoxOptions = LabelledControlOptions;
export class CheckBox extends LabelledControl {
readonly element = create_element<HTMLInputElement>("input", { class: "core_CheckBox" });
readonly element = input({ className: "core_CheckBox" });
readonly preferred_label_position = "right";

View File

@ -1,5 +1,5 @@
import { LabelledControl, LabelledControlOptions } from "./LabelledControl";
import { create_element, el, Icon, icon } from "./dom";
import { Icon, icon, input, span } from "./dom";
import "./ComboBox.css";
import "./Input.css";
import { Menu } from "./Menu";
@ -15,7 +15,7 @@ export type ComboBoxOptions<T> = LabelledControlOptions & {
};
export class ComboBox<T> extends LabelledControl {
readonly element = el.span({ class: "core_ComboBox core_Input" });
readonly element = span({ className: "core_ComboBox core_Input" });
readonly preferred_label_position = "left";
@ -23,7 +23,7 @@ export class ComboBox<T> extends LabelledControl {
private readonly to_label: (element: T) => string;
private readonly menu: Menu<T>;
private readonly input_element: HTMLInputElement = create_element("input");
private readonly input_element: HTMLInputElement = input();
private readonly _selected: WidgetProperty<T | undefined>;
constructor(options: ComboBoxOptions<T>) {
@ -89,17 +89,17 @@ export class ComboBox<T> extends LabelledControl {
this.menu.visible.set_val(false, { silent: false });
};
const down_arrow_element = el.span({}, icon(Icon.TriangleDown));
const down_arrow_element = span(icon(Icon.TriangleDown));
this.bind_hidden(down_arrow_element, this.menu.visible);
const up_arrow_element = el.span({}, icon(Icon.TriangleUp));
const up_arrow_element = span(icon(Icon.TriangleUp));
this.bind_hidden(
up_arrow_element,
this.menu.visible.map(v => !v),
);
const button_element = el.span(
{ class: "core_ComboBox_button" },
const button_element = span(
{ className: "core_ComboBox_button" },
down_arrow_element,
up_arrow_element,
);
@ -109,8 +109,8 @@ export class ComboBox<T> extends LabelledControl {
};
this.element.append(
el.span(
{ class: "core_ComboBox_inner core_Input_inner" },
span(
{ className: "core_ComboBox_inner core_Input_inner" },
this.input_element,
button_element,
),

View File

@ -1,4 +1,4 @@
import { disposable_listener, el, Icon } from "./dom";
import { disposable_listener, div, Icon } from "./dom";
import "./DropDown.css";
import { Property } from "../observable/property/Property";
import { Button, ButtonOptions } from "./Button";
@ -15,7 +15,7 @@ export type DropDownOptions<T> = ButtonOptions & {
};
export class DropDown<T> extends Control {
readonly element = el.div({ class: "core_DropDown" });
readonly element = div({ className: "core_DropDown" });
readonly chosen: Observable<T>;

View File

@ -1,7 +1,7 @@
import { ResizableWidget } from "./ResizableWidget";
import { el } from "./dom";
import { UnavailableView } from "../../quest_editor/gui/UnavailableView";
import "./ErrorView.css";
import { div } from "./dom";
export class ErrorView extends ResizableWidget {
readonly element: HTMLElement;
@ -9,11 +9,11 @@ export class ErrorView extends ResizableWidget {
constructor(message: string) {
super();
this.element = el.div(
{
class: "core_ErrorView",
},
this.element = div(
{ className: "core_ErrorView" },
this.disposable(new UnavailableView(message)).element,
);
this.finalize_construction();
}
}

View File

@ -1,4 +1,4 @@
import { create_element, el, icon, Icon } from "./dom";
import { icon, Icon, input, label, span } from "./dom";
import "./FileButton.css";
import { property } from "../observable";
import { Property } from "../observable/property/Property";
@ -11,14 +11,14 @@ export type FileButtonOptions = ControlOptions & {
};
export class FileButton extends Control {
readonly element = create_element("label", {
class: "core_FileButton core_Button",
readonly element = label({
className: "core_FileButton core_Button",
});
readonly files: Property<File[]>;
private input: HTMLInputElement = create_element("input", {
class: "core_FileButton_input core_Button_inner",
private input: HTMLInputElement = input({
className: "core_FileButton_input core_Button_inner",
});
private readonly _files: WritableProperty<File[]> = property<File[]>([]);
@ -39,20 +39,20 @@ export class FileButton extends Control {
if (options && options.accept) this.input.accept = options.accept;
const inner_element = el.span({
class: "core_FileButton_inner core_Button_inner",
const inner_element = span({
className: "core_FileButton_inner core_Button_inner",
});
if (options && options.icon_left != undefined) {
inner_element.append(
el.span(
{ class: "core_FileButton_left core_Button_left" },
span(
{ className: "core_FileButton_left core_Button_left" },
icon(options.icon_left),
),
);
}
inner_element.append(el.span({ class: "core_Button_center", text }));
inner_element.append(span({ className: "core_Button_center" }, text));
this.element.append(inner_element, this.input);

View File

@ -1,9 +1,9 @@
import { LabelledControl, LabelledControlOptions } from "./LabelledControl";
import { create_element, el } from "./dom";
import { WritableProperty } from "../observable/property/WritableProperty";
import { is_property, Property } from "../observable/property/Property";
import "./Input.css";
import { WidgetProperty } from "../observable/property/WidgetProperty";
import { input, span } from "./dom";
export type InputOptions = { readonly readonly?: boolean } & LabelledControlOptions;
@ -25,13 +25,13 @@ export abstract class Input<T> extends LabelledControl {
) {
super(options);
this.element = el.span({ class: `${class_name} core_Input` });
this.element = span({ className: `${class_name} core_Input` });
this._value = new WidgetProperty<T>(this, value, this.set_value);
this.value = this._value;
this.input_element = create_element("input", {
class: `${input_class_name} core_Input_inner`,
this.input_element = input({
className: `${input_class_name} core_Input_inner`,
});
this.input_element.type = input_type;
this.input_element.addEventListener("change", () => {

View File

@ -1,12 +1,12 @@
import { WidgetOptions, Widget } from "./Widget";
import { create_element } from "./dom";
import { Widget, WidgetOptions } from "./Widget";
import { WritableProperty } from "../observable/property/WritableProperty";
import "./Label.css";
import { Property } from "../observable/property/Property";
import { WidgetProperty } from "../observable/property/WidgetProperty";
import { label } from "./dom";
export class Label extends Widget {
readonly element = create_element<HTMLLabelElement>("label", { class: "core_Label" });
readonly element = label({ className: "core_Label" });
set for(id: string) {
this.element.htmlFor = id;

View File

@ -1,10 +1,10 @@
import { Widget } from "./Widget";
import { el } from "./dom";
import { Resizable } from "./Resizable";
import { ResizableWidget } from "./ResizableWidget";
import { div } from "./dom";
export class LazyWidget extends ResizableWidget {
readonly element = el.div({ class: "core_LazyView" });
readonly element = div({ className: "core_LazyView" });
private initialized = false;
private view: (Widget & Resizable) | undefined;

View File

@ -1,4 +1,4 @@
import { disposable_listener, el } from "./dom";
import { disposable_listener, div } from "./dom";
import { Widget } from "./Widget";
import { is_property, Property } from "../observable/property/Property";
import { property } from "../observable";
@ -13,12 +13,12 @@ export type MenuOptions<T> = {
};
export class Menu<T> extends Widget {
readonly element = el.div({ class: "core_Menu", tab_index: -1 });
readonly element = div({ className: "core_Menu", tabIndex: -1 });
readonly selected: WritableProperty<T | undefined>;
private readonly to_label: (element: T) => string;
private readonly items: Property<readonly T[]>;
private readonly inner_element = el.div({ class: "core_Menu_inner" });
private readonly inner_element = div({ className: "core_Menu_inner" });
private readonly related_element: HTMLElement;
private readonly _selected: WidgetProperty<T | undefined>;
private hovered_index?: number;
@ -48,10 +48,7 @@ export class Menu<T> extends Widget {
this.inner_element.innerHTML = "";
this.inner_element.append(
...items.map((item, index) =>
el.div({
text: this.to_label(item),
data: { index: index.toString() },
}),
div({ data: { index: index.toString() } }, this.to_label(item)),
),
);
this.hover_item();

View File

@ -1,9 +1,9 @@
import { ResizableWidget } from "./ResizableWidget";
import { el } from "./dom";
import { Renderer } from "../rendering/Renderer";
import { div } from "./dom";
export class RendererWidget extends ResizableWidget {
readonly element = el.div({ class: "core_RendererWidget" });
readonly element = div({ className: "core_RendererWidget" });
constructor(private renderer: Renderer) {
super();

View File

@ -1,5 +1,5 @@
import { LabelledControl, LabelledControlOptions, LabelPosition } from "./LabelledControl";
import { disposable_listener, el, Icon } from "./dom";
import { disposable_listener, div, Icon } from "./dom";
import "./Select.css";
import { is_property, Property } from "../observable/property/Property";
import { Button } from "./Button";
@ -14,7 +14,7 @@ export type SelectOptions<T> = LabelledControlOptions & {
};
export class Select<T> extends LabelledControl {
readonly element = el.div({ class: "core_Select" });
readonly element = div({ className: "core_Select" });
readonly preferred_label_position: LabelPosition;

View File

@ -1,9 +1,9 @@
import { Widget, WidgetOptions } from "./Widget";
import { create_element, el } from "./dom";
import { LazyWidget } from "./LazyWidget";
import { Resizable } from "./Resizable";
import { ResizableWidget } from "./ResizableWidget";
import "./TabContainer.css";
import { div, span } from "./dom";
export type Tab = {
title: string;
@ -20,11 +20,11 @@ type TabInfo = Tab & { tab_element: HTMLSpanElement; lazy_view: LazyWidget };
const BAR_HEIGHT = 28;
export class TabContainer extends ResizableWidget {
readonly element = el.div({ class: "core_TabContainer" });
readonly element = div({ className: "core_TabContainer" });
private tabs: TabInfo[] = [];
private bar_element = el.div({ class: "core_TabContainer_Bar" });
private panes_element = el.div({ class: "core_TabContainer_Panes" });
private bar_element = div({ className: "core_TabContainer_Bar" });
private panes_element = div({ className: "core_TabContainer_Panes" });
constructor(options: TabContainerOptions) {
super(options);
@ -32,11 +32,13 @@ export class TabContainer extends ResizableWidget {
this.bar_element.onmousedown = this.bar_mousedown;
for (const tab of options.tabs) {
const tab_element = create_element("span", {
class: "core_TabContainer_Tab",
text: tab.title,
const tab_element = span(
{
className: "core_TabContainer_Tab",
data: { key: tab.key },
});
},
tab.title,
);
this.bar_element.append(tab_element);
const lazy_view = this.disposable(new LazyWidget(tab.create_view));

View File

@ -1,5 +1,5 @@
import { Widget, WidgetOptions } from "./Widget";
import { bind_children_to, el } from "./dom";
import { bind_children_to, span, table, tbody, td, tfoot, th, thead, tr } from "./dom";
import { ListProperty } from "../observable/property/list/ListProperty";
import { Disposer } from "../observable/Disposer";
import "./Table.css";
@ -36,9 +36,9 @@ export type TableOptions<T> = WidgetOptions & {
};
export class Table<T> extends Widget {
readonly element = el.table({ class: "core_Table" });
readonly element = table({ className: "core_Table" });
private readonly tbody_element = el.tbody();
private readonly tbody_element = tbody();
private readonly footer_row_element?: HTMLTableRowElement;
private readonly values: ListProperty<T>;
private readonly columns: Column<T>[];
@ -51,32 +51,29 @@ export class Table<T> extends Widget {
const sort_columns: { column: Column<T>; direction: SortDirection }[] = [];
const thead_element = el.thead();
const header_tr_element = el.tr();
const thead_element = thead();
const header_tr_element = tr();
let left = 0;
let has_footer = false;
header_tr_element.append(
...this.columns.map((column, index) => {
const th = el.th(
{ data: { index: index.toString() } },
el.span({ text: column.title }),
);
const th_element = th({ data: { index: index.toString() } }, span(column.title));
if (column.fixed) {
th.style.position = "sticky";
th.style.left = `${left}px`;
th_element.style.position = "sticky";
th_element.style.left = `${left}px`;
left += column.width;
}
th.style.width = `${column.width}px`;
th_element.style.width = `${column.width}px`;
if (column.footer) {
has_footer = true;
}
return th;
return th_element;
}),
);
@ -125,12 +122,12 @@ export class Table<T> extends Widget {
}
thead_element.append(header_tr_element);
this.tbody_element = el.tbody();
this.tbody_element = tbody();
this.element.append(thead_element, this.tbody_element);
if (has_footer) {
this.footer_row_element = el.tr();
this.element.append(el.tfoot({}, this.footer_row_element));
this.footer_row_element = tr();
this.element.append(tfoot({}, this.footer_row_element));
this.create_footer();
}
@ -147,10 +144,9 @@ export class Table<T> extends Widget {
let left = 0;
return [
el.tr(
{},
tr(
...this.columns.map((column, i) => {
const cell = column.fixed ? el.th() : el.td();
const cell = column.fixed ? th() : td();
try {
const content = column.render_cell(value, disposer);
@ -190,7 +186,7 @@ export class Table<T> extends Widget {
for (let i = 0; i < this.columns.length; i++) {
const column = this.columns[i];
const cell = el.th();
const cell = th();
cell.style.width = `${column.width}px`;

View File

@ -1,8 +1,8 @@
import { LabelledControl, LabelledControlOptions } from "./LabelledControl";
import { el } from "./dom";
import { WritableProperty } from "../observable/property/WritableProperty";
import "./TextArea.css";
import { WidgetProperty } from "../observable/property/WidgetProperty";
import { div, textarea } from "./dom";
export type TextAreaOptions = LabelledControlOptions & {
max_length?: number;
@ -12,14 +12,14 @@ export type TextAreaOptions = LabelledControlOptions & {
};
export class TextArea extends LabelledControl {
readonly element = el.div({ class: "core_TextArea" });
readonly element = div({ className: "core_TextArea" });
readonly preferred_label_position = "left";
readonly value: WritableProperty<string>;
private readonly text_element: HTMLTextAreaElement = el.textarea({
class: "core_TextArea_inner",
private readonly text_element: HTMLTextAreaElement = textarea({
className: "core_TextArea_inner",
});
private readonly _value = new WidgetProperty<string>(this, "", this.set_value);

View File

@ -1,7 +1,7 @@
import { Widget, WidgetOptions } from "./Widget";
import { create_element } from "./dom";
import "./ToolBar.css";
import { LabelledControl } from "./LabelledControl";
import { div } from "./dom";
export type ToolBarOptions = WidgetOptions & {
children?: Widget[];
@ -10,7 +10,7 @@ export type ToolBarOptions = WidgetOptions & {
export class ToolBar extends Widget {
private readonly children: readonly Widget[];
readonly element = create_element("div", { class: "core_ToolBar" });
readonly element = div({ className: "core_ToolBar" });
readonly height = 33;
constructor(options?: ToolBarOptions) {
@ -21,7 +21,7 @@ export class ToolBar extends Widget {
for (const child of this.children) {
if (child instanceof LabelledControl && child.label) {
const group = create_element("div", { class: "core_ToolBar_group" });
const group = div({ className: "core_ToolBar_group" });
if (
child.preferred_label_position === "left" ||

View File

@ -1,7 +1,7 @@
import { Disposable } from "../observable/Disposable";
import { Disposer } from "../observable/Disposer";
import { Observable } from "../observable/Observable";
import { bind_attr, bind_hidden } from "./dom";
import { bind_hidden } from "./dom";
import { WritableProperty } from "../observable/property/WritableProperty";
import { WidgetProperty } from "../observable/property/WidgetProperty";
import { Property } from "../observable/property/Property";
@ -61,7 +61,7 @@ export abstract class Widget implements Disposable {
setTimeout(() => {
if (!this.construction_finalized) {
logger.warn(
logger.error(
`finalize_construction is never called for ${
Object.getPrototypeOf(this).constructor.name
}.`,

View File

@ -3,40 +3,21 @@ import { Observable } from "../observable/Observable";
import { is_property } from "../observable/property/Property";
import { SectionId } from "../model";
import {
ListChangeEvent,
ListChangeType,
ListProperty,
ListPropertyChangeEvent,
} from "../observable/property/list/ListProperty";
import { Disposer } from "../observable/Disposer";
type ElementAttributes = {
class?: string;
tab_index?: number;
text?: string;
title?: string;
data?: { [key: string]: string };
};
type Attributes<E> = Partial<E> & { data?: { [key: string]: string } };
export const el = {
div: (attributes?: ElementAttributes, ...children: HTMLElement[]): HTMLDivElement =>
create_element("div", attributes, ...children),
type Child = string | Node;
span: (attributes?: ElementAttributes, ...children: HTMLElement[]): HTMLSpanElement =>
create_element("span", attributes, ...children),
h2: (attributes?: ElementAttributes, ...children: HTMLElement[]): HTMLHeadingElement =>
create_element("h2", attributes, ...children),
p: (attributes?: ElementAttributes, ...children: HTMLElement[]): HTMLParagraphElement =>
create_element("p", attributes, ...children),
a: (
attributes?: ElementAttributes & {
href?: string;
},
...children: HTMLElement[]
): HTMLAnchorElement => {
const element = create_element<HTMLAnchorElement>("a", attributes, ...children);
export function a(
attributes?: Attributes<HTMLAnchorElement>,
...children: Child[]
): HTMLAnchorElement {
const element = create_element("a", attributes, ...children);
if (attributes && attributes.href && attributes.href.trimLeft().startsWith("http")) {
element.target = "_blank";
@ -44,92 +25,155 @@ export const el = {
}
return element;
},
}
img: (
attributes?: ElementAttributes & {
src?: string;
width?: number;
height?: number;
alt?: string;
},
export function button(
attributes?: Attributes<HTMLButtonElement>,
...children: Child[]
): HTMLButtonElement {
return create_element("button", attributes, ...children);
}
export function div(attributes?: Attributes<HTMLDivElement>, ...children: Child[]): HTMLDivElement {
return create_element("div", attributes, ...children);
}
export function h2(
attributes?: Attributes<HTMLHeadingElement>,
...children: Child[]
): HTMLHeadingElement {
return create_element("h2", attributes, ...children);
}
export function input(
attributes?: Attributes<HTMLInputElement>,
...children: HTMLImageElement[]
): HTMLImageElement => create_element("img", attributes, ...children),
): HTMLInputElement {
return create_element("input", attributes, ...children);
}
table: (attributes?: ElementAttributes, ...children: HTMLElement[]): HTMLTableElement =>
create_element("table", attributes, ...children),
export function img(
attributes?: Attributes<HTMLImageElement>,
...children: HTMLImageElement[]
): HTMLImageElement {
return create_element("img", attributes, ...children);
}
thead: (attributes?: ElementAttributes, ...children: HTMLElement[]): HTMLTableSectionElement =>
create_element("thead", attributes, ...children),
export function label(
attributes?: Attributes<HTMLLabelElement>,
...children: Child[]
): HTMLLabelElement {
return create_element("label", attributes, ...children);
}
tbody: (attributes?: ElementAttributes, ...children: HTMLElement[]): HTMLTableSectionElement =>
create_element("tbody", attributes, ...children),
export function li(attributes?: Attributes<HTMLLIElement>, ...children: Child[]): HTMLLIElement {
return create_element("li", attributes, ...children);
}
tfoot: (attributes?: ElementAttributes, ...children: HTMLElement[]): HTMLTableSectionElement =>
create_element("tfoot", attributes, ...children),
export function p(
attributes?: Attributes<HTMLParagraphElement>,
...children: Child[]
): HTMLParagraphElement {
return create_element("p", attributes, ...children);
}
tr: (attributes?: ElementAttributes, ...children: HTMLElement[]): HTMLTableRowElement =>
create_element("tr", attributes, ...children),
export function span(
attributes?: Attributes<HTMLSpanElement>,
...children: Child[]
): HTMLSpanElement {
return create_element("span", attributes, ...children);
}
th: (
attributes?: ElementAttributes & { col_span?: number },
...children: HTMLElement[]
): HTMLTableHeaderCellElement => create_element("th", attributes, ...children),
export function table(
attributes?: Attributes<HTMLTableElement>,
...children: Child[]
): HTMLTableElement {
return create_element("table", attributes, ...children);
}
td: (
attributes?: ElementAttributes & { col_span?: number },
...children: HTMLElement[]
): HTMLTableCellElement => create_element("td", attributes, ...children),
export function tbody(
attributes?: Attributes<HTMLTableSectionElement>,
...children: Child[]
): HTMLTableSectionElement {
return create_element("tbody", attributes, ...children);
}
button: (attributes?: ElementAttributes, ...children: HTMLElement[]): HTMLButtonElement =>
create_element("button", attributes, ...children),
export function td(
attributes?: Attributes<HTMLTableCellElement>,
...children: Child[]
): HTMLTableCellElement {
return create_element("td", attributes, ...children);
}
textarea: (attributes?: ElementAttributes, ...children: HTMLElement[]): HTMLTextAreaElement =>
create_element("textarea", attributes, ...children),
};
export function textarea(
attributes?: Attributes<HTMLTextAreaElement>,
...children: Child[]
): HTMLTextAreaElement {
return create_element("textarea", attributes, ...children);
}
export function create_element<T extends HTMLElement>(
export function tfoot(
attributes?: Attributes<HTMLTableSectionElement>,
...children: Child[]
): HTMLTableSectionElement {
return create_element("tfoot", attributes, ...children);
}
export function th(
attributes?: Attributes<HTMLTableHeaderCellElement>,
...children: Child[]
): HTMLTableHeaderCellElement {
return create_element("th", attributes, ...children);
}
export function thead(
attributes?: Attributes<HTMLTableSectionElement>,
...children: Child[]
): HTMLTableSectionElement {
return create_element("thead", attributes, ...children);
}
export function tr(
attributes?: Attributes<HTMLTableRowElement>,
...children: Child[]
): HTMLTableRowElement {
return create_element("tr", attributes, ...children);
}
export function ul(
attributes?: Attributes<HTMLUListElement>,
...children: Child[]
): HTMLUListElement {
return create_element("ul", attributes, ...children);
}
function create_element<E extends HTMLElement>(
tag_name: string,
attributes?: ElementAttributes & {
href?: string;
src?: string;
width?: number;
height?: number;
alt?: string;
col_span?: number;
},
...children: HTMLElement[]
): T {
const element = document.createElement(tag_name) as any;
attributes?: Attributes<E>,
...children: Child[]
): E {
const element = (document.createElement(tag_name) as any) as E;
if (attributes) {
if (attributes instanceof HTMLElement) {
// noinspection SuspiciousTypeOfGuard
if (attributes instanceof Element || typeof attributes === "string") {
element.append(attributes);
} else {
if (attributes.class != undefined) element.className = attributes.class;
if (attributes.text != undefined) element.textContent = attributes.text;
if (attributes.title != undefined) element.title = attributes.title;
if (attributes.href != undefined) element.href = attributes.href;
if (attributes.src != undefined) element.src = attributes.src;
if (attributes.width != undefined) element.width = attributes.width;
if (attributes.height != undefined) element.height = attributes.height;
if (attributes.alt != undefined) element.alt = attributes.alt;
const data = attributes.data;
delete attributes.data;
Object.assign(element, attributes);
if (attributes.data) {
for (const [key, val] of Object.entries(attributes.data)) {
if (data) {
for (const [key, val] of Object.entries(data)) {
element.dataset[key] = val;
}
}
if (attributes.col_span != undefined) element.colSpan = attributes.col_span;
if (attributes.tab_index != undefined) element.tabIndex = attributes.tab_index;
}
}
element.append(...children);
return (element as HTMLElement) as T;
return element;
}
export function bind_attr<E extends Element, A extends keyof E>(
@ -137,7 +181,7 @@ export function bind_attr<E extends Element, A extends keyof E>(
attribute: A,
observable: Observable<E[A]>,
): Disposable {
if (is_property(observable)) {
if (is_property<E[A]>(observable)) {
element[attribute] = observable.val;
}
@ -225,11 +269,11 @@ export function icon(icon: Icon): HTMLElement {
break;
}
return el.span({ class: icon_str });
return span({ className: icon_str });
}
export function section_id_icon(section_id: SectionId, options?: { size?: number }): HTMLElement {
const element = el.span();
const element = span();
const size = options && options.size;
element.style.display = "inline-block";
@ -310,7 +354,7 @@ export function bind_children_to<T>(
): Disposable {
const children_disposer = new Disposer();
const observer = list.observe_list((change: ListPropertyChangeEvent<T>) => {
const observer = list.observe_list((change: ListChangeEvent<T>) => {
if (change.type === ListChangeType.ListChange) {
splice_children(change.index, change.removed.length, change.inserted);
} else if (change.type === ListChangeType.ValueChange) {

View File

@ -1,6 +1,7 @@
import { Disposable } from "../Disposable";
import { Property, PropertyChangeEvent } from "./Property";
import { Property } from "./Property";
import { LogManager } from "../../Logger";
import { ChangeEvent } from "../Observable";
const logger = LogManager.get("core/observable/property/AbstractMinimalProperty");
@ -13,16 +14,16 @@ export abstract class AbstractMinimalProperty<T> implements Property<T> {
abstract get_val(): T;
protected readonly observers: ((change: PropertyChangeEvent<T>) => void)[] = [];
protected readonly observers: ((change: ChangeEvent<T>) => void)[] = [];
observe(
observer: (change: PropertyChangeEvent<T>) => void,
observer: (change: ChangeEvent<T>) => void,
options?: { call_now?: boolean },
): Disposable {
this.observers.push(observer);
if (options && options.call_now) {
this.call_observer(observer, this.val, this.val);
this.call_observer(observer, this.val);
}
return {
@ -40,21 +41,17 @@ export abstract class AbstractMinimalProperty<T> implements Property<T> {
abstract flat_map<U>(f: (element: T) => Property<U>): Property<U>;
protected emit(old_value: T): void {
protected emit(): void {
const value = this.val;
for (const observer of this.observers) {
this.call_observer(observer, value, old_value);
this.call_observer(observer, value);
}
}
private call_observer(
observer: (event: PropertyChangeEvent<T>) => void,
value: T,
old_value: T,
): void {
private call_observer(observer: (event: ChangeEvent<T>) => void, value: T): void {
try {
observer({ value, old_value });
observer({ value });
} catch (e) {
logger.error("Observer threw error.", e);
}

View File

@ -1,7 +1,8 @@
import { Disposable } from "../Disposable";
import { Disposer } from "../Disposer";
import { AbstractMinimalProperty } from "./AbstractMinimalProperty";
import { Property, PropertyChangeEvent } from "./Property";
import { Property } from "./Property";
import { ChangeEvent } from "../Observable";
/**
* Starts observing its dependencies when the first observer on this property is registered.
@ -29,12 +30,14 @@ export abstract class DependentProperty<T> extends AbstractMinimalProperty<T> {
}
observe(
observer: (event: PropertyChangeEvent<T>) => void,
observer: (event: ChangeEvent<T>) => void,
options: { call_now?: boolean } = {},
): Disposable {
const super_disposable = super.observe(observer, options);
if (this.dependency_disposer.length === 0) {
this._val = this.compute_value();
this.dependency_disposer.add_all(
...this.dependencies.map(dependency =>
dependency.observe(() => {
@ -42,13 +45,11 @@ export abstract class DependentProperty<T> extends AbstractMinimalProperty<T> {
this._val = this.compute_value();
if (this._val !== old_value) {
this.emit(old_value);
this.emit();
}
}),
),
);
this._val = this.compute_value();
}
return {

View File

@ -1,7 +1,8 @@
import { Disposable } from "../Disposable";
import { MappedProperty } from "./MappedProperty";
import { Property, PropertyChangeEvent } from "./Property";
import { Property } from "./Property";
import { DependentProperty } from "./DependentProperty";
import { ChangeEvent } from "../Observable";
export class FlatMappedProperty<T> extends DependentProperty<T> {
private computed_property?: Property<T>;
@ -15,7 +16,7 @@ export class FlatMappedProperty<T> extends DependentProperty<T> {
}
observe(
observer: (event: PropertyChangeEvent<T>) => void,
observer: (event: ChangeEvent<T>) => void,
options?: { call_now?: boolean },
): Disposable {
const super_disposable = super.observe(observer, options);
@ -46,10 +47,8 @@ export class FlatMappedProperty<T> extends DependentProperty<T> {
this.computed_property = this.compute();
const old_value = this.computed_property.val;
this.computed_disposable = this.computed_property.observe(() => {
this.emit(old_value);
this.emit();
});
return this.computed_property.val;

View File

@ -4,9 +4,9 @@ import { list_property } from "../index";
import { FlatMappedProperty } from "./FlatMappedProperty";
import { SimpleListProperty } from "./list/SimpleListProperty";
import { MappedListProperty } from "./list/MappedListProperty";
import { is_property, Property, PropertyChangeEvent } from "./Property";
import { is_list_property } from "./list/ListProperty";
import { is_property, Property } from "./Property";
import { FlatMappedListProperty } from "./list/FlatMappedListProperty";
import { ChangeEvent } from "../Observable";
// This suite tests every implementation of Property.
@ -25,7 +25,7 @@ function test_property(
test(`${name} should call observers immediately if added with call_now set to true`, () => {
const { property } = create();
const events: PropertyChangeEvent<any>[] = [];
const events: ChangeEvent<any>[] = [];
property.observe(event => events.push(event), { call_now: true });
@ -34,56 +34,52 @@ function test_property(
test(`${name} should propagate updates to mapped properties`, () => {
const { property, emit } = create();
let i = 0;
const mapped = property.map(() => i++);
const events: PropertyChangeEvent<any>[] = [];
const initial_value = mapped.val;
const events: ChangeEvent<any>[] = [];
mapped.observe(event => events.push(event));
emit();
expect(events.length).toBe(1);
expect(mapped.val !== initial_value).toBe(true);
});
test(`${name} should propagate updates to flat mapped properties`, () => {
const { property, emit } = create();
let i = 0;
const flat_mapped = property.flat_map(() => new SimpleProperty(i++));
const events: PropertyChangeEvent<any>[] = [];
const initial_value = flat_mapped.val;
const events: ChangeEvent<any>[] = [];
flat_mapped.observe(event => events.push(event));
emit();
expect(events.length).toBe(1);
expect(flat_mapped.val !== initial_value).toBe(true);
});
test(`${name} should correctly set value and old_value in emitted PropertyChangeEvents`, () => {
test(`${name} should correctly set value in emitted ChangeEvents`, () => {
const { property, emit } = create();
const events: PropertyChangeEvent<any>[] = [];
const events: ChangeEvent<any>[] = [];
property.observe(event => events.push(event));
const initial_value = property.val;
emit();
expect(events.length).toBe(1);
expect(events[0].value).toBe(property.val);
if (!is_list_property(property)) {
expect(events[0].old_value).toBe(initial_value);
}
emit();
expect(events.length).toBe(2);
expect(events[1].value).toBe(property.val);
if (!is_list_property(property)) {
expect(events[1].old_value).toBe(events[0].value);
}
});
}
@ -156,3 +152,14 @@ test_property(`${FlatMappedListProperty.name} (nested property emits)`, () => {
emit: () => list.get(0).push(10),
};
});
test("aaaaaaaaaaaaaaaaaaargh", () => {
const property: Property<{ x?: Property<number> }> = new SimpleProperty({});
const flat_mapped = property.flat_map(p => p.x ?? new SimpleProperty(13));
expect(flat_mapped.val).toBe(13);
property.val.x = new SimpleProperty(17);
expect(flat_mapped.val).toBe(17);
});

View File

@ -1,10 +1,6 @@
import { ChangeEvent, Observable } from "../Observable";
import { Disposable } from "../Disposable";
export interface PropertyChangeEvent<T> extends ChangeEvent<T> {
old_value: T;
}
export interface Property<T> extends Observable<T> {
readonly is_property: true;
@ -13,7 +9,7 @@ export interface Property<T> extends Observable<T> {
get_val(): T;
observe(
observer: (event: PropertyChangeEvent<T>) => void,
observer: (event: ChangeEvent<T>) => void,
options?: { call_now?: boolean },
): Disposable;

View File

@ -23,11 +23,10 @@ export class SimpleProperty<T> extends AbstractProperty<T> implements WritablePr
set_val(val: T, options: { silent?: boolean } = {}): void {
if (val !== this._val) {
const old_value = this._val;
this._val = val;
if (!options.silent) {
this.emit(old_value);
this.emit();
}
}
}
@ -37,7 +36,7 @@ export class SimpleProperty<T> extends AbstractProperty<T> implements WritablePr
}
bind_to(observable: Observable<T>): Disposable {
if (is_property(observable)) {
if (is_property<T>(observable)) {
this.val = observable.val;
}

View File

@ -1,7 +1,7 @@
import { SimpleProperty } from "./SimpleProperty";
import { SimpleListProperty } from "./list/SimpleListProperty";
import { PropertyChangeEvent } from "./Property";
import { WritableProperty } from "./WritableProperty";
import { ChangeEvent } from "../Observable";
// This suite tests every implementation of WritableProperty.
@ -13,9 +13,9 @@ function test_writable_property<T>(
create_val: () => T;
},
): void {
test(`${name} should emit a PropertyChangeEvent when val is modified`, () => {
test(`${name} should emit a ChangeEvent when val is modified`, () => {
const { property, create_val } = create();
const events: PropertyChangeEvent<T>[] = [];
const events: ChangeEvent<T>[] = [];
property.observe(event => events.push(event));

View File

@ -1,4 +1,4 @@
import { ListChangeType, ListProperty, ListPropertyChangeEvent } from "./ListProperty";
import { ListChangeType, ListProperty, ListChangeEvent } from "./ListProperty";
import { AbstractProperty } from "../AbstractProperty";
import { Disposable } from "../../Disposable";
import { Observable } from "../../Observable";
@ -28,7 +28,7 @@ class LengthProperty extends AbstractProperty<number> {
if (old_length !== length) {
this.length = length;
this.emit(old_length);
this.emit();
}
}
}
@ -49,7 +49,7 @@ export abstract class AbstractListProperty<T> extends AbstractProperty<readonly
/**
* External observers which are observing this list.
*/
protected readonly list_observers: ((change: ListPropertyChangeEvent<T>) => void)[] = [];
protected readonly list_observers: ((change: ListChangeEvent<T>) => void)[] = [];
protected constructor(extract_observables?: (element: T) => Observable<any>[]) {
super();
@ -64,7 +64,7 @@ export abstract class AbstractListProperty<T> extends AbstractProperty<readonly
}
observe_list(
observer: (change: ListPropertyChangeEvent<T>) => void,
observer: (change: ListChangeEvent<T>) => void,
options?: { call_now?: boolean },
): Disposable {
if (this.value_observers.length === 0 && this.extract_observables) {
@ -114,11 +114,11 @@ export abstract class AbstractListProperty<T> extends AbstractProperty<readonly
/**
* Does the following in the given order:
* - Updates value observers
* - Emits length PropertyChangeEvent if necessary
* - Emits length ChangeEvent if necessary
* - Emits ListPropertyChangeEvent
* - Emits PropertyChangeEvent
* - Emits ChangeEvent
*/
protected finalize_update(change: ListPropertyChangeEvent<T>): void {
protected finalize_update(change: ListChangeEvent<T>): void {
if (
this.list_observers.length &&
this.extract_observables &&
@ -133,12 +133,12 @@ export abstract class AbstractListProperty<T> extends AbstractProperty<readonly
this.call_list_observer(observer, change);
}
this.emit(this.val);
this.emit();
}
private call_list_observer(
observer: (change: ListPropertyChangeEvent<T>) => void,
change: ListPropertyChangeEvent<T>,
observer: (change: ListChangeEvent<T>) => void,
change: ListChangeEvent<T>,
): void {
try {
observer(change);

View File

@ -1,8 +1,9 @@
import { ListChangeType, ListPropertyChangeEvent } from "./ListProperty";
import { Property, PropertyChangeEvent } from "../Property";
import { ListChangeType, ListChangeEvent } from "./ListProperty";
import { Property } from "../Property";
import { Disposable } from "../../Disposable";
import { AbstractListProperty } from "./AbstractListProperty";
import { Disposer } from "../../Disposer";
import { ChangeEvent } from "../../Observable";
/**
* Starts observing its dependencies when the first observer on this property is registered.
@ -30,7 +31,7 @@ export abstract class DependentListProperty<T> extends AbstractListProperty<T> {
}
observe(
observer: (event: PropertyChangeEvent<readonly T[]>) => void,
observer: (event: ChangeEvent<readonly T[]>) => void,
options: { call_now?: boolean } = {},
): Disposable {
const super_disposable = super.observe(observer, options);
@ -46,7 +47,7 @@ export abstract class DependentListProperty<T> extends AbstractListProperty<T> {
}
observe_list(
observer: (change: ListPropertyChangeEvent<T>) => void,
observer: (change: ListChangeEvent<T>) => void,
options?: { call_now?: boolean },
): Disposable {
const super_disposable = super.observe_list(observer, options);
@ -73,6 +74,8 @@ export abstract class DependentListProperty<T> extends AbstractListProperty<T> {
private init_dependency_disposables(): void {
if (this.dependency_disposer.length === 0) {
this.values = this.compute_values();
this.dependency_disposer.add_all(
...this.dependencies.map(dependency =>
dependency.observe(() => {
@ -88,8 +91,6 @@ export abstract class DependentListProperty<T> extends AbstractListProperty<T> {
}),
),
);
this.values = this.compute_values();
}
}

View File

@ -1,10 +1,11 @@
import { Disposable } from "../../Disposable";
import { MappedProperty } from "../MappedProperty";
import { is_property, Property, PropertyChangeEvent } from "../Property";
import { ListProperty, ListPropertyChangeEvent } from "./ListProperty";
import { is_property, Property } from "../Property";
import { ListProperty, ListChangeEvent } from "./ListProperty";
import { FlatMappedProperty } from "../FlatMappedProperty";
import { DependentListProperty } from "./DependentListProperty";
import { MappedListProperty } from "./MappedListProperty";
import { ChangeEvent } from "../../Observable";
export class FlatMappedListProperty<T> extends DependentListProperty<T> {
private computed_property?: ListProperty<T>;
@ -18,7 +19,7 @@ export class FlatMappedListProperty<T> extends DependentListProperty<T> {
}
observe(
observer: (event: PropertyChangeEvent<readonly T[]>) => void,
observer: (event: ChangeEvent<readonly T[]>) => void,
options?: { call_now?: boolean },
): Disposable {
const super_disposable = super.observe(observer, options);
@ -37,7 +38,7 @@ export class FlatMappedListProperty<T> extends DependentListProperty<T> {
}
observe_list(
observer: (change: ListPropertyChangeEvent<T>) => void,
observer: (change: ListChangeEvent<T>) => void,
options?: { call_now?: boolean },
): Disposable {
const super_disposable = super.observe_list(observer, options);
@ -72,10 +73,8 @@ export class FlatMappedListProperty<T> extends DependentListProperty<T> {
this.computed_property = this.compute();
const old_value = this.computed_property.val;
this.computed_disposable = this.computed_property.observe(() => {
this.emit(old_value);
this.emit();
});
return this.computed_property.val;

View File

@ -2,7 +2,7 @@ import {
is_list_property,
ListChangeType,
ListProperty,
ListPropertyChangeEvent,
ListChangeEvent,
} from "./ListProperty";
import { SimpleListProperty } from "./SimpleListProperty";
import { MappedListProperty } from "./MappedListProperty";
@ -27,7 +27,7 @@ function test_list_property(
test(`${name} should propagate list changes to a filtered list`, () => {
const { property, emit_list_change } = create();
const filtered = property.filtered(() => true);
const events: ListPropertyChangeEvent<any>[] = [];
const events: ListChangeEvent<any>[] = [];
filtered.observe_list(event => events.push(event));

View File

@ -6,7 +6,7 @@ export enum ListChangeType {
ValueChange,
}
export type ListPropertyChangeEvent<T> = ListChange<T> | ListValueChange<T>;
export type ListChangeEvent<T> = ListChange<T> | ListValueChange<T>;
export type ListChange<T> = {
readonly type: ListChangeType.ListChange;
@ -29,7 +29,7 @@ export interface ListProperty<T> extends Property<readonly T[]> {
get(index: number): T;
observe_list(
observer: (change: ListPropertyChangeEvent<T>) => void,
observer: (change: ListChangeEvent<T>) => void,
options?: { call_now?: boolean },
): Disposable;

View File

@ -1,5 +1,5 @@
import { SimpleListProperty } from "./SimpleListProperty";
import { ListChangeType, ListPropertyChangeEvent } from "./ListProperty";
import { ListChangeType, ListChangeEvent } from "./ListProperty";
test("constructor", () => {
const list = new SimpleListProperty<number>(undefined, 1, 2, 3);
@ -9,7 +9,7 @@ test("constructor", () => {
});
test("push", () => {
const changes: ListPropertyChangeEvent<number>[] = [];
const changes: ListChangeEvent<number>[] = [];
const list = new SimpleListProperty<number>();
list.observe_list(change => changes.push(change));

View File

@ -1,23 +1,20 @@
import { el } from "../../core/gui/dom";
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import "./HelpView.css";
import { div, p } from "../../core/gui/dom";
export class HelpView extends ResizableWidget {
readonly element = el.div(
{ class: "hunt_optimizer_HelpView" },
el.p({
text:
readonly element = div(
{ className: "hunt_optimizer_HelpView" },
p(
"Add some items with the combo box on the left to see the optimal combination of hunt methods on the right.",
}),
el.p({
text:
),
p(
'At the moment a hunt method is simply a quest run-through. Partial quest run-throughs are coming. View the list of methods on the "Methods" tab. Each method takes a certain amount of time, which affects the optimization result. Make sure the times are correct for you.',
}),
el.p({ text: "Only enemy drops are considered. Box drops are coming." }),
el.p({
text:
),
p("Only enemy drops are considered. Box drops are coming."),
p(
"The optimal result is calculated using linear optimization. The optimizer takes into account rare enemies and the fact that pan arms can be split in two.",
}),
),
);
constructor() {

View File

@ -1,6 +1,5 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { el } from "../../core/gui/dom";
import { HuntMethodModel } from "../model/HuntMethodModel";
import {
ENEMY_NPC_TYPES,
@ -16,11 +15,12 @@ import { list_property } from "../../core/observable";
import { ServerMap } from "../../core/stores/ServerMap";
import { HuntMethodStore } from "../stores/HuntMethodStore";
import { LogManager } from "../../core/Logger";
import { div } from "../../core/gui/dom";
const logger = LogManager.get("hunt_optimizer/gui/MethodsForEpisodeView");
export class MethodsForEpisodeView extends ResizableWidget {
readonly element = el.div({ class: "hunt_optimizer_MethodsForEpisodeView" });
readonly element = div({ className: "hunt_optimizer_MethodsForEpisodeView" });
private readonly episode: Episode;
private readonly enemy_types: NpcType[];

View File

@ -1,5 +1,5 @@
import { Widget } from "../../core/gui/Widget";
import { el, section_id_icon } from "../../core/gui/dom";
import { div, h2, section_id_icon, span } from "../../core/gui/dom";
import { Column, Table } from "../../core/gui/Table";
import { Disposable } from "../../core/observable/Disposable";
import { list_property } from "../../core/observable";
@ -15,9 +15,9 @@ import { LogManager } from "../../core/Logger";
const logger = LogManager.get("hunt_optimizer/gui/OptimizationResultView");
export class OptimizationResultView extends Widget {
readonly element = el.div(
{ class: "hunt_optimizer_OptimizationResultView" },
el.h2({ text: "Ideal Combination of Methods" }),
readonly element = div(
{ className: "hunt_optimizer_OptimizationResultView" },
h2("Ideal Combination of Methods"),
);
private results_observer?: Disposable;
@ -116,8 +116,7 @@ export class OptimizationResultView extends Widget {
fixed: true,
width: 90,
render_cell(value: OptimalMethodModel) {
const element = el.span(
{},
const element = span(
...value.section_ids.map(sid => section_id_icon(sid, { size: 17 })),
);
element.style.display = "flex";

View File

@ -1,13 +1,13 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom";
import { WantedItemsView } from "./WantedItemsView";
import "./OptimizerView.css";
import { OptimizationResultView } from "./OptimizationResultView";
import { ServerMap } from "../../core/stores/ServerMap";
import { HuntOptimizerStore } from "../stores/HuntOptimizerStore";
import { div } from "../../core/gui/dom";
export class OptimizerView extends ResizableWidget {
readonly element = el.div({ class: "hunt_optimizer_OptimizerView" });
readonly element = div({ className: "hunt_optimizer_OptimizerView" });
constructor(hunt_optimizer_stores: ServerMap<HuntOptimizerStore>) {
super();

View File

@ -1,4 +1,4 @@
import { bind_children_to, el, Icon } from "../../core/gui/dom";
import { bind_children_to, div, h2, Icon, table, tbody, td, tr } from "../../core/gui/dom";
import "./WantedItemsView.css";
import { Button } from "../../core/gui/Button";
import { Disposer } from "../../core/observable/Disposer";
@ -16,9 +16,9 @@ import { LogManager } from "../../core/Logger";
const logger = LogManager.get("hunt_optimizer/gui/WantedItemsView");
export class WantedItemsView extends Widget {
readonly element = el.div({ class: "hunt_optimizer_WantedItemsView" });
readonly element = div({ className: "hunt_optimizer_WantedItemsView" });
private readonly tbody_element = el.tbody();
private readonly tbody_element = tbody();
private readonly store_disposer = this.disposable(new Disposer());
constructor(private readonly hunt_optimizer_stores: ServerMap<HuntOptimizerStore>) {
@ -42,11 +42,11 @@ export class WantedItemsView extends Widget {
);
this.element.append(
el.h2({ text: "Wanted Items" }),
h2("Wanted Items"),
combo_box.element,
el.div(
{ class: "hunt_optimizer_WantedItemsView_table_wrapper" },
el.table({}, this.tbody_element),
div(
{ className: "hunt_optimizer_WantedItemsView_table_wrapper" },
table(this.tbody_element),
),
);
@ -108,12 +108,7 @@ export class WantedItemsView extends Widget {
);
return [
el.tr(
{},
el.td({}, amount_input.element),
el.td({ text: wanted_item.item_type.name }),
el.td({}, remove_button.element),
),
tr(td(amount_input.element), td(wanted_item.item_type.name), td(remove_button.element)),
row_disposer,
];
};

View File

@ -0,0 +1,48 @@
import { Controller } from "../../core/controllers/Controller";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { Property } from "../../core/observable/property/Property";
import { QuestEventDagModel } from "../model/QuestEventDagModel";
import { ListProperty } from "../../core/observable/property/list/ListProperty";
import { flat_map_to_list, list_property } from "../../core/observable";
import { QuestEventModel } from "../model/QuestEventModel";
import { EditEventSectionIdAction } from "../actions/EditEventSectionIdAction";
import { EditEventDelayAction } from "../actions/EditEventDelayAction";
export class EventsController extends Controller {
readonly event_dags: ListProperty<QuestEventDagModel>;
readonly enabled: Property<boolean>;
readonly unavailable: Property<boolean>;
constructor(private readonly store: QuestEditorStore) {
super();
this.enabled = store.quest_runner.running.map(r => !r);
this.unavailable = store.current_quest.map(q => q == undefined);
this.event_dags = flat_map_to_list(
(quest, area) => {
if (quest && area) {
return quest.event_dags.filtered(dag => dag.area_id === area.id);
} else {
return list_property();
}
},
store.current_quest,
store.current_area,
);
}
focused = (): void => {
this.store.undo.make_current();
};
set_section_id = (event: QuestEventModel, section_id: number): void => {
this.store.undo
.push(new EditEventSectionIdAction(event, event.section_id.val, section_id))
.redo();
};
set_delay = (event: QuestEventModel, delay: number): void => {
this.store.undo.push(new EditEventDelayAction(event, event.delay.val, delay)).redo();
};
}

View File

@ -13,8 +13,8 @@ import { parse_quest, write_quest_qst } from "../../core/data_formats/parsing/qu
import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
import { Endianness } from "../../core/data_formats/Endianness";
import { convert_quest_from_model, convert_quest_to_model } from "../stores/model_conversion";
import { create_element } from "../../core/gui/dom";
import { LogManager } from "../../core/Logger";
import { input } from "../../core/gui/dom";
const logger = LogManager.get("quest_editor/controllers/QuestEditorToolBarController");
@ -97,14 +97,14 @@ export class QuestEditorToolBarController extends Controller {
}),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-O", () => {
const input: HTMLInputElement = create_element("input");
input.type = "file";
input.onchange = () => {
if (input.files && input.files.length) {
this.open_file(input.files[0]);
const input_element = input();
input_element.type = "file";
input_element.onchange = () => {
if (input_element.files && input_element.files.length) {
this.open_file(input_element.files[0]);
}
};
input.click();
input_element.click();
}),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-Shift-S", this.save_as),

View File

@ -1,5 +1,4 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom";
import { editor, KeyCode, KeyMod, Range } from "monaco-editor";
import { AsmEditorToolBar } from "./AsmEditorToolBar";
import { EditorHistory } from "./EditorHistory";
@ -8,6 +7,7 @@ import { ListChangeType } from "../../core/observable/property/list/ListProperty
import { GuiStore } from "../../core/stores/GuiStore";
import { AsmEditorStore } from "../stores/AsmEditorStore";
import { QuestRunner } from "../QuestRunner";
import { div } from "../../core/gui/dom";
import IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
editor.defineTheme("phantasmal-world", {
@ -37,8 +37,9 @@ export class AsmEditorView extends ResizableWidget {
private readonly history: EditorHistory;
private breakpoint_decoration_ids: string[] = [];
private execloc_decoration_id: string | undefined;
private old_pause_location?: number;
readonly element = el.div();
readonly element = div();
constructor(
gui_store: GuiStore,
@ -171,8 +172,9 @@ export class AsmEditorView extends ResizableWidget {
}),
asm_editor_store.pause_location.observe(e => {
const old_line_num = e.old_value;
const old_line_num = this.old_pause_location;
const new_line_num = e.value;
this.old_pause_location = new_line_num;
// remove old
if (old_line_num !== undefined && this.execloc_decoration_id !== undefined) {

View File

@ -1,5 +1,5 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { bind_attr, el } from "../../core/gui/dom";
import { bind_attr, div, table, td, th, tr } from "../../core/gui/dom";
import { UnavailableView } from "./UnavailableView";
import "./EntityInfoView.css";
import { NumberInput } from "../../core/gui/NumberInput";
@ -7,11 +7,11 @@ import { rad_to_deg } from "../../core/math";
import { EntityInfoController } from "../controllers/EntityInfoController";
export class EntityInfoView extends ResizableWidget {
readonly element = el.div({ class: "quest_editor_EntityInfoView", tab_index: -1 });
readonly element = div({ className: "quest_editor_EntityInfoView", tabIndex: -1 });
private readonly no_entity_view = new UnavailableView("No entity selected.");
private readonly table_element = el.table();
private readonly table_element = table();
private readonly type_element: HTMLTableCellElement;
private readonly name_element: HTMLTableCellElement;
@ -43,46 +43,18 @@ export class EntityInfoView extends ResizableWidget {
const coord_class = "quest_editor_EntityInfoView_coord";
this.table_element.append(
el.tr({}, el.th({ text: "Type:" }), (this.type_element = el.td())),
el.tr({}, el.th({ text: "Name:" }), (this.name_element = el.td())),
el.tr({}, el.th({ text: "Section:" }), (this.section_id_element = el.td())),
(this.wave_row_element = el.tr(
{},
el.th({ text: "Wave:" }),
(this.wave_element = el.td()),
)),
el.tr({}, el.th({ text: "Position:", col_span: 2 })),
el.tr(
{},
el.th({ text: "X:", class: coord_class }),
el.td({}, this.pos_x_element.element),
),
el.tr(
{},
el.th({ text: "Y:", class: coord_class }),
el.td({}, this.pos_y_element.element),
),
el.tr(
{},
el.th({ text: "Z:", class: coord_class }),
el.td({}, this.pos_z_element.element),
),
el.tr({}, el.th({ text: "Rotation:", col_span: 2 })),
el.tr(
{},
el.th({ text: "X:", class: coord_class }),
el.td({}, this.rot_x_element.element),
),
el.tr(
{},
el.th({ text: "Y:", class: coord_class }),
el.td({}, this.rot_y_element.element),
),
el.tr(
{},
el.th({ text: "Z:", class: coord_class }),
el.td({}, this.rot_z_element.element),
),
tr(th("Type:"), (this.type_element = td())),
tr(th("Name:"), (this.name_element = td())),
tr(th("Section:"), (this.section_id_element = td())),
(this.wave_row_element = tr(th("Wave:"), (this.wave_element = td()))),
tr(th({ colSpan: 2 }, "Position:")),
tr(th({ className: coord_class }, "X:"), td(this.pos_x_element.element)),
tr(th({ className: coord_class }, "Y:"), td(this.pos_y_element.element)),
tr(th({ className: coord_class }, "Z:"), td(this.pos_z_element.element)),
tr(th({ colSpan: 2 }, "Rotation:")),
tr(th({ className: coord_class }, "X:"), td(this.rot_x_element.element)),
tr(th({ className: coord_class }, "Y:"), td(this.rot_y_element.element)),
tr(th({ className: coord_class }, "Z:"), td(this.rot_z_element.element)),
);
this.element.append(this.table_element, this.no_entity_view.element);

View File

@ -1,4 +1,5 @@
.quest_editor_EntityListView {
outline: none;
overflow: auto;
}

View File

@ -1,5 +1,5 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { bind_children_to, el } from "../../core/gui/dom";
import { bind_children_to, div, img, span } from "../../core/gui/dom";
import "./EntityListView.css";
import { entity_data, EntityType } from "../../core/data_formats/parsing/quest/entities";
import { entity_dnd_source } from "./entity_dnd";
@ -20,9 +20,12 @@ export abstract class EntityListView<T extends EntityType> extends ResizableWidg
) {
super();
const list_element = el.div({ class: "quest_editor_EntityListView_entity_list" });
const list_element = div({ className: "quest_editor_EntityListView_entity_list" });
this.element = el.div({ class: `${class_name} quest_editor_EntityListView` }, list_element);
this.element = div(
{ className: `${class_name} quest_editor_EntityListView`, tabIndex: -1 },
list_element,
);
this.disposables(
bind_children_to(list_element, this.entities, this.create_entity_element),
@ -53,13 +56,13 @@ export abstract class EntityListView<T extends EntityType> extends ResizableWidg
}
private create_entity_element = (entity: T, index: number): HTMLElement => {
const entity_element = el.div({
class: "quest_editor_EntityListView_entity",
const entity_element = div({
className: "quest_editor_EntityListView_entity",
data: { index: index.toString() },
});
entity_element.draggable = true;
const img_element = el.img({ width: 100, height: 100 });
const img_element = img({ width: 100, height: 100 });
img_element.style.visibility = "hidden";
// Workaround for Chrome bug: when dragging an image, calling setDragImage on a DragEvent
// has no effect.
@ -71,9 +74,7 @@ export abstract class EntityListView<T extends EntityType> extends ResizableWidg
img_element.style.visibility = "visible";
});
const name_element = el.span({
text: entity_data(entity).name,
});
const name_element = span(entity_data(entity).name);
entity_element.append(name_element);
return entity_element;

View File

@ -1,4 +1,12 @@
.quest_editor_EventsView {
outline: none;
display: flex;
flex-direction: column;
align-items: stretch;
}
.quest_editor_EventsView_container {
flex: 1;
box-sizing: border-box;
overflow-y: auto;
display: flex;

View File

@ -1,16 +1,13 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom";
import { QuestEventDagModel } from "../model/QuestEventDagModel";
import { Disposer } from "../../core/observable/Disposer";
import { NumberInput } from "../../core/gui/NumberInput";
import "./EventsView.css";
import { Disposable } from "../../core/observable/Disposable";
import { EventsController } from "../controllers/EventsController";
import { UnavailableView } from "./UnavailableView";
import { bind_attr, div, table, td, th, tr } from "../../core/gui/dom";
import { ListChangeEvent, ListChangeType } from "../../core/observable/property/list/ListProperty";
import { defer } from "lodash";
import {
ListChangeType,
ListPropertyChangeEvent,
} from "../../core/observable/property/list/ListProperty";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { NumberInput } from "../../core/gui/NumberInput";
type DagGuiData = {
dag: QuestEventDagModel;
@ -25,19 +22,28 @@ type DagGuiData = {
export class EventsView extends ResizableWidget {
private readonly dag_gui_data: DagGuiData[] = [];
private event_dags_observer?: Disposable;
readonly element = el.div({ class: "quest_editor_EventsView" });
private readonly container_element = div({ className: "quest_editor_EventsView_container" });
private readonly unavailable_view = new UnavailableView("No quest loaded.");
constructor(private readonly quest_editor_store: QuestEditorStore) {
readonly element = div(
{ className: "quest_editor_EventsView", tabIndex: -1 },
this.container_element,
this.unavailable_view.element,
);
constructor(private readonly ctrl: EventsController) {
super();
this.element.addEventListener("focus", () => quest_editor_store.undo.make_current(), true);
this.element.addEventListener("focus", ctrl.focused, true);
this.disposables(
quest_editor_store.current_quest.observe(this.update),
quest_editor_store.current_area.observe(this.update),
this.enabled.bind_to(quest_editor_store.quest_runner.running.map(r => !r)),
bind_attr(this.container_element, "hidden", ctrl.unavailable),
this.unavailable_view.visible.bind_to(ctrl.unavailable),
this.enabled.bind_to(ctrl.enabled),
ctrl.event_dags.observe_list(this.observe_event_dags),
);
this.finalize_construction();
@ -57,54 +63,12 @@ export class EventsView extends ResizableWidget {
dispose(): void {
super.dispose();
if (this.event_dags_observer) {
this.event_dags_observer.dispose();
}
for (const { disposer } of this.dag_gui_data) {
disposer.dispose();
}
}
private update = (): void => {
if (this.event_dags_observer) {
this.event_dags_observer.dispose();
}
const quest = this.quest_editor_store.current_quest.val;
const area = this.quest_editor_store.current_area.val;
if (quest && area) {
const event_dags = quest.event_dags.filtered(dag => dag.area_id === area.id);
this.event_dags_observer = event_dags.observe_list(this.observe_event_dags);
this.redraw_event_dags(event_dags.val);
} else {
this.event_dags_observer = undefined;
this.redraw_event_dags([]);
}
};
private redraw_event_dags = (event_dags: readonly QuestEventDagModel[]): void => {
this.element.innerHTML = "";
for (const removed of this.dag_gui_data.splice(0, this.dag_gui_data.length)) {
removed.disposer.dispose();
}
let index = 0;
for (const dag of event_dags) {
const data = this.create_dag_ui_data(dag);
this.dag_gui_data.splice(index, 0, data);
this.element.append(data.element);
index++;
}
defer(this.update_edges);
};
private observe_event_dags = (change: ListPropertyChangeEvent<QuestEventDagModel>): void => {
private observe_event_dags = (change: ListChangeEvent<QuestEventDagModel>): void => {
if (change.type === ListChangeType.ListChange) {
for (const removed of this.dag_gui_data.splice(change.index, change.removed.length)) {
removed.element.remove();
@ -116,7 +80,10 @@ export class EventsView extends ResizableWidget {
for (const dag of change.inserted) {
const data = this.create_dag_ui_data(dag);
this.dag_gui_data.splice(index, 0, data);
this.element.insertBefore(data.element, this.element.children.item(index));
this.container_element.insertBefore(
data.element,
this.container_element.children.item(index),
);
index++;
}
@ -129,15 +96,13 @@ export class EventsView extends ResizableWidget {
const disposer = new Disposer();
const event_gui_data = new Map<number, { element: HTMLDivElement; position: number }>();
const element = el.div({ class: "quest_editor_EventsView_dag" });
const element = div({ className: "quest_editor_EventsView_dag" });
const edge_container_element = el.div({
class: "quest_editor_EventsView_edge_container",
const edge_container_element = div({
className: "quest_editor_EventsView_edge_container",
});
element.append(edge_container_element);
const inputs_enabled = this.quest_editor_store.quest_runner.running.map(r => !r);
dag.events.forEach((event, i) => {
const section_id_input = disposer.add(new NumberInput(event.section_id.val));
@ -145,25 +110,21 @@ export class EventsView extends ResizableWidget {
disposer.add_all(
section_id_input.value.bind_to(event.section_id),
section_id_input.value.observe(e =>
this.quest_editor_store.event_section_id_changed(event, e),
),
section_id_input.enabled.bind_to(inputs_enabled),
section_id_input.value.observe(e => this.ctrl.set_section_id(event, e.value)),
section_id_input.enabled.bind_to(this.ctrl.enabled),
delay_input.value.bind_to(event.delay),
delay_input.value.observe(e =>
this.quest_editor_store.event_delay_changed(event, e),
),
delay_input.enabled.bind_to(inputs_enabled),
delay_input.value.observe(e => this.ctrl.set_delay(event, e.value)),
delay_input.enabled.bind_to(this.ctrl.enabled),
);
const event_element = el.div(
{ class: "quest_editor_EventsView_event" },
el.table(
el.tr(el.th({ text: "ID:" }), el.td({ text: event.id.toString() })),
el.tr(el.th({ text: "Section:" }), el.td(section_id_input.element)),
el.tr(el.th({ text: "Wave:" }), el.td({ text: event.wave.toString() })),
el.tr(el.th({ text: "Delay:" }), el.td(delay_input.element)),
const event_element = div(
{ className: "quest_editor_EventsView_event" },
table(
tr(th("ID:"), td(event.id.toString())),
tr(th("Section:"), td(section_id_input.element)),
tr(th("Wave:"), td(event.wave.toString())),
tr(th("Delay:"), td(delay_input.element)),
),
);
@ -182,7 +143,7 @@ export class EventsView extends ResizableWidget {
/**
* This method does measurements of the event elements. So it should be called after the event
* elements have been added to the DOM and have been *laid out* by the browser.
* elements have been added to the DOM and have been laid out by the browser.
*/
private update_edges = (): void => {
const SPACING = 8;
@ -208,7 +169,7 @@ export class EventsView extends ResizableWidget {
)!;
const child_y_offset = child_element.offsetTop;
const edge_element = el.div({ class: "quest_editor_EventsView_edge" });
const edge_element = div({ className: "quest_editor_EventsView_edge" });
const top = Math.min(y_offset, child_y_offset) - 20;
const height = Math.max(y_offset, child_y_offset) - top + 20;

View File

@ -1,4 +1,4 @@
import { bind_children_to, el } from "../../core/gui/dom";
import { bind_children_to, div } from "../../core/gui/dom";
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { ToolBar } from "../../core/gui/ToolBar";
import "./LogView.css";
@ -9,7 +9,7 @@ import { LogEntry, LogLevel, LogLevels, time_to_string } from "../../core/Logger
const AUTOSCROLL_TRESHOLD = 5;
export class LogView extends ResizableWidget {
readonly element = el.div({ class: "quest_editor_LogView", tab_index: -1 });
readonly element = div({ className: "quest_editor_LogView", tabIndex: -1 });
// container is needed to get a scrollbar in the right place
private readonly list_container: HTMLElement;
@ -23,8 +23,8 @@ export class LogView extends ResizableWidget {
constructor() {
super();
this.list_container = el.div({ class: "quest_editor_LogView_list_container" });
this.list_element = el.div({ class: "quest_editor_LogView_message_list" });
this.list_container = div({ className: "quest_editor_LogView_list_container" });
this.list_element = div({ className: "quest_editor_LogView_message_list" });
this.level_filter = this.disposable(
new Select({
@ -87,25 +87,16 @@ export class LogView extends ResizableWidget {
};
private create_message_element = ({ time, level, message }: LogEntry): HTMLElement => {
return el.div(
return div(
{
class: [
className: [
"quest_editor_LogView_message",
"quest_editor_LogView_" + LogLevel[level] + "_message",
].join(" "),
},
el.div({
class: "quest_editor_LogView_message_timestamp",
text: time_to_string(time),
}),
el.div({
class: "quest_editor_LogView_message_level",
text: "[" + LogLevel[level] + "]",
}),
el.div({
class: "quest_editor_LogView_message_contents",
text: message,
}),
div({ className: "quest_editor_LogView_message_timestamp" }, time_to_string(time)),
div({ className: "quest_editor_LogView_message_level" }, "[" + LogLevel[level] + "]"),
div({ className: "quest_editor_LogView_message_contents" }, message),
);
};
}

View File

@ -1,13 +1,13 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { bind_attr, el } from "../../core/gui/dom";
import { bind_attr, div, table, td, th, tr } from "../../core/gui/dom";
import "./NpcCountsView.css";
import { UnavailableView } from "./UnavailableView";
import { NameWithCount, NpcCountsController } from "../controllers/NpcCountsController";
export class NpcCountsView extends ResizableWidget {
readonly element = el.div({ class: "quest_editor_NpcCountsView" });
readonly element = div({ className: "quest_editor_NpcCountsView" });
private readonly table_element = el.table();
private readonly table_element = table();
private readonly unavailable_view = new UnavailableView("No quest loaded.");
@ -31,7 +31,7 @@ export class NpcCountsView extends ResizableWidget {
const frag = document.createDocumentFragment();
for (const { name, count } of npcs) {
frag.append(el.tr({}, el.th({ text: name + ":" }), el.td({ text: String(count) })));
frag.append(tr(th(name + ":"), td(String(count))));
}
this.table_element.innerHTML = "";

View File

@ -1,5 +1,4 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { create_element, el } from "../../core/gui/dom";
import { QuestEditorToolBar } from "./QuestEditorToolBar";
import GoldenLayout, { Container, ContentItem, ItemConfigType } from "golden-layout";
import { QuestInfoView } from "./QuestInfoView";
@ -20,6 +19,7 @@ import { QuestEditorStore } from "../stores/QuestEditorStore";
import { QuestEditorUiPersister } from "../persistence/QuestEditorUiPersister";
import { LogManager } from "../../core/Logger";
import { ErrorView } from "../../core/gui/ErrorView";
import { div } from "../../core/gui/dom";
const logger = LogManager.get("quest_editor/gui/QuestEditorView");
@ -41,7 +41,7 @@ const DEFAULT_LAYOUT_CONFIG = {
};
export class QuestEditorView extends ResizableWidget {
readonly element = el.div({ class: "quest_editor_QuestEditorView" });
readonly element = div({ className: "quest_editor_QuestEditorView" });
/**
* Maps views to names and creation functions.
@ -51,7 +51,7 @@ export class QuestEditorView extends ResizableWidget {
{ name: string; create(): ResizableWidget }
>;
private readonly layout_element = create_element("div", { class: "quest_editor_gl_container" });
private readonly layout_element = div({ className: "quest_editor_gl_container" });
private readonly layout: Promise<GoldenLayout>;
private loaded_layout: GoldenLayout | undefined;

View File

@ -1,5 +1,4 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { NumberInput } from "../../core/gui/NumberInput";
import { Disposer } from "../../core/observable/Disposer";
@ -8,13 +7,14 @@ import { TextArea } from "../../core/gui/TextArea";
import "./QuestInfoView.css";
import { UnavailableView } from "./UnavailableView";
import { QuestInfoController } from "../controllers/QuestInfoController";
import { div, table, td, th, tr } from "../../core/gui/dom";
export class QuestInfoView extends ResizableWidget {
readonly element = el.div({ class: "quest_editor_QuestInfoView", tab_index: -1 });
readonly element = div({ className: "quest_editor_QuestInfoView", tabIndex: -1 });
private readonly table_element = el.table();
private readonly table_element = table();
private readonly episode_element: HTMLElement;
private readonly id_input = this.disposable(new NumberInput(0));
private readonly id_input = this.disposable(new NumberInput(0, { min: 0, step: 1 }));
private readonly name_input = this.disposable(
new TextInput("", {
max_length: 32,
@ -47,13 +47,13 @@ export class QuestInfoView extends ResizableWidget {
const quest = ctrl.current_quest;
this.table_element.append(
el.tr({}, el.th({ text: "Episode:" }), (this.episode_element = el.td())),
el.tr({}, el.th({ text: "ID:" }), el.td({}, this.id_input.element)),
el.tr({}, el.th({ text: "Name:" }), el.td({}, this.name_input.element)),
el.tr({}, el.th({ text: "Short description:", col_span: 2 })),
el.tr({}, el.td({ col_span: 2 }, this.short_description_input.element)),
el.tr({}, el.th({ text: "Long description:", col_span: 2 })),
el.tr({}, el.td({ col_span: 2 }, this.long_description_input.element)),
tr(th("Episode:"), (this.episode_element = td())),
tr(th("ID:"), td(this.id_input.element)),
tr(th("Name:"), td(this.name_input.element)),
tr(th({ colSpan: 2 }, "Short description:")),
tr(td({ colSpan: 2 }, this.short_description_input.element)),
tr(th({ colSpan: 2 }, "Long description:")),
tr(td({ colSpan: 2 }, this.long_description_input.element)),
);
this.bind_hidden(this.table_element, ctrl.unavailable);

View File

@ -2,8 +2,8 @@ import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { RendererWidget } from "../../core/gui/RendererWidget";
import { QuestRenderer } from "../rendering/QuestRenderer";
import { GuiStore, GuiTool } from "../../core/stores/GuiStore";
import { el } from "../../core/gui/dom";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { div } from "../../core/gui/dom";
export abstract class QuestRendererView extends ResizableWidget {
private readonly renderer_view: RendererWidget;
@ -20,7 +20,7 @@ export abstract class QuestRendererView extends ResizableWidget {
) {
super();
this.element = el.div({ class: className, tab_index: -1 });
this.element = div({ className: className, tabIndex: -1 });
this.renderer = renderer;
this.renderer_view = this.disposable(new RendererWidget(this.renderer));
this.element.append(this.renderer_view.element);

View File

@ -1,5 +1,4 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom";
import { REGISTER_COUNT } from "../scripting/vm/VirtualMachine";
import { TextInput } from "../../core/gui/TextInput";
import { ToolBar } from "../../core/gui/ToolBar";
@ -8,6 +7,7 @@ import { number_to_hex_string } from "../../core/util";
import "./RegistersView.css";
import { Select } from "../../core/gui/Select";
import { QuestRunner } from "../QuestRunner";
import { div } from "../../core/gui/dom";
enum RegisterDisplayType {
Signed,
@ -52,13 +52,13 @@ export class RegistersView extends ResizableWidget {
);
private readonly register_els: TextInput[];
private readonly list_element = el.div({ class: "quest_editor_RegistersView_list" });
private readonly container_element = el.div(
{ class: "quest_editor_RegistersView_container" },
private readonly list_element = div({ className: "quest_editor_RegistersView_list" });
private readonly container_element = div(
{ className: "quest_editor_RegistersView_container" },
this.list_element,
);
public readonly element = el.div(
{ class: "quest_editor_RegistersView" },
public readonly element = div(
{ className: "quest_editor_RegistersView" },
this.settings_bar.element,
this.container_element,
);
@ -79,8 +79,8 @@ export class RegistersView extends ResizableWidget {
}),
);
const wrapper_el = el.div(
{ class: "quest_editor_RegistersView_register" },
const wrapper_el = div(
{ className: "quest_editor_RegistersView_register" },
value_el.label!.element,
value_el.element,
);

View File

@ -1,13 +1,13 @@
import { Widget } from "../../core/gui/Widget";
import { el } from "../../core/gui/dom";
import { Label } from "../../core/gui/Label";
import "./UnavailableView.css";
import { div } from "../../core/gui/dom";
/**
* Used to show that a view exists but is unavailable at the moment.
*/
export class UnavailableView extends Widget {
readonly element = el.div({ class: "quest_editor_UnavailableView" });
readonly element = div({ className: "quest_editor_UnavailableView" });
private readonly label: Label;

View File

@ -25,7 +25,8 @@ exports[`Renders correctly with a current quest.: should render property inputs
>
<input
class="core_NumberInput_inner core_Input_inner"
step="any"
min="0"
step="1"
type="number"
/>
</span>
@ -134,7 +135,8 @@ exports[`Renders correctly without a current quest.: should render a "No quest l
>
<input
class="core_NumberInput_inner core_Input_inner"
step="any"
min="0"
step="1"
type="number"
/>
</span>

View File

@ -1,7 +1,7 @@
import { entity_data, EntityType } from "../../core/data_formats/parsing/quest/entities";
import { Disposable } from "../../core/observable/Disposable";
import { el } from "../../core/gui/dom";
import { Vector2 } from "three";
import { div } from "../../core/gui/dom";
export type EntityDragEvent = {
readonly entity_type: EntityType;
@ -69,7 +69,7 @@ export function entity_dnd_source(
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = "copy";
e.dataTransfer.setDragImage(el.div(), 0, 0);
e.dataTransfer.setDragImage(div(), 0, 0);
// setData is necessary for FireFox.
e.dataTransfer.setData(
"phantasmal-entity",

View File

@ -26,6 +26,7 @@ import { Disposer } from "../core/observable/Disposer";
import { Disposable } from "../core/observable/Disposable";
import { EntityInfoController } from "./controllers/EntityInfoController";
import { NpcCountsController } from "./controllers/NpcCountsController";
import { EventsController } from "./controllers/EventsController";
export function initialize_quest_editor(
http_client: HttpClient,
@ -78,7 +79,7 @@ export function initialize_quest_editor(
() => new EntityInfoView(disposer.add(new EntityInfoController(quest_editor_store))),
() => new NpcListView(quest_editor_store, entity_image_renderer),
() => new ObjectListView(quest_editor_store, entity_image_renderer),
() => new EventsView(quest_editor_store),
() => new EventsView(disposer.add(new EventsController(quest_editor_store))),
() =>
new QuestRunnerRendererView(
gui_store,

View File

@ -8,7 +8,7 @@ import { AreaUserData } from "./conversion/areas";
import {
ListChangeType,
ListProperty,
ListPropertyChangeEvent,
ListChangeEvent,
} from "../../core/observable/property/list/ListProperty";
import { QuestNpcModel } from "../model/QuestNpcModel";
import { QuestObjectModel } from "../model/QuestObjectModel";
@ -83,7 +83,7 @@ export abstract class QuestModelManager implements Disposable {
);
};
private npcs_changed = (change: ListPropertyChangeEvent<QuestNpcModel>): void => {
private npcs_changed = (change: ListChangeEvent<QuestNpcModel>): void => {
if (change.type === ListChangeType.ListChange) {
this.npc_model_manager.remove(change.removed);
@ -91,7 +91,7 @@ export abstract class QuestModelManager implements Disposable {
}
};
private objects_changed = (change: ListPropertyChangeEvent<QuestObjectModel>): void => {
private objects_changed = (change: ListChangeEvent<QuestObjectModel>): void => {
if (change.type === ListChangeType.ListChange) {
this.object_model_manager.remove(change.removed);

View File

@ -1,6 +1,6 @@
import { property } from "../../core/observable";
import { QuestModel } from "../model/QuestModel";
import { Property, PropertyChangeEvent } from "../../core/observable/property/Property";
import { Property } from "../../core/observable/property/Property";
import { QuestObjectModel } from "../model/QuestObjectModel";
import { QuestNpcModel } from "../model/QuestNpcModel";
import { AreaModel } from "../model/AreaModel";
@ -17,9 +17,6 @@ import { WritableProperty } from "../../core/observable/property/WritablePropert
import { QuestRunner } from "../QuestRunner";
import { AreaStore } from "./AreaStore";
import { disposable_listener } from "../../core/gui/dom";
import { QuestEventModel } from "../model/QuestEventModel";
import { EditEventSectionIdAction } from "../actions/EditEventSectionIdAction";
import { EditEventDelayAction } from "../actions/EditEventDelayAction";
import { Store } from "../../core/stores/Store";
import { LogManager } from "../../core/Logger";
@ -147,14 +144,6 @@ export class QuestEditorStore extends Store {
this.undo.push(new RemoveEntityAction(this, entity)).redo();
};
event_section_id_changed = (event: QuestEventModel, e: PropertyChangeEvent<number>): void => {
this.undo.push(new EditEventSectionIdAction(event, e.old_value, e.value)).redo();
};
event_delay_changed = (event: QuestEventModel, e: PropertyChangeEvent<number>): void => {
this.undo.push(new EditEventDelayAction(event, e.old_value, e.value)).redo();
};
async set_quest(quest?: QuestModel): Promise<void> {
this.undo.reset();

View File

@ -1,4 +1,4 @@
import { el, Icon } from "../../core/gui/dom";
import { div, Icon } from "../../core/gui/dom";
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { FileButton } from "../../core/gui/FileButton";
import { ToolBar } from "../../core/gui/ToolBar";
@ -9,7 +9,7 @@ import { TextureStore } from "../stores/TextureStore";
import { DisposableThreeRenderer } from "../../core/rendering/Renderer";
export class TextureView extends ResizableWidget {
readonly element = el.div({ class: "viewer_TextureView" });
readonly element = div({ className: "viewer_TextureView" });
private readonly open_file_button = new FileButton("Open file...", {
icon_left: Icon.File,

View File

@ -1,10 +1,10 @@
import { ResizableWidget } from "../../../core/gui/ResizableWidget";
import { create_element } from "../../../core/gui/dom";
import "./Model3DSelectListView.css";
import { Property } from "../../../core/observable/property/Property";
import { li, ul } from "../../../core/gui/dom";
export class Model3DSelectListView<T extends { name: string }> extends ResizableWidget {
readonly element = create_element("ul", { class: "viewer_Model3DSelectListView" });
readonly element = ul({ className: "viewer_Model3DSelectListView" });
set borders(borders: boolean) {
if (borders) {
@ -29,9 +29,7 @@ export class Model3DSelectListView<T extends { name: string }> extends Resizable
this.element.onclick = this.list_click;
models.forEach((model, index) => {
this.element.append(
create_element("li", { text: model.name, data: { index: index.toString() } }),
);
this.element.append(li({ data: { index: index.toString() } }, model.name));
});
this.disposable(

View File

@ -1,4 +1,3 @@
import { el } from "../../../core/gui/dom";
import { ResizableWidget } from "../../../core/gui/ResizableWidget";
import "./Model3DView.css";
import { GuiStore, GuiTool } from "../../../core/stores/GuiStore";
@ -10,12 +9,13 @@ import { CharacterClassModel } from "../../model/CharacterClassModel";
import { CharacterClassAnimationModel } from "../../model/CharacterClassAnimationModel";
import { Model3DStore } from "../../stores/Model3DStore";
import { DisposableThreeRenderer } from "../../../core/rendering/Renderer";
import { div } from "../../../core/gui/dom";
const MODEL_LIST_WIDTH = 100;
const ANIMATION_LIST_WIDTH = 140;
export class Model3DView extends ResizableWidget {
readonly element = el.div({ class: "viewer_Model3DView" });
readonly element = div({ className: "viewer_Model3DView" });
private tool_bar_view: Model3DToolBar;
private model_list_view: Model3DSelectListView<CharacterClassModel>;
@ -52,8 +52,8 @@ export class Model3DView extends ResizableWidget {
this.element.append(
this.tool_bar_view.element,
el.div(
{ class: "viewer_Model3DView_container" },
div(
{ className: "viewer_Model3DView_container" },
this.model_list_view.element,
this.animation_list_view.element,
this.renderer_view.element,