first commit

This commit is contained in:
douboer@gmail.com
2026-03-03 13:23:14 +08:00
commit 3b7c1d558a
161 changed files with 28120 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
import type { IReactive } from "../base/reactivity";
/**
* Get current cursor position in a textbox
* https://stackoverflow.com/16105482/get-current-cursor-position-in-a-textbox
*/
// TODO: compatibility check
function getCursorPosition<T extends HTMLElement>(field: T): number {
if ("selectionStart" in field) {
return field.selectionStart as number;
}
return 0;
}
export function updateCursor(
el: HTMLTextAreaElement,
data: IReactive<string>,
ptr: IReactive<number>
) {
let pos = getCursorPosition(el);
const len = data.value.length;
if (pos > len) {
pos = len;
} else if (pos < 0) {
pos = 0;
}
ptr.value = pos;
}

View File

@@ -0,0 +1,169 @@
import Disposable from "../base/disposable";
import type { IReactive } from "../base/reactivity";
import { isFunction } from "../helpers";
import { inputBuild } from "../renderer/index";
import { SPACE, THEME, h } from "../renderer/dom";
import { ENTER_KEY, addEvent } from "../renderer/events";
import { updateCursor } from "./cursor";
import { debounce } from "../base/debouncer";
import { createEffect, ref } from "../base/reactivity";
import type { IOutputInterface } from "../output/interface";
import type { IInputInterface } from "./interface";
import type { IKeyPress } from "../types";
/**
* Input Component
*/
export default class XInputComponent
extends Disposable
implements IInputInterface
{
public readonly el: HTMLTextAreaElement;
private buffer: string;
private data: IReactive<string>;
private ptr: IReactive<number>;
private isActive: IReactive<boolean>;
public showInput: IReactive<boolean>;
public isFocused: IReactive<boolean>;
public onkeypress?: ((ev: IKeyPress) => void) | undefined;
constructor(target: HTMLElement) {
super();
this.el = inputBuild(target);
this.buffer = "";
this.data = ref<string>("");
this.ptr = ref<number>(0);
this.isActive = ref<boolean>(true);
this.showInput = ref<boolean>(true);
this.isFocused = ref<boolean>(false);
// All reactive values are disposable
this.register(this.data);
this.register(this.ptr);
this.register(this.isActive);
this.register(this.showInput);
this.register(this.isFocused);
const cursorUpdater = () => updateCursor(this.el, this.data, this.ptr);
const cursorHandler = debounce(cursorUpdater);
const inputHandler = debounce(() => {
this.data.value = this.buffer = this.el.value;
});
createEffect(cursorUpdater);
this.register(
addEvent(this.el, "blur", () => (this.isFocused.value = false))
);
this.register(
addEvent(
this.el,
"focus",
() => (this.isFocused.value = true),
false
)
);
this.register(
addEvent(
this.el,
"keyup",
() => this.isActive.value && cursorHandler(),
false
)
);
this.register(
addEvent(this.el, "input", () => {
inputHandler();
cursorHandler();
})
);
this.register(
addEvent(this.el, "keydown", (ev: KeyboardEvent) => {
ev.stopImmediatePropagation();
const value = this.data.value;
if (ev.key === ENTER_KEY) {
ev.preventDefault();
if (this.el) this.el.value = "";
this.data.value = "";
this.buffer = "";
this.showInput.value = true;
}
if (!this.isActive.value) return;
if (!isFunction(this.onkeypress)) return;
this.onkeypress({
key: ev.key,
altKey: ev.altKey,
ctrlKey: ev.ctrlKey,
metaKey: ev.metaKey,
shiftKey: ev.shiftKey,
value,
cancel() {
ev.preventDefault();
ev.stopPropagation();
}
});
cursorHandler();
})
);
}
public blur(): void {
if (this.el) this.el.blur();
}
public focus(): void {
if (this.el) this.el.focus();
}
public pause(): void {
this.isActive.value = false;
}
public resume(): void {
this.isActive.value = true;
}
public setValue(str: string): void {
str = str || this.buffer;
if (this.el) this.el.value = str;
this.data.value = str;
}
public clear(): void {
this.buffer = "";
this.data.value = "";
this.el.value = "";
}
public pipe(output: IOutputInterface): void {
const txtBefore = h<HTMLSpanElement>("span");
const cursor = h<HTMLSpanElement>("span", {
class: THEME.CURSOR,
html: SPACE
});
const txtAfter = h<HTMLSpanElement>("span");
output.el?.append(txtBefore, cursor, txtAfter);
createEffect(() => {
const i = this.ptr.value;
const d = this.data.value;
if (!this.isActive.value || !this.showInput.value) {
txtBefore.textContent = "";
cursor.innerHTML = SPACE;
txtAfter.textContent = "";
return;
}
txtBefore.textContent = d.substring(0, i);
cursor.innerHTML = d.substring(i, i + 1).trim() || SPACE;
txtAfter.textContent = d.substring(i + 1);
});
}
}

View File

@@ -0,0 +1,47 @@
import type { IDisposable, IKeyPress } from "../types";
import type { IOutputInterface } from "../output/interface";
/**
* Interface: Input Component
*/
export interface IInputInterface extends IDisposable {
/**
* Blur the input element
*/
blur(): void;
/**
* Focus the input element
*/
focus(): void;
/**
* Deactivate the component
*/
pause(): void;
/**
* Activate the component
*/
resume(): void;
/**
* Callback function invoked on every key press
*/
onkeypress?: (ev: IKeyPress) => void;
/**
* Bridge the input to the output component: cursor & input
*/
pipe(output: IOutputInterface): void;
/**
* Set the value of the input element, updates the cursor
*/
setValue(str: string): void;
/**
* Clears the value of the input element
*/
clear(): void;
}