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/README.md
ADDED
package/core/activate.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// core/activate.js — run a capability's behavior over every element bearing one
|
|
2
|
+
// of its attributes, now and as elements mount. This is what makes a capability
|
|
3
|
+
// attribute (e.g. data-from-query) work on ANY element, component or not: the
|
|
4
|
+
// behavior is injected by a DOM walk, not implemented by the element's class.
|
|
5
|
+
//
|
|
6
|
+
// activate('[data-from-query]', (el) => …); // called once per matching element
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} selector — CSS selector for the capability's attribute
|
|
10
|
+
* @param {(el: Element) => void} fn — wiring run once per matching element
|
|
11
|
+
* @returns {() => void} stop the observer
|
|
12
|
+
*/
|
|
13
|
+
export function activate(selector, fn) {
|
|
14
|
+
if (typeof document === 'undefined') return () => {};
|
|
15
|
+
const seen = new WeakSet();
|
|
16
|
+
const scan = () => {
|
|
17
|
+
for (const el of document.querySelectorAll(selector)) {
|
|
18
|
+
if (seen.has(el)) continue;
|
|
19
|
+
seen.add(el);
|
|
20
|
+
try { fn(el); } catch (e) { console.error('[sol-loader] activator error for', selector, e); }
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
scan();
|
|
24
|
+
const mo = new MutationObserver(scan);
|
|
25
|
+
mo.observe(document.documentElement || document, { childList: true, subtree: true });
|
|
26
|
+
return () => mo.disconnect();
|
|
27
|
+
}
|
package/core/adopt.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style helpers for Constructable Stylesheets + adoptedStyleSheets.
|
|
3
|
+
*
|
|
4
|
+
* `sheetFrom(css)` returns a `CSSStyleSheet` on evergreen browsers, or
|
|
5
|
+
* `null` when the constructor is unavailable (e.g. Jest/node env). Callers
|
|
6
|
+
* should keep the raw `CSS` string export alongside the sheet so they can
|
|
7
|
+
* fall back to a `<style>` tag when `sheet` is null.
|
|
8
|
+
*
|
|
9
|
+
* `adopt(root, { sheet, css, extra })` wires a shadow root (or document)
|
|
10
|
+
* with the baseline module sheet (+ any extras). If no sheet is available
|
|
11
|
+
* it appends `<style>${css}</style>` into the root instead.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
let _supports = null;
|
|
15
|
+
function supports() {
|
|
16
|
+
if (_supports !== null) return _supports;
|
|
17
|
+
try {
|
|
18
|
+
const s = new CSSStyleSheet();
|
|
19
|
+
_supports = typeof s.replaceSync === 'function';
|
|
20
|
+
} catch {
|
|
21
|
+
_supports = false;
|
|
22
|
+
}
|
|
23
|
+
return _supports;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sheetFrom(css) {
|
|
27
|
+
if (!supports()) return null;
|
|
28
|
+
const s = new CSSStyleSheet();
|
|
29
|
+
s.replaceSync(css);
|
|
30
|
+
return s;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Adopt a CSSStyleSheet into `root` (ShadowRoot or Document). When sheets
|
|
34
|
+
// aren't supported, falls back to inserting a <style> with the given css.
|
|
35
|
+
export function adopt(root, { sheet, css, extra = [] } = {}) {
|
|
36
|
+
const host = root.adoptedStyleSheets !== undefined ? root : null;
|
|
37
|
+
if (host && sheet) {
|
|
38
|
+
const sheets = [sheet];
|
|
39
|
+
const strings = [];
|
|
40
|
+
for (const e of extra) {
|
|
41
|
+
if (e instanceof CSSStyleSheet) sheets.push(e);
|
|
42
|
+
else if (typeof e === 'string') strings.push(e);
|
|
43
|
+
}
|
|
44
|
+
host.adoptedStyleSheets = [...host.adoptedStyleSheets, ...sheets];
|
|
45
|
+
for (const s of strings) appendStyle(root, s);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Fallback path: inline <style> for baseline + extras.
|
|
49
|
+
if (css) appendStyle(root, css);
|
|
50
|
+
for (const e of extra) {
|
|
51
|
+
if (typeof e === 'string') appendStyle(root, e);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function appendStyle(root, css) {
|
|
56
|
+
const el = document.createElement('style');
|
|
57
|
+
el.textContent = css;
|
|
58
|
+
root.appendChild(el);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Ensure a named stylesheet exists in the given document/shadow-root head
|
|
62
|
+
// exactly once. Useful for light-DOM components.
|
|
63
|
+
export function ensureDocStyle(root, id, css) {
|
|
64
|
+
const target = root.nodeType === 11 ? root : (root.ownerDocument || document);
|
|
65
|
+
if (!target) return;
|
|
66
|
+
if (target.getElementById?.(id)) return;
|
|
67
|
+
const el = document.createElement('style');
|
|
68
|
+
el.id = id;
|
|
69
|
+
el.textContent = css;
|
|
70
|
+
(target.head || target).appendChild(el);
|
|
71
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export function originOf(url) {
|
|
2
|
+
return url.match(/^https?:\/\/[^/]+/)?.[0] || '';
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function baseDomain(host) {
|
|
6
|
+
const p = host.split('.');
|
|
7
|
+
return p.length >= 2 ? p.slice(-2).join('.') : host;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function sessionCoversOrigin(session, origin) {
|
|
11
|
+
if (!session.info?.isLoggedIn) return false;
|
|
12
|
+
try {
|
|
13
|
+
const rHost = new URL(origin).host;
|
|
14
|
+
const rBase = baseDomain(rHost);
|
|
15
|
+
for (const ref of [session.info.issuer, session.info.webId]) {
|
|
16
|
+
if (!ref) continue;
|
|
17
|
+
const h = new URL(ref).host;
|
|
18
|
+
if (rHost === h || rHost.endsWith('.' + h) || baseDomain(h) === rBase) return true;
|
|
19
|
+
}
|
|
20
|
+
} catch {}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isNoAuth(url, noAuthConfig) {
|
|
25
|
+
try {
|
|
26
|
+
const origin = new URL(url).origin;
|
|
27
|
+
if (!noAuthConfig) return false;
|
|
28
|
+
return Array.isArray(noAuthConfig) ? noAuthConfig.includes(origin) : origin === noAuthConfig;
|
|
29
|
+
} catch { return false; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getSessionFor(sessions, url, tag, noAuthConfig) {
|
|
33
|
+
if (!url || isNoAuth(url, noAuthConfig)) return null;
|
|
34
|
+
const own = sessions.get(tag);
|
|
35
|
+
if (own?.info?.isLoggedIn) return own;
|
|
36
|
+
const origin = originOf(url);
|
|
37
|
+
for (const [, s] of sessions) {
|
|
38
|
+
if (sessionCoversOrigin(s, origin)) return s;
|
|
39
|
+
}
|
|
40
|
+
return own || null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function makeFetchFor(sessions, url, tag, noAuthConfig, defaultFetch) {
|
|
44
|
+
if (isNoAuth(url, noAuthConfig)) return defaultFetch;
|
|
45
|
+
const session = tag
|
|
46
|
+
? getSessionFor(sessions, url, tag, noAuthConfig)
|
|
47
|
+
: (() => {
|
|
48
|
+
const origin = originOf(url);
|
|
49
|
+
for (const [, s] of sessions) {
|
|
50
|
+
if (sessionCoversOrigin(s, origin)) return s;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
})();
|
|
54
|
+
if (!session?.fetch) return defaultFetch;
|
|
55
|
+
return (input, init) => session.fetch(input, init);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isLoggedInFor(sessions, url, tag, noAuthConfig) {
|
|
59
|
+
if (!url || isNoAuth(url, noAuthConfig)) return true;
|
|
60
|
+
const s = getSessionFor(sessions, url, tag, noAuthConfig);
|
|
61
|
+
return s?.info?.isLoggedIn ?? false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getWebId(sessions, tag = 'default') {
|
|
65
|
+
return sessions.get(tag)?.info?.webId || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getFirstLoggedIn(sessions) {
|
|
69
|
+
for (const s of sessions.values()) {
|
|
70
|
+
if (s.info?.isLoggedIn) return s;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/auth-fetch.js — page-wide authenticated fetch lookup.
|
|
3
|
+
*
|
|
4
|
+
* Components that need to fetch resources (sol-query for SPARQL endpoints,
|
|
5
|
+
* sol-include for documents, the Comunica adapter, …) call getAuthFetch(url)
|
|
6
|
+
* to obtain a fetch function. If a logged-in <sol-login> is on the page,
|
|
7
|
+
* its session.fetch is returned; otherwise the global fetch is.
|
|
8
|
+
*
|
|
9
|
+
* The component-explicit `login` attribute used by sol-pod / sol-pod-ops /
|
|
10
|
+
* sol-wac still wins — pass `opts.element` here to plumb that through.
|
|
11
|
+
*
|
|
12
|
+
* Lookup is light-DOM only: <sol-login> hidden inside another shadow root
|
|
13
|
+
* isn't auto-discovered. That's by design — cross-shadow auth has to be
|
|
14
|
+
* explicit because the host component can't safely guess the user's intent.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Return a fetch function appropriate for `url`. Always returns a usable
|
|
19
|
+
* fetch (never null) — callers can use the result without null-checking.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} url — the URL the caller is about to fetch
|
|
22
|
+
* @param {object} [opts]
|
|
23
|
+
* @param {Element} [opts.element] — explicit sol-login element (overrides lookup)
|
|
24
|
+
* @param {string} [opts.tag] — session tag; defaults to the targeted
|
|
25
|
+
* element's `side`, else 'default'
|
|
26
|
+
* @returns {(input: RequestInfo, init?: RequestInit) => Promise<Response>}
|
|
27
|
+
*/
|
|
28
|
+
export function getAuthFetch(url, opts = {}) {
|
|
29
|
+
const login = opts.element || findFirstSolLogin();
|
|
30
|
+
// Sessions are keyed by tag — a <sol-login>'s `side`. An explicitly
|
|
31
|
+
// targeted element selects its own session by that side; auto-discovered
|
|
32
|
+
// or side-less logins use the 'default' tag.
|
|
33
|
+
const tag = opts.tag
|
|
34
|
+
|| (opts.element && typeof opts.element.getAttribute === 'function'
|
|
35
|
+
&& opts.element.getAttribute('side'))
|
|
36
|
+
|| 'default';
|
|
37
|
+
if (login && typeof login.fetchFor === 'function') {
|
|
38
|
+
try {
|
|
39
|
+
const f = login.fetchFor(url, tag);
|
|
40
|
+
if (typeof f === 'function') return f;
|
|
41
|
+
} catch { /* fall through to adopted / global fetch */ }
|
|
42
|
+
}
|
|
43
|
+
// No <sol-login> (or it declined): a host may have adopted a foreign
|
|
44
|
+
// authenticated fetch via SolidWebComponents.adoptFetch (e.g. PodOS's
|
|
45
|
+
// authenticatedFetch). Prefer it over the unauthenticated global fetch.
|
|
46
|
+
const adopted = (typeof window !== 'undefined') && window.SolidWebComponents?.adoptedFetch;
|
|
47
|
+
if (typeof adopted === 'function') return adopted;
|
|
48
|
+
// `globalThis.fetch` may be missing in some Node test environments —
|
|
49
|
+
// return undefined so callers fall back to their own default (most use
|
|
50
|
+
// `fetchFn = globalThis.fetch` as the parameter default).
|
|
51
|
+
return typeof globalThis.fetch === 'function' ? globalThis.fetch.bind(globalThis) : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find the first <sol-login> in the document, light-DOM only.
|
|
56
|
+
* Returns null if none is present (or in non-browser environments).
|
|
57
|
+
*/
|
|
58
|
+
function findFirstSolLogin() {
|
|
59
|
+
if (typeof document === 'undefined') return null;
|
|
60
|
+
return document.querySelector('sol-login');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* ── solFetch: auto-prompt fetch wrapper ──────────────────────────────
|
|
64
|
+
*
|
|
65
|
+
* solFetch(url, opts) wraps fetch and, when the response indicates auth
|
|
66
|
+
* is required, dispatches `sol-auth-needed` on document with a Promise
|
|
67
|
+
* resolver in the detail. A listener (typically <sol-login>) runs the
|
|
68
|
+
* login UI and resolves the promise; solFetch then retries once.
|
|
69
|
+
*
|
|
70
|
+
* Event contract (public API):
|
|
71
|
+
*
|
|
72
|
+
* document.addEventListener('sol-auth-needed', (e) => {
|
|
73
|
+
* const { url, response, resolve, reject } = e.detail;
|
|
74
|
+
* // Run login UI; call resolve(true) when authed, resolve(false)
|
|
75
|
+
* // to give up, reject(err) on error.
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* When no `<sol-login>` is mounted, solFetch falls through to a plain
|
|
79
|
+
* fetch (so swc widgets work in unauthed contexts). When AuthManager
|
|
80
|
+
* already has a covering session, the first request is authenticated
|
|
81
|
+
* and `sol-auth-needed` is never fired.
|
|
82
|
+
*
|
|
83
|
+
* Frame integration: a frame (dk, etc.) just mounts `<sol-login>` once
|
|
84
|
+
* — the listener is attached automatically by sol-login's
|
|
85
|
+
* connectedCallback. The frame supplies `default-issuer` via
|
|
86
|
+
* `<sol-default default-issuer="…">` or on the sol-login element
|
|
87
|
+
* itself; sol-login uses it as the auto-login target.
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
const AUTH_NEEDED_EVENT = 'sol-auth-needed';
|
|
91
|
+
const AUTH_PROMPT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
92
|
+
|
|
93
|
+
function getAuthManager() {
|
|
94
|
+
if (typeof window === 'undefined') return null;
|
|
95
|
+
return window.SolidWebComponents?.AuthManager?.shared || null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function hasLoginListener() {
|
|
99
|
+
if (typeof document === 'undefined') return false;
|
|
100
|
+
return !!document.querySelector('sol-login');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 401 → prompt always. 403 → prompt only when no session is active
|
|
104
|
+
* (with an active session, 403 means "logged in but not authorized",
|
|
105
|
+
* re-login won't help, so surface the response as-is). */
|
|
106
|
+
function shouldPrompt(response, am) {
|
|
107
|
+
if (response.status === 401) return true;
|
|
108
|
+
if (response.status === 403) {
|
|
109
|
+
if (!am) return true;
|
|
110
|
+
const anyLoggedIn = [...am.sessions.values()].some(s => s.info?.isLoggedIn);
|
|
111
|
+
return !anyLoggedIn;
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function awaitAuth(url, response, side) {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
let settled = false;
|
|
119
|
+
const finish = (ok) => { if (!settled) { settled = true; resolve(!!ok); } };
|
|
120
|
+
const fail = (err) => { if (!settled) { settled = true; reject(err); } };
|
|
121
|
+
document.dispatchEvent(new CustomEvent(AUTH_NEEDED_EVENT, {
|
|
122
|
+
bubbles: false, composed: false,
|
|
123
|
+
detail: { url, response, side, resolve: finish, reject: fail },
|
|
124
|
+
}));
|
|
125
|
+
setTimeout(() => finish(false), AUTH_PROMPT_TIMEOUT_MS);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Authenticated fetch with auto-prompt on 401 (and unauthenticated 403).
|
|
131
|
+
* @param {string|URL|Request} url
|
|
132
|
+
* @param {RequestInit} [opts]
|
|
133
|
+
* @returns {Promise<Response>}
|
|
134
|
+
*/
|
|
135
|
+
export async function solFetch(url, opts) {
|
|
136
|
+
const am = getAuthManager();
|
|
137
|
+
const tag = opts?.authTag;
|
|
138
|
+
const baseFetch = am ? am.fetchFor(url, tag) : (typeof fetch !== 'undefined' ? fetch : null);
|
|
139
|
+
if (!baseFetch) throw new Error('solFetch: no fetch implementation available');
|
|
140
|
+
|
|
141
|
+
const response = await baseFetch(url, opts);
|
|
142
|
+
if (!shouldPrompt(response, am)) return response;
|
|
143
|
+
if (!hasLoginListener()) return response;
|
|
144
|
+
|
|
145
|
+
const ok = await awaitAuth(url, response, tag);
|
|
146
|
+
if (!ok) return response;
|
|
147
|
+
|
|
148
|
+
const retryFetch = (am ?? getAuthManager())?.fetchFor(url, tag) || baseFetch;
|
|
149
|
+
return retryFetch(url, opts);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Event name listeners subscribe to. Re-exported so callers needn't
|
|
153
|
+
* string-match. */
|
|
154
|
+
export const SOL_AUTH_NEEDED = AUTH_NEEDED_EVENT;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Shared mount-into-target helper used by:
|
|
2
|
+
// - core/rdf-render.js renderComponentItem (menu / tabs item rendering)
|
|
3
|
+
// - web/sol-button.js (declarative launcher in chrome)
|
|
4
|
+
//
|
|
5
|
+
// Both place a custom-element instance inside a per-named wrapper
|
|
6
|
+
// (`<div data-menu-item="<name>" data-keep-alive="true|false">`) under
|
|
7
|
+
// a CSS-selector-addressed `target` element, so multiple consumers can
|
|
8
|
+
// coexist in the same display area and play nicely with keep-alive.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Locate the wrapper a prior mount left in `target` for the given name.
|
|
12
|
+
* Returns null when none exists.
|
|
13
|
+
* @param {HTMLElement} target
|
|
14
|
+
* @param {string} name
|
|
15
|
+
* @returns {HTMLElement | null}
|
|
16
|
+
*/
|
|
17
|
+
export function findItemWrapper(target, name) {
|
|
18
|
+
if (!target) return null;
|
|
19
|
+
const esc = (typeof CSS !== 'undefined' && CSS.escape) ? CSS.escape(name) : name;
|
|
20
|
+
return target.querySelector(`:scope > [data-menu-item="${esc}"]`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Hide every other named wrapper inside `target`. The mount model is
|
|
25
|
+
* "always-persistent tabs" — clicking a menu/button just brings its
|
|
26
|
+
* own wrapper to the foreground and parks the others. Components are
|
|
27
|
+
* never torn down on nav-away, so their internal state (login
|
|
28
|
+
* sessions, scroll position, in-flight fetches, open accordion
|
|
29
|
+
* panels, etc.) survives across menu switches.
|
|
30
|
+
*/
|
|
31
|
+
export function pruneSiblings(target, activeName) {
|
|
32
|
+
if (!target) return;
|
|
33
|
+
const wraps = target.querySelectorAll(':scope > [data-menu-item]');
|
|
34
|
+
for (const w of wraps) {
|
|
35
|
+
if (w.dataset.menuItem === activeName) continue;
|
|
36
|
+
w.hidden = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Mount (or re-show) a component inside `target`, wrapped for
|
|
42
|
+
* coexistence with siblings.
|
|
43
|
+
*
|
|
44
|
+
* Two modes:
|
|
45
|
+
* - **Persistent tab** (default, `replace` falsy): each
|
|
46
|
+
* `(target, name)` pair is created once and reused — clicking
|
|
47
|
+
* back to the same name just unhides the existing wrapper, so
|
|
48
|
+
* internal state (login, scroll, in-flight fetches) survives.
|
|
49
|
+
* - **Shared / overwrite tab** (`replace: true`): the wrapper for
|
|
50
|
+
* `name` is kept across activations but its component is torn
|
|
51
|
+
* down and rebuilt with the latest attrs on every call. Useful
|
|
52
|
+
* for a single "scratch" pane that multiple sources write into
|
|
53
|
+
* (e.g. external menu links, ad-hoc launchers).
|
|
54
|
+
*
|
|
55
|
+
* @param {object} o
|
|
56
|
+
* @param {HTMLElement} o.target Where to mount (typically a region pane).
|
|
57
|
+
* @param {string} o.name Wrapper id (data-menu-item value).
|
|
58
|
+
* @param {string} o.tag Custom-element tag of the component.
|
|
59
|
+
* @param {Iterable<[string, string]>} [o.attrs] Attributes to set on
|
|
60
|
+
* the new component. Re-applied each call in
|
|
61
|
+
* replace mode; ignored when reusing in
|
|
62
|
+
* persistent mode.
|
|
63
|
+
* @param {string} [o.embedClass] Optional CSS class added to the
|
|
64
|
+
* mounted component (e.g. 'sol-menu-embed').
|
|
65
|
+
* @param {boolean} [o.replace] When true, rebuild the inner
|
|
66
|
+
* component on every call (the wrapper itself
|
|
67
|
+
* persists; only its contents are swapped).
|
|
68
|
+
* @returns {HTMLElement} the wrapper (existing or freshly created).
|
|
69
|
+
*/
|
|
70
|
+
export function mountInTarget({ target, name, tag, attrs, embedClass, replace }) {
|
|
71
|
+
if (!target || !tag) return null;
|
|
72
|
+
|
|
73
|
+
let wrap = findItemWrapper(target, name);
|
|
74
|
+
if (wrap && !replace) {
|
|
75
|
+
pruneSiblings(target, name);
|
|
76
|
+
wrap.hidden = false;
|
|
77
|
+
fireTabActivate(target, name);
|
|
78
|
+
return wrap;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (wrap) {
|
|
82
|
+
// Replace mode: rebuild contents but keep the wrapper itself.
|
|
83
|
+
wrap.innerHTML = '';
|
|
84
|
+
wrap.hidden = false;
|
|
85
|
+
} else {
|
|
86
|
+
wrap = document.createElement('div');
|
|
87
|
+
wrap.dataset.menuItem = name;
|
|
88
|
+
target.appendChild(wrap);
|
|
89
|
+
}
|
|
90
|
+
if (replace) wrap.dataset.replace = 'true';
|
|
91
|
+
|
|
92
|
+
const el = document.createElement(tag);
|
|
93
|
+
if (attrs) for (const [k, v] of attrs) el.setAttribute(k, v);
|
|
94
|
+
if (embedClass) el.classList.add(embedClass);
|
|
95
|
+
wrap.appendChild(el);
|
|
96
|
+
|
|
97
|
+
pruneSiblings(target, name);
|
|
98
|
+
fireTabActivate(target, name);
|
|
99
|
+
return wrap;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Bubble + composed event so menus / buttons elsewhere on the page
|
|
103
|
+
// can sync their active-state visuals without each consumer wiring
|
|
104
|
+
// its own listener on every possible target.
|
|
105
|
+
function fireTabActivate(target, name) {
|
|
106
|
+
target.dispatchEvent(new CustomEvent('sol-tab-activate', {
|
|
107
|
+
bubbles: true, composed: true,
|
|
108
|
+
detail: { name, target },
|
|
109
|
+
}));
|
|
110
|
+
}
|
package/core/defaults.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Shared programmatic defaults — values components fall back to when
|
|
2
|
+
// their own attribute isn't set and no RDF source PropertyValue
|
|
3
|
+
// supplies it. Lives in a singleton <sol-default> element in the host
|
|
4
|
+
// page (see ../web/sol-default.js).
|
|
5
|
+
//
|
|
6
|
+
// CSS-driven knobs (theme, font-size) belong on :root as custom
|
|
7
|
+
// properties, not here. This module is for JS-side values like the
|
|
8
|
+
// CORS proxy URL.
|
|
9
|
+
|
|
10
|
+
import { register as registerService } from './services.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read the current value of a named default. Returns the matching
|
|
14
|
+
* attribute on the first <sol-default> element in the document, or
|
|
15
|
+
* null if no element exists or the attribute isn't set.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} name
|
|
18
|
+
* @returns {string|null}
|
|
19
|
+
*/
|
|
20
|
+
export function getDefault(name) {
|
|
21
|
+
const el = document.querySelector('sol-default');
|
|
22
|
+
if (!el) return null;
|
|
23
|
+
const v = el.getAttribute(name);
|
|
24
|
+
return v == null ? null : v;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Subscribe to changes on <sol-default>. The handler is invoked with
|
|
29
|
+
* (name, newValue, oldValue) for each attribute change. Returns an
|
|
30
|
+
* unsubscribe function — call it on disconnect to remove the listener.
|
|
31
|
+
*
|
|
32
|
+
* The event is dispatched by sol-default's attributeChangedCallback
|
|
33
|
+
* and bubbles up to document, so this works regardless of where the
|
|
34
|
+
* <sol-default> element sits in the tree.
|
|
35
|
+
*
|
|
36
|
+
* @param {(name: string, newValue: string|null, oldValue: string|null) => void} handler
|
|
37
|
+
* @returns {() => void}
|
|
38
|
+
*/
|
|
39
|
+
export function onDefaultChange(handler) {
|
|
40
|
+
const fn = (e) => handler(e.detail.name, e.detail.newValue, e.detail.oldValue);
|
|
41
|
+
document.addEventListener('sol-default-change', fn);
|
|
42
|
+
return () => document.removeEventListener('sol-default-change', fn);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Publish shared config as the `defaults` host-service so any component reaches
|
|
46
|
+
// it via window.SolidWebComponents.defaults — no import required. Registered
|
|
47
|
+
// unconditionally; the getters simply return null when no <sol-default> exists.
|
|
48
|
+
registerService('defaults', { get: getDefault, onChange: onDefaultChange });
|
package/core/define.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Idempotent wrapper around customElements.define. Prevents a throw when the
|
|
2
|
+
// same tag is registered twice — e.g. when a page loads both the all-in-one
|
|
3
|
+
// bundle and a per-component UMD build, or when a module is re-evaluated by
|
|
4
|
+
// a hot-reloader.
|
|
5
|
+
export function define(name, klass) {
|
|
6
|
+
if (typeof customElements === 'undefined') return;
|
|
7
|
+
const existing = customElements.get(name);
|
|
8
|
+
if (existing) {
|
|
9
|
+
if (existing !== klass && !window.__SolSuppressDefineWarn) {
|
|
10
|
+
console.warn(`[sol-components] <${name}> already registered; keeping the existing definition.`);
|
|
11
|
+
}
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
customElements.define(name, klass);
|
|
15
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Region resolver + mounter for launchers (menu items, sol-button).
|
|
2
|
+
//
|
|
3
|
+
// The model: content/structure lives in Turtle, all display lives in HTML.
|
|
4
|
+
// A launcher decides three things, all resolved here from the DOM:
|
|
5
|
+
//
|
|
6
|
+
// where — `region=` on the launcher / a container / <sol-menu> /
|
|
7
|
+
// <sol-default> (nearest wins). Value is a CSS selector (a
|
|
8
|
+
// persistent pane the author placed) OR a keyword
|
|
9
|
+
// (modal | floating | tab | window) that conjures an ephemeral
|
|
10
|
+
// surface with no author-placed element. A Turtle menu item,
|
|
11
|
+
// which has no HTML element, is routed by a host that claims it
|
|
12
|
+
// by id via `data-for`.
|
|
13
|
+
// how — the content element: a component tag, a <sol-include> of a
|
|
14
|
+
// same-origin href, or an <iframe> for an external href.
|
|
15
|
+
// lifetime — per pane: component → keep-alive wrapper, doc/iframe → replace.
|
|
16
|
+
//
|
|
17
|
+
// No RDF display vocabulary is involved; surfaces survive only as the HTML
|
|
18
|
+
// keyword values above.
|
|
19
|
+
|
|
20
|
+
import { mountInTarget } from './component-mount.js';
|
|
21
|
+
|
|
22
|
+
const SURFACE_KEYWORDS = new Set(['modal', 'floating', 'tab', 'window']);
|
|
23
|
+
|
|
24
|
+
/** True for a cross-origin http(s) URL. */
|
|
25
|
+
export function isExternal(href) {
|
|
26
|
+
if (!href) return false;
|
|
27
|
+
try {
|
|
28
|
+
const u = new URL(href, document.baseURI);
|
|
29
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
|
|
30
|
+
return u.origin !== location.origin;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Content element for a link href: same-origin → trusted sol-include
|
|
37
|
+
* (keep-alive); external → iframe (replace). */
|
|
38
|
+
export function contentForHref(href) {
|
|
39
|
+
return isExternal(href)
|
|
40
|
+
? { tag: 'iframe', attrs: [['src', href]], replace: true }
|
|
41
|
+
: { tag: 'sol-include', attrs: [['source', href], ['endpoint', href], ['trusted', 'true']], replace: false };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildElement(tag, attrs = [], embedClass = null) {
|
|
45
|
+
const el = document.createElement(tag);
|
|
46
|
+
for (const [k, v] of attrs) el.setAttribute(k, v);
|
|
47
|
+
if (embedClass) el.classList.add(embedClass);
|
|
48
|
+
return el;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function safeQuery(sel) {
|
|
52
|
+
try { return document.querySelector(sel); } catch { return null; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Find a region/host element that claims this item id via `data-for`
|
|
56
|
+
// (space-separated list of ids).
|
|
57
|
+
function claimedRegion(id) {
|
|
58
|
+
if (!id) return null;
|
|
59
|
+
for (const el of document.querySelectorAll('[data-for]')) {
|
|
60
|
+
if ((el.getAttribute('data-for') || '').split(/\s+/).includes(id)) return el;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve where a launcher's content should go.
|
|
67
|
+
* @returns {{kind:'element', element:Element} | {kind:'modal'|'floating'|'tab'|'window'} | {kind:null}}
|
|
68
|
+
*/
|
|
69
|
+
export function resolveRegion(launcher, id, fallbackEl = null) {
|
|
70
|
+
const claimed = claimedRegion(id);
|
|
71
|
+
if (claimed) return { kind: 'element', element: claimed };
|
|
72
|
+
|
|
73
|
+
let value = null;
|
|
74
|
+
const scope = launcher && launcher.closest ? launcher.closest('[region]') : null;
|
|
75
|
+
if (scope) value = scope.getAttribute('region');
|
|
76
|
+
if (!value) {
|
|
77
|
+
const def = document.querySelector('sol-default[region]');
|
|
78
|
+
if (def) value = def.getAttribute('region');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!value) return fallbackEl ? { kind: 'element', element: fallbackEl } : { kind: null };
|
|
82
|
+
|
|
83
|
+
const kw = value.toLowerCase();
|
|
84
|
+
if (SURFACE_KEYWORDS.has(kw)) return { kind: kw };
|
|
85
|
+
const el = safeQuery(value);
|
|
86
|
+
if (el) return { kind: 'element', element: el };
|
|
87
|
+
return fallbackEl ? { kind: 'element', element: fallbackEl } : { kind: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Place a launcher's content into its resolved region.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} o
|
|
94
|
+
* @param {Element|null} o.launcher element initiating (for region cascade)
|
|
95
|
+
* @param {string|null} o.id item id (for data-for routing)
|
|
96
|
+
* @param {string} o.name pane wrapper / surface title
|
|
97
|
+
* @param {string|null} o.tag content element tag
|
|
98
|
+
* @param {Array<[string,string]>} [o.attrs]
|
|
99
|
+
* @param {string|null} o.href used for tab/window surfaces
|
|
100
|
+
* @param {string|null} o.contents literal HTML
|
|
101
|
+
* @param {boolean} [o.replace] pane lifetime (true = rebuild each time)
|
|
102
|
+
* @param {string|null} [o.embedClass]
|
|
103
|
+
* @param {Element|null} [o.fallbackEl] where to mount if no region resolves
|
|
104
|
+
* @param {(tag:string)=>void} [o.ensure] lazy-load hook for conjured hosts
|
|
105
|
+
* @returns {Element|Window|null}
|
|
106
|
+
*/
|
|
107
|
+
export function displayItem({
|
|
108
|
+
launcher = null, id = null, name = '', tag = null, attrs = [],
|
|
109
|
+
href = null, contents = null, replace = false, embedClass = null,
|
|
110
|
+
fallbackEl = null, ensure = null,
|
|
111
|
+
}) {
|
|
112
|
+
const region = resolveRegion(launcher, id, fallbackEl);
|
|
113
|
+
const mount = (host) => {
|
|
114
|
+
if (contents != null) host.innerHTML = contents;
|
|
115
|
+
else if (tag) host.appendChild(buildElement(tag, attrs, embedClass));
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
switch (region.kind) {
|
|
119
|
+
case 'tab': return href ? window.open(href, '_blank', '') : null;
|
|
120
|
+
case 'window': return href ? window.open(href, '_blank', 'width=900,height=700,menubar=no,toolbar=no') : null;
|
|
121
|
+
case 'modal': return conjure('sol-modal', name, mount, ensure);
|
|
122
|
+
case 'floating': return conjure('sol-window', name, mount, ensure);
|
|
123
|
+
case 'element': return mountInElement(region.element, { tag, attrs, name, replace, contents, embedClass, mount });
|
|
124
|
+
default: return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function mountInElement(element, { tag, attrs, name, replace, contents, embedClass, mount }) {
|
|
129
|
+
const t = (element.tagName || '').toLowerCase();
|
|
130
|
+
|
|
131
|
+
if (t === 'sol-modal') {
|
|
132
|
+
element.handler = (body) => mount(body);
|
|
133
|
+
element.open();
|
|
134
|
+
return element;
|
|
135
|
+
}
|
|
136
|
+
if (t === 'sol-window') {
|
|
137
|
+
mount(element.body || element);
|
|
138
|
+
return element;
|
|
139
|
+
}
|
|
140
|
+
// A pane: literal HTML replaces its content; otherwise a keep-alive (or
|
|
141
|
+
// replace) named wrapper via the shared mount helper.
|
|
142
|
+
if (contents != null) { element.innerHTML = contents; return element; }
|
|
143
|
+
if (!tag) return null;
|
|
144
|
+
return mountInTarget({ target: element, name, tag, attrs, embedClass, replace });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Conjure an ephemeral host (sol-modal / sol-window) with no author element.
|
|
148
|
+
// Deferred via whenDefined so open()/body exist once the module upgrades.
|
|
149
|
+
function conjure(hostTag, name, mount, ensure) {
|
|
150
|
+
if (ensure) ensure(hostTag);
|
|
151
|
+
const run = () => {
|
|
152
|
+
const host = document.createElement(hostTag);
|
|
153
|
+
if (name) host.setAttribute('title', name);
|
|
154
|
+
if (hostTag === 'sol-modal') {
|
|
155
|
+
host.handler = (body) => mount(body);
|
|
156
|
+
host.open();
|
|
157
|
+
} else {
|
|
158
|
+
document.body.appendChild(host);
|
|
159
|
+
mount(host.body || host);
|
|
160
|
+
}
|
|
161
|
+
return host;
|
|
162
|
+
};
|
|
163
|
+
if (customElements.get(hostTag)) return run();
|
|
164
|
+
if (typeof customElements !== 'undefined') customElements.whenDefined(hostTag).then(run);
|
|
165
|
+
return null;
|
|
166
|
+
}
|