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,28 @@
|
|
|
1
|
+
// core/edit-placements.js — activate the `edit="inPlace"` placement.
|
|
2
|
+
//
|
|
3
|
+
// Loaded as part of the `rdf` capability (so an app that does
|
|
4
|
+
// `data-extend-with="rdf"` gets it for free). It walks the page — now and as
|
|
5
|
+
// elements mount — for every editable element whose placement resolves to
|
|
6
|
+
// "inPlace" and attaches an inline edit gear. "collected" elements are left for
|
|
7
|
+
// <sol-settings> to gather. Editability itself comes from the `edit` protocol
|
|
8
|
+
// (a `shape="…"` attribute, a class `static extensionPoints`/`shape`, or a
|
|
9
|
+
// manifest `interop.editable` descriptor); this module only decides PLACEMENT.
|
|
10
|
+
import { observeExtensionPoint } from './extension-points.js';
|
|
11
|
+
import { editPlacement } from './editor.js';
|
|
12
|
+
|
|
13
|
+
let _on = false;
|
|
14
|
+
/** Begin attaching inline gears to `edit="inPlace"` (or legacy `editor-self`)
|
|
15
|
+
* elements. Idempotent. */
|
|
16
|
+
export function activateInlinePlacements() {
|
|
17
|
+
if (_on || typeof document === 'undefined') return;
|
|
18
|
+
_on = true;
|
|
19
|
+
observeExtensionPoint('edit', (el, spec) => {
|
|
20
|
+
if (spec && spec.self) return; // component edits itself
|
|
21
|
+
if (editPlacement(el, spec) !== 'inPlace') return; // collected ⇒ sol-settings owns it
|
|
22
|
+
import('./editor-self.js').then(({ attachEditorSelfGear }) => {
|
|
23
|
+
try { attachEditorSelfGear(el, spec); } catch (_) { /* no shadowRoot etc. — skip */ }
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
activateInlinePlacements();
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Inline edit-in-place: page authors opt a component instance into
|
|
2
|
+
// rendering a small gear button by adding the `editor-self` attribute.
|
|
3
|
+
// Clicking the gear opens a <sol-modal> containing a <sol-form> bound
|
|
4
|
+
// to the component's editor (declared via its static `editor` getter)
|
|
5
|
+
// and its current `source` / `from-rdf` subject.
|
|
6
|
+
//
|
|
7
|
+
// Components call attachEditorSelfGear(this) in connectedCallback
|
|
8
|
+
// guarded by `this.hasAttribute('editor-self')`. The helper is a
|
|
9
|
+
// no-op for components that opt out via `editor = { inline: true }`.
|
|
10
|
+
//
|
|
11
|
+
// dk's pages do not exercise this path — every editable component on
|
|
12
|
+
// dk is shared-mode (no `editor-self` attribute), edited from
|
|
13
|
+
// dk-settings. The helper lives in swc because it's a property of
|
|
14
|
+
// the component infrastructure, useful to other consumers.
|
|
15
|
+
|
|
16
|
+
import { buildEditorElement, resolveEditorSpec } from './editor.js';
|
|
17
|
+
|
|
18
|
+
const GEAR_CSS = `
|
|
19
|
+
.sol-editor-self-gear {
|
|
20
|
+
position: absolute;
|
|
21
|
+
top: 2px;
|
|
22
|
+
right: 2px;
|
|
23
|
+
width: 1.4rem;
|
|
24
|
+
height: 1.4rem;
|
|
25
|
+
padding: 0;
|
|
26
|
+
border: 1px solid var(--border, #9e9e9e);
|
|
27
|
+
border-radius: var(--radius-sm, 4px);
|
|
28
|
+
background: var(--surface, #fff);
|
|
29
|
+
color: var(--text-muted, #4d4d4d);
|
|
30
|
+
font-size: 0.85rem;
|
|
31
|
+
line-height: 1;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
opacity: 0;
|
|
34
|
+
transition: opacity 120ms ease;
|
|
35
|
+
z-index: 1;
|
|
36
|
+
}
|
|
37
|
+
.sol-editor-self-gear:focus,
|
|
38
|
+
:host(:hover) .sol-editor-self-gear,
|
|
39
|
+
.sol-editor-self-gear:hover {
|
|
40
|
+
opacity: 1;
|
|
41
|
+
}
|
|
42
|
+
.sol-editor-self-gear:focus-visible {
|
|
43
|
+
outline: 2px solid var(--accent, #1F618D);
|
|
44
|
+
outline-offset: 1px;
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
let _gearSheet = null;
|
|
49
|
+
function gearSheet() {
|
|
50
|
+
if (_gearSheet) return _gearSheet;
|
|
51
|
+
_gearSheet = new CSSStyleSheet();
|
|
52
|
+
_gearSheet.replaceSync(GEAR_CSS);
|
|
53
|
+
return _gearSheet;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Attach an inline edit gear to a component instance. Idempotent —
|
|
58
|
+
* calling twice on the same element is a no-op.
|
|
59
|
+
*
|
|
60
|
+
* @param {HTMLElement} el - the host component (must have shadowRoot)
|
|
61
|
+
* @returns {HTMLButtonElement | null} the gear button, or null if
|
|
62
|
+
* the component opted out via `editor = { inline: true }` or has
|
|
63
|
+
* no editor at all.
|
|
64
|
+
*/
|
|
65
|
+
export function attachEditorSelfGear(el, spec) {
|
|
66
|
+
// `spec` (from a manifest's interop.editable) lets a FOREIGN element get a
|
|
67
|
+
// gear even though its class declares no editor; otherwise resolve from class.
|
|
68
|
+
if (!(spec || resolveEditorSpec(el.constructor, el))) return null;
|
|
69
|
+
if (el._editorSelfGear) return el._editorSelfGear;
|
|
70
|
+
|
|
71
|
+
const root = el.shadowRoot ?? el;
|
|
72
|
+
// Make the host a positioning context so the absolutely-positioned
|
|
73
|
+
// gear anchors correctly. Skip if host CSS already established one.
|
|
74
|
+
if (el.style && !el.style.position) el.style.position = 'relative';
|
|
75
|
+
|
|
76
|
+
// Adopt the gear stylesheet into the shadow root (or document for
|
|
77
|
+
// light-DOM hosts).
|
|
78
|
+
if (el.shadowRoot && el.shadowRoot.adoptedStyleSheets) {
|
|
79
|
+
if (!el.shadowRoot.adoptedStyleSheets.includes(gearSheet())) {
|
|
80
|
+
el.shadowRoot.adoptedStyleSheets = [...el.shadowRoot.adoptedStyleSheets, gearSheet()];
|
|
81
|
+
}
|
|
82
|
+
} else if (!document.adoptedStyleSheets.includes(gearSheet())) {
|
|
83
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, gearSheet()];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const btn = document.createElement('button');
|
|
87
|
+
btn.className = 'sol-editor-self-gear';
|
|
88
|
+
btn.type = 'button';
|
|
89
|
+
btn.setAttribute('aria-label', `Edit ${el.localName} settings`);
|
|
90
|
+
btn.textContent = '⚙'; // ⚙
|
|
91
|
+
btn.addEventListener('click', (e) => {
|
|
92
|
+
e.stopPropagation();
|
|
93
|
+
openEditorModal(el, spec);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
root.appendChild(btn);
|
|
97
|
+
el._editorSelfGear = btn;
|
|
98
|
+
return btn;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Programmatically open the editor modal for any component. Used by
|
|
103
|
+
* the gear handler above and by external surfaces.
|
|
104
|
+
*
|
|
105
|
+
* @param {HTMLElement} el - the component being edited
|
|
106
|
+
*/
|
|
107
|
+
export function openEditorModal(el, spec) {
|
|
108
|
+
const editor = buildEditorElement(el, spec);
|
|
109
|
+
if (!editor) return;
|
|
110
|
+
|
|
111
|
+
const modal = document.createElement('sol-modal');
|
|
112
|
+
modal.setAttribute('title', `Edit ${el.localName}`);
|
|
113
|
+
modal.setAttribute('open', '');
|
|
114
|
+
|
|
115
|
+
const close = () => {
|
|
116
|
+
if (typeof modal.close === 'function') modal.close();
|
|
117
|
+
else modal.removeAttribute('open');
|
|
118
|
+
};
|
|
119
|
+
const onSaved = () => {
|
|
120
|
+
if (typeof el.reload === 'function') el.reload().catch(() => {});
|
|
121
|
+
close();
|
|
122
|
+
};
|
|
123
|
+
editor.addEventListener('sol-form-save', onSaved);
|
|
124
|
+
|
|
125
|
+
modal.appendChild(editor);
|
|
126
|
+
document.body.appendChild(modal);
|
|
127
|
+
}
|
package/core/editor.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Resolve a component class's editor declaration into a canonical spec
|
|
2
|
+
// consumed by both `core/editor-self.js` (inline gear → modal) and
|
|
3
|
+
// `web/sol-settings.js` (accordion-mounted editors).
|
|
4
|
+
//
|
|
5
|
+
// Declarations a component class may carry:
|
|
6
|
+
//
|
|
7
|
+
// static get editor()
|
|
8
|
+
// - { inline: true } → opt out (sol-feed)
|
|
9
|
+
// - "https://…/form.ttl" (string) → legacy: sol-form with that ui:Form
|
|
10
|
+
// - { tag, subjectAttr?, attrs? } → explicit editor (e.g. sol-tree-edit)
|
|
11
|
+
//
|
|
12
|
+
// static get shape()
|
|
13
|
+
// - "https://…/shape.shacl" → implicit sol-form in shape-driven mode
|
|
14
|
+
//
|
|
15
|
+
// a `shape="…"` ATTRIBUTE on the instance overrides the class shape — so a
|
|
16
|
+
// generic element (e.g. <sol-default shape="./app-settings.shacl">) is
|
|
17
|
+
// configurable without any class-level declaration. This is what lets
|
|
18
|
+
// sol-settings work with anyone's components.
|
|
19
|
+
//
|
|
20
|
+
// `editor` takes precedence over `shape`. When neither is set the
|
|
21
|
+
// component is not editable; resolveEditorSpec returns null.
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {Function | undefined} Ctor — custom-element class
|
|
25
|
+
* @param {Element} [el] — the instance, so a per-instance `shape` attribute can
|
|
26
|
+
* override the class shape (and make a class-less element editable).
|
|
27
|
+
* @returns {{tag: string, subjectAttr: string, attrs: object, save: boolean} | null}
|
|
28
|
+
*/
|
|
29
|
+
export function resolveEditorSpec(Ctor, el) {
|
|
30
|
+
const ed = Ctor && Ctor.editor;
|
|
31
|
+
if (ed && typeof ed === 'object' && ed.inline) return null;
|
|
32
|
+
|
|
33
|
+
if (typeof ed === 'string') {
|
|
34
|
+
return { tag: 'sol-form', subjectAttr: 'subject', attrs: { source: ed }, save: true };
|
|
35
|
+
}
|
|
36
|
+
if (ed && typeof ed === 'object') {
|
|
37
|
+
const tag = ed.tag || 'sol-form';
|
|
38
|
+
return {
|
|
39
|
+
tag,
|
|
40
|
+
subjectAttr: ed.subjectAttr || 'subject',
|
|
41
|
+
attrs: { ...(ed.attrs || {}) },
|
|
42
|
+
save: tag === 'sol-form',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Instance attribute wins over the class default; either makes it editable.
|
|
47
|
+
// `data-edit-shape` is the canonical capability attribute; bare `shape` is the
|
|
48
|
+
// back-compat alias.
|
|
49
|
+
const shape = (el && el.getAttribute && (el.getAttribute('data-edit-shape') || el.getAttribute('shape'))) ||
|
|
50
|
+
(Ctor && typeof Ctor.shape === 'string' ? Ctor.shape : null);
|
|
51
|
+
if (shape) {
|
|
52
|
+
return { tag: 'sol-form', subjectAttr: 'subject', attrs: { shape }, save: true };
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Normalize a manifest-declared edit spec (from a manifest's
|
|
59
|
+
* `interop.editable` map) into the canonical editor spec — so a component that
|
|
60
|
+
* declares NO `static editor`/`shape` (e.g. a foreign library's element) can be
|
|
61
|
+
* made editable purely from a manifest descriptor. The descriptor distinguishes:
|
|
62
|
+
* - shape (a) ACCESSIBLE: SHACL for auto-generation; absent ⇒ null
|
|
63
|
+
* - forms: "self" (b) the component renders its OWN form; we don't generate
|
|
64
|
+
* - present (c) "inline" (button on the element) | "collected" (sol-settings)
|
|
65
|
+
* - subject.attr which attribute on the element holds the subject URI
|
|
66
|
+
* - open (self only) how to trigger the component's own editor
|
|
67
|
+
*
|
|
68
|
+
* @param {object} decl
|
|
69
|
+
* @returns {{tag,subjectAttr,attrs,save,subjectFrom?,present?} | {self:true,open,present} | null}
|
|
70
|
+
*/
|
|
71
|
+
export function editorSpecFromDecl(decl) {
|
|
72
|
+
if (!decl || typeof decl !== 'object') return null;
|
|
73
|
+
if (decl.forms === 'self') {
|
|
74
|
+
return { self: true, open: decl.open || null, present: decl.present || 'inPlace' };
|
|
75
|
+
}
|
|
76
|
+
if (!decl.shape) return null; // (a) no shape ⇒ not auto-editable
|
|
77
|
+
return {
|
|
78
|
+
tag: 'sol-form',
|
|
79
|
+
subjectAttr: 'subject',
|
|
80
|
+
attrs: { shape: decl.shape },
|
|
81
|
+
save: true,
|
|
82
|
+
subjectFrom: (decl.subject && decl.subject.attr) || null,
|
|
83
|
+
present: decl.present || 'collected',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the subject URI being edited from a host component instance.
|
|
89
|
+
* A spec's `subjectFrom` (from a manifest descriptor) wins; otherwise falls
|
|
90
|
+
* back through `source` → `from-rdf` → empty.
|
|
91
|
+
*/
|
|
92
|
+
export function editorSubjectOf(el, spec) {
|
|
93
|
+
if (spec && spec.subjectFrom) {
|
|
94
|
+
const v = el.getAttribute(spec.subjectFrom);
|
|
95
|
+
if (v) return v;
|
|
96
|
+
}
|
|
97
|
+
// `data-subject="…"` (canonical) / `subject="…"` (alias) is the explicit,
|
|
98
|
+
// foreign-friendly locator (a component whose subject isn't in
|
|
99
|
+
// `source`/`from-rdf` — e.g. a third party's element — just adds it). Then the
|
|
100
|
+
// usual fallbacks.
|
|
101
|
+
return el.getAttribute('data-subject') || el.getAttribute('subject')
|
|
102
|
+
|| el.getAttribute('source') || el.getAttribute('from-rdf') || '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Where an editable element's form lives:
|
|
107
|
+
* "inPlace" — a gear button ON the element (core/editor-self.js)
|
|
108
|
+
* "collected" — gathered into a <sol-settings> panel
|
|
109
|
+
* The element's `edit="inPlace|collected"` attribute is the canonical control;
|
|
110
|
+
* the legacy `editor-self` attribute (⇒ inPlace) and a manifest descriptor's
|
|
111
|
+
* `present` are honored too. Default: "collected".
|
|
112
|
+
*/
|
|
113
|
+
export function editPlacement(el, spec) {
|
|
114
|
+
const a = ((el && el.getAttribute && (el.getAttribute('data-edit-mode') || el.getAttribute('edit'))) || '').toLowerCase();
|
|
115
|
+
if (a === 'inplace' || a === 'inline') return 'inPlace';
|
|
116
|
+
if (a === 'collected') return 'collected';
|
|
117
|
+
if (el && el.hasAttribute && el.hasAttribute('editor-self')) return 'inPlace'; // legacy alias
|
|
118
|
+
const p = spec && spec.present && String(spec.present).toLowerCase();
|
|
119
|
+
if (p) return (p === 'inplace' || p === 'inline') ? 'inPlace' : 'collected';
|
|
120
|
+
return 'collected';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create the editor element for a component instance, fully wired with
|
|
125
|
+
* subject / save-to / additional attributes. Returns null if the
|
|
126
|
+
* component opts out or has no editor.
|
|
127
|
+
*
|
|
128
|
+
* Caller is responsible for inserting the element into the DOM and
|
|
129
|
+
* listening for `sol-form-save` if it wants to refresh the host.
|
|
130
|
+
*/
|
|
131
|
+
export function buildEditorElement(el, specOverride) {
|
|
132
|
+
const spec = specOverride || resolveEditorSpec(el.constructor, el);
|
|
133
|
+
if (!spec || spec.self) return null; // self-editor: caller triggers via its own UI
|
|
134
|
+
const subject = editorSubjectOf(el, spec);
|
|
135
|
+
|
|
136
|
+
const editorEl = document.createElement(spec.tag);
|
|
137
|
+
if (subject) {
|
|
138
|
+
const abs = absolute(subject);
|
|
139
|
+
editorEl.setAttribute(spec.subjectAttr, abs);
|
|
140
|
+
if (spec.save) editorEl.setAttribute('save-to', abs);
|
|
141
|
+
}
|
|
142
|
+
for (const [k, v] of Object.entries(spec.attrs)) editorEl.setAttribute(k, v);
|
|
143
|
+
return editorEl;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Trigger a component's OWN editor (forms:"self" in a manifest descriptor),
|
|
148
|
+
* via the declared `open` hook — a method on the element or an event to
|
|
149
|
+
* dispatch on it. Returns true if a hook fired.
|
|
150
|
+
*/
|
|
151
|
+
export function triggerSelfEditor(el, spec) {
|
|
152
|
+
const open = spec && spec.open;
|
|
153
|
+
if (!open) return false;
|
|
154
|
+
if (open.method && typeof el[open.method] === 'function') { el[open.method](); return true; }
|
|
155
|
+
if (open.event) { el.dispatchEvent(new CustomEvent(open.event, { bubbles: true, composed: true })); return true; }
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function absolute(uri) {
|
|
160
|
+
try { return new URL(uri, document.baseURI).href; }
|
|
161
|
+
catch { return uri; }
|
|
162
|
+
}
|
package/core/events.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// core/events.js — the canonical table of cross-component event names.
|
|
2
|
+
//
|
|
3
|
+
// Components coordinate by emitting/observing bubbling+composed CustomEvents on
|
|
4
|
+
// the document; this is the single source of truth for the names so authors use
|
|
5
|
+
// `SolidWebComponents.EVENTS.LOGIN` instead of hardcoding the string. Published
|
|
6
|
+
// onto `window.SolidWebComponents.EVENTS` by core/services.js.
|
|
7
|
+
//
|
|
8
|
+
// (Component-private events — e.g. sol-tab-change, sol-pod-pods-changed — stay
|
|
9
|
+
// local to their component. This table is the *shared* coordination vocabulary
|
|
10
|
+
// a third-party author is expected to emit or observe.)
|
|
11
|
+
|
|
12
|
+
export const EVENTS = Object.freeze({
|
|
13
|
+
READY: 'swc:ready', // loader finished its auto-load
|
|
14
|
+
CAPABILITY: 'swc:capability', // a data-extend-with capability finished loading
|
|
15
|
+
OFFER: 'swc:offer', // a component announces the extension points it offers
|
|
16
|
+
|
|
17
|
+
LOGIN: 'sol-login', // user authenticated
|
|
18
|
+
LOGOUT: 'sol-logout', // user signed out
|
|
19
|
+
AUTH_NEEDED: 'sol-auth-needed', // a fetch hit 401; a login listener resolves it
|
|
20
|
+
|
|
21
|
+
DEFAULT_CHANGE: 'sol-default-change', // a <sol-default> attribute changed
|
|
22
|
+
COMMAND: 'sol-command', // an app-registered command (non-component handler)
|
|
23
|
+
ERROR: 'sol-error', // a component/handler load or validation failure
|
|
24
|
+
FORM_SAVE: 'sol-form-save', // an editor persisted changes
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export default EVENTS;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// core/extension-points.js — the general "a capability discovers & enhances any
|
|
2
|
+
// component that offers a named point" protocol. Generalizes the editor/shape
|
|
3
|
+
// contract (core/editor.js + sol-settings discovery) so it's no longer just
|
|
4
|
+
// about editing.
|
|
5
|
+
//
|
|
6
|
+
// A COMPONENT author offers points, import-free, with one static getter:
|
|
7
|
+
//
|
|
8
|
+
// class AcmeMap extends HTMLElement {
|
|
9
|
+
// static get extensionPoints() {
|
|
10
|
+
// return {
|
|
11
|
+
// edit: { shape: 'https://acme/map.shacl' }, // the editing capability
|
|
12
|
+
// annotate: { vocab: 'https://acme/notes#' }, // some other capability
|
|
13
|
+
// };
|
|
14
|
+
// }
|
|
15
|
+
// }
|
|
16
|
+
//
|
|
17
|
+
// A CAPABILITY author (a module loaded via data-extend-with) finds and enhances
|
|
18
|
+
// every component offering its point — now or whenever one mounts later:
|
|
19
|
+
//
|
|
20
|
+
// observeExtensionPoint('annotate', (el, spec) => enhance(el, spec));
|
|
21
|
+
//
|
|
22
|
+
// Neither side imports the other; they meet in the DOM. `edit` is special: it
|
|
23
|
+
// delegates to core/editor.js so the existing `static get editor()` /
|
|
24
|
+
// `static get shape()` / `shape=` attribute all keep working as sugar for it.
|
|
25
|
+
|
|
26
|
+
import { resolveEditorSpec, editorSpecFromDecl } from './editor.js';
|
|
27
|
+
|
|
28
|
+
function own(o, k) { return Object.prototype.hasOwnProperty.call(o, k); }
|
|
29
|
+
function safe(fn) { try { return fn(); } catch (_) { return null; } }
|
|
30
|
+
|
|
31
|
+
// ── externally-registered points ───────────────────────────────────────────
|
|
32
|
+
// A component declares points with a `static get extensionPoints()`. But a
|
|
33
|
+
// FOREIGN component (another library's element) can't — so a manifest's
|
|
34
|
+
// `interop.editable` map lets a host register points for elements matching a
|
|
35
|
+
// CSS selector, with no class change and no library patch. findExtensionPoints
|
|
36
|
+
// consults this registry alongside class statics, so sol-form/sol-settings
|
|
37
|
+
// enhance those elements through the unchanged `edit` protocol.
|
|
38
|
+
const _registered = []; // [{ selector, points }]
|
|
39
|
+
|
|
40
|
+
/** Register extension points for every element matching `selector`. `points`
|
|
41
|
+
* is the same shape a class returns from `extensionPoints` (e.g.
|
|
42
|
+
* `{ edit: { shape, subject:{attr}, forms, present, open } }`). */
|
|
43
|
+
export function registerExtensionPoints(selector, points) {
|
|
44
|
+
if (!selector || !points) return;
|
|
45
|
+
_registered.push({ selector: String(selector), points });
|
|
46
|
+
// nudge live observers to re-scan (late registration / already-mounted els)
|
|
47
|
+
if (typeof document !== 'undefined') {
|
|
48
|
+
document.dispatchEvent(new CustomEvent('swc:offer', { bubbles: true, composed: true, detail: { selector, points } }));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The raw registered declaration for one point on `el`, or null.
|
|
53
|
+
function registeredPoint(el, point) {
|
|
54
|
+
if (!el || typeof el.matches !== 'function') return null;
|
|
55
|
+
for (const r of _registered) {
|
|
56
|
+
if (own(r.points, point) && safe(() => el.matches(r.selector))) return r.points[point];
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The component's declared point map (guarded — a class getter may throw).
|
|
62
|
+
function pointsMap(Ctor) {
|
|
63
|
+
const m = safe(() => (Ctor && Ctor.extensionPoints) || null);
|
|
64
|
+
return (m && typeof m === 'object') ? m : {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The `edit` point: legacy editor/shape/shape= (editor.js owns the rules), then
|
|
68
|
+
// extensionPoints.edit fed back through editor.js so a map-only component works.
|
|
69
|
+
function editPoint(Ctor, el) {
|
|
70
|
+
const legacy = safe(() => resolveEditorSpec(Ctor, el));
|
|
71
|
+
if (legacy) return legacy;
|
|
72
|
+
const e = pointsMap(Ctor).edit;
|
|
73
|
+
if (e != null) {
|
|
74
|
+
const synthetic = (typeof e === 'string') ? { editor: e }
|
|
75
|
+
: (e.shape ? { shape: e.shape } : { editor: e });
|
|
76
|
+
const spec = safe(() => resolveEditorSpec(synthetic, el));
|
|
77
|
+
if (spec) return spec;
|
|
78
|
+
}
|
|
79
|
+
// Finally, a manifest-registered edit descriptor for this element.
|
|
80
|
+
return editorSpecFromDecl(registeredPoint(el, 'edit'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The spec a component offers for ONE point, or null. */
|
|
84
|
+
export function resolveExtensionPoint(Ctor, el, point) {
|
|
85
|
+
if (point === 'edit') return editPoint(Ctor, el);
|
|
86
|
+
const raw = pointsMap(Ctor);
|
|
87
|
+
if (own(raw, point)) return raw[point];
|
|
88
|
+
return registeredPoint(el, point);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Every point a component offers, as { [point]: spec }. `edit` (if any) is the
|
|
92
|
+
* editor.js canonical spec; other points are their raw declarations. */
|
|
93
|
+
export function resolveExtensionPoints(Ctor, el) {
|
|
94
|
+
const out = {};
|
|
95
|
+
const raw = pointsMap(Ctor);
|
|
96
|
+
for (const k in raw) if (own(raw, k) && k !== 'edit') out[k] = raw[k];
|
|
97
|
+
// manifest-registered non-edit points for this element (class statics win)
|
|
98
|
+
for (const r of _registered) {
|
|
99
|
+
if (!safe(() => el && el.matches && el.matches(r.selector))) continue;
|
|
100
|
+
for (const k in r.points) if (own(r.points, k) && k !== 'edit' && !own(out, k)) out[k] = r.points[k];
|
|
101
|
+
}
|
|
102
|
+
const edit = editPoint(Ctor, el);
|
|
103
|
+
if (edit) out.edit = edit;
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Walk the document (crossing shadow roots) for every element offering `point`.
|
|
108
|
+
* Returns [{ el, spec }]. opts.root (default document), opts.skipAttr (an
|
|
109
|
+
* attribute that opts an element out; default 'data-swc-skip'). */
|
|
110
|
+
export function findExtensionPoints(point, opts) {
|
|
111
|
+
opts = opts || {};
|
|
112
|
+
const skipAttr = opts.skipAttr || 'data-swc-skip';
|
|
113
|
+
const out = [];
|
|
114
|
+
const seen = new WeakSet();
|
|
115
|
+
const visit = (r) => {
|
|
116
|
+
if (!r || !r.querySelectorAll) return;
|
|
117
|
+
for (const el of r.querySelectorAll('*')) {
|
|
118
|
+
if (seen.has(el)) continue;
|
|
119
|
+
seen.add(el);
|
|
120
|
+
if (el.hasAttribute && el.hasAttribute(skipAttr)) { if (el.shadowRoot) visit(el.shadowRoot); continue; }
|
|
121
|
+
// Resolve against the class statics AND the manifest registry (the latter
|
|
122
|
+
// works even for foreign elements whose ctor declares nothing).
|
|
123
|
+
const ctor = customElements.get(el.localName);
|
|
124
|
+
const spec = safe(() => resolveExtensionPoint(ctor, el, point));
|
|
125
|
+
if (spec) out.push({ el, spec });
|
|
126
|
+
if (el.shadowRoot) visit(el.shadowRoot);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
visit(opts.root || document);
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Call onMatch(el, spec) once for every element offering `point` — now and as
|
|
134
|
+
* components mount later (debounced MutationObserver) or announce via
|
|
135
|
+
* `swc:offer`. Returns an unsubscribe function. */
|
|
136
|
+
export function observeExtensionPoint(point, onMatch, opts) {
|
|
137
|
+
opts = opts || {};
|
|
138
|
+
const matched = new WeakSet();
|
|
139
|
+
const scan = () => {
|
|
140
|
+
for (const { el, spec } of findExtensionPoints(point, opts)) {
|
|
141
|
+
if (matched.has(el)) continue;
|
|
142
|
+
matched.add(el);
|
|
143
|
+
safe(() => onMatch(el, spec));
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
scan();
|
|
147
|
+
let timer = null;
|
|
148
|
+
const debounced = () => { clearTimeout(timer); timer = setTimeout(scan, 50); };
|
|
149
|
+
const target = (opts.root && opts.root.documentElement) || document.documentElement || document;
|
|
150
|
+
const mo = new MutationObserver(debounced);
|
|
151
|
+
mo.observe(target, { childList: true, subtree: true });
|
|
152
|
+
document.addEventListener('swc:offer', debounced);
|
|
153
|
+
return () => { mo.disconnect(); clearTimeout(timer); document.removeEventListener('swc:offer', debounced); };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Announce, for a component that can't declare statically (created dynamically),
|
|
157
|
+
* the points it offers — capabilities observing those points re-scan. */
|
|
158
|
+
export function offerExtensionPoint(el, points) {
|
|
159
|
+
el.dispatchEvent(new CustomEvent('swc:offer', {
|
|
160
|
+
bubbles: true, composed: true, detail: { el, points },
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Register every `interop.editable` entry the loader collected from the page's
|
|
165
|
+
* manifests, so a manifest can make any component (its own or a foreign
|
|
166
|
+
* library's) editable with no class change. Each entry is keyed by CSS
|
|
167
|
+
* selector → an edit descriptor `{ shape, subject:{attr}, forms, present, open }`.
|
|
168
|
+
* Idempotent (a one-shot guard avoids double-registering on re-import). */
|
|
169
|
+
export function registerInteropEditables() {
|
|
170
|
+
if (typeof window === 'undefined') return;
|
|
171
|
+
const api = window.SolidWebComponents;
|
|
172
|
+
const libs = (api && Array.isArray(api.interop)) ? api.interop : [];
|
|
173
|
+
for (const lib of libs) {
|
|
174
|
+
const editable = lib && lib.interop && lib.interop.editable;
|
|
175
|
+
if (!editable || typeof editable !== 'object') continue;
|
|
176
|
+
const seen = (lib._editableSeen = lib._editableSeen || {});
|
|
177
|
+
for (const selector in editable) {
|
|
178
|
+
if (!own(editable, selector) || seen[selector]) continue;
|
|
179
|
+
seen[selector] = true;
|
|
180
|
+
registerExtensionPoints(selector, { edit: editable[selector] });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Inline placement (edit="inPlace" / present:"inPlace") is activated by
|
|
184
|
+
// core/edit-placements.js, loaded with the rdf capability.
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Run on import: the rdf capability (sol-form/sol-settings → this module) loads
|
|
188
|
+
// after the loader has parsed manifests, so api.interop is already populated.
|
|
189
|
+
registerInteropEditables();
|