ogm-viewer 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/{lerc-CT61sKYw.js → lerc-Df0hDWkS.js} +6 -6
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/{ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-DKxSOrMP.js → ogm-alerts.ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-preview.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-DMEfXquS.js} +446 -80
- package/dist/cjs/ogm-alerts_11.cjs.entry.js +18 -0
- package/dist/cjs/ogm-viewer.cjs.js +1 -1
- package/dist/collection/assets/icons/exclamation-triangle-fill.svg +3 -0
- package/dist/collection/collection-manifest.json +2 -0
- package/dist/collection/components/ogm-alerts/ogm-alerts.css +51 -0
- package/dist/collection/components/ogm-alerts/ogm-alerts.js +71 -0
- package/dist/collection/components/ogm-alerts/ogm-alerts.test.js +26 -0
- package/dist/collection/components/ogm-image/ogm-image.js +57 -5
- package/dist/collection/components/ogm-map/ogm-map.css +34 -40
- package/dist/collection/components/ogm-map/ogm-map.js +76 -22
- package/dist/collection/components/ogm-menubar/ogm-menubar.test.js +21 -0
- package/dist/collection/components/ogm-preview/ogm-preview.css +16 -0
- package/dist/collection/components/ogm-preview/ogm-preview.js +144 -0
- package/dist/collection/components/ogm-preview/ogm-preview.test.js +37 -0
- package/dist/collection/components/ogm-previews/ogm-previews.css +3 -2
- package/dist/collection/components/ogm-previews/ogm-previews.js +7 -17
- package/dist/collection/components/ogm-previews/ogm-previews.test.js +83 -0
- package/dist/collection/components/ogm-settings/ogm-settings.js +1 -1
- package/dist/collection/components/ogm-sidebar/ogm-sidebar.js +2 -2
- package/dist/collection/components/ogm-viewer/ogm-viewer.js +36 -15
- package/dist/collection/lib/errors.js +137 -0
- package/dist/collection/lib/errors.test.js +96 -0
- package/dist/collection/lib/sources/geojson.js +2 -1
- package/dist/collection/lib/sources/iiif-manifest.js +2 -3
- package/dist/collection/lib/sources/iiif-manifest.test.js +4 -4
- package/dist/esm/{lerc-Bz409klg.js → lerc-CB_Jcu33.js} +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/esm/{ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-C9zV3wMe.js → ogm-alerts.ogm-attributes.ogm-image.ogm-map.ogm-menubar.ogm-metadata.ogm-preview.ogm-previews.ogm-settings.ogm-sidebar.ogm-viewer-Dlc6USWB.js} +446 -82
- package/dist/esm/ogm-alerts_11.entry.js +2 -0
- package/dist/esm/ogm-viewer.js +1 -1
- package/dist/ogm-viewer/assets/icons/exclamation-triangle-fill.svg +3 -0
- package/dist/ogm-viewer/ogm-viewer.esm.js +1 -1
- package/dist/ogm-viewer/p-7936ccc9.entry.js +1 -0
- package/dist/ogm-viewer/p-CWtUrqsM.js +2822 -0
- package/dist/ogm-viewer/{p-BrovdLv6.js → p-DqRDI3AC.js} +1 -1
- package/dist/types/components/ogm-alerts/ogm-alerts.d.ts +6 -0
- package/dist/types/components/ogm-alerts/ogm-alerts.test.d.ts +1 -0
- package/dist/types/components/ogm-image/ogm-image.d.ts +5 -0
- package/dist/types/components/ogm-map/ogm-map.d.ts +6 -0
- package/dist/types/components/ogm-preview/ogm-preview.d.ts +13 -0
- package/dist/types/components/ogm-preview/ogm-preview.test.d.ts +1 -0
- package/dist/types/components/ogm-previews/ogm-previews.d.ts +1 -5
- package/dist/types/components/ogm-previews/ogm-previews.test.d.ts +1 -0
- package/dist/types/components/ogm-viewer/ogm-viewer.d.ts +7 -2
- package/dist/types/components.d.ts +52 -0
- package/dist/types/lib/errors.d.ts +13 -0
- package/dist/types/lib/errors.test.d.ts +1 -0
- package/dist/types/lib/sources/source.d.ts +1 -1
- package/package.json +1 -1
- package/dist/cjs/ogm-attributes_9.cjs.entry.js +0 -16
- package/dist/esm/ogm-attributes_9.entry.js +0 -2
- package/dist/ogm-viewer/p-86b8fafc.entry.js +0 -1
- package/dist/ogm-viewer/p-CpCT3zlD.js +0 -2750
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { h, Host } from "@stencil/core";
|
|
2
|
+
import IIIFSource from "../../lib/sources/iiif";
|
|
3
|
+
// Wraps a single source's preview and surfaces error(s) during the preview.
|
|
4
|
+
export class OgmPreview {
|
|
5
|
+
theme;
|
|
6
|
+
source;
|
|
7
|
+
previewOpacity;
|
|
8
|
+
sidebarPadding;
|
|
9
|
+
error;
|
|
10
|
+
// A new source is a fresh load attempt, so clear any error left over from the previous one.
|
|
11
|
+
resetError() {
|
|
12
|
+
this.error = undefined;
|
|
13
|
+
}
|
|
14
|
+
// Catch the load error emitted by the child map/image and show it in place of the preview.
|
|
15
|
+
handlePreviewError(event) {
|
|
16
|
+
event.stopPropagation();
|
|
17
|
+
this.error = event.detail;
|
|
18
|
+
}
|
|
19
|
+
// IIIF sources use the image viewer; every other source type is previewed on the map.
|
|
20
|
+
renderPreview() {
|
|
21
|
+
if (this.source instanceof IIIFSource) {
|
|
22
|
+
return h("ogm-image", { theme: this.theme, source: this.source, padding: this.sidebarPadding });
|
|
23
|
+
}
|
|
24
|
+
return h("ogm-map", { "preview-opacity": this.previewOpacity, theme: this.theme, source: this.source, padding: this.sidebarPadding });
|
|
25
|
+
}
|
|
26
|
+
render() {
|
|
27
|
+
return (h(Host, { key: 'c962bdcbebdebb1abde8900b0d0811be218fca46' }, this.renderPreview(), this.error && h("ogm-alerts", { key: '87b8217bbb7d5101d9f306f2492326d3f9f92429', theme: this.theme, error: this.error })));
|
|
28
|
+
}
|
|
29
|
+
static get is() { return "ogm-preview"; }
|
|
30
|
+
static get encapsulation() { return "shadow"; }
|
|
31
|
+
static get originalStyleUrls() {
|
|
32
|
+
return {
|
|
33
|
+
"$": ["ogm-preview.css"]
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
static get styleUrls() {
|
|
37
|
+
return {
|
|
38
|
+
"$": ["ogm-preview.css"]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
static get properties() {
|
|
42
|
+
return {
|
|
43
|
+
"theme": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"mutable": false,
|
|
46
|
+
"complexType": {
|
|
47
|
+
"original": "'light' | 'dark'",
|
|
48
|
+
"resolved": "\"dark\" | \"light\"",
|
|
49
|
+
"references": {}
|
|
50
|
+
},
|
|
51
|
+
"required": false,
|
|
52
|
+
"optional": false,
|
|
53
|
+
"docs": {
|
|
54
|
+
"tags": [],
|
|
55
|
+
"text": ""
|
|
56
|
+
},
|
|
57
|
+
"getter": false,
|
|
58
|
+
"setter": false,
|
|
59
|
+
"reflect": false,
|
|
60
|
+
"attribute": "theme"
|
|
61
|
+
},
|
|
62
|
+
"source": {
|
|
63
|
+
"type": "unknown",
|
|
64
|
+
"mutable": false,
|
|
65
|
+
"complexType": {
|
|
66
|
+
"original": "Source",
|
|
67
|
+
"resolved": "Source",
|
|
68
|
+
"references": {
|
|
69
|
+
"Source": {
|
|
70
|
+
"location": "global",
|
|
71
|
+
"id": "global::Source"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"required": false,
|
|
76
|
+
"optional": false,
|
|
77
|
+
"docs": {
|
|
78
|
+
"tags": [],
|
|
79
|
+
"text": ""
|
|
80
|
+
},
|
|
81
|
+
"getter": false,
|
|
82
|
+
"setter": false
|
|
83
|
+
},
|
|
84
|
+
"previewOpacity": {
|
|
85
|
+
"type": "number",
|
|
86
|
+
"mutable": false,
|
|
87
|
+
"complexType": {
|
|
88
|
+
"original": "number",
|
|
89
|
+
"resolved": "number",
|
|
90
|
+
"references": {}
|
|
91
|
+
},
|
|
92
|
+
"required": false,
|
|
93
|
+
"optional": false,
|
|
94
|
+
"docs": {
|
|
95
|
+
"tags": [],
|
|
96
|
+
"text": ""
|
|
97
|
+
},
|
|
98
|
+
"getter": false,
|
|
99
|
+
"setter": false,
|
|
100
|
+
"reflect": false,
|
|
101
|
+
"attribute": "preview-opacity"
|
|
102
|
+
},
|
|
103
|
+
"sidebarPadding": {
|
|
104
|
+
"type": "number",
|
|
105
|
+
"mutable": false,
|
|
106
|
+
"complexType": {
|
|
107
|
+
"original": "number",
|
|
108
|
+
"resolved": "number",
|
|
109
|
+
"references": {}
|
|
110
|
+
},
|
|
111
|
+
"required": false,
|
|
112
|
+
"optional": false,
|
|
113
|
+
"docs": {
|
|
114
|
+
"tags": [],
|
|
115
|
+
"text": ""
|
|
116
|
+
},
|
|
117
|
+
"getter": false,
|
|
118
|
+
"setter": false,
|
|
119
|
+
"reflect": false,
|
|
120
|
+
"attribute": "sidebar-padding"
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
static get states() {
|
|
125
|
+
return {
|
|
126
|
+
"error": {}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
static get watchers() {
|
|
130
|
+
return [{
|
|
131
|
+
"propName": "source",
|
|
132
|
+
"methodName": "resetError"
|
|
133
|
+
}];
|
|
134
|
+
}
|
|
135
|
+
static get listeners() {
|
|
136
|
+
return [{
|
|
137
|
+
"name": "previewError",
|
|
138
|
+
"method": "handlePreviewError",
|
|
139
|
+
"target": undefined,
|
|
140
|
+
"capture": false,
|
|
141
|
+
"passive": false
|
|
142
|
+
}];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect, h, vi } from "@stencil/vitest";
|
|
2
|
+
// Render with Stencil's low-level render directly rather than @stencil/vitest's `render` wrapper: the
|
|
3
|
+
// wrapper re-throws lifecycle errors, and the <ogm-map>/<ogm-image> mounted inside a preview throw
|
|
4
|
+
// when they try to initialize WebGL/OpenSeadragon (unavailable in the test DOM).
|
|
5
|
+
import { render as stencilRender } from "@stencil/core";
|
|
6
|
+
import GeoJSONSource from "../../lib/sources/geojson";
|
|
7
|
+
import { referenceError } from "../../lib/errors";
|
|
8
|
+
// Let Stencil's RAF-based update cycle flush after a state change (mirrors the vitest waitForChanges).
|
|
9
|
+
const flush = () => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
10
|
+
const renderPreview = async (source) => {
|
|
11
|
+
const container = document.createElement('div');
|
|
12
|
+
document.body.appendChild(container);
|
|
13
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
14
|
+
await stencilRender(h("ogm-preview", { source: source }), container);
|
|
15
|
+
const el = container.firstElementChild;
|
|
16
|
+
await el.componentOnReady?.();
|
|
17
|
+
consoleError.mockRestore();
|
|
18
|
+
return el;
|
|
19
|
+
};
|
|
20
|
+
describe('ogm-preview', () => {
|
|
21
|
+
it('renders a preview for a source', async () => {
|
|
22
|
+
const el = await renderPreview(new GeoJSONSource('id', 'http://example.com/data.json'));
|
|
23
|
+
const shadowRoot = el.shadowRoot;
|
|
24
|
+
expect(shadowRoot.querySelector('ogm-map')).not.toBeNull();
|
|
25
|
+
expect(shadowRoot.querySelector('ogm-alerts')).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
it('shows the error over the preview when a previewError is reported', async () => {
|
|
28
|
+
const el = await renderPreview(new GeoJSONSource('id', 'http://example.com/data.json'));
|
|
29
|
+
const error = referenceError(new TypeError('Failed to fetch'), 'GeoJSON', 'http://example.com/data.json');
|
|
30
|
+
el.dispatchEvent(new CustomEvent('previewError', { detail: error, bubbles: true }));
|
|
31
|
+
await flush();
|
|
32
|
+
const shadowRoot = el.shadowRoot;
|
|
33
|
+
expect(shadowRoot.querySelector('ogm-alerts')).not.toBeNull();
|
|
34
|
+
// The preview stays mounted underneath so its in-flight load can finish and not leak loading state
|
|
35
|
+
expect(shadowRoot.querySelector('ogm-map')).not.toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -37,8 +37,9 @@ wa-tab::part(base) {
|
|
|
37
37
|
font-size: var(--wa-font-size-s);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
ogm-map
|
|
41
|
-
ogm-
|
|
40
|
+
/* ogm-preview wraps each tab's map/image; ogm-map is also rendered bare as the no-record fallback. */
|
|
41
|
+
ogm-preview,
|
|
42
|
+
ogm-map {
|
|
42
43
|
display: block;
|
|
43
44
|
height: 100%;
|
|
44
45
|
}
|
|
@@ -10,25 +10,15 @@ import XYZSource from "../../lib/sources/xyz";
|
|
|
10
10
|
import IIIFSource from "../../lib/sources/iiif";
|
|
11
11
|
import IIIFManifestSource from "../../lib/sources/iiif-manifest";
|
|
12
12
|
export class OgmPreviews {
|
|
13
|
-
el;
|
|
14
13
|
theme;
|
|
15
14
|
record;
|
|
16
15
|
previewOpacity;
|
|
17
16
|
sidebarPadding;
|
|
18
17
|
sources = [];
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
return this.renderMapPreview(source);
|
|
24
|
-
}
|
|
25
|
-
// Render an image preview
|
|
26
|
-
renderImagePreview(source) {
|
|
27
|
-
return h("ogm-image", { theme: this.theme, source: source, padding: this.sidebarPadding });
|
|
28
|
-
}
|
|
29
|
-
// Render a map preview
|
|
30
|
-
renderMapPreview(source) {
|
|
31
|
-
return h("ogm-map", { "preview-opacity": this.previewOpacity, theme: this.theme, source: source, padding: this.sidebarPadding });
|
|
18
|
+
// @Watch only fires on changes; handle the initial load here
|
|
19
|
+
componentWillLoad() {
|
|
20
|
+
if (this.record)
|
|
21
|
+
this.getSources(this.record);
|
|
32
22
|
}
|
|
33
23
|
// Given a record, get all of the valid sources that can be used to preview it on a map
|
|
34
24
|
getSources(record) {
|
|
@@ -58,9 +48,10 @@ export class OgmPreviews {
|
|
|
58
48
|
}
|
|
59
49
|
// Render as tabs for switching between sources
|
|
60
50
|
render() {
|
|
61
|
-
if
|
|
51
|
+
// Render a blank map if there is no record or no sources
|
|
52
|
+
if (!this.record || !this.sources.length)
|
|
62
53
|
return h("ogm-map", { theme: this.theme, padding: this.sidebarPadding });
|
|
63
|
-
return (h(Host, { class: this.theme && `wa-${this.theme}` }, h("wa-tab-group", null, this.sources.map((source, idx) => (h("wa-tab", { key: idx, panel: `${source.constructor.name}-${source.id}-${idx}` }, source.label()))), this.sources.map((source, idx) => (h("wa-tab-panel", { key: idx, name: `${source.constructor.name}-${source.id}-${idx}`, active: idx === 0 }, this.
|
|
54
|
+
return (h(Host, { class: this.theme && `wa-${this.theme}` }, h("wa-tab-group", null, this.sources.map((source, idx) => (h("wa-tab", { key: idx, panel: `${source.constructor.name}-${source.id}-${idx}` }, source.label()))), this.sources.map((source, idx) => (h("wa-tab-panel", { key: idx, name: `${source.constructor.name}-${source.id}-${idx}`, active: idx === 0 }, h("ogm-preview", { theme: this.theme, source: source, "preview-opacity": this.previewOpacity, "sidebar-padding": this.sidebarPadding })))))));
|
|
64
55
|
}
|
|
65
56
|
static get is() { return "ogm-previews"; }
|
|
66
57
|
static get encapsulation() { return "shadow"; }
|
|
@@ -162,7 +153,6 @@ export class OgmPreviews {
|
|
|
162
153
|
"sources": {}
|
|
163
154
|
};
|
|
164
155
|
}
|
|
165
|
-
static get elementRef() { return "el"; }
|
|
166
156
|
static get watchers() {
|
|
167
157
|
return [{
|
|
168
158
|
"propName": "record",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect, h, vi } from "@stencil/vitest";
|
|
2
|
+
// Render with Stencil's low-level render directly rather than @stencil/vitest's `render` wrapper: the
|
|
3
|
+
// wrapper re-throws lifecycle errors, and the <ogm-map> mounted inside a preview panel throws when it
|
|
4
|
+
// tries to initialize WebGL (unavailable in the test DOM). Stencil's own safeCall only routes that to
|
|
5
|
+
// console.error, so stencilRender still produces the ogm-previews shadow DOM we want to assert on.
|
|
6
|
+
import { render as stencilRender } from "@stencil/core";
|
|
7
|
+
import OgmRecord from "../../lib/record";
|
|
8
|
+
// Build a minimal Aardvark record, optionally with a WMS reference that yields one previewable source
|
|
9
|
+
const buildRecord = (previewable) => new OgmRecord({
|
|
10
|
+
id: 'berkeley-s7sq63',
|
|
11
|
+
dct_title_s: 'Calaveras County Contours',
|
|
12
|
+
gbl_resourceClass_sm: ['Datasets'],
|
|
13
|
+
dct_accessRights_s: 'Public',
|
|
14
|
+
gbl_mdVersion_s: 'Aardvark',
|
|
15
|
+
...(previewable
|
|
16
|
+
? {
|
|
17
|
+
gbl_wxsIdentifier_s: 's7sq63',
|
|
18
|
+
dct_references_s: JSON.stringify({ 'http://www.opengis.net/def/serviceType/ogc/wms': 'https://example.com/geoserver/wms' }),
|
|
19
|
+
}
|
|
20
|
+
: {}),
|
|
21
|
+
});
|
|
22
|
+
const renderPreviews = async (record) => {
|
|
23
|
+
const container = document.createElement('div');
|
|
24
|
+
document.body.appendChild(container);
|
|
25
|
+
// The mounted <ogm-map> logs a swallowed WebGL init error; keep test output clean
|
|
26
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
27
|
+
await stencilRender(h("ogm-previews", { record: record }), container);
|
|
28
|
+
const el = container.firstElementChild;
|
|
29
|
+
await el.componentOnReady?.();
|
|
30
|
+
consoleError.mockRestore();
|
|
31
|
+
return el.shadowRoot;
|
|
32
|
+
};
|
|
33
|
+
// Let Stencil's RAF-based update cycle flush so the nested <ogm-preview> renders its own shadow DOM.
|
|
34
|
+
const flush = () => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
35
|
+
// Render previews and drill into the single <ogm-preview> to see which preview (map or image) it
|
|
36
|
+
// chose. Going through <ogm-previews> means the source is constructed in the built component's module
|
|
37
|
+
// realm, so <ogm-preview>'s `instanceof IIIFSource` check holds (unlike a source built in the test).
|
|
38
|
+
const renderPreviewChild = async (record) => {
|
|
39
|
+
const container = document.createElement('div');
|
|
40
|
+
document.body.appendChild(container);
|
|
41
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
42
|
+
await stencilRender(h("ogm-previews", { record: record }), container);
|
|
43
|
+
const el = container.firstElementChild;
|
|
44
|
+
await el.componentOnReady?.();
|
|
45
|
+
const preview = el.shadowRoot.querySelector('ogm-preview');
|
|
46
|
+
await preview?.componentOnReady?.();
|
|
47
|
+
await flush();
|
|
48
|
+
consoleError.mockRestore();
|
|
49
|
+
return preview.shadowRoot;
|
|
50
|
+
};
|
|
51
|
+
// A minimal Aardvark record whose only reference is the given previewable one
|
|
52
|
+
const buildRecordWith = (references) => new OgmRecord({
|
|
53
|
+
id: 'test-record',
|
|
54
|
+
dct_title_s: 'Test Record',
|
|
55
|
+
gbl_resourceClass_sm: ['Datasets'],
|
|
56
|
+
dct_accessRights_s: 'Public',
|
|
57
|
+
gbl_mdVersion_s: 'Aardvark',
|
|
58
|
+
dct_references_s: JSON.stringify(references),
|
|
59
|
+
});
|
|
60
|
+
describe('ogm-previews', () => {
|
|
61
|
+
it('renders a tab for a record supplied at initial render', async () => {
|
|
62
|
+
const shadowRoot = await renderPreviews(buildRecord(true));
|
|
63
|
+
const tabs = shadowRoot.querySelectorAll('wa-tab');
|
|
64
|
+
expect(shadowRoot.querySelector('wa-tab-group')).not.toBeNull();
|
|
65
|
+
expect(tabs).toHaveLength(1);
|
|
66
|
+
expect(tabs[0].textContent).toContain('WMS');
|
|
67
|
+
});
|
|
68
|
+
it('falls back to an empty map for a record with no previewable sources', async () => {
|
|
69
|
+
const shadowRoot = await renderPreviews(buildRecord(false));
|
|
70
|
+
expect(shadowRoot.querySelector('wa-tab-group')).toBeNull();
|
|
71
|
+
expect(shadowRoot.querySelector('ogm-map')).not.toBeNull();
|
|
72
|
+
});
|
|
73
|
+
it('wraps a map source in an ogm-preview that renders the map', async () => {
|
|
74
|
+
const previewShadow = await renderPreviewChild(buildRecordWith({ 'http://geojson.org/geojson-spec.html': 'https://example.com/data.json' }));
|
|
75
|
+
expect(previewShadow.querySelector('ogm-map')).not.toBeNull();
|
|
76
|
+
expect(previewShadow.querySelector('ogm-image')).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
it('wraps a IIIF image source in an ogm-preview that renders the image viewer', async () => {
|
|
79
|
+
const previewShadow = await renderPreviewChild(buildRecordWith({ 'http://iiif.io/api/image': 'https://example.com/iiif/info.json' }));
|
|
80
|
+
expect(previewShadow.querySelector('ogm-image')).not.toBeNull();
|
|
81
|
+
expect(previewShadow.querySelector('ogm-map')).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -12,7 +12,7 @@ export class OgmSettings {
|
|
|
12
12
|
this.opacityChange.emit(this.waSlider.value);
|
|
13
13
|
}
|
|
14
14
|
render() {
|
|
15
|
-
return (h("div", { key: '
|
|
15
|
+
return (h("div", { key: 'e2b21549c2bb82aa80780e99bcbed78497683690', class: "settings" }, h("wa-slider", { key: 'bc45061a08522c2e227be5b422cac15fb6da04c3', disabled: !this.record || this.record.references.iiifOnly, label: "Layer opacity", min: "0", max: "100", step: "1", value: "100" })));
|
|
16
16
|
}
|
|
17
17
|
static get is() { return "ogm-settings"; }
|
|
18
18
|
static get encapsulation() { return "shadow"; }
|
|
@@ -20,7 +20,7 @@ export class OgmSidebar {
|
|
|
20
20
|
this.tabs.active = 'information';
|
|
21
21
|
}
|
|
22
22
|
render() {
|
|
23
|
-
return (h(Host, { key: '
|
|
23
|
+
return (h(Host, { key: 'f2b189bf52bcb7deaec53be53c79083423ac2cc7', class: this.theme && `wa-${this.theme}` }, h("div", { key: '42ecd1fd9391fde0dd9cd8c0f2f2ca78d9cedea1', class: `sidebar ${this.open ? 'open' : ''}`, role: "region", "aria-label": "Sidebar", "aria-hidden": this.open ? 'false' : 'true' }, h("wa-tab-group", { key: '61d629c83e125027857e2e634131254f6f621b71', placement: "start" }, h("wa-tab", { key: '35b9b6998f79be73d7e30ed9ee87045b95a06130', slot: "nav", panel: "information" }, h("wa-icon", { key: '7335b6c1e7024b64d5183057eb39f5c3daacc0e1', name: "info-circle-fill", label: "Information", canvas: "auto" })), h("wa-tab", { key: 'cf39ed59867f70ebfb58eddea1a916d4a9b839be', slot: "nav", panel: "rights" }, h("wa-icon", { key: 'b664695aeaedf956d0881a1916ad341841a23c1f', name: "c-circle", label: "Rights", canvas: "auto" })), h("wa-tab", { key: 'aa6172e06b0349fedb1c35cbc6ed03cb4cfa17a5', slot: "nav", panel: "links" }, h("wa-icon", { key: 'a47c7cb356f776f042a41fe610810119bc9ed6ca', name: "box-arrow-up-right", label: "Links", canvas: "auto" })), h("wa-tab", { key: '9befb56e552c93464ac2e1cc5979de92ada1832b', slot: "nav", panel: "record" }, h("wa-icon", { key: 'fd2d86ba6459eecce6303668e91a71723090829d', name: "braces", label: "Record", canvas: "auto" })), h("wa-tab", { key: '54ad126ec6eef8b89e5748528d6e30ef3f7dcd6f', slot: "nav", panel: "settings" }, h("wa-icon", { key: 'abf83b541d84ccca8caff08fa505c262b8338157', name: "sliders", label: "Settings", canvas: "auto" })), h("wa-tab-panel", { key: '933d76a9e75741b9b17e0924888963e8777d512d', name: "information" }, h("div", { key: 'd0639a93e8cf8d79cbc0486c699db57a6b46f207', class: "panel-header" }, "About this item"), h("div", { key: 'b4415b06fd5e7fbce31107b41a527b6e524bf056', class: "panel-content" }, h("ogm-metadata", { key: '1eb858dd9f4fd676e4fb1f4ec7ca5f8210e21f5e', record: this.record, fieldNames: [
|
|
24
24
|
'title',
|
|
25
25
|
'description',
|
|
26
26
|
'creators',
|
|
@@ -34,7 +34,7 @@ export class OgmSidebar {
|
|
|
34
34
|
'temporal',
|
|
35
35
|
'issued',
|
|
36
36
|
'mdModified',
|
|
37
|
-
] }))), h("wa-tab-panel", { key: '
|
|
37
|
+
] }))), h("wa-tab-panel", { key: '8bf2a486e918a0b172ec68261d43ab01d1e5927c', name: "rights" }, h("div", { key: '1ec5efad4edb737994982bc0e371174963437cb9', class: "panel-header" }, "Access conditions"), h("div", { key: '88e876ae699f82eec351217f731effcbd571036a', class: "panel-content" }, h("ogm-metadata", { key: '939c58b4fa0d96b3e042d951fac4199b448a757d', record: this.record, fieldNames: ['provider', 'license', 'rights', 'rightsHolder'] }))), h("wa-tab-panel", { key: '47563dce1a1d067f9ef2186e9ab423fe2f2725d9', name: "links" }, h("div", { key: '9021c78ef541235486f2d7a7d3fbeb88b02e3ec4', class: "panel-header" }, "Links"), h("div", { key: '868438dd07bfa1bdbfae1c8d9680c17aee026c31', class: "panel-content" }, h("ogm-metadata", { key: '868ac799f80c71e3730e80c9ee47afd887e4f22d', record: this.record, fieldNames: ['references'] }))), h("wa-tab-panel", { key: '1ca6a74b3b1a1b606a8a873ffae74ec6521a5fd6', name: "record" }, h("div", { key: '63f3998c6b8ba4cfed63e4b62d52b9ca7199ddcf', class: "panel-header" }, "Record view"), h("div", { key: '484f7b7ccd92f29ddfc1b9710b7a30868f39d63a', class: "panel-content" }, h("div", { key: '581a35264c287d71a77a897731c8a2d4098dcc4e', class: "record-json" }, JSON.stringify(this.record?.json, null, 2)))), h("wa-tab-panel", { key: '45f76acfae788d0e1fd5e158e54295c31b2b47ce', name: "settings" }, h("div", { key: '08e8c15c9e079d84a8444cb32bef6443feb303ce', class: "panel-header" }, "Settings"), h("div", { key: '9681b37b6e0c83cd5c2c953bbe90efbdfbbf6cc0', class: "panel-content" }, h("ogm-settings", { key: '00638f2ebf0a7a47f6df47aaf688492f42e86915', record: this.record })))))));
|
|
38
38
|
}
|
|
39
39
|
static get is() { return "ogm-sidebar"; }
|
|
40
40
|
static get encapsulation() { return "shadow"; }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { setBasePath, getBasePath, registerIconLibrary } from "@awesome.me/webawesome";
|
|
2
2
|
import { Host, getAssetPath, h } from "@stencil/core";
|
|
3
3
|
import OgmRecord from "../../lib/record";
|
|
4
|
+
import { fetchOrThrow, recordError } from "../../lib/errors";
|
|
4
5
|
// Only need to call this once, at the top level
|
|
5
6
|
setBasePath(getAssetPath(''));
|
|
6
7
|
// Serve icons from our self-hosted bootstrap-icons subset instead of the default Font Awesome library
|
|
@@ -10,9 +11,9 @@ registerIconLibrary('default', {
|
|
|
10
11
|
// Web Awesome's raw color palette is scoped to `:root`, which can only ever match the actual
|
|
11
12
|
// document root - never an element inside a shadow tree, so a component-scoped @import can't
|
|
12
13
|
// activate it. Loading it here, at the document level, lets it match `:root` for real, and lets
|
|
13
|
-
// its `.wa-light`/`.wa-dark` theme rules match
|
|
14
|
-
//
|
|
15
|
-
//
|
|
14
|
+
// its `.wa-light`/`.wa-dark` theme rules match Host element in each component because that element
|
|
15
|
+
// is part of the page light DOM and not the shadow DOM. Injecting it here means consumers
|
|
16
|
+
// don't have to remember to add it to their own page.
|
|
16
17
|
function loadWebAwesomeStylesheet() {
|
|
17
18
|
const id = 'ogm-viewer-webawesome-styles';
|
|
18
19
|
if (document.getElementById(id))
|
|
@@ -26,6 +27,7 @@ function loadWebAwesomeStylesheet() {
|
|
|
26
27
|
loadWebAwesomeStylesheet();
|
|
27
28
|
// Import all required Web Awesome components
|
|
28
29
|
import "@awesome.me/webawesome/dist/components/button/button.js";
|
|
30
|
+
import "@awesome.me/webawesome/dist/components/callout/callout.js";
|
|
29
31
|
import "@awesome.me/webawesome/dist/components/icon/icon.js";
|
|
30
32
|
import "@awesome.me/webawesome/dist/components/scroller/scroller.js";
|
|
31
33
|
import "@awesome.me/webawesome/dist/components/slider/slider.js";
|
|
@@ -39,9 +41,11 @@ export class OgmViewer {
|
|
|
39
41
|
recordUrl;
|
|
40
42
|
theme = this.getThemePreference();
|
|
41
43
|
record;
|
|
44
|
+
error;
|
|
42
45
|
previewOpacity = 100;
|
|
43
46
|
sidebarOpen = false;
|
|
44
47
|
loading = false;
|
|
48
|
+
loadingCount = 0;
|
|
45
49
|
sidebarPadding = 0;
|
|
46
50
|
// Prior to rendering, fetch the record if a URL is provided
|
|
47
51
|
async componentWillLoad() {
|
|
@@ -63,32 +67,44 @@ export class OgmViewer {
|
|
|
63
67
|
}
|
|
64
68
|
// Can be called externally to set the record directly
|
|
65
69
|
async loadRecord(record) {
|
|
70
|
+
this.error = undefined;
|
|
66
71
|
this.record = record;
|
|
67
72
|
}
|
|
68
73
|
// Listen for opacity changes from the sidebar and adjust the preview layer
|
|
69
74
|
adjustPreviewOpacity(event) {
|
|
70
75
|
this.previewOpacity = event.detail;
|
|
71
76
|
}
|
|
72
|
-
// Listen for
|
|
77
|
+
// Listen for a preview to report loading started
|
|
73
78
|
setLoadingStarted() {
|
|
79
|
+
this.loadingCount++;
|
|
74
80
|
this.loading = true;
|
|
75
81
|
}
|
|
76
|
-
//
|
|
82
|
+
// When all in-flight previews have loaded, clear loading state
|
|
77
83
|
setLoadingFinished() {
|
|
84
|
+
this.loadingCount = Math.max(0, this.loadingCount - 1);
|
|
85
|
+
this.loading = this.loadingCount > 0;
|
|
86
|
+
}
|
|
87
|
+
// When a new record loads, reset the loading count and loading state
|
|
88
|
+
resetLoading() {
|
|
89
|
+
this.loadingCount = 0;
|
|
78
90
|
this.loading = false;
|
|
79
91
|
}
|
|
80
|
-
// Fetch a record by URL and parse it into an OgmRecord instance
|
|
92
|
+
// Fetch a record by URL and parse it into an OgmRecord instance.
|
|
81
93
|
async fetchRecord(recordUrl) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
this.error = undefined;
|
|
95
|
+
try {
|
|
96
|
+
const response = await fetchOrThrow(recordUrl);
|
|
97
|
+
const data = await response.json();
|
|
98
|
+
return new OgmRecord(data);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
console.error(`Error loading record ${recordUrl}:`, error);
|
|
102
|
+
this.error = recordError(error, recordUrl);
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
85
105
|
}
|
|
86
106
|
render() {
|
|
87
|
-
return (
|
|
88
|
-
// Applying the theme class to the host (rather than an internal div) lets the document-level
|
|
89
|
-
// Web Awesome stylesheet's `.wa-light`/`.wa-dark` rules match it directly, since the host sits
|
|
90
|
-
// in the consuming page's own light DOM while everything inside this shadow root does not.
|
|
91
|
-
h(Host, { key: 'db64abdf2b961976aa7359d089a57dcaf37fb175', class: `wa-${this.theme}` }, h("div", { key: '8fc67a48693890f017f481b91d0b870a79a5dc58', class: "container" }, h("ogm-menubar", { key: '9818fefb02eec280b73319c87e0a11b93bd69626', theme: this.theme, record: this.record, loading: this.loading }), h("div", { key: '5653ffea28fe78c8aa2d254db29e5c5d0ece96eb', class: "main-container" }, h("ogm-sidebar", { key: '97bda51d3b84e9d535e25bc1abb7615703485450', theme: this.theme, record: this.record, open: this.sidebarOpen }), h("ogm-previews", { key: '97f8dab53279ac2a9ce5f0a1da2e9f0dcb4ec5fd', theme: this.theme, record: this.record, "preview-opacity": this.previewOpacity, "sidebar-padding": this.sidebarPadding })))));
|
|
107
|
+
return (h(Host, { key: '6fae950d74eac18eca8192c3ab22d27cde8f8215', class: `wa-${this.theme}` }, h("div", { key: 'ddaf990f2145b165c24ded5272ec3f0f11d32df1', class: "container" }, h("ogm-menubar", { key: '93de36998fdfe07aaea28ee1d32512d129402ae6', theme: this.theme, record: this.record, loading: this.loading }), h("div", { key: 'cafd7e0862a8c39008caee372da2e79b48df2aec', class: "main-container" }, h("ogm-sidebar", { key: '03b5f864d35a385de6941f3eac9b1da050422eb9', theme: this.theme, record: this.record, open: this.sidebarOpen }), this.error ? (h("ogm-alerts", { theme: this.theme, error: this.error })) : (h("ogm-previews", { theme: this.theme, record: this.record, "preview-opacity": this.previewOpacity, "sidebar-padding": this.sidebarPadding }))))));
|
|
92
108
|
}
|
|
93
109
|
static get is() { return "ogm-viewer"; }
|
|
94
110
|
static get encapsulation() { return "shadow"; }
|
|
@@ -148,8 +164,10 @@ export class OgmViewer {
|
|
|
148
164
|
static get states() {
|
|
149
165
|
return {
|
|
150
166
|
"record": {},
|
|
167
|
+
"error": {},
|
|
151
168
|
"previewOpacity": {},
|
|
152
|
-
"sidebarOpen": {}
|
|
169
|
+
"sidebarOpen": {},
|
|
170
|
+
"loading": {}
|
|
153
171
|
};
|
|
154
172
|
}
|
|
155
173
|
static get methods() {
|
|
@@ -186,6 +204,9 @@ export class OgmViewer {
|
|
|
186
204
|
return [{
|
|
187
205
|
"propName": "recordUrl",
|
|
188
206
|
"methodName": "updateRecord"
|
|
207
|
+
}, {
|
|
208
|
+
"propName": "record",
|
|
209
|
+
"methodName": "resetLoading"
|
|
189
210
|
}];
|
|
190
211
|
}
|
|
191
212
|
static get listeners() {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Supertype for errors thrown inside ogm-viewer. `message` is inherited from Error; redeclaring it
|
|
2
|
+
// here would emit an uninitialized class field that clobbers the value super() sets (under the
|
|
3
|
+
// es2022 define-fields semantics this project compiles with), leaving every subtype's message empty.
|
|
4
|
+
export class PreviewError extends Error {
|
|
5
|
+
title;
|
|
6
|
+
url;
|
|
7
|
+
}
|
|
8
|
+
// The OGM record URL responded with a non-OK HTTP status.
|
|
9
|
+
class RecordLoadError extends PreviewError {
|
|
10
|
+
title = "This record couldn't be loaded";
|
|
11
|
+
constructor(status, url) {
|
|
12
|
+
super(`The record URL responded with an error (HTTP ${status}). Check that the URL is correct and the record is publicly available.`);
|
|
13
|
+
this.url = url;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// The OGM record URL fetch failed due to a network error or CORS restriction.
|
|
17
|
+
class RecordNetworkError extends PreviewError {
|
|
18
|
+
title = "This record couldn't be reached";
|
|
19
|
+
constructor(url) {
|
|
20
|
+
super('The server may be unreachable, or it may be blocking cross-origin (CORS) requests from this page.');
|
|
21
|
+
this.url = url;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// The OGM record couldn't be parsed as JSON.
|
|
25
|
+
class RecordParseError extends PreviewError {
|
|
26
|
+
title = "This record couldn't be parsed";
|
|
27
|
+
constructor(url) {
|
|
28
|
+
super("The record URL didn't return valid JSON metadata. It may be pointing at an HTML page or a malformed file.");
|
|
29
|
+
this.url = url;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Something else went wrong reading the record.
|
|
33
|
+
class RecordReadError extends PreviewError {
|
|
34
|
+
title = "This record couldn't be read";
|
|
35
|
+
constructor(message, url) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.url = url;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// The reference URL responded with a non-OK HTTP status.
|
|
41
|
+
class ReferenceLoadError extends PreviewError {
|
|
42
|
+
constructor(refType, status, url) {
|
|
43
|
+
super(`The reference URL responded with an error (HTTP ${status}). Check that the URL is correct and the reference is publicly available.`);
|
|
44
|
+
this.title = `The ${refType} preview couldn't be loaded`;
|
|
45
|
+
this.url = url;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// The reference URL fetch failed due to a network error or CORS restriction.
|
|
49
|
+
class ReferenceNetworkError extends PreviewError {
|
|
50
|
+
constructor(refType, url) {
|
|
51
|
+
super('The server may be unreachable, or it may be blocking cross-origin (CORS) requests from this page.');
|
|
52
|
+
this.title = `The ${refType} preview couldn't be reached`;
|
|
53
|
+
this.url = url;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Something else went wrong reading the reference.
|
|
57
|
+
class ReferenceReadError extends PreviewError {
|
|
58
|
+
constructor(refType, message, url) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.title = `The ${refType} preview couldn't be read`;
|
|
61
|
+
this.url = url;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Used to re-raise HTTP responses from fetch() with a non-OK status as PreviewErrors
|
|
65
|
+
export class HttpError extends Error {
|
|
66
|
+
status;
|
|
67
|
+
statusText;
|
|
68
|
+
url;
|
|
69
|
+
constructor(url, status, statusText) {
|
|
70
|
+
super(`HTTP ${status} ${statusText} for ${url}`);
|
|
71
|
+
this.name = 'HttpError';
|
|
72
|
+
this.status = status;
|
|
73
|
+
this.statusText = statusText;
|
|
74
|
+
this.url = url;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Execute a fetch() and raise HttpError on non-OK status
|
|
78
|
+
export async function fetchOrThrow(url, init) {
|
|
79
|
+
const response = await fetch(url, init);
|
|
80
|
+
if (!response.ok)
|
|
81
|
+
throw new HttpError(url, response.status, response.statusText);
|
|
82
|
+
return response;
|
|
83
|
+
}
|
|
84
|
+
// Attempt to get a useful HTTP status code from any error. We only keep values
|
|
85
|
+
// over 400 because MapLibre uses code "0", which isn't a real HTTP status.
|
|
86
|
+
function statusOf(error) {
|
|
87
|
+
if (error instanceof HttpError)
|
|
88
|
+
return error.status;
|
|
89
|
+
const status = error?.status;
|
|
90
|
+
return typeof status === 'number' && status >= 400 ? status : undefined;
|
|
91
|
+
}
|
|
92
|
+
// A fetch() failure to reach the server at all, or a CORS block, rejects with a TypeError. MapLibre
|
|
93
|
+
// reports the same class of failure on its internal tile/data requests with a synthetic status of 0
|
|
94
|
+
// (not a real HTTP status). Treat both as a network error so the user sees the unreachable/CORS
|
|
95
|
+
// message rather than a generic read error or a nonsensical "HTTP 0".
|
|
96
|
+
function isNetworkError(error) {
|
|
97
|
+
if (error instanceof TypeError)
|
|
98
|
+
return true;
|
|
99
|
+
return error?.status === 0;
|
|
100
|
+
}
|
|
101
|
+
// Attempt get a useful message from any error.
|
|
102
|
+
function messageOf(error) {
|
|
103
|
+
if (error instanceof Error)
|
|
104
|
+
return error.message;
|
|
105
|
+
if (typeof error === 'string')
|
|
106
|
+
return error;
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
// Generate a PreviewError when something went wrong loading the record.
|
|
110
|
+
export function recordError(error, url) {
|
|
111
|
+
const status = statusOf(error);
|
|
112
|
+
const message = messageOf(error);
|
|
113
|
+
// The request failed to reach the server at all, or was blocked by CORS.
|
|
114
|
+
if (isNetworkError(error))
|
|
115
|
+
return new RecordNetworkError(url);
|
|
116
|
+
// If we have an HTTP status code, the server responded.
|
|
117
|
+
if (status !== undefined)
|
|
118
|
+
return new RecordLoadError(status, url);
|
|
119
|
+
// A SyntaxError here means we got a response but it wasn't valid JSON metadata.
|
|
120
|
+
if (error instanceof SyntaxError)
|
|
121
|
+
return new RecordParseError(url);
|
|
122
|
+
// Anything else (e.g. an unsupported metadata version thrown by OgmRecord's constructor).
|
|
123
|
+
return new RecordReadError(message ?? 'An unknown error occurred while reading this record.', url);
|
|
124
|
+
}
|
|
125
|
+
// Generate a PreviewError when something went wrong loading an individual reference.
|
|
126
|
+
export function referenceError(error, refType, url) {
|
|
127
|
+
const status = statusOf(error);
|
|
128
|
+
const message = messageOf(error);
|
|
129
|
+
// The request failed to reach the server at all, or was blocked by CORS.
|
|
130
|
+
if (isNetworkError(error))
|
|
131
|
+
return new ReferenceNetworkError(refType, url);
|
|
132
|
+
// If we have an HTTP status code, the server responded.
|
|
133
|
+
if (status !== undefined)
|
|
134
|
+
return new ReferenceLoadError(refType, status, url);
|
|
135
|
+
// Anything else.
|
|
136
|
+
return new ReferenceReadError(refType, message ?? 'An unknown error occurred while reading this reference.', url);
|
|
137
|
+
}
|