@kit-data-manager/pid-component
Version:
The PID-Component is a web component that can be used to evaluate and display FAIR Digital Objects, PIDs, ORCiDs, and possibly other identifiers in a user-friendly way. It is easily extensible to support other identifier types.
308 lines (304 loc) • 18.5 kB
JavaScript
/*!
*
* Copyright 2024-2026 Karlsruhe Institute of Technology.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { r as registerInstance, h } from './index-BeCqCMz1.js';
const jsonViewerCss = () => `.sc-json-viewer-h{display:block;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;--color-text-secondary:#6b7280}[theme=dark].sc-json-viewer-h{--color-text-secondary:#9ca3af;--color-background:#1f2937;--color-text:#f9fafb}[theme=light].sc-json-viewer-h{--color-text-secondary:#6b7280;--color-background:#fff;--color-text:#111827} (prefers-color-scheme:dark){[theme=system].sc-json-viewer-h{--color-text-secondary:#9ca3af;--color-background:#1f2937;--color-text:#f9fafb}}summary.sc-json-viewer{position:relative}summary.sc-json-viewer:before{content:"►";position:absolute;left:0;font-size:.75rem;color:var(--color-text-secondary);transform:rotate(0);transition:transform .2s}details[open].sc-json-viewer>summary.sc-json-viewer:before{transform:rotate(90deg)}`;
const JsonViewer = class {
constructor(hostRef) {
registerInstance(this, hostRef);
this.viewMode = 'tree';
this.maxHeight = 500;
this.showLineNumbers = true;
this.expandAll = false;
this.theme = 'system';
this.expandedNodes = new Set();
this.parsedData = null;
this.error = null;
this.copied = false;
this.isDarkMode = false;
this.sanitizeData = (data) => {
if (Array.isArray(data)) {
return data.map(this.sanitizeData);
}
if (data !== null && typeof data === 'object') {
const cleaned = {};
Object.entries(data).forEach(([key, value]) => {
if (!key.startsWith('$')) {
cleaned[key] = this.sanitizeData(value);
}
});
return cleaned;
}
return data;
};
this.toggleView = () => {
this.currentViewMode = this.currentViewMode === 'tree' ? 'code' : 'tree';
};
this.copyToClipboard = async () => {
try {
const jsonString = JSON.stringify(this.parsedData, null, 2);
await navigator.clipboard.writeText(jsonString);
this.copied = true;
setTimeout(() => {
this.copied = false;
}, 2000);
}
catch (err) {
console.error('Failed to copy JSON to clipboard:', err);
this.createFallbackCopyMethod(JSON.stringify(this.parsedData, null, 2));
}
};
this.handleDarkModeChange = () => {
this.updateDarkMode();
};
this.renderTreeNode = (key, value, depth = 0, path = '') => {
const isExpandable = typeof value === 'object' && value !== null;
const currentPath = path ? `${path}.${key}` : key;
const nodeId = `node-${currentPath}`;
const isArray = Array.isArray(value);
const handleKeyDown = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const details = e.target.closest('details');
if (details) {
details.open = !details.open;
if (details.open) {
this.expandedNodes.add(nodeId);
}
else {
this.expandedNodes.delete(nodeId);
}
this.expandedNodes = new Set(this.expandedNodes);
}
}
};
if (isExpandable) {
const entries = isArray ? Object.entries(value) : Object.entries(value);
const itemCount = entries.length;
const itemText = `${itemCount} ${itemCount === 1 ? 'item' : 'items'}`;
const nodeType = isArray ? 'array' : 'object';
const expandedState = this.expandedNodes.has(nodeId);
const toggleExpand = (e) => {
const details = e.target.closest('details');
if (details && details.open) {
this.expandedNodes.add(nodeId);
}
else {
this.expandedNodes.delete(nodeId);
}
this.expandedNodes = new Set(this.expandedNodes);
};
return (h("div", { class: "ml-4" }, h("details", { class: "mb-1", open: expandedState, onToggle: toggleExpand, id: nodeId }, h("summary", { class: `group relative flex cursor-pointer list-none items-center rounded py-1 pl-5 font-mono ${this.isDarkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-50'} focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 focus:outline-hidden`, onKeyDown: handleKeyDown, tabIndex: 0, role: "button", "aria-expanded": expandedState ? 'true' : 'false', "aria-controls": `${nodeId}-content`, "aria-label": `${key}: ${nodeType} with ${itemText}, ${expandedState ? 'click to collapse' : 'click to expand'}` }, h("div", { class: "flex w-full items-center" }, h("span", { class: `mr-2 font-medium ${this.isDarkMode ? 'text-blue-400' : 'text-blue-600'}` }, key, ": "), h("span", { class: `flex items-center gap-1 ${this.isDarkMode ? 'text-gray-400' : 'text-gray-500'}` }, h("span", null, isArray ? '[' : '{'), h("span", null, itemText), h("span", null, isArray ? ']' : '}'), h("span", { class: `ml-2 text-xs opacity-0 transition-opacity duration-200 group-hover:opacity-100 ${this.isDarkMode ? 'text-blue-400' : 'text-blue-500'}`, "aria-hidden": "true" }, expandedState ? 'Click to collapse' : 'Click to expand')))), h("div", { id: `${nodeId}-content`, class: `ml-4 border-l-2 pl-3 ${this.isDarkMode ? 'border-gray-600' : 'border-gray-200'}`, role: "group", "aria-label": `Contents of ${key} ${nodeType}` }, entries.map(([k, v], index) => (h("div", { key: `${nodeId}-${index}` }, this.renderTreeNode(isArray ? `${k}` : k, v, depth + 1, currentPath))))))));
}
let valueClass = '';
let displayValue = JSON.stringify(value);
let valueType = typeof value;
if (valueType === 'string') {
valueClass = this.isDarkMode ? 'text-green-400' : 'text-green-600';
}
else if (valueType === 'number') {
valueClass = this.isDarkMode ? 'text-blue-400' : 'text-blue-600';
}
else if (valueType === 'boolean') {
valueClass = this.isDarkMode ? 'text-purple-400' : 'text-purple-600';
}
else if (value === null) {
valueClass = this.isDarkMode ? 'text-gray-400' : 'text-gray-500';
displayValue = 'null';
valueType = 'undefined';
}
return (h("div", { class: `flex items-center py-1 font-mono text-sm ${this.isDarkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-50'} rounded-sm`, tabIndex: 0, role: "treeitem", "aria-label": `${key}: ${displayValue} (${valueType})` }, h("span", { class: `font-medium ${this.isDarkMode ? 'text-blue-400' : 'text-blue-600'}` }, key, ": "), h("span", { class: valueClass }, displayValue)));
};
this.formatCodeLine = (line) => {
const stringClass = this.isDarkMode ? 'text-green-400' : 'text-green-600';
const booleanClass = this.isDarkMode ? 'text-purple-400' : 'text-purple-600';
const nullClass = this.isDarkMode ? 'text-gray-400' : 'text-gray-500';
const numberClass = this.isDarkMode ? 'text-blue-400' : 'text-blue-600';
return line
.replace(/(".+?")(: )?/g, `<span class="${stringClass}">$1</span>$2`)
.replace(/: (true|false)([,}\]\s])/g, `: <span class="${booleanClass}">$1</span>$2`)
.replace(/: (null)([,}\]\s])/g, `: <span class="${nullClass}">$1</span>$2`)
.replace(/: ([0-9]+(\.[0-9]+)?)([,}\]\s])/g, `: <span class="${numberClass}">$1</span>$3`);
};
}
handleDataChange() {
this.parseData();
}
handleViewModeChange() {
this.currentViewMode = this.viewMode;
}
handleExpandAllChange() {
if (this.expandAll) {
this.expandAllNodes();
}
else {
this.collapseAllNodes();
}
}
handleThemeChange() {
this.updateDarkMode();
}
componentWillLoad() {
this.currentViewMode = this.viewMode;
this.parseData();
if (this.expandAll) {
this.expandAllNodes();
}
this.initializeDarkMode();
}
disconnectedCallback() {
this.cleanupDarkModeListener();
}
async expandAllNodes() {
this.expandNodeRecursive(this.parsedData, 'root');
}
async collapseAllNodes() {
this.expandedNodes = new Set();
}
render() {
if (this.error) {
return (h("div", { class: "p-4 text-center text-red-500", role: "alert", "aria-live": "assertive" }, h("p", null, "Invalid JSON: ", this.error), h("slot", null)));
}
if (!this.parsedData) {
return (h("div", { class: "p-4 text-center text-gray-500", role: "status", "aria-live": "polite" }, h("p", null, "No data provided"), h("slot", null)));
}
const formattedJson = JSON.stringify(this.parsedData, null, 2);
const containerStyle = this.maxHeight > 0 ? { maxHeight: `${this.maxHeight}px` } : {};
const viewerId = `json-viewer-${Math.random().toString(36).substring(2, 11)}`;
const contentId = `${viewerId}-content`;
return (h("div", { class: `overflow-hidden rounded-lg border shadow-sm ${this.isDarkMode ? 'border-gray-600 bg-gray-800 text-gray-50' : 'border-gray-200 bg-white text-gray-800'}`, role: "region", "aria-labelledby": `${viewerId}-title` }, h("div", { class: `flex items-center justify-between border-b p-3 ${this.isDarkMode ? 'border-gray-600' : 'border-gray-200'}` }, h("div", { class: "flex items-center gap-2" }, h("span", { id: `${viewerId}-title`, class: `text-sm font-medium ${this.isDarkMode ? 'text-gray-200' : 'text-gray-800'}` }, "JSON Viewer"), h("span", { class: `text-xs ${this.isDarkMode ? 'text-gray-400' : 'text-gray-500'}`, "aria-live": "polite" }, "(", Object.keys(this.parsedData).length, " ", Object.keys(this.parsedData).length === 1 ? 'property' : 'properties', ")")), h("button", { onClick: this.toggleView, class: `flex cursor-pointer items-center gap-1 rounded-md px-3 py-1.5 text-xs font-medium transition-all duration-200 ${this.isDarkMode
? 'border border-gray-600 bg-gray-900 text-gray-50 hover:border-blue-600 hover:bg-gray-700'
: 'border border-gray-200 bg-gray-100 text-gray-800 hover:border-blue-400 hover:bg-gray-50'} focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 focus:outline-hidden`, "aria-controls": contentId, "aria-label": `Switch to ${this.currentViewMode === 'tree' ? 'code' : 'tree'} view`, type: "button" }, this.currentViewMode === 'tree' ? 'Code View' : 'Tree View')), h("div", { id: contentId, class: `relative overflow-auto ${this.isDarkMode ? 'bg-gray-800' : 'bg-white'}`, style: containerStyle, role: "group", "aria-label": `JSON data in ${this.currentViewMode} view` }, h("button", { onClick: this.copyToClipboard, class: `absolute top-2 right-2 z-10 rounded-md p-1 transition-all duration-200 ${this.copied
? this.isDarkMode
? 'bg-green-600 text-white'
: 'bg-green-100 text-green-800'
: this.isDarkMode
? 'bg-gray-700 text-gray-300 hover:bg-gray-600 hover:text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-800'} opacity-75 hover:opacity-100 focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 focus:outline-hidden`, title: this.copied ? 'Copied!' : 'Copy JSON to clipboard', "aria-label": this.copied ? 'JSON copied to clipboard' : 'Copy JSON to clipboard', type: "button" }, h("span", { class: "sr-only" }, this.copied ? 'Copied!' : 'Copy JSON'), this.copied ? (h("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "h-4 w-4", "aria-hidden": "true" }, h("title", null, "Check mark"), h("polyline", { points: "20 6 9 17 4 12" }))) : (h("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "h-4 w-4", "aria-hidden": "true" }, h("title", null, "Copy icon"), h("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }), h("path", { d: "M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" })))), this.currentViewMode === 'tree' && (h("div", { class: `p-4 pr-12 ${this.isDarkMode ? '' : 'bg-gray-50'}` }, Object.entries(this.parsedData).map(([key, value], index) => (h("div", { key: `root-${index}` }, this.renderTreeNode(key, value, 0, 'root')))))), this.currentViewMode === 'code' && (h("div", { class: `pr-12 font-mono text-sm ${this.isDarkMode ? '' : 'bg-gray-50'}` }, this.showLineNumbers ? (h("div", { class: "flex" }, h("div", { class: `border-r px-2 py-4 text-right select-none ${this.isDarkMode ? 'border-gray-600 bg-gray-900 text-gray-400' : 'border-gray-200 bg-gray-100 text-gray-500'}` }, formattedJson.split('\n').map((_, i) => (h("div", { class: "min-h-5", key: `line-${i}` }, i + 1)))), h("pre", { class: `grow overflow-x-auto p-4 whitespace-pre-wrap ${this.isDarkMode ? 'text-gray-200' : 'text-gray-800'}` }, formattedJson.split('\n').map((line, i) => (h("div", { class: "min-h-5", key: `code-${i}`, innerHTML: this.formatCodeLine(line) })))))) : (h("pre", { class: "grow overflow-x-auto p-4 whitespace-pre-wrap" }, formattedJson.split('\n').map((line, i) => (h("div", { class: "min-h-5", key: `code-${i}`, innerHTML: this.formatCodeLine(line) }))))))))));
}
parseData() {
try {
let raw;
if (typeof this.data === 'string') {
raw = JSON.parse(this.data);
}
else if (this.data !== null && typeof this.data === 'object') {
raw = this.data;
}
else {
throw new Error('Invalid data format');
}
this.parsedData = this.sanitizeData(raw);
this.error = null;
}
catch (err) {
this.error = err.message;
this.parsedData = null;
}
}
createFallbackCopyMethod(text) {
const textArea = document.createElement('textarea');
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
textArea.style.opacity = '0';
textArea.style.pointerEvents = 'none';
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
this.copied = true;
setTimeout(() => {
this.copied = false;
}, 2000);
}
}
catch (err) {
console.error('Fallback copy failed:', err);
}
finally {
document.body.removeChild(textArea);
}
}
cleanupDarkModeListener() {
if (this.darkModeMediaQuery) {
if (this.darkModeMediaQuery.removeEventListener) {
this.darkModeMediaQuery.removeEventListener('change', this.handleDarkModeChange);
}
else if (this.darkModeMediaQuery.removeListener) {
this.darkModeMediaQuery.removeListener(this.handleDarkModeChange);
}
}
}
expandNodeRecursive(data, path) {
if (data !== null && typeof data === 'object') {
this.expandedNodes.add(path);
Object.entries(data).forEach(([key, value]) => {
const newPath = path ? `${path}.${key}` : key;
this.expandNodeRecursive(value, newPath);
});
this.expandedNodes = new Set(this.expandedNodes);
}
}
initializeDarkMode() {
if (window.matchMedia) {
this.darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
this.updateDarkMode();
if (this.darkModeMediaQuery.addEventListener) {
this.darkModeMediaQuery.addEventListener('change', this.handleDarkModeChange);
}
else if (this.darkModeMediaQuery.addListener) {
this.darkModeMediaQuery.addListener(this.handleDarkModeChange);
}
}
else {
this.isDarkMode = this.theme === 'dark';
}
}
updateDarkMode() {
if (this.theme === 'dark') {
this.isDarkMode = true;
}
else if (this.theme === 'light') {
this.isDarkMode = false;
}
else if (this.theme === 'system' && this.darkModeMediaQuery) {
this.isDarkMode = this.darkModeMediaQuery.matches;
}
}
static get watchers() { return {
"data": [{
"handleDataChange": 0
}],
"viewMode": [{
"handleViewModeChange": 0
}],
"expandAll": [{
"handleExpandAllChange": 0
}],
"theme": [{
"handleThemeChange": 0
}]
}; }
};
JsonViewer.style = jsonViewerCss();
export { JsonViewer as json_viewer };
//# sourceMappingURL=json-viewer.entry.js.map
//# sourceMappingURL=json-viewer.entry.js.map