improve object and array previews in console

This commit is contained in:
Reimar 2025-10-17 08:45:33 +02:00
parent 494b92ac8a
commit 245d4b2ad0

View File

@ -30,107 +30,138 @@ export class PlaygroundConsole {
return typeof arg;
}
stringifyObject(obj) {
return `Object { ${Object.keys(obj).join(", ")} }`.replace(/\s\s/, " ");
}
getValueString(value) {
if (typeof value === "function") {
return `function ${value.name}()`;
}
if (Array.isArray(value) || ArrayBuffer.isView(value) && value !== Array.prototype) {
let previewElems = [];
for (let i = 0; i < value.length; i++) {
previewElems.push(this.getValueString(value[i]) + this.getValueSuffix(value[i]));
}
return `${value.constructor.name}(${value.length}) [ ${previewElems.join(", ")}`;
}
if (typeof value === "object" && value !== null) {
return this.stringifyObject(value).length < 50
? this.stringifyObject(value)
: "Object { ... }";
return `Object { ${Object.keys(value).join(", ")}`.trim();
}
return `${value}`;
}
getValueSuffix(value) {
if (Array.isArray(value) || ArrayBuffer.isView(value) && value !== Array.prototype) {
return " ]";
}
if (typeof value === "object" && value !== null) {
return " }";
}
return "";
}
addKeyValue(_entryType, parent, property, value) {
if (property) {
const keyEl = document.createElement("span");
keyEl.style.paddingRight = "0.5rem";
keyEl.className = property === "__proto__" ? "prototype" : "property";
keyEl.textContent = property + ": ";
parent.appendChild(keyEl);
}
const valueEl = document.createElement("span");
valueEl.style.display = "list-item"; // Re-add arrow that was removed by applying flex on <summary>
valueEl.style.overflowX = "hidden";
valueEl.style.textOverflow = "ellipsis";
valueEl.textContent = this.getValueString(value);
valueEl.dataset.type = this.getTypeName(value);
parent.appendChild(valueEl);
}
addEntry(entryType, property, parent, ...args) {
for (const arg of args) {
// For objects, show a collapsed list of properties
if (typeof arg === "object" && arg !== null) {
const summary = document.createElement("summary");
summary.className = entryType;
summary.dataset.type = "object";
summary.style.marginLeft = "-1rem";
this.addKeyValue(entryType, summary, property, arg);
const details = document.createElement("details");
details.className = entryType;
details.style.marginLeft = "1rem";
details.open = entryType === "dir"; // Expand if console.dir() is used
details.appendChild(summary);
if (arg instanceof Error) {
// On Chrome, the first line of the stack trace is the error message repeated
if (globalThis.chrome) {
const trace = arg.stack.split("\n");
trace.shift();
arg.stack = trace.join("\n");
}
const el = document.createElement("p");
el.innerHTML = this.formatStacktrace(arg.stack);
details.appendChild(el);
} else {
// Add object properties
for (const prop of Object.getOwnPropertyNames(arg)) {
this.addEntry(
entryType,
prop,
details,
arg.__lookupGetter__(prop) ?? arg[prop],
);
}
// Add prototype if one exists
const prototype = Object.getPrototypeOf(arg);
if (prototype && Object.getOwnPropertyNames(prototype).length > 0) {
this.addEntry(entryType, "__proto__", details, prototype);
}
}
parent.appendChild(details);
return details;
}
// For non-object values, show it directly
const wrapper = document.createElement("p");
wrapper.className = entryType;
this.addKeyValue(entryType, wrapper, property, arg);
parent.appendChild(wrapper);
return wrapper;
// Suffix is added as a separate element so the css ellipsis will be applied inside the brackets
if (this.getValueSuffix(value)) {
const suffixEl = document.createElement("span");
suffixEl.textContent = this.getValueSuffix(value);
suffixEl.style.paddingLeft = "0.5rem";
parent.appendChild(suffixEl);
}
}
addEntry(entryType, property, parent, arg) {
// For objects, show a collapsed list of properties
if (typeof arg === "object" && arg !== null) {
const summary = document.createElement("summary");
summary.style.display = "flex";
summary.style.whiteSpace = "nowrap";
summary.className = entryType;
summary.dataset.type = "object";
summary.style.marginLeft = "-1rem";
this.addKeyValue(entryType, summary, property, arg);
const details = document.createElement("details");
details.className = entryType;
details.style.marginLeft = "1rem";
details.open = entryType === "dir"; // Expand if console.dir() is used
details.appendChild(summary);
if (arg instanceof Error) {
// On Chrome, the first line of the stack trace is the error message repeated
if (globalThis.chrome) {
const trace = arg.stack.split("\n");
trace.shift();
arg.stack = trace.join("\n");
}
const el = document.createElement("p");
el.innerHTML = this.formatStacktrace(arg.stack);
details.appendChild(el);
} else {
// Add object properties
for (const prop of Object.getOwnPropertyNames(arg)) {
this.addEntry(
entryType,
prop,
details,
arg.__lookupGetter__(prop) ?? arg[prop],
);
}
// Add prototype if one exists
const prototype = Object.getPrototypeOf(arg);
if (prototype && Object.getOwnPropertyNames(prototype).length > 0) {
this.addEntry(entryType, "__proto__", details, prototype);
}
}
parent.appendChild(details);
return details;
}
// For non-object values, show it directly
const wrapper = document.createElement("p");
wrapper.style.display = "flex";
wrapper.style.whiteSpace = "nowrap";
wrapper.className = entryType;
this.addKeyValue(entryType, wrapper, property, arg);
parent.appendChild(wrapper);
return wrapper;
}
addTopLevelEntry(entryType, ...args) {
const elem = this.addEntry(entryType, "", this.elem, ...args);
for (const arg of args) {
const elem = this.addEntry(entryType, "", this.elem, ...args);
elem.addEventListener("contextmenu", (ev) => {
// TODO add context menu
elem.addEventListener("contextmenu", (ev) => {
// TODO add context menu
ev.preventDefault();
});
ev.preventDefault();
});
}
this.elem.scrollTop = this.elem.scrollHeight;
}