html: replace with preact version

This commit is contained in:
Shuanglei Tao
2019-05-27 00:00:02 +08:00
parent 3f52934418
commit 66718bc1d4
32 changed files with 2037 additions and 10373 deletions

View File

@@ -0,0 +1,45 @@
import { h, Component } from 'preact';
import { ITerminalOptions, ITheme } from 'xterm';
import Terminal from './terminal';
if ((module as any).hot) {
require('preact/debug');
}
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const wsPath = window.location.pathname.endsWith('/') ? 'ws' : '/ws';
const url = [protocol, window.location.host, window.location.pathname, wsPath, window.location.search].join('');
const termOptions = {
fontSize: 13,
fontFamily: '"Menlo for Powerline", Menlo, Consolas, "Liberation Mono", Courier, monospace',
theme: {
foreground: '#d2d2d2',
background: '#2b2b2b',
cursor: '#adadad',
black: '#000000',
red: '#d81e00',
green: '#5ea702',
yellow: '#cfae00',
blue: '#427ab3',
magenta: '#89658e',
cyan: '#00a7aa',
white: '#dbded8',
brightBlack: '#686a66',
brightRed: '#f54235',
brightGreen: '#99e343',
brightYellow: '#fdeb61',
brightBlue: '#84b0d8',
brightMagenta: '#bc94b7',
brightCyan: '#37e6e8',
brightWhite: '#f1f1f0'
} as ITheme
} as ITerminalOptions;
export default class App extends Component {
public render() {
return (
<Terminal id='terminal-container' url={url} options={termOptions} />
);
}
}

View File

@@ -0,0 +1,188 @@
import { h, Component } from 'preact';
import { bind } from 'decko';
import { Terminal as Xterm, ITerminalOptions } from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit';
import * as overlay from './overlay';
import 'xterm/dist/xterm.css';
enum Command {
// server side
OUTPUT = '0',
SET_WINDOW_TITLE = '1',
SET_PREFERENCES = '2',
SET_RECONNECT = '3',
// client side
INPUT = '0',
RESIZE_TERMINAL = '1'
}
interface ITerminal extends Xterm {
fit(): void;
showOverlay(msg: string, timeout?: number): void;
}
interface Props {
id: string;
url: string;
options: ITerminalOptions;
}
export default class Terminal extends Component<Props> {
private textEncoder: TextEncoder;
private textDecoder: TextDecoder;
private container: HTMLElement;
private terminal: ITerminal;
private socket: WebSocket;
private title: string;
private autoReconnect: number;
private resizeTimeout: number;
constructor(props) {
super(props);
Xterm.applyAddon(fit);
Xterm.applyAddon(overlay);
this.textEncoder = new TextEncoder();
this.textDecoder = new TextDecoder();
}
componentDidMount() {
this.openTerminal();
}
componentWillUnmount() {
this.socket.close();
this.terminal.dispose();
window.removeEventListener('resize', this.onWindowResize);
window.removeEventListener('beforeunload', this.onWindowUnload);
}
@bind
onWindowResize() {
const { terminal } = this;
clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => terminal.fit(), 250) as any;
}
onWindowUnload(event: BeforeUnloadEvent): string {
const message = 'Close terminal? this will also terminate the command.';
event.returnValue = message;
return message;
}
@bind
openTerminal() {
if (this.terminal) {
this.terminal.dispose();
}
this.socket = new WebSocket(this.props.url, ['tty']);
this.terminal = new Xterm(this.props.options) as ITerminal;
const { socket, terminal, container } = this;
socket.binaryType = 'arraybuffer';
socket.onopen = this.onSocketOpen;
socket.onmessage = this.onSocketData;
socket.onclose = this.onSocketClose;
terminal.onTitleChange((data) => {
if (data && data !== '') {
document.title = (data + ' | ' + this.title);
}
});
terminal.onData(this.onTerminalData);
terminal.onResize(this.onTerminalResize);
terminal.open(container);
terminal.focus();
window.addEventListener('resize', this.onWindowResize);
window.addEventListener('beforeunload', this.onWindowUnload);
}
@bind
onSocketOpen() {
console.log('Websocket connection opened');
const { socket, textEncoder, terminal } = this;
socket.send(textEncoder.encode(JSON.stringify({AuthToken: ''})));
terminal.fit();
}
@bind
onSocketClose(event: CloseEvent) {
console.log('Websocket connection closed with code: ' + event.code);
const { terminal, openTerminal, autoReconnect } = this;
terminal.showOverlay('Connection Closed', null);
window.removeEventListener('beforeunload', this.onWindowUnload);
// 1008: POLICY_VIOLATION - Auth failure
if (event.code === 1008) {
window.location.reload();
}
// 1000: CLOSE_NORMAL
if (event.code !== 1000 && autoReconnect > 0) {
setTimeout(openTerminal, autoReconnect * 1000);
}
}
@bind
onSocketData(event: MessageEvent) {
const { terminal, textDecoder } = this;
let rawData = new Uint8Array(event.data),
cmd = String.fromCharCode(rawData[0]),
data = rawData.slice(1).buffer;
switch(cmd) {
case Command.OUTPUT:
terminal.write(textDecoder.decode(data));
break;
case Command.SET_WINDOW_TITLE:
this.title = textDecoder.decode(data);
document.title = this.title;
break;
case Command.SET_PREFERENCES:
let preferences = JSON.parse(textDecoder.decode(data));
Object.keys(preferences).forEach(function(key) {
console.log('Setting ' + key + ': ' + preferences[key]);
terminal.setOption(key, preferences[key]);
});
break;
case Command.SET_RECONNECT:
this.autoReconnect = JSON.parse(textDecoder.decode(data));
console.log('Enabling reconnect: ' + this.autoReconnect + ' seconds');
break;
default:
console.warn('Unknown command: ' + cmd);
break;
}
}
@bind
onTerminalResize(size: {cols: number, rows: number}) {
const { terminal, socket, textEncoder } = this;
if (socket.readyState === WebSocket.OPEN) {
let msg = JSON.stringify({columns: size.cols, rows: size.rows});
socket.send(textEncoder.encode(Command.RESIZE_TERMINAL + msg));
}
setTimeout(() => {terminal.showOverlay(size.cols + 'x' + size.rows)}, 500);
}
@bind
onTerminalData(data: string) {
const { socket, textEncoder } = this;
if (socket.readyState === WebSocket.OPEN) {
socket.send(textEncoder.encode(Command.INPUT + data));
}
}
public render({ id }: Props) {
return (
<div id={id} ref={(c) => this.container = c}></div>
);
}
}

View File

@@ -0,0 +1,74 @@
// ported from hterm.Terminal.prototype.showOverlay
// https://chromium.googlesource.com/apps/libapps/+/master/hterm/js/hterm_terminal.js
import { Terminal } from 'xterm';
interface IOverlayAddonTerminal extends Terminal {
__overlayNode: HTMLElement | null;
__overlayTimeout: number | null;
}
export function showOverlay(term: Terminal, msg: string, timeout?: number): void {
const addonTerminal = <IOverlayAddonTerminal> term;
if (!addonTerminal.__overlayNode) {
if (!term.element) {
return;
}
addonTerminal.__overlayNode = document.createElement('div');
addonTerminal.__overlayNode.style.cssText = (
'border-radius: 15px;' +
'font-size: xx-large;' +
'opacity: 0.75;' +
'padding: 0.2em 0.5em 0.2em 0.5em;' +
'position: absolute;' +
'-webkit-user-select: none;' +
'-webkit-transition: opacity 180ms ease-in;' +
'-moz-user-select: none;' +
'-moz-transition: opacity 180ms ease-in;');
addonTerminal.__overlayNode.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
}, true);
}
addonTerminal.__overlayNode.style.color = '#101010';
addonTerminal.__overlayNode.style.backgroundColor = '#f0f0f0';
addonTerminal.__overlayNode.textContent = msg;
addonTerminal.__overlayNode.style.opacity = '0.75';
if (!addonTerminal.__overlayNode.parentNode) {
term.element.appendChild(addonTerminal.__overlayNode);
}
const divSize = term.element.getBoundingClientRect();
const overlaySize = addonTerminal.__overlayNode.getBoundingClientRect();
addonTerminal.__overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px';
addonTerminal.__overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px';
if (addonTerminal.__overlayTimeout) {
clearTimeout(addonTerminal.__overlayTimeout);
}
if (timeout === null) {
return;
}
addonTerminal.__overlayTimeout = <number><any>setTimeout(() => {
addonTerminal.__overlayNode.style.opacity = '0';
addonTerminal.__overlayTimeout = <number><any>setTimeout(() => {
if (addonTerminal.__overlayNode.parentNode) {
addonTerminal.__overlayNode.parentNode.removeChild(addonTerminal.__overlayNode);
}
addonTerminal.__overlayTimeout = null;
addonTerminal.__overlayNode.style.opacity = '0.75';
}, 200);
}, timeout || 1500);
}
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).showOverlay = function (msg: string, timeout?: number): void {
return showOverlay(this, msg, timeout);
};
}