UNPKG

@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.

310 lines (305 loc) 18.8 kB
/*! * * 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. * */ 'use strict'; var index = require('./index-SLWnk0w6.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}@media (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) { index.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 (index.h("div", { class: "ml-4" }, index.h("details", { class: "mb-1", open: expandedState, onToggle: toggleExpand, id: nodeId }, index.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'}` }, index.h("div", { class: "flex w-full items-center" }, index.h("span", { class: `mr-2 font-medium ${this.isDarkMode ? 'text-blue-400' : 'text-blue-600'}` }, key, ": "), index.h("span", { class: `flex items-center gap-1 ${this.isDarkMode ? 'text-gray-400' : 'text-gray-500'}` }, index.h("span", null, isArray ? '[' : '{'), index.h("span", null, itemText), index.h("span", null, isArray ? ']' : '}'), index.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')))), index.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$1) => (index.h("div", { key: `${nodeId}-${index$1}` }, 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 (index.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})` }, index.h("span", { class: `font-medium ${this.isDarkMode ? 'text-blue-400' : 'text-blue-600'}` }, key, ": "), index.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 (index.h("div", { class: "p-4 text-center text-red-500", role: "alert", "aria-live": "assertive" }, index.h("p", null, "Invalid JSON: ", this.error), index.h("slot", null))); } if (!this.parsedData) { return (index.h("div", { class: "p-4 text-center text-gray-500", role: "status", "aria-live": "polite" }, index.h("p", null, "No data provided"), index.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 (index.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` }, index.h("div", { class: `flex items-center justify-between border-b p-3 ${this.isDarkMode ? 'border-gray-600' : 'border-gray-200'}` }, index.h("div", { class: "flex items-center gap-2" }, index.h("span", { id: `${viewerId}-title`, class: `text-sm font-medium ${this.isDarkMode ? 'text-gray-200' : 'text-gray-800'}` }, "JSON Viewer"), index.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', ")")), index.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')), index.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` }, index.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" }, index.h("span", { class: "sr-only" }, this.copied ? 'Copied!' : 'Copy JSON'), this.copied ? (index.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" }, index.h("title", null, "Check mark"), index.h("polyline", { points: "20 6 9 17 4 12" }))) : (index.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" }, index.h("title", null, "Copy icon"), index.h("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }), index.h("path", { d: "M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" })))), this.currentViewMode === 'tree' && (index.h("div", { class: `p-4 pr-12 ${this.isDarkMode ? '' : 'bg-gray-50'}` }, Object.entries(this.parsedData).map(([key, value], index$1) => (index.h("div", { key: `root-${index$1}` }, this.renderTreeNode(key, value, 0, 'root')))))), this.currentViewMode === 'code' && (index.h("div", { class: `pr-12 font-mono text-sm ${this.isDarkMode ? '' : 'bg-gray-50'}` }, this.showLineNumbers ? (index.h("div", { class: "flex" }, index.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) => (index.h("div", { class: "min-h-5", key: `line-${i}` }, i + 1)))), index.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) => (index.h("div", { class: "min-h-5", key: `code-${i}`, innerHTML: this.formatCodeLine(line) })))))) : (index.h("pre", { class: "grow overflow-x-auto p-4 whitespace-pre-wrap" }, formattedJson.split('\n').map((line, i) => (index.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(); exports.json_viewer = JsonViewer; //# sourceMappingURL=json-viewer.entry.cjs.js.map //# sourceMappingURL=json-viewer.cjs.entry.js.map