/* Copyright (c) 2023-forever Douglas Malnati. All rights reserved. See the /faq/tos page for details. (If this generated header is stamped on a file which is a 3rd party file or under a different license or copyright, then ignore this copyright statement and use that file's terms.) */ const INTL_NUMBER_FORMAT_EN_US = new Intl.NumberFormat("en-US"); export class StrAccumulator { constructor() { this.str = ``; this.indent = 0; this.sep = ""; } A(appendStr) { this.str = `${this.str}${this.sep}${" ".repeat(this.indent)}${appendStr}`; this.sep = "\n"; } IncrIndent() { this.indent += 4; } DecrIndent() { this.indent -= 4; } Get() { return this.str; } } export function Commas(num) { if (num == null) { return num; } if (typeof num == "number") { if (!Number.isFinite(num)) { return `${num}`; } let wholePart = Math.trunc(num); let wholeStr = INTL_NUMBER_FORMAT_EN_US.format(wholePart); if (Number.isInteger(num)) { return wholeStr; } let numStr = `${num}`; let dotIdx = numStr.indexOf("."); if (dotIdx == -1) { return wholeStr; } return wholeStr + numStr.substring(dotIdx); } if (typeof num == "string") { let str = num.trim(); if (str == "") { return str; } let sign = ""; if (str[0] == "-" || str[0] == "+") { sign = str[0]; str = str.substring(1); } let dotIdx = str.indexOf("."); let wholePart = dotIdx == -1 ? str : str.substring(0, dotIdx); let fracPart = dotIdx == -1 ? "" : str.substring(dotIdx); if (!/^\d+$/.test(wholePart)) { return num; } let wholeStr = INTL_NUMBER_FORMAT_EN_US.format(Number(wholePart)); return sign + wholeStr + fracPart; } return `${num}`; } // take in any css color string (hex, rgb, name, etc) // return array [r, g, b] export function GetRgbFromColor(color) { let d = document.createElement("div"); d.style.color = color; document.body.appendChild(d); // comes out as "rgb(r, g, b)" let rgbStrColor = window.getComputedStyle(d).color; document.body.removeChild(d); // extract r, g, b let rgbArr = rgbStrColor.substring(4, rgbStrColor.length-1).replace(/ /g, '').split(','); rgbArr[0] = parseInt(rgbArr[0]); rgbArr[1] = parseInt(rgbArr[1]); rgbArr[2] = parseInt(rgbArr[2]); return rgbArr; } // num1 can be higher/lower/equal to num2 // pct is a decimal value, so 50 means 50% // return a number that is pct between num1 and num2 export function PctBetween(pct, num1, num2) { if (pct < 0) { pct = 0; } if (pct > 100) { pct = 100; } let min = Math.min(num1, num2); let max = Math.max(num1, num2); let diff = max - min; if (diff < 0) { diff = -diff; } let retVal = min + ((pct / 100) * diff); return retVal; } // returns a string of "rgb(r, g, b)" export function ColorPctBetween(pct, colorStart, colorEnd, toInt = false) { let rgbArrStart = GetRgbFromColor(colorStart); let rgbArrEnd = GetRgbFromColor(colorEnd); let r = PctBetween(pct, rgbArrStart[0], rgbArrEnd[0]); let g = PctBetween(pct, rgbArrStart[1], rgbArrEnd[1]); let b = PctBetween(pct, rgbArrStart[2], rgbArrEnd[2]); if (toInt) { r = Math.round(r); g = Math.round(g); b = Math.round(b); } let retVal = `rgb(${r}, ${g}, ${b})`; return retVal; } export function Now() { return Date.now(); } export function DateYYYY() { return `${(new Date()).getFullYear()}`; } export function DateMM() { return String((new Date()).getMonth() + 1).padStart(2, '0'); } export function DateDD() { return String((new Date()).getDate()).padStart(2, '0'); } export function TimeHH() { return String((new Date()).getHours()).padStart(2, '0'); } export function TimeMM() { return String((new Date()).getMinutes()).padStart(2, '0'); } export function TimeSS() { return String((new Date()).getSeconds()).padStart(2, '0'); } export function ValOrDefaultFn(str, fn) { let retVal; if (str) { str = String(str).trim(); if (str == "") { retVal = fn(); } else { retVal = str; } } else { retVal = fn(); } return retVal; } export function ValOrDefault(str, strDefault) { let retVal; if (str) { str = String(str).trim(); if (str == "") { retVal = strDefault; } else { retVal = str; } } else { retVal = strDefault; } return retVal; } export function ParseDateTimeToIsoString(timeStr) { timeStr = timeStr.trim(); let timePartArr = timeStr.trim().split(" "); let YYYY, MM, DD; let hh, mm, ss; if (timePartArr.length >= 1) { let date = timePartArr[0]; let datePartList = date.split("-"); if (datePartList.length >= 1) { YYYY = datePartList[0]; } if (datePartList.length >= 2) { MM = datePartList[1].padStart(2, '0'); } else { MM = "01"; } if (datePartList.length >= 3) { DD = datePartList[2].padStart(2, '0'); } else { DD = "01"; } } if (timePartArr.length >= 2) { let time = timePartArr[1]; let timePartList = time.split(":"); if (timePartList.length >= 1) { hh = timePartList[0].padStart(2, '0'); } else { hh = "00"; } if (timePartList.length >= 2) { mm = timePartList[1].padStart(2, '0'); } else { mm = "00"; } if (timePartList.length >= 3) { ss = timePartList[2].padStart(2, '0'); } else { ss = "00"; } } YYYY = ValOrDefault(YYYY, DateYYYY()); MM = ValOrDefault(MM, "01"); DD = ValOrDefault(DD, "00"); hh = ValOrDefault(hh, "00"); mm = ValOrDefault(mm, "00"); ss = ValOrDefault(ss, "00"); let timeStrIso = `${YYYY}-${MM}-${DD}T${hh}:${mm}:${ss}`; return timeStrIso; } export function ConvertUtcToLocal(timeStr) { let isoStr = ParseDateTimeToIsoString(timeStr); isoStr += "Z"; let ms = (new Date(isoStr)).getTime(); let localTimeStr = MakeDateTimeFromMs(ms); return localTimeStr; } export function ConvertLocalToUtc(timeStr) { // we have a local time, what is that in UTC? // it is as different to us as we are from it // first, get the local time in ms let isoStr = ParseDateTimeToIsoString(timeStr); let msLocal = (new Date(isoStr)).getTime(); // next, get that same local time in ms, but pretend we're in UTC let isoStrUtc = isoStr + "Z"; let msUtcTmp = (new Date(isoStrUtc)).getTime(); // look at the difference let msDiff = msUtcTmp - msLocal; // calculate let msUtc = msLocal - msDiff; // convert to str let utcTimeStr = MakeDateTimeFromMs(msUtc); return utcTimeStr; } export function ParseTimeToMs(timeStr) { let timeStrIso = ParseDateTimeToIsoString(timeStr); let ms = Date.parse(timeStrIso); return ms; } export function MsDiff(time1, time2) { let msTime1 = ParseTimeToMs(time1); let msTime2 = ParseTimeToMs(time2); return msTime1 - msTime2; } export function MsUntil(timeStr) { let msNow = Now(); let msThen = ParseTimeToMs(timeStr); let msUntil = msThen - msNow; if (timeStr == "") { msUntil = 0; } return msUntil; } export function MsUntilDate(dateStr) { let msNow = ParseTimeToMs(MakeDateFromMs(Now())); let msThen = ParseTimeToMs(dateStr); let msUntil = msThen - msNow; if (dateStr == "") { msUntil = 0; } return msUntil; } export function DateTimeValid(timeStr) { return isNaN(ParseTimeToMs(timeStr)) == false; } export function MakeDateTimeFromDateTime(timeStr) { let timeStrIso = ParseDateTimeToIsoString(timeStr); // drop the T let partList = timeStrIso.split("T"); let timeStrIsoNoT = `${partList[0]} ${partList[1]}`; return timeStrIsoNoT; } export function MakeDateFrom(timeStr) { let dateTime = MakeDateTimeFromDateTime(timeStr); return dateTime.split(" ")[0]; } export function MakeDateTimeFromMs(ms) { let dt = new Date(ms); let YYYY = dt.getFullYear(); let MM = String((dt.getMonth() + 1)).padStart(2, '0') let DD = String(dt.getDate()).padStart(2, '0') let hh = String(dt.getHours()).padStart(2, '0') let mm = String(dt.getMinutes()).padStart(2, '0') let ss = String(dt.getSeconds()).padStart(2, '0') let dateTime = `${YYYY}-${MM}-${DD} ${hh}:${mm}:${ss}`; return dateTime; } export function MakeDateTimeNow() { return MakeDateTimeFromMs(Now()); } export function MakeDateFromMs(ms) { let dateTime = MakeDateTimeFromMs(ms); return dateTime.split(" ")[0]; } // only return the non-zero elements // eg 3 days, 0 hours, 5 mins = 3 days, 0 hours, 5 mins // eg 0 days, 0 hours, 5 mins = 5 mins // eg 0 days, 0 hours, 0 mins = 0 mins export function DurationStrTrim(str) { let partList = str.split(","); let partListNew = []; let keepRemaining = false; for (let i = 0; i < partList.length; ++i) { let part = partList[i].trim(); if (keepRemaining) { partListNew.push(partList[i]); } else if (part.charAt(0) != "0") { partListNew.push(partList[i]); keepRemaining = true; } else { // do nothing, drop } } if (partListNew.length == 0) { partListNew.push(partList[partList.length - 1]); } return partListNew.join(",").trim(); } export function MsToDurationStrDaysHoursMinutes(ms) { let full = MsToDurationStrDaysHoursMinutesSeconds(ms); return full.split(",").slice(0, 3).join(",").trim(); } export function MsToDurationStrMinutes(ms) { let full = MsToDurationStrDaysHoursMinutesSeconds(ms); return full.split(",").slice(2, 3).join(",").trim(); } export function MsToDurationStrDaysHoursMinutesSeconds(ms) { let val = ms; // ignore ms val = Math.floor(val / 1000); // how many days? let days = Math.floor(val / (24 * 60 * 60)); val -= days * (24 * 60 * 60); // how many hours? let hours = Math.floor(val / (60 * 60)); val -= hours * (60 * 60); // how many minutes? let mins = Math.floor(val / 60); val -= mins * 60; // how many seconds? let secs = val; let duration = ""; duration += `${days}` + (days == 1 ? " day" : " days"); duration += ", "; duration += `${hours}` + (hours == 1 ? " hour" : " hours"); duration += ", "; duration += `${mins}` + (mins == 1 ? " min" : " mins"); duration += ", "; duration += `${secs}` + (secs == 1 ? " sec" : " secs"); return duration; } export function Rotate(arr, count) { let retVal = [... arr]; if (count > 0) { while (count) { retVal.unshift(retVal.pop()); --count; } } else if (count < 0) { count = -count; while (count) { retVal.push(retVal.shift()); --count; } } return retVal; } export function ListIntersectionUnique(leftList, rightList) { let retVal = []; if (leftList.length == 0) { retVal = rightList; } else if (rightList.length == 0) { retVal = leftList; } else { let m = new Map(); for (const left of leftList) { m.set(left, 1); } for (const right of rightList) { if (m.has(right)) { m.set(right, 2); } } for (let [key, val] of m) { if (val == 2) { retVal.push(key); } } } retVal.sort(); return retVal; } export function GetSearchParam(paramName, defaultValue) { let retVal = defaultValue; const params = new URLSearchParams(window.location.search); if (params.has(paramName)) { let val = params.get(paramName); if (val != "null" && val != undefined && val != "undefined") { retVal = val.trim(); } } return retVal; } export function SetDomValBySearchParam(dom, paramName, defaultValue) { const params = new URLSearchParams(window.location.search); dom.value = GetSearchParam(paramName, defaultValue); } export function SetDomCheckedBySearchParam(dom, paramName) { const params = new URLSearchParams(window.location.search); dom.checked = params.get(paramName) != "0"; } export function StrToCssClassName(col) { return col.replace(/[.\s]/g, '_'); }; export function MakeTable(dataTable, synthesizeRowCountColumn) { // build table let table = document.createElement("table"); let rowHeader = dataTable[0]; let headerClassDataList = rowHeader.map(colVal => { let colClass = ""; let hdrClass = ""; let dataClass = ""; try { colClass = StrToCssClassName(`${colVal}_col`); hdrClass = StrToCssClassName(`${colVal}_hdr`); dataClass = StrToCssClassName(`${colVal}_data`); } catch (e) { } return { colClass, hdrClass, dataClass, }; }); let EscapeHtml = (val) => { return String(val) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); }; let MakeClassAttr = (classList) => { let filtered = classList.filter(str => str != ""); if (filtered.length == 0) { return ""; } return ` class="${filtered.join(" ")}"`; }; let SetTableCellContent = (dom, val) => { if (val instanceof Element) { dom.appendChild(val); } else if (typeof val == "string" && val.indexOf("<") != -1) { dom.innerHTML = val; } else { dom.textContent = val ?? ""; } }; // build column group which allows styling to affect the entire column if desired // https://stackoverflow.com/questions/27234480/can-i-color-table-columns-using-css-without-coloring-individual-cells let colgroup = document.createElement("colgroup"); if (synthesizeRowCountColumn) { let col = document.createElement("col"); col.classList.add(StrToCssClassName("row_col")); colgroup.appendChild(col); } for (let idx = 0; idx < rowHeader.length; ++idx) { let col = document.createElement("col"); let colClass = headerClassDataList[idx].colClass; if (colClass != "") { col.classList.add(colClass); } colgroup.appendChild(col); } table.appendChild(colgroup); // build header let thead = document.createElement("thead"); let trHeader = document.createElement("tr"); trHeader.classList.add(StrToCssClassName("headerRow")); if (synthesizeRowCountColumn) { let thRow = document.createElement("th"); thRow.innerHTML = "row"; thRow.classList.add(StrToCssClassName("row_col")); thRow.classList.add(StrToCssClassName(`row_hdr`)); trHeader.appendChild(thRow); } for (let idx = 0; idx < rowHeader.length; ++idx) { const colVal = rowHeader[idx]; let th = document.createElement("th"); th.textContent = colVal; let colClass = headerClassDataList[idx].colClass; let hdrClass = headerClassDataList[idx].hdrClass; if (colClass != "") { th.classList.add(colClass); } if (hdrClass != "") { th.classList.add(hdrClass); } trHeader.appendChild(th); } thead.appendChild(trHeader); table.appendChild(thead); // build body let tbody = document.createElement("tbody"); let useFastStringPath = true; for (let i = 1; i < dataTable.length && useFastStringPath; ++i) { for (const colVal of dataTable[i]) { if (colVal instanceof Element) { useFastStringPath = false; break; } } } if (useFastStringPath) { let html = ""; for (let i = 1; i < dataTable.length; ++i) { html += ""; if (synthesizeRowCountColumn) { html += `${i}`; } for (let idx = 0; idx < dataTable[i].length; ++idx) { let colVal = dataTable[i][idx]; let classAttr = MakeClassAttr([ headerClassDataList[idx]?.colClass || "", headerClassDataList[idx]?.dataClass || "", ]); let cellHtml = ""; if (typeof colVal == "string" && colVal.indexOf("<") != -1) { cellHtml = colVal; } else { cellHtml = EscapeHtml(colVal ?? ""); } html += `${cellHtml}`; } html += ""; } tbody.innerHTML = html; } else { let tbodyFrag = document.createDocumentFragment(); for (let i = 1; i < dataTable.length; ++i) { let tr = document.createElement("tr"); if (synthesizeRowCountColumn) { let tdRow = document.createElement("td"); tdRow.classList.add(StrToCssClassName("row_col")); tdRow.classList.add(StrToCssClassName("row_data")); tdRow.textContent = `${i}`; tr.appendChild(tdRow); } let idx = 0; for (const colVal of dataTable[i]) { let td = document.createElement("td"); let colClass = headerClassDataList[idx]?.colClass || ""; let dataClass = headerClassDataList[idx]?.dataClass || ""; if (colClass != "") { td.classList.add(colClass); } if (dataClass != "") { td.classList.add(dataClass); } ++idx; SetTableCellContent(td, colVal); tr.appendChild(td); } tbodyFrag.appendChild(tr); } tbody.appendChild(tbodyFrag); } table.appendChild(tbody); return table; } export function MakeTableTransposed(dataTable) { // build table let table = document.createElement("table"); for (let i = 0; i < dataTable[0].length; ++i) { let tr = document.createElement("tr"); let th = document.createElement("th"); let col = dataTable[0][i]; th.innerHTML = col; tr.appendChild(th); for (let j = 1; j < dataTable.length; ++j) { let val = dataTable[j][i]; let td = document.createElement("td"); td.innerHTML = val; tr.appendChild(td); } table.appendChild(tr); } return table; } export function SleepAsync(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } export function CopyElementToClipboard(id) { let dom = document.getElementById(id); let range = document.createRange(); range.selectNode(dom); window.getSelection().addRange(range); document.execCommand('copy'); let tmp = document.createElement("div"); document.body.appendChild(tmp); range.selectNode(tmp); window.getSelection().addRange(range); } export function CopyToClipboard(dom) { const type = "text/html"; const blob = new Blob([dom.outerHTML], { type }); const data = [new ClipboardItem({ [type]: blob })]; navigator.clipboard.write(data); } export function TableToCsv(table) { let retVal = ""; let trList = table.getElementsByTagName("tr"); // gather header for (let row = 0; row < 1 && row < trList.length; ++row) { let tr = trList[row]; let valList = []; let thList = tr.getElementsByTagName("th"); for (const th of thList) { valList.push(`"${th.textContent}"`); } let csvRow = valList.join(","); retVal += csvRow + "\n"; } // gather body for (let row = 1; row < trList.length; ++row) { const tr = trList[row]; let valList = []; let tdList = tr.getElementsByTagName("td"); for (const td of tdList) { valList.push(`"${td.textContent}"`); } let csvRow = valList.join(","); retVal += csvRow + "\n"; } return retVal; } export function Download(filename, format, data) { let href = format + "," + encodeURIComponent(data); let a = document.createElement('a'); a.setAttribute("href", href); a.setAttribute("download", filename); document.body.appendChild(a); // required for firefox a.click(); a.remove(); } export function DownloadCsv(filename, data) { let format = "data:text/csv;charset=utf-8"; Download(filename, format, data); } export function DownloadKml(filename, data) { let format = "data:application/vnd.google-earth.kml+xml;charset=utf-8"; Download(filename, format, data); } export function MakeLink(url, label) { return `${label}`; }; export function MakeFilename(str) { return str.replace(/ /g, "_").replace(/:/g, ""); } export function CtoF(tempC) { return (tempC * (9/5) + 32); } export function CtoF_Round(tempC) { return Math.round((tempC * (9/5) + 32)); } export function KnotsToKph(knots) { return knots * 1.852; } export function KnotsToKph_Round(knots) { return Math.round(KnotsToKph(knots)); } export function KnotsToMph(knots) { return knots * 1.15078; } export function KnotsToMph_Round(knots) { return Math.round(KnotsToMph(knots)); } export function MtoFt(meters) { return meters * 3.28084; } export function MtoFt_Round(meters) { return Math.round(MtoFt(meters)); } // https://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript/50937561 // spiced up with structuredClone // // basically, recursively copy one object into another // if member is another object, recurse into it // append elements where they don't exist // overwrite non-object members // // essentially, overlaying a copy of values from one object onto another // composition export function StructuredOverlay(obj, vals) { let setVals = function (obj, vals) { if (obj && vals) { for (let x in vals) { if (vals.hasOwnProperty(x)) { if (obj[x] && typeof vals[x] === 'object') { obj[x] = setVals(obj[x], vals[x]); } else { obj[x] = structuredClone(vals[x]); } } } } return obj; }; setVals(obj, vals); } // thanks chatgpt export async function SaveToFile(text, suggestedName) { const blob = new Blob([text], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = suggestedName; anchor.click(); URL.revokeObjectURL(url); // Clean up the object URL } // thanks chatgpt // eg acceptType ".json" or "text/plain" export async function LoadFromFile(acceptType) { return new Promise((resolve, reject) => { const input = document.createElement('input'); input.type = 'file'; if (acceptType) { input.accept = acceptType; } input.onchange = () => { const file = input.files[0]; if (!file) { reject(new Error('No file selected')); return; } const reader = new FileReader(); reader.onload = () => resolve(reader.result); // Resolve with file contents reader.onerror = () => reject(new Error('Error reading file')); reader.readAsText(file); // Read file as text }; input.click(); }); } export function GiveHotkeysVSCode(dom, onSaveCb) { const handler = new DomHotkeyHandler(dom, onSaveCb); } // thanks chatgpt for the structure but the implementation needed near rewrite class DomHotkeyHandler { constructor(dom, onSaveCb) { this.dom = dom; this.onSaveCb = onSaveCb == undefined ? () => {} : onSaveCb; this.undoStack = []; this.lastInputWasUndoable = false; this.init(); } // Initialize the event listener init() { this.dom.addEventListener("keydown", (event) => { if (event.ctrlKey && event.key.toLowerCase() === "z") { if (this.lastInputWasUndoable) { this.undo(); this.lastInputWasUndoable = true; event.preventDefault(); this.dom.dispatchEvent(new Event('input')); } } else if (event.ctrlKey && event.key === "/") { this.toggleComment(); this.lastInputWasUndoable = true; event.preventDefault(); this.dom.dispatchEvent(new Event('input')); } else if (event.shiftKey && event.altKey && event.key === "ArrowUp") { this.duplicateLineUp(); this.lastInputWasUndoable = true; event.preventDefault(); this.dom.dispatchEvent(new Event('input')); } else if (event.shiftKey && event.altKey && event.key === "ArrowDown") { this.duplicateLineDown(); this.lastInputWasUndoable = true; event.preventDefault(); this.dom.dispatchEvent(new Event('input')); } else if (event.altKey && event.key === "ArrowUp") { this.swapLineUp(); this.lastInputWasUndoable = true; event.preventDefault(); this.dom.dispatchEvent(new Event('input')); } else if (event.altKey && event.key === "ArrowDown") { this.swapLineDown(); this.lastInputWasUndoable = true; event.preventDefault(); this.dom.dispatchEvent(new Event('input')); } else if (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === "k") { this.deleteLine(); this.lastInputWasUndoable = true; event.preventDefault(); this.dom.dispatchEvent(new Event('input')); } else if (event.ctrlKey && event.key.toLowerCase() === "s") { event.preventDefault(); this.lastInputWasUndoable = true; this.dom.dispatchEvent(new Event('input')); this.onSaveCb(); } else { this.lastInputWasUndoable = false; } }); } // Save the current state to undo stack saveState() { this.undoStack.push({ value: this.dom.value, selectionStart: this.dom.selectionStart, selectionEnd: this.dom.selectionEnd, }); } // Restore the last saved state undo() { const lastState = this.undoStack.pop(); if (lastState) { this.dom.value = lastState.value; this.dom.setSelectionRange(lastState.selectionStart, lastState.selectionEnd); } } // Get current line details getLineDetails() { const value = this.dom.value; const lines = value.split("\n"); const cursorPos = this.dom.selectionStart; let charCount = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (cursorPos <= charCount + line.length) { return { lines, lineIndex: i, lineText: line, lineStart: charCount, relativeCursor: cursorPos - charCount, }; } charCount += line.length + 1; } return { lines: [], lineIndex: -1, lineText: "", lineStart: 0, relativeCursor: 0 }; } // Ctrl + / toggleComment() { this.saveState(); const { lines, lineIndex, lineText, lineStart, relativeCursor } = this.getLineDetails(); if (lineIndex === -1) return; const isCommented = lineText.trim().startsWith("//"); const cursorOffset = isCommented ? -3 : 3; if (isCommented) { lines[lineIndex] = lineText.replace(/^(\s*)\/\/\s?/, "$1"); } else { lines[lineIndex] = lineText.replace(/^(\s*)/, "$1// "); } this.dom.value = lines.join("\n"); this.dom.setSelectionRange(lineStart + relativeCursor + cursorOffset, lineStart + relativeCursor + cursorOffset); } // Shift + Alt + Up duplicateLineUp() { this.saveState(); const { lines, lineIndex, lineText, lineStart, relativeCursor } = this.getLineDetails(); lines.splice(lineIndex, 0, lineText); this.dom.value = lines.join("\n"); this.dom.setSelectionRange(lineStart + relativeCursor, lineStart + relativeCursor); } // Shift + Alt + Down duplicateLineDown() { this.saveState(); const { lines, lineIndex, lineText, lineStart, relativeCursor } = this.getLineDetails(); lines.splice(lineIndex + 1, 0, lineText); this.dom.value = lines.join("\n"); this.dom.setSelectionRange(lineStart + lineText.length + relativeCursor + 1, lineStart + lineText.length + relativeCursor + 1); } // Alt + Up swapLineUp() { this.saveState(); const { lines, lineIndex, lineText, lineStart, relativeCursor } = this.getLineDetails(); if (lineIndex > 0) { // calc new cursor index let cursorIdx = lineStart - lines[lineIndex - 1].length - 1 + relativeCursor; // swap lines [lines[lineIndex - 1], lines[lineIndex]] = [lines[lineIndex], lines[lineIndex - 1]]; this.dom.value = lines.join("\n"); this.dom.setSelectionRange(cursorIdx, cursorIdx); } } // Alt + Down swapLineDown() { this.saveState(); const { lines, lineIndex, lineText, lineStart, relativeCursor } = this.getLineDetails(); if (lineIndex < lines.length - 1) { // calc new cursor index let cursorIdx = lineStart + lines[lineIndex + 1].length + 1 + relativeCursor; // swap lines [lines[lineIndex], lines[lineIndex + 1]] = [lines[lineIndex + 1], lines[lineIndex]]; this.dom.value = lines.join("\n"); this.dom.setSelectionRange(cursorIdx, cursorIdx); } } // Ctrl + Shift + K deleteLine() { this.saveState(); const { lines, lineIndex, lineText, lineStart, relativeCursor } = this.getLineDetails(); let lineIndexNew = 0; if (lineIndex < lines.length - 1) { // you are not the last line (perhaps only line) // you will be replaced by the line that follows if there is one lineIndexNew = lineIndex + 1; } else { // you are the last line (perhaps only line) // you will be replaced by the line that precedes you if there is one lineIndexNew = lineIndex - 1; } let cursorIdx = 0; if (lines.length == 1) { // you are the only line, no replacement coming // default index here } else { // the replacement line exists and will take your place let lineTextNew = lines[lineIndexNew]; // the replacement line may be shorter than you, so at a maximum // go to the end of it to keep your place. let relativeCursorNew = Math.min(relativeCursor, lineTextNew.length); // is your replacement above or below you? if (lineIndexNew < lineIndex) { // above cursorIdx = lineStart - lineTextNew.length - 1 + relativeCursorNew; } else { // below cursorIdx = lineStart + relativeCursorNew; } } lines.splice(lineIndex, 1); this.dom.value = lines.join("\n"); this.dom.setSelectionRange(cursorIdx, cursorIdx); } }