forked from Im-Beast/deno_tui
-
Notifications
You must be signed in to change notification settings - Fork 0
/
combobox.ts
118 lines (98 loc) · 3.59 KB
/
combobox.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// Copyright 2022 Im-Beast. All rights reserved. MIT license.
import { Component, ComponentOptions } from "../component.ts";
import { BoxComponent } from "./box.ts";
import { ButtonComponent } from "./button.ts";
import { EmitterEvent } from "../event_emitter.ts";
import type { Rectangle } from "../types.ts";
import type { EventRecord } from "../event_emitter.ts";
/** Interface defining object that {ComboboxComponent}'s constructor can interpret */
export interface ComboboxComponentOptions<OptionType extends string[] = string[]> extends ComponentOptions {
rectangle: Rectangle;
/** Text displayed on combobox by default */
label?: string;
/** Possible values of combobox */
options: OptionType;
}
/** Complementary interface defining what's accessible in {ComboboxComponent} class in addition to {ComboboxComponentOptions} */
export interface ComboboxComponentPrivate<OptionType extends string[] = string[]> {
label: string;
/** Currently selected option */
value?: OptionType[number];
}
/** Implementation for {ComboboxComponent} class */
export type ComboboxComponentImplementation = ComboboxComponentOptions & ComboboxComponentPrivate;
/** EventMap that {ComboboxComponent} uses */
export type ComboboxComponentEventMap = {
valueChange: EmitterEvent<[ComboboxComponent<string[], EventRecord>]>;
};
/**
* Component that allows user to input value by selecting one from available options.
* If `label` isn't provided then first value from `options` will be used.
*/
export class ComboboxComponent<
OptionType extends string[] = string[],
EventMap extends EventRecord = Record<never, never>,
> extends BoxComponent<EventMap & ComboboxComponentEventMap> implements ComboboxComponentImplementation {
#lastInteraction = 0;
#temporaryComponents: Component[] = [];
label: string;
options: string[];
option?: OptionType[number];
constructor(options: ComboboxComponentOptions<OptionType>) {
super(options);
this.options = options.options;
this.label = options.label ?? this.options[0];
this.option = options.label ? undefined : this.options[0];
}
draw(): void {
super.draw();
if (this.label) {
const { style } = this;
const { canvas } = this.tui;
const { column, row, width, height } = this.rectangle;
canvas.draw(
column + (width / 2) - (this.label.length / 2),
row + (height / 2),
style(this.label),
);
}
}
interact(): void {
const now = Date.now();
const interactionDelay = now - this.#lastInteraction;
this.state = this.state === "focused" && interactionDelay < 500 ? "active" : "focused";
const temporaryComponents = this.#temporaryComponents;
if (this.state === "active") {
const { column, row, width, height } = this.rectangle;
for (const [i, option] of this.options.entries()) {
const button = new ButtonComponent({
tui: this.tui,
rectangle: {
column,
row: row + ((i + 1) * height),
height,
width,
},
label: option,
theme: this.theme,
zIndex: this.zIndex,
});
button.on("stateChange", (component) => {
const { state } = component;
if (state !== "active") return;
this.label = option;
this.option = option;
this.emit("valueChange", this);
this.interact();
});
temporaryComponents.push(button);
}
} else {
for (const component of temporaryComponents) {
component.remove();
}
temporaryComponents.length = 0;
}
this.#lastInteraction = now;
}
}