+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
+ ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ${story()} Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
+ eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis
+ aute ${story()} irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
+ officia deserunt mollit anim id est laborum.
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+ `,
+ },
+ },
+ },
+};
diff --git a/packages/stencil-library/src/components/color-highlight/color-highlight.tsx b/packages/stencil-library/src/components/color-highlight/color-highlight.tsx
new file mode 100644
index 0000000..6a5f126
--- /dev/null
+++ b/packages/stencil-library/src/components/color-highlight/color-highlight.tsx
@@ -0,0 +1,41 @@
+import { Component, h, Host, Prop, State } from '@stencil/core';
+import { HSLColor } from './HSLColor';
+
+@Component({
+ tag: 'color-highlight',
+ styleUrl: 'color-highlight.css',
+ shadow: false,
+})
+export class ColorHighlight {
+ /**
+ * The text to highlight.
+ * @type {string}
+ */
+ @Prop() text!: string;
+
+ /**
+ * The color of the text.
+ * @private
+ * @type {HSLColor}
+ */
+ @State() color: HSLColor;
+
+ async componentWillLoad() {
+ this.color = await HSLColor.generateColor(this.text);
+ }
+
+ render() {
+ return (
+
+
+ {this.text}
+
+
+ );
+ }
+}
diff --git a/packages/stencil-library/src/components/color-highlight/readme.md b/packages/stencil-library/src/components/color-highlight/readme.md
new file mode 100644
index 0000000..9ef3eca
--- /dev/null
+++ b/packages/stencil-library/src/components/color-highlight/readme.md
@@ -0,0 +1,15 @@
+# handle-highlight
+
+
+
+
+## Properties
+
+| Property | Attribute | Description | Type | Default |
+| ------------------- | --------- | ---------------------- | -------- | ----------- |
+| `text` _(required)_ | `text` | The text to highlight. | `string` | `undefined` |
+
+
+----------------------------------------------
+
+*Built with [StencilJS](https://stenciljs.com/)*
diff --git a/packages/stencil-library/src/components/copy-button/copy-button.stories.ts b/packages/stencil-library/src/components/copy-button/copy-button.stories.ts
new file mode 100644
index 0000000..92c805b
--- /dev/null
+++ b/packages/stencil-library/src/components/copy-button/copy-button.stories.ts
@@ -0,0 +1,35 @@
+import { Meta, StoryObj } from '@storybook/web-components';
+
+const meta: Meta = {
+ title: 'copy-button',
+ component: 'copy-button',
+ argTypes: {
+ value: {
+ description: 'The text to copy (required)',
+ control: {
+ required: true,
+ type: 'text',
+ },
+ },
+ },
+ args: {
+ value: 'Hello world!',
+ },
+};
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ value: 'Hello world!',
+ },
+ parameters: {
+ docs: {
+ source: {
+ code: `
+
+ `,
+ },
+ },
+ },
+};
diff --git a/packages/stencil-library/src/components/copy-button/copy-button.tsx b/packages/stencil-library/src/components/copy-button/copy-button.tsx
new file mode 100644
index 0000000..47eb567
--- /dev/null
+++ b/packages/stencil-library/src/components/copy-button/copy-button.tsx
@@ -0,0 +1,76 @@
+import { Component, h, Host, Prop } from '@stencil/core';
+
+@Component({
+ tag: 'copy-button',
+ shadow: false,
+})
+export class CopyButton {
+ /**
+ * The value to copy to the clipboard.
+ * @type {string}
+ * @public
+ */
+ @Prop() value!: string;
+
+ render() {
+ /**
+ * Copies the given value to the clipboard and changes the text of the button to "✓ Copied!" for 1.5 seconds.
+ * @param event The event that triggered this function.
+ * @param value The value to copy to the clipboard.
+ */
+ function copyValue(event: MouseEvent, value: string) {
+ if ('clipboard' in navigator) {
+ // Use the Async Clipboard API when available.
+ navigator.clipboard.writeText(value).then(() => showSuccess());
+ } else {
+ // ...Otherwise, use document.execCommand() fallback.
+ const textArea = document.createElement('textarea');
+ textArea.value = value;
+ textArea.style.opacity = '0';
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+ try {
+ const success = document.execCommand('copy');
+ console.debug(`Deprecated Text copy was ${success ? 'successful' : 'unsuccessful'}.`);
+ showSuccess();
+ } catch (err) {
+ console.error(err.name, err.message);
+ }
+ document.body.removeChild(textArea);
+ }
+
+ /**
+ * Shows the success message for 1.5 seconds.
+ */
+ function showSuccess() {
+ const el = event.target as HTMLButtonElement;
+ el.innerText = '✓ Copied!';
+ el.classList.remove('hover:bg-blue-200');
+ el.classList.remove('bg-white');
+ el.classList.add('bg-green-200');
+
+ // Reset the button after 1.5 seconds.
+ setTimeout(() => {
+ el.classList.remove('bg-green-200');
+ el.classList.add('hover:bg-blue-200');
+ el.classList.add('bg-white');
+ el.innerText = 'Copy';
+ }, 1500);
+ }
+ }
+
+ return (
+
+
+
+ );
+ }
+}
diff --git a/packages/stencil-library/src/components/copy-button/readme.md b/packages/stencil-library/src/components/copy-button/readme.md
new file mode 100644
index 0000000..c2768b0
--- /dev/null
+++ b/packages/stencil-library/src/components/copy-button/readme.md
@@ -0,0 +1,28 @@
+# copy-button
+
+
+
+
+## Properties
+
+| Property | Attribute | Description | Type | Default |
+| -------------------- | --------- | ----------------------------------- | -------- | ----------- |
+| `value` _(required)_ | `value` | The value to copy to the clipboard. | `string` | `undefined` |
+
+
+## Dependencies
+
+### Used by
+
+ - [pid-component](../pid-component)
+
+### Graph
+```mermaid
+graph TD;
+ pid-component --> copy-button
+ style copy-button fill:#f9f,stroke:#333,stroke-width:4px
+```
+
+----------------------------------------------
+
+*Built with [StencilJS](https://stenciljs.com/)*
diff --git a/packages/stencil-library/src/components/locale-vizualization/locale-visualization.stories.ts b/packages/stencil-library/src/components/locale-vizualization/locale-visualization.stories.ts
new file mode 100644
index 0000000..efb0261
--- /dev/null
+++ b/packages/stencil-library/src/components/locale-vizualization/locale-visualization.stories.ts
@@ -0,0 +1,60 @@
+import { Meta, StoryObj } from '@storybook/web-components';
+
+const meta: Meta = {
+ title: 'locale-visualization',
+ component: 'locale-visualization',
+ argTypes: {
+ locale: {
+ description: 'The locale to visualize.',
+ control: {
+ required: true,
+ type: 'text',
+ },
+ },
+ showFlag: {
+ description: 'Whether to show the flag of the region.',
+ control: {
+ required: false,
+ type: 'boolean',
+ default: true,
+ },
+ },
+ },
+ args: {
+ locale: 'de-DE',
+ showFlag: true,
+ },
+};
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ locale: 'de-DE',
+ },
+ parameters: {
+ docs: {
+ source: {
+ code: `
+
+ `,
+ },
+ },
+ },
+};
+
+export const WithoutFlag: Story = {
+ args: {
+ locale: 'en-US',
+ showFlag: false,
+ },
+ parameters: {
+ docs: {
+ source: {
+ code: `
+
+ `,
+ },
+ },
+ },
+};
diff --git a/packages/stencil-library/src/components/locale-vizualization/locale-visualization.tsx b/packages/stencil-library/src/components/locale-vizualization/locale-visualization.tsx
new file mode 100644
index 0000000..fdd849a
--- /dev/null
+++ b/packages/stencil-library/src/components/locale-vizualization/locale-visualization.tsx
@@ -0,0 +1,52 @@
+import { Component, h, Host, Prop } from '@stencil/core';
+
+@Component({
+ tag: 'locale-visualization',
+ shadow: false,
+})
+export class LocaleVisualization {
+ /**
+ * The locale to visualize.
+ * @type {string}
+ * @public
+ */
+ @Prop() locale!: string;
+
+ /**
+ * Whether to show the flag of the region.
+ * @type {boolean}
+ * @public
+ */
+ @Prop() showFlag: boolean = true;
+
+ render() {
+ const getLocaleDetail = (locale: string): string => {
+ const userLocale = [navigator.language.split('-')[0]];
+ const type = locale.split('-').length > 1 ? 'language' : 'region';
+ const friendlyName = new Intl.DisplayNames(userLocale, { type: type }).of(locale.toUpperCase());
+ if (friendlyName == locale.toUpperCase()) {
+ return new Intl.DisplayNames(userLocale, { type: 'language' }).of(locale.toUpperCase());
+ }
+ if (type === 'language') {
+ const flag = generateFlag(locale.split('-')[1]);
+ return `${flag}${friendlyName}`;
+ }
+ return `${generateFlag(locale)}${friendlyName}`;
+ };
+
+ const generateFlag = (locale: string): string => {
+ if (this.showFlag === false) return '';
+ const codePoints = locale
+ .toUpperCase()
+ .split('')
+ .map(char => 127397 + char.charCodeAt(0));
+ return String.fromCodePoint(...codePoints) + ' ';
+ };
+
+ return (
+
+ {getLocaleDetail(this.locale)}
+
+ );
+ }
+}
diff --git a/packages/stencil-library/src/components/locale-vizualization/readme.md b/packages/stencil-library/src/components/locale-vizualization/readme.md
new file mode 100644
index 0000000..52e6ac7
--- /dev/null
+++ b/packages/stencil-library/src/components/locale-vizualization/readme.md
@@ -0,0 +1,16 @@
+# locale-visualization
+
+
+
+
+## Properties
+
+| Property | Attribute | Description | Type | Default |
+| --------------------- | ----------- | --------------------------------------- | --------- | ----------- |
+| `locale` _(required)_ | `locale` | The locale to visualize. | `string` | `undefined` |
+| `showFlag` | `show-flag` | Whether to show the flag of the region. | `boolean` | `true` |
+
+
+----------------------------------------------
+
+*Built with [StencilJS](https://stenciljs.com/)*
diff --git a/src/components/pid-component/pid-component.css b/packages/stencil-library/src/components/pid-component/pid-component.css
similarity index 100%
rename from src/components/pid-component/pid-component.css
rename to packages/stencil-library/src/components/pid-component/pid-component.css
diff --git a/src/components/pid-component/pid-component.mdx b/packages/stencil-library/src/components/pid-component/pid-component.mdx
similarity index 86%
rename from src/components/pid-component/pid-component.mdx
rename to packages/stencil-library/src/components/pid-component/pid-component.mdx
index c53957b..d0a61b7 100644
--- a/src/components/pid-component/pid-component.mdx
+++ b/packages/stencil-library/src/components/pid-component/pid-component.mdx
@@ -48,13 +48,31 @@ Different components need different settings.
You can pass these settings to the component via the 'settings' property.
E.g., The ORCiD component enables you to decide whether you want to show the affilliation in the summary or not.
It also enables you to show the affilliation at a certain point of time.
+
+For better performance, the pid-component stores fetched data in the IndexedDB.
+To ensure that the data and representation are up-to-date, you can set a time-to-live (TTL) for each type.
+The TTL is the time in milliseconds after which the data is considered outdated and will be fetched again.
+In the example below, the TTL for the HandleType is set to 1 hour (3600000 ms) and for the ORCIDType to 1 day (86400000 ms).
This results in the following settings JSON that you have to provide in a stringified form:
```json
[
{
- "type": "ORCIDConfig",
+ "type": "HandleType",
+ "values": [
+ {
+ "name": "ttl",
+ "value": 3600000
+ }
+ ]
+ },
+ {
+ "type": "ORCIDType",
"values": [
+ {
+ "name": "ttl",
+ "value": 86400000
+ },
{
"name": "affiliationAt",
"value": 949363200000
@@ -109,7 +127,8 @@ You can easily add a new type by simply implementing the abstract class `Generic
You can also render multiple e-mail addresses separated by a comma.
-
+
+
### Date
@@ -127,6 +146,7 @@ It will be rendered in line while folded and will take up the whole width of the
You can make the component even smaller by setting the `emphasize-component` attribute to `false`.
+
This is an example where the subcomponents are hidden and the component is not emphasized.
diff --git a/src/components/pid-component/pid-component.stories.ts b/packages/stencil-library/src/components/pid-component/pid-component.stories.ts
similarity index 82%
rename from src/components/pid-component/pid-component.stories.ts
rename to packages/stencil-library/src/components/pid-component/pid-component.stories.ts
index ad9dc20..5606515 100644
--- a/src/components/pid-component/pid-component.stories.ts
+++ b/packages/stencil-library/src/components/pid-component/pid-component.stories.ts
@@ -27,7 +27,7 @@ const meta: Meta = {
},
table: {
defaultValue: {
- summary: "false",
+ summary: 'false',
},
type: {
summary: 'boolean',
@@ -43,7 +43,7 @@ const meta: Meta = {
},
table: {
defaultValue: {
- summary: "10",
+ summary: '10',
},
type: {
summary: 'number',
@@ -53,14 +53,14 @@ const meta: Meta = {
hideSubcomponents: {
name: 'hide-subcomponents',
description:
- 'Determines whether subcomponents should generally be shown or not. If set to true, the component won\'t show any subcomponents. If not set, the component will show subcomponents, if the current level of subcomponents is not the total level of subcomponents or greater.',
+ "Determines whether subcomponents should generally be shown or not. If set to true, the component won't show any subcomponents. If not set, the component will show subcomponents, if the current level of subcomponents is not the total level of subcomponents or greater.",
defaultValue: false,
control: {
type: 'boolean',
},
table: {
defaultValue: {
- summary: "false",
+ summary: 'false',
},
type: {
summary: 'boolean',
@@ -69,15 +69,14 @@ const meta: Meta = {
},
emphasizeComponent: {
name: 'emphasize-component',
- description:
- 'Determines whether components should be emphasized towards their surrounding by border and shadow.',
+ description: 'Determines whether components should be emphasized towards their surrounding by border and shadow.',
defaultValue: true,
control: {
type: 'boolean',
},
table: {
defaultValue: {
- summary: "true",
+ summary: 'true',
},
type: {
summary: 'boolean',
@@ -86,15 +85,14 @@ const meta: Meta = {
},
showTopLevelCopy: {
name: 'show-top-level-copy',
- description:
- ' Determines whether on the top level the copy button is shown.',
+ description: ' Determines whether on the top level the copy button is shown.',
defaultValue: true,
control: {
type: 'boolean',
},
table: {
defaultValue: {
- summary: "true",
+ summary: 'true',
},
type: {
summary: 'boolean',
@@ -110,7 +108,7 @@ const meta: Meta = {
},
table: {
defaultValue: {
- summary: "1",
+ summary: '1',
},
type: {
summary: 'number',
@@ -126,26 +124,26 @@ const meta: Meta = {
},
table: {
defaultValue: {
- summary: "0",
+ summary: '0',
},
type: {
summary: 'number',
},
},
},
- deleteCacheAfterDisconnect: {
- name: 'delete-cache-after-disconnect',
- description: 'Determines whether the cache should be deleted after the top level component is disconnected from the DOM.',
- defaultValue: true,
+ defaultTTL: {
+ name: 'default-TTL',
+ description: 'The default TTL for entries in the IndexedDB. Is used if no TTL is set in the settings.',
+ defaultValue: 24 * 60 * 60 * 1000,
control: {
- type: 'boolean',
+ type: 'number',
},
table: {
defaultValue: {
- summary: "true",
+ summary: '24*60*60*1000',
},
type: {
- summary: 'boolean',
+ summary: 'number',
},
},
},
@@ -159,21 +157,16 @@ const meta: Meta = {
emphasizeComponent: true,
levelOfSubcomponents: 1,
currentLevelOfSubcomponents: 0,
- deleteCacheAfterDisconnect: false,
+ defaultTTL: 24 * 60 * 60 * 1000,
},
};
-const textDecorator = story =>
- html`
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
- aliqua. Ut enim ad minim veniam, quis nostrud exercitation
- ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit
- esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
- occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ${story()} Lorem
- ipsum dolor sit amet, consectetur adipiscing elit, sed do
- eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
- ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis
- aute ${story()} irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
- Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
+const textDecorator = (story: () => unknown) =>
+ html`
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
+ ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ${story()} Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
+ eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis
+ aute ${story()} irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
officia deserunt mollit anim id est laborum.
+ The Typed PID Maker is an entry point to integrate digital resources into the FAIR Digital Object (FAIR DO) ecosystem. It allows creating PIDs for resources and to provide
+ them with the necessary metadata to ensure that the resources can be found and understood.
+ As a result, a machine-readable representation of all kinds of research artifacts allows act on such FAIR Digital Objects which present themselves as PID, e.g., ${story()},
+ but carry much more than just a pointer to a landing page.
+
+ `,
+ ],
+};
diff --git a/src/components/pid-component/pid-component.tsx b/packages/stencil-library/src/components/pid-component/pid-component.tsx
similarity index 65%
rename from src/components/pid-component/pid-component.tsx
rename to packages/stencil-library/src/components/pid-component/pid-component.tsx
index 27e698a..a7fcff0 100644
--- a/src/components/pid-component/pid-component.tsx
+++ b/packages/stencil-library/src/components/pid-component/pid-component.tsx
@@ -1,13 +1,14 @@
-import {Component, Host, h, Prop, State, Watch} from '@stencil/core';
-import {GenericIdentifierType} from '../../utils/GenericIdentifierType';
-import {FoldableItem} from '../../utils/FoldableItem';
-import {FoldableAction} from '../../utils/FoldableAction';
-import {Parser} from '../../utils/Parser';
+import { Component, h, Host, Prop, State, Watch } from '@stencil/core';
+import { GenericIdentifierType } from '../../utils/GenericIdentifierType';
+import { FoldableItem } from '../../utils/FoldableItem';
+import { FoldableAction } from '../../utils/FoldableAction';
+import { getEntity } from '../../utils/IndexedDBUtil';
+import { clearCache } from '../../utils/DataCache';
@Component({
tag: 'pid-component',
styleUrl: 'pid-component.css',
- shadow: true,
+ shadow: false,
})
export class PidComponent {
/**
@@ -34,7 +35,7 @@ export class PidComponent {
* ```
* @type {string}
*/
- @Prop() settings: string;
+ @Prop() settings: string = '[]';
/**
* Determines whether the component is open or not by default.
@@ -96,12 +97,14 @@ export class PidComponent {
@Prop() showTopLevelCopy: boolean = true;
/**
- * Determines whether the cache should be deleted after the component on the top level is disconnected.
- * Defaults to true.
+ * Determines the default time to live (TTL) for entries in the IndexedDB.
+ * Defaults to 24 hours.
+ * Units are in milliseconds.
* (optional)
- * @type {boolean}
+ * @type {number}
+ * @default 24 * 60 * 60 * 1000
*/
- @Prop() deleteCacheAfterDisconnect: boolean = true;
+ @Prop() defaultTTL: number = 24 * 60 * 60 * 1000;
/**
* Stores the parsed identifier object.
@@ -148,7 +151,7 @@ export class PidComponent {
async watchValue() {
this.displayStatus = 'loading';
// this.identifierObject = undefined;
- await this.connectedCallback();
+ await this.componentWillLoad();
}
/**
@@ -156,34 +159,38 @@ export class PidComponent {
*/
@Watch('loadSubcomponents')
async watchLoadSubcomponents() {
- this.temporarilyEmphasized = this.loadSubcomponents
+ this.temporarilyEmphasized = this.loadSubcomponents;
}
/**
* Parses the value and settings, generates the items and actions and sets the displayStatus to "loaded".
*/
- async connectedCallback() {
+ async componentWillLoad() {
let settings: {
type: string;
values: {
name: string;
value: any;
}[];
- }[];
+ }[] = [];
try {
settings = JSON.parse(this.settings);
} catch (e) {
- console.error("Failed to parse settings.", e)
+ console.error('Failed to parse settings.', e);
}
+ settings.forEach(value => {
+ if (!value.values.some(v => v.name === 'ttl')) {
+ value.values.push({ name: 'ttl', value: this.defaultTTL });
+ }
+ });
- // Get an object from the best fitting class implementing GenericIdentifierType
- const obj = await Parser.getBestFit(this.value, settings);
- this.identifierObject = obj;
+ // Get the renderer for the value
+ this.identifierObject = await getEntity(this.value, settings);
// Generate items and actions if subcomponents should be shown
if (!this.hideSubcomponents) {
- this.items = obj.items;
+ this.items = this.identifierObject.items;
this.items.sort((a, b) => {
// Sort by priority defined in the specific implementation of GenericIdentifierType (lower is better)
if (a.priority > b.priority) return 1;
@@ -193,95 +200,21 @@ export class PidComponent {
if (a.estimatedTypePriority > b.estimatedTypePriority) return 1;
if (a.estimatedTypePriority < b.estimatedTypePriority) return -1;
});
- this.actions = obj.actions;
+
+ this.actions = this.identifierObject.actions;
this.actions.sort((a, b) => a.priority - b.priority);
}
this.displayStatus = 'loaded';
console.log('Finished loading for ', this.value, this.identifierObject);
+ await clearCache();
}
- /**
- * Toggles the loadSubcomponents property if the current level of subcomponents is not the total level of subcomponents.
- */
- private toggleSubcomponents = () => {
- if (!this.hideSubcomponents && this.levelOfSubcomponents - this.currentLevelOfSubcomponents > 0) this.loadSubcomponents = !this.loadSubcomponents;
- };
-
- /**
- * Shows the tooltip of the hovered element.
- * @param event The event that triggered this function.
- */
- private showTooltip = (event: Event) => {
- let target = event.target as HTMLElement;
- do {
- target = target.parentElement as HTMLElement;
- } while (target !== null && target.tagName !== 'A');
- if (target !== null) target.children[1].classList.remove('hidden');
- };
-
- /**
- * Hides the tooltip of the hovered element.
- * @param event The event that triggered this function.
- */
- private hideTooltip = (event: Event) => {
- let target = event.target as HTMLElement;
- do {
- target = target.parentElement as HTMLElement;
- } while (target !== null && target.tagName !== 'A');
- if (target !== null) target.children[1].classList.add('hidden');
- };
-
/**
* Renders the component.
*/
render() {
- /**
- * Copies the given value to the clipboard and changes the text of the button to "✓ Copied!" for 1.5 seconds.
- * @param event The event that triggered this function.
- * @param value The value to copy to the clipboard.
- */
- function copyValue(event: MouseEvent, value: string) {
- if ('clipboard' in navigator) {
- // Use the Async Clipboard API when available.
- navigator.clipboard.writeText(value).then(() => showSuccess());
- } else {
- // ...Otherwise, use document.execCommand() fallback.
- const textArea = document.createElement('textarea');
- textArea.value = value;
- textArea.style.opacity = '0';
- document.body.appendChild(textArea);
- textArea.focus();
- textArea.select();
- try {
- const success = document.execCommand('copy');
- console.log(`Deprecated Text copy was ${success ? 'successful' : 'unsuccessful'}.`);
- showSuccess();
- } catch (err) {
- console.error(err.name, err.message);
- }
- document.body.removeChild(textArea);
- }
-
- /**
- * Shows the success message for 1.5 seconds.
- */
- function showSuccess() {
- const el = event.target as HTMLButtonElement;
- el.innerText = '✓ Copied!';
- el.classList.remove('hover:bg-blue-200');
- el.classList.remove('bg-white');
- el.classList.add('bg-green-200');
- setTimeout(() => {
- el.classList.remove('bg-green-200');
- el.classList.add('hover:bg-blue-200');
- el.classList.add('bg-white');
- el.innerText = 'Copy';
- }, 1500);
- }
- }
-
return (
-
+
{
// Check if there are any items or actions to show
(this.items.length === 0 && this.actions.length === 0) || this.hideSubcomponents ? (
@@ -290,39 +223,28 @@ export class PidComponent {
- {
- // Render the preview of the identifier object defined in the specific implementation of GenericIdentifierType
- this.identifierObject.renderPreview()
- }
+ {
+ // Render the preview of the identifier object defined in the specific implementation of GenericIdentifierType
+ this.identifierObject.renderPreview()
+ }
{
// When this component is on the top level, show the copy button in the summary, in all the other cases show it in the table (implemented farther down)
- this.currentLevelOfSubcomponents === 0 && this.showTopLevelCopy && this.emphasizeComponent ? (
-
- ) : (
- ''
- )
+ this.currentLevelOfSubcomponents === 0 && this.showTopLevelCopy && this.emphasizeComponent ? : ''
}
) : (
-
- ) : ('')
- }
+ ) : (
+ ''
+ )}
{
// Render the preview of the identifier object defined in the specific implementation of GenericIdentifierType
@@ -373,14 +296,7 @@ export class PidComponent {
{
// When this component is on the top level, show the copy button in the summary, in all the other cases show it in the table (implemented farther down)
this.currentLevelOfSubcomponents === 0 && this.showTopLevelCopy && (this.emphasizeComponent || this.temporarilyEmphasized) ? (
-
+
) : (
''
)
@@ -390,25 +306,23 @@ export class PidComponent {
// If there are any items to show, render the table
this.items.length > 0 ? (
-
+
-
-
Key
-
Value
-
+
+
Key
+
Value
+
-
- {this.items
- .filter((_, index) => {
- // Filter out items that are not on the current page
- return index >= this.tablePage * this.amountOfItems && index < this.tablePage * this.amountOfItems + this.amountOfItems;
- })
- .map(value => {
- // Render a row for every item
- return (
+
+ {this.items
+ .filter((_, index) => {
+ // Filter out items that are not on the current page
+ return index >= this.tablePage * this.amountOfItems && index < this.tablePage * this.amountOfItems + this.amountOfItems;
+ })
+ .map(value => (
+ // Render a row for every item
+ // return (
-
- {
- // Load a foldable subcomponent if subcomponents are not disabled (hideSubcomponents), and the current level of subcomponents is not the total level of subcomponents. If the subcomponent is on the bottom level of the hierarchy, render just a preview. If the value should not be resolved (isFoldable), just render the value as text.
- this.loadSubcomponents && !this.hideSubcomponents && !value.renderDynamically ? (
-
- ) : !this.hideSubcomponents && this.currentLevelOfSubcomponents === this.levelOfSubcomponents && !value.renderDynamically ? (
-
- ) : (
- value.value
- )
- }
-
-
+
+
- );
- })}
+ ))}
-
+
Showing
{1 + this.tablePage * this.amountOfItems}
to
- {Math.min(this.tablePage * this.amountOfItems + this.amountOfItems, this.items.length)}
+ {Math.min(this.tablePage * this.amountOfItems + this.amountOfItems, this.items.length)}
of
{this.items.length}
entries
@@ -603,8 +506,34 @@ export class PidComponent {
);
}
- disconnectedCallback() {
- console.log('Disconnected');
- if (this.deleteCacheAfterDisconnect && caches && this.currentLevelOfSubcomponents === 0) caches.delete('pid-component').then(() => console.log('Cache deleted'));
- }
+ /**
+ * Toggles the loadSubcomponents property if the current level of subcomponents is not the total level of subcomponents.
+ */
+ private toggleSubcomponents = () => {
+ if (!this.hideSubcomponents && this.levelOfSubcomponents - this.currentLevelOfSubcomponents > 0) this.loadSubcomponents = !this.loadSubcomponents;
+ };
+
+ /**
+ * Shows the tooltip of the hovered element.
+ * @param event The event that triggered this function.
+ */
+ private showTooltip = (event: Event) => {
+ let target = event.target as HTMLElement;
+ do {
+ target = target.parentElement as HTMLElement;
+ } while (target !== null && target.tagName !== 'A');
+ if (target !== null) target.children[1].classList.remove('hidden');
+ };
+
+ /**
+ * Hides the tooltip of the hovered element.
+ * @param event The event that triggered this function.
+ */
+ private hideTooltip = (event: Event) => {
+ let target = event.target as HTMLElement;
+ do {
+ target = target.parentElement as HTMLElement;
+ } while (target !== null && target.tagName !== 'A');
+ if (target !== null) target.children[1].classList.add('hidden');
+ };
}
diff --git a/src/components/pid-component/readme.md b/packages/stencil-library/src/components/pid-component/readme.md
similarity index 81%
rename from src/components/pid-component/readme.md
rename to packages/stencil-library/src/components/pid-component/readme.md
index eaae989..16965bf 100644
--- a/src/components/pid-component/readme.md
+++ b/packages/stencil-library/src/components/pid-component/readme.md
@@ -5,18 +5,18 @@
## Properties
-| Property | Attribute | Description | Type | Default |
-| ----------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ----------- |
-| `amountOfItems` | `amount-of-items` | The number of items to show in the table per page. Defaults to 10. (optional) | `number` | `10` |
-| `currentLevelOfSubcomponents` | `current-level-of-subcomponents` | The current level of subcomponents. Defaults to 0. (optional) | `number` | `0` |
-| `deleteCacheAfterDisconnect` | `delete-cache-after-disconnect` | Determines whether the cache should be deleted after the component on the top level is disconnected. Defaults to true. (optional) | `boolean` | `true` |
-| `emphasizeComponent` | `emphasize-component` | Determines whether components should be emphasized towards their surrounding by border and shadow. If set to true, border and shadows will be shown around the component. It not set, the component won't be surrounded by border and shadow. (optional) | `boolean` | `true` |
-| `hideSubcomponents` | `hide-subcomponents` | Determines whether subcomponents should generally be shown or not. If set to true, the component won't show any subcomponents. If not set, the component will show subcomponents if the current level of subcomponents is not the total level of subcomponents or greater. (optional) | `boolean` | `undefined` |
-| `levelOfSubcomponents` | `level-of-subcomponents` | The total number of levels of subcomponents to show. Defaults to 1. (optional) | `number` | `1` |
-| `openByDefault` | `open-by-default` | Determines whether the component is open or not by default. (optional) | `boolean` | `undefined` |
-| `settings` | `settings` | A stringified JSON object containing settings for this component. The resulting object is passed to every subcomponent, so that every component has the same settings. Values and the according type are defined by the components themselves. (optional) Schema: ```typescript { type: string, values: { name: string, value: any }[] }[] ``` | `string` | `undefined` |
-| `showTopLevelCopy` | `show-top-level-copy` | Determines whether on the top level the copy button is shown. If set to true, the copy button is shown also on the top level. It not set, the copy button is only shown for sub-components. (optional) | `boolean` | `true` |
-| `value` | `value` | The value to parse, evaluate and render. | `string` | `undefined` |
+| Property | Attribute | Description | Type | Default |
+| ----------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | --------------------- |
+| `amountOfItems` | `amount-of-items` | The number of items to show in the table per page. Defaults to 10. (optional) | `number` | `10` |
+| `currentLevelOfSubcomponents` | `current-level-of-subcomponents` | The current level of subcomponents. Defaults to 0. (optional) | `number` | `0` |
+| `defaultTTL` | `default-t-t-l` | Determines the default time to live (TTL) for entries in the IndexedDB. Defaults to 24 hours. Units are in milliseconds. (optional) | `number` | `24 * 60 * 60 * 1000` |
+| `emphasizeComponent` | `emphasize-component` | Determines whether components should be emphasized towards their surrounding by border and shadow. If set to true, border and shadows will be shown around the component. It not set, the component won't be surrounded by border and shadow. (optional) | `boolean` | `true` |
+| `hideSubcomponents` | `hide-subcomponents` | Determines whether subcomponents should generally be shown or not. If set to true, the component won't show any subcomponents. If not set, the component will show subcomponents if the current level of subcomponents is not the total level of subcomponents or greater. (optional) | `boolean` | `undefined` |
+| `levelOfSubcomponents` | `level-of-subcomponents` | The total number of levels of subcomponents to show. Defaults to 1. (optional) | `number` | `1` |
+| `openByDefault` | `open-by-default` | Determines whether the component is open or not by default. (optional) | `boolean` | `undefined` |
+| `settings` | `settings` | A stringified JSON object containing settings for this component. The resulting object is passed to every subcomponent, so that every component has the same settings. Values and the according type are defined by the components themselves. (optional) Schema: ```typescript { type: string, values: { name: string, value: any }[] }[] ``` | `string` | `'[]'` |
+| `showTopLevelCopy` | `show-top-level-copy` | Determines whether on the top level the copy button is shown. If set to true, the copy button is shown also on the top level. It not set, the copy button is only shown for sub-components. (optional) | `boolean` | `true` |
+| `value` | `value` | The value to parse, evaluate and render. | `string` | `undefined` |
## Dependencies
@@ -27,6 +27,7 @@
### Depends on
+- [copy-button](../copy-button)
- [pid-component](.)
### Graph
diff --git a/src/index.ts b/packages/stencil-library/src/index.ts
similarity index 100%
rename from src/index.ts
rename to packages/stencil-library/src/index.ts
diff --git a/src/utils/DateType.tsx b/packages/stencil-library/src/rendererModules/DateType.tsx
similarity index 92%
rename from src/utils/DateType.tsx
rename to packages/stencil-library/src/rendererModules/DateType.tsx
index ecfac33..8adbe4a 100644
--- a/src/utils/DateType.tsx
+++ b/packages/stencil-library/src/rendererModules/DateType.tsx
@@ -1,5 +1,5 @@
-import { GenericIdentifierType } from './GenericIdentifierType';
import { FunctionalComponent, h } from '@stencil/core';
+import { GenericIdentifierType } from '../utils/GenericIdentifierType';
/**
* This class specifies a custom renderer for dates.
diff --git a/packages/stencil-library/src/rendererModules/EmailType.tsx b/packages/stencil-library/src/rendererModules/EmailType.tsx
new file mode 100644
index 0000000..eda4a19
--- /dev/null
+++ b/packages/stencil-library/src/rendererModules/EmailType.tsx
@@ -0,0 +1,55 @@
+import { FunctionalComponent, h } from '@stencil/core';
+import { GenericIdentifierType } from '../utils/GenericIdentifierType';
+
+/**
+ * This class specifies a custom renderer for Email addresses.
+ * @extends GenericIdentifierType
+ */
+export class EmailType extends GenericIdentifierType {
+ getSettingsKey(): string {
+ return 'EmailType';
+ }
+
+ hasCorrectFormat(): boolean {
+ const regex = /^(([\w\-.]+@([\w-]+\.)+[\w-]{2,})(\s*,\s*)?)*$/gm;
+ return regex.test(this.value);
+ }
+
+ init(): Promise {
+ return;
+ }
+
+ isResolvable(): boolean {
+ return false;
+ }
+
+ renderPreview(): FunctionalComponent {
+ // mail icon from: https://heroicons.com/ (MIT license)
+ return (
+
+ {this.value
+ .split(new RegExp(/\s*,\s*/))
+ .filter(email => email.length > 0)
+ .map(email => {
+ return (
+
+
+
+
+ {email}
+
+ );
+ })}
+
+ );
+ }
+}
diff --git a/src/utils/FallbackType.tsx b/packages/stencil-library/src/rendererModules/FallbackType.tsx
similarity index 68%
rename from src/utils/FallbackType.tsx
rename to packages/stencil-library/src/rendererModules/FallbackType.tsx
index 2603081..81a4f5f 100644
--- a/src/utils/FallbackType.tsx
+++ b/packages/stencil-library/src/rendererModules/FallbackType.tsx
@@ -1,8 +1,8 @@
import { FunctionalComponent, h } from '@stencil/core';
-import { GenericIdentifierType } from './GenericIdentifierType';
+import { GenericIdentifierType } from '../utils/GenericIdentifierType';
/**
- * This class specifies a custom renderer that is used as a fallback for all types that are not supported.
+ * This class specifies a custom renderer used as a fallback for all types that are not supported.
* @extends GenericIdentifierType
*/
export class FallbackType extends GenericIdentifierType {
@@ -23,6 +23,6 @@ export class FallbackType extends GenericIdentifierType {
}
getSettingsKey(): string {
- return '';
+ return 'FallbackType';
}
}
diff --git a/packages/stencil-library/src/rendererModules/Handle/HandleType.tsx b/packages/stencil-library/src/rendererModules/Handle/HandleType.tsx
new file mode 100644
index 0000000..8040eae
--- /dev/null
+++ b/packages/stencil-library/src/rendererModules/Handle/HandleType.tsx
@@ -0,0 +1,116 @@
+import { PIDRecord } from './PIDRecord';
+import { PID } from './PID';
+import { PIDDataType } from './PIDDataType';
+import { FunctionalComponent, h } from '@stencil/core';
+import { GenericIdentifierType } from '../../utils/GenericIdentifierType';
+import { FoldableItem } from '../../utils/FoldableItem';
+import { FoldableAction } from '../../utils/FoldableAction';
+
+/**
+ * This class specifies a custom renderer for handles.
+ * @extends GenericIdentifierType
+ */
+export class HandleType extends GenericIdentifierType {
+ /**
+ * The parts of the PID separated by a slash.
+ * @type {{text: string; color: HSLColor, nextExists: boolean}[]}
+ * @private
+ */
+ private _parts: {
+ /**
+ * The text of the part.
+ * @type {string}
+ */
+ text: string;
+
+ /**
+ * Whether there is a next part.
+ * @type {boolean}
+ */
+ nextExists: boolean;
+ }[] = [];
+
+ /**
+ * The PID record.
+ * @type {PIDRecord}
+ * @private
+ */
+ private _pidRecord: PIDRecord;
+
+ get data(): string {
+ return JSON.stringify(this._pidRecord.toObject());
+ }
+
+ hasCorrectFormat(): boolean {
+ return PID.isPID(this.value);
+ }
+
+ async init(data?: string): Promise {
+ if (data !== undefined) {
+ this._pidRecord = PIDRecord.fromJSON(data);
+ this._parts = await Promise.all([
+ {
+ text: this._pidRecord.pid.prefix,
+ nextExists: true,
+ },
+ {
+ text: this._pidRecord.pid.suffix,
+ nextExists: false,
+ },
+ ]);
+ console.debug('reload PIDRecord from data', this._pidRecord);
+ } else {
+ const pid = PID.getPIDFromString(this.value);
+
+ // Generate the colors for the parts of the PID
+ this._parts = [
+ {
+ text: pid.prefix,
+ nextExists: true,
+ },
+ {
+ text: pid.suffix,
+ nextExists: false,
+ },
+ ];
+
+ // Resolve the PID
+ this._pidRecord = await pid.resolve();
+ console.debug('load PIDRecord from API', this._pidRecord);
+ }
+
+ for (const value of this._pidRecord.values) {
+ if (value.type instanceof PIDDataType) {
+ this.items.push(new FoldableItem(0, value.type.name, value.data.value, value.type.description, value.type.redirectURL, value.type.regex));
+ }
+ }
+
+ this.actions.push(new FoldableAction(0, 'Open in FAIR-DOscope', `https://kit-data-manager.github.io/fairdoscope/?pid=${this._pidRecord.pid.toString()}`, 'primary'));
+ this.actions.push(new FoldableAction(0, 'View in Handle.net registry', `https://hdl.handle.net/${this._pidRecord.pid.toString()}`, 'secondary'));
+
+ return;
+ }
+
+ isResolvable(): boolean {
+ return this._pidRecord.values.length > 0;
+ }
+
+ renderPreview(): FunctionalComponent {
+ return (
+
+ {this._parts.map(element => {
+ return (
+
+
+ {element.nextExists ? '/' : ''}
+
+ );
+ })}
+
+ );
+ }
+
+ getSettingsKey(): string {
+ return 'HandleType';
+ }
+}
diff --git a/src/utils/PID.ts b/packages/stencil-library/src/rendererModules/Handle/PID.ts
similarity index 60%
rename from src/utils/PID.ts
rename to packages/stencil-library/src/rendererModules/Handle/PID.ts
index 4ee67cd..fa99b89 100644
--- a/src/utils/PID.ts
+++ b/packages/stencil-library/src/rendererModules/Handle/PID.ts
@@ -1,7 +1,7 @@
import { PIDRecord } from './PIDRecord';
import { PIDDataType } from './PIDDataType';
-import { handleMap, unresolvables } from './utils';
-import { init } from './DataCache';
+import { handleMap, unresolvables } from '../../utils/utils';
+import { cachedFetch } from '../../utils/DataCache';
/**
* This class represents the PID itself.
@@ -48,29 +48,13 @@ export class PID {
return this._suffix;
}
- /**
- * Outputs the PID as a string.
- * @returns {string} as "prefix/suffix"
- */
- toString(): string {
- return `${this.prefix}/${this.suffix}`;
- }
-
/**
* Checks if a string has the format of a PID.
* @param text The string to check.
* @returns {boolean} True if the string could be a PID, false if not.
*/
public static isPID(text: string): boolean {
- return new RegExp('^([0-9A-Za-z])+\.([0-9A-Za-z])+/([!-~])+$').test(text);
- }
-
- /**
- * Checks if this PID is resolvable, by checking if it is in the unresolvables set or on the "forbidden" list.
- * @returns {boolean} True if the PID is resolvable, false if not.
- */
- isResolvable(): boolean {
- return !unresolvables.has(this) && !this.prefix.toUpperCase().match('^(0$|0\\.|HS_|10320$)');
+ return new RegExp('^([0-9A-Za-z])+.([0-9A-Za-z])+/([!-~])+$').test(text);
}
/**
@@ -85,6 +69,27 @@ export class PID {
return new PID(pidSplit[0], pidSplit[1]);
}
+ static fromJSON(serialized: string): PID {
+ const data: ReturnType = JSON.parse(serialized);
+ return new PID(data.prefix, data.suffix);
+ }
+
+ /**
+ * Outputs the PID as a string.
+ * @returns {string} as "prefix/suffix"
+ */
+ toString(): string {
+ return `${this.prefix}/${this.suffix}`;
+ }
+
+ /**
+ * Checks if this PID is resolvable, by checking if it is in the unresolvables set or on the "forbidden" list.
+ * @returns {boolean} True if the PID is resolvable, false if not.
+ */
+ isResolvable(): boolean {
+ return !unresolvables.has(this) && !this.prefix.toUpperCase().match('^(0$|0\\.|HS_|10320$)');
+ }
+
/**
* Resolves the PID to a PIDRecord and saves it in the handleMap.
* @returns {Promise} The PIDRecord of the PID.
@@ -95,27 +100,54 @@ export class PID {
if (unresolvables.has(this)) return undefined;
else if (handleMap.has(this)) return handleMap.get(this);
else {
- const dataCache = await init('pid-component');
- const rawJson = await dataCache.fetch(`https://hdl.handle.net/api/handles/${this.prefix}/${this.suffix}#resolve`);
- const record = new PIDRecord(this);
- for (let value of rawJson.values) {
- let type = PID.isPID(value.type) ? PID.getPIDFromString(value.type) : value.type;
- if (type instanceof PID) {
- const dataType = await PIDDataType.resolveDataType(type);
- if (dataType instanceof PIDDataType) type = dataType;
- }
- record.values.push({
+ const rawJson = await cachedFetch(`https://hdl.handle.net/api/handles/${this.prefix}/${this.suffix}#resolve`);
+ // .then(response => response.json);
+ console.log(rawJson);
+ const valuePromises = rawJson.values.map(async (value: { index: number; type: string; data: string; ttl: number; timestamp: string }) => {
+ const type: Promise = (async () => {
+ if (PID.isPID(value.type)) {
+ const pid = PID.getPIDFromString(value.type);
+ const dataType = await PIDDataType.resolveDataType(pid);
+ return dataType instanceof PIDDataType ? dataType : pid;
+ }
+ return value.type;
+ })();
+ return {
index: value.index,
- type: type,
+ type: await type,
data: value.data,
ttl: value.ttl,
timestamp: Date.parse(value.timestamp),
- });
- }
+ };
+ });
+ const values = await Promise.all(valuePromises);
+
+ // for (const value of rawJson.values) {
+ // let type = PID.isPID(value.type) ? PID.getPIDFromString(value.type) : value.type;
+ // if (type instanceof PID) {
+ // const dataType = await PIDDataType.resolveDataType(type);
+ // if (dataType instanceof PIDDataType) type = dataType;
+ // }
+ // values.push({
+ // index: value.index,
+ // type: type,
+ // data: value.data,
+ // ttl: value.ttl,
+ // timestamp: Date.parse(value.timestamp),
+ // });
+ // }
+ const record = new PIDRecord(this, values);
handleMap.set(this, record);
return record;
}
}
+
+ toObject() {
+ return {
+ prefix: this.prefix,
+ suffix: this.suffix,
+ };
+ }
}
/**
diff --git a/src/utils/PIDDataType.ts b/packages/stencil-library/src/rendererModules/Handle/PIDDataType.ts
similarity index 82%
rename from src/utils/PIDDataType.ts
rename to packages/stencil-library/src/rendererModules/Handle/PIDDataType.ts
index 6528fb7..9d944e0 100644
--- a/src/utils/PIDDataType.ts
+++ b/packages/stencil-library/src/rendererModules/Handle/PIDDataType.ts
@@ -1,6 +1,6 @@
import { locationType, PID } from './PID';
-import { typeMap, unresolvables } from './utils';
-import { init } from './DataCache';
+import { typeMap, unresolvables } from '../../utils/utils';
+import { cachedFetch } from '../../utils/DataCache';
/**
* This class represents a PID data type.
@@ -34,13 +34,6 @@ export class PIDDataType {
*/
private readonly _redirectURL: string;
- /**
- * The raw JSON object from the ePIC data type registry.
- * @private
- * @type {object}
- */
- private readonly _ePICJSON: object;
-
/**
* An optional regex to check if a value matches this data type.
* @private
@@ -54,17 +47,15 @@ export class PIDDataType {
* @param name The name of the data type.
* @param description The description of the data type.
* @param redirectURL The redirect URL of a user-friendly website.
- * @param ePICJSON The raw JSON object from the ePIC data type registry.
* @param regex An optional regex to check if a value matches this data type.
* @constructor
*/
- constructor(pid: PID, name: string, description: string, redirectURL: string, ePICJSON: Object, regex?: RegExp) {
+ constructor(pid: PID, name: string, description: string, redirectURL: string, regex?: RegExp) {
this._pid = pid;
this._name = name;
this._description = description;
this._regex = regex;
this._redirectURL = redirectURL;
- this._ePICJSON = ePICJSON;
}
/**
@@ -99,14 +90,6 @@ export class PIDDataType {
return this._redirectURL;
}
- /**
- * Outputs the raw JSON object from the ePIC data type registry.
- * @returns {object} The raw JSON object from the ePIC data type registry.
- */
- get ePICJSON(): object {
- return this._ePICJSON;
- }
-
/**
* Outputs the optional regex of the data type.
* @returns {RegExp} The optional regex of the data type.
@@ -126,20 +109,20 @@ export class PIDDataType {
// Check if PID is resolvable
if (!pid.isResolvable()) {
- console.debug(`PID ${pid.toString()} is not resolvable`);
+ console.debug(`PID ${pid.toString()} has been marked as unresolvable`);
return undefined;
}
// Resolve PID and make sure it isn't undefined
const pidRecord = await pid.resolve();
if (pidRecord === undefined) {
- console.debug(`PID ${pid.toString()} could not be resolved`);
+ console.debug(`PID ${pid.toString()} could not be resolved via the API`);
unresolvables.add(pid);
return undefined;
}
// Create a temporary object to store the information
- let tempDataType: {
+ const tempDataType: {
name: string;
description: string;
regex?: RegExp;
@@ -157,7 +140,7 @@ export class PIDDataType {
const xmlLocations = xmlDoc.getElementsByTagName('location');
for (let j = 0; j < xmlLocations.length; j++) {
// Extract link
- let newLocation = {
+ const newLocation = {
href: xmlLocations[j].getAttribute('href'),
weight: undefined,
view: undefined,
@@ -177,9 +160,9 @@ export class PIDDataType {
// Try to resolve the data from the link
try {
if (newLocation.view === 'json') {
- const dataCache = await init('pid-component');
- // if view is json then fetch the data from the link (ePIC data type registry) and save them into the temp object
- newLocation.resolvedData = await dataCache.fetch(newLocation.href);
+ // if view is json then cachedFetch the data from the link (ePIC data type registry) and save them into the temp object
+ newLocation.resolvedData = await cachedFetch(newLocation.href);
+ // .then(response => response.json());
tempDataType.ePICJSON = newLocation.resolvedData;
tempDataType.name = newLocation.resolvedData['name'];
tempDataType.description = newLocation.resolvedData['description'];
@@ -194,7 +177,7 @@ export class PIDDataType {
// Create a new PIDDataType object from the temp object
try {
- const type = new PIDDataType(pid, tempDataType.name, tempDataType.description, tempDataType.redirectURL, tempDataType.ePICJSON, tempDataType.regex);
+ const type = new PIDDataType(pid, tempDataType.name, tempDataType.description, tempDataType.redirectURL, tempDataType.regex);
typeMap.set(pid, type);
return type;
} catch (e) {
@@ -202,4 +185,19 @@ export class PIDDataType {
return undefined;
}
}
+
+ static fromJSON(serialized: string): PIDDataType {
+ const data: ReturnType = JSON.parse(serialized);
+ return new PIDDataType(PID.fromJSON(data.pid), data.name, data.description, data.redirectURL, data.regex);
+ }
+
+ toObject() {
+ return {
+ pid: JSON.stringify(this._pid.toObject()),
+ name: this._name,
+ description: this._description,
+ redirectURL: this._redirectURL,
+ regex: this._regex,
+ };
+ }
}
diff --git a/packages/stencil-library/src/rendererModules/Handle/PIDRecord.ts b/packages/stencil-library/src/rendererModules/Handle/PIDRecord.ts
new file mode 100644
index 0000000..81ae36d
--- /dev/null
+++ b/packages/stencil-library/src/rendererModules/Handle/PIDRecord.ts
@@ -0,0 +1,218 @@
+import { PIDDataType } from './PIDDataType';
+import { PID } from './PID';
+
+/**
+ * This class represents a PID record.
+ */
+export class PIDRecord {
+ /**
+ * The PID of the record.
+ * @type {PID}
+ * @private
+ */
+ private readonly _pid: PID;
+
+ /**
+ * The values of the record.
+ * @type {{
+ * index: number,
+ * type: PIDDataType | PID | string,
+ * data: {
+ * format: string,
+ * value: string
+ * },
+ * ttl: number,
+ * timestamp: number
+ * }[]}
+ * @private
+ * @default []
+ */
+ private readonly _values: {
+ /**
+ * The index of the value in the record.
+ * @type {number}
+ */
+ index: number;
+
+ /**
+ * The type of the value.
+ * This can be a PID, a PID data type or a string.
+ * If it is a string, it is most certainly not a PID.
+ * If it is a PID, it couldn't be resolved to a PIDDataType.
+ * If it is a PIDDataType, it has additional information that can be shown to the user.
+ * @type {PIDDataType | PID | string}
+ */
+ type: PIDDataType | PID | string;
+
+ /**
+ * The data of the value.
+ * @type {{
+ * format: string,
+ * value: string
+ * }}
+ */
+ data: {
+ /**
+ * The format of the data.
+ * @type {string}
+ */
+ format: string;
+
+ /**
+ * The value of the data.
+ * @type {string}
+ */
+ value: string;
+ };
+
+ /**
+ * The time to live of the value.
+ * @type {number}
+ */
+ ttl: number;
+
+ /**
+ * The timestamp of the value.
+ * @type {number}
+ */
+ timestamp: number;
+ }[] = [];
+
+ /**
+ * The constructor of PIDRecord.
+ * @param pid The PID of the record.
+ * @constructor
+ */
+ constructor(pid: PID);
+
+ constructor(
+ pid: PID,
+ values: {
+ index: number;
+ type: string | PID | PIDDataType;
+ data: {
+ format: string;
+ value: string;
+ };
+ ttl: number;
+ timestamp: number;
+ }[],
+ );
+
+ constructor(
+ pid: PID,
+ values?: {
+ index: number;
+ type: string | PID | PIDDataType;
+ data: {
+ format: string;
+ value: string;
+ };
+ ttl: number;
+ timestamp: number;
+ }[],
+ ) {
+ this._pid = pid;
+ this._values = values;
+ }
+
+ /**
+ * Outputs the PID of the record.
+ * @returns {PID} The PID of the record.
+ */
+ get pid(): PID {
+ return this._pid;
+ }
+
+ /**
+ * Outputs the values of the record.
+ * @returns {{
+ * index: number,
+ * type: PIDDataType | PID | string,
+ * data: {
+ * format: string,
+ * value: string
+ * },
+ * ttl: number,
+ * timestamp: number
+ * }[]} The values of the record.
+ */
+ get values(): {
+ index: number;
+ type: string | PID | PIDDataType;
+ data: {
+ format: string;
+ value: string;
+ };
+ ttl: number;
+ timestamp: number;
+ }[] {
+ return this._values;
+ }
+
+ static fromJSON(serialized: string): PIDRecord {
+ const data: ReturnType = JSON.parse(serialized);
+
+ const values: {
+ index: number;
+ type: string | PID | PIDDataType;
+ data: {
+ format: string;
+ value: string;
+ };
+ ttl: number;
+ timestamp: number;
+ }[] = data.values.map(value => {
+ const parsed: {
+ index: number;
+ type: string;
+ data: string;
+ ttl: number;
+ timestamp: number;
+ } = JSON.parse(value);
+
+ const parsedType = JSON.parse(parsed.type);
+ let type: PIDDataType | PID | string;
+ if (parsedType.pidDataType !== undefined) {
+ type = PIDDataType.fromJSON(parsedType.pidDataType);
+ } else if (parsedType.pid !== undefined) {
+ type = PID.fromJSON(parsedType.pid);
+ } else {
+ type = parsedType.string as string;
+ }
+
+ const data: {
+ format: string;
+ value: string;
+ } = JSON.parse(parsed.data);
+
+ return {
+ index: parsed.index,
+ type: type,
+ data: data,
+ ttl: parsed.ttl,
+ timestamp: parsed.timestamp,
+ };
+ });
+ return new PIDRecord(PID.fromJSON(data.pid), values);
+ }
+
+ toObject() {
+ return {
+ pid: JSON.stringify(this._pid.toObject()),
+ values: this._values.map(value =>
+ JSON.stringify({
+ index: value.index,
+ type: JSON.stringify({
+ pid: value.type instanceof PID ? JSON.stringify(value.type.toObject()) : undefined,
+ pidDataType: value.type instanceof PIDDataType ? JSON.stringify(value.type.toObject()) : undefined,
+ string: typeof value.type == 'string' ? value.type : undefined,
+ }),
+ data: JSON.stringify(value.data),
+ ttl: value.ttl,
+ timestamp: value.timestamp,
+ }),
+ ),
+ };
+ }
+}
diff --git a/packages/stencil-library/src/rendererModules/LocaleType.tsx b/packages/stencil-library/src/rendererModules/LocaleType.tsx
new file mode 100644
index 0000000..e90c652
--- /dev/null
+++ b/packages/stencil-library/src/rendererModules/LocaleType.tsx
@@ -0,0 +1,30 @@
+import { FunctionalComponent, h } from '@stencil/core';
+import { GenericIdentifierType } from '../utils/GenericIdentifierType';
+
+/**
+ * This class specifies a custom renderer for Email addresses.
+ * @extends GenericIdentifierType
+ */
+export class LocaleType extends GenericIdentifierType {
+ getSettingsKey(): string {
+ return 'LocaleType';
+ }
+
+ hasCorrectFormat(): boolean {
+ const regex = /^([a-zA-Z]{2})(-[A-Z]{2})?$/;
+ return regex.test(this.value);
+ }
+
+ init(): Promise {
+ return;
+ }
+
+ isResolvable(): boolean {
+ return false;
+ }
+
+ renderPreview(): FunctionalComponent {
+ // mail icon from: https://heroicons.com/ (MIT license)
+ return ;
+ }
+}
diff --git a/src/utils/ORCIDInfo.ts b/packages/stencil-library/src/rendererModules/ORCiD/ORCIDInfo.ts
similarity index 72%
rename from src/utils/ORCIDInfo.ts
rename to packages/stencil-library/src/rendererModules/ORCiD/ORCIDInfo.ts
index 42cba1a..811094e 100644
--- a/src/utils/ORCIDInfo.ts
+++ b/packages/stencil-library/src/rendererModules/ORCiD/ORCIDInfo.ts
@@ -1,4 +1,4 @@
-import { init } from './DataCache';
+import { cachedFetch } from '../../utils/DataCache';
/**
* This file contains the ORCIDInfo class, which is used to store information about an ORCiD.
@@ -32,32 +32,7 @@ export class ORCIDInfo {
* @private
* @type {{startDate: Date, endDate: Date | null, organization: string, department: string}[]}
*/
- private readonly _employments?: {
- /**
- * The start date of the employment.
- * @type {Date}
- */
- startDate: Date;
-
- /**
- * The end date of the employment.
- * If the employment is still ongoing, this is null.
- * @type {Date | null}
- */
- endDate: Date | null;
-
- /**
- * The organization of the employment.
- * @type {string}
- */
- organization: string;
-
- /**
- * The department of the employment.
- * @type {string}
- */
- department: string;
- }[];
+ private readonly _employments?: Employment[];
/**
* The preferred locale of the person.
@@ -177,12 +152,7 @@ export class ORCIDInfo {
ORCiDJSON: object,
familyName: string,
givenNames: string[],
- employments?: {
- startDate: Date;
- endDate: Date | null;
- organization: string;
- department: string;
- }[],
+ employments?: Employment[],
preferredLocale?: string,
biography?: string,
emails?: {
@@ -248,12 +218,7 @@ export class ORCIDInfo {
* It is a list of objects, each containing the start date, end date, organization and department of the employment.
* @returns {{startDate: Date, endDate: Date | null, organization: string, department: string}[]} The list of employments of the person.
*/
- get employments(): {
- startDate: Date;
- endDate: Date | null;
- organization: string;
- department: string;
- }[] {
+ get employments(): Employment[] {
return this._employments;
}
@@ -305,59 +270,14 @@ export class ORCIDInfo {
return this._country;
}
- /**
- * Outputs the employment object of the person at a given date.
- * @param date The date to check.
- * @returns {{startDate: Date, endDate: Date | null, organization: string, department: string} | undefined} The employment object of the person at the given date.
- */
- getAffiliationsAt(date: Date): {
- startDate: Date;
- endDate: Date | null;
- organization: string;
- department: string;
- }[] {
- let affiliations: {
- startDate: Date;
- endDate: Date | null;
- organization: string;
- department: string;
- }[] = [];
- for (const employment of this._employments) {
- if (employment.startDate <= date && employment.endDate === null) affiliations.push(employment);
- if (employment.startDate <= date && employment.endDate !== null && employment.endDate >= date) affiliations.push(employment);
- }
- return affiliations;
- }
-
- /**
- * Outputs a string representation of an affiliation object.
- * @param affiliation The affiliation object to convert to a string.
- * @param showDepartment Whether to show the department in the string.
- * @returns {string | undefined} The string representation of the affiliation object.
- */
- getAffiliationAsString(
- affiliation: {
- startDate: Date;
- endDate: Date | null;
- organization: string;
- department: string;
- },
- showDepartment: boolean = true,
- ): string | undefined {
- if (affiliation === undefined || affiliation.organization === null) return undefined;
- else {
- if (showDepartment && affiliation.department !== null) return `${affiliation.organization} [${affiliation.department}]`;
- else return affiliation.organization;
- }
- }
-
/**
* Checks if a string has the format of an ORCiD.
* @param text The string to check.
* @returns {boolean} True if the string could be an ORCiD, false if not.
*/
static isORCiD(text: string): boolean {
- return text.match('^(https://orcid.org/)?[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]$') !== null;
+ const regex = new RegExp('^(https://orcid.org/)?[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]$');
+ return regex.test(text);
}
/**
@@ -369,34 +289,37 @@ export class ORCIDInfo {
if (!ORCIDInfo.isORCiD(orcid)) throw new Error('Invalid input');
if (orcid.match('^(https://orcid.org/)?[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]$') !== null) orcid = orcid.replace('https://orcid.org/', '');
- const dataCache = await init('pid-component');
- const rawOrcidJSON = await dataCache.fetch(`https://pub.orcid.org/v3.0/${orcid}`, {
+ const rawOrcidJSON = await cachedFetch(`https://pub.orcid.org/v3.0/${orcid}`, {
headers: {
Accept: 'application/json',
},
});
+ // .then(response => response.json());
+
+ let familyName = '';
+ let givenNames = [];
+
+ try {
+ familyName = rawOrcidJSON['person']['name']['family-name']['value'];
+ } catch (e) {
+ console.debug(e);
+ }
- // Parse family name and given names
- const familyName = rawOrcidJSON['person']['name']['family-name']['value'];
- const givenNames = rawOrcidJSON['person']['name']['given-names']['value'];
+ try {
+ givenNames = rawOrcidJSON['person']['name']['given-names']['value'];
+ } catch (e) {
+ console.debug(e);
+ }
+ // const familyName = rawOrcidJSON['person']['name']['family-name']['value'] ? rawOrcidJSON['person']['name']['family-name']['value'] : '';
+ // const givenNames = rawOrcidJSON['person']['name']['given-names']['value'] ? rawOrcidJSON['person']['name']['given-names']['value'] : '';
// Parse employments, if available
const affiliations = rawOrcidJSON['activities-summary']['employments']['affiliation-group'];
- let employments: {
- startDate: Date;
- endDate: Date | null;
- organization: string;
- department: string;
- }[] = [];
+ const employments: Employment[] = [];
try {
for (let i = 0; i < affiliations.length; i++) {
const employmentSummary = affiliations[i]['summaries'][0]['employment-summary'];
- let employment = {
- startDate: new Date(),
- endDate: null,
- organization: null,
- department: null,
- };
+ const employment = new Employment(new Date(), null, '', '');
if (employmentSummary['start-date'] !== null)
employment.startDate = new Date(
employmentSummary['start-date']['year']['value'],
@@ -414,16 +337,18 @@ export class ORCIDInfo {
employments.push(employment);
}
- } catch (e) {}
+ } catch (e) {
+ console.debug(e);
+ }
// Parse preferred locale, if available
- let preferredLocale: string | undefined = rawOrcidJSON['preferences']['locale'] !== null ? rawOrcidJSON['preferences']['locale'] : undefined;
+ const preferredLocale: string | undefined = rawOrcidJSON['preferences']['locale'] !== null ? rawOrcidJSON['preferences']['locale'] : undefined;
// Parse biography, if available
- let biography: string | undefined = rawOrcidJSON['person']['biography'] !== null ? rawOrcidJSON['person']['biography']['content'] : undefined;
+ const biography: string | undefined = rawOrcidJSON['person']['biography'] !== null ? rawOrcidJSON['person']['biography']['content'] : undefined;
// Parse e-mail addresses, if available
- let emails: { email: string; primary: boolean; verified: boolean }[] | undefined = [];
+ const emails: { email: string; primary: boolean; verified: boolean }[] | undefined = [];
if (rawOrcidJSON['person']['emails']['email'] !== null) {
for (const email of rawOrcidJSON['person']['emails']['email']) {
emails.push({
@@ -435,7 +360,7 @@ export class ORCIDInfo {
}
// Parse keywords, if available, and sort them by index
- let keywords: { content: string; index: number }[] | undefined = [];
+ const keywords: { content: string; index: number }[] | undefined = [];
if (rawOrcidJSON['person']['keywords']['keyword'] !== null) {
for (const keyword of rawOrcidJSON['person']['keywords']['keyword']) {
keywords.push({
@@ -447,7 +372,7 @@ export class ORCIDInfo {
}
// Parse researcher URLs, if available, and sort them by index
- let researcherUrls: { url: string; name: string; index: number }[] | undefined = [];
+ const researcherUrls: { url: string; name: string; index: number }[] | undefined = [];
if (rawOrcidJSON['person']['researcher-urls']['researcher-url'] !== null) {
for (const researcherUrl of rawOrcidJSON['person']['researcher-urls']['researcher-url']) {
researcherUrls.push({
@@ -460,9 +385,122 @@ export class ORCIDInfo {
}
// Parse country, if available
- let country: string | undefined = rawOrcidJSON['person']['addresses']['address'].length > 0 ? rawOrcidJSON['person']['addresses']['address'][0]['country']['value'] : undefined;
+ const country: string | undefined =
+ rawOrcidJSON['person']['addresses']['address'].length > 0 ? rawOrcidJSON['person']['addresses']['address'][0]['country']['value'] : undefined;
// Return the ORCIDInfo object
return new ORCIDInfo(orcid, rawOrcidJSON, familyName, givenNames, employments, preferredLocale, biography, emails, keywords, researcherUrls, country);
}
+
+ static fromJSON(serialized: string): ORCIDInfo {
+ const data: ReturnType = JSON.parse(serialized);
+ const employments = data.employments.map(employment => Employment.fromJSON(employment));
+ return new ORCIDInfo(
+ data.orcid,
+ data.ORCiDJSON,
+ data.familyName,
+ data.givenNames,
+ employments,
+ data.preferredLocale,
+ data.biography,
+ data.emails,
+ data.keywords,
+ data.researcherUrls,
+ data.country,
+ );
+ }
+
+ /**
+ * Outputs the employment object of the person at a given date.
+ * @param date The date to check.
+ * @returns {{startDate: Date, endDate: Date | null, organization: string, department: string} | undefined} The employment object of the person at the given date.
+ */
+ getAffiliationsAt(date: Date): Employment[] {
+ const affiliations: Employment[] = [];
+ for (const employment of this._employments) {
+ if (employment.startDate <= date && employment.endDate === null) affiliations.push(employment);
+ if (employment.startDate <= date && employment.endDate !== null && employment.endDate >= date) affiliations.push(employment);
+ }
+ return affiliations;
+ }
+
+ /**
+ * Outputs a string representation of an affiliation object.
+ * @param affiliation The affiliation object to convert to a string.
+ * @param showDepartment Whether to show the department in the string.
+ * @returns {string | undefined} The string representation of the affiliation object.
+ */
+ getAffiliationAsString(affiliation: Employment, showDepartment: boolean = true): string | undefined {
+ if (affiliation === undefined || affiliation.organization === null) return undefined;
+ else {
+ if (showDepartment && affiliation.department !== null) return `${affiliation.organization} [${affiliation.department}]`;
+ else return affiliation.organization;
+ }
+ }
+
+ toObject() {
+ return {
+ orcid: this._orcid,
+ ORCiDJSON: this._ORCiDJSON,
+ familyName: this._familyName,
+ givenNames: this._givenNames,
+ employments: this._employments.map(employment => JSON.stringify(employment.toObject())),
+ preferredLocale: this._preferredLocale,
+ biography: this._biography,
+ emails: this._emails,
+ keywords: this._keywords,
+ researcherUrls: this._researcherUrls,
+ country: this._country,
+ };
+ }
+}
+
+class Employment {
+ /**
+ * The start date of the employment.
+ * @type {Date}
+ */
+ startDate: Date;
+
+ /**
+ * The end date of the employment.
+ * If the employment is still ongoing, this is null.
+ * @type {Date | null}
+ */
+ endDate: Date | null;
+
+ /**
+ * The organization of the employment.
+ * @type {string}
+ */
+ organization: string;
+
+ /**
+ * The department of the employment.
+ * @type {string}
+ */
+ department: string;
+
+ constructor(startDate: Date, endDate: Date | null, organization: string, department: string) {
+ this.startDate = startDate;
+ this.endDate = endDate;
+ this.organization = organization;
+ this.department = department;
+ }
+
+ static fromJSON(serialized: string): Employment {
+ const data: ReturnType = JSON.parse(serialized);
+ const startDate = new Date(data.startDate);
+ const endDate = data.endDate === null ? null : new Date(data.endDate);
+ return new Employment(startDate, endDate, data.organization, data.department);
+ }
+
+ toObject() {
+ return {
+ startDate: this.startDate,
+ endDate: this.endDate,
+ organization: this.organization,
+ department: this.department,
+ };
+ }
}
diff --git a/src/utils/ORCIDType.tsx b/packages/stencil-library/src/rendererModules/ORCiD/ORCIDType.tsx
similarity index 59%
rename from src/utils/ORCIDType.tsx
rename to packages/stencil-library/src/rendererModules/ORCiD/ORCIDType.tsx
index ee507a7..c4007b1 100644
--- a/src/utils/ORCIDType.tsx
+++ b/packages/stencil-library/src/rendererModules/ORCiD/ORCIDType.tsx
@@ -1,9 +1,8 @@
import { FunctionalComponent, h } from '@stencil/core';
-import { GenericIdentifierType } from './GenericIdentifierType';
-import { FoldableItem } from './FoldableItem';
-import { FoldableAction } from './FoldableAction';
+import { GenericIdentifierType } from '../../utils/GenericIdentifierType';
import { ORCIDInfo } from './ORCIDInfo';
-import { getLocaleDetail } from './utils';
+import { FoldableItem } from '../../utils/FoldableItem';
+import { FoldableAction } from '../../utils/FoldableAction';
/**
* This class specifies a custom renderer for ORCiDs.
@@ -33,16 +32,26 @@ export class ORCIDType extends GenericIdentifierType {
*/
private showAffiliation: boolean = true;
+ get data(): string {
+ return JSON.stringify(this._orcidInfo.toObject());
+ }
+
hasCorrectFormat(): boolean {
return ORCIDInfo.isORCiD(this.value);
}
- async init(): Promise {
- let parsed = await ORCIDInfo.getORCiDInfo(this.value);
- this._orcidInfo = parsed;
+ async init(data?: string): Promise {
+ if (data !== undefined) {
+ // this._orcidInfo = new ORCIDInfo(data.orcid, data.ORCiDJSON, data.familyName, data.givenNames, data.employments, data.preferredLocale, data.biography, data.emails, data.keywords, data.researcherUrls, data.country);
+ this._orcidInfo = ORCIDInfo.fromJSON(data);
+ console.debug('reload ORCIDInfo from data', this._orcidInfo);
+ } else {
+ this._orcidInfo = await ORCIDInfo.getORCiDInfo(this.value);
+ console.debug('load ORCIDInfo from API', this._orcidInfo);
+ }
if (this.settings) {
- for (let i of this.settings) {
+ for (const i of this.settings) {
switch (i['name']) {
case 'affiliationAt':
this.affiliationAt = new Date(i['value']);
@@ -55,44 +64,57 @@ export class ORCIDType extends GenericIdentifierType {
}
// Generate items and actions
-
this.items.push(
- ...[
- new FoldableItem(
- 0,
- 'ORCiD',
- parsed.orcid,
- 'ORCiD is a free service for researchers to distinguish themselves by creating a unique personal identifier.',
- 'https://orcid.org',
- undefined,
- true,
- ),
- new FoldableItem(1, 'Family Name', parsed.familyName, 'The family name of the person.'),
- new FoldableItem(2, 'Given Names', parsed.givenNames.toString(), 'The given names of the person.'),
- ],
+ new FoldableItem(
+ 0,
+ 'ORCiD',
+ this._orcidInfo.orcid,
+ 'ORCiD is a free service for researchers to distinguish themselves by creating a unique personal identifier.',
+ 'https://orcid.org',
+ undefined,
+ true,
+ ),
);
- this.actions.push(new FoldableAction(0, 'Open ORCiD profile', `https://orcid.org/${parsed.orcid}`, 'primary'));
+ try {
+ const familyName = this._orcidInfo.familyName;
+ if (familyName) {
+ new FoldableItem(1, 'Family Name', this._orcidInfo.familyName, 'The family name of the person.');
+ }
+ } catch (e) {
+ console.log('Failed to obtain family name from ORCiD record.', e);
+ }
+
+ try {
+ const givenNames = this._orcidInfo.givenNames;
+ if (givenNames) {
+ new FoldableItem(2, 'Given Names', this._orcidInfo.givenNames.toString(), 'The given names of the person.');
+ }
+ } catch (e) {
+ console.log('Failed to obtain given names from ORCiD record.', e);
+ }
+
+ this.actions.push(new FoldableAction(0, 'Open ORCiD profile', `https://orcid.org/${this._orcidInfo.orcid}`, 'primary'));
try {
- const affiliations = parsed.getAffiliationsAt(new Date(Date.now()));
- for (let data of affiliations) {
- const affiliation = parsed.getAffiliationAsString(data);
+ const affiliations = this._orcidInfo.getAffiliationsAt(new Date(Date.now()));
+ for (const data of affiliations) {
+ const affiliation = this._orcidInfo.getAffiliationAsString(data);
if (affiliation !== undefined && affiliation.length > 2)
this.items.push(new FoldableItem(50, 'Current Affiliation', affiliation, 'The current affiliation of the person.', undefined, undefined, false));
}
} catch (e) {
- console.log("Failed to obtain affiliations from ORCiD record.", e);
+ console.log('Failed to obtain affiliations from ORCiD record.', e);
}
if (
- parsed.getAffiliationsAt(this.affiliationAt) !== parsed.getAffiliationsAt(new Date()) &&
+ this._orcidInfo.getAffiliationsAt(this.affiliationAt) !== this._orcidInfo.getAffiliationsAt(new Date()) &&
this.affiliationAt.toLocaleDateString('en-US') !== new Date().toLocaleDateString('en-US')
) {
- const affiliationsThen = parsed.getAffiliationsAt(this.affiliationAt);
+ const affiliationsThen = this._orcidInfo.getAffiliationsAt(this.affiliationAt);
- for (let data of affiliationsThen) {
- const affiliation = parsed.getAffiliationAsString(data);
+ for (const data of affiliationsThen) {
+ const affiliation = this._orcidInfo.getAffiliationAsString(data);
this.items.push(
new FoldableItem(
49,
@@ -111,9 +133,9 @@ export class ORCIDType extends GenericIdentifierType {
);
}
}
- if (parsed.emails) {
- let primary = parsed.emails.filter(email => email.primary)[0];
- let other = parsed.emails.filter(email => !email.primary);
+ if (this._orcidInfo.emails) {
+ const primary = this._orcidInfo.emails.filter(email => email.primary)[0];
+ const other = this._orcidInfo.emails.filter(email => !email.primary);
// If there is a primary e-mail address, generate an item and an action to send email
if (primary) {
@@ -123,35 +145,23 @@ export class ORCIDType extends GenericIdentifierType {
// If there are other e-mail addresses, generate an item with a list of them
if (other.length > 0)
- this.items.push(new FoldableItem(70, 'Other E-Mail addresses', other.map(email => email.email).join(', '), 'All other e-mail addresses of the person.', undefined, undefined, false));
+ this.items.push(new FoldableItem(70, 'Other E-Mail addresses', other.map(email => email.email).join(', '), 'All other e-mail addresses of the person.'));
- if (parsed.preferredLocale)
- this.items.push(
- new FoldableItem(
- 25,
- 'Preferred Language',
- getLocaleDetail(parsed.preferredLocale, 'language'),
- 'The preferred locale/language of the person.',
- undefined,
- undefined,
- false,
- ),
- );
+ if (this._orcidInfo.preferredLocale)
+ this.items.push(new FoldableItem(25, 'Preferred Language', this._orcidInfo.preferredLocale, 'The preferred locale/language of the person.'));
- for (let url of parsed.researcherUrls) {
+ for (const url of this._orcidInfo.researcherUrls) {
this.items.push(new FoldableItem(100, url.name, url.url, 'A link to a website specified by the person.'));
}
- if (parsed.keywords.length > 50)
+ if (this._orcidInfo.keywords.length > 50)
this.items.push(
- new FoldableItem(60, 'Keywords', parsed.keywords.map(keyword => keyword.content).join(', '), 'Keywords specified by the person.', undefined, undefined, false),
+ new FoldableItem(60, 'Keywords', this._orcidInfo.keywords.map(keyword => keyword.content).join(', '), 'Keywords specified by the person.', undefined, undefined, false),
);
- if (parsed.biography) this.items.push(new FoldableItem(200, 'Biography', parsed.biography, 'The biography of the person.', undefined, undefined, false));
-
- if (parsed.country) this.items.push(new FoldableItem(30, 'Country', getLocaleDetail(parsed.country, 'region'), 'The country of the person.', undefined, undefined, false));
+ if (this._orcidInfo.biography) this.items.push(new FoldableItem(200, 'Biography', this._orcidInfo.biography, 'The biography of the person.', undefined, undefined, false));
- console.log(this._orcidInfo);
+ if (this._orcidInfo.country) this.items.push(new FoldableItem(30, 'Country', this._orcidInfo.country, 'The country of the person.'));
}
}
@@ -199,6 +209,6 @@ export class ORCIDType extends GenericIdentifierType {
}
getSettingsKey(): string {
- return 'ORCIDConfig';
+ return 'ORCIDType';
}
}
diff --git a/src/utils/URLType.tsx b/packages/stencil-library/src/rendererModules/URLType.tsx
similarity index 91%
rename from src/utils/URLType.tsx
rename to packages/stencil-library/src/rendererModules/URLType.tsx
index 2a87749..4ff99cc 100644
--- a/src/utils/URLType.tsx
+++ b/packages/stencil-library/src/rendererModules/URLType.tsx
@@ -1,5 +1,5 @@
-import { GenericIdentifierType } from './GenericIdentifierType';
import { FunctionalComponent, h } from '@stencil/core';
+import { GenericIdentifierType } from '../utils/GenericIdentifierType';
/**
* This class specifies a custom renderer for URLs.
diff --git a/src/tailwind.css b/packages/stencil-library/src/tailwind.css
similarity index 100%
rename from src/tailwind.css
rename to packages/stencil-library/src/tailwind.css
diff --git a/packages/stencil-library/src/utils/DataCache.ts b/packages/stencil-library/src/utils/DataCache.ts
new file mode 100644
index 0000000..91a259a
--- /dev/null
+++ b/packages/stencil-library/src/utils/DataCache.ts
@@ -0,0 +1,60 @@
+let cacheInstance: Cache;
+
+async function open() {
+ if ('caches' in self) {
+ cacheInstance = await caches.open('pid-component');
+ }
+}
+
+/**
+ * Fetches a resource from the cache or the network.
+ * Acts as a wrapper around the Cache API and the fetch API.
+ * @param url The URL of the resource to fetch.
+ * @param init The options for the fetch request.
+ * @returns {Promise} The response of the fetch request.
+ */
+export async function cachedFetch(url: string, init?: any): Promise {
+ await open();
+ if (cacheInstance) {
+ // If there is a cache available, check if the resource is cached.
+ const response = await cacheInstance.match(url);
+ if (response) {
+ // If the resource is cached, return it.
+ return response.json();
+ } else {
+ // If the resource is not cached, fetch it from the network, cache and return it.
+ let response: Response;
+ const parts = url.split('://');
+ if (parts[0] !== 'https') {
+ // if not https, make it https
+ response = await fetch(`https://${parts[1]}`, init);
+ if (!response) {
+ // if https fails, try http as fallback
+ console.log(`404 for https://${parts[1]} - trying http://${parts[1]}`);
+ response = await fetch(`http://${parts[1]}`, init);
+ }
+ } else {
+ // if https, use it
+ response = await fetch(url, init);
+ }
+
+ // add to cache and return
+ await cacheInstance.put(url, response.clone());
+ return response.json();
+ }
+ } else {
+ // If there is no cache available, fetch the resource from the network.
+ const response = await fetch(url, init);
+ return response.json();
+ }
+}
+
+/**
+ * Clears the cache.
+ * @returns {Promise} A promise that resolves when the cache is cleared.
+ */
+export async function clearCache(): Promise {
+ if (cacheInstance) {
+ await cacheInstance.delete('pid-component');
+ }
+}
diff --git a/src/utils/FoldableAction.ts b/packages/stencil-library/src/utils/FoldableAction.ts
similarity index 100%
rename from src/utils/FoldableAction.ts
rename to packages/stencil-library/src/utils/FoldableAction.ts
diff --git a/src/utils/FoldableItem.ts b/packages/stencil-library/src/utils/FoldableItem.ts
similarity index 96%
rename from src/utils/FoldableItem.ts
rename to packages/stencil-library/src/utils/FoldableItem.ts
index 935b1f1..1ff6c19 100644
--- a/src/utils/FoldableItem.ts
+++ b/packages/stencil-library/src/utils/FoldableItem.ts
@@ -1,7 +1,8 @@
import { Parser } from './Parser';
+import { renderers } from './utils';
/**
- * This is a class that is used to represent every line in the pid-component.
+ * This is a class used to represent every line in the pid-component.
*/
export class FoldableItem {
/**
@@ -83,7 +84,7 @@ export class FoldableItem {
this._valueRegex = valueRegex;
this._renderDynamically = renderDynamically;
// If the value shouldn't be rendered dynamically, the estimated type priority is the highest value possible (very unimportant information).
- if (renderDynamically) this._estimatedTypePriority = Parser._dataTypes.length;
+ if (renderDynamically) this._estimatedTypePriority = renderers.length;
else this._estimatedTypePriority = Parser.getEstimatedPriority(this._value);
}
diff --git a/src/utils/GenericIdentifierType.ts b/packages/stencil-library/src/utils/GenericIdentifierType.ts
similarity index 92%
rename from src/utils/GenericIdentifierType.ts
rename to packages/stencil-library/src/utils/GenericIdentifierType.ts
index 602a016..d269f79 100644
--- a/src/utils/GenericIdentifierType.ts
+++ b/packages/stencil-library/src/utils/GenericIdentifierType.ts
@@ -15,30 +15,6 @@ export abstract class GenericIdentifierType {
*/
private readonly _value: string;
- /**
- * The settings of the environment from which the settings for the component are extracted.
- * @private
- * @type {{name: string, value: any}[]}
- */
- private _settings: {
- name: string;
- value: any;
- }[] = [];
-
- /**
- * The list of items that should be rendered in the component.
- * @private
- * @type {FoldableItem[]}
- */
- private _items: FoldableItem[] = [];
-
- /**
- * The list of actions that should be rendered in the component.
- * @private
- * @type {FoldableAction[]}
- */
- private _actions: FoldableAction[] = [];
-
/**
* Creates a new GenericIdentifierType object
* @param value The value that should be parsed and rendered
@@ -58,12 +34,14 @@ export abstract class GenericIdentifierType {
}
/**
- * Returns the value that should be parsed and rendered
- * @returns {string} The value that should be parsed and rendered
+ * The settings of the environment from which the settings for the component are extracted.
+ * @private
+ * @type {{name: string, value: any}[]}
*/
- get value(): string {
- return this._value;
- }
+ private _settings: {
+ name: string;
+ value: any;
+ }[] = [];
/**
* Returns the settings of the environment from which the settings for the component are extracted.
@@ -81,6 +59,13 @@ export abstract class GenericIdentifierType {
this._settings = value;
}
+ /**
+ * The list of items that should be rendered in the component.
+ * @private
+ * @type {FoldableItem[]}
+ */
+ private _items: FoldableItem[] = [];
+
/**
* Returns the list of items that should be rendered in the component.
* @returns {FoldableItem[]} The list of items that should be rendered in the component.
@@ -89,6 +74,13 @@ export abstract class GenericIdentifierType {
return this._items;
}
+ /**
+ * The list of actions that should be rendered in the component.
+ * @private
+ * @type {FoldableAction[]}
+ */
+ private _actions: FoldableAction[] = [];
+
/**
* Sets the list of items that should be rendered in the component.
* @return {FoldableItem[]} The list of items that should be rendered in the component.
@@ -98,21 +90,30 @@ export abstract class GenericIdentifierType {
}
/**
- * This asynchronous method is called when the component is initialized.
- * It should be used to fetch data from external sources and generate the items and actions that should be rendered in the component.
- * It must be implemented by the child classes as it is abstract.
- * @abstract
+ * Returns the value that should be parsed and rendered
+ * @returns {string} The value that should be parsed and rendered
+ */
+ get value(): string {
+ return this._value;
+ }
+
+ /**
+ * Returns the data that is being rendered in the component.
+ * By default, it returns undefined, which means that there is no meaningful data.
+ * @returns {any} The data that is needed for rendering the component.
*/
- abstract init(): Promise;
+ get data(): any {
+ return undefined;
+ }
/**
- * This method indicates if a value is resolvable or not.
- * It could be used to resolve the value via an external API and check if the returned value is valid or even existing.
+ * This asynchronous method is called when the component is initialized.
+ * It should be used to fetch data from external sources and generate the items and actions that should be rendered in the component.
* It must be implemented by the child classes as it is abstract.
- * @returns {boolean} Whether the value is resolvable or not.
+ * @param data The data that is needed for rendering the component.
* @abstract
*/
- abstract isResolvable(): boolean;
+ abstract init(data?: any): Promise;
/**
* This method indicates if a value has the correct format or not.
diff --git a/packages/stencil-library/src/utils/IndexedDBUtil.ts b/packages/stencil-library/src/utils/IndexedDBUtil.ts
new file mode 100644
index 0000000..8556631
--- /dev/null
+++ b/packages/stencil-library/src/utils/IndexedDBUtil.ts
@@ -0,0 +1,216 @@
+import { GenericIdentifierType } from './GenericIdentifierType';
+import { Parser } from './Parser';
+import { renderers } from './utils';
+import { DBSchema, openDB } from '@tempfix/idb';
+
+const dbName: string = 'pid-component';
+const dbVersion: number = undefined;
+
+/**
+ * The database schema for the PID component.
+ * @interface PIDComponentDB
+ * @extends DBSchema
+ */
+export interface PIDComponentDB extends DBSchema {
+ entities: {
+ key: string;
+ value: {
+ value: string;
+ rendererKey: string;
+ context: string;
+ lastAccess: Date;
+ lastData: any;
+ };
+ indexes: {
+ 'by-context': string;
+ };
+ };
+
+ relations: {
+ key: string;
+ value: {
+ start: string;
+ description: string;
+ end: string;
+ };
+ indexes: {
+ 'by-start': string;
+ 'by-end': string;
+ 'by-description': string;
+ };
+ };
+}
+
+/**
+ * Opens the indexedDB database for the PID component and creates the object stores and indexes if they do not exist.
+ * @type {Promise>}
+ * @const
+ */
+const dbPromise = openDB(dbName, dbVersion, {
+ upgrade(db) {
+ const entityStore = db.createObjectStore('entities', {
+ keyPath: 'value',
+ });
+ entityStore.createIndex('by-context', 'context', { unique: false });
+
+ const relationStore = db.createObjectStore('relations', {
+ autoIncrement: true,
+ });
+
+ // Create indexes for the relations
+ relationStore.createIndex('by-start', 'start', { unique: false });
+ relationStore.createIndex('by-description', 'description', { unique: false });
+ relationStore.createIndex('by-end', 'end', { unique: false });
+ },
+});
+
+/**
+ * Adds an entity to the database.
+ * @param {GenericIdentifierType} renderer The renderer to add to the database.
+ */
+export async function addEntity(renderer: GenericIdentifierType) {
+ const context = document.documentURI;
+ const db = await dbPromise;
+
+ // Add the entity to the entities object store
+ await db
+ .add('entities', {
+ value: renderer.value,
+ rendererKey: renderer.getSettingsKey(),
+ context: context,
+ lastAccess: new Date(),
+ lastData: renderer.data,
+ })
+ .catch(reason => {
+ if (reason.name === 'ConstraintError') {
+ console.debug('Entity already exists', reason);
+ } else console.error('Could not add entity', reason);
+ });
+ console.debug('added entity', renderer);
+
+ // Add the relations to the relations object store
+ // Start a new transaction
+ const tx = db.transaction('relations', 'readwrite');
+ const promises = [];
+
+ for (const item of renderer.items) {
+ // Create a relation object
+ const relation = {
+ start: renderer.value,
+ description: item.keyTitle,
+ end: item.value,
+ };
+ // Check if the relation already exists
+ const index = tx.store.index('by-start');
+ let cursor = await index.openCursor();
+ while (cursor) {
+ if (cursor.value.start === relation.start && cursor.value.end === relation.end && cursor.value.description === relation.description) {
+ // relation already exists
+ return;
+ }
+ cursor = await cursor.continue();
+ }
+ // Add the relation to the relations object store if it does not exist
+ promises.push(tx.store.add(relation));
+ }
+ promises.push(tx.done);
+ await Promise.all(promises);
+ console.debug('added relations', promises);
+}
+
+/**
+ * Gets an entity from the database. If the entity does not exist, it is created.
+ * @returns {Promise} The renderer for the entity.
+ * @param {string} value The stringified value of the entity, e.g. the PID.
+ * @param {{type: string, values: {name: string, value: any}[]}[]} settings The settings for all renderers.
+ */
+export const getEntity = async function (
+ value: string,
+ settings: {
+ type: string;
+ values: {
+ name: string;
+ value: any;
+ }[];
+ }[],
+): Promise {
+ // Try to get the entity from the database
+ try {
+ const db = await dbPromise;
+ const entity:
+ | {
+ value: string;
+ rendererKey: string;
+ context: string;
+ lastAccess: Date;
+ lastData: any;
+ }
+ | undefined = await db.get('entities', value);
+
+ if (entity !== undefined) {
+ // If the entity was found, check if the TTL has expired
+ console.debug('Found entity for value in db', entity, value);
+ const entitySettings = settings.find(value => value.type === entity.rendererKey)?.values;
+ const ttl = entitySettings?.find(value => value.name === 'ttl');
+
+ if (ttl != undefined && ttl.value != undefined && (new Date().getTime() - entity.lastAccess.getTime() > ttl.value || ttl.value === 0)) {
+ // If the TTL has expired, delete the entity from the database and move on to creating a new one (down below)
+ console.log('TTL expired! Deleting entry in db', ttl.value, new Date().getTime() - entity.lastAccess.getTime());
+ await deleteEntity(value);
+ } else {
+ // If the TTL has not expired, get a new renderer and return it
+ console.log('TTL not expired or undefined', new Date().getTime() - entity.lastAccess.getTime());
+ const renderer = new (renderers.find(renderer => renderer.key === entity.rendererKey).constructor)(value, entitySettings);
+ renderer.settings = entitySettings;
+ await renderer.init(entity.lastData);
+ return renderer;
+ }
+ }
+ } catch (error) {
+ console.error('Could not get entity from db', error);
+ }
+
+ // If no entity was found, create a new one, initialize it and it to the database
+ console.debug('No valid entity found for value in db', value);
+ const renderer = await Parser.getBestFit(value, settings);
+ renderer.settings = settings.find(value => value.type === renderer.getSettingsKey())?.values;
+ await renderer.init();
+ await addEntity(renderer);
+ console.debug('added entity to db', value, renderer);
+ return renderer;
+};
+
+/**
+ * Deletes an entity from the database.
+ * @param value The value of the entity to delete.
+ */
+export async function deleteEntity(value: string) {
+ const db = await dbPromise;
+
+ // Delete the entity
+ await db.delete('entities', value);
+
+ // Delete all relations for the entity
+ const tx = db.transaction('relations', 'readwrite');
+ const index = tx.store.index('by-start');
+ let cursor = await index.openCursor();
+ while (cursor) {
+ if (cursor.value.start === value || cursor.value.end === value) {
+ await tx.store.delete(cursor.primaryKey);
+ }
+ cursor = await cursor.continue();
+ }
+ console.log('deleted entity', value);
+ await tx.done;
+}
+
+/**
+ * Clears all entities from the database.
+ * @returns {Promise} A promise that resolves when all entities have been deleted.
+ */
+export async function clearEntities() {
+ const db = await dbPromise;
+ await db.clear('entities');
+ await db.clear('relations');
+ console.log('cleared entities');
+}
diff --git a/src/utils/Parser.ts b/packages/stencil-library/src/utils/Parser.ts
similarity index 63%
rename from src/utils/Parser.ts
rename to packages/stencil-library/src/utils/Parser.ts
index d692240..87b42c3 100644
--- a/src/utils/Parser.ts
+++ b/packages/stencil-library/src/utils/Parser.ts
@@ -1,28 +1,10 @@
import { GenericIdentifierType } from './GenericIdentifierType';
-import { HandleType } from './HandleType';
-import { FallbackType } from './FallbackType';
-import { ORCIDType } from './ORCIDType';
-import { DateType } from './DateType';
-import { URLType } from './URLType';
-import { EmailType } from "./EmailType";
+import { renderers } from './utils';
/**
* Class that handles the parsing of a given value and returns the best fitting component object
*/
export class Parser {
- /**
- * Array of all component objects that can be used to parse a given value, ordered by priority (lower is better)
- * @type {(new(value: string, settings?: {name: string, value: any}[]) => GenericIdentifierType)[]}
- * @private
- */
- static readonly _dataTypes: (new (
- value: string,
- settings?: {
- name: string;
- value: any;
- }[],
- ) => GenericIdentifierType)[] = [DateType, ORCIDType, HandleType, EmailType, URLType, FallbackType];
-
/**
* Returns the priority of the best fitting component object for a given value (lower is better)
* @param value String value to parse and evaluate
@@ -30,8 +12,8 @@ export class Parser {
*/
static getEstimatedPriority(value: string): number {
let priority = 0;
- for (let i = 0; i < this._dataTypes.length; i++) {
- const obj = new this._dataTypes[i](value);
+ for (let i = 0; i < renderers.length; i++) {
+ const obj = new renderers[i].constructor(value);
if (obj.hasCorrectFormat()) {
priority = i;
break;
@@ -57,11 +39,11 @@ export class Parser {
}[],
): Promise {
// default to fallback
- let bestFit = new this._dataTypes[this._dataTypes.length - 1](value);
+ let bestFit = new renderers[renderers.length - 1].constructor(value);
// find best fit in _dataTypes array with the highest priority (lowest index has highest priority) and correct format
- for (let i = this._dataTypes.length - 1; i >= 0; i--) {
- const obj = new this._dataTypes[i](value);
+ for (let i = renderers.length - 1; i >= 0; i--) {
+ const obj = new renderers[i].constructor(value);
if (obj.hasCorrectFormat()) bestFit = obj;
}
@@ -70,7 +52,9 @@ export class Parser {
const settingsKey = bestFit.getSettingsKey();
const settingsValues = settings.find(value => value.type === settingsKey)?.values;
if (settingsValues) bestFit.settings = settingsValues;
- } catch (_) {}
+ } catch (e) {
+ console.warn('Error while adding settings to object:', e);
+ }
// initialize and return the object
await bestFit.init();
diff --git a/packages/stencil-library/src/utils/utils.ts b/packages/stencil-library/src/utils/utils.ts
new file mode 100644
index 0000000..9d4165d
--- /dev/null
+++ b/packages/stencil-library/src/utils/utils.ts
@@ -0,0 +1,75 @@
+import { PID } from '../rendererModules/Handle/PID';
+import { PIDDataType } from '../rendererModules/Handle/PIDDataType';
+import { PIDRecord } from '../rendererModules/Handle/PIDRecord';
+import { GenericIdentifierType } from './GenericIdentifierType';
+import { DateType } from '../rendererModules/DateType';
+import { ORCIDType } from '../rendererModules/ORCiD/ORCIDType';
+import { HandleType } from '../rendererModules/Handle/HandleType';
+import { EmailType } from '../rendererModules/EmailType';
+import { URLType } from '../rendererModules/URLType';
+import { FallbackType } from '../rendererModules/FallbackType';
+import { LocaleType } from '../rendererModules/LocaleType';
+
+/**
+ * Array of all component objects that can be used to parse a given value, ordered by priority (lower is better)
+ * @type {Array<{priority: number, key: string, constructor: GenericIdentifierType}>}
+ */
+export const renderers: {
+ priority: number;
+ key: string;
+ constructor: new (value: string, settings?: { name: string; value: any }[]) => GenericIdentifierType;
+}[] = [
+ {
+ priority: 0,
+ key: 'DateType',
+ constructor: DateType,
+ },
+ {
+ priority: 1,
+ key: 'ORCIDType',
+ constructor: ORCIDType,
+ },
+ {
+ priority: 2,
+ key: 'HandleType',
+ constructor: HandleType,
+ },
+ {
+ priority: 3,
+ key: 'EmailType',
+ constructor: EmailType,
+ },
+ {
+ priority: 4,
+ key: 'URLType',
+ constructor: URLType,
+ },
+ {
+ priority: 5,
+ key: 'LocaleType',
+ constructor: LocaleType,
+ },
+ {
+ priority: 5,
+ key: 'FallbackType',
+ constructor: FallbackType,
+ },
+];
+
+/**
+ * A map of all PID data types and their PIDs.
+ * @type {Map}
+ */
+export const typeMap: Map = new Map();
+
+/**
+ * A map of all PIDs and their PIDRecords.
+ * @type {Map}
+ */
+export const handleMap: Map = new Map();
+
+/**
+ * A set of all PIDs that are not resolvable.
+ * @type {Set}
+ */
+export const unresolvables: Set = new Set();
diff --git a/packages/stencil-library/stencil.config.ts b/packages/stencil-library/stencil.config.ts
new file mode 100644
index 0000000..1202ea0
--- /dev/null
+++ b/packages/stencil-library/stencil.config.ts
@@ -0,0 +1,53 @@
+import { Config } from '@stencil/core';
+import tailwind, { tailwindHMR } from 'stencil-tailwind-plugin';
+import { reactOutputTarget } from '@stencil/react-output-target';
+
+export const config: Config = {
+ namespace: 'pid-component',
+ outputTargets: [
+ {
+ type: 'dist-hydrate-script',
+ dir: './hydrate',
+ },
+ reactOutputTarget({
+ outDir: '../react-library/lib/components/stencil-generated/',
+ hydrateModule: '@kit-data-manager/pid-component/hydrate',
+ }),
+ {
+ type: 'dist',
+ esmLoaderPath: '../loader',
+ },
+ {
+ type: 'dist-custom-elements',
+ externalRuntime: false,
+ },
+ {
+ type: 'docs-readme',
+ },
+ {
+ type: 'www',
+ },
+ ],
+ testing: {
+ browserHeadless: 'new',
+ },
+ plugins: [tailwind(), tailwindHMR()],
+ extras: {
+ enableImportInjection: true,
+ },
+ preamble:
+ '\n' +
+ 'Copyright 2024 Karlsruhe Institute of Technology.\n' +
+ '\n' +
+ 'Licensed under the Apache License, Version 2.0 (the "License");\n' +
+ 'you may not use this file except in compliance with the License.\n' +
+ 'You may obtain a copy of the License at\n' +
+ '\n' +
+ ' http://www.apache.org/licenses/LICENSE-2.0\n' +
+ '\n' +
+ 'Unless required by applicable law or agreed to in writing, software\n' +
+ 'distributed under the License is distributed on an "AS IS" BASIS,\n' +
+ 'WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n' +
+ 'See the License for the specific language governing permissions and\n' +
+ 'limitations under the License.\n',
+};
diff --git a/tailwind.config.js b/packages/stencil-library/tailwind.config.ts
similarity index 63%
rename from tailwind.config.js
rename to packages/stencil-library/tailwind.config.ts
index e43a44b..31584b3 100644
--- a/tailwind.config.js
+++ b/packages/stencil-library/tailwind.config.ts
@@ -1,9 +1,10 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
+import type { Config } from 'tailwindcss';
+
+export default {
content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
darkMode: ['class', '[data-mode="dark"]'],
theme: {
extend: {},
},
plugins: [],
-};
+} satisfies Config;
diff --git a/packages/stencil-library/tsconfig.json b/packages/stencil-library/tsconfig.json
new file mode 100644
index 0000000..c8722a6
--- /dev/null
+++ b/packages/stencil-library/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "allowSyntheticDefaultImports": true,
+ "allowUnreachableCode": false,
+ "declaration": false,
+ "experimentalDecorators": true,
+ "lib": [
+ "dom",
+ "es2017"
+ ],
+ "moduleResolution": "node",
+ "module": "esnext",
+ "target": "es2017",
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "jsx": "react",
+ "jsxFactory": "h"
+ },
+ "include": [
+ "src"
+ ],
+ "exclude": [
+ "../../node_modules"
+ ]
+}
diff --git a/prettier.config.js b/prettier.config.js
deleted file mode 100644
index c261fc4..0000000
--- a/prettier.config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// prettier.config.js
-module.exports = {
- plugins: ['prettier-plugin-tailwindcss'],
- tailwindConfig: './tailwind.config.js',
-};
diff --git a/src/components/handle-highlight/handle-highlight.tsx b/src/components/handle-highlight/handle-highlight.tsx
deleted file mode 100644
index 406a649..0000000
--- a/src/components/handle-highlight/handle-highlight.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import { Component, Host, h, Prop, State } from '@stencil/core';
-import { PID } from '../../utils/PID';
-import { HSLColor } from '../../utils/HSLColor';
-
-/**
- * This component highlights a handle and links to the FAIR DO Scope.
- * It automatically generates colors for the parts of the handle (prefix and suffix) to make them easily distinguishable.
- */
-@Component({
- tag: 'handle-highlight',
- styleUrl: 'handle-highlight.css',
- shadow: true,
-})
-export class HandleHighlight {
- /**
- The private state of the parts of the handle. Consists of the text, the color and a boolean whether a next part exists.
- */
- @State() parts: { text: string; color: HSLColor; nextExists: boolean }[] = [];
-
- /**
- * The private state where this component should link to.
- */
- @State() link: string = '';
-
- /**
- * The Handle to highlight and link in this component.
- */
- @Prop() handle!: string;
-
- /**
- * An optional custom link to use instead of the default one which links to the FAIR DO Scope.
- */
- @Prop() linkTo: 'disable' | 'fairdoscope' | 'resolveRef' = 'fairdoscope';
-
- /**
- * Whether the component should use the filled or the outlined design.
- */
- @Prop() filled: boolean = false;
-
- /**
- * This method is called when the component is first connected to the DOM.
- * It generates the colors for the parts of the handle and stores them in the state.
- * Since the generation of the colors is asynchronous, the parts are added to the state as soon as they are generated.
- */
- async connectedCallback() {
- // Parse the PID
- const pid = PID.getPIDFromString(this.handle);
-
- // Generate the colors for the parts of the PID
- this.parts = [
- {
- text: pid.prefix,
- color: await HSLColor.generateColor(pid.prefix),
- nextExists: true,
- },
- {
- text: pid.suffix,
- color: await HSLColor.generateColor(pid.suffix),
- nextExists: false,
- },
- ];
-
- if (this.linkTo === 'fairdoscope') this.link = `https://kit-data-manager.github.io/fairdoscope/?pid=${this.handle}`;
- else if (this.linkTo === 'resolveRef') this.link = `https://hdl.handle.net/${this.handle}#resolve`;
- else if (this.linkTo === 'disable') this.link = '';
-
- console.log(this.link);
- console.log(this.linkTo);
- }
-
- render() {
- return (
-
- {
- if (this.link === '' || this.linkTo === 'disable') el.preventDefault();
- }}
- target={'_blank'}
- rel={'noopener noreferrer'}
- >
- {this.filled ? (
-
- {this.parts.map(element => {
- return (
-
- 50 ? 'text-gray-800' : 'text-gray-200'}`}
- >
- {element.text}
-
- {element.nextExists ? '/' : ''}
-
- );
- })}
-
- ) : (
-
- {this.parts.map(element => {
- return (
-
-
- {element.text}
-
- {element.nextExists ? '/' : ''}
-
- );
- })}
-
- )}
-
-
- );
- }
-}
diff --git a/src/components/handle-highlight/readme.md b/src/components/handle-highlight/readme.md
deleted file mode 100644
index 1128871..0000000
--- a/src/components/handle-highlight/readme.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# handle-highlight
-
-
-
-
-## Overview
-
-This component highlights a handle and links to the FAIR DO Scope.
-It automatically generates colors for the parts of the handle (prefix and suffix) to make them easily distinguishable.
-
-## Properties
-
-| Property | Attribute | Description | Type | Default |
-| --------------------- | --------- | ------------------------------------------------------------------------------------------- | -------------------------------------------- | --------------- |
-| `filled` | `filled` | Whether the component should use the filled or the outlined design. | `boolean` | `false` |
-| `handle` _(required)_ | `handle` | The Handle to highlight and link in this component. | `string` | `undefined` |
-| `linkTo` | `link-to` | An optional custom link to use instead of the default one which links to the FAIR DO Scope. | `"disable" \| "fairdoscope" \| "resolveRef"` | `'fairdoscope'` |
-
-
-----------------------------------------------
-
-*Built with [StencilJS](https://stenciljs.com/)*
diff --git a/src/index.html b/src/index.html
deleted file mode 100644
index 0d5b817..0000000
--- a/src/index.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
- PID-Component Example
-
-
-
-
-
-
PID-Component Example
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
- ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
- occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
- ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
- occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
- incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
- in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim
- id est laborum.
-
-
-
-
diff --git a/src/utils/DataCache.ts b/src/utils/DataCache.ts
deleted file mode 100644
index 23f56a7..0000000
--- a/src/utils/DataCache.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-/**
- * This class is a wrapper around the Cache API.
- */
-export class DataCache {
- /**
- * The name of the cache.
- * @type {string}
- * @private
- */
- private readonly name: string;
-
- /**
- * The opened cache instance from the Cache API.
- * @type {Cache}
- * @private
- */
- private cacheInstance: Cache = undefined;
-
- /**
- * Creates a new DataCache instance.
- * @param name The name of the cache.
- * @constructor
- */
- constructor(name: string) {
- this.name = name;
- }
-
- /**
- * Checks whether the Cache API is available in the browser and opens the cache.
- * @async
- */
- async open() {
- if ('caches' in self) {
- this.cacheInstance = await caches.open(this.name);
- }
- }
-
- /**
- * Fetches a resource from the cache or the network.
- * Acts as a wrapper around the Cache API and the fetch API.
- * @param url The URL of the resource to fetch.
- * @param init The options for the fetch request.
- * @returns {Promise} The response of the fetch request.
- */
- async fetch(url: string, init?: any): Promise {
- if (this.cacheInstance) {
- // If there is a cache available, check if the resource is cached.
- const response = await this.cacheInstance.match(url);
- if (response) {
- // If the resource is cached, return it.
- return response.json();
- } else {
- // If the resource is not cached, fetch it from the network, cache and return it.
- let response: Response;
- const parts = url.split('://');
- if (parts[0] !== 'https') {
- // if not https, make it https
- response = await fetch(`https://${parts[1]}`, init);
- if (!response) {
- // if https fails, try http as fallback
- console.log(`404 for https://${parts[1]} - trying http://${parts[1]}`);
- response = await fetch(`http://${parts[1]}`, init);
- }
- } else {
- // if https, use it
- response = await fetch(url, init);
- }
-
- // add to cache and return
- await this.cacheInstance.put(url, response.clone());
- return response.json();
- }
- } else {
- // If there is no cache available, fetch the resource from the network.
- const response = await fetch(url, init);
- return response.json();
- }
- }
-}
-
-/**
- * Initializes a new DataCache instance.
- * @param name The name of the cache.
- * @returns {Promise} The initialized DataCache instance.
- * @async
- */
-export async function init(name: string): Promise {
- const dataCache = new DataCache(name);
- await dataCache.open();
- return dataCache;
-}
diff --git a/src/utils/EmailType.tsx b/src/utils/EmailType.tsx
deleted file mode 100644
index e2b5af7..0000000
--- a/src/utils/EmailType.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import {GenericIdentifierType} from './GenericIdentifierType';
-import {FunctionalComponent, h} from '@stencil/core';
-
-/**
- * This class specifies a custom renderer for Email addresses.
- * @extends GenericIdentifierType
- */
-export class EmailType extends GenericIdentifierType {
- getSettingsKey(): string {
- return 'EmailType';
- }
-
- hasCorrectFormat(): boolean {
- // const regex = /^((\(.*\))?(\w|-|\+|_|\.)+(\(.*\))?@((((\(.*\))?\w+(\(.*\))?\.)+(\(.*\))?\w+(\(.*\))?\w+)|(\[((IPv6:((\w+:+)+\w+))|(\d+\.)+\d+)\]))\s*(,+|$)\s*)+$/
- const regex = /^(([\w\-\.]+@([\w-]+\.)+[\w-]{2,})(\s*,\s*)?)*$/gm;
- return regex.test(this.value);
- }
-
- init(): Promise {
- return;
- }
-
- isResolvable(): boolean {
- return false;
- }
-
- renderPreview(): FunctionalComponent {
- // mail icon from: https://heroicons.com/ (MIT license)
- return (
-
- {this.value.split(new RegExp(/\s*,\s*/)).filter(email => email.length > 0).map(email => {
- return (
-
-
-
-
-
- {email}
-
-
- );
- })}
-
- );
- }
-}
diff --git a/src/utils/HandleType.tsx b/src/utils/HandleType.tsx
deleted file mode 100644
index 0e65ed8..0000000
--- a/src/utils/HandleType.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-import {HSLColor} from './HSLColor';
-import {PIDRecord} from './PIDRecord';
-import {PID} from './PID';
-import {PIDDataType} from './PIDDataType';
-import {FunctionalComponent, h} from '@stencil/core';
-import {GenericIdentifierType} from './GenericIdentifierType';
-import {FoldableItem} from './FoldableItem';
-import {FoldableAction} from './FoldableAction';
-
-/**
- * This class specifies a custom renderer for handles.
- * @extends GenericIdentifierType
- */
-export class HandleType extends GenericIdentifierType {
- /**
- * The parts of the PID separated by a slash.
- * @type {{text: string; color: HSLColor, nextExists: boolean}[]}
- * @private
- */
- private _parts: {
- /**
- * The text of the part.
- * @type {string}
- */
- text: string;
-
- /**
- * The generated color for the part.
- * @type {HSLColor}
- */
- color: HSLColor;
-
- /**
- * Whether there is a next part.
- * @type {boolean}
- */
- nextExists: boolean;
- }[] = [];
-
- /**
- * The PID record.
- * @type {PIDRecord}
- * @private
- */
- private _pidRecord: PIDRecord;
-
- hasCorrectFormat(): boolean {
- return PID.isPID(this.value);
- }
-
- async init(): Promise {
- const pid = PID.getPIDFromString(this.value);
-
- // Generate the colors for the parts of the PID
- this._parts = [
- {
- text: pid.prefix,
- color: await HSLColor.generateColor(pid.prefix),
- nextExists: true,
- },
- {
- text: pid.suffix,
- color: await HSLColor.generateColor(pid.suffix),
- nextExists: false,
- },
- ];
-
- // Resolve the PID
- const resolved = await pid.resolve();
- this._pidRecord = resolved;
- for (const value of resolved.values) {
- if (value.type instanceof PIDDataType) {
- this.items.push(new FoldableItem(0, value.type.name, value.data.value, value.type.description, value.type.redirectURL, value.type.regex));
- }
- }
-
- this.actions.push(new FoldableAction(0, 'Open in FAIR-DOscope', `https://kit-data-manager.github.io/fairdoscope/?pid=${resolved.pid.toString()}`, 'primary'));
-
- return;
- }
-
- isResolvable(): boolean {
- return this._pidRecord.values.length > 0;
- }
-
- renderPreview(): FunctionalComponent {
- return (
-
- {this._parts.map(element => {
- return (
-
-
- {element.text}
-
- {element.nextExists ? '/' : ''}
-
- );
- })}
-
- );
- }
-
- getSettingsKey(): string {
- return 'HandleConfig';
- }
-}
diff --git a/src/utils/PIDRecord.ts b/src/utils/PIDRecord.ts
deleted file mode 100644
index 978d4a7..0000000
--- a/src/utils/PIDRecord.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { PIDDataType } from './PIDDataType';
-import { PID } from './PID';
-
-/**
- * This class represents a PID record.
- */
-export class PIDRecord {
- /**
- * The PID of the record.
- * @type {PID}
- * @private
- */
- private readonly _pid: PID;
-
- /**
- * The values of the record.
- * @type {{
- * index: number,
- * type: PIDDataType | PID | string,
- * data: {
- * format: string,
- * value: string
- * },
- * ttl: number,
- * timestamp: number
- * }[]}
- * @private
- * @default []
- */
- private readonly _values: {
- /**
- * The index of the value in the record.
- * @type {number}
- */
- index: number;
-
- /**
- * The type of the value.
- * This can be a PID, a PID data type or a string.
- * If it is a string, it is most certainly not a PID.
- * If it is a PID, it couldn't be resolved to a PIDDataType.
- * If it is a PIDDataType, it has additional information that can be shown to the user.
- * @type {PIDDataType | PID | string}
- */
- type: PIDDataType | PID | string;
-
- /**
- * The data of the value.
- * @type {{
- * format: string,
- * value: string
- * }}
- */
- data: {
- /**
- * The format of the data.
- * @type {string}
- */
- format: string;
-
- /**
- * The value of the data.
- * @type {string}
- */
- value: string;
- };
-
- /**
- * The time to live of the value.
- * @type {number}
- */
- ttl: number;
-
- /**
- * The timestamp of the value.
- * @type {number}
- */
- timestamp: number;
- }[] = [];
-
- /**
- * The constructor of PIDRecord.
- * @param pid The PID of the record.
- * @constructor
- */
- constructor(pid: PID) {
- this._pid = pid;
- }
-
- /**
- * Outputs the PID of the record.
- * @returns {PID} The PID of the record.
- */
- get pid(): PID {
- return this._pid;
- }
-
- /**
- * Outputs the values of the record.
- * @returns {{
- * index: number,
- * type: PIDDataType | PID | string,
- * data: {
- * format: string,
- * value: string
- * },
- * ttl: number,
- * timestamp: number
- * }[]} The values of the record.
- */
- get values(): {
- index: number;
- type: string | PID | PIDDataType;
- data: {
- format: string;
- value: string;
- };
- ttl: number;
- timestamp: number;
- }[] {
- return this._values;
- }
-}
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
deleted file mode 100644
index 0cec908..0000000
--- a/src/utils/utils.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { PID } from './PID';
-import { PIDDataType } from './PIDDataType';
-import { PIDRecord } from './PIDRecord';
-
-/**
- * A map of all PID data types and their PIDs.
- * @type {Map}
- */
-export const typeMap: Map = new Map();
-
-/**
- * A map of all PIDs and their PIDRecords.
- * @type {Map}
- */
-export const handleMap: Map = new Map();
-
-/**
- * A set of all PIDs that are not resolvable.
- * @type {Set}
- */
-export const unresolvables: Set = new Set();
-
-/**
- * Returns a user-friendly name of a locale.
- * If the locale is a language, it will return the name of the language.
- * If the locale is a region, it will return the flag of the region and the name of the region.
- * @param locale The locale to get the name of.
- * @param type The type of the locale.Either 'region' or 'language'.
- * @returns {string} The user-friendly name of the locale.
- */
-export function getLocaleDetail(locale: string, type: 'region' | 'language'): string {
- const friendlyName = new Intl.DisplayNames(['en'], { type: type }).of(locale.toUpperCase());
- if (type === 'language') return friendlyName;
-
- const codePoints = locale
- .toUpperCase()
- .split('')
- .map(char => 127397 + char.charCodeAt(0));
- const flag = String.fromCodePoint(...codePoints);
- return `${flag} ${friendlyName}`;
-}
diff --git a/stencil.config.ts b/stencil.config.ts
deleted file mode 100644
index 8d5ce88..0000000
--- a/stencil.config.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Config } from '@stencil/core';
-import tailwind, { tailwindHMR } from 'stencil-tailwind-plugin';
-
-export const config: Config = {
- namespace: 'pid-component',
- outputTargets: [
- {
- type: 'dist',
- esmLoaderPath: '../loader',
- },
- {
- type: 'dist-custom-elements',
- },
- {
- type: 'docs-readme',
- },
- {
- type: 'www',
- serviceWorker: null, // disable service workers
- },
- ],
- testing: {
- browserHeadless: 'new',
- },
- plugins: [tailwind(), tailwindHMR()],
-};
diff --git a/tsconfig.json b/tsconfig.json
index 5971d5d..9d0692d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,18 +1,15 @@
{
"compilerOptions": {
- "allowSyntheticDefaultImports": true,
- "allowUnreachableCode": false,
- "declaration": false,
+ "module": "commonjs",
+ "declaration": true,
+ "noImplicitAny": false,
+ "removeComments": true,
+ "noLib": false,
+ "emitDecoratorMetadata": true,
"experimentalDecorators": true,
- "lib": ["dom", "es2017"],
- "moduleResolution": "node",
- "module": "esnext",
- "target": "es2017",
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "jsx": "react",
- "jsxFactory": "h"
+ "target": "es6",
+ "sourceMap": true,
+ "lib": ["es6"]
},
- "include": ["src"],
- "exclude": ["node_modules"]
+ "exclude": ["node_modules", "**/*.spec.ts", "**/__tests__/**"]
}