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,63 @@
import { isArray, isObject } from "../helpers";
import type { IElementProps } from "./interface";
export const NEWLINE = "\n";
export const SPACE = " ";
/**
* CSS class names
*/
export const THEME = {
CONTAINER: "xt",
INACTIVE: "xt-inactive",
CURSOR: "xt-cursor",
OUTPUT: "xt-stdout",
INPUT: "xt-stdin"
};
export function scrollDown(el: HTMLElement): void {
if (el) el.scrollTo(0, el.scrollHeight);
}
/**
* Create a ready to use HTMLElement
*
* https://github.com/henryhale/render-functions
*
* https://vuejs.org/guide/extras/render-function.html
*
* @param tag The HTML element tag
* @param options The some properties of the element
* @returns The HTML Element
*/
export function h<T extends HTMLElement>(
tag: string,
options?: IElementProps
): T {
const elem = document.createElement(tag);
if (!isObject(options)) {
return elem as T;
}
if (options?.id) {
elem.id = options.id || "";
}
if (options?.class) {
elem.className = options.class || "";
}
if (options?.content) {
elem.appendChild(document.createTextNode(options.content || ""));
}
if (options?.html) {
elem.innerHTML = options.html;
}
if (isArray(options?.children)) {
options.children.forEach((c) => elem.append(c));
}
if (isObject(options?.props)) {
Object.entries(options.props).forEach((v) =>
elem.setAttribute(v[0], v[1])
);
}
return elem as T;
}

View File

@@ -0,0 +1,28 @@
import type { IDisposable } from "../types";
export const ENTER_KEY = "Enter",
TAB_KEY = "Tab",
ARROW_UP_KEY = "ArrowUp",
ARROW_DOWN_KEY = "ArrowDown";
/**
* Attaches an event listener to the element returning a disposable object
* to remove the event listener
*/
export function addEvent(
el: Element | Document | Window | VisualViewport | EventTarget,
type: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handler: (e: any) => void,
opt?: boolean | AddEventListenerOptions
): IDisposable {
el.addEventListener(type, handler, opt);
let disposed = false;
return {
dispose() {
if (disposed) return;
el.removeEventListener(type, handler, opt);
disposed = true;
}
};
}

View File

@@ -0,0 +1,68 @@
import { THEME, h } from "./dom";
/**
* Generate a build for the output component
* @param target The parent element in which it is mounted
* @returns DOM reference to the console box and output container
*/
export default function outputBuild(target: HTMLElement) {
const consoleBox = h<HTMLSpanElement>("span");
const outputBox = h<HTMLDivElement>("div", {
class: THEME.OUTPUT,
children: [consoleBox]
});
target.appendChild(outputBox);
return { outputBox, consoleBox };
}
/**
* Generate a build for the input component
* @param target The parent element in which it is mounted
* @returns DOM reference to the input element
*/
export function inputBuild(target: HTMLElement) {
const inputBox = h<HTMLTextAreaElement>("textarea", {
props: {
// 明确声明为普通文本输入,降低移动端将其识别为登录表单的概率。
spellcheck: false,
autocorrect: "off",
autocapitalize: "off",
autocomplete: "off",
name: "xterminal_input",
inputmode: "text",
enterkeyhint: "enter",
rows: 1,
wrap: "off"
}
});
// 部分密码管理器会读取 data-* 做识别,显式标记为非登录用途。
inputBox.setAttribute("data-form-type", "other");
inputBox.setAttribute("data-lpignore", "true");
inputBox.setAttribute("data-1p-ignore", "true");
inputBox.setAttribute("data-bwignore", "true");
inputBox.setAttribute("aria-autocomplete", "none");
inputBox.setAttribute("autocapitalize", "off");
inputBox.setAttribute("autocorrect", "off");
// 某些浏览器在首帧点击文本输入框时会误触发凭据弹窗;
// 先只读,获得焦点后立刻解除,可显著降低“刷新后点一下就弹登录”的概率。
inputBox.readOnly = true;
inputBox.addEventListener(
"focus",
() => {
inputBox.readOnly = false;
},
{ once: true }
);
const stdin = h<HTMLDivElement>("div", {
class: THEME.INPUT,
children: [inputBox]
});
target.appendChild(stdin);
return inputBox;
}

View File

@@ -0,0 +1,37 @@
import type { IDisposable } from "../types";
/**
* Render function - element props
*/
export interface IElementProps {
id?: string;
class?: string;
content?: string;
html?: string;
children?: (string | Node)[];
props?: object;
}
/**
* Key Bindings to the Input
*/
interface IKeyBindingAction {
(arg1: unknown, arg2?: unknown): void;
}
export interface IKeyBindings {
[key: string]: IKeyBindingAction;
}
/**
* Renderer
*/
export interface IRenderer extends IDisposable {
canInput: boolean;
setKeyBindings(options: IKeyBindings): void;
mount(el: HTMLElement): void;
focusInput(): void;
blurInput(): void;
clearConsole(): void;
output(data: string): void;
}