Files
protoloon/js/Utl.js
2026-04-01 12:29:07 -06:00

1359 lines
33 KiB
JavaScript

/*
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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
};
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 += "<tr>";
if (synthesizeRowCountColumn)
{
html += `<td class="row_col row_data">${i}</td>`;
}
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 += `<td${classAttr}>${cellHtml}</td>`;
}
html += "</tr>";
}
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 `<a href="${url}" target="_blank">${label}</a>`;
};
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);
}
}
*/