170 lines
5.2 KiB
TypeScript
170 lines
5.2 KiB
TypeScript
import XTerminal from "./index";
|
|
import XHistory from "./history/index";
|
|
import XOutputComponent from "./output/index";
|
|
import XInputComponent from "./input/index";
|
|
import { createEffect } from "./base/reactivity";
|
|
import { NEWLINE, THEME, h, scrollDown } from "./renderer/dom";
|
|
import { isFunction, isMobile } from "./helpers";
|
|
import {
|
|
ARROW_DOWN_KEY,
|
|
ARROW_UP_KEY,
|
|
ENTER_KEY,
|
|
TAB_KEY,
|
|
addEvent
|
|
} from "./renderer/events";
|
|
import { bounce } from "./base/debouncer";
|
|
import { ITerminalState } from "./interface";
|
|
|
|
/**
|
|
* Public Events
|
|
*/
|
|
export const DATA_EVENT = "data";
|
|
export const CLEAR_EVENT = "clear";
|
|
export const KEYPRESS_EVENT = "keypress";
|
|
export const PAUSE_EVENT = "pause";
|
|
export const RESUME_EVENT = "resume";
|
|
|
|
/**
|
|
* Composes the components of the terminal object and integrates them
|
|
* @param instance The terminal object
|
|
* @param target The DOM element in which the terminal is to be mounted
|
|
*/
|
|
export function setup(
|
|
instance: XTerminal,
|
|
target: HTMLElement
|
|
): ITerminalState {
|
|
const term = h<HTMLDivElement>("div", {
|
|
class: THEME.CONTAINER,
|
|
props: { tabindex: 0 }
|
|
});
|
|
|
|
const xhistory = new XHistory();
|
|
const output = new XOutputComponent(term);
|
|
const input = new XInputComponent(term);
|
|
|
|
const state = {
|
|
input,
|
|
output,
|
|
history: xhistory,
|
|
completer: (x: string) => x
|
|
};
|
|
|
|
// connect the input to output (cursor & input text)
|
|
input.pipe(output);
|
|
|
|
// scroll to the bottom on every output operation
|
|
output.onoutput = () => scrollDown(term);
|
|
|
|
const checkScrollPosition = () => {
|
|
if (input.isFocused.value) {
|
|
// 给软键盘弹出和页面重排一点时间
|
|
setTimeout(() => {
|
|
let bottomInset = 0;
|
|
if (window.visualViewport) {
|
|
// 当键盘弹出时,如果布局视口高于可视视口,计算出被遮挡的高度(键盘+辅助栏等)
|
|
bottomInset = Math.max(
|
|
0,
|
|
window.innerHeight -
|
|
(window.visualViewport.height +
|
|
window.visualViewport.offsetTop)
|
|
);
|
|
}
|
|
|
|
// 给输入区底部补充内间距,这样只需滚动内部容器,完全不需要让浏览器滚动 body (这会导致 Safari 底栏失去透明度)
|
|
term.style.paddingBottom =
|
|
bottomInset > 0
|
|
? `calc(var(--xt-padding) + var(--tc-toolbar-height, 0px) + ${bottomInset}px)`
|
|
: "";
|
|
|
|
scrollDown(term);
|
|
|
|
const cursor = term.querySelector(
|
|
`.${THEME.CURSOR}`
|
|
) as HTMLElement;
|
|
if (cursor) {
|
|
try {
|
|
cursor.scrollIntoView({
|
|
block: "nearest",
|
|
inline: "nearest"
|
|
});
|
|
} catch {
|
|
// fallback
|
|
}
|
|
}
|
|
}, 100);
|
|
} else {
|
|
term.style.paddingBottom = "";
|
|
}
|
|
};
|
|
|
|
if (window.visualViewport) {
|
|
instance.register(
|
|
addEvent(window.visualViewport, "resize", checkScrollPosition)
|
|
);
|
|
} else {
|
|
instance.register(addEvent(window, "resize", checkScrollPosition));
|
|
}
|
|
|
|
instance.register(
|
|
addEvent(term, "keydown", function (ev: KeyboardEvent) {
|
|
// focus input element
|
|
input.focus();
|
|
// redirect the `keyboard` event to the input in the next event loop
|
|
bounce(() => {
|
|
input.el.dispatchEvent(new KeyboardEvent("keydown", ev));
|
|
input.el.dispatchEvent(new KeyboardEvent("input", ev));
|
|
});
|
|
})
|
|
);
|
|
|
|
instance.register(
|
|
addEvent(term, "focus", () => (input.isFocused.value = true))
|
|
);
|
|
|
|
instance.register(
|
|
addEvent(term, "blur", () => (input.isFocused.value = false))
|
|
);
|
|
|
|
if (isMobile()) {
|
|
// Toggle keyboard on mobile devices (touchscreen) on tap
|
|
instance.register(addEvent(term, "click", input.focus.bind(input)));
|
|
}
|
|
|
|
target.appendChild(term);
|
|
|
|
// handle keydown event
|
|
input.onkeypress = (ev) => {
|
|
if (ev.key == ENTER_KEY) {
|
|
ev.cancel();
|
|
xhistory.add(ev.value);
|
|
output.writeSafe(ev.value + NEWLINE);
|
|
instance.emit(DATA_EVENT, ev.value);
|
|
} else if (ev.key == TAB_KEY) {
|
|
ev.cancel();
|
|
if (isFunction(state.completer)) {
|
|
input.setValue(state.completer(ev.value));
|
|
}
|
|
} else if (ev.key == ARROW_DOWN_KEY) {
|
|
ev.cancel();
|
|
input.setValue(xhistory.next());
|
|
} else if (ev.key == ARROW_UP_KEY) {
|
|
ev.cancel();
|
|
input.setValue(xhistory.previous());
|
|
} else {
|
|
instance.emit(KEYPRESS_EVENT, ev);
|
|
}
|
|
scrollDown(term);
|
|
};
|
|
|
|
createEffect(() => {
|
|
// Fill the cursor if focused, otherwise outline
|
|
if (input.isFocused.value) {
|
|
term.classList.remove(THEME.INACTIVE);
|
|
} else {
|
|
term.classList.add(THEME.INACTIVE);
|
|
}
|
|
});
|
|
|
|
return state;
|
|
}
|