first commit
This commit is contained in:
169
xterminal/source/instance.ts
Normal file
169
xterminal/source/instance.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user