sol-components 2.1.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/README.md +7 -0
- package/core/activate.js +27 -0
- package/core/adopt.js +71 -0
- package/core/auth-core.js +73 -0
- package/core/auth-fetch.js +154 -0
- package/core/component-mount.js +110 -0
- package/core/defaults.js +48 -0
- package/core/define.js +15 -0
- package/core/display-target.js +166 -0
- package/core/edit-placements.js +28 -0
- package/core/editor-self.js +127 -0
- package/core/editor.js +162 -0
- package/core/events.js +27 -0
- package/core/extension-points.js +189 -0
- package/core/form-utils.js +210 -0
- package/core/from-query.js +138 -0
- package/core/from-rdf.js +52 -0
- package/core/here.js +33 -0
- package/core/include-core.js +73 -0
- package/core/inrupt-global.js +18 -0
- package/core/menu-consumer.js +41 -0
- package/core/menu-rdf.js +154 -0
- package/core/pod-ops.js +392 -0
- package/core/pod-registry.js +82 -0
- package/core/popup-proxy.js +255 -0
- package/core/rdf-core.js +280 -0
- package/core/rdf-render.js +136 -0
- package/core/rdf-utils.js +411 -0
- package/core/rdf.js +154 -0
- package/core/services.js +106 -0
- package/core/shape-to-form.js +741 -0
- package/core/sparql-safety.js +20 -0
- package/core/utils.js +196 -0
- package/dist/importmap-cdn.json +49 -0
- package/dist/importmap-local.json +49 -0
- package/dist/sol-loader.manifest.json +140 -0
- package/dist/vendor/@comunica-query-sparql.js +137851 -0
- package/dist/vendor/@inrupt-solid-client-authn-browser.js +7503 -0
- package/dist/vendor/dompurify.js +1476 -0
- package/dist/vendor/ical.js.js +9739 -0
- package/dist/vendor/marked.js +85 -0
- package/dist/vendor/n3.js +14670 -0
- package/dist/vendor/rdf-validate-shacl.js +6970 -0
- package/dist/vendor/rdflib.js +35172 -0
- package/dist/vendor/solid-logic.js +6819 -0
- package/dist/vendor/solid-ui.js +21945 -0
- package/node/sol-form.js +133 -0
- package/node/sol-include.js +55 -0
- package/node/sol-login.js +632 -0
- package/node/sol-menu.js +639 -0
- package/node/sol-query.js +116 -0
- package/package.json +133 -0
- package/web/menu-from-rdf.js +23 -0
- package/web/scripts/prefs.js +25 -0
- package/web/sol-accordion.js +114 -0
- package/web/sol-basic.js +50 -0
- package/web/sol-breadcrumb.js +131 -0
- package/web/sol-button.js +244 -0
- package/web/sol-calendar.js +465 -0
- package/web/sol-default.js +118 -0
- package/web/sol-dropdown-button.js +222 -0
- package/web/sol-feed.js +1336 -0
- package/web/sol-form.js +949 -0
- package/web/sol-full.js +43 -0
- package/web/sol-gallery.js +303 -0
- package/web/sol-include.js +246 -0
- package/web/sol-live-edit.js +415 -0
- package/web/sol-login.js +856 -0
- package/web/sol-menu.js +593 -0
- package/web/sol-modal.js +377 -0
- package/web/sol-pod-extras.js +17 -0
- package/web/sol-pod-ops.js +680 -0
- package/web/sol-pod.js +1039 -0
- package/web/sol-query.js +546 -0
- package/web/sol-rolodex.js +95 -0
- package/web/sol-search.js +402 -0
- package/web/sol-settings.js +199 -0
- package/web/sol-solidos.js +93 -0
- package/web/sol-tabs.js +445 -0
- package/web/sol-time.js +194 -0
- package/web/sol-tree-edit.js +492 -0
- package/web/sol-wac.js +456 -0
- package/web/sol-weather.js +337 -0
- package/web/sol-window.js +142 -0
- package/web/styles/buttons-css.js +108 -0
- package/web/styles/help.css +242 -0
- package/web/styles/root.css +112 -0
- package/web/styles/sol-accordion-css.js +97 -0
- package/web/styles/sol-calendar-css.js +154 -0
- package/web/styles/sol-feed-css.js +475 -0
- package/web/styles/sol-form-css.js +471 -0
- package/web/styles/sol-gallery-css.js +181 -0
- package/web/styles/sol-include-css.js +95 -0
- package/web/styles/sol-live-edit-css.js +84 -0
- package/web/styles/sol-live-edit.css +101 -0
- package/web/styles/sol-login-css.js +116 -0
- package/web/styles/sol-menu-css.js +145 -0
- package/web/styles/sol-modal-css.js +134 -0
- package/web/styles/sol-pod-css.js +187 -0
- package/web/styles/sol-pod-modal-css.js +203 -0
- package/web/styles/sol-query-css.js +140 -0
- package/web/styles/sol-query-help.css +267 -0
- package/web/styles/sol-query-one-pager.css +67 -0
- package/web/styles/sol-search-css.js +157 -0
- package/web/styles/sol-solidos-css.js +7 -0
- package/web/styles/sol-tabs-css.js +114 -0
- package/web/styles/sol-time-css.js +30 -0
- package/web/styles/sol-wac-css.js +73 -0
- package/web/styles/sol-weather-css.js +59 -0
- package/web/styles/solid-logo.svg +9 -0
- package/web/styles/view-accordion-css.js +66 -0
- package/web/styles/view-anchorlist-css.js +22 -0
- package/web/styles/view-autocomplete-css.js +59 -0
- package/web/styles/view-rolodex-css.js +102 -0
- package/web/styles/view-select-css.js +21 -0
- package/web/utils/calendar-fetch.js +388 -0
- package/web/utils/code-mirror-editor.js +82 -0
- package/web/utils/commons-fetch.js +108 -0
- package/web/utils/feed-edit.js +159 -0
- package/web/utils/feed-edit.smoke.mjs +74 -0
- package/web/utils/feed-fetch.js +573 -0
- package/web/utils/live-edit-help/csv.js +64 -0
- package/web/utils/live-edit-help/graphviz.js +41 -0
- package/web/utils/live-edit-help/jsonld.js +55 -0
- package/web/utils/live-edit-help/markdown.js +52 -0
- package/web/utils/live-edit-help/mermaid.js +48 -0
- package/web/utils/live-edit-help/turtle.js +85 -0
- package/web/utils/rdf-config.js +125 -0
- package/web/utils/renderers/csv.js +124 -0
- package/web/utils/renderers/d3-force.js +82 -0
- package/web/utils/renderers/graphviz.js +13 -0
- package/web/utils/renderers/html.js +10 -0
- package/web/utils/renderers/jsonld.js +63 -0
- package/web/utils/renderers/markdown.js +19 -0
- package/web/utils/renderers/mermaid.js +54 -0
- package/web/utils/renderers/turtle.js +51 -0
- package/web/utils/sol-query-triple-patterns.js +151 -0
- package/web/utils/sol-query-ui.js +250 -0
- package/web/utils/sol-query-views.js +32 -0
- package/web/views/_helpers.js +34 -0
- package/web/views/accordion.js +133 -0
- package/web/views/anchorlist.js +59 -0
- package/web/views/auto-complete.js +183 -0
- package/web/views/dl.js +38 -0
- package/web/views/list.js +19 -0
- package/web/views/menu.js +56 -0
- package/web/views/rolodex.js +126 -0
- package/web/views/select.js +79 -0
- package/web/views/table.js +73 -0
- package/web/views/tabs.js +57 -0
package/web/sol-full.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sol-full.js — load every covered component in one tag.
|
|
3
|
+
*
|
|
4
|
+
* Side-effect aggregator: each child module registers its custom element
|
|
5
|
+
* on import. Externals actually imported by the source (rdflib, dompurify,
|
|
6
|
+
* marked) stay bare-specifier and are resolved by the consumer's importmap.
|
|
7
|
+
*
|
|
8
|
+
* Bring-your-own runtime peers (loaded via a `<script>` tag *before* this
|
|
9
|
+
* module so they self-attach to globals the components probe at runtime):
|
|
10
|
+
* - @inrupt/solid-client-authn-browser → `window.solidClientAuthn`
|
|
11
|
+
* (only needed by sol-login)
|
|
12
|
+
* - @comunica/query-sparql → `window.Comunica`
|
|
13
|
+
* (only needed for full SPARQL — LIMIT/OFFSET/ORDER BY/federation —
|
|
14
|
+
* against RDF documents)
|
|
15
|
+
* The corresponding vendored UMDs live in `dist/vendor/`.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* <script type="importmap" src="dist/importmap-cdn.json"></script>
|
|
19
|
+
* — or —
|
|
20
|
+
* <script type="importmap" src="dist/importmap-local.json"></script>
|
|
21
|
+
*
|
|
22
|
+
* <script type="module" src="web/sol-full.js"></script>
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import './sol-menu.js';
|
|
26
|
+
import './menu-from-rdf.js'; // keep sol-menu `from-rdf` working in this bundle
|
|
27
|
+
import './sol-include.js';
|
|
28
|
+
import './sol-query.js';
|
|
29
|
+
import './sol-login.js';
|
|
30
|
+
import './sol-feed.js';
|
|
31
|
+
|
|
32
|
+
// Deliberately NOT in sol-full: sol-weather, sol-time, sol-calendar — chrome
|
|
33
|
+
// widgets with their own heavy deps (e.g. ical). Load them as ESM where
|
|
34
|
+
// needed, e.g. import 'sol-components/sol-weather.js'.
|
|
35
|
+
//
|
|
36
|
+
// TODO: add the rest as their externals get vendored:
|
|
37
|
+
// sol-accordion, sol-modal, sol-tabs, sol-rolodex, sol-live-edit, sol-pod,
|
|
38
|
+
// sol-pod-ops, sol-wac (no extra externals expected — verify and add)
|
|
39
|
+
// sol-form (requires solid-ui — vendor solid-ui first)
|
|
40
|
+
// sol-solidos (requires mashlib)
|
|
41
|
+
// sol-login is in: it only needs `window.solidClientAuthn` (provided by
|
|
42
|
+
// dist/vendor/@inrupt-solid-client-authn-browser.umd.js loaded as a
|
|
43
|
+
// `<script>` tag before this aggregator).
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <sol-gallery> — pure image-grid display (masonry + lightbox).
|
|
3
|
+
*
|
|
4
|
+
* Source-blind: it renders the ImageItem RDF it is handed and emits events. It
|
|
5
|
+
* does NO network, no search, and knows nothing about Commons / Wikidata /
|
|
6
|
+
* files / SKOS / DCAT. A host (see the `sources/` providers) pumps pages of
|
|
7
|
+
* `schema:ImageObject` records in and decides what a selection means.
|
|
8
|
+
*
|
|
9
|
+
* Display contract
|
|
10
|
+
* clear() drop all tiles — a new collection was selected
|
|
11
|
+
* add(store) append one page of schema:ImageObject records ← the seam
|
|
12
|
+
* end() the host signals there are no more pages
|
|
13
|
+
* Events out
|
|
14
|
+
* 'item-opened' {detail:{iri}} a tile's lightbox opened (lazy per-item detail hook)
|
|
15
|
+
* 'load-more' scrolled near the end; the host should pump the next page
|
|
16
|
+
*
|
|
17
|
+
* Records are read with `readImageItems` from the shared contract, so the
|
|
18
|
+
* gallery and every provider agree on the vocab without either re-declaring it.
|
|
19
|
+
*
|
|
20
|
+
* @element sol-gallery
|
|
21
|
+
* @example
|
|
22
|
+
* const g = document.querySelector('sol-gallery');
|
|
23
|
+
* g.addEventListener('load-more', () => pumpNextPage());
|
|
24
|
+
* g.clear(); g.add(pageStore); // …; g.end();
|
|
25
|
+
*/
|
|
26
|
+
import { adopt } from '../core/adopt.js';
|
|
27
|
+
import { define } from '../core/define.js';
|
|
28
|
+
import { CSS as GALLERY_CSS, sheet as GALLERY_SHEET } from './styles/sol-gallery-css.js';
|
|
29
|
+
import { readImageItems } from '../sources/contract.js';
|
|
30
|
+
|
|
31
|
+
class SolGallery extends HTMLElement {
|
|
32
|
+
constructor() {
|
|
33
|
+
super();
|
|
34
|
+
this.attachShadow({ mode: 'open' });
|
|
35
|
+
/** @type {Array} flattened, position-ordered items across all pages. */
|
|
36
|
+
this._items = [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
connectedCallback() {
|
|
40
|
+
if (this._built) return; // build once; survives re-attach
|
|
41
|
+
this._built = true;
|
|
42
|
+
adopt(this.shadowRoot, { sheet: GALLERY_SHEET, css: GALLERY_CSS });
|
|
43
|
+
|
|
44
|
+
this._main = document.createElement('div');
|
|
45
|
+
this._main.className = 'gallery-main';
|
|
46
|
+
this._status = document.createElement('div');
|
|
47
|
+
this._status.className = 'gallery-status';
|
|
48
|
+
this._status.setAttribute('role', 'status');
|
|
49
|
+
this._status.setAttribute('aria-live', 'polite');
|
|
50
|
+
this._grid = document.createElement('div');
|
|
51
|
+
this._grid.className = 'gallery-grid';
|
|
52
|
+
this._main.append(this._status, this._grid);
|
|
53
|
+
this.shadowRoot.append(this._main);
|
|
54
|
+
this._buildLightbox();
|
|
55
|
+
|
|
56
|
+
this._renderEmpty('Pick a collection to see its images.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
disconnectedCallback() {
|
|
60
|
+
if (this._io) { this._io.disconnect(); this._io = null; }
|
|
61
|
+
document.removeEventListener('keydown', this._onKey);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setStatus(msg, isError = false) {
|
|
65
|
+
this._status.textContent = msg || '';
|
|
66
|
+
if (isError) this._status.setAttribute('data-error', '');
|
|
67
|
+
else this._status.removeAttribute('data-error');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Show a centred placeholder line in the grid (cleared by the next page). */
|
|
71
|
+
_renderEmpty(text) {
|
|
72
|
+
const empty = document.createElement('div');
|
|
73
|
+
empty.className = 'gallery-empty';
|
|
74
|
+
empty.textContent = text;
|
|
75
|
+
this._grid.replaceChildren(empty);
|
|
76
|
+
this._emptyEl = empty;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* ── display contract ───────────────────────────────────────────────────── */
|
|
80
|
+
|
|
81
|
+
/** Drop everything for a freshly selected collection; show a loading line. */
|
|
82
|
+
clear() {
|
|
83
|
+
this._items = [];
|
|
84
|
+
this._complete = false;
|
|
85
|
+
this._awaitingPage = false;
|
|
86
|
+
this._removeSentinel();
|
|
87
|
+
this._grid.replaceChildren();
|
|
88
|
+
this._emptyEl = null;
|
|
89
|
+
this.setStatus('Loading images…');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Append one page (an rdflib store of schema:ImageObject records). */
|
|
93
|
+
add(store) {
|
|
94
|
+
const page = readImageItems(store);
|
|
95
|
+
this._awaitingPage = false;
|
|
96
|
+
if (this._emptyEl) { this._emptyEl.remove(); this._emptyEl = null; }
|
|
97
|
+
|
|
98
|
+
const start = this._items.length;
|
|
99
|
+
this._items = this._items.concat(page);
|
|
100
|
+
for (let i = 0; i < page.length; i++) {
|
|
101
|
+
this._grid.appendChild(this._thumb(this._items[start + i], start + i));
|
|
102
|
+
}
|
|
103
|
+
this.setStatus(this._countLabel());
|
|
104
|
+
this._armSentinel(); // a page arrived → there may be more
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The host has no more pages: remove the sentinel; show empty if needed. */
|
|
108
|
+
end() {
|
|
109
|
+
this._complete = true;
|
|
110
|
+
this._removeSentinel();
|
|
111
|
+
if (!this._items.length) {
|
|
112
|
+
this.setStatus('');
|
|
113
|
+
this._renderEmpty('This collection has no images in its Commons category.');
|
|
114
|
+
} else {
|
|
115
|
+
this.setStatus(this._countLabel());
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_countLabel() {
|
|
120
|
+
const n = this._items.length;
|
|
121
|
+
return `${n} image${n === 1 ? '' : 's'}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ── lazy paging (the gallery only ASKS; the host fetches) ───────────────── */
|
|
125
|
+
|
|
126
|
+
/** Ensure a sentinel + observer exist so reaching the end emits 'load-more'.
|
|
127
|
+
* A Load-more button is added as a no-IntersectionObserver fallback. */
|
|
128
|
+
_armSentinel() {
|
|
129
|
+
if (this._complete) return;
|
|
130
|
+
if (!this._sentinel) {
|
|
131
|
+
this._sentinel = document.createElement('div');
|
|
132
|
+
this._sentinel.className = 'gallery-sentinel';
|
|
133
|
+
}
|
|
134
|
+
this._grid.appendChild(this._sentinel); // keep it last
|
|
135
|
+
|
|
136
|
+
if ('IntersectionObserver' in window) {
|
|
137
|
+
if (this._io) this._io.disconnect();
|
|
138
|
+
this._io = new IntersectionObserver((entries) => {
|
|
139
|
+
if (entries.some((e) => e.isIntersecting)) this._requestMore();
|
|
140
|
+
}, { root: this._main, rootMargin: '600px' });
|
|
141
|
+
this._io.observe(this._sentinel);
|
|
142
|
+
} else if (!this._moreBtn) {
|
|
143
|
+
this._moreBtn = document.createElement('button');
|
|
144
|
+
this._moreBtn.type = 'button';
|
|
145
|
+
this._moreBtn.className = 'gallery-more';
|
|
146
|
+
this._moreBtn.textContent = 'Load more';
|
|
147
|
+
this._moreBtn.addEventListener('click', () => this._requestMore());
|
|
148
|
+
this._main.appendChild(this._moreBtn);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_removeSentinel() {
|
|
153
|
+
if (this._io) { this._io.disconnect(); this._io = null; }
|
|
154
|
+
this._sentinel?.remove();
|
|
155
|
+
this._moreBtn?.remove();
|
|
156
|
+
this._moreBtn = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Ask the host for the next page (once; re-armed when add() lands). */
|
|
160
|
+
_requestMore() {
|
|
161
|
+
if (this._complete || this._awaitingPage) return;
|
|
162
|
+
this._awaitingPage = true;
|
|
163
|
+
if (this._io) { this._io.disconnect(); this._io = null; } // re-armed by add()
|
|
164
|
+
this.dispatchEvent(new CustomEvent('load-more', { bubbles: true, composed: true }));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ── thumbnails ──────────────────────────────────────────────────────────── */
|
|
168
|
+
|
|
169
|
+
/** One masonry thumbnail button → opens the lightbox at its index. */
|
|
170
|
+
_thumb(item, index) {
|
|
171
|
+
const b = document.createElement('button');
|
|
172
|
+
b.type = 'button';
|
|
173
|
+
b.className = 'gallery-thumb';
|
|
174
|
+
b.dataset.index = String(index);
|
|
175
|
+
b.setAttribute('aria-label', item.caption || 'image');
|
|
176
|
+
const el = document.createElement('img');
|
|
177
|
+
el.src = item.thumb;
|
|
178
|
+
el.alt = item.caption || '';
|
|
179
|
+
el.loading = 'lazy';
|
|
180
|
+
if (item.width && item.height) { el.width = item.width; el.height = item.height; }
|
|
181
|
+
el.addEventListener('error', () => b.remove());
|
|
182
|
+
b.appendChild(el);
|
|
183
|
+
b.addEventListener('click', () => this.openLightbox(index));
|
|
184
|
+
return b;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* ── lightbox ────────────────────────────────────────────────────────────── */
|
|
188
|
+
|
|
189
|
+
_buildLightbox() {
|
|
190
|
+
const lb = document.createElement('div');
|
|
191
|
+
lb.className = 'gallery-lightbox';
|
|
192
|
+
lb.hidden = true;
|
|
193
|
+
lb.setAttribute('role', 'dialog');
|
|
194
|
+
lb.setAttribute('aria-modal', 'true');
|
|
195
|
+
|
|
196
|
+
const img = document.createElement('img');
|
|
197
|
+
const caption = document.createElement('div');
|
|
198
|
+
caption.className = 'gallery-lb-caption';
|
|
199
|
+
const prev = document.createElement('button');
|
|
200
|
+
prev.type = 'button'; prev.className = 'gallery-lb-btn gallery-lb-prev';
|
|
201
|
+
prev.textContent = '‹'; prev.setAttribute('aria-label', 'Previous image');
|
|
202
|
+
const next = document.createElement('button');
|
|
203
|
+
next.type = 'button'; next.className = 'gallery-lb-btn gallery-lb-next';
|
|
204
|
+
next.textContent = '›'; next.setAttribute('aria-label', 'Next image');
|
|
205
|
+
const close = document.createElement('button');
|
|
206
|
+
close.type = 'button'; close.className = 'gallery-lb-close';
|
|
207
|
+
close.textContent = '✕'; close.setAttribute('aria-label', 'Close');
|
|
208
|
+
|
|
209
|
+
lb.append(close, prev, img, next, caption);
|
|
210
|
+
this.shadowRoot.appendChild(lb);
|
|
211
|
+
this._lb = { lb, img, caption, prev, next, close };
|
|
212
|
+
|
|
213
|
+
prev.addEventListener('click', () => this.stepLightbox(-1));
|
|
214
|
+
next.addEventListener('click', () => this.stepLightbox(1));
|
|
215
|
+
close.addEventListener('click', () => this.closeLightbox());
|
|
216
|
+
lb.addEventListener('click', (e) => { if (e.target === lb) this.closeLightbox(); });
|
|
217
|
+
// Click the image to toggle a full-bleed, actual-size (100%) view that pans
|
|
218
|
+
// via scroll; click again (or page / Esc) to return to fit. Only offered
|
|
219
|
+
// when the fit view shows fewer pixels than the image actually has.
|
|
220
|
+
img.addEventListener('click', (e) => { e.stopPropagation(); if (this._canZoom) this.setZoom(!this._lbZoom); });
|
|
221
|
+
img.addEventListener('load', () => this._refreshZoomable());
|
|
222
|
+
|
|
223
|
+
this._onKey = (e) => {
|
|
224
|
+
if (this._lb.lb.hidden) return;
|
|
225
|
+
if (e.key === 'Escape') this.closeLightbox();
|
|
226
|
+
else if (e.key === 'ArrowLeft') this.stepLightbox(-1);
|
|
227
|
+
else if (e.key === 'ArrowRight') this.stepLightbox(1);
|
|
228
|
+
};
|
|
229
|
+
document.addEventListener('keydown', this._onKey);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
openLightbox(index) {
|
|
233
|
+
this._lbIndex = index;
|
|
234
|
+
this.showLightboxImage();
|
|
235
|
+
this._lb.lb.hidden = false;
|
|
236
|
+
this._lb.close.focus();
|
|
237
|
+
// Lazy per-item detail hook: a host may listen and enrich (e.g. Wikidata).
|
|
238
|
+
const it = this._items[index];
|
|
239
|
+
if (it) this.dispatchEvent(new CustomEvent('item-opened', {
|
|
240
|
+
detail: { iri: it.iri }, bubbles: true, composed: true,
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
stepLightbox(delta) {
|
|
245
|
+
const imgs = this._items;
|
|
246
|
+
if (!imgs.length) return;
|
|
247
|
+
this._lbIndex = (this._lbIndex + delta + imgs.length) % imgs.length;
|
|
248
|
+
this.showLightboxImage();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Decide whether the current (fit) image can usefully zoom: only when its
|
|
252
|
+
* natural pixel width exceeds the fit-rendered width. Toggles the no-zoom
|
|
253
|
+
* class (hides the zoom-in cursor) and gates the click handler. */
|
|
254
|
+
_refreshZoomable() {
|
|
255
|
+
const { lb, img } = this._lb;
|
|
256
|
+
const fitW = this._lbZoom ? 0 : img.getBoundingClientRect().width;
|
|
257
|
+
this._canZoom = !this._lbZoom && img.naturalWidth > Math.ceil(fitW) + 1;
|
|
258
|
+
lb.classList.toggle('no-zoom', !this._canZoom);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
setZoom(on) {
|
|
262
|
+
this._lbZoom = on;
|
|
263
|
+
const { lb, img } = this._lb;
|
|
264
|
+
lb.classList.toggle('zoomed', on);
|
|
265
|
+
if (on) {
|
|
266
|
+
requestAnimationFrame(() => {
|
|
267
|
+
lb.scrollLeft = Math.max(0, (img.scrollWidth - lb.clientWidth) / 2);
|
|
268
|
+
lb.scrollTop = Math.max(0, (img.scrollHeight - lb.clientHeight) / 2);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
showLightboxImage() {
|
|
274
|
+
const it = this._items[this._lbIndex];
|
|
275
|
+
if (!it) return;
|
|
276
|
+
this.setZoom(false); // each image starts fit-to-screen
|
|
277
|
+
this._lb.img.src = it.full || it.thumb;
|
|
278
|
+
this._lb.img.alt = it.caption || '';
|
|
279
|
+
const bits = [it.caption, it.author, it.license].filter(Boolean);
|
|
280
|
+
this._lb.caption.textContent = bits.join(' · ');
|
|
281
|
+
if (it.detailUrl) {
|
|
282
|
+
this._lb.caption.append(' ');
|
|
283
|
+
const link = document.createElement('a');
|
|
284
|
+
link.href = it.detailUrl; link.target = '_blank'; link.rel = 'noopener';
|
|
285
|
+
link.textContent = 'View on Commons ↗';
|
|
286
|
+
this._lb.caption.appendChild(link);
|
|
287
|
+
}
|
|
288
|
+
const multi = this._items.length > 1;
|
|
289
|
+
this._lb.prev.style.display = multi ? '' : 'none';
|
|
290
|
+
this._lb.next.style.display = multi ? '' : 'none';
|
|
291
|
+
this._refreshZoomable(); // default to no-zoom until the image loads
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
closeLightbox() {
|
|
295
|
+
this.setZoom(false);
|
|
296
|
+
this._lb.lb.hidden = true;
|
|
297
|
+
this._lb.img.removeAttribute('src');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
define('sol-gallery', SolGallery);
|
|
302
|
+
|
|
303
|
+
export { SolGallery };
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <sol-include> — Fetch and display remote content inline.
|
|
3
|
+
*
|
|
4
|
+
* Supports HTML, Markdown, and plain text. Content is sanitized with
|
|
5
|
+
* DOMPurify by default. An optional CSS selector filters to a section
|
|
6
|
+
* of the fetched document.
|
|
7
|
+
*
|
|
8
|
+
* @element sol-include
|
|
9
|
+
* @attr {string} source - URL of the resource to fetch (required)
|
|
10
|
+
* @attr {string} if-logged-in - alternate source used when a user is logged in
|
|
11
|
+
* (a live Solid session OR the window.SolidKitchen dev flag,
|
|
12
|
+
* treated identically); falls back to `source` otherwise.
|
|
13
|
+
* Re-evaluates on sol-login / sol-logout.
|
|
14
|
+
* @attr {string} selector - CSS selector — show only matching elements
|
|
15
|
+
* @attr {boolean} raw - show source text verbatim without rendering
|
|
16
|
+
* @attr {boolean} trusted - skip DOMPurify sanitization
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* <sol-include source="https://example.org/readme.md"></sol-include>
|
|
20
|
+
* <sol-include source="page.html" selector="article"></sol-include>
|
|
21
|
+
* <sol-include source="guest.html" if-logged-in="owner.html"></sol-include>
|
|
22
|
+
*/
|
|
23
|
+
import { sanitizeHtml } from '../core/utils.js';
|
|
24
|
+
import { define } from '../core/define.js';
|
|
25
|
+
import { adopt } from '../core/adopt.js';
|
|
26
|
+
import { fetchIncludeContent, filterWithSelector } from '../core/include-core.js';
|
|
27
|
+
import { getAuthFetch } from '../core/auth-fetch.js';
|
|
28
|
+
import { CSS as INCLUDE_CSS, sheet as includeSheet } from './styles/sol-include-css.js';
|
|
29
|
+
|
|
30
|
+
function browserContainer(html) {
|
|
31
|
+
const div = document.createElement('div');
|
|
32
|
+
div.innerHTML = html;
|
|
33
|
+
return div;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Is there an authenticated user? True for a live Solid session (a logged-in
|
|
38
|
+
* <sol-login>, or any logged-in session in the shared AuthManager), and for the
|
|
39
|
+
* dev "kitchen" flag — declared as `solid-kitchen` on <sol-default> (or the
|
|
40
|
+
* legacy `window.SolidKitchen` global) — treated as exactly equivalent to being
|
|
41
|
+
* logged in. Used by the `if-logged-in` source switch.
|
|
42
|
+
*/
|
|
43
|
+
function isLoggedIn() {
|
|
44
|
+
try { if (typeof window !== 'undefined' && window.SolidKitchen === true) return true; } catch { /* ignore */ }
|
|
45
|
+
if (typeof document === 'undefined') return false;
|
|
46
|
+
if (document.querySelector('sol-default')?.hasAttribute('solid-kitchen')) return true;
|
|
47
|
+
const login = document.querySelector('sol-login');
|
|
48
|
+
if (login && login.isLoggedIn) return true;
|
|
49
|
+
try {
|
|
50
|
+
const am = window.SolidWebComponents?.AuthManager?.shared;
|
|
51
|
+
if (am && [...am.sessions.values()].some((s) => s.info?.isLoggedIn)) return true;
|
|
52
|
+
} catch { /* ignore */ }
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Fetch and display remote content inline.
|
|
58
|
+
*
|
|
59
|
+
* Supports HTML, Markdown, and plain text. Content is sanitized with
|
|
60
|
+
* DOMPurify by default.
|
|
61
|
+
*
|
|
62
|
+
* @class SolInclude
|
|
63
|
+
* @extends HTMLElement
|
|
64
|
+
* @attr {string} source - URL to fetch (required)
|
|
65
|
+
* @attr {string} selector - CSS selector — show only matching elements
|
|
66
|
+
* @attr {boolean} raw - show source text verbatim
|
|
67
|
+
* @attr {boolean} trusted - skip DOMPurify sanitization, render into LIGHT
|
|
68
|
+
* DOM (via a shadow <slot>) so host CSS reaches the content
|
|
69
|
+
*
|
|
70
|
+
* Layout: `:host` is a flex column with `flex: 1 1 auto; min-height: 0` and
|
|
71
|
+
* the `.si-content` wrapper gets the same treatment (matched in both the
|
|
72
|
+
* shadow and trusted-slotted modes), so a definite parent height propagates
|
|
73
|
+
* down to components placed inside (sol-pod, sol-menu, etc.). They can then
|
|
74
|
+
* fill and scroll on their own — sol-include itself never scrolls.
|
|
75
|
+
*/
|
|
76
|
+
class SolInclude extends HTMLElement {
|
|
77
|
+
static get observedAttributes() {
|
|
78
|
+
return ['source', 'if-logged-in', 'selector', 'raw', 'trusted'];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
constructor() {
|
|
82
|
+
super();
|
|
83
|
+
this.attachShadow({ mode: 'open' });
|
|
84
|
+
this._abortCtl = null;
|
|
85
|
+
this._authListener = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
connectedCallback() {
|
|
89
|
+
if (!this.isConnected) return;
|
|
90
|
+
// Only watch auth state when an `if-logged-in` alternate is declared, so the
|
|
91
|
+
// displayed source follows login / logout.
|
|
92
|
+
if (this.hasAttribute('if-logged-in') && !this._authListener) {
|
|
93
|
+
this._authListener = () => { if (this.isConnected) this._load(); };
|
|
94
|
+
document.addEventListener('sol-login', this._authListener);
|
|
95
|
+
document.addEventListener('sol-logout', this._authListener);
|
|
96
|
+
}
|
|
97
|
+
this._load();
|
|
98
|
+
}
|
|
99
|
+
attributeChangedCallback(n, oldV, newV) { if (oldV !== newV && this.isConnected) this._load(); }
|
|
100
|
+
disconnectedCallback() {
|
|
101
|
+
this._abortCtl?.abort(); this._abortCtl = null;
|
|
102
|
+
if (this._authListener) {
|
|
103
|
+
document.removeEventListener('sol-login', this._authListener);
|
|
104
|
+
document.removeEventListener('sol-logout', this._authListener);
|
|
105
|
+
this._authListener = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The source to show now: the `if-logged-in` alternate when authenticated,
|
|
110
|
+
* otherwise the plain `source`. */
|
|
111
|
+
_effectiveSource() {
|
|
112
|
+
const alt = this.getAttribute('if-logged-in');
|
|
113
|
+
if (alt && isLoggedIn()) return alt;
|
|
114
|
+
return this.getAttribute('source');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Main load ─────────────────────────────────────────────────────────────────
|
|
118
|
+
async _load() {
|
|
119
|
+
const source = this._effectiveSource();
|
|
120
|
+
const selector = this.getAttribute('selector') || '';
|
|
121
|
+
const raw = this.hasAttribute('raw');
|
|
122
|
+
const trusted = this.hasAttribute('trusted');
|
|
123
|
+
|
|
124
|
+
if (!source) { this._show('error', 'No source provided'); return; }
|
|
125
|
+
|
|
126
|
+
// Cancel any in-flight load triggered by an earlier attribute change.
|
|
127
|
+
this._abortCtl?.abort();
|
|
128
|
+
const ctl = new AbortController();
|
|
129
|
+
this._abortCtl = ctl;
|
|
130
|
+
|
|
131
|
+
this._show('loading', 'Loading…');
|
|
132
|
+
|
|
133
|
+
// `login` attribute (CSS selector for a sol-login) overrides auto-discovery.
|
|
134
|
+
const loginSel = this.getAttribute('login');
|
|
135
|
+
const loginEl = loginSel ? document.querySelector(loginSel) : null;
|
|
136
|
+
const fetchFn = getAuthFetch(source, { element: loginEl || undefined });
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
// When a selector is present, defer sanitization so the selector can
|
|
140
|
+
// match attributes (e.g. RDFa typeof/rel) that DOMPurify would strip.
|
|
141
|
+
const { type, content } = await fetchIncludeContent(source, {
|
|
142
|
+
raw,
|
|
143
|
+
trusted: trusted || !!selector,
|
|
144
|
+
sanitize: sanitizeHtml,
|
|
145
|
+
signal: ctl.signal,
|
|
146
|
+
fetchFn,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (ctl.signal.aborted) return;
|
|
150
|
+
|
|
151
|
+
if (type === 'raw') {
|
|
152
|
+
this._showRaw(content);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (selector) {
|
|
157
|
+
const filtered = filterWithSelector(content, selector, browserContainer);
|
|
158
|
+
if (ctl.signal.aborted) return;
|
|
159
|
+
if (filtered === null) {
|
|
160
|
+
this._show('empty', 'No elements matched selector');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this._showHtml(trusted ? filtered : await sanitizeHtml(filtered));
|
|
164
|
+
} else {
|
|
165
|
+
this._showHtml(content);
|
|
166
|
+
}
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (err.name === 'AbortError' || ctl.signal.aborted) return;
|
|
169
|
+
this._show('error', err.message);
|
|
170
|
+
this._fireError('load', err.message);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_fireError(kind, message) {
|
|
175
|
+
this.dispatchEvent(new CustomEvent('sol-error', {
|
|
176
|
+
bubbles: true, composed: true,
|
|
177
|
+
detail: { source: 'sol-include', kind, message },
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Display helpers ───────────────────────────────────────────────────────────
|
|
182
|
+
_showHtml(html) {
|
|
183
|
+
this._initShadow();
|
|
184
|
+
const div = document.createElement('div');
|
|
185
|
+
div.className = 'si-content';
|
|
186
|
+
div.innerHTML = html;
|
|
187
|
+
// When the consumer marked the source as `trusted`, render into
|
|
188
|
+
// LIGHT DOM rather than shadow DOM. Trusted content is, by
|
|
189
|
+
// definition, page-authored — it should inherit the host's
|
|
190
|
+
// stylesheets so .my-class rules apply, custom-element CSS lands,
|
|
191
|
+
// etc. The shadow root contains a <slot> in trusted mode so the
|
|
192
|
+
// light-DOM child still gets projected into the host's box.
|
|
193
|
+
// Untrusted content stays in shadow for isolation.
|
|
194
|
+
if (this.hasAttribute('trusted')) {
|
|
195
|
+
this.appendChild(div);
|
|
196
|
+
} else {
|
|
197
|
+
this.shadowRoot.appendChild(div);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Remove any prior trusted-mode light-DOM render so reload swaps
|
|
202
|
+
// cleanly without piling stale content next to the new. Called from
|
|
203
|
+
// _initShadow on every state transition; the trusted append path
|
|
204
|
+
// can assume it starts clean.
|
|
205
|
+
_clearLightContent() {
|
|
206
|
+
for (const child of Array.from(this.children)) {
|
|
207
|
+
if (child.classList?.contains('si-content')) child.remove();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
_showRaw(text) {
|
|
212
|
+
this._initShadow();
|
|
213
|
+
const pre = document.createElement('pre');
|
|
214
|
+
pre.className = 'si-raw';
|
|
215
|
+
pre.textContent = text;
|
|
216
|
+
this.shadowRoot.appendChild(pre);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
_show(type, message) {
|
|
220
|
+
this._initShadow();
|
|
221
|
+
const div = document.createElement('div');
|
|
222
|
+
div.className = `si-${type}`;
|
|
223
|
+
if (type === 'error') div.setAttribute('role', 'alert');
|
|
224
|
+
else if (type === 'loading' || type === 'empty') {
|
|
225
|
+
div.setAttribute('role', 'status');
|
|
226
|
+
div.setAttribute('aria-live', 'polite');
|
|
227
|
+
}
|
|
228
|
+
div.textContent = message;
|
|
229
|
+
this.shadowRoot.appendChild(div);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Reset for a fresh render. Drops any prior light-DOM content so
|
|
233
|
+
// reload doesn't double-stack, then resets the shadow root. In
|
|
234
|
+
// `trusted` mode the shadow holds a single <slot> so a subsequent
|
|
235
|
+
// light-DOM append shows through; in untrusted mode there's no
|
|
236
|
+
// slot, so any light children stay hidden as before.
|
|
237
|
+
_initShadow() {
|
|
238
|
+
this._clearLightContent();
|
|
239
|
+
this.shadowRoot.innerHTML = this.hasAttribute('trusted') ? '<slot></slot>' : '';
|
|
240
|
+
this.shadowRoot.adoptedStyleSheets = [];
|
|
241
|
+
adopt(this.shadowRoot, { sheet: includeSheet, css: INCLUDE_CSS });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
define('sol-include', SolInclude);
|
|
246
|
+
export { SolInclude };
|