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("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; }