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.

752 lines (751 loc) 33.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. * */ import { h, Host } from "@stencil/core"; import { Database } from "../../utils/IndexedDBUtil"; import { clearCache } from "../../utils/DataCache"; export class PidComponent { constructor() { this.settings = '[]'; this.itemsPerPage = 10; this.levelOfSubcomponents = 1; this.currentLevelOfSubcomponents = 0; this.emphasizeComponent = true; this.showTopLevelCopy = true; this.defaultTTL = 24 * 60 * 60 * 1000; this.darkMode = 'light'; this.fallbackToAll = true; this.isDarkMode = false; this.items = []; this.actions = []; this.loadSubcomponents = false; this.displayStatus = 'loading'; this.tablePage = 0; this.temporarilyEmphasized = false; this.isExpanded = false; this._lineHeight = 24; this.toggleSubcomponents = (event) => { if (event) { event.stopPropagation(); this.isExpanded = event.detail; if (event.detail) { if (!this.hideSubcomponents && this.levelOfSubcomponents - this.currentLevelOfSubcomponents > 0) { this.loadSubcomponents = true; setTimeout(() => { var _a; const collapsible = (_a = this.el.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('pid-collapsible'); if (collapsible && typeof collapsible.recalculateContentDimensions === 'function') { collapsible.recalculateContentDimensions(); } }, 50); } } else { this.loadSubcomponents = false; } } }; this.handleDarkModeChange = () => { this.updateDarkMode(); }; this.blockEventPropagation = (e) => { e.stopPropagation(); }; } get shouldShowFooter() { const hasActions = this.actions.length > 0; const hasPagination = this.items.length > this.itemsPerPage; return hasActions || hasPagination; } get shouldShowCollapsedPreview() { var _a; return this.items.length === 0 && this.actions.length === 0 && !((_a = this.identifierObject) === null || _a === void 0 ? void 0 : _a.renderBody()) || this.hideSubcomponents; } componentDidLoad() { this.ensureComponentId(); setTimeout(() => { var _a; const collapsible = (_a = this.el.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('pid-collapsible'); if (collapsible && typeof collapsible.recalculateContentDimensions === 'function') { collapsible.recalculateContentDimensions(); } }, 50); } async watchValue() { this.displayStatus = 'loading'; await this.componentWillLoad(); setTimeout(() => { var _a; const collapsible = (_a = this.el.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('pid-collapsible'); if (collapsible && typeof collapsible.recalculateContentDimensions === 'function') { collapsible.recalculateContentDimensions(); } }, 10); } async watchLoadSubcomponents() { this.temporarilyEmphasized = this.emphasizeComponent || this.loadSubcomponents; this._lineHeight = 24; } watchEmphasizeComponent() { this.temporarilyEmphasized = this.emphasizeComponent || this.loadSubcomponents; } watchOpenByDefault() { this.isExpanded = this.openByDefault; } watchIsExpanded() { if (this.isExpanded) { this.el.setAttribute('expanded', ''); } else { this.el.removeAttribute('expanded'); } } onItemsChange() { const maxPage = Math.ceil(this.items.length / this.itemsPerPage) - 1; if (this.tablePage > maxPage && maxPage >= 0) { this.tablePage = maxPage; } } validateItemsPerPage(newValue) { if (newValue <= 0) { console.warn(`pid-component: itemsPerPage prop must be positive. Received ${newValue}, defaulting to 10.`); this.itemsPerPage = 10; } } watchDarkMode() { this.updateDarkMode(); if (this.identifierObject) { const currentSettings = this.identifierObject.settings || []; const darkModeIndex = currentSettings.findIndex(s => s.name === 'darkMode'); if (darkModeIndex >= 0) { currentSettings[darkModeIndex].value = this.darkMode; } else { currentSettings.push({ name: 'darkMode', value: this.darkMode }); } this.identifierObject.settings = currentSettings; } } async componentWillLoad() { var _a, _b; this.ensureComponentId(); this.validateItemsPerPage(this.itemsPerPage); this.temporarilyEmphasized = this.emphasizeComponent || this.loadSubcomponents; if (this.openByDefault) { if (!this.hideSubcomponents && this.levelOfSubcomponents - this.currentLevelOfSubcomponents > 0) { this.isExpanded = true; this.loadSubcomponents = true; } } this.initializeDarkMode(); this.items = []; this.actions = []; let settings; if (typeof this.settings === 'string' && this.settings.trim().length > 0) { try { settings = JSON.parse(this.settings); } catch (e) { console.error('Failed to parse settings.', e); settings = []; } } else { settings = []; } if (settings.length === 0) { settings.push({ type: 'default', values: [ { name: 'ttl', value: this.defaultTTL }, { name: 'darkMode', value: this.darkMode }, ], }); } else { settings.forEach(value => { if (!value.values.some(v => v.name === 'ttl')) { value.values.push({ name: 'ttl', value: this.defaultTTL }); } const darkModeIndex = value.values.findIndex(v => v.name === 'darkMode'); if (darkModeIndex >= 0) { value.values[darkModeIndex].value = this.darkMode; } else { value.values.push({ name: 'darkMode', value: this.darkMode }); } }); } let orderedRendererKeys; if (typeof this.renderers === 'string' && this.renderers.trim().length > 0) { try { orderedRendererKeys = JSON.parse(this.renderers); if (!Array.isArray(orderedRendererKeys)) { console.error('renderers prop must be a JSON array of strings, got:', this.renderers); orderedRendererKeys = undefined; } } catch (e) { console.error('Failed to parse renderers prop:', e); orderedRendererKeys = undefined; } } try { const db = new Database(); const result = await db.getEntity(this.value, settings, orderedRendererKeys, this.fallbackToAll); if (result === null) { this.displayStatus = 'unmatched'; this.identifierObject = undefined; this.items = []; this.actions = []; return; } this.identifierObject = result; } catch (e) { console.error('Failed to get entity from db', e); this.displayStatus = 'error'; this.identifierObject = undefined; this.items = []; this.actions = []; return; } if (!this.hideSubcomponents) { const uniqueItems = []; (((_a = this.identifierObject) === null || _a === void 0 ? void 0 : _a.items) || []).forEach(item => { if (!uniqueItems.some(existing => item.equals(existing))) { uniqueItems.push(item); } }); this.items = uniqueItems; this.items.sort((a, b) => { if (a.priority > b.priority) return 1; if (a.priority < b.priority) return -1; if (a.estimatedTypePriority > b.estimatedTypePriority) return 1; if (a.estimatedTypePriority < b.estimatedTypePriority) return -1; if (a.keyTitle && b.keyTitle) { return a.keyTitle.localeCompare(b.keyTitle); } return 0; }); const uniqueActions = []; (((_b = this.identifierObject) === null || _b === void 0 ? void 0 : _b.actions) || []).forEach(action => { if (!uniqueActions.some(existing => action.equals(existing))) { uniqueActions.push(action); } }); this.actions = uniqueActions; this.actions.sort((a, b) => a.priority - b.priority); } this.displayStatus = 'loaded'; await clearCache(); } disconnectedCallback() { this.identifierObject = undefined; this.items = []; this.actions = []; if (this._abortController) { this._abortController.abort(); this._abortController = undefined; } this.cleanupDarkModeListener(); } render() { if (this.displayStatus === 'unmatched') { return h(Host, { class: "relative font-sans", style: { display: 'none' } }); } if (this.shouldShowCollapsedPreview) { if (this.identifierObject !== undefined && this.displayStatus === 'loaded') { return (h(Host, { class: "relative font-sans", "aria-label": `This component displays information about the identifier ${this.value}.` }, this.renderCollapsedPreviewContent())); } return (h(Host, { class: "relative font-sans", "aria-label": `This component displays information about the identifier ${this.value}.` }, this.renderStatusMessage())); } return this.renderExpandedState(); } ensureComponentId() { if (!this.el.id) { this.el.id = `pid-component-${Math.random().toString(36).substring(2, 9)}`; } } 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.darkMode === 'dark'; } } updateDarkMode() { if (this.darkMode === 'dark') { this.isDarkMode = true; } else if (this.darkMode === 'light') { this.isDarkMode = false; } else if (this.darkMode === 'system' && this.darkModeMediaQuery) { this.isDarkMode = this.darkModeMediaQuery.matches; } } cleanupDarkModeListener() { if (this.darkModeMediaQuery) { if (this.darkModeMediaQuery.removeEventListener) { this.darkModeMediaQuery.removeEventListener('change', this.handleDarkModeChange); } else if (this.darkModeMediaQuery.removeListener) { this.darkModeMediaQuery.removeListener(this.handleDarkModeChange); } } } shouldShowCopyButtonOnTopLevel() { return this.currentLevelOfSubcomponents === 0 && this.showTopLevelCopy && (this.emphasizeComponent || this.temporarilyEmphasized || this.isExpanded); } getPreviewClasses() { if (this.currentLevelOfSubcomponents === 0) { const base = this.emphasizeComponent || this.temporarilyEmphasized ? 'group rounded-md border py-0 shadow-sm ' + (this.isDarkMode ? 'border-gray-600 bg-gray-800' : 'border-gray-300 bg-white') + ' inline-flex cursor-pointer list-none flex-nowrap items-center overflow-hidden font-mono font-bold text-clip' : (this.isDarkMode ? 'bg-gray-800/60' : '') + ' inline-flex cursor-pointer list-none flex-nowrap items-center font-mono font-bold'; return base + (!this.isExpanded ? ` h-[${this._lineHeight || 24}px] leading-[${this._lineHeight || 24}px]` : ''); } return ''; } renderCollapsedPreviewContent() { var _a; return (h("span", { class: this.getPreviewClasses(), tabIndex: 0, role: "button", "aria-label": `Identifier preview for ${this.value}`, "aria-expanded": this.isExpanded }, h("span", { class: `inline-block font-mono font-medium select-all ${this.isExpanded ? 'text-xs' : 'text-sm'} ${this.isExpanded ? 'max-w-[60vw] overflow-x-auto whitespace-nowrap' : 'max-w-full truncate'}` }, (_a = this.identifierObject) === null || _a === void 0 ? void 0 : _a.renderPreview()), this.shouldShowCopyButtonOnTopLevel() ? (h("copy-button", { value: this.identifierObject.value, class: "ml-auto shrink-0", "aria-label": `Copy value: ${this.identifierObject.value}`, onClick: this.blockEventPropagation, "dark-mode": this.darkMode })) : null)); } renderStatusMessage() { if (this.displayStatus === 'error') { return (h("span", { class: 'inline-flex items-center font-mono text-sm text-gray-600 dark:text-gray-300', role: "status" }, this.value)); } return (h("span", { class: 'inline-flex items-center font-mono text-sm text-gray-500', role: "status", "aria-live": "polite" }, this.value)); } renderExpandedState() { var _a, _b; return (h(Host, { class: "relative font-sans", "aria-label": `This component displays information about the identifier ${this.value}. It can be expanded to show more details.` }, h("pid-collapsible", { expanded: this.isExpanded, open: this.isExpanded, previewScrollable: this.isExpanded, emphasize: this.emphasizeComponent || this.temporarilyEmphasized, initialWidth: this.currentLevelOfSubcomponents > 0 ? '100%' : this.width, initialHeight: this.height, lineHeight: this._lineHeight, showFooter: this.shouldShowFooter, darkMode: this.darkMode, onCollapsibleToggle: e => this.toggleSubcomponents(e), onClick: this.blockEventPropagation, "aria-label": `Collapsible section for ${this.value}`, "aria-describedby": `${this.el.id}-description` }, h("span", { slot: "summary", class: `font-mono font-medium select-all text-sm ${this.isExpanded ? 'overflow-x-auto whitespace-nowrap' : 'max-w-full truncate'}`, "aria-label": `Preview of ${this.value}` }, (_a = this.identifierObject) === null || _a === void 0 ? void 0 : _a.renderPreview()), this.shouldShowCopyButtonOnTopLevel() ? (h("copy-button", { slot: "summary-actions", value: this.value, class: "ml-auto pl-2 shrink-0", "aria-label": `Copy value: ${this.value}`, onClick: this.blockEventPropagation, "dark-mode": this.darkMode })) : null, this.items.length > 0 ? (h("pid-data-table", { items: this.items, itemsPerPage: this.itemsPerPage, currentPage: this.tablePage, loadSubcomponents: this.loadSubcomponents, hideSubcomponents: this.hideSubcomponents, currentLevelOfSubcomponents: this.currentLevelOfSubcomponents, levelOfSubcomponents: this.levelOfSubcomponents, settings: this.settings, darkMode: this.darkMode, onPageChange: e => (this.tablePage = e.detail), class: "w-full grow overflow-x-clip overflow-y-auto", "aria-label": `Data table for ${this.value}`, "aria-describedby": `${this.el.id}-table-description` })) : null, this.items.length > 0 && (h("span", { id: `${this.el.id}-table-description`, class: "sr-only fixed" }, "This table displays properties and values associated with the identifier ", this.value, ".")), (_b = this.identifierObject) === null || _b === void 0 ? void 0 : _b.renderBody(), this.items.length > 0 && (h("div", { slot: "footer", class: `relative z-50 w-full overflow-visible ${this.isDarkMode ? 'bg-gray-800' : 'bg-white'}` }, h("pid-pagination", { currentPage: this.tablePage, totalItems: this.items.length, itemsPerPage: this.itemsPerPage, darkMode: this.darkMode, onPageChange: e => (this.tablePage = e.detail), onItemsPerPageChange: e => (this.itemsPerPage = e.detail), "aria-label": `Pagination controls for ${this.value} data`, "aria-controls": `${this.el.id}-table` }))), this.actions.length > 0 && (h("pid-actions", { slot: "footer-actions", actions: this.actions, darkMode: this.darkMode, class: "my-0 shrink-0 overflow-x-auto", "aria-label": `Available actions for ${this.value}` }))))); } static get is() { return "pid-component"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["pid-component.css"] }; } static get styleUrls() { return { "$": ["pid-component.css"] }; } static get properties() { return { "value": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "type", "text": "{string}" }], "text": "The value to parse, evaluate and render." }, "getter": false, "setter": false, "reflect": false, "attribute": "value" }, "settings": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "type", "text": "{string}" }], "text": "A stringified JSON object containing settings for this component.\nThe resulting object is passed to every subcomponent, so that every component has the same settings.\nValues and the according type are defined by the components themselves.\n(optional)\n\nSchema:\n```typescript\n{\n type: string,\n values: {\n name: string,\n value: any\n }[]\n}[]\n```" }, "getter": false, "setter": false, "reflect": false, "attribute": "settings", "defaultValue": "'[]'" }, "openByDefault": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "type", "text": "{boolean}" }], "text": "Determines whether the component is open or not by default.\n(optional)" }, "getter": false, "setter": false, "reflect": false, "attribute": "open-by-default" }, "itemsPerPage": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "type", "text": "{number}" }], "text": "The number of items to show in the table per page.\nDefaults to 10.\n(optional)" }, "getter": false, "setter": false, "reflect": false, "attribute": "items-per-page", "defaultValue": "10" }, "levelOfSubcomponents": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "type", "text": "{number}" }], "text": "The total number of levels of subcomponents to show.\nDefaults to 1.\n(optional)" }, "getter": false, "setter": false, "reflect": false, "attribute": "level-of-subcomponents", "defaultValue": "1" }, "currentLevelOfSubcomponents": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "type", "text": "{number}" }], "text": "The current level of subcomponents.\nDefaults to 0.\n(optional)" }, "getter": false, "setter": false, "reflect": false, "attribute": "current-level-of-subcomponents", "defaultValue": "0" }, "hideSubcomponents": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "type", "text": "{boolean}" }], "text": "Determines whether subcomponents should generally be shown or not.\nIf set to true, the component won't show any subcomponents.\nIf not set, the component will show subcomponents\nif the current level of subcomponents is not the total level of subcomponents or greater.\n(optional)" }, "getter": false, "setter": false, "reflect": false, "attribute": "hide-subcomponents" }, "emphasizeComponent": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "type", "text": "{boolean}" }], "text": "Determines whether components should be emphasized towards their surrounding by border and shadow.\nIf set to true, border and shadows will be shown around the component.\nIt not set, the component won't be surrounded by border and shadow.\n(optional)" }, "getter": false, "setter": false, "reflect": false, "attribute": "emphasize-component", "defaultValue": "true" }, "showTopLevelCopy": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "type", "text": "{boolean}" }], "text": "Determines whether on the top level the copy button is shown.\nIf set to true, the copy button is shown also on the top level.\nIt not set, the copy button is only shown for sub-components.\n(optional)" }, "getter": false, "setter": false, "reflect": false, "attribute": "show-top-level-copy", "defaultValue": "true" }, "defaultTTL": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "type", "text": "{number}" }, { "name": "default", "text": "24 * 60 * 60 * 1000" }], "text": "Determines the default time to live (TTL) for entries in the IndexedDB.\nDefaults to 24 hours.\nUnits are in milliseconds.\n(optional)" }, "getter": false, "setter": false, "reflect": false, "attribute": "default-t-t-l", "defaultValue": "24 * 60 * 60 * 1000" }, "width": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [{ "name": "type", "text": "{string}" }], "text": "Initial width of the component (e.g. '500px', '50%').\nIf not set, defaults to 500px on large screens, 400px on medium screens, and 300px on small screens." }, "getter": false, "setter": false, "reflect": false, "attribute": "width" }, "height": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [{ "name": "type", "text": "{string}" }], "text": "Initial height of the component (e.g. '300px', '50vh').\nIf not set, defaults to 300px." }, "getter": false, "setter": false, "reflect": false, "attribute": "height" }, "darkMode": { "type": "string", "mutable": false, "complexType": { "original": "'light' | 'dark' | 'system'", "resolved": "\"dark\" | \"light\" | \"system\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "type", "text": "{string}" }], "text": "The dark mode setting for the component\nOptions: \"light\", \"dark\", \"system\"\nDefault: \"light\" for better compatibility" }, "getter": false, "setter": false, "reflect": false, "attribute": "dark-mode", "defaultValue": "'light'" }, "renderers": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [{ "name": "type", "text": "{string}" }], "text": "An ordered list of renderer keys to try first (JSON string array).\nThese renderers are tried in the specified order as a non-binding preselection.\nIf none match, the component falls back to the full default renderer registry\n(unless fallbackToAll is explicitly set to false).\nNot forwarded to child subcomponents \u2014 their types are independent.\n(optional)\n\nExample: '[\"DOIType\", \"ORCIDType\", \"HandleType\"]'" }, "getter": false, "setter": false, "reflect": false, "attribute": "renderers" }, "fallbackToAll": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "type", "text": "{boolean}" }], "text": "When renderers is set and no listed renderer matches the value, this flag\ncontrols whether to fall back to the full default renderer registry.\nDefault: true (always falls back to try all renderers).\nSet to false to strictly restrict detection to only the listed renderers.\nNot forwarded to child subcomponents.\n(optional)" }, "getter": false, "setter": false, "reflect": false, "attribute": "fallback-to-all", "defaultValue": "true" } }; } static get states() { return { "identifierObject": {}, "isDarkMode": {}, "items": {}, "actions": {}, "loadSubcomponents": {}, "displayStatus": {}, "tablePage": {}, "temporarilyEmphasized": {}, "isExpanded": {} }; } static get elementRef() { return "el"; } static get watchers() { return [{ "propName": "value", "methodName": "watchValue" }, { "propName": "loadSubcomponents", "methodName": "watchLoadSubcomponents" }, { "propName": "emphasizeComponent", "methodName": "watchEmphasizeComponent" }, { "propName": "openByDefault", "methodName": "watchOpenByDefault" }, { "propName": "isExpanded", "methodName": "watchIsExpanded" }, { "propName": "items", "methodName": "onItemsChange" }, { "propName": "itemsPerPage", "methodName": "validateItemsPerPage" }, { "propName": "darkMode", "methodName": "watchDarkMode" }]; } } //# sourceMappingURL=pid-component.js.map