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
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <sol-calendar> — inline calendar viewer web component.
|
|
3
|
+
*
|
|
4
|
+
* Fetches a public iCalendar (ICS) feed from any provider that exports
|
|
5
|
+
* one — Google Calendar, Apple iCloud, Outlook, Proton Calendar, or a
|
|
6
|
+
* Solid pod — and renders the events as an agenda list that fits
|
|
7
|
+
* whatever container the host page gives it.
|
|
8
|
+
*
|
|
9
|
+
* v1 ships the agenda view only. `mini` (today-only card) and `month`
|
|
10
|
+
* (grid + day popover) are planned and will land as a follow-up; the
|
|
11
|
+
* view dispatch in `connectedCallback` is a switch so adding them is
|
|
12
|
+
* local.
|
|
13
|
+
*
|
|
14
|
+
* Attributes:
|
|
15
|
+
* source One or more ICS URLs (whitespace-separated for >1),
|
|
16
|
+
* **or** `file.ttl#Subject` PropertyValue config. The
|
|
17
|
+
* RDF source may itself declare repeated `"source"`
|
|
18
|
+
* values (multi-calendar / amalgamated view), in
|
|
19
|
+
* which case events from every feed are fetched in
|
|
20
|
+
* parallel and merged into one sorted agenda.
|
|
21
|
+
* provider google | apple | outlook | proton | ics (default: ics)
|
|
22
|
+
* calendar-id For provider="google", the calendar email/id; URL is built
|
|
23
|
+
* via the public-ICS template. Other providers ignore it.
|
|
24
|
+
* view agenda (only value supported in v1)
|
|
25
|
+
* start ISO date YYYY-MM-DD (default: today)
|
|
26
|
+
* window-days Agenda lookahead in days (default: 30)
|
|
27
|
+
* max-events Cap on rendered events (default: 100)
|
|
28
|
+
* proxy CORS proxy pattern — supports `{url}` token or appended
|
|
29
|
+
* time-zone IANA TZ override (default: browser's resolved TZ)
|
|
30
|
+
* locale BCP-47 (default: browser locale)
|
|
31
|
+
* hide-header Boolean — when present, the title + provider strip
|
|
32
|
+
* above the agenda is omitted. Useful when the host page
|
|
33
|
+
* already labels the slot (dashboards, sidebars).
|
|
34
|
+
*
|
|
35
|
+
* The HTML attribute always wins over the same-named PropertyValue in
|
|
36
|
+
* the RDF `source`, matching the `sol-time` / `sol-weather` convention.
|
|
37
|
+
*
|
|
38
|
+
* @element sol-calendar
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* <!-- Direct URL -->
|
|
42
|
+
* <sol-calendar
|
|
43
|
+
* source="https://calendar.google.com/calendar/ical/.../public/basic.ics"
|
|
44
|
+
* proxy="http://localhost:3002/proxy?uri="></sol-calendar>
|
|
45
|
+
*
|
|
46
|
+
* <!-- Provider helper builds the URL -->
|
|
47
|
+
* <sol-calendar provider="google" calendar-id="alice@example.org"></sol-calendar>
|
|
48
|
+
*
|
|
49
|
+
* <!-- Pull every setting from a PropertyValue TTL -->
|
|
50
|
+
* <sol-calendar source="data/calendar-settings.ttl#Settings"></sol-calendar>
|
|
51
|
+
*/
|
|
52
|
+
import { adopt } from '../core/adopt.js';
|
|
53
|
+
import { define } from '../core/define.js';
|
|
54
|
+
import { CSS as CAL_CSS, sheet as CAL_SHEET } from './styles/sol-calendar-css.js';
|
|
55
|
+
import { getCalendarEvents, getMergedCalendarEvents, buildProviderUrl }
|
|
56
|
+
from './utils/calendar-fetch.js';
|
|
57
|
+
import { loadConfig } from './utils/rdf-config.js';
|
|
58
|
+
import { getDefault, onDefaultChange } from '../core/defaults.js';
|
|
59
|
+
import { attachEditorSelfGear } from '../core/editor-self.js';
|
|
60
|
+
|
|
61
|
+
/** Predicate URI → HTML attribute name. After the vocab migration
|
|
62
|
+
* (see swc/claude/plans/PLAN-vocab-migration.md) calendar settings
|
|
63
|
+
* use direct predicates from Dublin Core / Schema.org / OWL-Time / UI.
|
|
64
|
+
* `dct:source` is multi-valued; everything else is single. */
|
|
65
|
+
const DCT = 'http://purl.org/dc/terms/';
|
|
66
|
+
const SCHEMA = 'http://schema.org/';
|
|
67
|
+
const TIME_NS = 'http://www.w3.org/2006/time#';
|
|
68
|
+
const UI_NS = 'http://www.w3.org/ns/ui#';
|
|
69
|
+
const CONFIG_MAP = [
|
|
70
|
+
[DCT + 'format', 'provider'],
|
|
71
|
+
[UI_NS + 'view', 'view'],
|
|
72
|
+
[TIME_NS + 'days', 'window-days'],
|
|
73
|
+
[SCHEMA + 'numberOfItems', 'max-events'],
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
/** True iff `source` is a `something.ttl#Subject` PropertyValue pointer
|
|
77
|
+
* rather than a direct calendar URL. The presence of `#` plus the
|
|
78
|
+
* `.ttl`/`.shacl` extension is the disambiguator — an ICS URL with a
|
|
79
|
+
* fragment is exotic enough to leave for later. */
|
|
80
|
+
function isRdfConfigSource(source) {
|
|
81
|
+
if (!source || !source.includes('#')) return false;
|
|
82
|
+
const path = source.split('#', 1)[0].toLowerCase();
|
|
83
|
+
return path.endsWith('.ttl') || path.endsWith('.shacl');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Format a Date as the day label used in the agenda's date column,
|
|
87
|
+
* e.g. "Wed, May 28". Honours the component's `locale` attribute. */
|
|
88
|
+
function formatDate(d, locale) {
|
|
89
|
+
return d.toLocaleDateString(locale || undefined, {
|
|
90
|
+
weekday: 'short', month: 'short', day: 'numeric',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Compare two Dates for same-day (local TZ). Used to detect repeat
|
|
95
|
+
* dates in the agenda so the date column can blank-out without
|
|
96
|
+
* losing its layout slot. */
|
|
97
|
+
function sameYMD(a, b) {
|
|
98
|
+
return !!a && !!b
|
|
99
|
+
&& a.getFullYear() === b.getFullYear()
|
|
100
|
+
&& a.getMonth() === b.getMonth()
|
|
101
|
+
&& a.getDate() === b.getDate();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Two-digit zero-padded number — used for the agenda time column so
|
|
105
|
+
* `09:30` aligns under `14:00`. */
|
|
106
|
+
function pad2(n) { return n < 10 ? '0' + n : String(n); }
|
|
107
|
+
|
|
108
|
+
/** Format the time half of an agenda row: "09:30–10:15", "14:00" if
|
|
109
|
+
* the event has no end time / a same-instant end, or "All day" when
|
|
110
|
+
* the event was a DATE-only DTSTART. */
|
|
111
|
+
function formatEventTime(ev, locale) {
|
|
112
|
+
if (ev.allDay) return 'All day';
|
|
113
|
+
const start = `${pad2(ev.start.getHours())}:${pad2(ev.start.getMinutes())}`;
|
|
114
|
+
if (!ev.end || ev.end.getTime() === ev.start.getTime()) return start;
|
|
115
|
+
// Don't show the end time if it's the same minute as the start
|
|
116
|
+
// (some ICS sources use zero-duration events as bookmarks).
|
|
117
|
+
const endSameMinute =
|
|
118
|
+
ev.end.getFullYear() === ev.start.getFullYear() &&
|
|
119
|
+
ev.end.getMonth() === ev.start.getMonth() &&
|
|
120
|
+
ev.end.getDate() === ev.start.getDate() &&
|
|
121
|
+
ev.end.getHours() === ev.start.getHours() &&
|
|
122
|
+
ev.end.getMinutes() === ev.start.getMinutes();
|
|
123
|
+
if (endSameMinute) return start;
|
|
124
|
+
const end = `${pad2(ev.end.getHours())}:${pad2(ev.end.getMinutes())}`;
|
|
125
|
+
return `${start}–${end}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Pretty header label for the title strip. We don't have the calendar's
|
|
129
|
+
* own X-WR-CALNAME yet (could be added) so this falls back to a clean
|
|
130
|
+
* rendering of the calendar-id or the URL host. */
|
|
131
|
+
function deriveTitle({ source, calendarId }) {
|
|
132
|
+
if (calendarId) return calendarId;
|
|
133
|
+
if (!source) return 'Calendar';
|
|
134
|
+
try { return new URL(source, document.baseURI).hostname; }
|
|
135
|
+
catch { return 'Calendar'; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Inline calendar viewer.
|
|
140
|
+
*
|
|
141
|
+
* @class SolCalendar
|
|
142
|
+
* @extends HTMLElement
|
|
143
|
+
*/
|
|
144
|
+
class SolCalendar extends HTMLElement {
|
|
145
|
+
static get observedAttributes() {
|
|
146
|
+
return [
|
|
147
|
+
'source', 'provider', 'calendar-id', 'view',
|
|
148
|
+
'start', 'window-days', 'max-events', 'proxy',
|
|
149
|
+
'time-zone', 'locale', 'hide-header',
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** SHACL shape declaring the fixed schema (predicates + datatypes +
|
|
154
|
+
* cardinalities). sol-form's shape-driven mode generates a labelled
|
|
155
|
+
* field per property; only `dct:source` is multi-valued. dk-settings
|
|
156
|
+
* discovery picks this up. The legacy `editor` (ui:Form TTL) getter
|
|
157
|
+
* was dropped in the direct-predicate vocab migration — see
|
|
158
|
+
* swc/claude/plans/PLAN-vocab-migration.md. */
|
|
159
|
+
static get shape() {
|
|
160
|
+
return new URL('../shapes/calendar-settings.shacl', import.meta.url).href;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
constructor() {
|
|
164
|
+
super();
|
|
165
|
+
this.attachShadow({ mode: 'open' });
|
|
166
|
+
this._controller = null; // AbortController for the active fetch
|
|
167
|
+
this._refreshMs = 10 * 60 * 1000;
|
|
168
|
+
this._timer = null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async connectedCallback() {
|
|
172
|
+
adopt(this.shadowRoot, { sheet: CAL_SHEET, css: CAL_CSS });
|
|
173
|
+
|
|
174
|
+
this._status = document.createElement('div');
|
|
175
|
+
this._status.className = 'sol-calendar-status';
|
|
176
|
+
this._status.setAttribute('role', 'status');
|
|
177
|
+
this._status.setAttribute('aria-live', 'polite');
|
|
178
|
+
this._status.style.display = 'none';
|
|
179
|
+
|
|
180
|
+
this._root = document.createElement('div');
|
|
181
|
+
this._root.className = 'sol-calendar';
|
|
182
|
+
|
|
183
|
+
this.shadowRoot.append(this._status, this._root);
|
|
184
|
+
|
|
185
|
+
// PropertyValue config first (HTML attributes already win because
|
|
186
|
+
// _applySource only sets attributes that aren't already there).
|
|
187
|
+
await this._applySource();
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await this._update();
|
|
191
|
+
} catch (e) {
|
|
192
|
+
this._setStatus(e.message || String(e), true);
|
|
193
|
+
}
|
|
194
|
+
this._timer = setInterval(() => this._update().catch(() => {}), this._refreshMs);
|
|
195
|
+
|
|
196
|
+
// Re-fetch when <sol-default> changes the proxy at runtime.
|
|
197
|
+
this._unsubDefaults = onDefaultChange((name) => {
|
|
198
|
+
if (name === 'proxy') this.reload().catch(() => {});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (this.hasAttribute('editor-self')) attachEditorSelfGear(this);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
disconnectedCallback() {
|
|
205
|
+
if (this._controller) this._controller.abort();
|
|
206
|
+
if (this._timer) { clearInterval(this._timer); this._timer = null; }
|
|
207
|
+
if (this._unsubDefaults) { this._unsubDefaults(); this._unsubDefaults = null; }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Re-read `source` and re-fetch calendar events. Public hook used by
|
|
212
|
+
* external editors (e.g. dk-settings) after a configuration file
|
|
213
|
+
* changes.
|
|
214
|
+
*/
|
|
215
|
+
async reload() {
|
|
216
|
+
await this._applySource();
|
|
217
|
+
await this._update();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
attributeChangedCallback(_name, oldV, newV) {
|
|
221
|
+
if (oldV !== newV && this.isConnected && this._root) {
|
|
222
|
+
this._update().catch(() => {});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** If `source` points at an RDF config file, pull PropertyValue
|
|
227
|
+
* settings into attributes that aren't already explicitly set on
|
|
228
|
+
* the HTML element. Skipped when source is empty or looks like a
|
|
229
|
+
* direct ICS URL. */
|
|
230
|
+
async _applySource() {
|
|
231
|
+
const source = this.getAttribute('source');
|
|
232
|
+
if (!isRdfConfigSource(source)) return;
|
|
233
|
+
try {
|
|
234
|
+
const cfg = await loadConfig(source);
|
|
235
|
+
for (const [predicate, attr] of CONFIG_MAP) {
|
|
236
|
+
if (cfg[predicate] != null && !this.hasAttribute(attr)) {
|
|
237
|
+
this.setAttribute(attr, String(cfg[predicate]));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// dct:source provides the actual feed URLs (the element's `source`
|
|
241
|
+
// attribute was a config-doc pointer). Stash them on the instance
|
|
242
|
+
// instead of overwriting the attribute — sol-settings discovery
|
|
243
|
+
// and the editor need `source` to keep pointing at the config TTL.
|
|
244
|
+
const dctSource = cfg[DCT + 'source'];
|
|
245
|
+
if (dctSource != null) {
|
|
246
|
+
this._feedUrls = Array.isArray(dctSource)
|
|
247
|
+
? dctSource.map(String).filter(Boolean)
|
|
248
|
+
: [String(dctSource)].filter(Boolean);
|
|
249
|
+
}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
// Bad TTL or missing rdflib — surface in the status strip but
|
|
252
|
+
// keep going; explicit HTML attributes can still drive a render.
|
|
253
|
+
this._setStatus(`Config: ${err.message}`, true);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** ICS feed URLs to fetch. When `source` is an RDF-config pointer,
|
|
258
|
+
* `_applySource` populates `this._feedUrls` from dct:source — those
|
|
259
|
+
* win. Otherwise `source` is treated as a direct ICS URL (or a
|
|
260
|
+
* whitespace-separated list of them), per the documented inline
|
|
261
|
+
* usage. Empty result before _applySource has run on a config
|
|
262
|
+
* pointer; the post-load render fills it in. */
|
|
263
|
+
_sourceUrls() {
|
|
264
|
+
if (Array.isArray(this._feedUrls) && this._feedUrls.length) return this._feedUrls;
|
|
265
|
+
const raw = this.source;
|
|
266
|
+
if (!raw || isRdfConfigSource(raw)) return [];
|
|
267
|
+
return raw.split(/\s+/).filter(Boolean);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Update the polite live region. Pass `isError` to colour it red. */
|
|
271
|
+
_setStatus(msg, isError = false) {
|
|
272
|
+
if (!this._status) return;
|
|
273
|
+
this._status.textContent = msg || '';
|
|
274
|
+
this._status.style.display = msg ? '' : 'none';
|
|
275
|
+
if (isError) this._status.setAttribute('data-error', '');
|
|
276
|
+
else this._status.removeAttribute('data-error');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* ── attribute readers ───────────────────────────────────────────── */
|
|
280
|
+
|
|
281
|
+
get source() { return this.getAttribute('source') || ''; }
|
|
282
|
+
get provider() { return (this.getAttribute('provider') || 'ics').toLowerCase(); }
|
|
283
|
+
get calendarId() { return this.getAttribute('calendar-id') || ''; }
|
|
284
|
+
get view() { return (this.getAttribute('view') || 'agenda').toLowerCase(); }
|
|
285
|
+
get proxy() { return this.getAttribute('proxy') || getDefault('proxy') || ''; }
|
|
286
|
+
get locale() { return this.getAttribute('locale') || ''; }
|
|
287
|
+
get windowDays() { return Math.max(1, Number(this.getAttribute('window-days')) || 30); }
|
|
288
|
+
get maxEvents() { return Math.max(1, Number(this.getAttribute('max-events')) || 100); }
|
|
289
|
+
get startDate() {
|
|
290
|
+
const raw = this.getAttribute('start');
|
|
291
|
+
if (!raw) {
|
|
292
|
+
// Start at midnight local — same-day events that started a few
|
|
293
|
+
// minutes ago still appear, instead of being trimmed by the
|
|
294
|
+
// window cutoff.
|
|
295
|
+
const d = new Date();
|
|
296
|
+
d.setHours(0, 0, 0, 0);
|
|
297
|
+
return d;
|
|
298
|
+
}
|
|
299
|
+
// YYYY-MM-DD parses as UTC midnight; pin to local midnight so the
|
|
300
|
+
// agenda's day-grouping matches what the user expects.
|
|
301
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(raw);
|
|
302
|
+
if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
303
|
+
return new Date(raw);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* ── fetch + render dispatch ─────────────────────────────────────── */
|
|
307
|
+
|
|
308
|
+
async _update() {
|
|
309
|
+
if (!this._root) return;
|
|
310
|
+
|
|
311
|
+
// After _applySource, `source` holds either zero URLs (caller is
|
|
312
|
+
// using provider+calendar-id only), one URL, or N whitespace-
|
|
313
|
+
// separated URLs (multi-calendar amalgamation).
|
|
314
|
+
const urls = this._sourceUrls();
|
|
315
|
+
if (!urls.length && !this.calendarId) {
|
|
316
|
+
this._setStatus('No calendar source — set source= or provider= + calendar-id=', true);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (this._controller) this._controller.abort();
|
|
321
|
+
this._controller = new AbortController();
|
|
322
|
+
const signal = this._controller.signal;
|
|
323
|
+
|
|
324
|
+
this._setStatus(urls.length > 1
|
|
325
|
+
? `Loading ${urls.length} calendars…`
|
|
326
|
+
: 'Loading calendar…');
|
|
327
|
+
|
|
328
|
+
const opts = {
|
|
329
|
+
provider: this.provider,
|
|
330
|
+
calendarId: this.calendarId,
|
|
331
|
+
proxy: this.proxy,
|
|
332
|
+
start: this.startDate,
|
|
333
|
+
windowDays: this.windowDays,
|
|
334
|
+
maxEvents: this.maxEvents,
|
|
335
|
+
signal,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
if (urls.length > 1) {
|
|
340
|
+
// Amalgamated calendar — Promise.allSettled inside, so a single
|
|
341
|
+
// dead feed doesn't blank the rest. Surface the count of
|
|
342
|
+
// failures in the status strip without overriding the events.
|
|
343
|
+
const { events, errors } = await getMergedCalendarEvents(urls, opts);
|
|
344
|
+
this._renderAgenda(events);
|
|
345
|
+
if (errors.length) {
|
|
346
|
+
this._setStatus(
|
|
347
|
+
`Loaded ${urls.length - errors.length} of ${urls.length} calendars — ${errors.length} failed`,
|
|
348
|
+
true);
|
|
349
|
+
// Skip the per-feed warn when the failure is just our own
|
|
350
|
+
// AbortController firing (e.g. a later _update cancelled an
|
|
351
|
+
// in-flight one) — that's expected, not a real failure.
|
|
352
|
+
for (const e of errors) {
|
|
353
|
+
if (/aborted/i.test(e.message)) continue;
|
|
354
|
+
console.warn(`[sol-calendar] ${e.url}: ${e.message}`);
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
this._setStatus('');
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
const events = await getCalendarEvents(urls[0] || '', opts);
|
|
361
|
+
this._renderAgenda(events);
|
|
362
|
+
this._setStatus('');
|
|
363
|
+
}
|
|
364
|
+
} catch (e) {
|
|
365
|
+
if (e.name === 'AbortError') return;
|
|
366
|
+
this._renderEmpty(`Couldn't load calendar: ${e.message}`);
|
|
367
|
+
this._setStatus(e.message || String(e), true);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
_renderEmpty(msg) {
|
|
372
|
+
const wrap = document.createElement('div');
|
|
373
|
+
wrap.className = 'sol-calendar-empty';
|
|
374
|
+
wrap.textContent = msg;
|
|
375
|
+
this._root.replaceChildren(wrap);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
_renderAgenda(events) {
|
|
379
|
+
// hide-header: skip the title + provider strip entirely. Common for
|
|
380
|
+
// dashboards / sidebars that already label the slot themselves.
|
|
381
|
+
const showHeader = !this.hasAttribute('hide-header');
|
|
382
|
+
let header = null;
|
|
383
|
+
if (showHeader) {
|
|
384
|
+
header = document.createElement('div');
|
|
385
|
+
header.className = 'cal-header';
|
|
386
|
+
const title = document.createElement('span');
|
|
387
|
+
title.className = 'cal-title';
|
|
388
|
+
title.textContent = deriveTitle({ source: this.source, calendarId: this.calendarId });
|
|
389
|
+
const prov = document.createElement('span');
|
|
390
|
+
prov.className = 'cal-provider';
|
|
391
|
+
prov.textContent = this.provider;
|
|
392
|
+
header.append(title, prov);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const list = document.createElement('div');
|
|
396
|
+
list.className = 'cal-agenda';
|
|
397
|
+
list.setAttribute('aria-label', 'Upcoming events');
|
|
398
|
+
|
|
399
|
+
if (!events.length) {
|
|
400
|
+
const empty = document.createElement('div');
|
|
401
|
+
empty.className = 'sol-calendar-empty';
|
|
402
|
+
empty.textContent = `No events in the next ${this.windowDays} days.`;
|
|
403
|
+
list.appendChild(empty);
|
|
404
|
+
this._root.replaceChildren(...(header ? [header, list] : [list]));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const today = new Date();
|
|
409
|
+
const ul = document.createElement('ul');
|
|
410
|
+
ul.className = 'cal-rows';
|
|
411
|
+
|
|
412
|
+
// Flat list — one row per event, with date / time / event columns.
|
|
413
|
+
// The date is rendered on every row (preserving the grid alignment)
|
|
414
|
+
// but visually blanked when the previous row was the same day, so a
|
|
415
|
+
// run of same-day events reads cleanly without losing the column.
|
|
416
|
+
let prevDate = null;
|
|
417
|
+
for (const ev of events) {
|
|
418
|
+
const li = document.createElement('li');
|
|
419
|
+
li.className = 'cal-row' + (sameYMD(ev.start, today) ? ' today' : '');
|
|
420
|
+
|
|
421
|
+
const date = document.createElement('span');
|
|
422
|
+
date.className = 'cal-row-date' + (sameYMD(ev.start, prevDate) ? ' repeat' : '');
|
|
423
|
+
date.textContent = formatDate(ev.start, this.locale);
|
|
424
|
+
|
|
425
|
+
const time = document.createElement('span');
|
|
426
|
+
time.className = 'cal-row-time';
|
|
427
|
+
time.textContent = formatEventTime(ev, this.locale);
|
|
428
|
+
|
|
429
|
+
const body = document.createElement('div');
|
|
430
|
+
body.className = 'cal-row-body';
|
|
431
|
+
|
|
432
|
+
const summary = document.createElement('span');
|
|
433
|
+
summary.className = 'cal-row-summary';
|
|
434
|
+
if (ev.url) {
|
|
435
|
+
const a = document.createElement('a');
|
|
436
|
+
a.href = ev.url;
|
|
437
|
+
a.target = '_blank';
|
|
438
|
+
a.rel = 'noopener noreferrer';
|
|
439
|
+
a.textContent = ev.summary || '(untitled)';
|
|
440
|
+
summary.appendChild(a);
|
|
441
|
+
} else {
|
|
442
|
+
summary.textContent = ev.summary || '(untitled)';
|
|
443
|
+
}
|
|
444
|
+
body.appendChild(summary);
|
|
445
|
+
|
|
446
|
+
if (ev.location) {
|
|
447
|
+
const loc = document.createElement('span');
|
|
448
|
+
loc.className = 'cal-row-location';
|
|
449
|
+
loc.textContent = ev.location;
|
|
450
|
+
body.appendChild(loc);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
li.append(date, time, body);
|
|
454
|
+
ul.appendChild(li);
|
|
455
|
+
prevDate = ev.start;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
list.appendChild(ul);
|
|
459
|
+
this._root.replaceChildren(...(header ? [header, list] : [list]));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
define('sol-calendar', SolCalendar);
|
|
464
|
+
|
|
465
|
+
export { SolCalendar, buildProviderUrl };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <sol-default> — Singleton holder for shared programmatic defaults.
|
|
3
|
+
*
|
|
4
|
+
* Place once in the host page; sol-* components consult its
|
|
5
|
+
* attributes as the last fallback for knobs they take (e.g. `proxy`,
|
|
6
|
+
* default issuers list, default endpoint) before reaching for a
|
|
7
|
+
* hard-coded value.
|
|
8
|
+
*
|
|
9
|
+
* Resolution order in each consumer:
|
|
10
|
+
* 1. The component's own HTML attribute
|
|
11
|
+
* 2. The component's RDF source PropertyValue, if applicable
|
|
12
|
+
* 3. This element's matching attribute (via core/defaults.js getDefault)
|
|
13
|
+
* 4. Hard-coded fallback in the component
|
|
14
|
+
*
|
|
15
|
+
* Reactivity: every attribute change re-dispatches `sol-default-change`
|
|
16
|
+
* (bubbling, composed) with detail `{ name, newValue, oldValue }`. The
|
|
17
|
+
* helper `onDefaultChange` in core/defaults.js wraps that listener.
|
|
18
|
+
*
|
|
19
|
+
* The element renders nothing — it's a configuration record, not UI.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* <sol-default proxy="http://localhost:3002/proxy?uri="></sol-default>
|
|
23
|
+
*
|
|
24
|
+
* @class SolDefault
|
|
25
|
+
* @extends HTMLElement
|
|
26
|
+
* @fires sol-default-change - detail: { name, newValue, oldValue }
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { define } from '../core/define.js';
|
|
30
|
+
import { loadConfig } from './utils/rdf-config.js';
|
|
31
|
+
|
|
32
|
+
class SolDefault extends HTMLElement {
|
|
33
|
+
// observedAttributes is intentionally empty: a MutationObserver
|
|
34
|
+
// watches every attribute on the element, so consumers don't have to
|
|
35
|
+
// declare which knobs they care about in advance. (If we listed any
|
|
36
|
+
// name here, attributeChangedCallback would double-fire alongside
|
|
37
|
+
// the observer.)
|
|
38
|
+
static get observedAttributes() { return []; }
|
|
39
|
+
|
|
40
|
+
// No hardcoded shape: which knobs <sol-default> holds is app-specific, so the
|
|
41
|
+
// editable SHACL shape comes from a per-instance `shape="…"` attribute
|
|
42
|
+
// (resolved by core/editor.js → sol-form). e.g.
|
|
43
|
+
// <sol-default shape="./shapes/app-settings.shacl" theme="dark" …>
|
|
44
|
+
// Without a `shape`, sol-default is a non-editable defaults record.
|
|
45
|
+
|
|
46
|
+
constructor() {
|
|
47
|
+
super();
|
|
48
|
+
this.style.display = 'none';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async connectedCallback() {
|
|
52
|
+
if (this._observer) return;
|
|
53
|
+
this._observer = new MutationObserver((mutations) => {
|
|
54
|
+
for (const m of mutations) {
|
|
55
|
+
if (m.type !== 'attributes' || !m.attributeName) continue;
|
|
56
|
+
const name = m.attributeName;
|
|
57
|
+
const newValue = this.getAttribute(name);
|
|
58
|
+
const oldValue = m.oldValue;
|
|
59
|
+
if (newValue === oldValue) continue;
|
|
60
|
+
this._fire(name, newValue, oldValue);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
this._observer.observe(this, { attributes: true, attributeOldValue: true });
|
|
64
|
+
|
|
65
|
+
// RDF source: pulls each predicate's value into a matching HTML
|
|
66
|
+
// attribute (only when not already set explicitly on the element,
|
|
67
|
+
// so an inline override wins). camelCase predicates kebab-case
|
|
68
|
+
// their attribute name (`ui:defaultIssuers` → `default-issuers`).
|
|
69
|
+
const source = this.getAttribute('source');
|
|
70
|
+
if (source) await this._applySource(source);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async _applySource(source) {
|
|
74
|
+
try {
|
|
75
|
+
const cfg = await loadConfig(source);
|
|
76
|
+
for (const [predUri, value] of Object.entries(cfg)) {
|
|
77
|
+
const attr = attrFromPredicate(predUri);
|
|
78
|
+
if (!attr) continue;
|
|
79
|
+
if (this.hasAttribute(attr)) continue; // HTML override wins
|
|
80
|
+
this.setAttribute(attr, Array.isArray(value) ? value.join(' ') : String(value));
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.warn(`[sol-default] source ${source}: ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Public hook used by <sol-settings> after a save: re-read the RDF
|
|
88
|
+
* and re-emit change events for downstream consumers. */
|
|
89
|
+
async reload() {
|
|
90
|
+
const source = this.getAttribute('source');
|
|
91
|
+
if (source) await this._applySource(source);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
disconnectedCallback() {
|
|
95
|
+
if (this._observer) { this._observer.disconnect(); this._observer = null; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_fire(name, newValue, oldValue) {
|
|
99
|
+
this.dispatchEvent(new CustomEvent('sol-default-change', {
|
|
100
|
+
bubbles: true, composed: true,
|
|
101
|
+
detail: { name, newValue, oldValue },
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Map a predicate URI's local name to a kebab-case HTML attribute.
|
|
107
|
+
// `http://www.w3.org/ns/ui#proxy` → `proxy`
|
|
108
|
+
// `http://www.w3.org/ns/ui#defaultIssuers` → `default-issuers`
|
|
109
|
+
function attrFromPredicate(uri) {
|
|
110
|
+
const i = Math.max(uri.lastIndexOf('#'), uri.lastIndexOf('/'));
|
|
111
|
+
const local = i === -1 ? uri : uri.slice(i + 1);
|
|
112
|
+
if (!local || local === 'type') return null;
|
|
113
|
+
return local.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
define('sol-default', SolDefault);
|
|
117
|
+
export { SolDefault };
|
|
118
|
+
export default SolDefault;
|