Initial move
This commit is contained in:
76
frontend/src/ui/audio-recorder.ts
Normal file
76
frontend/src/ui/audio-recorder.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class AudioRecorder extends UINode {
|
||||
private audioElement: HTMLAudioElement;
|
||||
private mediaRecorder: MediaRecorder | null;
|
||||
private audioChunks: Blob[];
|
||||
private stream: MediaStream | null;
|
||||
private recording?: Blob;
|
||||
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.audioElement = document.createElement("audio");
|
||||
this.mediaRecorder = null;
|
||||
this.audioChunks = [];
|
||||
this.stream = null;
|
||||
|
||||
this.audioElement.setAttribute("controls", "true");
|
||||
this.audioElement.setAttribute("aria-label", title);
|
||||
this.element.appendChild(this.audioElement);
|
||||
|
||||
this.setRole("audio-recorder");
|
||||
}
|
||||
|
||||
public async startRecording() {
|
||||
try {
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({ audio: { autoGainControl: true, channelCount: 2, echoCancellation: false, noiseSuppression: false } });
|
||||
this.mediaRecorder = new MediaRecorder(this.stream);
|
||||
this.mediaRecorder.ondataavailable = (event) => {
|
||||
this.audioChunks.push(event.data);
|
||||
};
|
||||
this.mediaRecorder.onstop = () => {
|
||||
const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' });
|
||||
this.recording = audioBlob;
|
||||
this.audioChunks = [];
|
||||
const audioUrl = URL.createObjectURL(audioBlob);
|
||||
this.audioElement.src = audioUrl;
|
||||
this.triggerRecordingComplete(audioUrl);
|
||||
};
|
||||
this.mediaRecorder.start();
|
||||
} catch (error) {
|
||||
console.error("Error accessing microphone:", error);
|
||||
}
|
||||
}
|
||||
|
||||
public stopRecording() {
|
||||
if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
|
||||
this.mediaRecorder.stop();
|
||||
}
|
||||
if (this.stream) {
|
||||
this.stream.getTracks().forEach(track => track.stop());
|
||||
this.stream = null;
|
||||
}
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
public onRecordingComplete(callback: (audioUrl: string) => void) {
|
||||
this.element.addEventListener("recording-complete", (event: Event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
callback(customEvent.detail.audioUrl);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
protected triggerRecordingComplete(audioUrl: string) {
|
||||
const event = new CustomEvent("recording-complete", { detail: { audioUrl } });
|
||||
this.element.dispatchEvent(event);
|
||||
return this;
|
||||
}
|
||||
|
||||
public getRecording() {
|
||||
return this.recording;
|
||||
}
|
||||
}
|
66
frontend/src/ui/audio.ts
Normal file
66
frontend/src/ui/audio.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class Audio extends UINode {
|
||||
private audioElement: HTMLAudioElement;
|
||||
|
||||
public constructor(title: string, src: string | MediaStream = "") {
|
||||
super(title);
|
||||
this.audioElement = document.createElement("audio");
|
||||
if (typeof src === "string") {
|
||||
this.audioElement.src = src; // Set src if it's a string URL
|
||||
} else if (src instanceof MediaStream) {
|
||||
this.audioElement.srcObject = src; // Set srcObject if it's a MediaStream
|
||||
}
|
||||
this.audioElement.setAttribute("aria-label", title);
|
||||
this.element.appendChild(this.audioElement);
|
||||
this.setRole("audio");
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.audioElement;
|
||||
}
|
||||
|
||||
public setSource(src: string | MediaStream) {
|
||||
if (typeof src === "string") {
|
||||
this.audioElement.src = src;
|
||||
} else if (src instanceof MediaStream) {
|
||||
this.audioElement.srcObject = src;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public play() {
|
||||
this.audioElement.play();
|
||||
return this;
|
||||
}
|
||||
|
||||
public pause() {
|
||||
this.audioElement.pause();
|
||||
return this;
|
||||
}
|
||||
|
||||
public setControls(show: boolean) {
|
||||
this.audioElement.controls = show;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setLoop(loop: boolean) {
|
||||
this.audioElement.loop = loop;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setMuted(muted: boolean) {
|
||||
this.audioElement.muted = muted;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setAutoplay(autoplay: boolean) {
|
||||
this.audioElement.autoplay = autoplay;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setVolume(volume: number) {
|
||||
this.audioElement.volume = volume;
|
||||
return this;
|
||||
}
|
||||
}
|
39
frontend/src/ui/button.ts
Normal file
39
frontend/src/ui/button.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class Button extends UINode {
|
||||
private buttonElement: HTMLButtonElement;
|
||||
public constructor(title: string, hasPopup: boolean = false) {
|
||||
super(title);
|
||||
this.buttonElement = document.createElement("button");
|
||||
this.buttonElement.innerText = title;
|
||||
if (hasPopup) this.buttonElement.setAttribute("aria-haspopup", "true");
|
||||
this.element.appendChild(this.buttonElement);
|
||||
this.element.setAttribute("aria-label", this.title);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.buttonElement.focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.buttonElement.click();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.buttonElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.buttonElement.innerText = text;
|
||||
this.element.setAttribute("aria-label", this.title);
|
||||
return this;
|
||||
}
|
||||
|
||||
public setDisabled(val: boolean) {
|
||||
this.buttonElement.disabled = val;
|
||||
return this;
|
||||
}
|
||||
}
|
77
frontend/src/ui/camera.ts
Normal file
77
frontend/src/ui/camera.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class Camera extends UINode {
|
||||
private videoElement: HTMLVideoElement;
|
||||
private canvasElement: HTMLCanvasElement;
|
||||
private stream: MediaStream | null;
|
||||
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.videoElement = document.createElement("video");
|
||||
this.canvasElement = document.createElement("canvas");
|
||||
this.stream = null;
|
||||
|
||||
this.videoElement.setAttribute("aria-label", title);
|
||||
this.element.appendChild(this.videoElement);
|
||||
this.element.appendChild(this.canvasElement);
|
||||
|
||||
this.setRole("camera");
|
||||
}
|
||||
|
||||
public async startCamera() {
|
||||
try {
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||
this.videoElement.srcObject = this.stream;
|
||||
this.videoElement.play();
|
||||
} catch (error) {
|
||||
console.error("Error accessing camera:", error);
|
||||
}
|
||||
}
|
||||
|
||||
public stopCamera() {
|
||||
if (this.stream) {
|
||||
this.stream.getTracks().forEach(track => track.stop());
|
||||
this.stream = null;
|
||||
}
|
||||
this.videoElement.pause();
|
||||
this.videoElement.srcObject = null;
|
||||
}
|
||||
|
||||
public takePhoto(): HTMLCanvasElement | null {
|
||||
if (this.stream) {
|
||||
const context = this.canvasElement.getContext("2d");
|
||||
if (context) {
|
||||
this.canvasElement.width = this.videoElement.videoWidth;
|
||||
this.canvasElement.height = this.videoElement.videoHeight;
|
||||
context.drawImage(this.videoElement, 0, 0, this.canvasElement.width, this.canvasElement.height);
|
||||
return this.canvasElement;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public savePhoto(): string | null {
|
||||
const photoCanvas = this.takePhoto();
|
||||
if (photoCanvas) {
|
||||
return photoCanvas.toDataURL("image/png");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public savePhotoToBlob(): Promise<Blob | null> {
|
||||
return new Promise((resolve) => {
|
||||
const photoCanvas = this.takePhoto();
|
||||
if (photoCanvas) {
|
||||
photoCanvas.toBlob((blob) => {
|
||||
resolve(blob);
|
||||
});
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.element;
|
||||
}
|
||||
}
|
26
frontend/src/ui/canvas.ts
Normal file
26
frontend/src/ui/canvas.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class Canvas extends UINode {
|
||||
private canvasElement: HTMLCanvasElement;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.canvasElement = document.createElement("canvas");
|
||||
|
||||
this.canvasElement.setAttribute("tabindex", "-1");
|
||||
this.element.appendChild(this.canvasElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.canvasElement.focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.canvasElement.click();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.canvasElement;
|
||||
}
|
||||
}
|
50
frontend/src/ui/checkbox.ts
Normal file
50
frontend/src/ui/checkbox.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class Checkbox extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private checkboxElement: HTMLInputElement;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.id = `chkbx_title_${this.id}`;
|
||||
this.checkboxElement = document.createElement("input");
|
||||
this.checkboxElement.id = `chkbx_${this.id}`;
|
||||
this.checkboxElement.type = "checkbox";
|
||||
this.titleElement.appendChild(this.checkboxElement);
|
||||
this.titleElement.appendChild(document.createTextNode(this.title));
|
||||
this.element.appendChild(this.titleElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.checkboxElement.focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.checkboxElement.click();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.checkboxElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
this.element.setAttribute("aria-label", this.title);
|
||||
this.element.setAttribute("aria-roledescription", "checkbox");
|
||||
return this;
|
||||
}
|
||||
|
||||
public isChecked(): boolean {
|
||||
return this.checkboxElement.checked;
|
||||
}
|
||||
|
||||
public setChecked(value: boolean) {
|
||||
this.checkboxElement.checked = value;
|
||||
return this;
|
||||
}
|
||||
}
|
42
frontend/src/ui/collapsable-container.ts
Normal file
42
frontend/src/ui/collapsable-container.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Container } from "./container";
|
||||
|
||||
export class CollapsableContainer extends Container {
|
||||
private detailsElement: HTMLDetailsElement;
|
||||
private summaryElement: HTMLElement;
|
||||
private wrapperElement: HTMLDivElement;
|
||||
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.wrapperElement = document.createElement("div");
|
||||
this.detailsElement = document.createElement("details");
|
||||
this.summaryElement = document.createElement("summary");
|
||||
|
||||
this.summaryElement.innerText = title;
|
||||
this.detailsElement.appendChild(this.summaryElement);
|
||||
this.detailsElement.appendChild(this.containerElement);
|
||||
this.wrapperElement.appendChild(this.detailsElement);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.wrapperElement;
|
||||
}
|
||||
|
||||
public setTitle(text: string) {
|
||||
this.title = text;
|
||||
this.summaryElement.innerText = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public isCollapsed(): boolean {
|
||||
return this.detailsElement.hasAttribute("open");
|
||||
}
|
||||
|
||||
public expand(val: boolean) {
|
||||
if (val) {
|
||||
this.detailsElement.setAttribute("open", "true");
|
||||
} else {
|
||||
this.detailsElement.removeAttribute("open");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
55
frontend/src/ui/container.ts
Normal file
55
frontend/src/ui/container.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class Container extends UINode {
|
||||
public children: UINode[];
|
||||
protected containerElement: HTMLDivElement;
|
||||
private focused: number = 0;
|
||||
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.children = [];
|
||||
this.containerElement = document.createElement("div");
|
||||
this.containerElement.setAttribute("tabindex", "-1");
|
||||
this.focused = 0;
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.containerElement.focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public _onFocus() {
|
||||
this.children[this.focused].focus();
|
||||
}
|
||||
|
||||
public add(node: UINode) {
|
||||
this.children.push(node);
|
||||
node._onConnect();
|
||||
this.containerElement.appendChild(node.render());
|
||||
return this;
|
||||
}
|
||||
|
||||
public remove(node: UINode) {
|
||||
this.children.splice(this.children.indexOf(node), 1);
|
||||
node._onDisconnect();
|
||||
this.containerElement.removeChild(node.render());
|
||||
return this;
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.containerElement;
|
||||
}
|
||||
|
||||
public getChildren(): UINode[] {
|
||||
return this.children;
|
||||
}
|
||||
|
||||
public getElement() {
|
||||
return this.containerElement;
|
||||
}
|
||||
|
||||
public setAriaLabel(text: string) {
|
||||
this.containerElement.setAttribute("aria-label", text);
|
||||
return this;
|
||||
}
|
||||
}
|
44
frontend/src/ui/date-picker.ts
Normal file
44
frontend/src/ui/date-picker.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
|
||||
export class DatePicker extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private inputElement: HTMLInputElement;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `datepicker_title_${this.id}`;
|
||||
this.inputElement = document.createElement("input");
|
||||
this.inputElement.id = `datepicker_${this.id}`;
|
||||
this.inputElement.type = "date";
|
||||
this.titleElement.appendChild(this.inputElement);
|
||||
this.element.appendChild(this.titleElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.inputElement.focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.inputElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.inputElement.value;
|
||||
}
|
||||
|
||||
public setValue(value: string) {
|
||||
this.inputElement.value = value;
|
||||
return this;
|
||||
}
|
||||
}
|
78
frontend/src/ui/dialog.ts
Normal file
78
frontend/src/ui/dialog.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { UIWindow } from "./window";
|
||||
import { Button } from "./button";
|
||||
|
||||
export class Dialog<T> extends UIWindow {
|
||||
private resolvePromise!: (value: T | PromiseLike<T>) => void;
|
||||
private rejectPromise!: (reason?: any) => void;
|
||||
private promise: Promise<T>;
|
||||
private dialogElement!: HTMLDialogElement;
|
||||
protected okButton?: Button;
|
||||
protected cancelButton?: Button;
|
||||
|
||||
private previouslyFocusedElement!: HTMLElement;
|
||||
|
||||
public constructor(title: string, addButtons: boolean = true) {
|
||||
super(title, "dialog", false);
|
||||
this.dialogElement = document.createElement("dialog");
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.rejectPromise = reject;
|
||||
});
|
||||
|
||||
// Automatically add OK and Cancel buttons
|
||||
if (addButtons) {
|
||||
this.okButton = new Button("OK");
|
||||
this.okButton.setPosition(70, 90, 10, 5);
|
||||
this.okButton.onClick(() => this.choose(undefined));
|
||||
|
||||
this.cancelButton = new Button("Cancel");
|
||||
this.cancelButton.setPosition(20, 90, 10, 5);
|
||||
this.cancelButton.onClick(() => this.cancel());
|
||||
}
|
||||
}
|
||||
|
||||
public setOkAction(action: () => T) {
|
||||
if (!this.okButton) return;
|
||||
this.okButton.onClick(() => {
|
||||
const result = action();
|
||||
this.choose(result);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public setCancelAction(action: () => void) {
|
||||
if (!this.cancelButton) return;
|
||||
this.cancelButton.onClick(() => {
|
||||
action();
|
||||
this.cancel();
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public choose(item: T | undefined) {
|
||||
this.resolvePromise(item as T);
|
||||
document.body.removeChild(this.dialogElement);
|
||||
this.hide();
|
||||
this.previouslyFocusedElement.focus();
|
||||
}
|
||||
|
||||
public cancel(reason?: any) {
|
||||
this.rejectPromise(reason);
|
||||
|
||||
document.body.removeChild(this.dialogElement);
|
||||
this.hide();
|
||||
this.previouslyFocusedElement.focus();
|
||||
}
|
||||
|
||||
public open(): Promise<T> {
|
||||
this.previouslyFocusedElement = document.activeElement as HTMLElement;
|
||||
this.dialogElement.appendChild(this.show()!);
|
||||
if (this.okButton) this.add(this.okButton);
|
||||
if (this.cancelButton) this.add(this.cancelButton);
|
||||
document.body.appendChild(this.dialogElement);
|
||||
this.dialogElement.showModal();
|
||||
this.container.focus();
|
||||
|
||||
return this.promise;
|
||||
}
|
||||
}
|
77
frontend/src/ui/dropdown.ts
Normal file
77
frontend/src/ui/dropdown.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class Dropdown extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private selectElement: HTMLSelectElement;
|
||||
|
||||
public constructor(title: string, options: { key: string; value: string }[]) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `dd_title_${this.id}`;
|
||||
this.selectElement = document.createElement("select");
|
||||
this.selectElement.id = `dd_${this.id}`;
|
||||
this.titleElement.appendChild(this.selectElement);
|
||||
this.element.appendChild(this.titleElement);
|
||||
|
||||
this.setOptions(options);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.selectElement.focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.selectElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getSelectedValue(): string {
|
||||
return this.selectElement.value;
|
||||
}
|
||||
|
||||
public setSelectedValue(value: string) {
|
||||
this.selectElement.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setOptions(options: { key: string; value: string }[]) {
|
||||
this.clearOptions();
|
||||
options.forEach((option) => {
|
||||
this.addOption(option.key, option.value);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public addOption(key: string, value: string) {
|
||||
const optionElement = document.createElement("option");
|
||||
optionElement.value = key;
|
||||
optionElement.innerText = value;
|
||||
this.selectElement.appendChild(optionElement);
|
||||
return this;
|
||||
}
|
||||
|
||||
public removeOption(key: string) {
|
||||
const options = Array.from(this.selectElement.options);
|
||||
const optionToRemove = options.find(option => option.value === key);
|
||||
if (optionToRemove) {
|
||||
this.selectElement.removeChild(optionToRemove);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public clearOptions() {
|
||||
while (this.selectElement.firstChild) {
|
||||
this.selectElement.removeChild(this.selectElement.firstChild);
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
47
frontend/src/ui/file-input.ts
Normal file
47
frontend/src/ui/file-input.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class FileInput extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private inputElement: HTMLInputElement;
|
||||
public constructor(title: string, multiple: boolean = false) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `fileinpt_title_${this.id}`;
|
||||
this.inputElement = document.createElement("input");
|
||||
this.inputElement.id = `fileinpt_${this.id}`;
|
||||
this.inputElement.type = "file";
|
||||
if (multiple) {
|
||||
this.inputElement.multiple = true;
|
||||
}
|
||||
this.titleElement.appendChild(this.inputElement);
|
||||
this.element.appendChild(this.titleElement);
|
||||
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.inputElement.focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.inputElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getFiles(): FileList | null {
|
||||
return this.inputElement.files;
|
||||
}
|
||||
|
||||
public setAccept(accept: string) {
|
||||
this.inputElement.accept = accept;
|
||||
return this;
|
||||
}
|
||||
}
|
33
frontend/src/ui/image.ts
Normal file
33
frontend/src/ui/image.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class Image extends UINode {
|
||||
private imgElement: HTMLImageElement;
|
||||
public constructor(title: string, src: string, altText: string = "") {
|
||||
super(title);
|
||||
this.imgElement = document.createElement("img");
|
||||
this.imgElement.src = src;
|
||||
this.imgElement.alt = altText;
|
||||
this.element.appendChild(this.imgElement);
|
||||
this.element.setAttribute("aria-label", title);
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.imgElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.element.setAttribute("aria-label", text);
|
||||
return this;
|
||||
}
|
||||
|
||||
public setSource(src: string) {
|
||||
this.imgElement.src = src;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setAltText(altText: string) {
|
||||
this.imgElement.alt = altText;
|
||||
return this;
|
||||
}
|
||||
}
|
12
frontend/src/ui/index.ts
Normal file
12
frontend/src/ui/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export { UIWindow } from "./window";
|
||||
export { Button } from "./button";
|
||||
export { Container } from "./container";
|
||||
export { UINode } from "./node";
|
||||
export { List } from "./list";
|
||||
export { Text } from "./text";
|
||||
export { ListItem } from "./list-item";
|
||||
export { Checkbox } from "./checkbox";
|
||||
export { TextInput } from "./text-input";
|
||||
export { TabBar } from "./tab-bar";
|
||||
export { TabbedView } from "./tabbed-view";
|
||||
export { Canvas } from "./canvas";
|
37
frontend/src/ui/list-item.ts
Normal file
37
frontend/src/ui/list-item.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class ListItem extends UINode {
|
||||
private listElement: HTMLLIElement;
|
||||
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.listElement = document.createElement("li");
|
||||
this.listElement.innerText = this.title;
|
||||
this.listElement.setAttribute("tabindex", "-1");
|
||||
this.element.appendChild(this.listElement);
|
||||
this.listElement.setAttribute("aria-label", this.title);
|
||||
this.listElement.setAttribute("role", "option");
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.listElement.focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.listElement.click();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.listElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.listElement.innerText = text;
|
||||
this.element.setAttribute("aria-label", this.title);
|
||||
this.listElement.setAttribute("aria-label", this.title);
|
||||
return this;
|
||||
}
|
||||
}
|
171
frontend/src/ui/list.ts
Normal file
171
frontend/src/ui/list.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
|
||||
export class List extends UINode {
|
||||
public children: UINode[];
|
||||
protected listElement: HTMLUListElement;
|
||||
private focused: number;
|
||||
protected selectCallback?: (id: number) => void;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.children = [];
|
||||
this.listElement = document.createElement("ul");
|
||||
this.listElement.setAttribute("role", "listbox");
|
||||
this.listElement.style.listStyle = "none";
|
||||
this.element.appendChild(this.listElement);
|
||||
this.element.setAttribute("aria-label", this.title);
|
||||
this.focused = 0;
|
||||
}
|
||||
|
||||
public add(node: UINode) {
|
||||
this.children.push(node);
|
||||
node._onConnect();
|
||||
this.listElement.appendChild(node.render());
|
||||
if (this.children.length === 1) this.calculateTabIndex();
|
||||
node.onFocus(() => this.calculateFocused(node));
|
||||
return this;
|
||||
}
|
||||
|
||||
public addNodeAtIndex(node: UINode, index: number) {
|
||||
index = Math.max(0, Math.min(index, this.children.length));
|
||||
this.children.splice(index, 0, node);
|
||||
node._onConnect();
|
||||
this.listElement.insertBefore(node.render(), this.listElement.children[index]);
|
||||
if (this.children.length === 1) this.calculateTabIndex();
|
||||
node.onFocus(() => this.calculateFocused(node));
|
||||
return this;
|
||||
}
|
||||
|
||||
public remove(node: UINode) {
|
||||
const idx = this.children.indexOf(node);
|
||||
this.children.splice(idx, 1);
|
||||
node._onDisconnect();
|
||||
this.listElement.removeChild(node.render());
|
||||
if (idx === this.focused) {
|
||||
if (this.focused > 0) this.focused--;
|
||||
this.calculateTabIndex();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public _onFocus() {
|
||||
super._onFocus();
|
||||
this.children[this.focused].focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public _onClick() {
|
||||
// this.children[this.focused]._onClick();
|
||||
}
|
||||
|
||||
public _onSelect(id: number) {
|
||||
if (this.selectCallback) this.selectCallback(id);
|
||||
}
|
||||
|
||||
protected calculateStyle(): void {
|
||||
super.calculateStyle();
|
||||
this.element.style.overflowY = "scroll";
|
||||
this.listElement.style.overflowY = "scroll";
|
||||
}
|
||||
|
||||
public _onKeydown(key: string, alt: boolean = false, shift: boolean = false, ctrl: boolean = false): boolean {
|
||||
switch (key) {
|
||||
case "ArrowUp":
|
||||
this.children[this.focused].setTabbable(false);
|
||||
this.focused = Math.max(0, this.focused - 1);
|
||||
this.children[this.focused].setTabbable(true);
|
||||
this.children[this.focused].focus();
|
||||
return true;
|
||||
break;
|
||||
case "ArrowDown":
|
||||
this.children[this.focused].setTabbable(false);
|
||||
this.focused = Math.min(this.children.length - 1, this.focused + 1);
|
||||
this.children[this.focused].setTabbable(true);
|
||||
this.children[this.focused].focus();
|
||||
return true;
|
||||
break;
|
||||
case "Enter":
|
||||
this.children[this.focused].click();
|
||||
return true;
|
||||
break;
|
||||
case "Home":
|
||||
this.children[this.focused].setTabbable(false);
|
||||
this.focused = 0;
|
||||
this.children[this.focused].setTabbable(true);
|
||||
this.children[this.focused].focus();
|
||||
return true;
|
||||
break;
|
||||
case "End":
|
||||
this.children[this.focused].setTabbable(false);
|
||||
this.focused = this.children.length - 1;
|
||||
this.children[this.focused].setTabbable(true);
|
||||
this.children[this.focused].focus();
|
||||
return true;
|
||||
break;
|
||||
default:
|
||||
return this.children[this.focused]._onKeydown(key);
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected renderAsListItem(node: UINode) {
|
||||
let li = document.createElement("li");
|
||||
li.appendChild(node.render());
|
||||
return li;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.listElement;
|
||||
}
|
||||
|
||||
public isItemFocused(): boolean {
|
||||
const has = this.children.find((child) => child.isFocused);
|
||||
if (has) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private calculateTabIndex() {
|
||||
if (this.children.length < 1) return;
|
||||
this.children[this.focused].setTabbable(true);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.children.forEach((child) => this.remove(child));
|
||||
this.children = [];
|
||||
this.listElement.innerHTML = '';
|
||||
this.focused = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getFocusedChild() {
|
||||
return this.children[this.focused];
|
||||
}
|
||||
|
||||
public getFocus() {
|
||||
return this.focused;
|
||||
}
|
||||
|
||||
public onSelect(f: (id: number) => void) {
|
||||
this.selectCallback = f;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected calculateFocused(node: UINode) {
|
||||
const idx = this.children.indexOf(node);
|
||||
this._onSelect(idx);
|
||||
this.focused = idx;
|
||||
}
|
||||
|
||||
public scrollToBottom() {
|
||||
this.children.forEach((child) => child.setTabbable(false));
|
||||
const node = this.children[this.children.length - 1];
|
||||
node.getElement().scrollIntoView();
|
||||
// set the focused element for tab index without focusing directly.
|
||||
this.focused = this.children.length - 1;
|
||||
this.children[this.focused].setTabbable(true);
|
||||
return this;
|
||||
}
|
||||
}
|
47
frontend/src/ui/multiline-input.ts
Normal file
47
frontend/src/ui/multiline-input.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class MultilineInput extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private textareaElement: HTMLTextAreaElement;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `txtarea_title_${this.id}`;
|
||||
this.textareaElement = document.createElement("textarea");
|
||||
this.textareaElement.id = `txtarea_${this.id}`;
|
||||
this.titleElement.appendChild(this.textareaElement);
|
||||
this.element.appendChild(this.titleElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.textareaElement.focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.textareaElement.click();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.textareaElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.textareaElement.value;
|
||||
}
|
||||
|
||||
public setValue(value: string) {
|
||||
this.textareaElement.value = value;
|
||||
return this;
|
||||
}
|
||||
}
|
161
frontend/src/ui/node.ts
Normal file
161
frontend/src/ui/node.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { UITab } from "./tab";
|
||||
|
||||
export class UINode {
|
||||
protected title: string;
|
||||
protected element: HTMLDivElement;
|
||||
protected position!: {x: number, y: number, width: number, height: number};
|
||||
protected positionType: string = "fixed";
|
||||
protected calculateOwnStyle: boolean = true;
|
||||
protected keyDownCallback!: (key: string, alt?: boolean, shift?: boolean, ctrl?: boolean) => void | undefined;
|
||||
protected focusCallback?: () => void;
|
||||
protected blurCallback?: () => void;
|
||||
protected clickCallback?: () => void;
|
||||
protected globalKeydown: boolean = false;
|
||||
protected visible: boolean;
|
||||
public isFocused: boolean;
|
||||
private userdata: any;
|
||||
|
||||
public constructor(title: string) {
|
||||
this.title = title;
|
||||
this.element = document.createElement("div");
|
||||
this.element.setAttribute("tabindex", "-1");
|
||||
this.visible = false;
|
||||
this.isFocused = false;
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.element.click();
|
||||
}
|
||||
|
||||
public _onConnect() {
|
||||
this.calculateStyle();
|
||||
this.addListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
public _onDisconnect() {
|
||||
return;
|
||||
}
|
||||
|
||||
public _onFocus() {
|
||||
if (this.focusCallback) this.focusCallback();
|
||||
this.isFocused = true;
|
||||
return;
|
||||
}
|
||||
|
||||
public _onBlur() {
|
||||
if (this.blurCallback) this.blurCallback();
|
||||
this.isFocused = false;
|
||||
return;
|
||||
}
|
||||
|
||||
public _onClick() {
|
||||
if (this.clickCallback) this.clickCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
public _onKeydown(key: string, alt: boolean = false, shift: boolean = false, ctrl: boolean = false): boolean {
|
||||
if (this.keyDownCallback) {
|
||||
if (this.globalKeydown || (!this.globalKeydown && document.activeElement === this.getElement())) {
|
||||
this.keyDownCallback(key, alt, shift, ctrl);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public render(): HTMLElement {
|
||||
this.visible = true;
|
||||
return this.element;
|
||||
}
|
||||
|
||||
protected addListeners() {
|
||||
const elem = this.element;
|
||||
this.getElement().addEventListener("focus", (e) => this._onFocus());
|
||||
elem.addEventListener("blur", (e) => this._onBlur());
|
||||
elem.addEventListener("click", (e) => this._onClick());
|
||||
elem.addEventListener("keydown", e => this._onKeydown(e.key, e.altKey, e.shiftKey, e.ctrlKey));
|
||||
}
|
||||
|
||||
protected calculateStyle() {
|
||||
if (!this.calculateOwnStyle || !this.position) return;
|
||||
this.element.style.position = this.positionType;
|
||||
this.element.style.left = `${this.position.x}%`;
|
||||
this.element.style.top = `${this.position.y}%`;
|
||||
this.element.style.width = `${this.position.width}%`;
|
||||
this.element.style.height = `${this.position.height}%`;
|
||||
}
|
||||
|
||||
public setPosition(x: number, y: number, width: number, height: number, type: string = "fixed") {
|
||||
this.position = {
|
||||
x: x,
|
||||
y: y,
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
this.positionType = type;
|
||||
this.calculateOwnStyle = true;
|
||||
this.calculateStyle();
|
||||
return this;
|
||||
}
|
||||
|
||||
public onClick(f: () => void) {
|
||||
this.clickCallback = f;
|
||||
return this;
|
||||
}
|
||||
|
||||
public onFocus(f: () => void) {
|
||||
this.focusCallback = f;
|
||||
return this;
|
||||
}
|
||||
|
||||
public onKeyDown(f: (key: string, alt?: boolean, shift?: boolean, ctrl?: boolean) => void, global: boolean = false) {
|
||||
this.keyDownCallback = f;
|
||||
this.globalKeydown = global;
|
||||
return this;
|
||||
}
|
||||
|
||||
public onBlur(f: () => void) {
|
||||
this.blurCallback = f;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
public setTabbable(val: boolean) {
|
||||
this.getElement().setAttribute("tabindex",
|
||||
(val === true) ? "0" :
|
||||
"-1");
|
||||
return this;
|
||||
}
|
||||
|
||||
public setAriaLabel(text: string) {
|
||||
this.element.setAttribute("aria-label", text);
|
||||
return this;
|
||||
}
|
||||
|
||||
public setRole(role: string) {
|
||||
this.getElement().setAttribute("role", role);
|
||||
return this;
|
||||
}
|
||||
|
||||
public getUserData(): any {
|
||||
return this.userdata;
|
||||
}
|
||||
|
||||
public setUserData(obj: any) {
|
||||
this.userdata = obj;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setAccessKey(key: string) {
|
||||
this.getElement().accessKey = key;
|
||||
return this;
|
||||
}
|
||||
}
|
40
frontend/src/ui/progress-bar.ts
Normal file
40
frontend/src/ui/progress-bar.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class ProgressBar extends UINode {
|
||||
private progressElement: HTMLProgressElement;
|
||||
public constructor(title: string, max: number) {
|
||||
super(title);
|
||||
this.progressElement = document.createElement("progress");
|
||||
this.progressElement.max = max;
|
||||
this.element.appendChild(this.progressElement);
|
||||
this.element.setAttribute("aria-label", title);
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.progressElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.element.setAttribute("aria-label", text);
|
||||
return this;
|
||||
}
|
||||
|
||||
public getValue(): number {
|
||||
return this.progressElement.value;
|
||||
}
|
||||
|
||||
public setValue(value: number) {
|
||||
this.progressElement.value = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getMax(): number {
|
||||
return this.progressElement.max;
|
||||
}
|
||||
|
||||
public setMax(max: number) {
|
||||
this.progressElement.max = max;
|
||||
return this;
|
||||
}
|
||||
}
|
79
frontend/src/ui/radio-group.ts
Normal file
79
frontend/src/ui/radio-group.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class RadioGroup extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLegendElement;
|
||||
private containerElement: HTMLFieldSetElement;
|
||||
private radioElements: Map<string, HTMLInputElement>;
|
||||
private radioLabels: Map<string, HTMLLabelElement>;
|
||||
|
||||
public constructor(title: string, options: { key: string; value: string }[]) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("legend");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `rdgrp_title_${this.id}`;
|
||||
this.containerElement = document.createElement("fieldset");
|
||||
this.containerElement.appendChild(this.titleElement);
|
||||
this.element.appendChild(this.containerElement);
|
||||
|
||||
this.radioElements = new Map();
|
||||
this.radioLabels = new Map();
|
||||
|
||||
options.forEach((option) => {
|
||||
const radioId = `rd_${this.id}_${option.key}`;
|
||||
const radioElement = document.createElement("input");
|
||||
radioElement.id = radioId;
|
||||
radioElement.type = "radio";
|
||||
radioElement.name = `rdgrp_${this.id}`;
|
||||
radioElement.value = option.key;
|
||||
radioElement.setAttribute("aria-labeledby", `${radioId}_label`);
|
||||
|
||||
const radioLabel = document.createElement("label");
|
||||
radioLabel.innerText = option.value;
|
||||
radioLabel.id = `${radioId}_label`;
|
||||
radioLabel.setAttribute("for", radioId);
|
||||
|
||||
this.radioElements.set(option.key, radioElement);
|
||||
this.radioLabels.set(option.key, radioLabel);
|
||||
|
||||
this.containerElement.appendChild(radioElement);
|
||||
this.containerElement.appendChild(radioLabel);
|
||||
});
|
||||
}
|
||||
|
||||
public focus() {
|
||||
const firstRadioElement = this.radioElements.values().next().value;
|
||||
if (firstRadioElement) {
|
||||
firstRadioElement.focus();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.containerElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getSelectedValue(): string | null {
|
||||
for (const [key, radioElement] of this.radioElements.entries()) {
|
||||
if (radioElement.checked) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public setSelectedValue(value: string) {
|
||||
const radioElement = this.radioElements.get(value);
|
||||
if (radioElement) {
|
||||
radioElement.checked = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
51
frontend/src/ui/slider.ts
Normal file
51
frontend/src/ui/slider.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class Slider extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private sliderElement: HTMLInputElement;
|
||||
public constructor(title: string, min: number, max: number, step: number = 1) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `sldr_title_${this.id}`;
|
||||
this.sliderElement = document.createElement("input");
|
||||
this.sliderElement.id = `sldr_${this.id}`;
|
||||
this.sliderElement.type = "range";
|
||||
this.sliderElement.min = min.toString();
|
||||
this.sliderElement.max = max.toString();
|
||||
this.sliderElement.step = step.toString();
|
||||
this.titleElement.appendChild(this.sliderElement);
|
||||
this.element.appendChild(this.titleElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.sliderElement.focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.sliderElement.click();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.sliderElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getValue(): number {
|
||||
return parseInt(this.sliderElement.value);
|
||||
}
|
||||
|
||||
public setValue(value: number) {
|
||||
this.sliderElement.value = value.toString();
|
||||
return this;
|
||||
}
|
||||
}
|
102
frontend/src/ui/tab-bar.ts
Normal file
102
frontend/src/ui/tab-bar.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { UINode } from "./node";
|
||||
import { UITab } from "./tab";
|
||||
|
||||
export class TabBar extends UINode {
|
||||
private tabs: UITab[];
|
||||
private tabBarContainer: HTMLDivElement;
|
||||
private onTabChangeCallback?: (index: number) => void;
|
||||
private focused: number;
|
||||
|
||||
public constructor(title: string = "tab bar") {
|
||||
super(title);
|
||||
this.tabs = [];
|
||||
this.tabBarContainer = document.createElement("div");
|
||||
this.tabBarContainer.setAttribute("role", "tablist");
|
||||
this.tabBarContainer.style.display = "flex";
|
||||
this.tabBarContainer.style.alignItems = "center";
|
||||
// this.tabBarContainer.style.justifyContent = "space-between";
|
||||
this.tabBarContainer.style.overflow = "hidden";
|
||||
|
||||
this.element.appendChild(this.tabBarContainer);
|
||||
this.focused = 0;
|
||||
}
|
||||
|
||||
public _onFocus() {
|
||||
this.tabs[this.focused].focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.tabs[this.focused].focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public add(title: string) {
|
||||
const idx = this.tabs.length;
|
||||
const elem = new UITab(title);
|
||||
elem.onClick(() => {
|
||||
this.selectTab(idx);
|
||||
});
|
||||
this.tabs.push(elem);
|
||||
this.tabBarContainer.appendChild(elem.render());
|
||||
elem._onConnect();
|
||||
if (this.tabs.length === 1) this.calculateTabIndex();
|
||||
return this;
|
||||
}
|
||||
|
||||
public onTabChange(f: (index: number) => void) {
|
||||
this.onTabChangeCallback = f;
|
||||
return this;
|
||||
}
|
||||
|
||||
private selectTab(idx: number) {
|
||||
if (idx !== this.focused) {
|
||||
this.tabs[this.focused].setTabbable(false);
|
||||
this.focused = idx;
|
||||
}
|
||||
if (!this.onTabChangeCallback) return;
|
||||
this.onTabChangeCallback(idx);
|
||||
this.tabs[idx].setTabbable(true);
|
||||
this.tabs[idx].focus();
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
public _onKeydown(key: string): boolean {
|
||||
switch (key) {
|
||||
case "ArrowLeft":
|
||||
this.tabs[this.focused].setTabbable(false);
|
||||
this.focused = Math.max(0, this.focused - 1);
|
||||
this.tabs[this.focused].setTabbable(true);
|
||||
this.selectTab(this.focused);
|
||||
return true;
|
||||
break;
|
||||
case "ArrowRight":
|
||||
this.tabs[this.focused].setTabbable(false);
|
||||
this.focused = Math.min(this.tabs.length - 1, this.focused + 1);
|
||||
this.tabs[this.focused].setTabbable(true);
|
||||
this.selectTab(this.focused);
|
||||
return true;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private updateView() {
|
||||
for (let i = 0; i < this.tabs.length; i++) {
|
||||
this.tabs[i].setSelected(i === this.focused);
|
||||
}
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
|
||||
public calculateTabIndex() {
|
||||
this.tabs[this.focused].setTabbable(true);
|
||||
return this;
|
||||
}
|
||||
}
|
44
frontend/src/ui/tab.ts
Normal file
44
frontend/src/ui/tab.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class UITab extends UINode {
|
||||
private textElement: HTMLButtonElement;
|
||||
private selected: boolean;
|
||||
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.title = title;
|
||||
this.textElement = document.createElement("button");
|
||||
this.textElement.innerText = title;
|
||||
this.textElement.setAttribute("tabindex", "-1");
|
||||
this.textElement.setAttribute("role", "tab");
|
||||
this.textElement.setAttribute("aria-selected", "false");
|
||||
this.element.appendChild(this.textElement);
|
||||
this.selected = false;
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.textElement.focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.textElement.click();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.textElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.textElement.innerText = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setSelected(val: boolean) {
|
||||
this.selected = val;
|
||||
this.textElement.setAttribute("aria-selected", this.selected.toString());
|
||||
return this;
|
||||
}
|
||||
}
|
51
frontend/src/ui/tabbed-view.ts
Normal file
51
frontend/src/ui/tabbed-view.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { UINode } from "./node";
|
||||
import { TabBar } from "./tab-bar";
|
||||
import { Container } from "./container";
|
||||
|
||||
|
||||
export class TabbedView extends UINode {
|
||||
private bar: TabBar;
|
||||
private containers: Container[];
|
||||
private containerElement: HTMLDivElement;
|
||||
private barAtTop: boolean;
|
||||
private currentView?: Container;
|
||||
public constructor(title: string, barAtTop: boolean = true) {
|
||||
super(title);
|
||||
this.bar = new TabBar();
|
||||
this.bar._onConnect();
|
||||
this.bar.onTabChange((index: number) => this.onTabChanged(index));
|
||||
this.containers = [];
|
||||
this.containerElement = document.createElement("div");
|
||||
this.element.appendChild(this.bar.render());
|
||||
this.element.appendChild(this.containerElement);
|
||||
this.element.setAttribute("tabindex", "-1");
|
||||
this.barAtTop = barAtTop;
|
||||
}
|
||||
|
||||
public add(name: string, container: Container) {
|
||||
this.bar.add(name);
|
||||
container.setRole("tabpanel");
|
||||
this.containers.push(container);
|
||||
return this;
|
||||
}
|
||||
|
||||
private onTabChanged(idx: number) {
|
||||
if (this.currentView) {
|
||||
this.containerElement.removeChild(this.currentView.render());
|
||||
}
|
||||
this.currentView = this.containers[idx];
|
||||
this.containerElement.appendChild(this.currentView.render());
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.containerElement;
|
||||
}
|
||||
|
||||
protected calculateStyle(): void {
|
||||
if (this.barAtTop) {
|
||||
this.bar.setPosition(0, 0, 100, 5);
|
||||
} else {
|
||||
this.bar.setPosition(0, 90, 100, 5);
|
||||
}
|
||||
}
|
||||
}
|
48
frontend/src/ui/text-input.ts
Normal file
48
frontend/src/ui/text-input.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class TextInput extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private inputElement: HTMLInputElement;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `inpt_title_${this.id}`;
|
||||
this.inputElement = document.createElement("input");
|
||||
this.inputElement.id = `inpt_${this.id}`;
|
||||
this.inputElement.type = "text";
|
||||
this.titleElement.appendChild(this.inputElement);
|
||||
this.element.appendChild(this.titleElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.inputElement.focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.inputElement.click();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.inputElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.inputElement.value;
|
||||
}
|
||||
|
||||
public setValue(value: string) {
|
||||
this.inputElement.value = value;
|
||||
return this;
|
||||
}
|
||||
}
|
32
frontend/src/ui/text.ts
Normal file
32
frontend/src/ui/text.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class Text extends UINode {
|
||||
private textElement: HTMLSpanElement;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.textElement = document.createElement("span");
|
||||
this.textElement.innerText = title;
|
||||
this.textElement.setAttribute("tabindex", "-1");
|
||||
this.element.appendChild(this.textElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.textElement.focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.textElement.click();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.textElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.textElement.innerText = text;
|
||||
return this;
|
||||
}
|
||||
}
|
43
frontend/src/ui/time-picker.ts
Normal file
43
frontend/src/ui/time-picker.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class TimePicker extends UINode {
|
||||
private id: string;
|
||||
private titleElement: HTMLLabelElement;
|
||||
private inputElement: HTMLInputElement;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.id = Math.random().toString();
|
||||
this.titleElement = document.createElement("label");
|
||||
this.titleElement.innerText = title;
|
||||
this.titleElement.id = `timepicker_title_${this.id}`;
|
||||
this.inputElement = document.createElement("input");
|
||||
this.inputElement.id = `timepicker_${this.id}`;
|
||||
this.inputElement.type = "time";
|
||||
this.titleElement.appendChild(this.inputElement);
|
||||
this.element.appendChild(this.titleElement);
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.inputElement.focus();
|
||||
return this;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.inputElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.titleElement.innerText = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.inputElement.value;
|
||||
}
|
||||
|
||||
public setValue(value: string) {
|
||||
this.inputElement.value = value;
|
||||
return this;
|
||||
}
|
||||
}
|
0
frontend/src/ui/treelist-item.ts
Normal file
0
frontend/src/ui/treelist-item.ts
Normal file
0
frontend/src/ui/treelist.ts
Normal file
0
frontend/src/ui/treelist.ts
Normal file
198
frontend/src/ui/treeview-item.ts
Normal file
198
frontend/src/ui/treeview-item.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { UINode } from "./node";
|
||||
import { Treeview } from "./treeview";
|
||||
|
||||
export class TreeviewItem extends UINode {
|
||||
private listElement: HTMLLIElement;
|
||||
private childContainer!: HTMLUListElement;
|
||||
|
||||
public children: TreeviewItem[];
|
||||
|
||||
private expanded!: boolean;
|
||||
|
||||
private focused: number;
|
||||
|
||||
private parent?: TreeviewItem;
|
||||
|
||||
private root!: Treeview;
|
||||
|
||||
private previousItem?: TreeviewItem;
|
||||
private nextItem?: TreeviewItem;
|
||||
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.listElement = document.createElement("li");
|
||||
this.listElement.innerText = this.title;
|
||||
this.listElement.setAttribute("tabindex", "-1");
|
||||
this.listElement.setAttribute("role", "treeitem");
|
||||
this.element.appendChild(this.listElement);
|
||||
this.listElement.setAttribute("aria-label", this.title);
|
||||
this.children = [];
|
||||
this.focused = 0;
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.listElement.focus();
|
||||
}
|
||||
|
||||
public click() {
|
||||
this.listElement.click();
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.listElement;
|
||||
}
|
||||
|
||||
public setText(text: string) {
|
||||
this.title = text;
|
||||
this.listElement.innerText = text;
|
||||
this.element.setAttribute("aria-label", this.title);
|
||||
this.listElement.setAttribute("aria-label", this.title);
|
||||
}
|
||||
|
||||
public add(node: TreeviewItem) {
|
||||
this.children.push(node);
|
||||
node.setParent(this);
|
||||
this.setExpanded(false);
|
||||
if (this.children.length > 0) {
|
||||
this.previousItem = this.children[this.children.length - 1];
|
||||
this.previousItem.setNextItem(node);
|
||||
}
|
||||
}
|
||||
|
||||
public remove(node: TreeviewItem) {
|
||||
const idx = this.children.indexOf(node);
|
||||
if (idx > -1) {
|
||||
this.children.splice(idx, 1);
|
||||
this.updateShownItems();
|
||||
}
|
||||
}
|
||||
|
||||
public expand() {
|
||||
if (!this.isExpandable()) return;
|
||||
if (this.isExpandable() && this.isExpanded() && this.children[this.focused].isExpandable()) {
|
||||
this.children[this.focused].expand();
|
||||
return;
|
||||
}
|
||||
this.setExpanded(true);
|
||||
this.updateShownItems();
|
||||
this.children[this.focused].focus();
|
||||
}
|
||||
|
||||
public collapse(ignoreFocus: boolean = false) {
|
||||
if (!this.isExpandable()) {
|
||||
if (this.getElement() !== document.activeElement && ignoreFocus) return;
|
||||
this.parent?.collapse(true);
|
||||
return;
|
||||
}
|
||||
this.setExpanded(false);
|
||||
this.updateShownItems();
|
||||
setTimeout(() => this.focus(), 0);
|
||||
}
|
||||
|
||||
public isExpandable(): boolean {
|
||||
return this.children.length > 0;
|
||||
}
|
||||
|
||||
public isExpanded() {
|
||||
return this.expanded;
|
||||
}
|
||||
|
||||
private setExpanded(val: boolean) {
|
||||
this.expanded = val;
|
||||
if (this.expanded) {
|
||||
this.listElement.setAttribute("aria-expanded", "true");
|
||||
return;
|
||||
}
|
||||
this.listElement.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
private updateShownItems() {
|
||||
if (this.expanded) {
|
||||
if (!this.childContainer) {
|
||||
this.childContainer = document.createElement("ul");
|
||||
this.childContainer.setAttribute("role", "group");
|
||||
this.children.forEach((child) => this.childContainer.appendChild(child.render()));
|
||||
this.listElement.appendChild(this.childContainer);
|
||||
} else {
|
||||
this.childContainer.hidden = false;
|
||||
}
|
||||
} else {
|
||||
this.childContainer.hidden = true;
|
||||
// this.listElement.removeChild(this.childContainer);
|
||||
}
|
||||
}
|
||||
|
||||
public focusNext(): boolean {
|
||||
if (this.isExpandable() && this.isExpanded() && this.children[this.focused].isExpandable()) {
|
||||
return this.children[this.focused].focusNext();
|
||||
}
|
||||
this.children[this.focused].setTabbable(false);
|
||||
this.focused = Math.min(this.children.length - 1, this.focused + 1);
|
||||
this.children[this.focused].setTabbable(true);
|
||||
this.children[this.focused].focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
public focusPrevious(): boolean {
|
||||
if (this.isExpandable() && this.isExpanded() && this.children[this.focused].isExpandable()) {
|
||||
return this.children[this.focused].focusPrevious();
|
||||
}
|
||||
this.children[this.focused].setTabbable(false);
|
||||
this.focused = Math.max(0, this.focused - 1);
|
||||
this.children[this.focused].setTabbable(true);
|
||||
this.children[this.focused].focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
public setParent(item: TreeviewItem) {
|
||||
this.parent = this.parent;
|
||||
}
|
||||
|
||||
public getParent(): TreeviewItem|undefined {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
public setPrevious(node: TreeviewItem) {
|
||||
this.previousItem = node;
|
||||
}
|
||||
|
||||
public setNextItem(node: TreeviewItem) {
|
||||
this.nextItem = node;
|
||||
}
|
||||
|
||||
public getPrevious(): TreeviewItem|undefined {
|
||||
return this.previousItem;
|
||||
}
|
||||
|
||||
public getNext(): TreeviewItem|undefined {
|
||||
return this.nextItem;
|
||||
}
|
||||
|
||||
public _onKeydown(key: string, alt?: boolean, shift?: boolean, ctrl?: boolean): boolean {
|
||||
switch (key) {
|
||||
case "ArrowUp":
|
||||
this.focusPrevious();
|
||||
return true;
|
||||
break;
|
||||
case "ArrowDown":
|
||||
this.focusNext();
|
||||
return true;
|
||||
case "ArrowLeft":
|
||||
this.collapse();
|
||||
return true;
|
||||
case "ArrowRight":
|
||||
if (this.children[this.focused].isExpandable() && !this.children[this.focused].isExpanded()) {
|
||||
this.children[this.focused].expand();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public focusOnItem() {
|
||||
this.children[this.focused].focus();
|
||||
}
|
||||
}
|
151
frontend/src/ui/treeview.ts
Normal file
151
frontend/src/ui/treeview.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { UINode } from "./node";
|
||||
import { TreeviewItem } from "./treeview-item";
|
||||
|
||||
export class Treeview extends UINode {
|
||||
public children: TreeviewItem[];
|
||||
protected listElement: HTMLUListElement;
|
||||
private focused: number;
|
||||
protected selectCallback?: (id: number) => void;
|
||||
public constructor(title: string) {
|
||||
super(title);
|
||||
this.children = [];
|
||||
this.listElement = document.createElement("ul");
|
||||
this.listElement.setAttribute("role", "tree");
|
||||
this.listElement.style.listStyle = "none";
|
||||
this.element.appendChild(this.listElement);
|
||||
this.element.setAttribute("aria-label", this.title);
|
||||
this.focused = 0;
|
||||
}
|
||||
|
||||
public add(node: TreeviewItem) {
|
||||
this.children.push(node);
|
||||
node._onConnect();
|
||||
this.listElement.appendChild(node.render());
|
||||
if (this.children.length === 1) this.calculateTabIndex();
|
||||
node.onFocus(() => this.calculateFocused(node));
|
||||
}
|
||||
|
||||
public remove(node: TreeviewItem) {
|
||||
const idx = this.children.indexOf(node);
|
||||
this.children.splice(idx, 1);
|
||||
node._onDisconnect();
|
||||
this.listElement.removeChild(node.render());
|
||||
if (idx === this.focused) {
|
||||
if (this.focused > 0) this.focused--;
|
||||
this.calculateTabIndex();
|
||||
}
|
||||
}
|
||||
|
||||
public _onFocus() {
|
||||
super._onFocus();
|
||||
this.children[this.focused].focus();
|
||||
}
|
||||
|
||||
public _onClick() {
|
||||
this.children[this.focused]._onClick();
|
||||
}
|
||||
|
||||
public _onSelect(id: number) {
|
||||
if (this.selectCallback) this.selectCallback(id);
|
||||
}
|
||||
|
||||
protected calculateStyle(): void {
|
||||
super.calculateStyle();
|
||||
this.element.style.overflowY = "scroll";
|
||||
this.listElement.style.overflowY = "scroll";
|
||||
}
|
||||
|
||||
public _onKeydown(key: string, alt: boolean = false, shift: boolean = false, ctrl: boolean = false): boolean {
|
||||
switch (key) {
|
||||
case "ArrowUp":
|
||||
return this.focusPrevious();
|
||||
break;
|
||||
case "ArrowDown":
|
||||
return this.focusNext();
|
||||
break;
|
||||
case "Enter":
|
||||
this.children[this.focused].click();
|
||||
return true;
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
// this.children[this.focused].collapse();
|
||||
return true;
|
||||
break;
|
||||
case "ArrowRight":
|
||||
this.children[this.focused].expand();
|
||||
return true;
|
||||
break;
|
||||
default:
|
||||
return this.children[this.focused]._onKeydown(key);
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected renderAsListItem(node: UINode) {
|
||||
let li = document.createElement("li");
|
||||
li.appendChild(node.render());
|
||||
return li;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.listElement;
|
||||
}
|
||||
|
||||
public isItemFocused(): boolean {
|
||||
const has = this.children.find((child) => child.isFocused);
|
||||
if (has) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private calculateTabIndex() {
|
||||
this.children[this.focused].setTabbable(true);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.children.forEach((child) => this.remove(child));
|
||||
this.children = [];
|
||||
this.listElement.innerHTML = '';
|
||||
this.focused = 0;
|
||||
}
|
||||
|
||||
public getFocusedChild() {
|
||||
return this.children[this.focused];
|
||||
}
|
||||
|
||||
public getFocus() {
|
||||
return this.focused;
|
||||
}
|
||||
|
||||
public onSelect(f: (id: number) => void) {
|
||||
this.selectCallback = f;
|
||||
}
|
||||
|
||||
protected calculateFocused(node: TreeviewItem) {
|
||||
const idx = this.children.indexOf(node);
|
||||
this._onSelect(idx);
|
||||
}
|
||||
|
||||
public focusPrevious() {
|
||||
if (this.children[this.focused].isExpanded()) {
|
||||
// return this.children[this.focused].focusPrevious();
|
||||
} else {
|
||||
this.focused = Math.max(0, this.focused - 1);
|
||||
this.children[this.focused].focus();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public focusNext() {
|
||||
if (this.children[this.focused].isExpanded()) {
|
||||
// return this.children[this.focused].focusNext();
|
||||
} else {
|
||||
this.focused = Math.min(this.children.length - 1, this.focused + 1);
|
||||
this.children[this.focused].focus();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
61
frontend/src/ui/video.ts
Normal file
61
frontend/src/ui/video.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class Video extends UINode {
|
||||
private videoElement: HTMLVideoElement;
|
||||
|
||||
public constructor(title: string, src: string | MediaStream = "") {
|
||||
super(title);
|
||||
this.videoElement = document.createElement("video");
|
||||
if (typeof src === "string") {
|
||||
this.videoElement.src = src; // Set src if it's a string URL
|
||||
} else if (src instanceof MediaStream) {
|
||||
this.videoElement.srcObject = src; // Set srcObject if it's a MediaStream
|
||||
}
|
||||
this.videoElement.setAttribute("aria-label", title);
|
||||
this.element.appendChild(this.videoElement);
|
||||
this.setRole("video");
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.videoElement;
|
||||
}
|
||||
|
||||
public setSource(src: string | MediaStream) {
|
||||
if (typeof src === "string") {
|
||||
this.videoElement.src = src;
|
||||
} else if (src instanceof MediaStream) {
|
||||
this.videoElement.srcObject = src;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public play() {
|
||||
this.videoElement.play();
|
||||
return this;
|
||||
}
|
||||
|
||||
public pause() {
|
||||
this.videoElement.pause();
|
||||
return this;
|
||||
}
|
||||
|
||||
public setControls(show: boolean) {
|
||||
this.videoElement.controls = show;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setLoop(loop: boolean) {
|
||||
this.videoElement.loop = loop;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setMuted(muted: boolean) {
|
||||
this.videoElement.muted = muted;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setAutoplay(autoplay: boolean) {
|
||||
this.videoElement.autoplay = autoplay;
|
||||
return this;
|
||||
}
|
||||
}
|
82
frontend/src/ui/window.ts
Normal file
82
frontend/src/ui/window.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Container } from "./container";
|
||||
import { UINode } from "./node";
|
||||
|
||||
export class UIWindow {
|
||||
public title: string;
|
||||
public width!: number;
|
||||
public height!: number;
|
||||
public position!: { x: number; y: number; };
|
||||
public container: Container;
|
||||
public visible: boolean;
|
||||
private element: HTMLDivElement;
|
||||
private rendered!: boolean;
|
||||
private keyDown: (e: KeyboardEvent) => void;
|
||||
|
||||
public constructor(
|
||||
title: string,
|
||||
classname?: string,
|
||||
private setTitle: boolean = true
|
||||
) {
|
||||
this.title = title;
|
||||
this.container = new Container(this.title);
|
||||
this.container._onConnect();
|
||||
this.element = document.createElement("div");
|
||||
if (classname) {
|
||||
this.element.className = classname;
|
||||
}
|
||||
this.keyDown = this.onKeyDown.bind(this);
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
public add(node: UINode) {
|
||||
this.container.add(node);
|
||||
return this;
|
||||
}
|
||||
|
||||
public remove(node: UINode) {
|
||||
if (this.container.children.includes(node)) this.container.remove(node);
|
||||
return this;
|
||||
}
|
||||
|
||||
public show(): HTMLElement|undefined {
|
||||
if (this.visible) return;
|
||||
if (this.setTitle) document.title = this.title;
|
||||
if (this.rendered) return this.element;
|
||||
this.element.appendChild(this.container.render());
|
||||
this.element.addEventListener("keydown", this.keyDown);
|
||||
this.element.focus();
|
||||
this.visible = true;
|
||||
this.rendered = true;
|
||||
return this.element;
|
||||
}
|
||||
|
||||
public hide() {
|
||||
if (!this.visible) return;
|
||||
this.visible = false;
|
||||
this.rendered = false;
|
||||
this.element.replaceChildren();
|
||||
this.element.removeEventListener("keydown", this.keyDown);
|
||||
}
|
||||
|
||||
public onKeyDown(e: KeyboardEvent) {
|
||||
if (this.container._onKeydown(e.key, e.altKey, e.shiftKey, e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public onConnect() {
|
||||
return;
|
||||
}
|
||||
|
||||
public onDisconnect() {
|
||||
return;
|
||||
}
|
||||
|
||||
public getElement(): HTMLElement {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
public getContainer(): Container {
|
||||
return this.container;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user