UNPKG

ng-terminal

Version:

NgTerminal is a terminal component on Angular 16 or higher.

772 lines (766 loc) 64.6 kB
import * as i0 from '@angular/core'; import { Injectable, Optional, Component, ViewEncapsulation, EventEmitter, ChangeDetectionStrategy, Output, Input, ViewChild, NgModule } from '@angular/core'; import { Terminal } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; import { Subject } from 'rxjs'; import * as i2 from 'angular-resizable-element'; import { ResizableModule } from 'angular-resizable-element'; import * as i3 from '@angular/common'; import { CommonModule } from '@angular/common'; /** * * In the past, indivisual property changes caused very unexpected behavior on NgTerminal. * To solve issues related to property changes, Queue is applied. */ class LinearRenderService { constructor(hostRef) { this.hostRef = hostRef; this.handlerToCheckElementConnection = undefined; /** * This queue has items to have a influence on the view and items are handled in order. */ this.propertyChangeQueue = []; this.itemsToBeHandled = new Subject(); /** * Handle a next set of changes if it exists. */ this.handleImmediate = () => { if (!this.handlerToCheckElementConnection) { let list = this.propertyChangeQueue.splice(0, 1); if (list.length == 1) { this.itemsToBeHandled.next(list[0]); } } }; } get renderObservable() { return this.itemsToBeHandled; } /** * {@link handleNextOne()} calls {@link handleImmediate()} if an element is connected. * Otherwise, {@link pollAndHandle()} is called. */ handleNextOne(lazy = false) { if (!this.hostRef.nativeElement.isConnected || lazy) { this.pollAndHandle(!this.hostRef.nativeElement.isConnected ? 'NOT_CONNECTED_ELEMENT' : 'LAZY'); } else { this.handleImmediate(); } } /** * This method pushes item into {@link propertyChangeQueue}. * @param item */ pushAndHandle(item, lazy = false) { let changeWithDefault = Object.assign({}, item); this.propertyChangeQueue.push(changeWithDefault); this.handleNextOne(lazy); } /** * {@link pollAndHandle()} continues checking whether new item is put on a queue to call {@link handleImmediate()}. */ pollAndHandle(mode) { const pollFunction = () => { if (this.handlerToCheckElementConnection) return; const interval = setInterval(() => { if (this.hostRef.nativeElement.isConnected) { clearInterval(interval); this.handlerToCheckElementConnection = undefined; this.handleImmediate(); } }, mode === 'NOT_CONNECTED_ELEMENT' ? 500 : 30); this.handlerToCheckElementConnection = interval; }; // Start polling pollFunction(); } ngOnDestroy() { if (this.handlerToCheckElementConnection) clearInterval(this.handlerToCheckElementConnection); } } LinearRenderService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: LinearRenderService, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Injectable }); LinearRenderService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: LinearRenderService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: LinearRenderService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: function () { return [{ type: i0.ElementRef }]; } }); class ModuleOptions { constructor() { this.enableLog = true; } } class LoggingService { constructor(_options) { this.enableLog = false; if (_options) this.enableLog = _options.enableLog; } log(loggingFunc) { if (this.enableLog) loggingFunc(); } } LoggingService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: LoggingService, deps: [{ token: ModuleOptions, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); LoggingService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: LoggingService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: LoggingService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: function () { return [{ type: ModuleOptions, decorators: [{ type: Optional }] }]; } }); class GlobalStyleComponent { constructor() { } ngOnInit() { } } GlobalStyleComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: GlobalStyleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); GlobalStyleComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: GlobalStyleComponent, selector: "global-style", ngImport: i0, template: "", styles: ["/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * https://github.com/chjj/term.js\n * @license MIT\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n *\n * Originally forked from (with the author's permission):\n * Fabrice Bellard's javascript vt100 for jslinux:\n * http://bellard.org/jslinux/\n * Copyright (c) 2011 Fabrice Bellard\n * The original design remains. The terminal itself\n * has been extended to include xterm CSI codes, among\n * other features.\n */.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;inset:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;inset:0;z-index:10;color:transparent;pointer-events:none}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:double underline;text-decoration:double underline}.xterm-underline-3{-webkit-text-decoration:wavy underline;text-decoration:wavy underline}.xterm-underline-4{-webkit-text-decoration:dotted underline;text-decoration:dotted underline}.xterm-underline-5{-webkit-text-decoration:dashed underline;text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}\n"], encapsulation: i0.ViewEncapsulation.None }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: GlobalStyleComponent, decorators: [{ type: Component, args: [{ selector: 'global-style', encapsulation: ViewEncapsulation.None, template: "", styles: ["/**\n * Copyright (c) 2014 The xterm.js authors. All rights reserved.\n * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)\n * https://github.com/chjj/term.js\n * @license MIT\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n *\n * Originally forked from (with the author's permission):\n * Fabrice Bellard's javascript vt100 for jslinux:\n * http://bellard.org/jslinux/\n * Copyright (c) 2011 Fabrice Bellard\n * The original design remains. The terminal itself\n * has been extended to include xterm CSI codes, among\n * other features.\n */.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;inset:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;inset:0;z-index:10;color:transparent;pointer-events:none}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:double underline;text-decoration:double underline}.xterm-underline-3{-webkit-text-decoration:wavy underline;text-decoration:wavy underline}.xterm-underline-4{-webkit-text-decoration:dotted underline;text-decoration:dotted underline}.xterm-underline-5{-webkit-text-decoration:dashed underline;text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}\n"] }] }], ctorParameters: function () { return []; } }); class NgTerminalComponent { /** * A datsource is an observable where NgTerminal reads charactors. */ set _dataSource(dataSource) { if (!dataSource) return; if (this.dataSourceSubscription != undefined) this.dataSourceSubscription.unsubscribe(); this.dataSource = dataSource; this.dataSourceSubscription = this.dataSource.subscribe((data) => this.write(data)); } /** * Toggle draggable. */ set draggable(draggable) { this._draggable = draggable; } get draggable() { return this._draggable; } setMinWidth(width) { this._minWidthInput = width; } setMinHeight(height) { this._minHeightInput = height; } setDraggable(draggable) { this._draggable = draggable; this.lastDraggedPosition = undefined; // Reset position values } setXtermOptions(options) { this._xtermOptions = options; this.linearRender.pushAndHandle({ time: new Date(), type: 'none' }); } setRows(rows) { if (this._rowsInput != rows) { this._rowsInput = rows; this.linearRender.pushAndHandle({ time: new Date(), type: 'rowChanged', }); } } setCols(cols) { if (this._colsInput != cols) { this._colsInput = cols; this.linearRender.pushAndHandle({ time: new Date(), type: 'columnChanged', }); } } setStyle(styleObject) { var _a; if (JSON.stringify((_a = this._stylesInput) !== null && _a !== void 0 ? _a : {}) != JSON.stringify(styleObject !== null && styleObject !== void 0 ? styleObject : {})) { this._stylesInput = styleObject; this.linearRender.pushAndHandle({ time: new Date(), type: 'none', }); } } constructor(renderer, //Render is being used for fast rendering without markForCheck(). hostRef, loggingService) { this.renderer = renderer; this.hostRef = hostRef; this.loggingService = loggingService; this.dataSubject = new Subject(); this.keySubject = new Subject(); this.keyInputSubjectSubscription = undefined; this.keyEventSubjectSubscription = undefined; this.allLogsSubjectSubscription = undefined; this.dataSourceSubscription = undefined; this.resizableObservers = []; this.dataSource = undefined; this.paddingSize = 5; this.stylesForResizeBox = { display: 'block' }; /** * @deprecated use (data)='' instead. * An emitter emits printable characters pushed from xterm's onData() when a user typed on the terminal. */ this.keyInputEmitter = new EventEmitter(); /** * @deprecated use (key)='' instead. * An emitter emits key and keybaord event pushed from xterm's onKey() when a user typed on the terminal. */ this.keyEventEmitter = new EventEmitter(); /** * An emitter emits printable characters pushed from xterm's onData() when a user typed on the terminal. */ this.dataEmitter = this.keyInputEmitter; /** * An emitter emits key and keybaord event pushed from xterm's onKey() when a user typed on the terminal. */ this.keyEmitter = this.keyEventEmitter; /** * An wrapper of {@link ITerminalOptions} for Xterm. */ this._xtermOptions = {}; this.lastDraggedPosition = undefined; this._draggable = false; this._stylesInput = {}; this.handleToCheckLazyContainer = undefined; this.lastDetectedWidth = 0; this.linearRender = new LinearRenderService(hostRef); } setup() { if (this.xterm) { this.xterm.onData((input) => { this.dataSubject.next(input); }); this.xterm.onKey((e) => { this.keySubject.next(e); }); } this.keyInputSubjectSubscription = this.dataSubject.subscribe((data) => { this.keyInputEmitter.emit(data); }); this.keyEventSubjectSubscription = this.keySubject.subscribe((e) => { this.keyEventEmitter.emit(e); }); this.setupResizeObservers(); // if (ob3) this.resizableObservers.push(ob3); this.allLogsSubjectSubscription = this.linearRender.renderObservable.subscribe((change) => { if (change) this.coordinateOuterAndTerminal(change); else this.coordinateOuterAndTerminal(change); this.linearRender.handleNextOne(); }); this.linearRender.handleNextOne(); } /** * set dimensions */ setOuterDimensions(left, top, width, height) { this.linearRender.pushAndHandle({ time: new Date(), type: 'dragged', dragged: { draggedWidth: `${width}px`, draggedHeight: `${height}px` }, }); } applyStylesToResizeBox() { Object.keys(this.stylesForResizeBox) .map((key) => { return { key, value: this.stylesForResizeBox[key] }; }) .forEach(({ key, value }) => { if (this.resizeBox) { if (value) this.renderer.setStyle(this.resizeBox.nativeElement, key, value); else { this.renderer.removeStyle(this.resizeBox.nativeElement, key); } } }); this.stylesForResizeBox = this.stylesForResizeBox; //invalidate } ngOnInit() { } /** * It creates new terminal in #terminal. */ ngAfterViewInit() { this.fitAddon = new FitAddon(); this.xterm = new Terminal({ allowProposedApi: true, }); if (!this.terminalOuter.nativeElement.isConnected) { this.handleToCheckLazyContainer = setInterval(() => { if (this.terminalOuter.nativeElement.isConnected) { try { this.loggingService.log(() => console.debug("The container's been connected.")); this.xterm.open(this.terminalOuter.nativeElement); this.xterm.loadAddon(this.fitAddon); this.setup(); this.linearRender.pushAndHandle({ time: new Date(), type: 'none' }); } finally { if (this.handleToCheckLazyContainer) clearInterval(this.handleToCheckLazyContainer); } } }, 500); } else { this.xterm.open(this.terminalOuter.nativeElement); this.xterm.loadAddon(this.fitAddon); this.setup(); this.linearRender.pushAndHandle({ time: new Date(), type: 'none' }); } } ngOnChanges(changes) { var _a, _b, _c, _d; this.loggingService.log(() => console.group('onChanges')); this.loggingService.log(() => console.debug('prop: ', changes)); this.loggingService.log(() => console.groupEnd()); if (changes === null || changes === void 0 ? void 0 : changes['_rowsInput']) { if (((_a = changes === null || changes === void 0 ? void 0 : changes['_rowsInput']) === null || _a === void 0 ? void 0 : _a.previousValue) != ((_b = changes === null || changes === void 0 ? void 0 : changes['_rowsInput']) === null || _b === void 0 ? void 0 : _b.currentValue)) { this.linearRender.pushAndHandle({ time: new Date(), type: 'rowChanged', }); } } if (changes === null || changes === void 0 ? void 0 : changes['_colsInput']) { if (((_c = changes === null || changes === void 0 ? void 0 : changes['_colsInput']) === null || _c === void 0 ? void 0 : _c.previousValue) != ((_d = changes === null || changes === void 0 ? void 0 : changes['_colsInput']) === null || _d === void 0 ? void 0 : _d.currentValue)) { this.linearRender.pushAndHandle({ time: new Date(), type: 'columnChanged', }); } } if (changes === null || changes === void 0 ? void 0 : changes['draggable']) this.linearRender.pushAndHandle({ time: new Date(), type: 'none' }); } /** * It serves a callback function to adjust the dimensions of the xterm-screen, xterm-view, and resize box * after making any changes to the outer box, rows, or columns, or when the resize box is being dragged. * * There several factors that affect dimensions, as I mentioned earlier. * Regardless of whether the draggable feature is on, if new row or column value is input, this value will be applied. * - Draggable = New specified Row/Column value > Full (Default) * @param change This argument represents a single change that occured. * @returns */ coordinateOuterAndTerminal(change) { var _a; this.loggingService.log(() => console.debug(`changeList: ${JSON.stringify(change)}`)); if (!this.xterm) return; const isHostElementVisible = ((_a = this.hostRef.nativeElement) === null || _a === void 0 ? void 0 : _a.offsetParent) !== null; if (!isHostElementVisible) { // Do nothing if the host element is invisible. this.loggingService.log(() => console.debug('`display` of host element was set to `none`')); return; } this.doUpdateXtermStyles(); this.doAdjustDimensionOfResizeBox(change); this.doAdjustSizeOfXtermScreen(change); this.doUpdateViewportAndResizeBoxWithPixcelUnit(); } /** * apply options to the xterm terminal * @returns */ doUpdateXtermStyles() { var _a, _b; if (!this.xterm) return; this.xterm.options = Object.assign({}, this._xtermOptions); // apply the theme to the background of the handle if ((_a = this._xtermOptions.theme) === null || _a === void 0 ? void 0 : _a.background) { if (!this.resizeHandleStyleRule) this.resizeHandleStyleRule = this.findCssStyleRule('.resize-handle['); if (this.resizeHandleStyleRule) this.resizeHandleStyleRule.style.backgroundColor = this._xtermOptions.theme.background; } if ((_b = this._xtermOptions.theme) === null || _b === void 0 ? void 0 : _b.border) { if (!this.resizeHandleActiveStyleRule) this.resizeHandleActiveStyleRule = this.findCssStyleRule('.handle-active'); if (this.resizeHandleActiveStyleRule) this.resizeHandleActiveStyleRule.style.backgroundColor = this._xtermOptions.theme.border; } } /** * If the resize handles are moved, the resize box adjusts to the new dimensions; * otherwise, it defaults to a maximized size. * @param change */ doAdjustDimensionOfResizeBox(change) { var _a, _b; this.stylesForResizeBox = Object.assign(Object.assign(Object.assign({}, this.stylesForResizeBox), this._stylesInput), { width: this.stylesForResizeBox.width, height: this.stylesForResizeBox.height }); // Reset styles of the resize element if (change.type === 'dragged') { const minWidth = (_a = this._minWidthInput) !== null && _a !== void 0 ? _a : 24; const minHeight = (_b = this._minHeightInput) !== null && _b !== void 0 ? _b : 24; const width = parseInt(change.dragged.draggedWidth) > minWidth ? change.dragged.draggedWidth : `${minWidth}px`; const height = parseInt(change.dragged.draggedHeight) > minHeight ? change.dragged.draggedHeight : `${minHeight}px`; this.stylesForResizeBox.width = width; this.stylesForResizeBox.height = height; this.lastDraggedPosition = { width, height, }; this.applyStylesToResizeBox(); } else if (!(this.draggable && this.lastDraggedPosition)) { // When `_colsInput` and `draggable` is not enabled, // it fits the size to the host element. const currentHostWidth = getComputedStyle(this.hostRef.nativeElement).width; const detectBoxWidth = getComputedStyle(this.detectBox.nativeElement).width; let smallParent = false; if (parseFloat(detectBoxWidth) < parseFloat(currentHostWidth)) { // the width of the parent is smaller than that of resize-box element smallParent = true; } if (smallParent) { // It's been written to solve https://github.com/qwefgh90/ng-terminal/issues/79 // If the width of the flex-box (that is the parent of the host element) is smaller than that of child element, the host element is adjusted to the width of child element // host element: 1000px, resize-box(child): 985px -> host element: 985px, resize-box(child): 970px -> ... -> stop // This code check if the parent element (that is the parent of `<ng-terminal>), is smaller than `.resize-box` // and ensures that the width of the `<ng-terminal>` adjusts to match that of the parent element rather than the child elements, in the subsequent events. this.stylesForResizeBox.width = `${parseFloat(detectBoxWidth)}px`; this.applyStylesToResizeBox(); } else { // but if the dimension of host element is resized, update width and height // If `_rowsInput` is specified, NgTerminal keep the current height; otherwise, the height is set to 100% if (!this._rowsInput) this.stylesForResizeBox.height = '100%'; if (!this._colsInput) this.stylesForResizeBox.width = '100%'; this.applyStylesToResizeBox(); } } } /** * This function uses fitAddon() to adjust the dimension of xterm-screen to character unit * If the draggable value is true or there are no fixed values for the row or column, * it fits the xterm terminal boxes into the resize box; * otherwise, it resizes the xterm terminal with specified row and column values. */ doAdjustSizeOfXtermScreen(change) { var _a, _b, _c; if (!this.xterm) return; if ((change.type == 'rowChanged' && this._rowsInput) || (change.type == 'columnChanged' && this._colsInput)) { this.xterm.resize((_a = this._colsInput) !== null && _a !== void 0 ? _a : this.xterm.cols, (_b = this._rowsInput) !== null && _b !== void 0 ? _b : this.xterm.rows); } else { if (this.xterm.element) { // The fitAddon.fit() operation doesn't recognize the padding values of terminalOuter. // It seems to be using the padding values of xterm element instead. // Therefore, we establish a brief time frame to adjust the padding values before and after executing fitAddon.fit(). // If this line is removed, when dragging resize-box vertically, the width is decreased. this.xterm.element.style.paddingLeft = `${this.paddingSize}px`; this.printDimension('Before fitAddon.fit() of Xterm'); (_c = this.fitAddon) === null || _c === void 0 ? void 0 : _c.fit(); this.printDimension('After fitAddon.fit() of Xterm'); this.xterm.element.style.paddingLeft = `${0}px`; } } } /** * This functions sets width of the resize box, xterm-viewport and xterm-screen with specific pixel values. */ doUpdateViewportAndResizeBoxWithPixcelUnit() { var _a; if ((_a = this.xterm) === null || _a === void 0 ? void 0 : _a.element) { let xtermScreen = this.xterm.element.getElementsByClassName('xterm-screen')[0]; let xtermViewport = this.xterm.element.getElementsByClassName('xterm-viewport')[0]; const screenWidth = xtermScreen.clientWidth; const screenHeight = xtermScreen.clientHeight; const core = this.underlying._core; const scrollBarWidth = core.viewport.scrollBarWidth; const hostWidth = parseInt(getComputedStyle(this.hostRef.nativeElement).width); // It fixes a bug where the viewport's width isn't updated by fitAddon.fit() this.renderer.setStyle(xtermViewport, 'width', `${screenWidth + scrollBarWidth}px`); // It adjusts the dimension of the resize box to the xterm-screen element. const calulatedBoxWidth = screenWidth + scrollBarWidth + this.paddingSize * 2; const componentElement = this.hostRef.nativeElement; const componentWith = parseInt(getComputedStyle(componentElement).width); const restrictedWidth = calulatedBoxWidth > componentWith ? componentWith : calulatedBoxWidth; this.stylesForResizeBox = Object.assign(Object.assign({}, this.stylesForResizeBox), { width: `${restrictedWidth}px`, height: `${screenHeight + this.paddingSize * 2}px` }); this.applyStylesToResizeBox(); this.printDimension('After update the dimensions for all boxes with pixel values'); } } printDimension(title) { var _a; if ((_a = this.xterm) === null || _a === void 0 ? void 0 : _a.element) { let resizeBox = this.resizeBox.nativeElement; let xtermScreen = this.xterm.element.getElementsByClassName('xterm-screen')[0]; let xtermViewport = this.xterm.element.getElementsByClassName('xterm-viewport')[0]; const screenWidth = xtermScreen.clientWidth; const screenHeight = xtermScreen.clientHeight; this.loggingService.log(() => console.group(`${title}`)); this.loggingService.log(() => console.debug(`width(resizeBox): ${getComputedStyle(resizeBox).width}, width(viewport): ${getComputedStyle(xtermViewport).width}, width(screen): ${screenWidth} scrollBarWidth: ${this.scrollBarWidth}`)); this.loggingService.log(() => console.debug(`height(resizeBox): ${getComputedStyle(resizeBox).height}, height(viewport) ${getComputedStyle(xtermViewport).height}, height(screen): ${screenHeight}`)); this.loggingService.log(() => console.groupEnd()); } } /** * If pushAndHandle() were used, there could be an issue * because it can adjust the size of elements during a loop of ResizeObserver. */ setupResizeObservers() { this.resizableObservers = []; let ob1 = this.observeXtermViewportDimension(); let ob2 = this.observeHostDimension(); if (ob1) this.resizableObservers.push(ob1); if (ob2) this.resizableObservers.push(ob2); } observeXtermViewportDimension() { let xtermViewport = this.terminalOuter.nativeElement.querySelector('.xterm-viewport'); if (xtermViewport) { const resizeObserver = new ResizeObserver((entries) => { var _a, _b; const outerDivWidth = (_a = this.terminalOuter.nativeElement) === null || _a === void 0 ? void 0 : _a.clientWidth; const outerDivHeight = (_b = this.terminalOuter.nativeElement) === null || _b === void 0 ? void 0 : _b.clientHeight; for (let entry of entries) { if (entry.contentBoxSize.length > 0) { let width = entry.target.clientWidth; let height = entry.target.clientHeight; if ((outerDivWidth && width > outerDivWidth) || (outerDivHeight && height > outerDivHeight)) { this.loggingService.log(() => console.debug('Changes on a xterm viewport element will be handled.')); this.linearRender.pushAndHandle({ time: new Date(), type: 'xtermViewportExceedingOuterDiv', xtermViewportExceedingOuterDiv: { width: `${width}`, height: `${height}`, outerDivWidth: `${outerDivWidth}`, outerDivHeight: `${outerDivHeight}`, }, }, true); } } } }); resizeObserver.observe(xtermViewport); return resizeObserver; } else { this.loggingService.log(() => console.error('Invalid state is detected. xterm element should exist below .terminal-outer.')); } return undefined; } observeHostDimension() { let hostElement = this.hostRef.nativeElement; let detectBox = this.detectBox.nativeElement; if (hostElement && detectBox) { const resizeObserver = new ResizeObserver((entries) => { for (let entry of entries) { if (entry.target === hostElement) { if (entry.contentBoxSize.length > 0) { let width = getComputedStyle(entry.target).width; let height = getComputedStyle(entry.target).height; if (parseInt(width) >= 0 && parseInt(height) >= 0) { this.loggingService.log(() => console.debug('Changes on a host element will be handled.')); this.linearRender.pushAndHandle({ time: new Date(), type: 'hostResized', hostResized: { width: `${width}`, height: `${height}` }, }, true); } } } if (entry.target === detectBox) { if (entry.contentBoxSize.length > 0) { let width = getComputedStyle(entry.target).width; if (parseInt(width) >= 0 && parseInt(width) <= this.lastDetectedWidth) { this.loggingService.log(() => console.debug('Changes on a detect-box element will be handled.')); this.linearRender.pushAndHandle({ time: new Date(), type: 'detectBoxResized', detectBoxResized: { width: `${width}` }, }, true); } this.lastDetectedWidth = parseInt(width); } } } }); resizeObserver.observe(hostElement); resizeObserver.observe(detectBox); return resizeObserver; } else { this.loggingService.log(() => console.error('Invalid state is detected. xterm element should exist below .terminal-outer.')); } return undefined; } /** * clean all resources */ ngOnDestroy() { this.resizableObservers.forEach((ob) => ob.disconnect()); if (this.keyInputSubjectSubscription) this.keyInputSubjectSubscription.unsubscribe(); if (this.dataSourceSubscription) this.dataSourceSubscription.unsubscribe(); if (this.keyEventSubjectSubscription) this.keyEventSubjectSubscription.unsubscribe(); if (this.allLogsSubjectSubscription) this.allLogsSubjectSubscription.unsubscribe(); if (this.handleToCheckLazyContainer) clearInterval(this.handleToCheckLazyContainer); if (this.xterm) this.xterm.dispose(); this.loggingService.log(() => console.debug('All resources has been cleaned up.')); } write(chars) { var _a; (_a = this.xterm) === null || _a === void 0 ? void 0 : _a.write(chars); } get keyInput() { return this.dataSubject; } onData() { return this.dataSubject; } get keyEventInput() { return this.keySubject; } onKey() { return this.keySubject; } get underlying() { return this.xterm; } get isDraggableOnEdgeActivated() { // return this.displayOption.activateDraggableOnEdge != undefined && this.displayOption.fixedGrid == undefined; return this._draggable; } get scrollBarWidth() { const core = this.underlying._core; return core.viewport.scrollBarWidth; } /** * After user coordinate dimensions of terminal, it's called. * @param left * @param top * @param width * @param height */ onResizeEnd(left, top, width, height) { this.setOuterDimensions(left, top, width, height); } /** * Before onResizeEnd is called, it valiates dimensions to change. * @param re dimension to be submitted from resizable stuff */ validatorFactory() { const comp = this; return (re) => { if (this._draggable) { return true; } else return false; }; } findCssStyleRule(containingSelector) { for (let i = 0; i < document.styleSheets.length; i++) { let sheet = document.styleSheets.item(i); if (sheet) { for (let i = 0; i < sheet.cssRules.length; i++) { let rule = sheet.cssRules.item(i); if ('selectorText' in rule) if (rule.selectorText.includes(containingSelector)) return rule; } } } return undefined; } } NgTerminalComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgTerminalComponent, deps: [{ token: i0.Renderer2 }, { token: i0.ElementRef }, { token: LoggingService }], target: i0.ɵɵFactoryTarget.Component }); NgTerminalComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: NgTerminalComponent, selector: "ng-terminal", inputs: { _dataSource: ["dataSource", "_dataSource"], _rowsInput: ["rows", "_rowsInput"], _colsInput: ["cols", "_colsInput"], _minWidthInput: ["minWidth", "_minWidthInput"], _minHeightInput: ["minHeight", "_minHeightInput"], draggable: "draggable", _xtermOptions: ["xtermOptions", "_xtermOptions"] }, outputs: { keyInputEmitter: "keyInput", keyEventEmitter: "keyEvent", dataEmitter: "data", keyEmitter: "key" }, providers: [LinearRenderService], viewQueries: [{ propertyName: "terminalOuter", first: true, predicate: ["terminal"], descendants: true, static: true }, { propertyName: "resizeBox", first: true, predicate: ["resizeBox"], descendants: true, static: true }, { propertyName: "detectBox", first: true, predicate: ["detectBox"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Hierarchy: parent element(user) > host element(ng-terminal) > resize-box element > outer element -->\n<global-style></global-style>\n<div mwlResizable #resizeBox class=\"resize-box\" [validateResize]=\"validatorFactory()\" [enableGhostResize]=\"true\"\n (resizeEnd)=\"onResizeEnd($event.rectangle.left, $event.rectangle.top, $event.rectangle.width??0, $event.rectangle.height??0)\">\n <div #terminal class=\"terminal-outer\">\n </div>\n <div class=\"resize-handle resize-handle-top\" mwlResizeHandle [ngClass]=\"{'handle-active' : isDraggableOnEdgeActivated}\"\n [resizeEdges]=\"{ top: false }\"></div>\n <div class=\"resize-handle resize-handle-left\" mwlResizeHandle [ngClass]=\"{'handle-active' : isDraggableOnEdgeActivated}\"\n [resizeEdges]=\"{ left: false }\"></div>\n <div class=\"resize-handle resize-handle-right\" mwlResizeHandle [ngClass]=\"{'handle-active' : isDraggableOnEdgeActivated}\"\n [resizeEdges]=\"isDraggableOnEdgeActivated ? { right: true } : { right: false }\"></div>\n <div class=\"resize-handle resize-handle-bottom\" mwlResizeHandle [ngClass]=\"{'handle-active' : isDraggableOnEdgeActivated}\"\n [resizeEdges]=\"isDraggableOnEdgeActivated ? { bottom: true } : { bottom: false }\"></div>\n</div>\n<div #detectBox style=\"width: 100%; height: 0px; position: absolute;\">\n</div>", styles: [".terminal-outer{box-sizing:border-box;height:100%;width:100%;padding:5px}:host{display:block;height:100%;width:100%}.resize-box{box-sizing:border-box;height:100%;width:100%;position:relative}div>.handle-active{z-index:100;background-color:#85858a!important}.resize-handle{background-color:#000}.resize-handle-top.handle-active,.resize-handle-bottom.handle-active{cursor:row-resize}.resize-handle-left.handle-active,.resize-handle-right.handle-active{cursor:col-resize}.resize-handle-top,.resize-handle-bottom{position:absolute;height:5px;width:100%}.resize-handle-top{top:0}.resize-handle-bottom{bottom:0}.resize-handle-left,.resize-handle-right{position:absolute;height:100%;width:5px}.resize-handle-left{left:0;top:0}.resize-handle-right{right:0;top:0}\n"], dependencies: [{ kind: "directive", type: i2.ResizableDirective, selector: "[mwlResizable]", inputs: ["validateResize", "enableGhostResize", "resizeSnapGrid", "resizeCursors", "ghostElementPositioning", "allowNegativeResizes", "mouseMoveThrottleMS"], outputs: ["resizeStart", "resizing", "resizeEnd"], exportAs: ["mwlResizable"] }, { kind: "directive", type: i2.ResizeHandleDirective, selector: "[mwlResizeHandle]", inputs: ["resizeEdges", "resizableContainer"] }, { kind: "directive", type: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: GlobalStyleComponent, selector: "global-style" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: NgTerminalComponent, decorators: [{ type: Component, args: [{ selector: 'ng-terminal', providers: [LinearRenderService], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Hierarchy: parent element(user) > host element(ng-terminal) > resize-box element > outer element -->\n<global-style></global-style>\n<div mwlResizable #resizeBox class=\"resize-box\" [validateResize]=\"validatorFactory()\" [enableGhostResize]=\"true\"\n (resizeEnd)=\"onResizeEnd($event.rectangle.left, $event.rectangle.top, $event.rectangle.width??0, $event.rectangle.height??0)\">\n <div #terminal class=\"terminal-outer\">\n </div>\n <div class=\"resize-handle resize-handle-top\" mwlResizeHandle [ngClass]=\"{'handle-active' : isDraggableOnEdgeActivated}\"\n [resizeEdges]=\"{ top: false }\"></div>\n <div class=\"resize-handle resize-handle-left\" mwlResizeHandle [ngClass]=\"{'handle-active' : isDraggableOnEdgeActivated}\"\n [resizeEdges]=\"{ left: false }\"></div>\n <div class=\"resize-handle resize-handle-right\" mwlResizeHandle [ngClass]=\"{'handle-active' : isDraggableOnEdgeActivated}\"\n [resizeEdges]=\"isDraggableOnEdgeActivated ? { right: true } : { right: false }\"></div>\n <div class=\"resize-handle resize-handle-bottom\" mwlResizeHandle [ngClass]=\"{'handle-active' : isDraggableOnEdgeActivated}\"\n [resizeEdges]=\"isDraggableOnEdgeActivated ? { bottom: true } : { bottom: false }\"></div>\n</div>\n<div #detectBox style=\"width: 100%; height: 0px; position: absolute;\">\n</div>", styles: [".terminal-outer{box-sizing:border-box;height:100%;width:100%;padding:5px}:host{display:block;height:100%;width:100%}.resize-box{box-sizing:border-box;height:100%;width:100%;position:relative}div>.handle-active{z-index:100;background-color:#85858a!important}.resize-handle{background-color:#000}.resize-handle-top.handle-active,.resize-handle-bottom.handle-active{cursor:row-resize}.resize-handle-left.handle-active,.resize-handle-right.handle-active{cursor:col-resize}.resize-handle-top,.resize-handle-bottom{position:absolute;height:5px;width:100%}.resize-handle-top{top:0}.resize-handle-bottom{bottom:0}.resize-handle-left,.resize-handle-right{position:absolute;height:100%;width:5px}.resize-handle-left{left:0;top:0}.resize-handle-right{right:0;top:0}\n"] }] }], ctorParameters: function () { return [{ type: i0.Renderer2 }, { type: i0.ElementRef }, { type: LoggingService }]; }, propDecorators: { keyInputEmitter: [{ type: Output, args: ['keyInput'] }], keyEventEmitter: [{ type: Output, args: ['keyEvent'] }], dataEmitter: [{ type: Output, args: ['data'] }], keyEmitter: [{ type: Output, args: ['key'] }], _dataSource: [{ type: Input, args: ['dataSource'] }], _rowsInput: [{ type: Input, args: ['rows'] }], _colsInput: [{ type: Input, args: ['cols'] }], _minWidthInput: [{ type: Input, args: ['minWidth'] }], _minHeightInput: [{ type: Input, args: ['minHeight'] }], draggable: [{ type: Input, args: ['draggable'] }], _xtermOptions: [{ type: Input, args: ['xtermOptions'] }], terminalOuter: [{ type: ViewChild, args: ['terminal', { static: true }] }], resizeBox: [{ type: ViewChild, args: ['resizeBox', { static: true }] }], detectBox: [{ type: ViewChild, args: ['detectBox', { static: true }] }] } }); class NgTerminalModule { static forRoot(config) { return { ngModule: NgTerminalModule, providers: [ { provide: ModuleOptions, useValue: config