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,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <sol-search> — multi-engine search form, popup or inline.
|
|
3
|
+
*
|
|
4
|
+
* Two layouts, chosen by the `view` attribute:
|
|
5
|
+
*
|
|
6
|
+
* view="button" — (default) an icon trigger that opens a floating
|
|
7
|
+
* panel on click; the panel positions itself flush
|
|
8
|
+
* against the right edge of the trigger. Best for
|
|
9
|
+
* headers / toolbars where space is tight.
|
|
10
|
+
* view="inline" — the search field, Go button, and engine radios are
|
|
11
|
+
* rendered directly with no toggle. Best when you
|
|
12
|
+
* already have a dedicated strip for search.
|
|
13
|
+
*
|
|
14
|
+
* Engine sources, in order of precedence:
|
|
15
|
+
* `source` — URL of a Turtle file with a `schema:ItemList` whose
|
|
16
|
+
* `schema:itemListElement`s are `hydra:IriTemplate`
|
|
17
|
+
* entries (`hydra:template` = RFC 6570 search URL with
|
|
18
|
+
* `{query}`, `dct:title` = display name,
|
|
19
|
+
* `schema:position` = sort order). Parsed through
|
|
20
|
+
* feed-fetch.js#parseEngineList, sharing the single
|
|
21
|
+
* rdflib instance from core/rdf.js.
|
|
22
|
+
* `engines` — JSON array of {id,label,url} on the attribute itself.
|
|
23
|
+
* built-ins — a sensible default list (DuckDuckGo / Google /
|
|
24
|
+
* Wikipedia / prefix.cc / LOV / Etymology / YouTube /
|
|
25
|
+
* Wayback).
|
|
26
|
+
*
|
|
27
|
+
* Submitting opens the result in a shared "reader" window (the same
|
|
28
|
+
* window object is re-used across submissions so it never spawns a new
|
|
29
|
+
* tab per search).
|
|
30
|
+
*
|
|
31
|
+
* Attributes:
|
|
32
|
+
* view "button" | "inline" (default: button)
|
|
33
|
+
* source "file.ttl#TopicName" — RDF engines list
|
|
34
|
+
* engines JSON array of {id,label,url}
|
|
35
|
+
* default-engine id (or url) of the engine that starts selected
|
|
36
|
+
* placeholder input placeholder (default: "Search…")
|
|
37
|
+
*
|
|
38
|
+
* @element sol-search
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* <sol-search></sol-search>
|
|
42
|
+
* <sol-search view="inline" default-engine="ddg"></sol-search>
|
|
43
|
+
* <sol-search view="inline" source="data/search-engines.ttl#SearchEngines"></sol-search>
|
|
44
|
+
*/
|
|
45
|
+
import { adopt } from '../core/adopt.js';
|
|
46
|
+
import { define } from '../core/define.js';
|
|
47
|
+
import { CSS as SEARCH_CSS, sheet as SEARCH_SHEET } from './styles/sol-search-css.js';
|
|
48
|
+
import { parseEngineList } from './utils/feed-fetch.js';
|
|
49
|
+
import { attachEditorSelfGear } from '../core/editor-self.js';
|
|
50
|
+
|
|
51
|
+
/** Sensible defaults; callers can override via `engines` or `source`. */
|
|
52
|
+
const DEFAULT_ENGINES = [
|
|
53
|
+
{ id: 'ddg', label: 'DuckDuckGo', url: 'https://duckduckgo.com/?q=' },
|
|
54
|
+
{ id: 'g', label: 'Google', url: 'https://www.google.com/search?q=' },
|
|
55
|
+
{ id: 'wp', label: 'Wikipedia', url: 'https://en.wikipedia.org/w/index.php?search=' },
|
|
56
|
+
{ id: 'prefix', label: 'prefix.cc', url: 'https://prefix.cc/' },
|
|
57
|
+
{ id: 'lov', label: 'LOV', url: 'https://lov.linkeddata.es/dataset/lov/terms?q=' },
|
|
58
|
+
{ id: 'ety', label: 'Etymology', url: 'https://www.etymonline.com/search?q=' },
|
|
59
|
+
{ id: 'yt', label: 'YouTube', url: 'https://www.youtube.com/results?search_query=' },
|
|
60
|
+
{ id: 'wayback', label: 'Wayback', url: 'https://web.archive.org/web/*/' },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/** Shared "reader" window — kept across submissions so the same off-canvas
|
|
64
|
+
* window is re-used instead of spawning a fresh tab every time. Browsers
|
|
65
|
+
* clear a window's name on cross-origin navigation, so we cannot look it
|
|
66
|
+
* up by name later; we keep the handle that window.open() returns. */
|
|
67
|
+
let readerWindow = null;
|
|
68
|
+
|
|
69
|
+
/** A 1024×640 window flush against the right edge, vertically centred. */
|
|
70
|
+
function readerFeatures() {
|
|
71
|
+
const w = 1024, h = 640;
|
|
72
|
+
const left = Math.max(0, window.screen.availWidth - w);
|
|
73
|
+
const top = Math.max(0, Math.round((window.screen.availHeight - h) / 2));
|
|
74
|
+
return `width=${w},height=${h},left=${left},top=${top}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Open `url` in the shared reader window; returns true when handled. */
|
|
78
|
+
function openInReader(url) {
|
|
79
|
+
if (!url) return false;
|
|
80
|
+
if (readerWindow && !readerWindow.closed) {
|
|
81
|
+
readerWindow.location.href = url;
|
|
82
|
+
readerWindow.focus();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
readerWindow = window.open(url, 'sol-search-reader', readerFeatures());
|
|
86
|
+
if (readerWindow) { readerWindow.focus(); return true; }
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** HTML-escape a value for safe interpolation into innerHTML. */
|
|
91
|
+
function esc(s) {
|
|
92
|
+
return String(s).replace(/[&<>"']/g,
|
|
93
|
+
c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c]));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Derive a stable engine id from its label (slug-ish, lower-case). */
|
|
97
|
+
function slugify(label) {
|
|
98
|
+
return String(label).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Build the result URL for an engine + query. RDF-sourced engines
|
|
102
|
+
* carry `template` with a `{query}` placeholder (RFC 6570-ish);
|
|
103
|
+
* built-in / JSON-attr engines carry `url` as a prefix that the query
|
|
104
|
+
* is appended to. Both paths URI-encode the query. */
|
|
105
|
+
function expandQuery(eng, q) {
|
|
106
|
+
const encoded = encodeURIComponent(q);
|
|
107
|
+
if (eng?.template) return eng.template.replace(/\{query\}/g, encoded);
|
|
108
|
+
if (eng?.url) return eng.url + encoded;
|
|
109
|
+
return 'https://duckduckgo.com/?q=' + encoded;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// RDF parsing is delegated to feed-fetch.js#parseSourceList — the same
|
|
113
|
+
// utility sol-feed uses for its bookmark/SKOS source files, which routes
|
|
114
|
+
// through core/rdf.js (the single rdflib instance the suite shares).
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Multi-engine search component.
|
|
118
|
+
*
|
|
119
|
+
* @class SolSearch
|
|
120
|
+
* @extends HTMLElement
|
|
121
|
+
*/
|
|
122
|
+
class SolSearch extends HTMLElement {
|
|
123
|
+
static get observedAttributes() {
|
|
124
|
+
return ['view', 'source', 'engines', 'default-engine', 'placeholder'];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** SHACL shape describing the engines list. Discovery via
|
|
128
|
+
* <sol-settings> picks this up; the shape uses a forward
|
|
129
|
+
* `schema:itemListElement` so the editor lists each
|
|
130
|
+
* `hydra:IriTemplate` that's a member of the currently-edited
|
|
131
|
+
* `schema:ItemList`. */
|
|
132
|
+
static get shape() {
|
|
133
|
+
return new URL('../shapes/search-engines.shacl', import.meta.url).href;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
constructor() {
|
|
137
|
+
super();
|
|
138
|
+
this.attachShadow({ mode: 'open' });
|
|
139
|
+
this._open = false; // only relevant when view=button
|
|
140
|
+
this._engines = DEFAULT_ENGINES;
|
|
141
|
+
this._view = 'button'; // resolved in connectedCallback
|
|
142
|
+
this._built = false; // true once the shadow tree exists
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async connectedCallback() {
|
|
146
|
+
// Reset for the re-entry case (view change triggers a rebuild).
|
|
147
|
+
this.shadowRoot.adoptedStyleSheets = [];
|
|
148
|
+
this.shadowRoot.innerHTML = '';
|
|
149
|
+
adopt(this.shadowRoot, { sheet: SEARCH_SHEET, css: SEARCH_CSS });
|
|
150
|
+
|
|
151
|
+
this._view = (this.getAttribute('view') || 'button').toLowerCase();
|
|
152
|
+
// Surface the view on the host so external rules / parts can select on it.
|
|
153
|
+
this.dataset.view = this._view;
|
|
154
|
+
|
|
155
|
+
// The form body is shared between layouts — the only difference is
|
|
156
|
+
// whether it's wrapped in an [open]-toggled panel and preceded by a
|
|
157
|
+
// trigger button. The .engines-line wrapper keeps the engines row
|
|
158
|
+
// as a single flex item below the input + Go row; the engines
|
|
159
|
+
// inside flex-wrap onto a second (or third) row as space allows.
|
|
160
|
+
const formHTML = `
|
|
161
|
+
<form class="form" part="form">
|
|
162
|
+
<div class="row">
|
|
163
|
+
<input class="q" type="search" name="q" autocomplete="off" part="input">
|
|
164
|
+
<button class="go" type="submit" part="submit">Go</button>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="engines-line">
|
|
167
|
+
<div class="engines" aria-label="Search engine"></div>
|
|
168
|
+
</div>
|
|
169
|
+
</form>
|
|
170
|
+
`;
|
|
171
|
+
|
|
172
|
+
if (this._view === 'inline') {
|
|
173
|
+
// Render the form directly in the shadow root — no trigger, no
|
|
174
|
+
// floating panel, no document-level listeners. Engines simply
|
|
175
|
+
// wrap onto subsequent rows when the list outgrows the
|
|
176
|
+
// viewport.
|
|
177
|
+
const wrap = document.createElement('div');
|
|
178
|
+
wrap.innerHTML = formHTML;
|
|
179
|
+
while (wrap.firstChild) this.shadowRoot.appendChild(wrap.firstChild);
|
|
180
|
+
} else {
|
|
181
|
+
const wrap = document.createElement('div');
|
|
182
|
+
wrap.innerHTML = `
|
|
183
|
+
<button class="icon" type="button" part="trigger"
|
|
184
|
+
aria-haspopup="dialog" aria-expanded="false" title="Search">⌕</button>
|
|
185
|
+
<div class="panel" role="dialog" aria-modal="false" part="panel">
|
|
186
|
+
${formHTML}
|
|
187
|
+
<span class="sr-only">Press Escape to close</span>
|
|
188
|
+
</div>
|
|
189
|
+
`;
|
|
190
|
+
while (wrap.firstChild) this.shadowRoot.appendChild(wrap.firstChild);
|
|
191
|
+
this.$btn = this.shadowRoot.querySelector('button.icon');
|
|
192
|
+
this.$panel = this.shadowRoot.querySelector('.panel');
|
|
193
|
+
|
|
194
|
+
this._onDocPointerDown = (e) => {
|
|
195
|
+
const path = e.composedPath?.() ?? [];
|
|
196
|
+
if (!path.includes(this)) this.close();
|
|
197
|
+
};
|
|
198
|
+
this._onDocKeyDown = (e) => { if (e.key === 'Escape') this.close(); };
|
|
199
|
+
|
|
200
|
+
this.$btn.addEventListener('click', () => this.toggle());
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.$form = this.shadowRoot.querySelector('form.form');
|
|
204
|
+
this.$q = this.shadowRoot.querySelector('input.q');
|
|
205
|
+
this.$engines = this.shadowRoot.querySelector('.engines');
|
|
206
|
+
|
|
207
|
+
this._loadEngines();
|
|
208
|
+
this._renderEngines();
|
|
209
|
+
this._applyPlaceholder();
|
|
210
|
+
this._built = true;
|
|
211
|
+
|
|
212
|
+
this.$form.addEventListener('submit', (e) => {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
this._doSearch();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// If a `source` is set, fetch + parse it as a schema:ItemList of
|
|
218
|
+
// hydra:IriTemplate engines (see feed-fetch.js#parseEngineList).
|
|
219
|
+
// The default / engines-attr list is shown in the meantime so the
|
|
220
|
+
// UI is never blank, and stays put if the source request fails.
|
|
221
|
+
const source = this.getAttribute('source');
|
|
222
|
+
if (source) {
|
|
223
|
+
try {
|
|
224
|
+
const list = await parseEngineList(source);
|
|
225
|
+
if (list && list.length) {
|
|
226
|
+
this._engines = list.map((item, i) => ({
|
|
227
|
+
id: item.id || slugify(item.label) || `e${i}`,
|
|
228
|
+
label: item.label,
|
|
229
|
+
template: item.template,
|
|
230
|
+
}));
|
|
231
|
+
this._loadEngines();
|
|
232
|
+
this._renderEngines();
|
|
233
|
+
}
|
|
234
|
+
} catch (err) {
|
|
235
|
+
// Source failed — leave the default / engines-attr list in
|
|
236
|
+
// place; surface to the console so the page author sees it.
|
|
237
|
+
console.warn(`[sol-search] source ${source}: ${err.message}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (this.hasAttribute('editor-self')) attachEditorSelfGear(this);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
disconnectedCallback() {
|
|
245
|
+
if (this._onDocPointerDown) {
|
|
246
|
+
document.removeEventListener('pointerdown', this._onDocPointerDown, { capture: true });
|
|
247
|
+
}
|
|
248
|
+
if (this._onDocKeyDown) {
|
|
249
|
+
document.removeEventListener('keydown', this._onDocKeyDown);
|
|
250
|
+
}
|
|
251
|
+
this._built = false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Re-read `source` and rebuild the engines panel. Public hook used by
|
|
256
|
+
* external editors (e.g. dk-settings) after the engines TTL changes.
|
|
257
|
+
* sol-search loads its source inline in connectedCallback, so reload
|
|
258
|
+
* tears down and reconnects to walk the same path.
|
|
259
|
+
*/
|
|
260
|
+
async reload() {
|
|
261
|
+
this.disconnectedCallback();
|
|
262
|
+
await this.connectedCallback();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
attributeChangedCallback(name) {
|
|
266
|
+
// Static-attribute changes fire after the element is in the DOM but
|
|
267
|
+
// before our first connectedCallback runs (isConnected is already
|
|
268
|
+
// true). Bailing on `_built` is the correct gate — `connectedCallback`
|
|
269
|
+
// applies the attributes in order anyway, and we re-apply on later
|
|
270
|
+
// changes.
|
|
271
|
+
if (!this._built) return;
|
|
272
|
+
|
|
273
|
+
// Switching `view` requires a full rebuild — disconnect listeners,
|
|
274
|
+
// clear the root, and re-run connectedCallback (which resets the
|
|
275
|
+
// shadow root and re-loads the source if one is set).
|
|
276
|
+
if (name === 'view') {
|
|
277
|
+
this.disconnectedCallback();
|
|
278
|
+
this.connectedCallback();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
this._loadEngines();
|
|
282
|
+
this._renderEngines();
|
|
283
|
+
this._applyPlaceholder();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
_loadEngines() {
|
|
287
|
+
// `engines` attribute beats the built-in defaults; an RDF `source`
|
|
288
|
+
// result (resolved in connectedCallback's tail) beats both.
|
|
289
|
+
const enginesAttr = this.getAttribute('engines');
|
|
290
|
+
if (enginesAttr) {
|
|
291
|
+
try {
|
|
292
|
+
const parsed = JSON.parse(enginesAttr);
|
|
293
|
+
if (Array.isArray(parsed) && parsed.length) this._engines = parsed;
|
|
294
|
+
} catch { /* leave whatever was there on bad JSON */ }
|
|
295
|
+
} else if (!this.getAttribute('source')) {
|
|
296
|
+
this._engines = DEFAULT_ENGINES;
|
|
297
|
+
}
|
|
298
|
+
const def = this.getAttribute('default-engine');
|
|
299
|
+
// Allow matching by id or by URL prefix — convenient when defaults
|
|
300
|
+
// are RDF-sourced and ids are slugified labels.
|
|
301
|
+
this._defaultEngine = def
|
|
302
|
+
|| this._engines.find(e => /duckduckgo/i.test(e.label))?.id
|
|
303
|
+
|| this._engines[0]?.id
|
|
304
|
+
|| 'ddg';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
_applyPlaceholder() {
|
|
308
|
+
if (!this.$q) return;
|
|
309
|
+
this.$q.setAttribute('placeholder', this.getAttribute('placeholder') || 'Search…');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
_renderEngines() {
|
|
313
|
+
if (!this.$engines) return;
|
|
314
|
+
// Unique radio-group name per render so repeated re-renders don't
|
|
315
|
+
// accidentally cross-link with stale inputs in the same shadow root.
|
|
316
|
+
const name = `engine-${Math.random().toString(36).slice(2, 8)}`;
|
|
317
|
+
// Radios go directly in .engines — the container is a flex-wrap
|
|
318
|
+
// row, so a list that's too wide for one line continues on a
|
|
319
|
+
// second (or third) row beneath. No track wrapper, no carousel.
|
|
320
|
+
this.$engines.innerHTML = this._engines.map(eng => `
|
|
321
|
+
<label class="engine">
|
|
322
|
+
<input type="radio" name="${name}" value="${esc(eng.id)}">
|
|
323
|
+
<span>${esc(eng.label ?? eng.id)}</span>
|
|
324
|
+
</label>
|
|
325
|
+
`).join('');
|
|
326
|
+
|
|
327
|
+
const radios = [...this.shadowRoot.querySelectorAll(`input[name="${name}"]`)];
|
|
328
|
+
const pick = radios.find(r => r.value === this._defaultEngine) || radios[0];
|
|
329
|
+
if (pick) pick.checked = true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* ── view: button (popup) controls ─────────────────────────────────── */
|
|
333
|
+
|
|
334
|
+
toggle() {
|
|
335
|
+
if (this._view !== 'button') return;
|
|
336
|
+
this._open ? this.close() : this.openAtButton();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
openAtButton() {
|
|
340
|
+
if (this._view !== 'button') return;
|
|
341
|
+
this._open = true;
|
|
342
|
+
this.$btn.setAttribute('aria-expanded', 'true');
|
|
343
|
+
this.$panel.setAttribute('open', '');
|
|
344
|
+
|
|
345
|
+
// Measure once visible, then position so the panel's right edge lines
|
|
346
|
+
// up with the trigger's right edge (drops down-and-left). Clamps into
|
|
347
|
+
// the viewport with a 10px margin.
|
|
348
|
+
this.$panel.style.left = '0px';
|
|
349
|
+
this.$panel.style.top = '0px';
|
|
350
|
+
|
|
351
|
+
const btn = this.$btn.getBoundingClientRect();
|
|
352
|
+
const panel = this.$panel.getBoundingClientRect();
|
|
353
|
+
const margin = 10;
|
|
354
|
+
|
|
355
|
+
let left = btn.right - panel.width;
|
|
356
|
+
let top = btn.bottom + 4;
|
|
357
|
+
left = Math.max(margin, Math.min(left, window.innerWidth - panel.width - margin));
|
|
358
|
+
top = Math.max(margin, Math.min(top, window.innerHeight - panel.height - margin));
|
|
359
|
+
|
|
360
|
+
this.$panel.style.left = `${left}px`;
|
|
361
|
+
this.$panel.style.top = `${top}px`;
|
|
362
|
+
|
|
363
|
+
document.addEventListener('pointerdown', this._onDocPointerDown, { capture: true });
|
|
364
|
+
document.addEventListener('keydown', this._onDocKeyDown);
|
|
365
|
+
queueMicrotask(() => this.$q.focus());
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
close() {
|
|
369
|
+
if (this._view !== 'button') return;
|
|
370
|
+
if (!this._open) return;
|
|
371
|
+
this._open = false;
|
|
372
|
+
this.$btn.setAttribute('aria-expanded', 'false');
|
|
373
|
+
this.$panel.removeAttribute('open');
|
|
374
|
+
document.removeEventListener('pointerdown', this._onDocPointerDown, { capture: true });
|
|
375
|
+
document.removeEventListener('keydown', this._onDocKeyDown);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* ── shared submit ────────────────────────────────────────────────── */
|
|
379
|
+
|
|
380
|
+
_selectedEngine() {
|
|
381
|
+
const checked = this.shadowRoot.querySelector('input[type="radio"]:checked');
|
|
382
|
+
return this._engines.find(e => e.id === checked?.value) || this._engines[0];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
_doSearch() {
|
|
386
|
+
const q = (this.$q.value || '').trim();
|
|
387
|
+
if (!q) return;
|
|
388
|
+
const eng = this._selectedEngine();
|
|
389
|
+
const url = expandQuery(eng, q);
|
|
390
|
+
if (!openInReader(url)) {
|
|
391
|
+
// Popup blocked; fall through to a normal new-tab open so the user
|
|
392
|
+
// still gets the search result rather than nothing.
|
|
393
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
394
|
+
}
|
|
395
|
+
if (this._view === 'button') this.close();
|
|
396
|
+
// Inline view: leave the input populated so the user can refine and
|
|
397
|
+
// submit again without retyping.
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
define('sol-search', SolSearch);
|
|
402
|
+
export { SolSearch };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <sol-settings> — discovery-driven settings page.
|
|
3
|
+
*
|
|
4
|
+
* Walks the current document (crossing into every shadow root) for
|
|
5
|
+
* elements whose custom-element class declares an editor (`static get
|
|
6
|
+
* editor()` or `static get shape()`). For each, builds one accordion
|
|
7
|
+
* panel: summary shows a friendly label, body lazy-mounts the
|
|
8
|
+
* declared editor element on first expand, wired with the host's
|
|
9
|
+
* `source` / `from-rdf` subject. On successful save the host
|
|
10
|
+
* component's `reload()` (if present) is invoked.
|
|
11
|
+
*
|
|
12
|
+
* No configuration: drop a `<sol-settings></sol-settings>` anywhere
|
|
13
|
+
* on the page; widgets elsewhere on the page are picked up
|
|
14
|
+
* automatically. Hosts can render widgets into a keep-alive region pane so
|
|
15
|
+
* they stay mounted (hidden) when the user navigates to the settings page;
|
|
16
|
+
* otherwise discovery only sees widgets currently in the DOM.
|
|
17
|
+
*
|
|
18
|
+
* Attributes:
|
|
19
|
+
* none
|
|
20
|
+
*
|
|
21
|
+
* Methods:
|
|
22
|
+
* refresh() — re-walk and rebuild the accordion if the widget set
|
|
23
|
+
* has changed (signature: tag + subject). Cheap no-op
|
|
24
|
+
* when nothing changed. Use from consumer code when a
|
|
25
|
+
* new editable widget is mounted after sol-settings
|
|
26
|
+
* connected. (Tab activation triggers this automatically
|
|
27
|
+
* via the sol-tab-activate listener.)
|
|
28
|
+
*
|
|
29
|
+
* Events (consumed):
|
|
30
|
+
* sol-form-save — bubbling from any embedded editor; triggers
|
|
31
|
+
* `host.reload()` on the corresponding source widget.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { define } from '../core/define.js';
|
|
35
|
+
import { buildEditorElement, triggerSelfEditor, editPlacement } from '../core/editor.js';
|
|
36
|
+
import { findExtensionPoints } from '../core/extension-points.js';
|
|
37
|
+
import './sol-accordion.js';
|
|
38
|
+
|
|
39
|
+
class SolSettings extends HTMLElement {
|
|
40
|
+
connectedCallback() {
|
|
41
|
+
if (this._wired) return;
|
|
42
|
+
this._wired = true;
|
|
43
|
+
// Defer one microtask so the surrounding DOM (e.g., a sibling
|
|
44
|
+
// keep-alive wrapper that holds the dashboard widgets) is fully
|
|
45
|
+
// attached before discovery walks.
|
|
46
|
+
queueMicrotask(() => this._build());
|
|
47
|
+
|
|
48
|
+
// Re-discover when the editable-component set changes. Generic trigger: a
|
|
49
|
+
// debounced MutationObserver on the whole document — works with any app, no
|
|
50
|
+
// swc-specific navigation needed. `sol-tab-activate` stays as an extra hint
|
|
51
|
+
// for keep-alive tab UIs (harmless if no one fires it). The rebuild only
|
|
52
|
+
// happens when the discovered set actually changed (signature compare).
|
|
53
|
+
this._rebuild = () => {
|
|
54
|
+
if (this.offsetParent === null) return; // we're hidden; ignore
|
|
55
|
+
this._rebuildIfChanged();
|
|
56
|
+
};
|
|
57
|
+
this._mo = new MutationObserver(() => {
|
|
58
|
+
clearTimeout(this._moTimer);
|
|
59
|
+
this._moTimer = setTimeout(this._rebuild, 50);
|
|
60
|
+
});
|
|
61
|
+
this._mo.observe(document.documentElement, { childList: true, subtree: true });
|
|
62
|
+
document.addEventListener('sol-tab-activate', this._rebuild);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
disconnectedCallback() {
|
|
66
|
+
if (this._mo) { this._mo.disconnect(); this._mo = null; }
|
|
67
|
+
clearTimeout(this._moTimer);
|
|
68
|
+
if (this._rebuild) {
|
|
69
|
+
document.removeEventListener('sol-tab-activate', this._rebuild);
|
|
70
|
+
this._rebuild = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_build() {
|
|
75
|
+
const widgets = this._discover();
|
|
76
|
+
this._lastSignature = signatureOf(widgets);
|
|
77
|
+
this.innerHTML = '';
|
|
78
|
+
if (!widgets.length) {
|
|
79
|
+
this._empty();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const accordion = document.createElement('sol-accordion');
|
|
84
|
+
accordion.setAttribute('start-closed', '');
|
|
85
|
+
widgets.forEach((w, i) => {
|
|
86
|
+
const panel = document.createElement('div');
|
|
87
|
+
const head = document.createElement('div');
|
|
88
|
+
head.textContent = w.label;
|
|
89
|
+
const body = document.createElement('div');
|
|
90
|
+
body.className = 'sol-settings-slot';
|
|
91
|
+
body.dataset.widgetIdx = String(i);
|
|
92
|
+
panel.append(head, body);
|
|
93
|
+
accordion.appendChild(panel);
|
|
94
|
+
});
|
|
95
|
+
this.appendChild(accordion);
|
|
96
|
+
|
|
97
|
+
// sol-accordion runs synchronously on connect; once it has cloned
|
|
98
|
+
// the author divs into <details>, attach lazy-mount handlers.
|
|
99
|
+
Promise.resolve().then(() => this._wireLazy(accordion, widgets));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_rebuildIfChanged() {
|
|
103
|
+
const widgets = this._discover();
|
|
104
|
+
const sig = signatureOf(widgets);
|
|
105
|
+
if (sig === this._lastSignature) return;
|
|
106
|
+
this._build();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
refresh() { this._rebuildIfChanged(); }
|
|
110
|
+
|
|
111
|
+
_empty() {
|
|
112
|
+
const note = document.createElement('p');
|
|
113
|
+
note.className = 'sol-settings-empty';
|
|
114
|
+
note.textContent = 'No editable widgets found on this page.';
|
|
115
|
+
this.appendChild(note);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_wireLazy(accordion, widgets) {
|
|
119
|
+
const detailsList = accordion.querySelectorAll('details');
|
|
120
|
+
detailsList.forEach((det, i) => {
|
|
121
|
+
const widget = widgets[i];
|
|
122
|
+
if (!widget) return;
|
|
123
|
+
const section = det.querySelector('.accordion-content-section');
|
|
124
|
+
if (!section) return;
|
|
125
|
+
let mounted = false;
|
|
126
|
+
const mount = () => {
|
|
127
|
+
if (mounted) return;
|
|
128
|
+
mounted = true;
|
|
129
|
+
section.innerHTML = '';
|
|
130
|
+
// forms:"self" — the component renders its own editor; offer a trigger.
|
|
131
|
+
if (widget.spec && widget.spec.self) {
|
|
132
|
+
const btn = document.createElement('button');
|
|
133
|
+
btn.type = 'button';
|
|
134
|
+
btn.className = 'sol-settings-self-open';
|
|
135
|
+
btn.textContent = `Edit ${widget.label}…`;
|
|
136
|
+
btn.addEventListener('click', () => triggerSelfEditor(widget.el, widget.spec));
|
|
137
|
+
section.appendChild(btn);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const editor = buildEditorElement(widget.el, widget.spec);
|
|
141
|
+
if (!editor) {
|
|
142
|
+
section.textContent = 'No editor available.';
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
editor.addEventListener('sol-form-save', () => {
|
|
146
|
+
if (typeof widget.el.reload === 'function') {
|
|
147
|
+
widget.el.reload().catch(() => {});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
section.appendChild(editor);
|
|
151
|
+
};
|
|
152
|
+
if (det.open) mount();
|
|
153
|
+
det.addEventListener('toggle', () => { if (det.open) mount(); });
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Editable widgets = every element offering the `edit` extension point. The
|
|
158
|
+
// walk + shadow-crossing + resolution lives in core/extension-points.js; this
|
|
159
|
+
// is just the `edit` case of the general protocol. Opt-out attr stays
|
|
160
|
+
// `data-settings-skip` for back-compat with pages that use it.
|
|
161
|
+
_discover() {
|
|
162
|
+
return findExtensionPoints('edit', { skipAttr: 'data-settings-skip' })
|
|
163
|
+
.filter(({ el }) => el !== this && !this.contains(el))
|
|
164
|
+
// inPlace editors render a gear ON the element itself — sol-settings
|
|
165
|
+
// gathers only the "collected" ones (the default placement).
|
|
166
|
+
.filter(({ el, spec }) => editPlacement(el, spec) === 'collected')
|
|
167
|
+
.map(({ el, spec }) => ({
|
|
168
|
+
el, spec,
|
|
169
|
+
label: el.getAttribute('label') || labelFromTag(el.localName),
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Fallback label when an element has no `label` attribute. Drops the leading
|
|
175
|
+
// vendor-prefix segment (sol-, dk-, my-, …) generically and title-cases the
|
|
176
|
+
// rest — `sol-weather` → "Weather", `my-thing` → "Thing", `sol-dropdown-button`
|
|
177
|
+
// → "Dropdown Button". Any component can override with an explicit `label`.
|
|
178
|
+
function labelFromTag(tag) {
|
|
179
|
+
return tag
|
|
180
|
+
.replace(/^[a-z0-9]+-/, '')
|
|
181
|
+
.split('-')
|
|
182
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
183
|
+
.join(' ');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Stable identity for a discovered widget set, used to detect when a
|
|
187
|
+
* later re-discovery has actually changed anything. Tag + subject is
|
|
188
|
+
* enough — two instances of the same widget with the same source
|
|
189
|
+
* would render an identical accordion panel. */
|
|
190
|
+
function signatureOf(widgets) {
|
|
191
|
+
return widgets
|
|
192
|
+
.map(w => `${w.el.localName}#${w.el.getAttribute('source') || w.el.getAttribute('from-rdf') || ''}`)
|
|
193
|
+
.sort()
|
|
194
|
+
.join('|');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
define('sol-settings', SolSettings);
|
|
198
|
+
export { SolSettings };
|
|
199
|
+
export default SolSettings;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { define } from '../core/define.js';
|
|
2
|
+
import { ensureDocStyle } from '../core/adopt.js';
|
|
3
|
+
|
|
4
|
+
function getMashlib() {
|
|
5
|
+
const w = typeof window !== 'undefined' ? window : {};
|
|
6
|
+
const g = typeof globalThis !== 'undefined' ? globalThis : (typeof self !== 'undefined' ? self : w);
|
|
7
|
+
const Mashlib = w.Mashlib || g.Mashlib;
|
|
8
|
+
const SolidLogic = w.SolidLogic || g.SolidLogic;
|
|
9
|
+
const $rdf = w.$rdf || g.$rdf;
|
|
10
|
+
const panes = w.panes || g.panes;
|
|
11
|
+
if (!Mashlib || !panes) return null;
|
|
12
|
+
const initMainPage = Mashlib.initMainPage || Mashlib.default?.initMainPage || Mashlib.default;
|
|
13
|
+
if (!initMainPage) return null;
|
|
14
|
+
return { Mashlib, initMainPage, SolidLogic, $rdf, panes };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Hide mashlib's own header/footer chrome — when sol-solidos is mounted
|
|
18
|
+
// inside a host shell (dk), the host already owns login + help/prefs
|
|
19
|
+
// affordances, so a duplicate login button in mashlib's header is both
|
|
20
|
+
// confusing and broken (popup-mode pod sessions don't reach mashlib's
|
|
21
|
+
// default Inrupt session, so its login button stalls). Mashlib's reads
|
|
22
|
+
// go through rdflib's patched Fetcher → solFetch → sol-auth-needed →
|
|
23
|
+
// the host's <sol-login> chip handles the prompt and retries.
|
|
24
|
+
const HOST_CSS = `
|
|
25
|
+
sol-solidos { display: block; width: 100%; height: 100%; }
|
|
26
|
+
sol-solidos > #PageHeader,
|
|
27
|
+
sol-solidos > #PageFooter { display: none; }
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
class SolSolidos extends HTMLElement {
|
|
31
|
+
static get observedAttributes() { return ['source']; }
|
|
32
|
+
|
|
33
|
+
constructor() {
|
|
34
|
+
super();
|
|
35
|
+
this._ready = false;
|
|
36
|
+
this._m = null;
|
|
37
|
+
this._outliner = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
connectedCallback() {
|
|
41
|
+
if (this.isConnected && !this._ready) this._init();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
attributeChangedCallback(name, oldV, newV) {
|
|
45
|
+
if (name === 'source' && oldV !== newV && this._ready) {
|
|
46
|
+
this._goTo(newV);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_goTo(uri) {
|
|
51
|
+
if (!uri || !this._outliner) return;
|
|
52
|
+
const subject = this._m.$rdf.sym(uri);
|
|
53
|
+
this._outliner.GotoSubject(subject, true, undefined, true, undefined);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_init() {
|
|
57
|
+
const m = getMashlib();
|
|
58
|
+
if (!m) {
|
|
59
|
+
const w = typeof window !== 'undefined' ? window : {};
|
|
60
|
+
const g = typeof globalThis !== 'undefined' ? globalThis : {};
|
|
61
|
+
console.error('[sol-solidos] getMashlib() returned null. window.Mashlib:', w.Mashlib,
|
|
62
|
+
'globalThis.Mashlib:', g.Mashlib, 'window.panes:', w.panes, 'globalThis.panes:', g.panes);
|
|
63
|
+
this.textContent = 'mashlib not loaded \u2014 add <script src="mashlib.js"> to the page';
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this._m = m;
|
|
67
|
+
ensureDocStyle(document, 'sol-solidos-style', HOST_CSS);
|
|
68
|
+
|
|
69
|
+
// Build the DOM structure mashlib expects
|
|
70
|
+
this.innerHTML = `
|
|
71
|
+
<header id="PageHeader" role="banner"></header>
|
|
72
|
+
<main id="mainContent" tabindex="-1">
|
|
73
|
+
<div class="TabulatorOutline" id="DummyUUID">
|
|
74
|
+
<table id="outline"></table>
|
|
75
|
+
<div id="GlobalDashboard"></div>
|
|
76
|
+
</div>
|
|
77
|
+
</main>
|
|
78
|
+
<footer id="PageFooter" role="contentinfo"></footer>
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
const SL = m.SolidLogic?.solidLogicSingleton || m.SolidLogic?.default?.solidLogicSingleton;
|
|
82
|
+
const store = SL?.store;
|
|
83
|
+
const uri = this.getAttribute('source') || window.location.href;
|
|
84
|
+
|
|
85
|
+
this._outliner = m.panes.getOutliner(document);
|
|
86
|
+
m.initMainPage(store, uri);
|
|
87
|
+
|
|
88
|
+
this._ready = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
define('sol-solidos', SolSolidos);
|
|
93
|
+
export default SolSolidos;
|