@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.
645 lines (644 loc) • 30.2 kB
JavaScript
/*!
*
* Copyright 2024 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.amountOfItems = 10;
this.levelOfSubcomponents = 1;
this.currentLevelOfSubcomponents = 0;
this.emphasizeComponent = true;
this.showTopLevelCopy = true;
this.defaultTTL = 24 * 60 * 60 * 1000;
this.darkMode = 'system';
this.isDarkMode = false;
this.items = [];
this.actions = [];
this.loadSubcomponents = false;
this.displayStatus = 'loading';
this.tablePage = 0;
this.temporarilyEmphasized = false;
this.isExpanded = false;
this.toggleSubcomponents = (event) => {
if (event) {
event.stopPropagation();
this.isExpanded = event.detail;
if (event.detail && !this.hideSubcomponents && this.levelOfSubcomponents - this.currentLevelOfSubcomponents > 0) {
this.loadSubcomponents = true;
setTimeout(() => {
const collapsible = this.el.querySelector('pid-collapsible');
if (collapsible && typeof collapsible.recalculateContentDimensions === 'function') {
collapsible.recalculateContentDimensions();
}
}, 50);
}
}
};
this._lineHeight = 24;
this.handleDarkModeChange = () => {
this.updateDarkMode();
};
this.temporarilyEmphasized = this.emphasizeComponent;
}
componentDidLoad() {
this.ensureComponentId();
setTimeout(() => {
const collapsible = this.el.querySelector('pid-collapsible');
if (collapsible && typeof collapsible.recalculateContentDimensions === 'function') {
collapsible.recalculateContentDimensions();
}
}, 50);
}
ensureComponentId() {
if (!this.el.id) {
this.el.id = `pid-component-${Math.random().toString(36).substring(2, 9)}`;
}
}
async watchValue() {
this.displayStatus = 'loading';
await this.componentWillLoad();
setTimeout(() => {
const collapsible = this.el.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;
}
onItemsChange() {
const maxPage = Math.ceil(this.items.length / this.amountOfItems) - 1;
if (this.tablePage > maxPage && maxPage >= 0) {
this.tablePage = maxPage;
}
}
validateAmountOfItems(newValue) {
if (newValue <= 0) {
console.warn(`pid-component: amountOfItems prop must be positive. Received ${newValue}, defaulting to 10.`);
this.amountOfItems = 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.validateAmountOfItems(this.amountOfItems);
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 });
}
});
}
try {
const db = new Database();
this.identifierObject = await db.getEntity(this.value, settings);
}
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();
}
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);
}
}
}
get shouldShowFooter() {
const hasActions = this.actions.length > 0;
const hasPagination = this.items.length > this.amountOfItems;
return hasActions || hasPagination;
}
render() {
var _a, _b, _c, _d;
if (this.openByDefault) {
if (!this.hideSubcomponents && this.levelOfSubcomponents - this.currentLevelOfSubcomponents > 0) {
this.isExpanded = this.openByDefault;
this.loadSubcomponents = true;
setTimeout(() => {
const collapsible = this.el.querySelector('pid-collapsible');
if (collapsible && typeof collapsible.recalculateContentDimensions === 'function') {
collapsible.recalculateContentDimensions();
}
console.log(`Loaded subcomponents and recalculated dimensions. expanded: ${this.isExpanded}, loadSubcomponents: ${this.loadSubcomponents}, currentLevel: ${this.currentLevelOfSubcomponents}, totalLevels: ${this.levelOfSubcomponents}`);
}, 50);
}
}
return (h(Host, { key: 'd7141b3358c75470cf12a549f41a329019a4a26c', class: `relative font-sans` }, h("span", { key: '52e273e91081ec614176df2cbb03074a11869e27', id: `${this.el.id}-description`, class: "sr-only" }, "This component displays information about the identifier ", this.value, ". It can be expanded to show more details."), (this.items.length === 0 && this.actions.length === 0 && !((_a = this.identifierObject) === null || _a === void 0 ? void 0 : _a.renderBody())) || this.hideSubcomponents ? (this.identifierObject !== undefined && this.displayStatus === 'loaded' ? (h("span", { class: this.currentLevelOfSubcomponents === 0
?
'group rounded-md border px-2 py-0 shadow' +
(this.emphasizeComponent || this.temporarilyEmphasized
? this.isDarkMode
? 'border-gray-600 bg-gray-800'
: 'border-gray-300 bg-white'
: this.isDarkMode
? 'bg-gray-800/60'
: 'bg-white/60') +
' inline-flex w-full cursor-pointer list-none flex-nowrap items-center overflow-hidden font-mono font-bold text-clip transition-all duration-200 ease-in-out open:w-full open:align-top' +
(!this.isExpanded ? ` h-[${this._lineHeight || 24}px] leading-[${this._lineHeight || 24}px]` : '')
: '', tabIndex: 0, role: "button", "aria-label": `Identifier preview for ${this.value}`, "aria-expanded": this.isExpanded }, h("span", { class: `inline-flex max-w-full flex-nowrap overflow-x-auto font-mono font-medium text-ellipsis whitespace-nowrap select-all ${this.isExpanded ? 'text-xs' : 'text-sm'}` }, (_b = this.identifierObject) === null || _b === void 0 ? void 0 : _b.renderPreview()), this.currentLevelOfSubcomponents === 0 && this.showTopLevelCopy ? (h("copy-button", { value: this.identifierObject.value, class: "ml-2 flex-shrink-0", "aria-label": `Copy value: ${this.identifierObject.value}` })) : (''))) : this.displayStatus === 'error' ? (h("span", { class: 'inline-flex items-center font-medium text-red-600', role: "alert", "aria-live": "assertive" }, h("svg", { class: "mr-2 h-5 w-5", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true" }, h("path", { "fill-rule": "evenodd", d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15v-2h2v2h-2zm0-10v6h2V7h-2z", "clip-rule": "evenodd" })), "Error loading data for: ", this.value)) : (h("span", { class: 'inline-flex items-center transition ease-in-out', role: "status", "aria-live": "polite" }, h("svg", { class: "mr-3 ml-1 h-5 w-5 animate-spin text-black", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", "aria-hidden": "true" }, h("circle", { class: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", "stroke-width": "4" }), h("path", { class: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })), h("span", null, "Loading... ", this.value)))) : (h("pid-collapsible", { open: this.isExpanded, emphasize: this.emphasizeComponent || this.temporarilyEmphasized, initialWidth: this.width, initialHeight: this.height, lineHeight: this._lineHeight, showFooter: this.shouldShowFooter, darkMode: this.darkMode, onCollapsibleToggle: e => this.toggleSubcomponents(e), onClick: e => {
e.stopPropagation();
e.stopImmediatePropagation();
}, "aria-label": `Collapsible section for ${this.value}`, "aria-describedby": `${this.el.id}-description` }, h("span", { slot: "summary", class: `inline-flex items-center overflow-x-auto font-mono text-sm font-medium select-all ${this.isExpanded ? 'flex-wrap overflow-visible break-words' : 'flex-nowrap whitespace-nowrap'}`, "aria-label": `Preview of ${this.value}` }, (_c = this.identifierObject) === null || _c === void 0 ? void 0 : _c.renderPreview()), this.currentLevelOfSubcomponents === 0 && this.showTopLevelCopy && (this.emphasizeComponent || this.temporarilyEmphasized) ? (h("copy-button", { slot: "summary-actions", value: this.value, "aria-label": `Copy value: ${this.value}` })) : null, this.items.length > 0 ? (h("pid-data-table", { items: this.items, itemsPerPage: this.amountOfItems, 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 flex-grow overflow-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" }, "This table displays properties and values associated with the identifier ", this.value, ".")), (_d = this.identifierObject) === null || _d === void 0 ? void 0 :
_d.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.amountOfItems, darkMode: this.darkMode, onPageChange: e => (this.tablePage = e.detail), onItemsPerPageChange: e => (this.amountOfItems = 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: "mt-0 flex-shrink-0", "aria-label": `Available actions for ${this.value}` }))))));
}
static get is() { return "pid-component"; }
static get originalStyleUrls() {
return {
"$": ["pid-component.css"]
};
}
static get styleUrls() {
return {
"$": ["pid-component.css"]
};
}
static get properties() {
return {
"value": {
"type": "string",
"attribute": "value",
"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
},
"settings": {
"type": "string",
"attribute": "settings",
"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,
"defaultValue": "'[]'"
},
"openByDefault": {
"type": "boolean",
"attribute": "open-by-default",
"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
},
"amountOfItems": {
"type": "number",
"attribute": "amount-of-items",
"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,
"defaultValue": "10"
},
"levelOfSubcomponents": {
"type": "number",
"attribute": "level-of-subcomponents",
"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,
"defaultValue": "1"
},
"currentLevelOfSubcomponents": {
"type": "number",
"attribute": "current-level-of-subcomponents",
"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,
"defaultValue": "0"
},
"hideSubcomponents": {
"type": "boolean",
"attribute": "hide-subcomponents",
"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
},
"emphasizeComponent": {
"type": "boolean",
"attribute": "emphasize-component",
"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,
"defaultValue": "true"
},
"showTopLevelCopy": {
"type": "boolean",
"attribute": "show-top-level-copy",
"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,
"defaultValue": "true"
},
"defaultTTL": {
"type": "number",
"attribute": "default-t-t-l",
"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,
"defaultValue": "24 * 60 * 60 * 1000"
},
"width": {
"type": "string",
"attribute": "width",
"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
},
"height": {
"type": "string",
"attribute": "height",
"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
},
"darkMode": {
"type": "string",
"attribute": "dark-mode",
"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: \"system\""
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "'system'"
}
};
}
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": "items",
"methodName": "onItemsChange"
}, {
"propName": "amountOfItems",
"methodName": "validateAmountOfItems"
}, {
"propName": "darkMode",
"methodName": "watchDarkMode"
}];
}
}
//# sourceMappingURL=pid-component.js.map