diff --git a/.vscode/settings.json b/.vscode/settings.json index fa03d4e..7041835 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, - "eslint.validate": ["javascript"] + "eslint.validate": ["javascript"], + "files.eol": "\r\n" } \ No newline at end of file diff --git a/examples/tcp_simulator.mjs b/examples/tcp_simulator.mjs index d5cbcfe..203048e 100644 --- a/examples/tcp_simulator.mjs +++ b/examples/tcp_simulator.mjs @@ -268,6 +268,7 @@ GUI.on("keypressed", (key) => { id: "popupTypeMax", title: "Type max value", value: max, + placeholder: "100", numeric: true }).show().on("confirm", (_max) => { max = _max diff --git a/src/components/Screen.ts b/src/components/Screen.ts index c1fc1f8..c1cfd56 100644 --- a/src/components/Screen.ts +++ b/src/components/Screen.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "events" import chalk, { BackgroundColorName, ForegroundColorName } from "chalk" -import { StyledElement, StyleObject } from "./Utils.js" +import { StyledElement, StyleObject, visibleLength } from "./Utils.js" chalk.level = 3 /** @@ -81,7 +81,7 @@ export class Screen extends EventEmitter { const arg = args[i] if (arg.text !== undefined) { const txt = arg.text.toString() - const style: StyleIndexObject = { ...arg.style, index: [row.length, row.length + txt.length] } + const style: StyleIndexObject = { ...arg.style, index: [visibleLength(row), visibleLength(row) + visibleLength(txt)] } newStyleIndex.push(style) row += txt } @@ -90,7 +90,7 @@ export class Screen extends EventEmitter { // Now recalculate the styleIndex for the current row mixing the old one with the new one // Create a new styleIndex merging the old one with the new one - const mergedStyleIndex = this.mergeStyles(newStyleIndex, currentStyleIndex, this.cursor.x, row.length) + const mergedStyleIndex = this.mergeStyles(newStyleIndex, currentStyleIndex, this.cursor.x, visibleLength(row)) this.buffer[this.cursor.y].styleIndex = mergedStyleIndex this.buffer[this.cursor.y].text = this.replaceAt(this.buffer[this.cursor.y].text, this.cursor.x, row) diff --git a/src/components/Utils.ts b/src/components/Utils.ts index 25b94c8..d7fb232 100644 --- a/src/components/Utils.ts +++ b/src/components/Utils.ts @@ -3,16 +3,18 @@ import { BackgroundColorName, ForegroundColorName } from "chalk" /** * @typedef {string} HEX - The type of the HEX color. * @example const hexColor = "#FF0000" - * + * * @typedef {string} RGB - The type of the RGB color. * @example const rgbColor = "rgb(255, 0, 0)" */ export type HEX = `#${string}`; -export type RGB = `rgb(${number}, ${number}, ${number})` | `rgb(${number},${number},${number})`; +export type RGB = + | `rgb(${number}, ${number}, ${number})` + | `rgb(${number},${number},${number})`; /** * @description The type containing all the possible styles for the text. - * + * * @typedef {Object} StyleObject * @prop {chalk.ForegroundColorName | HEX | RGB | ""} [color] - The color of the text taken from the chalk library. * @prop {chalk.BackgroundColorName | HEX | RGB | ""} [backgroundColor] - The background color of the text taken from the chalk library. @@ -24,7 +26,7 @@ export type RGB = `rgb(${number}, ${number}, ${number})` | `rgb(${number},${numb * @prop {boolean} [hidden] - If the text is hidden. * @prop {boolean} [strikethrough] - If the text is strikethrough. * @prop {boolean} [overline] - If the text is overlined. - * + * * @example const textStyle = { color: "red", backgroundColor: "blue", bold: true, italic: true } * * @export @@ -32,25 +34,25 @@ export type RGB = `rgb(${number}, ${number}, ${number})` | `rgb(${number},${numb */ // @type definition export interface StyleObject { - color?: ForegroundColorName | HEX | RGB | ""; - bg?: BackgroundColorName | HEX | RGB | ""; - italic?: boolean; - bold?: boolean; - dim?: boolean; - underline?: boolean; - inverse?: boolean; - hidden?: boolean; - strikethrough?: boolean; - overline?: boolean; + color?: ForegroundColorName | HEX | RGB | ""; + bg?: BackgroundColorName | HEX | RGB | ""; + italic?: boolean; + bold?: boolean; + dim?: boolean; + underline?: boolean; + inverse?: boolean; + hidden?: boolean; + strikethrough?: boolean; + overline?: boolean; } /** * @description The type of the single styled text, stored in a line of the PageBuilder. - * + * * @typedef {Object} StyledElement * @prop {string} text - The text of the styled text. * @prop {StyleObject} style - The style of the styled text. - * + * * @example const styledText = { text: "Hello", style: { color: "red", backgroundColor: "blue", bold: true, italic: true } } * * @export @@ -58,13 +60,13 @@ export interface StyleObject { */ // @type definition export interface StyledElement { - text: string; - style: StyleObject; + text: string; + style: StyleObject; } /** * @description The type containing all the possible styles for the text and the text on the same level. It's used on the higher level. - * + * * @typedef {Object} SimplifiedStyledElement * @prop {string} text - The text of the styled text. * @prop {chalk.ForegroundColorName | HEX | RGB | ""} [color] - The color of the text taken from the chalk library. @@ -77,7 +79,7 @@ export interface StyledElement { * @prop {boolean} [hidden] - If the text is hidden. * @prop {boolean} [strikethrough] - If the text is strikethrough. * @prop {boolean} [overline] - If the text is overlined. - * + * * @example const textStyle = { color: "red", backgroundColor: "blue", bold: true, italic: true } * * @export @@ -85,17 +87,17 @@ export interface StyledElement { */ // @type definition export interface SimplifiedStyledElement { - text: string; - color?: ForegroundColorName | HEX | RGB | ""; - bg?: BackgroundColorName | HEX | RGB | "" | ""; - italic?: boolean; - bold?: boolean; - dim?: boolean; - underline?: boolean; - inverse?: boolean; - hidden?: boolean; - strikethrough?: boolean; - overline?: boolean; + text: string; + color?: ForegroundColorName | HEX | RGB | ""; + bg?: BackgroundColorName | HEX | RGB | "" | ""; + italic?: boolean; + bold?: boolean; + dim?: boolean; + underline?: boolean; + inverse?: boolean; + hidden?: boolean; + strikethrough?: boolean; + overline?: boolean; } /** @@ -106,11 +108,11 @@ export interface SimplifiedStyledElement { */ // @type definition export interface PhisicalValues { - x: number - y: number - width: number - height: number - id?: number + x: number; + y: number; + width: number; + height: number; + id?: number; } /** @const {Object} boxChars - The characters used to draw the box. */ @@ -162,7 +164,7 @@ export const boxChars = { start: "", end: "", color: "" as ForegroundColorName | HEX | RGB | "", - } + }, } /** @@ -172,12 +174,20 @@ export const boxChars = { * @param {boolean} useWordBoundary - If true, the truncation will be done at the end of the word. * @example CM.truncate("Hello world", 5, true) // "Hello..." */ -export function truncate(str: string, n: number, useWordBoundary: boolean): string { - if (str.length <= n) { return str } +export function truncate( + str: string, + n: number, + useWordBoundary: boolean +): string { + if (visibleLength(str) <= n) { + return str + } const subString = str.substring(0, n - 1) // the original check - return (useWordBoundary ? - subString.substring(0, subString.lastIndexOf(" ")) : - subString) + "…" + return ( + (useWordBoundary + ? subString.substring(0, subString.lastIndexOf(" ")) + : subString) + "…" + ) } /** @@ -186,11 +196,13 @@ export function truncate(str: string, n: number, useWordBoundary: boolean): stri * @export * @param {StyledElement} styled * @return {*} {SimplifiedStyledElement} - * + * * @example const simplifiedStyledElement = styledToSimplifiedStyled({ text: "Hello world", style: { color: "red", backgroundColor: "blue", bold: true, italic: true } }) * // returns { text: "Hello world", color: "red", backgroundColor: "blue", bold: true, italic: true } */ -export function styledToSimplifiedStyled(styled: StyledElement): SimplifiedStyledElement { +export function styledToSimplifiedStyled( + styled: StyledElement +): SimplifiedStyledElement { return { text: styled.text, color: styled.style?.color, @@ -212,11 +224,13 @@ export function styledToSimplifiedStyled(styled: StyledElement): SimplifiedStyle * @export * @param {SimplifiedStyledElement} simplifiedStyled * @return {*} {StyledElement} - * + * * @example const styledElement = simplifiedStyledToStyled({ text: "Hello world", color: "red", bold: true }) * // returns { text: "Hello world", style: { color: "red", bold: true } } */ -export function simplifiedStyledToStyled(simplifiedStyled: SimplifiedStyledElement): StyledElement { +export function simplifiedStyledToStyled( + simplifiedStyled: SimplifiedStyledElement +): StyledElement { return { text: simplifiedStyled.text, style: { @@ -230,6 +244,26 @@ export function simplifiedStyledToStyled(simplifiedStyled: SimplifiedStyledEleme hidden: simplifiedStyled?.hidden, strikethrough: simplifiedStyled?.strikethrough, overline: simplifiedStyled?.overline, - } + }, } -} \ No newline at end of file +} + +/** + * @description Count true visible length of a string + * + * @export + * @param {string} input + * @return {number} + * + * @author Vitalik Gordon (xpl) + */ +export function visibleLength(input: string): number { + // eslint-disable-next-line no-control-regex + const regex = new RegExp( + /* eslint-disable-next-line no-control-regex */ + "[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]", + "g" + ) + // Array.from is used to correctly count emojis + return Array.from(input.replace(regex, "")).length +} diff --git a/src/components/layout/DoubleLayout.ts b/src/components/layout/DoubleLayout.ts index 990c514..477e194 100644 --- a/src/components/layout/DoubleLayout.ts +++ b/src/components/layout/DoubleLayout.ts @@ -1,6 +1,13 @@ import { ForegroundColorName } from "chalk" import { ConsoleManager, PageBuilder } from "../../ConsoleGui.js" -import { boxChars, HEX, RGB, StyledElement, truncate } from "../Utils.js" +import { + boxChars, + visibleLength, + HEX, + RGB, + StyledElement, + truncate, +} from "../Utils.js" /** * @description The type containing all the possible options for the DoubleLayout. @@ -20,23 +27,23 @@ import { boxChars, HEX, RGB, StyledElement, truncate } from "../Utils.js" */ // @type definition export interface DoubleLayoutOptions { - showTitle?: boolean; - boxed?: boolean; - boxColor?: ForegroundColorName | HEX | RGB | ""; // add color list from chalk - boxStyle?: "bold"; - changeFocusKey: string; - direction?: "horizontal" | "vertical"; - page1Title?: string; - page2Title?: string; - pageRatio?: [number, number]; + showTitle?: boolean; + boxed?: boolean; + boxColor?: ForegroundColorName | HEX | RGB | ""; // add color list from chalk + boxStyle?: "bold"; + changeFocusKey: string; + direction?: "horizontal" | "vertical"; + page1Title?: string; + page2Title?: string; + pageRatio?: [number, number]; } /** * @class DoubleLayout * @description This class is a layout that has two pages. - * + * * ![double layout](https://user-images.githubusercontent.com/14907987/170996957-cb28414b-7be2-4aa0-938b-f6d1724cfa4c.png) - * + * * @param {PageBuilder} page1 The first page. * @param {PageBuilder} page2 The second page. * @param {boolean} options Layout options. @@ -56,8 +63,13 @@ export class DoubleLayout { realWidth: number | [number, number] = 0 isOdd: boolean | undefined - public constructor(page1 : PageBuilder, page2: PageBuilder, options: DoubleLayoutOptions, selected: 0 | 1 = 0) { - /** @const {ConsoleManager} CM the instance of ConsoleManager (singleton) */ + public constructor( + page1: PageBuilder, + page2: PageBuilder, + options: DoubleLayoutOptions, + selected: 0 | 1 = 0 + ) { + /** @const {ConsoleManager} CM the instance of ConsoleManager (singleton) */ this.CM = new ConsoleManager() this.options = options @@ -76,10 +88,10 @@ export class DoubleLayout { } /** - * @description This function is used to overwrite the page content. - * @param {PageBuilder} page the page to be added - * @memberof DoubleLayout - */ + * @description This function is used to overwrite the page content. + * @param {PageBuilder} page the page to be added + * @memberof DoubleLayout + */ public setPage(page: PageBuilder, index: number): void { if (index == 0) { this.page1 = page @@ -89,37 +101,41 @@ export class DoubleLayout { } /** - * @description This function is used to overwrite the page content. - * @param {PageBuilder} page the page to be added - * @memberof DoubleLayout - */ - public setPage1(page: PageBuilder): void { this.page1 = page } + * @description This function is used to overwrite the page content. + * @param {PageBuilder} page the page to be added + * @memberof DoubleLayout + */ + public setPage1(page: PageBuilder): void { + this.page1 = page + } /** - * @description This function is used to overwrite the page content. - * @param {PageBuilder} page the page to be added - * @memberof DoubleLayout - */ - public setPage2(page: PageBuilder): void { this.page2 = page } + * @description This function is used to overwrite the page content. + * @param {PageBuilder} page the page to be added + * @memberof DoubleLayout + */ + public setPage2(page: PageBuilder): void { + this.page2 = page + } /** - * @description This function is used to set the page titles. - * @param {string[]} titles the titles of the pages - * @memberof DoubleLayout - * @example layout.setTitles(["Page 1", "Page 2"]) - */ + * @description This function is used to set the page titles. + * @param {string[]} titles the titles of the pages + * @memberof DoubleLayout + * @example layout.setTitles(["Page 1", "Page 2"]) + */ public setTitles(titles: string[]) { this.page1Title = titles[0] this.page2Title = titles[1] } /** - * @description This function is used to set the page title at the given index. - * @param {string} title the title of the page - * @param {number} index the index of the page - * @memberof DoubleLayout - * @example layout.setTitle("Page 1", 0) - */ + * @description This function is used to set the page title at the given index. + * @param {string} title the title of the page + * @param {number} index the index of the page + * @memberof DoubleLayout + * @example layout.setTitle("Page 1", 0) + */ public setTitle(title: string, index: number): void { if (index == 0) { this.page1Title = title @@ -129,33 +145,37 @@ export class DoubleLayout { } /** - * @description This function is used to enable or disable the layout border. - * @param {boolean} border enable or disable the border - * @memberof DoubleLayout - */ - public setBorder(border: boolean): void { this.options.boxed = border } + * @description This function is used to enable or disable the layout border. + * @param {boolean} border enable or disable the border + * @memberof DoubleLayout + */ + public setBorder(border: boolean): void { + this.options.boxed = border + } /** - * @description This function is used to choose the page to be highlighted. - * @param {number} selected 0 for page1, 1 for page2 - * @memberof DoubleLayout - */ - public setSelected(selected: 0 | 1): void { this.selected = selected } + * @description This function is used to choose the page to be highlighted. + * @param {number} selected 0 for page1, 1 for page2 + * @memberof DoubleLayout + */ + public setSelected(selected: 0 | 1): void { + this.selected = selected + } /** - * @description This function is used to get the selected page. - * @returns {number} 0 for page1, 1 for page2 - * @memberof DoubleLayout - */ + * @description This function is used to get the selected page. + * @returns {number} 0 for page1, 1 for page2 + * @memberof DoubleLayout + */ public getSelected(): number { return this.selected } /** - * @description This function is used to get switch the selected page. - * @returns {void} - * @memberof DoubleLayout - */ + * @description This function is used to get switch the selected page. + * @returns {void} + * @memberof DoubleLayout + */ public changeLayout(): void { if (this.selected == 0) { this.selected = 1 @@ -165,200 +185,439 @@ export class DoubleLayout { } /** - * @description This function is used to change the page ratio. - * @param {Array} ratio the ratio of pages - * @memberof QuadLayout - * @example layout.setRatio([0.4, 0.6]) - */ + * @description This function is used to change the page ratio. + * @param {Array} ratio the ratio of pages + * @memberof QuadLayout + * @example layout.setRatio([0.4, 0.6]) + */ public setRatio(ratio: [number, number]): void { this.proportions = ratio } /** - * @description This function is used to increase the page ratio by the given ratio to add. (Only works if the direction is horizontal) - * @param {number} quantity the ratio to add - * @memberof QuadLayout - * @example layout.increaseRatio(0.01) - */ + * @description This function is used to increase the page ratio by the given ratio to add. (Only works if the direction is horizontal) + * @param {number} quantity the ratio to add + * @memberof QuadLayout + * @example layout.increaseRatio(0.01) + */ public increaseRatio(quantity: number): void { if (this.options.direction == "horizontal") { if (this.proportions[0] < 0.9) { - this.proportions[0] = Number((this.proportions[0] + quantity).toFixed(2)) - this.proportions[1] = Number((this.proportions[1] - quantity).toFixed(2)) + this.proportions[0] = Number( + (this.proportions[0] + quantity).toFixed(2) + ) + this.proportions[1] = Number( + (this.proportions[1] - quantity).toFixed(2) + ) } } } /** - * @description This function is used to decrease the page ratio by the given ratio to subtract. (Only works if the direction is horizontal). - * @param {number} quantity the ratio to subtract - * @memberof QuadLayout - * @example layout.decreaseRatio(0.01) - */ + * @description This function is used to decrease the page ratio by the given ratio to subtract. (Only works if the direction is horizontal). + * @param {number} quantity the ratio to subtract + * @memberof QuadLayout + * @example layout.decreaseRatio(0.01) + */ public decreaseRatio(quantity: number): void { if (this.options.direction == "horizontal") { if (this.proportions[0] > 0.1) { - this.proportions[0] = Number((this.proportions[0] - quantity).toFixed(2)) - this.proportions[1] = Number((this.proportions[1] + quantity).toFixed(2)) + this.proportions[0] = Number( + (this.proportions[0] - quantity).toFixed(2) + ) + this.proportions[1] = Number( + (this.proportions[1] + quantity).toFixed(2) + ) } } } /** - * @description This function is used to draw a single line of the layout to the screen. It also trim the line if it is too long. - * @param {Array} line the line to be drawn - * @param {number} lineIndex the index of the selected line - * @memberof DoubleLayout - * @returns {void} - */ - private drawLine(line : Array, secondLine? : Array, index = 0): void { - const dir = !this.options.direction || this.options.direction === "vertical" ? "vertical" : "horizontal" - const bsize = this.options.boxed ? dir === "vertical" ? 2 : 3 : 0 + * @description This function is used to draw a single line of the layout to the screen. It also trim the line if it is too long. + * @param {Array} line the line to be drawn + * @param {number} lineIndex the index of the selected line + * @memberof DoubleLayout + * @returns {void} + */ + private drawLine( + line: Array, + secondLine?: Array, + index = 0 + ): void { + const dir = + !this.options.direction || this.options.direction === "vertical" + ? "vertical" + : "horizontal" + const bsize = this.options.boxed ? (dir === "vertical" ? 2 : 3) : 0 let unformattedLine = [""] - let newLine = [ - [...line] - ] + let newLine = [[...line]] if (dir === "vertical") { - line.forEach(element => { + line.forEach((element) => { unformattedLine[0] += element.text }) } else { - newLine = [ - [...line], - [...secondLine? secondLine : line] - ] + newLine = [[...line], [...(secondLine ? secondLine : line)]] unformattedLine.push("") - line.forEach((element : StyledElement) => { + line.forEach((element: StyledElement) => { unformattedLine[0] += element.text }) - secondLine?.forEach((element : StyledElement) => { + secondLine?.forEach((element: StyledElement) => { unformattedLine[1] += element.text }) } - - if (unformattedLine.filter((e, i) => e.length > (typeof this.realWidth === "number" ? this.realWidth : this.realWidth[i]) - bsize).length > 0) { + + if ( + unformattedLine.filter( + (e, i) => + visibleLength(e) > + (typeof this.realWidth === "number" + ? this.realWidth + : this.realWidth[i]) - + bsize + ).length > 0 + ) { unformattedLine = unformattedLine.map((e, i) => { - const width = typeof this.realWidth === "number" ? this.realWidth : this.realWidth[i] - if (e.length > width - bsize) { // Need to truncate + const width = + typeof this.realWidth === "number" + ? this.realWidth + : this.realWidth[i] + if (visibleLength(e) > width - bsize) { + // Need to truncate const offset = 2 if (dir === "vertical") { newLine[i] = [...JSON.parse(JSON.stringify(line))] // Shallow copy because I just want to modify the values but not the original } else { - newLine[i] = i === 0 ? JSON.parse(JSON.stringify(line)) : JSON.parse(JSON.stringify(secondLine)) + newLine[i] = + i === 0 + ? JSON.parse(JSON.stringify(line)) + : JSON.parse(JSON.stringify(secondLine)) } - let diff = e.length - width + 1 + let diff = visibleLength(e) - width + 1 // remove truncated text for (let j = newLine[i].length - 1; j >= 0; j--) { - if (newLine[i][j].text.length > diff + offset) { - newLine[i][j].text = truncate(newLine[i][j].text, (newLine[i][j].text.length - diff) - offset, false) + if (visibleLength(newLine[i][j].text) > diff + offset) { + newLine[i][j].text = truncate( + newLine[i][j].text, + visibleLength(newLine[i][j].text) - diff - offset, + false + ) break } else { - diff -= newLine[i][j].text.length + diff -= visibleLength(newLine[i][j].text) newLine[i].splice(j, 1) } } // Update unformatted line - return newLine[i].map(element => element.text).join("") + return newLine[i].map((element) => element.text).join("") } return e }) } if (dir === "vertical") { - if (this.options.boxed) newLine[0].unshift({ text: boxChars["normal"].vertical, style: { color: this.selected === index ? this.options.boxColor : "white", bold: this.boxBold } }) - if (unformattedLine[0].length <= this.CM.Screen.width - bsize) { - newLine[0].push({ text: `${" ".repeat((this.CM.Screen.width - unformattedLine[0].length) - bsize)}`, style: { color: "" } }) + if (this.options.boxed) + newLine[0].unshift({ + text: boxChars["normal"].vertical, + style: { + color: this.selected === index ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) + if (visibleLength(unformattedLine[0]) <= this.CM.Screen.width - bsize) { + newLine[0].push({ + text: `${" ".repeat( + this.CM.Screen.width - + visibleLength(unformattedLine[0]) - + bsize + )}`, + style: { }, + }) } - if (this.options.boxed) newLine[0].push({ text: boxChars["normal"].vertical, style: { color: this.selected === index ? this.options.boxColor : "white", bold: this.boxBold } }) + if (this.options.boxed) + newLine[0].push({ + text: boxChars["normal"].vertical, + style: { + color: this.selected === index ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) this.CM.Screen.write(...newLine[0]) } else { - const width = typeof this.realWidth === "number" ? [this.realWidth, 0] : [this.realWidth[0], this.realWidth[1]] + const width = + typeof this.realWidth === "number" + ? [this.realWidth, 0] + : [this.realWidth[0], this.realWidth[1]] const ret: StyledElement[] = [] - if (this.options.boxed) ret.push({ text: boxChars["normal"].vertical, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }) + if (this.options.boxed) + ret.push({ + text: boxChars["normal"].vertical, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) ret.push(...newLine[0]) - if (unformattedLine[0].length <= width[0] - bsize) { - ret.push({ text: `${" ".repeat((width[0] - unformattedLine[0].length) - (bsize > 0 ? 2 : 0))}`, style: { color: "" } }) + if (visibleLength(unformattedLine[0]) <= width[0] - bsize) { + ret.push({ + text: `${" ".repeat( + width[0] - visibleLength(unformattedLine[0]) - (bsize > 0 ? 2 : 0) + )}`, + style: { color: "" }, + }) } - if (this.options.boxed) ret.push({ text: boxChars["normal"].vertical, style: { color: this.options.boxColor, bold: this.boxBold } }) + if (this.options.boxed) + ret.push({ + text: boxChars["normal"].vertical, + style: { color: this.options.boxColor, bold: this.boxBold }, + }) ret.push(...newLine[1]) - if (unformattedLine[1].length <= width[1] - bsize) { - ret.push({ text: `${" ".repeat((width[1] - unformattedLine[1].length) - (bsize > 0 ? 1 : 0))}`, style: { color: "" } }) + if (visibleLength(unformattedLine[1]) <= width[1] - bsize) { + ret.push({ + text: `${" ".repeat( + width[1] - visibleLength(unformattedLine[1]) - (bsize > 0 ? 1 : 0) + )}`, + style: { color: "" }, + }) } - if (this.options.boxed) ret.push({ text: boxChars["normal"].vertical, style: { color: this.selected === 1 ? this.options.boxColor : "white", bold: this.boxBold } }) + if (this.options.boxed) + ret.push({ + text: boxChars["normal"].vertical, + style: { + color: this.selected === 1 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) this.CM.Screen.write(...ret) } } /** - * @description This function is used to draw the layout to the screen. - * @memberof DoubleLayout - * @returns {void} - * @example layout.draw() - */ + * @description This function is used to draw the layout to the screen. + * @memberof DoubleLayout + * @returns {void} + * @example layout.draw() + */ public draw(): void { this.isOdd = this.CM.Screen.width % 2 === 1 if (!this.options.direction || this.options.direction === "vertical") { - this.realWidth = [Math.round(this.CM.Screen.width * 1), Math.round(this.CM.Screen.width * 1)] - const trimmedTitle = [truncate(this.page1Title, this.realWidth[0] - 4, false), truncate(this.page2Title, this.realWidth[1] - 4, false)] - if (this.options.boxed) { // Draw pages with borders + this.realWidth = [ + Math.round(this.CM.Screen.width * 1), + Math.round(this.CM.Screen.width * 1), + ] + const trimmedTitle = [ + truncate(this.page1Title, this.realWidth[0] - 4, false), + truncate(this.page2Title, this.realWidth[1] - 4, false), + ] + if (this.options.boxed) { + // Draw pages with borders if (this.options.showTitle) { - this.CM.Screen.write({ text: `${boxChars["normal"].topLeft}${boxChars["normal"].horizontal}${trimmedTitle[0]}${boxChars["normal"].horizontal.repeat(this.CM.Screen.width - trimmedTitle[0].length - 3)}${boxChars["normal"].topRight}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }) + this.CM.Screen.write({ + text: `${boxChars["normal"].topLeft}${ + boxChars["normal"].horizontal + }${trimmedTitle[0]}${boxChars["normal"].horizontal.repeat( + this.CM.Screen.width - visibleLength(trimmedTitle[0]) - 3 + )}${boxChars["normal"].topRight}`, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) } else { - this.CM.Screen.write({ text: `${boxChars["normal"].topLeft}${boxChars["normal"].horizontal}${boxChars["normal"].horizontal.repeat(this.CM.Screen.width - 3)}${boxChars["normal"].topRight}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }) + this.CM.Screen.write({ + text: `${boxChars["normal"].topLeft}${ + boxChars["normal"].horizontal + }${boxChars["normal"].horizontal.repeat(this.CM.Screen.width - 3)}${ + boxChars["normal"].topRight + }`, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) } this.page1.getContent().forEach((line: StyledElement[]) => { this.drawLine(line, undefined, 0) }) if (this.options.showTitle) { - this.CM.Screen.write({ text: `${boxChars["normal"].left}${boxChars["normal"].horizontal}${trimmedTitle[1]}${boxChars["normal"].horizontal.repeat(this.CM.Screen.width - trimmedTitle[1].length - 3)}${boxChars["normal"].right}`, style: { color: this.options.boxColor, bold: this.boxBold } }) + this.CM.Screen.write({ + text: `${boxChars["normal"].left}${boxChars["normal"].horizontal}${ + trimmedTitle[1] + }${boxChars["normal"].horizontal.repeat( + this.CM.Screen.width - visibleLength(trimmedTitle[1]) - 3 + )}${boxChars["normal"].right}`, + style: { color: this.options.boxColor, bold: this.boxBold }, + }) } else { - this.CM.Screen.write({ text: `${boxChars["normal"].left}${boxChars["normal"].horizontal.repeat(this.CM.Screen.width - 2)}${boxChars["normal"].right}`, style: { color: this.options.boxColor, bold: this.boxBold } }) + this.CM.Screen.write({ + text: `${boxChars["normal"].left}${boxChars[ + "normal" + ].horizontal.repeat(this.CM.Screen.width - 2)}${ + boxChars["normal"].right + }`, + style: { color: this.options.boxColor, bold: this.boxBold }, + }) } this.page2.getContent().forEach((line: StyledElement[]) => { this.drawLine(line, undefined, 1) }) - this.CM.Screen.write({ text: `${boxChars["normal"].bottomLeft}${boxChars["normal"].horizontal.repeat(this.CM.Screen.width - 2)}${boxChars["normal"].bottomRight}`, style: { color: this.selected === 1 ? this.options.boxColor : "white", bold: this.boxBold } }) - } else { // Draw pages without borders + this.CM.Screen.write({ + text: `${boxChars["normal"].bottomLeft}${boxChars[ + "normal" + ].horizontal.repeat(this.CM.Screen.width - 2)}${ + boxChars["normal"].bottomRight + }`, + style: { + color: this.selected === 1 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) + } else { + // Draw pages without borders if (this.options.showTitle) { - this.CM.Screen.write({ text: `${trimmedTitle[0]}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }) + this.CM.Screen.write({ + text: `${trimmedTitle[0]}`, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) } this.page1.getContent().forEach((line: StyledElement[]) => { this.drawLine(line, undefined, 0) }) if (this.options.showTitle) { - this.CM.Screen.write({ text: `${trimmedTitle[1]}`, style: { color: this.selected === 1 ? this.options.boxColor : "white", bold: this.boxBold } }) + this.CM.Screen.write({ + text: `${trimmedTitle[1]}`, + style: { + color: this.selected === 1 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) } this.page2.getContent().forEach((line: StyledElement[]) => { this.drawLine(line, undefined, 1) }) } - } else { // Draw horizontally - this.realWidth = [Math.round(this.CM.Screen.width * this.proportions[0]), Math.round(this.CM.Screen.width * this.proportions[1])] - const trimmedTitle = [truncate(this.page1Title, this.realWidth[0] - 4, false), truncate(this.page2Title, this.realWidth[1] - 3, false)] - const maxPageHeight = Math.max(this.page1.getViewedPageHeight(), this.page2.getViewedPageHeight()) + } else { + // Draw horizontally + this.realWidth = [ + Math.round(this.CM.Screen.width * this.proportions[0]), + Math.round(this.CM.Screen.width * this.proportions[1]), + ] + const trimmedTitle = [ + truncate(this.page1Title, this.realWidth[0] - 4, false), + truncate(this.page2Title, this.realWidth[1] - 3, false), + ] + const maxPageHeight = Math.max( + this.page1.getViewedPageHeight(), + this.page2.getViewedPageHeight() + ) const p1 = this.page1.getContent() const p2 = this.page2.getContent() - if (this.options.boxed) { // Draw pages with borders + if (this.options.boxed) { + // Draw pages with borders if (this.options.showTitle) { - this.CM.Screen.write({ text: `${boxChars["normal"].topLeft}${boxChars["normal"].horizontal}${trimmedTitle[0]}${boxChars["normal"].horizontal.repeat(this.realWidth[0] - trimmedTitle[0].length - 3)}${boxChars["normal"].top}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }, { text: `${boxChars["normal"].horizontal}${trimmedTitle[1]}${boxChars["normal"].horizontal.repeat(this.realWidth[1] - trimmedTitle[1].length - 2)}${boxChars["normal"].topRight}`, style: { color: this.selected === 1 ? this.options.boxColor : "white", bold: this.boxBold } }) + this.CM.Screen.write( + { + text: `${boxChars["normal"].topLeft}${ + boxChars["normal"].horizontal + }${trimmedTitle[0]}${boxChars["normal"].horizontal.repeat( + this.realWidth[0] - visibleLength(trimmedTitle[0]) - 3 + )}${boxChars["normal"].top}`, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }, + { + text: `${boxChars["normal"].horizontal}${ + trimmedTitle[1] + }${boxChars["normal"].horizontal.repeat( + this.realWidth[1] - visibleLength(trimmedTitle[1]) - 2 + )}${boxChars["normal"].topRight}`, + style: { + color: this.selected === 1 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + } + ) } else { - this.CM.Screen.write({ text: `${boxChars["normal"].topLeft}${boxChars["normal"].horizontal}${boxChars["normal"].horizontal.repeat(this.realWidth[0] - 3)}${boxChars["normal"].top}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }, { text: `${boxChars["normal"].horizontal}${boxChars["normal"].horizontal.repeat(this.realWidth[1] - 2)}${boxChars["normal"].topRight}`, style: { color: this.selected === 1 ? this.options.boxColor : "white", bold: this.boxBold } }) + this.CM.Screen.write( + { + text: `${boxChars["normal"].topLeft}${ + boxChars["normal"].horizontal + }${boxChars["normal"].horizontal.repeat(this.realWidth[0] - 3)}${ + boxChars["normal"].top + }`, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }, + { + text: `${boxChars["normal"].horizontal}${boxChars[ + "normal" + ].horizontal.repeat(this.realWidth[1] - 2)}${ + boxChars["normal"].topRight + }`, + style: { + color: this.selected === 1 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + } + ) } for (let i = 0; i < maxPageHeight; i++) { - this.drawLine(p1[i] || [{ text: "", style: { color: "" } }], p2[i] || [{ text: "", style: { color: "" } }]) + this.drawLine( + p1[i] || [{ text: "", style: { color: "" } }], + p2[i] || [{ text: "", style: { color: "" } }] + ) } // Draw the bottom border - this.CM.Screen.write({ text: `${boxChars["normal"].bottomLeft}${boxChars["normal"].horizontal.repeat(this.realWidth[0] - 2)}${boxChars["normal"].bottom}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }, { text: `${boxChars["normal"].horizontal.repeat(this.realWidth[1] - 1)}${boxChars["normal"].bottomRight}`, style: { color: this.selected === 1 ? this.options.boxColor : "white", bold: this.boxBold } }) - } else { // Draw pages without borders + this.CM.Screen.write( + { + text: `${boxChars["normal"].bottomLeft}${boxChars[ + "normal" + ].horizontal.repeat(this.realWidth[0] - 2)}${ + boxChars["normal"].bottom + }`, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }, + { + text: `${boxChars["normal"].horizontal.repeat( + this.realWidth[1] - 1 + )}${boxChars["normal"].bottomRight}`, + style: { + color: this.selected === 1 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + } + ) + } else { + // Draw pages without borders if (this.options.showTitle) { - this.CM.Screen.write({ text: `${trimmedTitle[0]}${" ".repeat(this.realWidth[0] - trimmedTitle[0].length)}${trimmedTitle[1]}`, style: { color: this.selected === 0 ? this.options.boxColor : "white", bold: this.boxBold } }) + this.CM.Screen.write({ + text: `${trimmedTitle[0]}${" ".repeat( + this.realWidth[0] - visibleLength(trimmedTitle[0]) + )}${trimmedTitle[1]}`, + style: { + color: this.selected === 0 ? this.options.boxColor : "white", + bold: this.boxBold, + }, + }) } for (let i = 0; i < maxPageHeight; i++) { - this.drawLine(p1[i] || [{ text: "", style: { color: "" } }], p2[i] || [{ text: "", style: { color: "" } }]) + this.drawLine( + p1[i] || [{ text: "", style: { color: "" } }], + p2[i] || [{ text: "", style: { color: "" } }] + ) } } } } } -export default DoubleLayout \ No newline at end of file +export default DoubleLayout diff --git a/src/components/widgets/Box.ts b/src/components/widgets/Box.ts index 8ff6edd..0a35f5f 100644 --- a/src/components/widgets/Box.ts +++ b/src/components/widgets/Box.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { ForegroundColorName } from "chalk/source/vendor/ansi-styles/index.js" import InPageWidgetBuilder from "../InPageWidgetBuilder.js" -import { boxChars, HEX, PhisicalValues, RGB, StyledElement, styledToSimplifiedStyled, truncate } from "../Utils.js" +import { boxChars, HEX, PhisicalValues, RGB, StyledElement, styledToSimplifiedStyled, truncate, visibleLength } from "../Utils.js" import Control from "./Control.js" import { KeyListenerArgs } from "../../ConsoleGui.js" import { RelativeMouseEvent } from "../MouseManager.js" @@ -137,19 +137,19 @@ export class Box extends Control { unformattedLine += element.text }) - if (unformattedLine.length > this.absoluteValues.width) { + if (visibleLength(unformattedLine) > this.absoluteValues.width) { const offset = 2 newLine = [...JSON.parse(JSON.stringify(line))] // Shallow copy because I just want to modify the values but not the original - let diff = unformattedLine.length - this.absoluteValues.width + 1 + let diff = visibleLength(unformattedLine) - this.absoluteValues.width + 1 // remove truncated text for (let j = newLine.length - 1; j >= 0; j--) { - if (newLine[j].text.length > diff + offset) { - newLine[j].text = truncate(newLine[j].text, (newLine[j].text.length - diff) - offset, false) + if (visibleLength(newLine[j].text) > diff + offset) { + newLine[j].text = truncate(newLine[j].text, (visibleLength(newLine[j].text) - diff) - offset, false) break } else { - diff -= newLine[j].text.length + diff -= visibleLength(newLine[j].text) newLine.splice(j, 1) } } @@ -157,11 +157,11 @@ export class Box extends Control { unformattedLine = newLine.map((element: { text: string; }) => element.text).join("") if (this.style.boxed) { - newLine.push({ text: `${" ".repeat(this.absoluteValues.width - unformattedLine.length - 1)}${boxChars["normal"].vertical}`, style: { color: this.style.color } }) + newLine.push({ text: `${" ".repeat(this.absoluteValues.width - visibleLength(unformattedLine) - 1)}${boxChars["normal"].vertical}`, style: { color: this.style.color } }) } } - if (unformattedLine.length <= this.absoluteValues.width) { - newLine.push({ text: `${" ".repeat(this.absoluteValues.width - unformattedLine.length)}`, style: { color: "" } }) + if (visibleLength(unformattedLine) <= this.absoluteValues.width) { + newLine.push({ text: `${" ".repeat(this.absoluteValues.width - visibleLength(unformattedLine))}`, style: { color: "" } }) } this.getContent().addRow(...newLine.map((element: StyledElement) => styledToSimplifiedStyled(element))) } @@ -178,10 +178,10 @@ export class Box extends Control { const truncatedText = this.style.label ? truncate(this.style.label, absVal.width - 2, false) : "" this.getContent().clear() - this.getContent().addRow({ text: `${boxChars["normal"].topLeft}${truncatedText}${boxChars["normal"].horizontal.repeat(absVal.width - (2 + truncatedText.length))}${boxChars["normal"].topRight}`, color: this.style.color }) + this.getContent().addRow({ text: `${boxChars["normal"].topLeft}${truncatedText}${boxChars["normal"].horizontal.repeat(absVal.width - (2 + visibleLength(truncatedText)))}${boxChars["normal"].topRight}`, color: this.style.color }) for (let i = 0; i < absVal.height - 2; i++) { if (this.content.getViewedPageHeight() > i) { - const rowlength = this.content.getContent()[i].reduce((acc, curr) => acc + curr.text.length, 0) + const rowlength = this.content.getContent()[i].reduce((acc, curr) => acc + visibleLength(curr.text), 0) const spaces = absVal.width - (rowlength + 2) const styledArr = [{ text: `${boxChars["normal"].vertical}`, style: { color: this.style.color }}, ...this.content.getContent()[i], { text: `${" ".repeat(spaces > 0 ? spaces : 0)}${boxChars["normal"].vertical}`, style: { color: this.style.color } }] as StyledElement[] diff --git a/src/components/widgets/Control.ts b/src/components/widgets/Control.ts index cc238e1..01bf96f 100644 --- a/src/components/widgets/Control.ts +++ b/src/components/widgets/Control.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "events" import { ConsoleManager, KeyListenerArgs, InPageWidgetBuilder } from "../../ConsoleGui.js" import { MouseEvent, RelativeMouseEvent } from "../MouseManager.js" -import { PhisicalValues, StyledElement, truncate } from "../Utils.js" +import { PhisicalValues, StyledElement, truncate, visibleLength } from "../Utils.js" /** * @typedef {Object} ControlConfig @@ -355,27 +355,27 @@ export class Control extends EventEmitter { unformattedLine += element.text }) - if (unformattedLine.length > this.absoluteValues.width) { + if (visibleLength(unformattedLine) > this.absoluteValues.width) { const offset = 2 newLine = [...JSON.parse(JSON.stringify(line))] // Shallow copy because I just want to modify the values but not the original - let diff = unformattedLine.length - this.CM.Screen.width + 1 + let diff = visibleLength(unformattedLine) - this.CM.Screen.width + 1 // remove truncated text for (let j = newLine.length - 1; j >= 0; j--) { - if (newLine[j].text.length > diff + offset) { - newLine[j].text = truncate(newLine[j].text, (newLine[j].text.length - diff) - offset, true) + if (visibleLength(newLine[j].text) > diff + offset) { + newLine[j].text = truncate(newLine[j].text, (visibleLength(newLine[j].text) - diff) - offset, true) break } else { - diff -= newLine[j].text.length + diff -= visibleLength(newLine[j].text) newLine.splice(j, 1) } } // Update unformatted line unformattedLine = newLine.map((element: { text: string; }) => element.text).join("") } - if (unformattedLine.length <= this.absoluteValues.width) { - newLine.push({ text: `${" ".repeat(this.absoluteValues.width - unformattedLine.length)}`, style: { color: "" } }) + if (visibleLength(unformattedLine) <= this.absoluteValues.width) { + newLine.push({ text: `${" ".repeat(this.absoluteValues.width - visibleLength(unformattedLine))}`, style: { color: "" } }) } this.CM.Screen.write(...newLine) } diff --git a/src/components/widgets/CustomPopup.ts b/src/components/widgets/CustomPopup.ts index a6ccb62..d3d3474 100644 --- a/src/components/widgets/CustomPopup.ts +++ b/src/components/widgets/CustomPopup.ts @@ -2,7 +2,7 @@ import { EventEmitter } from "events" import { ConsoleManager, KeyListenerArgs, EOL } from "../../ConsoleGui.js" import { MouseEvent } from "../MouseManager.js" import PageBuilder from "../PageBuilder.js" -import { boxChars, PhisicalValues, StyledElement, truncate } from "../Utils.js" +import { boxChars, PhisicalValues, StyledElement, truncate, visibleLength } from "../Utils.js" /** * @description The configuration for the CustomPopup class. @@ -270,17 +270,17 @@ export class CustomPopup extends EventEmitter { line.forEach((element: { text: string }) => { unformattedLine += element.text }) - if (unformattedLine.length > width - 2) { // Need to truncate + if (visibleLength(unformattedLine) > width - 2) { // Need to truncate const offset = 2 newLine = JSON.parse(JSON.stringify(line)) // Shallow copy because I don't want to modify the values but not the original - let diff = unformattedLine.length - width + let diff = visibleLength(unformattedLine) - width // remove truncated text for (let i = newLine.length - 1; i >= 0; i--) { - if (newLine[i].text.length > diff + offset) { - newLine[i].text = truncate(newLine[i].text, (newLine[i].text.length - diff) - offset, true) + if (visibleLength(newLine[i].text) > diff + offset) { + newLine[i].text = truncate(newLine[i].text, (visibleLength(newLine[i].text) - diff) - offset, true) break } else { - diff -= newLine[i].text.length + diff -= visibleLength(newLine[i].text) newLine.splice(i, 1) } } @@ -291,8 +291,8 @@ export class CustomPopup extends EventEmitter { }) } newLine.unshift({ text: boxChars["normal"].vertical, style: { color: "white" } }) - if (unformattedLine.length <= width) { - newLine.push({ text: `${" ".repeat((width - unformattedLine.length))}`, style: { color: "" } }) + if (visibleLength(unformattedLine) <= width) { + newLine.push({ text: `${" ".repeat((width - visibleLength(unformattedLine)))}`, style: { color: "" } }) } newLine.push({ text: boxChars["normal"].vertical, style: { color: "white" } }) this.CM.Screen.write(...newLine) diff --git a/src/components/widgets/InputPopup.ts b/src/components/widgets/InputPopup.ts index dddd209..255a59c 100644 --- a/src/components/widgets/InputPopup.ts +++ b/src/components/widgets/InputPopup.ts @@ -1,48 +1,51 @@ import { EventEmitter } from "events" import { ConsoleManager, KeyListenerArgs, EOL } from "../../ConsoleGui.js" import { MouseEvent } from "../MouseManager.js" -import { boxChars, PhisicalValues } from "../Utils.js" +import { boxChars, PhisicalValues, visibleLength } from "../Utils.js" +import chalk from "chalk" /** * @description The configuration for the InputPopup class. * @typedef {Object} InputPopupConfig - * + * * @prop {string} id - The id of the popup. * @prop {string} title - The title of the popup. * @prop {string | number} value - The value of the popup. * @prop {boolean} numeric - If the input is numeric. * @prop {boolean} [visible] - If the popup is visible. + * @prop {string} [placeholder] - Optional placeholder to show if empty * * @export * @interface InputPopupConfig */ // @type definition export interface InputPopupConfig { - id: string, - title: string, - value: string | number, - numeric?: boolean, - visible?: boolean, + id: string; + title: string; + value: string | number; + numeric?: boolean; + visible?: boolean; + placeholder?: string; } /** * @class InputPopup * @extends EventEmitter - * @description This class is used to create a popup with a text or numeric input. - * + * @description This class is used to create a popup with a text or numeric input. + * * ![InputPopup](https://user-images.githubusercontent.com/14907987/165752281-e836b862-a54a-48d5-b4e7-954374d6509f.gif) - * - * Emits the following events: + * + * Emits the following events: * - "confirm" when the user confirm the input * - "cancel" when the user cancel the input * - "exit" when the user exit the input * @param {InputPopupConfig} config - The config of the popup. - * - * @example ```ts + * + * @example ```ts * const popup = new InputPopup({ - * id: "popup1", - * title: "Choose the number", - * value: selectedNumber, + * id: "popup1", + * title: "Choose the number", + * value: selectedNumber, * numeric: true * }).show().on("confirm", (value) => { console.log(value) }) // show the popup and wait for the user to confirm * ``` @@ -52,6 +55,11 @@ export class InputPopup extends EventEmitter { readonly id: string title: string value: string | number + // Position of the cursor. 0-indexed (0 = before all the text) + cursorPos: number + flashLoop = setInterval(() => { + this.draw(); this.CM.refresh() + }, 500) private numeric: boolean private visible: boolean private marginTop: number @@ -62,8 +70,9 @@ export class InputPopup extends EventEmitter { private offsetY: number private absoluteValues: PhisicalValues private dragging = false - private dragStart: { x: number, y: number } = { x: 0, y: 0 } + private dragStart: { x: number; y: number } = { x: 0, y: 0 } private focused = false + private placeholder?: string public constructor(config: InputPopupConfig) { if (!config) throw new Error("InputPopup config is required") @@ -77,6 +86,7 @@ export class InputPopup extends EventEmitter { this.id = id this.title = title this.value = value + this.cursorPos = 0 this.numeric = numeric || false this.visible = visible this.marginTop = 4 @@ -88,6 +98,7 @@ export class InputPopup extends EventEmitter { width: 0, height: 0, } + this.placeholder = config.placeholder if (this.CM.popupCollection[this.id]) { this.CM.unregisterPopup(this) const message = `InputPopup ${this.id} already exists.` @@ -98,12 +109,12 @@ export class InputPopup extends EventEmitter { } /** - * @description This function is used to make the ConsoleManager handle the key events when the input is numeric and it is showed. - * Inside this function are defined all the keys that can be pressed and the actions to do when they are pressed. - * @param {string} _str - The string of the input. - * @param {Object} key - The key object. - * @memberof InputPopup - */ + * @description This function is used to make the ConsoleManager handle the key events when the input is numeric and it is showed. + * Inside this function are defined all the keys that can be pressed and the actions to do when they are pressed. + * @param {string} _str - The string of the input. + * @param {Object} key - The key object. + * @memberof InputPopup + */ public keyListenerNumeric(_str: string, key: KeyListenerArgs): void { const checkResult = this.CM.mouse.isMouseFrame(key, this.parsingMouseFrame) if (checkResult === 1) { @@ -137,37 +148,43 @@ export class InputPopup extends EventEmitter { case "backspace": // If backspace is pressed I remove the last character from the typed value if (this.value.toString().length > 0) { - if (this.value.toString().indexOf(".") === this.value.toString().length - 1) { + if ( + this.value.toString().indexOf(".") === + this.value.toString().length - 1 + ) { this.value = v.toString() - } else if (this.value.toString().indexOf(".") === this.value.toString().length - 2) { - this.value = this.value.toString().slice(0, this.value.toString().length - 1) + } else if ( + this.value.toString().indexOf(".") === + this.value.toString().length - 2 + ) { + this.value = this.value + .toString() + .slice(0, this.value.toString().length - 1) + } else if ( + this.value.toString().indexOf("-") === 0 && + this.value.toString().length === 2 + ) { + this.value = 0 } else { - this.value = Number(v.toString().slice(0, v.toString().length - 1)) + this.value = Number( + v.toString().slice(0, v.toString().length - 1) + ) } } break case "return": { - this.emit("confirm", Number(this.value)) - this.CM.unregisterPopup(this) - this.hide() - //delete this + this.confirmDel() } break case "escape": { - this.emit("cancel") - this.CM.unregisterPopup(this) - this.hide() - //delete this + this.delete() } break case "q": { - this.CM.emit("exit") - this.CM.unregisterPopup(this) - this.hide() - //delete this + this.delete() } break default: @@ -178,12 +195,12 @@ export class InputPopup extends EventEmitter { } /** - * @description This function is used to make the ConsoleManager handle the key events when the input is text and it is showed. - * Inside this function are defined all the keys that can be pressed and the actions to do when they are pressed. - * @param {string} _str - The string of the input. - * @param {Object} key - The key object. - * @memberof InputPopup - */ + * @description This function is used to make the ConsoleManager handle the key events when the input is text and it is showed. + * Inside this function are defined all the keys that can be pressed and the actions to do when they are pressed. + * @param {string} _str - The string of the input. + * @param {Object} key - The key object. + * @memberof InputPopup + */ public keyListenerText(_str: string, key: KeyListenerArgs): void { const checkResult = this.CM.mouse.isMouseFrame(key, this.parsingMouseFrame) if (checkResult === 1) { @@ -194,11 +211,6 @@ export class InputPopup extends EventEmitter { return } // Continue only if the result is 0 const v = this.value - if (v.toString().length < 20) { - let tmp = v.toString() - tmp += key.sequence - this.value = tmp - } switch (key.name) { case "backspace": // If backspace is pressed I remove the last character from the typed value @@ -208,49 +220,57 @@ export class InputPopup extends EventEmitter { break case "return": { - this.emit("confirm", this.value) - this.CM.unregisterPopup(this) - this.hide() - //delete this + this.confirmDel() } break case "escape": { - this.emit("cancel") - this.CM.unregisterPopup(this) - this.hide() - //delete this + this.delete() } break case "q": { - this.CM.emit("exit") - this.CM.unregisterPopup(this) - this.hide() - //delete this + this.delete() } break + case "delete": + { + // no-op for now + } + break + case "tab": + { + // Add two spaces + this.value = v.toString() + " " + } + break + default: + if (visibleLength(v.toString()) < 20 && key.sequence.length === 1) { + let tmp = v.toString() + tmp += key.sequence + this.value = tmp + } break } this.CM.refresh() } /** - * @description This function is used to get the value of the input. - * @returns {string | number} The value of the input. - * @memberof InputPopup - */ + * @description This function is used to get the value of the input. + * @returns {string | number} The value of the input. + * @memberof InputPopup + */ public getValue(): string | number { return this.value } /** - * @description This function is used to change the value of the input. It also refresh the ConsoleManager. - * @param {string | number} newValue - The new value of the input. - * @memberof InputPopup - * @returns {InputPopup} The instance of the InputPopup. - */ + * @description This function is used to change the value of the input. It also refresh the ConsoleManager. + * @param {string | number} newValue - The new value of the input. + * @memberof InputPopup + * @returns {InputPopup} The instance of the InputPopup. + */ public setValue(newValue: string | number): this { this.value = newValue this.CM.refresh() @@ -258,10 +278,10 @@ export class InputPopup extends EventEmitter { } /** - * @description This function is used to show the popup. It also register the key events and refresh the ConsoleManager. - * @returns {InputPopup} The instance of the InputPopup. - * @memberof InputPopup - */ + * @description This function is used to show the popup. It also register the key events and refresh the ConsoleManager. + * @returns {InputPopup} The instance of the InputPopup. + * @memberof InputPopup + */ public show(): InputPopup { if (!this.visible) { this.manageInput() @@ -273,10 +293,10 @@ export class InputPopup extends EventEmitter { } /** - * @description This function is used to hide the popup. It also unregister the key events and refresh the ConsoleManager. - * @returns {InputPopup} The instance of the InputPopup. - * @memberof InputPopup - */ + * @description This function is used to hide the popup. It also unregister the key events and refresh the ConsoleManager. + * @returns {InputPopup} The instance of the InputPopup. + * @memberof InputPopup + */ public hide(): InputPopup { if (this.visible) { this.unManageInput() @@ -288,31 +308,30 @@ export class InputPopup extends EventEmitter { } /** - * @description This function is used to get the visibility of the popup. - * @returns {boolean} The visibility of the popup. - * @memberof InputPopup - */ + * @description This function is used to get the visibility of the popup. + * @returns {boolean} The visibility of the popup. + * @memberof InputPopup + */ public isVisible(): boolean { return this.visible } - /** - * @description This function is used to return the PhisicalValues of the popup (x, y, width, height). - * @memberof InputPopup - * @private - * @returns {InputPopup} The instance of the InputPopup. - * @memberof InputPopup - */ + * @description This function is used to return the PhisicalValues of the popup (x, y, width, height). + * @memberof InputPopup + * @private + * @returns {InputPopup} The instance of the InputPopup. + * @memberof InputPopup + */ public getPosition(): PhisicalValues { return this.absoluteValues } /** - * @description This function is used to add the InputPopup key listener callback to te ConsoleManager. - * @returns {InputPopup} The instance of the InputPopup. - * @memberof InputPopup - */ + * @description This function is used to add the InputPopup key listener callback to te ConsoleManager. + * @returns {InputPopup} The instance of the InputPopup. + * @memberof InputPopup + */ private manageInput(): InputPopup { // Add a command input listener to change mode if (this.numeric) { @@ -320,37 +339,46 @@ export class InputPopup extends EventEmitter { } else { this.CM.setKeyListener(this.id, this.keyListenerText.bind(this)) } - if (this.CM.mouse) this.CM.setMouseListener(`${this.id}_mouse`, this.mouseListener.bind(this)) + if (this.CM.mouse) + this.CM.setMouseListener( + `${this.id}_mouse`, + this.mouseListener.bind(this) + ) return this } /** - * @description This function is used to remove the InputPopup key listener callback to te ConsoleManager. - * @returns {InputPopup} The instance of the InputPopup. - * @memberof InputPopup - */ + * @description This function is used to remove the InputPopup key listener callback to te ConsoleManager. + * @returns {InputPopup} The instance of the InputPopup. + * @memberof InputPopup + */ private unManageInput(): InputPopup { // Add a command input listener to change mode if (this.numeric) { - this.CM.removeKeyListener(this.id/*, this.keyListenerNumeric.bind(this)*/) + this.CM.removeKeyListener( + this.id /*, this.keyListenerNumeric.bind(this)*/ + ) } else { - this.CM.removeKeyListener(this.id/*, this.keyListenerText.bind(this)*/) + this.CM.removeKeyListener(this.id /*, this.keyListenerText.bind(this)*/) } if (this.CM.mouse) this.CM.removeMouseListener(`${this.id}_mouse`) return this } /** - * @description This function is used to manage the mouse events on the OptionPopup. - * @param {MouseEvent} event - The string of the input. - * @memberof OptionPopup - */ + * @description This function is used to manage the mouse events on the OptionPopup. + * @param {MouseEvent} event - The string of the input. + * @memberof OptionPopup + */ private mouseListener = (event: MouseEvent) => { const x = event.data.x const y = event.data.y //this.CM.log(event.name) - if (x > this.absoluteValues.x && x < this.absoluteValues.x + this.absoluteValues.width && y > this.absoluteValues.y && y < this.absoluteValues.y + this.absoluteValues.height) { + if (x > this.absoluteValues.x && + x < this.absoluteValues.x + this.absoluteValues.width && + y > this.absoluteValues.y && + y < this.absoluteValues.y + this.absoluteValues.height) { // The mouse is inside the popup //this.CM.log("Mouse inside popup") if (event.name === "MOUSE_WHEEL_DOWN") { @@ -372,45 +400,65 @@ export class InputPopup extends EventEmitter { } else { this.focused = false } - if (event.name === "MOUSE_DRAG" && event.data.left === true && this.dragging === false && this.focused) { + if ( + event.name === "MOUSE_DRAG" && + event.data.left === true && + this.dragging === false && + this.focused + ) { // check if the mouse is on the header of the popup (first three lines) - if (x > this.absoluteValues.x && x < this.absoluteValues.x + this.absoluteValues.width && y > this.absoluteValues.y && y < this.absoluteValues.y + 3/* 3 = header height */) { + if (x > this.absoluteValues.x && + x < this.absoluteValues.x + this.absoluteValues.width && + y > this.absoluteValues.y && + y < this.absoluteValues.y + 3 /* 3 = header height */) { this.dragging = true this.dragStart = { x: x, y: y } } - } else if (event.name === "MOUSE_DRAG" && event.data.left === true && this.dragging === true) { - if ((y - this.dragStart.y) + this.absoluteValues.y < 0) { + } else if (event.name === "MOUSE_DRAG" && + event.data.left === true && + this.dragging === true) { + if (y - this.dragStart.y + this.absoluteValues.y < 0) { return // prevent the popup to go out of the top of the screen } - if ((x - this.dragStart.x) + this.absoluteValues.x < 0) { + if (x - this.dragStart.x + this.absoluteValues.x < 0) { return // prevent the popup to go out of the left of the screen } this.offsetX += x - this.dragStart.x this.offsetY += y - this.dragStart.y this.dragStart = { x: x, y: y } this.CM.refresh() - } else if (event.name === "MOUSE_LEFT_BUTTON_RELEASED" && this.dragging === true) { + } else if ( + event.name === "MOUSE_LEFT_BUTTON_RELEASED" && + this.dragging === true + ) { this.dragging = false this.CM.refresh() } } /** - * @description This function is used to draw the InputPopup to the screen in the middle. - * @returns {InputPopup} The instance of the InputPopup. - * @memberof InputPopup - */ + * @description This function is used to draw the InputPopup to the screen in the middle. + * @returns {InputPopup} The instance of the InputPopup. + * @memberof InputPopup + */ public draw(): InputPopup { const offset = 2 - const windowWidth = this.title.length > this.value.toString().length ? this.title.length + (2 * offset) : this.value.toString().length + (2 * offset) + 1 + const windowWidth = + this.title.length > this.value.toString().length + ? this.title.length + 2 * offset + : this.value.toString().length + 2 * offset + 1 const halfWidth = Math.round((windowWidth - this.title.length) / 2) let header = boxChars["normal"].topLeft for (let i = 0; i < windowWidth; i++) { header += boxChars["normal"].horizontal } header += `${boxChars["normal"].topRight}${EOL}` - header += `${boxChars["normal"].vertical}${" ".repeat(halfWidth)}${this.title}${" ".repeat(windowWidth - halfWidth - this.title.length)}${boxChars["normal"].vertical}${EOL}` - header += `${boxChars["normal"].left}${boxChars["normal"].horizontal.repeat(windowWidth)}${boxChars["normal"].right}${EOL}` + header += `${boxChars["normal"].vertical}${" ".repeat(halfWidth)}${this.title + }${" ".repeat(windowWidth - halfWidth - this.title.length)}${boxChars["normal"].vertical + }${EOL}` + header += `${boxChars["normal"].left}${boxChars["normal"].horizontal.repeat( + windowWidth + )}${boxChars["normal"].right}${EOL}` let footer = boxChars["normal"].bottomLeft for (let i = 0; i < windowWidth; i++) { @@ -420,13 +468,42 @@ export class InputPopup extends EventEmitter { let content = "" // Draw an input field - content += `${boxChars["normal"].vertical}${"> "}${this.value}█${" ".repeat(windowWidth - this.value.toString().length - 3)}${boxChars["normal"].vertical}${EOL}` + // if (this.value.toString().length === 0 && this.placeholder?.length) + // content += `${boxChars["normal"].vertical}${"> "}${chalk.gray( + // this.placeholder + // )}${" ".repeat(windowWidth - this.placeholder.length - 2)}${boxChars["normal"].vertical + // }${EOL}` + // else + content += `${boxChars["normal"].vertical}${"> "}${this.value + }█${" ".repeat(windowWidth - this.value.toString().length - 3)}${boxChars["normal"].vertical + }${EOL}` const windowDesign = `${header}${content}${footer}` const windowDesignLines = windowDesign.split(EOL) - const centerScreen = Math.round((this.CM.Screen.width / 2) - (windowWidth / 2)) + const centerScreen = Math.round(this.CM.Screen.width / 2 - windowWidth / 2) windowDesign.split(EOL).forEach((line, index) => { - this.CM.Screen.cursorTo(centerScreen + this.offsetX, this.marginTop + index + this.offsetY) + this.CM.Screen.cursorTo( + centerScreen + this.offsetX, + this.marginTop + index + this.offsetY + ) + + if (index === 3 && this.placeholder?.length && this.value.toString().length === 0) { + const isOddSecond = Math.round(Date.now() / 100) % 2 + return this.CM.Screen.write({ + text: `${boxChars["normal"].vertical}${"> "}${isOddSecond ? "█" : " "}${chalk.gray( + this.placeholder + )}${" ".repeat(windowWidth - this.placeholder.length - 3)}${boxChars["normal"].vertical + }${EOL}`, style: { color: "white" } + }) + } else if (index === 3) { + const isOddSecond = Math.round(Date.now() / 100) % 2 + // write value and then the cursor (█) + return this.CM.Screen.write({ + text: `${boxChars["normal"].vertical}${"> "}${this.value + }${isOddSecond ? "█" : " "}${" ".repeat(windowWidth - this.value.toString().length - 3)}${boxChars["normal"].vertical + }${EOL}`, style: { color: "white" } + }) + } this.CM.Screen.write({ text: line, style: { color: "white" } }) }) this.absoluteValues = { @@ -437,6 +514,17 @@ export class InputPopup extends EventEmitter { } return this } + + confirmDel() { + this.emit("confirm", Number(this.value)) + this.delete() + } + + delete() { + this.CM.unregisterPopup(this) + this.hide() + clearInterval(this.flashLoop) + } } -export default InputPopup \ No newline at end of file +export default InputPopup