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 { NavigationView } from "./NavigationView";
import { MainContentView } from "./MainContentView"; import { MainContentView } from "./MainContentView";
import { el } from "../../core/gui/dom";
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; import { GuiStore, GuiTool } from "../../core/stores/GuiStore";
import { div } from "../../core/gui/dom";
/** /**
* The top-level view which contains all other views. * 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.menu_view = this.disposable(new NavigationView(gui_store));
this.main_content_view = this.disposable(new MainContentView(gui_store, tool_views)); this.main_content_view = this.disposable(new MainContentView(gui_store, tool_views));
this.element = el.div( this.element = div(
{ class: "application_ApplicationView" }, { className: "application_ApplicationView" },
this.menu_view.element, this.menu_view.element,
this.main_content_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 { GuiStore, GuiTool } from "../../core/stores/GuiStore";
import { LazyWidget } from "../../core/gui/LazyWidget"; import { LazyWidget } from "../../core/gui/LazyWidget";
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { ChangeEvent } from "../../core/observable/Observable"; import { ChangeEvent } from "../../core/observable/Observable";
import { div } from "../../core/gui/dom";
export class MainContentView extends ResizableWidget { export class MainContentView extends ResizableWidget {
readonly element = el.div({ class: "application_MainContentView" }); readonly element = div({ className: "application_MainContentView" });
private tool_views: Map<GuiTool, LazyWidget>; private tool_views: Map<GuiTool, LazyWidget>;

View File

@ -1,13 +1,13 @@
import { Widget } from "../../core/gui/Widget"; import { Widget } from "../../core/gui/Widget";
import { create_element, el } from "../../core/gui/dom";
import { GuiTool } from "../../core/stores/GuiStore"; import { GuiTool } from "../../core/stores/GuiStore";
import "./NavigationButton.css"; import "./NavigationButton.css";
import { input, label, span } from "../../core/gui/dom";
export class NavigationButton extends Widget { 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 input: HTMLInputElement = input();
private label: HTMLLabelElement = create_element("label"); private label: HTMLLabelElement = label();
constructor(tool: GuiTool, text: string) { constructor(tool: GuiTool, text: string) {
super(); 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 "./NavigationView.css";
import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; import { GuiStore, GuiTool } from "../../core/stores/GuiStore";
import { Widget } from "../../core/gui/Widget"; import { Widget } from "../../core/gui/Widget";
@ -26,22 +26,22 @@ export class NavigationView extends Widget {
}), }),
); );
readonly element = el.div( readonly element = div(
{ class: "application_NavigationView" }, { className: "application_NavigationView" },
...[...this.buttons.values()].map(button => button.element), ...[...this.buttons.values()].map(button => button.element),
el.div({ class: "application_NavigationView_spacer" }), div({ className: "application_NavigationView_spacer" }),
el.span( span(
{ class: "application_NavigationView_server" }, { className: "application_NavigationView_server" },
this.server_select.label!.element, this.server_select.label!.element,
this.server_select.element, this.server_select.element,
), ),
el.a( a(
{ {
class: "application_NavigationView_github", className: "application_NavigationView_github",
href: "https://github.com/DaanVandenBosch/phantasmal-world", href: "https://github.com/DaanVandenBosch/phantasmal-world",
title: "GitHub", 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 "./Button.css";
import { Observable } from "../observable/Observable"; import { Observable } from "../observable/Observable";
import { emitter } from "../observable"; import { emitter } from "../observable";
@ -15,7 +15,7 @@ export type ButtonOptions = WidgetOptions & {
}; };
export class Button extends Control { export class Button extends Control {
readonly element = el.button({ class: "core_Button" }); readonly element = button({ className: "core_Button" });
readonly mousedown: Observable<MouseEvent>; readonly mousedown: Observable<MouseEvent>;
readonly mouseup: Observable<MouseEvent>; readonly mouseup: Observable<MouseEvent>;
readonly click: Observable<MouseEvent>; readonly click: Observable<MouseEvent>;
@ -30,18 +30,20 @@ export class Button extends Control {
constructor(text: string | Property<string>, options?: ButtonOptions) { constructor(text: string | Property<string>, options?: ButtonOptions) {
super(options); 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) { 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); inner_element.append(this.center_element);
if (options && options.icon_right != undefined) { 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>(); this._mousedown = emitter<MouseEvent>();

View File

@ -1,12 +1,12 @@
import { create_element } from "./dom";
import { WritableProperty } from "../observable/property/WritableProperty"; import { WritableProperty } from "../observable/property/WritableProperty";
import { LabelledControl, LabelledControlOptions } from "./LabelledControl"; import { LabelledControl, LabelledControlOptions } from "./LabelledControl";
import { WidgetProperty } from "../observable/property/WidgetProperty"; import { WidgetProperty } from "../observable/property/WidgetProperty";
import { input } from "./dom";
export type CheckBoxOptions = LabelledControlOptions; export type CheckBoxOptions = LabelledControlOptions;
export class CheckBox extends LabelledControl { 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"; readonly preferred_label_position = "right";

View File

@ -1,5 +1,5 @@
import { LabelledControl, LabelledControlOptions } from "./LabelledControl"; import { LabelledControl, LabelledControlOptions } from "./LabelledControl";
import { create_element, el, Icon, icon } from "./dom"; import { Icon, icon, input, span } from "./dom";
import "./ComboBox.css"; import "./ComboBox.css";
import "./Input.css"; import "./Input.css";
import { Menu } from "./Menu"; import { Menu } from "./Menu";
@ -15,7 +15,7 @@ export type ComboBoxOptions<T> = LabelledControlOptions & {
}; };
export class ComboBox<T> extends LabelledControl { 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"; readonly preferred_label_position = "left";
@ -23,7 +23,7 @@ export class ComboBox<T> extends LabelledControl {
private readonly to_label: (element: T) => string; private readonly to_label: (element: T) => string;
private readonly menu: Menu<T>; 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>; private readonly _selected: WidgetProperty<T | undefined>;
constructor(options: ComboBoxOptions<T>) { constructor(options: ComboBoxOptions<T>) {
@ -89,17 +89,17 @@ export class ComboBox<T> extends LabelledControl {
this.menu.visible.set_val(false, { silent: false }); 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); 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( this.bind_hidden(
up_arrow_element, up_arrow_element,
this.menu.visible.map(v => !v), this.menu.visible.map(v => !v),
); );
const button_element = el.span( const button_element = span(
{ class: "core_ComboBox_button" }, { className: "core_ComboBox_button" },
down_arrow_element, down_arrow_element,
up_arrow_element, up_arrow_element,
); );
@ -109,8 +109,8 @@ export class ComboBox<T> extends LabelledControl {
}; };
this.element.append( this.element.append(
el.span( span(
{ class: "core_ComboBox_inner core_Input_inner" }, { className: "core_ComboBox_inner core_Input_inner" },
this.input_element, this.input_element,
button_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 "./DropDown.css";
import { Property } from "../observable/property/Property"; import { Property } from "../observable/property/Property";
import { Button, ButtonOptions } from "./Button"; import { Button, ButtonOptions } from "./Button";
@ -15,7 +15,7 @@ export type DropDownOptions<T> = ButtonOptions & {
}; };
export class DropDown<T> extends Control { export class DropDown<T> extends Control {
readonly element = el.div({ class: "core_DropDown" }); readonly element = div({ className: "core_DropDown" });
readonly chosen: Observable<T>; readonly chosen: Observable<T>;

View File

@ -1,7 +1,7 @@
import { ResizableWidget } from "./ResizableWidget"; import { ResizableWidget } from "./ResizableWidget";
import { el } from "./dom";
import { UnavailableView } from "../../quest_editor/gui/UnavailableView"; import { UnavailableView } from "../../quest_editor/gui/UnavailableView";
import "./ErrorView.css"; import "./ErrorView.css";
import { div } from "./dom";
export class ErrorView extends ResizableWidget { export class ErrorView extends ResizableWidget {
readonly element: HTMLElement; readonly element: HTMLElement;
@ -9,11 +9,11 @@ export class ErrorView extends ResizableWidget {
constructor(message: string) { constructor(message: string) {
super(); super();
this.element = el.div( this.element = div(
{ { className: "core_ErrorView" },
class: "core_ErrorView",
},
this.disposable(new UnavailableView(message)).element, 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 "./FileButton.css";
import { property } from "../observable"; import { property } from "../observable";
import { Property } from "../observable/property/Property"; import { Property } from "../observable/property/Property";
@ -11,14 +11,14 @@ export type FileButtonOptions = ControlOptions & {
}; };
export class FileButton extends Control { export class FileButton extends Control {
readonly element = create_element("label", { readonly element = label({
class: "core_FileButton core_Button", className: "core_FileButton core_Button",
}); });
readonly files: Property<File[]>; readonly files: Property<File[]>;
private input: HTMLInputElement = create_element("input", { private input: HTMLInputElement = input({
class: "core_FileButton_input core_Button_inner", className: "core_FileButton_input core_Button_inner",
}); });
private readonly _files: WritableProperty<File[]> = property<File[]>([]); 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; if (options && options.accept) this.input.accept = options.accept;
const inner_element = el.span({ const inner_element = span({
class: "core_FileButton_inner core_Button_inner", className: "core_FileButton_inner core_Button_inner",
}); });
if (options && options.icon_left != undefined) { if (options && options.icon_left != undefined) {
inner_element.append( inner_element.append(
el.span( span(
{ class: "core_FileButton_left core_Button_left" }, { className: "core_FileButton_left core_Button_left" },
icon(options.icon_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); this.element.append(inner_element, this.input);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
import { Disposable } from "../Disposable"; import { Disposable } from "../Disposable";
import { Disposer } from "../Disposer"; import { Disposer } from "../Disposer";
import { AbstractMinimalProperty } from "./AbstractMinimalProperty"; 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. * 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( observe(
observer: (event: PropertyChangeEvent<T>) => void, observer: (event: ChangeEvent<T>) => void,
options: { call_now?: boolean } = {}, options: { call_now?: boolean } = {},
): Disposable { ): Disposable {
const super_disposable = super.observe(observer, options); const super_disposable = super.observe(observer, options);
if (this.dependency_disposer.length === 0) { if (this.dependency_disposer.length === 0) {
this._val = this.compute_value();
this.dependency_disposer.add_all( this.dependency_disposer.add_all(
...this.dependencies.map(dependency => ...this.dependencies.map(dependency =>
dependency.observe(() => { dependency.observe(() => {
@ -42,13 +45,11 @@ export abstract class DependentProperty<T> extends AbstractMinimalProperty<T> {
this._val = this.compute_value(); this._val = this.compute_value();
if (this._val !== old_value) { if (this._val !== old_value) {
this.emit(old_value); this.emit();
} }
}), }),
), ),
); );
this._val = this.compute_value();
} }
return { return {

View File

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

View File

@ -4,9 +4,9 @@ import { list_property } from "../index";
import { FlatMappedProperty } from "./FlatMappedProperty"; import { FlatMappedProperty } from "./FlatMappedProperty";
import { SimpleListProperty } from "./list/SimpleListProperty"; import { SimpleListProperty } from "./list/SimpleListProperty";
import { MappedListProperty } from "./list/MappedListProperty"; import { MappedListProperty } from "./list/MappedListProperty";
import { is_property, Property, PropertyChangeEvent } from "./Property"; import { is_property, Property } from "./Property";
import { is_list_property } from "./list/ListProperty";
import { FlatMappedListProperty } from "./list/FlatMappedListProperty"; import { FlatMappedListProperty } from "./list/FlatMappedListProperty";
import { ChangeEvent } from "../Observable";
// This suite tests every implementation of Property. // 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`, () => { test(`${name} should call observers immediately if added with call_now set to true`, () => {
const { property } = create(); const { property } = create();
const events: PropertyChangeEvent<any>[] = []; const events: ChangeEvent<any>[] = [];
property.observe(event => events.push(event), { call_now: true }); property.observe(event => events.push(event), { call_now: true });
@ -34,56 +34,52 @@ function test_property(
test(`${name} should propagate updates to mapped properties`, () => { test(`${name} should propagate updates to mapped properties`, () => {
const { property, emit } = create(); const { property, emit } = create();
let i = 0; let i = 0;
const mapped = property.map(() => i++); const mapped = property.map(() => i++);
const events: PropertyChangeEvent<any>[] = []; const initial_value = mapped.val;
const events: ChangeEvent<any>[] = [];
mapped.observe(event => events.push(event)); mapped.observe(event => events.push(event));
emit(); emit();
expect(events.length).toBe(1); expect(events.length).toBe(1);
expect(mapped.val !== initial_value).toBe(true);
}); });
test(`${name} should propagate updates to flat mapped properties`, () => { test(`${name} should propagate updates to flat mapped properties`, () => {
const { property, emit } = create(); const { property, emit } = create();
let i = 0; let i = 0;
const flat_mapped = property.flat_map(() => new SimpleProperty(i++)); 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)); flat_mapped.observe(event => events.push(event));
emit(); emit();
expect(events.length).toBe(1); 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 { property, emit } = create();
const events: PropertyChangeEvent<any>[] = []; const events: ChangeEvent<any>[] = [];
property.observe(event => events.push(event)); property.observe(event => events.push(event));
const initial_value = property.val;
emit(); emit();
expect(events.length).toBe(1); expect(events.length).toBe(1);
expect(events[0].value).toBe(property.val); expect(events[0].value).toBe(property.val);
if (!is_list_property(property)) {
expect(events[0].old_value).toBe(initial_value);
}
emit(); emit();
expect(events.length).toBe(2); expect(events.length).toBe(2);
expect(events[1].value).toBe(property.val); 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), 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 { ChangeEvent, Observable } from "../Observable";
import { Disposable } from "../Disposable"; import { Disposable } from "../Disposable";
export interface PropertyChangeEvent<T> extends ChangeEvent<T> {
old_value: T;
}
export interface Property<T> extends Observable<T> { export interface Property<T> extends Observable<T> {
readonly is_property: true; readonly is_property: true;
@ -13,7 +9,7 @@ export interface Property<T> extends Observable<T> {
get_val(): T; get_val(): T;
observe( observe(
observer: (event: PropertyChangeEvent<T>) => void, observer: (event: ChangeEvent<T>) => void,
options?: { call_now?: boolean }, options?: { call_now?: boolean },
): Disposable; ): Disposable;

View File

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

View File

@ -1,7 +1,7 @@
import { SimpleProperty } from "./SimpleProperty"; import { SimpleProperty } from "./SimpleProperty";
import { SimpleListProperty } from "./list/SimpleListProperty"; import { SimpleListProperty } from "./list/SimpleListProperty";
import { PropertyChangeEvent } from "./Property";
import { WritableProperty } from "./WritableProperty"; import { WritableProperty } from "./WritableProperty";
import { ChangeEvent } from "../Observable";
// This suite tests every implementation of WritableProperty. // This suite tests every implementation of WritableProperty.
@ -13,9 +13,9 @@ function test_writable_property<T>(
create_val: () => T; create_val: () => T;
}, },
): void { ): 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 { property, create_val } = create();
const events: PropertyChangeEvent<T>[] = []; const events: ChangeEvent<T>[] = [];
property.observe(event => events.push(event)); 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 { AbstractProperty } from "../AbstractProperty";
import { Disposable } from "../../Disposable"; import { Disposable } from "../../Disposable";
import { Observable } from "../../Observable"; import { Observable } from "../../Observable";
@ -28,7 +28,7 @@ class LengthProperty extends AbstractProperty<number> {
if (old_length !== length) { if (old_length !== length) {
this.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. * 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>[]) { protected constructor(extract_observables?: (element: T) => Observable<any>[]) {
super(); super();
@ -64,7 +64,7 @@ export abstract class AbstractListProperty<T> extends AbstractProperty<readonly
} }
observe_list( observe_list(
observer: (change: ListPropertyChangeEvent<T>) => void, observer: (change: ListChangeEvent<T>) => void,
options?: { call_now?: boolean }, options?: { call_now?: boolean },
): Disposable { ): Disposable {
if (this.value_observers.length === 0 && this.extract_observables) { 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: * Does the following in the given order:
* - Updates value observers * - Updates value observers
* - Emits length PropertyChangeEvent if necessary * - Emits length ChangeEvent if necessary
* - Emits ListPropertyChangeEvent * - Emits ListPropertyChangeEvent
* - Emits PropertyChangeEvent * - Emits ChangeEvent
*/ */
protected finalize_update(change: ListPropertyChangeEvent<T>): void { protected finalize_update(change: ListChangeEvent<T>): void {
if ( if (
this.list_observers.length && this.list_observers.length &&
this.extract_observables && this.extract_observables &&
@ -133,12 +133,12 @@ export abstract class AbstractListProperty<T> extends AbstractProperty<readonly
this.call_list_observer(observer, change); this.call_list_observer(observer, change);
} }
this.emit(this.val); this.emit();
} }
private call_list_observer( private call_list_observer(
observer: (change: ListPropertyChangeEvent<T>) => void, observer: (change: ListChangeEvent<T>) => void,
change: ListPropertyChangeEvent<T>, change: ListChangeEvent<T>,
): void { ): void {
try { try {
observer(change); observer(change);

View File

@ -1,8 +1,9 @@
import { ListChangeType, ListPropertyChangeEvent } from "./ListProperty"; import { ListChangeType, ListChangeEvent } from "./ListProperty";
import { Property, PropertyChangeEvent } from "../Property"; import { Property } from "../Property";
import { Disposable } from "../../Disposable"; import { Disposable } from "../../Disposable";
import { AbstractListProperty } from "./AbstractListProperty"; import { AbstractListProperty } from "./AbstractListProperty";
import { Disposer } from "../../Disposer"; import { Disposer } from "../../Disposer";
import { ChangeEvent } from "../../Observable";
/** /**
* Starts observing its dependencies when the first observer on this property is registered. * 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( observe(
observer: (event: PropertyChangeEvent<readonly T[]>) => void, observer: (event: ChangeEvent<readonly T[]>) => void,
options: { call_now?: boolean } = {}, options: { call_now?: boolean } = {},
): Disposable { ): Disposable {
const super_disposable = super.observe(observer, options); const super_disposable = super.observe(observer, options);
@ -46,7 +47,7 @@ export abstract class DependentListProperty<T> extends AbstractListProperty<T> {
} }
observe_list( observe_list(
observer: (change: ListPropertyChangeEvent<T>) => void, observer: (change: ListChangeEvent<T>) => void,
options?: { call_now?: boolean }, options?: { call_now?: boolean },
): Disposable { ): Disposable {
const super_disposable = super.observe_list(observer, options); 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 { private init_dependency_disposables(): void {
if (this.dependency_disposer.length === 0) { if (this.dependency_disposer.length === 0) {
this.values = this.compute_values();
this.dependency_disposer.add_all( this.dependency_disposer.add_all(
...this.dependencies.map(dependency => ...this.dependencies.map(dependency =>
dependency.observe(() => { 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 { Disposable } from "../../Disposable";
import { MappedProperty } from "../MappedProperty"; import { MappedProperty } from "../MappedProperty";
import { is_property, Property, PropertyChangeEvent } from "../Property"; import { is_property, Property } from "../Property";
import { ListProperty, ListPropertyChangeEvent } from "./ListProperty"; import { ListProperty, ListChangeEvent } from "./ListProperty";
import { FlatMappedProperty } from "../FlatMappedProperty"; import { FlatMappedProperty } from "../FlatMappedProperty";
import { DependentListProperty } from "./DependentListProperty"; import { DependentListProperty } from "./DependentListProperty";
import { MappedListProperty } from "./MappedListProperty"; import { MappedListProperty } from "./MappedListProperty";
import { ChangeEvent } from "../../Observable";
export class FlatMappedListProperty<T> extends DependentListProperty<T> { export class FlatMappedListProperty<T> extends DependentListProperty<T> {
private computed_property?: ListProperty<T>; private computed_property?: ListProperty<T>;
@ -18,7 +19,7 @@ export class FlatMappedListProperty<T> extends DependentListProperty<T> {
} }
observe( observe(
observer: (event: PropertyChangeEvent<readonly T[]>) => void, observer: (event: ChangeEvent<readonly T[]>) => void,
options?: { call_now?: boolean }, options?: { call_now?: boolean },
): Disposable { ): Disposable {
const super_disposable = super.observe(observer, options); const super_disposable = super.observe(observer, options);
@ -37,7 +38,7 @@ export class FlatMappedListProperty<T> extends DependentListProperty<T> {
} }
observe_list( observe_list(
observer: (change: ListPropertyChangeEvent<T>) => void, observer: (change: ListChangeEvent<T>) => void,
options?: { call_now?: boolean }, options?: { call_now?: boolean },
): Disposable { ): Disposable {
const super_disposable = super.observe_list(observer, options); const super_disposable = super.observe_list(observer, options);
@ -72,10 +73,8 @@ export class FlatMappedListProperty<T> extends DependentListProperty<T> {
this.computed_property = this.compute(); this.computed_property = this.compute();
const old_value = this.computed_property.val;
this.computed_disposable = this.computed_property.observe(() => { this.computed_disposable = this.computed_property.observe(() => {
this.emit(old_value); this.emit();
}); });
return this.computed_property.val; return this.computed_property.val;

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { SimpleListProperty } from "./SimpleListProperty"; import { SimpleListProperty } from "./SimpleListProperty";
import { ListChangeType, ListPropertyChangeEvent } from "./ListProperty"; import { ListChangeType, ListChangeEvent } from "./ListProperty";
test("constructor", () => { test("constructor", () => {
const list = new SimpleListProperty<number>(undefined, 1, 2, 3); const list = new SimpleListProperty<number>(undefined, 1, 2, 3);
@ -9,7 +9,7 @@ test("constructor", () => {
}); });
test("push", () => { test("push", () => {
const changes: ListPropertyChangeEvent<number>[] = []; const changes: ListChangeEvent<number>[] = [];
const list = new SimpleListProperty<number>(); const list = new SimpleListProperty<number>();
list.observe_list(change => changes.push(change)); 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 { ResizableWidget } from "../../core/gui/ResizableWidget";
import "./HelpView.css"; import "./HelpView.css";
import { div, p } from "../../core/gui/dom";
export class HelpView extends ResizableWidget { export class HelpView extends ResizableWidget {
readonly element = el.div( readonly element = div(
{ class: "hunt_optimizer_HelpView" }, { className: "hunt_optimizer_HelpView" },
el.p({ p(
text: "Add some items with the combo box on the left to see the optimal combination of hunt methods on the right.",
"Add some items with the combo box on the left to see the optimal combination of hunt methods on the right.", ),
}), p(
el.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.',
text: ),
'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.', p("Only enemy drops are considered. Box drops are coming."),
}), p(
el.p({ text: "Only enemy drops are considered. Box drops are coming." }), "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.",
el.p({ ),
text:
"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() { constructor() {

View File

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

View File

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

View File

@ -1,13 +1,13 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom";
import { WantedItemsView } from "./WantedItemsView"; import { WantedItemsView } from "./WantedItemsView";
import "./OptimizerView.css"; import "./OptimizerView.css";
import { OptimizationResultView } from "./OptimizationResultView"; import { OptimizationResultView } from "./OptimizationResultView";
import { ServerMap } from "../../core/stores/ServerMap"; import { ServerMap } from "../../core/stores/ServerMap";
import { HuntOptimizerStore } from "../stores/HuntOptimizerStore"; import { HuntOptimizerStore } from "../stores/HuntOptimizerStore";
import { div } from "../../core/gui/dom";
export class OptimizerView extends ResizableWidget { 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>) { constructor(hunt_optimizer_stores: ServerMap<HuntOptimizerStore>) {
super(); 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 "./WantedItemsView.css";
import { Button } from "../../core/gui/Button"; import { Button } from "../../core/gui/Button";
import { Disposer } from "../../core/observable/Disposer"; import { Disposer } from "../../core/observable/Disposer";
@ -16,9 +16,9 @@ import { LogManager } from "../../core/Logger";
const logger = LogManager.get("hunt_optimizer/gui/WantedItemsView"); const logger = LogManager.get("hunt_optimizer/gui/WantedItemsView");
export class WantedItemsView extends Widget { 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()); private readonly store_disposer = this.disposable(new Disposer());
constructor(private readonly hunt_optimizer_stores: ServerMap<HuntOptimizerStore>) { constructor(private readonly hunt_optimizer_stores: ServerMap<HuntOptimizerStore>) {
@ -42,11 +42,11 @@ export class WantedItemsView extends Widget {
); );
this.element.append( this.element.append(
el.h2({ text: "Wanted Items" }), h2("Wanted Items"),
combo_box.element, combo_box.element,
el.div( div(
{ class: "hunt_optimizer_WantedItemsView_table_wrapper" }, { className: "hunt_optimizer_WantedItemsView_table_wrapper" },
el.table({}, this.tbody_element), table(this.tbody_element),
), ),
); );
@ -108,12 +108,7 @@ export class WantedItemsView extends Widget {
); );
return [ return [
el.tr( tr(td(amount_input.element), td(wanted_item.item_type.name), td(remove_button.element)),
{},
el.td({}, amount_input.element),
el.td({ text: wanted_item.item_type.name }),
el.td({}, remove_button.element),
),
row_disposer, 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 { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor";
import { Endianness } from "../../core/data_formats/Endianness"; import { Endianness } from "../../core/data_formats/Endianness";
import { convert_quest_from_model, convert_quest_to_model } from "../stores/model_conversion"; 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 { LogManager } from "../../core/Logger";
import { input } from "../../core/gui/dom";
const logger = LogManager.get("quest_editor/controllers/QuestEditorToolBarController"); 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", () => { gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-O", () => {
const input: HTMLInputElement = create_element("input"); const input_element = input();
input.type = "file"; input_element.type = "file";
input.onchange = () => { input_element.onchange = () => {
if (input.files && input.files.length) { if (input_element.files && input_element.files.length) {
this.open_file(input.files[0]); 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), 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 { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom";
import { editor, KeyCode, KeyMod, Range } from "monaco-editor"; import { editor, KeyCode, KeyMod, Range } from "monaco-editor";
import { AsmEditorToolBar } from "./AsmEditorToolBar"; import { AsmEditorToolBar } from "./AsmEditorToolBar";
import { EditorHistory } from "./EditorHistory"; import { EditorHistory } from "./EditorHistory";
@ -8,6 +7,7 @@ import { ListChangeType } from "../../core/observable/property/list/ListProperty
import { GuiStore } from "../../core/stores/GuiStore"; import { GuiStore } from "../../core/stores/GuiStore";
import { AsmEditorStore } from "../stores/AsmEditorStore"; import { AsmEditorStore } from "../stores/AsmEditorStore";
import { QuestRunner } from "../QuestRunner"; import { QuestRunner } from "../QuestRunner";
import { div } from "../../core/gui/dom";
import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; import IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
editor.defineTheme("phantasmal-world", { editor.defineTheme("phantasmal-world", {
@ -37,8 +37,9 @@ export class AsmEditorView extends ResizableWidget {
private readonly history: EditorHistory; private readonly history: EditorHistory;
private breakpoint_decoration_ids: string[] = []; private breakpoint_decoration_ids: string[] = [];
private execloc_decoration_id: string | undefined; private execloc_decoration_id: string | undefined;
private old_pause_location?: number;
readonly element = el.div(); readonly element = div();
constructor( constructor(
gui_store: GuiStore, gui_store: GuiStore,
@ -171,8 +172,9 @@ export class AsmEditorView extends ResizableWidget {
}), }),
asm_editor_store.pause_location.observe(e => { 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; const new_line_num = e.value;
this.old_pause_location = new_line_num;
// remove old // remove old
if (old_line_num !== undefined && this.execloc_decoration_id !== undefined) { if (old_line_num !== undefined && this.execloc_decoration_id !== undefined) {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,13 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom";
import { QuestEventDagModel } from "../model/QuestEventDagModel"; import { QuestEventDagModel } from "../model/QuestEventDagModel";
import { Disposer } from "../../core/observable/Disposer"; import { Disposer } from "../../core/observable/Disposer";
import { NumberInput } from "../../core/gui/NumberInput";
import "./EventsView.css"; 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 { defer } from "lodash";
import { import { NumberInput } from "../../core/gui/NumberInput";
ListChangeType,
ListPropertyChangeEvent,
} from "../../core/observable/property/list/ListProperty";
import { QuestEditorStore } from "../stores/QuestEditorStore";
type DagGuiData = { type DagGuiData = {
dag: QuestEventDagModel; dag: QuestEventDagModel;
@ -25,19 +22,28 @@ type DagGuiData = {
export class EventsView extends ResizableWidget { export class EventsView extends ResizableWidget {
private readonly dag_gui_data: DagGuiData[] = []; 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(); super();
this.element.addEventListener("focus", () => quest_editor_store.undo.make_current(), true); this.element.addEventListener("focus", ctrl.focused, true);
this.disposables( this.disposables(
quest_editor_store.current_quest.observe(this.update), bind_attr(this.container_element, "hidden", ctrl.unavailable),
quest_editor_store.current_area.observe(this.update), this.unavailable_view.visible.bind_to(ctrl.unavailable),
this.enabled.bind_to(quest_editor_store.quest_runner.running.map(r => !r)),
this.enabled.bind_to(ctrl.enabled),
ctrl.event_dags.observe_list(this.observe_event_dags),
); );
this.finalize_construction(); this.finalize_construction();
@ -57,54 +63,12 @@ export class EventsView extends ResizableWidget {
dispose(): void { dispose(): void {
super.dispose(); super.dispose();
if (this.event_dags_observer) {
this.event_dags_observer.dispose();
}
for (const { disposer } of this.dag_gui_data) { for (const { disposer } of this.dag_gui_data) {
disposer.dispose(); disposer.dispose();
} }
} }
private update = (): void => { private observe_event_dags = (change: ListChangeEvent<QuestEventDagModel>): 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 => {
if (change.type === ListChangeType.ListChange) { if (change.type === ListChangeType.ListChange) {
for (const removed of this.dag_gui_data.splice(change.index, change.removed.length)) { for (const removed of this.dag_gui_data.splice(change.index, change.removed.length)) {
removed.element.remove(); removed.element.remove();
@ -116,7 +80,10 @@ export class EventsView extends ResizableWidget {
for (const dag of change.inserted) { for (const dag of change.inserted) {
const data = this.create_dag_ui_data(dag); const data = this.create_dag_ui_data(dag);
this.dag_gui_data.splice(index, 0, data); 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++; index++;
} }
@ -129,15 +96,13 @@ export class EventsView extends ResizableWidget {
const disposer = new Disposer(); const disposer = new Disposer();
const event_gui_data = new Map<number, { element: HTMLDivElement; position: number }>(); 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({ const edge_container_element = div({
class: "quest_editor_EventsView_edge_container", className: "quest_editor_EventsView_edge_container",
}); });
element.append(edge_container_element); element.append(edge_container_element);
const inputs_enabled = this.quest_editor_store.quest_runner.running.map(r => !r);
dag.events.forEach((event, i) => { dag.events.forEach((event, i) => {
const section_id_input = disposer.add(new NumberInput(event.section_id.val)); const section_id_input = disposer.add(new NumberInput(event.section_id.val));
@ -145,25 +110,21 @@ export class EventsView extends ResizableWidget {
disposer.add_all( disposer.add_all(
section_id_input.value.bind_to(event.section_id), section_id_input.value.bind_to(event.section_id),
section_id_input.value.observe(e => section_id_input.value.observe(e => this.ctrl.set_section_id(event, e.value)),
this.quest_editor_store.event_section_id_changed(event, e), section_id_input.enabled.bind_to(this.ctrl.enabled),
),
section_id_input.enabled.bind_to(inputs_enabled),
delay_input.value.bind_to(event.delay), delay_input.value.bind_to(event.delay),
delay_input.value.observe(e => delay_input.value.observe(e => this.ctrl.set_delay(event, e.value)),
this.quest_editor_store.event_delay_changed(event, e), delay_input.enabled.bind_to(this.ctrl.enabled),
),
delay_input.enabled.bind_to(inputs_enabled),
); );
const event_element = el.div( const event_element = div(
{ class: "quest_editor_EventsView_event" }, { className: "quest_editor_EventsView_event" },
el.table( table(
el.tr(el.th({ text: "ID:" }), el.td({ text: event.id.toString() })), tr(th("ID:"), td(event.id.toString())),
el.tr(el.th({ text: "Section:" }), el.td(section_id_input.element)), tr(th("Section:"), td(section_id_input.element)),
el.tr(el.th({ text: "Wave:" }), el.td({ text: event.wave.toString() })), tr(th("Wave:"), td(event.wave.toString())),
el.tr(el.th({ text: "Delay:" }), el.td(delay_input.element)), 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 * 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 => { private update_edges = (): void => {
const SPACING = 8; const SPACING = 8;
@ -208,7 +169,7 @@ export class EventsView extends ResizableWidget {
)!; )!;
const child_y_offset = child_element.offsetTop; 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 top = Math.min(y_offset, child_y_offset) - 20;
const height = Math.max(y_offset, child_y_offset) - top + 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 { ResizableWidget } from "../../core/gui/ResizableWidget";
import { ToolBar } from "../../core/gui/ToolBar"; import { ToolBar } from "../../core/gui/ToolBar";
import "./LogView.css"; import "./LogView.css";
@ -9,7 +9,7 @@ import { LogEntry, LogLevel, LogLevels, time_to_string } from "../../core/Logger
const AUTOSCROLL_TRESHOLD = 5; const AUTOSCROLL_TRESHOLD = 5;
export class LogView extends ResizableWidget { 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 // container is needed to get a scrollbar in the right place
private readonly list_container: HTMLElement; private readonly list_container: HTMLElement;
@ -23,8 +23,8 @@ export class LogView extends ResizableWidget {
constructor() { constructor() {
super(); super();
this.list_container = el.div({ class: "quest_editor_LogView_list_container" }); this.list_container = div({ className: "quest_editor_LogView_list_container" });
this.list_element = el.div({ class: "quest_editor_LogView_message_list" }); this.list_element = div({ className: "quest_editor_LogView_message_list" });
this.level_filter = this.disposable( this.level_filter = this.disposable(
new Select({ new Select({
@ -87,25 +87,16 @@ export class LogView extends ResizableWidget {
}; };
private create_message_element = ({ time, level, message }: LogEntry): HTMLElement => { private create_message_element = ({ time, level, message }: LogEntry): HTMLElement => {
return el.div( return div(
{ {
class: [ className: [
"quest_editor_LogView_message", "quest_editor_LogView_message",
"quest_editor_LogView_" + LogLevel[level] + "_message", "quest_editor_LogView_" + LogLevel[level] + "_message",
].join(" "), ].join(" "),
}, },
el.div({ div({ className: "quest_editor_LogView_message_timestamp" }, time_to_string(time)),
class: "quest_editor_LogView_message_timestamp", div({ className: "quest_editor_LogView_message_level" }, "[" + LogLevel[level] + "]"),
text: time_to_string(time), div({ className: "quest_editor_LogView_message_contents" }, message),
}),
el.div({
class: "quest_editor_LogView_message_level",
text: "[" + LogLevel[level] + "]",
}),
el.div({
class: "quest_editor_LogView_message_contents",
text: message,
}),
); );
}; };
} }

View File

@ -1,13 +1,13 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget"; 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 "./NpcCountsView.css";
import { UnavailableView } from "./UnavailableView"; import { UnavailableView } from "./UnavailableView";
import { NameWithCount, NpcCountsController } from "../controllers/NpcCountsController"; import { NameWithCount, NpcCountsController } from "../controllers/NpcCountsController";
export class NpcCountsView extends ResizableWidget { 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."); private readonly unavailable_view = new UnavailableView("No quest loaded.");
@ -31,7 +31,7 @@ export class NpcCountsView extends ResizableWidget {
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
for (const { name, count } of npcs) { 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 = ""; this.table_element.innerHTML = "";

View File

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

View File

@ -1,5 +1,4 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget"; import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { el } from "../../core/gui/dom";
import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { NumberInput } from "../../core/gui/NumberInput"; import { NumberInput } from "../../core/gui/NumberInput";
import { Disposer } from "../../core/observable/Disposer"; import { Disposer } from "../../core/observable/Disposer";
@ -8,13 +7,14 @@ import { TextArea } from "../../core/gui/TextArea";
import "./QuestInfoView.css"; import "./QuestInfoView.css";
import { UnavailableView } from "./UnavailableView"; import { UnavailableView } from "./UnavailableView";
import { QuestInfoController } from "../controllers/QuestInfoController"; import { QuestInfoController } from "../controllers/QuestInfoController";
import { div, table, td, th, tr } from "../../core/gui/dom";
export class QuestInfoView extends ResizableWidget { 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 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( private readonly name_input = this.disposable(
new TextInput("", { new TextInput("", {
max_length: 32, max_length: 32,
@ -47,13 +47,13 @@ export class QuestInfoView extends ResizableWidget {
const quest = ctrl.current_quest; const quest = ctrl.current_quest;
this.table_element.append( this.table_element.append(
el.tr({}, el.th({ text: "Episode:" }), (this.episode_element = el.td())), tr(th("Episode:"), (this.episode_element = td())),
el.tr({}, el.th({ text: "ID:" }), el.td({}, this.id_input.element)), tr(th("ID:"), td(this.id_input.element)),
el.tr({}, el.th({ text: "Name:" }), el.td({}, this.name_input.element)), tr(th("Name:"), td(this.name_input.element)),
el.tr({}, el.th({ text: "Short description:", col_span: 2 })), tr(th({ colSpan: 2 }, "Short description:")),
el.tr({}, el.td({ col_span: 2 }, this.short_description_input.element)), tr(td({ colSpan: 2 }, this.short_description_input.element)),
el.tr({}, el.th({ text: "Long description:", col_span: 2 })), tr(th({ colSpan: 2 }, "Long description:")),
el.tr({}, el.td({ col_span: 2 }, this.long_description_input.element)), tr(td({ colSpan: 2 }, this.long_description_input.element)),
); );
this.bind_hidden(this.table_element, ctrl.unavailable); 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 { RendererWidget } from "../../core/gui/RendererWidget";
import { QuestRenderer } from "../rendering/QuestRenderer"; import { QuestRenderer } from "../rendering/QuestRenderer";
import { GuiStore, GuiTool } from "../../core/stores/GuiStore"; import { GuiStore, GuiTool } from "../../core/stores/GuiStore";
import { el } from "../../core/gui/dom";
import { QuestEditorStore } from "../stores/QuestEditorStore"; import { QuestEditorStore } from "../stores/QuestEditorStore";
import { div } from "../../core/gui/dom";
export abstract class QuestRendererView extends ResizableWidget { export abstract class QuestRendererView extends ResizableWidget {
private readonly renderer_view: RendererWidget; private readonly renderer_view: RendererWidget;
@ -20,7 +20,7 @@ export abstract class QuestRendererView extends ResizableWidget {
) { ) {
super(); super();
this.element = el.div({ class: className, tab_index: -1 }); this.element = div({ className: className, tabIndex: -1 });
this.renderer = renderer; this.renderer = renderer;
this.renderer_view = this.disposable(new RendererWidget(this.renderer)); this.renderer_view = this.disposable(new RendererWidget(this.renderer));
this.element.append(this.renderer_view.element); this.element.append(this.renderer_view.element);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import { AreaUserData } from "./conversion/areas";
import { import {
ListChangeType, ListChangeType,
ListProperty, ListProperty,
ListPropertyChangeEvent, ListChangeEvent,
} from "../../core/observable/property/list/ListProperty"; } from "../../core/observable/property/list/ListProperty";
import { QuestNpcModel } from "../model/QuestNpcModel"; import { QuestNpcModel } from "../model/QuestNpcModel";
import { QuestObjectModel } from "../model/QuestObjectModel"; 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) { if (change.type === ListChangeType.ListChange) {
this.npc_model_manager.remove(change.removed); 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) { if (change.type === ListChangeType.ListChange) {
this.object_model_manager.remove(change.removed); this.object_model_manager.remove(change.removed);

View File

@ -1,6 +1,6 @@
import { property } from "../../core/observable"; import { property } from "../../core/observable";
import { QuestModel } from "../model/QuestModel"; 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 { QuestObjectModel } from "../model/QuestObjectModel";
import { QuestNpcModel } from "../model/QuestNpcModel"; import { QuestNpcModel } from "../model/QuestNpcModel";
import { AreaModel } from "../model/AreaModel"; import { AreaModel } from "../model/AreaModel";
@ -17,9 +17,6 @@ import { WritableProperty } from "../../core/observable/property/WritablePropert
import { QuestRunner } from "../QuestRunner"; import { QuestRunner } from "../QuestRunner";
import { AreaStore } from "./AreaStore"; import { AreaStore } from "./AreaStore";
import { disposable_listener } from "../../core/gui/dom"; 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 { Store } from "../../core/stores/Store";
import { LogManager } from "../../core/Logger"; import { LogManager } from "../../core/Logger";
@ -147,14 +144,6 @@ export class QuestEditorStore extends Store {
this.undo.push(new RemoveEntityAction(this, entity)).redo(); 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> { async set_quest(quest?: QuestModel): Promise<void> {
this.undo.reset(); 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 { ResizableWidget } from "../../core/gui/ResizableWidget";
import { FileButton } from "../../core/gui/FileButton"; import { FileButton } from "../../core/gui/FileButton";
import { ToolBar } from "../../core/gui/ToolBar"; import { ToolBar } from "../../core/gui/ToolBar";
@ -9,7 +9,7 @@ import { TextureStore } from "../stores/TextureStore";
import { DisposableThreeRenderer } from "../../core/rendering/Renderer"; import { DisposableThreeRenderer } from "../../core/rendering/Renderer";
export class TextureView extends ResizableWidget { 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...", { private readonly open_file_button = new FileButton("Open file...", {
icon_left: Icon.File, icon_left: Icon.File,

View File

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

View File

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